mirror of
https://github.com/anchore/syft.git
synced 2026-10-31 22:46:36 +01:00
`indexedPaths` added each location's `RealPath`, which for a nested file is the outermost archive. So
`PackagesByPath("/opt/x.jar")` returned every package found inside it at any depth, a root OS package
owning that file got ownership-overlap edges to all of them, and
`ExcludeBinaryPackagesWithFileOwnershipOverlap` then deleted nested binary, ELF and JVM packages.
Packages are now indexed by full path and access path only. An archive's own package still sits at
root coordinates and is found by its path; what's inside it stays linked to it by CONTAINS edges
rather than by ownership of the archive file.
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
390 lines
11 KiB
Go
390 lines
11 KiB
Go
package relationship
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/anchore/syft/internal/cmptest"
|
|
"github.com/anchore/syft/syft/artifact"
|
|
"github.com/anchore/syft/syft/file"
|
|
"github.com/anchore/syft/syft/pkg"
|
|
)
|
|
|
|
type mockFR struct {
|
|
file.Resolver
|
|
translate map[string]string
|
|
}
|
|
|
|
func (m mockFR) FilesByPath(paths ...string) ([]file.Location, error) {
|
|
var results []file.Location
|
|
for _, p := range paths {
|
|
tPath, ok := m.translate[p]
|
|
if !ok {
|
|
tPath = p
|
|
}
|
|
results = append(results, file.NewLocation(tPath))
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
func TestOwnershipByFilesRelationship(t *testing.T) {
|
|
|
|
tests := []struct {
|
|
name string
|
|
resolver file.Resolver
|
|
setup func(t testing.TB) ([]pkg.Package, []artifact.Relationship)
|
|
}{
|
|
{
|
|
name: "owns-by-real-path",
|
|
setup: func(t testing.TB) ([]pkg.Package, []artifact.Relationship) {
|
|
parent := pkg.Package{
|
|
Locations: file.NewLocationSet(
|
|
file.NewVirtualLocation("/a/path", "/another/path"),
|
|
file.NewVirtualLocation("/b/path", "/bee/path"),
|
|
),
|
|
Type: pkg.RpmPkg,
|
|
Metadata: pkg.RpmDBEntry{
|
|
Files: []pkg.RpmFileRecord{
|
|
{Path: "/owning/path/1"},
|
|
{Path: "/owning/path/2"},
|
|
{Path: "/d/path"},
|
|
},
|
|
},
|
|
}
|
|
parent.SetID()
|
|
|
|
child := pkg.Package{
|
|
Locations: file.NewLocationSet(
|
|
file.NewVirtualLocation("/c/path", "/another/path"),
|
|
file.NewVirtualLocation("/d/path", "/another/path"),
|
|
),
|
|
Type: pkg.NpmPkg,
|
|
}
|
|
child.SetID()
|
|
|
|
relationship := artifact.Relationship{
|
|
From: parent,
|
|
To: child,
|
|
Type: artifact.OwnershipByFileOverlapRelationship,
|
|
Data: ownershipByFilesMetadata{
|
|
Files: []string{
|
|
"/d/path",
|
|
},
|
|
},
|
|
}
|
|
|
|
return []pkg.Package{parent, child}, []artifact.Relationship{relationship}
|
|
},
|
|
},
|
|
{
|
|
name: "package-within-archive-is-not-a-file-owner",
|
|
setup: func(t testing.TB) ([]pkg.Package, []artifact.Relationship) {
|
|
nestedOwner := pkg.Package{
|
|
Locations: file.NewLocationSet(file.NewLocationFromCoordinates(file.Coordinates{
|
|
RealPath: "/rootfs.tar",
|
|
FullPath: "/rootfs.tar!/var/lib/rpm/Packages",
|
|
})),
|
|
Type: pkg.RpmPkg,
|
|
Metadata: pkg.RpmDBEntry{
|
|
Files: []pkg.RpmFileRecord{{Path: "/d/path"}},
|
|
},
|
|
}
|
|
nestedOwner.SetID()
|
|
|
|
child := pkg.Package{
|
|
Locations: file.NewLocationSet(file.NewLocation("/d/path")),
|
|
Type: pkg.BinaryPkg,
|
|
}
|
|
child.SetID()
|
|
|
|
return []pkg.Package{nestedOwner, child}, nil
|
|
},
|
|
},
|
|
{
|
|
// the archive's own package (when it has one) is found by its path; what is inside it is
|
|
// linked to that package by CONTAINS, not owned by whoever owns the archive file
|
|
name: "top-level-owner-of-an-archive-does-not-own-packages-within-it",
|
|
setup: func(t testing.TB) ([]pkg.Package, []artifact.Relationship) {
|
|
parent := pkg.Package{
|
|
Locations: file.NewLocationSet(file.NewLocation("/var/lib/rpm/Packages")),
|
|
Type: pkg.RpmPkg,
|
|
Metadata: pkg.RpmDBEntry{
|
|
Files: []pkg.RpmFileRecord{{Path: "/opt/bundle.tar"}},
|
|
},
|
|
}
|
|
parent.SetID()
|
|
|
|
child := pkg.Package{
|
|
Locations: file.NewLocationSet(file.NewLocationFromCoordinates(file.Coordinates{
|
|
RealPath: "/opt/bundle.tar",
|
|
FullPath: "/opt/bundle.tar!/lib/thing.jar",
|
|
})),
|
|
Type: pkg.JavaPkg,
|
|
}
|
|
child.SetID()
|
|
|
|
return []pkg.Package{parent, child}, nil
|
|
},
|
|
},
|
|
{
|
|
name: "only-real-file-owner-gets-ownership-relationship-not-symlink-owner",
|
|
resolver: mockFR{
|
|
translate: map[string]string{
|
|
"/usr/bin/jenkins.war-symlink": "/usr/share/jenkins/jenkins.war", // symlink to the real file
|
|
},
|
|
},
|
|
setup: func(t testing.TB) ([]pkg.Package, []artifact.Relationship) {
|
|
// Package that owns the symlink
|
|
symlinkOwner := pkg.Package{
|
|
Type: pkg.ApkPkg,
|
|
Metadata: pkg.ApkDBEntry{
|
|
Package: "jenkins-symlink-pkg",
|
|
Files: []pkg.ApkFileRecord{
|
|
{Path: "/usr/bin/jenkins.war-symlink"}, // this symlinks to the real file
|
|
},
|
|
},
|
|
}
|
|
symlinkOwner.SetID()
|
|
|
|
// Package that owns the real file
|
|
realFileOwner := pkg.Package{
|
|
Type: pkg.ApkPkg,
|
|
Metadata: pkg.ApkDBEntry{
|
|
Package: "jenkins-real-pkg",
|
|
Files: []pkg.ApkFileRecord{
|
|
{Path: "/usr/share/jenkins/jenkins.war"}, // the real file
|
|
},
|
|
},
|
|
}
|
|
realFileOwner.SetID()
|
|
|
|
// A third package that is "located at" the real file path
|
|
// This simulates a package being discovered at the jenkins.war location
|
|
consumerPackage := pkg.Package{
|
|
Type: pkg.JavaPkg,
|
|
Locations: file.NewLocationSet(
|
|
file.NewVirtualLocation("/usr/share/jenkins/jenkins.war", "/usr/share/jenkins/jenkins.war"),
|
|
),
|
|
}
|
|
consumerPackage.SetID()
|
|
|
|
// Only the real file owner should have an ownership relationship with the consumer package
|
|
// The symlink owner should NOT have a relationship with the consumer
|
|
relationship := artifact.Relationship{
|
|
From: realFileOwner,
|
|
To: consumerPackage,
|
|
Type: artifact.OwnershipByFileOverlapRelationship,
|
|
Data: ownershipByFilesMetadata{
|
|
Files: []string{
|
|
"/usr/share/jenkins/jenkins.war",
|
|
},
|
|
},
|
|
}
|
|
|
|
return []pkg.Package{symlinkOwner, realFileOwner, consumerPackage}, []artifact.Relationship{relationship}
|
|
},
|
|
},
|
|
{
|
|
name: "misses-by-dead-symlink",
|
|
resolver: mockFR{
|
|
translate: map[string]string{
|
|
"/bin/gzip": "", // treat this as a dead symlink
|
|
},
|
|
},
|
|
setup: func(t testing.TB) ([]pkg.Package, []artifact.Relationship) {
|
|
parent := pkg.Package{
|
|
Type: pkg.DebPkg,
|
|
Metadata: pkg.DpkgDBEntry{
|
|
Files: []pkg.DpkgFileRecord{
|
|
{Path: "/bin/gzip"}, // this symlinks to gzip via /bin -> /usr/bin
|
|
},
|
|
},
|
|
}
|
|
parent.SetID()
|
|
|
|
child := pkg.Package{
|
|
Locations: file.NewLocationSet(
|
|
file.NewVirtualLocation("/usr/bin/gzip", "/usr/bin/gzip"),
|
|
),
|
|
Type: pkg.BinaryPkg,
|
|
}
|
|
child.SetID()
|
|
|
|
return []pkg.Package{parent, child}, nil // importantly, no relationship is expected
|
|
},
|
|
},
|
|
{
|
|
name: "owns-by-symlink",
|
|
resolver: mockFR{
|
|
translate: map[string]string{
|
|
"/bin/gzip": "/usr/bin/gzip", // if there is a string path of /bin/gzip then return the real path of /usr/bin/gzip
|
|
},
|
|
},
|
|
setup: func(t testing.TB) ([]pkg.Package, []artifact.Relationship) {
|
|
parent := pkg.Package{
|
|
Type: pkg.DebPkg,
|
|
Metadata: pkg.DpkgDBEntry{
|
|
Files: []pkg.DpkgFileRecord{
|
|
{Path: "/bin/gzip"}, // this symlinks to gzip via /bin -> /usr/bin
|
|
},
|
|
},
|
|
}
|
|
parent.SetID()
|
|
|
|
child := pkg.Package{
|
|
Locations: file.NewLocationSet(
|
|
file.NewVirtualLocation("/usr/bin/gzip", "/usr/bin/gzip"),
|
|
),
|
|
Type: pkg.BinaryPkg,
|
|
}
|
|
child.SetID()
|
|
|
|
relationship := artifact.Relationship{
|
|
From: parent,
|
|
To: child,
|
|
Type: artifact.OwnershipByFileOverlapRelationship,
|
|
Data: ownershipByFilesMetadata{
|
|
Files: []string{
|
|
"/usr/bin/gzip",
|
|
},
|
|
},
|
|
}
|
|
|
|
return []pkg.Package{parent, child}, []artifact.Relationship{relationship}
|
|
},
|
|
},
|
|
{
|
|
name: "owns-by-virtual-path",
|
|
setup: func(t testing.TB) ([]pkg.Package, []artifact.Relationship) {
|
|
parent := pkg.Package{
|
|
Locations: file.NewLocationSet(
|
|
file.NewVirtualLocation("/a/path", "/some/other/path"),
|
|
file.NewVirtualLocation("/b/path", "/bee/path"),
|
|
),
|
|
Type: pkg.RpmPkg,
|
|
Metadata: pkg.RpmDBEntry{
|
|
Files: []pkg.RpmFileRecord{
|
|
{Path: "/owning/path/1"},
|
|
{Path: "/owning/path/2"},
|
|
{Path: "/another/path"},
|
|
},
|
|
},
|
|
}
|
|
parent.SetID()
|
|
|
|
child := pkg.Package{
|
|
Locations: file.NewLocationSet(
|
|
file.NewVirtualLocation("/c/path", "/another/path"),
|
|
file.NewLocation("/d/path"),
|
|
),
|
|
Type: pkg.NpmPkg,
|
|
}
|
|
child.SetID()
|
|
|
|
relationship := artifact.Relationship{
|
|
From: parent,
|
|
To: child,
|
|
Type: artifact.OwnershipByFileOverlapRelationship,
|
|
Data: ownershipByFilesMetadata{
|
|
Files: []string{
|
|
"/another/path",
|
|
},
|
|
},
|
|
}
|
|
return []pkg.Package{parent, child}, []artifact.Relationship{relationship}
|
|
},
|
|
},
|
|
{
|
|
name: "ignore-empty-path",
|
|
setup: func(t testing.TB) ([]pkg.Package, []artifact.Relationship) {
|
|
parent := pkg.Package{
|
|
Locations: file.NewLocationSet(
|
|
file.NewVirtualLocation("/a/path", "/some/other/path"),
|
|
file.NewVirtualLocation("/b/path", "/bee/path"),
|
|
),
|
|
Type: pkg.RpmPkg,
|
|
Metadata: pkg.RpmDBEntry{
|
|
Files: []pkg.RpmFileRecord{
|
|
{Path: "/owning/path/1"},
|
|
{Path: "/owning/path/2"},
|
|
{Path: ""},
|
|
},
|
|
},
|
|
}
|
|
|
|
parent.SetID()
|
|
|
|
child := pkg.Package{
|
|
Locations: file.NewLocationSet(
|
|
file.NewVirtualLocation("/c/path", "/another/path"),
|
|
file.NewLocation("/d/path"),
|
|
),
|
|
Type: pkg.NpmPkg,
|
|
}
|
|
|
|
child.SetID()
|
|
|
|
return []pkg.Package{parent, child}, nil
|
|
},
|
|
},
|
|
{
|
|
// a CPAN distribution owns whatever its .packlist recorded, which is how a distribution that
|
|
// installed a binary or a shared object is related to the package cataloged from it
|
|
name: "cpan-distribution-owns-packlist-paths",
|
|
setup: func(t testing.TB) ([]pkg.Package, []artifact.Relationship) {
|
|
parent := pkg.Package{
|
|
Locations: file.NewLocationSet(
|
|
file.NewLocation("/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux/auto/Net/SSLeay/.packlist"),
|
|
),
|
|
Type: pkg.CpanPkg,
|
|
Metadata: pkg.CpanDistribution{
|
|
MainModule: "Net::SSLeay",
|
|
Files: []string{
|
|
"/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux/Net/SSLeay.pm",
|
|
"/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux/auto/Net/SSLeay/SSLeay.so",
|
|
},
|
|
},
|
|
}
|
|
parent.SetID()
|
|
|
|
child := pkg.Package{
|
|
Locations: file.NewLocationSet(
|
|
file.NewLocation("/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux/auto/Net/SSLeay/SSLeay.so"),
|
|
),
|
|
Type: pkg.BinaryPkg,
|
|
}
|
|
child.SetID()
|
|
|
|
return []pkg.Package{parent, child}, []artifact.Relationship{
|
|
{
|
|
From: parent,
|
|
To: child,
|
|
Type: artifact.OwnershipByFileOverlapRelationship,
|
|
Data: ownershipByFilesMetadata{
|
|
Files: []string{"/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux/auto/Net/SSLeay/SSLeay.so"},
|
|
},
|
|
},
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
pkgs, expectedRelations := test.setup(t)
|
|
c := pkg.NewCollection(pkgs...)
|
|
relationships := byFileOwnershipOverlap(test.resolver, c)
|
|
|
|
require.Len(t, relationships, len(expectedRelations))
|
|
for idx, expectedRelationship := range expectedRelations {
|
|
actualRelationship := relationships[idx]
|
|
if d := cmp.Diff(expectedRelationship, actualRelationship, cmptest.DefaultOptions()...); d != "" {
|
|
t.Errorf("unexpected relationship (-want, +got): %s", d)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|