From 01546d4e0641dbcde9a0686fa99a25ce91b07a44 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Wed, 21 Oct 2026 11:35:21 -0400 Subject: [PATCH] fix: don't index nested packages under the outer archive path `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 --- internal/relationship/by_file_ownership_test.go | 13 ++++--------- syft/pkg/collection.go | 8 ++++---- syft/pkg/collection_test.go | 17 +++++++++++++++++ 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/internal/relationship/by_file_ownership_test.go b/internal/relationship/by_file_ownership_test.go index 1e83a2c76..ed8282eb9 100644 --- a/internal/relationship/by_file_ownership_test.go +++ b/internal/relationship/by_file_ownership_test.go @@ -103,7 +103,9 @@ func TestOwnershipByFilesRelationship(t *testing.T) { }, }, { - name: "top-level-owner-of-an-archive-owns-packages-within-it", + // 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")), @@ -123,14 +125,7 @@ func TestOwnershipByFilesRelationship(t *testing.T) { } child.SetID() - relationship := artifact.Relationship{ - From: parent, - To: child, - Type: artifact.OwnershipByFileOverlapRelationship, - Data: ownershipByFilesMetadata{Files: []string{"/opt/bundle.tar"}}, - } - - return []pkg.Package{parent, child}, []artifact.Relationship{relationship} + return []pkg.Package{parent, child}, nil }, }, { diff --git a/syft/pkg/collection.go b/syft/pkg/collection.go index b808edf56..a566869c8 100644 --- a/syft/pkg/collection.go +++ b/syft/pkg/collection.go @@ -62,7 +62,7 @@ func (c *Collection) Package(id artifact.ID) *Package { } // PackagesByPath returns all packages that were discovered from the given path: a file's full path or -// access path, or the top path of an archive, which answers with every package found within it. +// access path. A package found within an archive is not returned for the archive's own path. func (c *Collection) PackagesByPath(path string) []Package { c.lock.RLock() defer c.lock.RUnlock() @@ -164,12 +164,12 @@ func (c *Collection) addPathsToIndex(p Package) { } } -// indexedPaths are the paths a package is found by: each location's full path and access path, and -// the top path of an archive it was found within. +// indexedPaths are the paths a package is found by: each location's full path and access path. The real +// path of a nested location is the outermost archive, so it is left out. func indexedPaths(p Package) []string { paths := strset.New() for _, l := range p.Locations.ToSlice() { - paths.Add(l.RealPath, l.AccessPath, l.FullNestedPath()) + paths.Add(l.AccessPath, l.FullNestedPath()) } paths.Remove("") return paths.List() diff --git a/syft/pkg/collection_test.go b/syft/pkg/collection_test.go index e1f08fd91..74bedc701 100644 --- a/syft/pkg/collection_test.go +++ b/syft/pkg/collection_test.go @@ -433,6 +433,23 @@ func TestCatalog_MergeRecords(t *testing.T) { } } +func TestCatalog_PackagesByPath_nestedPackagesAreNotFoundByTheOuterArchive(t *testing.T) { + jar := Package{Name: "jar", Type: JavaPkg, Locations: file.NewLocationSet(file.NewLocation("/opt/x.jar"))} + nested := file.Coordinates{RealPath: "/opt/x.jar", FullPath: "/opt/x.jar!/lib/a.so"} + so := Package{Name: "so", Type: BinaryPkg, Locations: file.NewLocationSet(file.NewLocationFromCoordinates(nested))} + c := NewCollection(jar, so) + + names := func(pkgs []Package) []string { + var out []string + for _, p := range pkgs { + out = append(out, p.Name) + } + return out + } + assert.Equal(t, []string{"jar"}, names(c.PackagesByPath("/opt/x.jar"))) + assert.Equal(t, []string{"so"}, names(c.PackagesByPath(nested.FullPath))) +} + func TestCatalog_EnumerateNilCatalog(t *testing.T) { var c *Collection assert.Empty(t, c.Enumerate())