From f46acb6e4c91b98efa4ea708208a3be9fdf63eca Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Thu, 29 Oct 2026 18:54:49 -0400 Subject: [PATCH] fix(cyclonedx): tighten source-distribution external refs (#5493) * build source PURL from metadata and gate it to CycloneDX 1.6+ - the source PURL is built from package metadata instead of editing the binary PURL: only `distro` (plus `epoch` and `rpmmod` for rpm) is carried over, subpath is dropped, and the ref is skipped when the PURL type does not match the metadata ecosystem - one emission rule for all ecosystems: only when the source name or version differs from the binary, so self-sourced rpm and alpm packages no longer get a redundant ref - source RPM names must end in `.src.rpm` or `.nosrc.rpm`, and `nosrc` maps to `arch=nosrc` - `source-distribution` refs are dropped when encoding below spec 1.6, since cyclonedx-go does not downgrade them (CycloneDX/cyclonedx-go#291) Signed-off-by: Alex Goodman * 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 --------- Signed-off-by: Alex Goodman --- syft/format/cyclonedxjson/encoder_test.go | 25 +++ .../TestCycloneDxSourcePackageEncoder.golden | 12 -- .../TestCycloneDxSourcePackageEncoder.golden | 10 - syft/format/internal/cyclonedxutil/encoder.go | 27 +++ .../cyclonedxutil/helpers/component.go | 10 +- .../helpers/external_references.go | 163 ++++++++++----- .../helpers/external_references_test.go | 194 +++++++++++++----- .../internal/testutil/source_package_input.go | 6 +- 8 files changed, 316 insertions(+), 131 deletions(-) diff --git a/syft/format/cyclonedxjson/encoder_test.go b/syft/format/cyclonedxjson/encoder_test.go index 8b57de976..c3d422cd2 100644 --- a/syft/format/cyclonedxjson/encoder_test.go +++ b/syft/format/cyclonedxjson/encoder_test.go @@ -128,6 +128,31 @@ func TestCycloneDxSourcePackageEncoder(t *testing.T) { ) } +func TestCycloneDxSourcePackageEncoder_SpecVersions(t *testing.T) { + s := testutil.SourcePackagesInput(t, t.TempDir()) + tests := []struct { + version string + want bool + }{ + {version: "1.4", want: false}, + {version: "1.5", want: false}, + {version: "1.6", want: true}, + {version: cyclonedxutil.DefaultVersion, want: true}, + } + for _, tt := range tests { + t.Run(tt.version, func(t *testing.T) { + enc, err := NewFormatEncoderWithConfig(EncoderConfig{Version: tt.version}) + require.NoError(t, err) + + var buf bytes.Buffer + require.NoError(t, enc.Encode(&buf, s)) + + // the source-distribution reference type only exists from spec 1.6 + assert.Equal(t, tt.want, strings.Contains(buf.String(), `"source-distribution"`)) + }) + } +} + 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 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/encoder.go b/syft/format/internal/cyclonedxutil/encoder.go index dc843734c..82305fdb6 100644 --- a/syft/format/internal/cyclonedxutil/encoder.go +++ b/syft/format/internal/cyclonedxutil/encoder.go @@ -2,6 +2,7 @@ package cyclonedxutil import ( "io" + "slices" "github.com/CycloneDX/cyclonedx-go" @@ -29,9 +30,35 @@ func NewEncoder(version string, format cyclonedx.BOMFileFormat, pretty bool) (En func (e Encoder) Encode(writer io.Writer, s sbom.SBOM) error { bom := cyclonedxhelpers.ToFormatModel(s) + if e.version < cyclonedx.SpecVersion1_6 { + // cyclonedx-go does not downgrade this 1.6+ reference type for older spec versions + // (see https://github.com/CycloneDX/cyclonedx-go/issues/291), so drop it rather than emit an invalid document + dropExternalReferenceType(bom.Components, cyclonedx.ERTypeSourceDistribution) + } enc := cyclonedx.NewBOMEncoder(writer, e.format) enc.SetPretty(e.pretty) enc.SetEscapeHTML(false) return enc.EncodeVersion(bom, e.version) } + +func dropExternalReferenceType(components *[]cyclonedx.Component, refType cyclonedx.ExternalReferenceType) { + if components == nil { + return + } + for i := range *components { + c := &(*components)[i] + dropExternalReferenceType(c.Components, refType) + if c.ExternalReferences == nil { + continue + } + refs := slices.DeleteFunc(*c.ExternalReferences, func(r cyclonedx.ExternalReference) bool { + return r.Type == refType + }) + if len(refs) == 0 { + c.ExternalReferences = nil + continue + } + c.ExternalReferences = &refs + } +} diff --git a/syft/format/internal/cyclonedxutil/helpers/component.go b/syft/format/internal/cyclonedxutil/helpers/component.go index dc788f5ba..2e289fc84 100644 --- a/syft/format/internal/cyclonedxutil/helpers/component.go +++ b/syft/format/internal/cyclonedxutil/helpers/component.go @@ -47,14 +47,6 @@ 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, @@ -67,7 +59,7 @@ func EncodeComponent(p pkg.Package, supplier string, locationSorter func(a, b fi Author: encodeAuthor(p), Publisher: encodePublisher(p), Description: encodeDescription(p), - ExternalReferences: externalRefs, + ExternalReferences: encodeExternalReferences(p), 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 2d2f45a56..0e71eed51 100644 --- a/syft/format/internal/cyclonedxutil/helpers/external_references.go +++ b/syft/format/internal/cyclonedxutil/helpers/external_references.go @@ -80,42 +80,68 @@ func encodeExternalReferences(p pkg.Package) *[]cyclonedx.ExternalReference { } } } - + if srcRef := encodeSourcePackageExternalReference(p); srcRef != nil { + refs = append(refs, *srcRef) + } if len(refs) > 0 { return &refs } return nil } +// 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 { - name, version, arch := sourceCoordinates(p) - if name == "" || version == "" { - return nil - } if p.PURL == "" { return nil } - - pkgPurl, err := packageurl.FromString(p.PURL) - if err != nil { + src := sourcePackageOf(p) + if src == nil { return nil } - qualifiers := pkgPurl.Qualifiers.Map() - delete(qualifiers, pkg.PURLQualifierUpstream) - delete(qualifiers, pkg.PURLQualifierArch) + // the binary PURL is only consulted for the namespace and a small set of qualifiers (e.g. distro), everything + // else describes the binary package and must not be restated as a fact about the source package. + binPurl, err := packageurl.FromString(p.PURL) + if err != nil || binPurl.Type != src.purlType { + return nil + } - if arch != "" { - qualifiers[pkg.PURLQualifierArch] = arch + qualifiers := map[string]string{} + for _, q := range binPurl.Qualifiers { + if src.keepQualifier(q.Key) { + qualifiers[q.Key] = q.Value + } + } + if src.arch != "" { + qualifiers[pkg.PURLQualifierArch] = src.arch } srcPurl := packageurl.NewPackageURL( - pkgPurl.Type, - pkgPurl.Namespace, - name, - version, + src.purlType, + binPurl.Namespace, + src.name, + src.version, packageurl.QualifiersFromMap(qualifiers), - pkgPurl.Subpath, + "", ) return &cyclonedx.ExternalReference{ @@ -124,51 +150,90 @@ func encodeSourcePackageExternalReference(p pkg.Package) *cyclonedx.ExternalRefe } } -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, "" - } +// sourcePackage is the identity of the source package a binary OS package was built from. +type sourcePackage struct { + purlType string + name string + version string + // arch is the ecosystem's source architecture marker ("source" for deb, "src" or "nosrc" for rpm) + arch string +} + +func (s sourcePackage) keepQualifier(key string) bool { + switch key { + case pkg.PURLQualifierDistro: + return true + case pkg.PURLQualifierEpoch, pkg.PURLQualifierRpmModularity: + return s.purlType == pkg.RpmPkg.PackageURLType() + } + return false +} + +// 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.DpkgDBEntry: - return dpkgSourceCoordinates(metadata) + src = dpkgSourcePackage(m) + binName, binVersion = m.Package, m.Version case pkg.DpkgArchiveEntry: - return dpkgSourceCoordinates(pkg.DpkgDBEntry(metadata)) + src = dpkgSourcePackage(pkg.DpkgDBEntry(m)) + binName, binVersion = m.Package, m.Version case pkg.RpmDBEntry: - return rpmSourceCoordinates(metadata.SourceRpm) + src = rpmSourcePackage(m.SourceRpm) + binName, binVersion = m.Name, m.Version+"-"+m.Release case pkg.RpmArchive: - return rpmSourceCoordinates(metadata.SourceRpm) - case pkg.AlpmDBEntry: - if metadata.BasePackage != "" { - return metadata.BasePackage, metadata.Version, "" - } + src = rpmSourcePackage(m.SourceRpm) + binName, binVersion = m.Name, m.Version+"-"+m.Release + default: + return nil } - return "", "", "" + if src.name == "" || src.version == "" { + return nil + } + if src.name == binName && src.version == binVersion { + return nil + } + return &src } -func dpkgSourceCoordinates(entry pkg.DpkgDBEntry) (name, version, arch string) { - if entry.Source == "" { - return "", "", "" +func dpkgSourcePackage(entry pkg.DpkgDBEntry) sourcePackage { + version := entry.SourceVersion + if version == "" { + version = entry.Version } - if entry.SourceVersion != "" { - return entry.Source, entry.SourceVersion, "source" - } - return entry.Source, entry.Version, "source" + return sourcePackage{purlType: pkg.DebPkg.PackageURLType(), name: entry.Source, version: version, arch: "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, "-") +// rpmSourcePackage parses a source RPM filename of the form --.(no)src.rpm. +func rpmSourcePackage(sourceRpm string) sourcePackage { + var arch string + switch { + case strings.HasSuffix(sourceRpm, ".src.rpm"): + arch = "src" + case strings.HasSuffix(sourceRpm, ".nosrc.rpm"): + arch = "nosrc" + default: + return sourcePackage{} + } + nvr := strings.TrimSuffix(sourceRpm, "."+arch+".rpm") + release := strings.LastIndex(nvr, "-") if release < 0 { - return "", "", "" + return sourcePackage{} } - ver := strings.LastIndex(sourceRpm[:release], "-") + ver := strings.LastIndex(nvr[:release], "-") if ver < 0 { - return "", "", "" + return sourcePackage{} } - return sourceRpm[:ver], sourceRpm[ver+1:], "src" + return sourcePackage{purlType: pkg.RpmPkg.PackageURLType(), name: nvr[:ver], version: nvr[ver+1:], arch: arch} } // supported algorithm in cycloneDX as of 1.4 diff --git a/syft/format/internal/cyclonedxutil/helpers/external_references_test.go b/syft/format/internal/cyclonedxutil/helpers/external_references_test.go index b91d590f0..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-package", - 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,25 +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", - }, - }, - { - // 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, }, { @@ -312,9 +293,7 @@ func Test_encodeSourcePackage(t *testing.T) { 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", - }, + expected: nil, }, { name: "from rpm archive", @@ -337,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", @@ -350,30 +330,148 @@ 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", - }, + expected: nil, }, { - // 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", + name: "from rpm with dashes in the source name", 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", + Name: "perl-Foo-Bar-libs", + Type: pkg.RpmPkg, + PURL: "pkg:rpm/redhat/perl-Foo-Bar-libs@1.0-1.el8?arch=x86_64&distro=rhel-8", + Metadata: pkg.RpmDBEntry{ + Name: "perl-Foo-Bar-libs", + Version: "1.0", + Release: "1.el8", + SourceRpm: "perl-Foo-Bar-1.0-1.el8.src.rpm", }, }, expected: &cyclonedx.ExternalReference{ - Type: cyclonedx.ERTypeSourceDistribution, URL: "pkg:alpm/arch/some-package@1.0.0?distro=arch-rolling", + Type: cyclonedx.ERTypeSourceDistribution, URL: "pkg:rpm/redhat/perl-Foo-Bar@1.0-1.el8?arch=src&distro=rhel-8", }, }, + { + name: "from rpm nosrc", + input: pkg.Package{ + Name: "some-firmware", + Type: pkg.RpmPkg, + PURL: "pkg:rpm/redhat/some-firmware@1.0-1.el8?arch=noarch&distro=rhel-8", + Metadata: pkg.RpmDBEntry{ + Name: "some-firmware", + Version: "1.0", + Release: "1.el8", + SourceRpm: "firmware-1.0-1.el8.nosrc.rpm", + }, + }, + expected: &cyclonedx.ExternalReference{ + Type: cyclonedx.ERTypeSourceDistribution, URL: "pkg:rpm/redhat/firmware@1.0-1.el8?arch=nosrc&distro=rhel-8", + }, + }, + { + name: "from rpm keeps rpmmod", + input: pkg.Package{ + Name: "nodejs-libs", + Type: pkg.RpmPkg, + PURL: "pkg:rpm/redhat/nodejs-libs@18.0-1.el8?arch=x86_64&distro=rhel-8&rpmmod=nodejs%3A18", + Metadata: pkg.RpmDBEntry{ + Name: "nodejs-libs", + Version: "18.0", + Release: "1.el8", + SourceRpm: "nodejs-18.0-1.el8.src.rpm", + }, + }, + expected: &cyclonedx.ExternalReference{ + Type: cyclonedx.ERTypeSourceDistribution, URL: "pkg:rpm/redhat/nodejs@18.0-1.el8?arch=src&distro=rhel-8&rpmmod=nodejs%3A18", + }, + }, + { + name: "from rpm with malformed source rpm", + input: pkg.Package{ + Name: "gpg-pubkey", + Type: pkg.RpmPkg, + PURL: "pkg:rpm/redhat/gpg-pubkey@1-1?distro=rhel-8", + Metadata: pkg.RpmDBEntry{ + Name: "gpg-pubkey", + Version: "1", + Release: "1", + SourceRpm: "(none)", + }, + }, + expected: nil, + }, + { + name: "from rpm with non-source rpm filename", + input: pkg.Package{ + Name: "bash", + Type: pkg.RpmPkg, + PURL: "pkg:rpm/redhat/bash@5.1-2?arch=x86_64&distro=rhel-8", + Metadata: pkg.RpmDBEntry{ + Name: "bash", + Version: "5.1", + Release: "2", + SourceRpm: "bash-5.1-2.x86_64.rpm", + }, + }, + expected: nil, + }, + { + name: "from rpm with too few dashes", + input: pkg.Package{ + Name: "bash", + Type: pkg.RpmPkg, + PURL: "pkg:rpm/redhat/bash@5.1-2?arch=x86_64&distro=rhel-8", + Metadata: pkg.RpmDBEntry{ + Name: "bash", + Version: "5.1", + Release: "2", + SourceRpm: "bash-5.1.src.rpm", + }, + }, + expected: nil, + }, + { + name: "binary qualifiers and subpath are not carried over", + input: pkg.Package{ + Name: "libssl3", + Type: pkg.DebPkg, + PURL: "pkg:deb/debian/libssl3@3.0.0-1?arch=amd64&distro=debian-12&checksum=sha256:dead&download_url=https://example.com/x.deb&cpes=cpe:2.3:a:x:x:1:*:*:*:*:*:*:*#sub/dir", + Metadata: pkg.DpkgDBEntry{ + Package: "libssl3", + Version: "3.0.0-1", + Source: "openssl", + }, + }, + expected: &cyclonedx.ExternalReference{ + Type: cyclonedx.ERTypeSourceDistribution, URL: "pkg:deb/debian/openssl@3.0.0-1?arch=source&distro=debian-12", + }, + }, + { + name: "PURL type does not match metadata", + input: pkg.Package{ + Name: "x", + Type: pkg.DebPkg, + PURL: "pkg:npm/x@1", + Metadata: pkg.DpkgDBEntry{ + Package: "x", + Version: "1", + Source: "lodash", + }, + }, + expected: nil, + }, + { + name: "invalid PURL", + input: pkg.Package{ + Name: "x", + Type: pkg.DebPkg, + PURL: "not-a-purl", + Metadata: pkg.DpkgDBEntry{ + Package: "x", + Version: "1", + Source: "y", + }, + }, + expected: nil, + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { 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",