From 11a5b91a1a4951bcd17d896830734b876eb6fc72 Mon Sep 17 00:00:00 2001 From: Teppei Fukuda Date: Thu, 27 Apr 2023 10:21:06 +0300 Subject: [PATCH] feat(sbom): add VEX support (#4053) --- .../configuration/cli/trivy_sbom.md | 1 + docs/docs/supply-chain/vex.md | 181 ++++++++++++++ go.mod | 3 +- go.sum | 4 +- integration/sbom_test.go | 22 +- mkdocs.yml | 1 + pkg/cloud/report/report.go | 26 +- pkg/commands/artifact/run.go | 20 +- pkg/detector/library/detect.go | 2 +- pkg/detector/ospkg/alma/alma.go | 2 +- pkg/detector/ospkg/alpine/alpine.go | 2 +- pkg/detector/ospkg/amazon/amazon.go | 2 +- pkg/detector/ospkg/chainguard/chainguard.go | 2 +- pkg/detector/ospkg/debian/debian.go | 2 +- pkg/detector/ospkg/mariner/mariner.go | 2 +- pkg/detector/ospkg/oracle/oracle.go | 2 +- pkg/detector/ospkg/photon/photon.go | 2 +- pkg/detector/ospkg/redhat/redhat.go | 2 +- pkg/detector/ospkg/rocky/rocky.go | 2 +- pkg/detector/ospkg/suse/suse.go | 2 +- pkg/detector/ospkg/ubuntu/ubuntu.go | 2 +- pkg/detector/ospkg/wolfi/wolfi.go | 2 +- pkg/flag/sbom_flags.go | 21 +- pkg/module/serialize/types_easyjson.go | 14 +- pkg/result/filter.go | 76 ++++-- pkg/result/filter_test.go | 232 ++++++++++++++++- pkg/result/testdata/openvex.json | 17 ++ pkg/sbom/cyclonedx/marshal.go | 2 +- pkg/sbom/cyclonedx/marshal_test.go | 4 +- pkg/sbom/cyclonedx/unmarshal.go | 17 +- pkg/sbom/sbom.go | 126 ++++++---- pkg/types/vulnerability.go | 10 +- pkg/vex/status.go | 11 + pkg/vex/testdata/cyclonedx.json | 23 ++ pkg/vex/testdata/openvex.cdx.json | 17 ++ pkg/vex/testdata/openvex.json | 17 ++ pkg/vex/testdata/unknown.json | 1 + pkg/vex/vex.go | 233 ++++++++++++++++++ pkg/vex/vex_test.go | 156 ++++++++++++ 39 files changed, 1121 insertions(+), 142 deletions(-) create mode 100644 docs/docs/supply-chain/vex.md create mode 100644 pkg/result/testdata/openvex.json create mode 100644 pkg/vex/status.go create mode 100644 pkg/vex/testdata/cyclonedx.json create mode 100644 pkg/vex/testdata/openvex.cdx.json create mode 100644 pkg/vex/testdata/openvex.json create mode 100644 pkg/vex/testdata/unknown.json create mode 100644 pkg/vex/vex.go create mode 100644 pkg/vex/vex_test.go diff --git a/docs/docs/references/configuration/cli/trivy_sbom.md b/docs/docs/references/configuration/cli/trivy_sbom.md index 3a5c8bc286..17b186be00 100644 --- a/docs/docs/references/configuration/cli/trivy_sbom.md +++ b/docs/docs/references/configuration/cli/trivy_sbom.md @@ -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") ``` diff --git a/docs/docs/supply-chain/vex.md b/docs/docs/supply-chain/vex.md new file mode 100644 index 0000000000..65a6421c9a --- /dev/null +++ b/docs/docs/supply-chain/vex.md @@ -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 < 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 < 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. diff --git a/go.mod b/go.mod index fc451bddc3..3fdc1f344b 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index bcc6379568..8ca507e1f4 100644 --- a/go.sum +++ b/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= diff --git a/integration/sbom_test.go b/integration/sbom_test.go index f43b5a493f..9a827b4b5f 100644 --- a/integration/sbom_test.go +++ b/integration/sbom_test.go @@ -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 } } diff --git a/mkdocs.yml b/mkdocs.yml index fca2ca3fbc..1c5d20d2d4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -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: diff --git a/pkg/cloud/report/report.go b/pkg/cloud/report/report.go index ae0d3d73b1..a2b4e6cc17 100644 --- a/pkg/cloud/report/report.go +++ b/pkg/cloud/report/report.go @@ -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 { diff --git a/pkg/commands/artifact/run.go b/pkg/commands/artifact/run.go index c0f6d8a34e..f45a25ae59 100644 --- a/pkg/commands/artifact/run.go +++ b/pkg/commands/artifact/run.go @@ -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) - if err != nil { - return types.Report{}, xerrors.Errorf("unable to filter vulnerabilities: %w", err) - } + 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("filtering error: %w", err) } + return report, nil } diff --git a/pkg/detector/library/detect.go b/pkg/detector/library/detect.go index 4caaedeb25..e128187043 100644 --- a/pkg/detector/library/detect.go +++ b/pkg/detector/library/detect.go @@ -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...) } diff --git a/pkg/detector/ospkg/alma/alma.go b/pkg/detector/ospkg/alma/alma.go index 24a6c0947c..55fb020c25 100644 --- a/pkg/detector/ospkg/alma/alma.go +++ b/pkg/detector/ospkg/alma/alma.go @@ -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, diff --git a/pkg/detector/ospkg/alpine/alpine.go b/pkg/detector/ospkg/alpine/alpine.go index f17f344f0e..e099982e03 100644 --- a/pkg/detector/ospkg/alpine/alpine.go +++ b/pkg/detector/ospkg/alpine/alpine.go @@ -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, }) diff --git a/pkg/detector/ospkg/amazon/amazon.go b/pkg/detector/ospkg/amazon/amazon.go index 493ff3beba..c127e3f5da 100644 --- a/pkg/detector/ospkg/amazon/amazon.go +++ b/pkg/detector/ospkg/amazon/amazon.go @@ -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, diff --git a/pkg/detector/ospkg/chainguard/chainguard.go b/pkg/detector/ospkg/chainguard/chainguard.go index 38db48dc17..594a62fbca 100644 --- a/pkg/detector/ospkg/chainguard/chainguard.go +++ b/pkg/detector/ospkg/chainguard/chainguard.go @@ -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, }) diff --git a/pkg/detector/ospkg/debian/debian.go b/pkg/detector/ospkg/debian/debian.go index 3091a3e93a..5490d4ce4b 100644 --- a/pkg/detector/ospkg/debian/debian.go +++ b/pkg/detector/ospkg/debian/debian.go @@ -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, diff --git a/pkg/detector/ospkg/mariner/mariner.go b/pkg/detector/ospkg/mariner/mariner.go index 1fd7349909..5bada4ea9f 100644 --- a/pkg/detector/ospkg/mariner/mariner.go +++ b/pkg/detector/ospkg/mariner/mariner.go @@ -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, } diff --git a/pkg/detector/ospkg/oracle/oracle.go b/pkg/detector/ospkg/oracle/oracle.go index 6917cc691e..284564105b 100644 --- a/pkg/detector/ospkg/oracle/oracle.go +++ b/pkg/detector/ospkg/oracle/oracle.go @@ -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, diff --git a/pkg/detector/ospkg/photon/photon.go b/pkg/detector/ospkg/photon/photon.go index 82b2d3f2b4..e665574a36 100644 --- a/pkg/detector/ospkg/photon/photon.go +++ b/pkg/detector/ospkg/photon/photon.go @@ -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, diff --git a/pkg/detector/ospkg/redhat/redhat.go b/pkg/detector/ospkg/redhat/redhat.go index 9d10510293..e2aa400712 100644 --- a/pkg/detector/ospkg/redhat/redhat.go +++ b/pkg/detector/ospkg/redhat/redhat.go @@ -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{ diff --git a/pkg/detector/ospkg/rocky/rocky.go b/pkg/detector/ospkg/rocky/rocky.go index d47f10a494..e418e8d30f 100644 --- a/pkg/detector/ospkg/rocky/rocky.go +++ b/pkg/detector/ospkg/rocky/rocky.go @@ -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, diff --git a/pkg/detector/ospkg/suse/suse.go b/pkg/detector/ospkg/suse/suse.go index ca394ccecf..94716d6681 100644 --- a/pkg/detector/ospkg/suse/suse.go +++ b/pkg/detector/ospkg/suse/suse.go @@ -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, diff --git a/pkg/detector/ospkg/ubuntu/ubuntu.go b/pkg/detector/ospkg/ubuntu/ubuntu.go index ceb9821e4f..1feb273ad6 100644 --- a/pkg/detector/ospkg/ubuntu/ubuntu.go +++ b/pkg/detector/ospkg/ubuntu/ubuntu.go @@ -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, diff --git a/pkg/detector/ospkg/wolfi/wolfi.go b/pkg/detector/ospkg/wolfi/wolfi.go index 6e8ca0ccb0..d0535380e2 100644 --- a/pkg/detector/ospkg/wolfi/wolfi.go +++ b/pkg/detector/ospkg/wolfi/wolfi.go @@ -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, }) diff --git a/pkg/flag/sbom_flags.go b/pkg/flag/sbom_flags.go index 6b822f292f..3e5f436e19 100644 --- a/pkg/flag/sbom_flags.go +++ b/pkg/flag/sbom_flags.go @@ -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 } diff --git a/pkg/module/serialize/types_easyjson.go b/pkg/module/serialize/types_easyjson.go index 9e90f087d3..9107489e39 100644 --- a/pkg/module/serialize/types_easyjson.go +++ b/pkg/module/serialize/types_easyjson.go @@ -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:]) diff --git a/pkg/result/filter.go b/pkg/result/filter.go index 014d1aea45..e90aa0c322 100644 --- a/pkg/result/filter.go +++ b/pkg/result/filter.go @@ -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) { diff --git a/pkg/result/filter_test.go b/pkg/result/filter_test.go index 3e1aa16d2f..25ffbb0f86 100644 --- a/pkg/result/filter_test.go +++ b/pkg/result/filter_test.go @@ -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 { - result types.Result - //vulns []types.DetectedVulnerability - //misconfs []types.DetectedMisconfiguration - //secrets []ftypes.SecretFinding + 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 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) diff --git a/pkg/result/testdata/openvex.json b/pkg/result/testdata/openvex.json new file mode 100644 index 0000000000..9748eac1f5 --- /dev/null +++ b/pkg/result/testdata/openvex.json @@ -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" + } + ] +} diff --git a/pkg/sbom/cyclonedx/marshal.go b/pkg/sbom/cyclonedx/marshal.go index a1e7dda449..acd8230fa5 100644 --- a/pkg/sbom/cyclonedx/marshal.go +++ b/pkg/sbom/cyclonedx/marshal.go @@ -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 } diff --git a/pkg/sbom/cyclonedx/marshal_test.go b/pkg/sbom/cyclonedx/marshal_test.go index 048bf217bc..a5f0d6df04 100644 --- a/pkg/sbom/cyclonedx/marshal_test.go +++ b/pkg/sbom/cyclonedx/marshal_test.go @@ -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", }, diff --git a/pkg/sbom/cyclonedx/unmarshal.go b/pkg/sbom/cyclonedx/unmarshal.go index 2846fc92e8..9c643deafc 100644 --- a/pkg/sbom/cyclonedx/unmarshal.go +++ b/pkg/sbom/cyclonedx/unmarshal.go @@ -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) } diff --git a/pkg/sbom/sbom.go b/pkg/sbom/sbom.go index e9619cfc17..ca983b3687 100644 --- a/pkg/sbom/sbom.go +++ b/pkg/sbom/sbom.go @@ -40,64 +40,102 @@ const ( var ErrUnknownFormat = xerrors.New("Unknown SBOM format") +type cdxHeader struct { + // XML specific field + XMLNS string `json:"-" xml:"xmlns,attr"` + + // JSON specific field + BOMFormat string `json:"bomFormat" xml:"-"` +} + +type spdxHeader struct { + SpdxID string `json:"SPDXID"` +} + +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 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 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) - type ( - cyclonedx struct { - // XML specific field - XMLNS string `json:"-" xml:"xmlns,attr"` - - // JSON specific field - BOMFormat string `json:"bomFormat" xml:"-"` - } - - spdx struct { - SpdxID string `json:"SPDXID"` - } - ) - // Try CycloneDX JSON - var cdxBom cyclonedx - if err := json.NewDecoder(r).Decode(&cdxBom); err == nil { - if cdxBom.BOMFormat == "CycloneDX" { - return FormatCycloneDXJSON, nil - } - } - - if _, err := r.Seek(0, io.SeekStart); err != nil { - return FormatUnknown, xerrors.Errorf("seek error: %w", err) + 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") { - return FormatCycloneDXXML, nil - } - } - - if _, err := r.Seek(0, io.SeekStart); err != nil { - return FormatUnknown, xerrors.Errorf("seek error: %w", err) + if ok, err := IsCycloneDXXML(r); err != nil { + return FormatUnknown, err + } else if ok { + return FormatCycloneDXXML, nil } // Try SPDX json - var spdxBom spdx - if err := json.NewDecoder(r).Decode(&spdxBom); err == nil { - if strings.HasPrefix(spdxBom.SpdxID, "SPDX") { - return FormatSPDXJSON, nil - } - } - - if _, err := r.Seek(0, io.SeekStart); err != nil { - return FormatUnknown, xerrors.Errorf("seek error: %w", err) + if ok, err := IsSPDXJSON(r); err != nil { + return FormatUnknown, err + } else if ok { + return FormatSPDXJSON, nil } // Try SPDX tag-value - if scanner := bufio.NewScanner(r); scanner.Scan() { - if strings.HasPrefix(scanner.Text(), "SPDX") { - return FormatSPDXTV, nil - } + 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 { diff --git a/pkg/types/vulnerability.go b/pkg/types/vulnerability.go index 18acd4fd86..fb8430f9cb 100644 --- a/pkg/types/vulnerability.go +++ b/pkg/types/vulnerability.go @@ -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"` diff --git a/pkg/vex/status.go b/pkg/vex/status.go new file mode 100644 index 0000000000..dee810fd99 --- /dev/null +++ b/pkg/vex/status.go @@ -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" +) diff --git a/pkg/vex/testdata/cyclonedx.json b/pkg/vex/testdata/cyclonedx.json new file mode 100644 index 0000000000..a87f382ba4 --- /dev/null +++ b/pkg/vex/testdata/cyclonedx.json @@ -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" + } + ] + } + ] +} diff --git a/pkg/vex/testdata/openvex.cdx.json b/pkg/vex/testdata/openvex.cdx.json new file mode 100644 index 0000000000..778d77537c --- /dev/null +++ b/pkg/vex/testdata/openvex.cdx.json @@ -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" + } + ] +} diff --git a/pkg/vex/testdata/openvex.json b/pkg/vex/testdata/openvex.json new file mode 100644 index 0000000000..2662c15a06 --- /dev/null +++ b/pkg/vex/testdata/openvex.json @@ -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" + } + ] +} diff --git a/pkg/vex/testdata/unknown.json b/pkg/vex/testdata/unknown.json new file mode 100644 index 0000000000..e0415f9056 --- /dev/null +++ b/pkg/vex/testdata/unknown.json @@ -0,0 +1 @@ +{unknown} \ No newline at end of file diff --git a/pkg/vex/vex.go b/pkg/vex/vex.go new file mode 100644 index 0000000000..500511ec87 --- /dev/null +++ b/pkg/vex/vex.go @@ -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 +} diff --git a/pkg/vex/vex_test.go b/pkg/vex/vex_test.go new file mode 100644 index 0000000000..85d80d4dae --- /dev/null +++ b/pkg/vex/vex_test.go @@ -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)) + }) + } +}