Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,12 @@
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;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import javax.xml.stream.XMLInputFactory;
Expand All @@ -59,6 +61,8 @@ public final class JavaMavenProvider extends BaseJavaProvider {
private final String mvnExecutable;
private static final String MVN = Operations.isWindows() ? "mvn.cmd" : "mvn";
private static final String ARG_VERSION = "-v";
private static final String DEP_TREE_PLUGIN =
"org.apache.maven.plugins:maven-dependency-plugin:3.6.0:tree";

public JavaMavenProvider(Path manifest) {
super(Type.MAVEN, manifest);
Expand Down Expand Up @@ -135,7 +139,7 @@ public Content provideStack() throws IOException {
var mvnTreeCmdArgs =
new ArrayList<>(
List.of(
"org.apache.maven.plugins:maven-dependency-plugin:3.6.0:tree",
DEP_TREE_PLUGIN,
"-Dscope=compile",
"-Dverbose",
"-DoutputType=text",
Expand Down Expand Up @@ -228,6 +232,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))
Expand All @@ -239,6 +244,87 @@ 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<DependencyAggregator> resolveVersionRanges(List<DependencyAggregator> 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 {
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
var cmd =
buildMvnCommandArgs(
DEP_TREE_PLUGIN,
"-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<String> lines = Files.readAllLines(tmpFile);
Map<String, String> 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.log(
Level.WARNING,
String.format(
"Failed to resolve version ranges via dependency tree, "
+ "using original versions: %s",
e.getMessage()),
e);
return deps;
} finally {
try {
Files.deleteIfExists(tmpFile);
} catch (IOException ignored) {
}
}

return deps;
}

private PackageURL getRoot(final Path manifestPath) throws IOException {
XMLStreamReader reader = null;
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import java.util.Arrays;
import java.util.Optional;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
Expand All @@ -56,7 +57,8 @@ static Stream<String> 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
Expand Down Expand Up @@ -143,12 +145,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<Operations> mockedOperations = mockStatic(Operations.class)) {
mockedOperations
.when(() -> Operations.runProcess(any(), any(), any()))
.thenAnswer(
invocationOnMock ->
getOutputFileAndOverwriteItWithMock(effectivePom, invocationOnMock, "-Doutput"));
invocationOnMock -> {
Comment thread
Strum355 marked this conversation as resolved.
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
Expand Down Expand Up @@ -189,12 +208,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<Operations> 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
Expand All @@ -209,6 +245,48 @@ void test_the_provideComponent_With_Path(String testFolder) throws IOException {
}
}

@Test
void test_provideComponent_fallsBack_when_depTree_fails() throws IOException {
String testFolder = "deps_with_version_range";

var targetPom = resolveFile(String.format("tst_manifests/maven/%s/pom.xml", testFolder));

String effectivePom;
try (var is =
getResourceAsStreamDecision(
getClass(), String.format("tst_manifests/maven/%s/effectivePom.xml", testFolder))) {
effectivePom = new String(is.readAllBytes());
}

try (MockedStatic<Operations> mockedOperations = mockStatic(Operations.class)) {
mockedOperations
.when(() -> Operations.runProcess(any(), any(), any()))
.thenAnswer(
invocationOnMock -> {
String result =
getOutputFileAndOverwriteItWithMock(
effectivePom, invocationOnMock, "-Doutput=");
if (result == null) {
throw new IOException("Simulated dependency:tree failure");
}
return result;
});
mockedOperations.when(() -> Operations.getCustomPathOrElse(anyString())).thenReturn("mvn");
mockedOperations
.when(() -> Operations.getExecutable(anyString(), anyString()))
.thenReturn("mvn");

var content = new JavaMavenProvider(targetPom).provideComponent();

assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE);
String sbom = new String(content.buffer);
assertThat(sbom).contains("log4j");
assertThat(sbom).contains("snappy-java");
// Version range should remain unresolved since dep tree failed
assertThat(sbom).contains("[1.2.17,1.3.0)");
}
}

private String dropIgnored(String s) {
return s.replaceAll("\\s+", "").replaceAll("\"timestamp\":\"[a-zA-Z0-9\\-\\:]+\",", "");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading