From 7d6c8b01310a1b250f9a661fe9950b92222d807d Mon Sep 17 00:00:00 2001 From: juan131 Date: Thu, 11 Jan 2024 11:20:19 +0100 Subject: [PATCH 1/6] fix(vex): CSAF filtering should consider relationships Signed-off-by: juan131 --- pkg/vex/csaf.go | 40 +++++- .../csaf-not-affected-sub-components.json | 126 ++++++++++++++++++ pkg/vex/vex_test.go | 26 ++++ 3 files changed, 186 insertions(+), 6 deletions(-) create mode 100644 pkg/vex/testdata/csaf-not-affected-sub-components.json diff --git a/pkg/vex/csaf.go b/pkg/vex/csaf.go index efed27b83f22..513168dae605 100644 --- a/pkg/vex/csaf.go +++ b/pkg/vex/csaf.go @@ -43,9 +43,9 @@ func (v *CSAF) affected(vuln *csaf.Vulnerability, purl *ftypes.PackageURL) bool var status Status switch { - case v.matchPURL(purl, vuln.ProductStatus.KnownNotAffected): + case v.affectedByStatus(purl, vuln.ProductStatus.KnownNotAffected): status = StatusNotAffected - case v.matchPURL(purl, vuln.ProductStatus.Fixed): + case v.affectedByStatus(purl, vuln.ProductStatus.Fixed): status = StatusFixed } @@ -59,10 +59,11 @@ func (v *CSAF) affected(vuln *csaf.Vulnerability, purl *ftypes.PackageURL) bool return true } -// matchPURL returns true if the given PackageURL is found in the ProductTree. -func (v *CSAF) matchPURL(purl *ftypes.PackageURL, products *csaf.Products) bool { - for _, product := range lo.FromPtr(products) { - helpers := v.advisory.ProductTree.CollectProductIdentificationHelpers(lo.FromPtr(product)) +// affectedByStatus returns true if a package (identified by a given PackageURL) belongs to certain status +// (e.g. KnownNotAffected, Fixed, etc.) which products are provided. +func (v *CSAF) affectedByStatus(purl *ftypes.PackageURL, statusProducts *csaf.Products) bool { + for _, product := range lo.FromPtr(statusProducts) { + helpers := v.collectProductIdentificationHelpers(lo.FromPtr(product)) purls := lo.FilterMap(helpers, func(helper *csaf.ProductIdentificationHelper, _ int) (string, bool) { if helper == nil || helper.PURL == nil { return "", false @@ -76,3 +77,30 @@ func (v *CSAF) matchPURL(purl *ftypes.PackageURL, products *csaf.Products) bool return false } + +// collectProductIdentificationHelpers collects ProductIdentificationHelpers from the given CSAF product. +func (v *CSAF) collectProductIdentificationHelpers(product csaf.ProductID) []*csaf.ProductIdentificationHelper { + helpers := v.advisory.ProductTree.CollectProductIdentificationHelpers(product) + // Iterate over relationships looking for sub-products that might be part of the original product. + var subProducts csaf.Products + if rels := v.advisory.ProductTree.RelationShips; rels != nil { + for _, rel := range lo.FromPtr(rels) { + if rel != nil { + switch lo.FromPtr(rel.Category) { + case csaf.CSAFRelationshipCategoryDefaultComponentOf, + csaf.CSAFRelationshipCategoryInstalledOn, + csaf.CSAFRelationshipCategoryInstalledWith: + if fpn := rel.FullProductName; fpn != nil && fpn.ProductID != nil && + lo.FromPtr(fpn.ProductID) == product { + subProducts = append(subProducts, rel.ProductReference) + } + } + } + } + } + for _, subProduct := range subProducts { + helpers = append(helpers, v.advisory.ProductTree.CollectProductIdentificationHelpers(lo.FromPtr(subProduct))...) + } + + return helpers +} 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 1feb37a7c41d..8864ac981796 100644 --- a/pkg/vex/vex_test.go +++ b/pkg/vex/vex_test.go @@ -297,6 +297,32 @@ 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: &ftypes.PackageURL{ + PackageURL: packageurl.PackageURL{ + Type: packageurl.TypeGolang, + Namespace: "k8s.io", + Name: "kubernetes", + Version: "v1.24.2", + }, + }, + }, + }, + }, + }, + want: []types.DetectedVulnerability{}, + }, { name: "unknown format", fields: fields{ From 8821df13779c9b790117c7a6d56bd9497ed6e8b1 Mon Sep 17 00:00:00 2001 From: juan131 Date: Thu, 11 Jan 2024 11:31:13 +0100 Subject: [PATCH 2/6] fix: merge main changes Signed-off-by: juan131 --- pkg/vex/vex_test.go | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/pkg/vex/vex_test.go b/pkg/vex/vex_test.go index 98a39b07c2e3..b9f1c91f4f24 100644 --- a/pkg/vex/vex_test.go +++ b/pkg/vex/vex_test.go @@ -285,13 +285,11 @@ func TestVEX_Filter(t *testing.T) { PkgName: "kubernetes", InstalledVersion: "v1.24.2", PkgIdentifier: ftypes.PkgIdentifier{ - PURL: &ftypes.PackageURL{ - PackageURL: packageurl.PackageURL{ - Type: packageurl.TypeGolang, - Namespace: "k8s.io", - Name: "kubernetes", - Version: "v1.24.2", - }, + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGolang, + Namespace: "k8s.io", + Name: "kubernetes", + Version: "v1.24.2", }, }, }, From e86aaa7bfd98f7b90f86c8f4939e0e19e02fd5cd Mon Sep 17 00:00:00 2001 From: juan131 Date: Thu, 25 Jan 2024 08:12:46 +0100 Subject: [PATCH 3/6] fix: warn message when filtering duet to relationships Signed-off-by: juan131 --- pkg/vex/csaf.go | 121 ++++++++++++++++++++++++++++++------------------ 1 file changed, 75 insertions(+), 46 deletions(-) diff --git a/pkg/vex/csaf.go b/pkg/vex/csaf.go index e180f7c7209e..ffe384adcfa3 100644 --- a/pkg/vex/csaf.go +++ b/pkg/vex/csaf.go @@ -41,73 +41,102 @@ func (v *CSAF) affected(vuln *csaf.Vulnerability, pkgURL *packageurl.PackageURL) return true } - var status Status - switch { - case v.affectedByStatus(vuln.ProductStatus.KnownNotAffected, pkgURL): - status = StatusNotAffected - case v.affectedByStatus(vuln.ProductStatus.Fixed, pkgURL): - status = StatusFixed - } - - if status != "" { - v.logger.Infow("Filtered out the detected vulnerability", - zap.String("vulnerability-id", string(*vuln.CVE)), - zap.String("status", string(status))) + matchProduct := func(purls []*purl.PackageURL, pkgURL *packageurl.PackageURL) bool { + for _, p := range purls { + if p.Match(pkgURL) { + return true + } + } return false } - return true -} - -// affectedByStatus returns true if a package (identified by a given PackageURL) belongs to certain status -// (e.g. KnownNotAffected, Fixed, etc.) which products are provided. -func (v *CSAF) affectedByStatus(statusProducts *csaf.Products, pkgURL *packageurl.PackageURL) bool { - for _, product := range lo.FromPtr(statusProducts) { - helpers := v.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 - } - 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 _, product := range lo.FromPtr(vuln.ProductStatus.KnownNotAffected) { + 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(StatusNotAffected))) + return false + } + for relationship, purls := range v.inspectProductRelationships(lo.FromPtr(product)) { + if matchProduct(purls, pkgURL) { + v.logger.Warnf("Filtered out the detected vulnerability", + zap.String("vulnerability-id", string(*vuln.CVE)), + zap.String("status", string(StatusNotAffected)), + zap.String("relationship", string(relationship))) + return false } - return p, true - }) - for _, p := range purls { - if p.Match(pkgURL) { - return true + } + } + + for _, product := range lo.FromPtr(vuln.ProductStatus.Fixed) { + 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(StatusFixed))) + return false + } + for relationship, purls := range v.inspectProductRelationships(lo.FromPtr(product)) { + if matchProduct(purls, pkgURL) { + v.logger.Warnf("Filtered out the detected vulnerability", + zap.String("vulnerability-id", string(*vuln.CVE)), + zap.String("status", string(StatusFixed)), + zap.String("relationship", string(relationship))) + return false } } } - return false + return true +} + +// 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)) } -// collectProductIdentificationHelpers collects ProductIdentificationHelpers from the given CSAF product. -func (v *CSAF) collectProductIdentificationHelpers(product csaf.ProductID) []*csaf.ProductIdentificationHelper { - helpers := v.advisory.ProductTree.CollectProductIdentificationHelpers(product) - // Iterate over relationships looking for sub-products that might be part of the original product. - var subProducts csaf.Products +// 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 rels := v.advisory.ProductTree.RelationShips; rels != nil { for _, rel := range lo.FromPtr(rels) { if rel != nil { - switch lo.FromPtr(rel.Category) { + relationship := lo.FromPtr(rel.Category) + switch relationship { case csaf.CSAFRelationshipCategoryDefaultComponentOf, csaf.CSAFRelationshipCategoryInstalledOn, csaf.CSAFRelationshipCategoryInstalledWith: - if fpn := rel.FullProductName; fpn != nil && fpn.ProductID != nil && - lo.FromPtr(fpn.ProductID) == product { - subProducts = append(subProducts, rel.ProductReference) + if fpn := rel.FullProductName; fpn != nil && fpn.ProductID != nil && lo.FromPtr(fpn.ProductID) == product { + subProductsMap[relationship] = append(subProductsMap[relationship], rel.ProductReference) } } } } } - for _, subProduct := range subProducts { - helpers = append(helpers, v.advisory.ProductTree.CollectProductIdentificationHelpers(lo.FromPtr(subProduct))...) + + 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 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 + }) } From cc30025aaf8a27a6abff497687b74268d08a3b2b Mon Sep 17 00:00:00 2001 From: Juan Ariza Toledano Date: Mon, 5 Feb 2024 10:49:14 +0100 Subject: [PATCH 4/6] Apply suggestions from code review Co-authored-by: Teppei Fukuda --- pkg/vex/csaf.go | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/pkg/vex/csaf.go b/pkg/vex/csaf.go index ffe384adcfa3..dfd360f7c8c8 100644 --- a/pkg/vex/csaf.go +++ b/pkg/vex/csaf.go @@ -59,7 +59,7 @@ func (v *CSAF) affected(vuln *csaf.Vulnerability, pkgURL *packageurl.PackageURL) } for relationship, purls := range v.inspectProductRelationships(lo.FromPtr(product)) { if matchProduct(purls, pkgURL) { - v.logger.Warnf("Filtered out the detected vulnerability", + v.logger.Warnw("Filtered out the detected vulnerability", zap.String("vulnerability-id", string(*vuln.CVE)), zap.String("status", string(StatusNotAffected)), zap.String("relationship", string(relationship))) @@ -77,7 +77,7 @@ func (v *CSAF) affected(vuln *csaf.Vulnerability, pkgURL *packageurl.PackageURL) } for relationship, purls := range v.inspectProductRelationships(lo.FromPtr(product)) { if matchProduct(purls, pkgURL) { - v.logger.Warnf("Filtered out the detected vulnerability", + v.logger.Warnw("Filtered out the detected vulnerability", zap.String("vulnerability-id", string(*vuln.CVE)), zap.String("status", string(StatusFixed)), zap.String("relationship", string(relationship))) @@ -98,17 +98,19 @@ func (v *CSAF) getProductPurls(product csaf.ProductID) []*purl.PackageURL { // 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 rels := v.advisory.ProductTree.RelationShips; rels != nil { - for _, rel := range lo.FromPtr(rels) { - if rel != nil { - relationship := lo.FromPtr(rel.Category) - switch relationship { - case csaf.CSAFRelationshipCategoryDefaultComponentOf, - csaf.CSAFRelationshipCategoryInstalledOn, - csaf.CSAFRelationshipCategoryInstalledWith: - if fpn := rel.FullProductName; fpn != nil && fpn.ProductID != nil && lo.FromPtr(fpn.ProductID) == product { - subProductsMap[relationship] = append(subProductsMap[relationship], rel.ProductReference) - } + 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 && fpn.ProductID != nil && lo.FromPtr(fpn.ProductID) == product { + subProductsMap[relationship] = append(subProductsMap[relationship], rel.ProductReference) } } } From 9518437375d43fc8b28e367bdd58514e95f9b245 Mon Sep 17 00:00:00 2001 From: juan131 Date: Mon, 5 Feb 2024 10:51:31 +0100 Subject: [PATCH 5/6] fix: simplify condition Signed-off-by: juan131 --- pkg/vex/csaf.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/vex/csaf.go b/pkg/vex/csaf.go index dfd360f7c8c8..c8d704c70cf7 100644 --- a/pkg/vex/csaf.go +++ b/pkg/vex/csaf.go @@ -109,7 +109,7 @@ func (v *CSAF) inspectProductRelationships(product csaf.ProductID) map[csaf.Rela case csaf.CSAFRelationshipCategoryDefaultComponentOf, csaf.CSAFRelationshipCategoryInstalledOn, csaf.CSAFRelationshipCategoryInstalledWith: - if fpn := rel.FullProductName; fpn != nil && fpn.ProductID != nil && lo.FromPtr(fpn.ProductID) == product { + if fpn := rel.FullProductName; fpn != nil && lo.FromPtr(fpn.ProductID) == product { subProductsMap[relationship] = append(subProductsMap[relationship], rel.ProductReference) } } From 9cbfc6bd4a4c9505ddbaa9becf27a105e52476a3 Mon Sep 17 00:00:00 2001 From: juan131 Date: Mon, 19 Feb 2024 10:40:14 +0100 Subject: [PATCH 6/6] fix: avoid repeating same logic twice Signed-off-by: juan131 --- pkg/vex/csaf.go | 46 +++++++++++++++++----------------------------- 1 file changed, 17 insertions(+), 29 deletions(-) diff --git a/pkg/vex/csaf.go b/pkg/vex/csaf.go index c8d704c70cf7..9a70341428d1 100644 --- a/pkg/vex/csaf.go +++ b/pkg/vex/csaf.go @@ -50,39 +50,27 @@ func (v *CSAF) affected(vuln *csaf.Vulnerability, pkgURL *packageurl.PackageURL) return false } - for _, product := range lo.FromPtr(vuln.ProductStatus.KnownNotAffected) { - 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(StatusNotAffected))) - return 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(StatusNotAffected)), - zap.String("relationship", string(relationship))) - return false - } - } + productStatusMap := map[string]csaf.Products{ + string(StatusNotAffected): lo.FromPtr(vuln.ProductStatus.KnownNotAffected), + string(StatusFixed): lo.FromPtr(vuln.ProductStatus.Fixed), } - - for _, product := range lo.FromPtr(vuln.ProductStatus.Fixed) { - 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(StatusFixed))) - return false - } - for relationship, purls := range v.inspectProductRelationships(lo.FromPtr(product)) { - if matchProduct(purls, pkgURL) { - v.logger.Warnw("Filtered out the detected vulnerability", + 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(StatusFixed)), - zap.String("relationship", string(relationship))) + zap.String("status", status)) return 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", status), + zap.String("relationship", string(relationship))) + return false + } + } } }