diff --git a/syft/format/internal/cyclonedxutil/helpers/external_references.go b/syft/format/internal/cyclonedxutil/helpers/external_references.go index 53bc1f0d1..54053d21f 100644 --- a/syft/format/internal/cyclonedxutil/helpers/external_references.go +++ b/syft/format/internal/cyclonedxutil/helpers/external_references.go @@ -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,7 +87,83 @@ func encodeExternalReferences(p pkg.Package) *[]cyclonedx.ExternalReference { } func encodeSourcePackageExternalReference(p pkg.Package) *cyclonedx.ExternalReference { - return nil + 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) { + // --.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 diff --git a/syft/format/internal/cyclonedxutil/helpers/external_references_test.go b/syft/format/internal/cyclonedxutil/helpers/external_references_test.go index baa014aea..ed6bfaf6b 100644 --- a/syft/format/internal/cyclonedxutil/helpers/external_references_test.go +++ b/syft/format/internal/cyclonedxutil/helpers/external_references_test.go @@ -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) {