Add nested archive cataloging (#5260)

Signed-off-by: Kudryavcev Nikolay <kydry.nikolau@gmail.com>
Signed-off-by: Thomas Bechtold <thomasbechtold@jpberlin.de>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Signed-off-by: Keith Zantow <kzantow@gmail.com>
Co-authored-by: Kudryavcev Nikolay <kydry.nikolau@gmail.com>
Co-authored-by: Thomas Bechtold <thomasbechtold@jpberlin.de>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Keith Zantow
2026-10-29 17:33:07 -04:00
committed by GitHub
co-authored by Kudryavcev Nikolay Thomas Bechtold Alex Goodman
parent 14a0beb93c
commit 4b8a3db5f3
224 changed files with 15918 additions and 1281 deletions
@@ -71,7 +71,7 @@ func compareElfBinaryPackages(s *sbom.SBOM) []artifact.ID {
pkgsToDelete := make([]artifact.ID, 0)
for _, elfPkg := range allElfPackages(s) {
for _, loc := range onlyPrimaryEvidenceLocations(elfPkg) {
for _, otherPkg := range s.Artifacts.Packages.PackagesByPath(loc.RealPath) {
for _, otherPkg := range s.Artifacts.Packages.PackagesByPath(loc.FullNestedPath()) {
// we only care about comparing binary packages to each other (not other types)
if otherPkg.Type != pkg.BinaryPkg {
continue
@@ -130,7 +130,7 @@ func populateRelationships(exec file.Executable, parentPkg pkg.Package, resolver
for _, loc := range libLocations {
// are you in our index?
realBaseName := path.Base(loc.RealPath)
realBaseName := path.Base(loc.FullNestedPath())
pkgCollection := index.owningLibraryPackage(realBaseName)
if pkgCollection.PackageCount() < 1 {
addRelationship(
@@ -3,6 +3,7 @@ package binary
import (
"path"
"github.com/anchore/syft/internal/archive"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/sbomsync"
"github.com/anchore/syft/syft/file"
@@ -63,7 +64,7 @@ func locationsThatProvideLibraries(accessor sbomsync.Accessor) (map[string]file.
continue
}
basename := path.Base(coord.RealPath)
basename := path.Base(coord.FullNestedPath())
set := libLocationsByBasename[basename]
set.Add(coord)
allLibLocations.Add(coord)
@@ -82,6 +83,11 @@ func packagesWithLibraryOwnership(resolver file.Resolver, accessor sbomsync.Acce
var ownedFilePaths []string
if p.Type == pkg.BinaryPkg {
for _, loc := range p.Locations.ToSlice() {
if archive.ContainerPath(loc.Coordinates) != "" {
// a path within an archive cannot be resolved from the scan root; the location is the library
addLibraryOwner(packagesByLibraryPath, allLibLocations, loc.Coordinates, p)
continue
}
ownedFilePaths = append(ownedFilePaths, loc.Path())
}
} else {
@@ -114,18 +120,19 @@ func populatePackagesByLibraryPath(
}
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)
addLibraryOwner(packagesByLibraryPath, allLibLocations, loc.Coordinates, p)
}
}
return packagesByLibraryPath
}
// addLibraryOwner records p as an owner of the file at coordinates when that file is a library.
func addLibraryOwner(packagesByLibraryPath map[file.Coordinates]*pkg.Collection, allLibLocations file.CoordinateSet, coordinates file.Coordinates, p pkg.Package) {
if !allLibLocations.Contains(coordinates) {
return
}
if _, ok := packagesByLibraryPath[coordinates]; !ok {
packagesByLibraryPath[coordinates] = pkg.NewCollection()
}
packagesByLibraryPath[coordinates].Add(p)
}
+20 -2
View File
@@ -6,6 +6,7 @@ import (
"github.com/bmatcuk/doublestar/v4"
"github.com/scylladb/go-set/strset"
"github.com/anchore/syft/internal/archive"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/sbomsync"
"github.com/anchore/syft/syft/artifact"
@@ -100,7 +101,7 @@ func findOwnershipByFilesRelationships(resolver file.Resolver, catalog *pkg.Coll
// Now establish relationships, considering symlinks
for _, candidateOwnerPkg := range catalog.Sorted() {
id := candidateOwnerPkg.ID()
if candidateOwnerPkg.Metadata == nil {
if candidateOwnerPkg.Metadata == nil || withinArchive(candidateOwnerPkg) {
continue
}
@@ -164,7 +165,7 @@ func directOwnersByPath(catalog *pkg.Collection) map[pkg.Type]*strset.Set {
// First, identify direct ownership of all files
for _, p := range catalog.Sorted() {
if p.Metadata == nil {
if p.Metadata == nil || withinArchive(p) {
continue
}
@@ -192,6 +193,21 @@ func directOwnersByPath(catalog *pkg.Collection) map[pkg.Type]*strset.Set {
return directOwnership
}
// withinArchive reports whether every location of p is inside an archive. Such a package's owned files
// are paths within its archive, not the scan root, so it is not treated as a file owner.
func withinArchive(p pkg.Package) bool {
locations := p.Locations.ToSlice()
if len(locations) == 0 {
return false
}
for _, l := range locations {
if archive.ContainerPath(l.Coordinates) == "" {
return false
}
}
return true
}
func resolvePaths(ownedFilePath string, resolver file.Resolver) []string {
// though we have a string path, we need to resolve symlinks and other filesystem oddities since we cannot assume this is a real path
var locs []file.Location
@@ -206,6 +222,8 @@ func resolvePaths(ownedFilePath string, resolver file.Resolver) []string {
ownedFilePaths := strset.New(ownedFilePath)
for _, loc := range locs {
// Owned file paths are currently based on the top-level resolver, which is always the RealPath and includes
// all files found within archives at that location
ownedFilePaths.Add(loc.RealPath)
}
return ownedFilePaths.List()
@@ -78,6 +78,56 @@ func TestOwnershipByFilesRelationship(t *testing.T) {
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{
+4 -16
View File
@@ -15,18 +15,9 @@ func TestRelationshipsEvidentBy(t *testing.T) {
c := pkg.NewCollection()
coordA := file.Coordinates{
RealPath: "/somewhere/real",
FileSystemID: "abc",
}
coordC := file.Coordinates{
RealPath: "/somewhere/real",
FileSystemID: "abc",
}
coordD := file.Coordinates{
RealPath: "/somewhere/real",
FileSystemID: "abc",
}
coordA := file.NewCoordinates("/somewhere/real", "abc")
coordC := file.NewCoordinates("/somewhere/real", "abc")
coordD := file.NewCoordinates("/somewhere/real", "abc")
pkgA := pkg.Package{
Locations: file.NewLocationSet(
// added!
@@ -39,10 +30,7 @@ func TestRelationshipsEvidentBy(t *testing.T) {
pkgA.SetID()
c.Add(pkgA)
coordB := file.Coordinates{
RealPath: "/somewhere-else/real",
FileSystemID: "def",
}
coordB := file.NewCoordinates("/somewhere-else/real", "def")
pkgB := pkg.Package{
Locations: file.NewLocationSet(
// added!
+10 -22
View File
@@ -21,12 +21,8 @@ func Test_Index(t *testing.T) {
p3 := pkg.Package{
Name: "pkg-3",
}
c1 := file.Coordinates{
RealPath: "/coords/1",
}
c2 := file.Coordinates{
RealPath: "/coords/2",
}
c1 := file.NewCoordinates("/coords/1", "")
c2 := file.NewCoordinates("/coords/2", "")
for _, p := range []*pkg.Package{&p1, &p2, &p3} {
p.SetID()
@@ -156,18 +152,10 @@ func Test_Coordinates(t *testing.T) {
p3 := pkg.Package{
Name: "pkg-3",
}
c1 := file.Coordinates{
RealPath: "/coords/1",
}
c2 := file.Coordinates{
RealPath: "/coords/2",
}
c3 := file.Coordinates{
RealPath: "/coords/3",
}
c4 := file.Coordinates{
RealPath: "/coords/4",
}
c1 := file.NewCoordinates("/coords/1", "")
c2 := file.NewCoordinates("/coords/2", "")
c3 := file.NewCoordinates("/coords/3", "")
c4 := file.NewCoordinates("/coords/4", "")
for _, p := range []*pkg.Package{&p1, &p2, &p3} {
p.SetID()
@@ -237,10 +225,10 @@ func TestRemove(t *testing.T) {
p1 := pkg.Package{Name: "pkg-1"}
p2 := pkg.Package{Name: "pkg-2"}
p3 := pkg.Package{Name: "pkg-3"}
c1 := file.Coordinates{RealPath: "/coords/1"}
c2 := file.Coordinates{RealPath: "/coords/2"}
c3 := file.Coordinates{RealPath: "/coords/3"}
c4 := file.Coordinates{RealPath: "/coords/4"}
c1 := file.NewCoordinates("/coords/1", "")
c2 := file.NewCoordinates("/coords/2", "")
c3 := file.NewCoordinates("/coords/3", "")
c4 := file.NewCoordinates("/coords/4", "")
for _, p := range []*pkg.Package{&p1, &p2, &p3} {
p.SetID()