From a447f6ba94b6f8b14177dc5e4369a788e2020d90 Mon Sep 17 00:00:00 2001 From: Teppei Fukuda Date: Tue, 28 May 2024 14:51:07 +0400 Subject: [PATCH] feat(vex): improve relationship support in CSAF VEX (#6735) Signed-off-by: knqyf263 --- docs/docs/supply-chain/vex.md | 138 +++++++++++++-- pkg/vex/csaf.go | 162 ++++++++++-------- pkg/vex/testdata/csaf-affected.json | 93 ---------- ...omponents.json => csaf-relationships.json} | 18 +- .../{csaf-not-affected.json => csaf.json} | 25 ++- pkg/vex/vex.go | 4 - pkg/vex/vex_test.go | 150 ++++++++-------- 7 files changed, 318 insertions(+), 272 deletions(-) delete mode 100644 pkg/vex/testdata/csaf-affected.json rename pkg/vex/testdata/{csaf-not-affected-sub-components.json => csaf-relationships.json} (87%) rename pkg/vex/testdata/{csaf-not-affected.json => csaf.json} (58%) diff --git a/docs/docs/supply-chain/vex.md b/docs/docs/supply-chain/vex.md index 59f3c5b97353..6dc02b6d088b 100644 --- a/docs/docs/supply-chain/vex.md +++ b/docs/docs/supply-chain/vex.md @@ -11,8 +11,6 @@ Currently, Trivy supports the following three formats: - [OpenVEX](https://github.com/openvex/spec) - [CSAF](https://oasis-open.github.io/csaf-documentation/specification.html) -This is still an experimental implementation, with only minimal functionality added. - ## CycloneDX | Target | Supported | |:---------------:|:---------:| @@ -40,7 +38,7 @@ The following steps are required: ### Generate the SBOM You can generate a CycloneDX SBOM with Trivy as follows: -```shell +```bash $ trivy image --format cyclonedx --output debian11.sbom.cdx debian:11 ``` @@ -49,7 +47,7 @@ Next, create a VEX based on the generated SBOM. Multiple vulnerability statuses can be defined under `vulnerabilities`. Take a look at the example below. -``` +```bash $ cat < trivy.vex.cdx { "bomFormat": "CycloneDX", @@ -105,7 +103,7 @@ For more details on CycloneDX VEX and BOM-Link, please refer to the following li ### Scan SBOM with VEX Provide the VEX when scanning the CycloneDX SBOM. -``` +```bash $ 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"} @@ -145,10 +143,10 @@ The following steps are required: ### Create the VEX document Please see also [the example](https://github.com/openvex/examples). -In Trivy, [the Package URL (PURL)][purl] is used as the product identifier. +Trivy requires [the Package URL (PURL)][purl] as the product identifier. -``` -$ cat < debian11.openvex +```bash +$ cat < debian11.openvex.json { "@context": "https://openvex.dev/ns/v0.2.0", "@id": "https://openvex.dev/docs/public/vex-2e67563e128250cbcb3e98930df948dd053e43271d70dc50cfa22d57e03fe96f", @@ -169,19 +167,107 @@ $ cat < debian11.openvex EOF ``` -In the above example, PURLs, located in `packages.externalRefs.referenceLocator` in SPDX are used for the product identifier. +In the above example, PURLs, `pkg:deb/debian/libdb5.3@5.3.28+dfsg1-0.8` are used for the product identifier. +You can find PURLs in the JSON report generated by Trivy. +This VEX statement is applied if the PURL specified in the VEX matches the PURL found during the scan. +See [here](#purl-matching) for more details of PURL matching. -!!! note - If a qualifier is specified in the PURL used as the product id in the VEX, the qualifier is compared. - Other qualifiers are ignored in the comparison. - `pkg:deb/debian/curl@7.50.3-1` in OpenVEX matches `pkg:deb/debian/curl@7.50.3-1?arch=i386`, - while `pkg:deb/debian/curl@7.50.3-1?arch=amd64` does not match `pkg:deb/debian/curl@7.50.3-1?arch=i386`. +Trivy also supports [OpenVEX subcomponents][openvex-subcomponent], which allow for more precise specification of the scope of a VEX statement, reducing the risk of incorrect filtering. +Let's say you want to suppress vulnerabilities within a container image. +If you only specify the PURL of the container image as the product, the resulting VEX would look like this: + +
+OpenVEX products only + +```json +"statements": [ + { + "vulnerability": {"name": "CVE-2024-32002"}, + "products": [ + {"@id": "pkg:oci/trivy?repository_url=ghcr.io%2Faquasecurity%2Ftrivy"} + ], + "status": "not_affected", + "justification": "vulnerable_code_not_in_execute_path" + } +] +``` + +
+ +However, this approach would suppress all instances of CVE-2024-32002 within the container image. +If the intention is to declare that the `git` package distributed by Alpine Linux within this image is not affected, subcomponents can be utilized as follows: + +
+OpenVEX subcomponents + +```json +"statements": [ + { + "vulnerability": {"name": "CVE-2024-32002"}, + "products": [ + { + "@id": "pkg:oci/trivy?repository_url=ghcr.io%2Faquasecurity%2Ftrivy", + "subcomponents": [ + {"@id": "pkg:apk/alpine/git"} + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_in_execute_path" + } +] +``` + +
+ +By declaring the subcomponent in this manner, Trivy will filter the results, considering only the `git` package within the `ghcr.io/aquasecurity/trivy` container image as not affected. +Omitting the version in the PURL applies the statement to all versions of the package. +More details about PURL matching can be found [here](#purl-matching). + +Furthermore, the product specified in a VEX statement does not necessarily need to be the target of the scan. +It is possible to specify a component that is included in the scan target as the product. +For example, you can designate a specific Go project as the product and its dependent modules as subcomponents. + +In the following example, the VEX statement declares that the `github.com/docker/docker` module, which is a dependency of the `github.com/aquasecurity/trivy` Go project, is not affected by CVE-2024-29018. + +
+OpenVEX intermediate components + +```json +"statements": [ + { + "vulnerability": {"name": "CVE-2024-29018"}, + "products": [ + { + "@id": "pkg:golang/github.com/aquasecurity/trivy", + "subcomponents": [ + { "@id": "pkg:golang/github.com/docker/docker" } + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_in_execute_path" + } +] +``` + +
+ +This VEX document can be used when scanning a container image as well as other targets. +The VEX statement will be applied when Trivy finds the Go binary within the container image. + +```bash +$ trivy image ghcr.io/aquasecurity/trivy:0.50.0 --vex trivy.openvex.json +``` + +VEX documents can indeed be reused across different container images, eliminating the need to issue separate VEX documents for each image. +This is particularly useful when there is a common component or library that is used across multiple projects or container images. ### Scan with VEX Provide the VEX when scanning your target. -``` -$ trivy image debian:11 --vex debian11.openvex +```bash +$ trivy image debian:11 --vex debian11.openvex.json ... 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"} @@ -215,7 +301,10 @@ The following steps are required: ### Create the CSAF document Create a CSAF document in JSON format as follows: -``` +
+CSAF VEX + +```bash $ cat < debian11.vex.csaf { "document": { @@ -313,10 +402,20 @@ $ cat < debian11.vex.csaf EOF ``` +
+ +Trivy also supports [CSAF relationships][csaf-relationship], reducing the risk of incorrect filtering. +It works in the same way as OpenVEX subcomponents. +At present, the specified relationship category is not taken into account and all the following categories are treated internally as "depends_on". + +- default_component_of +- installed_on +- installed_with + ### Scan with CSAF VEX Provide the CSAF document when scanning your target. -```console +```bash $ trivy image debian:11 --vex debian11.vex.csaf ... 2024-01-02T10:28:26.704+0100 INFO Filtered out the detected vulnerability {"VEX format": "CSAF", "vulnerability-id": "CVE-2019-8457", "status": "not_affected"} @@ -376,3 +475,6 @@ does not match: [openvex]: https://github.com/openvex/spec [purl]: https://github.com/package-url/purl-spec [purl-matching]: https://github.com/openvex/spec/issues/27 + +[openvex-subcomponent]: https://github.com/openvex/spec/blob/main/OPENVEX-SPEC.md#subcomponent +[csaf-relationship]: https://docs.oasis-open.org/csaf/csaf/v2.0/os/csaf-v2.0-os.html#3224-product-tree-property---relationships \ No newline at end of file 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 cce5b31d8517..d951b1795908 100644 --- a/pkg/vex/vex_test.go +++ b/pkg/vex/vex_test.go @@ -129,6 +129,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, @@ -165,6 +202,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) { @@ -390,90 +436,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", @@ -579,3 +572,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 +}