mirror of
https://github.com/anchore/syft.git
synced 2026-10-30 14:02:26 +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"
|
"strings"
|
||||||
|
|
||||||
"github.com/anchore/syft/syft/file"
|
"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
|
// 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
|
// 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.
|
// archive cannot compute them since the archive is not inside itself.
|
||||||
Digests []file.Digest
|
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
|
// 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
|
// 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
|
// 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.
|
// 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()
|
archivePath := traversal.ContentsArchivePath()
|
||||||
extracted, err := archive.Extract(ctx, archiveContent, location.FileSystemID, archivePath, c.limiter, c.exclusions)
|
extracted, err := archive.Extract(ctx, archiveContent, location.FileSystemID, archivePath, c.limiter, c.exclusions)
|
||||||
if errors.Is(err, archive.ErrDiskLimitReached) {
|
if errors.Is(err, archive.ErrDiskLimitReached) {
|
||||||
@@ -240,6 +240,7 @@ func (c *archiveCataloger) processArchive(ctx context.Context, parentResolver fi
|
|||||||
|
|
||||||
c.progress.Increment()
|
c.progress.Increment()
|
||||||
scratch := c.runSubPipeline(ctx, extracted, location.Coordinates)
|
scratch := c.runSubPipeline(ctx, extracted, location.Coordinates)
|
||||||
|
traversal.Packages = packagesAt(scratch, location.Coordinates)
|
||||||
children := c.discoverArchives(extracted)
|
children := c.discoverArchives(extracted)
|
||||||
mergeArchiveResults(location.Coordinates, archivePath, children, scratch, builder)
|
mergeArchiveResults(location.Coordinates, archivePath, children, scratch, builder)
|
||||||
c.progress.AtomicStage.Set(fmt.Sprintf("%s archives (%s)", humanize.Comma(c.progress.Current()), archivePath))
|
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
|
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) {
|
func recordArchiveUnknown(builder sbomsync.Builder, coordinates file.Coordinates, reason string) {
|
||||||
appendUnknowns(builder, ArchiveCatalogerTaskName, []unknown.CoordinateError{{Coordinates: coordinates, Reason: errors.New(reason)}})
|
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) {
|
func TestArchiveCataloger_descriptorRecordsArchiveConfig(t *testing.T) {
|
||||||
// a consumer must be able to tell from the SBOM alone how deep archive contents were searched
|
// a consumer must be able to tell from the SBOM alone how deep archive contents were searched
|
||||||
cfg := DefaultCreateSBOMConfig().
|
cfg := DefaultCreateSBOMConfig().
|
||||||
|
|||||||
@@ -82,7 +82,18 @@ func (c *archiveCataloger) catalogExtracted(ctx context.Context, trav *archive.T
|
|||||||
return nil, nil, err
|
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 {
|
if err != nil {
|
||||||
// attributed to the archive, as the generic cataloger attributes a parser failure to its file
|
// attributed to the archive, as the generic cataloger attributes a parser failure to its file
|
||||||
return pkgs, relationships, unknown.New(trav.Location, err)
|
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
|
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.
|
// isJavaArchiveName reports whether the path matches the globs this cataloger opens files by.
|
||||||
func isJavaArchiveName(path string) bool {
|
func isJavaArchiveName(path string) bool {
|
||||||
for _, glob := range archiveFormatGlobs {
|
for _, glob := range archiveFormatGlobs {
|
||||||
|
|||||||
Reference in New Issue
Block a user