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 }