Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add relationships to ELF packages #2715

Merged
merged 30 commits into from
May 9, 2024
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
2e3cdc0
[wip] prototype binary relationships
wagoodman Mar 13, 2024
b9662b1
tests: add base test cases for new binary relationship framework
spiffcs Mar 14, 2024
3149508
docs: add diagrams that illustrate fixture
spiffcs Mar 15, 2024
e0b1aaf
dependency relationship updates
brian-ebarb Mar 22, 2024
646cf53
Adds binary dependency relationships to sbom outputs
brian-ebarb Apr 17, 2024
2da3785
[wip] adding tests for ELF package relationships
wagoodman Apr 17, 2024
d66ed7a
test: add unit test to cover relationships for binary packages
spiffcs Apr 25, 2024
6607bd5
fix: update static analysis checks for unit tests
spiffcs Apr 25, 2024
14551fd
test: refactor test for more cases
spiffcs Apr 26, 2024
df5e0ac
test: modify test harness to incorporate new cases
spiffcs Apr 26, 2024
68ec277
test: add test case where we link against multiple rpm given inconcls…
spiffcs Apr 26, 2024
e79eea8
test: final test cases
spiffcs Apr 26, 2024
bb8d07b
tests: add no resolver match test case
spiffcs Apr 26, 2024
b06f587
fix: SA for unpram
spiffcs Apr 26, 2024
fce7fab
bug: fix collection allocation so multiple relationships created
spiffcs Apr 29, 2024
4fe0c98
chore: allow relationship to coordinates
spiffcs Apr 29, 2024
f2f0d02
Merge remote-tracking branch 'anchore/main'
brian-ebarb May 7, 2024
31c7440
chore: add new coordinate function and disable test linting for Goland
spiffcs May 8, 2024
523d2d5
test: test relationship_index
spiffcs May 8, 2024
78fa58d
fix: add owningLibrary locations
spiffcs May 8, 2024
d0b710b
test: explicit tests for shared library index
spiffcs May 8, 2024
7514bee
feat: new intersection approach
spiffcs May 8, 2024
7b44cf9
ci: acceptence tests failing in mac-os; bump to v5
spiffcs May 8, 2024
391cac9
chore: evident locations
spiffcs May 8, 2024
d848aad
test: add failing test for remove package
spiffcs May 8, 2024
55ec10d
test: passing tests for removal
spiffcs May 8, 2024
17dbf5a
tests: cover tests for overlap and delete
spiffcs May 8, 2024
87113bd
fix: pr comments on style and execution flow
spiffcs May 8, 2024
9e17ded
test: builder test for delete
spiffcs May 9, 2024
52c08e1
chore: update test
spiffcs May 9, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/actions/bootstrap/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ runs:
using: "composite"
steps:
# note: go mod and build is automatically cached on default with v4+
- uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe #v4.1.0
- uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 #v5.0.1
if: inputs.go-version != ''
with:
go-version: ${{ inputs.go-version }}
Expand Down
1 change: 1 addition & 0 deletions .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ output:
uniq-by-line: false
run:
timeout: 10m
tests: false

# do not enable...
# - deadcode # The owner seems to have abandoned the linter. Replaced by "unused".
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
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",
},
// TODO: we need to capture devel package
spiffcs marked this conversation as resolved.
Show resolved Hide resolved
}

// 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: 0 additions & 1 deletion cmd/syft/internal/test/integration/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ func catalogFixtureImageWithConfig(t *testing.T, fixtureImageName string, cfg *s
cfg.CatalogerSelection = cfg.CatalogerSelection.WithDefaults(pkgcataloging.ImageTag)

// get the fixture image tar file
imagetest.GetFixtureImage(t, "docker-archive", fixtureImageName)
tarPath := imagetest.GetFixtureImageTarPath(t, fixtureImageName)

// get the source to build an SBOM against
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ require (
modernc.org/sqlite v1.29.9
)

require google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 // indirect

require (
dario.cat/mergo v1.0.0 // indirect
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
Expand Down Expand Up @@ -227,7 +229,6 @@ require (
golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.19.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect
google.golang.org/grpc v1.59.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
Expand Down
171 changes: 171 additions & 0 deletions internal/relationship/binary/binary_dependencies.go
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,
},
)
}
}
}
}
Loading
Loading