From ab69742d252a6e981a6ca05c4fe1e5d4cd556fc5 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Wed, 30 Sep 2026 09:43:48 -0400 Subject: [PATCH] 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 --- .../TestCycloneDxSourcePackageEncoder.golden | 12 ---- .../TestCycloneDxSourcePackageEncoder.golden | 10 ---- .../helpers/external_references.go | 40 +++++++++---- .../helpers/external_references_test.go | 57 ++++--------------- .../internal/testutil/source_package_input.go | 6 +- 5 files changed, 42 insertions(+), 83 deletions(-) diff --git a/syft/format/cyclonedxjson/testdata/snapshot/TestCycloneDxSourcePackageEncoder.golden b/syft/format/cyclonedxjson/testdata/snapshot/TestCycloneDxSourcePackageEncoder.golden index 6995d4eae..ffcef5ad6 100644 --- a/syft/format/cyclonedxjson/testdata/snapshot/TestCycloneDxSourcePackageEncoder.golden +++ b/syft/format/cyclonedxjson/testdata/snapshot/TestCycloneDxSourcePackageEncoder.golden @@ -156,12 +156,6 @@ "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", @@ -207,12 +201,6 @@ "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", diff --git a/syft/format/cyclonedxxml/testdata/snapshot/TestCycloneDxSourcePackageEncoder.golden b/syft/format/cyclonedxxml/testdata/snapshot/TestCycloneDxSourcePackageEncoder.golden index 830062cdb..3fc05e87f 100644 --- a/syft/format/cyclonedxxml/testdata/snapshot/TestCycloneDxSourcePackageEncoder.golden +++ b/syft/format/cyclonedxxml/testdata/snapshot/TestCycloneDxSourcePackageEncoder.golden @@ -73,11 +73,6 @@ 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 @@ -94,11 +89,6 @@ 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 diff --git a/syft/format/internal/cyclonedxutil/helpers/external_references.go b/syft/format/internal/cyclonedxutil/helpers/external_references.go index 5f02ea6db..0e71eed51 100644 --- a/syft/format/internal/cyclonedxutil/helpers/external_references.go +++ b/syft/format/internal/cyclonedxutil/helpers/external_references.go @@ -89,9 +89,26 @@ func encodeExternalReferences(p pkg.Package) *[]cyclonedx.ExternalReference { return nil } -// encodeSourcePackageExternalReference returns a "source-distribution" reference holding the PURL of the source -// package that an OS package was built from. 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. +// encodeSourcePackageExternalReference returns a "source-distribution" reference holding the PURL of the distro +// source package that a binary OS package was built from (e.g. libpam-runtime -> pkg:deb/debian/pam@...?arch=source). +// 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 { if p.PURL == "" { return nil @@ -138,7 +155,7 @@ type sourcePackage struct { purlType string name 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 } @@ -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. +// +// 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 { var src sourcePackage var binName, binVersion string 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: src = dpkgSourcePackage(m) binName, binVersion = m.Package, m.Version @@ -173,10 +193,6 @@ func sourcePackageOf(p pkg.Package) *sourcePackage { case pkg.RpmArchive: src = rpmSourcePackage(m.SourceRpm) 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: return nil } diff --git a/syft/format/internal/cyclonedxutil/helpers/external_references_test.go b/syft/format/internal/cyclonedxutil/helpers/external_references_test.go index a73a19117..c222e0d6e 100644 --- a/syft/format/internal/cyclonedxutil/helpers/external_references_test.go +++ b/syft/format/internal/cyclonedxutil/helpers/external_references_test.go @@ -159,20 +159,20 @@ func Test_encodeSourcePackage(t *testing.T) { { 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-origin", - Version: "1.0.0-r1", - Architecture: "x86_64", + Name: "libpam-runtime", + Version: "1.5.2-6+deb12u1", + Type: pkg.DebPkg, + Metadata: pkg.DpkgDBEntry{ + Package: "libpam-runtime", + Version: "1.5.2-6+deb12u1", + Source: "pam", }, }, 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{ Name: "libc-utils", Version: "0.7.2-r3", @@ -185,24 +185,6 @@ func Test_encodeSourcePackage(t *testing.T) { 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, }, { @@ -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{ Name: "gcc-libs", Version: "13.2.1-3", @@ -347,24 +330,6 @@ func Test_encodeSourcePackage(t *testing.T) { 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, }, { diff --git a/syft/format/internal/testutil/source_package_input.go b/syft/format/internal/testutil/source_package_input.go index afc1bdc7e..b5b33dfa6 100644 --- a/syft/format/internal/testutil/source_package_input.go +++ b/syft/format/internal/testutil/source_package_input.go @@ -43,7 +43,7 @@ func newSourcePackageCatalog() *pkg.Collection { 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{ Name: "libc-utils", 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{ Name: "busybox", 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{ Name: "gcc-libs", Version: "13.2.1-3",