diff --git a/internal/formats/common/cyclonedxhelpers/component.go b/internal/formats/common/cyclonedxhelpers/component.go index 949d6d004d0..d4f6606aeaf 100644 --- a/internal/formats/common/cyclonedxhelpers/component.go +++ b/internal/formats/common/cyclonedxhelpers/component.go @@ -5,6 +5,7 @@ import ( "github.com/CycloneDX/cyclonedx-go" + "github.com/anchore/packageurl-go" "github.com/anchore/syft/internal/formats/common" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/source" @@ -39,9 +40,23 @@ func encodeComponent(p pkg.Package) cyclonedx.Component { Description: encodeDescription(p), ExternalReferences: encodeExternalReferences(p), Properties: properties, + BOMRef: deriveBomRef(p), } } +func deriveBomRef(p pkg.Package) string { + // try and parse the PURL if possible and append syft id to it, to make + // the purl unique in the BOM. + // TODO: In the future we may want to dedupe by PURL and combine components with + // the same PURL while preserving their unique metadata. + if parsedPURL, err := packageurl.FromString(p.PURL); err == nil { + parsedPURL.Qualifiers = append(parsedPURL.Qualifiers, packageurl.Qualifier{Key: "syft-id", Value: string(p.ID())}) + return parsedPURL.ToString() + } + // fallback is to use strictly the ID if there is no valid pURL + return string(p.ID()) +} + func hasMetadata(p pkg.Package) bool { return p.Metadata != nil } diff --git a/internal/formats/common/cyclonedxhelpers/component_test.go b/internal/formats/common/cyclonedxhelpers/component_test.go index 4a6dc50518a..ab7f3b812bb 100644 --- a/internal/formats/common/cyclonedxhelpers/component_test.go +++ b/internal/formats/common/cyclonedxhelpers/component_test.go @@ -1,6 +1,7 @@ package cyclonedxhelpers import ( + "fmt" "testing" "github.com/CycloneDX/cyclonedx-go" @@ -139,3 +140,54 @@ func Test_encodeComponentProperties(t *testing.T) { }) } } + +func Test_deriveBomRef(t *testing.T) { + pkgWithPurl := pkg.Package{ + Name: "django", + Version: "1.11.1", + PURL: "pkg:pypi/django@1.11.1", + } + pkgWithPurl.SetID() + + pkgWithOutPurl := pkg.Package{ + Name: "django", + Version: "1.11.1", + PURL: "", + } + pkgWithOutPurl.SetID() + + pkgWithBadPurl := pkg.Package{ + Name: "django", + Version: "1.11.1", + PURL: "pkg:pyjango@1.11.1", + } + pkgWithBadPurl.SetID() + + tests := []struct { + name string + pkg pkg.Package + want string + }{ + { + name: "use pURL-id hybrid", + pkg: pkgWithPurl, + want: fmt.Sprintf("pkg:pypi/django@1.11.1?syft-id=%s", pkgWithPurl.ID()), + }, + { + name: "fallback to ID when pURL is invalid", + pkg: pkgWithBadPurl, + want: string(pkgWithBadPurl.ID()), + }, + { + name: "fallback to ID when pURL is missing", + pkg: pkgWithOutPurl, + want: string(pkgWithOutPurl.ID()), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.pkg.ID() + assert.Equal(t, tt.want, deriveBomRef(tt.pkg)) + }) + } +} diff --git a/internal/formats/common/testutils/utils.go b/internal/formats/common/testutils/utils.go index 809889456cb..b1004d506fb 100644 --- a/internal/formats/common/testutils/utils.go +++ b/internal/formats/common/testutils/utils.go @@ -169,7 +169,7 @@ func populateImageCatalog(catalog *pkg.Catalog, img *image.Image) { Name: "package-1", Version: "1.0.1", }, - PURL: "a-purl-1", + PURL: "a-purl-1", // intentionally a bad pURL for test fixtures CPEs: []pkg.CPE{ pkg.MustCPE("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*"), }, @@ -187,7 +187,7 @@ func populateImageCatalog(catalog *pkg.Catalog, img *image.Image) { Package: "package-2", Version: "2.0.1", }, - PURL: "a-purl-2", + PURL: "pkg:deb/debian/package-2@2.0.1", CPEs: []pkg.CPE{ pkg.MustCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"), }, @@ -249,7 +249,7 @@ func newDirectoryCatalog() *pkg.Catalog { }, }, }, - PURL: "a-purl-2", + PURL: "a-purl-2", // intentionally a bad pURL for test fixtures CPEs: []pkg.CPE{ pkg.MustCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"), }, @@ -267,7 +267,7 @@ func newDirectoryCatalog() *pkg.Catalog { Package: "package-2", Version: "2.0.1", }, - PURL: "a-purl-2", + PURL: "pkg:deb/debian/package-2@2.0.1", CPEs: []pkg.CPE{ pkg.MustCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"), }, diff --git a/internal/formats/cyclonedxjson/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden b/internal/formats/cyclonedxjson/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden index 3063fdd1c8c..69b562d973e 100644 --- a/internal/formats/cyclonedxjson/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden +++ b/internal/formats/cyclonedxjson/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden @@ -1,10 +1,10 @@ { "bomFormat": "CycloneDX", "specVersion": "1.4", - "serialNumber": "urn:uuid:498e659b-0758-4a7f-816e-91bee18df634", + "serialNumber": "urn:uuid:dec3f6b4-8458-48bb-b60d-dfd312f6ec4e", "version": 1, "metadata": { - "timestamp": "2022-03-08T12:30:39Z", + "timestamp": "2022-04-01T11:48:04-04:00", "tools": [ { "vendor": "anchore", @@ -20,6 +20,7 @@ }, "components": [ { + "bom-ref": "b85dbb4e6ece5082", "type": "library", "name": "package-1", "version": "1.0.1", @@ -56,11 +57,12 @@ ] }, { + "bom-ref": "pkg:deb/debian/package-2@2.0.1?syft-id=ceda99598967ae8d", "type": "library", "name": "package-2", "version": "2.0.1", "cpe": "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*", - "purl": "a-purl-2", + "purl": "pkg:deb/debian/package-2@2.0.1", "properties": [ { "name": "syft:package:foundBy", diff --git a/internal/formats/cyclonedxjson/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden b/internal/formats/cyclonedxjson/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden index 62f5871eb4d..14478f6c8dd 100644 --- a/internal/formats/cyclonedxjson/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden +++ b/internal/formats/cyclonedxjson/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden @@ -1,10 +1,10 @@ { "bomFormat": "CycloneDX", "specVersion": "1.4", - "serialNumber": "urn:uuid:342c3d2c-d26e-47b6-94d6-92fbf41da945", + "serialNumber": "urn:uuid:054d973e-fe99-4762-92e4-eaf01997ae41", "version": 1, "metadata": { - "timestamp": "2022-03-08T12:30:39Z", + "timestamp": "2022-04-01T11:48:04-04:00", "tools": [ { "vendor": "anchore", @@ -13,7 +13,7 @@ } ], "component": { - "bom-ref": "711095b1cdf90cce", + "bom-ref": "e777314b02b362e4", "type": "container", "name": "user-image-input", "version": "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368" @@ -21,6 +21,7 @@ }, "components": [ { + "bom-ref": "2a46171f91c8d4bc", "type": "library", "name": "package-1", "version": "1.0.1", @@ -52,7 +53,7 @@ }, { "name": "syft:location:0:layerID", - "value": "sha256:16e64541f2ddf59a90391ce7bb8af90313f7d373f2105d88f3d3267b72e0ebab" + "value": "sha256:fb6beecb75b39f4bb813dbf177e501edd5ddb3e69bb45cedeb78c676ee1b7a59" }, { "name": "syft:location:0:path", @@ -61,11 +62,12 @@ ] }, { + "bom-ref": "pkg:deb/debian/package-2@2.0.1?syft-id=ae77680e9b1d087e", "type": "library", "name": "package-2", "version": "2.0.1", "cpe": "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*", - "purl": "a-purl-2", + "purl": "pkg:deb/debian/package-2@2.0.1", "properties": [ { "name": "syft:package:foundBy", @@ -81,7 +83,7 @@ }, { "name": "syft:location:0:layerID", - "value": "sha256:de6c235f76ea24c8503ec08891445b5d6a8bdf8249117ed8d8b0b6fb3ebe4f67" + "value": "sha256:319b588ce64253a87b533c8ed01cf0025e0eac98e7b516e12532957e1244fdec" }, { "name": "syft:location:0:path", diff --git a/internal/formats/cyclonedxjson/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden b/internal/formats/cyclonedxjson/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden index c483fa49b75..5b5b8030509 100644 Binary files a/internal/formats/cyclonedxjson/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden and b/internal/formats/cyclonedxjson/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden differ diff --git a/internal/formats/cyclonedxxml/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden b/internal/formats/cyclonedxxml/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden index 3e416a26e26..b75e0c629dd 100644 --- a/internal/formats/cyclonedxxml/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden +++ b/internal/formats/cyclonedxxml/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden @@ -1,7 +1,7 @@ - + - 2022-03-08T12:30:33Z + 2022-04-01T11:57:46-04:00 anchore @@ -14,7 +14,7 @@ - + package-1 1.0.1 @@ -32,11 +32,11 @@ /some/path/pkg1 - + package-2 2.0.1 cpe:2.3:*:some:package:2:*:*:*:*:*:*:* - a-purl-2 + pkg:deb/debian/package-2@2.0.1 the-cataloger-2 DpkgMetadata diff --git a/internal/formats/cyclonedxxml/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden b/internal/formats/cyclonedxxml/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden index 5d1b9dae417..a82e85c7f45 100644 --- a/internal/formats/cyclonedxxml/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden +++ b/internal/formats/cyclonedxxml/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden @@ -1,7 +1,7 @@ - + - 2022-03-08T12:30:33Z + 2022-04-01T11:57:46-04:00 anchore @@ -9,13 +9,13 @@ [not provided] - + user-image-input sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368 - + package-1 1.0.1 @@ -30,20 +30,20 @@ python PythonPackageMetadata python - sha256:16e64541f2ddf59a90391ce7bb8af90313f7d373f2105d88f3d3267b72e0ebab + sha256:fb6beecb75b39f4bb813dbf177e501edd5ddb3e69bb45cedeb78c676ee1b7a59 /somefile-1.txt - + package-2 2.0.1 cpe:2.3:*:some:package:2:*:*:*:*:*:*:* - a-purl-2 + pkg:deb/debian/package-2@2.0.1 the-cataloger-2 DpkgMetadata deb - sha256:de6c235f76ea24c8503ec08891445b5d6a8bdf8249117ed8d8b0b6fb3ebe4f67 + sha256:319b588ce64253a87b533c8ed01cf0025e0eac98e7b516e12532957e1244fdec /somefile-2.txt 0 diff --git a/internal/formats/cyclonedxxml/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden b/internal/formats/cyclonedxxml/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden index c483fa49b75..5b5b8030509 100644 Binary files a/internal/formats/cyclonedxxml/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden and b/internal/formats/cyclonedxxml/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden differ diff --git a/internal/formats/spdx22json/test-fixtures/snapshot/TestSPDXJSONDirectoryEncoder.golden b/internal/formats/spdx22json/test-fixtures/snapshot/TestSPDXJSONDirectoryEncoder.golden index 22f7729c7e3..3299321a589 100644 --- a/internal/formats/spdx22json/test-fixtures/snapshot/TestSPDXJSONDirectoryEncoder.golden +++ b/internal/formats/spdx22json/test-fixtures/snapshot/TestSPDXJSONDirectoryEncoder.golden @@ -3,7 +3,7 @@ "name": "/some/path", "spdxVersion": "SPDX-2.2", "creationInfo": { - "created": "2022-03-30T21:48:28.297464Z", + "created": "2022-04-01T15:48:39.459232Z", "creators": [ "Organization: Anchore, Inc", "Tool: syft-[not provided]" @@ -11,7 +11,7 @@ "licenseListVersion": "3.16" }, "dataLicense": "CC0-1.0", - "documentNamespace": "https://anchore.com/syft/dir/some/path-e188d59b-76f6-4c7f-a9f2-1ae7d0577781", + "documentNamespace": "https://anchore.com/syft/dir/some/path-8d335d81-29c9-4236-84f1-2292ea92aaf5", "packages": [ { "SPDXID": "SPDXRef-b85dbb4e6ece5082", @@ -48,7 +48,7 @@ }, { "referenceCategory": "PACKAGE_MANAGER", - "referenceLocator": "a-purl-2", + "referenceLocator": "pkg:deb/debian/package-2@2.0.1", "referenceType": "purl" } ], diff --git a/internal/formats/spdx22json/test-fixtures/snapshot/TestSPDXJSONImageEncoder.golden b/internal/formats/spdx22json/test-fixtures/snapshot/TestSPDXJSONImageEncoder.golden index 7e97a75fe97..42260c91d43 100644 --- a/internal/formats/spdx22json/test-fixtures/snapshot/TestSPDXJSONImageEncoder.golden +++ b/internal/formats/spdx22json/test-fixtures/snapshot/TestSPDXJSONImageEncoder.golden @@ -3,7 +3,7 @@ "name": "user-image-input", "spdxVersion": "SPDX-2.2", "creationInfo": { - "created": "2022-03-30T21:48:28.303986Z", + "created": "2022-04-01T15:48:39.465643Z", "creators": [ "Organization: Anchore, Inc", "Tool: syft-[not provided]" @@ -11,7 +11,7 @@ "licenseListVersion": "3.16" }, "dataLicense": "CC0-1.0", - "documentNamespace": "https://anchore.com/syft/image/user-image-input-9e4f4190-c5ae-4e31-a852-d1ab71357516", + "documentNamespace": "https://anchore.com/syft/image/user-image-input-e64e0be8-5031-4eec-842d-e59fb6deb518", "packages": [ { "SPDXID": "SPDXRef-2a46171f91c8d4bc", @@ -48,7 +48,7 @@ }, { "referenceCategory": "PACKAGE_MANAGER", - "referenceLocator": "a-purl-2", + "referenceLocator": "pkg:deb/debian/package-2@2.0.1", "referenceType": "purl" } ], diff --git a/internal/formats/spdx22tagvalue/test-fixtures/snapshot/TestSPDXTagValueDirectoryEncoder.golden b/internal/formats/spdx22tagvalue/test-fixtures/snapshot/TestSPDXTagValueDirectoryEncoder.golden index 7959c2f0d34..ba0ba4c69a6 100644 --- a/internal/formats/spdx22tagvalue/test-fixtures/snapshot/TestSPDXTagValueDirectoryEncoder.golden +++ b/internal/formats/spdx22tagvalue/test-fixtures/snapshot/TestSPDXTagValueDirectoryEncoder.golden @@ -2,11 +2,11 @@ SPDXVersion: SPDX-2.2 DataLicense: CC0-1.0 SPDXID: SPDXRef-DOCUMENT DocumentName: /some/path -DocumentNamespace: https://anchore.com/syft/dir/some/path-71aa3553-1a73-405f-9f1f-6347d6d4593b +DocumentNamespace: https://anchore.com/syft/dir/some/path-d227b0f2-4ee8-4e10-ac43-019db86d16ff LicenseListVersion: 3.16 Creator: Organization: Anchore, Inc Creator: Tool: syft-[not provided] -Created: 2022-03-30T21:48:22Z +Created: 2022-04-01T15:48:44Z ##### Package: package-2 @@ -19,7 +19,7 @@ PackageLicenseConcluded: NONE PackageLicenseDeclared: NONE PackageCopyrightText: NOASSERTION ExternalRef: SECURITY cpe23Type cpe:2.3:*:some:package:2:*:*:*:*:*:*:* -ExternalRef: PACKAGE_MANAGER purl a-purl-2 +ExternalRef: PACKAGE_MANAGER purl pkg:deb/debian/package-2@2.0.1 ##### Package: package-1 diff --git a/internal/formats/spdx22tagvalue/test-fixtures/snapshot/TestSPDXTagValueImageEncoder.golden b/internal/formats/spdx22tagvalue/test-fixtures/snapshot/TestSPDXTagValueImageEncoder.golden index 4d9011b1cdd..f2e7d394f0e 100644 --- a/internal/formats/spdx22tagvalue/test-fixtures/snapshot/TestSPDXTagValueImageEncoder.golden +++ b/internal/formats/spdx22tagvalue/test-fixtures/snapshot/TestSPDXTagValueImageEncoder.golden @@ -2,11 +2,11 @@ SPDXVersion: SPDX-2.2 DataLicense: CC0-1.0 SPDXID: SPDXRef-DOCUMENT DocumentName: user-image-input -DocumentNamespace: https://anchore.com/syft/image/user-image-input-e46e20f4-43a4-40e7-9f82-fd55b8a89e5f +DocumentNamespace: https://anchore.com/syft/image/user-image-input-49f98c61-3418-4427-9e00-8b1c735e9799 LicenseListVersion: 3.16 Creator: Organization: Anchore, Inc Creator: Tool: syft-[not provided] -Created: 2022-03-30T21:48:22Z +Created: 2022-04-01T15:48:44Z ##### Package: package-2 @@ -19,7 +19,7 @@ PackageLicenseConcluded: NONE PackageLicenseDeclared: NONE PackageCopyrightText: NOASSERTION ExternalRef: SECURITY cpe23Type cpe:2.3:*:some:package:2:*:*:*:*:*:*:* -ExternalRef: PACKAGE_MANAGER purl a-purl-2 +ExternalRef: PACKAGE_MANAGER purl pkg:deb/debian/package-2@2.0.1 ##### Package: package-1 diff --git a/internal/formats/syftjson/test-fixtures/snapshot/TestDirectoryEncoder.golden b/internal/formats/syftjson/test-fixtures/snapshot/TestDirectoryEncoder.golden index 71d089e1947..ae51ed71720 100644 --- a/internal/formats/syftjson/test-fixtures/snapshot/TestDirectoryEncoder.golden +++ b/internal/formats/syftjson/test-fixtures/snapshot/TestDirectoryEncoder.golden @@ -51,7 +51,7 @@ "cpes": [ "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*" ], - "purl": "a-purl-2", + "purl": "pkg:deb/debian/package-2@2.0.1", "metadataType": "DpkgMetadata", "metadata": { "package": "package-2", diff --git a/internal/formats/syftjson/test-fixtures/snapshot/TestImageEncoder.golden b/internal/formats/syftjson/test-fixtures/snapshot/TestImageEncoder.golden index a2f5673d096..10a5ced626f 100644 --- a/internal/formats/syftjson/test-fixtures/snapshot/TestImageEncoder.golden +++ b/internal/formats/syftjson/test-fixtures/snapshot/TestImageEncoder.golden @@ -48,7 +48,7 @@ "cpes": [ "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*" ], - "purl": "a-purl-2", + "purl": "pkg:deb/debian/package-2@2.0.1", "metadataType": "DpkgMetadata", "metadata": { "package": "package-2",