From b625541b151538ac8a408c59a0da8a051f34ac5b Mon Sep 17 00:00:00 2001 From: Sertac Ozercan Date: Thu, 12 Jun 2025 04:30:52 +0000 Subject: [PATCH 1/7] fix: check if trivy exist Signed-off-by: Sertac Ozercan --- Dockerfile | 2 +- docs/docs/trivy.md | 2 +- pkg/scanners/trivy/types.go | 29 +++++- pkg/scanners/trivy/types_test.go | 162 ++++++++++++++++++++++++++++++- 4 files changed, 188 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9685f651fd..5f4ca88c51 100644 --- a/Dockerfile +++ b/Dockerfile @@ -66,7 +66,7 @@ ENTRYPOINT ["/remover"] FROM --platform=$TARGETPLATFORM gcr.io/distroless/static:latest as trivy-scanner COPY --from=trivy-scanner-build /workspace/out/trivy-scanner / -COPY --from=trivy-binary /usr/local/bin/trivy / +COPY --from=trivy-binary /usr/local/bin/trivy /trivy WORKDIR /var/lib/trivy ENTRYPOINT ["/trivy-scanner"] diff --git a/docs/docs/trivy.md b/docs/docs/trivy.md index d19e9777db..5421605c93 100644 --- a/docs/docs/trivy.md +++ b/docs/docs/trivy.md @@ -3,4 +3,4 @@ title: Trivy --- ## Trivy Provider Options -The Trivy provider is used in Eraser for image scanning and detecting vulnerabilities. See [Customization](https://eraser-dev.github.io/eraser/docs/customization#scanner-options) for more details on configuring the scanner. +The Trivy provider is used in Eraser for image scanning and detecting vulnerabilities. The scanner will first look for the trivy executable at `/trivy` (the default path in the container), and if not found, will fall back to searching for `trivy` in the system PATH. See [Customization](https://eraser-dev.github.io/eraser/docs/customization#scanner-options) for more details on configuring the scanner. diff --git a/pkg/scanners/trivy/types.go b/pkg/scanners/trivy/types.go index 61472484a1..9fd7ebb64b 100644 --- a/pkg/scanners/trivy/types.go +++ b/pkg/scanners/trivy/types.go @@ -14,6 +14,10 @@ import ( "github.com/eraser-dev/eraser/pkg/utils" ) +// currentExecutingLookPath is a variable that points to exec.LookPath by default, +// but can be overridden for testing purposes +var currentExecutingLookPath = exec.LookPath + const ( StatusFailed ScanStatus = iota StatusNonCompliant @@ -172,12 +176,33 @@ type ImageScanner struct { timer *time.Timer } +func (s *ImageScanner) findTrivyExecutable() (string, error) { + // First, check if trivy exists at the hardcoded path + if _, err := os.Stat(trivyCommandName); err == nil { + return trivyCommandName, nil + } + + // If not found at hardcoded path, try to find it in PATH + path, err := currentExecutingLookPath("trivy") + if err != nil { + return "", fmt.Errorf("trivy executable not found at %s and not found in PATH: %w", trivyCommandName, err) + } + + return path, nil +} + func (s *ImageScanner) Scan(img unversioned.Image) (ScanStatus, error) { refs := make([]string, 0, len(img.Names)+len(img.Digests)) refs = append(refs, img.Digests...) refs = append(refs, img.Names...) scanSucceeded := false + // Find trivy executable path + trivyPath, err := s.findTrivyExecutable() + if err != nil { + return StatusFailed, err + } + log.Info("scanning image with id", "imageID", img.ImageID, "refs", refs) for i := 0; i < len(refs) && !scanSucceeded; i++ { log.Info("scanning image with ref", "ref", refs[i]) @@ -186,13 +211,13 @@ func (s *ImageScanner) Scan(img unversioned.Image) (ScanStatus, error) { stderr := new(bytes.Buffer) cliArgs := s.config.cliArgs(refs[i]) - cmd := exec.Command(trivyCommandName, cliArgs...) + cmd := exec.Command(trivyPath, cliArgs...) cmd.Stdout = stdout cmd.Stderr = stderr cmd.Env = append(cmd.Env, os.Environ()...) cmd.Env = setRuntimeSocketEnvVars(cmd, s.config.Runtime) - log.V(1).Info("scanning image ref", "ref", refs[i], "cli_invocation", fmt.Sprintf("%s %s", trivyCommandName, strings.Join(cliArgs, " ")), "env", cmd.Env) + log.V(1).Info("scanning image ref", "ref", refs[i], "cli_invocation", fmt.Sprintf("%s %s", trivyPath, strings.Join(cliArgs, " ")), "env", cmd.Env) if err := cmd.Run(); err != nil { log.Error(err, "error scanning image", "imageID", img.ImageID, "reference", refs[i], "stderr", stderr.String()) continue diff --git a/pkg/scanners/trivy/types_test.go b/pkg/scanners/trivy/types_test.go index 335ac7bcfb..daa4c1a5c7 100644 --- a/pkg/scanners/trivy/types_test.go +++ b/pkg/scanners/trivy/types_test.go @@ -4,16 +4,18 @@ import ( "strings" "testing" + "errors" + "fmt" + "github.com/eraser-dev/eraser/api/unversioned" + + "github.com/stretchr/testify/assert" ) const ref = "image:tag" var testDuration = unversioned.Duration(100000000000) -func init() { -} - func TestCLIArgs(t *testing.T) { type testCell struct { desc string @@ -168,3 +170,157 @@ func TestCLIArgs(t *testing.T) { }) } } + +// TestImageScanner_findTrivyExecutable tests the findTrivyExecutable method in isolation. +func TestImageScanner_findTrivyExecutable(t *testing.T) { + // Store original function to restore after tests + originalLookPath := currentExecutingLookPath + defer func() { currentExecutingLookPath = originalLookPath }() + + scanner := &ImageScanner{} + + testCases := []struct { + name string + lookPathSetup func() + expectedPath string + expectedError bool + expectedErrorMatch string + }{ + { + name: "Trivy found in PATH only", + lookPathSetup: func() { + currentExecutingLookPath = func(file string) (string, error) { + if file == "trivy" { + return "/usr/bin/trivy", nil + } + return "", errors.New("not found") + } + }, + expectedPath: "/usr/bin/trivy", + expectedError: false, + }, + { + name: "Trivy not found anywhere", + lookPathSetup: func() { + currentExecutingLookPath = func(file string) (string, error) { + return "", errors.New("executable file not found in $PATH") + } + }, + expectedPath: "", + expectedError: true, + expectedErrorMatch: "trivy executable not found at /trivy and not found in PATH", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tc.lookPathSetup() + + path, err := scanner.findTrivyExecutable() + + if tc.expectedError { + assert.Error(t, err) + assert.Contains(t, err.Error(), tc.expectedErrorMatch) + assert.Empty(t, path) + } else { + assert.NoError(t, err) + assert.Equal(t, tc.expectedPath, path) + } + }) + } +} + +// TestImageScanner_Scan_TrivyPathLookup tests the logic for finding the trivy executable. +func TestImageScanner_Scan_TrivyPathLookup(t *testing.T) { + // Store original function to restore after tests + originalLookPath := currentExecutingLookPath + defer func() { currentExecutingLookPath = originalLookPath }() + + // Base configuration for the scanner + baseConfig := DefaultConfig() + scanner := &ImageScanner{ + config: *baseConfig, + } + // Dummy image for testing + img := unversioned.Image{ImageID: "test-image-id", Names: []string{"test-image:latest"}} + + // Expected error message prefix when trivy is not found + expectedNotFoundErrorMsgPrefix := fmt.Sprintf("trivy executable not found at %s", trivyCommandName) + + testCases := []struct { + name string + lookPathSetup func() // Sets up the mock for exec.LookPath + expectedStatus ScanStatus + expectNotFoundError bool // True if we expect the specific "trivy not found by LookPath" error + expectedErrorMsgContains string // The prefix for the "not found" error message + }{ + { + name: "Trivy found at hardcoded path /trivy", + lookPathSetup: func() { + currentExecutingLookPath = func(file string) (string, error) { + if file == "trivy" { + return "/usr/bin/trivy", nil // Found in PATH + } + return originalLookPath(file) // Fallback for any other calls + } + }, + // Scan will likely still fail due to inability to run actual scan in test, + // but it should not be the "trivy not found by LookPath" error. + expectedStatus: StatusFailed, + expectNotFoundError: false, + }, + { + name: "Trivy found in $PATH, not at /trivy", + lookPathSetup: func() { + currentExecutingLookPath = func(file string) (string, error) { + if file == "trivy" { + return "/usr/bin/trivy", nil // Found in $PATH + } + return originalLookPath(file) + } + }, + expectedStatus: StatusFailed, // Similar to above, subsequent scan steps will fail. + expectNotFoundError: false, + }, + { + name: "Trivy not found anywhere", + lookPathSetup: func() { + currentExecutingLookPath = func(file string) (string, error) { + if file == "trivy" { + return "", errors.New("mock: trivy not in $PATH") + } + return originalLookPath(file) + } + }, + expectedStatus: StatusFailed, + expectNotFoundError: true, + expectedErrorMsgContains: expectedNotFoundErrorMsgPrefix, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tc.lookPathSetup() + + status, err := scanner.Scan(img) + + if tc.expectNotFoundError { + assert.Error(t, err, "Expected an error when trivy is not found") + if err != nil { // Check prefix only if error is not nil + assert.True(t, strings.HasPrefix(err.Error(), tc.expectedErrorMsgContains), + "Error message should start with '%s'. Got: %s", tc.expectedErrorMsgContains, err.Error()) + } + assert.Equal(t, tc.expectedStatus, status, "ScanStatus should be StatusFailed") + } else { + // If trivy was "found" by LookPath, any error should be from subsequent operations (e.g., cmd.Run, JSON unmarshal), + // not the specific "trivy executable not found by LookPath..." error. + if err != nil { + assert.False(t, strings.HasPrefix(err.Error(), expectedNotFoundErrorMsgPrefix), + "Error should not be the 'trivy not found by LookPath' error. Got: %s", err.Error()) + } + // The status might still be StatusFailed due to these subsequent errors, + // which is acceptable for this test's focus on path lookup. + } + }) + } +} From 8da96c7e906842e38c7068c48bd01f5140968633 Mon Sep 17 00:00:00 2001 From: Sertac Ozercan Date: Thu, 12 Jun 2025 05:22:39 +0000 Subject: [PATCH 2/7] lint Signed-off-by: Sertac Ozercan --- .golangci.yaml | 1 - build/tooling/Dockerfile | 2 +- go.mod | 14 ++++++----- go.sum | 40 ++++++++++++++++++++++---------- pkg/scanners/trivy/types.go | 2 +- pkg/scanners/trivy/types_test.go | 37 +++++++++++++++-------------- 6 files changed, 57 insertions(+), 39 deletions(-) diff --git a/.golangci.yaml b/.golangci.yaml index 2d7cd3eaca..2d38ffc6cf 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -20,7 +20,6 @@ linters: disable-all: true enable: - errcheck - - exportloopref - forcetypeassert - gocritic - goconst diff --git a/build/tooling/Dockerfile b/build/tooling/Dockerfile index 175c4e9925..310185f8af 100644 --- a/build/tooling/Dockerfile +++ b/build/tooling/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.23-bullseye@sha256:80eb3147ef40b58d527d9c2e634b1a79a750aee09de6f973844db38b33f0550b +FROM golang:1.24-bullseye@sha256:dfd72198d14bc22f270c9e000c304a2ffd19f5a5f693fad82643311afdc6b568 RUN GO111MODULE=on go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.14.0 RUN GO111MODULE=on go install k8s.io/code-generator/cmd/conversion-gen@v0.29.0 diff --git a/go.mod b/go.mod index eb07c13396..cc7f948e93 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module github.com/eraser-dev/eraser -go 1.18 +go 1.23.0 + +toolchain go1.24.3 require ( github.com/aquasecurity/trivy v0.35.0 @@ -16,7 +18,7 @@ require ( go.opentelemetry.io/otel/sdk/metric v0.34.0 go.uber.org/zap v1.24.0 golang.org/x/exp v0.0.0-20230321023759-10a507213a29 - golang.org/x/sys v0.18.0 + golang.org/x/sys v0.33.0 google.golang.org/grpc v1.58.3 k8s.io/api v0.26.11 k8s.io/apimachinery v0.26.11 @@ -110,11 +112,11 @@ require ( go.uber.org/atomic v1.10.0 // indirect go.uber.org/goleak v1.2.1 // indirect go.uber.org/multierr v1.9.0 // indirect - golang.org/x/net v0.23.0 // indirect + golang.org/x/net v0.41.0 // indirect golang.org/x/oauth2 v0.10.0 // indirect - golang.org/x/sync v0.3.0 // indirect - golang.org/x/term v0.18.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/sync v0.15.0 // indirect + golang.org/x/term v0.32.0 // indirect + golang.org/x/text v0.26.0 // indirect golang.org/x/time v0.3.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect diff --git a/go.sum b/go.sum index 6b811a5736..c9c6fba78d 100644 --- a/go.sum +++ b/go.sum @@ -617,6 +617,7 @@ github.com/Microsoft/go-winio v0.4.17-0.20210211115548-6eac466e5fa3/go.mod h1:JP github.com/Microsoft/go-winio v0.4.17-0.20210324224401-5516f17a5958/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/Microsoft/go-winio v0.4.17/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= +github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= github.com/Microsoft/hcsshim v0.8.7-0.20190325164909-8abdbb8205e4/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= github.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEYNSzOYtcBQ= @@ -687,6 +688,7 @@ github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70= +github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd h1:rFt+Y/IK1aEZkEHchZRSq9OQbsSzIT/OrI8YFFmRIng= github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= @@ -752,6 +754,7 @@ github.com/containerd/cgroups v0.0.0-20200824123100-0b889c03f102/go.mod h1:s5q4S github.com/containerd/cgroups v0.0.0-20210114181951-8a68de567b68/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE= github.com/containerd/cgroups v1.0.1/go.mod h1:0SJrPIenamHDcZhEcJMNBB85rHcUsw4f25ZfBiPYRkU= github.com/containerd/cgroups v1.0.4 h1:jN/mbWBEaz+T1pi5OFtnkQ+8qnmEbAr1Oo1FRm5B0dA= +github.com/containerd/cgroups v1.0.4/go.mod h1:nLNQtsF7Sl2HxNebu77i1R0oDlhiTG+kO4JTrUzo6IA= github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= github.com/containerd/console v0.0.0-20181022165439-0650fd9eeb50/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= github.com/containerd/console v0.0.0-20191206165004-02ecf6a7291e/go.mod h1:8Pf4gM6VEbTNRIT26AyyU7hxdQU3MvAvxVI0sc00XBE= @@ -848,6 +851,7 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ= github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s= @@ -917,6 +921,7 @@ github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6Ni github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= +github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= @@ -1034,6 +1039,7 @@ github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomodule/redigo v1.8.2 h1:H5XSIre1MB5NbPYFp+i1NBbb5qN1W8Y8YAQoAYbkm8k= +github.com/gomodule/redigo v1.8.2/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= @@ -1112,6 +1118,7 @@ github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= +github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= @@ -1145,6 +1152,7 @@ github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= @@ -1210,6 +1218,7 @@ github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -1262,6 +1271,7 @@ github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0Gq github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= +github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= github.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGqsZeMYowQ= github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= github.com/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA= @@ -1422,6 +1432,7 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= @@ -1630,7 +1641,8 @@ golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1692,8 +1704,9 @@ golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1773,8 +1786,8 @@ golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1822,8 +1835,9 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1956,8 +1970,8 @@ golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1970,8 +1984,8 @@ golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= -golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= -golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1990,8 +2004,8 @@ golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -2076,8 +2090,9 @@ golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= -golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss= golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= +golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= +golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -2423,6 +2438,7 @@ gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81 gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= +gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/pkg/scanners/trivy/types.go b/pkg/scanners/trivy/types.go index 9fd7ebb64b..322d84f94f 100644 --- a/pkg/scanners/trivy/types.go +++ b/pkg/scanners/trivy/types.go @@ -15,7 +15,7 @@ import ( ) // currentExecutingLookPath is a variable that points to exec.LookPath by default, -// but can be overridden for testing purposes +// but can be overridden for testing purposes. var currentExecutingLookPath = exec.LookPath const ( diff --git a/pkg/scanners/trivy/types_test.go b/pkg/scanners/trivy/types_test.go index daa4c1a5c7..6fd668fa49 100644 --- a/pkg/scanners/trivy/types_test.go +++ b/pkg/scanners/trivy/types_test.go @@ -1,18 +1,21 @@ package main import ( - "strings" - "testing" - "errors" "fmt" + "strings" + "testing" "github.com/eraser-dev/eraser/api/unversioned" "github.com/stretchr/testify/assert" ) -const ref = "image:tag" +const ( + ref = "image:tag" + trivyExecutableName = "trivy" + trivyPathBin = "/usr/bin/trivy" +) var testDuration = unversioned.Duration(100000000000) @@ -190,19 +193,19 @@ func TestImageScanner_findTrivyExecutable(t *testing.T) { name: "Trivy found in PATH only", lookPathSetup: func() { currentExecutingLookPath = func(file string) (string, error) { - if file == "trivy" { - return "/usr/bin/trivy", nil + if file == trivyExecutableName { + return trivyPathBin, nil } return "", errors.New("not found") } }, - expectedPath: "/usr/bin/trivy", + expectedPath: trivyPathBin, expectedError: false, }, { name: "Trivy not found anywhere", lookPathSetup: func() { - currentExecutingLookPath = func(file string) (string, error) { + currentExecutingLookPath = func(_ string) (string, error) { return "", errors.New("executable file not found in $PATH") } }, @@ -258,8 +261,8 @@ func TestImageScanner_Scan_TrivyPathLookup(t *testing.T) { name: "Trivy found at hardcoded path /trivy", lookPathSetup: func() { currentExecutingLookPath = func(file string) (string, error) { - if file == "trivy" { - return "/usr/bin/trivy", nil // Found in PATH + if file == trivyExecutableName { + return trivyPathBin, nil // Found in PATH } return originalLookPath(file) // Fallback for any other calls } @@ -273,8 +276,8 @@ func TestImageScanner_Scan_TrivyPathLookup(t *testing.T) { name: "Trivy found in $PATH, not at /trivy", lookPathSetup: func() { currentExecutingLookPath = func(file string) (string, error) { - if file == "trivy" { - return "/usr/bin/trivy", nil // Found in $PATH + if file == trivyExecutableName { + return trivyPathBin, nil // Found in $PATH } return originalLookPath(file) } @@ -286,7 +289,7 @@ func TestImageScanner_Scan_TrivyPathLookup(t *testing.T) { name: "Trivy not found anywhere", lookPathSetup: func() { currentExecutingLookPath = func(file string) (string, error) { - if file == "trivy" { + if file == trivyExecutableName { return "", errors.New("mock: trivy not in $PATH") } return originalLookPath(file) @@ -311,13 +314,11 @@ func TestImageScanner_Scan_TrivyPathLookup(t *testing.T) { "Error message should start with '%s'. Got: %s", tc.expectedErrorMsgContains, err.Error()) } assert.Equal(t, tc.expectedStatus, status, "ScanStatus should be StatusFailed") - } else { + } else if err != nil { // If trivy was "found" by LookPath, any error should be from subsequent operations (e.g., cmd.Run, JSON unmarshal), // not the specific "trivy executable not found by LookPath..." error. - if err != nil { - assert.False(t, strings.HasPrefix(err.Error(), expectedNotFoundErrorMsgPrefix), - "Error should not be the 'trivy not found by LookPath' error. Got: %s", err.Error()) - } + assert.False(t, strings.HasPrefix(err.Error(), expectedNotFoundErrorMsgPrefix), + "Error should not be the 'trivy not found by LookPath' error. Got: %s", err.Error()) // The status might still be StatusFailed due to these subsequent errors, // which is acceptable for this test's focus on path lookup. } From 15b56994a09703d5a06102cb66517040da976f9c Mon Sep 17 00:00:00 2001 From: Sertac Ozercan Date: Thu, 12 Jun 2025 17:27:57 +0000 Subject: [PATCH 3/7] move to initscanner Signed-off-by: Sertac Ozercan --- pkg/scanners/trivy/trivy.go | 26 ++++++++- pkg/scanners/trivy/types.go | 30 ++-------- pkg/scanners/trivy/types_test.go | 98 ++++++++------------------------ 3 files changed, 52 insertions(+), 102 deletions(-) diff --git a/pkg/scanners/trivy/trivy.go b/pkg/scanners/trivy/trivy.go index 52a2ff492a..b1100fd643 100644 --- a/pkg/scanners/trivy/trivy.go +++ b/pkg/scanners/trivy/trivy.go @@ -147,6 +147,21 @@ func runProfileServer() { log.Error(err, "pprof server failed") } +func findTrivyExecutable() (string, error) { + // First, check if trivy exists at the hardcoded path + if _, err := os.Stat(trivyCommandName); err == nil { + return trivyCommandName, nil + } + + // If not found at hardcoded path, try to find it in PATH + path, err := currentExecutingLookPath("trivy") + if err != nil { + return "", fmt.Errorf("trivy executable not found at %s and not found in PATH: %w", trivyCommandName, err) + } + + return path, nil +} + func initScanner(userConfig *Config) (Scanner, error) { if userConfig == nil { return nil, fmt.Errorf("invalid trivy scanner config") @@ -165,12 +180,19 @@ func initScanner(userConfig *Config) (Scanner, error) { Address: utils.CRIPath, } + // Find trivy executable path during initialization + trivyPath, err := findTrivyExecutable() + if err != nil { + return nil, err + } + totalTimeout := time.Duration(userConfig.Timeout.Total) timer := time.NewTimer(totalTimeout) var s Scanner = &ImageScanner{ - config: *userConfig, - timer: timer, + config: *userConfig, + timer: timer, + trivyPath: trivyPath, } return s, nil } diff --git a/pkg/scanners/trivy/types.go b/pkg/scanners/trivy/types.go index 322d84f94f..4bd7e0650b 100644 --- a/pkg/scanners/trivy/types.go +++ b/pkg/scanners/trivy/types.go @@ -172,23 +172,9 @@ func (c *Config) getRuntimeVar() (string, error) { } type ImageScanner struct { - config Config - timer *time.Timer -} - -func (s *ImageScanner) findTrivyExecutable() (string, error) { - // First, check if trivy exists at the hardcoded path - if _, err := os.Stat(trivyCommandName); err == nil { - return trivyCommandName, nil - } - - // If not found at hardcoded path, try to find it in PATH - path, err := currentExecutingLookPath("trivy") - if err != nil { - return "", fmt.Errorf("trivy executable not found at %s and not found in PATH: %w", trivyCommandName, err) - } - - return path, nil + config Config + timer *time.Timer + trivyPath string } func (s *ImageScanner) Scan(img unversioned.Image) (ScanStatus, error) { @@ -197,12 +183,6 @@ func (s *ImageScanner) Scan(img unversioned.Image) (ScanStatus, error) { refs = append(refs, img.Names...) scanSucceeded := false - // Find trivy executable path - trivyPath, err := s.findTrivyExecutable() - if err != nil { - return StatusFailed, err - } - log.Info("scanning image with id", "imageID", img.ImageID, "refs", refs) for i := 0; i < len(refs) && !scanSucceeded; i++ { log.Info("scanning image with ref", "ref", refs[i]) @@ -211,13 +191,13 @@ func (s *ImageScanner) Scan(img unversioned.Image) (ScanStatus, error) { stderr := new(bytes.Buffer) cliArgs := s.config.cliArgs(refs[i]) - cmd := exec.Command(trivyPath, cliArgs...) + cmd := exec.Command(s.trivyPath, cliArgs...) // nolint:gosec // G204: Subprocess launched with variable cmd.Stdout = stdout cmd.Stderr = stderr cmd.Env = append(cmd.Env, os.Environ()...) cmd.Env = setRuntimeSocketEnvVars(cmd, s.config.Runtime) - log.V(1).Info("scanning image ref", "ref", refs[i], "cli_invocation", fmt.Sprintf("%s %s", trivyPath, strings.Join(cliArgs, " ")), "env", cmd.Env) + log.V(1).Info("scanning image ref", "ref", refs[i], "cli_invocation", fmt.Sprintf("%s %s", s.trivyPath, strings.Join(cliArgs, " ")), "env", cmd.Env) if err := cmd.Run(); err != nil { log.Error(err, "error scanning image", "imageID", img.ImageID, "reference", refs[i], "stderr", stderr.String()) continue diff --git a/pkg/scanners/trivy/types_test.go b/pkg/scanners/trivy/types_test.go index 6fd668fa49..3147c1ce11 100644 --- a/pkg/scanners/trivy/types_test.go +++ b/pkg/scanners/trivy/types_test.go @@ -2,7 +2,6 @@ package main import ( "errors" - "fmt" "strings" "testing" @@ -174,14 +173,12 @@ func TestCLIArgs(t *testing.T) { } } -// TestImageScanner_findTrivyExecutable tests the findTrivyExecutable method in isolation. -func TestImageScanner_findTrivyExecutable(t *testing.T) { +// TestFindTrivyExecutable tests the findTrivyExecutable function in isolation. +func TestFindTrivyExecutable(t *testing.T) { // Store original function to restore after tests originalLookPath := currentExecutingLookPath defer func() { currentExecutingLookPath = originalLookPath }() - scanner := &ImageScanner{} - testCases := []struct { name string lookPathSetup func() @@ -219,7 +216,7 @@ func TestImageScanner_findTrivyExecutable(t *testing.T) { t.Run(tc.name, func(t *testing.T) { tc.lookPathSetup() - path, err := scanner.findTrivyExecutable() + path, err := findTrivyExecutable() if tc.expectedError { assert.Error(t, err) @@ -233,94 +230,45 @@ func TestImageScanner_findTrivyExecutable(t *testing.T) { } } -// TestImageScanner_Scan_TrivyPathLookup tests the logic for finding the trivy executable. +// TestImageScanner_Scan_TrivyPathLookup tests the logic for using the trivy executable path. func TestImageScanner_Scan_TrivyPathLookup(t *testing.T) { - // Store original function to restore after tests - originalLookPath := currentExecutingLookPath - defer func() { currentExecutingLookPath = originalLookPath }() - // Base configuration for the scanner baseConfig := DefaultConfig() - scanner := &ImageScanner{ - config: *baseConfig, - } // Dummy image for testing img := unversioned.Image{ImageID: "test-image-id", Names: []string{"test-image:latest"}} - // Expected error message prefix when trivy is not found - expectedNotFoundErrorMsgPrefix := fmt.Sprintf("trivy executable not found at %s", trivyCommandName) - testCases := []struct { - name string - lookPathSetup func() // Sets up the mock for exec.LookPath - expectedStatus ScanStatus - expectNotFoundError bool // True if we expect the specific "trivy not found by LookPath" error - expectedErrorMsgContains string // The prefix for the "not found" error message + name string + trivyPath string + expectedStatus ScanStatus }{ { - name: "Trivy found at hardcoded path /trivy", - lookPathSetup: func() { - currentExecutingLookPath = func(file string) (string, error) { - if file == trivyExecutableName { - return trivyPathBin, nil // Found in PATH - } - return originalLookPath(file) // Fallback for any other calls - } - }, - // Scan will likely still fail due to inability to run actual scan in test, - // but it should not be the "trivy not found by LookPath" error. - expectedStatus: StatusFailed, - expectNotFoundError: false, + name: "Trivy path set to hardcoded path /trivy", + trivyPath: trivyCommandName, + expectedStatus: StatusFailed, // Will fail during actual execution but not due to path issues }, { - name: "Trivy found in $PATH, not at /trivy", - lookPathSetup: func() { - currentExecutingLookPath = func(file string) (string, error) { - if file == trivyExecutableName { - return trivyPathBin, nil // Found in $PATH - } - return originalLookPath(file) - } - }, - expectedStatus: StatusFailed, // Similar to above, subsequent scan steps will fail. - expectNotFoundError: false, - }, - { - name: "Trivy not found anywhere", - lookPathSetup: func() { - currentExecutingLookPath = func(file string) (string, error) { - if file == trivyExecutableName { - return "", errors.New("mock: trivy not in $PATH") - } - return originalLookPath(file) - } - }, - expectedStatus: StatusFailed, - expectNotFoundError: true, - expectedErrorMsgContains: expectedNotFoundErrorMsgPrefix, + name: "Trivy path set to system PATH location", + trivyPath: trivyPathBin, + expectedStatus: StatusFailed, // Will fail during actual execution but not due to path issues }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - tc.lookPathSetup() + scanner := &ImageScanner{ + config: *baseConfig, + trivyPath: tc.trivyPath, + } status, err := scanner.Scan(img) - if tc.expectNotFoundError { - assert.Error(t, err, "Expected an error when trivy is not found") - if err != nil { // Check prefix only if error is not nil - assert.True(t, strings.HasPrefix(err.Error(), tc.expectedErrorMsgContains), - "Error message should start with '%s'. Got: %s", tc.expectedErrorMsgContains, err.Error()) - } - assert.Equal(t, tc.expectedStatus, status, "ScanStatus should be StatusFailed") - } else if err != nil { - // If trivy was "found" by LookPath, any error should be from subsequent operations (e.g., cmd.Run, JSON unmarshal), - // not the specific "trivy executable not found by LookPath..." error. - assert.False(t, strings.HasPrefix(err.Error(), expectedNotFoundErrorMsgPrefix), - "Error should not be the 'trivy not found by LookPath' error. Got: %s", err.Error()) - // The status might still be StatusFailed due to these subsequent errors, - // which is acceptable for this test's focus on path lookup. + // The scan will likely fail due to inability to run actual scan in test, + // but it should not be a "trivy not found" error since the path is already set + assert.Equal(t, tc.expectedStatus, status, "ScanStatus should be StatusFailed") + if err != nil { + assert.NotContains(t, err.Error(), "trivy executable not found", + "Error should not be about trivy not being found since path is pre-set") } }) } From 1288f1bd893a565add33a3852225b83de627a41c Mon Sep 17 00:00:00 2001 From: Sertac Ozercan Date: Thu, 12 Jun 2025 19:04:17 +0000 Subject: [PATCH 4/7] copyloopvar to replace exportloopref Signed-off-by: Sertac Ozercan --- .golangci.yaml | 1 + pkg/remover/remover_test.go | 1 - pkg/scanners/trivy/types_test.go | 1 - 3 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.golangci.yaml b/.golangci.yaml index 2d38ffc6cf..c278dfc710 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -20,6 +20,7 @@ linters: disable-all: true enable: - errcheck + - copyloopvar - forcetypeassert - gocritic - goconst diff --git a/pkg/remover/remover_test.go b/pkg/remover/remover_test.go index e09ae9640b..d6ca179578 100644 --- a/pkg/remover/remover_test.go +++ b/pkg/remover/remover_test.go @@ -31,7 +31,6 @@ func TestRemoveImages(t *testing.T) { } for k, tc := range cases { - tc := tc t.Run(k, func(t *testing.T) { client := &testClient{t: t} added := make(map[string]struct{}) diff --git a/pkg/scanners/trivy/types_test.go b/pkg/scanners/trivy/types_test.go index 3147c1ce11..31d1cb79bc 100644 --- a/pkg/scanners/trivy/types_test.go +++ b/pkg/scanners/trivy/types_test.go @@ -151,7 +151,6 @@ func TestCLIArgs(t *testing.T) { } for _, tt := range tests { - tt := tt t.Run(tt.desc, func(t *testing.T) { actual := tt.config.cliArgs(ref) if len(actual) != len(tt.expected) { From a04d0cd08cb02211e2e3cfbf1eb905cfeb939144 Mon Sep 17 00:00:00 2001 From: Sertac Ozercan Date: Mon, 16 Jun 2025 21:17:21 +0000 Subject: [PATCH 5/7] address review comments Signed-off-by: Sertac Ozercan --- pkg/scanners/trivy/trivy.go | 36 +++-- pkg/scanners/trivy/types.go | 13 +- pkg/scanners/trivy/types_test.go | 227 +++++++++++++++++++++---------- 3 files changed, 183 insertions(+), 93 deletions(-) diff --git a/pkg/scanners/trivy/trivy.go b/pkg/scanners/trivy/trivy.go index b1100fd643..ad9a5f8272 100644 --- a/pkg/scanners/trivy/trivy.go +++ b/pkg/scanners/trivy/trivy.go @@ -7,6 +7,7 @@ import ( "fmt" "net/http" "os" + "os/exec" "time" "github.com/eraser-dev/eraser/api/unversioned" @@ -147,19 +148,28 @@ func runProfileServer() { log.Error(err, "pprof server failed") } -func findTrivyExecutable() (string, error) { - // First, check if trivy exists at the hardcoded path - if _, err := os.Stat(trivyCommandName); err == nil { - return trivyCommandName, nil +// ensureTrivyExecutable ensures that a trivy executable is available at the target path. +// If the executable is not found at the target path, it searches for trivy in PATH +// and creates a symlink at the target path pointing to the found executable. +func ensureTrivyExecutable(targetPath string) error { + // Check if trivy already exists at the target path + if _, err := os.Stat(targetPath); err == nil { + return nil } - // If not found at hardcoded path, try to find it in PATH - path, err := currentExecutingLookPath("trivy") + // If not found at target path, try to find it in PATH + pathLocation, err := exec.LookPath("trivy") if err != nil { - return "", fmt.Errorf("trivy executable not found at %s and not found in PATH: %w", trivyCommandName, err) + return fmt.Errorf("trivy executable not found at %s and not found in PATH: %w", targetPath, err) } - return path, nil + // Create symlink at target path pointing to the found executable + if err := os.Symlink(pathLocation, targetPath); err != nil { + return fmt.Errorf("failed to create symlink from %s to %s: %w", targetPath, pathLocation, err) + } + + log.Info("created trivy symlink", "from", targetPath, "to", pathLocation) + return nil } func initScanner(userConfig *Config) (Scanner, error) { @@ -180,9 +190,8 @@ func initScanner(userConfig *Config) (Scanner, error) { Address: utils.CRIPath, } - // Find trivy executable path during initialization - trivyPath, err := findTrivyExecutable() - if err != nil { + // Ensure trivy executable is available at the hardcoded path + if err := ensureTrivyExecutable(trivyCommandName); err != nil { return nil, err } @@ -190,9 +199,8 @@ func initScanner(userConfig *Config) (Scanner, error) { timer := time.NewTimer(totalTimeout) var s Scanner = &ImageScanner{ - config: *userConfig, - timer: timer, - trivyPath: trivyPath, + config: *userConfig, + timer: timer, } return s, nil } diff --git a/pkg/scanners/trivy/types.go b/pkg/scanners/trivy/types.go index 4bd7e0650b..61472484a1 100644 --- a/pkg/scanners/trivy/types.go +++ b/pkg/scanners/trivy/types.go @@ -14,10 +14,6 @@ import ( "github.com/eraser-dev/eraser/pkg/utils" ) -// currentExecutingLookPath is a variable that points to exec.LookPath by default, -// but can be overridden for testing purposes. -var currentExecutingLookPath = exec.LookPath - const ( StatusFailed ScanStatus = iota StatusNonCompliant @@ -172,9 +168,8 @@ func (c *Config) getRuntimeVar() (string, error) { } type ImageScanner struct { - config Config - timer *time.Timer - trivyPath string + config Config + timer *time.Timer } func (s *ImageScanner) Scan(img unversioned.Image) (ScanStatus, error) { @@ -191,13 +186,13 @@ func (s *ImageScanner) Scan(img unversioned.Image) (ScanStatus, error) { stderr := new(bytes.Buffer) cliArgs := s.config.cliArgs(refs[i]) - cmd := exec.Command(s.trivyPath, cliArgs...) // nolint:gosec // G204: Subprocess launched with variable + cmd := exec.Command(trivyCommandName, cliArgs...) cmd.Stdout = stdout cmd.Stderr = stderr cmd.Env = append(cmd.Env, os.Environ()...) cmd.Env = setRuntimeSocketEnvVars(cmd, s.config.Runtime) - log.V(1).Info("scanning image ref", "ref", refs[i], "cli_invocation", fmt.Sprintf("%s %s", s.trivyPath, strings.Join(cliArgs, " ")), "env", cmd.Env) + log.V(1).Info("scanning image ref", "ref", refs[i], "cli_invocation", fmt.Sprintf("%s %s", trivyCommandName, strings.Join(cliArgs, " ")), "env", cmd.Env) if err := cmd.Run(); err != nil { log.Error(err, "error scanning image", "imageID", img.ImageID, "reference", refs[i], "stderr", stderr.String()) continue diff --git a/pkg/scanners/trivy/types_test.go b/pkg/scanners/trivy/types_test.go index 31d1cb79bc..50f2500cde 100644 --- a/pkg/scanners/trivy/types_test.go +++ b/pkg/scanners/trivy/types_test.go @@ -1,13 +1,14 @@ package main import ( - "errors" + "os" + "path/filepath" "strings" "testing" "github.com/eraser-dev/eraser/api/unversioned" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) const ( @@ -172,103 +173,189 @@ func TestCLIArgs(t *testing.T) { } } -// TestFindTrivyExecutable tests the findTrivyExecutable function in isolation. -func TestFindTrivyExecutable(t *testing.T) { - // Store original function to restore after tests - originalLookPath := currentExecutingLookPath - defer func() { currentExecutingLookPath = originalLookPath }() +// TestEnsureTrivyExecutable tests the ensureTrivyExecutable function in isolation. +func TestEnsureTrivyExecutable(t *testing.T) { + // Save original PATH to restore after tests + originalPath := os.Getenv("PATH") + defer func() { os.Setenv("PATH", originalPath) }() testCases := []struct { - name string - lookPathSetup func() - expectedPath string - expectedError bool - expectedErrorMatch string + name string + setupFunc func(t *testing.T) (targetPath string, cleanup func()) + expectedError bool + expectedErrorMsg string + validateFunc func(t *testing.T, targetPath string) }{ { - name: "Trivy found in PATH only", - lookPathSetup: func() { - currentExecutingLookPath = func(file string) (string, error) { - if file == trivyExecutableName { - return trivyPathBin, nil - } - return "", errors.New("not found") - } + name: "Trivy already exists at target path", + setupFunc: func(t *testing.T) (string, func()) { + tempDir := t.TempDir() + targetPath := filepath.Join(tempDir, "trivy") + + // Create a trivy executable at the target path + file, err := os.Create(targetPath) + require.NoError(t, err) + file.Close() + err = os.Chmod(targetPath, 0o755) + require.NoError(t, err) + + return targetPath, func() {} }, - expectedPath: trivyPathBin, expectedError: false, + validateFunc: func(t *testing.T, targetPath string) { + // Should not create a symlink, original file should still exist + info, err := os.Lstat(targetPath) + require.NoError(t, err) + assert.Equal(t, os.FileMode(0o755), info.Mode().Perm(), "Original file should be preserved") + + // Verify it's not a symlink + assert.Equal(t, 0, int(info.Mode()&os.ModeSymlink), "Should not be a symlink") + }, + }, + { + name: "Trivy found in PATH, symlink created successfully", + setupFunc: func(t *testing.T) (string, func()) { + tempDir := t.TempDir() + pathDir := filepath.Join(tempDir, "bin") + err := os.Mkdir(pathDir, 0o755) + require.NoError(t, err) + + // Create a trivy executable in the PATH directory + trivyInPath := filepath.Join(pathDir, "trivy") + file, err := os.Create(trivyInPath) + require.NoError(t, err) + file.Close() + err = os.Chmod(trivyInPath, 0o755) + require.NoError(t, err) + + // Set PATH to include our temp bin directory + os.Setenv("PATH", pathDir) + + // Target path where symlink should be created + targetPath := filepath.Join(tempDir, "target_trivy") + + return targetPath, func() {} + }, + expectedError: false, + validateFunc: func(t *testing.T, targetPath string) { + // Should create a symlink at target path + info, err := os.Lstat(targetPath) + require.NoError(t, err) + + // Verify it's a symlink + assert.NotEqual(t, 0, int(info.Mode()&os.ModeSymlink), "Should be a symlink") + + // Verify symlink points to the correct location + linkTarget, err := os.Readlink(targetPath) + require.NoError(t, err) + assert.Contains(t, linkTarget, "trivy", "Symlink should point to trivy executable") + }, }, { name: "Trivy not found anywhere", - lookPathSetup: func() { - currentExecutingLookPath = func(_ string) (string, error) { - return "", errors.New("executable file not found in $PATH") + setupFunc: func(t *testing.T) (string, func()) { + tempDir := t.TempDir() + emptyPathDir := filepath.Join(tempDir, "empty") + err := os.Mkdir(emptyPathDir, 0o755) + require.NoError(t, err) + + // Set PATH to empty directory without trivy + os.Setenv("PATH", emptyPathDir) + + targetPath := filepath.Join(tempDir, "target_trivy") + return targetPath, func() {} + }, + expectedError: true, + expectedErrorMsg: "trivy executable not found", + validateFunc: func(t *testing.T, targetPath string) { + // Should not create any file or symlink + _, err := os.Lstat(targetPath) + assert.True(t, os.IsNotExist(err), "Target path should not exist when trivy is not found") + }, + }, + { + name: "Symlink creation fails due to permission", + setupFunc: func(t *testing.T) (string, func()) { + if os.Getuid() == 0 { + t.Skip("Skipping permission test when running as root") + } + + tempDir := t.TempDir() + pathDir := filepath.Join(tempDir, "bin") + err := os.Mkdir(pathDir, 0o755) + require.NoError(t, err) + + // Create a trivy executable in PATH + trivyInPath := filepath.Join(pathDir, "trivy") + file, err := os.Create(trivyInPath) + require.NoError(t, err) + file.Close() + err = os.Chmod(trivyInPath, 0o755) + require.NoError(t, err) + + os.Setenv("PATH", pathDir) + + // Try to create symlink in root directory (should fail for non-root users) + targetPath := "/trivy_test_symlink" + + return targetPath, func() { + // Clean up any created symlink + os.Remove(targetPath) } }, - expectedPath: "", - expectedError: true, - expectedErrorMatch: "trivy executable not found at /trivy and not found in PATH", + expectedError: true, + expectedErrorMsg: "failed to create symlink", + validateFunc: func(t *testing.T, targetPath string) { + // Should not create symlink due to permission error + _, err := os.Lstat(targetPath) + assert.True(t, os.IsNotExist(err), "Target path should not exist when symlink creation fails") + }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - tc.lookPathSetup() + targetPath, cleanup := tc.setupFunc(t) + defer cleanup() - path, err := findTrivyExecutable() + // Test the ensureTrivyExecutable function + err := ensureTrivyExecutable(targetPath) if tc.expectedError { assert.Error(t, err) - assert.Contains(t, err.Error(), tc.expectedErrorMatch) - assert.Empty(t, path) + assert.Contains(t, err.Error(), tc.expectedErrorMsg) } else { assert.NoError(t, err) - assert.Equal(t, tc.expectedPath, path) + } + + if tc.validateFunc != nil { + tc.validateFunc(t, targetPath) } }) } } -// TestImageScanner_Scan_TrivyPathLookup tests the logic for using the trivy executable path. -func TestImageScanner_Scan_TrivyPathLookup(t *testing.T) { - // Base configuration for the scanner - baseConfig := DefaultConfig() - // Dummy image for testing - img := unversioned.Image{ImageID: "test-image-id", Names: []string{"test-image:latest"}} +// TestInitScanner_TrivySymlinkCreation tests the symlink creation logic in initScanner. +func TestInitScanner_TrivySymlinkCreation(t *testing.T) { + // This test verifies that initScanner properly calls ensureTrivyExecutable + // and handles errors appropriately. Since ensureTrivyExecutable is tested + // comprehensively above, this focuses on the integration aspect. - testCases := []struct { - name string - trivyPath string - expectedStatus ScanStatus - }{ - { - name: "Trivy path set to hardcoded path /trivy", - trivyPath: trivyCommandName, - expectedStatus: StatusFailed, // Will fail during actual execution but not due to path issues - }, - { - name: "Trivy path set to system PATH location", - trivyPath: trivyPathBin, - expectedStatus: StatusFailed, // Will fail during actual execution but not due to path issues - }, - } + // Save original PATH to restore after test + originalPath := os.Getenv("PATH") + defer func() { os.Setenv("PATH", originalPath) }() - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - scanner := &ImageScanner{ - config: *baseConfig, - trivyPath: tc.trivyPath, - } + t.Run("initScanner fails when trivy not found", func(t *testing.T) { + // Set PATH to empty directory + tempDir := t.TempDir() + os.Setenv("PATH", tempDir) - status, err := scanner.Scan(img) + config := DefaultConfig() + scanner, err := initScanner(config) - // The scan will likely fail due to inability to run actual scan in test, - // but it should not be a "trivy not found" error since the path is already set - assert.Equal(t, tc.expectedStatus, status, "ScanStatus should be StatusFailed") - if err != nil { - assert.NotContains(t, err.Error(), "trivy executable not found", - "Error should not be about trivy not being found since path is pre-set") - } - }) - } + // Should fail because trivy is not found + assert.Error(t, err) + assert.Contains(t, err.Error(), "trivy executable not found") + assert.Nil(t, scanner) + }) } From 22cea7d04598d24a588e558f87c0711a5158f9cc Mon Sep 17 00:00:00 2001 From: Sertac Ozercan Date: Mon, 16 Jun 2025 21:25:29 +0000 Subject: [PATCH 6/7] Revert "address review comments" This reverts commit eebe5b95620ce05d074c1eacdde8d8fafd86b7c2. Signed-off-by: Sertac Ozercan --- pkg/scanners/trivy/trivy.go | 36 ++--- pkg/scanners/trivy/types.go | 13 +- pkg/scanners/trivy/types_test.go | 227 ++++++++++--------------------- 3 files changed, 93 insertions(+), 183 deletions(-) diff --git a/pkg/scanners/trivy/trivy.go b/pkg/scanners/trivy/trivy.go index ad9a5f8272..b1100fd643 100644 --- a/pkg/scanners/trivy/trivy.go +++ b/pkg/scanners/trivy/trivy.go @@ -7,7 +7,6 @@ import ( "fmt" "net/http" "os" - "os/exec" "time" "github.com/eraser-dev/eraser/api/unversioned" @@ -148,28 +147,19 @@ func runProfileServer() { log.Error(err, "pprof server failed") } -// ensureTrivyExecutable ensures that a trivy executable is available at the target path. -// If the executable is not found at the target path, it searches for trivy in PATH -// and creates a symlink at the target path pointing to the found executable. -func ensureTrivyExecutable(targetPath string) error { - // Check if trivy already exists at the target path - if _, err := os.Stat(targetPath); err == nil { - return nil +func findTrivyExecutable() (string, error) { + // First, check if trivy exists at the hardcoded path + if _, err := os.Stat(trivyCommandName); err == nil { + return trivyCommandName, nil } - // If not found at target path, try to find it in PATH - pathLocation, err := exec.LookPath("trivy") + // If not found at hardcoded path, try to find it in PATH + path, err := currentExecutingLookPath("trivy") if err != nil { - return fmt.Errorf("trivy executable not found at %s and not found in PATH: %w", targetPath, err) + return "", fmt.Errorf("trivy executable not found at %s and not found in PATH: %w", trivyCommandName, err) } - // Create symlink at target path pointing to the found executable - if err := os.Symlink(pathLocation, targetPath); err != nil { - return fmt.Errorf("failed to create symlink from %s to %s: %w", targetPath, pathLocation, err) - } - - log.Info("created trivy symlink", "from", targetPath, "to", pathLocation) - return nil + return path, nil } func initScanner(userConfig *Config) (Scanner, error) { @@ -190,8 +180,9 @@ func initScanner(userConfig *Config) (Scanner, error) { Address: utils.CRIPath, } - // Ensure trivy executable is available at the hardcoded path - if err := ensureTrivyExecutable(trivyCommandName); err != nil { + // Find trivy executable path during initialization + trivyPath, err := findTrivyExecutable() + if err != nil { return nil, err } @@ -199,8 +190,9 @@ func initScanner(userConfig *Config) (Scanner, error) { timer := time.NewTimer(totalTimeout) var s Scanner = &ImageScanner{ - config: *userConfig, - timer: timer, + config: *userConfig, + timer: timer, + trivyPath: trivyPath, } return s, nil } diff --git a/pkg/scanners/trivy/types.go b/pkg/scanners/trivy/types.go index 61472484a1..4bd7e0650b 100644 --- a/pkg/scanners/trivy/types.go +++ b/pkg/scanners/trivy/types.go @@ -14,6 +14,10 @@ import ( "github.com/eraser-dev/eraser/pkg/utils" ) +// currentExecutingLookPath is a variable that points to exec.LookPath by default, +// but can be overridden for testing purposes. +var currentExecutingLookPath = exec.LookPath + const ( StatusFailed ScanStatus = iota StatusNonCompliant @@ -168,8 +172,9 @@ func (c *Config) getRuntimeVar() (string, error) { } type ImageScanner struct { - config Config - timer *time.Timer + config Config + timer *time.Timer + trivyPath string } func (s *ImageScanner) Scan(img unversioned.Image) (ScanStatus, error) { @@ -186,13 +191,13 @@ func (s *ImageScanner) Scan(img unversioned.Image) (ScanStatus, error) { stderr := new(bytes.Buffer) cliArgs := s.config.cliArgs(refs[i]) - cmd := exec.Command(trivyCommandName, cliArgs...) + cmd := exec.Command(s.trivyPath, cliArgs...) // nolint:gosec // G204: Subprocess launched with variable cmd.Stdout = stdout cmd.Stderr = stderr cmd.Env = append(cmd.Env, os.Environ()...) cmd.Env = setRuntimeSocketEnvVars(cmd, s.config.Runtime) - log.V(1).Info("scanning image ref", "ref", refs[i], "cli_invocation", fmt.Sprintf("%s %s", trivyCommandName, strings.Join(cliArgs, " ")), "env", cmd.Env) + log.V(1).Info("scanning image ref", "ref", refs[i], "cli_invocation", fmt.Sprintf("%s %s", s.trivyPath, strings.Join(cliArgs, " ")), "env", cmd.Env) if err := cmd.Run(); err != nil { log.Error(err, "error scanning image", "imageID", img.ImageID, "reference", refs[i], "stderr", stderr.String()) continue diff --git a/pkg/scanners/trivy/types_test.go b/pkg/scanners/trivy/types_test.go index 50f2500cde..31d1cb79bc 100644 --- a/pkg/scanners/trivy/types_test.go +++ b/pkg/scanners/trivy/types_test.go @@ -1,14 +1,13 @@ package main import ( - "os" - "path/filepath" + "errors" "strings" "testing" "github.com/eraser-dev/eraser/api/unversioned" + "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) const ( @@ -173,189 +172,103 @@ func TestCLIArgs(t *testing.T) { } } -// TestEnsureTrivyExecutable tests the ensureTrivyExecutable function in isolation. -func TestEnsureTrivyExecutable(t *testing.T) { - // Save original PATH to restore after tests - originalPath := os.Getenv("PATH") - defer func() { os.Setenv("PATH", originalPath) }() +// TestFindTrivyExecutable tests the findTrivyExecutable function in isolation. +func TestFindTrivyExecutable(t *testing.T) { + // Store original function to restore after tests + originalLookPath := currentExecutingLookPath + defer func() { currentExecutingLookPath = originalLookPath }() testCases := []struct { - name string - setupFunc func(t *testing.T) (targetPath string, cleanup func()) - expectedError bool - expectedErrorMsg string - validateFunc func(t *testing.T, targetPath string) + name string + lookPathSetup func() + expectedPath string + expectedError bool + expectedErrorMatch string }{ { - name: "Trivy already exists at target path", - setupFunc: func(t *testing.T) (string, func()) { - tempDir := t.TempDir() - targetPath := filepath.Join(tempDir, "trivy") - - // Create a trivy executable at the target path - file, err := os.Create(targetPath) - require.NoError(t, err) - file.Close() - err = os.Chmod(targetPath, 0o755) - require.NoError(t, err) - - return targetPath, func() {} - }, - expectedError: false, - validateFunc: func(t *testing.T, targetPath string) { - // Should not create a symlink, original file should still exist - info, err := os.Lstat(targetPath) - require.NoError(t, err) - assert.Equal(t, os.FileMode(0o755), info.Mode().Perm(), "Original file should be preserved") - - // Verify it's not a symlink - assert.Equal(t, 0, int(info.Mode()&os.ModeSymlink), "Should not be a symlink") - }, - }, - { - name: "Trivy found in PATH, symlink created successfully", - setupFunc: func(t *testing.T) (string, func()) { - tempDir := t.TempDir() - pathDir := filepath.Join(tempDir, "bin") - err := os.Mkdir(pathDir, 0o755) - require.NoError(t, err) - - // Create a trivy executable in the PATH directory - trivyInPath := filepath.Join(pathDir, "trivy") - file, err := os.Create(trivyInPath) - require.NoError(t, err) - file.Close() - err = os.Chmod(trivyInPath, 0o755) - require.NoError(t, err) - - // Set PATH to include our temp bin directory - os.Setenv("PATH", pathDir) - - // Target path where symlink should be created - targetPath := filepath.Join(tempDir, "target_trivy") - - return targetPath, func() {} + name: "Trivy found in PATH only", + lookPathSetup: func() { + currentExecutingLookPath = func(file string) (string, error) { + if file == trivyExecutableName { + return trivyPathBin, nil + } + return "", errors.New("not found") + } }, + expectedPath: trivyPathBin, expectedError: false, - validateFunc: func(t *testing.T, targetPath string) { - // Should create a symlink at target path - info, err := os.Lstat(targetPath) - require.NoError(t, err) - - // Verify it's a symlink - assert.NotEqual(t, 0, int(info.Mode()&os.ModeSymlink), "Should be a symlink") - - // Verify symlink points to the correct location - linkTarget, err := os.Readlink(targetPath) - require.NoError(t, err) - assert.Contains(t, linkTarget, "trivy", "Symlink should point to trivy executable") - }, }, { name: "Trivy not found anywhere", - setupFunc: func(t *testing.T) (string, func()) { - tempDir := t.TempDir() - emptyPathDir := filepath.Join(tempDir, "empty") - err := os.Mkdir(emptyPathDir, 0o755) - require.NoError(t, err) - - // Set PATH to empty directory without trivy - os.Setenv("PATH", emptyPathDir) - - targetPath := filepath.Join(tempDir, "target_trivy") - return targetPath, func() {} - }, - expectedError: true, - expectedErrorMsg: "trivy executable not found", - validateFunc: func(t *testing.T, targetPath string) { - // Should not create any file or symlink - _, err := os.Lstat(targetPath) - assert.True(t, os.IsNotExist(err), "Target path should not exist when trivy is not found") - }, - }, - { - name: "Symlink creation fails due to permission", - setupFunc: func(t *testing.T) (string, func()) { - if os.Getuid() == 0 { - t.Skip("Skipping permission test when running as root") - } - - tempDir := t.TempDir() - pathDir := filepath.Join(tempDir, "bin") - err := os.Mkdir(pathDir, 0o755) - require.NoError(t, err) - - // Create a trivy executable in PATH - trivyInPath := filepath.Join(pathDir, "trivy") - file, err := os.Create(trivyInPath) - require.NoError(t, err) - file.Close() - err = os.Chmod(trivyInPath, 0o755) - require.NoError(t, err) - - os.Setenv("PATH", pathDir) - - // Try to create symlink in root directory (should fail for non-root users) - targetPath := "/trivy_test_symlink" - - return targetPath, func() { - // Clean up any created symlink - os.Remove(targetPath) + lookPathSetup: func() { + currentExecutingLookPath = func(_ string) (string, error) { + return "", errors.New("executable file not found in $PATH") } }, - expectedError: true, - expectedErrorMsg: "failed to create symlink", - validateFunc: func(t *testing.T, targetPath string) { - // Should not create symlink due to permission error - _, err := os.Lstat(targetPath) - assert.True(t, os.IsNotExist(err), "Target path should not exist when symlink creation fails") - }, + expectedPath: "", + expectedError: true, + expectedErrorMatch: "trivy executable not found at /trivy and not found in PATH", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - targetPath, cleanup := tc.setupFunc(t) - defer cleanup() + tc.lookPathSetup() - // Test the ensureTrivyExecutable function - err := ensureTrivyExecutable(targetPath) + path, err := findTrivyExecutable() if tc.expectedError { assert.Error(t, err) - assert.Contains(t, err.Error(), tc.expectedErrorMsg) + assert.Contains(t, err.Error(), tc.expectedErrorMatch) + assert.Empty(t, path) } else { assert.NoError(t, err) - } - - if tc.validateFunc != nil { - tc.validateFunc(t, targetPath) + assert.Equal(t, tc.expectedPath, path) } }) } } -// TestInitScanner_TrivySymlinkCreation tests the symlink creation logic in initScanner. -func TestInitScanner_TrivySymlinkCreation(t *testing.T) { - // This test verifies that initScanner properly calls ensureTrivyExecutable - // and handles errors appropriately. Since ensureTrivyExecutable is tested - // comprehensively above, this focuses on the integration aspect. +// TestImageScanner_Scan_TrivyPathLookup tests the logic for using the trivy executable path. +func TestImageScanner_Scan_TrivyPathLookup(t *testing.T) { + // Base configuration for the scanner + baseConfig := DefaultConfig() + // Dummy image for testing + img := unversioned.Image{ImageID: "test-image-id", Names: []string{"test-image:latest"}} - // Save original PATH to restore after test - originalPath := os.Getenv("PATH") - defer func() { os.Setenv("PATH", originalPath) }() + testCases := []struct { + name string + trivyPath string + expectedStatus ScanStatus + }{ + { + name: "Trivy path set to hardcoded path /trivy", + trivyPath: trivyCommandName, + expectedStatus: StatusFailed, // Will fail during actual execution but not due to path issues + }, + { + name: "Trivy path set to system PATH location", + trivyPath: trivyPathBin, + expectedStatus: StatusFailed, // Will fail during actual execution but not due to path issues + }, + } - t.Run("initScanner fails when trivy not found", func(t *testing.T) { - // Set PATH to empty directory - tempDir := t.TempDir() - os.Setenv("PATH", tempDir) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + scanner := &ImageScanner{ + config: *baseConfig, + trivyPath: tc.trivyPath, + } - config := DefaultConfig() - scanner, err := initScanner(config) + status, err := scanner.Scan(img) - // Should fail because trivy is not found - assert.Error(t, err) - assert.Contains(t, err.Error(), "trivy executable not found") - assert.Nil(t, scanner) - }) + // The scan will likely fail due to inability to run actual scan in test, + // but it should not be a "trivy not found" error since the path is already set + assert.Equal(t, tc.expectedStatus, status, "ScanStatus should be StatusFailed") + if err != nil { + assert.NotContains(t, err.Error(), "trivy executable not found", + "Error should not be about trivy not being found since path is pre-set") + } + }) + } } From 5d37828a594510dd0c964f3ba39c867eea8db3ed Mon Sep 17 00:00:00 2001 From: Sertac Ozercan Date: Mon, 16 Jun 2025 22:02:47 +0000 Subject: [PATCH 7/7] update Signed-off-by: Sertac Ozercan --- pkg/scanners/trivy/trivy.go | 3 +- pkg/scanners/trivy/types.go | 4 --- pkg/scanners/trivy/types_test.go | 51 +++++++++++++++++++------------- 3 files changed, 33 insertions(+), 25 deletions(-) diff --git a/pkg/scanners/trivy/trivy.go b/pkg/scanners/trivy/trivy.go index b1100fd643..285235c32a 100644 --- a/pkg/scanners/trivy/trivy.go +++ b/pkg/scanners/trivy/trivy.go @@ -7,6 +7,7 @@ import ( "fmt" "net/http" "os" + "os/exec" "time" "github.com/eraser-dev/eraser/api/unversioned" @@ -154,7 +155,7 @@ func findTrivyExecutable() (string, error) { } // If not found at hardcoded path, try to find it in PATH - path, err := currentExecutingLookPath("trivy") + path, err := exec.LookPath("trivy") if err != nil { return "", fmt.Errorf("trivy executable not found at %s and not found in PATH: %w", trivyCommandName, err) } diff --git a/pkg/scanners/trivy/types.go b/pkg/scanners/trivy/types.go index 4bd7e0650b..7ffe6c1de5 100644 --- a/pkg/scanners/trivy/types.go +++ b/pkg/scanners/trivy/types.go @@ -14,10 +14,6 @@ import ( "github.com/eraser-dev/eraser/pkg/utils" ) -// currentExecutingLookPath is a variable that points to exec.LookPath by default, -// but can be overridden for testing purposes. -var currentExecutingLookPath = exec.LookPath - const ( StatusFailed ScanStatus = iota StatusNonCompliant diff --git a/pkg/scanners/trivy/types_test.go b/pkg/scanners/trivy/types_test.go index 31d1cb79bc..1d6a447a90 100644 --- a/pkg/scanners/trivy/types_test.go +++ b/pkg/scanners/trivy/types_test.go @@ -1,13 +1,14 @@ package main import ( - "errors" + "os" + "path/filepath" "strings" "testing" "github.com/eraser-dev/eraser/api/unversioned" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) const ( @@ -174,36 +175,43 @@ func TestCLIArgs(t *testing.T) { // TestFindTrivyExecutable tests the findTrivyExecutable function in isolation. func TestFindTrivyExecutable(t *testing.T) { - // Store original function to restore after tests - originalLookPath := currentExecutingLookPath - defer func() { currentExecutingLookPath = originalLookPath }() + // Save original PATH to restore after tests + originalPath := os.Getenv("PATH") + defer func() { os.Setenv("PATH", originalPath) }() testCases := []struct { name string - lookPathSetup func() + setupFunc func(t *testing.T) string // returns tempdir path expectedPath string expectedError bool expectedErrorMatch string }{ { name: "Trivy found in PATH only", - lookPathSetup: func() { - currentExecutingLookPath = func(file string) (string, error) { - if file == trivyExecutableName { - return trivyPathBin, nil - } - return "", errors.New("not found") - } + setupFunc: func(t *testing.T) string { + tempDir := t.TempDir() + trivyPath := filepath.Join(tempDir, "trivy") + + // Create executable file + file, err := os.Create(trivyPath) + require.NoError(t, err) + file.Close() + err = os.Chmod(trivyPath, 0o755) + require.NoError(t, err) + + // Set PATH to include temp directory + os.Setenv("PATH", tempDir) + return trivyPath }, - expectedPath: trivyPathBin, expectedError: false, }, { name: "Trivy not found anywhere", - lookPathSetup: func() { - currentExecutingLookPath = func(_ string) (string, error) { - return "", errors.New("executable file not found in $PATH") - } + setupFunc: func(t *testing.T) string { + // Set PATH to empty temp directory without trivy + tempDir := t.TempDir() + os.Setenv("PATH", tempDir) + return "" }, expectedPath: "", expectedError: true, @@ -213,7 +221,10 @@ func TestFindTrivyExecutable(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - tc.lookPathSetup() + expectedPath := tc.setupFunc(t) + if expectedPath == "" { + expectedPath = tc.expectedPath + } path, err := findTrivyExecutable() @@ -223,7 +234,7 @@ func TestFindTrivyExecutable(t *testing.T) { assert.Empty(t, path) } else { assert.NoError(t, err) - assert.Equal(t, tc.expectedPath, path) + assert.Equal(t, expectedPath, path) } }) }