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)
+}