diff --git a/pkg/vex/csaf.go b/pkg/vex/csaf.go index f70f626b6702..33c5c8975b3e 100644 --- a/pkg/vex/csaf.go +++ b/pkg/vex/csaf.go @@ -46,40 +46,94 @@ func (v *CSAF) match(vuln *csaf.Vulnerability, pkgURL *packageurl.PackageURL) ty return "" } - var status types.FindingStatus - switch { - case v.matchPURL(vuln.ProductStatus.KnownNotAffected, pkgURL): - status = types.FindingStatusNotAffected - case v.matchPURL(vuln.ProductStatus.Fixed, pkgURL): - status = types.FindingStatusFixed + matchProduct := func(purls []*purl.PackageURL, pkgURL *packageurl.PackageURL) bool { + for _, p := range purls { + if p.Match(pkgURL) { + return true + } + } + return false } - return status -} - -// matchPURL returns true if the given PackageURL is found in the ProductTree. -func (v *CSAF) matchPURL(products *csaf.Products, pkgURL *packageurl.PackageURL) bool { - for _, product := range lo.FromPtr(products) { - helpers := v.advisory.ProductTree.CollectProductIdentificationHelpers(lo.FromPtr(product)) - purls := lo.FilterMap(helpers, func(helper *csaf.ProductIdentificationHelper, _ int) (*purl.PackageURL, bool) { - if helper == nil || helper.PURL == nil { - return nil, false + productStatusMap := map[types.FindingStatus]csaf.Products{ + types.FindingStatusNotAffected: lo.FromPtr(vuln.ProductStatus.KnownNotAffected), + 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.Infow("Filtered out the detected vulnerability", + zap.String("vulnerability-id", string(*vuln.CVE)), + zap.String("status", string(status))) + return status } - p, err := purl.FromString(string(*helper.PURL)) - if err != nil { - v.logger.Errorw("Invalid PURL", zap.String("purl", string(*helper.PURL)), zap.Error(err)) - return nil, false + for relationship, purls := range v.inspectProductRelationships(lo.FromPtr(product)) { + if matchProduct(purls, pkgURL) { + v.logger.Warnw("Filtered out the detected vulnerability", + zap.String("vulnerability-id", string(*vuln.CVE)), + zap.String("status", string(status)), + zap.String("relationship", string(relationship))) + return status + } } - return p, true - }) - for _, p := range purls { - if p.Match(pkgURL) { - return true + } + } + + 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)) +} + +// 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) + if v.advisory.ProductTree.RelationShips == nil { + return nil + } + + 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) + } } } } - return false + 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))...) + } + purlsMap[relationship] = purlsFromProductIdentificationHelpers(helpers) + } + + return purlsMap +} + +// purlsFromProductIdentificationHelpers returns a slice of PackageURLs given a slice of ProductIdentificationHelpers. +func 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.Logger.Errorw("Invalid PURL", zap.String("purl", string(*helper.PURL)), zap.Error(err)) + return nil, false + } + return p, true + }) } func statement(vuln *csaf.Vulnerability) string { diff --git a/pkg/vex/testdata/csaf-not-affected-sub-components.json b/pkg/vex/testdata/csaf-not-affected-sub-components.json new file mode 100644 index 000000000000..a6fc9f088257 --- /dev/null +++ b/pkg/vex/testdata/csaf-not-affected-sub-components.json @@ -0,0 +1,126 @@ +{ + "document": { + "category": "csaf_vex", + "csaf_version": "2.0", + "publisher": { + "category": "vendor", + "name": "VMWare, Inc.", + "namespace": "https://tanzu.vmware.com/application-catalog" + }, + "title": "ArgoCD 2.9.3-2 Amd64 Debian12 Advisory", + "tracking": { + "current_release_date": "2024-01-04T17:17:25+01:00", + "generator": { + "engine": { + "name": "Bitnami VEX CLI", + "version": "1.0.0" + } + }, + "id": "fcf5bd33-41c3-45f9-885a-c2ee812f49c9", + "initial_release_date": "2024-01-04T17:17:25+01:00", + "revision_history": [ + { + "date": "2024-01-04T17:17:25+01:00", + "number": "1", + "summary": "Initial version." + } + ], + "status": "final", + "version": "1" + } + }, + "product_tree": { + "branches": [ + { + "branches": [ + { + "branches": [ + { + "category": "product_version", + "name": "2.9.3-2", + "product": { + "name": "Argo CD 2.9.3-2", + "product_id": "argo-cd-2.9.3-2-amd64-debian-12", + "product_identification_helper": { + "purl": "pkg:bitnami/argo-cd@2.9.3-2?arch=amd64\u0026distro=debian-12" + } + } + } + ], + "category": "product_name", + "name": "Argo CD" + } + ], + "category": "vendor", + "name": "VMWare, Inc." + }, + { + "branches": [ + { + "branches": [ + { + "category": "product_version", + "name": "v1.24.2", + "product": { + "name": "Kubernetes v1.24.2", + "product_id": "kubernetes-v1.24.2", + "product_identification_helper": { + "purl": "pkg:golang/k8s.io/kubernetes@v1.24.2" + } + } + } + ], + "category": "product_name", + "name": "kubernetes" + } + ], + "category": "vendor", + "name": "k8s.io" + } + ], + "relationships": [ + { + "product_reference": "kubernetes-v1.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", + "name": "Argo CD uses kubernetes golang library" + } + } + ] + }, + "vulnerabilities": [ + { + "cve": "CVE-2023-2727", + "flags": [ + { + "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" + ] + } + ], + "notes": [ + { + "category": "description", + "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": [ + "argo-cd-2.9.3-2-amd64-debian-12-kubernetes" + ] + }, + "threats": [ + { + "category": "impact", + "date": "2024-01-04T17:17:25+01:00", + "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_test.go b/pkg/vex/vex_test.go index 11241cb98652..08724884074d 100644 --- a/pkg/vex/vex_test.go +++ b/pkg/vex/vex_test.go @@ -298,6 +298,30 @@ func TestVEX_Filter(t *testing.T) { }, }, }, + { + name: "CSAF (not affected vuln) with sub components", + fields: fields{ + filePath: "testdata/csaf-not-affected-sub-components.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", + }, + }, + }, + }, + }, + want: []types.DetectedVulnerability{}, + }, { name: "unknown format", fields: fields{