mirror of
https://github.com/anchore/syft.git
synced 2026-11-02 07:26:34 +01:00
chore: simplify and cleanup
Signed-off-by: Keith Zantow <keith.zantow@anchore.com>
This commit is contained in:
@@ -5,7 +5,6 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/bmatcuk/doublestar/v4"
|
||||
"github.com/scylladb/go-set/strset"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
@@ -14,30 +13,19 @@ import (
|
||||
"github.com/anchore/syft/syft/file"
|
||||
)
|
||||
|
||||
// archiveEntries is where an archive parser reads a java archive's entries from.
|
||||
//
|
||||
// A parser needs three things of an archive: which entries match a glob, the contents of some of
|
||||
// them, and the digest of the archive as a whole. Everything else - which manifest wins, how
|
||||
// pom.properties decides identity, license discovery - is logic over those answers, so naming the
|
||||
// three lets the same logic run over an archive file and over one already extracted to a filesystem.
|
||||
//
|
||||
// Entry names are archive-relative with no leading slash, matching zip entry names, since they become
|
||||
// the map keys and pom paths identity is derived from. An implementation over a filesystem must strip
|
||||
// the root to match.
|
||||
// archiveEntries is where the archive parser reads a java archive's entries from, so the same parser
|
||||
// runs over an archive file and over an archive already extracted to a filesystem. Entry names are
|
||||
// archive-relative with no leading slash, as zip entry names are.
|
||||
type archiveEntries interface {
|
||||
glob(caseInsensitive bool, patterns ...string) []string
|
||||
// glob returns the entries matching any pattern. Patterns are written against a leading slash
|
||||
// ("/META-INF/*"); the names returned have none.
|
||||
glob(patterns ...string) []string
|
||||
|
||||
// contents reads the named entries, returning a map keyed by the same names.
|
||||
// contents reads the named entries, keyed by the same names.
|
||||
contents(ctx context.Context, names ...string) (map[string]string, error)
|
||||
|
||||
// digests returns the digests of the archive as a whole, a property of the archive file rather
|
||||
// than of anything inside it.
|
||||
digests(ctx context.Context) ([]file.Digest, error)
|
||||
}
|
||||
|
||||
// zipEntries reads entries out of a java archive on disk, for scans where nothing has already
|
||||
// extracted it. It copies the archive to a temp file, reads its central directory once, and reopens
|
||||
// it for each set of entries it is asked for.
|
||||
// zipEntries reads entries out of a java archive file on disk.
|
||||
type zipEntries struct {
|
||||
archivePath string
|
||||
fileManifest intFile.ZipFileManifest
|
||||
@@ -51,41 +39,20 @@ func newZipEntries(ctx context.Context, archivePath string) (*zipEntries, error)
|
||||
return &zipEntries{archivePath: archivePath, fileManifest: fileManifest}, nil
|
||||
}
|
||||
|
||||
func (z *zipEntries) glob(caseInsensitive bool, patterns ...string) []string {
|
||||
return z.fileManifest.GlobMatch(caseInsensitive, patterns...)
|
||||
func (z *zipEntries) glob(patterns ...string) []string {
|
||||
return z.fileManifest.GlobMatch(patterns...)
|
||||
}
|
||||
|
||||
func (z *zipEntries) contents(ctx context.Context, names ...string) (map[string]string, error) {
|
||||
return intFile.ContentsFromZip(ctx, z.archivePath, names...)
|
||||
}
|
||||
|
||||
func (z *zipEntries) digests(ctx context.Context) ([]file.Digest, error) {
|
||||
return getDigestsFromArchive(ctx, z.archivePath)
|
||||
}
|
||||
|
||||
// resolverEntries reads entries out of an archive already extracted and indexed by the archive
|
||||
// cataloger task. Nothing is copied or reopened. The archive's own digests come from whoever
|
||||
// extracted it, since the archive file is not present in its own contents.
|
||||
// resolverEntries reads entries out of an archive the archive cataloger task has extracted.
|
||||
type resolverEntries struct {
|
||||
resolver file.Resolver
|
||||
archiveDigests []file.Digest
|
||||
resolver file.Resolver
|
||||
}
|
||||
|
||||
func newResolverEntries(resolver file.Resolver, archiveDigests []file.Digest) *resolverEntries {
|
||||
return &resolverEntries{
|
||||
resolver: resolver,
|
||||
archiveDigests: archiveDigests,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *resolverEntries) glob(caseInsensitive bool, patterns ...string) []string {
|
||||
if caseInsensitive {
|
||||
return r.globCaseInsensitive(patterns...)
|
||||
}
|
||||
|
||||
// deduplicated within this call only: the parser globs the same pattern more than once (identity
|
||||
// resolution and aux package discovery both ask for the poms), so remembering across calls would
|
||||
// return nothing the second time and lose every package derived from a bundled pom.properties
|
||||
func (r *resolverEntries) glob(patterns ...string) []string {
|
||||
seen := strset.New()
|
||||
var names []string
|
||||
for _, pattern := range patterns {
|
||||
@@ -106,37 +73,6 @@ func (r *resolverEntries) glob(caseInsensitive bool, patterns ...string) []strin
|
||||
return names
|
||||
}
|
||||
|
||||
// globCaseInsensitive matches the way zipEntries does, by lowercasing both sides. The resolver's own
|
||||
// glob is case sensitive, so this lists the archive's files once and matches them here. License
|
||||
// discovery is the only caller.
|
||||
func (r *resolverEntries) globCaseInsensitive(patterns ...string) []string {
|
||||
all, err := r.resolver.FilesByGlob("**/*")
|
||||
if err != nil {
|
||||
log.WithFields("error", err).Trace("unable to list archive contents")
|
||||
return nil
|
||||
}
|
||||
seen := strset.New()
|
||||
var names []string
|
||||
for _, pattern := range patterns {
|
||||
lowered := strings.ToLower(pattern)
|
||||
for _, location := range all {
|
||||
// matched with a leading slash, since the patterns are written that way ("/META-INF/*") and
|
||||
// zipEntries normalizes the same way; an extracted archive's resolver reports names without
|
||||
// one, so matching raw would match nothing
|
||||
name := strings.TrimPrefix(location.Path(), "/")
|
||||
if ok, matchErr := doublestar.Match(lowered, "/"+strings.ToLower(name)); matchErr != nil || !ok {
|
||||
continue
|
||||
}
|
||||
if seen.Has(name) {
|
||||
continue
|
||||
}
|
||||
seen.Add(name)
|
||||
names = append(names, name)
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func (r *resolverEntries) contents(_ context.Context, names ...string) (map[string]string, error) {
|
||||
out := make(map[string]string, len(names))
|
||||
for _, name := range names {
|
||||
@@ -156,15 +92,7 @@ func (r *resolverEntries) contents(_ context.Context, names ...string) (map[stri
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (r *resolverEntries) digests(_ context.Context) ([]file.Digest, error) {
|
||||
return r.archiveDigests, nil
|
||||
}
|
||||
|
||||
// read reads one entry's content to the end and closes the stream, returning a string because every
|
||||
// consumer parses text.
|
||||
//
|
||||
// The copy is bounded the way zipEntries bounds its own: an entry of an extracted archive is still
|
||||
// attacker-supplied, and reading it into a string is the allocation a decompression bomb targets.
|
||||
// read returns one entry's content, bounded against decompression bombs as zipEntries bounds its own.
|
||||
func (r *resolverEntries) read(location file.Location) (string, error) {
|
||||
reader, err := r.resolver.FileContentsByLocation(location)
|
||||
if err != nil {
|
||||
|
||||
@@ -17,8 +17,10 @@ import (
|
||||
"golang.org/x/exp/maps"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/archive"
|
||||
intFile "github.com/anchore/syft/internal/file"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/tmpdir"
|
||||
"github.com/anchore/syft/internal/unknown"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
@@ -56,27 +58,25 @@ var javaArchiveHashes = []crypto.Hash{
|
||||
crypto.SHA1,
|
||||
}
|
||||
|
||||
// archiveParser reads one java archive, whichever source it came from: everything it does is logic
|
||||
// over an archiveSource's entries and identity, so it does not know whether this cataloger opened an
|
||||
// archive file or the archive cataloger task already extracted one.
|
||||
// archiveParser reads one java archive, whether this cataloger opened the archive file or the archive
|
||||
// cataloger task already extracted it.
|
||||
type archiveParser struct {
|
||||
archiveSource
|
||||
entries archiveEntries
|
||||
// location is the archive's location in its parent filesystem; archive.VirtualPath(location) is its
|
||||
// identity with the chain of containing archives prepended: "app.war:WEB-INF/lib/dep.jar"
|
||||
location file.Location
|
||||
fileInfo archiveFilename
|
||||
// archiveDigests are of the archive file as a whole. When the archive cataloger task extracted the
|
||||
// archive they come from that extraction, since the archive is not inside itself.
|
||||
archiveDigests []file.Digest
|
||||
// contentPath is the directory nested archives are unzipped into. Set only when this cataloger
|
||||
// opened the archive itself, since only then does it recurse.
|
||||
contentPath string
|
||||
detectNested bool
|
||||
cfg ArchiveCatalogerConfig
|
||||
maven *maven.Resolver
|
||||
}
|
||||
|
||||
// newArchiveParser builds the parser for one archive. detectNested is whether this parser recurses
|
||||
// into the java archives inside this one, which only a file-backed source can do.
|
||||
func newArchiveParser(src archiveSource, detectNested bool, cfg ArchiveCatalogerConfig) *archiveParser {
|
||||
return &archiveParser{
|
||||
archiveSource: src,
|
||||
detectNested: detectNested,
|
||||
cfg: cfg,
|
||||
maven: maven.NewResolver(nil, cfg.mavenConfig()),
|
||||
}
|
||||
}
|
||||
|
||||
type genericArchiveParserAdapter struct {
|
||||
cfg ArchiveCatalogerConfig
|
||||
}
|
||||
@@ -92,9 +92,7 @@ func (gap genericArchiveParserAdapter) parseJavaArchive(ctx context.Context, _ f
|
||||
|
||||
// processJavaArchive processes an archive for java contents, returning all Java libraries and nested archives
|
||||
func (gap genericArchiveParserAdapter) processJavaArchive(ctx context.Context, reader file.LocationReadCloser, parentPkg *pkg.Package) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
// when the archive cataloger task owns recursion, unarchiving here would catalog nested archives
|
||||
// twice
|
||||
parser, cleanupFn, err := newJavaArchiveParser(ctx, reader, !gap.cfg.NestedArchivesHandledExternally, gap.cfg)
|
||||
parser, cleanupFn, err := newJavaArchiveParser(ctx, reader, !archive.NestedCatalogingEnabled(ctx), gap.cfg)
|
||||
// note: even on error, we should always run cleanup functions
|
||||
defer cleanupFn()
|
||||
if err != nil {
|
||||
@@ -111,14 +109,65 @@ func uniquePkgKey(groupID string, p *pkg.Package) string {
|
||||
return fmt.Sprintf("%s|%s|%s", groupID, p.Name, p.Version)
|
||||
}
|
||||
|
||||
// newJavaArchiveParser returns a new java archive parser for an archive file this cataloger opens
|
||||
// itself. Can be configured to discover and parse nested archives or ignore them.
|
||||
// newJavaArchiveParser returns a parser for an archive file this cataloger opens itself, copied to a
|
||||
// temp dir. Can be configured to discover and parse nested archives or ignore them. The returned
|
||||
// cleanup must run even when an error is returned.
|
||||
func newJavaArchiveParser(ctx context.Context, reader file.LocationReadCloser, detectNested bool, cfg ArchiveCatalogerConfig) (*archiveParser, func(), error) {
|
||||
src, cleanupFn, err := newFileArchiveSource(ctx, reader)
|
||||
// fetch the last element of the virtual path
|
||||
virtualElements := strings.Split(archive.VirtualPath(reader.Location), ":")
|
||||
currentFilepath := virtualElements[len(virtualElements)-1]
|
||||
|
||||
td := tmpdir.FromContext(ctx)
|
||||
if td == nil {
|
||||
return nil, func() {}, fmt.Errorf("no temp dir factory in context")
|
||||
}
|
||||
contentPath, archivePath, cleanupFn, err := saveArchiveToTmp(td, currentFilepath, reader)
|
||||
if err != nil {
|
||||
return nil, cleanupFn, fmt.Errorf("unable to process java archive: %w", err)
|
||||
}
|
||||
|
||||
entries, err := newZipEntries(ctx, archivePath)
|
||||
if err != nil {
|
||||
return nil, cleanupFn, err
|
||||
}
|
||||
return newArchiveParser(src, detectNested, cfg), cleanupFn, nil
|
||||
|
||||
digests, err := getDigestsFromArchive(ctx, archivePath)
|
||||
if err != nil {
|
||||
return nil, cleanupFn, err
|
||||
}
|
||||
|
||||
return &archiveParser{
|
||||
entries: entries,
|
||||
location: reader.Location,
|
||||
fileInfo: newJavaArchiveFilename(currentFilepath),
|
||||
archiveDigests: digests,
|
||||
contentPath: contentPath,
|
||||
detectNested: detectNested,
|
||||
cfg: cfg,
|
||||
maven: maven.NewResolver(nil, cfg.mavenConfig()),
|
||||
}, cleanupFn, nil
|
||||
}
|
||||
|
||||
// newExtractedArchiveParser returns a parser for the archive whose extracted contents the resolver
|
||||
// holds. Its identity comes from the traversal, since none of it is inside the archive, and it never
|
||||
// recurses: archives nested inside it are the archive cataloger task's to find.
|
||||
func newExtractedArchiveParser(trav *archive.Traversal, resolver file.Resolver, cfg ArchiveCatalogerConfig) (*archiveParser, error) {
|
||||
if resolver == nil {
|
||||
return nil, fmt.Errorf("no resolver for the contents of %q", archive.VirtualPath(trav.Location))
|
||||
}
|
||||
return &archiveParser{
|
||||
entries: &resolverEntries{resolver: resolver},
|
||||
location: trav.Location,
|
||||
fileInfo: newJavaArchiveFilename(path.Base(trav.Location.Path())),
|
||||
archiveDigests: trav.Digests,
|
||||
cfg: cfg,
|
||||
maven: maven.NewResolver(nil, cfg.mavenConfig()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// virtualPath is the archive's path with the chain of containing archives prepended.
|
||||
func (j *archiveParser) virtualPath() string {
|
||||
return archive.VirtualPath(j.location)
|
||||
}
|
||||
|
||||
// parse the loaded archive and return all packages found.
|
||||
@@ -177,11 +226,10 @@ func (j *archiveParser) parse(ctx context.Context, parentPkg *pkg.Package) ([]pk
|
||||
}
|
||||
pkgs = append(pkgs, nestedPkgs...)
|
||||
relationships = append(relationships, nestedRelationships...)
|
||||
} else if !j.cfg.NestedArchivesHandledExternally {
|
||||
// nothing will catalog these: this parser is not recursing and the archive cataloger task is
|
||||
// disabled, so record them as unknowns
|
||||
} else if !archive.NestedCatalogingEnabled(ctx) {
|
||||
// nothing will catalog these, so record them as unknowns
|
||||
// .jar and .war files are present in archives, are others? or generally just consider them top-level?
|
||||
nestedArchives := j.entries.glob(true, "**/*.jar", "**/*.war")
|
||||
nestedArchives := j.entries.glob("**/*.jar", "**/*.war")
|
||||
if len(nestedArchives) > 0 {
|
||||
slices.Sort(nestedArchives)
|
||||
errs = unknown.Appendf(errs, j.location, "nested archives not cataloged: %v", strings.Join(nestedArchives, ", "))
|
||||
@@ -214,7 +262,7 @@ func finalizePackage(p *pkg.Package) {
|
||||
// discoverMainPackage parses the root Java manifest used as the parent package to all discovered nested packages.
|
||||
func (j *archiveParser) discoverMainPackage(ctx context.Context) (*pkg.Package, error) {
|
||||
// search and parse java manifest files
|
||||
manifestMatches := j.entries.glob(false, manifestGlob)
|
||||
manifestMatches := j.entries.glob(manifestGlob)
|
||||
if len(manifestMatches) > 1 {
|
||||
return nil, fmt.Errorf("found multiple manifests in the jar: %+v", manifestMatches)
|
||||
} else if len(manifestMatches) == 0 {
|
||||
@@ -230,7 +278,7 @@ func (j *archiveParser) discoverMainPackage(ctx context.Context) (*pkg.Package,
|
||||
|
||||
// parse the manifest file into a rich object
|
||||
manifestContents := contents[manifestMatches[0]]
|
||||
manifest, err := parseJavaManifest(j.virtualPath, strings.NewReader(manifestContents))
|
||||
manifest, err := parseJavaManifest(j.virtualPath(), strings.NewReader(manifestContents))
|
||||
if err != nil {
|
||||
log.Debugf("failed to parse java manifest (%s): %+v", j.location, err)
|
||||
return nil, nil
|
||||
@@ -243,12 +291,6 @@ func (j *archiveParser) discoverMainPackage(ctx context.Context) (*pkg.Package,
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// grab and assign digest for the entire archive
|
||||
digests, err := j.entries.digests(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
name, version, lics, parsedPom := j.discoverNameVersionLicense(ctx, manifest)
|
||||
var pkgPomProject *pkg.JavaPomProject
|
||||
if parsedPom != nil {
|
||||
@@ -266,10 +308,10 @@ func (j *archiveParser) discoverMainPackage(ctx context.Context) (*pkg.Package,
|
||||
),
|
||||
Type: j.fileInfo.pkgType(),
|
||||
Metadata: pkg.JavaArchive{
|
||||
VirtualPath: j.virtualPath,
|
||||
VirtualPath: j.virtualPath(),
|
||||
Manifest: manifest,
|
||||
PomProject: pkgPomProject,
|
||||
ArchiveDigests: digests,
|
||||
ArchiveDigests: j.archiveDigests,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -382,8 +424,8 @@ type parsedPomProject struct {
|
||||
// discoverMainPackageFromPomInfo attempts to resolve maven groupId, artifactId, version and other info from found pom information
|
||||
func (j *archiveParser) discoverMainPackageFromPomInfo(ctx context.Context) (group, name, version string, parsedPom *parsedPomProject) {
|
||||
// Find the pom.properties/pom.xml if the names seem like a plausible match
|
||||
properties, _ := pomPropertiesByParentPath(ctx, j.entries, j.location, j.entries.glob(false, pomPropertiesGlob))
|
||||
projects, _ := pomProjectByParentPath(ctx, j.entries, j.location, j.entries.glob(false, pomXMLGlob))
|
||||
properties, _ := pomPropertiesByParentPath(ctx, j.entries, j.location, j.entries.glob(pomPropertiesGlob))
|
||||
projects, _ := pomProjectByParentPath(ctx, j.entries, j.location, j.entries.glob(pomXMLGlob))
|
||||
|
||||
artifactsMap := j.buildArtifactsMap(properties)
|
||||
pomProperties, parsedPom := j.findBestPomMatch(properties, projects, artifactsMap)
|
||||
@@ -514,13 +556,13 @@ func (j *archiveParser) discoverPkgsFromAllMavenFiles(ctx context.Context, paren
|
||||
var pkgs []pkg.Package
|
||||
|
||||
// pom.properties
|
||||
properties, err := pomPropertiesByParentPath(ctx, j.entries, j.location, j.entries.glob(false, pomPropertiesGlob))
|
||||
properties, err := pomPropertiesByParentPath(ctx, j.entries, j.location, j.entries.glob(pomPropertiesGlob))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// pom.xml
|
||||
projects, err := pomProjectByParentPath(ctx, j.entries, j.location, j.entries.glob(false, pomXMLGlob))
|
||||
projects, err := pomProjectByParentPath(ctx, j.entries, j.location, j.entries.glob(pomXMLGlob))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -531,7 +573,7 @@ func (j *archiveParser) discoverPkgsFromAllMavenFiles(ctx context.Context, paren
|
||||
parsedPom = proj
|
||||
}
|
||||
|
||||
pkgFromPom := newPackageFromMavenData(ctx, j.maven, propertiesObj, parsedPom, parentPkg, j.location, j.virtualPath)
|
||||
pkgFromPom := newPackageFromMavenData(ctx, j.maven, propertiesObj, parsedPom, parentPkg, j.location)
|
||||
if pkgFromPom != nil {
|
||||
pkgs = append(pkgs, *pkgFromPom)
|
||||
}
|
||||
@@ -563,7 +605,7 @@ func (j *archiveParser) getLicenseFromFileInArchive(ctx context.Context) []pkg.L
|
||||
|
||||
for _, glob := range []string{"/META-INF/*", "/*"} {
|
||||
var licenseMatches []string
|
||||
for _, f := range j.entries.glob(true, glob) {
|
||||
for _, f := range j.entries.glob(glob) {
|
||||
if licenses.IsLicenseFile(path.Base(f)) {
|
||||
licenseMatches = append(licenseMatches, f)
|
||||
}
|
||||
@@ -614,7 +656,7 @@ func (j *archiveParser) versionFromPropertiesFile(ctx context.Context, manifest
|
||||
return ""
|
||||
}
|
||||
|
||||
matches := j.entries.glob(true, "/version.properties")
|
||||
matches := j.entries.glob("/version.properties")
|
||||
if len(matches) != 1 {
|
||||
return ""
|
||||
}
|
||||
@@ -641,9 +683,9 @@ func (j *archiveParser) versionFromPropertiesFile(ctx context.Context, manifest
|
||||
return ""
|
||||
}
|
||||
|
||||
// discoverPkgsFromNestedArchives recurses into the java archives inside this one. Only zipEntries
|
||||
// applies, since recursion needs an archive file to unzip; with resolverEntries the archive cataloger
|
||||
// task has already extracted everything and meets those nested archives itself.
|
||||
// discoverPkgsFromNestedArchives recurses into the java archives inside this one, which needs an
|
||||
// archive file to unzip; an already-extracted archive's nested archives are the archive cataloger
|
||||
// task's to find.
|
||||
func (j *archiveParser) discoverPkgsFromNestedArchives(ctx context.Context, parentPkg *pkg.Package) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
zipped, ok := j.entries.(*zipEntries)
|
||||
if !ok {
|
||||
@@ -657,7 +699,7 @@ func (j *archiveParser) discoverPkgsFromNestedArchives(ctx context.Context, pare
|
||||
// associating each discovered package to the given parent package.
|
||||
func discoverPkgsFromZip(ctx context.Context, location file.Location, archivePath, contentPath string, fileManifest intFile.ZipFileManifest, parentPkg *pkg.Package, cfg ArchiveCatalogerConfig) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
// search and parse pom.properties files & fetch the contents
|
||||
openers, err := intFile.ExtractFromZipToUniqueTempFile(ctx, archivePath, contentPath, fileManifest.GlobMatch(false, archiveFormatGlobs...)...)
|
||||
openers, err := intFile.ExtractFromZipToUniqueTempFile(ctx, archivePath, contentPath, fileManifest.GlobMatch(archiveFormatGlobs...)...)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("unable to extract files from zip: %w", err)
|
||||
}
|
||||
@@ -778,7 +820,7 @@ func pomProjectByParentPath(ctx context.Context, entries archiveEntries, locatio
|
||||
|
||||
// newPackageFromMavenData processes a single Maven POM properties for a given parent package, returning all listed Java packages found and
|
||||
// associating each discovered package to the given parent package. Note the pom.xml is optional, the pom.properties is not.
|
||||
func newPackageFromMavenData(ctx context.Context, r *maven.Resolver, pomProperties pkg.JavaPomProperties, parsedPom *parsedPomProject, parentPkg *pkg.Package, location file.Location, virtualPath string) *pkg.Package {
|
||||
func newPackageFromMavenData(ctx context.Context, r *maven.Resolver, pomProperties pkg.JavaPomProperties, parsedPom *parsedPomProject, parentPkg *pkg.Package, location file.Location) *pkg.Package {
|
||||
// keep the artifact name within the virtual path if this package does not match the parent package
|
||||
vPathSuffix := ""
|
||||
groupID := ""
|
||||
@@ -798,7 +840,7 @@ func newPackageFromMavenData(ctx context.Context, r *maven.Resolver, pomProperti
|
||||
// https://github.com/anchore/syft/issues/1944
|
||||
vPathSuffix += ":" + pomProperties.GroupID + ":" + pomProperties.ArtifactID
|
||||
}
|
||||
virtualPath += vPathSuffix
|
||||
virtualPath := archive.VirtualPath(location) + vPathSuffix
|
||||
|
||||
var pkgPomProject *pkg.JavaPomProject
|
||||
|
||||
|
||||
@@ -1155,7 +1155,7 @@ func Test_newPackageFromMavenData(t *testing.T) {
|
||||
test.expectedParent.Locations = locations
|
||||
|
||||
r := maven.NewResolver(nil, maven.DefaultConfig())
|
||||
actualPackage := newPackageFromMavenData(pkgtest.Context(t), r, test.props, test.project, test.parent, file.NewLocation(virtualPath), virtualPath)
|
||||
actualPackage := newPackageFromMavenData(pkgtest.Context(t), r, test.props, test.project, test.parent, file.NewLocation(virtualPath))
|
||||
if test.expectedPackage == nil {
|
||||
require.Nil(t, actualPackage)
|
||||
} else {
|
||||
|
||||
@@ -3,6 +3,7 @@ package java
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -20,68 +21,12 @@ import (
|
||||
"github.com/anchore/syft/syft/source/directorysource"
|
||||
)
|
||||
|
||||
func Test_virtualPathFromArchiveTraversal(t *testing.T) {
|
||||
jarBytes := makeTestZip(t, map[string][]byte{
|
||||
"META-INF/MANIFEST.MF": testManifest("example-app", "1.0.0"),
|
||||
})
|
||||
|
||||
parse := func(t *testing.T, traversal *archive.Traversal, readerPath string) pkg.Package {
|
||||
ctx := pkgtest.Context(t)
|
||||
if traversal != nil {
|
||||
ctx = archive.WithTraversal(ctx, traversal)
|
||||
}
|
||||
gap := newGenericArchiveParserAdapter(ArchiveCatalogerConfig{})
|
||||
pkgs, _, err := gap.processJavaArchive(ctx, file.LocationReadCloser{
|
||||
Location: file.NewLocation(readerPath),
|
||||
ReadCloser: io.NopCloser(bytes.NewReader(jarBytes)),
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, pkgs, 1)
|
||||
return pkgs[0]
|
||||
}
|
||||
|
||||
t.Run("no traversal keeps reader path as virtual path", func(t *testing.T) {
|
||||
p := parse(t, nil, "some/dir/example-app-1.0.0.jar")
|
||||
metadata, ok := p.Metadata.(pkg.JavaArchive)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "some/dir/example-app-1.0.0.jar", metadata.VirtualPath)
|
||||
assert.Equal(t, "example-app", p.Name)
|
||||
assert.Equal(t, "1.0.0", p.Version)
|
||||
})
|
||||
|
||||
t.Run("traversal prepends containing archive chain", func(t *testing.T) {
|
||||
trav := &archive.Traversal{
|
||||
Location: file.NewLocation("/app.war"),
|
||||
VirtualPath: "/app.war",
|
||||
}
|
||||
p := parse(t, trav, "/WEB-INF/lib/example-app-1.0.0.jar")
|
||||
metadata, ok := p.Metadata.(pkg.JavaArchive)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "/app.war:WEB-INF/lib/example-app-1.0.0.jar", metadata.VirtualPath)
|
||||
// name/version inference from the last virtual path element is unaffected by the chain
|
||||
assert.Equal(t, "example-app", p.Name)
|
||||
assert.Equal(t, "1.0.0", p.Version)
|
||||
})
|
||||
|
||||
t.Run("multi-level traversal chains all parents", func(t *testing.T) {
|
||||
outer := &archive.Traversal{VirtualPath: "/dist.zip"}
|
||||
inner := &archive.Traversal{
|
||||
VirtualPath: outer.VirtualPathOf("/app.war"),
|
||||
}
|
||||
p := parse(t, inner, "/WEB-INF/lib/example-app-1.0.0.jar")
|
||||
metadata, ok := p.Metadata.(pkg.JavaArchive)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "/dist.zip:app.war:WEB-INF/lib/example-app-1.0.0.jar", metadata.VirtualPath)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_virtualPathFromArchiveTraversal_colonInEntryPath is the java-cataloger-level counterpart of
|
||||
// internal/archive's escaping tests: a package reached through an entry whose path contains a colon
|
||||
// must carry it escaped as %3A end to end in its virtual path
|
||||
// (decisions.md#colon-in-an-entry-path-is-escaped), while the maven-derived ":groupID:artifactID"
|
||||
// suffix that distinguishes a pom.properties package from its containing archive is still appended
|
||||
// with real, unescaped delimiters.
|
||||
func Test_virtualPathFromArchiveTraversal_colonInEntryPath(t *testing.T) {
|
||||
// must carry it escaped as %3A end to end in its virtual path, while the maven-derived
|
||||
// ":groupID:artifactID" suffix that distinguishes a pom.properties package from its containing archive
|
||||
// is still appended with real, unescaped delimiters.
|
||||
func Test_virtualPathFromArchiveLocation_colonInEntryPath(t *testing.T) {
|
||||
jarBytes := makeTestZip(t, map[string][]byte{
|
||||
"META-INF/MANIFEST.MF": testManifest("outer-app", "1.0.0"),
|
||||
"META-INF/maven/com.example/different-lib/pom.properties": []byte(
|
||||
@@ -89,15 +34,13 @@ func Test_virtualPathFromArchiveTraversal_colonInEntryPath(t *testing.T) {
|
||||
),
|
||||
})
|
||||
|
||||
trav := &archive.Traversal{
|
||||
Location: file.NewLocation("/outer.zip"),
|
||||
VirtualPath: "outer.zip",
|
||||
}
|
||||
ctx := archive.WithTraversal(pkgtest.Context(t), trav)
|
||||
// as the archive cataloger task's resolver would present an entry of outer.zip
|
||||
location := file.NewLocation("/lib/dep:special.jar")
|
||||
location.ArchivePath = "outer.zip"
|
||||
|
||||
gap := newGenericArchiveParserAdapter(ArchiveCatalogerConfig{})
|
||||
pkgs, _, err := gap.processJavaArchive(ctx, file.LocationReadCloser{
|
||||
Location: file.NewLocation("/lib/dep:special.jar"),
|
||||
pkgs, _, err := gap.processJavaArchive(pkgtest.Context(t), file.LocationReadCloser{
|
||||
Location: location,
|
||||
ReadCloser: io.NopCloser(bytes.NewReader(jarBytes)),
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
@@ -134,9 +77,9 @@ func Test_nestedArchiveOwnershipSwitch(t *testing.T) {
|
||||
"lib/nested-lib-2.0.jar": nestedJar,
|
||||
})
|
||||
|
||||
parse := func(t *testing.T, cfg ArchiveCatalogerConfig) []pkg.Package {
|
||||
parse := func(t *testing.T, ctx context.Context, cfg ArchiveCatalogerConfig) []pkg.Package {
|
||||
gap := newGenericArchiveParserAdapter(cfg)
|
||||
pkgs, _, err := gap.processJavaArchive(pkgtest.Context(t), file.LocationReadCloser{
|
||||
pkgs, _, err := gap.processJavaArchive(ctx, file.LocationReadCloser{
|
||||
Location: file.NewLocation("example-app-1.0.0.jar"),
|
||||
ReadCloser: io.NopCloser(bytes.NewReader(outerJar)),
|
||||
}, nil)
|
||||
@@ -145,7 +88,7 @@ func Test_nestedArchiveOwnershipSwitch(t *testing.T) {
|
||||
}
|
||||
|
||||
t.Run("nested archive cataloging disabled: this cataloger unarchives nested jars itself", func(t *testing.T) {
|
||||
pkgs := parse(t, DefaultArchiveCatalogerConfig()) // MaxDepth defaults to 0 (disabled)
|
||||
pkgs := parse(t, pkgtest.Context(t), DefaultArchiveCatalogerConfig()) // MaxDepth defaults to 0 (disabled)
|
||||
require.Len(t, pkgs, 2)
|
||||
|
||||
var names, virtualPaths []string
|
||||
@@ -163,12 +106,11 @@ func Test_nestedArchiveOwnershipSwitch(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("nested archive cataloging enabled: this cataloger does not unarchive", func(t *testing.T) {
|
||||
cfg := DefaultArchiveCatalogerConfig()
|
||||
// only CreateSBOMConfig sets this in production; it is not reachable from user config
|
||||
cfg.NestedArchivesHandledExternally = true
|
||||
// only syft.CreateSBOM marks the context in production; it is not reachable from user config
|
||||
ctx := archive.WithNestedCataloging(pkgtest.Context(t))
|
||||
|
||||
// no error expected: the nested jar is deliberately left to the archive cataloger task
|
||||
pkgs := parse(t, cfg)
|
||||
pkgs := parse(t, ctx, DefaultArchiveCatalogerConfig())
|
||||
require.Len(t, pkgs, 1)
|
||||
assert.Equal(t, "example-app", pkgs[0].Name)
|
||||
})
|
||||
@@ -179,9 +121,8 @@ func Test_nestedArchiveOwnershipSwitch(t *testing.T) {
|
||||
// wrong silently drops nested jars for every consumer that is not the CLI.
|
||||
cfg := DefaultArchiveCatalogerConfig()
|
||||
cfg.ArchiveSearchConfig = cfg.ArchiveSearchConfig.WithMaxDepth(2)
|
||||
require.False(t, cfg.NestedArchivesHandledExternally)
|
||||
|
||||
pkgs := parse(t, cfg)
|
||||
pkgs := parse(t, pkgtest.Context(t), cfg)
|
||||
require.Len(t, pkgs, 2, "this cataloger must still recurse itself")
|
||||
})
|
||||
}
|
||||
@@ -195,7 +136,7 @@ func Test_NewArchiveCataloger_wrappedParserRegistration(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "placeholder.txt"), []byte("x"), 0o600))
|
||||
|
||||
queriedGlobs := func(t *testing.T, cfg ArchiveCatalogerConfig) *pkgtest.ObservingResolver {
|
||||
queriedGlobs := func(t *testing.T, ctx context.Context, cfg ArchiveCatalogerConfig) *pkgtest.ObservingResolver {
|
||||
t.Helper()
|
||||
src, err := directorysource.NewFromPath(dir)
|
||||
require.NoError(t, err)
|
||||
@@ -203,7 +144,7 @@ func Test_NewArchiveCataloger_wrappedParserRegistration(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
observed := pkgtest.NewObservingResolver(resolver)
|
||||
_, _, err = NewArchiveCataloger(cfg).Catalog(pkgtest.Context(t), observed)
|
||||
_, _, err = NewArchiveCataloger(cfg).Catalog(ctx, observed)
|
||||
require.NoError(t, err)
|
||||
return observed
|
||||
}
|
||||
@@ -212,9 +153,8 @@ func Test_NewArchiveCataloger_wrappedParserRegistration(t *testing.T) {
|
||||
cfg := DefaultArchiveCatalogerConfig()
|
||||
cfg.IncludeIndexedArchives = true
|
||||
cfg.IncludeUnindexedArchives = true
|
||||
cfg.NestedArchivesHandledExternally = true
|
||||
|
||||
observed := queriedGlobs(t, cfg)
|
||||
observed := queriedGlobs(t, archive.WithNestedCataloging(pkgtest.Context(t)), cfg)
|
||||
// outside a traversal the cataloger opens no files at all: the task brings every archive back
|
||||
// as an extracted filesystem, and each is described there
|
||||
assert.False(t, observed.ObservedPathQuery("**/*.zip"), "no generic zip parser may be registered")
|
||||
@@ -227,7 +167,7 @@ func Test_NewArchiveCataloger_wrappedParserRegistration(t *testing.T) {
|
||||
cfg.IncludeIndexedArchives = true
|
||||
cfg.IncludeUnindexedArchives = true
|
||||
|
||||
observed := queriedGlobs(t, cfg)
|
||||
observed := queriedGlobs(t, pkgtest.Context(t), cfg)
|
||||
assert.True(t, observed.ObservedPathQuery("**/*.zip"), "existing behavior must be unchanged")
|
||||
assert.True(t, observed.ObservedPathQuery("**/*.tar"), "existing behavior must be unchanged")
|
||||
})
|
||||
@@ -237,7 +177,7 @@ func Test_NewArchiveCataloger_wrappedParserRegistration(t *testing.T) {
|
||||
cfg.IncludeIndexedArchives = false
|
||||
cfg.IncludeUnindexedArchives = false
|
||||
|
||||
observed := queriedGlobs(t, cfg)
|
||||
observed := queriedGlobs(t, pkgtest.Context(t), cfg)
|
||||
assert.False(t, observed.ObservedPathQuery("**/*.zip"))
|
||||
assert.False(t, observed.ObservedPathQuery("**/*.tar"))
|
||||
})
|
||||
@@ -246,8 +186,8 @@ func Test_NewArchiveCataloger_wrappedParserRegistration(t *testing.T) {
|
||||
func Test_mavenVirtualPathSuffixUnderTraversal(t *testing.T) {
|
||||
// a package derived from a pom.properties whose artifact does not match the containing archive gets
|
||||
// a trailing ":<groupID>:<artifactID>" appended to the archive's virtual path. That suffix form is in
|
||||
// published SBOMs, and once the archive cataloger drives the parser the base comes from the traversal
|
||||
// rather than the reader path - so the two must compose rather than one clobbering the other.
|
||||
// published SBOMs, and once the archive cataloger drives the parser the base carries the location's
|
||||
// archive path - so the two must compose rather than one clobbering the other.
|
||||
jarBytes := makeTestZip(t, map[string][]byte{
|
||||
"META-INF/MANIFEST.MF": testManifest("example-app", "1.0.0"),
|
||||
"META-INF/maven/com.example.other/other-lib/pom.properties": []byte(
|
||||
@@ -255,15 +195,13 @@ func Test_mavenVirtualPathSuffixUnderTraversal(t *testing.T) {
|
||||
),
|
||||
})
|
||||
|
||||
parse := func(t *testing.T, traversal *archive.Traversal, readerPath string) []pkg.Package {
|
||||
parse := func(t *testing.T, archivePath, readerPath string) []pkg.Package {
|
||||
t.Helper()
|
||||
ctx := pkgtest.Context(t)
|
||||
if traversal != nil {
|
||||
ctx = archive.WithTraversal(ctx, traversal)
|
||||
}
|
||||
location := file.NewLocation(readerPath)
|
||||
location.ArchivePath = archivePath
|
||||
gap := newGenericArchiveParserAdapter(ArchiveCatalogerConfig{})
|
||||
pkgs, _, err := gap.processJavaArchive(ctx, file.LocationReadCloser{
|
||||
Location: file.NewLocation(readerPath),
|
||||
pkgs, _, err := gap.processJavaArchive(pkgtest.Context(t), file.LocationReadCloser{
|
||||
Location: location,
|
||||
ReadCloser: io.NopCloser(bytes.NewReader(jarBytes)),
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
@@ -284,14 +222,14 @@ func Test_mavenVirtualPathSuffixUnderTraversal(t *testing.T) {
|
||||
return ""
|
||||
}
|
||||
|
||||
t.Run("no traversal: suffix hangs off the reader path", func(t *testing.T) {
|
||||
pkgs := parse(t, nil, "/example-app-1.0.0.jar")
|
||||
t.Run("outside an archive: suffix hangs off the reader path", func(t *testing.T) {
|
||||
pkgs := parse(t, "", "/example-app-1.0.0.jar")
|
||||
assert.Equal(t, "/example-app-1.0.0.jar:com.example.other:other-lib",
|
||||
virtualPathOf(t, pkgs, "other-lib"))
|
||||
})
|
||||
|
||||
t.Run("under traversal: suffix hangs off the archive chain", func(t *testing.T) {
|
||||
pkgs := parse(t, &archive.Traversal{VirtualPath: "app.war"}, "/WEB-INF/lib/example-app-1.0.0.jar")
|
||||
t.Run("inside an archive: suffix hangs off the archive chain", func(t *testing.T) {
|
||||
pkgs := parse(t, "app.war", "/WEB-INF/lib/example-app-1.0.0.jar")
|
||||
assert.Equal(t, "app.war:WEB-INF/lib/example-app-1.0.0.jar:com.example.other:other-lib",
|
||||
virtualPathOf(t, pkgs, "other-lib"))
|
||||
})
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
package java
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/bmatcuk/doublestar/v4"
|
||||
|
||||
"github.com/anchore/syft/internal/archive"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/tmpdir"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
)
|
||||
|
||||
// archiveSource is one java archive to describe: where its entries are read from, and the identity
|
||||
// that does not live inside them - the archive's own location, the colon-delimited virtual path
|
||||
// SBOMs carry, and the file name that name and version fall back to.
|
||||
//
|
||||
// Two of these exist because two things can own extraction. When this cataloger owns it, the archive
|
||||
// is a file it copies to a temp dir and reads as a zip. When the archive cataloger task owns it, the
|
||||
// archive has already been extracted and indexed, and arrives as a resolver plus the traversal
|
||||
// describing the file it came from. Everything past this point - which manifest wins, how
|
||||
// pom.properties decides identity, license discovery - is logic over a source, so it runs unchanged
|
||||
// either way.
|
||||
type archiveSource struct {
|
||||
entries archiveEntries
|
||||
location file.Location
|
||||
virtualPath string
|
||||
fileInfo archiveFilename
|
||||
|
||||
// contentPath is the directory nested archives are extracted into. Set only when this cataloger
|
||||
// opened the archive itself: recursing needs somewhere to unzip into, and an already-extracted
|
||||
// archive has none. The archive file itself is held by zipEntries.
|
||||
contentPath string
|
||||
}
|
||||
|
||||
// newFileArchiveSource copies the archive to a temp dir and reads its central directory, for a scan
|
||||
// where nothing has already extracted it. The returned cleanup must run even when an error is
|
||||
// returned.
|
||||
func newFileArchiveSource(ctx context.Context, reader file.LocationReadCloser) (archiveSource, func(), error) {
|
||||
// the full virtual path of this archive: when the archive cataloger task drove us here, the context
|
||||
// traversal supplies the containing chain; otherwise the reader path already carries any
|
||||
// colon-delimited nesting from this cataloger's own recursion
|
||||
virtualPath := archive.TraversalFromContext(ctx).VirtualPathOf(reader.Path())
|
||||
|
||||
// fetch the last element of the virtual path
|
||||
virtualElements := strings.Split(virtualPath, ":")
|
||||
currentFilepath := virtualElements[len(virtualElements)-1]
|
||||
|
||||
td := tmpdir.FromContext(ctx)
|
||||
if td == nil {
|
||||
return archiveSource{}, func() {}, fmt.Errorf("no temp dir factory in context")
|
||||
}
|
||||
contentPath, archivePath, cleanupFn, err := saveArchiveToTmp(td, currentFilepath, reader)
|
||||
if err != nil {
|
||||
return archiveSource{}, cleanupFn, fmt.Errorf("unable to process java archive: %w", err)
|
||||
}
|
||||
|
||||
entries, err := newZipEntries(ctx, archivePath)
|
||||
if err != nil {
|
||||
return archiveSource{}, cleanupFn, err
|
||||
}
|
||||
|
||||
return archiveSource{
|
||||
entries: entries,
|
||||
location: reader.Location,
|
||||
virtualPath: virtualPath,
|
||||
fileInfo: newJavaArchiveFilename(currentFilepath),
|
||||
contentPath: contentPath,
|
||||
}, cleanupFn, nil
|
||||
}
|
||||
|
||||
// newExtractedArchiveSource describes the archive whose extracted contents the given resolver holds.
|
||||
// Nothing is copied or reopened, and identity comes from the traversal: the location, the chain the
|
||||
// archive cataloger task composed, the file name, and the digests taken at extraction - none of which
|
||||
// are inside the archive.
|
||||
func newExtractedArchiveSource(trav *archive.Traversal, resolver file.Resolver) (archiveSource, error) {
|
||||
if resolver == nil {
|
||||
return archiveSource{}, fmt.Errorf("no resolver for the contents of %q", trav.VirtualPath)
|
||||
}
|
||||
return archiveSource{
|
||||
entries: newResolverEntries(resolver, trav.Digests),
|
||||
location: trav.Location,
|
||||
virtualPath: trav.VirtualPath,
|
||||
fileInfo: newJavaArchiveFilename(archiveFileNameOf(trav)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// archiveFileNameOf returns the archive's own file name, where a jar's name and version come from
|
||||
// when its manifest does not say. It takes the last element of the virtual path rather than the real
|
||||
// path, so a nested chain and a plain scan agree.
|
||||
func archiveFileNameOf(trav *archive.Traversal) string {
|
||||
name := trav.VirtualPath
|
||||
if name == "" {
|
||||
name = trav.Location.Path()
|
||||
}
|
||||
for i := len(name) - 1; i >= 0; i-- {
|
||||
if name[i] == ':' || name[i] == '/' {
|
||||
return name[i+1:]
|
||||
}
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// isJavaArchiveName reports whether the path is one of the java archive formats, by the same globs
|
||||
// this cataloger registers against when it is the one opening files.
|
||||
func isJavaArchiveName(path string) bool {
|
||||
for _, glob := range archiveFormatGlobs {
|
||||
if ok, err := doublestar.Match(glob, path); err == nil && ok {
|
||||
return true
|
||||
}
|
||||
if ok, err := doublestar.Match(glob, "/"+path); err == nil && ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
log.WithFields("path", path).Trace("archive contents are not a java archive")
|
||||
return false
|
||||
}
|
||||
@@ -55,9 +55,8 @@ func Test_archiveContentsCataloger_agreesWithTheArchiveReadingCataloger(t *testi
|
||||
|
||||
digests := digestsOfFile(t, jarPath)
|
||||
trav := &archive.Traversal{
|
||||
Location: file.NewLocation(archivePath),
|
||||
VirtualPath: archivePath,
|
||||
Digests: digests,
|
||||
Location: file.NewLocation(archivePath),
|
||||
Digests: digests,
|
||||
}
|
||||
ctx := archive.WithTraversal(context.Background(), trav)
|
||||
fromContents, _, err := NewArchiveCataloger(cfg).Catalog(ctx, contentsResolver)
|
||||
@@ -70,16 +69,14 @@ func Test_archiveContentsCataloger_reportsNothingOutsideAnArchive(t *testing.T)
|
||||
// with the archive cataloger task owning extraction, every archive comes back with a traversal
|
||||
// naming it. Outside one there is nothing to describe: opening archive files here as well would
|
||||
// catalog each of them a second time.
|
||||
cfg := DefaultArchiveCatalogerConfig()
|
||||
cfg.NestedArchivesHandledExternally = true
|
||||
|
||||
dir := t.TempDir()
|
||||
jarPath := writeJar(t, dir, "example-lib-1.2.3.jar", jarEntries())
|
||||
require.FileExists(t, jarPath)
|
||||
resolver, err := fileresolver.NewFromDirectory(dir, "")
|
||||
require.NoError(t, err)
|
||||
|
||||
pkgs, _, err := NewArchiveCataloger(cfg).Catalog(context.Background(), resolver)
|
||||
ctx := archive.WithNestedCataloging(context.Background())
|
||||
pkgs, _, err := NewArchiveCataloger(DefaultArchiveCatalogerConfig()).Catalog(ctx, resolver)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, pkgs)
|
||||
}
|
||||
@@ -93,8 +90,7 @@ func Test_archiveContentsCataloger_reportsNothingForAnArchiveThatIsNotJava(t *te
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := archive.WithTraversal(context.Background(), &archive.Traversal{
|
||||
Location: file.NewLocation("/bundle.zip"),
|
||||
VirtualPath: "/bundle.zip",
|
||||
Location: file.NewLocation("/bundle.zip"),
|
||||
})
|
||||
pkgs, _, err := NewArchiveCataloger(DefaultArchiveCatalogerConfig()).Catalog(ctx, resolver)
|
||||
require.NoError(t, err)
|
||||
@@ -117,8 +113,7 @@ func Test_archiveContentsCataloger_noManifestIsNoPackage(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := archive.WithTraversal(context.Background(), &archive.Traversal{
|
||||
Location: file.NewLocation("no-manifest-1.0.0.jar"),
|
||||
VirtualPath: "no-manifest-1.0.0.jar",
|
||||
Location: file.NewLocation("no-manifest-1.0.0.jar"),
|
||||
})
|
||||
pkgs, _, err := NewArchiveCataloger(DefaultArchiveCatalogerConfig()).Catalog(ctx, resolver)
|
||||
assert.Empty(t, pkgs)
|
||||
@@ -135,11 +130,6 @@ func Test_archiveContentsCataloger_readsWhatWasExtractedAndNoMore(t *testing.T)
|
||||
// extraction produced. An archive truncated by a limit yields a package built from the entries that
|
||||
// were written, so identity degrades rather than disappearing - the manifest is there, the maven
|
||||
// metadata is not.
|
||||
//
|
||||
// Observed on a real artifact: jenkins-core-2.578.jar holds more than the default 10,000-entry bound,
|
||||
// so its META-INF/maven pom is never extracted and its group id falls back from org.jenkins-ci.main
|
||||
// to the artifact id. The archive-reading cataloger never felt this, reading the archive's own
|
||||
// central directory with no bound on it.
|
||||
entries := map[string]string{
|
||||
"META-INF/MANIFEST.MF": "Manifest-Version: 1.0\n" +
|
||||
"Implementation-Title: example-lib\nImplementation-Version: 1.2.3\n",
|
||||
@@ -153,8 +143,7 @@ func Test_archiveContentsCataloger_readsWhatWasExtractedAndNoMore(t *testing.T)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := archive.WithTraversal(context.Background(), &archive.Traversal{
|
||||
Location: file.NewLocation("example-lib-1.2.3.jar"),
|
||||
VirtualPath: "example-lib-1.2.3.jar",
|
||||
Location: file.NewLocation("example-lib-1.2.3.jar"),
|
||||
})
|
||||
pkgs, _, err := NewArchiveCataloger(DefaultArchiveCatalogerConfig()).Catalog(ctx, resolver)
|
||||
require.NoError(t, err)
|
||||
@@ -249,9 +238,7 @@ func joinSorted(in []string) string {
|
||||
|
||||
func digestsOfFile(t *testing.T, path string) []file.Digest {
|
||||
t.Helper()
|
||||
entries, err := newZipEntries(context.Background(), path)
|
||||
require.NoError(t, err)
|
||||
digests, err := entries.digests(context.Background())
|
||||
digests, err := getDigestsFromArchive(context.Background(), path)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, digests)
|
||||
return digests
|
||||
|
||||
@@ -6,6 +6,8 @@ package java
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/bmatcuk/doublestar/v4"
|
||||
|
||||
"github.com/anchore/syft/internal/archive"
|
||||
"github.com/anchore/syft/internal/unknown"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
@@ -14,46 +16,39 @@ import (
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/generic"
|
||||
)
|
||||
|
||||
const ArchiveCatalogerName = "java-archive-cataloger"
|
||||
|
||||
// NewArchiveCataloger returns a new Java archive cataloger object for detecting packages with archives (jar, war, ear, par, sar, jpi, hpi, and native-image formats)
|
||||
func NewArchiveCataloger(cfg ArchiveCatalogerConfig) pkg.Cataloger {
|
||||
gap := newGenericArchiveParserAdapter(cfg)
|
||||
|
||||
files := generic.NewCataloger(ArchiveCatalogerName).
|
||||
c := generic.NewCataloger("java-archive-cataloger").
|
||||
WithParserByGlobs(gap.parseJavaArchive, archiveFormatGlobs...)
|
||||
|
||||
if cfg.IncludeIndexedArchives {
|
||||
// java archives wrapped within zip files
|
||||
gzp := newGenericZipWrappedJavaArchiveParser(cfg)
|
||||
files.WithParserByGlobs(gzp.parseZipWrappedJavaArchive, genericZipGlobs...)
|
||||
c.WithParserByGlobs(gzp.parseZipWrappedJavaArchive, genericZipGlobs...)
|
||||
}
|
||||
|
||||
if cfg.IncludeUnindexedArchives {
|
||||
// java archives wrapped within tar files
|
||||
gtp := newGenericTarWrappedJavaArchiveParser(cfg)
|
||||
files.WithParserByGlobs(gtp.parseTarWrappedJavaArchive, genericTarGlobs...)
|
||||
c.WithParserByGlobs(gtp.parseTarWrappedJavaArchive, genericTarGlobs...)
|
||||
}
|
||||
|
||||
return &archiveCataloger{cfg: cfg, files: files}
|
||||
return &archiveCataloger{cfg: cfg, extractingCataloger: c}
|
||||
}
|
||||
|
||||
// archiveCataloger describes java archives from whichever source this scan makes them available in.
|
||||
//
|
||||
// Which source that is turns on who owns extraction, and there are only two answers. Inside an
|
||||
// archive the archive cataloger task extracted, the resolver is that archive's contents and the
|
||||
// traversal names the file it came from, so the archive to describe is the one this resolver *is*.
|
||||
// Everywhere else the archives are files in the resolver, and the cataloger opens each itself.
|
||||
//
|
||||
// One cataloger rather than one per source, so a package carries the same FoundBy and answers to the
|
||||
// same --select-catalogers name however the scan was configured.
|
||||
// archiveCataloger catalogs java archives two ways. Inside an archive the archive cataloger task has
|
||||
// extracted, the resolver is that archive's contents and the traversal on the context names the
|
||||
// archive, so the archive to describe is the one the resolver holds. Everywhere else, archives are
|
||||
// files in the resolver that this cataloger opens itself.
|
||||
type archiveCataloger struct {
|
||||
cfg ArchiveCatalogerConfig
|
||||
files *generic.Cataloger
|
||||
cfg ArchiveCatalogerConfig
|
||||
extractingCataloger *generic.Cataloger
|
||||
}
|
||||
|
||||
func (c *archiveCataloger) Name() string {
|
||||
return ArchiveCatalogerName
|
||||
return c.extractingCataloger.Name()
|
||||
}
|
||||
|
||||
func (c *archiveCataloger) Catalog(ctx context.Context, resolver file.Resolver) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
@@ -61,42 +56,46 @@ func (c *archiveCataloger) Catalog(ctx context.Context, resolver file.Resolver)
|
||||
return c.catalogExtracted(ctx, trav, resolver)
|
||||
}
|
||||
|
||||
if c.cfg.NestedArchivesHandledExternally {
|
||||
// every archive in this scan comes back through the archive cataloger task with a traversal
|
||||
// naming it, and is described above. Opening archive files here as well would catalog each of
|
||||
// them a second time.
|
||||
if archive.NestedCatalogingEnabled(ctx) {
|
||||
// the archive cataloger task extracts every archive and runs this cataloger inside each; opening
|
||||
// them here too would catalog each twice
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
return c.files.Catalog(ctx, resolver)
|
||||
// process archives directly, using the original locally managed java archive extraction
|
||||
return c.extractingCataloger.Catalog(ctx, resolver)
|
||||
}
|
||||
|
||||
// catalogExtracted reports the java package of the archive whose contents this resolver holds, if it
|
||||
// is a java archive at all.
|
||||
//
|
||||
// It does not glob for the archives inside it: the task that extracted this one meets those on its
|
||||
// own, at the next nesting level. See syft/java-archive-nesting#single-owner-of-archive-recursion.
|
||||
// catalogExtracted reports the java package of the archive whose contents the resolver holds, if it
|
||||
// is a java archive. Archives nested inside it are left to the archive cataloger task.
|
||||
func (c *archiveCataloger) catalogExtracted(ctx context.Context, trav *archive.Traversal, resolver file.Resolver) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
if !isJavaArchiveName(trav.Location.Path()) {
|
||||
// the rule the globs apply when this cataloger opens files: a .zip is an archive but not a java
|
||||
// archive
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
src, err := newExtractedArchiveSource(trav, resolver)
|
||||
parser, err := newExtractedArchiveParser(trav, resolver, c.cfg)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
pkgs, relationships, err := newArchiveParser(src, false, c.cfg).parse(ctx, nil)
|
||||
pkgs, relationships, err := parser.parse(ctx, nil)
|
||||
if err != nil {
|
||||
// attributed to the archive rather than returned bare, as the generic cataloger attributes a
|
||||
// parser's failure to the file it was parsing, so the scan carries on
|
||||
// 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, nil
|
||||
}
|
||||
|
||||
// isJavaArchiveName reports whether the path matches the globs this cataloger opens files by.
|
||||
func isJavaArchiveName(path string) bool {
|
||||
for _, glob := range archiveFormatGlobs {
|
||||
if ok, _ := doublestar.Match(glob, path); ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// NewPomCataloger returns a cataloger capable of parsing dependencies from a pom.xml file.
|
||||
// Pom files list dependencies that maybe not be locally installed yet.
|
||||
func NewPomCataloger(cfg ArchiveCatalogerConfig) pkg.Cataloger {
|
||||
|
||||
@@ -33,15 +33,6 @@ type ArchiveCatalogerConfig struct {
|
||||
// ResolveTransitiveDependencies enables resolving transitive dependencies for java packages found within archives.
|
||||
// app-config: java.resolve-transitive-dependencies
|
||||
ResolveTransitiveDependencies bool `yaml:"resolve-transitive-dependencies" json:"resolve-transitive-dependencies" mapstructure:"resolve-transitive-dependencies"`
|
||||
|
||||
// NestedArchivesHandledExternally indicates that another mechanism owns recursion into nested
|
||||
// archives, so this cataloger must not unarchive them itself. It has no yaml/json/mapstructure
|
||||
// key, so no config file, flag or environment variable reaches it.
|
||||
//
|
||||
// Only syft.CreateSBOMConfig writes it, deriving it from the nested-archive cataloging depth so
|
||||
// the two cannot disagree. Callers building this config should leave it alone; the zero value
|
||||
// recurses, this cataloger's default.
|
||||
NestedArchivesHandledExternally bool `yaml:"-" json:"-" mapstructure:"-"`
|
||||
}
|
||||
|
||||
func DefaultArchiveCatalogerConfig() ArchiveCatalogerConfig {
|
||||
|
||||
@@ -3,7 +3,6 @@ package java
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
@@ -309,8 +308,7 @@ func pomParent(ctx context.Context, r *maven.Resolver, pom *maven.Project) *pkg.
|
||||
// The java-archive-cataloger identifies these archives from pom.properties, whose values are always
|
||||
// resolved, so skipping them here avoids phantom dependency packages.
|
||||
func isArchiveMetaPom(location file.Location) bool {
|
||||
p := filepath.ToSlash(location.Path())
|
||||
return strings.Contains(p, "META-INF/maven/")
|
||||
return strings.Contains(location.Path(), "META-INF/maven/")
|
||||
}
|
||||
|
||||
func cleanDescription(original string) (cleaned string) {
|
||||
|
||||
@@ -17,13 +17,13 @@ import (
|
||||
func readManifest(t *testing.T, ctx context.Context, parser *archiveParser) *pkg.JavaManifest {
|
||||
t.Helper()
|
||||
|
||||
matches := parser.entries.glob(false, manifestGlob)
|
||||
matches := parser.entries.glob(manifestGlob)
|
||||
require.Len(t, matches, 1)
|
||||
|
||||
contents, err := parser.entries.contents(ctx, matches...)
|
||||
require.NoError(t, err)
|
||||
|
||||
manifest, err := parseJavaManifest(parser.virtualPath, strings.NewReader(contents[matches[0]]))
|
||||
manifest, err := parseJavaManifest(parser.virtualPath(), strings.NewReader(contents[matches[0]]))
|
||||
require.NoError(t, err)
|
||||
|
||||
return manifest
|
||||
|
||||
Reference in New Issue
Block a user