From 87ea73fcf1133c48454d3a769726e899c5c9639a Mon Sep 17 00:00:00 2001 From: Richard Hagen Date: Fri, 12 Apr 2024 15:44:12 +0200 Subject: [PATCH 1/4] Enable Workload identity in Vulnerability Scanner (#57) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Install ACR Cred helper and clenaup dockerfile * WIP ACR Workload Identity Token * add token store * cleanup Dockerfile * cleanup * fix test * fix lint in test code * Add token store and mutex * Read expiration from token * cleanup * cleanup * Refactor init code * Add token reuse * remove test code * add scope name * cleanup * cleanup Main * remove manuall test * Add tests * fix linter * disable checksum and chmod since it requires BuildKit * added suggestions (#58) * fix tests * Add configuration to enable workflow identity * fix lint bug * Update pkg/tokenstore/token_store.go Co-authored-by: Nils Gustav Stråbø <65334626+nilsgstrabo@users.noreply.github.com> * Update pkg/scan/snyk.go Co-authored-by: Nils Gustav Stråbø <65334626+nilsgstrabo@users.noreply.github.com> * Fix comments * Fix comments * remove test helpers from Dockercfg, renamed DockerCfg types --------- Co-authored-by: Nils Gustav Stråbø <65334626+nilsgstrabo@users.noreply.github.com> --- Dockerfile | 10 +- Makefile | 2 + .../templates/deployment.yaml | 3 + .../radix-vulnerability-scanner/values.yaml | 4 + go.mod | 8 +- go.sum | 4 +- install_tools.sh | 7 - main.go | 96 +++++++- pkg/db/gorm.go | 28 ++- pkg/db/repository.go | 16 +- pkg/dockercfg/config.go | 49 +++-- pkg/dockercfg/config_test.go | 30 +++ pkg/handler/handler.go | 6 +- pkg/handler/handler_test.go | 18 +- pkg/imageworker/worker.go | 2 +- pkg/imageworker/worker_test.go | 8 +- pkg/observe/radixdeployment.go | 26 +-- pkg/observe/radixdeployment_test.go | 12 +- pkg/{server => options}/load.go | 3 +- pkg/options/options.go | 41 ++++ pkg/registry/auth.go | 13 ++ pkg/registry/mock/auth.go | 51 +++++ pkg/scan/executor/executor.go | 32 +++ pkg/scan/executor/mock/executor.go | 50 +++++ pkg/scan/mock/scanner.go | 8 +- pkg/scan/scanner.go | 2 +- pkg/scan/snyk.go | 102 ++++----- pkg/scan/snyk_test.go | 207 ++++++++---------- pkg/server/options.go | 40 ---- pkg/server/server.go | 101 +-------- pkg/tokenstore/token_store.go | 65 ++++++ pkg/tokenstore/token_store_test.go | 40 ++++ pkg/tokenstore/tokensource/acr.go | 141 ++++++++++++ pkg/tokenstore/tokensource/acr_test.go | 96 ++++++++ 34 files changed, 919 insertions(+), 402 deletions(-) delete mode 100644 install_tools.sh create mode 100644 pkg/dockercfg/config_test.go rename pkg/{server => options}/load.go (96%) create mode 100644 pkg/options/options.go create mode 100644 pkg/registry/auth.go create mode 100644 pkg/registry/mock/auth.go create mode 100644 pkg/scan/executor/executor.go create mode 100644 pkg/scan/executor/mock/executor.go delete mode 100644 pkg/server/options.go create mode 100644 pkg/tokenstore/token_store.go create mode 100644 pkg/tokenstore/token_store_test.go create mode 100644 pkg/tokenstore/tokensource/acr.go create mode 100644 pkg/tokenstore/tokensource/acr_test.go diff --git a/Dockerfile b/Dockerfile index a0d799c..5386a84 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,22 +16,18 @@ COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -ldflags "-s -w" -a -installsuffix cgo -o ./rootfs/radix-vulnerability-scanner # Install SNYK - FROM alpine:3 as tools - -COPY install_tools.sh /install/install_tools.sh -RUN chmod +x /install/install_tools.sh -RUN sh /install/install_tools.sh +ADD https://github.com/snyk/snyk/releases/download/v1.1286.1/snyk-alpine / +RUN chmod +x /snyk-alpine # Run scanner - FROM alpine:3 RUN apk update && \ apk add ca-certificates libstdc++ COPY --from=builder /go/src/github.com/equinor/radix-vulnerability-scanner/rootfs/radix-vulnerability-scanner /usr/local/bin/radix-vulnerability-scanner -COPY --from=tools /usr/local/bin/snyk /usr/local/bin/snyk +COPY --from=tools /snyk-alpine /usr/local/bin/snyk RUN addgroup -S -g 1000 radix-vulnerability-scanner RUN adduser -S -u 1000 -G radix-vulnerability-scanner radix-vulnerability-scanner diff --git a/Makefile b/Makefile index f728f21..6503860 100644 --- a/Makefile +++ b/Makefile @@ -24,6 +24,8 @@ lint: bootstrap mocks: bootstrap mockgen -source ./pkg/db/repository.go -destination ./pkg/db/mock/repository.go -package mock mockgen -source ./pkg/scan/scanner.go -destination ./pkg/scan/mock/scanner.go -package mock + mockgen -source ./pkg/scan/executor/executor.go -destination ./pkg/scan/executor/mock/executor.go -package mock + mockgen -source ./pkg/registry/auth.go -destination ./pkg/registry/mock/auth.go -package mock HAS_GOLANGCI_LINT := $(shell command -v golangci-lint;) HAS_MOCKGEN := $(shell command -v mockgen;) diff --git a/charts/radix-vulnerability-scanner/templates/deployment.yaml b/charts/radix-vulnerability-scanner/templates/deployment.yaml index 354060f..49dd9e4 100644 --- a/charts/radix-vulnerability-scanner/templates/deployment.yaml +++ b/charts/radix-vulnerability-scanner/templates/deployment.yaml @@ -42,6 +42,9 @@ spec: {{- if not (empty .Values.appNameExcludeList) }} - --app-name-exclude-list={{ uniq .Values.appNameExcludeList | join "," }} {{- end}} + {{- if not (empty .Values.workloadIdentityRegistries) }} + - --workload-identity-registries={{ uniq .Values.workloadIdentityRegistries | join "," }} + {{- end}} {{- if ge (.Values.workers | int) 1 }} - --workers={{ .Values.workers | int }} {{- end}} diff --git a/charts/radix-vulnerability-scanner/values.yaml b/charts/radix-vulnerability-scanner/values.yaml index 4eb7e36..31e70cf 100644 --- a/charts/radix-vulnerability-scanner/values.yaml +++ b/charts/radix-vulnerability-scanner/values.yaml @@ -41,6 +41,10 @@ appNameExcludeList: [] # - canarycicd-test2 # - canarycicd-test1 +workloadIdentityRegistries: [] + # - radixdev.azurecr.io + # - radixprod.azurecr.io + # Number of workers to process images # workers: 1 diff --git a/go.mod b/go.mod index 4f7f872..bb49a80 100644 --- a/go.mod +++ b/go.mod @@ -5,9 +5,12 @@ go 1.21 toolchain go1.21.0 require ( + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 github.com/containerd/containerd v1.7.14 github.com/equinor/radix-common v1.9.2 github.com/equinor/radix-operator v1.50.2 + github.com/golang-jwt/jwt/v5 v5.2.1 github.com/golang/mock v1.6.0 github.com/microsoft/go-mssqldb v1.7.0 github.com/mitchellh/mapstructure v1.5.0 @@ -16,6 +19,7 @@ require ( github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.18.2 github.com/stretchr/testify v1.9.0 + golang.org/x/oauth2 v0.15.0 gorm.io/driver/sqlserver v1.5.3 gorm.io/gorm v1.25.7 k8s.io/api v0.29.0 @@ -26,8 +30,6 @@ require ( require ( dario.cat/mergo v1.0.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1 // indirect - github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect @@ -39,7 +41,6 @@ require ( github.com/go-openapi/jsonreference v0.20.4 // indirect github.com/go-openapi/swag v0.22.7 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang-jwt/jwt/v5 v5.2.0 // indirect github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/golang-sql/sqlexp v0.1.0 // indirect github.com/golang/protobuf v1.5.3 // indirect @@ -77,7 +78,6 @@ require ( golang.org/x/crypto v0.18.0 // indirect golang.org/x/exp v0.0.0-20231226003508-02704c960a9b // indirect golang.org/x/net v0.20.0 // indirect - golang.org/x/oauth2 v0.15.0 // indirect golang.org/x/sys v0.16.0 // indirect golang.org/x/term v0.16.0 // indirect golang.org/x/text v0.14.0 // indirect diff --git a/go.sum b/go.sum index 8b88785..e767be6 100644 --- a/go.sum +++ b/go.sum @@ -63,8 +63,8 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69 github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= -github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= diff --git a/install_tools.sh b/install_tools.sh deleted file mode 100644 index 6582ac4..0000000 --- a/install_tools.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -SNYK_VERSION=1.1110.0 - -wget https://github.com/snyk/snyk/releases/download/v${SNYK_VERSION}/snyk-alpine -mv snyk-alpine /usr/local/bin -mv /usr/local/bin/snyk-alpine /usr/local/bin/snyk -chmod +x /usr/local/bin/snyk diff --git a/main.go b/main.go index 00d8c29..7beb1ba 100644 --- a/main.go +++ b/main.go @@ -8,16 +8,28 @@ import ( "syscall" "time" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + radix "github.com/equinor/radix-operator/pkg/client/clientset/versioned" + "github.com/equinor/radix-vulnerability-scanner/pkg/db" + "github.com/equinor/radix-vulnerability-scanner/pkg/dockercfg" + "github.com/equinor/radix-vulnerability-scanner/pkg/options" + "github.com/equinor/radix-vulnerability-scanner/pkg/scan" + "github.com/equinor/radix-vulnerability-scanner/pkg/scan/executor" "github.com/equinor/radix-vulnerability-scanner/pkg/server" + "github.com/equinor/radix-vulnerability-scanner/pkg/tokenstore" + "github.com/equinor/radix-vulnerability-scanner/pkg/tokenstore/tokensource" "github.com/rs/zerolog" "github.com/rs/zerolog/log" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" ) func main() { ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer cancel() - opts, err := server.LoadOptions(os.Args[1:]) + opts, err := options.LoadOptions(os.Args[1:]) if err != nil { log.Fatal().Msg(err.Error()) } @@ -28,8 +40,20 @@ func main() { } logOptions(opts) + scanOptions := setupScanOptions(ctx, opts) + scanner := scan.NewSnykScanner(executor.New(), scanOptions...) - srv, err := server.New(opts) + repo, err := db.NewGormRepository(&opts.DB) + if err != nil { + log.Fatal().Msg(err.Error()) + } + + kubeClient, radixClient, err := getKubernetesClients(&opts.Kube) + if err != nil { + log.Fatal().Msg(err.Error()) + } + + srv, err := server.New(kubeClient, radixClient, scanner, repo, opts) if err != nil { log.Fatal().Msg(err.Error()) } @@ -40,7 +64,42 @@ func main() { } } -func setupLogger(opts *server.Options, ctx context.Context) (context.Context, error) { +func setupScanOptions(ctx context.Context, opts *options.Options) []scan.SnykOption { + tokenstore, err := setupTokenStore(ctx, opts.WorkloadIdentityForRegistries) + if err != nil { + log.Fatal().Msg(err.Error()) + } + + scanOptions := []scan.SnykOption{scan.WithAuthProvider(tokenstore)} + + if opts.Docker.AuthsFile != "" { + dockerConfig, err := dockercfg.NewFromFile(opts.Docker.AuthsFile) + if err != nil { + log.Fatal().Msg(err.Error()) + } + scanOptions = append(scanOptions, scan.WithAuthProvider(dockerConfig)) + } + return scanOptions +} + +func setupTokenStore(ctx context.Context, registries []string) (*tokenstore.TokenStore, error) { + cred, err := azidentity.NewDefaultAzureCredential(nil) + if err != nil { + return nil, err + } + + var sources []tokenstore.SourceOption + for _, registry := range registries { + source := tokensource.NewACRTokenSource(ctx, registry, tokensource.WithCredentialOption(cred)) + + sources = append(sources, tokenstore.WithTokenSource(registry, source)) + } + + ts := tokenstore.New(sources...) + return ts, nil +} + +func setupLogger(opts *options.Options, ctx context.Context) (context.Context, error) { zerolog.DurationFieldUnit = time.Millisecond level, err := zerolog.ParseLevel(opts.LogLevel) if err != nil { @@ -56,7 +115,7 @@ func setupLogger(opts *server.Options, ctx context.Context) (context.Context, er return ctx, nil } -func logOptions(opts *server.Options) { +func logOptions(opts *options.Options) { log.Info().Msg("Configuration") log.Info().Msgf(" full-sync-cron-spec: %v", opts.FullSyncCronSpec) log.Info().Msgf(" app-name-exclude-list: %v", strings.Join(opts.AppNameExcludeList, ",")) @@ -67,4 +126,33 @@ func logOptions(opts *server.Options) { log.Info().Msgf(" vulnerability-rescan-age: %s", opts.VulnerabilityScan.RescanAge) log.Info().Msgf(" docker-config-file: %s", opts.Docker.AuthsFile) log.Info().Msgf(" kube-config-file: %s", opts.Kube.KubeConfigFile) + log.Info().Msgf(" workload-identity-registries: %s", strings.Join(opts.WorkloadIdentityForRegistries, ",")) +} + +func getKubernetesClients(opts *options.KubeOptions) (kubernetes.Interface, radix.Interface, error) { + var clientConfig *rest.Config + var err error + + if len(opts.KubeConfigFile) > 0 { + loadingRules := &clientcmd.ClientConfigLoadingRules{ExplicitPath: opts.KubeConfigFile} + loader := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, &clientcmd.ConfigOverrides{}) + clientConfig, err = loader.ClientConfig() + } else { + clientConfig, err = rest.InClusterConfig() + } + if err != nil { + return nil, nil, err + } + + kubeClient, err := kubernetes.NewForConfig(clientConfig) + if err != nil { + return nil, nil, err + } + + radixClient, err := radix.NewForConfig(clientConfig) + if err != nil { + return nil, nil, err + } + + return kubeClient, radixClient, nil } diff --git a/pkg/db/gorm.go b/pkg/db/gorm.go index 7e5def4..41b7cae 100644 --- a/pkg/db/gorm.go +++ b/pkg/db/gorm.go @@ -3,12 +3,18 @@ package db import ( "context" "database/sql" + "fmt" "time" + commongorm "github.com/equinor/radix-common/pkg/gorm" "github.com/equinor/radix-vulnerability-scanner/pkg/generic" + "github.com/equinor/radix-vulnerability-scanner/pkg/options" mssql "github.com/microsoft/go-mssqldb" + "github.com/microsoft/go-mssqldb/azuread" + "gorm.io/driver/sqlserver" "gorm.io/gorm" "gorm.io/gorm/clause" + "gorm.io/gorm/schema" ) const vulnerabilityBulkTypeTvpName = "dbo.VulnerabilityBulkType" @@ -20,10 +26,26 @@ type gormRepository struct { } // NewGormRepository returns a Repository using a Gorm ORM (https://gorm.io/index.html) database to access data -func NewGormRepository(db *gorm.DB) Repository { - return &gormRepository{ - db: db, +func NewGormRepository(opts *options.DBOptions) (Repository, error) { + + dsn := fmt.Sprintf("server=%s;database=%s;fedauth=ActiveDirectoryDefault", opts.Server, opts.Database) + dialector := sqlserver.New(sqlserver.Config{ + DriverName: azuread.DriverName, + DSN: dsn, + }) + + gormdb, err := gorm.Open(dialector, &gorm.Config{ + NamingStrategy: schema.NamingStrategy{NoLowerCase: true}, + Logger: commongorm.NewLogger(), + DisableAutomaticPing: false, + }) + if err != nil { + return nil, err } + + return &gormRepository{ + db: gormdb, + }, nil } func (r *gormRepository) GetLastImageScan(ctx context.Context, image string) (*ImageScanDto, error) { diff --git a/pkg/db/repository.go b/pkg/db/repository.go index 8d29cf9..8f359c1 100644 --- a/pkg/db/repository.go +++ b/pkg/db/repository.go @@ -5,12 +5,10 @@ import ( "time" ) -type ( - // Repository defines methods for reading and storing data about vulnerability scans - Repository interface { - // GetLastImageScan returns the last vulnerability scan for an image - GetLastImageScan(ctx context.Context, image string) (*ImageScanDto, error) - // RegisterImageScan stores information about a vulnerability scan for an image - RegisterImageScan(ctx context.Context, image string, baseImage *string, scanTime time.Time, success bool, vulnerabilities []VulnerabilityBulkDto, identifiers []VulnerabilityIdentifierBulkDto, references []VulnerabilityReferenceBulkDto) error - } -) +// Repository defines methods for reading and storing data about vulnerability scans +type Repository interface { + // GetLastImageScan returns the last vulnerability scan for an image + GetLastImageScan(ctx context.Context, image string) (*ImageScanDto, error) + // RegisterImageScan stores information about a vulnerability scan for an image + RegisterImageScan(ctx context.Context, image string, baseImage *string, scanTime time.Time, success bool, vulnerabilities []VulnerabilityBulkDto, identifiers []VulnerabilityIdentifierBulkDto, references []VulnerabilityReferenceBulkDto) error +} diff --git a/pkg/dockercfg/config.go b/pkg/dockercfg/config.go index 1add5df..75535dc 100644 --- a/pkg/dockercfg/config.go +++ b/pkg/dockercfg/config.go @@ -1,40 +1,59 @@ package dockercfg import ( + "context" "encoding/json" "errors" "os" + + "github.com/containerd/containerd/reference/docker" + "github.com/equinor/radix-vulnerability-scanner/pkg/registry" ) -type DockerConfigAuthJSON struct { - Auths DockerAuthConfig `json:"auths"` +type Config struct { + Auths AuthMap `json:"auths"` } -// DockerAuthConfig defines a map of registry names and authentication information -type DockerAuthConfig map[string]DockerAuthConfigEntry +// AuthMap defines a map of registry names and authentication information +type AuthMap map[string]AuthEntry -// DockerAuthConfigEntry contains username and password for a docker registry -type DockerAuthConfigEntry struct { +// AuthEntry contains username and password for a docker registry +type AuthEntry struct { Username string `json:"username"` Password string `json:"password"` } -// ReadDockerAuthConfigFromFile attempts to read docker configJSON from a given file path. -func ReadDockerAuthConfigFromFile(filePath string) (cfg DockerAuthConfig, err error) { +// NewFromFile attempts to read docker configJSON from a given file path. +func NewFromFile(filePath string) (cfg *Config, err error) { var contents []byte if contents, err = os.ReadFile(filePath); err != nil { return nil, err } - return ReadDockerAuthConfigFromBytes(contents) + return NewFromBytes(contents) } -// ReadDockerAuthConfigFromFile attempts to unmarshal a slice of bytes to a DockerAuthConfig. -func ReadDockerAuthConfigFromBytes(contents []byte) (cfg DockerAuthConfig, err error) { - var cfgJSON DockerConfigAuthJSON - if err = json.Unmarshal(contents, &cfgJSON); err != nil { +// NewFromBytes attempts to unmarshal a slice of bytes to a Config. +func NewFromBytes(contents []byte) (*Config, error) { + var cfgJSON Config + if err := json.Unmarshal(contents, &cfgJSON); err != nil { return nil, errors.New("error occurred while trying to unmarshal json") } - cfg = cfgJSON.Auths - return + return &cfgJSON, nil +} + +func (c Config) GetAuth(_ context.Context, image string) (*registry.Auth, error) { + named, err := docker.ParseDockerRef(image) + if err != nil { + return nil, err + } + registryName := docker.Domain(named) + + if len(c.Auths) > 0 { + if auth, found := c.Auths[registryName]; found { + return ®istry.Auth{Username: auth.Username, Password: auth.Password}, nil + } + } + + return nil, nil } diff --git a/pkg/dockercfg/config_test.go b/pkg/dockercfg/config_test.go new file mode 100644 index 0000000..41f75b5 --- /dev/null +++ b/pkg/dockercfg/config_test.go @@ -0,0 +1,30 @@ +package dockercfg_test + +import ( + "context" + "testing" + + "github.com/equinor/radix-vulnerability-scanner/pkg/dockercfg" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetAuthValid(t *testing.T) { + cfg := dockercfg.Config{Auths: dockercfg.AuthMap{"anyregistry.io": {"admin", "password"}}} + auth, err := cfg.GetAuth(context.Background(), "anyregistry.io/anyimage:anytag") + require.NoError(t, err) + require.NotNil(t, auth) + assert.Equal(t, "admin", auth.Username) + assert.Equal(t, "password", auth.Password) +} + +func TestMissingGetAuthIsEmpty(t *testing.T) { + auth, err := dockercfg.Config{}.GetAuth(context.Background(), "anyregistry.io/anyimage:anytag") + require.NoError(t, err) + assert.Nil(t, auth) +} + +func TestGetAuth_InvalidImageErrors(t *testing.T) { + _, err := dockercfg.Config{}.GetAuth(context.Background(), "anyregistry.io/:anytag") + assert.ErrorContains(t, err, "invalid reference format") +} diff --git a/pkg/handler/handler.go b/pkg/handler/handler.go index 3b4c438..0c5da6e 100644 --- a/pkg/handler/handler.go +++ b/pkg/handler/handler.go @@ -20,7 +20,7 @@ type ( // Handler interface Handler interface { - Handle(ctx context.Context, imageName string, dockerAuth dockercfg.DockerAuthConfig) error + Handle(ctx context.Context, imageName string, dockerConfig dockercfg.Config) error } // Option configuration for vulnerability scanner @@ -65,7 +65,7 @@ func New(scanner scan.Scanner, repository db.Repository, opts ...Option) Handler } // Handle scans an image and stores the result -func (s *imageVulnerabilityScanner) Handle(ctx context.Context, imageName string, dockerAuth dockercfg.DockerAuthConfig) error { +func (s *imageVulnerabilityScanner) Handle(ctx context.Context, imageName string, dockerConfig dockercfg.Config) error { // TODO: use https://github.com/go-redsync/redsync for distributed locking // As long as we run the scanner as a single pod/replica and with a single worker "thread" // we don't need to worry about locking @@ -82,7 +82,7 @@ func (s *imageVulnerabilityScanner) Handle(ctx context.Context, imageName string log.Info().Str("image", imageName).Msgf("scanning image") scanCtx, cancel := context.WithTimeout(ctx, s.scanTimeout) defer cancel() - scanResult, err := s.scanner.Scan(scanCtx, imageName, dockerAuth) + scanResult, err := s.scanner.Scan(scanCtx, imageName, dockerConfig) if err != nil { log.Warn().Str("image", imageName).Err(err).Msgf("error scanning image") } diff --git a/pkg/handler/handler_test.go b/pkg/handler/handler_test.go index df78f69..b215c46 100644 --- a/pkg/handler/handler_test.go +++ b/pkg/handler/handler_test.go @@ -57,7 +57,7 @@ func (s *scanTestSuite) Test_MustNotScanWhenLastScanWithinMaxAge() { image := "image:latest" s.repo.EXPECT().GetLastImageScan(gomock.Any(), image).Return(&db.ImageScanDto{ScanTime: time.Now()}, nil).Times(1) sut := New(s.scanner, s.repo, WithRescanAge(time.Minute)) - err := sut.Handle(context.TODO(), image, nil) + err := sut.Handle(context.TODO(), image, dockercfg.Config{}) s.Require().NoError(err) } @@ -66,23 +66,23 @@ func (s *scanTestSuite) Test_MustNotScanWhenGetLastImageScanReturnsError() { expectedErr := errors.New("any error") s.repo.EXPECT().GetLastImageScan(gomock.Any(), image).Return(nil, expectedErr).Times(1) sut := New(s.scanner, s.repo, WithRescanAge(time.Minute)) - err := sut.Handle(context.TODO(), image, nil) + err := sut.Handle(context.TODO(), image, dockercfg.Config{}) s.ErrorIs(err, expectedErr) } func (s *scanTestSuite) Test_MustScanWhenLastScanExceedMaxAge() { scanResult := &scan.ScanResult{} s.repo.EXPECT().GetLastImageScan(gomock.Any(), gomock.Any()).Return(&db.ImageScanDto{ScanTime: time.Now().Add(-1 * time.Hour)}, nil).Times(1) - s.scanner.EXPECT().Scan(gomock.Any(), gomock.Any(), nil).Return(scanResult, nil).Times(1) + s.scanner.EXPECT().Scan(gomock.Any(), gomock.Any(), gomock.Any()).Return(scanResult, nil).Times(1) s.repo.EXPECT().RegisterImageScan(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Times(1) sut := New(s.scanner, s.repo, WithRescanAge(time.Minute)) - err := sut.Handle(context.TODO(), "foo", nil) + err := sut.Handle(context.TODO(), "foo", dockercfg.Config{}) s.Require().NoError(err) } func (s *scanTestSuite) Test_SuccessfulScanStoresScanResults() { image := "image:latest" - dockerCfg := dockercfg.DockerAuthConfig{"any": {}} + dockerCfg := dockercfg.Config{Auths: dockercfg.AuthMap{"any": {}}} scanResult := &scan.ScanResult{ Docker: scan.DockerInfo{BaseImage: s.stringPtr("baseimage")}, Vulnerabilities: []scan.Vulnerability{ @@ -148,20 +148,20 @@ func (s *scanTestSuite) Test_FailedScanStoresScanFailed() { image := "image:latest" s.repo.EXPECT().GetLastImageScan(gomock.Any(), image).Return(&db.ImageScanDto{ScanTime: time.Now().Add(-1 * time.Hour)}, nil).Times(1) - s.scanner.EXPECT().Scan(gomock.Any(), gomock.Any(), nil).Return(nil, errors.New("any error")).Times(1) + s.scanner.EXPECT().Scan(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errors.New("any error")).Times(1) s.repo.EXPECT().RegisterImageScan(gomock.Any(), image, gomock.Any(), skewTimeNowMatcher{skewFrom: -5 * time.Second, skewTo: 5 * time.Second}, false, []db.VulnerabilityBulkDto{}, []db.VulnerabilityIdentifierBulkDto{}, []db.VulnerabilityReferenceBulkDto{}).Return(nil).Times(1) sut := New(s.scanner, s.repo, WithRescanAge(time.Minute)) - err := sut.Handle(context.TODO(), image, nil) + err := sut.Handle(context.TODO(), image, dockercfg.Config{}) s.Require().NoError(err) } func (s *scanTestSuite) Test_FailingToStoreScanResultsReturnsError() { expectedErr := errors.New("any error") s.repo.EXPECT().GetLastImageScan(gomock.Any(), gomock.Any()).Return(&db.ImageScanDto{ScanTime: time.Now().Add(-1 * time.Hour)}, nil).Times(1) - s.scanner.EXPECT().Scan(gomock.Any(), gomock.Any(), nil).Return(nil, nil).Times(1) + s.scanner.EXPECT().Scan(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil).Times(1) s.repo.EXPECT().RegisterImageScan(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(expectedErr).Times(1) sut := New(s.scanner, s.repo, WithRescanAge(time.Minute)) - err := sut.Handle(context.TODO(), "anyimage", nil) + err := sut.Handle(context.TODO(), "anyimage", dockercfg.Config{}) s.ErrorIs(err, expectedErr) } diff --git a/pkg/imageworker/worker.go b/pkg/imageworker/worker.go index 75dc6a7..7dd8d6f 100644 --- a/pkg/imageworker/worker.go +++ b/pkg/imageworker/worker.go @@ -94,7 +94,7 @@ func (w *Worker) processItem(ctx context.Context, item any) { if image, ok := item.(*observe.ImageInfo); ok { log.Info().Str("image", image.ImageName).Msg("processing image") - if err := w.handler.Handle(ctx, image.ImageName, image.DockerAuths); err != nil { + if err := w.handler.Handle(ctx, image.ImageName, image.DockerConfig); err != nil { requeues := w.queue.NumRequeues(image) if requeues < maxNumberOfRequeues { log.Info().Str("image", image.ImageName).Err(err).Msgf("requeuing scan of image (attempt %d of %d) due to error", requeues+1, maxNumberOfRequeues) diff --git a/pkg/imageworker/worker_test.go b/pkg/imageworker/worker_test.go index 4886a0b..f5f600f 100644 --- a/pkg/imageworker/worker_test.go +++ b/pkg/imageworker/worker_test.go @@ -46,8 +46,8 @@ type mockHandler struct { mock.Mock } -func (f *mockHandler) Handle(ctx context.Context, image string, auths dockercfg.DockerAuthConfig) error { - args := f.Called(ctx, image, auths) +func (f *mockHandler) Handle(ctx context.Context, image string, dockerConfig dockercfg.Config) error { + args := f.Called(ctx, image, dockerConfig) return args.Error(0) } @@ -84,14 +84,14 @@ func Test_WorkerProcessImageWithSuccess(t *testing.T) { imageSubject.AttachObserver(sut) // Handle image with no error - imageInfo := observe.ImageInfo{ImageName: "image:tag", DockerAuths: dockercfg.DockerAuthConfig{"any": {}}} + imageInfo := observe.ImageInfo{ImageName: "image:tag", DockerConfig: dockercfg.Config{Auths: dockercfg.AuthMap{"any": {}}}} queue.On("Add", &imageInfo).Times(1) queue.On("Get").Times(1).Return(&imageInfo, false) // First call to get returns image queue.On("Get").Times(1).Return("", true) // Second call to get (in loop) returns shutdown true queue.On("ShuttingDown").Times(1).Return(false) queue.On("Done", &imageInfo).Times(1) queue.On("Forget", &imageInfo).Times(1) - imgHandler.On("Handle", mock.Anything, imageInfo.ImageName, imageInfo.DockerAuths).Times(1).Return(nil) + imgHandler.On("Handle", mock.Anything, imageInfo.ImageName, imageInfo.DockerConfig).Times(1).Return(nil) imageSubject.Next(imageInfo) time.Sleep(100 * time.Millisecond) // Sleep to let worker go-routines process the queue diff --git a/pkg/observe/radixdeployment.go b/pkg/observe/radixdeployment.go index e66ae59..b056fff 100644 --- a/pkg/observe/radixdeployment.go +++ b/pkg/observe/radixdeployment.go @@ -17,8 +17,8 @@ import ( type ( // ImageInfo is sent to observers of RadixDeploymentContainerImageMapper ImageInfo struct { - ImageName string - DockerAuths dockercfg.DockerAuthConfig + ImageName string + DockerConfig dockercfg.Config } // RadixDeploymentContainerImageMapper receives RadixDeployments and emits images defined in jobs and componenets @@ -39,38 +39,38 @@ func (m *RadixDeploymentContainerImageMapper) Receive(rd *v1.RadixDeployment) { return } - dockerAuths, err := m.readDockerConfigJSON(rd.Namespace) + dockerConfig, err := m.readDockerConfigJSON(rd.Namespace) if err != nil { log.Warn().Err(err).Msg("unable to read dockerconfigjson secret") } for _, c := range rd.Spec.Components { - m.notifyObservers(ImageInfo{ImageName: c.GetImage(), DockerAuths: dockerAuths}) + m.notifyObservers(ImageInfo{ImageName: c.GetImage(), DockerConfig: dockerConfig}) } for _, c := range rd.Spec.Jobs { - m.notifyObservers(ImageInfo{ImageName: c.GetImage(), DockerAuths: dockerAuths}) + m.notifyObservers(ImageInfo{ImageName: c.GetImage(), DockerConfig: dockerConfig}) } } -func (m *RadixDeploymentContainerImageMapper) readDockerConfigJSON(namespace string) (dockercfg.DockerAuthConfig, error) { +func (m *RadixDeploymentContainerImageMapper) readDockerConfigJSON(namespace string) (dockercfg.Config, error) { + var cfg = dockercfg.Config{} secret, err := m.KubeClient.CoreV1().Secrets(namespace).Get(context.Background(), defaults.PrivateImageHubSecretName, metav1.GetOptions{}) if err != nil { - return nil, err + return cfg, err } - authJSON, ok := secret.Data[corev1.DockerConfigJsonKey] + configJSON, ok := secret.Data[corev1.DockerConfigJsonKey] if !ok { - return nil, fmt.Errorf("secret %s in namespace %s does not contain data for %s", defaults.PrivateImageHubSecretName, namespace, corev1.DockerConfigJsonKey) + return cfg, fmt.Errorf("secret %s in namespace %s does not contain data for %s", defaults.PrivateImageHubSecretName, namespace, corev1.DockerConfigJsonKey) } - var dockerAuths dockercfg.DockerConfigAuthJSON - err = json.Unmarshal(authJSON, &dockerAuths) + err = json.Unmarshal(configJSON, &cfg) if err != nil { - return nil, err + return cfg, err } - return dockerAuths.Auths, nil + return cfg, nil } func (m *RadixDeploymentListMapper) Receive(rds []*v1.RadixDeployment) { diff --git a/pkg/observe/radixdeployment_test.go b/pkg/observe/radixdeployment_test.go index 3dc8a45..f4929a6 100644 --- a/pkg/observe/radixdeployment_test.go +++ b/pkg/observe/radixdeployment_test.go @@ -20,8 +20,8 @@ func Test_RadixDeploymentContainerImageMapperWithDockerAuthSecret(t *testing.T) var receivedImages []ImageInfo fakeObserver := fakeImageInfoObserver{OnReceive: func(receivedObj ImageInfo) { receivedImages = append(receivedImages, receivedObj) }} kubeClient := kubefake.NewSimpleClientset() - dockerAuth := dockercfg.DockerConfigAuthJSON{Auths: dockercfg.DockerAuthConfig{"docker.io": {}}} - dockerAuthBytes, _ := json.Marshal(&dockerAuth) + dockerConfig := dockercfg.Config{Auths: dockercfg.AuthMap{"docker.io": {}}} + dockerAuthBytes, _ := json.Marshal(&dockerConfig) _, err := kubeClient.CoreV1().Secrets(namespace).Create(context.Background(), &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{Name: defaults.PrivateImageHubSecretName}, Data: map[string][]byte{ @@ -42,10 +42,10 @@ func Test_RadixDeploymentContainerImageMapperWithDockerAuthSecret(t *testing.T) assert.ElementsMatch(t, []ImageInfo{ - {ImageName: "c1", DockerAuths: dockerAuth.Auths}, - {ImageName: "c2", DockerAuths: dockerAuth.Auths}, - {ImageName: "j1", DockerAuths: dockerAuth.Auths}, - {ImageName: "j2", DockerAuths: dockerAuth.Auths}, + {ImageName: "c1", DockerConfig: dockerConfig}, + {ImageName: "c2", DockerConfig: dockerConfig}, + {ImageName: "j1", DockerConfig: dockerConfig}, + {ImageName: "j2", DockerConfig: dockerConfig}, }, receivedImages) } diff --git a/pkg/server/load.go b/pkg/options/load.go similarity index 96% rename from pkg/server/load.go rename to pkg/options/load.go index aaf75a3..88c924b 100644 --- a/pkg/server/load.go +++ b/pkg/options/load.go @@ -1,4 +1,4 @@ -package server +package options import ( "fmt" @@ -45,6 +45,7 @@ func newOptionsFlagSet() *pflag.FlagSet { flagset.Uint("workers", 1, "Number for image scan workers") flagset.Bool("pretty-print", false, "Print colored text instead of json") flagset.String("log-level", "debug", "Set log level (trace,debug,info,warn,error)") + flagset.StringSlice("workload-identity-registries", nil, "List registries authenticated with workload identity") flagset.AddFlagSet(dbFlagset()) flagset.AddFlagSet(dockerFlagset()) flagset.AddFlagSet(kubeFlagset()) diff --git a/pkg/options/options.go b/pkg/options/options.go new file mode 100644 index 0000000..a164b8f --- /dev/null +++ b/pkg/options/options.go @@ -0,0 +1,41 @@ +package options + +import "time" + +type ( + // Options for server + Options struct { + FullSyncCronSpec string `flag:"full-sync-cron-spec" cfg:"full_sync_cron_spec"` + AppNameExcludeList []string `flag:"app-name-exclude-list" cfg:"app_name_exclude_list"` + WorkloadIdentityForRegistries []string `flag:"workload-identity-registries" cfg:"workload_identity_registries"` + Workers uint `flag:"workers" cfg:"workers"` + PrettyPrint bool `flag:"pretty-print" cfg:"pretty_print" default:"false"` + LogLevel string `flag:"log-level" cfg:"log_level" default:"debug"` + DB DBOptions `cfg:",squash"` + Docker DockerOptions `cfg:",squash"` + Kube KubeOptions `cfg:",squash"` + VulnerabilityScan VulnerabilityScanOptions `cfg:",squash"` + } + + // VulnerabilityScanOptions + VulnerabilityScanOptions struct { + ScanTimeout time.Duration `flag:"vulnerability-scan-timeout" cfg:"vulnerability_scan_timeout"` + RescanAge time.Duration `flag:"vulnerability-rescan-age" cfg:"vulnerability_rescan_age"` + } + + // DBOptions contains configuration for database connection + DBOptions struct { + Server string `flag:"db-server" cfg:"db_server"` + Database string `flag:"db-database" cfg:"db_database"` + } + + // DockerOptions contains configuration for accessing docker images + DockerOptions struct { + AuthsFile string `flag:"docker-config-file" cfg:"docker_config_file"` + } + + // KubeOptions contains configuration for connecting to the Kubernetes API server + KubeOptions struct { + KubeConfigFile string `flag:"kube-config-file" cfg:"kube_config_file"` + } +) diff --git a/pkg/registry/auth.go b/pkg/registry/auth.go new file mode 100644 index 0000000..f349fdf --- /dev/null +++ b/pkg/registry/auth.go @@ -0,0 +1,13 @@ +package registry + +import "context" + +type AuthProvider interface { + // GetAuth should return nil, if registry is not supported + GetAuth(ctx context.Context, image string) (*Auth, error) +} + +type Auth struct { + Username string + Password string +} diff --git a/pkg/registry/mock/auth.go b/pkg/registry/mock/auth.go new file mode 100644 index 0000000..4d34cd4 --- /dev/null +++ b/pkg/registry/mock/auth.go @@ -0,0 +1,51 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./pkg/registry/auth.go + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + registry "github.com/equinor/radix-vulnerability-scanner/pkg/registry" + gomock "github.com/golang/mock/gomock" +) + +// MockAuthProvider is a mock of AuthProvider interface. +type MockAuthProvider struct { + ctrl *gomock.Controller + recorder *MockAuthProviderMockRecorder +} + +// MockAuthProviderMockRecorder is the mock recorder for MockAuthProvider. +type MockAuthProviderMockRecorder struct { + mock *MockAuthProvider +} + +// NewMockAuthProvider creates a new mock instance. +func NewMockAuthProvider(ctrl *gomock.Controller) *MockAuthProvider { + mock := &MockAuthProvider{ctrl: ctrl} + mock.recorder = &MockAuthProviderMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockAuthProvider) EXPECT() *MockAuthProviderMockRecorder { + return m.recorder +} + +// GetAuth mocks base method. +func (m *MockAuthProvider) GetAuth(ctx context.Context, image string) (*registry.Auth, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAuth", ctx, image) + ret0, _ := ret[0].(*registry.Auth) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAuth indicates an expected call of GetAuth. +func (mr *MockAuthProviderMockRecorder) GetAuth(ctx, image interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuth", reflect.TypeOf((*MockAuthProvider)(nil).GetAuth), ctx, image) +} diff --git a/pkg/scan/executor/executor.go b/pkg/scan/executor/executor.go new file mode 100644 index 0000000..6e0b167 --- /dev/null +++ b/pkg/scan/executor/executor.go @@ -0,0 +1,32 @@ +package executor + +import ( + "context" + "io" + "os/exec" + + "github.com/equinor/radix-vulnerability-scanner/pkg/utils/logwriter" + "github.com/rs/zerolog" +) + +type Executor interface { + Execute(ctx context.Context, command string, args []string, stdOutWriter io.Writer) error +} + +type CommandExecutor struct{} + +func New() *CommandExecutor { + return &CommandExecutor{} +} + +func (CommandExecutor) Execute(ctx context.Context, command string, args []string, stdOutWriter io.Writer) error { + cmd := exec.CommandContext(ctx, command, args...) + cmd.Stderr = logwriter.New(zerolog.Ctx(ctx), zerolog.ErrorLevel) + cmd.Stdout = stdOutWriter + + if err := cmd.Start(); err != nil { + return err + } + + return cmd.Wait() +} diff --git a/pkg/scan/executor/mock/executor.go b/pkg/scan/executor/mock/executor.go new file mode 100644 index 0000000..162a3d5 --- /dev/null +++ b/pkg/scan/executor/mock/executor.go @@ -0,0 +1,50 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./pkg/scan/executor/executor.go + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + io "io" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// MockExecutor is a mock of Executor interface. +type MockExecutor struct { + ctrl *gomock.Controller + recorder *MockExecutorMockRecorder +} + +// MockExecutorMockRecorder is the mock recorder for MockExecutor. +type MockExecutorMockRecorder struct { + mock *MockExecutor +} + +// NewMockExecutor creates a new mock instance. +func NewMockExecutor(ctrl *gomock.Controller) *MockExecutor { + mock := &MockExecutor{ctrl: ctrl} + mock.recorder = &MockExecutorMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockExecutor) EXPECT() *MockExecutorMockRecorder { + return m.recorder +} + +// Execute mocks base method. +func (m *MockExecutor) Execute(ctx context.Context, command string, args []string, stdOutWriter io.Writer) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Execute", ctx, command, args, stdOutWriter) + ret0, _ := ret[0].(error) + return ret0 +} + +// Execute indicates an expected call of Execute. +func (mr *MockExecutorMockRecorder) Execute(ctx, command, args, stdOutWriter interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Execute", reflect.TypeOf((*MockExecutor)(nil).Execute), ctx, command, args, stdOutWriter) +} diff --git a/pkg/scan/mock/scanner.go b/pkg/scan/mock/scanner.go index 9584628..26fea13 100644 --- a/pkg/scan/mock/scanner.go +++ b/pkg/scan/mock/scanner.go @@ -37,16 +37,16 @@ func (m *MockScanner) EXPECT() *MockScannerMockRecorder { } // Scan mocks base method. -func (m *MockScanner) Scan(ctx context.Context, image string, auth dockercfg.DockerAuthConfig) (*scan.ScanResult, error) { +func (m *MockScanner) Scan(ctx context.Context, image string, dockerConfig dockercfg.Config) (*scan.ScanResult, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Scan", ctx, image, auth) + ret := m.ctrl.Call(m, "Scan", ctx, image, dockerConfig) ret0, _ := ret[0].(*scan.ScanResult) ret1, _ := ret[1].(error) return ret0, ret1 } // Scan indicates an expected call of Scan. -func (mr *MockScannerMockRecorder) Scan(ctx, image, auth interface{}) *gomock.Call { +func (mr *MockScannerMockRecorder) Scan(ctx, image, dockerConfig interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Scan", reflect.TypeOf((*MockScanner)(nil).Scan), ctx, image, auth) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Scan", reflect.TypeOf((*MockScanner)(nil).Scan), ctx, image, dockerConfig) } diff --git a/pkg/scan/scanner.go b/pkg/scan/scanner.go index 1dff626..c787dd6 100644 --- a/pkg/scan/scanner.go +++ b/pkg/scan/scanner.go @@ -9,5 +9,5 @@ import ( // Scanner defines methods for scanning Docker images for vulnerabilities type Scanner interface { // Scan scans a Docker image for vulnerabilities - Scan(ctx context.Context, image string, auth dockercfg.DockerAuthConfig) (*ScanResult, error) + Scan(ctx context.Context, image string, dockerConfig dockercfg.Config) (*ScanResult, error) } diff --git a/pkg/scan/snyk.go b/pkg/scan/snyk.go index 5575d64..fe38424 100644 --- a/pkg/scan/snyk.go +++ b/pkg/scan/snyk.go @@ -8,59 +8,51 @@ import ( "io" "os/exec" - "github.com/containerd/containerd/reference/docker" "github.com/equinor/radix-vulnerability-scanner/pkg/dockercfg" - "github.com/equinor/radix-vulnerability-scanner/pkg/utils/logwriter" - "github.com/rs/zerolog" + "github.com/equinor/radix-vulnerability-scanner/pkg/registry" + "github.com/equinor/radix-vulnerability-scanner/pkg/scan/executor" + "github.com/rs/zerolog/log" ) -var _ Scanner = &snykScanner{} +var _ Scanner = &SnykScanner{} -type commandExecutor interface { - Execute(ctx context.Context, command string, args []string, stdOutWriter io.Writer) error +type SnykScanner struct { + executor executor.Executor + commonDockerAuths []registry.AuthProvider } -type commandExecutorImpl struct{} +type SnykOption func(*SnykScanner) -func (commandExecutorImpl) Execute(ctx context.Context, command string, args []string, stdOutWriter io.Writer) error { - cmd := exec.CommandContext(ctx, command, args...) - cmd.Stderr = logwriter.New(zerolog.Ctx(ctx), zerolog.ErrorLevel) - cmd.Stdout = stdOutWriter - - if err := cmd.Start(); err != nil { - return err +func WithAuthProvider(auth registry.AuthProvider) SnykOption { + return func(ss *SnykScanner) { + ss.commonDockerAuths = append(ss.commonDockerAuths, auth) } - - return cmd.Wait() } -type snykScanner struct { - commonAuths dockercfg.DockerAuthConfig - executor commandExecutor -} +// NewSnykScanner create a Scanner that use SNYK to scan for vulnerabilities +func NewSnykScanner(executor executor.Executor, opts ...SnykOption) *SnykScanner { + scanner := &SnykScanner{executor: executor} -// NewSnyk create a Scanner that use SNYK to scan for vulnerabilities -func NewSnyk(commonAuths dockercfg.DockerAuthConfig) Scanner { - return &snykScanner{commonAuths: commonAuths, executor: commandExecutorImpl{}} + for _, opt := range opts { + opt(scanner) + } + + return scanner } -func (s *snykScanner) Scan(ctx context.Context, image string, auths dockercfg.DockerAuthConfig) (*ScanResult, error) { +func (s *SnykScanner) Scan(ctx context.Context, image string, dockerConfig dockercfg.Config) (*ScanResult, error) { + auths := append(s.commonDockerAuths, &dockerConfig) + var credArgs []string // Try to get docker creds for image from common auths - credArgs, err := s.getCredentialArgs(image, s.commonAuths) - if err != nil { - return nil, err - } - - // If creds was not found in commonAuth, try to get creds from auths argument - var credsFromAuthsArg bool - if len(credArgs) == 0 { - credArgs, err = s.getCredentialArgs(image, auths) + for _, auth := range auths { + tmpCreds, err := s.getCredentialArgs(image, auth) if err != nil { return nil, err } + credArgs = tmpCreds if len(credArgs) > 0 { - credsFromAuthsArg = true + break } } @@ -77,8 +69,14 @@ func (s *snykScanner) Scan(ctx context.Context, image string, auths dockercfg.Do testArgsWithCreds = append(testArgsWithCreds, testArgs...) testArgsWithCreds = append(testArgsWithCreds, credArgs...) buf := &bytes.Buffer{} + err := scanFn(ctx, testArgsWithCreds, buf) + log.Trace().Stringer("result", buf).Strs("args", testArgsWithCreds).Err(err).Msg("scan completed") + + if err != nil { + if len(credArgs) == 0 { + return nil, err + } - if err := scanFn(ctx, testArgsWithCreds, buf); err != nil { // Rescan the image without credentials args if credentials were resolved from the `auths` parameter. // If invalid credentials are supplied for a public image (that requires no authentication), e.g. docker.io, // the scan will fail with an unauthorized error. @@ -86,11 +84,9 @@ func (s *snykScanner) Scan(ctx context.Context, image string, auths dockercfg.Do // parameter contains invalid credentials for docker.io. Even if redis:latest is public, the invalid credentials // from the `auths` parameter causes the scan to fail. We'll therefore try to do a second scan // without supplying credential arguments - if credsFromAuthsArg { - buf = &bytes.Buffer{} - err = scanFn(ctx, testArgs, buf) - } - + buf = &bytes.Buffer{} + err = scanFn(ctx, testArgs, buf) + log.Trace().Stringer("result", buf).Strs("args", testArgsWithCreds).Err(err).Msg("retry scan completed") if err != nil { return nil, err } @@ -104,39 +100,23 @@ func (s *snykScanner) Scan(ctx context.Context, image string, auths dockercfg.Do return &result, nil } -func (s *snykScanner) getCredentialArgs(image string, auths dockercfg.DockerAuthConfig) ([]string, error) { - auth, err := s.getDockerAuth(image, auths) +func (s *SnykScanner) getCredentialArgs(image string, authProvider registry.AuthProvider) ([]string, error) { + auth, err := authProvider.GetAuth(context.Background(), image) if err != nil { return nil, err } - if auth.Username != "" && auth.Password != "" { + if auth != nil && auth.Username != "" && auth.Password != "" { return []string{fmt.Sprintf("--username=%v", auth.Username), fmt.Sprintf("--password=%v", auth.Password)}, nil } return nil, nil } -func (s *snykScanner) getDockerAuth(image string, auths dockercfg.DockerAuthConfig) (dockercfg.DockerAuthConfigEntry, error) { - if len(auths) > 0 { - named, err := docker.ParseDockerRef(image) - if err != nil { - return dockercfg.DockerAuthConfigEntry{}, err - } - - registry := docker.Domain(named) - if auth, found := auths[registry]; found { - return auth, nil - } - } - - return dockercfg.DockerAuthConfigEntry{}, nil -} - -func (s *snykScanner) executeSnykWithArgs(ctx context.Context, args []string, stdOutWriter io.Writer) error { +func (s *SnykScanner) executeSnykWithArgs(ctx context.Context, args []string, stdOutWriter io.Writer) error { return s.executor.Execute(ctx, "snyk", args, stdOutWriter) } -func (s *snykScanner) isSnykScanSuccessErr(err error) bool { +func (s *SnykScanner) isSnykScanSuccessErr(err error) bool { if err == nil { return true } diff --git a/pkg/scan/snyk_test.go b/pkg/scan/snyk_test.go index abf515f..5e16e78 100644 --- a/pkg/scan/snyk_test.go +++ b/pkg/scan/snyk_test.go @@ -9,157 +9,132 @@ import ( "github.com/equinor/radix-common/utils" "github.com/equinor/radix-vulnerability-scanner/pkg/dockercfg" + "github.com/equinor/radix-vulnerability-scanner/pkg/registry" + authprovidermock "github.com/equinor/radix-vulnerability-scanner/pkg/registry/mock" + executormock "github.com/equinor/radix-vulnerability-scanner/pkg/scan/executor/mock" + "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" ) -type mockCommandExec struct { - mock.Mock -} +var emptyObj = struct{}{} -func (c *mockCommandExec) Execute(ctx context.Context, command string, args []string, stdOutWriter io.Writer) error { - mockArgs := c.Called(ctx, command, args, stdOutWriter) - return mockArgs.Error(0) +type SnykTestSuite struct { + suite.Suite + executor *executormock.MockExecutor + authprovider *authprovidermock.MockAuthProvider + ctrl *gomock.Controller } -func Test_NewSnyk(t *testing.T) { - dockerCfg := dockercfg.DockerAuthConfig{"registry": dockercfg.DockerAuthConfigEntry{}} - sut := NewSnyk(dockerCfg).(*snykScanner) - assert.Equal(t, dockerCfg, sut.commonAuths) - assert.IsType(t, commandExecutorImpl{}, sut.executor) +func (s *SnykTestSuite) SetupSuite() { + s.ctrl = gomock.NewController(s.T()) + s.executor = executormock.NewMockExecutor(s.ctrl) + s.authprovider = authprovidermock.NewMockAuthProvider(s.ctrl) +} +func Test_SnykTestSuite(t *testing.T) { + suite.Run(t, new(SnykTestSuite)) } -func Test_SnykScanImageWithCommonAuthEntry(t *testing.T) { - dockerCfg := dockercfg.DockerAuthConfig{ - "my.registry.com": dockercfg.DockerAuthConfigEntry{Username: "reguser", Password: "regpwd"}, - "another.registry.com": dockercfg.DockerAuthConfigEntry{Username: "someuser", Password: "somepwd"}, - } +func (s *SnykTestSuite) Test_NewSnyk() { + sut := NewSnykScanner(s.executor) - expectedArgs := []string{"container", "test", "--json", "my.registry.com/image:tag", "--username=reguser", "--password=regpwd"} - cmdExec := new(mockCommandExec) - call := cmdExec.On("Execute", mock.Anything, "snyk", expectedArgs, mock.Anything).Return(nil).Times(1) - call.RunFn = func(a mock.Arguments) { - _, err := a.Get(3).(io.Writer).Write([]byte("{}")) - require.NoError(t, err) - } - sut := &snykScanner{commonAuths: dockerCfg, executor: cmdExec} - _, actualErr := sut.Scan(context.Background(), "my.registry.com/image:tag", nil) - assert.NoError(t, actualErr) - cmdExec.AssertExpectations(t) + assert.IsType(s.T(), &SnykScanner{}, sut) } -func Test_SnykScanImageWithCommonAuthEntryAndArgumentAuth(t *testing.T) { - dockerCfg := dockercfg.DockerAuthConfig{ - "my.registry.com": dockercfg.DockerAuthConfigEntry{Username: "reguser", Password: "regpwd"}, - } - +func (s *SnykTestSuite) Test_SnykScanImageWithCommonAuthEntry() { + s.authprovider.EXPECT().GetAuth(gomock.Any(), "my.registry.com/image:tag").Return(®istry.Auth{Username: "reguser", Password: "regpwd"}, nil).Times(1) expectedArgs := []string{"container", "test", "--json", "my.registry.com/image:tag", "--username=reguser", "--password=regpwd"} - cmdExec := new(mockCommandExec) - call := cmdExec.On("Execute", mock.Anything, "snyk", expectedArgs, mock.Anything).Return(nil).Times(1) - call.RunFn = func(a mock.Arguments) { - _, err := a.Get(3).(io.Writer).Write([]byte("{}")) - require.NoError(t, err) - } - sut := &snykScanner{commonAuths: dockerCfg, executor: cmdExec} - _, actualErr := sut.Scan(context.Background(), "my.registry.com/image:tag", dockercfg.DockerAuthConfig{"my.registry.com": dockercfg.DockerAuthConfigEntry{Username: "anotheruser", Password: "anotherpwd"}}) - assert.NoError(t, actualErr) - cmdExec.AssertExpectations(t) + s.executor.EXPECT(). + Execute(gomock.Any(), "snyk", expectedArgs, gomock.Any()). + Do(s.fakeWriter(emptyObj)). + Return(nil). + Times(1) + + sut := NewSnykScanner(s.executor, WithAuthProvider(s.authprovider)) + + _, err := sut.Scan(context.Background(), "my.registry.com/image:tag", dockercfg.Config{}) + assert.NoError(s.T(), err) } -func Test_SnykScanImageWithNoCommonAuthEntryButWithArgumentAuth(t *testing.T) { - dockerCfg := dockercfg.DockerAuthConfig{} +func (s *SnykTestSuite) Test_SnykScanImageWithNoCommonAuthEntryButWithArgumentAuth() { expectedArgs := []string{"container", "test", "--json", "my.registry.com/image:tag", "--username=anotheruser", "--password=anotherpwd"} - cmdExec := new(mockCommandExec) - call := cmdExec.On("Execute", mock.Anything, "snyk", expectedArgs, mock.Anything).Return(nil).Times(1) - call.RunFn = func(a mock.Arguments) { - _, err := a.Get(3).(io.Writer).Write([]byte("{}")) - require.NoError(t, err) - } - sut := &snykScanner{commonAuths: dockerCfg, executor: cmdExec} - _, actualErr := sut.Scan(context.Background(), "my.registry.com/image:tag", dockercfg.DockerAuthConfig{"my.registry.com": dockercfg.DockerAuthConfigEntry{Username: "anotheruser", Password: "anotherpwd"}}) - assert.NoError(t, actualErr) - cmdExec.AssertExpectations(t) + s.executor.EXPECT(). + Execute(gomock.Any(), "snyk", expectedArgs, gomock.Any()). + Do(s.fakeWriter(emptyObj)). + Return(nil). + Times(1) + sut := NewSnykScanner(s.executor, WithAuthProvider(dockercfg.Config{})) + + extraCfg := &dockercfg.Config{Auths: dockercfg.AuthMap{"my.registry.com": {Username: "anotheruser", Password: "anotherpwd"}}} + _, actualErr := sut.Scan(context.Background(), "my.registry.com/image:tag", *extraCfg) + assert.NoError(s.T(), actualErr) } -func Test_SnykScanImageAuthArgFailsShouldRetryWithoutAuth(t *testing.T) { - dockerCfg := dockercfg.DockerAuthConfig{} +func (s *SnykTestSuite) Test_SnykScanImageAuthArgFailsShouldRetryWithoutAuth() { + dockerCfg := dockercfg.Config{} - cmdExec := new(mockCommandExec) expectedArgsWithAuth := []string{"container", "test", "--json", "my.registry.com/image:tag", "--username=anotheruser", "--password=anotherpwd"} - cmdExec.On("Execute", mock.Anything, "snyk", expectedArgsWithAuth, mock.Anything).Return(errors.New("any error")).Times(1) + s.executor.EXPECT().Execute(gomock.Any(), "snyk", expectedArgsWithAuth, gomock.Any()).Return(errors.New("any error")).Times(1) expectedArgsWithoutAuth := []string{"container", "test", "--json", "my.registry.com/image:tag"} - call := cmdExec.On("Execute", mock.Anything, "snyk", expectedArgsWithoutAuth, mock.Anything).Return(nil).Times(1) - call.RunFn = func(a mock.Arguments) { - _, err := a.Get(3).(io.Writer).Write([]byte("{}")) - require.NoError(t, err) - } - sut := &snykScanner{commonAuths: dockerCfg, executor: cmdExec} - _, actualErr := sut.Scan(context.Background(), "my.registry.com/image:tag", dockercfg.DockerAuthConfig{"my.registry.com": dockercfg.DockerAuthConfigEntry{Username: "anotheruser", Password: "anotherpwd"}}) - assert.NoError(t, actualErr) - cmdExec.AssertExpectations(t) -} + s.executor.EXPECT().Execute(gomock.Any(), "snyk", expectedArgsWithoutAuth, gomock.Any()).Do(s.fakeWriter(emptyObj)).Return(nil).Times(1) -func Test_SnykScanImageWithNoAuthEntry(t *testing.T) { - dockerCfg := dockercfg.DockerAuthConfig{ - "another.registry.com": dockercfg.DockerAuthConfigEntry{Username: "someuser", Password: "somepwd"}, - } + sut := NewSnykScanner(s.executor, WithAuthProvider(dockerCfg)) + extraConfig := dockercfg.Config{Auths: dockercfg.AuthMap{"my.registry.com": {Username: "anotheruser", Password: "anotherpwd"}}} + _, actualErr := sut.Scan(context.Background(), "my.registry.com/image:tag", extraConfig) + s.NoError(actualErr) +} +func (s *SnykTestSuite) Test_SnykScanImageWithNoAuthEntry() { + dockerCfg := dockercfg.Config{Auths: dockercfg.AuthMap{"another.registry.com": {Username: "someuser", Password: "somepwd"}}} expectedArgs := []string{"container", "test", "--json", "my.registry.com/image:tag"} - cmdExec := new(mockCommandExec) - call := cmdExec.On("Execute", mock.Anything, "snyk", expectedArgs, mock.Anything).Return(nil).Times(1) - call.RunFn = func(a mock.Arguments) { - _, err := a.Get(3).(io.Writer).Write([]byte("{}")) - require.NoError(t, err) - } - sut := &snykScanner{commonAuths: dockerCfg, executor: cmdExec} - _, actualErr := sut.Scan(context.Background(), "my.registry.com/image:tag", nil) - assert.NoError(t, actualErr) - cmdExec.AssertExpectations(t) + s.executor.EXPECT().Execute(gomock.Any(), "snyk", expectedArgs, gomock.Any()).Do(s.fakeWriter(emptyObj)).Return(nil).Times(1) + sut := NewSnykScanner(s.executor, WithAuthProvider(dockerCfg)) + + _, actualErr := sut.Scan(context.Background(), "my.registry.com/image:tag", dockercfg.Config{}) + + s.NoError(actualErr) } -func Test_SnykScanImageWithInvalidImageStringReturnsError(t *testing.T) { - dockerCfg := dockercfg.DockerAuthConfig{ - "any.registry.com": dockercfg.DockerAuthConfigEntry{Username: "someuser", Password: "somepwd"}, - } - cmdExec := new(mockCommandExec) - cmdExec.On("Execute", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Times(0) - sut := &snykScanner{commonAuths: dockerCfg, executor: cmdExec} - _, actualErr := sut.Scan(context.Background(), "my.registry.com/image:tag", nil) - assert.Error(t, actualErr) - cmdExec.AssertExpectations(t) +func (s *SnykTestSuite) Test_SnykScanImageWithInvalidImageStringReturnsError() { + dockerCfg := dockercfg.Config{Auths: dockercfg.AuthMap{"any.registry.com": {Username: "someuser", Password: "somepwd"}}} + s.executor.EXPECT().Execute(gomock.Any(), "snyk", gomock.Any(), gomock.Any()).Times(0) + sut := NewSnykScanner(s.executor, WithAuthProvider(dockerCfg)) + _, actualErr := sut.Scan(context.Background(), "my.registry.com/:image:tag", dockercfg.Config{}) + + s.Error(actualErr) } -func Test_SnykScanErrorFromExecutorReturned(t *testing.T) { - dockerCfg := dockercfg.DockerAuthConfig{} +func (s *SnykTestSuite) Test_SnykScanErrorFromExecutorReturned() { + dockerCfg := dockercfg.Config{} expectedErr := errors.New("any error") - cmdExec := new(mockCommandExec) - cmdExec.On("Execute", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(expectedErr).Times(1) - sut := &snykScanner{commonAuths: dockerCfg, executor: cmdExec} - _, actualErr := sut.Scan(context.Background(), "my.registry.com/image:", nil) - assert.ErrorIs(t, expectedErr, actualErr) - cmdExec.AssertExpectations(t) + s.executor.EXPECT().Execute(gomock.Any(), "snyk", gomock.Any(), gomock.Any()).Return(expectedErr).Times(1) + sut := NewSnykScanner(s.executor, WithAuthProvider(dockerCfg)) + _, actualErr := sut.Scan(context.Background(), "my.registry.com/image:test", dockercfg.Config{}) + s.ErrorIs(expectedErr, actualErr) } -func Test_SnykScanStdOutFromExecutorUnmarshalled(t *testing.T) { - dockerCfg := dockercfg.DockerAuthConfig{} +func (s *SnykTestSuite) Test_SnykScanStdOutFromExecutorUnmarshalled() { + dockerCfg := dockercfg.Config{} expectedResults := ScanResult{ Docker: DockerInfo{BaseImage: utils.StringPtr("base")}, Vulnerabilities: []Vulnerability{{Id: "id1"}}, } - cmdExec := new(mockCommandExec) - call := cmdExec.On("Execute", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Times(1) - call.RunFn = func(a mock.Arguments) { - resultBytes, err := json.Marshal(&expectedResults) - require.NoError(t, err) - _, err = a.Get(3).(io.Writer).Write(resultBytes) - require.NoError(t, err) + + s.executor.EXPECT().Execute(gomock.Any(), "snyk", gomock.Any(), gomock.Any()).Do(s.fakeWriter(expectedResults)).Return(nil).Times(1) + + sut := NewSnykScanner(s.executor, WithAuthProvider(dockerCfg)) + actualScanResult, actualErr := sut.Scan(context.Background(), "my.registry.com/image:test", dockercfg.Config{}) + s.NoError(actualErr) + s.Equal(expectedResults, *actualScanResult) +} + +func (s *SnykTestSuite) fakeWriter(content any) func(_ context.Context, _ string, _ []string, w io.Writer) { + return func(_ context.Context, _ string, _ []string, w io.Writer) { + resultBytes, err := json.Marshal(&content) + s.Require().NoError(err) + _, err = w.Write(resultBytes) + s.Require().NoError(err) } - sut := &snykScanner{commonAuths: dockerCfg, executor: cmdExec} - actualScanResult, actualErr := sut.Scan(context.Background(), "my.registry.com/image:", nil) - assert.NoError(t, actualErr) - assert.Equal(t, expectedResults, *actualScanResult) - cmdExec.AssertExpectations(t) } diff --git a/pkg/server/options.go b/pkg/server/options.go deleted file mode 100644 index 3682577..0000000 --- a/pkg/server/options.go +++ /dev/null @@ -1,40 +0,0 @@ -package server - -import "time" - -type ( - // Options for server - Options struct { - FullSyncCronSpec string `flag:"full-sync-cron-spec" cfg:"full_sync_cron_spec"` - AppNameExcludeList []string `flag:"app-name-exclude-list" cfg:"app_name_exclude_list"` - Workers uint `flag:"workers" cfg:"workers"` - PrettyPrint bool `flag:"pretty-print" cfg:"pretty_print" default:"false"` - LogLevel string `flag:"log-level" cfg:"log_level" default:"debug"` - DB DBOptions `cfg:",squash"` - Docker DockerOptions `cfg:",squash"` - Kube KubeOptions `cfg:",squash"` - VulnerabilityScan VulnerabilityScanOptions `cfg:",squash"` - } - - // VulnerabilityScanOptions - VulnerabilityScanOptions struct { - ScanTimeout time.Duration `flag:"vulnerability-scan-timeout" cfg:"vulnerability_scan_timeout"` - RescanAge time.Duration `flag:"vulnerability-rescan-age" cfg:"vulnerability_rescan_age"` - } - - // DBOptions contains configuration for database connection - DBOptions struct { - Server string `flag:"db-server" cfg:"db_server"` - Database string `flag:"db-database" cfg:"db_database"` - } - - // DockerOptions contains configuration for accessing docker images - DockerOptions struct { - AuthsFile string `flag:"docker-config-file" cfg:"docker_config_file"` - } - - // KubeOptions contains configuration for connecting to the Kubernetes API server - KubeOptions struct { - KubeConfigFile string `flag:"kube-config-file" cfg:"kube_config_file"` - } -) diff --git a/pkg/server/server.go b/pkg/server/server.go index eab2408..5cb4285 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -1,28 +1,20 @@ package server import ( - "fmt" "sync" - commongorm "github.com/equinor/radix-common/pkg/gorm" v1 "github.com/equinor/radix-operator/pkg/apis/radix/v1" radix "github.com/equinor/radix-operator/pkg/client/clientset/versioned" radixinformer "github.com/equinor/radix-operator/pkg/client/informers/externalversions" "github.com/equinor/radix-vulnerability-scanner/pkg/db" - "github.com/equinor/radix-vulnerability-scanner/pkg/dockercfg" "github.com/equinor/radix-vulnerability-scanner/pkg/handler" "github.com/equinor/radix-vulnerability-scanner/pkg/imageworker" "github.com/equinor/radix-vulnerability-scanner/pkg/observe" + "github.com/equinor/radix-vulnerability-scanner/pkg/options" "github.com/equinor/radix-vulnerability-scanner/pkg/scan" "github.com/equinor/radix-vulnerability-scanner/pkg/utils" - "github.com/microsoft/go-mssqldb/azuread" - "gorm.io/driver/sqlserver" - "gorm.io/gorm" - "gorm.io/gorm/schema" "k8s.io/apimachinery/pkg/labels" "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" - "k8s.io/client-go/tools/clientcmd" ) // Server sets up a pipeline of RadixDeployment observables, @@ -33,17 +25,13 @@ type Server struct { handler handler.Handler running bool runningMu sync.Mutex - opts *Options + opts *options.Options } // New creates a new Server -func New(opts *Options) (*Server, error) { - kubeClient, radixClient, err := getKubernetesClients(&opts.Kube) - if err != nil { - return nil, err - } +func New(kubeClient kubernetes.Interface, radixClient radix.Interface, scanner scan.Scanner, repo db.Repository, opts *options.Options) (*Server, error) { - imageHandler, err := getImageHandler(opts) + imageHandler, err := getImageHandler(scanner, repo, &opts.VulnerabilityScan) if err != nil { return nil, err } @@ -149,88 +137,17 @@ func (s *Server) run(stopCh <-chan struct{}) error { return nil } -func getRepository(opts *DBOptions) (db.Repository, error) { - - dsn := fmt.Sprintf("server=%s;database=%s;fedauth=ActiveDirectoryDefault", opts.Server, opts.Database) - dialector := sqlserver.New(sqlserver.Config{ - DriverName: azuread.DriverName, - DSN: dsn, - }) - - gormdb, err := gorm.Open(dialector, &gorm.Config{ - NamingStrategy: schema.NamingStrategy{NoLowerCase: true}, - Logger: commongorm.NewLogger(), - DisableAutomaticPing: false, - }) - if err != nil { - return nil, err - } - - return db.NewGormRepository(gormdb), nil -} - -func getKubernetesClients(opts *KubeOptions) (kubernetes.Interface, radix.Interface, error) { - var clientConfig *rest.Config - var err error - - if len(opts.KubeConfigFile) > 0 { - loadingRules := &clientcmd.ClientConfigLoadingRules{ExplicitPath: opts.KubeConfigFile} - loader := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, &clientcmd.ConfigOverrides{}) - clientConfig, err = loader.ClientConfig() - } else { - clientConfig, err = rest.InClusterConfig() - } - if err != nil { - return nil, nil, err - } - - kubeClient, err := kubernetes.NewForConfig(clientConfig) - if err != nil { - return nil, nil, err - } - - radixClient, err := radix.NewForConfig(clientConfig) - if err != nil { - return nil, nil, err - } - - return kubeClient, radixClient, nil -} - -func getImageHandler(opts *Options) (handler.Handler, error) { - scanner, err := getImageScanner(&opts.Docker) - if err != nil { - return nil, err - } - - repo, err := getRepository(&opts.DB) - if err != nil { - return nil, err - } +func getImageHandler(scanner scan.Scanner, repo db.Repository, opts *options.VulnerabilityScanOptions) (handler.Handler, error) { var scanOpts []handler.Option - if opts.VulnerabilityScan.ScanTimeout > 0 { - scanOpts = append(scanOpts, handler.WithScanTimeout(opts.VulnerabilityScan.ScanTimeout)) + if opts.ScanTimeout > 0 { + scanOpts = append(scanOpts, handler.WithScanTimeout(opts.ScanTimeout)) } - if opts.VulnerabilityScan.RescanAge > 0 { - scanOpts = append(scanOpts, handler.WithRescanAge(opts.VulnerabilityScan.RescanAge)) + if opts.RescanAge > 0 { + scanOpts = append(scanOpts, handler.WithRescanAge(opts.RescanAge)) } return handler.New(scanner, repo, scanOpts...), nil } - -func getImageScanner(opts *DockerOptions) (scan.Scanner, error) { - var dockerAuth dockercfg.DockerAuthConfig - var err error - - if opts.AuthsFile != "" { - dockerAuth, err = dockercfg.ReadDockerAuthConfigFromFile(opts.AuthsFile) - if err != nil { - return nil, err - } - } - - return scan.NewSnyk(dockerAuth), nil -} diff --git a/pkg/tokenstore/token_store.go b/pkg/tokenstore/token_store.go new file mode 100644 index 0000000..1c0bb26 --- /dev/null +++ b/pkg/tokenstore/token_store.go @@ -0,0 +1,65 @@ +package tokenstore + +import ( + "context" + "errors" + + "github.com/containerd/containerd/reference/docker" + "github.com/equinor/radix-vulnerability-scanner/pkg/registry" + "github.com/rs/zerolog/log" + "golang.org/x/oauth2" +) + +var ( + ErrRegistryNotFound = errors.New("registry is not found") +) + +type TokenStore struct { + tokens map[string]oauth2.TokenSource +} + +type SourceOption func(*TokenStore) + +func WithTokenSource(registry string, tokenSource oauth2.TokenSource) SourceOption { + return func(tokenStore *TokenStore) { + tokenStore.tokens[registry] = tokenSource + } +} + +type WithSource struct { + Source oauth2.TokenSource + Registry string +} + +func New(options ...SourceOption) *TokenStore { + store := &TokenStore{ + tokens: make(map[string]oauth2.TokenSource), + } + + for _, option := range options { + option(store) + } + + return store +} + +func (t *TokenStore) GetAuth(ctx context.Context, image string) (*registry.Auth, error) { + named, err := docker.ParseDockerRef(image) + if err != nil { + return nil, err + } + registryName := docker.Domain(named) + + log.Ctx(ctx).Debug().Str("Registry", registryName).Msg("Get token from Source") + + _, ok := t.tokens[registryName] + if !ok { + return nil, nil + } + + token, err := t.tokens[registryName].Token() + if err != nil { + return nil, err + } + return ®istry.Auth{Username: "00000000-0000-0000-0000-000000000000", Password: token.AccessToken}, nil +} diff --git a/pkg/tokenstore/token_store_test.go b/pkg/tokenstore/token_store_test.go new file mode 100644 index 0000000..cd2373e --- /dev/null +++ b/pkg/tokenstore/token_store_test.go @@ -0,0 +1,40 @@ +package tokenstore_test + +import ( + "context" + "testing" + + "github.com/equinor/radix-vulnerability-scanner/pkg/tokenstore" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/oauth2" +) + +// TokenSourceFunc is a interface adapter for oauth2.TokenSource +type FakeTokenSourceFunc func() (*oauth2.Token, error) + +func (s FakeTokenSourceFunc) Token() (*oauth2.Token, error) { + return s() +} + +func TestTokenStore(t *testing.T) { + sourceFunc := FakeTokenSourceFunc(func() (*oauth2.Token, error) { + return &oauth2.Token{ + AccessToken: "access-token", + }, nil + }) + store := tokenstore.New(tokenstore.WithTokenSource( + "someregistry.io", + sourceFunc, + )) + auth, err := store.GetAuth(context.Background(), "someregistry.io/someimage:sometag") + require.NoError(t, err) + assert.Equal(t, "00000000-0000-0000-0000-000000000000", auth.Username) + assert.Equal(t, "access-token", auth.Password) +} +func TestUnknownRegistryFails(t *testing.T) { + store := tokenstore.New() + auth, err := store.GetAuth(context.Background(), "someregistry.io/someimage:sometag") + assert.Nil(t, auth) + assert.Nil(t, err) +} diff --git a/pkg/tokenstore/tokensource/acr.go b/pkg/tokenstore/tokensource/acr.go new file mode 100644 index 0000000..62f47d1 --- /dev/null +++ b/pkg/tokenstore/tokensource/acr.go @@ -0,0 +1,141 @@ +package tokensource + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "sync" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" + "github.com/golang-jwt/jwt/v5" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "golang.org/x/oauth2" +) + +var ErrOnlyACRRegistriesAllowed = errors.New("forbidden registry. only .azurecr.io registries allowed") + +type AcrTokenSource struct { + credential azcore.TokenCredential + client *http.Client + registry string + logger *zerolog.Logger + ctx context.Context + mutex sync.Mutex +} + +type AcrOption func(*AcrTokenSource) + +func WithCredentialOption(cred azcore.TokenCredential) AcrOption { + return func(source *AcrTokenSource) { + source.credential = cred + } +} + +func WithHttpClient(client *http.Client) AcrOption { + return func(store *AcrTokenSource) { + store.client = client + } +} + +func NewACRTokenSource(ctx context.Context, registryName string, options ...AcrOption) oauth2.TokenSource { + // Make sure we never sends tokens other places than to Azure Container Registry + if !strings.HasSuffix(registryName, ".azurecr.io") { + panic(ErrOnlyACRRegistriesAllowed) + } + + source := &AcrTokenSource{ + registry: registryName, + logger: log.Ctx(ctx), + client: http.DefaultClient, + ctx: ctx, + } + + for _, option := range options { + option(source) + } + + return oauth2.ReuseTokenSource(nil, source) +} + +func (s *AcrTokenSource) Token() (*oauth2.Token, error) { + s.logger.Debug().Str("registry", s.registry).Msg("Fetching new ACR token") + s.mutex.Lock() + defer s.mutex.Unlock() + + acrScope := "https://containerregistry.azure.net/.default" + adToken, err := s.credential.GetToken(s.ctx, policy.TokenRequestOptions{Scopes: []string{acrScope}}) + if err != nil { + return nil, err + } + + acrToken, err := s.exchangeAdTokenWithAcrToken(adToken) + if err != nil { + return nil, err + } + + expirationTime, err := getTokenExpiration(acrToken) + if err != nil { + return nil, err + } + + return &oauth2.Token{ + AccessToken: acrToken, + TokenType: "", + RefreshToken: "", + Expiry: expirationTime, + }, err +} + +func (s *AcrTokenSource) exchangeAdTokenWithAcrToken(token azcore.AccessToken) (string, error) { + + formData := url.Values{} + formData.Set("grant_type", "access_token") + formData.Set("service", s.registry) + formData.Set("access_token", token.Token) + + exchangeUrl := fmt.Sprintf("https://%s/oauth2/exchange", s.registry) + res, err := s.client.PostForm(exchangeUrl, formData) + if err != nil { + return "", err + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + err = runtime.NewResponseError(res) + return "", err + } + + bytes, err := io.ReadAll(res.Body) + if err != nil { + return "", err + } + + var resToken oauth2.Token + err = json.Unmarshal(bytes, &resToken) + + // ACR Token exchange only returns a refresh token, we will use it as our Access token later on + return resToken.RefreshToken, err +} + +func getTokenExpiration(acrToken string) (time.Time, error) { + // Will be verified by ACR + token, _, err := jwt.NewParser().ParseUnverified(acrToken, &jwt.MapClaims{}) + if err != nil { + return time.Time{}, err + } + + expirationTime, err := token.Claims.GetExpirationTime() + if err != nil { + return time.Time{}, err + } + return expirationTime.Time, nil +} diff --git a/pkg/tokenstore/tokensource/acr_test.go b/pkg/tokenstore/tokensource/acr_test.go new file mode 100644 index 0000000..75a6132 --- /dev/null +++ b/pkg/tokenstore/tokensource/acr_test.go @@ -0,0 +1,96 @@ +package tokensource_test + +import ( + "bytes" + "context" + "fmt" + "io" + "net/http" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/fake" + "github.com/equinor/radix-vulnerability-scanner/pkg/tokenstore/tokensource" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type AcrTestSuite struct { + suite.Suite + ctrl *gomock.Controller +} + +func (s *AcrTestSuite) SetupSuite() { + s.ctrl = gomock.NewController(s.T()) +} +func Test_AcrSourceTestSuite(t *testing.T) { + suite.Run(t, new(AcrTestSuite)) +} + +// RoundTripFunc is an adapter to implement http.RoundTripper +type RoundTripFunc func(req *http.Request) *http.Response + +func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { + return f(req), nil +} + +func (s *AcrTestSuite) TestGetToken() { + fakeToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1MTYyNTkwMjN9.0SDkcr_pdPoY95pDEfzA1sV8RiuY7H69-GUQ9kRdLso" + registryName := "radix.azurecr.io" + + client := &http.Client{ + Transport: RoundTripFunc(func(r *http.Request) *http.Response { + body, err := io.ReadAll(r.Body) + s.Require().NoError(err) + + s.Assert().Equal("access_token=fake_token&grant_type=access_token&service=radix.azurecr.io", string(body)) + + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewBufferString(fmt.Sprintf(`{"refresh_token": "%s"}`, fakeToken))), + Header: make(http.Header), + } + }), + } + + source := tokensource.NewACRTokenSource( + context.Background(), registryName, + tokensource.WithHttpClient(client), + tokensource.WithCredentialOption(&fake.TokenCredential{}), + ) + + token, err := source.Token() + s.Require().NoError(err) + s.Assert().Equal(fakeToken, token.AccessToken) +} + +func (s *AcrTestSuite) TestGetTokenForbidden() { + + client := &http.Client{ + Transport: RoundTripFunc(func(r *http.Request) *http.Response { + return &http.Response{ + StatusCode: http.StatusForbidden, + Body: io.NopCloser(bytes.NewBufferString(`{"error": "forbidden"}`)), + Header: make(http.Header), + } + }), + } + + source := tokensource.NewACRTokenSource( + context.Background(), "radix.azurecr.io", + tokensource.WithHttpClient(client), + tokensource.WithCredentialOption(&fake.TokenCredential{}), + ) + + _, err := source.Token() + assert.Error(s.T(), err) +} + +func TestNonAzureFails(t *testing.T) { + defer func() { + err := recover().(error) + assert.ErrorIs(t, err, tokensource.ErrOnlyACRRegistriesAllowed) + }() + + tokensource.NewACRTokenSource(context.Background(), "radix.evil-cr.io") +} From d6a7fede79b4d0bb05ed0faffb3d1066b8957129 Mon Sep 17 00:00:00 2001 From: Richard Hagen Date: Mon, 15 Apr 2024 08:43:09 +0200 Subject: [PATCH 2/4] bump helm chart to release deploy WI feature (#59) --- charts/radix-vulnerability-scanner/Chart.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/charts/radix-vulnerability-scanner/Chart.yaml b/charts/radix-vulnerability-scanner/Chart.yaml index 46d4c74..903cbf0 100644 --- a/charts/radix-vulnerability-scanner/Chart.yaml +++ b/charts/radix-vulnerability-scanner/Chart.yaml @@ -1,5 +1,5 @@ apiVersion: v1 -appVersion: 1.0.0 -version: 1.0.0 +appVersion: 1.1.0 +version: 1.1.0 description: Scan images in RadixDeployments for vulnerabilities name: radix-vulnerability-scanner From db57bc970e2c9d670281528a28c573f1ea42116a Mon Sep 17 00:00:00 2001 From: Richard Hagen Date: Mon, 15 Apr 2024 11:04:06 +0200 Subject: [PATCH 3/4] Add log statements when auth is found, and pkg to all statements (#60) --- .env.template | 1 + charts/radix-vulnerability-scanner/Chart.yaml | 4 ++-- pkg/dockercfg/config.go | 4 +++- pkg/handler/handler.go | 8 ++++---- pkg/imageworker/worker.go | 8 ++++---- pkg/scan/snyk.go | 6 ++++-- pkg/tokenstore/token_store.go | 8 +------- pkg/tokenstore/tokensource/acr.go | 2 +- 8 files changed, 20 insertions(+), 21 deletions(-) diff --git a/.env.template b/.env.template index 732f072..8d93429 100644 --- a/.env.template +++ b/.env.template @@ -8,3 +8,4 @@ RVS_APP_NAME_EXCLUDE_LIST= RVS_WORKERS= RVS_DB_SERVER= RVS_DB_DATABASE= +RVS_WORKLOAD_IDENTITY_REGISTRIES=radixdev.azurecr.io diff --git a/charts/radix-vulnerability-scanner/Chart.yaml b/charts/radix-vulnerability-scanner/Chart.yaml index 903cbf0..6d50a41 100644 --- a/charts/radix-vulnerability-scanner/Chart.yaml +++ b/charts/radix-vulnerability-scanner/Chart.yaml @@ -1,5 +1,5 @@ apiVersion: v1 -appVersion: 1.1.0 -version: 1.1.0 +appVersion: 1.1.1 +version: 1.1.1 description: Scan images in RadixDeployments for vulnerabilities name: radix-vulnerability-scanner diff --git a/pkg/dockercfg/config.go b/pkg/dockercfg/config.go index 75535dc..cbaba02 100644 --- a/pkg/dockercfg/config.go +++ b/pkg/dockercfg/config.go @@ -8,6 +8,7 @@ import ( "github.com/containerd/containerd/reference/docker" "github.com/equinor/radix-vulnerability-scanner/pkg/registry" + "github.com/rs/zerolog/log" ) type Config struct { @@ -42,7 +43,7 @@ func NewFromBytes(contents []byte) (*Config, error) { return &cfgJSON, nil } -func (c Config) GetAuth(_ context.Context, image string) (*registry.Auth, error) { +func (c Config) GetAuth(ctx context.Context, image string) (*registry.Auth, error) { named, err := docker.ParseDockerRef(image) if err != nil { return nil, err @@ -51,6 +52,7 @@ func (c Config) GetAuth(_ context.Context, image string) (*registry.Auth, error) if len(c.Auths) > 0 { if auth, found := c.Auths[registryName]; found { + log.Ctx(ctx).Debug().Str("pkg", "dockercfg").Str("registry", registryName).Msg("found auth") return ®istry.Auth{Username: auth.Username, Password: auth.Password}, nil } } diff --git a/pkg/handler/handler.go b/pkg/handler/handler.go index 0c5da6e..83e1457 100644 --- a/pkg/handler/handler.go +++ b/pkg/handler/handler.go @@ -75,16 +75,16 @@ func (s *imageVulnerabilityScanner) Handle(ctx context.Context, imageName string if skipScan, err := s.isLastScanWithinRescanThreshold(ctx, imageName); err != nil { return err } else if skipScan { - log.Info().Str("image", imageName).Msgf("skipping scan of image, recently scanned") + log.Info().Str("pkg", "handler").Str("image", imageName).Msgf("skipping scan of image, recently scanned") return nil } - log.Info().Str("image", imageName).Msgf("scanning image") + log.Info().Str("pkg", "handler").Str("image", imageName).Msgf("scanning image") scanCtx, cancel := context.WithTimeout(ctx, s.scanTimeout) defer cancel() scanResult, err := s.scanner.Scan(scanCtx, imageName, dockerConfig) if err != nil { - log.Warn().Str("image", imageName).Err(err).Msgf("error scanning image") + log.Warn().Str("pkg", "handler").Str("image", imageName).Err(err).Msgf("error scanning image") } scanSuccess := err == nil vulnerabilitiesBulk := []db.VulnerabilityBulkDto{} @@ -133,7 +133,7 @@ func (s *imageVulnerabilityScanner) Handle(ctx context.Context, imageName string dbCtx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() - log.Info().Str("image", imageName).Msgf("storing scan results for image") + log.Info().Str("pkg", "handler").Str("image", imageName).Msgf("storing scan results for image") return s.repository.RegisterImageScan(dbCtx, imageName, baseImage, time.Now(), scanSuccess, vulnerabilitiesBulk, identifiersBulk, referencesBulk) } diff --git a/pkg/imageworker/worker.go b/pkg/imageworker/worker.go index 7dd8d6f..3aeeab3 100644 --- a/pkg/imageworker/worker.go +++ b/pkg/imageworker/worker.go @@ -43,7 +43,7 @@ func New(handler handler.Handler) *Worker { // Receive implementation of Observer func (w *Worker) Receive(obj observe.ImageInfo) { - log.Info().Str("image", obj.ImageName).Msg("enqueuing image") + log.Info().Str("pkg", "imageworker").Str("image", obj.ImageName).Msg("enqueuing image") w.queue.Add(&obj) } @@ -93,16 +93,16 @@ func (w *Worker) processItem(ctx context.Context, item any) { defer w.queue.Done(item) if image, ok := item.(*observe.ImageInfo); ok { - log.Info().Str("image", image.ImageName).Msg("processing image") + log.Info().Str("pkg", "imageworker").Str("image", image.ImageName).Msg("processing image") if err := w.handler.Handle(ctx, image.ImageName, image.DockerConfig); err != nil { requeues := w.queue.NumRequeues(image) if requeues < maxNumberOfRequeues { - log.Info().Str("image", image.ImageName).Err(err).Msgf("requeuing scan of image (attempt %d of %d) due to error", requeues+1, maxNumberOfRequeues) + log.Info().Str("pkg", "imageworker").Str("image", image.ImageName).Err(err).Msgf("requeuing scan of image (attempt %d of %d) due to error", requeues+1, maxNumberOfRequeues) w.queue.AddRateLimited(item) return } else { w.queue.Forget(item) - log.Error().Str("image", image.ImageName).Err(err).Msgf("scan failed for image after %d retries", requeues) + log.Error().Str("pkg", "imageworker").Str("image", image.ImageName).Err(err).Msgf("scan failed for image after %d retries", requeues) return } } diff --git a/pkg/scan/snyk.go b/pkg/scan/snyk.go index fe38424..ab93847 100644 --- a/pkg/scan/snyk.go +++ b/pkg/scan/snyk.go @@ -64,13 +64,14 @@ func (s *SnykScanner) Scan(ctx context.Context, image string, dockerConfig docke return nil } + log.Ctx(ctx).Debug().Str("pkg", "scan").Str("image", image).Msg("scanning image") testArgs := []string{"container", "test", "--json", image} var testArgsWithCreds []string testArgsWithCreds = append(testArgsWithCreds, testArgs...) testArgsWithCreds = append(testArgsWithCreds, credArgs...) buf := &bytes.Buffer{} err := scanFn(ctx, testArgsWithCreds, buf) - log.Trace().Stringer("result", buf).Strs("args", testArgsWithCreds).Err(err).Msg("scan completed") + log.Trace().Str("pkg", "scan").Stringer("result", buf).Strs("args", testArgsWithCreds).Err(err).Msg("scan completed") if err != nil { if len(credArgs) == 0 { @@ -84,9 +85,10 @@ func (s *SnykScanner) Scan(ctx context.Context, image string, dockerConfig docke // parameter contains invalid credentials for docker.io. Even if redis:latest is public, the invalid credentials // from the `auths` parameter causes the scan to fail. We'll therefore try to do a second scan // without supplying credential arguments + log.Ctx(ctx).Debug().Str("pkg", "scan").Str("image", image).Msg("scanning image again without creds") buf = &bytes.Buffer{} err = scanFn(ctx, testArgs, buf) - log.Trace().Stringer("result", buf).Strs("args", testArgsWithCreds).Err(err).Msg("retry scan completed") + log.Trace().Str("pkg", "scan").Stringer("result", buf).Strs("args", testArgsWithCreds).Err(err).Msg("retry scan completed") if err != nil { return nil, err } diff --git a/pkg/tokenstore/token_store.go b/pkg/tokenstore/token_store.go index 1c0bb26..1f1592d 100644 --- a/pkg/tokenstore/token_store.go +++ b/pkg/tokenstore/token_store.go @@ -2,7 +2,6 @@ package tokenstore import ( "context" - "errors" "github.com/containerd/containerd/reference/docker" "github.com/equinor/radix-vulnerability-scanner/pkg/registry" @@ -10,10 +9,6 @@ import ( "golang.org/x/oauth2" ) -var ( - ErrRegistryNotFound = errors.New("registry is not found") -) - type TokenStore struct { tokens map[string]oauth2.TokenSource } @@ -50,8 +45,6 @@ func (t *TokenStore) GetAuth(ctx context.Context, image string) (*registry.Auth, } registryName := docker.Domain(named) - log.Ctx(ctx).Debug().Str("Registry", registryName).Msg("Get token from Source") - _, ok := t.tokens[registryName] if !ok { return nil, nil @@ -61,5 +54,6 @@ func (t *TokenStore) GetAuth(ctx context.Context, image string) (*registry.Auth, if err != nil { return nil, err } + log.Ctx(ctx).Debug().Str("pkg", "tokenstore").Str("registry", registryName).Msg("found auth") return ®istry.Auth{Username: "00000000-0000-0000-0000-000000000000", Password: token.AccessToken}, nil } diff --git a/pkg/tokenstore/tokensource/acr.go b/pkg/tokenstore/tokensource/acr.go index 62f47d1..96d5ded 100644 --- a/pkg/tokenstore/tokensource/acr.go +++ b/pkg/tokenstore/tokensource/acr.go @@ -67,7 +67,7 @@ func NewACRTokenSource(ctx context.Context, registryName string, options ...AcrO } func (s *AcrTokenSource) Token() (*oauth2.Token, error) { - s.logger.Debug().Str("registry", s.registry).Msg("Fetching new ACR token") + s.logger.Debug().Str("pkg", "tokensource").Str("registry", s.registry).Msg("fetching new ACR token") s.mutex.Lock() defer s.mutex.Unlock() From 64e720368b61161a624365d919883792dc7c7994 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Gustav=20Str=C3=A5b=C3=B8?= Date: Mon, 15 Apr 2024 12:55:22 +0200 Subject: [PATCH 4/4] fix incorrect volumeMount indentation in chart --- charts/radix-vulnerability-scanner/Chart.yaml | 4 ++-- charts/radix-vulnerability-scanner/templates/deployment.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/charts/radix-vulnerability-scanner/Chart.yaml b/charts/radix-vulnerability-scanner/Chart.yaml index 6d50a41..aa25dcf 100644 --- a/charts/radix-vulnerability-scanner/Chart.yaml +++ b/charts/radix-vulnerability-scanner/Chart.yaml @@ -1,5 +1,5 @@ apiVersion: v1 -appVersion: 1.1.1 -version: 1.1.1 +appVersion: 1.1.2 +version: 1.1.2 description: Scan images in RadixDeployments for vulnerabilities name: radix-vulnerability-scanner diff --git a/charts/radix-vulnerability-scanner/templates/deployment.yaml b/charts/radix-vulnerability-scanner/templates/deployment.yaml index 49dd9e4..7792244 100644 --- a/charts/radix-vulnerability-scanner/templates/deployment.yaml +++ b/charts/radix-vulnerability-scanner/templates/deployment.yaml @@ -81,7 +81,7 @@ spec: mountPath: {{ template "vulnerability-scan.dockerConfigFilePath" . }} {{- end }} {{- with .Values.extraVolumeMounts }} - {{- toYaml . | nindent 14 }} + {{- toYaml . | nindent 12 }} {{- end }} {{- with .Values.resources }} resources: