From 409ac78f62a44de7cffa5edaef2f05e41708a727 Mon Sep 17 00:00:00 2001 From: texasich <101962694+texasich@users.noreply.github.com> Date: Wed, 23 Sep 2026 12:20:28 -0500 Subject: [PATCH] fix(java): resolve Groovy 4+ group IDs to org.apache.groovy (#5313) * fix(java): resolve Groovy 4+ group IDs to org.apache.groovy Groovy 4.0 moved from the Codehaus coordinates (org.codehaus.groovy) to the Apache Software Foundation (org.apache.groovy). JARs that ship no pom metadata fall through to the static artifact-to-group map, which carries the 3.x-era org.codehaus.groovy entries for the whole groovy family, so 4.x artifacts were attributed to an obsolete group even though their manifest identifies the Apache vendor. Where the static map attributes an artifact to the historical group, a major-version migration resolves groovy 4.x and newer to org.apache.groovy, while 3.x and earlier keep their historical group. The Eclipse-hosted groovy-eclipse-* artifacts, which share the prefix but kept their Codehaus coordinates, are excluded. Fixes #5311 Signed-off-by: texasich <101962694+texasich@users.noreply.github.com> * refactor(java): share version-aware groovy group ID lookup with CPE generation - move the artifact-to-group lookup behind `cpegenerate.ArtifactIDToGroupID(artifactID, version)` so the PURL and CPE paths agree; the exported mutable map is now unexported - version-dependent entries are expressed as a `groupIDMigration{before, after, atMajor}` (groovy moved to `org.apache.groovy` at 4.0); unconditional entries, including `groovy-eclipse-*` and apache-only 4.0+ modules, stay in the plain map - add apache-only modules (`groovy-ginq`, `groovy-contracts`, `groovy-callsite`, ...) that otherwise took the group from their per-module `Automatic-Module-Name` This changes CPEs for pom-less groovy 4+ jars (all real ones): vendors are now `apache` (e.g. `cpe:2.3:a:apache:groovy:4.0.33`) instead of `codehaus`, matching the PURL, jars with pom metadata, and NVD. Signed-off-by: Alex Goodman --------- Signed-off-by: texasich <101962694+texasich@users.noreply.github.com> Signed-off-by: Alex Goodman Co-authored-by: Alex Goodman --- .../internal/cpegenerate/generate_test.go | 18 ++ .../cataloger/internal/cpegenerate/java.go | 12 +- .../internal/cpegenerate/java_groupid_map.go | 262 +++++++++++------- .../cpegenerate/java_groupid_map_test.go | 49 ++++ .../internal/cpegenerate/java_test.go | 12 +- syft/pkg/cataloger/java/archive_parser.go | 8 +- syft/pkg/cataloger/java/package_url.go | 17 +- syft/pkg/cataloger/java/package_url_test.go | 55 +++- 8 files changed, 311 insertions(+), 122 deletions(-) create mode 100644 syft/pkg/cataloger/internal/cpegenerate/java_groupid_map_test.go diff --git a/syft/pkg/cataloger/internal/cpegenerate/generate_test.go b/syft/pkg/cataloger/internal/cpegenerate/generate_test.go index 704468780..d0ace066c 100644 --- a/syft/pkg/cataloger/internal/cpegenerate/generate_test.go +++ b/syft/pkg/cataloger/internal/cpegenerate/generate_test.go @@ -182,6 +182,24 @@ func TestGeneratePackageCPEs(t *testing.T) { "cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*", }, }, + { + // groovy moved to org.apache.groovy at 4.0 and real jars ship no pom metadata, so the known package + // list drives the vendor. See https://github.com/anchore/syft/issues/5311 + name: "groovy 4 module without pom metadata", + p: pkg.Package{ + Name: "groovy-json", + Version: "4.0.33", + FoundBy: "some-analyzer", + Language: pkg.Java, + Type: pkg.JavaPkg, + Metadata: pkg.JavaArchive{Manifest: &pkg.JavaManifest{}}, + }, + expected: []string{ + "cpe:2.3:a:apache:groovy-json:4.0.33:*:*:*:*:*:*:*", + "cpe:2.3:a:apache:groovy_json:4.0.33:*:*:*:*:*:*:*", + "cpe:2.3:a:apache:groovy:4.0.33:*:*:*:*:*:*:*", + }, + }, { name: "java language with groupID", p: pkg.Package{ diff --git a/syft/pkg/cataloger/internal/cpegenerate/java.go b/syft/pkg/cataloger/internal/cpegenerate/java.go index 5f7129562..69d9f85c2 100644 --- a/syft/pkg/cataloger/internal/cpegenerate/java.go +++ b/syft/pkg/cataloger/internal/cpegenerate/java.go @@ -190,16 +190,16 @@ func GroupIDsFromJavaPackage(p pkg.Package) (groupIDs []string) { return nil } - return GroupIDsFromJavaMetadata(p.Name, metadata) + return GroupIDsFromJavaMetadata(p.Name, p.Version, metadata) } // GroupIDsFromJavaMetadata returns the possible group IDs for a Java package // This function is similar to GroupIDFromJavaPackage, but returns all possible group IDs and is less strict // It is used as a way to generate possible candidates for CPE matching. -func GroupIDsFromJavaMetadata(pkgName string, metadata pkg.JavaArchive) (groupIDs []string) { +func GroupIDsFromJavaMetadata(pkgName, version string, metadata pkg.JavaArchive) (groupIDs []string) { groupIDs = append(groupIDs, groupIDsFromPomProperties(metadata.PomProperties)...) groupIDs = append(groupIDs, groupIDsFromPomProject(metadata.PomProject)...) - groupIDs = append(groupIDs, groupIDsFromJavaManifest(pkgName, metadata.Manifest)...) + groupIDs = append(groupIDs, groupIDsFromJavaManifest(pkgName, version, metadata.Manifest)...) return groupIDs } @@ -253,8 +253,8 @@ func addGroupIDsFromGroupIDsAndArtifactID(groupID, artifactID string) (groupIDs return groupIDs } -func groupIDsFromJavaManifest(pkgName string, manifest *pkg.JavaManifest) []string { - if groupID, ok := DefaultArtifactIDToGroupID[pkgName]; ok { +func groupIDsFromJavaManifest(pkgName, version string, manifest *pkg.JavaManifest) []string { + if groupID, ok := ArtifactIDToGroupID(pkgName, version); ok { return []string{groupID} } @@ -303,7 +303,7 @@ func GetManifestFieldGroupIDs(manifest *pkg.JavaManifest, fields []string) (grou func cleanGroupID(groupID string) string { groupID = strings.TrimSpace(strings.Split(removeOSCIDirectives(groupID), "#")[0]) - if replacement, ok := GroupIDCorrections[groupID]; ok { + if replacement, ok := groupIDCorrections[groupID]; ok { return replacement } return groupID diff --git a/syft/pkg/cataloger/internal/cpegenerate/java_groupid_map.go b/syft/pkg/cataloger/internal/cpegenerate/java_groupid_map.go index 9dcb71567..9cb50ffd6 100644 --- a/syft/pkg/cataloger/internal/cpegenerate/java_groupid_map.go +++ b/syft/pkg/cataloger/internal/cpegenerate/java_groupid_map.go @@ -1,10 +1,17 @@ package cpegenerate -var GroupIDCorrections = map[string]string{ +import ( + "strconv" + "strings" +) + +var groupIDCorrections = map[string]string{ "org.lz4.java": "org.lz4", } -var DefaultArtifactIDToGroupID = map[string]string{ +// defaultArtifactIDToGroupID holds artifacts that always map to the same group ID. Look up group IDs with +// ArtifactIDToGroupID instead of reading this map directly, since migratedArtifactIDToGroupID takes precedence. +var defaultArtifactIDToGroupID = map[string]string{ "ant": "org.apache.ant", "ant-antlr": "org.apache.ant", "ant-antunit": "org.apache.ant", @@ -1832,104 +1839,83 @@ var DefaultArtifactIDToGroupID = map[string]string{ "commons-jelly-tags-velocity": "commons-jelly", "commons-jelly-tags-xml": "commons-jelly", "commons-jelly-tags-xmlunit": "commons-jelly", - "groovy": "org.codehaus.groovy", - "groovy-all": "org.codehaus.groovy", - "groovy-all-jdk14": "org.codehaus.groovy", - "groovy-all-minimal": "org.codehaus.groovy", - "groovy-all-tests": "org.codehaus.groovy", - "groovy-ant": "org.codehaus.groovy", - "groovy-astbuilder": "org.codehaus.groovy", - "groovy-backports-compat23": "org.codehaus.groovy", - "groovy-binary": "org.codehaus.groovy", - "groovy-bom": "org.codehaus.groovy", - "groovy-bsf": "org.codehaus.groovy", - "groovy-cli-commons": "org.codehaus.groovy", - "groovy-cli-picocli": "org.codehaus.groovy", - "groovy-console": "org.codehaus.groovy", - "groovy-datetime": "org.codehaus.groovy", - "groovy-dateutil": "org.codehaus.groovy", - "groovy-docgenerator": "org.codehaus.groovy", - "groovy-eclipse-batch": "org.codehaus.groovy", - "groovy-eclipse-compiler": "org.codehaus.groovy", - "groovy-groovydoc": "org.codehaus.groovy", - "groovy-groovysh": "org.codehaus.groovy", - "groovy-jaxb": "org.codehaus.groovy", - "groovy-jdk14": "org.codehaus.groovy", - "groovy-jmx": "org.codehaus.groovy", - "groovy-json": "org.codehaus.groovy", - "groovy-json-direct": "org.codehaus.groovy", - "groovy-jsr223": "org.codehaus.groovy", - "groovy-macro": "org.codehaus.groovy", - "groovy-nio": "org.codehaus.groovy", - "groovy-servlet": "org.codehaus.groovy", - "groovy-sql": "org.codehaus.groovy", - "groovy-swing": "org.codehaus.groovy", - "groovy-templates": "org.codehaus.groovy", - "groovy-test": "org.codehaus.groovy", - "groovy-test-junit5": "org.codehaus.groovy", - "groovy-testng": "org.codehaus.groovy", - "groovy-tests-vm8": "org.codehaus.groovy", - "groovy-xml": "org.codehaus.groovy", - "groovy-xmlrpc": "org.codehaus.groovy", - "groovy-yaml": "org.codehaus.groovy", - "kafka-clients": "org.apache.kafka", - "kafka-examples": "org.apache.kafka", - "kafka-group-coordinator": "org.apache.kafka", - "kafka-hadoop-consumer": "org.apache.kafka", - "kafka-hadoop-producer": "org.apache.kafka", - "kafka-java-examples": "org.apache.kafka", - "kafka-log4j-appender": "org.apache.kafka", - "kafka-metadata": "org.apache.kafka", - "kafka-perf_2.10": "org.apache.kafka", - "kafka-perf_2.8.0": "org.apache.kafka", - "kafka-perf_2.8.2": "org.apache.kafka", - "kafka-perf_2.9.1": "org.apache.kafka", - "kafka-perf_2.9.2": "org.apache.kafka", - "kafka-raft": "org.apache.kafka", - "kafka-server-common": "org.apache.kafka", - "kafka-shell": "org.apache.kafka", - "kafka-storage": "org.apache.kafka", - "kafka-storage-api": "org.apache.kafka", - "kafka-streams": "org.apache.kafka", - "kafka-streams-examples": "org.apache.kafka", - "kafka-streams-scala_2.11": "org.apache.kafka", - "kafka-streams-scala_2.12": "org.apache.kafka", - "kafka-streams-scala_2.13": "org.apache.kafka", - "kafka-streams-test-utils": "org.apache.kafka", - "kafka-streams-upgrade-system-tests-0100": "org.apache.kafka", - "kafka-streams-upgrade-system-tests-0101": "org.apache.kafka", - "kafka-streams-upgrade-system-tests-0102": "org.apache.kafka", - "kafka-streams-upgrade-system-tests-0110": "org.apache.kafka", - "kafka-streams-upgrade-system-tests-10": "org.apache.kafka", - "kafka-streams-upgrade-system-tests-11": "org.apache.kafka", - "kafka-streams-upgrade-system-tests-20": "org.apache.kafka", - "kafka-streams-upgrade-system-tests-21": "org.apache.kafka", - "kafka-streams-upgrade-system-tests-22": "org.apache.kafka", - "kafka-streams-upgrade-system-tests-23": "org.apache.kafka", - "kafka-streams-upgrade-system-tests-24": "org.apache.kafka", - "kafka-streams-upgrade-system-tests-25": "org.apache.kafka", - "kafka-streams-upgrade-system-tests-26": "org.apache.kafka", - "kafka-streams-upgrade-system-tests-27": "org.apache.kafka", - "kafka-streams-upgrade-system-tests-28": "org.apache.kafka", - "kafka-streams-upgrade-system-tests-30": "org.apache.kafka", - "kafka-streams-upgrade-system-tests-31": "org.apache.kafka", - "kafka-streams-upgrade-system-tests-32": "org.apache.kafka", - "kafka-streams-upgrade-system-tests-33": "org.apache.kafka", - "kafka-streams-upgrade-system-tests-34": "org.apache.kafka", - "kafka-streams-upgrade-system-tests-35": "org.apache.kafka", - "kafka-tools": "org.apache.kafka", - "kafka-tools-api": "org.apache.kafka", - "kafka_2.10": "org.apache.kafka", - "kafka_2.11": "org.apache.kafka", - "kafka_2.12": "org.apache.kafka", - "kafka_2.13": "org.apache.kafka", - "kafka_2.8.0": "org.apache.kafka", - "kafka_2.8.2": "org.apache.kafka", - "kafka_2.9.1": "org.apache.kafka", - "kafka_2.9.2": "org.apache.kafka", + // the Eclipse-hosted groovy-eclipse-* artifacts stayed on codehaus past 4.0, and the org.apache.groovy modules + // below were added in 4.0+ and only exist under apache. See https://github.com/anchore/syft/issues/5311 + "groovy-eclipse-batch": "org.codehaus.groovy", + "groovy-eclipse-compiler": "org.codehaus.groovy", + "groovy-callsite": "org.apache.groovy", + "groovy-concurrent-java": "org.apache.groovy", + "groovy-contracts": "org.apache.groovy", + "groovy-csv": "org.apache.groovy", + "groovy-ginq": "org.apache.groovy", + "groovy-grape-ivy": "org.apache.groovy", + "groovy-grape-maven": "org.apache.groovy", + "groovy-http-builder": "org.apache.groovy", + "groovy-macro-library": "org.apache.groovy", + "groovy-markdown": "org.apache.groovy", + "groovy-reactor": "org.apache.groovy", + "groovy-rxjava": "org.apache.groovy", + "groovy-test-junit6": "org.apache.groovy", + "groovy-toml": "org.apache.groovy", + "groovy-typecheckers": "org.apache.groovy", + "kafka-clients": "org.apache.kafka", + "kafka-examples": "org.apache.kafka", + "kafka-group-coordinator": "org.apache.kafka", + "kafka-hadoop-consumer": "org.apache.kafka", + "kafka-hadoop-producer": "org.apache.kafka", + "kafka-java-examples": "org.apache.kafka", + "kafka-log4j-appender": "org.apache.kafka", + "kafka-metadata": "org.apache.kafka", + "kafka-perf_2.10": "org.apache.kafka", + "kafka-perf_2.8.0": "org.apache.kafka", + "kafka-perf_2.8.2": "org.apache.kafka", + "kafka-perf_2.9.1": "org.apache.kafka", + "kafka-perf_2.9.2": "org.apache.kafka", + "kafka-raft": "org.apache.kafka", + "kafka-server-common": "org.apache.kafka", + "kafka-shell": "org.apache.kafka", + "kafka-storage": "org.apache.kafka", + "kafka-storage-api": "org.apache.kafka", + "kafka-streams": "org.apache.kafka", + "kafka-streams-examples": "org.apache.kafka", + "kafka-streams-scala_2.11": "org.apache.kafka", + "kafka-streams-scala_2.12": "org.apache.kafka", + "kafka-streams-scala_2.13": "org.apache.kafka", + "kafka-streams-test-utils": "org.apache.kafka", + "kafka-streams-upgrade-system-tests-0100": "org.apache.kafka", + "kafka-streams-upgrade-system-tests-0101": "org.apache.kafka", + "kafka-streams-upgrade-system-tests-0102": "org.apache.kafka", + "kafka-streams-upgrade-system-tests-0110": "org.apache.kafka", + "kafka-streams-upgrade-system-tests-10": "org.apache.kafka", + "kafka-streams-upgrade-system-tests-11": "org.apache.kafka", + "kafka-streams-upgrade-system-tests-20": "org.apache.kafka", + "kafka-streams-upgrade-system-tests-21": "org.apache.kafka", + "kafka-streams-upgrade-system-tests-22": "org.apache.kafka", + "kafka-streams-upgrade-system-tests-23": "org.apache.kafka", + "kafka-streams-upgrade-system-tests-24": "org.apache.kafka", + "kafka-streams-upgrade-system-tests-25": "org.apache.kafka", + "kafka-streams-upgrade-system-tests-26": "org.apache.kafka", + "kafka-streams-upgrade-system-tests-27": "org.apache.kafka", + "kafka-streams-upgrade-system-tests-28": "org.apache.kafka", + "kafka-streams-upgrade-system-tests-30": "org.apache.kafka", + "kafka-streams-upgrade-system-tests-31": "org.apache.kafka", + "kafka-streams-upgrade-system-tests-32": "org.apache.kafka", + "kafka-streams-upgrade-system-tests-33": "org.apache.kafka", + "kafka-streams-upgrade-system-tests-34": "org.apache.kafka", + "kafka-streams-upgrade-system-tests-35": "org.apache.kafka", + "kafka-tools": "org.apache.kafka", + "kafka-tools-api": "org.apache.kafka", + "kafka_2.10": "org.apache.kafka", + "kafka_2.11": "org.apache.kafka", + "kafka_2.12": "org.apache.kafka", + "kafka_2.13": "org.apache.kafka", + "kafka_2.8.0": "org.apache.kafka", + "kafka_2.8.2": "org.apache.kafka", + "kafka_2.9.1": "org.apache.kafka", + "kafka_2.9.2": "org.apache.kafka", // legacy "Jackson 1.x" (aka "jackson-asl") artifacts predate the convention of embedding - // META-INF/maven/.../pom.properties in the jar, so groupIDFromKnownPackageList is the only + // META-INF/maven/.../pom.properties in the jar, so ArtifactIDToGroupID is the only // way to recover the correct group ID for jars built without that metadata (e.g. Ant-built // jars from before ~2014). Without this, the group ID falls back to the artifact name itself, // producing purls that do not match the vulnerability database's namespace. @@ -1940,3 +1926,83 @@ var DefaultArtifactIDToGroupID = map[string]string{ "jackson-xc": "org.codehaus.jackson", "jackson-smile": "org.codehaus.jackson", } + +// groupIDMigration describes an artifact that moved from one group ID to another at a major version. +type groupIDMigration struct { + before, after string + atMajor int +} + +// groovy moved from org.codehaus.groovy to org.apache.groovy at 4.0 +var groovyMigration = groupIDMigration{before: "org.codehaus.groovy", after: "org.apache.groovy", atMajor: 4} + +// migratedArtifactIDToGroupID holds artifacts whose group ID depends on the version. It takes precedence over +// defaultArtifactIDToGroupID; use ArtifactIDToGroupID rather than reading it directly. +var migratedArtifactIDToGroupID = map[string]groupIDMigration{ + // groovy artifacts published under both coordinates. Without these, jars that ship no pom metadata get a + // stale codehaus group or one guessed from the per-module Automatic-Module-Name (e.g. org.apache.groovy.json). + // See https://github.com/anchore/syft/issues/5311 + "groovy": groovyMigration, + "groovy-all": groovyMigration, + "groovy-all-jdk14": groovyMigration, + "groovy-all-minimal": groovyMigration, + "groovy-all-tests": groovyMigration, + "groovy-ant": groovyMigration, + "groovy-astbuilder": groovyMigration, + "groovy-backports-compat23": groovyMigration, + "groovy-binary": groovyMigration, + "groovy-bom": groovyMigration, + "groovy-bsf": groovyMigration, + "groovy-cli-commons": groovyMigration, + "groovy-cli-picocli": groovyMigration, + "groovy-console": groovyMigration, + "groovy-datetime": groovyMigration, + "groovy-dateutil": groovyMigration, + "groovy-docgenerator": groovyMigration, + "groovy-groovydoc": groovyMigration, + "groovy-groovysh": groovyMigration, + "groovy-jaxb": groovyMigration, + "groovy-jdk14": groovyMigration, + "groovy-jmx": groovyMigration, + "groovy-json": groovyMigration, + "groovy-json-direct": groovyMigration, + "groovy-jsr223": groovyMigration, + "groovy-macro": groovyMigration, + "groovy-nio": groovyMigration, + "groovy-servlet": groovyMigration, + "groovy-sql": groovyMigration, + "groovy-swing": groovyMigration, + "groovy-templates": groovyMigration, + "groovy-test": groovyMigration, + "groovy-test-junit5": groovyMigration, + "groovy-testng": groovyMigration, + "groovy-tests-vm8": groovyMigration, + "groovy-xml": groovyMigration, + "groovy-xmlrpc": groovyMigration, + "groovy-yaml": groovyMigration, +} + +// ArtifactIDToGroupID returns the known group ID for the given artifact ID at the given version. For migrated +// artifacts an empty or unparseable version resolves to the pre-migration group ID. +func ArtifactIDToGroupID(artifactID, version string) (string, bool) { + if m, ok := migratedArtifactIDToGroupID[artifactID]; ok { + if majorVersion(version) >= m.atMajor { + return m.after, true + } + return m.before, true + } + groupID, ok := defaultArtifactIDToGroupID[artifactID] + return groupID, ok +} + +// majorVersion returns the leading numeric component of a version (e.g. "4.0.33" and "4-SNAPSHOT" +// resolve to 4), or 0 when the version does not start with a number. +func majorVersion(version string) int { + leading, _, _ := strings.Cut(version, ".") + leading, _, _ = strings.Cut(leading, "-") + n, err := strconv.Atoi(leading) + if err != nil { + return 0 + } + return n +} diff --git a/syft/pkg/cataloger/internal/cpegenerate/java_groupid_map_test.go b/syft/pkg/cataloger/internal/cpegenerate/java_groupid_map_test.go new file mode 100644 index 000000000..8232f0e93 --- /dev/null +++ b/syft/pkg/cataloger/internal/cpegenerate/java_groupid_map_test.go @@ -0,0 +1,49 @@ +package cpegenerate + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestArtifactIDToGroupID(t *testing.T) { + tests := []struct { + name string + artifactID string + version string + want string + wantOK bool + }{ + {name: "unconditional entry", artifactID: "ant-antlr", version: "1.10.0", want: "org.apache.ant", wantOK: true}, + {name: "unknown artifact", artifactID: "not-a-real-artifact", version: "1.0.0"}, + {name: "groovy 4 migrates to apache", artifactID: "groovy", version: "4.0.33", want: "org.apache.groovy", wantOK: true}, + {name: "groovy 3 stays on codehaus", artifactID: "groovy", version: "3.0.22", want: "org.codehaus.groovy", wantOK: true}, + {name: "groovy without a version falls back to codehaus", artifactID: "groovy-json", want: "org.codehaus.groovy", wantOK: true}, + {name: "groovy-eclipse stays on codehaus at 4.x", artifactID: "groovy-eclipse-batch", version: "4.0.33-02", want: "org.codehaus.groovy", wantOK: true}, + {name: "apache-only groovy module", artifactID: "groovy-ginq", version: "4.0.33", want: "org.apache.groovy", wantOK: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, ok := ArtifactIDToGroupID(tt.artifactID, tt.version) + assert.Equal(t, tt.wantOK, ok) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_majorVersion(t *testing.T) { + tests := map[string]int{ + "4.0.33": 4, + "4.0.0-alpha-1": 4, + "4-SNAPSHOT": 4, + "4": 4, + "3.0.22": 3, + "v4.0.33": 0, + "": 0, + } + for version, expect := range tests { + t.Run(version, func(t *testing.T) { + assert.Equal(t, expect, majorVersion(version)) + }) + } +} diff --git a/syft/pkg/cataloger/internal/cpegenerate/java_test.go b/syft/pkg/cataloger/internal/cpegenerate/java_test.go index 6529598c7..6b9211a41 100644 --- a/syft/pkg/cataloger/internal/cpegenerate/java_test.go +++ b/syft/pkg/cataloger/internal/cpegenerate/java_test.go @@ -400,6 +400,16 @@ func Test_groupIDsFromJavaPackage(t *testing.T) { }, expects: nil, }, + { + // pom-less groovy 4 jars (all real ones) must resolve to the apache group, same as the purl + name: "groovy 4 without pom metadata", + pkg: pkg.Package{ + Name: "groovy", + Version: "4.0.33", + Metadata: pkg.JavaArchive{Manifest: &pkg.JavaManifest{}}, + }, + expects: []string{"org.apache.groovy"}, + }, { name: "no manifest or pom info", pkg: pkg.Package{ @@ -562,7 +572,7 @@ func Test_groupIDsFromJavaManifest(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - got := groupIDsFromJavaManifest(test.name, &test.manifest) + got := groupIDsFromJavaManifest(test.name, "", &test.manifest) require.Equal(t, test.expected, got) }) } diff --git a/syft/pkg/cataloger/java/archive_parser.go b/syft/pkg/cataloger/java/archive_parser.go index 0ec5b0054..d5608b89a 100644 --- a/syft/pkg/cataloger/java/archive_parser.go +++ b/syft/pkg/cataloger/java/archive_parser.go @@ -327,7 +327,7 @@ func (j *archiveParser) discoverNameVersionLicense(ctx context.Context, manifest // findLicenseFromJavaMetadata attempts to find license information from all available maven metadata properties and pom info func (j *archiveParser) findLicenseFromJavaMetadata(ctx context.Context, groupID, artifactID, version string, parsedPom *parsedPomProject, manifest *pkg.JavaManifest) []pkg.License { if groupID == "" { - if gID := groupIDFromJavaMetadata(artifactID, pkg.JavaArchive{Manifest: manifest}); gID != "" { + if gID := groupIDFromJavaMetadata(artifactID, version, pkg.JavaArchive{Manifest: manifest}); gID != "" { groupID = gID } } @@ -786,7 +786,7 @@ func newPackageFromMavenData(ctx context.Context, r *maven.Resolver, pomProperti vPathSuffix := "" groupID := "" if parentMetadata, ok := parentPkg.Metadata.(pkg.JavaArchive); ok { - groupID = groupIDFromJavaMetadata(parentPkg.Name, parentMetadata) + groupID = groupIDFromJavaMetadata(parentPkg.Name, parentPkg.Version, parentMetadata) } parentKey := fmt.Sprintf("%s:%s:%s", groupID, parentPkg.Name, parentPkg.Version) @@ -863,8 +863,8 @@ func packageIdentitiesMatch(p pkg.Package, parentPkg *pkg.Package) bool { } // try to determine identity with the metadata - groupID := groupIDFromJavaMetadata(p.Name, metadata) - parentGroupID := groupIDFromJavaMetadata(parentPkg.Name, parentMetadata) + groupID := groupIDFromJavaMetadata(p.Name, p.Version, metadata) + parentGroupID := groupIDFromJavaMetadata(parentPkg.Name, parentPkg.Version, parentMetadata) if uniquePkgKey(groupID, &p) == uniquePkgKey(parentGroupID, parentPkg) { return true } diff --git a/syft/pkg/cataloger/java/package_url.go b/syft/pkg/cataloger/java/package_url.go index e47e9a7e6..477b89a46 100644 --- a/syft/pkg/cataloger/java/package_url.go +++ b/syft/pkg/cataloger/java/package_url.go @@ -12,7 +12,7 @@ import ( func packageURL(name, version string, metadata pkg.JavaArchive) string { var groupID = name - if gID := groupIDFromJavaMetadata(name, metadata); gID != "" { + if gID := groupIDFromJavaMetadata(name, version, metadata); gID != "" { groupID = gID } @@ -30,9 +30,9 @@ func packageURL(name, version string, metadata pkg.JavaArchive) string { // The order of precedence is: // 1. The group ID from the POM properties // 2. The group ID from the POM project -// 3. The group ID from a select map of known group IDs +// 3. The group ID from a select map of known group IDs (see cpegenerate.ArtifactIDToGroupID) // 4. The group ID from the Java manifest -func groupIDFromJavaMetadata(pkgName string, metadata pkg.JavaArchive) (groupID string) { +func groupIDFromJavaMetadata(pkgName, version string, metadata pkg.JavaArchive) (groupID string) { if groupID = groupIDFromPomProperties(metadata.PomProperties); groupID != "" { return groupID } @@ -41,8 +41,8 @@ func groupIDFromJavaMetadata(pkgName string, metadata pkg.JavaArchive) (groupID return groupID } - if groupID = groupIDFromKnownPackageList(pkgName); groupID != "" { - return groupID + if gID, ok := cpegenerate.ArtifactIDToGroupID(pkgName, version); ok { + return gID } if groupID = groupIDFromJavaManifest(metadata.Manifest); groupID != "" { @@ -52,13 +52,6 @@ func groupIDFromJavaMetadata(pkgName string, metadata pkg.JavaArchive) (groupID return groupID } -func groupIDFromKnownPackageList(pkgName string) (groupID string) { - if groupID, ok := cpegenerate.DefaultArtifactIDToGroupID[pkgName]; ok { - return groupID - } - return groupID -} - func groupIDFromJavaManifest(manifest *pkg.JavaManifest) (groupID string) { if manifest == nil { return groupID diff --git a/syft/pkg/cataloger/java/package_url_test.go b/syft/pkg/cataloger/java/package_url_test.go index 5bb92fd00..a6fa7d973 100644 --- a/syft/pkg/cataloger/java/package_url_test.go +++ b/syft/pkg/cataloger/java/package_url_test.go @@ -132,6 +132,26 @@ func Test_packageURL(t *testing.T) { }, expect: "pkg:maven/parent/example-java-app-maven@0.1.0", }, + { + // regression for github.com/anchore/syft/issues/5311 + name: "groovy 4 without pom metadata", + pkg: pkg.Package{ + Name: "groovy", + Version: "4.0.33", + Language: pkg.Java, + Type: pkg.JavaPkg, + Metadata: pkg.JavaArchive{ + VirtualPath: "groovy-4.0.33.jar", + Manifest: &pkg.JavaManifest{ + Main: []pkg.KeyValue{ + {Key: "Bundle-SymbolicName", Value: "groovy"}, + {Key: "Automatic-Module-Name", Value: "org.apache.groovy.core"}, + }, + }, + }, + }, + expect: "pkg:maven/org.apache.groovy/groovy@4.0.33", + }, } for _, tt := range tests { t.Run(tt.expect, func(t *testing.T) { @@ -144,6 +164,7 @@ func Test_groupIDFromJavaMetadata(t *testing.T) { tests := []struct { name string pkgName string + version string metadata pkg.JavaArchive expect string }{ @@ -218,6 +239,38 @@ func Test_groupIDFromJavaMetadata(t *testing.T) { metadata: pkg.JavaArchive{}, expect: "org.apache.derby", }, + { + // regression for github.com/anchore/syft/issues/5311: groovy 4+ moved to org.apache.groovy, but + // jars without pom metadata fall through to the known package list, which keeps the codehaus-era + // coordinates. The map must also beat the per-module Automatic-Module-Name in the manifest. + name: "known package list groovy 4 beats the manifest", + pkgName: "groovy", + version: "4.0.33", + metadata: pkg.JavaArchive{ + Manifest: &pkg.JavaManifest{ + Main: []pkg.KeyValue{ + {Key: "Bundle-SymbolicName", Value: "groovy"}, + {Key: "Automatic-Module-Name", Value: "org.apache.groovy.core"}, + }, + }, + }, + expect: "org.apache.groovy", + }, + { + // modules only published under the apache coordinates would otherwise take the group ID + // from the per-module Automatic-Module-Name (org.apache.groovy.ginq) + name: "known package list apache-only groovy module", + pkgName: "groovy-ginq", + version: "4.0.33", + metadata: pkg.JavaArchive{ + Manifest: &pkg.JavaManifest{ + Main: []pkg.KeyValue{ + {Key: "Automatic-Module-Name", Value: "org.apache.groovy.ginq"}, + }, + }, + }, + expect: "org.apache.groovy", + }, { name: "java manifest", metadata: pkg.JavaArchive{ @@ -241,7 +294,7 @@ func Test_groupIDFromJavaMetadata(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.expect, groupIDFromJavaMetadata(tt.pkgName, tt.metadata)) + assert.Equal(t, tt.expect, groupIDFromJavaMetadata(tt.pkgName, tt.version, tt.metadata)) }) } }