diff --git a/pkg/vex/csaf.go b/pkg/vex/csaf.go index 8f6ecc9a84cb..35680a8ddfc5 100644 --- a/pkg/vex/csaf.go +++ b/pkg/vex/csaf.go @@ -2,7 +2,6 @@ package vex import ( "github.com/csaf-poc/csaf_distribution/v3/csaf" - "github.com/package-url/packageurl-go" "github.com/samber/lo" "github.com/aquasecurity/trivy/pkg/log" @@ -16,6 +15,11 @@ type CSAF struct { logger *log.Logger } +type relationship struct { + Product *purl.PackageURL + SubProducts []*purl.PackageURL +} + func newCSAF(advisory csaf.Advisory) VEX { return &CSAF{ advisory: advisory, @@ -23,36 +27,28 @@ func newCSAF(advisory csaf.Advisory) VEX { } } -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 - }) - if !ok { - return true - } +func (v *CSAF) Filter(result *types.Result, bom *core.BOM) { + filterVulnerabilities(result, bom, v.NotAffected) +} - if status := v.match(found, vuln.PkgIdentifier.PURL); status != "" { - result.ModifiedFindings = append(result.ModifiedFindings, - types.NewModifiedFinding(vuln, status, statement(found), "CSAF VEX")) - return false - } - return true +func (v *CSAF) NotAffected(vuln types.DetectedVulnerability, product, subProduct *core.Component) (types.ModifiedFinding, bool) { + found, ok := lo.Find(v.advisory.Vulnerabilities, func(item *csaf.Vulnerability) bool { + return string(*item.CVE) == vuln.VulnerabilityID }) -} + if !ok { + return types.ModifiedFinding{}, false + } -func (v *CSAF) match(vuln *csaf.Vulnerability, pkgURL *packageurl.PackageURL) types.FindingStatus { - if pkgURL == nil || vuln.ProductStatus == nil { - return "" + status := v.match(found, product, subProduct) + if status == "" { + return types.ModifiedFinding{}, false } + return types.NewModifiedFinding(vuln, status, v.statement(found), "CSAF VEX"), true +} - matchProduct := func(purls []*purl.PackageURL, pkgURL *packageurl.PackageURL) bool { - for _, p := range purls { - if p.Match(pkgURL) { - return true - } - } - return false +func (v *CSAF) match(vuln *csaf.Vulnerability, product, subProduct *core.Component) types.FindingStatus { + if product == nil || product.PkgIdentifier.PURL == nil || vuln.ProductStatus == nil { + return "" } productStatusMap := map[types.FindingStatus]csaf.Products{ @@ -60,83 +56,115 @@ func (v *CSAF) match(vuln *csaf.Vulnerability, pkgURL *packageurl.PackageURL) ty types.FindingStatusFixed: lo.FromPtr(vuln.ProductStatus.Fixed), } for status, productRange := range productStatusMap { - for _, product := range productRange { - if matchProduct(v.getProductPurls(lo.FromPtr(product)), pkgURL) { - v.logger.Info("Filtered out the detected vulnerability", - log.String("vulnerability-id", string(*vuln.CVE)), - log.String("status", string(status))) + for _, p := range productRange { + productID := lo.FromPtr(p) + logger := v.logger.With(log.String("vulnerability-id", string(*vuln.CVE)), + log.String("product-id", string(productID)), log.String("status", string(status))) + + // Check if the product is affected + if v.matchProduct(productID, product) { + logger.Info("Filtered out the detected vulnerability") return status } - for relationship, purls := range v.inspectProductRelationships(lo.FromPtr(product)) { - if matchProduct(purls, pkgURL) { - v.logger.Warn("Filtered out the detected vulnerability", - log.String("vulnerability-id", string(*vuln.CVE)), - log.String("status", string(status)), - log.String("relationship", string(relationship))) - return status - } + + // Check if the relationship between the product and the subcomponent is affected + if category, match := v.matchRelationship(productID, product, subProduct); match { + logger.Info("Filtered out the detected vulnerability", + log.String("relationship", string(category))) + return status } } } - return "" } -// getProductPurls returns a slice of PackageURLs associated to a given product -func (v *CSAF) getProductPurls(product csaf.ProductID) []*purl.PackageURL { - return purlsFromProductIdentificationHelpers(v.advisory.ProductTree.CollectProductIdentificationHelpers(product)) +func (v *CSAF) matchProduct(productID csaf.ProductID, product *core.Component) bool { + for _, productPURL := range v.productPURLs(productID) { + if productPURL.Match(product.PkgIdentifier.PURL) { + return true + } + } + return false +} + +func (v *CSAF) matchRelationship(fullProductID csaf.ProductID, product, subProduct *core.Component) ( + csaf.RelationshipCategory, bool) { + + for category, relationships := range v.inspectProductRelationships(fullProductID) { + for _, rel := range relationships { + if !rel.Product.Match(product.PkgIdentifier.PURL) { + continue + } + for _, subProductPURL := range rel.SubProducts { + if subProductPURL.Match(subProduct.PkgIdentifier.PURL) { + return category, true + } + } + } + } + return "", false +} + +// productPURLs returns a slice of PackageURLs associated to a given product +func (v *CSAF) productPURLs(product csaf.ProductID) []*purl.PackageURL { + return v.purlsFromProductIdentificationHelpers(v.advisory.ProductTree.CollectProductIdentificationHelpers(product)) } // inspectProductRelationships returns a map of PackageURLs associated to each relationship category // iterating over relationships looking for sub-products that might be part of the original product -func (v *CSAF) inspectProductRelationships(product csaf.ProductID) map[csaf.RelationshipCategory][]*purl.PackageURL { - subProductsMap := make(map[csaf.RelationshipCategory]csaf.Products) +func (v *CSAF) inspectProductRelationships(fullProductID csaf.ProductID) map[csaf.RelationshipCategory][]relationship { if v.advisory.ProductTree.RelationShips == nil { return nil } + relationships := make(map[csaf.RelationshipCategory][]relationship) for _, rel := range lo.FromPtr(v.advisory.ProductTree.RelationShips) { - if rel != nil { - relationship := lo.FromPtr(rel.Category) - switch relationship { - case csaf.CSAFRelationshipCategoryDefaultComponentOf, - csaf.CSAFRelationshipCategoryInstalledOn, - csaf.CSAFRelationshipCategoryInstalledWith: - if fpn := rel.FullProductName; fpn != nil && lo.FromPtr(fpn.ProductID) == product { - subProductsMap[relationship] = append(subProductsMap[relationship], rel.ProductReference) - } - } + if rel == nil || rel.FullProductName == nil { + continue + } else if lo.FromPtr(rel.FullProductName.ProductID) != fullProductID { + continue } - } - purlsMap := make(map[csaf.RelationshipCategory][]*purl.PackageURL) - for relationship, subProducts := range subProductsMap { - var helpers []*csaf.ProductIdentificationHelper - for _, subProductRef := range subProducts { - helpers = append(helpers, v.advisory.ProductTree.CollectProductIdentificationHelpers(lo.FromPtr(subProductRef))...) + category := lo.FromPtr(rel.Category) + switch category { + case csaf.CSAFRelationshipCategoryDefaultComponentOf, + csaf.CSAFRelationshipCategoryInstalledOn, + csaf.CSAFRelationshipCategoryInstalledWith: + + productID := lo.FromPtr(rel.RelatesToProductReference) + productPURLs := v.productPURLs(productID) + + subProductID := lo.FromPtr(rel.ProductReference) + subProductPURLs := v.productPURLs(subProductID) + + for _, productPURL := range productPURLs { + relationships[category] = append(relationships[category], relationship{ + Product: productPURL, + SubProducts: subProductPURLs, + }) + } } - purlsMap[relationship] = purlsFromProductIdentificationHelpers(helpers) } - return purlsMap + return relationships } -// purlsFromProductIdentificationHelpers returns a slice of PackageURLs given a slice of ProductIdentificationHelpers. -func purlsFromProductIdentificationHelpers(helpers []*csaf.ProductIdentificationHelper) []*purl.PackageURL { +// purlsFromProductIdentificationHelpers returns a slice of PURLs given a slice of ProductIdentificationHelpers. +func (v *CSAF) purlsFromProductIdentificationHelpers(helpers []*csaf.ProductIdentificationHelper) []*purl.PackageURL { return lo.FilterMap(helpers, func(helper *csaf.ProductIdentificationHelper, _ int) (*purl.PackageURL, bool) { if helper == nil || helper.PURL == nil { return nil, false } p, err := purl.FromString(string(*helper.PURL)) if err != nil { - log.Error("Invalid PURL", log.String("purl", string(*helper.PURL)), log.Err(err)) + v.logger.Error("Invalid PURL", log.String("purl", string(*helper.PURL)), log.Err(err)) return nil, false } return p, true }) } -func statement(vuln *csaf.Vulnerability) string { +func (v *CSAF) statement(vuln *csaf.Vulnerability) string { threat, ok := lo.Find(vuln.Threats, func(threat *csaf.Threat) bool { return lo.FromPtr(threat.Category) == csaf.CSAFThreatCategoryImpact }) diff --git a/pkg/vex/testdata/csaf-affected.json b/pkg/vex/testdata/csaf-affected.json deleted file mode 100644 index 56e9bb4d8a53..000000000000 --- a/pkg/vex/testdata/csaf-affected.json +++ /dev/null @@ -1,93 +0,0 @@ -{ - "document": { - "category": "csaf_vex", - "csaf_version": "2.0", - "notes": [ - { - "category": "summary", - "text": "Example Company VEX document. Unofficial content for demonstration purposes only.", - "title": "Author comment" - } - ], - "publisher": { - "category": "vendor", - "name": "Example Company ProductCERT", - "namespace": "https://psirt.example.com" - }, - "title": "Example VEX Document Use Case 1 - Affected", - "tracking": { - "current_release_date": "2022-03-03T11:00:00.000Z", - "generator": { - "date": "2022-03-03T11:00:00.000Z", - "engine": { - "name": "Secvisogram", - "version": "1.11.0" - } - }, - "id": "2022-EVD-UC-01-A-001", - "initial_release_date": "2022-03-03T11:00:00.000Z", - "revision_history": [ - { - "date": "2022-03-03T11:00:00.000Z", - "number": "1", - "summary": "Initial version." - } - ], - "status": "final", - "version": "1" - } - }, - "product_tree": { - "branches": [ - { - "branches": [ - { - "branches": [ - { - "category": "product_version", - "name": "1.0", - "product": { - "name": "Example Company DEF 1.0", - "product_id": "CSAFPID-0001", - "product_identification_helper": { - "purl": "pkg:maven/org.example.company/def@1.0" - } - } - } - ], - "category": "product_name", - "name": "DEF" - } - ], - "category": "vendor", - "name": "Example Company" - } - ] - }, - "vulnerabilities": [ - { - "cve": "CVE-2021-44228", - "notes": [ - { - "category": "description", - "text": "Apache Log4j2 2.0-beta9 through 2.15.0 (excluding security releases 2.12.2, 2.12.3, and 2.3.1) JNDI features used in configuration, log messages, and parameters do not protect against attacker controlled LDAP and other JNDI related endpoints. An attacker who can control log messages or log message parameters can execute arbitrary code loaded from LDAP servers when message lookup substitution is enabled. From log4j 2.15.0, this behavior has been disabled by default. From version 2.16.0 (along with 2.12.2, 2.12.3, and 2.3.1), this functionality has been completely removed. Note that this vulnerability is specific to log4j-core and does not affect log4net, log4cxx, or other Apache Logging Services projects.", - "title": "CVE description" - } - ], - "product_status": { - "known_affected": [ - "CSAFPID-0001" - ] - }, - "remediations": [ - { - "category": "vendor_fix", - "details": "Customers should update to version 1.1 of product DEF which fixes the issue.", - "product_ids": [ - "CSAFPID-0001" - ] - } - ] - } - ] -} diff --git a/pkg/vex/testdata/csaf-not-affected-sub-components.json b/pkg/vex/testdata/csaf-relationships.json similarity index 87% rename from pkg/vex/testdata/csaf-not-affected-sub-components.json rename to pkg/vex/testdata/csaf-relationships.json index a6fc9f088257..ab58d2bfe492 100644 --- a/pkg/vex/testdata/csaf-not-affected-sub-components.json +++ b/pkg/vex/testdata/csaf-relationships.json @@ -60,18 +60,18 @@ "branches": [ { "category": "product_version", - "name": "v1.24.2", + "name": "v0.24.2", "product": { - "name": "Kubernetes v1.24.2", - "product_id": "kubernetes-v1.24.2", + "name": "client-go v0.24.2", + "product_id": "client-go-v0.24.2", "product_identification_helper": { - "purl": "pkg:golang/k8s.io/kubernetes@v1.24.2" + "purl": "pkg:golang/k8s.io/client-go@0.24.2" } } } ], "category": "product_name", - "name": "kubernetes" + "name": "client-go" } ], "category": "vendor", @@ -80,11 +80,11 @@ ], "relationships": [ { - "product_reference": "kubernetes-v1.24.2", + "product_reference": "client-go-v0.24.2", "category": "default_component_of", "relates_to_product_reference": "argo-cd-2.9.3-2-amd64-debian-12", "full_product_name": { - "product_id": "argo-cd-2.9.3-2-amd64-debian-12-kubernetes", + "product_id": "argo-cd-2.9.3-2-amd64-debian-12-client-go", "name": "Argo CD uses kubernetes golang library" } } @@ -98,7 +98,7 @@ "date": "2024-01-04T17:17:25+01:00", "label": "vulnerable_code_cannot_be_controlled_by_adversary", "product_ids": [ - "argo-cd-2.9.3-2-amd64-debian-12-kubernetes" + "argo-cd-2.9.3-2-amd64-debian-12-client-go" ] } ], @@ -111,7 +111,7 @@ ], "product_status": { "known_not_affected": [ - "argo-cd-2.9.3-2-amd64-debian-12-kubernetes" + "argo-cd-2.9.3-2-amd64-debian-12-client-go" ] }, "threats": [ diff --git a/pkg/vex/testdata/csaf-not-affected.json b/pkg/vex/testdata/csaf.json similarity index 58% rename from pkg/vex/testdata/csaf-not-affected.json rename to pkg/vex/testdata/csaf.json index dce0b4a712d6..28389af24fcc 100644 --- a/pkg/vex/testdata/csaf-not-affected.json +++ b/pkg/vex/testdata/csaf.json @@ -14,7 +14,7 @@ "name": "Example Company ProductCERT", "namespace": "https://psirt.example.com" }, - "title": "AquaSecurity example VEX document", + "title": "Aqua Security example VEX document", "tracking": { "current_release_date": "2022-03-03T11:00:00.000Z", "generator": { @@ -45,47 +45,44 @@ "branches": [ { "category": "product_version", - "name": "2.6.0", + "name": "v0.24.2", "product": { - "name": "Spring Boot 2.6.0", - "product_id": "SPB-00260", + "name": "client-go v0.24.2", + "product_id": "client-go-v0.24.2", "product_identification_helper": { - "purl": "pkg:maven/org.springframework.boot/spring-boot@2.6.0" + "purl": "pkg:golang/k8s.io/client-go@0.24.2" } } } ], "category": "product_name", - "name": "Spring Boot" + "name": "client-go" } ], "category": "vendor", - "name": "Spring" + "name": "k8s.io" } ] }, "vulnerabilities": [ { - "cve": "CVE-2021-44228", + "cve": "CVE-2023-2727", "notes": [ { "category": "description", - "text": "Apache Log4j2 2.0-beta9 through 2.15.0 (excluding security releases 2.12.2, 2.12.3, and 2.3.1) JNDI features used in configuration, log messages, and parameters do not protect against attacker controlled LDAP and other JNDI related endpoints. An attacker who can control log messages or log message parameters can execute arbitrary code loaded from LDAP servers when message lookup substitution is enabled. From log4j 2.15.0, this behavior has been disabled by default. From version 2.16.0 (along with 2.12.2, 2.12.3, and 2.3.1), this functionality has been completely removed. Note that this vulnerability is specific to log4j-core and does not affect log4net, log4cxx, or other Apache Logging Services projects.", + "text": "Users may be able to launch containers using images that are restricted by ImagePolicyWebhook when using ephemeral containers. Kubernetes clusters are only affected if the ImagePolicyWebhook admission plugin is used together with ephemeral containers.", "title": "CVE description" } ], "product_status": { "known_not_affected": [ - "SPB-00260" + "client-go-v0.24.2" ] }, "threats": [ { "category": "impact", - "details": "Class with vulnerable code was removed before shipping.", - "product_ids": [ - "SPB-00260" - ] + "details": "The asset uses the component as a dependency in the code, but the vulnerability only affects Kubernetes clusters https://github.com/kubernetes/kubernetes/issues/118640" } ] } diff --git a/pkg/vex/vex.go b/pkg/vex/vex.go index c2b5ad10ea43..f4cf265997a0 100644 --- a/pkg/vex/vex.go +++ b/pkg/vex/vex.go @@ -116,10 +116,6 @@ func filterVulnerabilities(result *types.Result, bom *core.BOM, fn NotAffected) }) result.Vulnerabilities = lo.Filter(result.Vulnerabilities, func(vuln types.DetectedVulnerability, _ int) bool { - if vuln.PkgIdentifier.PURL == nil { - return true - } - c, ok := components[vuln.PkgIdentifier.UID] if !ok { log.Error("Component not found", log.String("uid", vuln.PkgIdentifier.UID)) diff --git a/pkg/vex/vex_test.go b/pkg/vex/vex_test.go index e7699aff2fb9..ea7fcd18194d 100644 --- a/pkg/vex/vex_test.go +++ b/pkg/vex/vex_test.go @@ -130,6 +130,43 @@ var ( }, }, } + argoComponent = core.Component{ + Type: core.TypeLibrary, + Name: "argo-cd", + Version: "2.9.3-2", + PkgIdentifier: ftypes.PkgIdentifier{ + UID: "07", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeBitnami, + Name: "argo-cd", + Version: "2.9.3-2", + Qualifiers: packageurl.Qualifiers{ + { + Key: "arch", + Value: "amd64", + }, + { + Key: "distro", + Value: "debian-12", + }, + }, + }, + }, + } + clientGoComponent = core.Component{ + Type: core.TypeLibrary, + Name: "k8s.io/client-go", + Version: "0.24.2", + PkgIdentifier: ftypes.PkgIdentifier{ + UID: "08", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGolang, + Namespace: "k8s.io", + Name: "client-go", + Version: "0.24.2", + }, + }, + } vuln1 = types.DetectedVulnerability{ VulnerabilityID: "CVE-2021-44228", PkgName: springComponent.Name, @@ -166,6 +203,15 @@ var ( PURL: goTransitiveComponent.PkgIdentifier.PURL, }, } + vuln5 = types.DetectedVulnerability{ + VulnerabilityID: "CVE-2023-2727", + PkgName: clientGoComponent.Name, + InstalledVersion: clientGoComponent.Version, + PkgIdentifier: ftypes.PkgIdentifier{ + UID: clientGoComponent.PkgIdentifier.UID, + PURL: clientGoComponent.PkgIdentifier.PURL, + }, + } ) func TestMain(m *testing.M) { @@ -391,90 +437,37 @@ func TestVEX_Filter(t *testing.T) { }, }, { - name: "CSAF (not affected vuln)", + name: "CSAF, not affected", fields: fields{ - filePath: "testdata/csaf-not-affected.json", + filePath: "testdata/csaf.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", - }, - }, - }, - }, + bom: newTestBOM5(), + vulns: []types.DetectedVulnerability{vuln5}, }, want: []types.DetectedVulnerability{}, }, { - name: "CSAF (affected vuln)", + name: "CSAF with relationships, not affected", fields: fields{ - filePath: "testdata/csaf-affected.json", + filePath: "testdata/csaf-relationships.json", }, args: args{ - vulns: []types.DetectedVulnerability{ - { - VulnerabilityID: "CVE-2021-44228", - PkgName: "def", - InstalledVersion: "1.0", - PkgIdentifier: ftypes.PkgIdentifier{ - PURL: &packageurl.PackageURL{ - Type: packageurl.TypeMaven, - Namespace: "org.example.company", - Name: "def", - Version: "1.0", - }, - }, - }, - }, - }, - want: []types.DetectedVulnerability{ - { - VulnerabilityID: "CVE-2021-44228", - PkgName: "def", - InstalledVersion: "1.0", - PkgIdentifier: ftypes.PkgIdentifier{ - PURL: &packageurl.PackageURL{ - Type: packageurl.TypeMaven, - Namespace: "org.example.company", - Name: "def", - Version: "1.0", - }, - }, - }, + bom: newTestBOM5(), + vulns: []types.DetectedVulnerability{vuln5}, }, + want: []types.DetectedVulnerability{}, }, { - name: "CSAF (not affected vuln) with sub components", + name: "CSAF with relationships, affected", fields: fields{ - filePath: "testdata/csaf-not-affected-sub-components.json", + filePath: "testdata/csaf-relationships.json", }, args: args{ - vulns: []types.DetectedVulnerability{ - { - VulnerabilityID: "CVE-2023-2727", - PkgName: "kubernetes", - InstalledVersion: "v1.24.2", - PkgIdentifier: ftypes.PkgIdentifier{ - PURL: &packageurl.PackageURL{ - Type: packageurl.TypeGolang, - Namespace: "k8s.io", - Name: "kubernetes", - Version: "v1.24.2", - }, - }, - }, - }, + bom: newTestBOM6(), + vulns: []types.DetectedVulnerability{vuln5}, }, - want: []types.DetectedVulnerability{}, + want: []types.DetectedVulnerability{vuln5}, }, { name: "unknown format", @@ -580,3 +573,26 @@ func newTestBOM4() *core.BOM { bom.AddRelationship(&goDirectComponent2, &goTransitiveComponent, core.RelationshipDependsOn) return bom } + +func newTestBOM5() *core.BOM { + // - oci:debian?tag=12 + // - pkg:bitnami/argo-cd@2.9.3-2?arch=amd64&distro=debian-12 + // - pkg:golang/k8s.io/client-go@0.24.2 + bom := core.NewBOM(core.Options{Parents: true}) + bom.AddComponent(&ociComponent) + bom.AddComponent(&argoComponent) + bom.AddComponent(&clientGoComponent) + bom.AddRelationship(&ociComponent, &argoComponent, core.RelationshipContains) + bom.AddRelationship(&argoComponent, &clientGoComponent, core.RelationshipDependsOn) + return bom +} + +func newTestBOM6() *core.BOM { + // - oci:debian?tag=12 + // - pkg:golang/k8s.io/client-go@0.24.2 + bom := core.NewBOM(core.Options{Parents: true}) + bom.AddComponent(&ociComponent) + bom.AddComponent(&clientGoComponent) + bom.AddRelationship(&ociComponent, &clientGoComponent, core.RelationshipContains) + return bom +}