fix: emit archive-to-archive CONTAINS edges and drop self-loops

the archive's `CONTAINS` edges were built from every coordinate in the sub-pipeline's scratch SBOM, which gets one of two things wrong:
- with the default `FilesOwnedByPackageSelection`, nothing records the nested archives as files, so the archive-to-archive chain was missing and grandchild packages hung off coordinates with no incoming edge
- the scratch SBOM also holds coordinates from the parent's frame, most notably unknowns recorded at the archive's own coordinates (e.g. a jar with no manifest). Those turned into `x.jar CONTAINS x.jar`

edges now go to the nested archives found in it (always, whatever the file selection), plus coordinates whose `ArchivePath` is this archive's own chain, i.e. files that are actually inside it. The walk reuses that one discovery pass for the recursion.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Alex Goodman
2026-09-24 13:26:59 -04:00
parent fe4fdc786c
commit 14bae1a0d9
3 changed files with 62 additions and 15 deletions
+24
View File
@@ -32,6 +32,7 @@ import (
"github.com/anchore/syft/internal/mimetype"
"github.com/anchore/syft/internal/sbomsync"
"github.com/anchore/syft/internal/unknown"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/cataloging"
"github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/event/monitor"
@@ -396,6 +397,29 @@ func Test_archiveCataloger_decompressionBudgetIsSharedByATreeAndFreshPerTopLevel
assert.Equal(t, 1, fileCounts["/sibling.zip"], "another top-level archive starts with a fresh budget")
}
func Test_archiveCataloger_noSelfContainsEdge(t *testing.T) {
// a jar java cannot identify records an unknown at its own coordinates; that must not turn into
// "x.jar CONTAINS x.jar", which a graph walker would loop on
rootDir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(rootDir, "x.jar"), makeZip(t, map[string][]byte{"readme.txt": []byte("no manifest")}), 0o600))
tsk := newTestTask(t, cataloging.DefaultArchiveSearchConfig().WithMaxDepth(1), javaTask(), fileMetadataTask())
s := newTestSBOM()
s.Artifacts.FileMetadata = map[file.Coordinates]file.Metadata{}
_ = tsk.Execute(context.Background(), dirTestResolver{dir: rootDir}, sbomsync.NewBuilder(s))
require.Contains(t, s.Artifacts.Unknowns, file.Coordinates{RealPath: "/x.jar"}, "the fixture must reproduce the unknown")
var contains int
for _, rel := range s.Relationships {
if rel.Type != artifact.ContainsRelationship {
continue
}
contains++
assert.NotEqual(t, rel.From.ID(), rel.To.ID(), "self-referential CONTAINS edge: %+v", rel)
}
assert.Positive(t, contains, "the archive still contains its files")
}
func Test_NewArchiveCatalogerTask_dropsItselfFromSubPipeline(t *testing.T) {
// a copy of itself in the sub-pipeline would process every nesting level twice
cfg := cataloging.DefaultArchiveSearchConfig().WithMaxDepth(2)