mirror of
https://github.com/anchore/syft.git
synced 2026-10-30 22:12:28 +01:00
fix: link nested java packages to their enclosing jar when nested cataloging is on
the legacy recursion links a nested jar to the jar that holds it: a `DEPENDENCY_OF` edge from inner to outer, and `JavaArchive.Parent` on the inner package. Inside an extracted archive java called `parse(ctx, nil)`, so both went away as soon as the feature was on. What replaced them was `file CONTAINS package`, which SPDX and CycloneDX drop, so those formats lost all war-to-jar structure. - `archive.Traversal` gains `Parent` and `Packages` (the packages found describing that archive, filled in by the task after its sub-pipeline runs) - java picks the enclosing jar's main package from the parent traversal (the one whose virtual path is the archive's own; pom-derived aux packages carry a suffixed one) and passes it to `parse`, then sets `Parent` the same way the legacy path does Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
// Traversal describes the archive whose contents a cataloger is running against. The archive
|
||||
@@ -20,6 +21,14 @@ type Traversal struct {
|
||||
// Digests are of the archive file itself, taken during extraction; a cataloger running inside the
|
||||
// archive cannot compute them since the archive is not inside itself.
|
||||
Digests []file.Digest
|
||||
|
||||
// Parent is the traversal of the archive this one is nested in, nil for an archive at the scan root.
|
||||
Parent *Traversal
|
||||
|
||||
// Packages are those the catalogers found describing this archive (located at it), filled in once
|
||||
// they have run. Catalogers inside a nested archive read them through Parent, as java does to link a
|
||||
// nested jar to the jar that holds it.
|
||||
Packages []pkg.Package
|
||||
}
|
||||
|
||||
// ContentsArchivePath is the ArchivePath every file inside the archive carries: the chain to it, built
|
||||
|
||||
@@ -203,7 +203,7 @@ func (c *archiveCataloger) processArchive(ctx context.Context, parentResolver fi
|
||||
// extracted files keep the filesystem ID of where the archive was found; the nesting chain is
|
||||
// carried separately as the archive path. The chain names real paths, not the link the archive was
|
||||
// reached by, so it is stable and joins to the archive's package location by prefix.
|
||||
traversal := &archive.Traversal{Location: location}
|
||||
traversal := &archive.Traversal{Location: location, Parent: archive.TraversalFromContext(ctx)}
|
||||
archivePath := traversal.ContentsArchivePath()
|
||||
extracted, err := archive.Extract(ctx, archiveContent, location.FileSystemID, archivePath, c.limiter, c.exclusions)
|
||||
if errors.Is(err, archive.ErrDiskLimitReached) {
|
||||
@@ -240,6 +240,7 @@ func (c *archiveCataloger) processArchive(ctx context.Context, parentResolver fi
|
||||
|
||||
c.progress.Increment()
|
||||
scratch := c.runSubPipeline(ctx, extracted, location.Coordinates)
|
||||
traversal.Packages = packagesAt(scratch, location.Coordinates)
|
||||
children := c.discoverArchives(extracted)
|
||||
mergeArchiveResults(location.Coordinates, archivePath, children, scratch, builder)
|
||||
c.progress.AtomicStage.Set(fmt.Sprintf("%s archives (%s)", humanize.Comma(c.progress.Current()), archivePath))
|
||||
@@ -277,6 +278,17 @@ func (c *archiveCataloger) isAncestor(digests []file.Digest) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// packagesAt returns the packages located at the given coordinates, i.e. those describing the file there.
|
||||
func packagesAt(s *sbom.SBOM, coordinates file.Coordinates) []pkg.Package {
|
||||
var out []pkg.Package
|
||||
for _, p := range s.Artifacts.Packages.Sorted() {
|
||||
if p.Locations.CoordinateSet().Contains(coordinates) {
|
||||
out = append(out, p)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func recordArchiveUnknown(builder sbomsync.Builder, coordinates file.Coordinates, reason string) {
|
||||
appendUnknowns(builder, ArchiveCatalogerTaskName, []unknown.CoordinateError{{Coordinates: coordinates, Reason: errors.New(reason)}})
|
||||
}
|
||||
|
||||
@@ -381,6 +381,44 @@ func TestArchiveCataloger_defaultFileSelectionCoversRootJars(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestArchiveCataloger_javaParentLinkageMatchesTheLegacyPath(t *testing.T) {
|
||||
// a consumer walking java DEPENDENCY_OF edges (the only structure SPDX and CycloneDX keep) must see
|
||||
// the same war-to-jar linkage whether or not the archive task does the recursion
|
||||
dir := t.TempDir()
|
||||
war := buildZipBytesRaw(t, map[string][]byte{
|
||||
"META-INF/MANIFEST.MF": []byte("Manifest-Version: 1.0\nImplementation-Title: app\nImplementation-Version: 1.0\n"),
|
||||
"WEB-INF/lib/lib-2.0.jar": jarBytes(t, "lib", "2.0"),
|
||||
})
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "app.war"), war, 0o644))
|
||||
|
||||
for name, depth := range map[string]int{"feature off": 0, "feature on": 2} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
cfg := DefaultCreateSBOMConfig().
|
||||
WithCatalogerSelection(cataloging.NewSelectionRequest().WithDefaults("java")).
|
||||
WithArchiveConfig(cataloging.DefaultArchiveSearchConfig().WithMaxDepth(depth))
|
||||
s := scanDirWithExclusions(t, dir, cfg)
|
||||
|
||||
var edges []string
|
||||
for _, rel := range s.Relationships {
|
||||
from, fromOK := rel.From.(pkg.Package)
|
||||
to, toOK := rel.To.(pkg.Package)
|
||||
if fromOK && toOK && rel.Type == artifact.DependencyOfRelationship {
|
||||
edges = append(edges, from.Name+" -> "+to.Name)
|
||||
}
|
||||
}
|
||||
assert.Equal(t, []string{"lib -> app"}, edges)
|
||||
|
||||
var parent string
|
||||
for _, p := range s.Artifacts.Packages.Sorted() {
|
||||
if m, ok := p.Metadata.(pkg.JavaArchive); ok && p.Name == "lib" && m.Parent != nil {
|
||||
parent = m.Parent.Name
|
||||
}
|
||||
}
|
||||
assert.Equal(t, "app", parent)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestArchiveCataloger_descriptorRecordsArchiveConfig(t *testing.T) {
|
||||
// a consumer must be able to tell from the SBOM alone how deep archive contents were searched
|
||||
cfg := DefaultCreateSBOMConfig().
|
||||
|
||||
@@ -82,7 +82,18 @@ func (c *archiveCataloger) catalogExtracted(ctx context.Context, trav *archive.T
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
pkgs, relationships, err := parser.parse(ctx, nil)
|
||||
// link to the jar this one is nested in, as the legacy recursion does: a DEPENDENCY_OF edge from
|
||||
// parse, and Parent on whatever does not already have one
|
||||
parent := enclosingJavaPackage(trav)
|
||||
pkgs, relationships, err := parser.parse(ctx, parent)
|
||||
if parent != nil {
|
||||
for i := range pkgs {
|
||||
if metadata, ok := pkgs[i].Metadata.(pkg.JavaArchive); ok && metadata.Parent == nil {
|
||||
metadata.Parent = parent
|
||||
pkgs[i].Metadata = metadata
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
// attributed to the archive, as the generic cataloger attributes a parser failure to its file
|
||||
return pkgs, relationships, unknown.New(trav.Location, err)
|
||||
@@ -90,6 +101,22 @@ func (c *archiveCataloger) catalogExtracted(ctx context.Context, trav *archive.T
|
||||
return pkgs, relationships, nil
|
||||
}
|
||||
|
||||
// enclosingJavaPackage returns the main java package of the archive the traversal's archive is nested
|
||||
// in, if there is one. Of the java packages located at an archive, the main one carries the archive's
|
||||
// own virtual path; those found from embedded pom files carry it with their group and artifact appended.
|
||||
func enclosingJavaPackage(trav *archive.Traversal) *pkg.Package {
|
||||
if trav.Parent == nil {
|
||||
return nil
|
||||
}
|
||||
virtualPath := archive.VirtualPath(trav.Parent.Location)
|
||||
for _, p := range trav.Parent.Packages {
|
||||
if metadata, ok := p.Metadata.(pkg.JavaArchive); ok && metadata.VirtualPath == virtualPath {
|
||||
return &p
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isJavaArchiveName reports whether the path matches the globs this cataloger opens files by.
|
||||
func isJavaArchiveName(path string) bool {
|
||||
for _, glob := range archiveFormatGlobs {
|
||||
|
||||
Reference in New Issue
Block a user