limit source-distribution refs to deb and rpm

- apk origins and alpm pkgbases are build recipes, not published source packages, and their names are usually also real binary packages, so a `source-distribution` ref there would point at a binary package. They keep relying on the `upstream` PURL qualifier.
- document why this is an external reference rather than `pedigree.ancestors`, with links to the CycloneDX spec discussion (CycloneDX/specification#612)

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Alex Goodman
2026-09-30 12:56:05 -04:00
parent bf8ded3cf8
commit ab69742d25
5 changed files with 42 additions and 83 deletions
@@ -156,12 +156,6 @@
"name": "gcc-libs", "name": "gcc-libs",
"version": "13.2.1-3", "version": "13.2.1-3",
"purl": "pkg:alpm/arch/gcc-libs@13.2.1-3?arch=x86_64&distro=arch-rolling&upstream=gcc", "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": [ "properties": [
{ {
"name": "syft:package:foundBy", "name": "syft:package:foundBy",
@@ -207,12 +201,6 @@
"name": "libc-utils", "name": "libc-utils",
"version": "0.7.2-r3", "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", "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": [ "properties": [
{ {
"name": "syft:package:foundBy", "name": "syft:package:foundBy",
@@ -73,11 +73,6 @@
<name>gcc-libs</name> <name>gcc-libs</name>
<version>13.2.1-3</version> <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> <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> <properties>
<property name="syft:package:foundBy">alpm-db-cataloger</property> <property name="syft:package:foundBy">alpm-db-cataloger</property>
<property name="syft:package:type">alpm</property> <property name="syft:package:type">alpm</property>
@@ -94,11 +89,6 @@
<name>libc-utils</name> <name>libc-utils</name>
<version>0.7.2-r3</version> <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> <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> <properties>
<property name="syft:package:foundBy">apk-db-cataloger</property> <property name="syft:package:foundBy">apk-db-cataloger</property>
<property name="syft:package:type">apk</property> <property name="syft:package:type">apk</property>
@@ -89,9 +89,26 @@ func encodeExternalReferences(p pkg.Package) *[]cyclonedx.ExternalReference {
return nil return nil
} }
// encodeSourcePackageExternalReference returns a "source-distribution" reference holding the PURL of the source // encodeSourcePackageExternalReference returns a "source-distribution" reference holding the PURL of the distro
// package that an OS package was built from. It is only emitted when the source differs from the binary package // source package that a binary OS package was built from (e.g. libpam-runtime -> pkg:deb/debian/pam@...?arch=source).
// (by name or version) and when the package PURL is of the type implied by the metadata. // It is only emitted when the source differs from the binary package (by name or version) and when the package PURL
// is of the type implied by the metadata. The main consumer is vulnerability matching, since distros tend to report
// vulnerabilities against the source package rather than each binary package built from it.
//
// why an external reference and not pedigree: `component.pedigree.ancestors` looks like the obvious spot for "this
// binary came from that source", but pedigree describes code lineage (forks, patched variants, and the patches that
// separate them). A binary built unmodified from its own distro source package is the same code in a different form,
// not a fork, and CycloneDX components are deliberately agnostic to source vs binary form (see the comments from the
// CycloneDX maintainers in https://github.com/CycloneDX/specification/issues/612#issuecomment-2958800363). The
// ancestor slot is better left for the upstream project a distro patched (e.g. Linux-PAM for Debian's pam), which
// syft does not know today.
//
// why "source-distribution": the spec defines it as "the location where the source code distributable can be
// obtained", and a deb/rpm source package is exactly that distributable (a .dsc and tarballs in the Debian source
// pool, a .src.rpm in an SRPMS repo). The value is a PURL (an identifier) rather than a download URL, which the
// schema allows since external reference URLs are URIs of any scheme, and a CycloneDX maintainer confirmed this exact
// shape is valid in https://github.com/CycloneDX/specification/issues/612#issuecomment-5428057874. A first-class
// "source" component type was discussed in that issue and deferred to CycloneDX 2.0.
func encodeSourcePackageExternalReference(p pkg.Package) *cyclonedx.ExternalReference { func encodeSourcePackageExternalReference(p pkg.Package) *cyclonedx.ExternalReference {
if p.PURL == "" { if p.PURL == "" {
return nil return nil
@@ -138,7 +155,7 @@ type sourcePackage struct {
purlType string purlType string
name string name string
version string version string
// arch is the ecosystem's source architecture marker, empty when the ecosystem has no such convention // arch is the ecosystem's source architecture marker ("source" for deb, "src" or "nosrc" for rpm)
arch string arch string
} }
@@ -153,14 +170,17 @@ func (s sourcePackage) keepQualifier(key string) bool {
} }
// sourcePackageOf returns the source package for p, or nil when there is none or it is the same as the binary. // sourcePackageOf returns the source package for p, or nil when there is none or it is the same as the binary.
//
// only deb and rpm are supported, since those are the ecosystems that publish real source packages with their own
// PURL identity (arch=source / arch=src). apk (origin) and alpm (pkgbase) are intentionally left out: there the
// "source" is a build recipe rather than a published source distributable, and its name is usually also the name of
// a real binary package (e.g. pkg:apk/alpine/libc-dev@0.7.2-r3 is an installable package, not a source archive), so
// a "source-distribution" reference would point at a binary package. Whether those ecosystems have a meaningful
// source PURL is still an open question, and until then the `upstream` PURL qualifier continues to carry that info.
func sourcePackageOf(p pkg.Package) *sourcePackage { func sourcePackageOf(p pkg.Package) *sourcePackage {
var src sourcePackage var src sourcePackage
var binName, binVersion string var binName, binVersion string
switch m := p.Metadata.(type) { switch m := p.Metadata.(type) {
case pkg.ApkDBEntry:
// apk has no source arch convention, so the source PURL carries no arch
src = sourcePackage{purlType: pkg.ApkPkg.PackageURLType(), name: m.OriginPackage, version: m.Version}
binName, binVersion = m.Package, m.Version
case pkg.DpkgDBEntry: case pkg.DpkgDBEntry:
src = dpkgSourcePackage(m) src = dpkgSourcePackage(m)
binName, binVersion = m.Package, m.Version binName, binVersion = m.Package, m.Version
@@ -173,10 +193,6 @@ func sourcePackageOf(p pkg.Package) *sourcePackage {
case pkg.RpmArchive: case pkg.RpmArchive:
src = rpmSourcePackage(m.SourceRpm) src = rpmSourcePackage(m.SourceRpm)
binName, binVersion = m.Name, m.Version+"-"+m.Release binName, binVersion = m.Name, m.Version+"-"+m.Release
case pkg.AlpmDBEntry:
// alpm has no source arch convention, so the source PURL carries no arch
src = sourcePackage{purlType: pkg.AlpmPkg.PackageURLType(), name: m.BasePackage, version: m.Version}
binName, binVersion = m.Package, m.Version
default: default:
return nil return nil
} }
@@ -159,20 +159,20 @@ func Test_encodeSourcePackage(t *testing.T) {
{ {
name: "no PURL", name: "no PURL",
input: pkg.Package{ input: pkg.Package{
Name: "some-package", Name: "libpam-runtime",
Version: "1.0.0-r1", Version: "1.5.2-6+deb12u1",
Type: pkg.ApkPkg, Type: pkg.DebPkg,
Metadata: pkg.ApkDBEntry{ Metadata: pkg.DpkgDBEntry{
Package: "some-package", Package: "libpam-runtime",
OriginPackage: "some-origin", Version: "1.5.2-6+deb12u1",
Version: "1.0.0-r1", Source: "pam",
Architecture: "x86_64",
}, },
}, },
expected: nil, expected: nil,
}, },
{ {
name: "from apk", // apk origins are build recipes, not published source packages, so they are not supported
name: "from apk is not supported",
input: pkg.Package{ input: pkg.Package{
Name: "libc-utils", Name: "libc-utils",
Version: "0.7.2-r3", Version: "0.7.2-r3",
@@ -185,24 +185,6 @@ func Test_encodeSourcePackage(t *testing.T) {
Architecture: "x86_64", Architecture: "x86_64",
}, },
}, },
expected: &cyclonedx.ExternalReference{
Type: cyclonedx.ERTypeSourceDistribution, URL: "pkg:apk/alpine/libc-dev@0.7.2-r3?distro=alpine-3.16.3",
},
},
{
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, expected: nil,
}, },
{ {
@@ -334,7 +316,8 @@ func Test_encodeSourcePackage(t *testing.T) {
}, },
}, },
{ {
name: "from alpm", // alpm pkgbases are build recipes, not published source packages, so they are not supported
name: "from alpm is not supported",
input: pkg.Package{ input: pkg.Package{
Name: "gcc-libs", Name: "gcc-libs",
Version: "13.2.1-3", Version: "13.2.1-3",
@@ -347,24 +330,6 @@ func Test_encodeSourcePackage(t *testing.T) {
Architecture: "x86_64", Architecture: "x86_64",
}, },
}, },
expected: &cyclonedx.ExternalReference{
Type: cyclonedx.ERTypeSourceDistribution, URL: "pkg:alpm/arch/gcc@13.2.1-3?distro=arch-rolling",
},
},
{
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: nil, expected: nil,
}, },
{ {
@@ -43,7 +43,7 @@ func newSourcePackageCatalog() *pkg.Collection {
catalog := pkg.NewCollection() catalog := pkg.NewCollection()
// apk: the origin differs from the package name, and shares its version // apk: the origin differs from the package name, but apk is not supported so no source reference is expected
catalog.Add(pkg.Package{ catalog.Add(pkg.Package{
Name: "libc-utils", Name: "libc-utils",
Version: "0.7.2-r3", Version: "0.7.2-r3",
@@ -59,7 +59,7 @@ func newSourcePackageCatalog() *pkg.Collection {
}, },
}) })
// apk: negative control -- the package is its own origin, so no source reference is expected // apk: negative control -- the package is its own origin
catalog.Add(pkg.Package{ catalog.Add(pkg.Package{
Name: "busybox", Name: "busybox",
Version: "1.35.0-r17", Version: "1.35.0-r17",
@@ -126,7 +126,7 @@ func newSourcePackageCatalog() *pkg.Collection {
}, },
}) })
// alpm: the base package differs from the package name, and shares its version // alpm: the base package differs from the package name, but alpm is not supported so no source reference is expected
catalog.Add(pkg.Package{ catalog.Add(pkg.Package{
Name: "gcc-libs", Name: "gcc-libs",
Version: "13.2.1-3", Version: "13.2.1-3",