Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(vex): CSAF filtering should consider relationships #5923

Merged
merged 11 commits into from
Feb 22, 2024
117 changes: 87 additions & 30 deletions pkg/vex/csaf.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,45 +41,102 @@ func (v *CSAF) affected(vuln *csaf.Vulnerability, pkgURL *packageurl.PackageURL)
return true
}

var status Status
switch {
case v.matchPURL(vuln.ProductStatus.KnownNotAffected, pkgURL):
status = StatusNotAffected
case v.matchPURL(vuln.ProductStatus.Fixed, pkgURL):
status = StatusFixed
matchProduct := func(purls []*purl.PackageURL, pkgURL *packageurl.PackageURL) bool {
for _, p := range purls {
if p.Match(pkgURL) {
return true
}
}
return false
}

if status != "" {
v.logger.Infow("Filtered out the detected vulnerability",
zap.String("vulnerability-id", string(*vuln.CVE)),
zap.String("status", string(status)))
return false
for _, product := range lo.FromPtr(vuln.ProductStatus.KnownNotAffected) {
juan131 marked this conversation as resolved.
Show resolved Hide resolved
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",
juan131 marked this conversation as resolved.
Show resolved Hide resolved
zap.String("vulnerability-id", string(*vuln.CVE)),
zap.String("status", string(StatusNotAffected)),
zap.String("relationship", string(relationship)))
return false
}
}
}

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",
juan131 marked this conversation as resolved.
Show resolved Hide resolved
zap.String("vulnerability-id", string(*vuln.CVE)),
zap.String("status", string(StatusFixed)),
zap.String("relationship", string(relationship)))
return false
}
}
}

return true
}

// 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
}
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
}
return p, true
})
for _, p := range purls {
if p.Match(pkgURL) {
return true
// getProductPurls returns a slice of PackageURLs associated to a given product
func (v *CSAF) getProductPurls(product csaf.ProductID) []*purl.PackageURL {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: In what cases can a single product id have multiple PURLs?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honest, I can't think of any situation like that but the official library is returning a slice:

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 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 {
juan131 marked this conversation as resolved.
Show resolved Hide resolved
subProductsMap[relationship] = append(subProductsMap[relationship], rel.ProductReference)
}
}
juan131 marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

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
})
}
126 changes: 126 additions & 0 deletions pkg/vex/testdata/csaf-not-affected-sub-components.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
]
}
24 changes: 24 additions & 0 deletions pkg/vex/vex_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,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{
Expand Down
Loading