add cdx group as purl namespace (#3922)

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Alex Goodman 2025-05-20 15:56:08 -04:00 committed by GitHub
parent e23ca43a83
commit ac883f52ed
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 198 additions and 38 deletions

View File

@ -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{}
}
}

View File

@ -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{},
},
},
}

View File

@ -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 != "" {

View File

@ -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)
})
}
}

View File

@ -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{},
},
},
},