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>
This commit is contained in:
swadaptive
2026-10-29 17:42:00 -04:00
committed by GitHub
parent 4b8a3db5f3
commit 168722d880
8 changed files with 925 additions and 1 deletions
+14
View File
@@ -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...).
@@ -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"
}
]
}
]
}
+14
View File
@@ -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...).
@@ -0,0 +1,131 @@
<?xml version="1.0" encoding="UTF-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.7" serialNumber="redacted" version="1">
<metadata>
<timestamp>redacted</timestamp>
<tools>
<components>
<component type="application">
<author>anchore</author>
<name>syft</name>
<version>v0.42.0-bogus</version>
</component>
</components>
</tools>
<component bom-ref="redacted" type="file">
<name>some/path</name>
</component>
</metadata>
<components>
<component bom-ref="pkg:deb/debian/attr@1%3A2.4.47-2%2Bb1?arch=amd64&amp;distro=debian-12&amp;package-id=5cc0e8979d7f5115&amp;upstream=attr%401%3A2.4.47-2" type="library">
<name>attr</name>
<version>1:2.4.47-2+b1</version>
<purl>pkg:deb/debian/attr@1%3A2.4.47-2%2Bb1?arch=amd64&amp;distro=debian-12&amp;upstream=attr%401%3A2.4.47-2</purl>
<externalReferences>
<reference type="source-distribution">
<url>pkg:deb/debian/attr@1%3A2.4.47-2?arch=source&amp;distro=debian-12</url>
</reference>
</externalReferences>
<properties>
<property name="syft:package:foundBy">dpkg-db-cataloger</property>
<property name="syft:package:type">deb</property>
<property name="syft:package:metadataType">dpkg-db-entry</property>
<property name="syft:location:0:path">/var/lib/dpkg/status</property>
<property name="syft:metadata:installedSize">0</property>
<property name="syft:metadata:source">attr</property>
<property name="syft:metadata:sourceVersion">1:2.4.47-2</property>
</properties>
</component>
<component bom-ref="pkg:rpm/centos/bind-export-libs@9.11.13-3.el8?arch=x86_64&amp;distro=centos-8&amp;epoch=32&amp;package-id=6008fca05ca7b418&amp;upstream=bind-9.11.13-3.el8.src.rpm" type="library">
<name>bind-export-libs</name>
<version>32:9.11.13-3.el8</version>
<purl>pkg:rpm/centos/bind-export-libs@9.11.13-3.el8?arch=x86_64&amp;distro=centos-8&amp;epoch=32&amp;upstream=bind-9.11.13-3.el8.src.rpm</purl>
<externalReferences>
<reference type="source-distribution">
<url>pkg:rpm/centos/bind@9.11.13-3.el8?arch=src&amp;distro=centos-8&amp;epoch=32</url>
</reference>
</externalReferences>
<properties>
<property name="syft:package:foundBy">rpm-db-cataloger</property>
<property name="syft:package:type">rpm</property>
<property name="syft:package:metadataType">rpm-db-entry</property>
<property name="syft:location:0:path">/var/lib/rpm/Packages</property>
<property name="syft:metadata:epoch">32</property>
<property name="syft:metadata:release">3.el8</property>
<property name="syft:metadata:size">0</property>
<property name="syft:metadata:sourceRpm">bind-9.11.13-3.el8.src.rpm</property>
</properties>
</component>
<component bom-ref="pkg:apk/alpine/busybox@1.35.0-r17?arch=x86_64&amp;distro=alpine-3.16.3&amp;package-id=aa74af1190de1c40" type="library">
<name>busybox</name>
<version>1.35.0-r17</version>
<purl>pkg:apk/alpine/busybox@1.35.0-r17?arch=x86_64&amp;distro=alpine-3.16.3</purl>
<properties>
<property name="syft:package:foundBy">apk-db-cataloger</property>
<property name="syft:package:type">apk</property>
<property name="syft:package:metadataType">apk-db-entry</property>
<property name="syft:location:0:path">/lib/apk/db/installed</property>
<property name="syft:metadata:installedSize">0</property>
<property name="syft:metadata:originPackage">busybox</property>
<property name="syft:metadata:size">0</property>
</properties>
</component>
<component bom-ref="pkg:alpm/arch/gcc-libs@13.2.1-3?arch=x86_64&amp;distro=arch-rolling&amp;package-id=58a2fcf749a9a3cc&amp;upstream=gcc" type="library">
<name>gcc-libs</name>
<version>13.2.1-3</version>
<purl>pkg:alpm/arch/gcc-libs@13.2.1-3?arch=x86_64&amp;distro=arch-rolling&amp;upstream=gcc</purl>
<externalReferences>
<reference type="source-distribution">
<url>pkg:alpm/arch/gcc@13.2.1-3?distro=arch-rolling</url>
</reference>
</externalReferences>
<properties>
<property name="syft:package:foundBy">alpm-db-cataloger</property>
<property name="syft:package:type">alpm</property>
<property name="syft:package:metadataType">alpm-db-entry</property>
<property name="syft:location:0:path">/var/lib/pacman/local/gcc-libs-13.2.1-3/desc</property>
<property name="syft:metadata:architecture">x86_64</property>
<property name="syft:metadata:basepackage">gcc</property>
<property name="syft:metadata:package">gcc-libs</property>
<property name="syft:metadata:size">0</property>
<property name="syft:metadata:version">13.2.1-3</property>
</properties>
</component>
<component bom-ref="pkg:apk/alpine/libc-utils@0.7.2-r3?arch=x86_64&amp;distro=alpine-3.16.3&amp;package-id=16a8c7b19fde6f4f&amp;upstream=libc-dev" type="library">
<name>libc-utils</name>
<version>0.7.2-r3</version>
<purl>pkg:apk/alpine/libc-utils@0.7.2-r3?arch=x86_64&amp;distro=alpine-3.16.3&amp;upstream=libc-dev</purl>
<externalReferences>
<reference type="source-distribution">
<url>pkg:apk/alpine/libc-dev@0.7.2-r3?distro=alpine-3.16.3</url>
</reference>
</externalReferences>
<properties>
<property name="syft:package:foundBy">apk-db-cataloger</property>
<property name="syft:package:type">apk</property>
<property name="syft:package:metadataType">apk-db-entry</property>
<property name="syft:location:0:path">/lib/apk/db/installed</property>
<property name="syft:metadata:installedSize">0</property>
<property name="syft:metadata:originPackage">libc-dev</property>
<property name="syft:metadata:size">0</property>
</properties>
</component>
<component bom-ref="pkg:deb/debian/libpam-runtime@1.5.2-6%2Bdeb12u1?arch=all&amp;distro=debian-12&amp;package-id=7b4ddc97df57aec4&amp;upstream=pam" type="library">
<name>libpam-runtime</name>
<version>1.5.2-6+deb12u1</version>
<purl>pkg:deb/debian/libpam-runtime@1.5.2-6%2Bdeb12u1?arch=all&amp;distro=debian-12&amp;upstream=pam</purl>
<externalReferences>
<reference type="source-distribution">
<url>pkg:deb/debian/pam@1.5.2-6%2Bdeb12u1?arch=source&amp;distro=debian-12</url>
</reference>
</externalReferences>
<properties>
<property name="syft:package:foundBy">dpkg-db-cataloger</property>
<property name="syft:package:type">deb</property>
<property name="syft:package:metadataType">dpkg-db-entry</property>
<property name="syft:location:0:path">/var/lib/dpkg/status</property>
<property name="syft:metadata:installedSize">0</property>
<property name="syft:metadata:source">pam</property>
</properties>
</component>
</components>
</bom>
@@ -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),
}
@@ -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) {
// <name>-<version>-<release>.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"
@@ -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
@@ -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
}