diff --git a/syft/pkg/cataloger/java/parse_pom_xml.go b/syft/pkg/cataloger/java/parse_pom_xml.go index 572dfed8e..357b426a6 100644 --- a/syft/pkg/cataloger/java/parse_pom_xml.go +++ b/syft/pkg/cataloger/java/parse_pom_xml.go @@ -3,6 +3,7 @@ package java import ( "context" "errors" + "path/filepath" "strings" "github.com/anchore/syft/internal" @@ -40,6 +41,9 @@ func (p pomXMLCataloger) Catalog(ctx context.Context, fileResolver file.Resolver var poms []*maven.Project pomLocations := map[*maven.Project]file.Location{} for _, pomLocation := range locations { + if isArchiveMetaPom(pomLocation) { + continue + } pom, err := readPomFromLocation(fileResolver, pomLocation) if err != nil || pom == nil { log.WithFields("error", err, "pomLocation", pomLocation).Debug("error while reading pom") @@ -299,6 +303,20 @@ func pomParent(ctx context.Context, r *maven.Resolver, pom *maven.Project) *pkg. } } +// isArchiveMetaPom returns true if the pom.xml location is inside a META-INF/maven directory, +// indicating it is archive metadata embedded by the Maven build process rather than a project +// pom.xml. These embedded pom.xml files are verbatim copies of the original build POM with +// potentially unresolved ${property} references and dependency versions inherited from parent +// POMs that are not available inside the archive. The java-archive-cataloger already uses +// pom.properties (which always contains resolved values) to identify these archives, so the +// pom cataloger should skip them to avoid creating phantom dependency packages. +func isArchiveMetaPom(location file.Location) bool { + p := location.Path() + // normalize to forward slashes for consistent matching + p = filepath.ToSlash(p) + return strings.Contains(p, "META-INF/maven/") +} + func cleanDescription(original string) (cleaned string) { descriptionLines := strings.Split(original, "\n") for _, line := range descriptionLines { diff --git a/syft/pkg/cataloger/java/parse_pom_xml_test.go b/syft/pkg/cataloger/java/parse_pom_xml_test.go index 44dc5da05..94fde2542 100644 --- a/syft/pkg/cataloger/java/parse_pom_xml_test.go +++ b/syft/pkg/cataloger/java/parse_pom_xml_test.go @@ -690,6 +690,123 @@ func getCommonsTextExpectedPackages(resolved bool) expected { return expected{pkgs, relationships} } +func Test_isArchiveMetaPom(t *testing.T) { + tests := []struct { + name string + path string + expected bool + }{ + { + name: "META-INF maven pom", + path: "META-INF/maven/com.example/my-lib/pom.xml", + expected: true, + }, + { + name: "nested META-INF maven pom", + path: "some/path/META-INF/maven/org.apache/commons/pom.xml", + expected: true, + }, + { + name: "project pom.xml", + path: "pom.xml", + expected: false, + }, + { + name: "module pom.xml", + path: "submodule/pom.xml", + expected: false, + }, + { + name: "META-INF but not maven", + path: "META-INF/pom.xml", + expected: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + loc := file.NewLocation(test.path) + assert.Equal(t, test.expected, isArchiveMetaPom(loc)) + }) + } +} + +func Test_pomCatalogerSkipsMetaInfPoms(t *testing.T) { + // A directory that only contains a pom.xml under META-INF/maven/ should produce + // no packages, since these are archive metadata not project files. + cat := NewPomCataloger(ArchiveCatalogerConfig{ + ArchiveSearchConfig: cataloging.ArchiveSearchConfig{ + IncludeIndexedArchives: true, + IncludeUnindexedArchives: true, + }, + }) + + pkgtest.NewCatalogTester(). + FromDirectory(t, "testdata/pom/meta-inf-archive"). + Expects(nil, nil). + TestCataloger(t, cat) +} + +func Test_pomCatalogerSkipsMetaInfButKeepsProjectPom(t *testing.T) { + // When a directory contains both a project pom.xml and a META-INF/maven pom.xml, + // only the project pom.xml should be cataloged. The META-INF pom.xml and its + // dependencies should be ignored. + pomLocation := file.NewLocationSet(file.NewLocation("pom.xml")) + + myApp := pkg.Package{ + Name: "my-app", + Version: "2.0.0", + PURL: "pkg:maven/org.anchore/my-app@2.0.0", + Language: pkg.Java, + Type: pkg.JavaPkg, + FoundBy: pomCatalogerName, + Locations: pomLocation, + Metadata: pkg.JavaArchive{ + PomProject: &pkg.JavaPomProject{ + GroupID: "org.anchore", + ArtifactID: "my-app", + Version: "2.0.0", + }, + }, + } + finalizePackage(&myApp) + + guava := pkg.Package{ + Name: "guava", + Version: "31.1-jre", + PURL: "pkg:maven/com.google.guava/guava@31.1-jre", + Language: pkg.Java, + Type: pkg.JavaPkg, + FoundBy: pomCatalogerName, + Locations: pomLocation, + Metadata: pkg.JavaArchive{ + PomProperties: &pkg.JavaPomProperties{ + GroupID: "com.google.guava", + ArtifactID: "guava", + }, + }, + } + finalizePackage(&guava) + + expectedPkgs := []pkg.Package{myApp, guava} + expectedRelationships := []artifact.Relationship{ + { + From: guava, + To: myApp, + Type: artifact.DependencyOfRelationship, + }, + } + + cat := NewPomCataloger(ArchiveCatalogerConfig{ + ArchiveSearchConfig: cataloging.ArchiveSearchConfig{ + IncludeIndexedArchives: true, + IncludeUnindexedArchives: true, + }, + }) + + pkgtest.TestCataloger(t, "testdata/pom/mixed-meta-inf-and-project", cat, expectedPkgs, expectedRelationships) +} + func expectedTransientPackageData() expected { epl2 := pkg.NewLicenseSet(pkg.License{ Value: "Eclipse Public License v2.0", diff --git a/syft/pkg/cataloger/java/testdata/pom/meta-inf-archive/META-INF/maven/com.example/my-lib/pom.xml b/syft/pkg/cataloger/java/testdata/pom/meta-inf-archive/META-INF/maven/com.example/my-lib/pom.xml new file mode 100644 index 000000000..ff3d28844 --- /dev/null +++ b/syft/pkg/cataloger/java/testdata/pom/meta-inf-archive/META-INF/maven/com.example/my-lib/pom.xml @@ -0,0 +1,24 @@ + + + 4.0.0 + + com.example + my-lib + 1.0.0 + + + + org.phantom + phantom-dep + ${phantom.version} + + + org.testonly + test-dep + 2.0.0 + test + + + diff --git a/syft/pkg/cataloger/java/testdata/pom/mixed-meta-inf-and-project/META-INF/maven/com.example/embedded-lib/pom.xml b/syft/pkg/cataloger/java/testdata/pom/mixed-meta-inf-and-project/META-INF/maven/com.example/embedded-lib/pom.xml new file mode 100644 index 000000000..cf2ab1b72 --- /dev/null +++ b/syft/pkg/cataloger/java/testdata/pom/mixed-meta-inf-and-project/META-INF/maven/com.example/embedded-lib/pom.xml @@ -0,0 +1,18 @@ + + + 4.0.0 + + com.example + embedded-lib + 3.0.0 + + + + org.phantom + should-not-appear + ${unresolvable.version} + + + diff --git a/syft/pkg/cataloger/java/testdata/pom/mixed-meta-inf-and-project/pom.xml b/syft/pkg/cataloger/java/testdata/pom/mixed-meta-inf-and-project/pom.xml new file mode 100644 index 000000000..32b8e2426 --- /dev/null +++ b/syft/pkg/cataloger/java/testdata/pom/mixed-meta-inf-and-project/pom.xml @@ -0,0 +1,18 @@ + + + 4.0.0 + + org.anchore + my-app + 2.0.0 + + + + com.google.guava + guava + 31.1-jre + + +