mirror of
https://github.com/anchore/syft.git
synced 2026-10-30 14:02:26 +01:00
feat: add relationships to ELF package discovery (#2715)
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>
This commit is contained in:
co-authored by
Alex Goodman
parent
74b01a1c38
commit
4194a2cd34
@@ -0,0 +1,171 @@
|
||||
package binary
|
||||
|
||||
import (
|
||||
"path"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
|
||||
"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 NewDependencyRelationships(resolver file.Resolver, accessor sbomsync.Accessor) []artifact.Relationship {
|
||||
// TODO: consider library format (e.g. ELF, Mach-O, PE) for the meantime assume all binaries are homogeneous format
|
||||
// start with building new package-to-package relationships for executables-to-executables
|
||||
// each relationship must be unique, store in a map[id]map[id]relationship to avoid duplicates
|
||||
// 1 & 2... build an index of all shared libraries and their owning packages to search against
|
||||
index := newShareLibIndex(resolver, accessor)
|
||||
|
||||
// 3. craft package-to-package relationships for each binary that represent shared library dependencies
|
||||
//note: we only care about package-to-package relationships
|
||||
var relIndex *relationshipIndex
|
||||
accessor.ReadFromSBOM(func(s *sbom.SBOM) {
|
||||
relIndex = newRelationshipIndex(s.Relationships...)
|
||||
})
|
||||
|
||||
return generateRelationships(resolver, accessor, index, relIndex)
|
||||
}
|
||||
|
||||
func generateRelationships(resolver file.Resolver, accessor sbomsync.Accessor, index *sharedLibraryIndex, relIndex *relationshipIndex) []artifact.Relationship {
|
||||
// read all existing dependencyOf relationships
|
||||
accessor.ReadFromSBOM(func(s *sbom.SBOM) {
|
||||
for _, r := range s.Relationships {
|
||||
if r.Type != artifact.DependencyOfRelationship {
|
||||
continue
|
||||
}
|
||||
relIndex.track(r)
|
||||
}
|
||||
})
|
||||
|
||||
// find all package-to-package relationships for shared library dependencies
|
||||
accessor.ReadFromSBOM(func(s *sbom.SBOM) {
|
||||
for _, parentPkg := range s.Artifacts.Packages.Sorted(pkg.BinaryPkg) {
|
||||
for _, evidentLocation := range parentPkg.Locations.ToSlice() {
|
||||
if evidentLocation.Annotations[pkg.EvidenceAnnotationKey] != pkg.PrimaryEvidenceAnnotation {
|
||||
continue
|
||||
}
|
||||
|
||||
// find all libraries that this package depends on
|
||||
exec, ok := s.Artifacts.Executables[evidentLocation.Coordinates]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
populateRelationships(exec, parentPkg, resolver, relIndex, index)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return relIndex.newRelationships()
|
||||
}
|
||||
|
||||
// PackagesToRemove returns a list of binary packages (resolved by the ELF cataloger) that should be removed from the SBOM
|
||||
// These packages are removed because they are already represented by a higher order packages in the SBOM.
|
||||
func PackagesToRemove(resolver file.Resolver, accessor sbomsync.Accessor) []artifact.ID {
|
||||
pkgsToDelete := make([]artifact.ID, 0)
|
||||
accessor.ReadFromSBOM(func(s *sbom.SBOM) {
|
||||
// OTHER > ELF > Binary
|
||||
pkgsToDelete = append(pkgsToDelete, getBinaryPackagesToDelete(resolver, s)...)
|
||||
pkgsToDelete = append(pkgsToDelete, compareElfBinaryPackages(resolver, s)...)
|
||||
})
|
||||
return pkgsToDelete
|
||||
}
|
||||
|
||||
func compareElfBinaryPackages(resolver file.Resolver, s *sbom.SBOM) []artifact.ID {
|
||||
pkgsToDelete := make([]artifact.ID, 0)
|
||||
for _, p := range s.Artifacts.Packages.Sorted(pkg.BinaryPkg) {
|
||||
for _, loc := range p.Locations.ToSlice() {
|
||||
if loc.Annotations[pkg.EvidenceAnnotationKey] != pkg.PrimaryEvidenceAnnotation {
|
||||
continue
|
||||
}
|
||||
locations, err := resolver.FilesByPath(loc.RealPath)
|
||||
if err != nil {
|
||||
log.WithFields("error", err).Trace("unable to find path for owned file")
|
||||
continue
|
||||
}
|
||||
for _, ownedL := range locations {
|
||||
for _, pathPkg := range s.Artifacts.Packages.PackagesByPath(ownedL.RealPath) {
|
||||
// we only care about comparing binary packages to each other (not other types)
|
||||
if pathPkg.Type != pkg.BinaryPkg {
|
||||
continue
|
||||
}
|
||||
if _, ok := pathPkg.Metadata.(pkg.ELFBinaryPackageNoteJSONPayload); !ok {
|
||||
pkgsToDelete = append(pkgsToDelete, pathPkg.ID())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return pkgsToDelete
|
||||
}
|
||||
|
||||
func getBinaryPackagesToDelete(resolver file.Resolver, s *sbom.SBOM) []artifact.ID {
|
||||
pkgsToDelete := make([]artifact.ID, 0)
|
||||
for p := range s.Artifacts.Packages.Enumerate() {
|
||||
if p.Type == pkg.BinaryPkg {
|
||||
continue
|
||||
}
|
||||
fileOwner, ok := p.Metadata.(pkg.FileOwner)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
ownedFiles := fileOwner.OwnedFiles()
|
||||
locations, err := resolver.FilesByPath(ownedFiles...)
|
||||
if err != nil {
|
||||
log.WithFields("error", err).Trace("unable to find path for owned file")
|
||||
continue
|
||||
}
|
||||
for _, loc := range locations {
|
||||
for _, pathPkg := range s.Artifacts.Packages.PackagesByPath(loc.RealPath) {
|
||||
if pathPkg.Type == pkg.BinaryPkg {
|
||||
pkgsToDelete = append(pkgsToDelete, pathPkg.ID())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return pkgsToDelete
|
||||
}
|
||||
|
||||
func populateRelationships(exec file.Executable, parentPkg pkg.Package, resolver file.Resolver, relIndex *relationshipIndex, index *sharedLibraryIndex) {
|
||||
for _, libReference := range exec.ImportedLibraries {
|
||||
// for each library reference, check s.Artifacts.Packages.Sorted(pkg.BinaryPkg) for a binary package that represents that library
|
||||
// if found, create a relationship between the parent package and the library package
|
||||
// if not found do nothing.
|
||||
// note: we only care about package-to-package relationships
|
||||
|
||||
// find the basename of the library
|
||||
libBasename := path.Base(libReference)
|
||||
libLocations, err := resolver.FilesByGlob("**/" + libBasename)
|
||||
if err != nil {
|
||||
log.WithFields("lib", libReference, "error", err).Trace("unable to resolve library basename")
|
||||
continue
|
||||
}
|
||||
|
||||
for _, loc := range libLocations {
|
||||
// are you in our index?
|
||||
realBaseName := path.Base(loc.RealPath)
|
||||
pkgCollection := index.owningLibraryPackage(realBaseName)
|
||||
if pkgCollection.PackageCount() < 1 {
|
||||
relIndex.add(
|
||||
artifact.Relationship{
|
||||
From: loc.Coordinates,
|
||||
To: parentPkg,
|
||||
Type: artifact.DependencyOfRelationship,
|
||||
},
|
||||
)
|
||||
}
|
||||
for _, p := range pkgCollection.Sorted() {
|
||||
relIndex.add(
|
||||
artifact.Relationship{
|
||||
From: p,
|
||||
To: parentPkg,
|
||||
Type: artifact.DependencyOfRelationship,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package binary
|
||||
|
||||
import (
|
||||
"github.com/scylladb/go-set/strset"
|
||||
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
)
|
||||
|
||||
type relationshipIndex struct {
|
||||
typesByFromTo map[artifact.ID]map[artifact.ID]*strset.Set
|
||||
additional []artifact.Relationship
|
||||
}
|
||||
|
||||
func newRelationshipIndex(existing ...artifact.Relationship) *relationshipIndex {
|
||||
r := &relationshipIndex{
|
||||
typesByFromTo: make(map[artifact.ID]map[artifact.ID]*strset.Set),
|
||||
additional: make([]artifact.Relationship, 0),
|
||||
}
|
||||
for _, rel := range existing {
|
||||
r.track(rel)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// track this relationship as "exists" in the index (this is used to prevent duplicate relationships from being added).
|
||||
// returns true if the relationship is new to the index, false otherwise.
|
||||
func (i *relationshipIndex) track(r artifact.Relationship) bool {
|
||||
fromID := r.From.ID()
|
||||
if _, ok := i.typesByFromTo[fromID]; !ok {
|
||||
i.typesByFromTo[fromID] = make(map[artifact.ID]*strset.Set)
|
||||
}
|
||||
|
||||
toID := r.To.ID()
|
||||
if _, ok := i.typesByFromTo[fromID][toID]; !ok {
|
||||
i.typesByFromTo[fromID][toID] = strset.New()
|
||||
}
|
||||
|
||||
var exists bool
|
||||
if i.typesByFromTo[fromID][toID].Has(string(r.Type)) {
|
||||
exists = true
|
||||
}
|
||||
|
||||
i.typesByFromTo[fromID][toID].Add(string(r.Type))
|
||||
return !exists
|
||||
}
|
||||
|
||||
// add a new relationship to the index, returning true if the relationship is new to the index, false otherwise (thus is a duplicate).
|
||||
// nolint:unparam
|
||||
func (i *relationshipIndex) add(r artifact.Relationship) bool {
|
||||
if i.track(r) {
|
||||
i.additional = append(i.additional, r)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (i *relationshipIndex) newRelationships() []artifact.Relationship {
|
||||
return i.additional
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package binary
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/scylladb/go-set/strset"
|
||||
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
)
|
||||
|
||||
func Test_newRelationshipIndex(t *testing.T) {
|
||||
from := fakeIdentifiable{id: "from"}
|
||||
to := fakeIdentifiable{id: "to"}
|
||||
tests := []struct {
|
||||
name string
|
||||
given []artifact.Relationship
|
||||
want *relationshipIndex
|
||||
}{
|
||||
{
|
||||
name: "newRelationshipIndex returns an empty index with no existing relationships",
|
||||
want: &relationshipIndex{
|
||||
typesByFromTo: make(map[artifact.ID]map[artifact.ID]*strset.Set),
|
||||
additional: make([]artifact.Relationship, 0),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "newRelationshipIndex returns an index which tracks existing relationships",
|
||||
given: []artifact.Relationship{
|
||||
{
|
||||
From: from,
|
||||
To: to,
|
||||
Type: artifact.EvidentByRelationship,
|
||||
},
|
||||
},
|
||||
want: &relationshipIndex{
|
||||
typesByFromTo: map[artifact.ID]map[artifact.ID]*strset.Set{
|
||||
"from": {
|
||||
"to": strset.New("evident-by"),
|
||||
},
|
||||
},
|
||||
additional: make([]artifact.Relationship, 0),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := newRelationshipIndex(tt.given...); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("newRelationshipIndex() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_relationshipIndex_track(t *testing.T) {
|
||||
from := fakeIdentifiable{id: "from"}
|
||||
to := fakeIdentifiable{id: "to"}
|
||||
relationship := artifact.Relationship{From: from, To: to, Type: artifact.EvidentByRelationship}
|
||||
tests := []struct {
|
||||
name string
|
||||
existing []artifact.Relationship
|
||||
given artifact.Relationship
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "track returns true for a new relationship",
|
||||
existing: []artifact.Relationship{},
|
||||
given: relationship,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "track returns false for an existing relationship",
|
||||
existing: []artifact.Relationship{relationship},
|
||||
given: relationship,
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
i := newRelationshipIndex(tt.existing...)
|
||||
if got := i.track(tt.given); got != tt.want {
|
||||
t.Errorf("track() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_relationshipIndex_add(t *testing.T) {
|
||||
from := fakeIdentifiable{id: "from"}
|
||||
to := fakeIdentifiable{id: "to"}
|
||||
relationship := artifact.Relationship{From: from, To: to, Type: artifact.EvidentByRelationship}
|
||||
tests := []struct {
|
||||
name string
|
||||
existing []artifact.Relationship
|
||||
given artifact.Relationship
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "add returns true for a new relationship",
|
||||
existing: []artifact.Relationship{},
|
||||
given: relationship,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "add returns false for an existing relationship",
|
||||
existing: []artifact.Relationship{relationship},
|
||||
given: relationship,
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
i := newRelationshipIndex(tt.existing...)
|
||||
if got := i.add(tt.given); got != tt.want {
|
||||
t.Errorf("add() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type fakeIdentifiable struct {
|
||||
id string
|
||||
}
|
||||
|
||||
func (f fakeIdentifiable) ID() artifact.ID {
|
||||
return artifact.ID(f.id)
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package binary
|
||||
|
||||
import (
|
||||
"path"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/sbomsync"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
type sharedLibraryIndex struct {
|
||||
libLocationsByBasename map[string]file.CoordinateSet
|
||||
allLibLocations file.CoordinateSet
|
||||
packagesByLibraryPath map[file.Coordinates]*pkg.Collection
|
||||
}
|
||||
|
||||
func newShareLibIndex(resolver file.Resolver, accessor sbomsync.Accessor) *sharedLibraryIndex {
|
||||
s := &sharedLibraryIndex{
|
||||
libLocationsByBasename: make(map[string]file.CoordinateSet),
|
||||
allLibLocations: file.NewCoordinateSet(),
|
||||
packagesByLibraryPath: make(map[file.Coordinates]*pkg.Collection),
|
||||
}
|
||||
|
||||
s.build(resolver, accessor)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (i *sharedLibraryIndex) build(resolver file.Resolver, accessor sbomsync.Accessor) {
|
||||
// 1. map out all locations that provide libraries (indexed by the basename)
|
||||
i.libLocationsByBasename, i.allLibLocations = locationsThatProvideLibraries(accessor)
|
||||
|
||||
// 2. for each library path, find all packages that claim ownership of the library
|
||||
i.packagesByLibraryPath = packagesWithLibraryOwnership(resolver, accessor, i.allLibLocations)
|
||||
}
|
||||
|
||||
func (i *sharedLibraryIndex) owningLibraryPackage(libraryBasename string) *pkg.Collection {
|
||||
// find all packages that own a library by its basename
|
||||
collection := pkg.NewCollection()
|
||||
if set, ok := i.libLocationsByBasename[libraryBasename]; ok {
|
||||
for _, coord := range set.ToSlice() {
|
||||
if pkgSet, ok := i.packagesByLibraryPath[coord]; ok {
|
||||
toAdd := pkgSet.Sorted()
|
||||
collection.Add(toAdd...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return collection
|
||||
}
|
||||
|
||||
func locationsThatProvideLibraries(accessor sbomsync.Accessor) (map[string]file.CoordinateSet, file.CoordinateSet) {
|
||||
// map out all locations that provide libraries (indexed by the basename)
|
||||
libLocationsByBasename := make(map[string]file.CoordinateSet)
|
||||
allLibLocations := file.NewCoordinateSet()
|
||||
|
||||
accessor.ReadFromSBOM(func(s *sbom.SBOM) {
|
||||
// PROBLEM: this does not consider all symlinks to real paths that are libraries
|
||||
for coord, f := range s.Artifacts.Executables {
|
||||
if !f.HasExports {
|
||||
continue
|
||||
}
|
||||
|
||||
basename := path.Base(coord.RealPath)
|
||||
set := libLocationsByBasename[basename]
|
||||
set.Add(coord)
|
||||
allLibLocations.Add(coord)
|
||||
libLocationsByBasename[basename] = set
|
||||
}
|
||||
})
|
||||
|
||||
return libLocationsByBasename, allLibLocations
|
||||
}
|
||||
func packagesWithLibraryOwnership(resolver file.Resolver, accessor sbomsync.Accessor, allLibLocations file.CoordinateSet) map[file.Coordinates]*pkg.Collection {
|
||||
// map out all packages that claim ownership of a library at a specific path
|
||||
packagesByLibraryPath := make(map[file.Coordinates]*pkg.Collection)
|
||||
|
||||
accessor.ReadFromSBOM(func(s *sbom.SBOM) {
|
||||
for _, p := range s.Artifacts.Packages.Sorted() {
|
||||
var ownedFilePaths []string
|
||||
if p.Type == pkg.BinaryPkg {
|
||||
for _, loc := range p.Locations.ToSlice() {
|
||||
ownedFilePaths = append(ownedFilePaths, loc.Path())
|
||||
}
|
||||
} else {
|
||||
fileOwner, ok := p.Metadata.(pkg.FileOwner)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
ownedFilePaths = fileOwner.OwnedFiles()
|
||||
}
|
||||
|
||||
packagesByLibraryPath = populatePackagesByLibraryPath(resolver, allLibLocations, packagesByLibraryPath, p, ownedFilePaths)
|
||||
}
|
||||
})
|
||||
|
||||
return packagesByLibraryPath
|
||||
}
|
||||
|
||||
func populatePackagesByLibraryPath(
|
||||
resolver file.Resolver,
|
||||
allLibLocations file.CoordinateSet,
|
||||
packagesByLibraryPath map[file.Coordinates]*pkg.Collection,
|
||||
p pkg.Package,
|
||||
ownedFilePaths []string,
|
||||
) map[file.Coordinates]*pkg.Collection {
|
||||
for _, pth := range ownedFilePaths {
|
||||
ownedLocation, err := resolver.FilesByPath(pth)
|
||||
if err != nil {
|
||||
log.WithFields("error", err, "path", pth).Trace("unable to find path for owned file")
|
||||
continue
|
||||
}
|
||||
|
||||
for _, loc := range ownedLocation {
|
||||
// if the location is a library, add the package to the set of packages that own the library
|
||||
if !allLibLocations.Contains(loc.Coordinates) {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := packagesByLibraryPath[loc.Coordinates]; !ok {
|
||||
packagesByLibraryPath[loc.Coordinates] = pkg.NewCollection()
|
||||
}
|
||||
|
||||
// we have a library path, add the package to the set of packages that own the library
|
||||
packagesByLibraryPath[loc.Coordinates].Add(p)
|
||||
}
|
||||
}
|
||||
return packagesByLibraryPath
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package binary
|
||||
|
||||
import (
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func Test_newShareLibIndex(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
resolver file.Resolver
|
||||
coordinateIndex map[file.Coordinates]file.Executable
|
||||
packages []pkg.Package
|
||||
prexistingRelationships []artifact.Relationship
|
||||
}{
|
||||
{
|
||||
name: "constructor",
|
||||
resolver: file.NewMockResolverForPaths(),
|
||||
coordinateIndex: map[file.Coordinates]file.Executable{},
|
||||
packages: []pkg.Package{},
|
||||
prexistingRelationships: []artifact.Relationship{},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
accessor := newAccesor(tt.packages, tt.coordinateIndex, tt.prexistingRelationships)
|
||||
sharedLibraryIndex := newShareLibIndex(tt.resolver, accessor)
|
||||
if sharedLibraryIndex == nil {
|
||||
t.Errorf("newShareLibIndex() = %v, want non-nil", sharedLibraryIndex)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_sharedLibraryIndex_build(t *testing.T) {
|
||||
glibcCoordinate := file.NewCoordinates("/usr/lib64/libc.so.6", "")
|
||||
secondGlibcCoordinate := file.NewCoordinates("/usr/local/lib64/libc.so.6", "")
|
||||
glibcExecutable := file.Executable{
|
||||
Format: "elf",
|
||||
HasExports: true,
|
||||
HasEntrypoint: true,
|
||||
ImportedLibraries: []string{
|
||||
path.Base(glibcCoordinate.RealPath),
|
||||
path.Base(secondGlibcCoordinate.RealPath),
|
||||
},
|
||||
}
|
||||
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",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
resolver file.Resolver
|
||||
coordinateIndex map[file.Coordinates]file.Executable
|
||||
packages []pkg.Package
|
||||
prexistingRelationships []artifact.Relationship
|
||||
}{
|
||||
{
|
||||
name: "build with locations and packages",
|
||||
resolver: file.NewMockResolverForPaths([]string{
|
||||
glibcCoordinate.RealPath,
|
||||
secondGlibcCoordinate.RealPath,
|
||||
}...),
|
||||
coordinateIndex: map[file.Coordinates]file.Executable{
|
||||
glibcCoordinate: glibcExecutable,
|
||||
secondGlibcCoordinate: glibcExecutable,
|
||||
},
|
||||
packages: []pkg.Package{
|
||||
glibCPackage,
|
||||
},
|
||||
prexistingRelationships: []artifact.Relationship{},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
accessor := newAccesor(tt.packages, tt.coordinateIndex, tt.prexistingRelationships)
|
||||
sharedLibraryIndex := newShareLibIndex(tt.resolver, accessor)
|
||||
sharedLibraryIndex.build(tt.resolver, accessor)
|
||||
pkgs := sharedLibraryIndex.owningLibraryPackage(path.Base(glibcCoordinate.RealPath))
|
||||
if pkgs.PackageCount() < 1 {
|
||||
t.Errorf("owningLibraryPackage() = %v, want non-empty", pkgs)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
../../../../syft/pkg/cataloger/binary/test-fixtures/elf-test-fixtures
|
||||
Reference in New Issue
Block a user