mirror of
https://github.com/anchore/syft.git
synced 2025-11-17 08:23:15 +01:00
This PR adds DependencyOf relationships when ELF packages have been discovered by the binary cataloger. The discovered file.Executable type has a []ImportedLibraries that's read from the file when discovered by syft. By mapping these imported libraries back to the package collection, syft is able to create relationships showing which packages are dependencies of other packages by just reading metadata from the ELF executable. --------- Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> Signed-off-by: Brian Ebarb <ebarb.brian@sers.noreply.github.com> Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
338 lines
11 KiB
Go
338 lines
11 KiB
Go
package binary
|
|
|
|
import (
|
|
"path"
|
|
"testing"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
|
|
"github.com/anchore/syft/internal/sbomsync"
|
|
"github.com/anchore/syft/syft/artifact"
|
|
"github.com/anchore/syft/syft/file"
|
|
"github.com/anchore/syft/syft/pkg"
|
|
"github.com/anchore/syft/syft/sbom"
|
|
)
|
|
|
|
func TestPackagesToRemove(t *testing.T) {
|
|
glibcCoordinate := file.NewCoordinates("/usr/lib64/libc.so.6", "")
|
|
glibCPackage := pkg.Package{
|
|
Name: "glibc",
|
|
Version: "2.28-236.el8_9.12",
|
|
Locations: file.NewLocationSet(
|
|
file.NewLocation(glibcCoordinate.RealPath),
|
|
),
|
|
Type: pkg.RpmPkg,
|
|
Metadata: pkg.RpmDBEntry{
|
|
Files: []pkg.RpmFileRecord{
|
|
{
|
|
Path: glibcCoordinate.RealPath,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
glibCPackage.SetID()
|
|
|
|
glibCBinaryELFPackage := pkg.Package{
|
|
Name: "glibc",
|
|
Version: "",
|
|
Locations: file.NewLocationSet(
|
|
file.NewLocation(glibcCoordinate.RealPath).WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
|
|
),
|
|
Language: "",
|
|
Type: pkg.BinaryPkg,
|
|
Metadata: pkg.ELFBinaryPackageNoteJSONPayload{
|
|
Type: "testfixture",
|
|
Vendor: "syft",
|
|
System: "syftsys",
|
|
SourceRepo: "https://github.com/someone/somewhere.git",
|
|
Commit: "5534c38d0ffef9a3f83154f0b7a7fb6ab0ab6dbb",
|
|
},
|
|
}
|
|
glibCBinaryELFPackage.SetID()
|
|
|
|
glibCBinaryClassifierPackage := pkg.Package{
|
|
Name: "glibc",
|
|
Version: "",
|
|
Locations: file.NewLocationSet(
|
|
file.NewLocation(glibcCoordinate.RealPath).WithAnnotation(pkg.EvidenceAnnotationKey, pkg.SupportingEvidenceAnnotation),
|
|
),
|
|
Language: "",
|
|
Type: pkg.BinaryPkg,
|
|
Metadata: pkg.BinarySignature{},
|
|
}
|
|
glibCBinaryClassifierPackage.SetID()
|
|
|
|
tests := []struct {
|
|
name string
|
|
resolver file.Resolver
|
|
accessor sbomsync.Accessor
|
|
want []artifact.ID
|
|
}{
|
|
{
|
|
name: "remove packages that are overlapping rpm --> binary",
|
|
resolver: file.NewMockResolverForPaths(glibcCoordinate.RealPath),
|
|
accessor: newAccesor([]pkg.Package{glibCPackage, glibCBinaryELFPackage}, map[file.Coordinates]file.Executable{}, nil),
|
|
want: []artifact.ID{glibCBinaryELFPackage.ID()},
|
|
},
|
|
{
|
|
name: "remove no packages when there is a single binary package",
|
|
resolver: file.NewMockResolverForPaths(glibcCoordinate.RealPath),
|
|
accessor: newAccesor([]pkg.Package{glibCBinaryELFPackage}, map[file.Coordinates]file.Executable{}, nil),
|
|
want: []artifact.ID{},
|
|
},
|
|
{
|
|
name: "remove packages when there is a single binary package and a classifier package",
|
|
resolver: file.NewMockResolverForPaths(glibcCoordinate.RealPath),
|
|
accessor: newAccesor([]pkg.Package{glibCBinaryELFPackage, glibCBinaryClassifierPackage}, map[file.Coordinates]file.Executable{}, nil),
|
|
want: []artifact.ID{glibCBinaryClassifierPackage.ID()},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
pkgsToDelete := PackagesToRemove(tt.resolver, tt.accessor)
|
|
if diff := cmp.Diff(tt.want, pkgsToDelete); diff != "" {
|
|
t.Errorf("unexpected packages to delete (-want, +got): %s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewDependencyRelationships(t *testing.T) {
|
|
// coordinates for the files under test
|
|
glibcCoordinate := file.NewCoordinates("/usr/lib64/libc.so.6", "")
|
|
secondGlibcCoordinate := file.NewCoordinates("/usr/local/lib64/libc.so.6", "")
|
|
nestedLibCoordinate := file.NewCoordinates("/usr/local/bin/elftests/elfbinwithnestedlib/bin/elfbinwithnestedlib", "")
|
|
parrallelLibCoordinate := file.NewCoordinates("/usr/local/bin/elftests/elfbinwithsisterlib/bin/elfwithparallellibbin1", "")
|
|
|
|
// rpm package that was discovered in linked section of the ELF binary package
|
|
glibCPackage := pkg.Package{
|
|
Name: "glibc",
|
|
Version: "2.28-236.el8_9.12",
|
|
Locations: file.NewLocationSet(
|
|
file.NewLocation(glibcCoordinate.RealPath),
|
|
file.NewLocation("some/other/path"),
|
|
),
|
|
Type: pkg.RpmPkg,
|
|
Metadata: pkg.RpmDBEntry{
|
|
Files: []pkg.RpmFileRecord{
|
|
{
|
|
Path: glibcCoordinate.RealPath,
|
|
},
|
|
{
|
|
Path: "some/other/path",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
glibCPackage.SetID()
|
|
|
|
// second rpm package that could be discovered in linked section of the ELF binary package (same base path as above)
|
|
glibCustomPackage := pkg.Package{
|
|
Name: "glibc",
|
|
Version: "2.28-236.el8_9.12",
|
|
Locations: file.NewLocationSet(file.NewLocation(secondGlibcCoordinate.RealPath)),
|
|
Type: pkg.RpmPkg,
|
|
Metadata: pkg.RpmDBEntry{
|
|
Files: []pkg.RpmFileRecord{
|
|
{
|
|
Path: secondGlibcCoordinate.RealPath,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
glibCustomPackage.SetID()
|
|
|
|
// binary package that is an executable that can link against above rpm packages
|
|
syftTestFixturePackage := pkg.Package{
|
|
Name: "syfttestfixture",
|
|
Version: "0.01",
|
|
PURL: "pkg:generic/syftsys/syfttestfixture@0.01",
|
|
FoundBy: "",
|
|
Locations: file.NewLocationSet(
|
|
file.NewLocation(nestedLibCoordinate.RealPath).WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
|
|
file.NewLocation(parrallelLibCoordinate.RealPath).WithAnnotation(pkg.EvidenceAnnotationKey, pkg.SupportingEvidenceAnnotation),
|
|
),
|
|
Language: "",
|
|
Type: pkg.BinaryPkg,
|
|
Metadata: pkg.ELFBinaryPackageNoteJSONPayload{
|
|
Type: "testfixture",
|
|
Vendor: "syft",
|
|
System: "syftsys",
|
|
SourceRepo: "https://github.com/someone/somewhere.git",
|
|
Commit: "5534c38d0ffef9a3f83154f0b7a7fb6ab0ab6dbb",
|
|
},
|
|
}
|
|
syftTestFixturePackage.SetID()
|
|
|
|
// dummy executable representation of glibc
|
|
glibcExecutable := file.Executable{
|
|
Format: "elf",
|
|
HasExports: true,
|
|
HasEntrypoint: true,
|
|
ImportedLibraries: []string{},
|
|
}
|
|
|
|
// executable representation of the syftTestFixturePackage
|
|
syftTestFixtureExecutable := file.Executable{
|
|
Format: "elf",
|
|
HasExports: true,
|
|
HasEntrypoint: true,
|
|
ImportedLibraries: []string{
|
|
path.Base(glibcCoordinate.RealPath),
|
|
},
|
|
}
|
|
|
|
// second executable representation that has no parent package
|
|
syftTestFixtureExecutable2 := file.Executable{
|
|
Format: "elf",
|
|
HasExports: true,
|
|
HasEntrypoint: true,
|
|
ImportedLibraries: []string{
|
|
// this should not be a relationship because it is not a coordinate
|
|
"foo.so.6",
|
|
},
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
resolver file.Resolver
|
|
coordinateIndex map[file.Coordinates]file.Executable
|
|
packages []pkg.Package
|
|
prexistingRelationships []artifact.Relationship
|
|
want []artifact.Relationship
|
|
}{
|
|
{
|
|
name: "blank sbom and accessor returns empty relationships",
|
|
resolver: nil,
|
|
coordinateIndex: map[file.Coordinates]file.Executable{},
|
|
packages: []pkg.Package{},
|
|
want: make([]artifact.Relationship, 0),
|
|
},
|
|
{
|
|
name: "given a package that imports glibc, expect a relationship between the two packages when the package is an executable",
|
|
resolver: file.NewMockResolverForPaths(
|
|
glibcCoordinate.RealPath,
|
|
nestedLibCoordinate.RealPath,
|
|
parrallelLibCoordinate.RealPath,
|
|
),
|
|
// path -> executable (above mock resolver needs to be able to resolve to paths in this map)
|
|
coordinateIndex: map[file.Coordinates]file.Executable{
|
|
glibcCoordinate: glibcExecutable,
|
|
nestedLibCoordinate: syftTestFixtureExecutable,
|
|
parrallelLibCoordinate: syftTestFixtureExecutable2,
|
|
},
|
|
packages: []pkg.Package{glibCPackage, syftTestFixturePackage},
|
|
want: []artifact.Relationship{
|
|
{
|
|
From: glibCPackage,
|
|
To: syftTestFixturePackage,
|
|
Type: artifact.DependencyOfRelationship,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "given an executable maps to one base path represented by two RPM we make two relationships",
|
|
resolver: file.NewMockResolverForPaths(
|
|
glibcCoordinate.RealPath,
|
|
secondGlibcCoordinate.RealPath,
|
|
nestedLibCoordinate.RealPath,
|
|
parrallelLibCoordinate.RealPath,
|
|
),
|
|
coordinateIndex: map[file.Coordinates]file.Executable{
|
|
glibcCoordinate: glibcExecutable,
|
|
secondGlibcCoordinate: glibcExecutable,
|
|
nestedLibCoordinate: syftTestFixtureExecutable,
|
|
parrallelLibCoordinate: syftTestFixtureExecutable2,
|
|
},
|
|
packages: []pkg.Package{glibCPackage, glibCustomPackage, syftTestFixturePackage},
|
|
want: []artifact.Relationship{
|
|
{
|
|
From: glibCPackage,
|
|
To: syftTestFixturePackage,
|
|
Type: artifact.DependencyOfRelationship,
|
|
},
|
|
{
|
|
From: glibCustomPackage,
|
|
To: syftTestFixturePackage,
|
|
Type: artifact.DependencyOfRelationship,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "given some dependency relationships already exist, expect no duplicate relationships to be created",
|
|
resolver: file.NewMockResolverForPaths(
|
|
glibcCoordinate.RealPath,
|
|
nestedLibCoordinate.RealPath,
|
|
parrallelLibCoordinate.RealPath,
|
|
),
|
|
coordinateIndex: map[file.Coordinates]file.Executable{
|
|
glibcCoordinate: glibcExecutable,
|
|
nestedLibCoordinate: syftTestFixtureExecutable,
|
|
parrallelLibCoordinate: syftTestFixtureExecutable2,
|
|
},
|
|
packages: []pkg.Package{glibCPackage, glibCustomPackage, syftTestFixturePackage},
|
|
prexistingRelationships: []artifact.Relationship{
|
|
{
|
|
From: glibCPackage,
|
|
To: syftTestFixturePackage,
|
|
Type: artifact.DependencyOfRelationship,
|
|
},
|
|
},
|
|
want: []artifact.Relationship{},
|
|
},
|
|
{
|
|
name: "given a package that imports a library that is not tracked by the resolver, expect no relationships to be created",
|
|
resolver: file.NewMockResolverForPaths(),
|
|
coordinateIndex: map[file.Coordinates]file.Executable{
|
|
glibcCoordinate: glibcExecutable,
|
|
nestedLibCoordinate: syftTestFixtureExecutable,
|
|
parrallelLibCoordinate: syftTestFixtureExecutable2,
|
|
},
|
|
packages: []pkg.Package{glibCPackage, syftTestFixturePackage},
|
|
want: []artifact.Relationship{},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
accessor := newAccesor(tt.packages, tt.coordinateIndex, tt.prexistingRelationships)
|
|
// given a resolver that knows about the paths of the packages and executables,
|
|
// and given an SBOM accessor that knows about the packages and executables,
|
|
// we should be able to create a set of relationships between the packages and executables
|
|
relationships := NewDependencyRelationships(tt.resolver, accessor)
|
|
if diff := relationshipComparer(tt.want, relationships); diff != "" {
|
|
t.Errorf("unexpected relationships (-want, +got): %s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func relationshipComparer(x, y []artifact.Relationship) string {
|
|
return cmp.Diff(x, y, cmpopts.IgnoreUnexported(
|
|
pkg.Package{},
|
|
artifact.Relationship{},
|
|
file.LocationSet{},
|
|
pkg.LicenseSet{},
|
|
))
|
|
}
|
|
|
|
func newAccesor(pkgs []pkg.Package, coordinateIndex map[file.Coordinates]file.Executable, prexistingRelationships []artifact.Relationship) sbomsync.Accessor {
|
|
sb := sbom.SBOM{
|
|
Artifacts: sbom.Artifacts{
|
|
Packages: pkg.NewCollection(),
|
|
},
|
|
}
|
|
|
|
builder := sbomsync.NewBuilder(&sb)
|
|
builder.AddPackages(pkgs...)
|
|
|
|
accessor := builder.(sbomsync.Accessor)
|
|
accessor.WriteToSBOM(func(s *sbom.SBOM) {
|
|
s.Artifacts.Executables = coordinateIndex
|
|
if prexistingRelationships != nil {
|
|
s.Relationships = prexistingRelationships
|
|
}
|
|
})
|
|
return accessor
|
|
}
|