mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-22 15:16:33 -08:00
feat(lang): add dependency origin graph (#1970)
Co-authored-by: knqyf263 <knqyf263@gmail.com>
This commit is contained in:
@@ -6,6 +6,90 @@
|
|||||||
$ trivy image -f table golang:1.12-alpine
|
$ trivy image -f table golang:1.12-alpine
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Show origins of vulnerable dependencies
|
||||||
|
|
||||||
|
!!! warning "EXPERIMENTAL"
|
||||||
|
This feature might change without preserving backwards compatibility.
|
||||||
|
|
||||||
|
Modern software development relies on the use of third-party libraries.
|
||||||
|
Third-party dependencies also depend on others so a list of dependencies can be represented as a dependency graph.
|
||||||
|
In some cases, vulnerable dependencies are not linked directly, and it requires analyses of the tree.
|
||||||
|
To make this task simpler Trivy can show a dependency origin tree with the `--dependency-tree` flag.
|
||||||
|
This flag is available with the `--format table` flag only.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
In table output, it looks like:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ trivy fs --severity HIGH,CRITICAL --dependency-tree /path/to/your_node_project
|
||||||
|
|
||||||
|
package-lock.json (npm)
|
||||||
|
=======================
|
||||||
|
Total: 2 (HIGH: 1, CRITICAL: 1)
|
||||||
|
|
||||||
|
┌──────────────────┬────────────────┬──────────┬───────────────────┬───────────────┬────────────────────────────────────────────────────────────┐
|
||||||
|
│ Library │ Vulnerability │ Severity │ Installed Version │ Fixed Version │ Title │
|
||||||
|
├──────────────────┼────────────────┼──────────┼───────────────────┼───────────────┼────────────────────────────────────────────────────────────┤
|
||||||
|
│ follow-redirects │ CVE-2022-0155 │ HIGH │ 1.14.6 │ 1.14.7 │ follow-redirects: Exposure of Private Personal Information │
|
||||||
|
│ │ │ │ │ │ to an Unauthorized Actor │
|
||||||
|
│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2022-0155 │
|
||||||
|
├──────────────────┼────────────────┼──────────┼───────────────────┼───────────────┼────────────────────────────────────────────────────────────┤
|
||||||
|
│ glob-parent │ CVE-2020-28469 │ CRITICAL │ 3.1.0 │ 5.1.2 │ nodejs-glob-parent: Regular expression denial of service │
|
||||||
|
│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2020-28469 │
|
||||||
|
└──────────────────┴────────────────┴──────────┴───────────────────┴───────────────┴────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
Dependency Origin Tree
|
||||||
|
======================
|
||||||
|
package-lock.json
|
||||||
|
├── follow-redirects@1.14.6, (HIGH: 1, CRITICAL: 0)
|
||||||
|
│ └── axios@0.21.4
|
||||||
|
└── glob-parent@3.1.0, (HIGH: 0, CRITICAL: 1)
|
||||||
|
└── chokidar@2.1.8
|
||||||
|
└── watchpack-chokidar2@2.0.1
|
||||||
|
└── watchpack@1.7.5
|
||||||
|
└── webpack@4.46.0
|
||||||
|
└── cra-append-sw@2.7.0
|
||||||
|
```
|
||||||
|
|
||||||
|
Vulnerable dependencies are shown in the top level of the tree.
|
||||||
|
Lower levels show how those vulnerabilities are introduced.
|
||||||
|
In the example above **axios@0.21.4** included in the project directly depends on the vulnerable **follow-redirects@1.14.6**.
|
||||||
|
Also, **glob-parent@3.1.0** with some vulnerabilities is included through chain of dependencies that is added by **cra-append-sw@2.7.0**.
|
||||||
|
|
||||||
|
Then, you can try to update **axios@0.21.4** and **cra-append-sw@2.7.0** to resolve vulnerabilities in **follow-redirects@1.14.6** and **glob-parent@3.1.0**.
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
Only Node.js (package-lock.json) is supported at the moment.
|
||||||
|
|
||||||
|
## JSON
|
||||||
|
Similar structure is included in JSON output format
|
||||||
|
```json
|
||||||
|
"VulnerabilityID": "CVE-2022-0235",
|
||||||
|
"PkgID": "node-fetch@1.7.3",
|
||||||
|
"PkgName": "node-fetch",
|
||||||
|
"PkgParents": [
|
||||||
|
{
|
||||||
|
"ID": "isomorphic-fetch@2.2.1",
|
||||||
|
"Parents": [
|
||||||
|
{
|
||||||
|
"ID": "fbjs@0.8.18",
|
||||||
|
"Parents": [
|
||||||
|
{
|
||||||
|
"ID": "styled-components@3.1.3"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! caution
|
||||||
|
As of May 2022 the feature is supported for `npm` dependency parser only
|
||||||
|
|
||||||
## JSON
|
## JSON
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
30
go.mod
30
go.mod
@@ -7,6 +7,7 @@ require (
|
|||||||
github.com/Masterminds/sprig/v3 v3.2.2
|
github.com/Masterminds/sprig/v3 v3.2.2
|
||||||
github.com/NYTimes/gziphandler v1.1.1
|
github.com/NYTimes/gziphandler v1.1.1
|
||||||
github.com/aquasecurity/bolt-fixtures v0.0.0-20200903104109-d34e7f983986
|
github.com/aquasecurity/bolt-fixtures v0.0.0-20200903104109-d34e7f983986
|
||||||
|
github.com/aquasecurity/fanal v0.0.0-20220615115521-e411bc995c6d
|
||||||
github.com/aquasecurity/go-dep-parser v0.0.0-20220607141748-ab2deea55bdf
|
github.com/aquasecurity/go-dep-parser v0.0.0-20220607141748-ab2deea55bdf
|
||||||
github.com/aquasecurity/go-gem-version v0.0.0-20201115065557-8eed6fe000ce
|
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-npm-version v0.0.0-20201110091526-0b796d180798
|
||||||
@@ -42,6 +43,7 @@ require (
|
|||||||
github.com/tetratelabs/wazero v0.0.0-20220606011721-119b069ba23e
|
github.com/tetratelabs/wazero v0.0.0-20220606011721-119b069ba23e
|
||||||
github.com/twitchtv/twirp v8.1.2+incompatible
|
github.com/twitchtv/twirp v8.1.2+incompatible
|
||||||
github.com/urfave/cli/v2 v2.8.1
|
github.com/urfave/cli/v2 v2.8.1
|
||||||
|
github.com/xlab/treeprint v1.1.0
|
||||||
go.uber.org/zap v1.21.0
|
go.uber.org/zap v1.21.0
|
||||||
golang.org/x/exp v0.0.0-20220407100705-7b9b53b0aca4
|
golang.org/x/exp v0.0.0-20220407100705-7b9b53b0aca4
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1
|
||||||
@@ -50,8 +52,6 @@ require (
|
|||||||
k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9
|
k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9
|
||||||
)
|
)
|
||||||
|
|
||||||
require github.com/aquasecurity/fanal v0.0.0-20220615115521-e411bc995c6d
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go v0.99.0 // indirect
|
cloud.google.com/go v0.99.0 // indirect
|
||||||
cloud.google.com/go/storage v1.14.0 // indirect
|
cloud.google.com/go/storage v1.14.0 // indirect
|
||||||
@@ -257,7 +257,6 @@ require (
|
|||||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||||
github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca // indirect
|
|
||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||||
github.com/yashtewari/glob-intersection v0.1.0 // indirect
|
github.com/yashtewari/glob-intersection v0.1.0 // indirect
|
||||||
github.com/zclconf/go-cty v1.10.0 // indirect
|
github.com/zclconf/go-cty v1.10.0 // indirect
|
||||||
@@ -284,9 +283,22 @@ require (
|
|||||||
google.golang.org/grpc v1.47.0 // indirect
|
google.golang.org/grpc v1.47.0 // indirect
|
||||||
gopkg.in/cheggaaa/pb.v1 v1.0.28 // indirect
|
gopkg.in/cheggaaa/pb.v1 v1.0.28 // indirect
|
||||||
gopkg.in/go-playground/validator.v9 v9.31.0 // indirect
|
gopkg.in/go-playground/validator.v9 v9.31.0 // indirect
|
||||||
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
|
lukechampine.com/uint128 v1.1.1 // indirect
|
||||||
|
modernc.org/cc/v3 v3.36.0 // indirect
|
||||||
|
modernc.org/ccgo/v3 v3.16.6 // indirect
|
||||||
|
modernc.org/libc v1.16.7 // indirect
|
||||||
|
modernc.org/mathutil v1.4.1 // indirect
|
||||||
|
modernc.org/memory v1.1.1 // indirect
|
||||||
|
modernc.org/opt v0.1.1 // indirect
|
||||||
|
modernc.org/sqlite v1.17.3 // indirect
|
||||||
|
modernc.org/strutil v1.1.1 // indirect
|
||||||
|
modernc.org/token v1.0.0 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||||
gopkg.in/square/go-jose.v2 v2.5.1 // indirect
|
gopkg.in/square/go-jose.v2 v2.5.1 // indirect
|
||||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
gotest.tools v2.2.0+incompatible
|
gotest.tools v2.2.0+incompatible
|
||||||
helm.sh/helm/v3 v3.9.0 // indirect
|
helm.sh/helm/v3 v3.9.0 // indirect
|
||||||
@@ -300,16 +312,6 @@ require (
|
|||||||
k8s.io/klog/v2 v2.60.1 // indirect
|
k8s.io/klog/v2 v2.60.1 // indirect
|
||||||
k8s.io/kube-openapi v0.0.0-20220328201542-3ee0da9b0b42 // indirect
|
k8s.io/kube-openapi v0.0.0-20220328201542-3ee0da9b0b42 // indirect
|
||||||
k8s.io/kubectl v0.24.1 // indirect
|
k8s.io/kubectl v0.24.1 // indirect
|
||||||
lukechampine.com/uint128 v1.1.1 // indirect
|
|
||||||
modernc.org/cc/v3 v3.36.0 // indirect
|
|
||||||
modernc.org/ccgo/v3 v3.16.6 // indirect
|
|
||||||
modernc.org/libc v1.16.7 // indirect
|
|
||||||
modernc.org/mathutil v1.4.1 // indirect
|
|
||||||
modernc.org/memory v1.1.1 // indirect
|
|
||||||
modernc.org/opt v0.1.1 // indirect
|
|
||||||
modernc.org/sqlite v1.17.3 // indirect
|
|
||||||
modernc.org/strutil v1.1.1 // indirect
|
|
||||||
modernc.org/token v1.0.0 // indirect
|
|
||||||
oras.land/oras-go v1.1.1 // indirect
|
oras.land/oras-go v1.1.1 // indirect
|
||||||
sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect
|
sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect
|
||||||
sigs.k8s.io/kustomize/api v0.11.4 // indirect
|
sigs.k8s.io/kustomize/api v0.11.4 // indirect
|
||||||
|
|||||||
3
go.sum
3
go.sum
@@ -1252,8 +1252,9 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:
|
|||||||
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
|
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
|
||||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||||
github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca h1:1CFlNzQhALwjS9mBAUkycX616GzgsuYUOCHA5+HSlXI=
|
|
||||||
github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg=
|
github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg=
|
||||||
|
github.com/xlab/treeprint v1.1.0 h1:G/1DjNkPpfZCFt9CSh6b5/nY4VimlbHF3Rh4obvtzDk=
|
||||||
|
github.com/xlab/treeprint v1.1.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
|
||||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||||
|
|||||||
2
integration/testdata/nodejs.json.golden
vendored
2
integration/testdata/nodejs.json.golden
vendored
@@ -22,6 +22,7 @@
|
|||||||
"Vulnerabilities": [
|
"Vulnerabilities": [
|
||||||
{
|
{
|
||||||
"VulnerabilityID": "CVE-2019-11358",
|
"VulnerabilityID": "CVE-2019-11358",
|
||||||
|
"PkgID": "jquery@3.3.9",
|
||||||
"PkgName": "jquery",
|
"PkgName": "jquery",
|
||||||
"InstalledVersion": "3.3.9",
|
"InstalledVersion": "3.3.9",
|
||||||
"FixedVersion": "3.4.0",
|
"FixedVersion": "3.4.0",
|
||||||
@@ -137,6 +138,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"VulnerabilityID": "CVE-2019-10744",
|
"VulnerabilityID": "CVE-2019-10744",
|
||||||
|
"PkgID": "lodash@4.17.4",
|
||||||
"PkgName": "lodash",
|
"PkgName": "lodash",
|
||||||
"InstalledVersion": "4.17.4",
|
"InstalledVersion": "4.17.4",
|
||||||
"FixedVersion": "4.17.12",
|
"FixedVersion": "4.17.12",
|
||||||
|
|||||||
@@ -371,6 +371,12 @@ var (
|
|||||||
EnvVars: []string{"TRIVY_SECRET_CONFIG"},
|
EnvVars: []string{"TRIVY_SECRET_CONFIG"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dependencyTree = cli.BoolFlag{
|
||||||
|
Name: "dependency-tree",
|
||||||
|
Usage: "show dependency origin tree (EXPERIMENTAL)",
|
||||||
|
EnvVars: []string{"TRIVY_DEPENDENCY_TREE"},
|
||||||
|
}
|
||||||
|
|
||||||
// Global flags
|
// Global flags
|
||||||
globalFlags = []cli.Flag{
|
globalFlags = []cli.Flag{
|
||||||
&quietFlag,
|
&quietFlag,
|
||||||
@@ -499,6 +505,7 @@ func NewImageCommand() *cli.Command {
|
|||||||
&insecureFlag,
|
&insecureFlag,
|
||||||
&dbRepositoryFlag,
|
&dbRepositoryFlag,
|
||||||
&secretConfig,
|
&secretConfig,
|
||||||
|
&dependencyTree,
|
||||||
stringSliceFlag(skipFiles),
|
stringSliceFlag(skipFiles),
|
||||||
stringSliceFlag(skipDirs),
|
stringSliceFlag(skipDirs),
|
||||||
|
|
||||||
@@ -545,6 +552,7 @@ func NewFilesystemCommand() *cli.Command {
|
|||||||
&offlineScan,
|
&offlineScan,
|
||||||
&dbRepositoryFlag,
|
&dbRepositoryFlag,
|
||||||
&secretConfig,
|
&secretConfig,
|
||||||
|
&dependencyTree,
|
||||||
stringSliceFlag(skipFiles),
|
stringSliceFlag(skipFiles),
|
||||||
stringSliceFlag(skipDirs),
|
stringSliceFlag(skipDirs),
|
||||||
|
|
||||||
@@ -595,6 +603,7 @@ func NewRootfsCommand() *cli.Command {
|
|||||||
&offlineScan,
|
&offlineScan,
|
||||||
&dbRepositoryFlag,
|
&dbRepositoryFlag,
|
||||||
&secretConfig,
|
&secretConfig,
|
||||||
|
&dependencyTree,
|
||||||
stringSliceFlag(skipFiles),
|
stringSliceFlag(skipFiles),
|
||||||
stringSliceFlag(skipDirs),
|
stringSliceFlag(skipDirs),
|
||||||
stringSliceFlag(configPolicy),
|
stringSliceFlag(configPolicy),
|
||||||
@@ -641,6 +650,7 @@ func NewRepositoryCommand() *cli.Command {
|
|||||||
&insecureFlag,
|
&insecureFlag,
|
||||||
&dbRepositoryFlag,
|
&dbRepositoryFlag,
|
||||||
&secretConfig,
|
&secretConfig,
|
||||||
|
&dependencyTree,
|
||||||
stringSliceFlag(skipFiles),
|
stringSliceFlag(skipFiles),
|
||||||
stringSliceFlag(skipDirs),
|
stringSliceFlag(skipDirs),
|
||||||
},
|
},
|
||||||
@@ -681,6 +691,7 @@ func NewClientCommand() *cli.Command {
|
|||||||
&offlineScan,
|
&offlineScan,
|
||||||
&insecureFlag,
|
&insecureFlag,
|
||||||
&secretConfig,
|
&secretConfig,
|
||||||
|
&dependencyTree,
|
||||||
|
|
||||||
&token,
|
&token,
|
||||||
&tokenHeader,
|
&tokenHeader,
|
||||||
|
|||||||
@@ -243,6 +243,7 @@ func (r *runner) Report(opt Option, report types.Report) error {
|
|||||||
AppVersion: opt.GlobalOption.AppVersion,
|
AppVersion: opt.GlobalOption.AppVersion,
|
||||||
Format: opt.Format,
|
Format: opt.Format,
|
||||||
Output: opt.Output,
|
Output: opt.Output,
|
||||||
|
Tree: opt.DependencyTree,
|
||||||
Severities: opt.Severities,
|
Severities: opt.Severities,
|
||||||
OutputTemplate: opt.Template,
|
OutputTemplate: opt.Template,
|
||||||
IncludeNonFailures: opt.IncludeNonFailures,
|
IncludeNonFailures: opt.IncludeNonFailures,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
type ReportOption struct {
|
type ReportOption struct {
|
||||||
Format string
|
Format string
|
||||||
Template string
|
Template string
|
||||||
|
DependencyTree bool
|
||||||
|
|
||||||
IgnoreFile string
|
IgnoreFile string
|
||||||
IgnoreUnfixed bool
|
IgnoreUnfixed bool
|
||||||
@@ -43,6 +44,7 @@ func NewReportOption(c *cli.Context) ReportOption {
|
|||||||
return ReportOption{
|
return ReportOption{
|
||||||
output: c.String("output"),
|
output: c.String("output"),
|
||||||
Format: c.String("format"),
|
Format: c.String("format"),
|
||||||
|
DependencyTree: c.Bool("dependency-tree"),
|
||||||
Template: c.String("template"),
|
Template: c.String("template"),
|
||||||
IgnorePolicy: c.String("ignore-policy"),
|
IgnorePolicy: c.String("ignore-policy"),
|
||||||
|
|
||||||
@@ -76,6 +78,11 @@ func (c *ReportOption) Init(output io.Writer, logger *zap.SugaredLogger) error {
|
|||||||
logger.Warn(`"--list-all-pkgs" cannot be used with "--format table". Try "--format json" or other formats.`)
|
logger.Warn(`"--list-all-pkgs" cannot be used with "--format table". Try "--format json" or other formats.`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// "--dependency-tree" option is available only with "--format table".
|
||||||
|
if c.DependencyTree && c.Format != "table" {
|
||||||
|
logger.Warn(`"--dependency-tree" can be used only with "--format table".`)
|
||||||
|
}
|
||||||
|
|
||||||
if c.forceListAllPkgs(logger) {
|
if c.forceListAllPkgs(logger) {
|
||||||
c.ListAllPkgs = true
|
c.ListAllPkgs = true
|
||||||
}
|
}
|
||||||
@@ -141,6 +148,10 @@ func (c *ReportOption) forceListAllPkgs(logger *zap.SugaredLogger) bool {
|
|||||||
logger.Debugf("'github', 'cyclonedx', 'spdx', and 'spdx-json' automatically enables '--list-all-pkgs'.")
|
logger.Debugf("'github', 'cyclonedx', 'spdx', and 'spdx-json' automatically enables '--list-all-pkgs'.")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
if c.DependencyTree {
|
||||||
|
logger.Debugf("'--dependency-tree' enables '--list-all-pkgs'.")
|
||||||
|
return true
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ func Detect(libType string, pkgs []ftypes.Package) ([]types.DetectedVulnerabilit
|
|||||||
func detect(driver Driver, libs []ftypes.Package) ([]types.DetectedVulnerability, error) {
|
func detect(driver Driver, libs []ftypes.Package) ([]types.DetectedVulnerability, error) {
|
||||||
var vulnerabilities []types.DetectedVulnerability
|
var vulnerabilities []types.DetectedVulnerability
|
||||||
for _, lib := range libs {
|
for _, lib := range libs {
|
||||||
vulns, err := driver.DetectVulnerabilities(lib.Name, lib.Version)
|
vulns, err := driver.DetectVulnerabilities(lib.ID, lib.Name, lib.Version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, xerrors.Errorf("failed to detect %s vulnerabilities: %w", driver.Type(), err)
|
return nil, xerrors.Errorf("failed to detect %s vulnerabilities: %w", driver.Type(), err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ func (d *Driver) Type() string {
|
|||||||
// If "ecosystem" is pip, it looks for buckets with "pip::" and gets security advisories from those buckets.
|
// If "ecosystem" is pip, it looks for buckets with "pip::" and gets security advisories from those buckets.
|
||||||
// It allows us to add a new data source with the ecosystem prefix (e.g. pip::new-data-source)
|
// It allows us to add a new data source with the ecosystem prefix (e.g. pip::new-data-source)
|
||||||
// and detect vulnerabilities without specifying a specific bucket name.
|
// and detect vulnerabilities without specifying a specific bucket name.
|
||||||
func (d *Driver) DetectVulnerabilities(pkgName, pkgVer string) ([]types.DetectedVulnerability, error) {
|
func (d *Driver) DetectVulnerabilities(pkgID, pkgName, pkgVer string) ([]types.DetectedVulnerability, error) {
|
||||||
// e.g. "pip::", "npm::"
|
// e.g. "pip::", "npm::"
|
||||||
prefix := fmt.Sprintf("%s::", d.ecosystem)
|
prefix := fmt.Sprintf("%s::", d.ecosystem)
|
||||||
advisories, err := d.dbc.GetAdvisories(prefix, vulnerability.NormalizePkgName(d.ecosystem, pkgName))
|
advisories, err := d.dbc.GetAdvisories(prefix, vulnerability.NormalizePkgName(d.ecosystem, pkgName))
|
||||||
@@ -91,6 +91,7 @@ func (d *Driver) DetectVulnerabilities(pkgName, pkgVer string) ([]types.Detected
|
|||||||
|
|
||||||
vuln := types.DetectedVulnerability{
|
vuln := types.DetectedVulnerability{
|
||||||
VulnerabilityID: adv.VulnerabilityID,
|
VulnerabilityID: adv.VulnerabilityID,
|
||||||
|
PkgID: pkgID,
|
||||||
PkgName: pkgName,
|
PkgName: pkgName,
|
||||||
InstalledVersion: pkgVer,
|
InstalledVersion: pkgVer,
|
||||||
FixedVersion: createFixedVersions(adv),
|
FixedVersion: createFixedVersions(adv),
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ func TestDriver_Detect(t *testing.T) {
|
|||||||
driver, err := library.NewDriver(tt.libType)
|
driver, err := library.NewDriver(tt.libType)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
got, err := driver.DetectVulnerabilities(tt.args.pkgName, tt.args.pkgVer)
|
got, err := driver.DetectVulnerabilities("", tt.args.pkgName, tt.args.pkgVer)
|
||||||
if tt.wantErr != "" {
|
if tt.wantErr != "" {
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), tt.wantErr)
|
assert.Contains(t, err.Error(), tt.wantErr)
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import (
|
|||||||
|
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/liamg/tml"
|
"github.com/liamg/tml"
|
||||||
|
"github.com/samber/lo"
|
||||||
|
"github.com/xlab/treeprint"
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
|
|
||||||
ftypes "github.com/aquasecurity/fanal/types"
|
ftypes "github.com/aquasecurity/fanal/types"
|
||||||
@@ -34,6 +36,9 @@ type TableWriter struct {
|
|||||||
Severities []dbTypes.Severity
|
Severities []dbTypes.Severity
|
||||||
Output io.Writer
|
Output io.Writer
|
||||||
|
|
||||||
|
// Show dependency origin tree
|
||||||
|
Tree bool
|
||||||
|
|
||||||
// We have to show a message once about using the '-format json' subcommand to get the full pkgPath
|
// We have to show a message once about using the '-format json' subcommand to get the full pkgPath
|
||||||
ShowMessageOnce *sync.Once
|
ShowMessageOnce *sync.Once
|
||||||
|
|
||||||
@@ -122,6 +127,10 @@ func (tw TableWriter) write(result types.Result) {
|
|||||||
_, _ = fmt.Fprint(tw.Output, NewMisconfigRenderer(result.Target, result.Misconfigurations, tw.IncludeNonFailures, tw.isOutputToTerminal()).Render())
|
_, _ = fmt.Fprint(tw.Output, NewMisconfigRenderer(result.Target, result.Misconfigurations, tw.IncludeNonFailures, tw.isOutputToTerminal()).Render())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if tw.Tree {
|
||||||
|
tw.renderDependencyTree(result)
|
||||||
|
}
|
||||||
|
|
||||||
// For debugging
|
// For debugging
|
||||||
if tw.Trace {
|
if tw.Trace {
|
||||||
tw.outputTrace(result)
|
tw.outputTrace(result)
|
||||||
@@ -196,6 +205,81 @@ func (tw TableWriter) setVulnerabilityRows(tableWriter *table.Table, vulns []typ
|
|||||||
tableWriter.AddRow(row...)
|
tableWriter.AddRow(row...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
func (tw TableWriter) renderDependencyTree(result types.Result) {
|
||||||
|
// Get parents of each dependency
|
||||||
|
parents := reverseDeps(result.Packages)
|
||||||
|
if len(parents) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
root := treeprint.NewWithRoot(fmt.Sprintf(`
|
||||||
|
Dependency Origin Tree
|
||||||
|
======================
|
||||||
|
%s`, result.Target))
|
||||||
|
|
||||||
|
// This count is next to the package ID.
|
||||||
|
// e.g. node-fetch@1.7.3 (MEDIUM: 2, HIGH: 1, CRITICAL: 3)
|
||||||
|
pkgSeverityCount := map[string]map[string]int{}
|
||||||
|
for _, vuln := range result.Vulnerabilities {
|
||||||
|
cnts, ok := pkgSeverityCount[vuln.PkgID]
|
||||||
|
if !ok {
|
||||||
|
cnts = map[string]int{}
|
||||||
|
}
|
||||||
|
|
||||||
|
cnts[vuln.Severity]++
|
||||||
|
pkgSeverityCount[vuln.PkgID] = cnts
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render tree
|
||||||
|
seen := map[string]struct{}{}
|
||||||
|
for _, vuln := range result.Vulnerabilities {
|
||||||
|
if _, ok := seen[vuln.PkgID]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
_, summaries := tw.summary(pkgSeverityCount[vuln.PkgID])
|
||||||
|
topLvlID := fmt.Sprintf("%s, (%s)", vuln.PkgID, strings.Join(summaries, ", "))
|
||||||
|
if tw.isOutputToTerminal() {
|
||||||
|
topLvlID = color.HiRedString(topLvlID)
|
||||||
|
}
|
||||||
|
|
||||||
|
seen[vuln.PkgID] = struct{}{}
|
||||||
|
branch := root.AddBranch(topLvlID)
|
||||||
|
addParents(branch, vuln.PkgID, parents)
|
||||||
|
|
||||||
|
}
|
||||||
|
tw.Println(root.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func addParents(topItem treeprint.Tree, pkgID string, parentMap map[string][]string) {
|
||||||
|
parents, ok := parentMap[pkgID]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, parent := range parents {
|
||||||
|
branch := topItem.AddBranch(parent)
|
||||||
|
addParents(branch, parent, parentMap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range reversed {
|
||||||
|
reversed[k] = lo.Uniq(v)
|
||||||
|
}
|
||||||
|
return reversed
|
||||||
|
}
|
||||||
|
|
||||||
func (tw TableWriter) outputTrace(result types.Result) {
|
func (tw TableWriter) outputTrace(result types.Result) {
|
||||||
blue := color.New(color.FgBlue).SprintFunc()
|
blue := color.New(color.FgBlue).SprintFunc()
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
ftypes "github.com/aquasecurity/fanal/types"
|
||||||
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
|
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
|
||||||
"github.com/aquasecurity/trivy/pkg/report"
|
"github.com/aquasecurity/trivy/pkg/report"
|
||||||
"github.com/aquasecurity/trivy/pkg/types"
|
"github.com/aquasecurity/trivy/pkg/types"
|
||||||
@@ -136,6 +137,91 @@ func TestReportWriter_Table(t *testing.T) {
|
|||||||
name: "no vulns",
|
name: "no vulns",
|
||||||
expectedOutput: ``,
|
expectedOutput: ``,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "happy path with vulnerability origin graph",
|
||||||
|
results: types.Results{
|
||||||
|
{
|
||||||
|
Target: "package-lock.json",
|
||||||
|
Class: "lang-pkgs",
|
||||||
|
Type: "npm",
|
||||||
|
Packages: []ftypes.Package{
|
||||||
|
{
|
||||||
|
ID: "node-fetch@1.7.3",
|
||||||
|
Name: "node-fetch",
|
||||||
|
Version: "1.7.3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "isomorphic-fetch@2.2.1",
|
||||||
|
Name: "isomorphic-fetch",
|
||||||
|
Version: "2.2.1",
|
||||||
|
DependsOn: []string{
|
||||||
|
"node-fetch@1.7.3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "fbjs@0.8.18",
|
||||||
|
Name: "fbjs",
|
||||||
|
Version: "0.8.18",
|
||||||
|
DependsOn: []string{
|
||||||
|
"isomorphic-fetch@2.2.1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "styled-components@3.1.3",
|
||||||
|
Name: "styled-components",
|
||||||
|
Version: "3.1.3",
|
||||||
|
DependsOn: []string{
|
||||||
|
"fbjs@0.8.18",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Vulnerabilities: []types.DetectedVulnerability{
|
||||||
|
{
|
||||||
|
VulnerabilityID: "CVE-2022-0235",
|
||||||
|
PkgID: "node-fetch@1.7.3",
|
||||||
|
PkgName: "node-fetch",
|
||||||
|
Vulnerability: dbTypes.Vulnerability{
|
||||||
|
Title: "foobar",
|
||||||
|
Description: "baz",
|
||||||
|
Severity: "HIGH",
|
||||||
|
},
|
||||||
|
InstalledVersion: "1.7.3",
|
||||||
|
FixedVersion: "2.6.7, 3.1.1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
VulnerabilityID: "CVE-2021-26539",
|
||||||
|
PkgID: "sanitize-html@1.20.0",
|
||||||
|
PkgName: "sanitize-html",
|
||||||
|
Vulnerability: dbTypes.Vulnerability{
|
||||||
|
Title: "foobar",
|
||||||
|
Description: "baz",
|
||||||
|
Severity: "MEDIUM",
|
||||||
|
},
|
||||||
|
InstalledVersion: "1.20.0",
|
||||||
|
FixedVersion: "2.3.1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedOutput: `┌───────────────┬────────────────┬──────────┬───────────────────┬───────────────┬────────┐
|
||||||
|
│ Library │ Vulnerability │ Severity │ Installed Version │ Fixed Version │ Title │
|
||||||
|
├───────────────┼────────────────┼──────────┼───────────────────┼───────────────┼────────┤
|
||||||
|
│ node-fetch │ CVE-2022-0235 │ HIGH │ 1.7.3 │ 2.6.7, 3.1.1 │ foobar │
|
||||||
|
├───────────────┼────────────────┼──────────┼───────────────────┼───────────────┤ │
|
||||||
|
│ sanitize-html │ CVE-2021-26539 │ MEDIUM │ 1.20.0 │ 2.3.1 │ │
|
||||||
|
└───────────────┴────────────────┴──────────┴───────────────────┴───────────────┴────────┘
|
||||||
|
|
||||||
|
Dependency Origin Tree
|
||||||
|
======================
|
||||||
|
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
|
||||||
|
└── sanitize-html@1.20.0, (MEDIUM: 1, HIGH: 0)
|
||||||
|
|
||||||
|
`,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
@@ -144,7 +230,9 @@ func TestReportWriter_Table(t *testing.T) {
|
|||||||
err := report.Write(types.Report{Results: tc.results}, report.Option{
|
err := report.Write(types.Report{Results: tc.results}, report.Option{
|
||||||
Format: "table",
|
Format: "table",
|
||||||
Output: &tableWritten,
|
Output: &tableWritten,
|
||||||
|
Tree: true,
|
||||||
IncludeNonFailures: tc.includeNonFailures,
|
IncludeNonFailures: tc.includeNonFailures,
|
||||||
|
Severities: []dbTypes.Severity{dbTypes.SeverityHigh, dbTypes.SeverityMedium},
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, tc.expectedOutput, tableWritten.String(), tc.name)
|
assert.Equal(t, tc.expectedOutput, tableWritten.String(), tc.name)
|
||||||
|
|||||||
@@ -29,11 +29,13 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Option struct {
|
type Option struct {
|
||||||
|
AppVersion string
|
||||||
|
|
||||||
Format string
|
Format string
|
||||||
Output io.Writer
|
Output io.Writer
|
||||||
|
Tree bool
|
||||||
Severities []dbTypes.Severity
|
Severities []dbTypes.Severity
|
||||||
OutputTemplate string
|
OutputTemplate string
|
||||||
AppVersion string
|
|
||||||
|
|
||||||
// For misconfigurations
|
// For misconfigurations
|
||||||
IncludeNonFailures bool
|
IncludeNonFailures bool
|
||||||
@@ -48,6 +50,7 @@ func Write(report types.Report, option Option) error {
|
|||||||
writer = &TableWriter{
|
writer = &TableWriter{
|
||||||
Output: option.Output,
|
Output: option.Output,
|
||||||
Severities: option.Severities,
|
Severities: option.Severities,
|
||||||
|
Tree: option.Tree,
|
||||||
ShowMessageOnce: &sync.Once{},
|
ShowMessageOnce: &sync.Once{},
|
||||||
IncludeNonFailures: option.IncludeNonFailures,
|
IncludeNonFailures: option.IncludeNonFailures,
|
||||||
Trace: option.Trace,
|
Trace: option.Trace,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
type DetectedVulnerability struct {
|
type DetectedVulnerability struct {
|
||||||
VulnerabilityID string `json:",omitempty"`
|
VulnerabilityID string `json:",omitempty"`
|
||||||
VendorIDs []string `json:",omitempty"`
|
VendorIDs []string `json:",omitempty"`
|
||||||
|
PkgID string `json:",omitempty"` // It is used to construct dependency graph.
|
||||||
PkgName string `json:",omitempty"`
|
PkgName string `json:",omitempty"`
|
||||||
PkgPath string `json:",omitempty"` // It will be filled in the case of language-specific packages such as egg/wheel and gemspec
|
PkgPath string `json:",omitempty"` // It will be filled in the case of language-specific packages such as egg/wheel and gemspec
|
||||||
InstalledVersion string `json:",omitempty"`
|
InstalledVersion string `json:",omitempty"`
|
||||||
|
|||||||
Reference in New Issue
Block a user