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",