diff --git a/providers/os/connection/container/image/docker.go b/providers/os/connection/container/image/docker.go index eee866926d..ae9231b922 100644 --- a/providers/os/connection/container/image/docker.go +++ b/providers/os/connection/container/image/docker.go @@ -12,7 +12,6 @@ import ( v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/daemon" "github.com/google/go-containerregistry/pkg/v1/tarball" - "go.mondoo.com/cnquery/v10/providers/os/connection/container/cache" ) type ShaReference struct { @@ -39,40 +38,17 @@ func (r ShaReference) Scope(scope string) string { return "" } -func LoadImageFromDockerEngine(sha string, disableBuffer bool) (v1.Image, io.ReadCloser, error) { +func LoadImageFromDockerEngine(sha string, disableBuffer bool) (v1.Image, error) { opts := []daemon.Option{} if disableBuffer { opts = append(opts, daemon.WithUnbufferedOpener()) } img, err := daemon.Image(&ShaReference{SHA: strings.Replace(sha, "sha256:", "", -1)}, opts...) - if err != nil { - return nil, nil, err - } - - // write image to disk (conmpressed, unflattened) - // Otherwise we can not later recognize it as a valid image - f, err := writeCompressedTarImage(img, sha) - if err != nil { - return nil, nil, err - } - - return img, f, nil -} - -// writeCompressedTarImage writes image including the metradata unflattened to disk -func writeCompressedTarImage(img v1.Image, digest string) (*os.File, error) { - f, err := cache.RandomFile() if err != nil { return nil, err } - filename := f.Name() - if err := WriteCompressedTarImageToFile(img, digest, f); err != nil { - os.Remove(filename) - return nil, err - - } - return f, nil + return img, nil } func WriteCompressedTarImageToFile(img v1.Image, digest string, f *os.File) error { diff --git a/providers/os/connection/container/image_connection.go b/providers/os/connection/container/image_connection.go new file mode 100644 index 0000000000..0d5c1ab2f4 --- /dev/null +++ b/providers/os/connection/container/image_connection.go @@ -0,0 +1,169 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package container + +import ( + "errors" + "os" + + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/tarball" + "github.com/rs/zerolog/log" + "go.mondoo.com/cnquery/v10/providers-sdk/v1/inventory" + "go.mondoo.com/cnquery/v10/providers/os/connection/container/auth" + "go.mondoo.com/cnquery/v10/providers/os/connection/container/image" + "go.mondoo.com/cnquery/v10/providers/os/connection/tar" + "go.mondoo.com/cnquery/v10/providers/os/id/containerid" +) + +// NewImageConnection uses a container image reference as input and creates a tar connection +func NewImageConnection(id uint32, conf *inventory.Config, asset *inventory.Asset, img v1.Image) (*tar.Connection, error) { + f, err := tar.RandomFile() + if err != nil { + return nil, err + } + + conf.Options[tar.OPTION_FILE] = f.Name() + + return tar.NewConnection(id, conf, asset, + tar.WithFetchFn(func() (string, error) { + err = tar.StreamToTmpFile(mutate.Extract(img), f) + if err != nil { + _ = os.Remove(f.Name()) + return "", err + } + log.Debug().Msg("tar> extracted image to temporary file") + return f.Name(), nil + }), + tar.WithCloseFn(func() { + log.Debug().Str("tar", f.Name()).Msg("tar> remove temporary tar file on connection close") + _ = os.Remove(f.Name()) + }), + ) +} + +// NewRegistryImage loads a container image from a remote registry +func NewRegistryImage(id uint32, conf *inventory.Config, asset *inventory.Asset) (*tar.Connection, error) { + ref, err := name.ParseReference(conf.Host, name.WeakValidation) + if err != nil { + return nil, errors.New("invalid container registry reference: " + conf.Host) + } + log.Debug().Str("ref", ref.Name()).Msg("found valid container registry reference") + + registryOpts := []image.Option{image.WithInsecure(conf.Insecure)} + remoteOpts := auth.AuthOption(conf.Credentials) + registryOpts = append(registryOpts, remoteOpts...) + + img, err := image.LoadImageFromRegistry(ref, registryOpts...) + if err != nil { + return nil, err + } + if conf.Options == nil { + conf.Options = map[string]string{} + } + + conn, err := NewImageConnection(id, conf, asset, img) + if err != nil { + return nil, err + } + + var identifier string + hash, err := img.Digest() + if err == nil { + identifier = containerid.MondooContainerImageID(hash.String()) + } + + conn.PlatformIdentifier = identifier + conn.Metadata.Name = containerid.ShortContainerImageID(hash.String()) + + repoName := ref.Context().Name() + imgDigest := hash.String() + containerAssetName := repoName + "@" + containerid.ShortContainerImageID(imgDigest) + if asset.Name == "" { + asset.Name = containerAssetName + } + if len(asset.PlatformIds) == 0 { + asset.PlatformIds = []string{identifier} + } else { + asset.PlatformIds = append(asset.PlatformIds, identifier) + } + + // set the platform architecture using the image configuration + imgConfig, err := img.ConfigFile() + if err == nil { + conn.PlatformArchitecture = imgConfig.Architecture + } + + labels := map[string]string{} + labels["docker.io/digests"] = ref.String() + + manifest, err := img.Manifest() + if err == nil { + labels["mondoo.com/image-id"] = manifest.Config.Digest.String() + } + + conn.Metadata.Labels = labels + asset.Labels = labels + + return conn, err +} + +func NewFromTar(id uint32, conf *inventory.Config, asset *inventory.Asset) (*tar.Connection, error) { + if conf == nil || len(conf.Options[tar.OPTION_FILE]) == 0 { + return nil, errors.New("tar provider requires a valid tar file") + } + + if conf.Options == nil { + conf.Options = map[string]string{} + } + + filename := conf.Options[tar.OPTION_FILE] + var identifier string + + // try to determine if the tar is a container image + img, iErr := tarball.ImageFromPath(filename, nil) + if iErr != nil { + return nil, iErr + } + + hash, err := img.Digest() + if err != nil { + return nil, err + } + identifier = containerid.MondooContainerImageID(hash.String()) + + // we need to extract the image from the tar file and create a new tar connection + imageFilename := "" + + f, err := tar.RandomFile() + if err != nil { + return nil, err + } + imageFilename = f.Name() + conf.Options[tar.OPTION_FILE] = imageFilename + + c, err := tar.NewConnection(id, conf, asset, + tar.WithFetchFn(func() (string, error) { + err = tar.StreamToTmpFile(mutate.Extract(img), f) + if err != nil { + _ = os.Remove(imageFilename) + return imageFilename, err + } + return imageFilename, nil + }), + tar.WithCloseFn(func() { + // remove temporary file on stream close + log.Debug().Str("tar", imageFilename).Msg("tar> remove temporary flattened image file on connection close") + _ = os.Remove(imageFilename) + }), + ) + if err != nil { + return nil, err + } + + c.PlatformIdentifier = identifier + return c, nil +} diff --git a/providers/os/connection/container/image_connection_test.go b/providers/os/connection/container/image_connection_test.go new file mode 100644 index 0000000000..c1348251f5 --- /dev/null +++ b/providers/os/connection/container/image_connection_test.go @@ -0,0 +1,281 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package container_test + +import ( + "io" + "net/http" + "os" + "regexp" + "testing" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/tarball" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.mondoo.com/cnquery/v10/providers-sdk/v1/inventory" + "go.mondoo.com/cnquery/v10/providers/os/connection/container" + "go.mondoo.com/cnquery/v10/providers/os/connection/tar" +) + +const ( + alpineImage = "alpine:3.15" + alpineContainerPath = "./alpine-container.tar" + + centosImage = "centos:7" + centosContainerPath = "./centos-container.tar" +) + +func cacheImageToTar(source string, filename string) error { + // check if the cache is already there + _, err := os.Stat(filename) + if err == nil { + return nil + } + + tag, err := name.NewTag(source, name.WeakValidation) + if err != nil { + return err + } + + auth, err := authn.DefaultKeychain.Resolve(tag.Registry) + if err != nil { + return err + } + + img, err := remote.Image(tag, remote.WithAuth(auth), remote.WithTransport(http.DefaultTransport)) + if err != nil { + return err + } + + return tarball.WriteToFile(filename, tag, img) +} + +func cacheAlpine() error { + return cacheImageToTar(alpineImage, alpineContainerPath) +} + +func cacheCentos() error { + return cacheImageToTar(centosImage, centosContainerPath) +} + +type dockerConnTest struct { + name string + conn *tar.Connection + testfile string +} + +func TestImageConnections(t *testing.T) { + var testConnections []dockerConnTest + + // create a connection to ta downloaded alpine image + err := cacheAlpine() + require.NoError(t, err, "should create tar without error") + alpineConn, err := container.NewFromTar(0, &inventory.Config{ + Type: "tar", + Options: map[string]string{ + tar.OPTION_FILE: alpineContainerPath, + }, + }, &inventory.Asset{}) + require.NoError(t, err, "should create connection without error") + testConnections = append(testConnections, dockerConnTest{ + name: "alpine", + conn: alpineConn, + testfile: "/etc/alpine-release", + }) + + // create a connection to ta downloaded centos image + err = cacheCentos() + require.NoError(t, err, "should create tar without error") + centosConn, err := container.NewFromTar(0, &inventory.Config{ + Type: "tar", + Options: map[string]string{ + tar.OPTION_FILE: centosContainerPath, + }, + }, &inventory.Asset{}) + require.NoError(t, err, "should create connection without error") + testConnections = append(testConnections, dockerConnTest{ + name: "centos", + conn: centosConn, + testfile: "/etc/centos-release", + }) + + // create a connection to a remote alpine image + alpineRemoteConn, err := container.NewRegistryImage(0, &inventory.Config{ + Type: "docker-image", + Host: alpineImage, + }, &inventory.Asset{}) + require.NoError(t, err, "should create remote connection without error") + testConnections = append(testConnections, dockerConnTest{ + name: "alpine", + conn: alpineRemoteConn, + testfile: "/etc/alpine-release", + }) + + for _, test := range testConnections { + t.Run("Test Connection for "+test.name, func(t *testing.T) { + conn := test.conn + require.NotNil(t, conn) + t.Run("Test Run Command", func(t *testing.T) { + cmd, err := conn.RunCommand("ls /") + assert.Nil(t, err, "should execute without error") + assert.Equal(t, -1, cmd.ExitStatus, "command should not be executed") + stdoutContent, _ := io.ReadAll(cmd.Stdout) + assert.Equal(t, "", string(stdoutContent), "output should be correct") + stderrContent, _ := io.ReadAll(cmd.Stdout) + assert.Equal(t, "", string(stderrContent), "output should be correct") + }) + + t.Run("Test Platform Identifier", func(t *testing.T) { + platformId, err := conn.Identifier() + require.NoError(t, err) + assert.True(t, len(platformId) > 0) + }) + + t.Run("Test File Stat", func(t *testing.T) { + f, err := conn.FileSystem().Open(test.testfile) + assert.Nil(t, err) + assert.Equal(t, nil, err, "should execute without error") + + p := f.Name() + assert.Equal(t, test.testfile, p, "path should be correct") + + stat, err := f.Stat() + assert.True(t, stat.Size() >= 6, "should read file size") + assert.Equal(t, nil, err, "should execute without error") + + content, err := io.ReadAll(f) + assert.Equal(t, nil, err, "should execute without error") + assert.True(t, len(content) >= 6, "should read the full content") + }) + + t.Run("Test File Permissions", func(t *testing.T) { + path := test.testfile + details, err := conn.FileInfo(path) + require.NoError(t, err) + assert.Equal(t, int64(0), details.Uid) + assert.Equal(t, int64(0), details.Gid) + assert.True(t, details.Size >= 0) + assert.Equal(t, false, details.Mode.IsDir()) + assert.Equal(t, true, details.Mode.IsRegular()) + assert.Equal(t, "-rw-r--r--", details.Mode.String()) + assert.True(t, details.Mode.UserReadable()) + assert.True(t, details.Mode.UserWriteable()) + assert.False(t, details.Mode.UserExecutable()) + assert.True(t, details.Mode.GroupReadable()) + assert.False(t, details.Mode.GroupWriteable()) + assert.False(t, details.Mode.GroupExecutable()) + assert.True(t, details.Mode.OtherReadable()) + assert.False(t, details.Mode.OtherWriteable()) + assert.False(t, details.Mode.OtherExecutable()) + assert.False(t, details.Mode.Suid()) + assert.False(t, details.Mode.Sgid()) + assert.False(t, details.Mode.Sticky()) + + path = "/etc" + details, err = conn.FileInfo(path) + require.NoError(t, err) + assert.Equal(t, int64(0), details.Uid) + assert.Equal(t, int64(0), details.Gid) + assert.True(t, details.Size >= 0) + assert.True(t, details.Mode.IsDir()) + assert.False(t, details.Mode.IsRegular()) + assert.Equal(t, "drwxr-xr-x", details.Mode.String()) + assert.True(t, details.Mode.UserReadable()) + assert.True(t, details.Mode.UserWriteable()) + assert.True(t, details.Mode.UserExecutable()) + assert.True(t, details.Mode.GroupReadable()) + assert.False(t, details.Mode.GroupWriteable()) + assert.True(t, details.Mode.GroupExecutable()) + assert.True(t, details.Mode.OtherReadable()) + assert.False(t, details.Mode.OtherWriteable()) + assert.True(t, details.Mode.OtherExecutable()) + assert.False(t, details.Mode.Suid()) + assert.False(t, details.Mode.Sgid()) + assert.False(t, details.Mode.Sticky()) + }) + + t.Run("Test Files Find", func(t *testing.T) { + fs := conn.FileSystem() + fSearch := fs.(*tar.FS) + + if test.testfile == "/etc/alpine-release" { + infos, err := fSearch.Find("/", regexp.MustCompile(`alpine-release`), "file") + require.NoError(t, err) + assert.Equal(t, 1, len(infos)) + } else if test.testfile == "/etc/centos-release" { + infos, err := fSearch.Find("/", regexp.MustCompile(`centos-release`), "file") + require.NoError(t, err) + assert.Equal(t, 6, len(infos)) + } + }) + }) + } + +} + +func TestTarSymlinkFile(t *testing.T) { + err := cacheAlpine() + require.NoError(t, err, "should create tar without error") + + c, err := container.NewFromTar(0, &inventory.Config{ + Type: "tar", + Options: map[string]string{ + tar.OPTION_FILE: alpineContainerPath, + }, + }, &inventory.Asset{}) + assert.Equal(t, nil, err, "should create tar without error") + + f, err := c.FileSystem().Open("/bin/cat") + assert.Nil(t, err) + if assert.NotNil(t, f) { + assert.Equal(t, nil, err, "should execute without error") + + p := f.Name() + assert.Equal(t, "/bin/cat", p, "path should be correct") + + stat, err := f.Stat() + assert.Equal(t, nil, err, "should stat without error") + assert.True(t, stat.Size() > 0, "should read file size") + + content, err := io.ReadAll(f) + assert.Equal(t, nil, err, "should execute without error") + assert.True(t, len(content) > 0, "should read the full content") + } +} + +// deactivate test for now for speedier testing +// in contrast to alpine, the symlink on centos is pointing to a relative target and not an absolute one +func TestTarRelativeSymlinkFileCentos(t *testing.T) { + err := cacheCentos() + require.NoError(t, err, "should create tar without error") + + c, err := container.NewFromTar(0, &inventory.Config{ + Type: "tar", + Options: map[string]string{ + tar.OPTION_FILE: centosContainerPath, + }, + }, &inventory.Asset{}) + assert.Equal(t, nil, err, "should create tar without error") + + f, err := c.FileSystem().Open("/etc/redhat-release") + require.NoError(t, err) + + if assert.NotNil(t, f) { + assert.Equal(t, nil, err, "should execute without error") + + p := f.Name() + assert.Equal(t, "/etc/redhat-release", p, "path should be correct") + + stat, err := f.Stat() + assert.Equal(t, nil, err, "should stat without error") + assert.Equal(t, int64(37), stat.Size(), "should read file size") + + content, err := io.ReadAll(f) + assert.Equal(t, nil, err, "should execute without error") + assert.Equal(t, 37, len(content), "should read the full content") + } +} diff --git a/providers/os/connection/container/docker_engine/command.go b/providers/os/connection/docker/command.go similarity index 99% rename from providers/os/connection/container/docker_engine/command.go rename to providers/os/connection/docker/command.go index e4b60c905a..dcc1bcd069 100644 --- a/providers/os/connection/container/docker_engine/command.go +++ b/providers/os/connection/docker/command.go @@ -1,7 +1,7 @@ // Copyright (c) Mondoo, Inc. // SPDX-License-Identifier: BUSL-1.1 -package docker_engine +package docker import ( "bufio" diff --git a/providers/os/connection/container/docker_engine/command_test.go b/providers/os/connection/docker/command_test.go similarity index 99% rename from providers/os/connection/container/docker_engine/command_test.go rename to providers/os/connection/docker/command_test.go index 5f70eb0566..019528d61f 100644 --- a/providers/os/connection/container/docker_engine/command_test.go +++ b/providers/os/connection/docker/command_test.go @@ -4,7 +4,7 @@ //go:build debugtest // +build debugtest -package docker_engine +package docker import ( "context" diff --git a/providers/os/connection/docker_container.go b/providers/os/connection/docker/container_connection.go similarity index 63% rename from providers/os/connection/docker_container.go rename to providers/os/connection/docker/container_connection.go index 93269a7d5e..f96a130bd6 100644 --- a/providers/os/connection/docker_container.go +++ b/providers/os/connection/docker/container_connection.go @@ -1,12 +1,14 @@ // Copyright (c) Mondoo, Inc. // SPDX-License-Identifier: BUSL-1.1 -package connection +package docker import ( "context" "errors" + "github.com/google/go-containerregistry/pkg/v1/mutate" "io" + "os" "strconv" "strings" @@ -16,22 +18,22 @@ import ( "github.com/spf13/afero" "go.mondoo.com/cnquery/v10/providers-sdk/v1/inventory" "go.mondoo.com/cnquery/v10/providers-sdk/v1/plugin" - "go.mondoo.com/cnquery/v10/providers/os/connection/container/auth" - "go.mondoo.com/cnquery/v10/providers/os/connection/container/docker_engine" + "go.mondoo.com/cnquery/v10/providers/os/connection/container" "go.mondoo.com/cnquery/v10/providers/os/connection/container/image" "go.mondoo.com/cnquery/v10/providers/os/connection/shared" "go.mondoo.com/cnquery/v10/providers/os/connection/ssh/cat" + "go.mondoo.com/cnquery/v10/providers/os/connection/tar" "go.mondoo.com/cnquery/v10/providers/os/id/containerid" - docker_discovery "go.mondoo.com/cnquery/v10/providers/os/resources/discovery/docker_engine" + dockerDiscovery "go.mondoo.com/cnquery/v10/providers/os/resources/discovery/docker_engine" ) const ( - DockerContainer shared.ConnectionType = "docker-container" + ContainerConnectionType shared.ConnectionType = "docker-container" ) -var _ shared.Connection = &DockerContainerConnection{} +var _ shared.Connection = &ContainerConnection{} -type DockerContainerConnection struct { +type ContainerConnection struct { plugin.Connection asset *inventory.Asset @@ -51,7 +53,7 @@ type DockerContainerConnection struct { runtime string } -func NewDockerContainerConnection(id uint32, conf *inventory.Config, asset *inventory.Asset) (*DockerContainerConnection, error) { +func NewContainerConnection(id uint32, conf *inventory.Config, asset *inventory.Asset) (*ContainerConnection, error) { // expect unix shell by default dockerClient, err := GetDockerClient() if err != nil { @@ -68,7 +70,7 @@ func NewDockerContainerConnection(id uint32, conf *inventory.Config, asset *inve return nil, errors.New("container " + data.ID + " is not running") } - conn := &DockerContainerConnection{ + conn := &ContainerConnection{ Connection: plugin.NewConnection(id, asset), asset: asset, Client: dockerClient, @@ -104,27 +106,27 @@ func GetDockerClient() (*client.Client, error) { return cli, nil } -func (c *DockerContainerConnection) Name() string { - return string(DockerContainer) +func (c *ContainerConnection) Name() string { + return string(ContainerConnectionType) } -func (c *DockerContainerConnection) Type() shared.ConnectionType { - return DockerContainer +func (c *ContainerConnection) Type() shared.ConnectionType { + return ContainerConnectionType } -func (c *DockerContainerConnection) Asset() *inventory.Asset { +func (c *ContainerConnection) Asset() *inventory.Asset { return c.asset } -func (c *DockerContainerConnection) ContainerId() string { +func (c *ContainerConnection) ContainerId() string { return c.container } -func (c *DockerContainerConnection) Capabilities() shared.Capabilities { +func (c *ContainerConnection) Capabilities() shared.Capabilities { return shared.Capability_File | shared.Capability_RunCommand } -func (c *DockerContainerConnection) FileInfo(path string) (shared.FileInfoDetails, error) { +func (c *ContainerConnection) FileInfo(path string) (shared.FileInfoDetails, error) { fs := c.FileSystem() afs := &afero.Afero{Fs: fs} stat, err := afs.Stat(path) @@ -150,13 +152,13 @@ func (c *DockerContainerConnection) FileInfo(path string) (shared.FileInfoDetail }, nil } -func (c *DockerContainerConnection) FileSystem() afero.Fs { +func (c *ContainerConnection) FileSystem() afero.Fs { return c.Fs } -func (c *DockerContainerConnection) RunCommand(command string) (*shared.Command, error) { +func (c *ContainerConnection) RunCommand(command string) (*shared.Command, error) { log.Debug().Str("command", command).Msg("docker> run command") - cmd := &docker_engine.Command{Client: c.Client, Container: c.container} + cmd := &Command{Client: c.Client, Container: c.container} res, err := cmd.Exec(command) // this happens, when we try to run /bin/sh in a container, which does not have it if err == nil && res.ExitStatus == 126 { @@ -171,76 +173,9 @@ func (c *DockerContainerConnection) RunCommand(command string) (*shared.Command, return res, err } -// NewContainerRegistryImage loads a container image from a remote registry -func NewContainerRegistryImage(id uint32, conf *inventory.Config, asset *inventory.Asset) (*TarConnection, error) { - ref, err := name.ParseReference(conf.Host, name.WeakValidation) - if err == nil { - log.Debug().Str("ref", ref.Name()).Msg("found valid container registry reference") - - registryOpts := []image.Option{image.WithInsecure(conf.Insecure)} - remoteOpts := auth.AuthOption(conf.Credentials) - registryOpts = append(registryOpts, remoteOpts...) - - img, err := image.LoadImageFromRegistry(ref, registryOpts...) - if err != nil { - return nil, err - } - if asset.Connections[0].Options == nil { - asset.Connections[0].Options = map[string]string{} - } - - conn, err := NewTarConnectionForContainer(id, conf, asset, img) - if err != nil { - return nil, err - } - - var identifier string - hash, err := img.Digest() - if err == nil { - identifier = containerid.MondooContainerImageID(hash.String()) - } - - conn.PlatformIdentifier = identifier - conn.Metadata.Name = containerid.ShortContainerImageID(hash.String()) - - repoName := ref.Context().Name() - imgDigest := hash.String() - name := repoName + "@" + containerid.ShortContainerImageID(imgDigest) - if asset.Name == "" { - asset.Name = name - } - if len(asset.PlatformIds) == 0 { - asset.PlatformIds = []string{identifier} - } else { - asset.PlatformIds = append(asset.PlatformIds, identifier) - } - - // set the platform architecture using the image configuration - imgConfig, err := img.ConfigFile() - if err == nil { - conn.PlatformArchitecture = imgConfig.Architecture - } - - labels := map[string]string{} - labels["docker.io/digests"] = ref.String() - - manifest, err := img.Manifest() - if err == nil { - labels["mondoo.com/image-id"] = manifest.Config.Digest.String() - } - - conn.Metadata.Labels = labels - asset.Labels = labels - - return conn, err - } - log.Debug().Str("image", conf.Host).Msg("Could not detect a valid repository url") - return nil, err -} - func NewDockerEngineContainer(id uint32, conf *inventory.Config, asset *inventory.Asset) (shared.Connection, error) { // could be an image id/name, container id/name or a short reference to an image in docker engine - ded, err := docker_discovery.NewDockerEngineDiscovery() + ded, err := dockerDiscovery.NewDockerEngineDiscovery() if err != nil { return nil, err } @@ -253,7 +188,7 @@ func NewDockerEngineContainer(id uint32, conf *inventory.Config, asset *inventor if ci.Running { log.Debug().Msg("found running container " + ci.ID) - conn, err := NewDockerContainerConnection(id, &inventory.Config{ + conn, err := NewContainerConnection(id, &inventory.Config{ Host: ci.ID, }, asset) if err != nil { @@ -267,7 +202,7 @@ func NewDockerEngineContainer(id uint32, conf *inventory.Config, asset *inventor return conn, nil } else { log.Debug().Msg("found stopped container " + ci.ID) - conn, err := NewFromDockerEngine(id, &inventory.Config{ + conn, err := NewSnapshotConnection(id, &inventory.Config{ Host: ci.ID, }, asset) if err != nil { @@ -282,7 +217,7 @@ func NewDockerEngineContainer(id uint32, conf *inventory.Config, asset *inventor } } -func NewDockerContainerImageConnection(id uint32, conf *inventory.Config, asset *inventory.Asset) (*TarConnection, error) { +func NewContainerImageConnection(id uint32, conf *inventory.Config, asset *inventory.Asset) (*tar.Connection, error) { disableInmemoryCache := false if _, ok := conf.Options["disable-cache"]; ok { var err error @@ -292,7 +227,7 @@ func NewDockerContainerImageConnection(id uint32, conf *inventory.Config, asset } } // Determine whether the image is locally present or not. - resolver := docker_discovery.Resolver{} + resolver := dockerDiscovery.Resolver{} resolvedAssets, err := resolver.Resolve(context.Background(), asset, conf, nil) if err != nil { return nil, err @@ -307,11 +242,11 @@ func NewDockerContainerImageConnection(id uint32, conf *inventory.Config, asset asset.Name = resolvedAssets[0].Name asset.PlatformIds = resolvedAssets[0].PlatformIds asset.Labels = resolvedAssets[0].Labels - return NewContainerRegistryImage(id, conf, asset) + return container.NewRegistryImage(id, conf, asset) } // could be an image id/name, container id/name or a short reference to an image in docker engine - ded, err := docker_discovery.NewDockerEngineDiscovery() + ded, err := dockerDiscovery.NewDockerEngineDiscovery() if err != nil { return nil, err } @@ -336,10 +271,6 @@ func NewDockerContainerImageConnection(id uint32, conf *inventory.Config, asset if ii.Size > 1024 && !disableInmemoryCache { // > 1GB 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`") } - _, rc, err := image.LoadImageFromDockerEngine(ii.ID, disableInmemoryCache) - if err != nil { - return nil, err - } identifier := containerid.MondooContainerImageID(labelImageId) @@ -347,7 +278,39 @@ func NewDockerContainerImageConnection(id uint32, conf *inventory.Config, asset asset.Name = ii.Name asset.Labels = ii.Labels - tarConn, err := NewWithReader(id, conf, asset, rc) + // cache file locally + var filename string + tmpFile, err := tar.RandomFile() + if err != nil { + return nil, err + } + filename = tmpFile.Name() + + if conf.Options == nil { + conf.Options = map[string]string{} + } + conf.Options[tar.OPTION_FILE] = filename + + tarConn, err := tar.NewConnection( + id, + conf, + asset, + tar.WithFetchFn(func() (string, error) { + img, err := image.LoadImageFromDockerEngine(ii.ID, disableInmemoryCache) + if err != nil { + return filename, err + } + err = tar.StreamToTmpFile(mutate.Extract(img), tmpFile) + if err != nil { + _ = os.Remove(filename) + return filename, err + } + return filename, nil + }), + tar.WithCloseFn(func() { + log.Debug().Str("tar", filename).Msg("tar> remove temporary tar file on connection close") + _ = os.Remove(filename) + })) if err != nil { return nil, err } @@ -357,12 +320,12 @@ func NewDockerContainerImageConnection(id uint32, conf *inventory.Config, asset return tarConn, nil } -// based on the target, try and find out what kind of connection we are dealing with, this can be either a +// FindDockerObjectConnectionType tries to find out what kind of connection we are dealing with, this can be either a // 1. a container, referenced by name or id // 2. a locally present image, referenced by tag or digest // 3. a remote image, referenced by tag or digest -func FetchConnectionType(target string) (string, error) { - ded, err := docker_discovery.NewDockerEngineDiscovery() +func FindDockerObjectConnectionType(target string) (string, error) { + ded, err := dockerDiscovery.NewDockerEngineDiscovery() if err != nil { return "", err } diff --git a/providers/os/connection/docker/container_connection_test.go b/providers/os/connection/docker/container_connection_test.go new file mode 100644 index 0000000000..f8f335bcd3 --- /dev/null +++ b/providers/os/connection/docker/container_connection_test.go @@ -0,0 +1,133 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package docker + +import ( + "context" + "fmt" + "github.com/docker/docker/client" + "io" + "os" + "testing" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/network" + "github.com/google/uuid" + specs "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.mondoo.com/cnquery/v10/providers-sdk/v1/inventory" + "go.mondoo.com/cnquery/v10/providers/os/connection/tar" +) + +// This test has an external dependency on the gcr.io registry +// To test this specific case, we cannot use a stored image, we need to call remote.Get +func TestAssetNameForRemoteImages(t *testing.T) { + var err error + var conn *tar.Connection + var asset *inventory.Asset + retries := 3 + counter := 0 + + for { + config := &inventory.Config{ + Type: "docker-image", + Host: "gcr.io/google-containers/busybox:1.27.2", + } + asset = &inventory.Asset{ + Connections: []*inventory.Config{config}, + } + conn, err = NewContainerImageConnection(0, config, asset) + if counter > retries || (err == nil && conn != nil) { + break + } + counter++ + } + require.NoError(t, err) + require.NotNil(t, conn) + + assert.Equal(t, "gcr.io/google-containers/busybox@545e6a6310a2", asset.Name) + assert.Contains(t, asset.PlatformIds, "//platformid.api.mondoo.app/runtime/docker/images/545e6a6310a27636260920bc07b994a299b6708a1b26910cfefd335fdfb60d2b") +} + +func fetchAndCreateImage(t *testing.T, ctx context.Context, dClient *client.Client, image string) container.CreateResponse { + // If docker is not available, then skip the test. + _, err := dClient.ServerVersion(ctx) + if err != nil { + t.SkipNow() + } + + responseBody, err := dClient.ImagePull(ctx, image, types.ImagePullOptions{}) + defer func() { + err = responseBody.Close() + if err != nil { + panic(err) + } + }() + require.NoError(t, err) + + _, err = io.Copy(os.Stdout, responseBody) + require.NoError(t, err) + + // Make sure the docker image is cleaned up + defer func() { + _, err := dClient.ImageRemove(ctx, image, types.ImageRemoveOptions{ + Force: true, + }) + // ignore error, worst case is that the image is not removed but parallel tests may fail otherwise + fmt.Printf("failed to cleanup pre-pulled docker image: %v", err) + }() + + cfg := &container.Config{ + AttachStdin: false, + AttachStdout: false, + AttachStderr: false, + StdinOnce: false, + Image: image, + } + + uuidVal := uuid.New() + created, err := dClient.ContainerCreate(ctx, cfg, &container.HostConfig{}, &network.NetworkingConfig{}, &specs.Platform{}, uuidVal.String()) + require.NoError(t, err) + + require.NoError(t, dClient.ContainerStart(ctx, created.ID, container.StartOptions{})) + + return created +} + +// TestDockerContainerConnection creates a new running container and tests the connection +func TestDockerContainerConnection(t *testing.T) { + ctx := context.Background() + image := "docker.io/nginx:stable" + dClient, err := GetDockerClient() + assert.NoError(t, err) + created := fetchAndCreateImage(t, ctx, dClient, image) + + // Make sure the container is cleaned up + defer func() { + err := dClient.ContainerRemove(ctx, created.ID, container.RemoveOptions{ + Force: true, + }) + require.NoError(t, err) + }() + + fmt.Println("inject: " + created.ID) + conn, err := NewContainerConnection(0, &inventory.Config{ + Host: created.ID, + }, &inventory.Asset{ + // for the test we need to set the platform + Platform: &inventory.Platform{ + Name: "debian", + Version: "11", + Family: []string{"debian", "linux"}, + }, + }) + require.NoError(t, err) + + cmd, err := conn.RunCommand("ls /") + require.NoError(t, err) + assert.NotNil(t, cmd) + assert.Equal(t, 0, cmd.ExitStatus) +} diff --git a/providers/os/connection/docker/docker.go b/providers/os/connection/docker/docker.go new file mode 100644 index 0000000000..ae160e8ca6 --- /dev/null +++ b/providers/os/connection/docker/docker.go @@ -0,0 +1,15 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package docker + +// The docker connection package provides a connection to a docker engine and handles: +// +// - docker containers +// - docker images +// - docker snapshots +// +// Each of these types of connections is implemented as a separate connection type, since the data format is different. +// All of these connections are based on the tar connection, which is a generic connection type that can handle tar +// files. All docker connections are implemented as a wrapper around the tar connection and prepare the +// data in the correct format for the tar connection. diff --git a/providers/os/connection/docker_file.go b/providers/os/connection/docker/file.go similarity index 96% rename from providers/os/connection/docker_file.go rename to providers/os/connection/docker/file.go index 9e228ad4a6..2ee658abbf 100644 --- a/providers/os/connection/docker_file.go +++ b/providers/os/connection/docker/file.go @@ -1,7 +1,7 @@ // Copyright (c) Mondoo, Inc. // SPDX-License-Identifier: BUSL-1.1 -package connection +package docker import ( "bytes" @@ -20,7 +20,7 @@ import ( "go.mondoo.com/cnquery/v10/providers/os/fsutil" ) -func FileOpen(dockerClient *client.Client, path string, container string, conn *DockerContainerConnection, catFs *cat.Fs) (afero.File, error) { +func FileOpen(dockerClient *client.Client, path string, container string, conn *ContainerConnection, catFs *cat.Fs) (afero.File, error) { f := &File{ path: path, dockerClient: dockerClient, @@ -36,7 +36,7 @@ type File struct { path string container string dockerClient *client.Client - connection *DockerContainerConnection + connection *ContainerConnection reader *bytes.Reader catFs *cat.Fs } diff --git a/providers/os/connection/docker_fs.go b/providers/os/connection/docker/filesytem.go similarity index 97% rename from providers/os/connection/docker_fs.go rename to providers/os/connection/docker/filesytem.go index e276e9a249..e5630cdbcf 100644 --- a/providers/os/connection/docker_fs.go +++ b/providers/os/connection/docker/filesytem.go @@ -1,7 +1,7 @@ // Copyright (c) Mondoo, Inc. // SPDX-License-Identifier: BUSL-1.1 -package connection +package docker import ( "errors" @@ -17,7 +17,7 @@ import ( type FS struct { Container string dockerClient *client.Client - Connection *DockerContainerConnection + Connection *ContainerConnection catFS *cat.Fs } diff --git a/providers/os/connection/docker/snapshot_connection.go b/providers/os/connection/docker/snapshot_connection.go new file mode 100644 index 0000000000..0efc864edc --- /dev/null +++ b/providers/os/connection/docker/snapshot_connection.go @@ -0,0 +1,78 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package docker + +import ( + "context" + "os" + + "go.mondoo.com/cnquery/v10/providers-sdk/v1/inventory" + "go.mondoo.com/cnquery/v10/providers/os/connection/shared" + "go.mondoo.com/cnquery/v10/providers/os/connection/tar" +) + +var _ shared.Connection = &SnapshotConnection{} + +type SnapshotConnection struct { + *tar.Connection +} + +// NewSnapshotConnection creates a snapshot for a docker engine container and opens it +func NewSnapshotConnection(id uint32, conf *inventory.Config, asset *inventory.Asset) (*SnapshotConnection, error) { + // cache container on local disk + f, err := tar.RandomFile() + if err != nil { + return nil, err + } + + if conf.Options == nil { + conf.Options = map[string]string{} + } + conf.Options[tar.OPTION_FILE] = f.Name() + + tarConnection, err := tar.NewConnection( + id, + conf, + asset, + tar.WithFetchFn(func() (string, error) { + err := exportSnapshot(conf.Host, f) + if err != nil { + return "", err + } + + return f.Name(), nil + }), + tar.WithCloseFn(func() { + // remove temporary file on stream close + _ = os.Remove(f.Name()) + })) + if err != nil { + return nil, err + } + + return &SnapshotConnection{tarConnection}, nil +} + +// ExportSnapshot exports a given container from docker engine to a tar file +func exportSnapshot(containerid string, f *os.File) error { + dc, err := GetDockerClient() + if err != nil { + return err + } + + rc, err := dc.ContainerExport(context.Background(), containerid) + if err != nil { + return err + } + + return tar.StreamToTmpFile(rc, f) +} + +func (p *SnapshotConnection) Name() string { + return string(shared.Type_DockerSnapshot) +} + +func (p *SnapshotConnection) Type() shared.ConnectionType { + return shared.Type_DockerSnapshot +} diff --git a/providers/os/connection/docker/snapshot_connection_test.go b/providers/os/connection/docker/snapshot_connection_test.go new file mode 100644 index 0000000000..e27eecaf1e --- /dev/null +++ b/providers/os/connection/docker/snapshot_connection_test.go @@ -0,0 +1,45 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package docker + +import ( + "context" + "github.com/docker/docker/api/types/container" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.mondoo.com/cnquery/v10/providers-sdk/v1/inventory" + "testing" +) + +func TestSnapshotConnection(t *testing.T) { + ctx := context.Background() + image := "docker.io/nginx:stable" + dClient, err := GetDockerClient() + assert.NoError(t, err) + created := fetchAndCreateImage(t, ctx, dClient, image) + // Make sure the container is cleaned up + defer func() { + err := dClient.ContainerRemove(ctx, created.ID, container.RemoveOptions{ + Force: true, + }) + require.NoError(t, err) + }() + + conn, err := NewSnapshotConnection(0, &inventory.Config{ + Host: created.ID, + }, &inventory.Asset{ + // for the test we need to set the platform + Platform: &inventory.Platform{ + Name: "debian", + Version: "11", + Family: []string{"debian", "linux"}, + }, + }) + require.NoError(t, err) + + fi, err := conn.FileInfo("/etc/os-release") + require.NoError(t, err) + assert.NotNil(t, fi) + assert.True(t, fi.Size > 0) +} diff --git a/providers/os/connection/docker_container_test.go b/providers/os/connection/docker_container_test.go deleted file mode 100644 index a78a94f3c1..0000000000 --- a/providers/os/connection/docker_container_test.go +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) Mondoo, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package connection - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "go.mondoo.com/cnquery/v10/providers-sdk/v1/inventory" -) - -// This test has an external dependency on the gcr.io registry -// To test this specific case, we cannot use a stored image, we need to call remote.Get -func TestAssetNameForRemoteImages(t *testing.T) { - var err error - var conn *TarConnection - var asset *inventory.Asset - retries := 3 - counter := 0 - - for { - config := &inventory.Config{ - Type: "docker-image", - Host: "gcr.io/google-containers/busybox:1.27.2", - } - asset = &inventory.Asset{ - Connections: []*inventory.Config{config}, - } - conn, err = NewDockerContainerImageConnection(0, config, asset) - if counter > retries || (err == nil && conn != nil) { - break - } - counter++ - } - require.NoError(t, err) - require.NotNil(t, conn) - - assert.Equal(t, "gcr.io/google-containers/busybox@545e6a6310a2", asset.Name) - assert.Contains(t, asset.PlatformIds, "//platformid.api.mondoo.app/runtime/docker/images/545e6a6310a27636260920bc07b994a299b6708a1b26910cfefd335fdfb60d2b") -} diff --git a/providers/os/connection/docker_snapshot.go b/providers/os/connection/docker_snapshot.go deleted file mode 100644 index 1ffe469799..0000000000 --- a/providers/os/connection/docker_snapshot.go +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright (c) Mondoo, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package connection - -import ( - "context" - "os" - - "go.mondoo.com/cnquery/v10/providers-sdk/v1/inventory" - "go.mondoo.com/cnquery/v10/providers/os/connection/container/cache" - "go.mondoo.com/cnquery/v10/providers/os/connection/shared" -) - -var _ shared.Connection = &DockerSnapshotConnection{} - -type DockerSnapshotConnection struct { - TarConnection -} - -func NewDockerSnapshotConnection(id uint32, conf *inventory.Config, asset *inventory.Asset) (*DockerSnapshotConnection, error) { - tarConnection, err := NewWithClose(id, conf, asset, func() {}) - if err != nil { - return nil, err - } - - // FIXME: ??? use NewFromDockerEngine - - return &DockerSnapshotConnection{*tarConnection}, nil -} - -// NewFromDockerEngine creates a snapshot for a docker engine container and opens it -func NewFromDockerEngine(id uint32, conf *inventory.Config, asset *inventory.Asset) (*DockerSnapshotConnection, error) { - // cache container on local disk - f, err := cache.RandomFile() - if err != nil { - return nil, err - } - - err = ExportSnapshot(conf.Host, f) - if err != nil { - return nil, err - } - - tarConnection, err := NewWithClose(id, &inventory.Config{ - Type: "tar", - Options: map[string]string{ - OPTION_FILE: f.Name(), - }, - }, asset, func() { - // remove temporary file on stream close - os.Remove(f.Name()) - }) - if err != nil { - return nil, err - } - - return &DockerSnapshotConnection{*tarConnection}, nil -} - -// ExportSnapshot exports a given container from docker engine to a tar file -func ExportSnapshot(containerid string, f *os.File) error { - dc, err := GetDockerClient() - if err != nil { - return err - } - - rc, err := dc.ContainerExport(context.Background(), containerid) - if err != nil { - return err - } - - return cache.StreamToTmpFile(rc, f) -} - -func (p *DockerSnapshotConnection) Name() string { - return string(shared.Type_DockerSnapshot) -} - -func (p *DockerSnapshotConnection) Type() shared.ConnectionType { - return shared.Type_DockerSnapshot -} diff --git a/providers/os/connection/fs/filesystem_test.go b/providers/os/connection/fs/filesystem_test.go index 5d6940ccdc..0a5df722ad 100644 --- a/providers/os/connection/fs/filesystem_test.go +++ b/providers/os/connection/fs/filesystem_test.go @@ -11,8 +11,8 @@ import ( "github.com/stretchr/testify/require" "go.mondoo.com/cnquery/v10/providers-sdk/v1/inventory" "go.mondoo.com/cnquery/v10/providers/os/connection/fs" - "go.mondoo.com/cnquery/v10/providers/os/connection/fs/fsutil" "go.mondoo.com/cnquery/v10/providers/os/detector" + "go.mondoo.com/cnquery/v10/providers/os/fsutil" ) func TestOsDetection(t *testing.T) { diff --git a/providers/os/connection/fs/fsutil/hash.go b/providers/os/connection/fs/fsutil/hash.go deleted file mode 100644 index 49492f2290..0000000000 --- a/providers/os/connection/fs/fsutil/hash.go +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) Mondoo, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package fsutil - -import ( - "crypto/md5" - "crypto/sha256" - "encoding/hex" - "io" - - "github.com/spf13/afero" -) - -func Md5(f afero.File) (string, error) { - h := md5.New() - if _, err := io.Copy(h, f); err != nil { - return "", err - } - - return hex.EncodeToString(h.Sum(nil)), nil -} - -func Sha256(f afero.File) (string, error) { - h := sha256.New() - if _, err := io.Copy(h, f); err != nil { - return "", err - } - - return hex.EncodeToString(h.Sum(nil)), nil -} - -// LocalFileSha256 determines the hashsum for a local file -func LocalFileSha256(filename string) (string, error) { - osFs := afero.NewOsFs() - f, err := osFs.Open(filename) - if err != nil { - return "", err - } - - defer f.Close() - hash, err := Sha256(f) - if err != nil { - return "", err - } - return hash, nil -} diff --git a/providers/os/connection/fs/fsutil/hash_test.go b/providers/os/connection/fs/fsutil/hash_test.go deleted file mode 100644 index 56d29886dc..0000000000 --- a/providers/os/connection/fs/fsutil/hash_test.go +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) Mondoo, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package fsutil_test - -import ( - "testing" - - "github.com/spf13/afero" - "github.com/stretchr/testify/assert" - "go.mondoo.com/cnquery/v10/providers-sdk/v1/inventory" - "go.mondoo.com/cnquery/v10/providers/os/connection/fs/fsutil" - "go.mondoo.com/cnquery/v10/providers/os/connection/local" -) - -func TestFileResource(t *testing.T) { - path := "/tmp/test_hash" - - conn := local.NewConnection(0, nil, &inventory.Asset{}) - assert.NotNil(t, conn) - - fs := conn.FileSystem() - afutil := afero.Afero{Fs: fs} - - // create the file and set the content - err := afutil.WriteFile(path, []byte("hello world"), 0o666) - assert.Nil(t, err) - - f, err := fs.Open(path) - assert.Nil(t, err) - if assert.NotNil(t, f) { - assert.Equal(t, path, f.Name(), "they should be equal") - - md5, err := fsutil.Md5(f) - assert.Nil(t, err) - assert.Equal(t, "5eb63bbbe01eeed093cb22bb8f5acdc3", md5) - - sha256, err := fsutil.Sha256(f) - assert.Nil(t, err) - assert.Equal(t, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", sha256) - } -} diff --git a/providers/os/connection/ssh.go b/providers/os/connection/ssh/ssh.go similarity index 95% rename from providers/os/connection/ssh.go rename to providers/os/connection/ssh/ssh.go index 5c22418bd7..e4072fe206 100644 --- a/providers/os/connection/ssh.go +++ b/providers/os/connection/ssh/ssh.go @@ -1,7 +1,7 @@ // Copyright (c) Mondoo, Inc. // SPDX-License-Identifier: BUSL-1.1 -package connection +package ssh import ( "bytes" @@ -38,11 +38,11 @@ import ( ) var ( - _ shared.Connection = (*SshConnection)(nil) - _ plugin.Closer = (*SshConnection)(nil) + _ shared.Connection = (*Connection)(nil) + _ plugin.Closer = (*Connection)(nil) ) -type SshConnection struct { +type Connection struct { plugin.Connection conf *inventory.Config asset *inventory.Asset @@ -56,8 +56,8 @@ type SshConnection struct { SSHClient *ssh.Client } -func NewSshConnection(id uint32, conf *inventory.Config, asset *inventory.Asset) (*SshConnection, error) { - res := SshConnection{ +func NewConnection(id uint32, conf *inventory.Config, asset *inventory.Asset) (*Connection, error) { + res := Connection{ Connection: plugin.NewConnection(id, asset), conf: conf, asset: asset, @@ -119,30 +119,30 @@ func NewSshConnection(id uint32, conf *inventory.Config, asset *inventory.Asset) return &res, nil } -func (c *SshConnection) Name() string { +func (c *Connection) Name() string { return "ssh" } -func (c *SshConnection) Type() shared.ConnectionType { +func (c *Connection) Type() shared.ConnectionType { return shared.Type_SSH } -func (p *SshConnection) Asset() *inventory.Asset { +func (p *Connection) Asset() *inventory.Asset { return p.asset } -func (p *SshConnection) Capabilities() shared.Capabilities { +func (p *Connection) Capabilities() shared.Capabilities { return shared.Capability_File | shared.Capability_RunCommand } -func (c *SshConnection) RunCommand(command string) (*shared.Command, error) { +func (c *Connection) RunCommand(command string) (*shared.Command, error) { if c.Sudo != nil && c.Sudo.Active { command = shared.BuildSudoCommand(c.Sudo, command) } return c.runRawCommand(command) } -func (c *SshConnection) runRawCommand(command string) (*shared.Command, error) { +func (c *Connection) runRawCommand(command string) (*shared.Command, error) { log.Debug().Str("command", command).Str("provider", "ssh").Msg("run command") if c.SSHClient == nil { @@ -197,7 +197,7 @@ func (c *SshConnection) runRawCommand(command string) (*shared.Command, error) { return &res, err } -func (c *SshConnection) FileSystem() afero.Fs { +func (c *Connection) FileSystem() afero.Fs { if c.fs != nil { return c.fs } @@ -244,7 +244,7 @@ func (c *SshConnection) FileSystem() afero.Fs { return c.fs } -func (c *SshConnection) FileInfo(path string) (shared.FileInfoDetails, error) { +func (c *Connection) FileInfo(path string) (shared.FileInfoDetails, error) { fs := c.FileSystem() afs := &afero.Afero{Fs: fs} stat, err := afs.Stat(path) @@ -276,14 +276,14 @@ func (c *SshConnection) FileInfo(path string) (shared.FileInfoDetails, error) { }, nil } -func (c *SshConnection) Close() { +func (c *Connection) Close() { if c.SSHClient != nil { c.SSHClient.Close() } } // checks the connection config and set default values if not provided by the user -func (c *SshConnection) setDefaultSettings() { +func (c *Connection) setDefaultSettings() { // we always want to ensure we use the default port if nothing was specified if c.conf.Port == 0 { c.conf.Port = 22 @@ -295,7 +295,7 @@ func (c *SshConnection) setDefaultSettings() { } } -func (c *SshConnection) Connect() error { +func (c *Connection) Connect() error { cc := c.conf c.setDefaultSettings() @@ -353,7 +353,7 @@ func (c *SshConnection) Connect() error { return nil } -func (c *SshConnection) PlatformID() (string, error) { +func (c *Connection) PlatformID() (string, error) { return PlatformIdentifier(c.HostKey), nil } @@ -666,7 +666,7 @@ func prepareConnection(conf *inventory.Config) ([]ssh.AuthMethod, []io.Closer, e return auths, closer, nil } -func (c *SshConnection) verify() error { +func (c *Connection) verify() error { var out *shared.Command var err error if c.Sudo != nil { diff --git a/providers/os/connection/ssh_test.go b/providers/os/connection/ssh/ssh_test.go similarity index 74% rename from providers/os/connection/ssh_test.go rename to providers/os/connection/ssh/ssh_test.go index 364c42ad1b..f52919f526 100644 --- a/providers/os/connection/ssh_test.go +++ b/providers/os/connection/ssh/ssh_test.go @@ -1,7 +1,7 @@ // Copyright (c) Mondoo, Inc. // SPDX-License-Identifier: BUSL-1.1 -package connection +package ssh import ( "testing" @@ -12,7 +12,7 @@ import ( ) func TestSSHDefaultSettings(t *testing.T) { - conn := &SshConnection{ + conn := &Connection{ conf: &inventory.Config{ Sudo: &inventory.Sudo{ Active: true, @@ -25,12 +25,12 @@ func TestSSHDefaultSettings(t *testing.T) { } func TestSSHProviderError(t *testing.T) { - _, err := NewSshConnection(0, &inventory.Config{Type: shared.Type_Local.String(), Host: "example.local"}, &inventory.Asset{}) + _, err := NewConnection(0, &inventory.Config{Type: shared.Type_Local.String(), Host: "example.local"}, &inventory.Asset{}) assert.Equal(t, "provider type does not match", err.Error()) } func TestSSHAuthError(t *testing.T) { - _, err := NewSshConnection(0, &inventory.Config{Type: shared.Type_SSH.String(), Host: "example.local"}, &inventory.Asset{}) + _, err := NewConnection(0, &inventory.Config{Type: shared.Type_SSH.String(), Host: "example.local"}, &inventory.Asset{}) assert.True(t, // local testing if ssh agent is available err.Error() == "dial tcp: lookup example.local: no such host" || diff --git a/providers/os/connection/tar.go b/providers/os/connection/tar.go deleted file mode 100644 index e2585a1cc3..0000000000 --- a/providers/os/connection/tar.go +++ /dev/null @@ -1,357 +0,0 @@ -// Copyright (c) Mondoo, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package connection - -import ( - "archive/tar" - "bytes" - "errors" - "io" - "os" - "sync" - - v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/google/go-containerregistry/pkg/v1/mutate" - "github.com/google/go-containerregistry/pkg/v1/tarball" - "github.com/rs/zerolog/log" - "github.com/spf13/afero" - "go.mondoo.com/cnquery/v10/providers-sdk/v1/inventory" - "go.mondoo.com/cnquery/v10/providers-sdk/v1/plugin" - "go.mondoo.com/cnquery/v10/providers/os/connection/container/cache" - "go.mondoo.com/cnquery/v10/providers/os/connection/shared" - provider_tar "go.mondoo.com/cnquery/v10/providers/os/connection/tar" - "go.mondoo.com/cnquery/v10/providers/os/fsutil" - "go.mondoo.com/cnquery/v10/providers/os/id/containerid" -) - -const ( - OPTION_FILE = "path" - FLATTENED_IMAGE = "flattened_path" -) - -var ( - _ shared.Connection = (*TarConnection)(nil) - _ plugin.Closer = (*TarConnection)(nil) -) - -type TarConnection struct { - plugin.Connection - asset *inventory.Asset - conf *inventory.Config - fetchFn func() (string, error) - fetchOnce sync.Once - - Fs *provider_tar.FS - CloseFN func() - // fields are exposed since the tar backend is re-used for the docker backend - PlatformKind string - PlatformRuntime string - PlatformIdentifier string - PlatformArchitecture string - // optional metadata to store additional information - Metadata struct { - Name string - Labels map[string]string - } -} - -func (p *TarConnection) Name() string { - return string(shared.Type_Tar) -} - -func (p *TarConnection) Type() shared.ConnectionType { - return shared.Type_Tar -} - -func (p *TarConnection) Asset() *inventory.Asset { - return p.asset -} - -func (p *TarConnection) Conf() *inventory.Config { - return p.conf -} - -func (c *TarConnection) Identifier() (string, error) { - return c.PlatformIdentifier, nil -} - -func (c *TarConnection) Capabilities() shared.Capabilities { - return shared.Capability_File | shared.Capability_FileSearch | shared.Capability_FindFile -} - -func (p *TarConnection) RunCommand(command string) (*shared.Command, error) { - res := shared.Command{Command: command, Stdout: &bytes.Buffer{}, Stderr: &bytes.Buffer{}, ExitStatus: -1} - return &res, nil -} - -func (p *TarConnection) EnsureLoaded() { - if p.fetchFn != nil { - p.fetchOnce.Do(func() { - f, err := p.fetchFn() - if err != nil { - log.Error().Err(err).Msg("tar> could not fetch tar file") - return - } - if err := p.LoadFile(f); err != nil { - log.Error().Err(err).Msg("tar> could not load tar file") - return - } - }) - } -} - -func (p *TarConnection) FileSystem() afero.Fs { - p.EnsureLoaded() - return p.Fs -} - -func (c *TarConnection) FileInfo(path string) (shared.FileInfoDetails, error) { - fs := c.FileSystem() - afs := &afero.Afero{Fs: fs} - stat, err := afs.Stat(path) - if err != nil { - return shared.FileInfoDetails{}, err - } - - uid := int64(-1) - gid := int64(-1) - if stat, ok := stat.Sys().(*tar.Header); ok { - uid = int64(stat.Uid) - gid = int64(stat.Gid) - } - mode := stat.Mode() - - return shared.FileInfoDetails{ - Mode: shared.FileModeDetails{mode}, - Size: stat.Size(), - Uid: uid, - Gid: gid, - }, nil -} - -func (c *TarConnection) Close() { - if c.CloseFN != nil { - c.CloseFN() - } -} - -func (c *TarConnection) Load(stream io.Reader) error { - tr := tar.NewReader(stream) - for { - h, err := tr.Next() - if err == io.EOF { - break - } - if err != nil { - log.Error().Err(err).Msg("tar> error reading tar stream") - return err - } - - path := provider_tar.Abs(h.Name) - c.Fs.FileMap[path] = h - } - log.Debug().Int("files", len(c.Fs.FileMap)).Msg("tar> successfully loaded") - return nil -} - -func (c *TarConnection) LoadFile(path string) error { - log.Debug().Str("path", path).Msg("tar> load tar file into backend") - - f, err := os.Open(path) - if err != nil { - return err - } - defer f.Close() - return c.Load(f) -} - -func (c *TarConnection) Kind() string { - return c.PlatformKind -} - -func (c *TarConnection) Runtime() string { - return c.PlatformRuntime -} - -func NewTarConnectionForContainer(id uint32, conf *inventory.Config, asset *inventory.Asset, img v1.Image) (*TarConnection, error) { - f, err := cache.RandomFile() - if err != nil { - return nil, err - } - - return &TarConnection{ - Connection: plugin.NewConnection(id, asset), - asset: asset, - Fs: provider_tar.NewFs(f.Name()), - fetchFn: func() (string, error) { - err = cache.StreamToTmpFile(mutate.Extract(img), f) - if err != nil { - os.Remove(f.Name()) - return "", err - } - log.Debug().Msg("tar> extracted image to temporary file") - asset.Connections[0].Options[FLATTENED_IMAGE] = f.Name() - return f.Name(), nil - }, - CloseFN: func() { - log.Debug().Str("tar", f.Name()).Msg("tar> remove temporary tar file on connection close") - os.Remove(f.Name()) - }, - PlatformKind: conf.Type, - PlatformRuntime: conf.Runtime, - conf: conf, - }, nil -} - -// TODO: this one is used by plain tar connection -func NewTarConnection(id uint32, conf *inventory.Config, asset *inventory.Asset) (*TarConnection, error) { - return NewWithClose(id, conf, asset, nil) -} - -// Used with docker snapshots -// NewWithReader provides a tar provider from a container image stream -func NewWithReader(id uint32, conf *inventory.Config, asset *inventory.Asset, rc io.ReadCloser) (*TarConnection, error) { - filename := "" - if x, ok := rc.(*os.File); ok { - filename = x.Name() - } else { - // cache file locally - f, err := cache.RandomFile() - if err != nil { - return nil, err - } - - // we return a pure tar image - filename = f.Name() - - err = cache.StreamToTmpFile(rc, f) - if err != nil { - os.Remove(filename) - return nil, err - } - } - - return NewWithClose(id, &inventory.Config{ - Type: "tar", - Runtime: "docker-image", - Options: map[string]string{ - OPTION_FILE: filename, - }, - }, asset, func() { - log.Debug().Str("tar", filename).Msg("tar> remove temporary tar file on connection close") - os.Remove(filename) - }) -} - -func NewWithClose(id uint32, conf *inventory.Config, asset *inventory.Asset, closeFn func()) (*TarConnection, error) { - if conf == nil || len(conf.Options[OPTION_FILE]) == 0 { - return nil, errors.New("tar provider requires a valid tar file") - } - - filename := conf.Options[OPTION_FILE] - var identifier string - - // try to determine if the tar is a container image - img, iErr := tarball.ImageFromPath(filename, nil) - if iErr == nil { - hash, err := img.Digest() - if err != nil { - return nil, err - } - identifier = containerid.MondooContainerImageID(hash.String()) - - // we cache the flattened image locally - c, err := newWithFlattenedImage(id, conf, asset, &img, closeFn) - if err != nil { - return nil, err - } - - c.PlatformIdentifier = identifier - return c, nil - } else { - hash, err := fsutil.LocalFileSha256(filename) - if err != nil { - return nil, err - } - identifier = "//platformid.api.mondoo.app/runtime/tar/hash/" + hash - - c := &TarConnection{ - Connection: plugin.NewConnection(id, asset), - asset: asset, - Fs: provider_tar.NewFs(filename), - CloseFN: closeFn, - PlatformKind: conf.Type, - PlatformRuntime: conf.Runtime, - } - - err = c.LoadFile(filename) - if err != nil { - log.Error().Err(err).Str("tar", filename).Msg("tar> could not load tar file") - return nil, err - } - - c.PlatformIdentifier = identifier - return c, nil - } -} - -func newWithFlattenedImage(id uint32, conf *inventory.Config, asset *inventory.Asset, img *v1.Image, closeFn func()) (*TarConnection, error) { - imageFilename := "" - useCached := false - if asset != nil && len(asset.Connections) > 0 { - if x, ok := asset.Connections[0].Options[FLATTENED_IMAGE]; ok && x != "" { - log.Debug().Str("tar", asset.Connections[0].Options[FLATTENED_IMAGE]).Msg("tar> use cached tar file") - imageFilename = asset.Connections[0].Options[FLATTENED_IMAGE] - useCached = true - } - } - if !useCached { - f, err := cache.RandomFile() - if err != nil { - return nil, err - } - imageFilename = f.Name() - err = cache.StreamToTmpFile(mutate.Extract(*img), f) - if err != nil { - os.Remove(imageFilename) - return nil, err - } - } - - c := &TarConnection{ - Connection: plugin.NewConnection(id, asset), - asset: asset, - Fs: provider_tar.NewFs(imageFilename), - CloseFN: func() { - if closeFn != nil { - closeFn() - } - // remove temporary file on stream close - log.Debug().Str("tar", imageFilename).Msg("tar> remove temporary flattened image file on connection close") - os.Remove(imageFilename) - }, - PlatformKind: conf.Type, - PlatformRuntime: conf.Runtime, - conf: &inventory.Config{ - Options: map[string]string{ - OPTION_FILE: imageFilename, - }, - }, - } - if asset != nil && len(asset.Connections) > 0 { - if asset.Connections[0].Options == nil { - asset.Connections[0].Options = map[string]string{} - } - asset.Connections[0].Options[FLATTENED_IMAGE] = imageFilename - } - - err := c.LoadFile(imageFilename) - if err != nil { - log.Error().Err(err).Str("tar", imageFilename).Msg("tar> could not load tar file") - os.Remove(imageFilename) - return nil, err - } - - return c, nil -} diff --git a/providers/os/connection/tar/connection.go b/providers/os/connection/tar/connection.go index c46c2c4051..b6120b6b3d 100644 --- a/providers/os/connection/tar/connection.go +++ b/providers/os/connection/tar/connection.go @@ -3,24 +3,232 @@ package tar -/* -func (c *TarConnection) Identifier() (string, error) { +import ( + "archive/tar" + "bytes" + "errors" + "io" + "os" + "sync" + + "github.com/rs/zerolog/log" + "github.com/spf13/afero" + "go.mondoo.com/cnquery/v10/providers-sdk/v1/inventory" + "go.mondoo.com/cnquery/v10/providers-sdk/v1/plugin" + "go.mondoo.com/cnquery/v10/providers/os/connection/shared" + "go.mondoo.com/cnquery/v10/providers/os/fsutil" +) + +const ( + OPTION_FILE = "path" +) + +var ( + _ shared.Connection = (*Connection)(nil) + _ plugin.Closer = (*Connection)(nil) +) + +type Connection struct { + plugin.Connection + asset *inventory.Asset + conf *inventory.Config + fetchFn func() (string, error) + fetchOnce sync.Once + + fs *FS + closeFN func() + // fields are exposed since the tar backend is re-used for the docker backend + PlatformKind string + PlatformRuntime string + PlatformIdentifier string + PlatformArchitecture string + // optional metadata to store additional information + Metadata struct { + Name string + Labels map[string]string + } +} + +func (p *Connection) Name() string { + return string(shared.Type_Tar) +} + +func (p *Connection) Type() shared.ConnectionType { + return shared.Type_Tar +} + +func (p *Connection) Asset() *inventory.Asset { + return p.asset +} + +func (p *Connection) Conf() *inventory.Config { + return p.conf +} + +func (c *Connection) Identifier() (string, error) { return c.PlatformIdentifier, nil } -func (c *TarConnection) Labels() map[string]string { - return c.Metadata.Labels +func (c *Connection) Capabilities() shared.Capabilities { + return shared.Capability_File | shared.Capability_FileSearch | shared.Capability_FindFile +} + +func (p *Connection) RunCommand(command string) (*shared.Command, error) { + res := shared.Command{Command: command, Stdout: &bytes.Buffer{}, Stderr: &bytes.Buffer{}, ExitStatus: -1} + return &res, nil +} + +func (p *Connection) EnsureLoaded() { + if p.fetchFn != nil { + p.fetchOnce.Do(func() { + f, err := p.fetchFn() + if err != nil { + log.Error().Err(err).Msg("tar> could not fetch tar file") + return + } + if err := p.LoadFile(f); err != nil { + log.Error().Err(err).Msg("tar> could not load tar file") + return + } + }) + } +} + +func (p *Connection) FileSystem() afero.Fs { + p.EnsureLoaded() + return p.fs +} + +func (c *Connection) FileInfo(path string) (shared.FileInfoDetails, error) { + fs := c.FileSystem() + afs := &afero.Afero{Fs: fs} + stat, err := afs.Stat(path) + if err != nil { + return shared.FileInfoDetails{}, err + } + + uid := int64(-1) + gid := int64(-1) + if stat, ok := stat.Sys().(*tar.Header); ok { + uid = int64(stat.Uid) + gid = int64(stat.Gid) + } + mode := stat.Mode() + + return shared.FileInfoDetails{ + Mode: shared.FileModeDetails{mode}, + Size: stat.Size(), + Uid: uid, + Gid: gid, + }, nil +} + +func (c *Connection) Close() { + if c.closeFN != nil { + c.closeFN() + } +} + +func (c *Connection) Load(stream io.Reader) error { + tr := tar.NewReader(stream) + for { + h, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + log.Error().Err(err).Msg("tar> error reading tar stream") + return err + } + + path := Abs(h.Name) + c.fs.FileMap[path] = h + } + log.Debug().Int("files", len(c.fs.FileMap)).Msg("tar> successfully loaded") + return nil +} + +func (c *Connection) LoadFile(path string) error { + log.Debug().Str("path", path).Msg("tar> load tar file into backend") + + f, err := os.Open(path) + if err != nil { + return err + } + defer f.Close() + return c.Load(f) +} + +func (c *Connection) Kind() string { + return c.PlatformKind +} + +func (c *Connection) Runtime() string { + return c.PlatformRuntime +} + +type tarConnectionOptions struct { + // provide a close function which is called when the connection is closed + closeFn func() + // function to fetch the tar file from a remote location on first access + fetchFn func() (string, error) +} + +type tarClientOption func(*tarConnectionOptions) + +func WithCloseFn(closeFn func()) tarClientOption { + return func(o *tarConnectionOptions) { + o.closeFn = closeFn + } } -func (c *TarConnection) PlatformName() string { - return c.Metadata.Name +func WithFetchFn(fetchFn func() (string, error)) tarClientOption { + return func(o *tarConnectionOptions) { + o.fetchFn = fetchFn + } } -*/ +// NewConnection is opening a tar file and creating a new tar connection. The tar file is expected to be a valid +// tar file and contains a flattened file structure. Nested tar files as used in docker images are not supported and +// need to be extracted before using this connection. +func NewConnection(id uint32, conf *inventory.Config, asset *inventory.Asset, opts ...tarClientOption) (*Connection, error) { + if conf == nil || len(conf.Options[OPTION_FILE]) == 0 { + return nil, errors.New("tar provider requires a valid tar file") + } -// FIXME: How to handle this now? -// func (c *TarConnection) PlatformIdDetectors() []providers.PlatformIdDetector { -// return []providers.PlatformIdDetector{ -// providers.TransportPlatformIdentifierDetector, -// } -// } + filename := conf.Options[OPTION_FILE] + var identifier string + + hash, err := fsutil.LocalFileSha256(filename) + if err != nil { + return nil, err + } + identifier = "//platformid.api.mondoo.app/runtime/tar/hash/" + hash + + params := &tarConnectionOptions{} + for _, o := range opts { + o(params) + } + + c := &Connection{ + Connection: plugin.NewConnection(id, asset), + asset: asset, + fs: NewFs(filename), + closeFN: params.closeFn, + fetchFn: params.fetchFn, + PlatformKind: conf.Type, + PlatformRuntime: conf.Runtime, + conf: conf, + } + + // if no fetch function is provided, we use the local file as source which allows to use load the tar file + // from a local file system asynchronous. + if params.fetchFn == nil { + c.fetchFn = func() (string, error) { + return filename, nil + } + } + + c.PlatformIdentifier = identifier + return c, nil +} diff --git a/providers/os/connection/tar_test.go b/providers/os/connection/tar/connection_test.go similarity index 87% rename from providers/os/connection/tar_test.go rename to providers/os/connection/tar/connection_test.go index 24d7ee5e8f..f5414d945b 100644 --- a/providers/os/connection/tar_test.go +++ b/providers/os/connection/tar/connection_test.go @@ -1,23 +1,23 @@ // Copyright (c) Mondoo, Inc. // SPDX-License-Identifier: BUSL-1.1 -package connection_test +package tar_test import ( "io" "net/http" "os" "regexp" + "strings" "testing" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/mutate" "github.com/google/go-containerregistry/pkg/v1/remote" - "github.com/google/go-containerregistry/pkg/v1/tarball" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.mondoo.com/cnquery/v10/providers-sdk/v1/inventory" - "go.mondoo.com/cnquery/v10/providers/os/connection" "go.mondoo.com/cnquery/v10/providers/os/connection/tar" ) @@ -33,10 +33,10 @@ func TestTarCommand(t *testing.T) { err := cacheAlpine() require.NoError(t, err, "should create tar without error") - c, err := connection.NewTarConnection(0, &inventory.Config{ + c, err := tar.NewConnection(0, &inventory.Config{ Type: "tar", Options: map[string]string{ - connection.OPTION_FILE: alpineContainerPath, + tar.OPTION_FILE: alpineContainerPath, }, }, &inventory.Asset{}) assert.Equal(t, nil, err, "should create tar without error") @@ -57,26 +57,27 @@ func TestPlatformIdentifier(t *testing.T) { err := cacheAlpine() require.NoError(t, err, "should create tar without error") - conn, err := connection.NewTarConnection(0, &inventory.Config{ + conn, err := tar.NewConnection(0, &inventory.Config{ Type: "tar", Options: map[string]string{ - connection.OPTION_FILE: alpineContainerPath, + tar.OPTION_FILE: alpineContainerPath, }, }, &inventory.Asset{}) require.NoError(t, err) platformId, err := conn.Identifier() require.NoError(t, err) assert.True(t, len(platformId) > 0) + assert.True(t, strings.HasPrefix(platformId, "//platformid.api.mondoo.app/runtime/tar/hash/")) } func TestTarSymlinkFile(t *testing.T) { err := cacheAlpine() require.NoError(t, err, "should create tar without error") - c, err := connection.NewTarConnection(0, &inventory.Config{ + c, err := tar.NewConnection(0, &inventory.Config{ Type: "tar", Options: map[string]string{ - connection.OPTION_FILE: alpineContainerPath, + tar.OPTION_FILE: alpineContainerPath, }, }, &inventory.Asset{}) assert.Equal(t, nil, err, "should create tar without error") @@ -105,10 +106,10 @@ func TestTarRelativeSymlinkFileCentos(t *testing.T) { err := cacheCentos() require.NoError(t, err, "should create tar without error") - c, err := connection.NewTarConnection(0, &inventory.Config{ + c, err := tar.NewConnection(0, &inventory.Config{ Type: "tar", Options: map[string]string{ - connection.OPTION_FILE: centosContainerPath, + tar.OPTION_FILE: centosContainerPath, }, }, &inventory.Asset{}) assert.Equal(t, nil, err, "should create tar without error") @@ -136,10 +137,10 @@ func TestTarFile(t *testing.T) { err := cacheAlpine() require.NoError(t, err, "should create tar without error") - c, err := connection.NewTarConnection(0, &inventory.Config{ + c, err := tar.NewConnection(0, &inventory.Config{ Type: "tar", Options: map[string]string{ - connection.OPTION_FILE: alpineContainerPath, + tar.OPTION_FILE: alpineContainerPath, }, }, &inventory.Asset{}) assert.Equal(t, nil, err, "should create tar without error") @@ -166,10 +167,10 @@ func TestFilePermissions(t *testing.T) { err := cacheAlpine() require.NoError(t, err, "should create tar without error") - c, err := connection.NewTarConnection(0, &inventory.Config{ + c, err := tar.NewConnection(0, &inventory.Config{ Type: "tar", Options: map[string]string{ - connection.OPTION_FILE: alpineContainerPath, + tar.OPTION_FILE: alpineContainerPath, }, }, &inventory.Asset{}) require.NoError(t, err) @@ -223,10 +224,10 @@ func TestTarFileFind(t *testing.T) { err := cacheAlpine() require.NoError(t, err, "should create tar without error") - c, err := connection.NewTarConnection(0, &inventory.Config{ + c, err := tar.NewConnection(0, &inventory.Config{ Type: "tar", Options: map[string]string{ - connection.OPTION_FILE: alpineContainerPath, + tar.OPTION_FILE: alpineContainerPath, }, }, &inventory.Asset{}) assert.Equal(t, nil, err, "should create tar without error") @@ -271,5 +272,12 @@ func cacheImageToTar(source string, filename string) error { return err } - return tarball.WriteToFile(filename, tag, img) + // it is important that we extract the image here, since tar does not understand the OCI image + // format and its layers + w, err := os.Create(filename) + if err != nil { + return err + } + + return tar.StreamToTmpFile(mutate.Extract(img), w) } diff --git a/providers/os/connection/container/cache/stream.go b/providers/os/connection/tar/stream.go similarity index 81% rename from providers/os/connection/container/cache/stream.go rename to providers/os/connection/tar/stream.go index 139e5d8e2c..aee0695e9e 100644 --- a/providers/os/connection/container/cache/stream.go +++ b/providers/os/connection/tar/stream.go @@ -1,7 +1,7 @@ // Copyright (c) Mondoo, Inc. // SPDX-License-Identifier: BUSL-1.1 -package cache +package tar import ( "io" @@ -12,7 +12,7 @@ func RandomFile() (*os.File, error) { return os.CreateTemp("", "mondoo.inspection") } -// This streams a binary stream into a file. The user of this method +// StreamToTmpFile streams a binary stream into a file. The user of this method // is responsible for deleting the file late func StreamToTmpFile(r io.ReadCloser, outFile *os.File) error { defer outFile.Close() diff --git a/providers/os/connection/vagrant.go b/providers/os/connection/vagrant/vagrant.go similarity index 84% rename from providers/os/connection/vagrant.go rename to providers/os/connection/vagrant/vagrant.go index a23f91e426..3f6f815143 100644 --- a/providers/os/connection/vagrant.go +++ b/providers/os/connection/vagrant/vagrant.go @@ -1,7 +1,7 @@ // Copyright (c) Mondoo, Inc. // SPDX-License-Identifier: BUSL-1.1 -package connection +package vagrant import ( "errors" @@ -11,7 +11,7 @@ import ( "go.mondoo.com/cnquery/v10/providers-sdk/v1/vault" "go.mondoo.com/cnquery/v10/providers/os/connection/local" "go.mondoo.com/cnquery/v10/providers/os/connection/shared" - "go.mondoo.com/cnquery/v10/providers/os/connection/vagrant" + "go.mondoo.com/cnquery/v10/providers/os/connection/ssh" "go.mondoo.com/cnquery/v10/providers/os/id/ids" ) @@ -22,7 +22,7 @@ const ( var _ shared.Connection = (*VagrantConnection)(nil) type VagrantConnection struct { - SshConnection + ssh.Connection } func NewVagrantConnection(id uint32, conf *inventory.Config, asset *inventory.Asset) (*VagrantConnection, error) { @@ -32,7 +32,7 @@ func NewVagrantConnection(id uint32, conf *inventory.Config, asset *inventory.As return nil, err } res := VagrantConnection{ - SshConnection: *conn, + Connection: *conn, } return &res, nil @@ -46,7 +46,7 @@ func (p *VagrantConnection) Type() shared.ConnectionType { return Vagrant } -func resolveVagrantSshConf(id uint32, conf *inventory.Config, root *inventory.Asset) (*SshConnection, error) { +func resolveVagrantSshConf(id uint32, conf *inventory.Config, root *inventory.Asset) (*ssh.Connection, error) { // For now, we do not provide the conf to the local connection // conf might include sudo, which is only intended for the actual vagrant connection // local currently does not need it. Quite the contrary, it cause issues. @@ -59,7 +59,7 @@ func resolveVagrantSshConf(id uint32, conf *inventory.Config, root *inventory.As return nil, err } - vmStatus, err := vagrant.ParseVagrantStatus(cmd.Stdout) + vmStatus, err := ParseVagrantStatus(cmd.Stdout) if err != nil { return nil, err } @@ -79,7 +79,7 @@ func resolveVagrantSshConf(id uint32, conf *inventory.Config, root *inventory.As return nil, err } - vmSshConfig, err := vagrant.ParseVagrantSshConfig(cmd.Stdout) + vmSshConfig, err := ParseVagrantSshConfig(cmd.Stdout) if err != nil { return nil, err } @@ -88,10 +88,10 @@ func resolveVagrantSshConf(id uint32, conf *inventory.Config, root *inventory.As if err != nil { return nil, err } - return NewSshConnection(id, root.Connections[0], root) + return ssh.NewConnection(id, root.Connections[0], root) } -func migrateVagrantAssetToSsh(id uint32, sshConfig *vagrant.VagrantVmSSHConfig, rootTransportConfig *inventory.Config, asset *inventory.Asset) error { +func migrateVagrantAssetToSsh(id uint32, sshConfig *VagrantVmSSHConfig, rootTransportConfig *inventory.Config, asset *inventory.Asset) error { if sshConfig == nil { return errors.New("missing vagrant ssh config") } diff --git a/providers/os/connection/winrm.go b/providers/os/connection/winrm/winrm.go similarity index 79% rename from providers/os/connection/winrm.go rename to providers/os/connection/winrm/winrm.go index 63a969444c..dad88e6f87 100644 --- a/providers/os/connection/winrm.go +++ b/providers/os/connection/winrm/winrm.go @@ -1,7 +1,7 @@ // Copyright (c) Mondoo, Inc. // SPDX-License-Identifier: BUSL-1.1 -package connection +package winrm import ( "bytes" @@ -16,11 +16,10 @@ import ( "go.mondoo.com/cnquery/v10/providers-sdk/v1/plugin" "go.mondoo.com/cnquery/v10/providers-sdk/v1/vault" "go.mondoo.com/cnquery/v10/providers/os/connection/shared" - winrmConn "go.mondoo.com/cnquery/v10/providers/os/connection/winrm" "go.mondoo.com/cnquery/v10/providers/os/connection/winrm/cat" ) -var _ shared.Connection = (*WinrmConnection)(nil) +var _ shared.Connection = (*Connection)(nil) func VerifyConfig(config *inventory.Config) (*winrm.Endpoint, error) { if config.Type != string(shared.Type_Winrm) { @@ -40,8 +39,8 @@ func VerifyConfig(config *inventory.Config) (*winrm.Endpoint, error) { return winrmEndpoint, nil } -// NewWinrmConnection creates a winrm client and establishes a connection to verify the connection -func NewWinrmConnection(id uint32, conf *inventory.Config, asset *inventory.Asset) (*WinrmConnection, error) { +// NewConnection creates a winrm client and establishes a connection to verify the connection +func NewConnection(id uint32, conf *inventory.Config, asset *inventory.Asset) (*Connection, error) { // ensure all required configs are set winrmEndpoint, err := VerifyConfig(conf) if err != nil { @@ -49,7 +48,7 @@ func NewWinrmConnection(id uint32, conf *inventory.Config, asset *inventory.Asse } // set default config if required - winrmEndpoint = winrmConn.DefaultConfig(winrmEndpoint) + winrmEndpoint = DefaultConfig(winrmEndpoint) params := winrm.DefaultParameters params.TransportDecorator = func() winrm.Transporter { return &winrm.ClientNTLM{} } @@ -78,7 +77,7 @@ func NewWinrmConnection(id uint32, conf *inventory.Config, asset *inventory.Asse } log.Debug().Msg("winrm> connection established") - return &WinrmConnection{ + return &Connection{ Connection: plugin.NewConnection(id, asset), conf: conf, asset: asset, @@ -87,7 +86,7 @@ func NewWinrmConnection(id uint32, conf *inventory.Config, asset *inventory.Asse }, nil } -type WinrmConnection struct { +type Connection struct { plugin.Connection conf *inventory.Config asset *inventory.Asset @@ -98,23 +97,23 @@ type WinrmConnection struct { Client *winrm.Client } -func (c *WinrmConnection) Name() string { +func (c *Connection) Name() string { return "ssh" } -func (c *WinrmConnection) Type() shared.ConnectionType { +func (c *Connection) Type() shared.ConnectionType { return shared.Type_Winrm } -func (p *WinrmConnection) Asset() *inventory.Asset { +func (p *Connection) Asset() *inventory.Asset { return p.asset } -func (p *WinrmConnection) Capabilities() shared.Capabilities { +func (p *Connection) Capabilities() shared.Capabilities { return shared.Capability_File | shared.Capability_RunCommand } -func (p *WinrmConnection) RunCommand(command string) (*shared.Command, error) { +func (p *Connection) RunCommand(command string) (*shared.Command, error) { log.Debug().Str("command", command).Str("provider", "winrm").Msg("winrm> run command") stdoutBuffer := &bytes.Buffer{} @@ -143,7 +142,7 @@ func (p *WinrmConnection) RunCommand(command string) (*shared.Command, error) { return res, nil } -func (p *WinrmConnection) FileInfo(path string) (shared.FileInfoDetails, error) { +func (p *Connection) FileInfo(path string) (shared.FileInfoDetails, error) { fs := p.FileSystem() afs := &afero.Afero{Fs: fs} stat, err := afs.Stat(path) @@ -163,7 +162,7 @@ func (p *WinrmConnection) FileInfo(path string) (shared.FileInfoDetails, error) }, nil } -func (p *WinrmConnection) FileSystem() afero.Fs { +func (p *Connection) FileSystem() afero.Fs { if p.fs == nil { p.fs = cat.New(p) } diff --git a/providers/os/connection/winrm_test.go b/providers/os/connection/winrm/winrm_test.go similarity index 91% rename from providers/os/connection/winrm_test.go rename to providers/os/connection/winrm/winrm_test.go index 459d4f5ca2..ecc3804116 100644 --- a/providers/os/connection/winrm_test.go +++ b/providers/os/connection/winrm/winrm_test.go @@ -4,7 +4,7 @@ //go:build debugtest // +build debugtest -package connection +package winrm import ( "testing" @@ -24,7 +24,7 @@ func TestWinrmConnection(t *testing.T) { } cred.PreProcess() - conn, err := NewWinrmConnection(0, &inventory.Config{ + conn, err := NewConnection(0, &inventory.Config{ Type: shared.Type_Winrm.String(), Host: "192.168.1.111", Credentials: []*vault.Credential{cred}, diff --git a/providers/os/detector/platform_resolver.go b/providers/os/detector/platform_resolver.go index 8ff1d0be01..72b5268d48 100644 --- a/providers/os/detector/platform_resolver.go +++ b/providers/os/detector/platform_resolver.go @@ -6,8 +6,9 @@ package detector import ( "github.com/rs/zerolog/log" "go.mondoo.com/cnquery/v10/providers-sdk/v1/inventory" - "go.mondoo.com/cnquery/v10/providers/os/connection" + "go.mondoo.com/cnquery/v10/providers/os/connection/docker" "go.mondoo.com/cnquery/v10/providers/os/connection/shared" + "go.mondoo.com/cnquery/v10/providers/os/connection/tar" ) type detect func(r *PlatformResolver, pf *inventory.Platform, conn shared.Connection) (bool, error) @@ -29,7 +30,7 @@ func (r *PlatformResolver) Resolve(conn shared.Connection) (*inventory.Platform, // if we have a container image use the architecture specified in the transport as it is resolved // using the container image properties - tarConn, ok := conn.(*connection.TarConnection) + tarConn, ok := conn.(*tar.Connection) if resolved && ok { pi.Arch = tarConn.PlatformArchitecture di.Runtime = "docker-image" @@ -43,7 +44,7 @@ func (r *PlatformResolver) Resolve(conn shared.Connection) (*inventory.Platform, } } - containerConn, ok := conn.(*connection.DockerContainerConnection) + containerConn, ok := conn.(*docker.ContainerConnection) if resolved && ok { pi.Arch = containerConn.PlatformArchitecture di.Runtime = string(containerConn.Type()) diff --git a/providers/os/id/sshhostkey/sshhostkey.go b/providers/os/id/sshhostkey/sshhostkey.go index 9bd3683d48..66706c3a08 100644 --- a/providers/os/id/sshhostkey/sshhostkey.go +++ b/providers/os/id/sshhostkey/sshhostkey.go @@ -10,14 +10,14 @@ import ( "github.com/cockroachdb/errors" "github.com/rs/zerolog/log" "go.mondoo.com/cnquery/v10/providers-sdk/v1/inventory" - "go.mondoo.com/cnquery/v10/providers/os/connection" "go.mondoo.com/cnquery/v10/providers/os/connection/shared" + sshconn "go.mondoo.com/cnquery/v10/providers/os/connection/ssh" "golang.org/x/crypto/ssh" ) func Detect(t shared.Connection, p *inventory.Platform) ([]string, error) { // if we are using an ssh connection we can read the hostkey from the connection - if sshTransport, ok := t.(*connection.SshConnection); ok { + if sshTransport, ok := t.(*sshconn.Connection); ok { identifier, err := sshTransport.PlatformID() if err != nil { return nil, err @@ -53,7 +53,7 @@ func Detect(t shared.Connection, p *inventory.Platform) ([]string, error) { return nil, errors.Wrap(err, "could not parse public key file:"+hostKeyFilePath) } - identifiers = append(identifiers, connection.PlatformIdentifier(publicKey)) + identifiers = append(identifiers, sshconn.PlatformIdentifier(publicKey)) } return identifiers, nil } diff --git a/providers/os/provider/provider.go b/providers/os/provider/provider.go index b7abc001a5..9c8ca8863b 100644 --- a/providers/os/provider/provider.go +++ b/providers/os/provider/provider.go @@ -16,11 +16,16 @@ import ( "go.mondoo.com/cnquery/v10/providers-sdk/v1/plugin" "go.mondoo.com/cnquery/v10/providers-sdk/v1/upstream" "go.mondoo.com/cnquery/v10/providers-sdk/v1/vault" - "go.mondoo.com/cnquery/v10/providers/os/connection" + "go.mondoo.com/cnquery/v10/providers/os/connection/container" + "go.mondoo.com/cnquery/v10/providers/os/connection/docker" "go.mondoo.com/cnquery/v10/providers/os/connection/fs" "go.mondoo.com/cnquery/v10/providers/os/connection/local" "go.mondoo.com/cnquery/v10/providers/os/connection/mock" "go.mondoo.com/cnquery/v10/providers/os/connection/shared" + "go.mondoo.com/cnquery/v10/providers/os/connection/ssh" + "go.mondoo.com/cnquery/v10/providers/os/connection/tar" + "go.mondoo.com/cnquery/v10/providers/os/connection/vagrant" + "go.mondoo.com/cnquery/v10/providers/os/connection/winrm" "go.mondoo.com/cnquery/v10/providers/os/id" "go.mondoo.com/cnquery/v10/providers/os/resources" "go.mondoo.com/cnquery/v10/providers/os/resources/discovery/container_registry" @@ -91,7 +96,7 @@ func (s *Service) ParseCLI(req *plugin.ParseCLIReq) (*plugin.ParseCLIRes, error) conf.Host = req.Args[1] } } else { - connType, err := connection.FetchConnectionType(req.Args[0]) + connType, err := docker.FindDockerObjectConnectionType(req.Args[0]) if err != nil { return nil, err } @@ -230,7 +235,7 @@ func (s *Service) Connect(req *plugin.ConnectReq, callback plugin.ProviderCallba connType := conn.Asset().Connections[0].Type switch connType { case "docker-registry": - tarConn := conn.(*connection.TarConnection) + tarConn := conn.(*tar.Connection) inv, err = s.discoverRegistry(tarConn) if err != nil { return nil, err @@ -303,7 +308,7 @@ func (s *Service) connect(req *plugin.ConnectReq, callback plugin.ProviderCallba } case shared.Type_SSH.String(): - conn, err = connection.NewSshConnection(connId, conf, asset) + conn, err = ssh.NewConnection(connId, conf, asset) if err != nil { return nil, err } @@ -318,7 +323,7 @@ func (s *Service) connect(req *plugin.ConnectReq, callback plugin.ProviderCallba } case shared.Type_Winrm.String(): - conn, err = connection.NewWinrmConnection(connId, conf, asset) + conn, err = winrm.NewConnection(connId, conf, asset) if err != nil { return nil, err } @@ -331,7 +336,7 @@ func (s *Service) connect(req *plugin.ConnectReq, callback plugin.ProviderCallba } case shared.Type_Tar.String(): - conn, err = connection.NewTarConnection(connId, conf, asset) + conn, err = tar.NewConnection(connId, conf, asset) if err != nil { return nil, err } @@ -344,7 +349,7 @@ func (s *Service) connect(req *plugin.ConnectReq, callback plugin.ProviderCallba } case shared.Type_DockerSnapshot.String(): - conn, err = connection.NewDockerSnapshotConnection(connId, conf, asset) + conn, err = docker.NewSnapshotConnection(connId, conf, asset) if err != nil { return nil, err } @@ -357,7 +362,7 @@ func (s *Service) connect(req *plugin.ConnectReq, callback plugin.ProviderCallba } case shared.Type_Vagrant.String(): - conn, err = connection.NewVagrantConnection(connId, conf, asset) + conn, err = vagrant.NewVagrantConnection(connId, conf, asset) if err != nil { return nil, err } @@ -369,16 +374,16 @@ func (s *Service) connect(req *plugin.ConnectReq, callback plugin.ProviderCallba } case shared.Type_DockerImage.String(): - conn, err = connection.NewDockerContainerImageConnection(connId, conf, asset) + conn, err = docker.NewContainerImageConnection(connId, conf, asset) case shared.Type_DockerContainer.String(): - conn, err = connection.NewDockerEngineContainer(connId, conf, asset) + conn, err = docker.NewDockerEngineContainer(connId, conf, asset) case shared.Type_DockerRegistry.String(), shared.Type_ContainerRegistry.String(): - conn, err = connection.NewContainerRegistryImage(connId, conf, asset) + conn, err = container.NewRegistryImage(connId, conf, asset) case shared.Type_RegistryImage.String(): - conn, err = connection.NewContainerRegistryImage(connId, conf, asset) + conn, err = container.NewRegistryImage(connId, conf, asset) case shared.Type_FileSystem.String(): conn, err = fs.NewConnection(connId, conf, asset) @@ -439,7 +444,7 @@ func (s *Service) connect(req *plugin.ConnectReq, callback plugin.ProviderCallba return runtime.Connection.(shared.Connection), nil } -func (s *Service) discoverRegistry(conn *connection.TarConnection) (*inventory.Inventory, error) { +func (s *Service) discoverRegistry(conn *tar.Connection) (*inventory.Inventory, error) { conf := conn.Asset().Connections[0] if conf == nil { return nil, nil diff --git a/providers/os/resources/container.go b/providers/os/resources/container.go index 298c13c9b8..0c45464ec6 100644 --- a/providers/os/resources/container.go +++ b/providers/os/resources/container.go @@ -7,14 +7,14 @@ import ( "github.com/google/go-containerregistry/pkg/name" "go.mondoo.com/cnquery/v10/llx" "go.mondoo.com/cnquery/v10/providers-sdk/v1/plugin" - "go.mondoo.com/cnquery/v10/providers/os/connection" + "go.mondoo.com/cnquery/v10/providers/os/connection/tar" ) func initContainerImage(runtime *plugin.Runtime, args map[string]*llx.RawData) (map[string]*llx.RawData, plugin.Resource, error) { if len(args) > 1 { return args, nil, nil } - conn := runtime.Connection.(*connection.TarConnection) + conn := runtime.Connection.(*tar.Connection) reference := conn.Metadata.Labels["docker.io/digests"] ref, err := name.ParseReference(reference) diff --git a/providers/os/resources/discovery/container_registry/registry.go b/providers/os/resources/discovery/container_registry/registry.go index f68f2a828f..a7eb513ca4 100644 --- a/providers/os/resources/discovery/container_registry/registry.go +++ b/providers/os/resources/discovery/container_registry/registry.go @@ -11,9 +11,8 @@ import ( "net/url" "time" - "github.com/google/go-containerregistry/pkg/authn" - "github.com/cockroachdb/errors" + "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/rs/zerolog/log" diff --git a/providers/os/resources/os.go b/providers/os/resources/os.go index b4bdd1be22..5105801f5d 100644 --- a/providers/os/resources/os.go +++ b/providers/os/resources/os.go @@ -13,8 +13,9 @@ import ( "github.com/rs/zerolog/log" "go.mondoo.com/cnquery/v10/llx" "go.mondoo.com/cnquery/v10/providers-sdk/v1/inventory" - "go.mondoo.com/cnquery/v10/providers/os/connection" + "go.mondoo.com/cnquery/v10/providers/os/connection/docker" "go.mondoo.com/cnquery/v10/providers/os/connection/shared" + "go.mondoo.com/cnquery/v10/providers/os/connection/tar" "go.mondoo.com/cnquery/v10/providers/os/id/hostname" "go.mondoo.com/cnquery/v10/providers/os/id/platformid" "go.mondoo.com/cnquery/v10/providers/os/resources/reboot" @@ -26,9 +27,9 @@ import ( func (p *mqlOs) rebootpending() (bool, error) { switch p.MqlRuntime.Connection.(type) { - case *connection.DockerSnapshotConnection: + case *docker.SnapshotConnection: return false, nil - case *connection.TarConnection: + case *tar.Connection: return false, nil } @@ -324,9 +325,9 @@ func (p *mqlOsBase) id() (string, error) { func (p *mqlOsBase) rebootpending() (bool, error) { // it is a container image, a reboot is never required switch p.MqlRuntime.Connection.(type) { - case *connection.DockerSnapshotConnection: + case *docker.SnapshotConnection: return false, nil - case *connection.TarConnection: + case *tar.Connection: return false, nil } diff --git a/providers/os/resources/processes/docker_test.go b/providers/os/resources/processes/docker_test.go index 762e2c1373..7dbf1f8a46 100644 --- a/providers/os/resources/processes/docker_test.go +++ b/providers/os/resources/processes/docker_test.go @@ -1,13 +1,11 @@ // Copyright (c) Mondoo, Inc. // SPDX-License-Identifier: BUSL-1.1 -//go:build debugtest -// +build debugtest - package processes import ( "context" + "fmt" "io" "os" "testing" @@ -19,13 +17,14 @@ import ( specs "github.com/opencontainers/image-spec/specs-go/v1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "go.mondoo.com/cnquery/v10/providers/os/connection" + "go.mondoo.com/cnquery/v10/providers-sdk/v1/inventory" + "go.mondoo.com/cnquery/v10/providers/os/connection/docker" ) func TestDockerProcsList(t *testing.T) { image := "docker.io/nginx:stable" ctx := context.Background() - dClient, err := connection.GetDockerClient() + dClient, err := docker.GetDockerClient() assert.NoError(t, err) // If docker is not available, then skip the test. @@ -35,7 +34,12 @@ func TestDockerProcsList(t *testing.T) { } responseBody, err := dClient.ImagePull(ctx, image, types.ImagePullOptions{}) - defer responseBody.Close() + defer func() { + err = responseBody.Close() + if err != nil { + panic(err) + } + }() require.NoError(t, err) _, err = io.Copy(os.Stdout, responseBody) @@ -43,8 +47,11 @@ func TestDockerProcsList(t *testing.T) { // Make sure the docker image is cleaned up defer func() { - _, err := dClient.ImageRemove(ctx, image, types.ImageRemoveOptions{}) - require.NoError(t, err, "failed to cleanup pre-pulled docker image") + _, err := dClient.ImageRemove(ctx, image, types.ImageRemoveOptions{ + Force: true, + }) + // ignore error, worst case is that the image is not removed but parallel tests may fail otherwise + fmt.Printf("failed to cleanup pre-pulled docker image: %v", err) }() cfg := &container.Config{ @@ -55,26 +62,35 @@ func TestDockerProcsList(t *testing.T) { Image: image, } - uuid := uuid.New() - created, err := dClient.ContainerCreate(ctx, cfg, &container.HostConfig{}, &network.NetworkingConfig{}, &specs.Platform{}, uuid.String()) + uuidVal := uuid.New() + created, err := dClient.ContainerCreate(ctx, cfg, &container.HostConfig{}, &network.NetworkingConfig{}, &specs.Platform{}, uuidVal.String()) require.NoError(t, err) - require.NoError(t, dClient.ContainerStart(ctx, created.ID, types.ContainerStartOptions{})) + require.NoError(t, dClient.ContainerStart(ctx, created.ID, container.StartOptions{})) // Make sure the container is cleaned up defer func() { - err := dClient.ContainerRemove(ctx, created.ID, types.ContainerRemoveOptions{Force: true}) + err := dClient.ContainerRemove(ctx, created.ID, container.RemoveOptions{Force: true}) require.NoError(t, err) }() - panic("inject: " + created.ID) - provider, err := connection.NewDockerContainerConnection(0, nil, nil) - assert.NoError(t, err) + fmt.Println("inject: " + created.ID) + conn, err := docker.NewContainerConnection(0, &inventory.Config{ + Host: created.ID, + }, &inventory.Asset{ + // for the test we need to set the platform + Platform: &inventory.Platform{ + Name: "debian", + Version: "11", + Family: []string{"debian", "linux"}, + }, + }) + require.NoError(t, err) pMan, err := ResolveManager(conn) assert.NoError(t, err) - procs, err := pMan.List() + proc, err := pMan.Process(1) assert.NoError(t, err) - assert.NotEmpty(t, procs) + assert.NotEmpty(t, proc) } diff --git a/providers/os/resources/processes/dockertop.go b/providers/os/resources/processes/dockertop.go index 55bfd41b2f..ce4e5a8e3f 100644 --- a/providers/os/resources/processes/dockertop.go +++ b/providers/os/resources/processes/dockertop.go @@ -9,7 +9,7 @@ import ( "strconv" "go.mondoo.com/cnquery/v10/providers-sdk/v1/plugin" - "go.mondoo.com/cnquery/v10/providers/os/connection" + "go.mondoo.com/cnquery/v10/providers/os/connection/docker" "go.mondoo.com/cnquery/v10/providers/os/connection/shared" ) @@ -24,7 +24,7 @@ func (lpm *DockerTopManager) Name() string { // List lists the processes running in a Docker container. Note that currently this function returns child // processes as well. func (lpm *DockerTopManager) List() ([]*OSProcess, error) { - dockerConn, ok := lpm.conn.(*connection.DockerContainerConnection) + dockerConn, ok := lpm.conn.(*docker.ContainerConnection) if !ok { return nil, fmt.Errorf("wrong transport type") } diff --git a/providers/os/resources/processes/manager.go b/providers/os/resources/processes/manager.go index 975d2c2b04..5700b856d9 100644 --- a/providers/os/resources/processes/manager.go +++ b/providers/os/resources/processes/manager.go @@ -7,9 +7,9 @@ import ( "errors" "go.mondoo.com/cnquery/v10/providers-sdk/v1/plugin" - "go.mondoo.com/cnquery/v10/providers/os/connection" "go.mondoo.com/cnquery/v10/providers/os/connection/mock" "go.mondoo.com/cnquery/v10/providers/os/connection/shared" + "go.mondoo.com/cnquery/v10/providers/os/connection/ssh" ) type OSProcess struct { @@ -41,7 +41,7 @@ func ResolveManager(conn shared.Connection) (OSProcessManager, error) { // procfs over ssh is super slow, lets deactivate until we have a faster approach disableProcFs := false switch conn.(type) { - case *connection.SshConnection: + case *ssh.Connection: disableProcFs = true case *mock.Connection: disableProcFs = true