mirror of
https://github.com/anchore/syft.git
synced 2025-11-19 17:33:18 +01:00
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>
462 lines
11 KiB
Go
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)
|
|
}
|
|
}
|