Skip to content

Commit f1e4e15

Browse files
Strum355claude
andauthored
fix(maven): resolve version ranges in component analysis (guacsec#536)
Maven version ranges (e.g. `[1.2.17,1.3.0)`) in pom.xml dependencies are preserved verbatim by `mvn help:effective-pom`, which is used for component analysis. The backend cannot look up vulnerabilities for a range-valued purl like `pkg:maven/log4j/log4j@[1.2.17,1.3.0)`, so these dependencies were silently dropped from analysis results. Stack analysis was unaffected because it uses `mvn dependency:tree` which resolves ranges to concrete versions. Add `#resolveVersionRanges` to detect range-valued versions in the effective POM output and resolve them by running `mvn dependency:tree -DoutputType=text -Dscope=compile`, parsing the direct dependencies (depth 1) to extract the concrete versions Maven selects. The resolution is guarded: it only runs when at least one dependency has a version range, and falls back to the original range values if the tree invocation fails. Ref: fabric8-analytics/fabric8-analytics-vscode-extension#812 Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 00f52e4 commit f1e4e15

9 files changed

Lines changed: 469 additions & 9 deletions

File tree

src/providers/base_java.js

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export default class Base_Java {
5959
}
6060
let index = 0;
6161
let target = lines[index];
62-
let targetDepth = this.#getDepth(target);
62+
let targetDepth = this._getDepth(target);
6363
while (targetDepth > srcDepth && index < lines.length) {
6464
if (targetDepth === srcDepth + 1) {
6565
let from = this.parseDep(src);
@@ -71,23 +71,22 @@ export default class Base_Java {
7171
sbom.addDependency(from, to)
7272
}
7373
} else {
74-
this.parseDependencyTree(lines[index - 1], this.#getDepth(lines[index - 1]), lines.slice(index), sbom)
74+
this.parseDependencyTree(lines[index - 1], this._getDepth(lines[index - 1]), lines.slice(index), sbom)
7575
}
7676
target = lines[++index];
77-
targetDepth = this.#getDepth(target);
77+
targetDepth = this._getDepth(target);
7878
}
7979
}
8080

8181
/**
8282
* Calculates how deep in the graph is the given line
8383
* @param {string} line - line to calculate the depth from
8484
* @returns {number} The calculated depth
85-
* @private
85+
* @protected
8686
*/
87-
#getDepth(line) {
88-
if (line === undefined) {
89-
return -1;
90-
}
87+
_getDepth(line) {
88+
if (!line || line.trim() === '') { return -1; }
89+
if (line.match(/^\w/)) { return 0; }
9190
return ((line.indexOf('-') - 1) / 3) + 1;
9291
}
9392

src/providers/java_maven.js

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ export default class Java_maven extends Base_java {
198198
/** @type [Dependency] */
199199
let dependencies = this.#getDependencies(tmpEffectivePom)
200200
.filter(d => !this.#dependencyIn(d, ignored))
201+
dependencies = this.#resolveVersionRanges(dependencies, manifestPath, opts)
201202
let sbom = new Sbom();
202203
let rootDependency = this.#getRootFromPom(tmpEffectivePom, manifestPath);
203204
let purlRoot = this.toPurl(rootDependency.groupId, rootDependency.artifactId, rootDependency.version)
@@ -309,6 +310,82 @@ export default class Java_maven extends Base_java {
309310
#dependencyIn(dep, deps) {
310311
return deps.filter(d => dep.artifactId === d.artifactId && dep.groupId === d.groupId && dep.scope === d.scope).length > 0
311312
}
313+
314+
/**
315+
* Returns true if the given version string is a Maven version range
316+
* (starts with '[' or '(').
317+
* @param {string} version
318+
* @returns {boolean}
319+
* @private
320+
*/
321+
#isVersionRange(version) {
322+
return typeof version === 'string' && (version.startsWith('[') || version.startsWith('('))
323+
}
324+
325+
/**
326+
* Resolves Maven version ranges in the given dependency list by running
327+
* maven-dependency-plugin:tree and reading the concrete versions it selects.
328+
* If no dependency uses a version range, returns the list unchanged.
329+
* @param {Dependency[]} dependencies
330+
* @param {string} manifestPath
331+
* @param {object} opts
332+
* @returns {Dependency[]}
333+
* @private
334+
*/
335+
#resolveVersionRanges(dependencies, manifestPath, opts = {}) {
336+
// short-circuit if no dependency has a version range
337+
if (!dependencies.some(dep => this.#isVersionRange(dep.version))) {
338+
return dependencies
339+
}
340+
341+
const mvn = this.selectToolBinary(manifestPath, opts)
342+
const mvnArgs = JSON.parse(getCustom('TRUSTIFY_DA_MVN_ARGS', '[]', opts));
343+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'trustify_da_range_'))
344+
const tmpDepTree = path.join(tmpDir, 'mvn_deptree_ranges.txt')
345+
346+
try {
347+
this._invokeCommand(mvn, [
348+
'-q',
349+
'org.apache.maven.plugins:maven-dependency-plugin:3.6.0:tree',
350+
'-Dscope=compile',
351+
'-DoutputType=text',
352+
`-DoutputFile=${tmpDepTree}`,
353+
...mvnArgs
354+
], { cwd: path.dirname(manifestPath) })
355+
356+
const content = fs.readFileSync(tmpDepTree)
357+
const lines = content.toString().split(EOL).filter(l => l.trim() !== '')
358+
359+
// Build a map of groupId:artifactId -> resolved version from depth-1 entries
360+
/** @type {Map<string, string>} */
361+
const resolvedVersions = new Map()
362+
for (const line of lines) {
363+
if (this._getDepth(line) === 1) {
364+
const purl = this.parseDep(line)
365+
resolvedVersions.set(`${purl.namespace}:${purl.name}`, purl.version)
366+
}
367+
}
368+
369+
// Replace version ranges with resolved concrete versions
370+
return dependencies.map(dep => {
371+
if (this.#isVersionRange(dep.version)) {
372+
const key = `${dep.groupId}:${dep.artifactId}`
373+
const resolved = resolvedVersions.get(key)
374+
if (resolved) {
375+
return { ...dep, version: resolved }
376+
}
377+
}
378+
return dep
379+
})
380+
} catch (error) {
381+
if (process.env["TRUSTIFY_DA_DEBUG"] === "true") {
382+
console.error("Failed to resolve Maven version ranges: " + error.message)
383+
}
384+
return dependencies
385+
} finally {
386+
fs.rmSync(tmpDir, { recursive: true, force: true })
387+
}
388+
}
312389
}
313390

314391
const DEFAULT_MAVEN_DISCOVERY_IGNORE = [

test/providers/java_maven.test.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ suite('testing the java-maven data provider', async () => {
8787
"pom_deps_with_no_ignore",
8888
"poms_deps_with_ignore_long",
8989
"poms_deps_with_no_ignore_long",
90-
"pom_deps_with_no_ignore_common_paths"
90+
"pom_deps_with_no_ignore_common_paths",
91+
"pom_deps_with_version_range"
9192
].forEach(testCase => {
9293
let scenario = testCase.replace('pom_deps_', '').replaceAll('_', ' ')
9394

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
{
2+
"bomFormat": "CycloneDX",
3+
"specVersion": "1.4",
4+
"version": 1,
5+
"metadata": {
6+
"timestamp": "2023-08-07T00:00:00.000Z",
7+
"component": {
8+
"group": "pom-with-deps-version-range",
9+
"name": "pom-with-version-range-for-tests",
10+
"version": "0.0.1",
11+
"purl": "pkg:maven/pom-with-deps-version-range/pom-with-version-range-for-tests@0.0.1",
12+
"type": "application",
13+
"bom-ref": "pkg:maven/pom-with-deps-version-range/pom-with-version-range-for-tests@0.0.1"
14+
}
15+
},
16+
"components": [
17+
{
18+
"group": "log4j",
19+
"name": "log4j",
20+
"version": "1.2.17",
21+
"purl": "pkg:maven/log4j/log4j@1.2.17",
22+
"type": "library",
23+
"bom-ref": "pkg:maven/log4j/log4j@1.2.17"
24+
},
25+
{
26+
"group": "org.xerial.snappy",
27+
"name": "snappy-java",
28+
"version": "1.1.10.0",
29+
"purl": "pkg:maven/org.xerial.snappy/snappy-java@1.1.10.0",
30+
"type": "library",
31+
"bom-ref": "pkg:maven/org.xerial.snappy/snappy-java@1.1.10.0"
32+
}
33+
],
34+
"dependencies": [
35+
{
36+
"ref": "pkg:maven/pom-with-deps-version-range/pom-with-version-range-for-tests@0.0.1",
37+
"dependsOn": [
38+
"pkg:maven/log4j/log4j@1.2.17",
39+
"pkg:maven/org.xerial.snappy/snappy-java@1.1.10.0"
40+
]
41+
},
42+
{
43+
"ref": "pkg:maven/log4j/log4j@1.2.17",
44+
"dependsOn": []
45+
},
46+
{
47+
"ref": "pkg:maven/org.xerial.snappy/snappy-java@1.1.10.0",
48+
"dependsOn": []
49+
}
50+
]
51+
}

0 commit comments

Comments
 (0)