mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-21 14:50:53 -08:00
feat(sbom): add VEX support (#4053)
This commit is contained in:
@@ -62,6 +62,7 @@ trivy sbom [flags] SBOM_PATH
|
||||
-t, --template string output template
|
||||
--token string for authentication in client/server mode
|
||||
--token-header string specify a header name for token in client/server mode (default "Trivy-Token")
|
||||
--vex string [EXPERIMENTAL] file path to VEX
|
||||
--vuln-type string comma-separated list of vulnerability types (os,library) (default "os,library")
|
||||
```
|
||||
|
||||
|
||||
181
docs/docs/supply-chain/vex.md
Normal file
181
docs/docs/supply-chain/vex.md
Normal file
@@ -0,0 +1,181 @@
|
||||
# Vulnerability Exploitability Exchange (VEX)
|
||||
|
||||
!!! warning "EXPERIMENTAL"
|
||||
This feature might change without preserving backwards compatibility.
|
||||
|
||||
Trivy supports filtering detected vulnerabilities using [the Vulnerability Exploitability Exchange (VEX)](https://www.ntia.gov/files/ntia/publications/vex_one-page_summary.pdf), a standardized format for sharing and exchanging information about vulnerabilities.
|
||||
By providing VEX alongside the Software Bill of Materials (SBOM) during scanning, it is possible to filter vulnerabilities based on their status.
|
||||
Currently, Trivy supports the following two formats:
|
||||
|
||||
- [CycloneDX](https://cyclonedx.org/capabilities/vex/)
|
||||
- [OpenVEX](https://github.com/openvex/spec)
|
||||
|
||||
This is still an experimental implementation, with only minimal functionality added.
|
||||
|
||||
## CycloneDX
|
||||
There are [two VEX formats](https://cyclonedx.org/capabilities/vex/) for CycloneDX:
|
||||
|
||||
- Independent BOM and VEX BOM
|
||||
- BOM With Embedded VEX
|
||||
|
||||
Trivy only supports the Independent BOM and VEX BOM format, so you need to provide a separate VEX file alongside the SBOM.
|
||||
The input SBOM format must be in CycloneDX format.
|
||||
|
||||
The following steps are required:
|
||||
|
||||
1. Generate a CycloneDX SBOM
|
||||
2. Create a VEX based on the SBOM generated in step 1
|
||||
3. Provide the VEX when scanning the CycloneDX SBOM
|
||||
|
||||
### Generating the SBOM
|
||||
You can generate a CycloneDX SBOM with Trivy as follows:
|
||||
|
||||
```shell
|
||||
$ trivy image --format cyclonedx --output debian11.sbom.cdx debian:11
|
||||
```
|
||||
|
||||
### Create the VEX
|
||||
Next, create a VEX based on the generated SBOM.
|
||||
Multiple vulnerability statuses can be defined under `vulnerabilities`.
|
||||
Take a look at the example below.
|
||||
|
||||
```
|
||||
$ cat <<EOF > trivy.vex.cdx
|
||||
{
|
||||
"bomFormat": "CycloneDX",
|
||||
"specVersion": "1.4",
|
||||
"version": 1,
|
||||
"vulnerabilities": [
|
||||
{
|
||||
"id": "CVE-2020-8911",
|
||||
"analysis": {
|
||||
"state": "not_affected",
|
||||
"justification": "code_not_reachable",
|
||||
"response": ["will_not_fix", "update"],
|
||||
"detail": "The vulnerable function is not called"
|
||||
},
|
||||
"affects": [
|
||||
{
|
||||
"ref": "urn:cdx:3e671687-395b-41f5-a30f-a58921a69b79/1#pkg:golang/github.com/aws/aws-sdk-go@1.44.234"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
```
|
||||
|
||||
This is a VEX document in the CycloneDX format.
|
||||
The vulnerability ID, such as a CVE-ID or GHSA-ID, should be placed in `vulnerabilities.id`.
|
||||
When the `analysis.state` is set to `not_affected`, Trivy will not detect the vulnerability.
|
||||
|
||||
BOM-Links must be placed in `affects.ref`.
|
||||
The BOM-Link has the following syntax and consists of three elements:
|
||||
|
||||
```
|
||||
urn:cdx:serialNumber/version#bom-ref
|
||||
```
|
||||
|
||||
- serialNumber
|
||||
- version
|
||||
- bom-ref
|
||||
|
||||
These values must be obtained from the CycloneDX SBOM.
|
||||
Please note that while the serialNumber starts with `urn:uuid:`, the BOM-Link starts with `urn:cdx:`.
|
||||
|
||||
The `bom-ref` must contain the BOM-Ref of the package affected by the vulnerability.
|
||||
In the example above, since the Go package `github.com/aws/aws-sdk-go` is affected by CVE-2020-8911, it was necessary to specify the SBOM's BOM-Ref, `pkg:golang/github.com/aws/aws-sdk-go@1.44.234`.
|
||||
|
||||
For more details on CycloneDX VEX and BOM-Link, please refer to the following links:
|
||||
|
||||
- [CycloneDX VEX](https://cyclonedx.org/capabilities/vex/)
|
||||
- [BOM-Link](https://cyclonedx.org/capabilities/bomlink/)
|
||||
- [Examples](https://github.com/CycloneDX/bom-examples/tree/master)
|
||||
|
||||
### Scan SBOM with VEX
|
||||
Provide the VEX when scanning the CycloneDX SBOM.
|
||||
|
||||
```
|
||||
$ trivy sbom trivy.sbom.cdx --vex trivy.vex.cdx
|
||||
...
|
||||
2023-04-13T12:55:44.838+0300 INFO Filtered out the detected vulnerability {"VEX format": "CycloneDX", "vulnerability-id": "CVE-2020-8911", "status": "not_affected", "justification": "code_not_reachable"}
|
||||
|
||||
go.mod (gomod)
|
||||
==============
|
||||
Total: 1 (UNKNOWN: 0, LOW: 1, MEDIUM: 0, HIGH: 0, CRITICAL: 0)
|
||||
|
||||
┌───────────────────────────┬───────────────┬──────────┬───────────────────┬───────────────┬────────────────────────────────────────────────────────────┐
|
||||
│ Library │ Vulnerability │ Severity │ Installed Version │ Fixed Version │ Title │
|
||||
├───────────────────────────┼───────────────┼──────────┼───────────────────┼───────────────┼────────────────────────────────────────────────────────────┤
|
||||
│ github.com/aws/aws-sdk-go │ CVE-2020-8912 │ LOW │ 1.44.234 │ │ aws-sdk-go: In-band key negotiation issue in AWS S3 Crypto │
|
||||
│ │ │ │ │ │ SDK for golang... │
|
||||
│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2020-8912 │
|
||||
└───────────────────────────┴───────────────┴──────────┴───────────────────┴───────────────┴────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
CVE-2020-8911 is no longer shown as it is filtered out according to the given CycloneDX VEX document.
|
||||
|
||||
## OpenVEX
|
||||
Trivy also supports [OpenVEX](https://github.com/openvex/spec) that is designed to be minimal, compliant, interoperable, and embeddable.
|
||||
Since OpenVEX aims to be SBOM format agnostic, both CycloneDX and SPDX formats are available for use as input SBOMs in Trivy.
|
||||
|
||||
The following steps are required:
|
||||
|
||||
1. Generate a SBOM (CycloneDX or SPDX)
|
||||
2. Create a VEX based on the SBOM generated in step 1
|
||||
3. Provide the VEX when scanning the SBOM
|
||||
|
||||
### Generating the SBOM
|
||||
You can generate a CycloneDX or SPDX SBOM with Trivy as follows:
|
||||
|
||||
```shell
|
||||
$ trivy image --format spdx-json --output debian11.spdx.json debian:11
|
||||
```
|
||||
|
||||
### Create the VEX
|
||||
Please see also [the example](https://github.com/openvex/examples).
|
||||
The product identifiers differ depending on the SBOM format the VEX references.
|
||||
|
||||
- SPDX: [Package URL (PURL)](https://github.com/package-url/purl-spec)
|
||||
- CycloneDX: [BOM-Link](https://cyclonedx.org/capabilities/bomlink/)
|
||||
|
||||
```
|
||||
$ cat <<EOF > trivy.openvex
|
||||
{
|
||||
"@context": "https://openvex.dev/ns",
|
||||
"@id": "https://openvex.dev/docs/public/vex-2e67563e128250cbcb3e98930df948dd053e43271d70dc50cfa22d57e03fe96f",
|
||||
"author": "Aqua Security",
|
||||
"timestamp": "2023-01-16T19:07:16.853479631-06:00",
|
||||
"version": "1",
|
||||
"statements": [
|
||||
{
|
||||
"vulnerability": "CVE-2019-8457",
|
||||
"products": [
|
||||
"pkg:deb/debian/libdb5.3@5.3.28+dfsg1-0.8?arch=arm64\u0026distro=debian-11.6"
|
||||
],
|
||||
"status": "not_affected",
|
||||
"justification": "vulnerable_code_not_in_execute_path"
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
```
|
||||
|
||||
In the above example, PURLs, located in `packages.externalRefs.referenceLocator` are used since the input SBOM format is SPDX.
|
||||
|
||||
As for CycloneDX BOM-Link, please reference [the CycloneDX section](#cyclonedx).
|
||||
|
||||
### Scan SBOM with VEX
|
||||
Provide the VEX when scanning the SBOM.
|
||||
|
||||
```
|
||||
$ trivy sbom debian11.spdx.json --vex trivy.openvex
|
||||
...
|
||||
2023-04-26T17:56:05.358+0300 INFO Filtered out the detected vulnerability {"VEX format": "OpenVEX", "vulnerability-id": "CVE-2019-8457", "status": "not_affected", "justification": "vulnerable_code_not_in_execute_path"}
|
||||
|
||||
debian11.spdx.json (debian 11.6)
|
||||
================================
|
||||
Total: 80 (UNKNOWN: 0, LOW: 58, MEDIUM: 6, HIGH: 16, CRITICAL: 0)
|
||||
```
|
||||
|
||||
CVE-2019-8457 is no longer shown as it is filtered out according to the given OpenVEX document.
|
||||
3
go.mod
3
go.mod
@@ -72,12 +72,14 @@ require (
|
||||
github.com/open-policy-agent/opa v0.45.0
|
||||
github.com/opencontainers/go-digest v1.0.0
|
||||
github.com/opencontainers/image-spec v1.1.0-rc2.0.20221020182949-4df8887994e8
|
||||
github.com/openvex/go-vex v0.2.0
|
||||
github.com/owenrumney/go-sarif/v2 v2.1.2
|
||||
github.com/package-url/packageurl-go v0.1.1-0.20220428063043-89078438f170
|
||||
github.com/samber/lo v1.37.0
|
||||
github.com/saracen/walker v0.1.3
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.5.0
|
||||
github.com/sigstore/rekor v1.1.0
|
||||
github.com/sirupsen/logrus v1.9.0
|
||||
github.com/sosedoff/gitkit v0.3.0
|
||||
github.com/spdx/tools-golang v0.5.0
|
||||
github.com/spf13/cast v1.5.0
|
||||
@@ -327,7 +329,6 @@ require (
|
||||
github.com/sergi/go-diff v1.2.0 // indirect
|
||||
github.com/shibumi/go-pathspec v1.3.0 // indirect
|
||||
github.com/shopspring/decimal v1.2.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.0 // indirect
|
||||
github.com/skeema/knownhosts v1.1.0 // indirect
|
||||
github.com/spf13/afero v1.9.3 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
|
||||
4
go.sum
4
go.sum
@@ -1427,6 +1427,8 @@ github.com/opencontainers/selinux v1.11.0 h1:+5Zbo97w3Lbmb3PeqQtpmTkMwsW5nRI3YaL
|
||||
github.com/opencontainers/selinux v1.11.0/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec=
|
||||
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
|
||||
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
|
||||
github.com/openvex/go-vex v0.2.0 h1:7Q6VzdpZSZzYUyXB1dio/9LCGHp1iL3JldC+hMsbFg0=
|
||||
github.com/openvex/go-vex v0.2.0/go.mod h1:jYmYbhQAO/0hquryXND/jMVDBcob8/KkVgzUEUAHsFI=
|
||||
github.com/owenrumney/go-sarif v1.1.1/go.mod h1:dNDiPlF04ESR/6fHlPyq7gHKmrM0sHUvAGjsoh8ZH0U=
|
||||
github.com/owenrumney/go-sarif/v2 v2.1.2 h1:PMDK7tXShJ9zsB7bfvlpADH5NEw1dfA9xwU8Xtdj73U=
|
||||
github.com/owenrumney/go-sarif/v2 v2.1.2/go.mod h1:MSqMMx9WqlBSY7pXoOZWgEsVB4FDNfhcaXDA1j6Sr+w=
|
||||
@@ -1510,7 +1512,7 @@ github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
|
||||
github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rubenv/sql-migrate v1.2.0 h1:fOXMPLMd41sK7Tg75SXDec15k3zg5WNV6SjuDRiNfcU=
|
||||
github.com/rubenv/sql-migrate v1.2.0/go.mod h1:Z5uVnq7vrIrPmHbVFfR4YLHRZquxeHpckCnRq0P/K9Y=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
|
||||
@@ -68,9 +68,9 @@ func TestSBOM(t *testing.T) {
|
||||
{
|
||||
Target: "testdata/fixtures/sbom/centos-7-spdx.txt (centos 7.6.1810)",
|
||||
Vulnerabilities: []types.DetectedVulnerability{
|
||||
{Ref: "pkg:rpm/centos/bash@4.2.46-31.el7?arch=x86_64&distro=centos-7.6.1810"},
|
||||
{Ref: "pkg:rpm/centos/openssl-libs@1.0.2k-16.el7?arch=x86_64&epoch=1&distro=centos-7.6.1810"},
|
||||
{Ref: "pkg:rpm/centos/openssl-libs@1.0.2k-16.el7?arch=x86_64&epoch=1&distro=centos-7.6.1810"},
|
||||
{PkgRef: "pkg:rpm/centos/bash@4.2.46-31.el7?arch=x86_64&distro=centos-7.6.1810"},
|
||||
{PkgRef: "pkg:rpm/centos/openssl-libs@1.0.2k-16.el7?arch=x86_64&epoch=1&distro=centos-7.6.1810"},
|
||||
{PkgRef: "pkg:rpm/centos/openssl-libs@1.0.2k-16.el7?arch=x86_64&epoch=1&distro=centos-7.6.1810"},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -91,9 +91,9 @@ func TestSBOM(t *testing.T) {
|
||||
{
|
||||
Target: "testdata/fixtures/sbom/centos-7-spdx.json (centos 7.6.1810)",
|
||||
Vulnerabilities: []types.DetectedVulnerability{
|
||||
{Ref: "pkg:rpm/centos/bash@4.2.46-31.el7?arch=x86_64&distro=centos-7.6.1810"},
|
||||
{Ref: "pkg:rpm/centos/openssl-libs@1.0.2k-16.el7?arch=x86_64&epoch=1&distro=centos-7.6.1810"},
|
||||
{Ref: "pkg:rpm/centos/openssl-libs@1.0.2k-16.el7?arch=x86_64&epoch=1&distro=centos-7.6.1810"},
|
||||
{PkgRef: "pkg:rpm/centos/bash@4.2.46-31.el7?arch=x86_64&distro=centos-7.6.1810"},
|
||||
{PkgRef: "pkg:rpm/centos/openssl-libs@1.0.2k-16.el7?arch=x86_64&epoch=1&distro=centos-7.6.1810"},
|
||||
{PkgRef: "pkg:rpm/centos/openssl-libs@1.0.2k-16.el7?arch=x86_64&epoch=1&distro=centos-7.6.1810"},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -107,7 +107,13 @@ func TestSBOM(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
osArgs := []string{
|
||||
"--cache-dir", cacheDir, "sbom", "-q", "--skip-db-update", "--format", tt.args.format,
|
||||
"--cache-dir",
|
||||
cacheDir,
|
||||
"sbom",
|
||||
"-q",
|
||||
"--skip-db-update",
|
||||
"--format",
|
||||
tt.args.format,
|
||||
}
|
||||
|
||||
// Set up the output file
|
||||
@@ -154,7 +160,7 @@ func compareSBOMReports(t *testing.T, wantFile, gotFile string, overrideWant typ
|
||||
for i, result := range overrideWant.Results {
|
||||
want.Results[i].Target = result.Target
|
||||
for j, vuln := range result.Vulnerabilities {
|
||||
want.Results[i].Vulnerabilities[j].Ref = vuln.Ref
|
||||
want.Results[i].Vulnerabilities[j].PkgRef = vuln.PkgRef
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -87,6 +87,7 @@ nav:
|
||||
- SBOM: docs/supply-chain/attestation/sbom.md
|
||||
- Cosign Vulnerability Scan Record: docs/supply-chain/attestation/vuln.md
|
||||
- SBOM Attestation in Rekor: docs/supply-chain/attestation/rekor.md
|
||||
- VEX: docs/supply-chain/vex.md
|
||||
- Compliance:
|
||||
- Reports: docs/compliance/compliance.md
|
||||
- Advanced:
|
||||
|
||||
@@ -6,18 +6,13 @@ import (
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
||||
|
||||
"github.com/aquasecurity/tml"
|
||||
|
||||
"github.com/aquasecurity/trivy/pkg/flag"
|
||||
|
||||
"github.com/aquasecurity/trivy/pkg/report"
|
||||
|
||||
"github.com/aquasecurity/trivy/pkg/result"
|
||||
|
||||
"github.com/aquasecurity/defsec/pkg/scan"
|
||||
"github.com/aquasecurity/tml"
|
||||
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
||||
"github.com/aquasecurity/trivy/pkg/flag"
|
||||
"github.com/aquasecurity/trivy/pkg/report"
|
||||
pkgReport "github.com/aquasecurity/trivy/pkg/report"
|
||||
"github.com/aquasecurity/trivy/pkg/result"
|
||||
"github.com/aquasecurity/trivy/pkg/types"
|
||||
)
|
||||
|
||||
@@ -70,16 +65,7 @@ func Write(rep *Report, opt flag.Options, fromCache bool) error {
|
||||
for _, resultsAtTime := range rep.Results {
|
||||
for _, res := range resultsAtTime.Results {
|
||||
resCopy := res
|
||||
if err := result.Filter(
|
||||
ctx,
|
||||
&resCopy,
|
||||
opt.Severities,
|
||||
false,
|
||||
false,
|
||||
"",
|
||||
"",
|
||||
nil,
|
||||
); err != nil {
|
||||
if err := result.FilterResult(ctx, &resCopy, result.FilterOption{Severities: opt.Severities}); err != nil {
|
||||
return err
|
||||
}
|
||||
sort.Slice(resCopy.Misconfigurations, func(i, j int) bool {
|
||||
|
||||
@@ -271,16 +271,20 @@ func (r *runner) scanArtifact(ctx context.Context, opts flag.Options, initialize
|
||||
}
|
||||
|
||||
func (r *runner) Filter(ctx context.Context, opts flag.Options, report types.Report) (types.Report, error) {
|
||||
results := report.Results
|
||||
|
||||
// Filter results
|
||||
for i := range results {
|
||||
err := result.Filter(ctx, &results[i], opts.Severities, opts.IgnoreUnfixed, opts.IncludeNonFailures,
|
||||
opts.IgnoreFile, opts.IgnorePolicy, opts.IgnoredLicenses)
|
||||
err := result.Filter(ctx, report, result.FilterOption{
|
||||
Severities: opts.Severities,
|
||||
IgnoreUnfixed: opts.IgnoreUnfixed,
|
||||
IncludeNonFailures: opts.IncludeNonFailures,
|
||||
IgnoreFile: opts.IgnoreFile,
|
||||
PolicyFile: opts.IgnorePolicy,
|
||||
IgnoreLicenses: opts.IgnoredLicenses,
|
||||
VEXPath: opts.VEXPath,
|
||||
})
|
||||
if err != nil {
|
||||
return types.Report{}, xerrors.Errorf("unable to filter vulnerabilities: %w", err)
|
||||
}
|
||||
return types.Report{}, xerrors.Errorf("filtering error: %w", err)
|
||||
}
|
||||
|
||||
return report, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ func detect(driver Driver, libs []ftypes.Package) ([]types.DetectedVulnerability
|
||||
for i := range vulns {
|
||||
vulns[i].Layer = lib.Layer
|
||||
vulns[i].PkgPath = lib.FilePath
|
||||
vulns[i].Ref = lib.Ref
|
||||
vulns[i].PkgRef = lib.Ref
|
||||
}
|
||||
vulnerabilities = append(vulnerabilities, vulns...)
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ func (s *Scanner) Detect(osVer string, _ *ftypes.Repository, pkgs []ftypes.Packa
|
||||
PkgName: pkg.Name,
|
||||
InstalledVersion: installed,
|
||||
FixedVersion: fixedVersion.String(),
|
||||
Ref: pkg.Ref,
|
||||
PkgRef: pkg.Ref,
|
||||
Layer: pkg.Layer,
|
||||
DataSource: adv.DataSource,
|
||||
Custom: adv.Custom,
|
||||
|
||||
@@ -130,7 +130,7 @@ func (s *Scanner) Detect(osVer string, repo *ftypes.Repository, pkgs []ftypes.Pa
|
||||
InstalledVersion: utils.FormatVersion(pkg),
|
||||
FixedVersion: adv.FixedVersion,
|
||||
Layer: pkg.Layer,
|
||||
Ref: pkg.Ref,
|
||||
PkgRef: pkg.Ref,
|
||||
Custom: adv.Custom,
|
||||
DataSource: adv.DataSource,
|
||||
})
|
||||
|
||||
@@ -105,7 +105,7 @@ func (s *Scanner) Detect(osVer string, _ *ftypes.Repository, pkgs []ftypes.Packa
|
||||
PkgName: pkg.Name,
|
||||
InstalledVersion: installed,
|
||||
FixedVersion: adv.FixedVersion,
|
||||
Ref: pkg.Ref,
|
||||
PkgRef: pkg.Ref,
|
||||
Layer: pkg.Layer,
|
||||
Custom: adv.Custom,
|
||||
DataSource: adv.DataSource,
|
||||
|
||||
@@ -82,7 +82,7 @@ func (s *Scanner) Detect(_ string, _ *ftypes.Repository, pkgs []ftypes.Package)
|
||||
InstalledVersion: installed,
|
||||
FixedVersion: adv.FixedVersion,
|
||||
Layer: pkg.Layer,
|
||||
Ref: pkg.Ref,
|
||||
PkgRef: pkg.Ref,
|
||||
Custom: adv.Custom,
|
||||
DataSource: adv.DataSource,
|
||||
})
|
||||
|
||||
@@ -104,7 +104,7 @@ func (s *Scanner) Detect(osVer string, _ *ftypes.Repository, pkgs []ftypes.Packa
|
||||
PkgName: pkg.Name,
|
||||
InstalledVersion: utils.FormatVersion(pkg),
|
||||
FixedVersion: adv.FixedVersion,
|
||||
Ref: pkg.Ref,
|
||||
PkgRef: pkg.Ref,
|
||||
Layer: pkg.Layer,
|
||||
Custom: adv.Custom,
|
||||
DataSource: adv.DataSource,
|
||||
|
||||
@@ -52,7 +52,7 @@ func (s *Scanner) Detect(osVer string, _ *ftypes.Repository, pkgs []ftypes.Packa
|
||||
VulnerabilityID: adv.VulnerabilityID,
|
||||
PkgName: pkg.Name,
|
||||
InstalledVersion: utils.FormatVersion(pkg),
|
||||
Ref: pkg.Ref,
|
||||
PkgRef: pkg.Ref,
|
||||
Layer: pkg.Layer,
|
||||
DataSource: adv.DataSource,
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ func (s *Scanner) Detect(osVer string, _ *ftypes.Repository, pkgs []ftypes.Packa
|
||||
PkgID: pkg.ID,
|
||||
PkgName: pkg.Name,
|
||||
InstalledVersion: installed,
|
||||
Ref: pkg.Ref,
|
||||
PkgRef: pkg.Ref,
|
||||
Layer: pkg.Layer,
|
||||
Custom: adv.Custom,
|
||||
DataSource: adv.DataSource,
|
||||
|
||||
@@ -80,7 +80,7 @@ func (s *Scanner) Detect(osVer string, _ *ftypes.Repository, pkgs []ftypes.Packa
|
||||
PkgID: pkg.ID,
|
||||
PkgName: pkg.Name,
|
||||
InstalledVersion: installed,
|
||||
Ref: pkg.Ref,
|
||||
PkgRef: pkg.Ref,
|
||||
Layer: pkg.Layer,
|
||||
Custom: adv.Custom,
|
||||
DataSource: adv.DataSource,
|
||||
|
||||
@@ -158,7 +158,7 @@ func (s *Scanner) detect(osVer string, pkg ftypes.Package) ([]types.DetectedVuln
|
||||
PkgID: pkg.ID,
|
||||
PkgName: pkg.Name,
|
||||
InstalledVersion: utils.FormatVersion(pkg),
|
||||
Ref: pkg.Ref,
|
||||
PkgRef: pkg.Ref,
|
||||
Layer: pkg.Layer,
|
||||
SeveritySource: vulnerability.RedHat,
|
||||
Vulnerability: dbTypes.Vulnerability{
|
||||
|
||||
@@ -91,7 +91,7 @@ func (s *Scanner) Detect(osVer string, _ *ftypes.Repository, pkgs []ftypes.Packa
|
||||
PkgName: pkg.Name,
|
||||
InstalledVersion: installed,
|
||||
FixedVersion: fixedVersion.String(),
|
||||
Ref: pkg.Ref,
|
||||
PkgRef: pkg.Ref,
|
||||
Layer: pkg.Layer,
|
||||
DataSource: adv.DataSource,
|
||||
Custom: adv.Custom,
|
||||
|
||||
@@ -132,7 +132,7 @@ func (s *Scanner) Detect(osVer string, _ *ftypes.Repository, pkgs []ftypes.Packa
|
||||
PkgID: pkg.ID,
|
||||
PkgName: pkg.Name,
|
||||
InstalledVersion: installed,
|
||||
Ref: pkg.Ref,
|
||||
PkgRef: pkg.Ref,
|
||||
Layer: pkg.Layer,
|
||||
Custom: adv.Custom,
|
||||
DataSource: adv.DataSource,
|
||||
|
||||
@@ -118,7 +118,7 @@ func (s *Scanner) Detect(osVer string, _ *ftypes.Repository, pkgs []ftypes.Packa
|
||||
PkgName: pkg.Name,
|
||||
InstalledVersion: utils.FormatVersion(pkg),
|
||||
FixedVersion: adv.FixedVersion,
|
||||
Ref: pkg.Ref,
|
||||
PkgRef: pkg.Ref,
|
||||
Layer: pkg.Layer,
|
||||
Custom: adv.Custom,
|
||||
DataSource: adv.DataSource,
|
||||
|
||||
@@ -81,7 +81,7 @@ func (s *Scanner) Detect(_ string, _ *ftypes.Repository, pkgs []ftypes.Package)
|
||||
InstalledVersion: installed,
|
||||
FixedVersion: adv.FixedVersion,
|
||||
Layer: pkg.Layer,
|
||||
Ref: pkg.Ref,
|
||||
PkgRef: pkg.Ref,
|
||||
Custom: adv.Custom,
|
||||
DataSource: adv.DataSource,
|
||||
})
|
||||
|
||||
@@ -21,22 +21,29 @@ var (
|
||||
Usage: "deprecated",
|
||||
Deprecated: true,
|
||||
}
|
||||
VEXFlag = Flag{
|
||||
Name: "vex",
|
||||
ConfigName: "sbom.vex",
|
||||
Value: "",
|
||||
Usage: "[EXPERIMENTAL] file path to VEX",
|
||||
}
|
||||
)
|
||||
|
||||
type SBOMFlagGroup struct {
|
||||
ArtifactType *Flag // deprecated
|
||||
SBOMFormat *Flag // deprecated
|
||||
VEXPath *Flag
|
||||
}
|
||||
|
||||
type SBOMOptions struct {
|
||||
ArtifactType string // deprecated
|
||||
SBOMFormat string // deprecated
|
||||
VEXPath string
|
||||
}
|
||||
|
||||
func NewSBOMFlagGroup() *SBOMFlagGroup {
|
||||
return &SBOMFlagGroup{
|
||||
ArtifactType: &ArtifactTypeFlag,
|
||||
SBOMFormat: &SBOMFormatFlag,
|
||||
VEXPath: &VEXFlag,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +52,11 @@ func (f *SBOMFlagGroup) Name() string {
|
||||
}
|
||||
|
||||
func (f *SBOMFlagGroup) Flags() []*Flag {
|
||||
return []*Flag{f.ArtifactType, f.SBOMFormat}
|
||||
return []*Flag{
|
||||
f.ArtifactType,
|
||||
f.SBOMFormat,
|
||||
f.VEXPath,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *SBOMFlagGroup) ToOptions() (SBOMOptions, error) {
|
||||
@@ -58,5 +69,7 @@ func (f *SBOMFlagGroup) ToOptions() (SBOMOptions, error) {
|
||||
return SBOMOptions{}, xerrors.New("'--artifact-type' and '--sbom-format' are no longer available")
|
||||
}
|
||||
|
||||
return SBOMOptions{}, nil
|
||||
return SBOMOptions{
|
||||
VEXPath: getString(f.VEXPath),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -1304,8 +1304,8 @@ func easyjson6601e8cdDecodeGithubComAquasecurityTrivyPkgTypes(in *jlexer.Lexer,
|
||||
out.SeveritySource = types2.SourceID(in.String())
|
||||
case "PrimaryURL":
|
||||
out.PrimaryURL = string(in.String())
|
||||
case "Ref":
|
||||
out.Ref = string(in.String())
|
||||
case "PkgRef":
|
||||
out.PkgRef = string(in.String())
|
||||
case "DataSource":
|
||||
if in.IsNull() {
|
||||
in.Skip()
|
||||
@@ -1559,15 +1559,15 @@ func easyjson6601e8cdEncodeGithubComAquasecurityTrivyPkgTypes(out *jwriter.Write
|
||||
}
|
||||
out.String(string(in.PrimaryURL))
|
||||
}
|
||||
if in.Ref != "" {
|
||||
const prefix string = ",\"Ref\":"
|
||||
if in.PkgRef != "" {
|
||||
const prefix string = ",\"PkgRef\":"
|
||||
if first {
|
||||
first = false
|
||||
out.RawString(prefix[1:])
|
||||
} else {
|
||||
out.RawString(prefix)
|
||||
}
|
||||
out.String(string(in.Ref))
|
||||
out.String(string(in.PkgRef))
|
||||
}
|
||||
if in.DataSource != nil {
|
||||
const prefix string = ",\"DataSource\":"
|
||||
@@ -1953,7 +1953,7 @@ func easyjson6601e8cdDecodeGithubComAquasecurityTrivyPkgFanalTypes(in *jlexer.Le
|
||||
}
|
||||
easyjson6601e8cdDecodeGithubComAquasecurityTrivyPkgFanalTypes7(in, out.BuildInfo)
|
||||
}
|
||||
case "Ref":
|
||||
case "PkgRef":
|
||||
out.Ref = string(in.String())
|
||||
case "Indirect":
|
||||
out.Indirect = bool(in.Bool())
|
||||
@@ -2134,7 +2134,7 @@ func easyjson6601e8cdEncodeGithubComAquasecurityTrivyPkgFanalTypes(out *jwriter.
|
||||
easyjson6601e8cdEncodeGithubComAquasecurityTrivyPkgFanalTypes7(out, *in.BuildInfo)
|
||||
}
|
||||
if in.Ref != "" {
|
||||
const prefix string = ",\"Ref\":"
|
||||
const prefix string = ",\"PkgRef\":"
|
||||
if first {
|
||||
first = false
|
||||
out.RawString(prefix[1:])
|
||||
|
||||
@@ -9,9 +9,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/open-policy-agent/opa/rego"
|
||||
"github.com/samber/lo"
|
||||
"golang.org/x/exp/maps"
|
||||
"golang.org/x/exp/slices"
|
||||
"golang.org/x/xerrors"
|
||||
@@ -20,6 +19,7 @@ import (
|
||||
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
||||
"github.com/aquasecurity/trivy/pkg/log"
|
||||
"github.com/aquasecurity/trivy/pkg/types"
|
||||
"github.com/aquasecurity/trivy/pkg/vex"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -27,19 +27,43 @@ const (
|
||||
DefaultIgnoreFile = ".trivyignore"
|
||||
)
|
||||
|
||||
// Filter filters out the vulnerabilities
|
||||
func Filter(ctx context.Context, result *types.Result, severities []dbTypes.Severity, ignoreUnfixed, includeNonFailures bool,
|
||||
ignoreFile, policyFile string, ignoreLicenses []string) error {
|
||||
ignoredIDs := getIgnoredIDs(ignoreFile)
|
||||
type FilterOption struct {
|
||||
Severities []dbTypes.Severity
|
||||
IgnoreUnfixed bool
|
||||
IncludeNonFailures bool
|
||||
IgnoreFile string
|
||||
PolicyFile string
|
||||
IgnoreLicenses []string
|
||||
VEXPath string
|
||||
}
|
||||
|
||||
filteredVulns := filterVulnerabilities(result.Vulnerabilities, severities, ignoreUnfixed, ignoredIDs)
|
||||
misconfSummary, filteredMisconfs := filterMisconfigurations(result.Misconfigurations, severities, includeNonFailures, ignoredIDs)
|
||||
result.Secrets = filterSecrets(result.Secrets, severities, ignoredIDs)
|
||||
result.Licenses = filterLicenses(result.Licenses, severities, ignoreLicenses)
|
||||
// Filter filters out the report
|
||||
func Filter(ctx context.Context, report types.Report, opt FilterOption) error {
|
||||
// Filter out vulnerabilities based on the given VEX document.
|
||||
if err := filterByVEX(report, opt); err != nil {
|
||||
return xerrors.Errorf("VEX error: %w", err)
|
||||
}
|
||||
|
||||
if policyFile != "" {
|
||||
for i := range report.Results {
|
||||
if err := FilterResult(ctx, &report.Results[i], opt); err != nil {
|
||||
return xerrors.Errorf("unable to filter vulnerabilities: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FilterResult filters out the result
|
||||
func FilterResult(ctx context.Context, result *types.Result, opt FilterOption) error {
|
||||
ignoredIDs := getIgnoredIDs(opt.IgnoreFile)
|
||||
|
||||
filteredVulns := filterVulnerabilities(result.Vulnerabilities, opt.Severities, opt.IgnoreUnfixed, ignoredIDs, opt.VEXPath)
|
||||
misconfSummary, filteredMisconfs := filterMisconfigurations(result.Misconfigurations, opt.Severities, opt.IncludeNonFailures, ignoredIDs)
|
||||
result.Secrets = filterSecrets(result.Secrets, opt.Severities, ignoredIDs)
|
||||
result.Licenses = filterLicenses(result.Licenses, opt.Severities, opt.IgnoreLicenses)
|
||||
|
||||
if opt.PolicyFile != "" {
|
||||
var err error
|
||||
filteredVulns, filteredMisconfs, err = applyPolicy(ctx, filteredVulns, filteredMisconfs, policyFile)
|
||||
filteredVulns, filteredMisconfs, err = applyPolicy(ctx, filteredVulns, filteredMisconfs, opt.PolicyFile)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("failed to apply the policy: %w", err)
|
||||
}
|
||||
@@ -53,9 +77,30 @@ func Filter(ctx context.Context, result *types.Result, severities []dbTypes.Seve
|
||||
return nil
|
||||
}
|
||||
|
||||
func filterVulnerabilities(vulns []types.DetectedVulnerability, severities []dbTypes.Severity,
|
||||
ignoreUnfixed bool, ignoredIDs []string) []types.DetectedVulnerability {
|
||||
// filterByVEX determines whether a detected vulnerability should be filtered out based on the provided VEX document.
|
||||
// If the VEX document is not nil and the vulnerability is either not affected or fixed according to the VEX statement,
|
||||
// the vulnerability is filtered out.
|
||||
func filterByVEX(report types.Report, opt FilterOption) error {
|
||||
vexDoc, err := vex.New(opt.VEXPath, report)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if vexDoc == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for i, result := range report.Results {
|
||||
if len(result.Vulnerabilities) == 0 {
|
||||
continue
|
||||
}
|
||||
report.Results[i].Vulnerabilities = vexDoc.Filter(result.Vulnerabilities)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func filterVulnerabilities(vulns []types.DetectedVulnerability, severities []dbTypes.Severity, ignoreUnfixed bool,
|
||||
ignoredIDs []string, vexPath string) []types.DetectedVulnerability {
|
||||
uniqVulns := make(map[string]types.DetectedVulnerability)
|
||||
|
||||
for _, vuln := range vulns {
|
||||
if vuln.Severity == "" {
|
||||
vuln.Severity = dbTypes.SeverityUnknown.String()
|
||||
@@ -136,6 +181,9 @@ func filterSecrets(secrets []ftypes.SecretFinding, severities []dbTypes.Severity
|
||||
}
|
||||
|
||||
func filterLicenses(licenses []types.DetectedLicense, severities []dbTypes.Severity, ignoredLicenses []string) []types.DetectedLicense {
|
||||
if len(licenses) == 0 {
|
||||
return nil
|
||||
}
|
||||
return lo.Filter(licenses, func(l types.DetectedLicense, _ int) bool {
|
||||
// Skip the license if it is included in ignored licenses.
|
||||
if slices.Contains(ignoredLicenses, l.Name) {
|
||||
|
||||
@@ -7,19 +7,210 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/aquasecurity/trivy/pkg/result"
|
||||
|
||||
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
|
||||
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
||||
"github.com/aquasecurity/trivy/pkg/result"
|
||||
"github.com/aquasecurity/trivy/pkg/types"
|
||||
)
|
||||
|
||||
func TestClient_Filter(t *testing.T) {
|
||||
func TestFilter(t *testing.T) {
|
||||
type args struct {
|
||||
report types.Report
|
||||
severities []dbTypes.Severity
|
||||
vexPath string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want types.Report
|
||||
}{
|
||||
{
|
||||
name: "severities",
|
||||
args: args{
|
||||
report: types.Report{
|
||||
Results: []types.Result{
|
||||
{
|
||||
Vulnerabilities: []types.DetectedVulnerability{
|
||||
{
|
||||
VulnerabilityID: "CVE-2019-0001",
|
||||
PkgName: "foo",
|
||||
InstalledVersion: "1.2.3",
|
||||
FixedVersion: "1.2.4",
|
||||
Vulnerability: dbTypes.Vulnerability{
|
||||
Severity: dbTypes.SeverityLow.String(),
|
||||
},
|
||||
},
|
||||
{
|
||||
VulnerabilityID: "CVE-2019-0002",
|
||||
PkgName: "bar",
|
||||
InstalledVersion: "1.2.3",
|
||||
FixedVersion: "1.2.4",
|
||||
Vulnerability: dbTypes.Vulnerability{
|
||||
Severity: dbTypes.SeverityCritical.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
Misconfigurations: []types.DetectedMisconfiguration{
|
||||
{
|
||||
Type: ftypes.Kubernetes,
|
||||
ID: "ID100",
|
||||
Title: "Bad Deployment",
|
||||
Message: "something bad",
|
||||
Severity: dbTypes.SeverityCritical.String(),
|
||||
Status: types.StatusFailure,
|
||||
},
|
||||
{
|
||||
Type: ftypes.Kubernetes,
|
||||
ID: "ID200",
|
||||
Title: "Bad Pod",
|
||||
Message: "something bad",
|
||||
Severity: dbTypes.SeverityMedium.String(),
|
||||
Status: types.StatusPassed,
|
||||
},
|
||||
},
|
||||
Secrets: []ftypes.SecretFinding{
|
||||
{
|
||||
RuleID: "generic-critical-rule",
|
||||
Severity: dbTypes.SeverityCritical.String(),
|
||||
Title: "Critical Secret should pass filter",
|
||||
StartLine: 1,
|
||||
EndLine: 2,
|
||||
Match: "*****",
|
||||
},
|
||||
{
|
||||
RuleID: "generic-low-rule",
|
||||
Severity: dbTypes.SeverityLow.String(),
|
||||
Title: "Low Secret should be ignored",
|
||||
StartLine: 3,
|
||||
EndLine: 4,
|
||||
Match: "*****",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
severities: []dbTypes.Severity{
|
||||
dbTypes.SeverityCritical,
|
||||
},
|
||||
},
|
||||
want: types.Report{
|
||||
Results: []types.Result{
|
||||
{
|
||||
Vulnerabilities: []types.DetectedVulnerability{
|
||||
{
|
||||
VulnerabilityID: "CVE-2019-0002",
|
||||
PkgName: "bar",
|
||||
InstalledVersion: "1.2.3",
|
||||
FixedVersion: "1.2.4",
|
||||
Vulnerability: dbTypes.Vulnerability{
|
||||
Severity: dbTypes.SeverityCritical.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
MisconfSummary: &types.MisconfSummary{
|
||||
Successes: 0,
|
||||
Failures: 1,
|
||||
Exceptions: 0,
|
||||
},
|
||||
Misconfigurations: []types.DetectedMisconfiguration{
|
||||
{
|
||||
Type: ftypes.Kubernetes,
|
||||
ID: "ID100",
|
||||
Title: "Bad Deployment",
|
||||
Message: "something bad",
|
||||
Severity: dbTypes.SeverityCritical.String(),
|
||||
Status: types.StatusFailure,
|
||||
},
|
||||
},
|
||||
Secrets: []ftypes.SecretFinding{
|
||||
{
|
||||
RuleID: "generic-critical-rule",
|
||||
Severity: dbTypes.SeverityCritical.String(),
|
||||
Title: "Critical Secret should pass filter",
|
||||
StartLine: 1,
|
||||
EndLine: 2,
|
||||
Match: "*****",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "filter by VEX",
|
||||
args: args{
|
||||
report: types.Report{
|
||||
Results: types.Results{
|
||||
types.Result{
|
||||
Vulnerabilities: []types.DetectedVulnerability{
|
||||
{
|
||||
VulnerabilityID: "CVE-2019-0001",
|
||||
PkgName: "foo",
|
||||
PkgRef: "pkg:golang/github.com/aquasecurity/foo@1.2.3",
|
||||
InstalledVersion: "1.2.3",
|
||||
FixedVersion: "1.2.4",
|
||||
Vulnerability: dbTypes.Vulnerability{
|
||||
Severity: dbTypes.SeverityLow.String(),
|
||||
},
|
||||
},
|
||||
{
|
||||
VulnerabilityID: "CVE-2019-0001",
|
||||
PkgName: "bar",
|
||||
PkgRef: "pkg:golang/github.com/aquasecurity/bar@1.2.3",
|
||||
InstalledVersion: "1.2.3",
|
||||
FixedVersion: "1.2.4",
|
||||
Vulnerability: dbTypes.Vulnerability{
|
||||
Severity: dbTypes.SeverityCritical.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
severities: []dbTypes.Severity{
|
||||
dbTypes.SeverityCritical,
|
||||
dbTypes.SeverityHigh,
|
||||
dbTypes.SeverityMedium,
|
||||
dbTypes.SeverityLow,
|
||||
dbTypes.SeverityUnknown,
|
||||
},
|
||||
vexPath: "testdata/openvex.json",
|
||||
},
|
||||
want: types.Report{
|
||||
Results: types.Results{
|
||||
types.Result{
|
||||
Vulnerabilities: []types.DetectedVulnerability{
|
||||
{
|
||||
VulnerabilityID: "CVE-2019-0001",
|
||||
PkgName: "bar",
|
||||
PkgRef: "pkg:golang/github.com/aquasecurity/bar@1.2.3",
|
||||
InstalledVersion: "1.2.3",
|
||||
FixedVersion: "1.2.4",
|
||||
Vulnerability: dbTypes.Vulnerability{
|
||||
Severity: dbTypes.SeverityCritical.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := result.Filter(context.Background(), tt.args.report, result.FilterOption{
|
||||
Severities: tt.args.severities,
|
||||
VEXPath: tt.args.vexPath,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.want, tt.args.report)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
func TestFilterResult(t *testing.T) {
|
||||
type args struct {
|
||||
result types.Result
|
||||
//vulns []types.DetectedVulnerability
|
||||
//misconfs []types.DetectedMisconfiguration
|
||||
//secrets []ftypes.SecretFinding
|
||||
severities []dbTypes.Severity
|
||||
ignoreUnfixed bool
|
||||
ignoreFile string
|
||||
@@ -122,7 +313,11 @@ func TestClient_Filter(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
severities: []dbTypes.Severity{dbTypes.SeverityCritical, dbTypes.SeverityHigh, dbTypes.SeverityUnknown},
|
||||
severities: []dbTypes.Severity{
|
||||
dbTypes.SeverityCritical,
|
||||
dbTypes.SeverityHigh,
|
||||
dbTypes.SeverityUnknown,
|
||||
},
|
||||
ignoreUnfixed: false,
|
||||
},
|
||||
wantVulns: []types.DetectedVulnerability{
|
||||
@@ -479,7 +674,11 @@ func TestClient_Filter(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
severities: []dbTypes.Severity{dbTypes.SeverityCritical, dbTypes.SeverityHigh, dbTypes.SeverityUnknown},
|
||||
severities: []dbTypes.Severity{
|
||||
dbTypes.SeverityCritical,
|
||||
dbTypes.SeverityHigh,
|
||||
dbTypes.SeverityUnknown,
|
||||
},
|
||||
ignoreUnfixed: false,
|
||||
},
|
||||
wantVulns: []types.DetectedVulnerability{
|
||||
@@ -607,7 +806,11 @@ func TestClient_Filter(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
severities: []dbTypes.Severity{dbTypes.SeverityCritical, dbTypes.SeverityHigh, dbTypes.SeverityUnknown},
|
||||
severities: []dbTypes.Severity{
|
||||
dbTypes.SeverityCritical,
|
||||
dbTypes.SeverityHigh,
|
||||
dbTypes.SeverityUnknown,
|
||||
},
|
||||
ignoreUnfixed: false,
|
||||
},
|
||||
wantVulns: []types.DetectedVulnerability{
|
||||
@@ -666,8 +869,13 @@ func TestClient_Filter(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := result.Filter(context.Background(), &tt.args.result,
|
||||
tt.args.severities, tt.args.ignoreUnfixed, false, tt.args.ignoreFile, tt.args.policyFile, tt.args.ignoreLicenses)
|
||||
err := result.FilterResult(context.Background(), &tt.args.result, result.FilterOption{
|
||||
Severities: tt.args.severities,
|
||||
IgnoreUnfixed: tt.args.ignoreUnfixed,
|
||||
IgnoreFile: tt.args.ignoreFile,
|
||||
PolicyFile: tt.args.policyFile,
|
||||
IgnoreLicenses: tt.args.ignoreLicenses,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.wantVulns, tt.args.result.Vulnerabilities)
|
||||
assert.Equal(t, tt.wantMisconfSummary, tt.args.result.MisconfSummary)
|
||||
|
||||
17
pkg/result/testdata/openvex.json
vendored
Normal file
17
pkg/result/testdata/openvex.json
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"@context": "https://openvex.dev/ns",
|
||||
"author": "Aqua Security",
|
||||
"role": "Project Release Bot",
|
||||
"timestamp": "2023-01-16T19:07:16.853479631-06:00",
|
||||
"version": "1",
|
||||
"statements": [
|
||||
{
|
||||
"vulnerability": "CVE-2019-0001",
|
||||
"products": [
|
||||
"pkg:golang/github.com/aquasecurity/foo@1.2.3"
|
||||
],
|
||||
"status": "not_affected",
|
||||
"justification": "vulnerable_code_not_in_execute_path"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -120,7 +120,7 @@ func (e *Marshaler) MarshalVulnerabilities(report types.Report) (*cdx.BOM, error
|
||||
vulnMap := map[string]cdx.Vulnerability{}
|
||||
for _, result := range report.Results {
|
||||
for _, vuln := range result.Vulnerabilities {
|
||||
ref, err := externalRef(report.CycloneDX.SerialNumber, vuln.Ref)
|
||||
ref, err := externalRef(report.CycloneDX.SerialNumber, vuln.PkgRef)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1288,7 +1288,7 @@ func TestMarshaler_MarshalVulnerabilities(t *testing.T) {
|
||||
VulnerabilityID: "CVE-2018-20623",
|
||||
PkgName: "binutils",
|
||||
InstalledVersion: "2.30-93.el8",
|
||||
Ref: "pkg:rpm/centos/binutils@2.30-93.el8?arch=aarch64&distro=centos-8.3.2011",
|
||||
PkgRef: "pkg:rpm/centos/binutils@2.30-93.el8?arch=aarch64&distro=centos-8.3.2011",
|
||||
Layer: ftypes.Layer{
|
||||
DiffID: "sha256:d871dadfb37b53ef1ca45be04fc527562b91989991a8f545345ae3be0b93f92a",
|
||||
},
|
||||
@@ -1481,7 +1481,7 @@ func TestMarshaler_MarshalVulnerabilities(t *testing.T) {
|
||||
VulnerabilityID: "CVE-2018-20623",
|
||||
PkgName: "binutils",
|
||||
InstalledVersion: "2.30-93.el8",
|
||||
Ref: "pkg:rpm/centos/binutils@2.30-93.el8?arch=aarch64&distro=centos-8.3.2011",
|
||||
PkgRef: "pkg:rpm/centos/binutils@2.30-93.el8?arch=aarch64&distro=centos-8.3.2011",
|
||||
Layer: ftypes.Layer{
|
||||
DiffID: "sha256:d871dadfb37b53ef1ca45be04fc527562b91989991a8f545345ae3be0b93f92a",
|
||||
},
|
||||
|
||||
@@ -3,6 +3,7 @@ package cyclonedx
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -29,14 +30,22 @@ type CycloneDX struct {
|
||||
components map[string]cdx.Component
|
||||
}
|
||||
|
||||
func DecodeJSON(r io.Reader) (*cdx.BOM, error) {
|
||||
bom := cdx.NewBOM()
|
||||
decoder := cdx.NewBOMDecoder(r, cdx.BOMFileFormatJSON)
|
||||
if err := decoder.Decode(bom); err != nil {
|
||||
return nil, xerrors.Errorf("CycloneDX decode error: %w", err)
|
||||
}
|
||||
return bom, nil
|
||||
}
|
||||
|
||||
func (c *CycloneDX) UnmarshalJSON(b []byte) error {
|
||||
log.Logger.Debug("Unmarshaling CycloneDX JSON...")
|
||||
if c.SBOM == nil {
|
||||
c.SBOM = &types.SBOM{}
|
||||
}
|
||||
bom := cdx.NewBOM()
|
||||
decoder := cdx.NewBOMDecoder(bytes.NewReader(b), cdx.BOMFileFormatJSON)
|
||||
if err := decoder.Decode(bom); err != nil {
|
||||
bom, err := DecodeJSON(bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return xerrors.Errorf("CycloneDX decode error: %w", err)
|
||||
}
|
||||
|
||||
@@ -45,7 +54,7 @@ func (c *CycloneDX) UnmarshalJSON(b []byte) error {
|
||||
log.Logger.Warnf("Recommend using Trivy to generate SBOMs")
|
||||
}
|
||||
|
||||
if err := c.parseSBOM(bom); err != nil {
|
||||
if err = c.parseSBOM(bom); err != nil {
|
||||
return xerrors.Errorf("failed to parse sbom: %w", err)
|
||||
}
|
||||
|
||||
|
||||
102
pkg/sbom/sbom.go
102
pkg/sbom/sbom.go
@@ -40,65 +40,103 @@ const (
|
||||
|
||||
var ErrUnknownFormat = xerrors.New("Unknown SBOM format")
|
||||
|
||||
func DetectFormat(r io.ReadSeeker) (Format, error) {
|
||||
// Rewind the SBOM file at the end
|
||||
defer r.Seek(0, io.SeekStart)
|
||||
|
||||
type (
|
||||
cyclonedx struct {
|
||||
type cdxHeader struct {
|
||||
// XML specific field
|
||||
XMLNS string `json:"-" xml:"xmlns,attr"`
|
||||
|
||||
// JSON specific field
|
||||
BOMFormat string `json:"bomFormat" xml:"-"`
|
||||
}
|
||||
}
|
||||
|
||||
spdx struct {
|
||||
type spdxHeader struct {
|
||||
SpdxID string `json:"SPDXID"`
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Try CycloneDX JSON
|
||||
var cdxBom cyclonedx
|
||||
func IsCycloneDXJSON(r io.ReadSeeker) (bool, error) {
|
||||
if _, err := r.Seek(0, io.SeekStart); err != nil {
|
||||
return false, xerrors.Errorf("seek error: %w", err)
|
||||
}
|
||||
|
||||
var cdxBom cdxHeader
|
||||
if err := json.NewDecoder(r).Decode(&cdxBom); err == nil {
|
||||
if cdxBom.BOMFormat == "CycloneDX" {
|
||||
return FormatCycloneDXJSON, nil
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
func IsCycloneDXXML(r io.ReadSeeker) (bool, error) {
|
||||
if _, err := r.Seek(0, io.SeekStart); err != nil {
|
||||
return false, xerrors.Errorf("seek error: %w", err)
|
||||
}
|
||||
|
||||
var cdxBom cdxHeader
|
||||
if err := xml.NewDecoder(r).Decode(&cdxBom); err == nil {
|
||||
if strings.HasPrefix(cdxBom.XMLNS, "http://cyclonedx.org") {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func IsSPDXJSON(r io.ReadSeeker) (bool, error) {
|
||||
if _, err := r.Seek(0, io.SeekStart); err != nil {
|
||||
return FormatUnknown, xerrors.Errorf("seek error: %w", err)
|
||||
return false, xerrors.Errorf("seek error: %w", err)
|
||||
}
|
||||
|
||||
var spdxBom spdxHeader
|
||||
if err := json.NewDecoder(r).Decode(&spdxBom); err == nil {
|
||||
if strings.HasPrefix(spdxBom.SpdxID, "SPDX") {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func IsSPDXTV(r io.ReadSeeker) (bool, error) {
|
||||
if _, err := r.Seek(0, io.SeekStart); err != nil {
|
||||
return false, xerrors.Errorf("seek error: %w", err)
|
||||
}
|
||||
|
||||
if scanner := bufio.NewScanner(r); scanner.Scan() {
|
||||
if strings.HasPrefix(scanner.Text(), "SPDX") {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func DetectFormat(r io.ReadSeeker) (Format, error) {
|
||||
// Rewind the SBOM file at the end
|
||||
defer r.Seek(0, io.SeekStart)
|
||||
|
||||
// Try CycloneDX JSON
|
||||
if ok, err := IsCycloneDXJSON(r); err != nil {
|
||||
return FormatUnknown, err
|
||||
} else if ok {
|
||||
return FormatCycloneDXJSON, nil
|
||||
}
|
||||
|
||||
// Try CycloneDX XML
|
||||
if err := xml.NewDecoder(r).Decode(&cdxBom); err == nil {
|
||||
if strings.HasPrefix(cdxBom.XMLNS, "http://cyclonedx.org") {
|
||||
if ok, err := IsCycloneDXXML(r); err != nil {
|
||||
return FormatUnknown, err
|
||||
} else if ok {
|
||||
return FormatCycloneDXXML, nil
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := r.Seek(0, io.SeekStart); err != nil {
|
||||
return FormatUnknown, xerrors.Errorf("seek error: %w", err)
|
||||
}
|
||||
|
||||
// Try SPDX json
|
||||
var spdxBom spdx
|
||||
if err := json.NewDecoder(r).Decode(&spdxBom); err == nil {
|
||||
if strings.HasPrefix(spdxBom.SpdxID, "SPDX") {
|
||||
if ok, err := IsSPDXJSON(r); err != nil {
|
||||
return FormatUnknown, err
|
||||
} else if ok {
|
||||
return FormatSPDXJSON, nil
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := r.Seek(0, io.SeekStart); err != nil {
|
||||
return FormatUnknown, xerrors.Errorf("seek error: %w", err)
|
||||
}
|
||||
|
||||
// Try SPDX tag-value
|
||||
if scanner := bufio.NewScanner(r); scanner.Scan() {
|
||||
if strings.HasPrefix(scanner.Text(), "SPDX") {
|
||||
if ok, err := IsSPDXTV(r); err != nil {
|
||||
return FormatUnknown, err
|
||||
} else if ok {
|
||||
return FormatSPDXTV, nil
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := r.Seek(0, io.SeekStart); err != nil {
|
||||
return FormatUnknown, xerrors.Errorf("seek error: %w", err)
|
||||
|
||||
@@ -11,13 +11,19 @@ type DetectedVulnerability struct {
|
||||
VendorIDs []string `json:",omitempty"`
|
||||
PkgID string `json:",omitempty"` // It is used to construct dependency graph.
|
||||
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"` // This field is populated in the case of language-specific packages such as egg/wheel and gemspec
|
||||
InstalledVersion string `json:",omitempty"`
|
||||
FixedVersion string `json:",omitempty"`
|
||||
Layer ftypes.Layer `json:",omitempty"`
|
||||
SeveritySource types.SourceID `json:",omitempty"`
|
||||
PrimaryURL string `json:",omitempty"`
|
||||
Ref string `json:",omitempty"`
|
||||
|
||||
// PkgRef is populated only when scanning SBOM and contains the reference ID used in the SBOM.
|
||||
// It could be PURL, UUID, etc.
|
||||
// e.g.
|
||||
// - pkg:npm/acme/component@1.0.0
|
||||
// - b2a46a4b-8367-4bae-9820-95557cfe03a8
|
||||
PkgRef string `json:",omitempty"`
|
||||
|
||||
// DataSource holds where the advisory comes from
|
||||
DataSource *types.DataSource `json:",omitempty"`
|
||||
|
||||
11
pkg/vex/status.go
Normal file
11
pkg/vex/status.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package vex
|
||||
|
||||
type Status string
|
||||
|
||||
const (
|
||||
StatusNotAffected Status = "not_affected"
|
||||
StatusAffected Status = "affected"
|
||||
StatusFixed Status = "fixed"
|
||||
StatusUnderInvestigation Status = "under_investigation"
|
||||
StatusUnknown Status = "unknown"
|
||||
)
|
||||
23
pkg/vex/testdata/cyclonedx.json
vendored
Normal file
23
pkg/vex/testdata/cyclonedx.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"bomFormat": "CycloneDX",
|
||||
"specVersion": "1.4",
|
||||
"version": 1,
|
||||
"vulnerabilities": [
|
||||
{
|
||||
"id": "CVE-2018-7489",
|
||||
"source": {
|
||||
"name": "NVD",
|
||||
"url": "https://nvd.nist.gov/vuln/detail/CVE-2019-9997"
|
||||
},
|
||||
"analysis": {
|
||||
"state": "not_affected",
|
||||
"justification": "code_not_reachable"
|
||||
},
|
||||
"affects": [
|
||||
{
|
||||
"ref": "urn:cdx:3e671687-395b-41f5-a30f-a58921a69b79/1#jackson-databind-2.8.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
17
pkg/vex/testdata/openvex.cdx.json
vendored
Normal file
17
pkg/vex/testdata/openvex.cdx.json
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"@context": "https://openvex.dev/ns",
|
||||
"author": "Aqua Security",
|
||||
"role": "Project Release Bot",
|
||||
"timestamp": "2023-01-16T19:07:16.853479631-06:00",
|
||||
"version": "1",
|
||||
"statements": [
|
||||
{
|
||||
"vulnerability": "CVE-2018-7489",
|
||||
"products": [
|
||||
"urn:cdx:3e671687-395b-41f5-a30f-a58921a69b79/1#jackson-databind-2.8.0"
|
||||
],
|
||||
"status": "not_affected",
|
||||
"justification": "vulnerable_code_not_in_execute_path"
|
||||
}
|
||||
]
|
||||
}
|
||||
17
pkg/vex/testdata/openvex.json
vendored
Normal file
17
pkg/vex/testdata/openvex.json
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"@context": "https://openvex.dev/ns",
|
||||
"author": "Aqua Security",
|
||||
"role": "Project Release Bot",
|
||||
"timestamp": "2023-01-16T19:07:16.853479631-06:00",
|
||||
"version": "1",
|
||||
"statements": [
|
||||
{
|
||||
"vulnerability": "CVE-2021-44228",
|
||||
"products": [
|
||||
"pkg:maven/org.springframework.boot/spring-boot@2.6.0"
|
||||
],
|
||||
"status": "not_affected",
|
||||
"justification": "vulnerable_code_not_in_execute_path"
|
||||
}
|
||||
]
|
||||
}
|
||||
1
pkg/vex/testdata/unknown.json
vendored
Normal file
1
pkg/vex/testdata/unknown.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{unknown}
|
||||
233
pkg/vex/vex.go
Normal file
233
pkg/vex/vex.go
Normal file
@@ -0,0 +1,233 @@
|
||||
package vex
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
cdx "github.com/CycloneDX/cyclonedx-go"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
openvex "github.com/openvex/go-vex/pkg/vex"
|
||||
"github.com/samber/lo"
|
||||
"github.com/sirupsen/logrus"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/exp/slices"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
||||
"github.com/aquasecurity/trivy/pkg/log"
|
||||
"github.com/aquasecurity/trivy/pkg/sbom"
|
||||
"github.com/aquasecurity/trivy/pkg/sbom/cyclonedx"
|
||||
"github.com/aquasecurity/trivy/pkg/types"
|
||||
)
|
||||
|
||||
// VEX represents Vulnerability Exploitability eXchange. It abstracts multiple VEX formats.
|
||||
// Note: This is in the experimental stage and does not yet support many specifications.
|
||||
// The implementation may change significantly.
|
||||
type VEX interface {
|
||||
Filter([]types.DetectedVulnerability) []types.DetectedVulnerability
|
||||
}
|
||||
|
||||
type Statement struct {
|
||||
VulnerabilityID string
|
||||
Affects []string
|
||||
Status Status
|
||||
Justification string // TODO: define a type
|
||||
}
|
||||
|
||||
type OpenVEX struct {
|
||||
statements []Statement
|
||||
logger *zap.SugaredLogger
|
||||
}
|
||||
|
||||
func newOpenVEX(cycloneDX *ftypes.CycloneDX, vex openvex.VEX) VEX {
|
||||
logger := log.Logger.With(zap.String("VEX format", "OpenVEX"))
|
||||
|
||||
openvex.SortStatements(vex.Statements, lo.FromPtr(vex.Timestamp))
|
||||
|
||||
// Convert openvex.Statement to Statement
|
||||
stmts := lo.Map(vex.Statements, func(stmt openvex.Statement, index int) Statement {
|
||||
return Statement{
|
||||
// TODO: add subcomponents, etc.
|
||||
VulnerabilityID: stmt.Vulnerability,
|
||||
Affects: stmt.Products,
|
||||
Status: Status(stmt.Status),
|
||||
Justification: string(stmt.Justification),
|
||||
}
|
||||
})
|
||||
// Reverse sorted statements so that the latest statement can come first.
|
||||
stmts = lo.Reverse(stmts)
|
||||
|
||||
// If the SBOM format referenced by OpenVEX is CycloneDX
|
||||
if cycloneDX != nil {
|
||||
return &CycloneDX{
|
||||
sbom: cycloneDX,
|
||||
statements: stmts,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
return &OpenVEX{
|
||||
statements: stmts,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (v *OpenVEX) Filter(vulns []types.DetectedVulnerability) []types.DetectedVulnerability {
|
||||
return lo.Filter(vulns, func(vuln types.DetectedVulnerability, _ int) bool {
|
||||
stmt, ok := lo.Find(v.statements, func(item Statement) bool {
|
||||
return item.VulnerabilityID == vuln.VulnerabilityID
|
||||
})
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
return v.affected(vuln, stmt)
|
||||
})
|
||||
}
|
||||
|
||||
func (v *OpenVEX) affected(vuln types.DetectedVulnerability, stmt Statement) bool {
|
||||
if slices.Contains(stmt.Affects, vuln.PkgRef) &&
|
||||
(stmt.Status == StatusNotAffected || stmt.Status == StatusFixed) {
|
||||
v.logger.Infow("Filtered out the detected vulnerability", zap.String("vulnerability-id", vuln.VulnerabilityID),
|
||||
zap.String("status", string(stmt.Status)), zap.String("justification", stmt.Justification))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type CycloneDX struct {
|
||||
sbom *ftypes.CycloneDX
|
||||
statements []Statement
|
||||
logger *zap.SugaredLogger
|
||||
}
|
||||
|
||||
func newCycloneDX(sbom *ftypes.CycloneDX, vex *cdx.BOM) *CycloneDX {
|
||||
var stmts []Statement
|
||||
for _, vuln := range lo.FromPtr(vex.Vulnerabilities) {
|
||||
affects := lo.Map(lo.FromPtr(vuln.Affects), func(item cdx.Affects, index int) string {
|
||||
return item.Ref
|
||||
})
|
||||
|
||||
analysis := lo.FromPtr(vuln.Analysis)
|
||||
|
||||
stmts = append(stmts, Statement{
|
||||
VulnerabilityID: vuln.ID,
|
||||
Affects: affects,
|
||||
Status: cdxStatus(analysis.State),
|
||||
Justification: string(analysis.Justification),
|
||||
})
|
||||
}
|
||||
return &CycloneDX{
|
||||
sbom: sbom,
|
||||
statements: stmts,
|
||||
logger: log.Logger.With(zap.String("VEX format", "CycloneDX")),
|
||||
}
|
||||
}
|
||||
|
||||
func (v *CycloneDX) Filter(vulns []types.DetectedVulnerability) []types.DetectedVulnerability {
|
||||
return lo.Filter(vulns, func(vuln types.DetectedVulnerability, _ int) bool {
|
||||
stmt, ok := lo.Find(v.statements, func(item Statement) bool {
|
||||
return item.VulnerabilityID == vuln.VulnerabilityID
|
||||
})
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
return v.affected(vuln, stmt)
|
||||
})
|
||||
}
|
||||
|
||||
func (v *CycloneDX) affected(vuln types.DetectedVulnerability, stmt Statement) bool {
|
||||
for _, affect := range stmt.Affects {
|
||||
// Affect must be BOM-Link at the moment
|
||||
link, err := cdx.ParseBOMLink(affect)
|
||||
if err != nil {
|
||||
v.logger.Warnw("Unable to parse BOM-Link", zap.String("affect", affect))
|
||||
continue
|
||||
}
|
||||
if v.sbom.SerialNumber != link.SerialNumber() || v.sbom.Version != link.Version() {
|
||||
v.logger.Warnw("URN doesn't match with SBOM", zap.String("serial number", link.SerialNumber()),
|
||||
zap.Int("version", link.Version()))
|
||||
continue
|
||||
}
|
||||
if vuln.PkgRef == link.Reference() &&
|
||||
(stmt.Status == StatusNotAffected || stmt.Status == StatusFixed) {
|
||||
v.logger.Infow("Filtered out the detected vulnerability", zap.String("vulnerability-id", vuln.VulnerabilityID),
|
||||
zap.String("status", string(stmt.Status)), zap.String("justification", stmt.Justification))
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func cdxStatus(s cdx.ImpactAnalysisState) Status {
|
||||
switch s {
|
||||
case cdx.IASResolved, cdx.IASResolvedWithPedigree:
|
||||
return StatusFixed
|
||||
case cdx.IASExploitable:
|
||||
return StatusAffected
|
||||
case cdx.IASInTriage:
|
||||
return StatusUnderInvestigation
|
||||
case cdx.IASFalsePositive, cdx.IASNotAffected:
|
||||
return StatusNotAffected
|
||||
}
|
||||
return StatusUnknown
|
||||
}
|
||||
|
||||
func New(filePath string, report types.Report) (VEX, error) {
|
||||
if filePath == "" {
|
||||
return nil, nil
|
||||
}
|
||||
f, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("file open error: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var errs error
|
||||
|
||||
// Try CycloneDX JSON
|
||||
if ok, err := sbom.IsCycloneDXJSON(f); err != nil {
|
||||
errs = multierror.Append(errs, err)
|
||||
} else if ok {
|
||||
return decodeCycloneDXJSON(f, report)
|
||||
}
|
||||
|
||||
// Try OpenVEX
|
||||
if v, err := decodeOpenVEX(f, report); err != nil {
|
||||
errs = multierror.Append(errs, err)
|
||||
} else {
|
||||
return v, nil
|
||||
}
|
||||
|
||||
return nil, xerrors.Errorf("unable to load VEX: %w", errs)
|
||||
}
|
||||
|
||||
func decodeCycloneDXJSON(r io.ReadSeeker, report types.Report) (VEX, error) {
|
||||
if _, err := r.Seek(0, io.SeekStart); err != nil {
|
||||
return nil, xerrors.Errorf("seek error: %w", err)
|
||||
}
|
||||
vex, err := cyclonedx.DecodeJSON(r)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("json decode error: %w", err)
|
||||
}
|
||||
if report.CycloneDX == nil {
|
||||
return nil, xerrors.New("CycloneDX VEX can be used with CycloneDX SBOM")
|
||||
}
|
||||
return newCycloneDX(report.CycloneDX, vex), nil
|
||||
}
|
||||
|
||||
func decodeOpenVEX(r io.ReadSeeker, report types.Report) (VEX, error) {
|
||||
// openvex/go-vex outputs log messages by default
|
||||
logrus.SetOutput(io.Discard)
|
||||
|
||||
if _, err := r.Seek(0, io.SeekStart); err != nil {
|
||||
return nil, xerrors.Errorf("seek error: %w", err)
|
||||
}
|
||||
var openVEX openvex.VEX
|
||||
if err := json.NewDecoder(r).Decode(&openVEX); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if openVEX.Context == "" {
|
||||
return nil, nil
|
||||
}
|
||||
return newOpenVEX(report.CycloneDX, openVEX), nil
|
||||
}
|
||||
156
pkg/vex/vex_test.go
Normal file
156
pkg/vex/vex_test.go
Normal file
@@ -0,0 +1,156 @@
|
||||
package vex_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
||||
"github.com/aquasecurity/trivy/pkg/types"
|
||||
"github.com/aquasecurity/trivy/pkg/vex"
|
||||
)
|
||||
|
||||
func TestVEX_Filter(t *testing.T) {
|
||||
type fields struct {
|
||||
filePath string
|
||||
report types.Report
|
||||
}
|
||||
type args struct {
|
||||
vulns []types.DetectedVulnerability
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want []types.DetectedVulnerability
|
||||
}{
|
||||
{
|
||||
name: "OpenVEX",
|
||||
fields: fields{
|
||||
filePath: "testdata/openvex.json",
|
||||
},
|
||||
args: args{
|
||||
vulns: []types.DetectedVulnerability{
|
||||
{
|
||||
VulnerabilityID: "CVE-2021-44228",
|
||||
PkgName: "spring-boot",
|
||||
InstalledVersion: "2.6.0",
|
||||
PkgRef: "pkg:maven/org.springframework.boot/spring-boot@2.6.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []types.DetectedVulnerability{},
|
||||
},
|
||||
{
|
||||
name: "CycloneDX SBOM with OpenVEX",
|
||||
fields: fields{
|
||||
filePath: "testdata/openvex.cdx.json",
|
||||
report: types.Report{
|
||||
CycloneDX: &ftypes.CycloneDX{
|
||||
SerialNumber: "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
|
||||
Version: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
vulns: []types.DetectedVulnerability{
|
||||
{
|
||||
VulnerabilityID: "CVE-2018-7489",
|
||||
PkgName: "jackson-databind",
|
||||
InstalledVersion: "2.8.0",
|
||||
PkgRef: "jackson-databind-2.8.0",
|
||||
},
|
||||
{
|
||||
VulnerabilityID: "CVE-2018-7490",
|
||||
PkgName: "jackson-databind",
|
||||
InstalledVersion: "2.8.0",
|
||||
PkgRef: "jackson-databind-2.8.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []types.DetectedVulnerability{
|
||||
{
|
||||
VulnerabilityID: "CVE-2018-7490",
|
||||
PkgName: "jackson-databind",
|
||||
InstalledVersion: "2.8.0",
|
||||
PkgRef: "jackson-databind-2.8.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "CycloneDX SBOM with CycloneDX VEX",
|
||||
fields: fields{
|
||||
filePath: "testdata/cyclonedx.json",
|
||||
report: types.Report{
|
||||
CycloneDX: &ftypes.CycloneDX{
|
||||
SerialNumber: "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
|
||||
Version: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
vulns: []types.DetectedVulnerability{
|
||||
{
|
||||
VulnerabilityID: "CVE-2018-7489",
|
||||
PkgName: "jackson-databind",
|
||||
InstalledVersion: "2.8.0",
|
||||
PkgRef: "jackson-databind-2.8.0",
|
||||
},
|
||||
{
|
||||
VulnerabilityID: "CVE-2018-7490",
|
||||
PkgName: "jackson-databind",
|
||||
InstalledVersion: "2.8.0",
|
||||
PkgRef: "jackson-databind-2.8.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []types.DetectedVulnerability{
|
||||
{
|
||||
VulnerabilityID: "CVE-2018-7490",
|
||||
PkgName: "jackson-databind",
|
||||
InstalledVersion: "2.8.0",
|
||||
PkgRef: "jackson-databind-2.8.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "CycloneDX VEX wrong URN",
|
||||
fields: fields{
|
||||
filePath: "testdata/cyclonedx.json",
|
||||
report: types.Report{
|
||||
CycloneDX: &ftypes.CycloneDX{
|
||||
SerialNumber: "urn:uuid:wrong",
|
||||
Version: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
vulns: []types.DetectedVulnerability{
|
||||
{
|
||||
VulnerabilityID: "CVE-2018-7489",
|
||||
PkgName: "jackson-databind",
|
||||
InstalledVersion: "2.8.0",
|
||||
PkgRef: "jackson-databind-2.8.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []types.DetectedVulnerability{
|
||||
{
|
||||
VulnerabilityID: "CVE-2018-7489",
|
||||
PkgName: "jackson-databind",
|
||||
InstalledVersion: "2.8.0",
|
||||
PkgRef: "jackson-databind-2.8.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
v, err := vex.New(tt.fields.filePath, tt.fields.report)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.want, v.Filter(tt.args.vulns))
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user