diff --git a/providers/os/connection/container/auth/auth.go b/providers/os/connection/container/auth/auth.go index db482bb3cf..71e6eeb624 100644 --- a/providers/os/connection/container/auth/auth.go +++ b/providers/os/connection/container/auth/auth.go @@ -4,36 +4,39 @@ package auth import ( + "strings" + + "github.com/awslabs/amazon-ecr-credential-helper/ecr-login" "github.com/google/go-containerregistry/pkg/authn" "github.com/rs/zerolog/log" - "go.mondoo.com/cnquery/v10/logger" - "go.mondoo.com/cnquery/v10/providers-sdk/v1/vault" - "go.mondoo.com/cnquery/v10/providers/os/connection/container/image" + "go.mondoo.com/cnquery/v10/providers/os/connection/container/acr" +) + +const ( + acrIndicator = ".azurecr.io" + ecrIndicator = ".ecr." ) -func AuthOption(credentials []*vault.Credential) []image.Option { - remoteOpts := []image.Option{} - for i := range credentials { - cred := credentials[i] - switch cred.Type { - case vault.CredentialType_password: - log.Debug().Msg("add password authentication") - cfg := authn.AuthConfig{ - Username: cred.User, - Password: string(cred.Secret), - } - remoteOpts = append(remoteOpts, image.WithAuthenticator((authn.FromConfig(cfg)))) - case vault.CredentialType_bearer: - log.Debug().Str("token", string(cred.Secret)).Msg("add bearer authentication") - cfg := authn.AuthConfig{ - Username: cred.User, - RegistryToken: string(cred.Secret), - } - remoteOpts = append(remoteOpts, image.WithAuthenticator((authn.FromConfig(cfg)))) - default: - log.Warn().Msg("unknown credentials for container image") - logger.DebugJSON(credentials) +func getKeychains(name string) []authn.Keychain { + kcs := []authn.Keychain{ + authn.DefaultKeychain, + } + if strings.Contains(name, ecrIndicator) { + kcs = append(kcs, authn.NewKeychainFromHelper(ecr.NewECRHelper())) + } + if strings.Contains(name, acrIndicator) { + acr, err := acr.NewAcrAuthHelper() + if err == nil { + kcs = append(kcs, authn.NewKeychainFromHelper(acr)) + } else { + log.Debug().Err(err).Msg("failed to create ACR auth helper") } } - return remoteOpts + return kcs +} + +// ConstructKeychain creates a keychain for the given registry name +// It will add the default docker keychain and additional keychains for ECR and ACR, if those are determined to be used +func ConstructKeychain(name string) authn.Keychain { + return authn.NewMultiKeychain(getKeychains(name)...) } diff --git a/providers/os/connection/container/auth/auth_test.go b/providers/os/connection/container/auth/auth_test.go new file mode 100644 index 0000000000..1159b4153e --- /dev/null +++ b/providers/os/connection/container/auth/auth_test.go @@ -0,0 +1,26 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package auth + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestConstructKeychain(t *testing.T) { + t.Run("default keychain only", func(t *testing.T) { + keychain := getKeychains("test") + require.Equal(t, 1, len(keychain)) + }) + t.Run("default keychain and ecr keychain", func(t *testing.T) { + keychain := getKeychains("0000000000.dkr.ecr.us-east-1.amazonaws.com/test") + require.Equal(t, 2, len(keychain)) + }) + + t.Run("default keychain and acr keychain", func(t *testing.T) { + keychain := getKeychains("test.azurecr.io") + require.Equal(t, 2, len(keychain)) + }) +} diff --git a/providers/os/connection/container/image/registry.go b/providers/os/connection/container/image/registry.go index a38a88cce2..c83548ba08 100644 --- a/providers/os/connection/container/image/registry.go +++ b/providers/os/connection/container/image/registry.go @@ -5,18 +5,18 @@ package image import ( "crypto/tls" - "fmt" "net" "net/http" - "strings" "time" - ecr "github.com/awslabs/amazon-ecr-credential-helper/ecr-login" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/remote" - "go.mondoo.com/cnquery/v10/providers/os/connection/container/acr" + "github.com/rs/zerolog/log" + "go.mondoo.com/cnquery/v10/logger" + "go.mondoo.com/cnquery/v10/providers-sdk/v1/vault" + "go.mondoo.com/cnquery/v10/providers/os/connection/container/auth" ) // Option is a functional option @@ -42,6 +42,38 @@ func WithAuthenticator(auth authn.Authenticator) Option { } } +func AuthOption(credentials []*vault.Credential) []Option { + remoteOpts := []Option{} + for i := range credentials { + cred := credentials[i] + switch cred.Type { + case vault.CredentialType_password: + log.Debug().Msg("add password authentication") + cfg := authn.AuthConfig{ + Username: cred.User, + Password: string(cred.Secret), + } + remoteOpts = append(remoteOpts, WithAuthenticator((authn.FromConfig(cfg)))) + case vault.CredentialType_bearer: + log.Debug().Str("token", string(cred.Secret)).Msg("add bearer authentication") + cfg := authn.AuthConfig{ + Username: cred.User, + RegistryToken: string(cred.Secret), + } + remoteOpts = append(remoteOpts, WithAuthenticator((authn.FromConfig(cfg)))) + default: + log.Warn().Msg("unknown credentials for container image") + logger.DebugJSON(credentials) + } + } + return remoteOpts +} + +func DefaultAuthOpts(ref name.Reference) (authn.Authenticator, error) { + kc := auth.ConstructKeychain(ref.Name()) + return kc.Resolve(ref.Context()) +} + func GetImageDescriptor(ref name.Reference, opts ...Option) (*remote.Descriptor, error) { o := &options{ insecure: false, @@ -54,28 +86,8 @@ func GetImageDescriptor(ref name.Reference, opts ...Option) (*remote.Descriptor, } if o.auth == nil { - kc := authn.NewMultiKeychain( - authn.DefaultKeychain, - ) - if strings.Contains(ref.Name(), ".ecr.") { - kc = authn.NewMultiKeychain( - authn.DefaultKeychain, - authn.NewKeychainFromHelper(ecr.NewECRHelper()), - ) - } - if strings.Contains(ref.Name(), "azurecr.io") { - acr, err := acr.NewAcrAuthHelper() - if err != nil { - return nil, err - } - kc = authn.NewMultiKeychain( - authn.DefaultKeychain, - authn.NewKeychainFromHelper(acr), - ) - } - auth, err := kc.Resolve(ref.Context()) + auth, err := DefaultAuthOpts(ref) if err != nil { - fmt.Printf("getting creds for %q: %v", ref, err) return nil, err } o.auth = auth @@ -96,28 +108,8 @@ func LoadImageFromRegistry(ref name.Reference, opts ...Option) (v1.Image, error) } if o.auth == nil { - kc := authn.NewMultiKeychain( - authn.DefaultKeychain, - ) - if strings.Contains(ref.Name(), ".ecr.") { - kc = authn.NewMultiKeychain( - authn.DefaultKeychain, - authn.NewKeychainFromHelper(ecr.NewECRHelper()), - ) - } - if strings.Contains(ref.Name(), "azurecr.io") { - acr, err := acr.NewAcrAuthHelper() - if err != nil { - return nil, err - } - kc = authn.NewMultiKeychain( - authn.DefaultKeychain, - authn.NewKeychainFromHelper(acr), - ) - } - auth, err := kc.Resolve(ref.Context()) + auth, err := DefaultAuthOpts(ref) if err != nil { - fmt.Printf("getting creds for %q: %v", ref, err) return nil, err } o.auth = auth diff --git a/providers/os/connection/container/image_connection.go b/providers/os/connection/container/image_connection.go index b16d18a20c..67424ba4b0 100644 --- a/providers/os/connection/container/image_connection.go +++ b/providers/os/connection/container/image_connection.go @@ -15,7 +15,6 @@ import ( "github.com/rs/zerolog/log" "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/image" "go.mondoo.com/cnquery/v10/providers/os/connection/tar" "go.mondoo.com/cnquery/v10/providers/os/id/containerid" @@ -62,7 +61,7 @@ func NewRegistryImage(id uint32, conf *inventory.Config, asset *inventory.Asset) log.Debug().Str("ref", ref.Name()).Msg("found valid container registry reference") registryOpts := []image.Option{image.WithInsecure(conf.Insecure)} - remoteOpts := auth.AuthOption(conf.Credentials) + remoteOpts := image.AuthOption(conf.Credentials) registryOpts = append(registryOpts, remoteOpts...) img, err := image.LoadImageFromRegistry(ref, registryOpts...) diff --git a/providers/os/connection/container/registry_connection.go b/providers/os/connection/container/registry_connection.go new file mode 100644 index 0000000000..b7b48f12d6 --- /dev/null +++ b/providers/os/connection/container/registry_connection.go @@ -0,0 +1,84 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package container + +import ( + "errors" + + "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/resources/discovery/container_registry" +) + +var _ shared.Connection = &RegistryConnection{} + +type RegistryConnection struct { + plugin.Connection + asset *inventory.Asset +} + +func (r *RegistryConnection) Capabilities() shared.Capabilities { + return shared.Capabilities(0) +} + +func (r *RegistryConnection) FileInfo(path string) (shared.FileInfoDetails, error) { + return shared.FileInfoDetails{}, plugin.ErrFileInfoNotImplemented +} + +func (r *RegistryConnection) FileSystem() afero.Fs { + panic("unimplemented") +} + +func (r *RegistryConnection) RunCommand(command string) (*shared.Command, error) { + return nil, errors.New("unimplemented") +} + +func (r *RegistryConnection) Type() shared.ConnectionType { + return shared.Type_ContainerRegistry +} + +func (r *RegistryConnection) UpdateAsset(asset *inventory.Asset) { + r.asset = asset +} + +func NewRegistryConnection(id uint32, asset *inventory.Asset) (*RegistryConnection, error) { + conn := &RegistryConnection{ + Connection: plugin.NewConnection(id, asset), + asset: asset, + } + + return conn, nil +} + +func (r *RegistryConnection) Name() string { + return "container-registry" +} + +func (r *RegistryConnection) ID() uint32 { + return r.Connection.ID() +} + +func (r *RegistryConnection) ParentID() uint32 { + return r.Connection.ParentID() +} + +func (r *RegistryConnection) Close() error { + return nil +} + +func (r *RegistryConnection) Asset() *inventory.Asset { + return r.asset +} + +func (r *RegistryConnection) DiscoverImages() (*inventory.Inventory, error) { + resolver := container_registry.NewContainerRegistryResolver() + host := r.asset.Connections[0].Host + assets, err := resolver.ListRegistry(host) + if err != nil { + return nil, err + } + return inventory.New(inventory.WithAssets(assets...)), nil +} diff --git a/providers/os/connection/docker/container_connection.go b/providers/os/connection/docker/container_connection.go index b3fc317c84..390750a9f7 100644 --- a/providers/os/connection/docker/container_connection.go +++ b/providers/os/connection/docker/container_connection.go @@ -258,7 +258,7 @@ func NewContainerImageConnection(id uint32, conf *inventory.Config, asset *inven } // The requested image isn't locally available, but we can pull it from a remote registry. - if len(resolvedAssets) > 0 && resolvedAssets[0].Connections[0].Type == "container-registry" { + if len(resolvedAssets) > 0 && resolvedAssets[0].Connections[0].Type == shared.Type_RegistryImage.String() { asset.Name = resolvedAssets[0].Name asset.PlatformIds = resolvedAssets[0].PlatformIds asset.Labels = resolvedAssets[0].Labels diff --git a/providers/os/provider/provider.go b/providers/os/provider/provider.go index 1ea4a8f2e5..cc8933e787 100644 --- a/providers/os/provider/provider.go +++ b/providers/os/provider/provider.go @@ -28,7 +28,6 @@ import ( "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" "go.mondoo.com/cnquery/v10/providers/os/resources/discovery/docker_engine" "go.mondoo.com/cnquery/v10/utils/stringx" ) @@ -88,6 +87,7 @@ func (s *Service) ParseCLI(req *plugin.ParseCLIReq) (*plugin.ParseCLIRes, error) case "registry": conf.Type = "docker-registry" conf.Host = req.Args[1] + conf.DelayDiscovery = true case "tar": conf.Type = "docker-snapshot" conf.Path = req.Args[1] @@ -112,6 +112,7 @@ func (s *Service) ParseCLI(req *plugin.ParseCLIReq) (*plugin.ParseCLIRes, error) case "registry": conf.Type = "docker-registry" conf.Host = req.Args[1] + conf.DelayDiscovery = true case "tar": conf.Type = "docker-snapshot" conf.Path = req.Args[1] @@ -234,13 +235,13 @@ func (s *Service) Connect(req *plugin.ConnectReq, callback plugin.ProviderCallba var inv *inventory.Inventory connType := conn.Asset().Connections[0].Type switch connType { - case "docker-registry": - tarConn := conn.(*tar.Connection) - inv, err = s.discoverRegistry(tarConn) + case shared.Type_DockerRegistry.String(), shared.Type_ContainerRegistry.String(): + regConn := conn.(*container.RegistryConnection) + inv, err = regConn.DiscoverImages() if err != nil { return nil, err } - case "local", "docker-container": + case shared.Type_Local.String(), shared.Type_DockerContainer.String(): inv, err = s.discoverLocalContainers(conn.Asset().Connections[0]) if err != nil { return nil, err @@ -381,7 +382,7 @@ func (s *Service) connect(req *plugin.ConnectReq, callback plugin.ProviderCallba conn, err = docker.NewDockerEngineContainer(connId, conf, asset) case shared.Type_DockerRegistry.String(), shared.Type_ContainerRegistry.String(): - conn, err = container.NewRegistryImage(connId, conf, asset) + conn, err = container.NewRegistryConnection(connId, asset) case shared.Type_RegistryImage.String(): conn, err = container.NewRegistryImage(connId, conf, asset) @@ -445,29 +446,6 @@ func (s *Service) connect(req *plugin.ConnectReq, callback plugin.ProviderCallba return runtime.Connection.(shared.Connection), nil } -func (s *Service) discoverRegistry(conn *tar.Connection) (*inventory.Inventory, error) { - conf := conn.Asset().Connections[0] - if conf == nil { - return nil, nil - } - - resolver := container_registry.Resolver{} - resolvedAssets, err := resolver.Resolve(context.Background(), conn.Asset(), conf, nil) - if err != nil { - return nil, err - } - - inventory := &inventory.Inventory{} - // we detect the platform for each asset we discover here - for _, a := range resolvedAssets { - // ignore the error. we will retry detection if we connect to the asset - _ = s.detect(a, conn) - } - inventory.AddAssets(resolvedAssets...) - - return inventory, nil -} - func (s *Service) discoverLocalContainers(conf *inventory.Config) (*inventory.Inventory, error) { if conf == nil || conf.Discover == nil { return nil, nil diff --git a/providers/os/resources/discovery/container_registry/registry.go b/providers/os/resources/discovery/container_registry/registry.go index a7eb513ca4..0ffe4fc0cd 100644 --- a/providers/os/resources/discovery/container_registry/registry.go +++ b/providers/os/resources/discovery/container_registry/registry.go @@ -12,7 +12,6 @@ import ( "time" "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" @@ -20,6 +19,7 @@ import ( "go.mondoo.com/cnquery/v10/providers-sdk/v1/vault" "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/shared" "go.mondoo.com/cnquery/v10/providers/os/id/containerid" ) @@ -32,15 +32,15 @@ type DockerRegistryImages struct { DisableKeychainAuth bool } -func (a *DockerRegistryImages) remoteOptions() []remote.Option { +func (a *DockerRegistryImages) remoteOptions(name string) []remote.Option { options := []remote.Option{} // does not work with bearer auth, therefore it need to be disabled when other remote auth options are used // TODO: we should implement this a bit differently - if a.DisableKeychainAuth == false { - options = append(options, remote.WithAuthFromKeychain(authn.DefaultKeychain)) + if !a.DisableKeychainAuth { + kcs := auth.ConstructKeychain(name) + options = append(options, remote.WithAuthFromKeychain(kcs)) } - if a.Insecure { // NOTE: config to get remote running with an insecure registry, we need to override the TLSClientConfig tr := &http.Transport{ @@ -70,7 +70,8 @@ func (a *DockerRegistryImages) Repositories(reg name.Registry) ([]string, error) last := "" var res []string for { - page, err := remote.CatalogPage(reg, last, n, a.remoteOptions()...) + opts := a.remoteOptions(reg.Name()) + page, err := remote.CatalogPage(reg, last, n, opts...) if err != nil { return nil, err } @@ -131,7 +132,7 @@ func (a *DockerRegistryImages) ListRepository(repoName string) ([]*inventory.Ass } // fetch tags - tags, err := remote.List(repo, a.remoteOptions()...) + tags, err := remote.List(repo, a.remoteOptions(repo.Name())...) if err != nil { return nil, handleUnauthorizedError(err, repo.Name()) } @@ -186,7 +187,7 @@ func (a *DockerRegistryImages) GetImage(ref name.Reference, creds []*vault.Crede } func (a *DockerRegistryImages) toAsset(ref name.Reference, creds []*vault.Credential, opts ...remote.Option) (*inventory.Asset, error) { - desc, err := image.GetImageDescriptor(ref, auth.AuthOption(creds)...) + desc, err := image.GetImageDescriptor(ref, image.AuthOption(creds)...) if err != nil { return nil, handleUnauthorizedError(err, ref.Name()) } @@ -200,7 +201,7 @@ func (a *DockerRegistryImages) toAsset(ref name.Reference, creds []*vault.Creden Name: name, Connections: []*inventory.Config{ { - Type: "container-registry", + Type: string(shared.Type_RegistryImage), Host: imageUrl, Credentials: creds, }, diff --git a/providers/os/resources/discovery/container_registry/registry_test.go b/providers/os/resources/discovery/container_registry/registry_test.go index 02f38a16e8..1127eb28ba 100644 --- a/providers/os/resources/discovery/container_registry/registry_test.go +++ b/providers/os/resources/discovery/container_registry/registry_test.go @@ -49,7 +49,7 @@ func TestHarbor(t *testing.T) { require.NoError(t, err, url) // check that we resolved it correctly and we got a specific shasum - assert.Equal(t, "container-registry", a.Connections[0].Type) + assert.Equal(t, "registry-image", a.Connections[0].Type) assert.True(t, strings.HasPrefix(a.Connections[0].Host, "index.docker.io/library/centos"), url) assert.True(t, len(strings.Split(a.Connections[0].Host, "@")) == 2, url) } diff --git a/providers/os/resources/discovery/docker_engine/resolver.go b/providers/os/resources/discovery/docker_engine/resolver.go index 0a6b678a38..385bcb631b 100644 --- a/providers/os/resources/discovery/docker_engine/resolver.go +++ b/providers/os/resources/discovery/docker_engine/resolver.go @@ -199,7 +199,6 @@ func (k *Resolver) images(ctx context.Context, root *inventory.Asset, conf *inve rr := container_registry.Resolver{ NoStrictValidation: true, } - // return rr.Resolve(ctx, root, conf, credsResolver, sfn) return rr.Resolve(ctx, root, conf, credsResolver) }