syft/syft/pkg/package_test.go
William Murphy b7a6d5e946
feat: Record where CPEs come from (#2552)
Syft can get CPEs from several source, including generating them based on
package data, finding them in the NVD CPE dictionary, or finding them declared
in a manifest or existing SBOM. Record where Syft got CPEs so that consumers of
SBOMs can reason about how trustworthy they are.

Signed-off-by: Will Murphy <will.murphy@anchore.com>
2024-02-02 16:17:52 +00:00

462 lines
11 KiB
Go

package pkg
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/file"
)
func TestIDUniqueness(t *testing.T) {
originalLocation := file.NewVirtualLocationFromCoordinates(
file.Coordinates{
RealPath: "39.0742° N, 21.8243° E",
FileSystemID: "Earth",
},
"/Ancient-Greece",
)
originalPkg := Package{
Name: "pi",
Version: "3.14",
FoundBy: "Archimedes",
Locations: file.NewLocationSet(
originalLocation,
),
Licenses: NewLicenseSet(
NewLicense("MIT"),
NewLicense("cc0-1.0"),
),
Language: "math",
Type: PythonPkg,
CPEs: []cpe.CPE{
cpe.Must(`cpe:2.3:a:Archimedes:pi:3.14:*:*:*:*:math:*:*`, cpe.NVDDictionaryLookupSource),
},
PURL: "pkg:pypi/pi@3.14",
Metadata: PythonPackage{
Name: "pi",
Version: "3.14",
Author: "Archimedes",
AuthorEmail: "Archimedes@circles.io",
Platform: "universe",
SitePackagesRootPath: "Pi",
},
}
// this is a set of differential tests, ensuring that select mutations are reflected in the fingerprint (or not)
tests := []struct {
name string
transform func(pkg Package) Package
expectedIDComparison assert.ComparisonAssertionFunc
}{
{
name: "go case (no transform)",
transform: func(pkg Package) Package {
// do nothing!
return pkg
},
expectedIDComparison: assert.Equal,
},
{
name: "same metadata is ignored",
transform: func(pkg Package) Package {
// note: this is the same as the original values, just a new allocation
pkg.Metadata = PythonPackage{
Name: "pi",
Version: "3.14",
Author: "Archimedes",
AuthorEmail: "Archimedes@circles.io",
Platform: "universe",
SitePackagesRootPath: "Pi",
}
return pkg
},
expectedIDComparison: assert.Equal,
},
{
name: "licenses order is ignored",
transform: func(pkg Package) Package {
// note: same as the original package, only a different order
pkg.Licenses = NewLicenseSet(
NewLicense("cc0-1.0"),
NewLicense("MIT"),
)
return pkg
},
expectedIDComparison: assert.Equal,
},
{
name: "name is reflected",
transform: func(pkg Package) Package {
pkg.Name = "new!"
return pkg
},
expectedIDComparison: assert.NotEqual,
},
{
name: "location is reflected",
transform: func(pkg Package) Package {
locations := file.NewLocationSet(pkg.Locations.ToSlice()...)
locations.Add(file.NewLocation("/somewhere/new"))
pkg.Locations = locations
return pkg
},
expectedIDComparison: assert.NotEqual,
},
{
name: "licenses is reflected",
transform: func(pkg Package) Package {
pkg.Licenses = NewLicenseSet(NewLicense("new!"))
return pkg
},
expectedIDComparison: assert.NotEqual,
},
{
name: "same path for different filesystem is NOT reflected",
transform: func(pkg Package) Package {
newLocation := originalLocation
newLocation.FileSystemID = "Mars"
pkg.Locations = file.NewLocationSet(newLocation)
return pkg
},
expectedIDComparison: assert.Equal,
},
{
name: "multiple equivalent paths for different filesystem is NOT reflected",
transform: func(pkg Package) Package {
newLocation := originalLocation
newLocation.FileSystemID = "Mars"
locations := file.NewLocationSet(pkg.Locations.ToSlice()...)
locations.Add(newLocation, originalLocation)
pkg.Locations = locations
return pkg
},
expectedIDComparison: assert.Equal,
},
{
name: "version is reflected",
transform: func(pkg Package) Package {
pkg.Version = "new!"
return pkg
},
expectedIDComparison: assert.NotEqual,
},
{
name: "type is reflected",
transform: func(pkg Package) Package {
pkg.Type = RustPkg
return pkg
},
expectedIDComparison: assert.NotEqual,
},
{
name: "CPEs is ignored",
transform: func(pkg Package) Package {
pkg.CPEs = []cpe.CPE{}
return pkg
},
expectedIDComparison: assert.Equal,
},
{
name: "pURL is ignored",
transform: func(pkg Package) Package {
pkg.PURL = "new!"
return pkg
},
expectedIDComparison: assert.Equal,
},
{
name: "language is NOT reflected",
transform: func(pkg Package) Package {
pkg.Language = Rust
return pkg
},
expectedIDComparison: assert.Equal,
},
{
name: "metadata mutation is reflected",
transform: func(pkg Package) Package {
metadata := pkg.Metadata.(PythonPackage)
metadata.Name = "new!"
pkg.Metadata = metadata
return pkg
},
expectedIDComparison: assert.NotEqual,
},
{
name: "new metadata is reflected",
transform: func(pkg Package) Package {
pkg.Metadata = PythonPackage{
Name: "new!",
}
return pkg
},
expectedIDComparison: assert.NotEqual,
},
{
name: "nil metadata is reflected",
transform: func(pkg Package) Package {
pkg.Metadata = nil
return pkg
},
expectedIDComparison: assert.NotEqual,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
originalPkg.SetID()
transformedPkg := test.transform(originalPkg)
transformedPkg.SetID()
originalFingerprint := originalPkg.ID()
assert.NotEmpty(t, originalFingerprint)
transformedFingerprint := transformedPkg.ID()
assert.NotEmpty(t, transformedFingerprint)
test.expectedIDComparison(t, originalFingerprint, transformedFingerprint)
})
}
}
func TestPackage_Merge(t *testing.T) {
originalLocation := file.NewVirtualLocationFromCoordinates(
file.Coordinates{
RealPath: "39.0742° N, 21.8243° E",
FileSystemID: "Earth",
},
"/Ancient-Greece",
)
similarLocation := originalLocation
similarLocation.FileSystemID = "Mars"
tests := []struct {
name string
subject Package
other Package
expected *Package
}{
{
name: "merge two packages (different cpes + locations)",
subject: Package{
Name: "pi",
Version: "3.14",
FoundBy: "Archimedes",
Locations: file.NewLocationSet(
originalLocation,
),
Language: "math",
Type: PythonPkg,
CPEs: []cpe.CPE{
cpe.Must(`cpe:2.3:a:Archimedes:pi:3.14:*:*:*:*:math:*:*`, cpe.NVDDictionaryLookupSource),
},
PURL: "pkg:pypi/pi@3.14",
Metadata: PythonPackage{
Name: "pi",
Version: "3.14",
Author: "Archimedes",
AuthorEmail: "Archimedes@circles.io",
Platform: "universe",
SitePackagesRootPath: "Pi",
},
},
other: Package{
Name: "pi",
Version: "3.14",
FoundBy: "Archimedes",
Locations: file.NewLocationSet(
similarLocation, // NOTE: difference; we have a different layer but the same path
),
Language: "math",
Type: PythonPkg,
CPEs: []cpe.CPE{
cpe.Must(`cpe:2.3:a:DIFFERENT:pi:3.14:*:*:*:*:math:*:*`, cpe.NVDDictionaryLookupSource), // NOTE: difference
},
PURL: "pkg:pypi/pi@3.14",
Metadata: PythonPackage{
Name: "pi",
Version: "3.14",
Author: "Archimedes",
AuthorEmail: "Archimedes@circles.io",
Platform: "universe",
SitePackagesRootPath: "Pi",
},
},
expected: &Package{
Name: "pi",
Version: "3.14",
FoundBy: "Archimedes",
Locations: file.NewLocationSet(
originalLocation,
similarLocation, // NOTE: merge!
),
Language: "math",
Type: PythonPkg,
CPEs: []cpe.CPE{
cpe.Must(`cpe:2.3:a:Archimedes:pi:3.14:*:*:*:*:math:*:*`, cpe.NVDDictionaryLookupSource),
cpe.Must(`cpe:2.3:a:DIFFERENT:pi:3.14:*:*:*:*:math:*:*`, cpe.NVDDictionaryLookupSource), // NOTE: merge!
},
PURL: "pkg:pypi/pi@3.14",
Metadata: PythonPackage{
Name: "pi",
Version: "3.14",
Author: "Archimedes",
AuthorEmail: "Archimedes@circles.io",
Platform: "universe",
SitePackagesRootPath: "Pi",
},
},
},
{
name: "error when there are different IDs",
subject: Package{
Name: "pi",
Version: "3.14",
FoundBy: "Archimedes",
Locations: file.NewLocationSet(
originalLocation,
),
Language: "math",
Type: PythonPkg,
CPEs: []cpe.CPE{
cpe.Must(`cpe:2.3:a:Archimedes:pi:3.14:*:*:*:*:math:*:*`, cpe.NVDDictionaryLookupSource),
},
PURL: "pkg:pypi/pi@3.14",
Metadata: PythonPackage{
Name: "pi",
Version: "3.14",
Author: "Archimedes",
AuthorEmail: "Archimedes@circles.io",
Platform: "universe",
SitePackagesRootPath: "Pi",
},
},
other: Package{
Name: "pi-DIFFERENT", // difference
Version: "3.14",
FoundBy: "Archimedes",
Locations: file.NewLocationSet(
originalLocation,
),
Language: "math",
Type: PythonPkg,
CPEs: []cpe.CPE{
cpe.Must(`cpe:2.3:a:Archimedes:pi:3.14:*:*:*:*:math:*:*`, cpe.NVDDictionaryLookupSource),
},
PURL: "pkg:pypi/pi@3.14",
Metadata: PythonPackage{
Name: "pi",
Version: "3.14",
Author: "Archimedes",
AuthorEmail: "Archimedes@circles.io",
Platform: "universe",
SitePackagesRootPath: "Pi",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.subject.SetID()
tt.other.SetID()
err := tt.subject.merge(tt.other)
if tt.expected == nil {
require.Error(t, err)
return
}
require.NoError(t, err)
tt.expected.SetID()
require.Equal(t, tt.expected.id, tt.subject.id)
if diff := cmp.Diff(*tt.expected, tt.subject,
cmp.AllowUnexported(Package{}),
cmp.Comparer(
func(x, y file.LocationSet) bool {
xs := x.ToSlice()
ys := y.ToSlice()
if len(xs) != len(ys) {
return false
}
for i, xe := range xs {
ye := ys[i]
if !locationComparer(xe, ye) {
return false
}
}
return true
},
),
cmp.Comparer(
func(x, y LicenseSet) bool {
xs := x.ToSlice()
ys := y.ToSlice()
if len(xs) != len(ys) {
return false
}
for i, xe := range xs {
ye := ys[i]
if !licenseComparer(xe, ye) {
return false
}
}
return true
},
),
cmp.Comparer(locationComparer),
); diff != "" {
t.Errorf("unexpected result from parsing (-expected +actual)\n%s", diff)
}
})
}
}
func licenseComparer(x, y License) bool {
return cmp.Equal(x, y, cmp.Comparer(locationComparer))
}
func locationComparer(x, y file.Location) bool {
return cmp.Equal(x.Coordinates, y.Coordinates) && cmp.Equal(x.AccessPath, y.AccessPath)
}
func TestIsValid(t *testing.T) {
cases := []struct {
name string
given *Package
want bool
}{
{
name: "nil",
given: nil,
want: false,
},
{
name: "has-name",
given: &Package{Name: "paul"},
want: true,
},
{
name: "has-no-name",
given: &Package{},
want: false,
},
}
for _, c := range cases {
require.Equal(t, c.want, IsValid(c.given), "when package: %s", c.name)
}
}