Source distribution external reference utilities and test coverage

Signed-off-by: swadaptive <268603322+swadaptive@users.noreply.github.com>
This commit is contained in:
swadaptive
2026-09-30 12:56:05 -04:00
committed by Alex Goodman
parent ecf6b9eff5
commit 06451adc43
2 changed files with 206 additions and 4 deletions
@@ -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"
@@ -86,8 +87,84 @@ func encodeExternalReferences(p pkg.Package) *[]cyclonedx.ExternalReference {
}
func encodeSourcePackageExternalReference(p pkg.Package) *cyclonedx.ExternalReference {
name, version := 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)
qualifiers[pkg.PURLQualifierArch] = "source"
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 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 string) {
if entry.Source == "" {
return "", ""
}
if entry.SourceVersion != "" {
return entry.Source, entry.SourceVersion
}
return entry.Source, entry.Version
}
func rpmSourceCoordinates(sourceRpm string) (name, version 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:]
}
// supported algorithm in cycloneDX as of 1.4
// "MD5", "SHA-1", "SHA-256", "SHA-384", "SHA-512",
@@ -156,6 +156,21 @@ func Test_encodeSourcePackage(t *testing.T) {
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{
@@ -171,11 +186,28 @@ func Test_encodeSourcePackage(t *testing.T) {
},
},
expected: &cyclonedx.ExternalReference{
Type: cyclonedx.ERTypeSourceDistribution, URL: "pkg:apk/alpine/libc-dev@0.7.2-r3&arch=source&distro=alpine-3.16.3",
Type: cyclonedx.ERTypeSourceDistribution, URL: "pkg:apk/alpine/libc-dev@0.7.2-r3?arch=source&distro=alpine-3.16.3",
},
},
{
name: "from dpkg",
// 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",
@@ -193,7 +225,7 @@ func Test_encodeSourcePackage(t *testing.T) {
},
},
{
name: "from dpkg with source version",
name: "from deb with source version",
input: pkg.Package{
Name: "attr",
Version: "1:2.4.47-2+b1",
@@ -211,6 +243,39 @@ func Test_encodeSourcePackage(t *testing.T) {
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{
@@ -231,6 +296,46 @@ func Test_encodeSourcePackage(t *testing.T) {
Type: cyclonedx.ERTypeSourceDistribution, URL: "pkg:rpm/centos/bind@9.11.13-3.el8?arch=source&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: &rpm_epoch,
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=source&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: &rpm_epoch,
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=source&epoch=32",
},
},
{
name: "from alpm",
input: pkg.Package{
@@ -249,6 +354,26 @@ func Test_encodeSourcePackage(t *testing.T) {
Type: cyclonedx.ERTypeSourceDistribution, URL: "pkg:alpm/arch/gcc@13.2.1-3?arch=source&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?arch=source&distro=arch-rolling",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {