-
Notifications
You must be signed in to change notification settings - Fork 574
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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>
- Loading branch information
1 parent
74b01a1
commit 4194a2c
Showing
20 changed files
with
1,298 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
65 changes: 65 additions & 0 deletions
65
cmd/syft/internal/test/integration/package_binary_elf_relationships_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
package integration | ||
|
||
import ( | ||
"github.com/anchore/syft/syft/artifact" | ||
"github.com/anchore/syft/syft/source" | ||
"github.com/stretchr/testify/require" | ||
"testing" | ||
) | ||
|
||
func TestBinaryElfRelationships(t *testing.T) { | ||
// node --> ["dependency of" nodes] | ||
expectedGraph := map[string][]string{ | ||
"glibc": { | ||
"libhello_world.so", | ||
"syfttestfixture", | ||
}, | ||
"libstdc++": { | ||
"syfttestfixture", | ||
}, | ||
"libhello_world.so": { | ||
"syfttestfixture", | ||
}, | ||
} | ||
|
||
// run the test... | ||
sbom, _ := catalogFixtureImage(t, "elf-test-fixtures", source.SquashedScope) | ||
|
||
// get a mapping of package names to their IDs | ||
nameToId := map[string]artifact.ID{} | ||
|
||
recordPkgId := func(name string) { | ||
pkgs := sbom.Artifacts.Packages.PackagesByName(name) | ||
require.NotEmpty(t, pkgs, "expected package %q to be present in the SBOM", name) | ||
for _, p := range pkgs { | ||
nameToId[p.Name] = p.ID() | ||
} | ||
} | ||
for name, depNames := range expectedGraph { | ||
recordPkgId(name) | ||
for _, depName := range depNames { | ||
recordPkgId(depName) | ||
} | ||
} | ||
|
||
for name, expectedDepNames := range expectedGraph { | ||
pkgId := nameToId[name] | ||
p := sbom.Artifacts.Packages.Package(pkgId) | ||
require.NotNil(t, p, "expected package %q to be present in the SBOM", name) | ||
|
||
rels := sbom.RelationshipsForPackage(*p, artifact.DependencyOfRelationship) | ||
require.NotEmpty(t, rels, "expected package %q to have relationships", name) | ||
|
||
toIds := map[artifact.ID]struct{}{} | ||
for _, rel := range rels { | ||
toIds[rel.To.ID()] = struct{}{} | ||
} | ||
|
||
for _, depName := range expectedDepNames { | ||
depId := nameToId[depName] | ||
_, exists := toIds[depId] | ||
require.True(t, exists, "expected package %q to have a relationship to %q", name, depName) | ||
} | ||
} | ||
|
||
} |
1 change: 1 addition & 0 deletions
1
cmd/syft/internal/test/integration/test-fixtures/elf-test-fixtures
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
../../../../../../syft/pkg/cataloger/binary/test-fixtures/elf-test-fixtures |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
}, | ||
) | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.