From 65ae6dd9e436db02b90f57653577f25d804c0dbf Mon Sep 17 00:00:00 2001 From: Noah Santschi-Cooney Date: Thu, 21 May 2026 13:55:31 +0100 Subject: [PATCH] fix(maven): resolve version ranges in component analysis 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: https://github.com/fabric8-analytics/fabric8-analytics-vscode-extension/issues/812 Co-Authored-By: Claude Opus 4.6 --- src/providers/base_java.js | 15 +- src/providers/java_maven.js | 77 ++++++ test/providers/java_maven.test.js | 3 +- .../component_analysis_expected_sbom.json | 51 ++++ .../effective-pom.xml | 252 ++++++++++++++++++ .../mvn_deptree.txt | 3 + .../mvn_deptree_ranges.txt | 3 + .../maven/pom_deps_with_version_range/pom.xml | 23 ++ .../stack_analysis_expected_sbom.json | 51 ++++ 9 files changed, 469 insertions(+), 9 deletions(-) create mode 100644 test/providers/tst_manifests/maven/pom_deps_with_version_range/component_analysis_expected_sbom.json create mode 100644 test/providers/tst_manifests/maven/pom_deps_with_version_range/effective-pom.xml create mode 100644 test/providers/tst_manifests/maven/pom_deps_with_version_range/mvn_deptree.txt create mode 100644 test/providers/tst_manifests/maven/pom_deps_with_version_range/mvn_deptree_ranges.txt create mode 100644 test/providers/tst_manifests/maven/pom_deps_with_version_range/pom.xml create mode 100644 test/providers/tst_manifests/maven/pom_deps_with_version_range/stack_analysis_expected_sbom.json diff --git a/src/providers/base_java.js b/src/providers/base_java.js index 4e9b8916..ced65ce5 100644 --- a/src/providers/base_java.js +++ b/src/providers/base_java.js @@ -59,7 +59,7 @@ export default class Base_Java { } let index = 0; let target = lines[index]; - let targetDepth = this.#getDepth(target); + let targetDepth = this._getDepth(target); while (targetDepth > srcDepth && index < lines.length) { if (targetDepth === srcDepth + 1) { let from = this.parseDep(src); @@ -71,10 +71,10 @@ export default class Base_Java { sbom.addDependency(from, to) } } else { - this.parseDependencyTree(lines[index - 1], this.#getDepth(lines[index - 1]), lines.slice(index), sbom) + this.parseDependencyTree(lines[index - 1], this._getDepth(lines[index - 1]), lines.slice(index), sbom) } target = lines[++index]; - targetDepth = this.#getDepth(target); + targetDepth = this._getDepth(target); } } @@ -82,12 +82,11 @@ export default class Base_Java { * Calculates how deep in the graph is the given line * @param {string} line - line to calculate the depth from * @returns {number} The calculated depth - * @private + * @protected */ - #getDepth(line) { - if (line === undefined) { - return -1; - } + _getDepth(line) { + if (!line || line.trim() === '') { return -1; } + if (line.match(/^\w/)) { return 0; } return ((line.indexOf('-') - 1) / 3) + 1; } diff --git a/src/providers/java_maven.js b/src/providers/java_maven.js index 7a03947c..1f0d6ccf 100644 --- a/src/providers/java_maven.js +++ b/src/providers/java_maven.js @@ -198,6 +198,7 @@ export default class Java_maven extends Base_java { /** @type [Dependency] */ let dependencies = this.#getDependencies(tmpEffectivePom) .filter(d => !this.#dependencyIn(d, ignored)) + dependencies = this.#resolveVersionRanges(dependencies, manifestPath, opts) let sbom = new Sbom(); let rootDependency = this.#getRootFromPom(tmpEffectivePom, manifestPath); let purlRoot = this.toPurl(rootDependency.groupId, rootDependency.artifactId, rootDependency.version) @@ -309,6 +310,82 @@ export default class Java_maven extends Base_java { #dependencyIn(dep, deps) { return deps.filter(d => dep.artifactId === d.artifactId && dep.groupId === d.groupId && dep.scope === d.scope).length > 0 } + + /** + * Returns true if the given version string is a Maven version range + * (starts with '[' or '('). + * @param {string} version + * @returns {boolean} + * @private + */ + #isVersionRange(version) { + return typeof version === 'string' && (version.startsWith('[') || version.startsWith('(')) + } + + /** + * Resolves Maven version ranges in the given dependency list by running + * maven-dependency-plugin:tree and reading the concrete versions it selects. + * If no dependency uses a version range, returns the list unchanged. + * @param {Dependency[]} dependencies + * @param {string} manifestPath + * @param {object} opts + * @returns {Dependency[]} + * @private + */ + #resolveVersionRanges(dependencies, manifestPath, opts = {}) { + // short-circuit if no dependency has a version range + if (!dependencies.some(dep => this.#isVersionRange(dep.version))) { + return dependencies + } + + const mvn = this.selectToolBinary(manifestPath, opts) + const mvnArgs = JSON.parse(getCustom('TRUSTIFY_DA_MVN_ARGS', '[]', opts)); + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'trustify_da_range_')) + const tmpDepTree = path.join(tmpDir, 'mvn_deptree_ranges.txt') + + try { + this._invokeCommand(mvn, [ + '-q', + 'org.apache.maven.plugins:maven-dependency-plugin:3.6.0:tree', + '-Dscope=compile', + '-DoutputType=text', + `-DoutputFile=${tmpDepTree}`, + ...mvnArgs + ], { cwd: path.dirname(manifestPath) }) + + const content = fs.readFileSync(tmpDepTree) + const lines = content.toString().split(EOL).filter(l => l.trim() !== '') + + // Build a map of groupId:artifactId -> resolved version from depth-1 entries + /** @type {Map} */ + const resolvedVersions = new Map() + for (const line of lines) { + if (this._getDepth(line) === 1) { + const purl = this.parseDep(line) + resolvedVersions.set(`${purl.namespace}:${purl.name}`, purl.version) + } + } + + // Replace version ranges with resolved concrete versions + return dependencies.map(dep => { + if (this.#isVersionRange(dep.version)) { + const key = `${dep.groupId}:${dep.artifactId}` + const resolved = resolvedVersions.get(key) + if (resolved) { + return { ...dep, version: resolved } + } + } + return dep + }) + } catch (error) { + if (process.env["TRUSTIFY_DA_DEBUG"] === "true") { + console.error("Failed to resolve Maven version ranges: " + error.message) + } + return dependencies + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }) + } + } } const DEFAULT_MAVEN_DISCOVERY_IGNORE = [ diff --git a/test/providers/java_maven.test.js b/test/providers/java_maven.test.js index 1f8b9b7b..0c5b977d 100644 --- a/test/providers/java_maven.test.js +++ b/test/providers/java_maven.test.js @@ -87,7 +87,8 @@ suite('testing the java-maven data provider', async () => { "pom_deps_with_no_ignore", "poms_deps_with_ignore_long", "poms_deps_with_no_ignore_long", - "pom_deps_with_no_ignore_common_paths" + "pom_deps_with_no_ignore_common_paths", + "pom_deps_with_version_range" ].forEach(testCase => { let scenario = testCase.replace('pom_deps_', '').replaceAll('_', ' ') diff --git a/test/providers/tst_manifests/maven/pom_deps_with_version_range/component_analysis_expected_sbom.json b/test/providers/tst_manifests/maven/pom_deps_with_version_range/component_analysis_expected_sbom.json new file mode 100644 index 00000000..b75d0bee --- /dev/null +++ b/test/providers/tst_manifests/maven/pom_deps_with_version_range/component_analysis_expected_sbom.json @@ -0,0 +1,51 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "version": 1, + "metadata": { + "timestamp": "2023-08-07T00:00:00.000Z", + "component": { + "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", + "type": "application", + "bom-ref": "pkg:maven/pom-with-deps-version-range/pom-with-version-range-for-tests@0.0.1" + } + }, + "components": [ + { + "group": "log4j", + "name": "log4j", + "version": "1.2.17", + "purl": "pkg:maven/log4j/log4j@1.2.17", + "type": "library", + "bom-ref": "pkg:maven/log4j/log4j@1.2.17" + }, + { + "group": "org.xerial.snappy", + "name": "snappy-java", + "version": "1.1.10.0", + "purl": "pkg:maven/org.xerial.snappy/snappy-java@1.1.10.0", + "type": "library", + "bom-ref": "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": [] + } + ] +} diff --git a/test/providers/tst_manifests/maven/pom_deps_with_version_range/effective-pom.xml b/test/providers/tst_manifests/maven/pom_deps_with_version_range/effective-pom.xml new file mode 100644 index 00000000..75847060 --- /dev/null +++ b/test/providers/tst_manifests/maven/pom_deps_with_version_range/effective-pom.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/exhort-javascript-api/test/providers/tst_manifests/maven/pom_deps_with_version_range/src/main/java + /home/zgrinber/git/exhort-javascript-api/test/providers/tst_manifests/maven/pom_deps_with_version_range/src/main/scripts + /home/zgrinber/git/exhort-javascript-api/test/providers/tst_manifests/maven/pom_deps_with_version_range/src/test/java + /home/zgrinber/git/exhort-javascript-api/test/providers/tst_manifests/maven/pom_deps_with_version_range/target/classes + /home/zgrinber/git/exhort-javascript-api/test/providers/tst_manifests/maven/pom_deps_with_version_range/target/test-classes + + + /home/zgrinber/git/exhort-javascript-api/test/providers/tst_manifests/maven/pom_deps_with_version_range/src/main/resources + + + + + /home/zgrinber/git/exhort-javascript-api/test/providers/tst_manifests/maven/pom_deps_with_version_range/src/test/resources + + + /home/zgrinber/git/exhort-javascript-api/test/providers/tst_manifests/maven/pom_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/exhort-javascript-api/test/providers/tst_manifests/maven/pom_deps_with_version_range/target/site + + + org.apache.maven.plugins + maven-project-info-reports-plugin + + + + + + default-deploy + site-deploy + + deploy + + + /home/zgrinber/git/exhort-javascript-api/test/providers/tst_manifests/maven/pom_deps_with_version_range/target/site + + + org.apache.maven.plugins + maven-project-info-reports-plugin + + + + + + + /home/zgrinber/git/exhort-javascript-api/test/providers/tst_manifests/maven/pom_deps_with_version_range/target/site + + + org.apache.maven.plugins + maven-project-info-reports-plugin + + + + + + + + /home/zgrinber/git/exhort-javascript-api/test/providers/tst_manifests/maven/pom_deps_with_version_range/target/site + + diff --git a/test/providers/tst_manifests/maven/pom_deps_with_version_range/mvn_deptree.txt b/test/providers/tst_manifests/maven/pom_deps_with_version_range/mvn_deptree.txt new file mode 100644 index 00000000..761020ff --- /dev/null +++ b/test/providers/tst_manifests/maven/pom_deps_with_version_range/mvn_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/test/providers/tst_manifests/maven/pom_deps_with_version_range/mvn_deptree_ranges.txt b/test/providers/tst_manifests/maven/pom_deps_with_version_range/mvn_deptree_ranges.txt new file mode 100644 index 00000000..761020ff --- /dev/null +++ b/test/providers/tst_manifests/maven/pom_deps_with_version_range/mvn_deptree_ranges.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/test/providers/tst_manifests/maven/pom_deps_with_version_range/pom.xml b/test/providers/tst_manifests/maven/pom_deps_with_version_range/pom.xml new file mode 100644 index 00000000..238896af --- /dev/null +++ b/test/providers/tst_manifests/maven/pom_deps_with_version_range/pom.xml @@ -0,0 +1,23 @@ + + + 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 + + + + diff --git a/test/providers/tst_manifests/maven/pom_deps_with_version_range/stack_analysis_expected_sbom.json b/test/providers/tst_manifests/maven/pom_deps_with_version_range/stack_analysis_expected_sbom.json new file mode 100644 index 00000000..b75d0bee --- /dev/null +++ b/test/providers/tst_manifests/maven/pom_deps_with_version_range/stack_analysis_expected_sbom.json @@ -0,0 +1,51 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "version": 1, + "metadata": { + "timestamp": "2023-08-07T00:00:00.000Z", + "component": { + "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", + "type": "application", + "bom-ref": "pkg:maven/pom-with-deps-version-range/pom-with-version-range-for-tests@0.0.1" + } + }, + "components": [ + { + "group": "log4j", + "name": "log4j", + "version": "1.2.17", + "purl": "pkg:maven/log4j/log4j@1.2.17", + "type": "library", + "bom-ref": "pkg:maven/log4j/log4j@1.2.17" + }, + { + "group": "org.xerial.snappy", + "name": "snappy-java", + "version": "1.1.10.0", + "purl": "pkg:maven/org.xerial.snappy/snappy-java@1.1.10.0", + "type": "library", + "bom-ref": "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": [] + } + ] +}