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 <thomasbechtold@jpberlin.de>
This commit is contained in:
Thomas Bechtold
2026-04-27 08:49:00 +02:00
parent bb7dbf2ff8
commit 41a1998d45
5 changed files with 195 additions and 0 deletions
+18
View File
@@ -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 {
@@ -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",
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>my-lib</artifactId>
<version>1.0.0</version>
<dependencies>
<dependency>
<groupId>org.phantom</groupId>
<artifactId>phantom-dep</artifactId>
<version>${phantom.version}</version>
</dependency>
<dependency>
<groupId>org.testonly</groupId>
<artifactId>test-dep</artifactId>
<version>2.0.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>embedded-lib</artifactId>
<version>3.0.0</version>
<dependencies>
<dependency>
<groupId>org.phantom</groupId>
<artifactId>should-not-appear</artifactId>
<version>${unresolvable.version}</version>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.anchore</groupId>
<artifactId>my-app</artifactId>
<version>2.0.0</version>
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
</dependency>
</dependencies>
</project>