From 168722d88099f31430b5b79f932ab51d97301bfe Mon Sep 17 00:00:00 2001 From: swadaptive <268603322+swadaptive@users.noreply.github.com> Date: Thu, 29 Oct 2026 21:42:00 +0000 Subject: [PATCH] feat(cyclonedx): output source PURL as external reference (#5246) * Add tests for source packages in external references Signed-off-by: swadaptive <268603322+swadaptive@users.noreply.github.com> * Source distribution external reference utilities and test coverage Signed-off-by: swadaptive <268603322+swadaptive@users.noreply.github.com> * Add source-distribution external reference to components Signed-off-by: swadaptive <268603322+swadaptive@users.noreply.github.com> * Golden test fixtures Signed-off-by: swadaptive <268603322+swadaptive@users.noreply.github.com> * Adjust arch qualifier for source PURLs Signed-off-by: swadaptive <268603322+swadaptive@users.noreply.github.com> --------- Signed-off-by: swadaptive <268603322+swadaptive@users.noreply.github.com> --- syft/format/cyclonedxjson/encoder_test.go | 14 + .../TestCycloneDxSourcePackageEncoder.golden | 287 ++++++++++++++++++ syft/format/cyclonedxxml/encoder_test.go | 14 + .../TestCycloneDxSourcePackageEncoder.golden | 131 ++++++++ .../cyclonedxutil/helpers/component.go | 10 +- .../helpers/external_references.go | 86 ++++++ .../helpers/external_references_test.go | 238 +++++++++++++++ .../internal/testutil/source_package_input.go | 146 +++++++++ 8 files changed, 925 insertions(+), 1 deletion(-) create mode 100644 syft/format/cyclonedxjson/testdata/snapshot/TestCycloneDxSourcePackageEncoder.golden create mode 100644 syft/format/cyclonedxxml/testdata/snapshot/TestCycloneDxSourcePackageEncoder.golden create mode 100644 syft/format/internal/testutil/source_package_input.go diff --git a/syft/format/cyclonedxjson/encoder_test.go b/syft/format/cyclonedxjson/encoder_test.go index 00ece4a8a..8b57de976 100644 --- a/syft/format/cyclonedxjson/encoder_test.go +++ b/syft/format/cyclonedxjson/encoder_test.go @@ -114,6 +114,20 @@ func TestCycloneDxImageEncoder(t *testing.T) { ) } +func TestCycloneDxSourcePackageEncoder(t *testing.T) { + dir := t.TempDir() + testutil.AssertEncoderAgainstGoldenSnapshot(t, + testutil.EncoderSnapshotTestConfig{ + Subject: testutil.SourcePackagesInput(t, dir), + Format: getEncoder(t), + UpdateSnapshot: *updateSnapshot, + PersistRedactionsInSnapshot: true, + IsJSON: true, + Redactor: redactor(dir), + }, + ) +} + func redactor(values ...string) testutil.Redactor { return testutil.NewRedactions(). WithValuesRedacted(values...). diff --git a/syft/format/cyclonedxjson/testdata/snapshot/TestCycloneDxSourcePackageEncoder.golden b/syft/format/cyclonedxjson/testdata/snapshot/TestCycloneDxSourcePackageEncoder.golden new file mode 100644 index 000000000..6995d4eae --- /dev/null +++ b/syft/format/cyclonedxjson/testdata/snapshot/TestCycloneDxSourcePackageEncoder.golden @@ -0,0 +1,287 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.7.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.7", + "serialNumber": "urn:uuid:redacted", + "version": 1, + "metadata": { + "timestamp": "timestamp:redacted", + "tools": { + "components": [ + { + "type": "application", + "author": "anchore", + "name": "syft", + "version": "v0.42.0-bogus" + } + ] + }, + "component": { + "bom-ref": "redacted", + "type": "file", + "name": "some/path" + } + }, + "components": [ + { + "bom-ref": "pkg:deb/debian/attr@1%3A2.4.47-2%2Bb1?arch=amd64&distro=debian-12&package-id=5cc0e8979d7f5115&upstream=attr%401%3A2.4.47-2", + "type": "library", + "name": "attr", + "version": "1:2.4.47-2+b1", + "purl": "pkg:deb/debian/attr@1%3A2.4.47-2%2Bb1?arch=amd64&distro=debian-12&upstream=attr%401%3A2.4.47-2", + "externalReferences": [ + { + "url": "pkg:deb/debian/attr@1%3A2.4.47-2?arch=source&distro=debian-12", + "type": "source-distribution" + } + ], + "properties": [ + { + "name": "syft:package:foundBy", + "value": "dpkg-db-cataloger" + }, + { + "name": "syft:package:type", + "value": "deb" + }, + { + "name": "syft:package:metadataType", + "value": "dpkg-db-entry" + }, + { + "name": "syft:location:0:path", + "value": "/var/lib/dpkg/status" + }, + { + "name": "syft:metadata:installedSize", + "value": "0" + }, + { + "name": "syft:metadata:source", + "value": "attr" + }, + { + "name": "syft:metadata:sourceVersion", + "value": "1:2.4.47-2" + } + ] + }, + { + "bom-ref": "pkg:rpm/centos/bind-export-libs@9.11.13-3.el8?arch=x86_64&distro=centos-8&epoch=32&package-id=6008fca05ca7b418&upstream=bind-9.11.13-3.el8.src.rpm", + "type": "library", + "name": "bind-export-libs", + "version": "32:9.11.13-3.el8", + "purl": "pkg:rpm/centos/bind-export-libs@9.11.13-3.el8?arch=x86_64&distro=centos-8&epoch=32&upstream=bind-9.11.13-3.el8.src.rpm", + "externalReferences": [ + { + "url": "pkg:rpm/centos/bind@9.11.13-3.el8?arch=src&distro=centos-8&epoch=32", + "type": "source-distribution" + } + ], + "properties": [ + { + "name": "syft:package:foundBy", + "value": "rpm-db-cataloger" + }, + { + "name": "syft:package:type", + "value": "rpm" + }, + { + "name": "syft:package:metadataType", + "value": "rpm-db-entry" + }, + { + "name": "syft:location:0:path", + "value": "/var/lib/rpm/Packages" + }, + { + "name": "syft:metadata:epoch", + "value": "32" + }, + { + "name": "syft:metadata:release", + "value": "3.el8" + }, + { + "name": "syft:metadata:size", + "value": "0" + }, + { + "name": "syft:metadata:sourceRpm", + "value": "bind-9.11.13-3.el8.src.rpm" + } + ] + }, + { + "bom-ref": "pkg:apk/alpine/busybox@1.35.0-r17?arch=x86_64&distro=alpine-3.16.3&package-id=aa74af1190de1c40", + "type": "library", + "name": "busybox", + "version": "1.35.0-r17", + "purl": "pkg:apk/alpine/busybox@1.35.0-r17?arch=x86_64&distro=alpine-3.16.3", + "properties": [ + { + "name": "syft:package:foundBy", + "value": "apk-db-cataloger" + }, + { + "name": "syft:package:type", + "value": "apk" + }, + { + "name": "syft:package:metadataType", + "value": "apk-db-entry" + }, + { + "name": "syft:location:0:path", + "value": "/lib/apk/db/installed" + }, + { + "name": "syft:metadata:installedSize", + "value": "0" + }, + { + "name": "syft:metadata:originPackage", + "value": "busybox" + }, + { + "name": "syft:metadata:size", + "value": "0" + } + ] + }, + { + "bom-ref": "pkg:alpm/arch/gcc-libs@13.2.1-3?arch=x86_64&distro=arch-rolling&package-id=58a2fcf749a9a3cc&upstream=gcc", + "type": "library", + "name": "gcc-libs", + "version": "13.2.1-3", + "purl": "pkg:alpm/arch/gcc-libs@13.2.1-3?arch=x86_64&distro=arch-rolling&upstream=gcc", + "externalReferences": [ + { + "url": "pkg:alpm/arch/gcc@13.2.1-3?distro=arch-rolling", + "type": "source-distribution" + } + ], + "properties": [ + { + "name": "syft:package:foundBy", + "value": "alpm-db-cataloger" + }, + { + "name": "syft:package:type", + "value": "alpm" + }, + { + "name": "syft:package:metadataType", + "value": "alpm-db-entry" + }, + { + "name": "syft:location:0:path", + "value": "/var/lib/pacman/local/gcc-libs-13.2.1-3/desc" + }, + { + "name": "syft:metadata:architecture", + "value": "x86_64" + }, + { + "name": "syft:metadata:basepackage", + "value": "gcc" + }, + { + "name": "syft:metadata:package", + "value": "gcc-libs" + }, + { + "name": "syft:metadata:size", + "value": "0" + }, + { + "name": "syft:metadata:version", + "value": "13.2.1-3" + } + ] + }, + { + "bom-ref": "pkg:apk/alpine/libc-utils@0.7.2-r3?arch=x86_64&distro=alpine-3.16.3&package-id=16a8c7b19fde6f4f&upstream=libc-dev", + "type": "library", + "name": "libc-utils", + "version": "0.7.2-r3", + "purl": "pkg:apk/alpine/libc-utils@0.7.2-r3?arch=x86_64&distro=alpine-3.16.3&upstream=libc-dev", + "externalReferences": [ + { + "url": "pkg:apk/alpine/libc-dev@0.7.2-r3?distro=alpine-3.16.3", + "type": "source-distribution" + } + ], + "properties": [ + { + "name": "syft:package:foundBy", + "value": "apk-db-cataloger" + }, + { + "name": "syft:package:type", + "value": "apk" + }, + { + "name": "syft:package:metadataType", + "value": "apk-db-entry" + }, + { + "name": "syft:location:0:path", + "value": "/lib/apk/db/installed" + }, + { + "name": "syft:metadata:installedSize", + "value": "0" + }, + { + "name": "syft:metadata:originPackage", + "value": "libc-dev" + }, + { + "name": "syft:metadata:size", + "value": "0" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libpam-runtime@1.5.2-6%2Bdeb12u1?arch=all&distro=debian-12&package-id=7b4ddc97df57aec4&upstream=pam", + "type": "library", + "name": "libpam-runtime", + "version": "1.5.2-6+deb12u1", + "purl": "pkg:deb/debian/libpam-runtime@1.5.2-6%2Bdeb12u1?arch=all&distro=debian-12&upstream=pam", + "externalReferences": [ + { + "url": "pkg:deb/debian/pam@1.5.2-6%2Bdeb12u1?arch=source&distro=debian-12", + "type": "source-distribution" + } + ], + "properties": [ + { + "name": "syft:package:foundBy", + "value": "dpkg-db-cataloger" + }, + { + "name": "syft:package:type", + "value": "deb" + }, + { + "name": "syft:package:metadataType", + "value": "dpkg-db-entry" + }, + { + "name": "syft:location:0:path", + "value": "/var/lib/dpkg/status" + }, + { + "name": "syft:metadata:installedSize", + "value": "0" + }, + { + "name": "syft:metadata:source", + "value": "pam" + } + ] + } + ] +} diff --git a/syft/format/cyclonedxxml/encoder_test.go b/syft/format/cyclonedxxml/encoder_test.go index 5f25bec76..c38bd1363 100644 --- a/syft/format/cyclonedxxml/encoder_test.go +++ b/syft/format/cyclonedxxml/encoder_test.go @@ -87,6 +87,20 @@ func TestCycloneDxImageEncoder(t *testing.T) { ) } +func TestCycloneDxSourcePackageEncoder(t *testing.T) { + dir := t.TempDir() + testutil.AssertEncoderAgainstGoldenSnapshot(t, + testutil.EncoderSnapshotTestConfig{ + Subject: testutil.SourcePackagesInput(t, dir), + Format: getEncoder(t), + UpdateSnapshot: *updateSnapshot, + PersistRedactionsInSnapshot: true, + IsJSON: false, + Redactor: redactor(dir), + }, + ) +} + func redactor(values ...string) testutil.Redactor { return testutil.NewRedactions(). WithValuesRedacted(values...). diff --git a/syft/format/cyclonedxxml/testdata/snapshot/TestCycloneDxSourcePackageEncoder.golden b/syft/format/cyclonedxxml/testdata/snapshot/TestCycloneDxSourcePackageEncoder.golden new file mode 100644 index 000000000..830062cdb --- /dev/null +++ b/syft/format/cyclonedxxml/testdata/snapshot/TestCycloneDxSourcePackageEncoder.golden @@ -0,0 +1,131 @@ + + + + redacted + + + + anchore + syft + v0.42.0-bogus + + + + + some/path + + + + + attr + 1:2.4.47-2+b1 + pkg:deb/debian/attr@1%3A2.4.47-2%2Bb1?arch=amd64&distro=debian-12&upstream=attr%401%3A2.4.47-2 + + + pkg:deb/debian/attr@1%3A2.4.47-2?arch=source&distro=debian-12 + + + + dpkg-db-cataloger + deb + dpkg-db-entry + /var/lib/dpkg/status + 0 + attr + 1:2.4.47-2 + + + + bind-export-libs + 32:9.11.13-3.el8 + pkg:rpm/centos/bind-export-libs@9.11.13-3.el8?arch=x86_64&distro=centos-8&epoch=32&upstream=bind-9.11.13-3.el8.src.rpm + + + pkg:rpm/centos/bind@9.11.13-3.el8?arch=src&distro=centos-8&epoch=32 + + + + rpm-db-cataloger + rpm + rpm-db-entry + /var/lib/rpm/Packages + 32 + 3.el8 + 0 + bind-9.11.13-3.el8.src.rpm + + + + busybox + 1.35.0-r17 + pkg:apk/alpine/busybox@1.35.0-r17?arch=x86_64&distro=alpine-3.16.3 + + apk-db-cataloger + apk + apk-db-entry + /lib/apk/db/installed + 0 + busybox + 0 + + + + gcc-libs + 13.2.1-3 + pkg:alpm/arch/gcc-libs@13.2.1-3?arch=x86_64&distro=arch-rolling&upstream=gcc + + + pkg:alpm/arch/gcc@13.2.1-3?distro=arch-rolling + + + + alpm-db-cataloger + alpm + alpm-db-entry + /var/lib/pacman/local/gcc-libs-13.2.1-3/desc + x86_64 + gcc + gcc-libs + 0 + 13.2.1-3 + + + + libc-utils + 0.7.2-r3 + pkg:apk/alpine/libc-utils@0.7.2-r3?arch=x86_64&distro=alpine-3.16.3&upstream=libc-dev + + + pkg:apk/alpine/libc-dev@0.7.2-r3?distro=alpine-3.16.3 + + + + apk-db-cataloger + apk + apk-db-entry + /lib/apk/db/installed + 0 + libc-dev + 0 + + + + libpam-runtime + 1.5.2-6+deb12u1 + pkg:deb/debian/libpam-runtime@1.5.2-6%2Bdeb12u1?arch=all&distro=debian-12&upstream=pam + + + pkg:deb/debian/pam@1.5.2-6%2Bdeb12u1?arch=source&distro=debian-12 + + + + dpkg-db-cataloger + deb + dpkg-db-entry + /var/lib/dpkg/status + 0 + pam + + + + \ No newline at end of file diff --git a/syft/format/internal/cyclonedxutil/helpers/component.go b/syft/format/internal/cyclonedxutil/helpers/component.go index 2e289fc84..dc788f5ba 100644 --- a/syft/format/internal/cyclonedxutil/helpers/component.go +++ b/syft/format/internal/cyclonedxutil/helpers/component.go @@ -47,6 +47,14 @@ func EncodeComponent(p pkg.Package, supplier string, locationSorter func(a, b fi componentType = cyclonedx.ComponentTypeMachineLearningModel } + externalRefs := encodeExternalReferences(p) + if srcRef := encodeSourcePackageExternalReference(p); srcRef != nil { + if externalRefs == nil { + externalRefs = &[]cyclonedx.ExternalReference{} + } + *externalRefs = append(*externalRefs, *srcRef) + } + return cyclonedx.Component{ Type: componentType, Name: p.Name, @@ -59,7 +67,7 @@ func EncodeComponent(p pkg.Package, supplier string, locationSorter func(a, b fi Author: encodeAuthor(p), Publisher: encodePublisher(p), Description: encodeDescription(p), - ExternalReferences: encodeExternalReferences(p), + ExternalReferences: externalRefs, Properties: properties, BOMRef: DeriveBomRef(p), } diff --git a/syft/format/internal/cyclonedxutil/helpers/external_references.go b/syft/format/internal/cyclonedxutil/helpers/external_references.go index 4f7762142..2d2f45a56 100644 --- a/syft/format/internal/cyclonedxutil/helpers/external_references.go +++ b/syft/format/internal/cyclonedxutil/helpers/external_references.go @@ -7,6 +7,7 @@ import ( "github.com/CycloneDX/cyclonedx-go" + "github.com/anchore/packageurl-go" "github.com/anchore/syft/internal/file" syftFile "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/pkg" @@ -79,12 +80,97 @@ func encodeExternalReferences(p pkg.Package) *[]cyclonedx.ExternalReference { } } } + if len(refs) > 0 { return &refs } return nil } +func encodeSourcePackageExternalReference(p pkg.Package) *cyclonedx.ExternalReference { + name, version, arch := sourceCoordinates(p) + if name == "" || version == "" { + return nil + } + if p.PURL == "" { + return nil + } + + pkgPurl, err := packageurl.FromString(p.PURL) + if err != nil { + return nil + } + + qualifiers := pkgPurl.Qualifiers.Map() + delete(qualifiers, pkg.PURLQualifierUpstream) + delete(qualifiers, pkg.PURLQualifierArch) + + if arch != "" { + qualifiers[pkg.PURLQualifierArch] = arch + } + + srcPurl := packageurl.NewPackageURL( + pkgPurl.Type, + pkgPurl.Namespace, + name, + version, + packageurl.QualifiersFromMap(qualifiers), + pkgPurl.Subpath, + ) + + return &cyclonedx.ExternalReference{ + Type: cyclonedx.ERTypeSourceDistribution, + URL: srcPurl.ToString(), + } +} + +func sourceCoordinates(p pkg.Package) (name, version, arch string) { + switch metadata := p.Metadata.(type) { + case pkg.ApkDBEntry: + if metadata.OriginPackage != "" && metadata.OriginPackage != metadata.Package { + return metadata.OriginPackage, metadata.Version, "" + } + case pkg.DpkgDBEntry: + return dpkgSourceCoordinates(metadata) + case pkg.DpkgArchiveEntry: + return dpkgSourceCoordinates(pkg.DpkgDBEntry(metadata)) + case pkg.RpmDBEntry: + return rpmSourceCoordinates(metadata.SourceRpm) + case pkg.RpmArchive: + return rpmSourceCoordinates(metadata.SourceRpm) + case pkg.AlpmDBEntry: + if metadata.BasePackage != "" { + return metadata.BasePackage, metadata.Version, "" + } + } + return "", "", "" +} + +func dpkgSourceCoordinates(entry pkg.DpkgDBEntry) (name, version, arch string) { + if entry.Source == "" { + return "", "", "" + } + if entry.SourceVersion != "" { + return entry.Source, entry.SourceVersion, "source" + } + return entry.Source, entry.Version, "source" +} + +func rpmSourceCoordinates(sourceRpm string) (name, version, arch string) { + // --.src.rpm + sourceRpm = strings.TrimSuffix(sourceRpm, ".rpm") + sourceRpm = strings.TrimSuffix(strings.TrimSuffix(sourceRpm, ".src"), ".nosrc") + release := strings.LastIndex(sourceRpm, "-") + if release < 0 { + return "", "", "" + } + ver := strings.LastIndex(sourceRpm[:release], "-") + if ver < 0 { + return "", "", "" + } + return sourceRpm[:ver], sourceRpm[ver+1:], "src" +} + // supported algorithm in cycloneDX as of 1.4 // "MD5", "SHA-1", "SHA-256", "SHA-384", "SHA-512", // "SHA3-256", "SHA3-384", "SHA3-512", "BLAKE2b-256", "BLAKE2b-384", "BLAKE2b-512", "BLAKE3" diff --git a/syft/format/internal/cyclonedxutil/helpers/external_references_test.go b/syft/format/internal/cyclonedxutil/helpers/external_references_test.go index 6da98024e..b91d590f0 100644 --- a/syft/format/internal/cyclonedxutil/helpers/external_references_test.go +++ b/syft/format/internal/cyclonedxutil/helpers/external_references_test.go @@ -144,6 +144,244 @@ func Test_encodeExternalReferences(t *testing.T) { } } +func Test_encodeSourcePackage(t *testing.T) { + rpmEpoch := 32 + tests := []struct { + name string + input pkg.Package + expected *cyclonedx.ExternalReference + }{ + { + name: "no metadata", + input: pkg.Package{}, + expected: nil, + }, + { + name: "no PURL", + input: pkg.Package{ + Name: "some-package", + Version: "1.0.0-r1", + Type: pkg.ApkPkg, + Metadata: pkg.ApkDBEntry{ + Package: "some-package", + OriginPackage: "some-package", + Version: "1.0.0-r1", + Architecture: "x86_64", + }, + }, + expected: nil, + }, + { + name: "from apk", + input: pkg.Package{ + Name: "libc-utils", + Version: "0.7.2-r3", + Type: pkg.ApkPkg, + PURL: "pkg:apk/alpine/libc-utils@0.7.2-r3?arch=x86_64&distro=alpine-3.16.3&upstream=libc-dev", + Metadata: pkg.ApkDBEntry{ + Package: "libc-utils", + OriginPackage: "libc-dev", + Version: "0.7.2-r3", + Architecture: "x86_64", + }, + }, + expected: &cyclonedx.ExternalReference{ + Type: cyclonedx.ERTypeSourceDistribution, URL: "pkg:apk/alpine/libc-dev@0.7.2-r3?distro=alpine-3.16.3", + }, + }, + { + // Matching existing implementation where upstream qualifier is omitted when OriginPackage = Package for Alpine + name: "from apk with source = bin", + input: pkg.Package{ + Name: "some-package", + Version: "1.0.0-r1", + Type: pkg.ApkPkg, + PURL: "pkg:apk/alpine/some-package@1.0.0-r1?arch=x86_64&distro=alpine-3.16.3", + Metadata: pkg.ApkDBEntry{ + Package: "some-package", + OriginPackage: "some-package", + Version: "1.0.0-r1", + Architecture: "x86_64", + }, + }, + expected: nil, + }, + { + name: "from deb", + input: pkg.Package{ + Name: "libpam-runtime", + Version: "1.5.2-6+deb12u1", + Type: pkg.DebPkg, + PURL: "pkg:deb/debian/libpam-runtime@1.5.2-6%2Bdeb12u1?arch=all&distro=debian-12&upstream=pam", + Metadata: pkg.DpkgDBEntry{ + Package: "libpam-runtime", + Version: "1.5.2-6+deb12u1", + Source: "pam", + Architecture: "all", + }, + }, + expected: &cyclonedx.ExternalReference{ + Type: cyclonedx.ERTypeSourceDistribution, URL: "pkg:deb/debian/pam@1.5.2-6%2Bdeb12u1?arch=source&distro=debian-12", + }, + }, + { + name: "from deb with source version", + input: pkg.Package{ + Name: "attr", + Version: "1:2.4.47-2+b1", + Type: pkg.DebPkg, + PURL: "pkg:deb/debian/attr@1%3A2.4.47-2%2Bb1?arch=amd64&distro=debian-12&upstream=attr%401%3A2.4.47-2", + Metadata: pkg.DpkgDBEntry{ + Package: "attr", + Version: "1:2.4.47-2+b1", + Source: "attr", + SourceVersion: "1:2.4.47-2", + Architecture: "amd64", + }, + }, + expected: &cyclonedx.ExternalReference{ + Type: cyclonedx.ERTypeSourceDistribution, URL: "pkg:deb/debian/attr@1%3A2.4.47-2?arch=source&distro=debian-12", + }, + }, + { + name: "from deb with empty source", + input: pkg.Package{ + Name: "some-package", + Version: "1.0.0", + Type: pkg.DebPkg, + PURL: "pkg:deb/debian/some-package@1.0.0?arch=all&distro=debian-12", + Metadata: pkg.DpkgDBEntry{ + Package: "some-package", + Version: "1.0.0", + Architecture: "all", + }, + }, + expected: nil, + }, + { + name: "from deb archive", + input: pkg.Package{ + Name: "libpam-runtime", + Version: "1.5.2-6+deb12u1", + Type: pkg.DebPkg, + PURL: "pkg:deb/libpam-runtime@1.5.2-6%2Bdeb12u1?arch=all&upstream=pam", + Metadata: pkg.DpkgArchiveEntry{ + Package: "libpam-runtime", + Version: "1.5.2-6+deb12u1", + Source: "pam", + Architecture: "all", + }, + }, + expected: &cyclonedx.ExternalReference{ + Type: cyclonedx.ERTypeSourceDistribution, URL: "pkg:deb/pam@1.5.2-6%2Bdeb12u1?arch=source", + }, + }, + { + name: "from rpm", + input: pkg.Package{ + Name: "bind-export-libs", + Version: "32:9.11.13-3.el8", + Type: pkg.RpmPkg, + PURL: "pkg:rpm/centos/bind-export-libs@9.11.13-3.el8?arch=x86_64&distro=centos-8&epoch=32&upstream=bind-9.11.13-3.el8.src.rpm", + Metadata: pkg.RpmDBEntry{ + Name: "bind-export-libs", + Version: "9.11.13", + Release: "3.el8", + Epoch: &rpmEpoch, + Arch: "x86_64", + SourceRpm: "bind-9.11.13-3.el8.src.rpm", + }, + }, + expected: &cyclonedx.ExternalReference{ + Type: cyclonedx.ERTypeSourceDistribution, URL: "pkg:rpm/centos/bind@9.11.13-3.el8?arch=src&distro=centos-8&epoch=32", + }, + }, + { + name: "from rpm with source = bin", + input: pkg.Package{ + Name: "some-package", + Version: "32:1.0.0-1.el8", + Type: pkg.RpmPkg, + PURL: "pkg:rpm/centos/some-package@1.0.0-1.el8?arch=x86_64&distro=centos-8&epoch=32&upstream=some-package-1.0.0-1.el8.src.rpm", + Metadata: pkg.RpmDBEntry{ + Name: "some-package", + Version: "1.0.0", + Release: "1.el8", + Epoch: &rpmEpoch, + Arch: "x86_64", + SourceRpm: "some-package-1.0.0-1.el8.src.rpm", + }, + }, + expected: &cyclonedx.ExternalReference{ + Type: cyclonedx.ERTypeSourceDistribution, URL: "pkg:rpm/centos/some-package@1.0.0-1.el8?arch=src&distro=centos-8&epoch=32", + }, + }, + { + name: "from rpm archive", + input: pkg.Package{ + Name: "bind-export-libs", + Version: "32:9.11.13-3.el8", + Type: pkg.RpmPkg, + PURL: "pkg:rpm/bind-export-libs@9.11.13-3.el8?arch=x86_64&epoch=32&upstream=bind-9.11.13-3.el8.src.rpm", + Metadata: pkg.RpmArchive{ + Name: "bind-export-libs", + Version: "9.11.13", + Release: "3.el8", + Epoch: &rpmEpoch, + Arch: "x86_64", + SourceRpm: "bind-9.11.13-3.el8.src.rpm", + }, + }, + expected: &cyclonedx.ExternalReference{ + Type: cyclonedx.ERTypeSourceDistribution, URL: "pkg:rpm/bind@9.11.13-3.el8?arch=src&epoch=32", + }, + }, + { + name: "from alpm", + input: pkg.Package{ + Name: "gcc-libs", + Version: "13.2.1-3", + Type: pkg.AlpmPkg, + PURL: "pkg:alpm/arch/gcc-libs@13.2.1-3?arch=x86_64&distro=arch-rolling&upstream=gcc", + Metadata: pkg.AlpmDBEntry{ + Package: "gcc-libs", + Version: "13.2.1-3", + BasePackage: "gcc", + Architecture: "x86_64", + }, + }, + expected: &cyclonedx.ExternalReference{ + Type: cyclonedx.ERTypeSourceDistribution, URL: "pkg:alpm/arch/gcc@13.2.1-3?distro=arch-rolling", + }, + }, + { + // Matching existing implementation where upstream qualifier is *not* omitted when BasePackage == Package for Alpm + // This differs from the Alpine case where it is omitted. + name: "from alpm with source = bin", + input: pkg.Package{ + Name: "some-package", + Version: "1.0.0", + Type: pkg.AlpmPkg, + PURL: "pkg:alpm/arch/some-package@1.0.0?arch=x86_64&distro=arch-rolling&upstream=some-package", + Metadata: pkg.AlpmDBEntry{ + Package: "some-package", + Version: "1.0.0", + BasePackage: "some-package", + Architecture: "x86_64", + }, + }, + expected: &cyclonedx.ExternalReference{ + Type: cyclonedx.ERTypeSourceDistribution, URL: "pkg:alpm/arch/some-package@1.0.0?distro=arch-rolling", + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expected, encodeSourcePackageExternalReference(test.input)) + }) + } +} + func Test_isValidExternalRef(t *testing.T) { tests := []struct { name string diff --git a/syft/format/internal/testutil/source_package_input.go b/syft/format/internal/testutil/source_package_input.go new file mode 100644 index 000000000..afc1bdc7e --- /dev/null +++ b/syft/format/internal/testutil/source_package_input.go @@ -0,0 +1,146 @@ +package testutil + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/sbom" + "github.com/anchore/syft/syft/source/directorysource" +) + +func SourcePackagesInput(t testing.TB, dir string) sbom.SBOM { + path := filepath.Join(dir, "some", "path") + require.NoError(t, os.MkdirAll(path, 0755)) + + src, err := directorysource.New( + directorysource.Config{ + Path: path, + Base: dir, + }, + ) + require.NoError(t, err) + + return sbom.SBOM{ + Artifacts: sbom.Artifacts{ + Packages: newSourcePackageCatalog(), + }, + Source: src.Describe(), + Descriptor: sbom.Descriptor{ + Name: "syft", + Version: "v0.42.0-bogus", + }, + } +} + +//nolint:funlen +func newSourcePackageCatalog() *pkg.Collection { + epoch := 32 + + catalog := pkg.NewCollection() + + // apk: the origin differs from the package name, and shares its version + catalog.Add(pkg.Package{ + Name: "libc-utils", + Version: "0.7.2-r3", + Type: pkg.ApkPkg, + FoundBy: "apk-db-cataloger", + Locations: file.NewLocationSet(file.NewLocation("/lib/apk/db/installed")), + PURL: "pkg:apk/alpine/libc-utils@0.7.2-r3?arch=x86_64&distro=alpine-3.16.3&upstream=libc-dev", + Metadata: pkg.ApkDBEntry{ + Package: "libc-utils", + OriginPackage: "libc-dev", + Version: "0.7.2-r3", + Architecture: "x86_64", + }, + }) + + // apk: negative control -- the package is its own origin, so no source reference is expected + catalog.Add(pkg.Package{ + Name: "busybox", + Version: "1.35.0-r17", + Type: pkg.ApkPkg, + FoundBy: "apk-db-cataloger", + Locations: file.NewLocationSet(file.NewLocation("/lib/apk/db/installed")), + PURL: "pkg:apk/alpine/busybox@1.35.0-r17?arch=x86_64&distro=alpine-3.16.3", + Metadata: pkg.ApkDBEntry{ + Package: "busybox", + OriginPackage: "busybox", + Version: "1.35.0-r17", + Architecture: "x86_64", + }, + }) + + // deb: source name differs, and the source version is implied by the binary version + catalog.Add(pkg.Package{ + Name: "libpam-runtime", + Version: "1.5.2-6+deb12u1", + Type: pkg.DebPkg, + FoundBy: "dpkg-db-cataloger", + Locations: file.NewLocationSet(file.NewLocation("/var/lib/dpkg/status")), + PURL: "pkg:deb/debian/libpam-runtime@1.5.2-6%2Bdeb12u1?arch=all&distro=debian-12&upstream=pam", + Metadata: pkg.DpkgDBEntry{ + Package: "libpam-runtime", + Version: "1.5.2-6+deb12u1", + Source: "pam", + Architecture: "all", + }, + }) + + // deb: the source version differs from the binary version + catalog.Add(pkg.Package{ + Name: "attr", + Version: "1:2.4.47-2+b1", + Type: pkg.DebPkg, + FoundBy: "dpkg-db-cataloger", + Locations: file.NewLocationSet(file.NewLocation("/var/lib/dpkg/status")), + PURL: "pkg:deb/debian/attr@1%3A2.4.47-2%2Bb1?arch=amd64&distro=debian-12&upstream=attr%401%3A2.4.47-2", + Metadata: pkg.DpkgDBEntry{ + Package: "attr", + Version: "1:2.4.47-2+b1", + Source: "attr", + SourceVersion: "1:2.4.47-2", + Architecture: "amd64", + }, + }) + + // rpm: name and version both come out of the source RPM filename, and the epoch is carried over + catalog.Add(pkg.Package{ + Name: "bind-export-libs", + Version: "32:9.11.13-3.el8", + Type: pkg.RpmPkg, + FoundBy: "rpm-db-cataloger", + Locations: file.NewLocationSet(file.NewLocation("/var/lib/rpm/Packages")), + PURL: "pkg:rpm/centos/bind-export-libs@9.11.13-3.el8?arch=x86_64&distro=centos-8&epoch=32&upstream=bind-9.11.13-3.el8.src.rpm", + Metadata: pkg.RpmDBEntry{ + Name: "bind-export-libs", + Version: "9.11.13", + Release: "3.el8", + Epoch: &epoch, + Arch: "x86_64", + SourceRpm: "bind-9.11.13-3.el8.src.rpm", + }, + }) + + // alpm: the base package differs from the package name, and shares its version + catalog.Add(pkg.Package{ + Name: "gcc-libs", + Version: "13.2.1-3", + Type: pkg.AlpmPkg, + FoundBy: "alpm-db-cataloger", + Locations: file.NewLocationSet(file.NewLocation("/var/lib/pacman/local/gcc-libs-13.2.1-3/desc")), + PURL: "pkg:alpm/arch/gcc-libs@13.2.1-3?arch=x86_64&distro=arch-rolling&upstream=gcc", + Metadata: pkg.AlpmDBEntry{ + Package: "gcc-libs", + Version: "13.2.1-3", + BasePackage: "gcc", + Architecture: "x86_64", + }, + }) + + return catalog +}