From ac883f52edb8ca1f5a0a61d12c288d4b34ea3897 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Tue, 20 May 2025 15:56:08 -0400 Subject: [PATCH] add cdx group as purl namespace (#3922) Signed-off-by: Alex Goodman --- syft/format/internal/backfill.go | 27 +--- syft/format/internal/backfill_test.go | 10 +- .../cyclonedxutil/helpers/component.go | 42 ++++- .../cyclonedxutil/helpers/component_test.go | 147 +++++++++++++++++- syft/format/purls/decoder_test.go | 10 +- 5 files changed, 198 insertions(+), 38 deletions(-) diff --git a/syft/format/internal/backfill.go b/syft/format/internal/backfill.go index b8cc66f9f..f40aa9215 100644 --- a/syft/format/internal/backfill.go +++ b/syft/format/internal/backfill.go @@ -71,31 +71,14 @@ func Backfill(p *pkg.Package) { } } -func setJavaMetadataFromPurl(p *pkg.Package, purl packageurl.PackageURL) { +func setJavaMetadataFromPurl(p *pkg.Package, _ packageurl.PackageURL) { if p.Type != pkg.JavaPkg { return } - if purl.Namespace != "" { - if p.Metadata == nil { - p.Metadata = pkg.JavaArchive{} - } - meta, got := p.Metadata.(pkg.JavaArchive) - if got && meta.PomProperties == nil { - meta.PomProperties = &pkg.JavaPomProperties{} - p.Metadata = meta - } - if meta.PomProperties != nil { - // capture the group id from the purl if it is not already set - if meta.PomProperties.ArtifactID == "" { - meta.PomProperties.ArtifactID = purl.Name - } - if meta.PomProperties.GroupID == "" { - meta.PomProperties.GroupID = purl.Namespace - } - if meta.PomProperties.Version == "" { - meta.PomProperties.Version = purl.Version - } - } + if p.Metadata == nil { + // since we don't know if the purl elements directly came from pom properties or the manifest, + // we can only go as far as to set the type to JavaArchive, but not fill in the group id and artifact id + p.Metadata = pkg.JavaArchive{} } } diff --git a/syft/format/internal/backfill_test.go b/syft/format/internal/backfill_test.go index fa5baf732..0f88fa604 100644 --- a/syft/format/internal/backfill_test.go +++ b/syft/format/internal/backfill_test.go @@ -101,13 +101,9 @@ func Test_Backfill(t *testing.T) { Language: pkg.Java, Name: "some-thing", Version: "1.2.3", - Metadata: pkg.JavaArchive{ - PomProperties: &pkg.JavaPomProperties{ - GroupID: "org.apache", - ArtifactID: "some-thing", - Version: "1.2.3", - }, - }, + // we intentionally don't claim we found a pom properties file with a groupID from the purl. + // but we do claim that we found java data with an empty type. + Metadata: pkg.JavaArchive{}, }, }, } diff --git a/syft/format/internal/cyclonedxutil/helpers/component.go b/syft/format/internal/cyclonedxutil/helpers/component.go index aae3488e2..b4b686fb9 100644 --- a/syft/format/internal/cyclonedxutil/helpers/component.go +++ b/syft/format/internal/cyclonedxutil/helpers/component.go @@ -3,6 +3,7 @@ package helpers import ( "fmt" "reflect" + "strings" "github.com/CycloneDX/cyclonedx-go" @@ -90,15 +91,19 @@ func decodeComponent(c *cyclonedx.Component) *pkg.Package { Locations: decodeLocations(values), Licenses: pkg.NewLicenseSet(decodeLicenses(c)...), CPEs: decodeCPEs(c), - PURL: c.PackageURL, } + // note: this may write in syft package type information DecodeInto(p, values, "syft:package", CycloneDXFields) metadataType := values["syft:package:metadataType"] p.Metadata = decodePackageMetadata(values, c, metadataType) + // this will either use the purl from the component or generate a new one based off of any type information + // that was decoded above. + p.PURL = getPURL(c, p.Type) + if p.Type == "" { p.Type = pkg.TypeFromPURL(p.PURL) } @@ -111,6 +116,41 @@ func decodeComponent(c *cyclonedx.Component) *pkg.Package { return p } +func getPURL(c *cyclonedx.Component, ty pkg.Type) string { + if c.PackageURL != "" { + // if there is a purl that where the namespace does not match the group information, we may + // accidentally drop group. We should consider adding group as a top-level syft package field. + return c.PackageURL + } + + if strings.HasPrefix(c.BOMRef, "pkg:") { + // the bomref is a purl, so try to use that as the purl + _, err := packageurl.FromString(c.BOMRef) + if err == nil { + return c.BOMRef + } + } + + if ty == "" { + return "" + } + + tyStr := ty.PackageURLType() + switch tyStr { + case "", packageurl.TypeGeneric: + return "" + } + + purl := packageurl.PackageURL{ + Type: tyStr, + Namespace: c.Group, + Name: c.Name, + Version: c.Version, + } + + return purl.ToString() +} + func setPackageName(p *pkg.Package, c *cyclonedx.Component) { name := c.Name if c.Group != "" { diff --git a/syft/format/internal/cyclonedxutil/helpers/component_test.go b/syft/format/internal/cyclonedxutil/helpers/component_test.go index c48f61f27..1ed912140 100644 --- a/syft/format/internal/cyclonedxutil/helpers/component_test.go +++ b/syft/format/internal/cyclonedxutil/helpers/component_test.go @@ -275,6 +275,7 @@ func Test_decodeComponent(t *testing.T) { component cyclonedx.Component wantLanguage pkg.Language wantMetadata any + wantPURL string }{ { name: "derive language from pURL if missing", @@ -286,6 +287,18 @@ func Test_decodeComponent(t *testing.T) { BOMRef: "pkg:maven/ch.qos.logback/logback-classic@1.2.3", }, wantLanguage: pkg.Java, + wantPURL: "pkg:maven/ch.qos.logback/logback-classic@1.2.3", + }, + { + name: "derive language from bomref if missing", + component: cyclonedx.Component{ + Name: "ch.qos.logback/logback-classic", + Version: "1.2.3", + Type: "library", + BOMRef: "pkg:maven/ch.qos.logback/logback-classic@1.2.3", + }, + wantLanguage: pkg.Java, + wantPURL: "pkg:maven/ch.qos.logback/logback-classic@1.2.3", }, { name: "handle RpmdbMetadata type without properties", @@ -303,6 +316,7 @@ func Test_decodeComponent(t *testing.T) { }, }, wantMetadata: pkg.RpmDBEntry{}, + wantPURL: "pkg:rpm/centos/acl@2.2.53-1.el8?arch=x86_64&upstream=acl-2.2.53-1.el8.src.rpm&distro=centos-8", }, { name: "handle RpmdbMetadata type with properties", @@ -326,6 +340,24 @@ func Test_decodeComponent(t *testing.T) { wantMetadata: pkg.RpmDBEntry{ Release: "some-release", }, + wantPURL: "pkg:rpm/centos/acl@2.2.53-1.el8?arch=x86_64&upstream=acl-2.2.53-1.el8.src.rpm&distro=centos-8", + }, + { + name: "generate a purl from package type", + component: cyclonedx.Component{ + Name: "log4j", + Group: "org.apache.logging.log4j", + Version: "2.0.4", + Type: "library", + BOMRef: "log4j", + Properties: &[]cyclonedx.Property{ + { + Name: "syft:package:type", + Value: "java-archive", + }, + }, + }, + wantPURL: "pkg:maven/org.apache.logging.log4j/log4j@2.0.4", }, } @@ -338,9 +370,122 @@ func Test_decodeComponent(t *testing.T) { if tt.wantMetadata != nil { assert.Truef(t, reflect.DeepEqual(tt.wantMetadata, p.Metadata), "metadata should match: %+v != %+v", tt.wantMetadata, p.Metadata) } - if tt.wantMetadata == nil && tt.wantLanguage == "" { + + if tt.wantPURL != "" { + assert.Equal(t, tt.wantPURL, p.PURL, "purl should match") + } + + if tt.wantMetadata == nil && tt.wantLanguage == "" && tt.wantPURL == "" { t.Fatal("this is a useless test, please remove it") } }) } } + +func TestGetPURL(t *testing.T) { + tests := []struct { + name string + component *cyclonedx.Component + pkgType pkg.Type + expected string + }{ + { + name: "component with PackageURL", + component: &cyclonedx.Component{ + PackageURL: "pkg:npm/lodash@4.17.21", + Name: "lodash", + Version: "4.17.20", // different version to verify PackageURL is used + Group: "npm", + }, + pkgType: pkg.NpmPkg, + expected: "pkg:npm/lodash@4.17.21", + }, + { + name: "component with BOMRef as valid PURL", + component: &cyclonedx.Component{ + BOMRef: "pkg:maven/org.apache.commons/commons-lang3@3.12.0", + Name: "commons-lang3", + Version: "3.11.0", // different version to verify BOMRef is used + Group: "org.apache.commons", + }, + pkgType: pkg.JavaPkg, + expected: "pkg:maven/org.apache.commons/commons-lang3@3.12.0", + }, + { + name: "component with BOMRef not a valid PURL", + component: &cyclonedx.Component{ + BOMRef: "not-a-purl-ref", + Name: "commons-lang3", + Version: "3.12.0", + Group: "org.apache.commons", + }, + pkgType: pkg.JavaPkg, + expected: "pkg:maven/org.apache.commons/commons-lang3@3.12.0", + }, + { + name: "component with unknown package type", + component: &cyclonedx.Component{ + Name: "some-component", + Version: "1.0.0", + Group: "org.example", + }, + pkgType: pkg.UnknownPkg, + expected: "", + }, + { + name: "component with empty package type", + component: &cyclonedx.Component{ + Name: "some-component", + Version: "1.0.0", + Group: "org.example", + }, + pkgType: "", + expected: "", + }, + { + name: "component with generic package type", + component: &cyclonedx.Component{ + Name: "some-component", + Version: "1.0.0", + Group: "org.example", + }, + pkgType: pkg.LinuxKernelModulePkg, + expected: "", + }, + { + name: "component with valid package type", + component: &cyclonedx.Component{ + Name: "react", + Version: "18.2.0", + Group: "facebook", + }, + pkgType: pkg.NpmPkg, + expected: "pkg:npm/facebook/react@18.2.0", + }, + { + name: "component with no group", + component: &cyclonedx.Component{ + Name: "express", + Version: "4.18.2", + }, + pkgType: pkg.NpmPkg, + expected: "pkg:npm/express@4.18.2", + }, + { + name: "component with no version", + component: &cyclonedx.Component{ + Name: "express", + Group: "npm", + }, + pkgType: pkg.NpmPkg, + expected: "pkg:npm/npm/express", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := getPURL(tt.component, tt.pkgType) + assert.Equal(t, tt.expected, result) + }) + } +} diff --git a/syft/format/purls/decoder_test.go b/syft/format/purls/decoder_test.go index 26484ecd4..8609bcb7a 100644 --- a/syft/format/purls/decoder_test.go +++ b/syft/format/purls/decoder_test.go @@ -165,13 +165,9 @@ func TestDecoder_Decode(t *testing.T) { Type: pkg.JavaPkg, PURL: "pkg:maven/org.apache/some-pkg@4.11.3", Language: pkg.Java, - Metadata: pkg.JavaArchive{ - PomProperties: &pkg.JavaPomProperties{ - GroupID: "org.apache", - ArtifactID: "some-pkg", - Version: "4.11.3", - }, - }, + // we intentionally do not claim we found a pom properties file (don't derive this from the purl). + // but we need a metadata allocated since all Java packages have a this metadata type (a consistency point) + Metadata: pkg.JavaArchive{}, }, }, },