Skip to content

Commit

Permalink
[wip] adding tests for ELF package relationships
Browse files Browse the repository at this point in the history
Signed-off-by: Alex Goodman <[email protected]>
  • Loading branch information
wagoodman committed Apr 17, 2024
1 parent e14a6a3 commit 221ebf8
Show file tree
Hide file tree
Showing 8 changed files with 251 additions and 18 deletions.
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
}

// 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
22 changes: 16 additions & 6 deletions internal/relationship/binary/binary_dependencies.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package binary

import (
"github.com/anchore/syft/internal/log"
"path"

"github.com/anchore/syft/internal/sbomsync"
Expand All @@ -16,7 +17,7 @@ func NewDependencyRelationships(resolver file.Resolver, accessor sbomsync.Access
// 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

Check failure on line 18 in internal/relationship/binary/binary_dependencies.go

View workflow job for this annotation

GitHub Actions / Static analysis

commentFormatting: put a space between `//` and comment text (gocritic)
// 1 & 2... build an index of all shared libraries and their owning packages to search against
//index := newShareLibIndex(resolver, accessor)
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
Expand Down Expand Up @@ -44,16 +45,25 @@ func NewDependencyRelationships(resolver file.Resolver, accessor sbomsync.Access
}

for _, libReference := range exec.ImportedLibraries {
//for each library reference, check s.Artifacts.Packages.Sorted(pkg.BinaryPkg) for a bianry package that represents that library
//for each library reference, check s.Artifacts.Packages.Sorted(pkg.BinaryPkg) for a binary package that represents that library

Check failure on line 48 in internal/relationship/binary/binary_dependencies.go

View workflow job for this annotation

GitHub Actions / Static analysis

commentFormatting: put a space between `//` and comment text (gocritic)
//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)
for _, p := range s.Artifacts.Packages.Sorted(pkg.BinaryPkg) {
if p.Name == libBasename {
//create a relationship between the parent package and the library package
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)

for _, p := range pkgCollection.Sorted() {
relIndex.add(
artifact.Relationship{
From: p,
Expand All @@ -62,8 +72,8 @@ func NewDependencyRelationships(resolver file.Resolver, accessor sbomsync.Access
},
)
}
}

}
}
}

Expand Down
152 changes: 146 additions & 6 deletions internal/relationship/binary/binary_dependencies_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package binary

import (
"reflect"
"github.com/google/go-cmp/cmp"
"testing"

"github.com/anchore/syft/internal/sbomsync"
Expand All @@ -11,7 +11,7 @@ import (
)

func TestNewDependencyRelationships(t *testing.T) {
s := &sbom.SBOM{}

tests := []struct {
name string
resolver file.Resolver
Expand All @@ -21,15 +21,155 @@ func TestNewDependencyRelationships(t *testing.T) {
{
name: "blank sbom and accessor returns empty relationships",
resolver: nil,
accessor: sbomsync.NewBuilder(s).(sbomsync.Accessor),
want: make([]artifact.Relationship, 0),
accessor: func() sbomsync.Accessor {
return sbomsync.NewBuilder(&sbom.SBOM{}).(sbomsync.Accessor)
}(),
want: make([]artifact.Relationship, 0),
},
//{
// name: "binary elf cataloger test fixture",
// resolver: nil,
// accessor: func() sbomsync.Accessor {
// s := sbom.SBOM{
// Artifacts: sbom.Artifacts{
// Packages: pkg.NewCollection(),
// },
// }
// builder := sbomsync.NewBuilder(&s)
//
// // add ELF packages
// builder.AddPackages(
// []pkg.Package{
// {
// Name: "glibc",
// Version: "2.28-236.el8_9.12",
// Type: pkg.RpmPkg,
// Metadata: pkg.RpmDBEntry{
// Files: []pkg.RpmFileRecord{
// // TODO...?
// },
// },
// },
// {
// Name: "libstdc++",
// Version: "8.5.0-20.el8",
// Type: pkg.RpmPkg,
// Metadata: pkg.RpmDBEntry{
// Files: []pkg.RpmFileRecord{
// // TODO...?
// },
// },
// },
// {
// Name: "libhello_world.so",
// Version: "0.01",
// PURL: "pkg:generic/syftsys/[email protected]",
// FoundBy: "",
// Locations: file.NewLocationSet(
// file.NewVirtualLocation("/usr/local/bin/elftests/elfbinwithnestedlib/bin/lib/libhello_world.so", "/usr/local/bin/elftests/elfbinwithnestedlib/bin/lib/libhello_world.so"),
// file.NewVirtualLocation("/usr/local/bin/elftests/elfbinwithsisterlib/lib/libhello_world.so", "/usr/local/bin/elftests/elfbinwithsisterlib/lib/libhello_world.so"),
// file.NewVirtualLocation("/usr/local/bin/elftests/elfbinwithsisterlib/lib/libhello_world2.so", "/usr/local/bin/elftests/elfbinwithsisterlib/lib/libhello_world2.so"),
// ),
// Language: "",
// Type: pkg.BinaryPkg,
// Metadata: pkg.ELFBinaryPackageNoteJSONPayload{
// Type: "testfixture",
// Vendor: "syft",
// System: "syftsys",
// SourceRepo: "https://github.com/someone/somewhere.git",
// Commit: "5534c38d0ffef9a3f83154f0b7a7fb6ab0ab6dbb",
// },
// },
// {
// Name: "syfttestfixture",
// Version: "0.01",
// PURL: "pkg:generic/syftsys/[email protected]",
// FoundBy: "",
// Locations: file.NewLocationSet(
// file.NewLocation("/usr/local/bin/elftests/elfbinwithnestedlib/bin/elfbinwithnestedlib").WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
// file.NewLocation("/usr/local/bin/elftests/elfbinwithsisterlib/bin/elfwithparallellibbin1").WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
// file.NewLocation("/usr/local/bin/elftests/elfbinwithsisterlib/bin/elfwithparallellibbin2").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",
// },
// },
// }...)
//
// // add executables
//
// libstdcCoord := file.Coordinates{
// RealPath: "/usr/lib64/libstdc++.so.6.0.25",
// }
//
// glibcCoord := file.Coordinates{
// RealPath: "/usr/lib64/libc.so.6",
// }
//
// accessor := builder.(sbomsync.Accessor)
// accessor.WriteToSBOM(func(s *sbom.SBOM) {
// // add the libstdc++ executable
// s.Artifacts.Executables[libstdcCoord] = file.Executable{
// Format: "elf",
// HasExports: true,
// HasEntrypoint: true,
// ImportedLibraries: []string{
// "libm.so.6",
// "libc.so.6",
// "ld-linux-aarch64.so.1",
// "libgcc_s.so.1",
// },
// }
// })
//
// return accessor
// }(),
// want: []artifact.Relationship{},
//},
//{
// name: "binary elf cataloger test fixture",
// resolver: nil,
// accessor: func() sbomsync.Accessor {
// s := sbom.SBOM{
// Artifacts: sbom.Artifacts{
// Packages: pkg.NewCollection(),
// },
// }
// builder := sbomsync.NewBuilder(&s)
//
// fixtureName := "elf-test-fixtures"
// img := imagetest.GetFixtureImage(t, "docker-archive", fixtureName)
//
// src := stereoscopesource.New(img, stereoscopesource.ImageConfig{
// Reference: fixtureName,
// })
//
// r, err := src.FileResolver(source.SquashedScope)
// require.NoError(t, err)
//
// cat := binary.NewELFPackageCataloger()
// pkgs, relationships, err := cat.Catalog(context.Background(), r)
// require.NoError(t, err)
//
// builder.AddPackages(pkgs...)
// builder.AddRelationships(relationships...)
//
// return builder.(sbomsync.Accessor)
// }(),
// want: []artifact.Relationship{},
//},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
relationships := NewDependencyRelationships(tt.resolver, tt.accessor)
if !reflect.DeepEqual(relationships, tt.want) {
t.Errorf("NewDependencyRelationships() = %v, want %v", relationships, tt.want)
if d := cmp.Diff(tt.want, relationships); d != "" {
t.Errorf("unexpected relationships (-want, +got): %s", d)
}
})
}
Expand Down
17 changes: 13 additions & 4 deletions internal/relationship/binary/shared_library_index.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ func locationsThatProvideLibraries(accessor sbomsync.Accessor) (map[string]file.
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
Expand All @@ -84,12 +85,20 @@ func packagesWithLibraryOwnership(resolver file.Resolver, accessor sbomsync.Acce

accessor.ReadFromSBOM(func(s *sbom.SBOM) {
for _, p := range s.Artifacts.Packages.Sorted() {
fileOwner, ok := p.Metadata.(pkg.FileOwner)
if !ok {
continue
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()
}

for _, pth := range fileOwner.OwnedFiles() {
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")
Expand Down
9 changes: 8 additions & 1 deletion syft/sbom/sbom.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,22 @@ func (s SBOM) RelationshipsForPackage(p pkg.Package, rt ...artifact.Relationship
rt = artifact.AllRelationshipTypes()
}

pId := p.ID()

Check failure on line 78 in syft/sbom/sbom.go

View workflow job for this annotation

GitHub Actions / Static analysis

var-naming: var pId should be pID (revive)

var relationships []artifact.Relationship
for _, relationship := range s.Relationships {
if relationship.From == nil || relationship.To == nil {
log.Debugf("relationship has nil edge, skipping: %#v", relationship)
continue
}
if relationship.From.ID() != p.ID() {
fromId := relationship.From.ID()

Check failure on line 86 in syft/sbom/sbom.go

View workflow job for this annotation

GitHub Actions / Static analysis

var-naming: var fromId should be fromID (revive)
toId := relationship.To.ID()

Check failure on line 87 in syft/sbom/sbom.go

View workflow job for this annotation

GitHub Actions / Static analysis

var-naming: var toId should be toID (revive)
hasPkgId := fromId == pId || toId == pId

Check failure on line 88 in syft/sbom/sbom.go

View workflow job for this annotation

GitHub Actions / Static analysis

var-naming: var hasPkgId should be hasPkgID (revive)

if !hasPkgId {
continue
}

// check if the relationship is one we're searching for; rt is inclusive
if !slices.ContainsFunc(rt, func(r artifact.RelationshipType) bool { return relationship.Type == r }) {
continue
Expand Down

0 comments on commit 221ebf8

Please sign in to comment.