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
+
+
+