package walker import ( "errors" "io/fs" "os" "path/filepath" "strings" "golang.org/x/xerrors" "github.com/aquasecurity/trivy/pkg/log" xio "github.com/aquasecurity/trivy/pkg/x/io" ) // FS is the filesystem walker type FS struct{} func NewFS() *FS { return &FS{} } // Walk walks the filesystem rooted at root, calling fn for each unfiltered file. func (w *FS) Walk(root string, opt Option, fn WalkFunc) error { opt.SkipFiles = w.BuildSkipPaths(root, opt.SkipFiles) opt.SkipDirs = w.BuildSkipPaths(root, opt.SkipDirs) opt.SkipDirs = append(opt.SkipDirs, defaultSkipDirs...) walkDirFunc := w.WalkDirFunc(root, fn, opt) walkDirFunc = w.onError(walkDirFunc) // Walk the filesystem if err := filepath.WalkDir(root, walkDirFunc); err != nil { return xerrors.Errorf("walk dir error: %w", err) } return nil } func (w *FS) WalkDirFunc(root string, fn WalkFunc, opt Option) fs.WalkDirFunc { return func(filePath string, d fs.DirEntry, err error) error { if err != nil { return err } // For exported rootfs (e.g. images/alpine/etc/alpine-release) relPath, err := filepath.Rel(root, filePath) if err != nil { return xerrors.Errorf("filepath rel (%s): %w", relPath, err) } relPath = filepath.ToSlash(relPath) // Skip unnecessary files switch { case d.IsDir(): if SkipPath(relPath, opt.SkipDirs) { return filepath.SkipDir } return nil case !d.Type().IsRegular(): return nil case SkipPath(relPath, opt.SkipFiles): return nil } info, err := d.Info() if err != nil { return xerrors.Errorf("file info error: %w", err) } if err = fn(relPath, info, fileOpener(filePath)); err != nil { return xerrors.Errorf("failed to analyze file: %w", err) } return nil } } func (w *FS) onError(wrapped fs.WalkDirFunc) fs.WalkDirFunc { return func(filePath string, d fs.DirEntry, err error) error { err = wrapped(filePath, d, err) switch { // Unwrap fs.SkipDir error case errors.Is(err, fs.SkipDir): return fs.SkipDir // Ignore permission errors case os.IsPermission(err): return nil case err != nil: // halt traversal on any other error return xerrors.Errorf("unknown error with %s: %w", filePath, err) } return nil } } // BuildSkipPaths builds correct patch for defaultSkipDirs and skipFiles func (w *FS) BuildSkipPaths(base string, paths []string) []string { var relativePaths []string absBase, err := filepath.Abs(base) if err != nil { log.Warn("Failed to get an absolute path", log.String("base", base), log.Err(err)) return nil } for _, path := range paths { // Supports three types of flag specification. // All of them are converted into the relative path from the root directory. // 1. Relative skip dirs/files from the root directory // The specified dirs and files will be used as is. // e.g. $ trivy fs --skip-dirs bar ./foo // The skip dir from the root directory will be `bar/`. // 2. Relative skip dirs/files from the working directory // The specified dirs and files wll be converted to the relative path from the root directory. // e.g. $ trivy fs --skip-dirs ./foo/bar ./foo // The skip dir will be converted to `bar/`. // 3. Absolute skip dirs/files // The specified dirs and files wll be converted to the relative path from the root directory. // e.g. $ trivy fs --skip-dirs /bar/foo/baz ./foo // When the working directory is // 3.1 /bar: the skip dir will be converted to `baz/`. // 3.2 /hoge : the skip dir will be converted to `../../bar/foo/baz/`. absSkipPath, err := filepath.Abs(path) if err != nil { log.Warn("Failed to get an absolute path", log.String("base", base), log.Err(err)) continue } rel, err := filepath.Rel(absBase, absSkipPath) if err != nil { log.Warn("Failed to get a relative path", log.String("from", base), log.String("to", path), log.Err(err)) continue } var relPath string switch { case !filepath.IsAbs(path) && strings.HasPrefix(rel, ".."): // #1: Use the path as is relPath = path case !filepath.IsAbs(path) && !strings.HasPrefix(rel, ".."): // #2: Use the relative path from the root directory relPath = rel case filepath.IsAbs(path): // #3: Use the relative path from the root directory relPath = rel } relPath = filepath.ToSlash(relPath) relativePaths = append(relativePaths, relPath) } relativePaths = CleanSkipPaths(relativePaths) return relativePaths } // fileOpener returns a function opening a file. func fileOpener(filePath string) func() (xio.ReadSeekCloserAt, error) { return func() (xio.ReadSeekCloserAt, error) { return os.Open(filePath) } }