go.mondoo.com/cnquery@v0.0.0-20231005093811-59568235f6ea/providers/os/connection/docker_container.go (about)

     1  // Copyright (c) Mondoo, Inc.
     2  // SPDX-License-Identifier: BUSL-1.1
     3  
     4  package connection
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"io"
    10  	"os"
    11  	"strconv"
    12  	"strings"
    13  
    14  	"github.com/docker/docker/client"
    15  	"github.com/google/go-containerregistry/pkg/name"
    16  	v1 "github.com/google/go-containerregistry/pkg/v1"
    17  	"github.com/rs/zerolog/log"
    18  	"github.com/spf13/afero"
    19  	"go.mondoo.com/cnquery/providers-sdk/v1/inventory"
    20  	"go.mondoo.com/cnquery/providers/os/connection/container/auth"
    21  	"go.mondoo.com/cnquery/providers/os/connection/container/docker_engine"
    22  	"go.mondoo.com/cnquery/providers/os/connection/container/image"
    23  	"go.mondoo.com/cnquery/providers/os/connection/shared"
    24  	"go.mondoo.com/cnquery/providers/os/connection/ssh/cat"
    25  	"go.mondoo.com/cnquery/providers/os/id/containerid"
    26  	docker_discovery "go.mondoo.com/cnquery/providers/os/resources/discovery/docker_engine"
    27  )
    28  
    29  const (
    30  	DockerContainer shared.ConnectionType = "docker-container"
    31  )
    32  
    33  var _ shared.Connection = &DockerContainerConnection{}
    34  
    35  type DockerContainerConnection struct {
    36  	id    uint32
    37  	asset *inventory.Asset
    38  
    39  	Client    *client.Client
    40  	container string
    41  	Fs        *FS
    42  
    43  	PlatformIdentifier   string
    44  	PlatformArchitecture string
    45  	// optional metadata to store additional information
    46  	Metadata struct {
    47  		Name   string
    48  		Labels map[string]string
    49  	}
    50  
    51  	kind    string
    52  	runtime string
    53  }
    54  
    55  func NewDockerContainerConnection(id uint32, conf *inventory.Config, asset *inventory.Asset) (*DockerContainerConnection, error) {
    56  	// expect unix shell by default
    57  	dockerClient, err := GetDockerClient()
    58  	if err != nil {
    59  		return nil, err
    60  	}
    61  
    62  	// check if we are having a container
    63  	data, err := dockerClient.ContainerInspect(context.Background(), conf.Host)
    64  	if err != nil {
    65  		return nil, errors.New("cannot find container " + conf.Host)
    66  	}
    67  
    68  	if !data.State.Running {
    69  		return nil, errors.New("container " + data.ID + " is not running")
    70  	}
    71  
    72  	conn := &DockerContainerConnection{
    73  		asset:     asset,
    74  		Client:    dockerClient,
    75  		container: conf.Host,
    76  		kind:      "container",
    77  		runtime:   "docker",
    78  	}
    79  
    80  	// this can later be used for containers build from scratch
    81  	serverVersion, err := dockerClient.ServerVersion(context.Background())
    82  	if err != nil {
    83  		log.Debug().Err(err).Msg("docker> cannot get server version")
    84  	} else {
    85  		log.Debug().Interface("serverVersion", serverVersion).Msg("docker> server version")
    86  		conn.PlatformArchitecture = serverVersion.Arch
    87  	}
    88  
    89  	conn.Fs = &FS{
    90  		dockerClient: conn.Client,
    91  		Container:    conn.container,
    92  		Connection:   conn,
    93  		catFS:        cat.New(conn),
    94  	}
    95  	return conn, nil
    96  }
    97  
    98  func GetDockerClient() (*client.Client, error) {
    99  	cli, err := client.NewClientWithOpts(client.FromEnv)
   100  	if err != nil {
   101  		return nil, err
   102  	}
   103  	cli.NegotiateAPIVersion(context.Background())
   104  	return cli, nil
   105  }
   106  
   107  func (c *DockerContainerConnection) ID() uint32 {
   108  	return c.id
   109  }
   110  
   111  func (c *DockerContainerConnection) Name() string {
   112  	return string(DockerContainer)
   113  }
   114  
   115  func (c *DockerContainerConnection) Type() shared.ConnectionType {
   116  	return DockerContainer
   117  }
   118  
   119  func (c *DockerContainerConnection) Asset() *inventory.Asset {
   120  	return c.asset
   121  }
   122  
   123  func (c *DockerContainerConnection) ContainerId() string {
   124  	return c.container
   125  }
   126  
   127  func (c *DockerContainerConnection) Capabilities() shared.Capabilities {
   128  	return shared.Capability_File | shared.Capability_RunCommand
   129  }
   130  
   131  func (c *DockerContainerConnection) FileInfo(path string) (shared.FileInfoDetails, error) {
   132  	fs := c.FileSystem()
   133  	afs := &afero.Afero{Fs: fs}
   134  	stat, err := afs.Stat(path)
   135  	if err != nil {
   136  		return shared.FileInfoDetails{}, err
   137  	}
   138  
   139  	mode := stat.Mode()
   140  
   141  	uid := int64(-1)
   142  	gid := int64(-1)
   143  
   144  	if stat, ok := stat.Sys().(*shared.FileInfo); ok {
   145  		uid = stat.Uid
   146  		gid = stat.Gid
   147  	}
   148  
   149  	return shared.FileInfoDetails{
   150  		Mode: shared.FileModeDetails{mode},
   151  		Size: stat.Size(),
   152  		Uid:  uid,
   153  		Gid:  gid,
   154  	}, nil
   155  }
   156  
   157  func (c *DockerContainerConnection) FileSystem() afero.Fs {
   158  	return c.Fs
   159  }
   160  
   161  func (c *DockerContainerConnection) RunCommand(command string) (*shared.Command, error) {
   162  	log.Debug().Str("command", command).Msg("docker> run command")
   163  	cmd := &docker_engine.Command{Client: c.Client, Container: c.container}
   164  	res, err := cmd.Exec(command)
   165  	// this happens, when we try to run /bin/sh in a container, which does not have it
   166  	if err == nil && res.ExitStatus == 126 {
   167  		output := ""
   168  		b, err := io.ReadAll(res.Stdout)
   169  		if err == nil {
   170  			output = string(b)
   171  		}
   172  		err = errors.New("could not execute command: " + output)
   173  	}
   174  	return res, err
   175  }
   176  
   177  // NewContainerRegistryImage loads a container image from a remote registry
   178  func NewContainerRegistryImage(id uint32, conf *inventory.Config, asset *inventory.Asset) (*TarConnection, error) {
   179  	ref, err := name.ParseReference(conf.Host, name.WeakValidation)
   180  	if err == nil {
   181  		log.Debug().Str("ref", ref.Name()).Msg("found valid container registry reference")
   182  
   183  		registryOpts := []image.Option{image.WithInsecure(conf.Insecure)}
   184  		remoteOpts := auth.AuthOption(conf.Credentials)
   185  		for i := range remoteOpts {
   186  			registryOpts = append(registryOpts, remoteOpts[i])
   187  		}
   188  
   189  		var img v1.Image
   190  		var rc io.ReadCloser
   191  		loadedImage := false
   192  		if asset.Connections[0].Options != nil {
   193  			if _, ok := asset.Connections[0].Options[COMPRESSED_IMAGE]; ok {
   194  				// read image from disk
   195  				img, rc, err = image.LoadImageFromDisk(asset.Connections[0].Options[COMPRESSED_IMAGE])
   196  				if err != nil {
   197  					return nil, err
   198  				}
   199  				loadedImage = true
   200  			}
   201  		}
   202  		if !loadedImage {
   203  			img, rc, err = image.LoadImageFromRegistry(ref, registryOpts...)
   204  			if err != nil {
   205  				return nil, err
   206  			}
   207  			if asset.Connections[0].Options == nil {
   208  				asset.Connections[0].Options = map[string]string{}
   209  			}
   210  			osFile := rc.(*os.File)
   211  			filename := osFile.Name()
   212  			asset.Connections[0].Options[COMPRESSED_IMAGE] = filename
   213  		}
   214  
   215  		var identifier string
   216  		hash, err := img.Digest()
   217  		if err == nil {
   218  			identifier = containerid.MondooContainerImageID(hash.String())
   219  		}
   220  
   221  		conn, err := NewWithReader(id, conf, asset, rc)
   222  		if err != nil {
   223  			return nil, err
   224  		}
   225  		conn.PlatformIdentifier = identifier
   226  		conn.Metadata.Name = containerid.ShortContainerImageID(hash.String())
   227  
   228  		asset.PlatformIds = []string{identifier}
   229  		repoName := ref.Context().Name()
   230  		imgDigest := hash.String()
   231  		name := repoName + "@" + containerid.ShortContainerImageID(imgDigest)
   232  		asset.Name = name
   233  
   234  		// set the platform architecture using the image configuration
   235  		imgConfig, err := img.ConfigFile()
   236  		if err == nil {
   237  			conn.PlatformArchitecture = imgConfig.Architecture
   238  		}
   239  
   240  		labels := map[string]string{}
   241  		labels["docker.io/digests"] = ref.String()
   242  
   243  		manifest, err := img.Manifest()
   244  		if err == nil {
   245  			labels["mondoo.com/image-id"] = manifest.Config.Digest.String()
   246  		}
   247  
   248  		conn.Metadata.Labels = labels
   249  		asset.Labels = labels
   250  
   251  		return conn, err
   252  	}
   253  	log.Debug().Str("image", conf.Host).Msg("Could not detect a valid repository url")
   254  	return nil, err
   255  }
   256  
   257  func NewDockerEngineContainer(id uint32, conf *inventory.Config, asset *inventory.Asset) (shared.Connection, error) {
   258  	// could be an image id/name, container id/name or a short reference to an image in docker engine
   259  	ded, err := docker_discovery.NewDockerEngineDiscovery()
   260  	if err != nil {
   261  		return nil, err
   262  	}
   263  
   264  	ci, err := ded.ContainerInfo(conf.Host)
   265  	if err != nil {
   266  		return nil, err
   267  	}
   268  
   269  	if ci.Running {
   270  		log.Debug().Msg("found running container " + ci.ID)
   271  
   272  		conn, err := NewDockerContainerConnection(id, &inventory.Config{
   273  			Host: ci.ID,
   274  		}, asset)
   275  		if err != nil {
   276  			return nil, err
   277  		}
   278  		conn.PlatformIdentifier = containerid.MondooContainerID(ci.ID)
   279  		conn.Metadata.Name = containerid.ShortContainerImageID(ci.ID)
   280  		conn.Metadata.Labels = ci.Labels
   281  		asset.Name = ci.Name
   282  		asset.PlatformIds = []string{containerid.MondooContainerID(ci.ID)}
   283  		return conn, nil
   284  	} else {
   285  		log.Debug().Msg("found stopped container " + ci.ID)
   286  		conn, err := NewFromDockerEngine(id, &inventory.Config{
   287  			Host: ci.ID,
   288  		}, asset)
   289  		if err != nil {
   290  			return nil, err
   291  		}
   292  		conn.PlatformIdentifier = containerid.MondooContainerID(ci.ID)
   293  		conn.Metadata.Name = containerid.ShortContainerImageID(ci.ID)
   294  		conn.Metadata.Labels = ci.Labels
   295  		asset.Name = ci.Name
   296  		asset.PlatformIds = []string{containerid.MondooContainerID(ci.ID)}
   297  		return conn, nil
   298  	}
   299  }
   300  
   301  func NewDockerContainerImageConnection(id uint32, conf *inventory.Config, asset *inventory.Asset) (*TarConnection, error) {
   302  	disableInmemoryCache := false
   303  	if _, ok := conf.Options["disable-cache"]; ok {
   304  		var err error
   305  		disableInmemoryCache, err = strconv.ParseBool(conf.Options["disable-cache"])
   306  		if err != nil {
   307  			return nil, err
   308  		}
   309  	}
   310  	// Determine whether the image is locally present or not.
   311  	resolver := docker_discovery.Resolver{}
   312  	resolvedAssets, err := resolver.Resolve(context.Background(), asset, conf, nil)
   313  	if err != nil {
   314  		return nil, err
   315  	}
   316  
   317  	if len(resolvedAssets) > 1 {
   318  		return nil, errors.New("provided image name resolved to more than one container image")
   319  	}
   320  
   321  	// The requested image isn't locally available, but we can pull it from a remote registry.
   322  	if len(resolvedAssets) > 0 && resolvedAssets[0].Connections[0].Type == "container-registry" {
   323  		asset.Name = resolvedAssets[0].Name
   324  		asset.PlatformIds = resolvedAssets[0].PlatformIds
   325  		asset.Labels = resolvedAssets[0].Labels
   326  		return NewContainerRegistryImage(id, conf, asset)
   327  	}
   328  
   329  	// could be an image id/name, container id/name or a short reference to an image in docker engine
   330  	ded, err := docker_discovery.NewDockerEngineDiscovery()
   331  	if err != nil {
   332  		return nil, err
   333  	}
   334  
   335  	ii, err := ded.ImageInfo(conf.Host)
   336  	if err != nil {
   337  		return nil, err
   338  	}
   339  
   340  	labelImageId := ii.ID
   341  	splitLabels := strings.Split(ii.Labels["docker.io/digests"], ",")
   342  	if len(splitLabels) > 1 {
   343  		labelImageIdFull := splitLabels[0]
   344  		splitFullLabel := strings.Split(labelImageIdFull, "@")
   345  		if len(splitFullLabel) > 1 {
   346  			labelImageId = strings.Split(labelImageIdFull, "@")[1]
   347  		}
   348  	}
   349  
   350  	// This is the image id that is used to pull the image from the registry.
   351  	log.Debug().Msg("found docker engine image " + labelImageId)
   352  	if ii.Size > 1024 && !disableInmemoryCache { // > 1GB
   353  		log.Warn().Int64("size", ii.Size).Msg("Because the image is larger than 1 GB, this task will require a lot of memory. Consider disabling the in-memory cache by adding this flag to the command: `--disable-cache=true`")
   354  	}
   355  	_, rc, err := image.LoadImageFromDockerEngine(ii.ID, disableInmemoryCache)
   356  	if err != nil {
   357  		return nil, err
   358  	}
   359  
   360  	identifier := containerid.MondooContainerImageID(labelImageId)
   361  
   362  	asset.PlatformIds = []string{identifier}
   363  	asset.Name = ii.Name
   364  	asset.Labels = ii.Labels
   365  
   366  	tarConn, err := NewWithReader(id, conf, asset, rc)
   367  	if err != nil {
   368  		return nil, err
   369  	}
   370  	tarConn.PlatformIdentifier = identifier
   371  	tarConn.Metadata.Name = ii.Name
   372  	tarConn.Metadata.Labels = ii.Labels
   373  	return tarConn, nil
   374  }
   375  
   376  // based on the target, try and find out what kind of connection we are dealing with, this can be either a
   377  // 1. a container, referenced by name or id
   378  // 2. a locally present image, referenced by tag or digest
   379  // 3. a remote image, referenced by tag or digest
   380  func FetchConnectionType(target string) (string, error) {
   381  	ded, err := docker_discovery.NewDockerEngineDiscovery()
   382  	if err != nil {
   383  		return "", err
   384  	}
   385  
   386  	if ded != nil {
   387  		_, err = ded.ContainerInfo(target)
   388  		if err == nil {
   389  			return "docker-container", nil
   390  		}
   391  		_, err = ded.ImageInfo(target)
   392  		if err == nil {
   393  			return "docker-image", nil
   394  		}
   395  	}
   396  	_, err = name.ParseReference(target, name.WeakValidation)
   397  	if err == nil {
   398  		return "docker-image", nil
   399  	}
   400  
   401  	return "", errors.New("could not find container or image " + target)
   402  }