Files
syft/syft/format/internal/cyclonedxutil/helpers/component_test.go
T
Will Murphy fe42bcec38 fix(purl-backfill): respect arch qualifier (#4987)
* fix(purl-backfill): respect arch qualifier

Previously, when constructing rpm, alpm, and apk metadata struct from a
PURL, Syft would ignore the arch qualifier. Start respecting that
qualifier.

Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>

* chore: fix static analysis

Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>

* Clean up control flow in PURL backfill code

Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>

---------

Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>
2026-06-23 15:23:46 -04:00

554 lines
15 KiB
Go

package helpers
import (
"fmt"
"reflect"
"testing"
"github.com/CycloneDX/cyclonedx-go"
"github.com/stretchr/testify/assert"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
)
func Test_encodeComponentProperties(t *testing.T) {
epoch := 2
tests := []struct {
name string
input pkg.Package
expected []cyclonedx.Property
}{
{
name: "no metadata",
input: pkg.Package{},
expected: nil,
},
{
name: "from apk",
input: pkg.Package{
FoundBy: "cataloger",
Locations: file.NewLocationSet(
file.NewLocationFromCoordinates(file.Coordinates{RealPath: "test"}),
),
Metadata: pkg.ApkDBEntry{
Package: "libc-utils",
OriginPackage: "libc-dev",
Maintainer: "Natanael Copa <ncopa@alpinelinux.org>",
Version: "0.7.2-r0",
Architecture: "x86_64",
URL: "http://alpinelinux.org",
Description: "Meta package to pull in correct libc",
Size: 0,
InstalledSize: 4096,
Dependencies: []string{"musl-utils"},
Provides: []string{"so:libc.so.1"},
Checksum: "Q1p78yvTLG094tHE1+dToJGbmYzQE=",
GitCommit: "97b1c2842faa3bfa30f5811ffbf16d5ff9f1a479",
Files: []pkg.ApkFileRecord{},
},
},
expected: []cyclonedx.Property{
{Name: "syft:package:foundBy", Value: "cataloger"},
{Name: "syft:package:metadataType", Value: "apk-db-entry"},
{Name: "syft:location:0:path", Value: "test"},
{Name: "syft:metadata:gitCommitOfApkPort", Value: "97b1c2842faa3bfa30f5811ffbf16d5ff9f1a479"},
{Name: "syft:metadata:installedSize", Value: "4096"},
{Name: "syft:metadata:originPackage", Value: "libc-dev"},
{Name: "syft:metadata:provides:0", Value: "so:libc.so.1"},
{Name: "syft:metadata:pullChecksum", Value: "Q1p78yvTLG094tHE1+dToJGbmYzQE="},
{Name: "syft:metadata:pullDependencies:0", Value: "musl-utils"},
{Name: "syft:metadata:size", Value: "0"},
},
},
{
name: "from dpkg",
input: pkg.Package{
Metadata: pkg.DpkgDBEntry{
Package: "tzdata",
Version: "2020a-0+deb10u1",
Source: "tzdata-dev",
SourceVersion: "1.0",
Architecture: "all",
InstalledSize: 3036,
Maintainer: "GNU Libc Maintainers <debian-glibc@lists.debian.org>",
Files: []pkg.DpkgFileRecord{},
},
},
expected: []cyclonedx.Property{
{Name: "syft:package:metadataType", Value: "dpkg-db-entry"},
{Name: "syft:metadata:installedSize", Value: "3036"},
{Name: "syft:metadata:source", Value: "tzdata-dev"},
{Name: "syft:metadata:sourceVersion", Value: "1.0"},
},
},
{
name: "from go bin",
input: pkg.Package{
Name: "golang.org/x/net",
Version: "v0.0.0-20211006190231-62292e806868",
Language: pkg.Go,
Type: pkg.GoModulePkg,
Metadata: pkg.GolangBinaryBuildinfoEntry{
GoCompiledVersion: "1.17",
Architecture: "amd64",
H1Digest: "h1:KlOXYy8wQWTUJYFgkUI40Lzr06ofg5IRXUK5C7qZt1k=",
},
},
expected: []cyclonedx.Property{
{Name: "syft:package:language", Value: pkg.Go.String()},
{Name: "syft:package:metadataType", Value: "go-module-buildinfo-entry"},
{Name: "syft:package:type", Value: "go-module"},
{Name: "syft:metadata:architecture", Value: "amd64"},
{Name: "syft:metadata:goCompiledVersion", Value: "1.17"},
{Name: "syft:metadata:h1Digest", Value: "h1:KlOXYy8wQWTUJYFgkUI40Lzr06ofg5IRXUK5C7qZt1k="},
},
},
{
name: "from go mod",
input: pkg.Package{
Name: "golang.org/x/net",
Version: "v0.0.0-20211006190231-62292e806868",
Language: pkg.Go,
Type: pkg.GoModulePkg,
Metadata: pkg.GolangModuleEntry{
H1Digest: "h1:KlOXYy8wQWTUJYFgkUI40Lzr06ofg5IRXUK5C7qZt1k=",
},
},
expected: []cyclonedx.Property{
{Name: "syft:package:language", Value: pkg.Go.String()},
{Name: "syft:package:metadataType", Value: "go-module-entry"},
{Name: "syft:package:type", Value: "go-module"},
{Name: "syft:metadata:h1Digest", Value: "h1:KlOXYy8wQWTUJYFgkUI40Lzr06ofg5IRXUK5C7qZt1k="},
},
},
{
name: "from rpm",
input: pkg.Package{
Name: "dive",
Version: "0.9.2-1",
Type: pkg.RpmPkg,
Metadata: pkg.RpmDBEntry{
Name: "dive",
Epoch: &epoch,
Arch: "x86_64",
Release: "1",
Version: "0.9.2",
SourceRpm: "dive-0.9.2-1.src.rpm",
Size: 12406784,
Vendor: "",
Files: []pkg.RpmFileRecord{},
},
},
expected: []cyclonedx.Property{
{Name: "syft:package:metadataType", Value: "rpm-db-entry"},
{Name: "syft:package:type", Value: "rpm"},
{Name: "syft:metadata:epoch", Value: "2"},
{Name: "syft:metadata:release", Value: "1"},
{Name: "syft:metadata:size", Value: "12406784"},
{Name: "syft:metadata:sourceRpm", Value: "dive-0.9.2-1.src.rpm"},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
sbomSupplier := ""
c := EncodeComponent(test.input, sbomSupplier, file.LocationSorter(nil))
if test.expected == nil {
if c.Properties != nil {
t.Fatalf("expected no properties, got: %+v", *c.Properties)
}
return
}
assert.ElementsMatch(t, test.expected, *c.Properties)
})
}
}
func Test_encodeCompomentType(t *testing.T) {
tests := []struct {
name string
pkg pkg.Package
want cyclonedx.Component
}{
{
name: "non-binary package",
pkg: pkg.Package{
Name: "pkg1",
Version: "1.9.2",
Type: pkg.GoModulePkg,
},
want: cyclonedx.Component{
Name: "pkg1",
Version: "1.9.2",
Type: cyclonedx.ComponentTypeLibrary,
Properties: &[]cyclonedx.Property{
{
Name: "syft:package:type",
Value: "go-module",
},
},
},
},
{
name: "non-binary package",
pkg: pkg.Package{
Name: "pkg1",
Version: "3.1.2",
Type: pkg.BinaryPkg,
},
want: cyclonedx.Component{
Name: "pkg1",
Version: "3.1.2",
Type: cyclonedx.ComponentTypeApplication,
Properties: &[]cyclonedx.Property{
{
Name: "syft:package:type",
Value: "binary",
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.pkg.ID()
sbomSupplier := ""
p := EncodeComponent(tt.pkg, sbomSupplier, file.LocationSorter(nil))
assert.Equal(t, tt.want, p)
})
}
}
func Test_deriveBomRef(t *testing.T) {
pkgWithPurl := pkg.Package{
Name: "django",
Version: "1.11.1",
PURL: "pkg:pypi/django@1.11.1",
}
pkgWithPurl.SetID()
pkgWithOutPurl := pkg.Package{
Name: "django",
Version: "1.11.1",
PURL: "",
}
pkgWithOutPurl.SetID()
pkgWithBadPurl := pkg.Package{
Name: "django",
Version: "1.11.1",
PURL: "pkg:pyjango@1.11.1",
}
pkgWithBadPurl.SetID()
tests := []struct {
name string
pkg pkg.Package
want string
}{
{
name: "use pURL-id hybrid",
pkg: pkgWithPurl,
want: fmt.Sprintf("pkg:pypi/django@1.11.1?package-id=%s", pkgWithPurl.ID()),
},
{
name: "fallback to ID when pURL is invalid",
pkg: pkgWithBadPurl,
want: string(pkgWithBadPurl.ID()),
},
{
name: "fallback to ID when pURL is missing",
pkg: pkgWithOutPurl,
want: string(pkgWithOutPurl.ID()),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.pkg.ID()
assert.Equal(t, tt.want, DeriveBomRef(tt.pkg))
})
}
}
func Test_decodeComponent(t *testing.T) {
tests := []struct {
name string
component cyclonedx.Component
wantLanguage pkg.Language
wantMetadata any
wantPURL string
}{
{
name: "derive language from pURL if missing",
component: cyclonedx.Component{
Name: "ch.qos.logback/logback-classic",
Version: "1.2.3",
PackageURL: "pkg:maven/ch.qos.logback/logback-classic@1.2.3",
Type: "library",
BOMRef: "pkg:maven/ch.qos.logback/logback-classic@1.2.3",
},
wantLanguage: pkg.Java,
wantPURL: "pkg:maven/ch.qos.logback/logback-classic@1.2.3",
},
{
name: "derive language from bomref if missing",
component: cyclonedx.Component{
Name: "ch.qos.logback/logback-classic",
Version: "1.2.3",
Type: "library",
BOMRef: "pkg:maven/ch.qos.logback/logback-classic@1.2.3",
},
wantLanguage: pkg.Java,
wantPURL: "pkg:maven/ch.qos.logback/logback-classic@1.2.3",
},
{
name: "handle RpmdbMetadata type without properties",
component: cyclonedx.Component{
Name: "acl",
Version: "2.2.53-1.el8",
PackageURL: "pkg:rpm/centos/acl@2.2.53-1.el8?arch=x86_64&upstream=acl-2.2.53-1.el8.src.rpm&distro=centos-8",
Type: "library",
BOMRef: "pkg:rpm/centos/acl@2.2.53-1.el8?arch=x86_64&upstream=acl-2.2.53-1.el8.src.rpm&distro=centos-8",
Properties: &[]cyclonedx.Property{
{
Name: "syft:package:metadataType",
Value: "RpmdbMetadata",
},
},
},
wantMetadata: pkg.RpmDBEntry{Arch: "x86_64"},
wantPURL: "pkg:rpm/centos/acl@2.2.53-1.el8?arch=x86_64&upstream=acl-2.2.53-1.el8.src.rpm&distro=centos-8",
},
{
name: "handle RpmdbMetadata type with properties",
component: cyclonedx.Component{
Name: "acl",
Version: "2.2.53-1.el8",
PackageURL: "pkg:rpm/centos/acl@2.2.53-1.el8?arch=x86_64&upstream=acl-2.2.53-1.el8.src.rpm&distro=centos-8",
Type: "library",
BOMRef: "pkg:rpm/centos/acl@2.2.53-1.el8?arch=x86_64&upstream=acl-2.2.53-1.el8.src.rpm&distro=centos-8",
Properties: &[]cyclonedx.Property{
{
Name: "syft:package:metadataType",
Value: "RpmDBMetadata",
},
{
Name: "syft:metadata:release",
Value: "some-release",
},
},
},
wantMetadata: pkg.RpmDBEntry{
Release: "some-release",
Arch: "x86_64",
},
wantPURL: "pkg:rpm/centos/acl@2.2.53-1.el8?arch=x86_64&upstream=acl-2.2.53-1.el8.src.rpm&distro=centos-8",
},
{
name: "generate a purl from package type",
component: cyclonedx.Component{
Name: "log4j",
Group: "org.apache.logging.log4j",
Version: "2.0.4",
Type: "library",
BOMRef: "log4j",
Properties: &[]cyclonedx.Property{
{
Name: "syft:package:type",
Value: "java-archive",
},
},
},
wantPURL: "pkg:maven/org.apache.logging.log4j/log4j@2.0.4",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := decodeComponent(&tt.component)
if tt.wantLanguage != "" {
assert.Equal(t, tt.wantLanguage, p.Language)
}
if tt.wantMetadata != nil {
assert.Truef(t, reflect.DeepEqual(tt.wantMetadata, p.Metadata), "metadata should match: %+v != %+v", tt.wantMetadata, p.Metadata)
}
if tt.wantPURL != "" {
assert.Equal(t, tt.wantPURL, p.PURL, "purl should match")
}
if tt.wantMetadata == nil && tt.wantLanguage == "" && tt.wantPURL == "" {
t.Fatal("this is a useless test, please remove it")
}
})
}
}
func Test_setPackageName(t *testing.T) {
tests := []struct {
name string
pkg pkg.Package
comp cyclonedx.Component
wantName string
}{
{
name: "debian group excluded from name",
pkg: pkg.Package{Type: pkg.DebPkg},
comp: cyclonedx.Component{Name: "wget", Group: "debian"},
wantName: "wget",
},
{
name: "rpm group excluded from name",
pkg: pkg.Package{Type: pkg.RpmPkg},
comp: cyclonedx.Component{Name: "acl", Group: "centos"},
wantName: "acl",
},
{
name: "apk group excluded from name",
pkg: pkg.Package{Type: pkg.ApkPkg},
comp: cyclonedx.Component{Name: "musl", Group: "alpine"},
wantName: "musl",
},
{
name: "npm group included in name",
pkg: pkg.Package{Type: pkg.NpmPkg},
comp: cyclonedx.Component{Name: "node", Group: "@types"},
wantName: "@types/node",
},
{
name: "go module group included in name",
pkg: pkg.Package{Type: pkg.GoModulePkg},
comp: cyclonedx.Component{Name: "net", Group: "golang.org/x"},
wantName: "golang.org/x/net",
},
{
name: "no group leaves name unchanged",
pkg: pkg.Package{Type: pkg.DebPkg},
comp: cyclonedx.Component{Name: "wget"},
wantName: "wget",
},
{
name: "java group stored in metadata not name",
pkg: pkg.Package{Type: pkg.JavaPkg},
comp: cyclonedx.Component{Name: "log4j", Group: "org.apache.logging.log4j"},
wantName: "log4j",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := tt.pkg
setPackageName(&p, &tt.comp)
assert.Equal(t, tt.wantName, p.Name)
})
}
}
func TestGetPURL(t *testing.T) {
tests := []struct {
name string
component *cyclonedx.Component
pkgType pkg.Type
expected string
}{
{
name: "component with PackageURL",
component: &cyclonedx.Component{
PackageURL: "pkg:npm/lodash@4.17.21",
Name: "lodash",
Version: "4.17.20", // different version to verify PackageURL is used
Group: "npm",
},
pkgType: pkg.NpmPkg,
expected: "pkg:npm/lodash@4.17.21",
},
{
name: "component with BOMRef as valid PURL",
component: &cyclonedx.Component{
BOMRef: "pkg:maven/org.apache.commons/commons-lang3@3.12.0",
Name: "commons-lang3",
Version: "3.11.0", // different version to verify BOMRef is used
Group: "org.apache.commons",
},
pkgType: pkg.JavaPkg,
expected: "pkg:maven/org.apache.commons/commons-lang3@3.12.0",
},
{
name: "component with BOMRef not a valid PURL",
component: &cyclonedx.Component{
BOMRef: "not-a-purl-ref",
Name: "commons-lang3",
Version: "3.12.0",
Group: "org.apache.commons",
},
pkgType: pkg.JavaPkg,
expected: "pkg:maven/org.apache.commons/commons-lang3@3.12.0",
},
{
name: "component with unknown package type",
component: &cyclonedx.Component{
Name: "some-component",
Version: "1.0.0",
Group: "org.example",
},
pkgType: pkg.UnknownPkg,
expected: "",
},
{
name: "component with empty package type",
component: &cyclonedx.Component{
Name: "some-component",
Version: "1.0.0",
Group: "org.example",
},
pkgType: "",
expected: "",
},
{
name: "component with generic package type",
component: &cyclonedx.Component{
Name: "some-component",
Version: "1.0.0",
Group: "org.example",
},
pkgType: pkg.LinuxKernelModulePkg,
expected: "",
},
{
name: "component with valid package type",
component: &cyclonedx.Component{
Name: "react",
Version: "18.2.0",
Group: "facebook",
},
pkgType: pkg.NpmPkg,
expected: "pkg:npm/facebook/react@18.2.0",
},
{
name: "component with no group",
component: &cyclonedx.Component{
Name: "express",
Version: "4.18.2",
},
pkgType: pkg.NpmPkg,
expected: "pkg:npm/express@4.18.2",
},
{
name: "component with no version",
component: &cyclonedx.Component{
Name: "express",
Group: "npm",
},
pkgType: pkg.NpmPkg,
expected: "pkg:npm/npm/express",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := getPURL(tt.component, tt.pkgType)
assert.Equal(t, tt.expected, result)
})
}
}