diff --git a/src/main/java/io/github/guacsec/trustifyda/providers/JavaMavenProvider.java b/src/main/java/io/github/guacsec/trustifyda/providers/JavaMavenProvider.java index da01e03b..7b798b33 100644 --- a/src/main/java/io/github/guacsec/trustifyda/providers/JavaMavenProvider.java +++ b/src/main/java/io/github/guacsec/trustifyda/providers/JavaMavenProvider.java @@ -36,6 +36,7 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -228,6 +229,7 @@ private Content generateSbomFromEffectivePom() throws IOException { .filter(DependencyAggregator::isTestDependency) .collect(Collectors.toSet()); var deps = getDependencies(tmpEffPom); + deps = resolveVersionRanges(deps); var sbom = SbomFactory.newInstance().addRoot(getRoot(tmpEffPom), readLicenseFromManifest()); deps.stream() .filter(dep -> !testsDeps.contains(dep)) @@ -239,6 +241,82 @@ private Content generateSbomFromEffectivePom() throws IOException { return new Content(sbom.getAsJsonString().getBytes(), Api.CYCLONEDX_MEDIA_TYPE); } + /** + * Checks whether the given version string is a Maven version range expression (starts with '[' or + * '('). + */ + private static boolean isVersionRange(String version) { + if (version == null || version.isEmpty()) return false; + char first = version.charAt(0); + return first == '[' || first == '('; + } + + /** + * Resolves Maven version ranges by running the dependency tree plugin and replacing range + * expressions with concrete resolved versions. If no version ranges are present, the original + * list is returned unchanged. On failure, the original list is returned with a warning logged. + */ + private List resolveVersionRanges(List deps) + throws IOException { + // Short-circuit: if no dep has a version range, return unchanged + boolean hasRanges = deps.stream().anyMatch(d -> isVersionRange(d.version)); + if (!hasRanges) { + return deps; + } + + Path tmpFile = Files.createTempFile("TRUSTIFY_DA_range_tree_", ".txt"); + try { + var cmd = + buildMvnCommandArgs( + "org.apache.maven.plugins:maven-dependency-plugin:3.6.0:tree", + "-Dscope=compile", + "-DoutputType=text", + String.format("-DoutputFile=%s", tmpFile.toString()), + "-f", + manifestPath.toString(), + "--batch-mode", + "-q"); + Operations.runProcess(manifestPath.getParent(), cmd.toArray(String[]::new), getMvnExecEnvs()); + + // Read the dependency tree output and build a lookup map of resolved versions + List lines = Files.readAllLines(tmpFile); + Map resolvedVersions = new HashMap<>(); + for (String line : lines) { + if (getDepth(line) == 1) { + DependencyAggregator resolved = parseDep(line); + resolvedVersions.put(resolved.groupId + ":" + resolved.artifactId, resolved.version); + } + } + + // Replace version ranges with resolved concrete versions + for (DependencyAggregator dep : deps) { + if (isVersionRange(dep.version)) { + String key = dep.groupId + ":" + dep.artifactId; + String resolved = resolvedVersions.get(key); + if (resolved != null) { + if (debugLoggingIsNeeded()) { + log.info( + String.format( + "Resolved version range for %s: %s -> %s", key, dep.version, resolved)); + } + dep.version = resolved; + } + } + } + } catch (Exception e) { + log.warning( + String.format( + "Failed to resolve version ranges via dependency tree, " + + "using original versions: %s", + e.getMessage())); + return deps; + } finally { + Files.deleteIfExists(tmpFile); + } + + return deps; + } + private PackageURL getRoot(final Path manifestPath) throws IOException { XMLStreamReader reader = null; try { diff --git a/src/test/java/io/github/guacsec/trustifyda/providers/Java_Maven_Provider_Test.java b/src/test/java/io/github/guacsec/trustifyda/providers/Java_Maven_Provider_Test.java index ed70a06d..22e21060 100644 --- a/src/test/java/io/github/guacsec/trustifyda/providers/Java_Maven_Provider_Test.java +++ b/src/test/java/io/github/guacsec/trustifyda/providers/Java_Maven_Provider_Test.java @@ -56,7 +56,8 @@ static Stream testFolders() { "deps_with_ignore_on_version", "deps_with_ignore_on_wrong", "deps_with_no_ignore", - "pom_deps_with_no_ignore_common_paths"); + "pom_deps_with_no_ignore_common_paths", + "deps_with_version_range"); } @ParameterizedTest @@ -143,12 +144,29 @@ void test_the_provideComponent(String testFolder) throws IOException { getClass(), String.format("tst_manifests/maven/%s/effectivePom.xml", testFolder))) { effectivePom = new String(is.readAllBytes()); } + + String depTree; + try (var is = + getResourceAsStreamDecision( + getClass(), String.format("tst_manifests/maven/%s/depTree.txt", testFolder))) { + depTree = new String(is.readAllBytes()); + } + try (MockedStatic mockedOperations = mockStatic(Operations.class)) { mockedOperations .when(() -> Operations.runProcess(any(), any(), any())) .thenAnswer( - invocationOnMock -> - getOutputFileAndOverwriteItWithMock(effectivePom, invocationOnMock, "-Doutput")); + invocationOnMock -> { + String result = + getOutputFileAndOverwriteItWithMock( + effectivePom, invocationOnMock, "-Doutput="); + if (result == null) { + result = + getOutputFileAndOverwriteItWithMock( + depTree, invocationOnMock, "-DoutputFile"); + } + return result; + }); // Mock Operations.getCustomPathOrElse to return "mvn" mockedOperations.when(() -> Operations.getCustomPathOrElse(anyString())).thenReturn("mvn"); mockedOperations @@ -189,12 +207,29 @@ void test_the_provideComponent_With_Path(String testFolder) throws IOException { getClass(), String.format("tst_manifests/maven/%s/effectivePom.xml", testFolder))) { effectivePom = new String(is.readAllBytes()); } + + String depTree; + try (var is = + getResourceAsStreamDecision( + getClass(), String.format("tst_manifests/maven/%s/depTree.txt", testFolder))) { + depTree = new String(is.readAllBytes()); + } + try (MockedStatic mockedOperations = mockStatic(Operations.class)) { mockedOperations .when(() -> Operations.runProcess(any(), any(), any())) .thenAnswer( - invocationOnMock -> - getOutputFileAndOverwriteItWithMock(effectivePom, invocationOnMock, "-Doutput")); + invocationOnMock -> { + String result = + getOutputFileAndOverwriteItWithMock( + effectivePom, invocationOnMock, "-Doutput="); + if (result == null) { + result = + getOutputFileAndOverwriteItWithMock( + depTree, invocationOnMock, "-DoutputFile"); + } + return result; + }); // Mock Operations.getCustomPathOrElse to return "mvn" mockedOperations.when(() -> Operations.getCustomPathOrElse(anyString())).thenReturn("mvn"); mockedOperations diff --git a/src/test/resources/tst_manifests/maven/deps_with_version_range/depTree.txt b/src/test/resources/tst_manifests/maven/deps_with_version_range/depTree.txt new file mode 100644 index 00000000..761020ff --- /dev/null +++ b/src/test/resources/tst_manifests/maven/deps_with_version_range/depTree.txt @@ -0,0 +1,3 @@ +pom-with-deps-version-range:pom-with-version-range-for-tests:jar:0.0.1 ++- log4j:log4j:jar:1.2.17:compile +\- org.xerial.snappy:snappy-java:jar:1.1.10.0:compile diff --git a/src/test/resources/tst_manifests/maven/deps_with_version_range/effectivePom.xml b/src/test/resources/tst_manifests/maven/deps_with_version_range/effectivePom.xml new file mode 100644 index 00000000..15f020c9 --- /dev/null +++ b/src/test/resources/tst_manifests/maven/deps_with_version_range/effectivePom.xml @@ -0,0 +1,252 @@ + + + + + + + + + + + + + + + 4.0.0 + pom-with-deps-version-range + pom-with-version-range-for-tests + 0.0.1 + + + log4j + log4j + [1.2.17,1.3.0) + compile + + + org.xerial.snappy + snappy-java + 1.1.10.0 + compile + + + + + + false + + central + Central Repository + https://repo.maven.apache.org/maven2 + + + + + + never + + + false + + central + Central Repository + https://repo.maven.apache.org/maven2 + + + + /home/zgrinber/git/trustify-da-java-client/src/test/resources/tst_manifests/maven/deps_with_version_range/src/main/java + /home/zgrinber/git/trustify-da-java-client/src/test/resources/tst_manifests/maven/deps_with_version_range/src/main/scripts + /home/zgrinber/git/trustify-da-java-client/src/test/resources/tst_manifests/maven/deps_with_version_range/src/test/java + /home/zgrinber/git/trustify-da-java-client/src/test/resources/tst_manifests/maven/deps_with_version_range/target/classes + /home/zgrinber/git/trustify-da-java-client/src/test/resources/tst_manifests/maven/deps_with_version_range/target/test-classes + + + /home/zgrinber/git/trustify-da-java-client/src/test/resources/tst_manifests/maven/deps_with_version_range/src/main/resources + + + + + /home/zgrinber/git/trustify-da-java-client/src/test/resources/tst_manifests/maven/deps_with_version_range/src/test/resources + + + /home/zgrinber/git/trustify-da-java-client/src/test/resources/tst_manifests/maven/deps_with_version_range/target + pom-with-version-range-for-tests-0.0.1 + + + + maven-antrun-plugin + 1.3 + + + maven-assembly-plugin + 2.2-beta-5 + + + maven-dependency-plugin + 2.8 + + + maven-release-plugin + 2.5.3 + + + + + + maven-clean-plugin + 2.5 + + + default-clean + clean + + clean + + + + + + maven-resources-plugin + 2.6 + + + default-testResources + process-test-resources + + testResources + + + + default-resources + process-resources + + resources + + + + + + maven-jar-plugin + 2.4 + + + default-jar + package + + jar + + + + + + maven-compiler-plugin + 3.1 + + + default-compile + compile + + compile + + + + default-testCompile + test-compile + + testCompile + + + + + + maven-surefire-plugin + 2.12.4 + + + default-test + test + + test + + + + + + maven-install-plugin + 2.4 + + + default-install + install + + install + + + + + + maven-deploy-plugin + 2.7 + + + default-deploy + deploy + + deploy + + + + + + maven-site-plugin + 3.3 + + + default-site + site + + site + + + /home/zgrinber/git/trustify-da-java-client/src/test/resources/tst_manifests/maven/deps_with_version_range/target/site + + + org.apache.maven.plugins + maven-project-info-reports-plugin + + + + + + default-deploy + site-deploy + + deploy + + + /home/zgrinber/git/trustify-da-java-client/src/test/resources/tst_manifests/maven/deps_with_version_range/target/site + + + org.apache.maven.plugins + maven-project-info-reports-plugin + + + + + + + /home/zgrinber/git/trustify-da-java-client/src/test/resources/tst_manifests/maven/deps_with_version_range/target/site + + + org.apache.maven.plugins + maven-project-info-reports-plugin + + + + + + + + /home/zgrinber/git/trustify-da-java-client/src/test/resources/tst_manifests/maven/deps_with_version_range/target/site + + diff --git a/src/test/resources/tst_manifests/maven/deps_with_version_range/expected_component_sbom.json b/src/test/resources/tst_manifests/maven/deps_with_version_range/expected_component_sbom.json new file mode 100644 index 00000000..753f4abe --- /dev/null +++ b/src/test/resources/tst_manifests/maven/deps_with_version_range/expected_component_sbom.json @@ -0,0 +1,51 @@ +{ + "bomFormat" : "CycloneDX", + "specVersion" : "1.4", + "version" : 1, + "metadata" : { + "timestamp" : "2025-04-09T12:15:45Z", + "component" : { + "type" : "application", + "bom-ref" : "pkg:maven/pom-with-deps-version-range/pom-with-version-range-for-tests@0.0.1", + "group" : "pom-with-deps-version-range", + "name" : "pom-with-version-range-for-tests", + "version" : "0.0.1", + "purl" : "pkg:maven/pom-with-deps-version-range/pom-with-version-range-for-tests@0.0.1" + } + }, + "components" : [ + { + "type" : "library", + "bom-ref" : "pkg:maven/log4j/log4j@1.2.17", + "group" : "log4j", + "name" : "log4j", + "version" : "1.2.17", + "purl" : "pkg:maven/log4j/log4j@1.2.17" + }, + { + "type" : "library", + "bom-ref" : "pkg:maven/org.xerial.snappy/snappy-java@1.1.10.0", + "group" : "org.xerial.snappy", + "name" : "snappy-java", + "version" : "1.1.10.0", + "purl" : "pkg:maven/org.xerial.snappy/snappy-java@1.1.10.0" + } + ], + "dependencies" : [ + { + "ref" : "pkg:maven/pom-with-deps-version-range/pom-with-version-range-for-tests@0.0.1", + "dependsOn" : [ + "pkg:maven/log4j/log4j@1.2.17", + "pkg:maven/org.xerial.snappy/snappy-java@1.1.10.0" + ] + }, + { + "ref" : "pkg:maven/log4j/log4j@1.2.17", + "dependsOn" : [ ] + }, + { + "ref" : "pkg:maven/org.xerial.snappy/snappy-java@1.1.10.0", + "dependsOn" : [ ] + } + ] +} \ No newline at end of file diff --git a/src/test/resources/tst_manifests/maven/deps_with_version_range/expected_stack_sbom.json b/src/test/resources/tst_manifests/maven/deps_with_version_range/expected_stack_sbom.json new file mode 100644 index 00000000..fc3489fb --- /dev/null +++ b/src/test/resources/tst_manifests/maven/deps_with_version_range/expected_stack_sbom.json @@ -0,0 +1,51 @@ +{ + "bomFormat" : "CycloneDX", + "specVersion" : "1.4", + "version" : 1, + "metadata" : { + "timestamp" : "2025-04-09T12:14:35Z", + "component" : { + "type" : "application", + "bom-ref" : "pkg:maven/pom-with-deps-version-range/pom-with-version-range-for-tests@0.0.1", + "group" : "pom-with-deps-version-range", + "name" : "pom-with-version-range-for-tests", + "version" : "0.0.1", + "purl" : "pkg:maven/pom-with-deps-version-range/pom-with-version-range-for-tests@0.0.1" + } + }, + "components" : [ + { + "type" : "library", + "bom-ref" : "pkg:maven/log4j/log4j@1.2.17", + "group" : "log4j", + "name" : "log4j", + "version" : "1.2.17", + "purl" : "pkg:maven/log4j/log4j@1.2.17" + }, + { + "type" : "library", + "bom-ref" : "pkg:maven/org.xerial.snappy/snappy-java@1.1.10.0", + "group" : "org.xerial.snappy", + "name" : "snappy-java", + "version" : "1.1.10.0", + "purl" : "pkg:maven/org.xerial.snappy/snappy-java@1.1.10.0" + } + ], + "dependencies" : [ + { + "ref" : "pkg:maven/pom-with-deps-version-range/pom-with-version-range-for-tests@0.0.1", + "dependsOn" : [ + "pkg:maven/log4j/log4j@1.2.17", + "pkg:maven/org.xerial.snappy/snappy-java@1.1.10.0" + ] + }, + { + "ref" : "pkg:maven/log4j/log4j@1.2.17", + "dependsOn" : [ ] + }, + { + "ref" : "pkg:maven/org.xerial.snappy/snappy-java@1.1.10.0", + "dependsOn" : [ ] + } + ] +} \ No newline at end of file diff --git a/src/test/resources/tst_manifests/maven/deps_with_version_range/pom.xml b/src/test/resources/tst_manifests/maven/deps_with_version_range/pom.xml new file mode 100644 index 00000000..cb277e06 --- /dev/null +++ b/src/test/resources/tst_manifests/maven/deps_with_version_range/pom.xml @@ -0,0 +1,22 @@ + + + 4.0.0 + + pom-with-deps-version-range + pom-with-version-range-for-tests + 0.0.1 + + + + log4j + log4j + [1.2.17,1.3.0) + + + org.xerial.snappy + snappy-java + 1.1.10.0 + + +