diff --git a/integration/testdata/pom-cyclonedx.json.golden b/integration/testdata/pom-cyclonedx.json.golden index ac245de144ce..a77c75eb1f65 100644 --- a/integration/testdata/pom-cyclonedx.json.golden +++ b/integration/testdata/pom-cyclonedx.json.golden @@ -44,7 +44,8 @@ { "bom-ref": "pkg:maven/com.example/log4shell@1.0-SNAPSHOT", "type": "library", - "name": "com.example:log4shell", + "group": "com.example", + "name": "log4shell", "version": "1.0-SNAPSHOT", "purl": "pkg:maven/com.example/log4shell@1.0-SNAPSHOT", "properties": [ @@ -61,7 +62,8 @@ { "bom-ref": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.9.1", "type": "library", - "name": "com.fasterxml.jackson.core:jackson-databind", + "group": "com.fasterxml.jackson.core", + "name": "jackson-databind", "version": "2.9.1", "purl": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.9.1", "properties": [ diff --git a/pkg/sbom/cyclonedx/marshal.go b/pkg/sbom/cyclonedx/marshal.go index 59564dddd6f8..287b4fee40cc 100644 --- a/pkg/sbom/cyclonedx/marshal.go +++ b/pkg/sbom/cyclonedx/marshal.go @@ -7,6 +7,7 @@ import ( "strings" cdx "github.com/CycloneDX/cyclonedx-go" + "github.com/package-url/packageurl-go" "github.com/samber/lo" "golang.org/x/xerrors" @@ -323,8 +324,11 @@ func pkgComponent(pkg Package) (*core.Component, error) { // e.g. local Go packages if pu := pkg.Identifier.PURL; pu != nil { version = pu.Version - // use `group` field for GroupID and `name` for ArtifactID for jar files - if pkg.Type == ftypes.Jar { + // Use `group` field for GroupID and `name` for ArtifactID for java files + // https://github.com/aquasecurity/trivy/issues/4675 + // Use `group` field for npm scopes + // https://github.com/aquasecurity/trivy/issues/5908 + if pu.Type == packageurl.TypeMaven || pu.Type == packageurl.TypeNPM { name = pu.Name group = pu.Namespace } diff --git a/pkg/sbom/cyclonedx/marshal_test.go b/pkg/sbom/cyclonedx/marshal_test.go index 5a3022c91b0c..9d1f2d33aab0 100644 --- a/pkg/sbom/cyclonedx/marshal_test.go +++ b/pkg/sbom/cyclonedx/marshal_test.go @@ -1224,6 +1224,26 @@ func TestMarshaler_Marshal(t *testing.T) { }, }, }, + { + Target: "yarn.lock", + Class: types.ClassLangPkg, + Type: ftypes.Yarn, + Packages: []ftypes.Package{ + { + ID: "@babel/helper-string-parser@7.23.4", + Name: "@babel/helper-string-parser", + Version: "7.23.4", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeNPM, + Namespace: "@babel", + Name: "helper-string-parser", + Version: "7.23.4", + }, + }, + }, + }, + }, }, }, want: &cdx.BOM{ @@ -1270,6 +1290,21 @@ func TestMarshaler_Marshal(t *testing.T) { }, }, }, + { + BOMRef: "3ff14136-e09f-4df9-80ea-000000000004", + Type: cdx.ComponentTypeApplication, + Name: "yarn.lock", + Properties: &[]cdx.Property{ + { + Name: "aquasecurity:trivy:Class", + Value: "lang-pkgs", + }, + { + Name: "aquasecurity:trivy:Type", + Value: "yarn", + }, + }, + }, { BOMRef: "pkg:gem/actioncable@6.1.4.1", Type: "library", @@ -1301,6 +1336,24 @@ func TestMarshaler_Marshal(t *testing.T) { }, }, }, + { + BOMRef: "pkg:npm/%40babel/helper-string-parser@7.23.4", + Type: "library", + Name: "helper-string-parser", + Group: "@babel", + Version: "7.23.4", + PackageURL: "pkg:npm/%40babel/helper-string-parser@7.23.4", + Properties: &[]cdx.Property{ + { + Name: "aquasecurity:trivy:PkgID", + Value: "@babel/helper-string-parser@7.23.4", + }, + { + Name: "aquasecurity:trivy:PkgType", + Value: "yarn", + }, + }, + }, }, Vulnerabilities: &[]cdx.Vulnerability{}, Dependencies: &[]cdx.Dependency{ @@ -1308,6 +1361,7 @@ func TestMarshaler_Marshal(t *testing.T) { Ref: "3ff14136-e09f-4df9-80ea-000000000002", Dependencies: &[]string{ "3ff14136-e09f-4df9-80ea-000000000003", + "3ff14136-e09f-4df9-80ea-000000000004", "pkg:maven/org.springframework/spring-web@5.3.22?file_path=spring-web-5.3.22.jar", }, }, @@ -1317,6 +1371,12 @@ func TestMarshaler_Marshal(t *testing.T) { "pkg:gem/actioncable@6.1.4.1", }, }, + { + Ref: "3ff14136-e09f-4df9-80ea-000000000004", + Dependencies: &[]string{ + "pkg:npm/%40babel/helper-string-parser@7.23.4", + }, + }, { Ref: "pkg:gem/actioncable@6.1.4.1", Dependencies: lo.ToPtr([]string{}), @@ -1325,6 +1385,10 @@ func TestMarshaler_Marshal(t *testing.T) { Ref: "pkg:maven/org.springframework/spring-web@5.3.22?file_path=spring-web-5.3.22.jar", Dependencies: lo.ToPtr([]string{}), }, + { + Ref: "pkg:npm/%40babel/helper-string-parser@7.23.4", + Dependencies: lo.ToPtr([]string{}), + }, }, }, }, diff --git a/pkg/sbom/cyclonedx/testdata/happy/bom.json b/pkg/sbom/cyclonedx/testdata/happy/bom.json index 924daef7f405..ada1488b8cbc 100644 --- a/pkg/sbom/cyclonedx/testdata/happy/bom.json +++ b/pkg/sbom/cyclonedx/testdata/happy/bom.json @@ -121,8 +121,9 @@ ] }, { - "bom-ref": "pkg:npm/bootstrap@5.0.2?file_path=app%2Fapp%2Fpackage.json", + "bom-ref": "pkg:npm/@example/bootstrap@5.0.2?file_path=app%2Fapp%2Fpackage.json", "type": "library", + "group": "@example", "name": "bootstrap", "version": "5.0.2", "licenses": [ @@ -132,7 +133,7 @@ } } ], - "purl": "pkg:npm/bootstrap@5.0.2", + "purl": "pkg:npm/@example/bootstrap@5.0.2", "properties": [ { "name": "aquasecurity:trivy:FilePath", @@ -265,7 +266,7 @@ "60e9f57b-d4a6-4f71-ad14-0893ac609182", "pkg:maven/org.codehaus.mojo/child-project@1.0?file_path=app%2Fmaven%2Ftarget%2Fchild-project-1.0.jar", "pkg:maven/com.example/example@0.0.1", - "pkg:npm/bootstrap@5.0.2?file_path=app%2Fapp%2Fpackage.json", + "pkg:npm/@example/bootstrap@5.0.2?file_path=app%2Fapp%2Fpackage.json", "100925ff-7c0a-470f-a725-8fb973b40e7b", "1a111e6b-a682-470e-8b0e-aaa49d93cd39" ] diff --git a/pkg/sbom/cyclonedx/unmarshal.go b/pkg/sbom/cyclonedx/unmarshal.go index 96fa09364640..921c3455d121 100644 --- a/pkg/sbom/cyclonedx/unmarshal.go +++ b/pkg/sbom/cyclonedx/unmarshal.go @@ -419,10 +419,11 @@ func toTrivyCdxComponent(component cdx.Component) ftypes.Component { } func packageName(typ, pkgNameFromPurl string, component cdx.Component) string { - if typ == packageurl.TypeMaven { - // Jar uses `Group` field for `GroupID` + if typ == packageurl.TypeMaven || typ == packageurl.TypeNPM { + // Maven uses `Group` field for `GroupID` + // Npm uses `Group` field for `Scope` if component.Group != "" { - return fmt.Sprintf("%s:%s", component.Group, component.Name) + return fmt.Sprintf("%s%s%s", component.Group, packageNameSeparator(typ), component.Name) } else { // use name derived from purl if `Group` doesn't exist return pkgNameFromPurl @@ -431,6 +432,14 @@ func packageName(typ, pkgNameFromPurl string, component cdx.Component) string { return component.Name } +// packageNameSeparator selects separator to join `group` and `name` fields of the component +func packageNameSeparator(typ string) string { + if typ == packageurl.TypeMaven { + return ":" + } + return "/" +} + // parsePackageLicenses checks all supported license fields and returns a list of licenses. // https://cyclonedx.org/docs/1.5/json/#components_items_licenses func parsePackageLicenses(l *cdx.Licenses) []string { diff --git a/pkg/sbom/cyclonedx/unmarshal_test.go b/pkg/sbom/cyclonedx/unmarshal_test.go index f62677a9ac70..61b24082343e 100644 --- a/pkg/sbom/cyclonedx/unmarshal_test.go +++ b/pkg/sbom/cyclonedx/unmarshal_test.go @@ -172,15 +172,16 @@ func TestUnmarshaler_Unmarshal(t *testing.T) { FilePath: "", Libraries: ftypes.Packages{ { - Name: "bootstrap", + Name: "@example/bootstrap", Version: "5.0.2", Identifier: ftypes.PkgIdentifier{ PURL: &packageurl.PackageURL{ - Type: packageurl.TypeNPM, - Name: "bootstrap", - Version: "5.0.2", + Type: packageurl.TypeNPM, + Namespace: "@example", + Name: "bootstrap", + Version: "5.0.2", }, - BOMRef: "pkg:npm/bootstrap@5.0.2?file_path=app%2Fapp%2Fpackage.json", + BOMRef: "pkg:npm/@example/bootstrap@5.0.2?file_path=app%2Fapp%2Fpackage.json", }, Licenses: []string{"MIT"}, Layer: ftypes.Layer{