diff --git a/docs/docs/vulnerability/examples/report.md b/docs/docs/vulnerability/examples/report.md index 1f7ae0c1e1e4..07c92085d32e 100644 --- a/docs/docs/vulnerability/examples/report.md +++ b/docs/docs/vulnerability/examples/report.md @@ -34,6 +34,8 @@ The following packages/languages are currently supported: - Bundler: Gemfile.lock - Rust - Binaries built with [cargo-auditable][cargo-auditable] + - Go + - Modules: go.mod This tree is the reverse of the npm list command. However, if you want to resolve a vulnerability in a particular indirect dependency, the reversed tree is useful to know where that dependency comes from and identify which package you actually need to update. diff --git a/docs/docs/vulnerability/languages/golang.md b/docs/docs/vulnerability/languages/golang.md index b1ed71d300e4..5cbae62a6184 100644 --- a/docs/docs/vulnerability/languages/golang.md +++ b/docs/docs/vulnerability/languages/golang.md @@ -4,10 +4,10 @@ Trivy supports two types of Go scanning, Go Modules and binaries built by Go. The following table provides an outline of the features Trivy offers. -| Artifact | Offline[^1] | Dev dependencies | License | -|----------|:-----------:|:-----------------|:-------:| -| Modules | ✓ | Include | ✓[^2] | -| Binaries | ✓ | Exclude | - | +| Artifact | Offline[^1] | Dev dependencies | License | Dependency graph | +|----------|:-----------:|:-----------------|:-------:|:----------------:| +| Modules | ✓ | Include | ✓[^2] | ✓[^2] | +| Binaries | ✓ | Exclude | - | - | !!! note Trivy scans only dependencies of the Go project. @@ -49,6 +49,10 @@ If you want to have better detection, please consider updating the Go version in $ go mod tidy -go=1.18 ``` +To identify licenses and dependency relationships, you need to download modules to local cache beforehand, +such as `go mod download`, `go mod tidy`, etc. +Trivy traverses `$GOPATH/pkg/mod` and collect those extra information. + ### Go binaries Trivy scans binaries built by Go. If there is a Go binary in your container image, Trivy automatically finds and scans it. @@ -60,4 +64,4 @@ $ trivy fs ./your_binary ``` [^1]: It doesn't require the Internet access. -[^2]: Need to download modules to local cache beforehand, like `go mod download` \ No newline at end of file +[^2]: Need to download modules to local cache beforehand \ No newline at end of file diff --git a/go.mod b/go.mod index 412e1050bdba..a8c67321e9b0 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/alicebob/miniredis/v2 v2.23.0 github.com/aquasecurity/bolt-fixtures v0.0.0-20200903104109-d34e7f983986 github.com/aquasecurity/defsec v0.82.11-0.20230227200028-1372c6329e1f - github.com/aquasecurity/go-dep-parser v0.0.0-20230227085514-f6e7eca87043 + github.com/aquasecurity/go-dep-parser v0.0.0-20230228091112-63a15cdc6bc3 github.com/aquasecurity/go-gem-version v0.0.0-20201115065557-8eed6fe000ce github.com/aquasecurity/go-npm-version v0.0.0-20201110091526-0b796d180798 github.com/aquasecurity/go-pep440-version v0.0.0-20210121094942-22b2f8951d46 diff --git a/go.sum b/go.sum index dbc75ae274bc..247282c5f63e 100644 --- a/go.sum +++ b/go.sum @@ -317,8 +317,8 @@ github.com/aquasecurity/bolt-fixtures v0.0.0-20200903104109-d34e7f983986 h1:2a30 github.com/aquasecurity/bolt-fixtures v0.0.0-20200903104109-d34e7f983986/go.mod h1:NT+jyeCzXk6vXR5MTkdn4z64TgGfE5HMLC8qfj5unl8= github.com/aquasecurity/defsec v0.82.11-0.20230227200028-1372c6329e1f h1:GMsUbqXjrGzNOyxFGJt/WtJrBFXzQWtJJ2h+uoAUguQ= github.com/aquasecurity/defsec v0.82.11-0.20230227200028-1372c6329e1f/go.mod h1:AJswzQrwesjdpF03Ev7lcPdr5REBJLAmDqjvOitvr94= -github.com/aquasecurity/go-dep-parser v0.0.0-20230227085514-f6e7eca87043 h1:3YbIYXC9/HLODABe2P4v3nPJjK9MEFOtOmnSbiCIXdo= -github.com/aquasecurity/go-dep-parser v0.0.0-20230227085514-f6e7eca87043/go.mod h1:xx5OX/gVENa5dY60k9EliVvTbUf/EmRw1tJKzdskKGw= +github.com/aquasecurity/go-dep-parser v0.0.0-20230228091112-63a15cdc6bc3 h1:vOnS6iiH+b056rNsJjg6Km/a6N9oX52Cw83lTI3qlFI= +github.com/aquasecurity/go-dep-parser v0.0.0-20230228091112-63a15cdc6bc3/go.mod h1:xx5OX/gVENa5dY60k9EliVvTbUf/EmRw1tJKzdskKGw= github.com/aquasecurity/go-gem-version v0.0.0-20201115065557-8eed6fe000ce h1:QgBRgJvtEOBtUXilDb1MLi1p1MWoyFDXAu5DEUl5nwM= github.com/aquasecurity/go-gem-version v0.0.0-20201115065557-8eed6fe000ce/go.mod h1:HXgVzOPvXhVGLJs4ZKO817idqr/xhwsTcj17CLYY74s= github.com/aquasecurity/go-mock-aws v0.0.0-20220726154943-99847deb62b0 h1:tihCUjLWkF0b1SAjAKcFltUs3SpsqGrLtI+Frye0D10= diff --git a/integration/testdata/gomod-skip.json.golden b/integration/testdata/gomod-skip.json.golden index 3d428ec00ffe..f64a81d6e3f7 100644 --- a/integration/testdata/gomod-skip.json.golden +++ b/integration/testdata/gomod-skip.json.golden @@ -22,6 +22,7 @@ "Vulnerabilities": [ { "VulnerabilityID": "GMS-2022-20", + "PkgID": "github.com/docker/distribution@v2.7.1+incompatible", "PkgName": "github.com/docker/distribution", "InstalledVersion": "2.7.1+incompatible", "FixedVersion": "v2.8.0", @@ -43,6 +44,7 @@ }, { "VulnerabilityID": "CVE-2022-23628", + "PkgID": "github.com/open-policy-agent/opa@v0.35.0", "PkgName": "github.com/open-policy-agent/opa", "InstalledVersion": "0.35.0", "FixedVersion": "0.37.0", @@ -81,6 +83,7 @@ }, { "VulnerabilityID": "CVE-2021-38561", + "PkgID": "golang.org/x/text@v0.3.6", "PkgName": "golang.org/x/text", "InstalledVersion": "0.3.6", "FixedVersion": "0.3.7", @@ -108,6 +111,7 @@ "Vulnerabilities": [ { "VulnerabilityID": "GMS-2022-20", + "PkgID": "github.com/docker/distribution@v2.7.1+incompatible", "PkgName": "github.com/docker/distribution", "InstalledVersion": "2.7.1+incompatible", "FixedVersion": "v2.8.0", diff --git a/integration/testdata/gomod.json.golden b/integration/testdata/gomod.json.golden index d9ca71adc53f..022437ae8616 100644 --- a/integration/testdata/gomod.json.golden +++ b/integration/testdata/gomod.json.golden @@ -22,6 +22,7 @@ "Vulnerabilities": [ { "VulnerabilityID": "GMS-2022-20", + "PkgID": "github.com/docker/distribution@v2.7.1+incompatible", "PkgName": "github.com/docker/distribution", "InstalledVersion": "2.7.1+incompatible", "FixedVersion": "v2.8.0", @@ -43,6 +44,7 @@ }, { "VulnerabilityID": "CVE-2022-23628", + "PkgID": "github.com/open-policy-agent/opa@v0.35.0", "PkgName": "github.com/open-policy-agent/opa", "InstalledVersion": "0.35.0", "FixedVersion": "0.37.0", @@ -81,6 +83,7 @@ }, { "VulnerabilityID": "CVE-2021-38561", + "PkgID": "golang.org/x/text@v0.3.6", "PkgName": "golang.org/x/text", "InstalledVersion": "0.3.6", "FixedVersion": "0.3.7", @@ -108,6 +111,7 @@ "Vulnerabilities": [ { "VulnerabilityID": "GMS-2022-20", + "PkgID": "github.com/docker/distribution@v2.7.1+incompatible", "PkgName": "github.com/docker/distribution", "InstalledVersion": "2.7.1+incompatible", "FixedVersion": "v2.8.0", @@ -136,6 +140,7 @@ "Vulnerabilities": [ { "VulnerabilityID": "GMS-2022-20", + "PkgID": "github.com/docker/distribution@v2.7.1+incompatible", "PkgName": "github.com/docker/distribution", "InstalledVersion": "2.7.1+incompatible", "FixedVersion": "v2.8.0", diff --git a/pkg/fanal/analyzer/language/golang/mod/mod.go b/pkg/fanal/analyzer/language/golang/mod/mod.go index 0e7f0942b171..c10ad07ffc98 100644 --- a/pkg/fanal/analyzer/language/golang/mod/mod.go +++ b/pkg/fanal/analyzer/language/golang/mod/mod.go @@ -44,14 +44,19 @@ var ( ) type gomodAnalyzer struct { + // root go.mod/go.sum modParser godeptypes.Parser sumParser godeptypes.Parser + + // go.mod/go.sum in dependencies + leafModParser godeptypes.Parser } func newGoModAnalyzer(_ analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) { return &gomodAnalyzer{ - modParser: mod.NewParser(), - sumParser: sum.NewParser(), + modParser: mod.NewParser(true), // Only the root module should replace + sumParser: sum.NewParser(), + leafModParser: mod.NewParser(false), }, nil } @@ -94,8 +99,8 @@ func (a *gomodAnalyzer) PostAnalyze(_ context.Context, input analyzer.PostAnalys return nil, xerrors.Errorf("walk error: %w", err) } - if err = fillLicenses(apps); err != nil { - return nil, xerrors.Errorf("unable to identify licenses: %w", err) + if err = a.fillAdditionalData(apps); err != nil { + return nil, xerrors.Errorf("unable to collect additional info: %w", err) } return &analyzer.AnalysisResult{ @@ -116,6 +121,97 @@ func (a *gomodAnalyzer) Version() int { return version } +// fillAdditionalData collects licenses and dependency relationships, then update applications. +func (a *gomodAnalyzer) fillAdditionalData(apps []types.Application) error { + gopath := os.Getenv("GOPATH") + if gopath == "" { + gopath = build.Default.GOPATH + } + + // $GOPATH/pkg/mod + modPath := filepath.Join(gopath, "pkg", "mod") + if !fsutils.DirExists(modPath) { + log.Logger.Debugf("GOPATH (%s) not found. Need 'go mod download' to fill licenses and dependency relationships", modPath) + return nil + } + + licenses := map[string][]string{} + for i, app := range apps { + // Actually used dependencies + usedLibs := lo.SliceToMap(app.Libraries, func(pkg types.Package) (string, types.Package) { + return pkg.Name, pkg + }) + for j, lib := range app.Libraries { + if l, ok := licenses[lib.ID]; ok { + // Fill licenses + apps[i].Libraries[j].Licenses = l + continue + } + + // e.g. $GOPATH/pkg/mod/github.com/aquasecurity/go-dep-parser@v1.0.0 + modDir := filepath.Join(modPath, fmt.Sprintf("%s@v%s", normalizeModName(lib.Name), lib.Version)) + + // Collect licenses + if licenseNames, err := findLicense(modDir); err != nil { + return xerrors.Errorf("license error: %w", err) + } else { + // Cache the detected licenses + licenses[lib.ID] = licenseNames + + // Fill licenses + apps[i].Libraries[j].Licenses = licenseNames + } + + // Collect dependencies of the direct dependency + if dep, err := a.collectDeps(modDir, lib.ID); err != nil { + return xerrors.Errorf("dependency graph error: %w", err) + } else if dep.ID == "" { + // go.mod not found + continue + } else { + // Filter out unused dependencies and convert module names to module IDs + apps[i].Libraries[j].DependsOn = lo.FilterMap(dep.DependsOn, func(modName string, _ int) (string, bool) { + if m, ok := usedLibs[modName]; !ok { + return "", false + } else { + return m.ID, true + } + }) + } + } + } + return nil +} + +func (a *gomodAnalyzer) collectDeps(modDir string, pkgID string) (godeptypes.Dependency, error) { + // e.g. $GOPATH/pkg/mod/github.com/aquasecurity/go-dep-parser@v0.0.0-20220406074731-71021a481237/go.mod + modPath := filepath.Join(modDir, "go.mod") + f, err := os.Open(modPath) + if errors.Is(err, fs.ErrNotExist) { + log.Logger.Debugf("Unable to identify dependencies of %s as it doesn't support Go modules", pkgID) + return godeptypes.Dependency{}, nil + } else if err != nil { + return godeptypes.Dependency{}, xerrors.Errorf("file open error: %w", err) + } + defer f.Close() + + // Parse go.mod under $GOPATH/pkg/mod + libs, _, err := a.leafModParser.Parse(f) + if err != nil { + return godeptypes.Dependency{}, xerrors.Errorf("%s parse error: %w", modPath, err) + } + + // Filter out indirect dependencies + dependsOn := lo.FilterMap(libs, func(lib godeptypes.Library, index int) (string, bool) { + return lib.Name, !lib.Indirect + }) + + return godeptypes.Dependency{ + ID: pkgID, + DependsOn: dependsOn, + }, nil +} + func parse(fsys fs.FS, path string, parser godeptypes.Parser) (*types.Application, error) { f, err := fsys.Open(path) if err != nil { @@ -133,6 +229,7 @@ func parse(fsys fs.FS, path string, parser godeptypes.Parser) (*types.Applicatio if err != nil { return nil, xerrors.Errorf("%s parse error: %w", path, err) } + return language.ToApplication(types.GoModule, path, "", libs, deps), nil } @@ -171,51 +268,7 @@ func mergeGoSum(gomod, gosum *types.Application) { gomod.Libraries = maps.Values(uniq) } -func fillLicenses(apps []types.Application) error { - gopath := os.Getenv("GOPATH") - if gopath == "" { - gopath = build.Default.GOPATH - } - - // $GOPATH/pkg/mod - modPath := filepath.Join(gopath, "pkg", "mod") - if !fsutils.DirExists(modPath) { - log.Logger.Debugf("GOPATH (%s) not found. Need 'go mod download' to fill license information", modPath) - return nil - } - - licenses := map[string][]string{} - for i, app := range apps { - for j, lib := range app.Libraries { - libID := lib.Name + "@v" + lib.Version - if l, ok := licenses[libID]; ok { - // Fill licenses - apps[i].Libraries[j].Licenses = l - continue - } - - // e.g. $GOPATH/pkg/mod/github.com/aquasecurity/go-dep-parser@v1.0.0 - modDir := filepath.Join(modPath, fmt.Sprintf("%s@v%s", normalizeModName(lib.Name), lib.Version)) - l, err := findLicense(modDir) - if err != nil { - return xerrors.Errorf("golang license error: %w", err) - } else if l == nil || len(l.Findings) == 0 { - continue - } - licenseNames := lo.Map(l.Findings, func(finding types.LicenseFinding, _ int) string { - return finding.Name - }) - // Cache the detected licenses - licenses[libID] = licenseNames - - // Fill licenses - apps[i].Libraries[j].Licenses = licenseNames - } - } - return nil -} - -func findLicense(dir string) (*types.LicenseFile, error) { +func findLicense(dir string) ([]string, error) { var license *types.LicenseFile err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { if err != nil { @@ -249,8 +302,13 @@ func findLicense(dir string) (*types.LicenseFile, error) { return nil, nil } else if err != nil && !errors.Is(err, io.EOF) { return nil, fmt.Errorf("finding a known open source license: %w", err) + } else if license == nil || len(license.Findings) == 0 { + return nil, nil } - return license, nil + + return lo.Map(license.Findings, func(finding types.LicenseFinding, _ int) string { + return finding.Name + }), nil } // normalizeModName escapes upper characters diff --git a/pkg/fanal/analyzer/language/golang/mod/mod_test.go b/pkg/fanal/analyzer/language/golang/mod/mod_test.go index bb94b1efd33b..1adb7bb00396 100644 --- a/pkg/fanal/analyzer/language/golang/mod/mod_test.go +++ b/pkg/fanal/analyzer/language/golang/mod/mod_test.go @@ -30,13 +30,18 @@ func Test_gomodAnalyzer_Analyze(t *testing.T) { FilePath: "go.mod", Libraries: []types.Package{ { + ID: "github.com/aquasecurity/go-dep-parser@v0.0.0-20220406074731-71021a481237", Name: "github.com/aquasecurity/go-dep-parser", Version: "0.0.0-20220406074731-71021a481237", Licenses: []string{ "MIT", }, + DependsOn: []string{ + "golang.org/x/xerrors@v0.0.0-20200804184101-5ec99f83aff1", + }, }, { + ID: "golang.org/x/xerrors@v0.0.0-20200804184101-5ec99f83aff1", Name: "golang.org/x/xerrors", Version: "0.0.0-20200804184101-5ec99f83aff1", Indirect: true, @@ -56,10 +61,15 @@ func Test_gomodAnalyzer_Analyze(t *testing.T) { FilePath: "go.mod", Libraries: []types.Package{ { + ID: "github.com/aquasecurity/go-dep-parser@v0.0.0-20230219131432-590b1dfb6edd", Name: "github.com/aquasecurity/go-dep-parser", Version: "0.0.0-20230219131432-590b1dfb6edd", + DependsOn: []string{ + "github.com/BurntSushi/toml@v0.3.1", + }, }, { + ID: "github.com/BurntSushi/toml@v0.3.1", Name: "github.com/BurntSushi/toml", Version: "0.3.1", Indirect: true, @@ -82,6 +92,7 @@ func Test_gomodAnalyzer_Analyze(t *testing.T) { FilePath: "go.mod", Libraries: []types.Package{ { + ID: "github.com/aquasecurity/go-dep-parser@v0.0.0-20211110174639-8257534ffed3", Name: "github.com/aquasecurity/go-dep-parser", Version: "0.0.0-20211110174639-8257534ffed3", }, diff --git a/pkg/fanal/analyzer/language/golang/mod/testdata/pkg/mod/github.com/aquasecurity/go-dep-parser@v0.0.0-20220406074731-71021a481237/go.mod b/pkg/fanal/analyzer/language/golang/mod/testdata/pkg/mod/github.com/aquasecurity/go-dep-parser@v0.0.0-20220406074731-71021a481237/go.mod new file mode 100644 index 000000000000..9c840195c345 --- /dev/null +++ b/pkg/fanal/analyzer/language/golang/mod/testdata/pkg/mod/github.com/aquasecurity/go-dep-parser@v0.0.0-20220406074731-71021a481237/go.mod @@ -0,0 +1,31 @@ +module github.com/aquasecurity/go-dep-parser + +go 1.18 + +require ( + github.com/BurntSushi/toml v1.2.1 + github.com/aquasecurity/go-pep440-version v0.0.0-20210121094942-22b2f8951d46 + github.com/hashicorp/go-multierror v1.1.1 + github.com/hashicorp/go-retryablehttp v0.7.2 + github.com/liamg/jfather v0.0.7 + github.com/microsoft/go-rustaudit v0.0.0-20220808201409-204dfee52032 + github.com/samber/lo v1.37.0 + github.com/stretchr/testify v1.8.1 + go.uber.org/zap v1.24.0 + golang.org/x/exp v0.0.0-20220407100705-7b9b53b0aca4 + golang.org/x/mod v0.8.0 + golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f + golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/aquasecurity/go-version v0.0.0-20210121072130-637058cfe492 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/hashicorp/errwrap v1.0.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + golang.org/x/text v0.3.8 // indirect +) diff --git a/pkg/fanal/analyzer/language/golang/mod/testdata/pkg/mod/github.com/aquasecurity/go-dep-parser@v0.0.0-20230219131432-590b1dfb6edd/go.mod b/pkg/fanal/analyzer/language/golang/mod/testdata/pkg/mod/github.com/aquasecurity/go-dep-parser@v0.0.0-20230219131432-590b1dfb6edd/go.mod new file mode 100644 index 000000000000..9c840195c345 --- /dev/null +++ b/pkg/fanal/analyzer/language/golang/mod/testdata/pkg/mod/github.com/aquasecurity/go-dep-parser@v0.0.0-20230219131432-590b1dfb6edd/go.mod @@ -0,0 +1,31 @@ +module github.com/aquasecurity/go-dep-parser + +go 1.18 + +require ( + github.com/BurntSushi/toml v1.2.1 + github.com/aquasecurity/go-pep440-version v0.0.0-20210121094942-22b2f8951d46 + github.com/hashicorp/go-multierror v1.1.1 + github.com/hashicorp/go-retryablehttp v0.7.2 + github.com/liamg/jfather v0.0.7 + github.com/microsoft/go-rustaudit v0.0.0-20220808201409-204dfee52032 + github.com/samber/lo v1.37.0 + github.com/stretchr/testify v1.8.1 + go.uber.org/zap v1.24.0 + golang.org/x/exp v0.0.0-20220407100705-7b9b53b0aca4 + golang.org/x/mod v0.8.0 + golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f + golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/aquasecurity/go-version v0.0.0-20210121072130-637058cfe492 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/hashicorp/errwrap v1.0.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + golang.org/x/text v0.3.8 // indirect +) diff --git a/pkg/licensing/classifier.go b/pkg/licensing/classifier.go index 34f2342fcb30..c0a9e9b76216 100644 --- a/pkg/licensing/classifier.go +++ b/pkg/licensing/classifier.go @@ -25,7 +25,7 @@ func initGoogleClassifier() error { // This loading is expensive and should be called only when the license classification is needed. var err error classifierOnce.Do(func() { - log.Logger.Debug("Loading the the default license classifier...") + log.Logger.Debug("Loading the default license classifier...") cf, err = assets.DefaultClassifier() }) return err diff --git a/pkg/report/table/table_test.go b/pkg/report/table/table_test.go index 6f97e5705a5b..cf94af6580f7 100644 --- a/pkg/report/table/table_test.go +++ b/pkg/report/table/table_test.go @@ -170,26 +170,34 @@ Total: 1 (MEDIUM: 0, HIGH: 1) Type: "npm", Packages: []ftypes.Package{ { - ID: "node-fetch@1.7.3", - Name: "node-fetch", - Version: "1.7.3", + ID: "node-fetch@1.7.3", + Name: "node-fetch", + Version: "1.7.3", + Indirect: true, }, { - ID: "isomorphic-fetch@2.2.1", - Name: "isomorphic-fetch", - Version: "2.2.1", + ID: "isomorphic-fetch@2.2.1", + Name: "isomorphic-fetch", + Version: "2.2.1", + Indirect: true, DependsOn: []string{ "node-fetch@1.7.3", }, }, { - ID: "fbjs@0.8.18", - Name: "fbjs", - Version: "0.8.18", + ID: "fbjs@0.8.18", + Name: "fbjs", + Version: "0.8.18", + Indirect: true, DependsOn: []string{ "isomorphic-fetch@2.2.1", }, }, + { + ID: "sanitize-html@1.20.0", + Name: "sanitize-html", + Version: "1.20.0", + }, { ID: "styled-components@3.1.3", Name: "styled-components", @@ -244,9 +252,8 @@ Dependency Origin Tree (Reversed) ================================= package-lock.json ├── node-fetch@1.7.3, (MEDIUM: 0, HIGH: 1) -│ └── isomorphic-fetch@2.2.1 -│ └── fbjs@0.8.18 -│ └── styled-components@3.1.3 +│ └── ...(omitted)... +│ └── styled-components@3.1.3 └── sanitize-html@1.20.0, (MEDIUM: 1, HIGH: 0) `, }, @@ -260,7 +267,10 @@ package-lock.json Output: &tableWritten, Tree: true, IncludeNonFailures: tc.includeNonFailures, - Severities: []dbTypes.Severity{dbTypes.SeverityHigh, dbTypes.SeverityMedium}, + Severities: []dbTypes.Severity{ + dbTypes.SeverityHigh, + dbTypes.SeverityMedium, + }, }) assert.NoError(t, err) assert.Equal(t, tc.expectedOutput, tableWritten.String(), tc.name) diff --git a/pkg/report/table/vulnerability.go b/pkg/report/table/vulnerability.go index 90ea96de34dc..e9a7cfb37c5b 100644 --- a/pkg/report/table/vulnerability.go +++ b/pkg/report/table/vulnerability.go @@ -4,15 +4,16 @@ import ( "bytes" "fmt" "path/filepath" + "sort" "strings" "sync" "github.com/samber/lo" "github.com/xlab/treeprint" - - "github.com/aquasecurity/tml" + "golang.org/x/exp/maps" "github.com/aquasecurity/table" + "github.com/aquasecurity/tml" dbTypes "github.com/aquasecurity/trivy-db/pkg/types" ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" "github.com/aquasecurity/trivy/pkg/log" @@ -71,7 +72,14 @@ func (r *vulnerabilityRenderer) setHeaders() { if len(r.result.Vulnerabilities) == 0 { return } - header := []string{"Library", "Vulnerability", "Severity", "Installed Version", "Fixed Version", "Title"} + header := []string{ + "Library", + "Vulnerability", + "Severity", + "Installed Version", + "Fixed Version", + "Title", + } r.tableWriter.SetHeaders(header...) } @@ -106,12 +114,21 @@ func (r *vulnerabilityRenderer) setVulnerabilityRows(vulns []types.DetectedVulne var row []string if r.isTerminal { row = []string{ - lib, v.VulnerabilityID, ColorizeSeverity(v.Severity, v.Severity), - v.InstalledVersion, v.FixedVersion, strings.TrimSpace(title), + lib, + v.VulnerabilityID, + ColorizeSeverity(v.Severity, v.Severity), + v.InstalledVersion, + v.FixedVersion, + strings.TrimSpace(title), } } else { row = []string{ - lib, v.VulnerabilityID, v.Severity, v.InstalledVersion, v.FixedVersion, strings.TrimSpace(title), + lib, + v.VulnerabilityID, + v.Severity, + v.InstalledVersion, + v.FixedVersion, + strings.TrimSpace(title), } } @@ -133,6 +150,7 @@ func (r *vulnerabilityRenderer) renderDependencyTree() { if len(parents) == 0 { return } + ancestors := traverseAncestors(r.result.Packages, parents) root := treeprint.NewWithRoot(fmt.Sprintf(` Dependency Origin Tree (Reversed) @@ -152,19 +170,20 @@ Dependency Origin Tree (Reversed) pkgSeverityCount[vuln.PkgID] = cnts } - // Render tree - seen := map[string]struct{}{} - for _, vuln := range r.result.Vulnerabilities { - if _, ok := seen[vuln.PkgID]; ok { - continue - } + // Extract vulnerable packages + vulnPkgs := lo.Filter(r.result.Packages, func(pkg ftypes.Package, _ int) bool { + return lo.ContainsBy(r.result.Vulnerabilities, func(vuln types.DetectedVulnerability) bool { + return pkg.ID == vuln.PkgID + }) + }) - _, summaries := summarize(r.severities, pkgSeverityCount[vuln.PkgID]) - topLvlID := tml.Sprintf("%s, (%s)", vuln.PkgID, strings.Join(summaries, ", ")) + // Render tree + for _, vulnPkg := range vulnPkgs { + _, summaries := summarize(r.severities, pkgSeverityCount[vulnPkg.ID]) + topLvlID := tml.Sprintf("%s, (%s)", vulnPkg.ID, strings.Join(summaries, ", ")) - seen[vuln.PkgID] = struct{}{} branch := root.AddBranch(topLvlID) - addParents(branch, vuln.PkgID, parents, map[string]struct{}{}) + addParents(branch, vulnPkg, parents, ancestors, map[string]struct{}{vulnPkg.ID: {}}, 1) } r.printf(root.String()) @@ -175,33 +194,82 @@ func (r *vulnerabilityRenderer) printf(format string, args ...interface{}) { _ = tml.Fprintf(r.w, format, args...) } -func addParents(topItem treeprint.Tree, pkgID string, parentMap map[string][]string, seen map[string]struct{}) { - seen[pkgID] = struct{}{} // to avoid infinite loops +func addParents(topItem treeprint.Tree, pkg ftypes.Package, parentMap map[string]ftypes.Packages, ancestors map[string][]string, + seen map[string]struct{}, depth int) { + if !pkg.Indirect { + return + } - for _, parent := range parentMap[pkgID] { - if _, ok := seen[parent]; ok { - return + roots := map[string]struct{}{} + for _, parent := range parentMap[pkg.ID] { + if _, ok := seen[parent.ID]; ok { + continue + } + seen[parent.ID] = struct{}{} // to avoid infinite loops + + if depth == 1 && !parent.Indirect { + topItem.AddBranch(parent.ID) + } else { + // We omit intermediate dependencies and show only direct dependencies + // as this could make the dependency tree huge. + for _, ancestor := range ancestors[parent.ID] { + roots[ancestor] = struct{}{} + } + } + } + + // Omitted + rootIDs := lo.Filter(maps.Keys(roots), func(pkgID string, _ int) bool { + _, ok := seen[pkgID] + return !ok + }) + sort.Strings(rootIDs) + if len(rootIDs) > 0 { + branch := topItem.AddBranch("...(omitted)...") + for _, rootID := range rootIDs { + branch.AddBranch(rootID) } - branch := topItem.AddBranch(parent) - addParents(branch, parent, parentMap, seen) } } -func reverseDeps(libs []ftypes.Package) map[string][]string { - reversed := make(map[string][]string) - for _, lib := range libs { - for _, dependOn := range lib.DependsOn { - items, ok := reversed[dependOn] - if !ok { - reversed[dependOn] = []string{lib.ID} - } else { - reversed[dependOn] = append(items, lib.ID) - } +func reverseDeps(pkgs []ftypes.Package) map[string]ftypes.Packages { + reversed := make(map[string]ftypes.Packages) + for _, pkg := range pkgs { + for _, dependOn := range pkg.DependsOn { + reversed[dependOn] = append(reversed[dependOn], pkg) } } for k, v := range reversed { - reversed[k] = lo.Uniq(v) + reversed[k] = lo.UniqBy(v, func(pkg ftypes.Package) string { + return pkg.ID + }) } return reversed } + +func traverseAncestors(pkgs []ftypes.Package, parentMap map[string]ftypes.Packages) map[string][]string { + ancestors := map[string][]string{} + for _, pkg := range pkgs { + ancestors[pkg.ID] = findAncestor(pkg.ID, parentMap, map[string]struct{}{}) + } + return ancestors +} + +func findAncestor(pkgID string, parentMap map[string]ftypes.Packages, seen map[string]struct{}) []string { + ancestors := map[string]struct{}{} + seen[pkgID] = struct{}{} + for _, parent := range parentMap[pkgID] { + if _, ok := seen[parent.ID]; ok { + continue + } + if !parent.Indirect { + ancestors[parent.ID] = struct{}{} + } else { + for _, a := range findAncestor(parent.ID, parentMap, seen) { + ancestors[a] = struct{}{} + } + } + } + return maps.Keys(ancestors) +}