Skip to content

Commit 742ce99

Browse files
authored
Merge branch 'main' into TC-4707
2 parents fec2474 + 1fba9c2 commit 742ce99

10 files changed

Lines changed: 556 additions & 13 deletions

File tree

.github/workflows/pr.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ jobs:
4545
- name: Install pnpm
4646
run: npm install -g pnpm
4747
- name: Install uv
48-
uses: astral-sh/setup-uv@v8.1.0
48+
uses: astral-sh/setup-uv@v8.2.0
4949
- name: get Python location
5050
id: python-location
5151
run: |

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ jobs:
9696

9797
- name: Install uv
9898
# Pin setup-uv to a specific version; update as needed when upgrading uv or CI tooling
99-
uses: astral-sh/setup-uv@v8.1.0
99+
uses: astral-sh/setup-uv@v8.2.0
100100

101101
- name: Compute Maven profiles
102102
id: compute_profiles

pom.xml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@
1919
<maven.compiler.release>21</maven.compiler.release>
2020
<!-- Dependencies -->
2121
<trustify-da-api-model.version>2.0.8</trustify-da-api-model.version>
22-
<jackson.version>2.21.3</jackson.version>
23-
<jackson-annotations.version>2.21</jackson-annotations.version>
22+
<jackson.version>2.22.0</jackson.version>
23+
<jackson-annotations.version>2.22</jackson-annotations.version>
2424
<jakarta.annotation-api.version>3.0.0</jakarta.annotation-api.version>
2525
<jakarta.mail.version>2.0.5</jakarta.mail.version>
2626
<cyclonedx.version>12.2.0</cyclonedx.version>
@@ -33,7 +33,7 @@
3333
<!-- Plugins -->
3434
<maven-clean-plugin.version>3.5.0</maven-clean-plugin.version>
3535
<maven-compiler-plugin.version>3.15.0</maven-compiler-plugin.version>
36-
<maven-dependency-plugin.version>3.10.0</maven-dependency-plugin.version>
36+
<maven-dependency-plugin.version>3.11.0</maven-dependency-plugin.version>
3737
<maven-deploy-plugin.version>3.1.4</maven-deploy-plugin.version>
3838
<maven-enforcer-plugin.version>3.6.3</maven-enforcer-plugin.version>
3939
<maven-gpg-plugin.version>3.2.8</maven-gpg-plugin.version>
@@ -53,10 +53,10 @@
5353
<jacoco-maven-plugin.version>0.8.14</jacoco-maven-plugin.version>
5454
<junit-platform-maven-plugin.version>1.1.8</junit-platform-maven-plugin.version>
5555
<license-maven-plugin.version>5.0.0</license-maven-plugin.version>
56-
<pitest-maven.version>1.25.1</pitest-maven.version>
56+
<pitest-maven.version>1.25.4</pitest-maven.version>
5757
<pitest-junit5-plugin.version>1.2.3</pitest-junit5-plugin.version>
5858
<versions-maven-plugin.version>2.21.0</versions-maven-plugin.version>
59-
<maven-failsafe-plugin.version>3.5.5</maven-failsafe-plugin.version>
59+
<maven-failsafe-plugin.version>3.5.6</maven-failsafe-plugin.version>
6060
<spotless-maven-plugin.version>3.6.0</spotless-maven-plugin.version>
6161
<central-publishing-maven-plugin.version>0.10.0</central-publishing-maven-plugin.version>
6262
</properties>

src/main/java/io/github/guacsec/trustifyda/providers/JavaMavenProvider.java

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,12 @@
3636
import java.nio.file.Paths;
3737
import java.util.ArrayList;
3838
import java.util.Collections;
39+
import java.util.HashMap;
3940
import java.util.List;
4041
import java.util.Map;
4142
import java.util.Objects;
4243
import java.util.Optional;
44+
import java.util.logging.Level;
4345
import java.util.logging.Logger;
4446
import java.util.stream.Collectors;
4547
import javax.xml.stream.XMLInputFactory;
@@ -59,6 +61,8 @@ public final class JavaMavenProvider extends BaseJavaProvider {
5961
private final String mvnExecutable;
6062
private static final String MVN = Operations.isWindows() ? "mvn.cmd" : "mvn";
6163
private static final String ARG_VERSION = "-v";
64+
private static final String DEP_TREE_PLUGIN =
65+
"org.apache.maven.plugins:maven-dependency-plugin:3.6.0:tree";
6266

6367
public JavaMavenProvider(Path manifest) {
6468
super(Type.MAVEN, manifest);
@@ -135,7 +139,7 @@ public Content provideStack() throws IOException {
135139
var mvnTreeCmdArgs =
136140
new ArrayList<>(
137141
List.of(
138-
"org.apache.maven.plugins:maven-dependency-plugin:3.6.0:tree",
142+
DEP_TREE_PLUGIN,
139143
"-Dscope=compile",
140144
"-Dverbose",
141145
"-DoutputType=text",
@@ -228,6 +232,7 @@ private Content generateSbomFromEffectivePom() throws IOException {
228232
.filter(DependencyAggregator::isTestDependency)
229233
.collect(Collectors.toSet());
230234
var deps = getDependencies(tmpEffPom);
235+
deps = resolveVersionRanges(deps);
231236
var sbom = SbomFactory.newInstance().addRoot(getRoot(tmpEffPom), readLicenseFromManifest());
232237
deps.stream()
233238
.filter(dep -> !testsDeps.contains(dep))
@@ -239,6 +244,87 @@ private Content generateSbomFromEffectivePom() throws IOException {
239244
return new Content(sbom.getAsJsonString().getBytes(), Api.CYCLONEDX_MEDIA_TYPE);
240245
}
241246

247+
/**
248+
* Checks whether the given version string is a Maven version range expression (starts with '[' or
249+
* '(').
250+
*/
251+
private static boolean isVersionRange(String version) {
252+
if (version == null || version.isEmpty()) return false;
253+
char first = version.charAt(0);
254+
return first == '[' || first == '(';
255+
}
256+
257+
/**
258+
* Resolves Maven version ranges by running the dependency tree plugin and replacing range
259+
* expressions with concrete resolved versions. If no version ranges are present, the original
260+
* list is returned unchanged. On failure, the original list is returned with a warning logged.
261+
*/
262+
private List<DependencyAggregator> resolveVersionRanges(List<DependencyAggregator> deps)
263+
throws IOException {
264+
// Short-circuit: if no dep has a version range, return unchanged
265+
boolean hasRanges = deps.stream().anyMatch(d -> isVersionRange(d.version));
266+
if (!hasRanges) {
267+
return deps;
268+
}
269+
270+
Path tmpFile = Files.createTempFile("TRUSTIFY_DA_range_tree_", ".txt");
271+
try {
272+
var cmd =
273+
buildMvnCommandArgs(
274+
DEP_TREE_PLUGIN,
275+
"-Dscope=compile",
276+
"-DoutputType=text",
277+
String.format("-DoutputFile=%s", tmpFile.toString()),
278+
"-f",
279+
manifestPath.toString(),
280+
"--batch-mode",
281+
"-q");
282+
Operations.runProcess(manifestPath.getParent(), cmd.toArray(String[]::new), getMvnExecEnvs());
283+
284+
// Read the dependency tree output and build a lookup map of resolved versions
285+
List<String> lines = Files.readAllLines(tmpFile);
286+
Map<String, String> resolvedVersions = new HashMap<>();
287+
for (String line : lines) {
288+
if (getDepth(line) == 1) {
289+
DependencyAggregator resolved = parseDep(line);
290+
resolvedVersions.put(resolved.groupId + ":" + resolved.artifactId, resolved.version);
291+
}
292+
}
293+
294+
// Replace version ranges with resolved concrete versions
295+
for (DependencyAggregator dep : deps) {
296+
if (isVersionRange(dep.version)) {
297+
String key = dep.groupId + ":" + dep.artifactId;
298+
String resolved = resolvedVersions.get(key);
299+
if (resolved != null) {
300+
if (debugLoggingIsNeeded()) {
301+
log.info(
302+
String.format(
303+
"Resolved version range for %s: %s -> %s", key, dep.version, resolved));
304+
}
305+
dep.version = resolved;
306+
}
307+
}
308+
}
309+
} catch (Exception e) {
310+
log.log(
311+
Level.WARNING,
312+
String.format(
313+
"Failed to resolve version ranges via dependency tree, "
314+
+ "using original versions: %s",
315+
e.getMessage()),
316+
e);
317+
return deps;
318+
} finally {
319+
try {
320+
Files.deleteIfExists(tmpFile);
321+
} catch (IOException ignored) {
322+
}
323+
}
324+
325+
return deps;
326+
}
327+
242328
private PackageURL getRoot(final Path manifestPath) throws IOException {
243329
XMLStreamReader reader = null;
244330
try {

src/test/java/io/github/guacsec/trustifyda/providers/Java_Maven_Provider_Test.java

Lines changed: 83 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import java.util.Arrays;
3131
import java.util.Optional;
3232
import java.util.stream.Stream;
33+
import org.junit.jupiter.api.Test;
3334
import org.junit.jupiter.api.extension.ExtendWith;
3435
import org.junit.jupiter.params.ParameterizedTest;
3536
import org.junit.jupiter.params.provider.MethodSource;
@@ -56,7 +57,8 @@ static Stream<String> testFolders() {
5657
"deps_with_ignore_on_version",
5758
"deps_with_ignore_on_wrong",
5859
"deps_with_no_ignore",
59-
"pom_deps_with_no_ignore_common_paths");
60+
"pom_deps_with_no_ignore_common_paths",
61+
"deps_with_version_range");
6062
}
6163

6264
@ParameterizedTest
@@ -143,12 +145,29 @@ void test_the_provideComponent(String testFolder) throws IOException {
143145
getClass(), String.format("tst_manifests/maven/%s/effectivePom.xml", testFolder))) {
144146
effectivePom = new String(is.readAllBytes());
145147
}
148+
149+
String depTree;
150+
try (var is =
151+
getResourceAsStreamDecision(
152+
getClass(), String.format("tst_manifests/maven/%s/depTree.txt", testFolder))) {
153+
depTree = new String(is.readAllBytes());
154+
}
155+
146156
try (MockedStatic<Operations> mockedOperations = mockStatic(Operations.class)) {
147157
mockedOperations
148158
.when(() -> Operations.runProcess(any(), any(), any()))
149159
.thenAnswer(
150-
invocationOnMock ->
151-
getOutputFileAndOverwriteItWithMock(effectivePom, invocationOnMock, "-Doutput"));
160+
invocationOnMock -> {
161+
String result =
162+
getOutputFileAndOverwriteItWithMock(
163+
effectivePom, invocationOnMock, "-Doutput=");
164+
if (result == null) {
165+
result =
166+
getOutputFileAndOverwriteItWithMock(
167+
depTree, invocationOnMock, "-DoutputFile");
168+
}
169+
return result;
170+
});
152171
// Mock Operations.getCustomPathOrElse to return "mvn"
153172
mockedOperations.when(() -> Operations.getCustomPathOrElse(anyString())).thenReturn("mvn");
154173
mockedOperations
@@ -189,12 +208,29 @@ void test_the_provideComponent_With_Path(String testFolder) throws IOException {
189208
getClass(), String.format("tst_manifests/maven/%s/effectivePom.xml", testFolder))) {
190209
effectivePom = new String(is.readAllBytes());
191210
}
211+
212+
String depTree;
213+
try (var is =
214+
getResourceAsStreamDecision(
215+
getClass(), String.format("tst_manifests/maven/%s/depTree.txt", testFolder))) {
216+
depTree = new String(is.readAllBytes());
217+
}
218+
192219
try (MockedStatic<Operations> mockedOperations = mockStatic(Operations.class)) {
193220
mockedOperations
194221
.when(() -> Operations.runProcess(any(), any(), any()))
195222
.thenAnswer(
196-
invocationOnMock ->
197-
getOutputFileAndOverwriteItWithMock(effectivePom, invocationOnMock, "-Doutput"));
223+
invocationOnMock -> {
224+
String result =
225+
getOutputFileAndOverwriteItWithMock(
226+
effectivePom, invocationOnMock, "-Doutput=");
227+
if (result == null) {
228+
result =
229+
getOutputFileAndOverwriteItWithMock(
230+
depTree, invocationOnMock, "-DoutputFile");
231+
}
232+
return result;
233+
});
198234
// Mock Operations.getCustomPathOrElse to return "mvn"
199235
mockedOperations.when(() -> Operations.getCustomPathOrElse(anyString())).thenReturn("mvn");
200236
mockedOperations
@@ -209,6 +245,48 @@ void test_the_provideComponent_With_Path(String testFolder) throws IOException {
209245
}
210246
}
211247

248+
@Test
249+
void test_provideComponent_fallsBack_when_depTree_fails() throws IOException {
250+
String testFolder = "deps_with_version_range";
251+
252+
var targetPom = resolveFile(String.format("tst_manifests/maven/%s/pom.xml", testFolder));
253+
254+
String effectivePom;
255+
try (var is =
256+
getResourceAsStreamDecision(
257+
getClass(), String.format("tst_manifests/maven/%s/effectivePom.xml", testFolder))) {
258+
effectivePom = new String(is.readAllBytes());
259+
}
260+
261+
try (MockedStatic<Operations> mockedOperations = mockStatic(Operations.class)) {
262+
mockedOperations
263+
.when(() -> Operations.runProcess(any(), any(), any()))
264+
.thenAnswer(
265+
invocationOnMock -> {
266+
String result =
267+
getOutputFileAndOverwriteItWithMock(
268+
effectivePom, invocationOnMock, "-Doutput=");
269+
if (result == null) {
270+
throw new IOException("Simulated dependency:tree failure");
271+
}
272+
return result;
273+
});
274+
mockedOperations.when(() -> Operations.getCustomPathOrElse(anyString())).thenReturn("mvn");
275+
mockedOperations
276+
.when(() -> Operations.getExecutable(anyString(), anyString()))
277+
.thenReturn("mvn");
278+
279+
var content = new JavaMavenProvider(targetPom).provideComponent();
280+
281+
assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE);
282+
String sbom = new String(content.buffer);
283+
assertThat(sbom).contains("log4j");
284+
assertThat(sbom).contains("snappy-java");
285+
// Version range should remain unresolved since dep tree failed
286+
assertThat(sbom).contains("[1.2.17,1.3.0)");
287+
}
288+
}
289+
212290
private String dropIgnored(String s) {
213291
return s.replaceAll("\\s+", "").replaceAll("\"timestamp\":\"[a-zA-Z0-9\\-\\:]+\",", "");
214292
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
pom-with-deps-version-range:pom-with-version-range-for-tests:jar:0.0.1
2+
+- log4j:log4j:jar:1.2.17:compile
3+
\- org.xerial.snappy:snappy-java:jar:1.1.10.0:compile

0 commit comments

Comments
 (0)