From 41a1998d45351bea238ea611e79e0f333a985d4e Mon Sep 17 00:00:00 2001 From: Thomas Bechtold Date: Fri, 10 Apr 2026 14:49:31 +0200 Subject: [PATCH] fix(java): skip META-INF/maven pom.xml in pom cataloger The java-pom-cataloger was processing pom.xml files found inside META-INF/maven/ directories, which are archive metadata embedded by the Maven build process. These are verbatim copies of the original build POM containing unresolved ${property} references and dependency versions inherited from parent POMs that are unavailable inside the archive. This caused phantom packages with UNKNOWN versions to appear when recursive archive cataloging opened JARs and exposed their internal pom.xml files to the pom cataloger. The java-archive-cataloger already uses pom.properties (which always contains resolved values) to identify these archives, so the pom cataloger should skip META-INF/maven/ pom.xml files entirely. Signed-off-by: Thomas Bechtold --- syft/pkg/cataloger/java/parse_pom_xml.go | 18 +++ syft/pkg/cataloger/java/parse_pom_xml_test.go | 117 ++++++++++++++++++ .../META-INF/maven/com.example/my-lib/pom.xml | 24 ++++ .../maven/com.example/embedded-lib/pom.xml | 18 +++ .../pom/mixed-meta-inf-and-project/pom.xml | 18 +++ 5 files changed, 195 insertions(+) create mode 100644 syft/pkg/cataloger/java/testdata/pom/meta-inf-archive/META-INF/maven/com.example/my-lib/pom.xml create mode 100644 syft/pkg/cataloger/java/testdata/pom/mixed-meta-inf-and-project/META-INF/maven/com.example/embedded-lib/pom.xml create mode 100644 syft/pkg/cataloger/java/testdata/pom/mixed-meta-inf-and-project/pom.xml 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 + + +