From c4022d61b39a4f4139f01f6254f182ab81d2bc35 Mon Sep 17 00:00:00 2001 From: Teppei Fukuda Date: Tue, 19 Mar 2024 09:51:18 +0900 Subject: [PATCH] feat(vex): consider root component for relationships (#6313) Co-authored-by: DmitriyLewen --- pkg/result/filter.go | 9 +- pkg/vex/csaf.go | 3 +- pkg/vex/cyclonedx.go | 2 +- pkg/vex/openvex.go | 16 ++- pkg/vex/testdata/openvex-oci.json | 26 +++++ pkg/vex/vex.go | 3 +- pkg/vex/vex_test.go | 181 +++++++++++++++++++++--------- 7 files changed, 180 insertions(+), 60 deletions(-) create mode 100644 pkg/vex/testdata/openvex-oci.json diff --git a/pkg/result/filter.go b/pkg/result/filter.go index dad9c0767316..6edcef72046a 100644 --- a/pkg/result/filter.go +++ b/pkg/result/filter.go @@ -14,6 +14,8 @@ import ( "golang.org/x/xerrors" dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/sbom/core" + sbomio "github.com/aquasecurity/trivy/pkg/sbom/io" "github.com/aquasecurity/trivy/pkg/types" "github.com/aquasecurity/trivy/pkg/vex" ) @@ -87,11 +89,16 @@ func filterByVEX(report types.Report, opt FilterOption) error { return nil } + bom, err := sbomio.NewEncoder(core.Options{}).Encode(report) + if err != nil { + return xerrors.Errorf("unable to encode the SBOM: %w", err) + } + for i, result := range report.Results { if len(result.Vulnerabilities) == 0 { continue } - vexDoc.Filter(&report.Results[i]) + vexDoc.Filter(&report.Results[i], bom) } return nil } diff --git a/pkg/vex/csaf.go b/pkg/vex/csaf.go index 33c5c8975b3e..d5d68f76adb9 100644 --- a/pkg/vex/csaf.go +++ b/pkg/vex/csaf.go @@ -8,6 +8,7 @@ import ( "github.com/aquasecurity/trivy/pkg/log" "github.com/aquasecurity/trivy/pkg/purl" + "github.com/aquasecurity/trivy/pkg/sbom/core" "github.com/aquasecurity/trivy/pkg/types" ) @@ -23,7 +24,7 @@ func newCSAF(advisory csaf.Advisory) VEX { } } -func (v *CSAF) Filter(result *types.Result) { +func (v *CSAF) Filter(result *types.Result, _ *core.BOM) { result.Vulnerabilities = lo.Filter(result.Vulnerabilities, func(vuln types.DetectedVulnerability, _ int) bool { found, ok := lo.Find(v.advisory.Vulnerabilities, func(item *csaf.Vulnerability) bool { return string(*item.CVE) == vuln.VulnerabilityID diff --git a/pkg/vex/cyclonedx.go b/pkg/vex/cyclonedx.go index a956703da3ae..685fefebf304 100644 --- a/pkg/vex/cyclonedx.go +++ b/pkg/vex/cyclonedx.go @@ -45,7 +45,7 @@ func newCycloneDX(sbom *core.BOM, vex *cdx.BOM) *CycloneDX { } } -func (v *CycloneDX) Filter(result *types.Result) { +func (v *CycloneDX) Filter(result *types.Result, _ *core.BOM) { result.Vulnerabilities = lo.Filter(result.Vulnerabilities, func(vuln types.DetectedVulnerability, _ int) bool { stmt, ok := lo.Find(v.statements, func(item Statement) bool { return item.VulnerabilityID == vuln.VulnerabilityID diff --git a/pkg/vex/openvex.go b/pkg/vex/openvex.go index 24e2bb6cca9c..a6cae6de7ac8 100644 --- a/pkg/vex/openvex.go +++ b/pkg/vex/openvex.go @@ -4,6 +4,7 @@ import ( openvex "github.com/openvex/go-vex/pkg/vex" "github.com/samber/lo" + "github.com/aquasecurity/trivy/pkg/sbom/core" "github.com/aquasecurity/trivy/pkg/types" ) @@ -17,13 +18,13 @@ func newOpenVEX(vex openvex.VEX) VEX { } } -func (v *OpenVEX) Filter(result *types.Result) { +func (v *OpenVEX) Filter(result *types.Result, bom *core.BOM) { result.Vulnerabilities = lo.Filter(result.Vulnerabilities, func(vuln types.DetectedVulnerability, _ int) bool { if vuln.PkgIdentifier.PURL == nil { return true } - stmts := v.vex.Matches(vuln.VulnerabilityID, vuln.PkgIdentifier.PURL.String(), nil) + stmts := v.Matches(vuln, bom) if len(stmts) == 0 { return true } @@ -41,6 +42,17 @@ func (v *OpenVEX) Filter(result *types.Result) { }) } +func (v *OpenVEX) Matches(vuln types.DetectedVulnerability, bom *core.BOM) []openvex.Statement { + root := bom.Root() + if root != nil && root.PkgID.PURL != nil { + stmts := v.vex.Matches(vuln.VulnerabilityID, root.PkgID.PURL.String(), []string{vuln.PkgIdentifier.PURL.String()}) + if len(stmts) != 0 { + return stmts + } + } + return v.vex.Matches(vuln.VulnerabilityID, vuln.PkgIdentifier.PURL.String(), nil) +} + func findingStatus(status openvex.Status) types.FindingStatus { switch status { case openvex.StatusNotAffected: diff --git a/pkg/vex/testdata/openvex-oci.json b/pkg/vex/testdata/openvex-oci.json new file mode 100644 index 000000000000..667ca5e3d049 --- /dev/null +++ b/pkg/vex/testdata/openvex-oci.json @@ -0,0 +1,26 @@ +{ + "@context": "https://openvex.dev/ns/v0.2.0", + "author": "Aqua Security", + "role": "Project Release Bot", + "timestamp": "2023-01-16T19:07:16.853479631-06:00", + "version": 1, + "statements": [ + { + "vulnerability": { + "name": "CVE-2022-3715" + }, + "products": [ + { + "@id": "pkg:oci/debian", + "subcomponents": [ + { + "@id": "pkg:deb/debian/bash" + } + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_in_execute_path" + } + ] +} diff --git a/pkg/vex/vex.go b/pkg/vex/vex.go index dc2c118b56bc..0e47bf03bf52 100644 --- a/pkg/vex/vex.go +++ b/pkg/vex/vex.go @@ -13,6 +13,7 @@ import ( ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" "github.com/aquasecurity/trivy/pkg/sbom" + "github.com/aquasecurity/trivy/pkg/sbom/core" "github.com/aquasecurity/trivy/pkg/sbom/cyclonedx" "github.com/aquasecurity/trivy/pkg/types" ) @@ -21,7 +22,7 @@ import ( // Note: This is in the experimental stage and does not yet support many specifications. // The implementation may change significantly. type VEX interface { - Filter(*types.Result) + Filter(*types.Result, *core.BOM) } func New(filePath string, report types.Report) (VEX, error) { diff --git a/pkg/vex/vex_test.go b/pkg/vex/vex_test.go index d591ccfdc6c6..77d2aff3c63e 100644 --- a/pkg/vex/vex_test.go +++ b/pkg/vex/vex_test.go @@ -16,6 +16,48 @@ import ( "github.com/aquasecurity/trivy/pkg/vex" ) +var ( + vuln1 = types.DetectedVulnerability{ + VulnerabilityID: "CVE-2021-44228", + PkgName: "spring-boot", + InstalledVersion: "2.6.0", + PkgIdentifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "org.springframework.boot", + Name: "spring-boot", + Version: "2.6.0", + }, + }, + } + vuln2 = types.DetectedVulnerability{ + VulnerabilityID: "CVE-2021-0001", + PkgName: "spring-boot", + InstalledVersion: "2.6.0", + PkgIdentifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "org.springframework.boot", + Name: "spring-boot", + Version: "2.6.0", + }, + }, + } + vuln3 = types.DetectedVulnerability{ + VulnerabilityID: "CVE-2022-3715", + PkgName: "bash", + InstalledVersion: "5.2.15", + PkgIdentifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeDebian, + Namespace: "debian", + Name: "bash", + Version: "5.2.15", + }, + }, + } +) + func TestMain(m *testing.M) { log.InitLogger(false, true) os.Exit(m.Run()) @@ -28,6 +70,7 @@ func TestVEX_Filter(t *testing.T) { } type args struct { vulns []types.DetectedVulnerability + bom *core.BOM } tests := []struct { name string @@ -42,21 +85,8 @@ func TestVEX_Filter(t *testing.T) { filePath: "testdata/openvex.json", }, args: args{ - vulns: []types.DetectedVulnerability{ - { - VulnerabilityID: "CVE-2021-44228", - PkgName: "spring-boot", - InstalledVersion: "2.6.0", - PkgIdentifier: ftypes.PkgIdentifier{ - PURL: &packageurl.PackageURL{ - Type: packageurl.TypeMaven, - Namespace: "org.springframework.boot", - Name: "spring-boot", - Version: "2.6.0", - }, - }, - }, - }, + vulns: []types.DetectedVulnerability{vuln1}, + bom: newTestBOM(), }, want: []types.DetectedVulnerability{}, }, @@ -67,49 +97,38 @@ func TestVEX_Filter(t *testing.T) { }, args: args{ vulns: []types.DetectedVulnerability{ - { - VulnerabilityID: "CVE-2021-44228", - PkgName: "spring-boot", - InstalledVersion: "2.6.0", - PkgIdentifier: ftypes.PkgIdentifier{ - PURL: &packageurl.PackageURL{ - Type: packageurl.TypeMaven, - Namespace: "org.springframework.boot", - Name: "spring-boot", - Version: "2.6.0", - }, - }, - }, - { - VulnerabilityID: "CVE-2021-0001", - PkgName: "spring-boot", - InstalledVersion: "2.6.0", - PkgIdentifier: ftypes.PkgIdentifier{ - PURL: &packageurl.PackageURL{ - Type: packageurl.TypeMaven, - Namespace: "org.springframework.boot", - Name: "spring-boot", - Version: "2.6.0", - }, - }, - }, + vuln1, // filtered by VEX + vuln2, }, + bom: newTestBOM(), }, want: []types.DetectedVulnerability{ - { - VulnerabilityID: "CVE-2021-0001", - PkgName: "spring-boot", - InstalledVersion: "2.6.0", - PkgIdentifier: ftypes.PkgIdentifier{ - PURL: &packageurl.PackageURL{ - Type: packageurl.TypeMaven, - Namespace: "org.springframework.boot", - Name: "spring-boot", - Version: "2.6.0", - }, - }, + vuln2, + }, + }, + { + name: "OpenVEX, subcomponents, oci image", + fields: fields{ + filePath: "testdata/openvex-oci.json", + }, + args: args{ + vulns: []types.DetectedVulnerability{ + vuln3, }, + bom: newTestBOM(), + }, + want: []types.DetectedVulnerability{}, + }, + { + name: "OpenVEX, subcomponents, wrong oci image", + fields: fields{ + filePath: "testdata/openvex-oci.json", + }, + args: args{ + vulns: []types.DetectedVulnerability{vuln3}, + bom: newTestBOM2(), }, + want: []types.DetectedVulnerability{vuln3}, }, { name: "CycloneDX SBOM with CycloneDX VEX", @@ -347,8 +366,62 @@ func TestVEX_Filter(t *testing.T) { got := &types.Result{ Vulnerabilities: tt.args.vulns, } - v.Filter(got) + v.Filter(got, tt.args.bom) assert.Equal(t, tt.want, got.Vulnerabilities) }) } } + +func newTestBOM() *core.BOM { + bom := core.NewBOM(core.Options{}) + bom.AddComponent(&core.Component{ + Root: true, + Type: core.TypeContainerImage, + Name: "debian:12", + PkgID: core.PkgID{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeOCI, + Name: "debian", + Version: "sha256:4482958b4461ff7d9fabc24b3a9ab1e9a2c85ece07b2db1840c7cbc01d053e90", + Qualifiers: packageurl.Qualifiers{ + { + Key: "tag", + Value: "12", + }, + { + Key: "repository_url", + Value: "docker.io/library/debian", + }, + }, + }, + }, + }) + return bom +} + +func newTestBOM2() *core.BOM { + bom := core.NewBOM(core.Options{}) + bom.AddComponent(&core.Component{ + Root: true, + Type: core.TypeContainerImage, + Name: "ubuntu:24.04", + PkgID: core.PkgID{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeOCI, + Name: "ubuntu", + Version: "sha256:4482958b4461ff7d9fabc24b3a9ab1e9a2c85ece07b2db1840c7cbc01d053e90", + Qualifiers: packageurl.Qualifiers{ + { + Key: "tag", + Value: "24.04", + }, + { + Key: "repository_url", + Value: "docker.io/library/ubuntu", + }, + }, + }, + }, + }) + return bom +}