Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ linters:
disable-all: true
enable:
- errcheck
- exportloopref
- copyloopvar
- forcetypeassert
- gocritic
- goconst
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

Expand Down
2 changes: 1 addition & 1 deletion build/tooling/Dockerfile
Original file line number Diff line number Diff line change
@@ -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/[email protected]
RUN GO111MODULE=on go install k8s.io/code-generator/cmd/[email protected]
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/trivy.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
14 changes: 8 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
40 changes: 28 additions & 12 deletions go.sum

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion pkg/remover/remover_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{})
Expand Down
27 changes: 25 additions & 2 deletions pkg/scanners/trivy/trivy.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"net/http"
"os"
"os/exec"
"time"

"github.com/eraser-dev/eraser/api/unversioned"
Expand Down Expand Up @@ -147,6 +148,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 := exec.LookPath("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")
Expand All @@ -165,12 +181,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
}
Expand Down
9 changes: 5 additions & 4 deletions pkg/scanners/trivy/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,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) {
Expand All @@ -186,13 +187,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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you make the initScanner function create a symlink at /trivy to the path where trivy was found? That way we don't have to disable a security lint.

Copy link
Member Author

@sozercan sozercan Jun 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point, this might be why we used /trivy path instead of relying on PATH. However, will this break read only root file system pod security?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On second thought, we can change /trivy to trivy in the call to exec.Command and it will be located in the PATH automatically

Thanks to Mo for the fresh perspective :)

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
Expand Down
125 changes: 120 additions & 5 deletions pkg/scanners/trivy/types_test.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
package main

import (
"os"
"path/filepath"
"strings"
"testing"

"github.com/eraser-dev/eraser/api/unversioned"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

const ref = "image:tag"
const (
ref = "image:tag"
trivyExecutableName = "trivy"
trivyPathBin = "/usr/bin/trivy"
)

var testDuration = unversioned.Duration(100000000000)

func init() {
}

func TestCLIArgs(t *testing.T) {
type testCell struct {
desc string
Expand Down Expand Up @@ -147,7 +152,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) {
Expand All @@ -168,3 +172,114 @@ func TestCLIArgs(t *testing.T) {
})
}
}

// TestFindTrivyExecutable tests the findTrivyExecutable function in isolation.
func TestFindTrivyExecutable(t *testing.T) {
// Save original PATH to restore after tests
originalPath := os.Getenv("PATH")
defer func() { os.Setenv("PATH", originalPath) }()

testCases := []struct {
name string
setupFunc func(t *testing.T) string // returns tempdir path
expectedPath string
expectedError bool
expectedErrorMatch string
}{
{
name: "Trivy found in PATH only",
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
},
expectedError: false,
},
{
name: "Trivy not found anywhere",
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,
expectedErrorMatch: "trivy executable not found at /trivy and not found in PATH",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
expectedPath := tc.setupFunc(t)
if expectedPath == "" {
expectedPath = tc.expectedPath
}

path, err := 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, expectedPath, path)
}
})
}
}

// 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"}}

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
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
scanner := &ImageScanner{
config: *baseConfig,
trivyPath: tc.trivyPath,
}

status, err := scanner.Scan(img)

// 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")
}
})
}
}
Loading