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)) }) } }