Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow restricting filesystem walk to specific folders #5481

Closed
wants to merge 1 commit into from
Closed
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
1 change: 1 addition & 0 deletions docs/docs/references/configuration/cli/trivy_config.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ trivy config [flags] DIR
--include-non-failures include successes and exceptions, available with '--scanners misconfig'
--k8s-version string specify k8s version to validate outdated api by it (example: 1.21.0)
--module-dir string specify directory to the wasm modules that will be loaded (default "$HOME/.trivy/modules")
--only-dirs strings specify the directories where the traversal is allowed
-o, --output string output file name
--password strings password. Comma-separated passwords allowed. TRIVY_PASSWORD should be used for security reasons.
--policy-bundle-repository string OCI registry URL to retrieve policy bundle from (default "ghcr.io/aquasecurity/trivy-policies:0")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ trivy filesystem [flags] PATH
--module-dir string specify directory to the wasm modules that will be loaded (default "$HOME/.trivy/modules")
--no-progress suppress progress bar
--offline-scan do not issue API requests to identify dependencies
--only-dirs strings specify the directories where the traversal is allowed
-o, --output string output file name
--parallel int number of goroutines enabled for parallel scanning, set 0 to auto-detect parallelism (default 5)
--password strings password. Comma-separated passwords allowed. TRIVY_PASSWORD should be used for security reasons.
Expand Down
1 change: 1 addition & 0 deletions docs/docs/references/configuration/cli/trivy_image.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ trivy image [flags] IMAGE_NAME
--module-dir string specify directory to the wasm modules that will be loaded (default "$HOME/.trivy/modules")
--no-progress suppress progress bar
--offline-scan do not issue API requests to identify dependencies
--only-dirs strings specify the directories where the traversal is allowed
-o, --output string output file name
--parallel int number of goroutines enabled for parallel scanning, set 0 to auto-detect parallelism (default 5)
--password strings password. Comma-separated passwords allowed. TRIVY_PASSWORD should be used for security reasons.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ trivy kubernetes [flags] { cluster | all | specific resources like kubectl. eg:
--no-progress suppress progress bar
--node-collector-namespace string specify the namespace in which the node-collector job should be deployed (default "trivy-temp")
--offline-scan do not issue API requests to identify dependencies
--only-dirs strings specify the directories where the traversal is allowed
-o, --output string output file name
--parallel int number of goroutines enabled for parallel scanning, set 0 to auto-detect parallelism (default 5)
--password strings password. Comma-separated passwords allowed. TRIVY_PASSWORD should be used for security reasons.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ trivy repository [flags] (REPO_PATH | REPO_URL)
--module-dir string specify directory to the wasm modules that will be loaded (default "$HOME/.trivy/modules")
--no-progress suppress progress bar
--offline-scan do not issue API requests to identify dependencies
--only-dirs strings specify the directories where the traversal is allowed
-o, --output string output file name
--parallel int number of goroutines enabled for parallel scanning, set 0 to auto-detect parallelism (default 5)
--password strings password. Comma-separated passwords allowed. TRIVY_PASSWORD should be used for security reasons.
Expand Down
1 change: 1 addition & 0 deletions docs/docs/references/configuration/cli/trivy_rootfs.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ trivy rootfs [flags] ROOTDIR
--module-dir string specify directory to the wasm modules that will be loaded (default "$HOME/.trivy/modules")
--no-progress suppress progress bar
--offline-scan do not issue API requests to identify dependencies
--only-dirs strings specify the directories where the traversal is allowed
-o, --output string output file name
--parallel int number of goroutines enabled for parallel scanning, set 0 to auto-detect parallelism (default 5)
--password strings password. Comma-separated passwords allowed. TRIVY_PASSWORD should be used for security reasons.
Expand Down
1 change: 1 addition & 0 deletions docs/docs/references/configuration/cli/trivy_sbom.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ trivy sbom [flags] SBOM_PATH
--list-all-pkgs enabling the option will output all packages regardless of vulnerability
--no-progress suppress progress bar
--offline-scan do not issue API requests to identify dependencies
--only-dirs strings specify the directories where the traversal is allowed
-o, --output string output file name
--redis-ca string redis ca file location, if using redis as cache backend
--redis-cert string redis certificate file location, if using redis as cache backend
Expand Down
1 change: 1 addition & 0 deletions docs/docs/references/configuration/cli/trivy_vm.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ trivy vm [flags] VM_IMAGE
--module-dir string specify directory to the wasm modules that will be loaded (default "$HOME/.trivy/modules")
--no-progress suppress progress bar
--offline-scan do not issue API requests to identify dependencies
--only-dirs strings specify the directories where the traversal is allowed
-o, --output string output file name
--parallel int number of goroutines enabled for parallel scanning, set 0 to auto-detect parallelism (default 5)
--policy-bundle-repository string OCI registry URL to retrieve policy bundle from (default "ghcr.io/aquasecurity/trivy-policies:0")
Expand Down
1 change: 1 addition & 0 deletions pkg/commands/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,7 @@ func NewConfigCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command {
// Enable only '--skip-dirs' and '--skip-files' and disable other flags
SkipDirs: &flag.SkipDirsFlag,
SkipFiles: &flag.SkipFilesFlag,
OnlyDirs: &flag.OnlyDirsFlag,
FilePatterns: &flag.FilePatternsFlag,
}

Expand Down
1 change: 1 addition & 0 deletions pkg/commands/artifact/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,7 @@ func initScannerConfig(opts flag.Options, cacheClient cache.Cache) (ScannerConfi
DisabledAnalyzers: disabledAnalyzers(opts),
SkipFiles: opts.SkipFiles,
SkipDirs: opts.SkipDirs,
OnlyDirs: opts.OnlyDirs,
FilePatterns: opts.FilePatterns,
Offline: opts.OfflineScan,
NoProgress: opts.NoProgress || opts.Quiet,
Expand Down
4 changes: 2 additions & 2 deletions pkg/commands/artifact/scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ func sbomRemoteScanner(ctx context.Context, conf ScannerConfig) (scanner.Scanner
// vmStandaloneScanner initializes a VM scanner in standalone mode
func vmStandaloneScanner(ctx context.Context, conf ScannerConfig) (scanner.Scanner, func(), error) {
// TODO: The walker should be initialized in initializeVMScanner after https://github.com/aquasecurity/trivy/pull/5180
w := walker.NewVM(conf.ArtifactOption.SkipFiles, conf.ArtifactOption.SkipDirs)
w := walker.NewVM(conf.ArtifactOption.SkipFiles, conf.ArtifactOption.SkipDirs, conf.ArtifactOption.OnlyDirs)
s, cleanup, err := initializeVMScanner(ctx, conf.Target, conf.ArtifactCache, conf.LocalArtifactCache,
w, conf.ArtifactOption)
if err != nil {
Expand All @@ -123,7 +123,7 @@ func vmStandaloneScanner(ctx context.Context, conf ScannerConfig) (scanner.Scann
// vmRemoteScanner initializes a VM scanner in client/server mode
func vmRemoteScanner(ctx context.Context, conf ScannerConfig) (scanner.Scanner, func(), error) {
// TODO: The walker should be initialized in initializeVMScanner after https://github.com/aquasecurity/trivy/pull/5180
w := walker.NewVM(conf.ArtifactOption.SkipFiles, conf.ArtifactOption.SkipDirs)
w := walker.NewVM(conf.ArtifactOption.SkipFiles, conf.ArtifactOption.SkipDirs, conf.ArtifactOption.OnlyDirs)
s, cleanup, err := initializeRemoteVMScanner(ctx, conf.Target, conf.ArtifactCache, w, conf.ServerOption, conf.ArtifactOption)
if err != nil {
return scanner.Scanner{}, func() {}, xerrors.Errorf("unable to initialize a remote vm scanner: %w", err)
Expand Down
2 changes: 2 additions & 0 deletions pkg/fanal/artifact/artifact.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type Option struct {
DisabledHandlers []types.HandlerType
SkipFiles []string
SkipDirs []string
OnlyDirs []string
FilePatterns []string
NoProgress bool
Insecure bool
Expand Down Expand Up @@ -78,6 +79,7 @@ func (o *Option) Sort() {
sort.Strings(o.SkipFiles)
sort.Strings(o.SkipDirs)
sort.Strings(o.FilePatterns)
sort.Strings(o.OnlyDirs)
}

type Artifact interface {
Expand Down
2 changes: 1 addition & 1 deletion pkg/fanal/artifact/image/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func NewArtifact(img types.Image, c cache.ArtifactCache, opt artifact.Option) (a
return Artifact{
image: img,
cache: c,
walker: walker.NewLayerTar(opt.SkipFiles, opt.SkipDirs),
walker: walker.NewLayerTar(opt.SkipFiles, opt.SkipDirs, opt.OnlyDirs),
analyzer: a,
configAnalyzer: ca,
handlerManager: handlerManager,
Expand Down
2 changes: 1 addition & 1 deletion pkg/fanal/artifact/local/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func NewArtifact(rootPath string, c cache.ArtifactCache, opt artifact.Option) (a
return Artifact{
rootPath: filepath.ToSlash(filepath.Clean(rootPath)),
cache: c,
walker: walker.NewFS(buildPathsToSkip(rootPath, opt.SkipFiles), buildPathsToSkip(rootPath, opt.SkipDirs),
walker: walker.NewFS(buildPathsToSkip(rootPath, opt.SkipFiles), buildPathsToSkip(rootPath, opt.SkipDirs), buildPathsToSkip(rootPath, opt.OnlyDirs),
opt.Parallel, opt.WalkOption.ErrorCallback),
analyzer: a,
handlerManager: handlerManager,
Expand Down
3 changes: 2 additions & 1 deletion pkg/fanal/cache/key.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ func CalcKey(id string, analyzerVersions analyzer.Versions, hookVersions map[str
HookVersions map[string]int
SkipFiles []string
SkipDirs []string
OnlyDirs []string `json:",omitempty"`
FilePatterns []string `json:",omitempty"`
}{id, analyzerVersions, hookVersions, artifactOpt.SkipFiles, artifactOpt.SkipDirs, artifactOpt.FilePatterns}
}{id, analyzerVersions, hookVersions, artifactOpt.SkipFiles, artifactOpt.SkipDirs, artifactOpt.OnlyDirs, artifactOpt.FilePatterns}

if err := json.NewEncoder(h).Encode(keyBase); err != nil {
return "", xerrors.Errorf("json encode error: %w", err)
Expand Down
5 changes: 2 additions & 3 deletions pkg/fanal/walker/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type FS struct {
errCallback ErrorCallback
}

func NewFS(skipFiles, skipDirs []string, parallel int, errCallback ErrorCallback) FS {
func NewFS(skipFiles, skipDirs, onlyDirs []string, parallel int, errCallback ErrorCallback) FS {
if errCallback == nil {
errCallback = func(pathname string, err error) error {
// ignore permission errors
Expand All @@ -33,8 +33,7 @@ func NewFS(skipFiles, skipDirs []string, parallel int, errCallback ErrorCallback
}

return FS{
walker: newWalker(skipFiles, skipDirs),
parallel: parallel,
walker: newWalker(skipFiles, skipDirs, onlyDirs),
errCallback: errCallback,
}
}
Expand Down
3 changes: 2 additions & 1 deletion pkg/fanal/walker/fs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ func TestDir_Walk(t *testing.T) {
type fields struct {
skipFiles []string
skipDirs []string
onlyDirs []string
errCallback walker.ErrorCallback
}
tests := []struct {
Expand Down Expand Up @@ -93,7 +94,7 @@ func TestDir_Walk(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := walker.NewFS(tt.fields.skipFiles, tt.fields.skipDirs, 1, tt.fields.errCallback)
w := walker.NewFS(tt.fields.skipFiles, tt.fields.skipDirs, tt.fields.onlyDirs, 1, tt.fields.errCallback)

err := w.Walk(tt.rootDir, tt.analyzeFn)
if tt.wantErr != "" {
Expand Down
4 changes: 2 additions & 2 deletions pkg/fanal/walker/tar.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ type LayerTar struct {
threshold int64
}

func NewLayerTar(skipFiles, skipDirs []string) LayerTar {
func NewLayerTar(skipFiles, skipDirs, onlyDirs []string) LayerTar {
threshold := defaultSizeThreshold
return LayerTar{
walker: newWalker(skipFiles, skipDirs),
walker: newWalker(skipFiles, skipDirs, onlyDirs),
threshold: threshold,
}
}
Expand Down
3 changes: 2 additions & 1 deletion pkg/fanal/walker/tar_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ func TestLayerTar_Walk(t *testing.T) {
type fields struct {
skipFiles []string
skipDirs []string
onlyDirs []string
}
tests := []struct {
name string
Expand Down Expand Up @@ -81,7 +82,7 @@ func TestLayerTar_Walk(t *testing.T) {
f, err := os.Open("testdata/test.tar")
require.NoError(t, err)

w := walker.NewLayerTar(tt.fields.skipFiles, tt.fields.skipDirs)
w := walker.NewLayerTar(tt.fields.skipFiles, tt.fields.skipDirs, tt.fields.onlyDirs)

gotOpqDirs, gotWhFiles, err := w.Walk(f, tt.analyzeFn)
if tt.wantErr != "" {
Expand Down
4 changes: 2 additions & 2 deletions pkg/fanal/walker/vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@ type VM struct {
analyzeFn WalkFunc
}

func NewVM(skipFiles, skipDirs []string) *VM {
func NewVM(skipFiles, skipDirs, onlyDirs []string) *VM {
threshold := defaultSizeThreshold
return &VM{
walker: newWalker(skipFiles, skipDirs),
walker: newWalker(skipFiles, skipDirs, onlyDirs),
threshold: threshold,
}
}
Expand Down
43 changes: 42 additions & 1 deletion pkg/fanal/walker/walk.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,15 @@ type WalkFunc func(filePath string, info os.FileInfo, opener analyzer.Opener) er
type walker struct {
skipFiles []string
skipDirs []string
onlyDirs []pattern
}

func newWalker(skipFiles, skipDirs []string) walker {
type pattern struct {
pattern string
base string
}

func newWalker(skipFiles, skipDirs, onlyDirs []string) walker {
var cleanSkipFiles, cleanSkipDirs []string
for _, skipFile := range skipFiles {
skipFile = filepath.ToSlash(filepath.Clean(skipFile))
Expand All @@ -45,9 +51,18 @@ func newWalker(skipFiles, skipDirs []string) walker {
cleanSkipDirs = append(cleanSkipDirs, skipDir)
}

var cleanOnlyDirs []pattern
for _, onlyDir := range onlyDirs {
onlyDir = filepath.ToSlash(filepath.Clean(onlyDir))
onlyDir = strings.TrimLeft(onlyDir, "/")
base, _ := doublestar.SplitPattern(onlyDir)
cleanOnlyDirs = append(cleanOnlyDirs, pattern{base: base, pattern: onlyDir})
}

return walker{
skipFiles: cleanSkipFiles,
skipDirs: cleanSkipDirs,
onlyDirs: cleanOnlyDirs,
}
}

Expand All @@ -64,6 +79,21 @@ func (w *walker) shouldSkipFile(filePath string) bool {
return true
}
}

if len(w.onlyDirs) > 0 {
dir := filepath.ToSlash(filepath.Dir(filePath))
for _, onlyDir := range w.onlyDirs {
match, err := doublestar.Match(onlyDir.pattern, dir)
if err != nil {
return false // return early if bad pattern
} else if match {
log.Logger.Debugf("Skipping file: %s", filePath)
return false
}
}
return true
}

return false
}

Expand All @@ -86,5 +116,16 @@ func (w *walker) shouldSkipDir(dir string) bool {
}
}

if dir != "." && len(w.onlyDirs) > 0 {
for _, onlyDir := range w.onlyDirs {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does it work with the nested dir? Let's say --only-dirs /*/bar/** is passed. Then, Trivy walks from the root and reaches /foo. This function returns false as /*/bar/** doesn't match /foo. It never reaches /foo/bar even though /foo/bar/baz.txt exists. I may be missing something.

https://go.dev/play/p/wdZ1cZ89kSx

Copy link
Contributor Author

@lebauce lebauce Nov 6, 2023

Choose a reason for hiding this comment

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

You're correct. At first I didn't support wildcards but when rebasing, I saw the use of "doublestar.Match" and switched to it. In this case, it only works with leading "wildcards" - which brings no value - because of the case you mentionned.

I suggest toswitch back to use regular paths. WDYT ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I kept the patterns but apply them when considering files. When possible, it also skips some directories to avoid having traversing them.

if onlyDir.base == "." || strings.HasPrefix(onlyDir.base+"/", dir+"/") || strings.HasPrefix(dir+"/", onlyDir.base+"/") {
return false
}
}

log.Logger.Debugf("Skipping directory: %s", dir)
return true
}

return false
}
86 changes: 84 additions & 2 deletions pkg/fanal/walker/walk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func Test_shouldSkipFile(t *testing.T) {

for i, tc := range testCases {
t.Run(fmt.Sprint(i), func(t *testing.T) {
w := newWalker(tc.skipFiles, nil)
w := newWalker(tc.skipFiles, nil, nil)
for file, skipResult := range tc.skipMap {
assert.Equal(t, skipResult, w.shouldSkipFile(filepath.ToSlash(filepath.Clean(file))), fmt.Sprintf("skipFiles: %s, file: %s", tc.skipFiles, file))
}
Expand Down Expand Up @@ -115,10 +115,92 @@ func Test_shouldSkipDir(t *testing.T) {

for i, tc := range testCases {
t.Run(fmt.Sprint(i), func(t *testing.T) {
w := newWalker(nil, tc.skipDirs)
w := newWalker(nil, tc.skipDirs, nil)
for dir, skipResult := range tc.skipMap {
assert.Equal(t, skipResult, w.shouldSkipDir(filepath.ToSlash(filepath.Clean(dir))), fmt.Sprintf("skipDirs: %s, dir: %s", tc.skipDirs, dir))
}
})
}
}

func Test_onlyDir(t *testing.T) {
testCases := []struct {
skipDirs []string
onlyDirs []string
skipMap map[string][2]bool
}{
{
skipDirs: nil,
onlyDirs: []string{"/etc/**"},
skipMap: map[string][2]bool{
"/etc/foo/bar": {false, false},
"/var/log/bar": {true, true},
},
},
{
skipDirs: nil,
onlyDirs: []string{"/**"},
skipMap: map[string][2]bool{
"/foo": {false, false},
"/etc/foo": {false, false},
"/var/log/bar": {false, false},
},
},
{
skipDirs: nil,
onlyDirs: []string{"/*"},
skipMap: map[string][2]bool{
"/foo": {false, false},
"/etc/foo": {false, false},
"/etc/foo/bar": {true, false},
},
},
{
skipDirs: nil,
onlyDirs: []string{"/etc/foo/*"},
skipMap: map[string][2]bool{
"/etc/foo/bar": {true, false},
"/etc/foo2/bar": {true, false},
"/var/etc/foo2/bar": {true, true},
},
},
{
onlyDirs: []string{"/*/bar"},
skipMap: map[string][2]bool{
"/etc/bar/bar": {false, false},
"/etc/foo/bar": {true, false},
"/etc/bar/foo": {false, false},
"/etc/foo/bar/foo": {true, false},
},
},
{
onlyDirs: []string{"**/bar"},
skipMap: map[string][2]bool{
"/etc/bar/bar": {false, false},
"/etc/foo/bar": {true, false},
"/etc/bar/foo": {false, false},
"/etc/foo/bar/foo": {false, false},
},
},
{
onlyDirs: []string{"**/foo/*/bar"},
skipMap: map[string][2]bool{
"/foo/foo/bar/bar": {false, false},
"/etc/foo/bar": {true, false},
"/etc/bar/foo": {true, false},
"/etc/foo/bar/bar": {true, false},
},
},
}

for i, tc := range testCases {
t.Run(fmt.Sprint(i), func(t *testing.T) {
w := newWalker(nil, tc.skipDirs, tc.onlyDirs)
for file, skipResult := range tc.skipMap {
// todo: add check on skip dir
assert.Equal(t, skipResult[0], w.shouldSkipFile(filepath.ToSlash(filepath.Clean(file))), fmt.Sprintf("skipDirs: %s, dir: %s", tc.skipDirs, file))
assert.Equal(t, skipResult[1], w.shouldSkipDir(filepath.ToSlash(filepath.Clean(file))), fmt.Sprintf("skipDirs: %s, dir: %s", tc.skipDirs, file))
}
})
}
}
Loading
Loading