Skip to content

feat(providers): add SHA-256 hash computation for Maven dependencies - #612

Open
a-oren wants to merge 2 commits into
guacsec:mainfrom
a-oren:worktree-TC-5549
Open

feat(providers): add SHA-256 hash computation for Maven dependencies#612
a-oren wants to merge 2 commits into
guacsec:mainfrom
a-oren:worktree-TC-5549

Conversation

@a-oren

@a-oren a-oren commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add SHA-256 hash computation for Maven dependencies by reading artifact files from the local .m2/repository/ cache
  • Parse raw dependency tree lines to correctly handle packaging types (bundle/eclipse-plugin → .jar) and classifiers
  • Support custom Maven repo path via TRUSTIFY_DA_MVN_REPO env var
  • Gracefully omit hashes when artifact files are not in the local cache (no errors)
  • Guard against empty DEP_REGEX parts in _buildMavenHashMap to prevent malformed .m2 paths (review feedback fix)

Implements TC-5549
Implements TC-5581

Test plan

  • All 33 existing Maven golden SBOM tests pass unchanged
  • 9 new tests for hash computation, graceful degradation, custom repo path, packaging mapping, classifier handling, SBOM integration, duplicate skipping, and empty field guard
  • ESLint clean on all modified files
  • CI passes on Node 22 and Node 24

🤖 Generated with Claude Code

Compute SHA-256 hashes from artifact files in the local Maven repository
cache (~/.m2/repository/) and include them in Maven SBOM components.

- Add _buildMavenHashMap() that parses dependency tree lines to extract
  groupId, artifactId, packaging, version, and optional classifier, then
  constructs the correct .m2 file path and computes the hash
- Handle packaging-to-extension mapping (bundle/eclipse-plugin → .jar)
- Skip POM-only artifacts (no hash for metadata-only dependencies)
- Support classified dependencies with correct file path construction
- Support custom Maven repo path via TRUSTIFY_DA_MVN_REPO env var
- Gracefully omit hashes when artifact files are not in the local cache
- Pass hash map through parseDependencyTree() to sbom.addDependency()

Implements TC-5549

Assisted-by: Claude Code
@sourcery-ai

sourcery-ai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds SHA-256 hash computation for Maven dependencies by reading artifacts from the local Maven repository, threads hashes through SBOM generation, and extends tests to cover hash behavior, packaging/classifier handling, and duplicate skipping.

Sequence diagram for Maven dependency hash computation and SBOM integration

sequenceDiagram
    participant Java_maven
    participant MavenRepo as Maven_repo_fs
    participant Base_Java
    participant Sbom

    Java_maven->>Java_maven: _buildMavenHashMap(depTreeText, opts)
    loop for each dependency line
        Java_maven->>MavenRepo: fs.readFileSync(artifactPath)
        MavenRepo-->>Java_maven: fileContent
        Java_maven->>Java_maven: crypto.createHash('sha256').update(fileContent).digest('hex')
        Java_maven->>Java_maven: toPurl(groupId, artifactId, purlVersion).toString()
        Java_maven->>Java_maven: hashMap.set(purl, [{alg: SHA-256, content: digest}])
    end

    Java_maven->>Java_maven: createSbomFileFromTextFormat(depTreeText, ignoredDeps, opts, manifestPath, hashMap)
    Java_maven->>Sbom: addRoot(rootPurl, license)
    Java_maven->>Base_Java: parseDependencyTree(root, 0, lines, sbom, hashMap)
    loop for each parsed dependency
        Base_Java->>Sbom: addDependency(from, to, undefined, hashes)
    end
    Sbom-->>Java_maven: getAsJsonString(opts)
Loading

File-Level Changes

Change Details Files
Compute SHA-256 hashes for Maven artifacts from the local Maven repository and attach them to SBOM components via a hash map keyed by PURL.
  • Introduce a _buildMavenHashMap helper that parses mvn dependency:tree output, maps packaging types and classifiers to artifact file paths under the Maven repo, reads artifact files, computes SHA-256 digests, and stores them in a Map keyed by PURL.
  • Add PACKAGING_TO_JAR and MAVEN_SCOPES static properties to normalize non-jar packaging types (e.g., bundle, eclipse-plugin) and to detect classifier positions based on Maven scopes.
  • Respect TRUSTIFY_DA_MVN_REPO (or default ~/.m2/repository) when resolving artifact paths and gracefully skip missing artifacts while optionally logging in debug mode.
src/providers/java_maven.js
Plumb the hash map into SBOM creation so dependency entries carry hash metadata.
  • Update getDependencies to build the Maven hash map from the dependency tree content and pass it to createSbomFileFromTextFormat.
  • Extend createSbomFileFromTextFormat to accept an optional hashMap parameter and pass it into parseDependencyTree.
  • Modify Base_Java.parseDependencyTree to accept an optional hashMap and, when adding dependencies to the SBOM, look up hashes by PURL and pass them into sbom.addDependency.
src/providers/java_maven.js
src/providers/base_java.js
Extend Maven provider tests to cover SHA-256 computation, repo path handling, packaging/classifier behavior, SBOM hash integration, and duplicate skipping.
  • Switch test imports to node:crypto, node:fs, node:os, and node:path to support hash computation and temporary repo setup.
  • Add a new test suite that builds a temporary Maven repo structure, verifies hash computation for present artifacts, omission for missing artifacts, respect for TRUSTIFY_DA_MVN_REPO, bundle→jar mapping, pom-skipping, classifier handling, SBOM hash propagation, and skipping of parenthesized duplicate dependency tree lines.
test/providers/java_maven.test.js

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • In _buildMavenHashMap, the catch {} block silently swallows all filesystem errors; consider handling ENOENT separately for missing artifacts while logging or surfacing other error types to avoid hiding unexpected issues.
  • The synchronous fs.readFileSync inside the dependency-tree loop may become a bottleneck for large Maven projects; consider switching to an async implementation or batching reads to avoid blocking the event loop.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `_buildMavenHashMap`, the `catch {}` block silently swallows all filesystem errors; consider handling `ENOENT` separately for missing artifacts while logging or surfacing other error types to avoid hiding unexpected issues.
- The synchronous `fs.readFileSync` inside the dependency-tree loop may become a bottleneck for large Maven projects; consider switching to an async implementation or batching reads to avoid blocking the event loop.

## Individual Comments

### Comment 1
<location path="src/providers/java_maven.js" line_range="179-184" />
<code_context>
+			const trimmed = rawLine.trim()
+			if (!trimmed || trimmed.startsWith('(')) { continue }
+
+			const parts = trimmed.split(':').map(p => p ? p.match(this.DEP_REGEX)?.[0] ?? '' : '')
+			if (parts.length < 4) { continue }
+
+			const groupId = parts[0]
+			const artifactId = parts[1]
+			const packaging = parts[2]
+
+			if (packaging === 'pom') { continue }
</code_context>
<issue_to_address>
**issue:** Guard against incomplete parts when DEP_REGEX does not match segments.

When `DEP_REGEX` doesn’t match a segment, the corresponding `parts` entry is set to `''`, but later logic still treats `groupId`, `artifactId`, and `packaging` as valid. This can lead to malformed paths and unnecessary filesystem access. Add a guard that bails out when any of these fields is empty before computing or using the artifact path.
</issue_to_address>

### Comment 2
<location path="src/providers/java_maven.js" line_range="201-219" />
<code_context>
+				: `${artifactId}-${version}.${ext}`
+			const artifactPath = path.join(m2Repo, groupPath, artifactId, version, fileName)
+
+			try {
+				const fileContent = fs.readFileSync(artifactPath)
+				const digest = crypto.createHash('sha256').update(fileContent).digest('hex')
+				// Key by the PURL that parseDep() will produce for this line
+				const purlVersion = classifier ? `${version}-${classifier}` : version
+				const purl = this.toPurl(groupId, artifactId, purlVersion).toString()
+				hashMap.set(purl, [{ alg: 'SHA-256', content: digest }])
+			} catch {
+				if (process.env['TRUSTIFY_DA_DEBUG'] === 'true') {
</code_context>
<issue_to_address>
**suggestion (performance):** Avoid recomputing hashes for duplicate artifacts appearing multiple times in the dependency tree.

When the same artifact appears multiple times in the tree, you currently re-read and hash the file for each occurrence, repeatedly overwriting the same entry in `hashMap`. Check `hashMap.has(purl)` and skip recomputing when it’s already present to avoid redundant I/O and hashing, especially for large Maven repositories.

```suggestion
			const ext = Java_maven.PACKAGING_TO_JAR[packaging] || packaging
			const groupPath = groupId.replaceAll('.', path.sep)
			const fileName = classifier
				? `${artifactId}-${version}-${classifier}.${ext}`
				: `${artifactId}-${version}.${ext}`
			const artifactPath = path.join(m2Repo, groupPath, artifactId, version, fileName)

			// Key by the PURL that parseDep() will produce for this line
			const purlVersion = classifier ? `${version}-${classifier}` : version
			const purl = this.toPurl(groupId, artifactId, purlVersion).toString()

			// Avoid recomputing hashes for duplicate artifacts
			if (hashMap.has(purl)) {
				continue
			}

			try {
				const fileContent = fs.readFileSync(artifactPath)
				const digest = crypto.createHash('sha256').update(fileContent).digest('hex')
				hashMap.set(purl, [{ alg: 'SHA-256', content: digest }])
			} catch {
				if (process.env['TRUSTIFY_DA_DEBUG'] === 'true') {
					console.error(`Maven hash: artifact not found at ${artifactPath}, omitting hash`)
				}
			}
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/providers/java_maven.js
Comment thread src/providers/java_maven.js
@codecov-commenter

codecov-commenter commented Aug 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.32%. Comparing base (329b3cf) to head (0690f65).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #612      +/-   ##
==========================================
+ Coverage   91.22%   91.32%   +0.10%     
==========================================
  Files          42       43       +1     
  Lines        9175     9467     +292     
  Branches     1624     1693      +69     
==========================================
+ Hits         8370     8646     +276     
- Misses        805      821      +16     
Flag Coverage Δ
unit-tests 91.32% <100.00%> (+0.10%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/providers/base_java.js 91.62% <100.00%> (+0.09%) ⬆️

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@a-oren

a-oren commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

[sdlc-workflow/verify-pr] Re: @sourcery-ai[bot] review —

  1. ENOENT handling: Classified as suggestion — proposes separating ENOENT from other errors in the catch block. The project uses bare catch blocks elsewhere (e.g., readLicenseFromManifest). No convention or pattern requires granular error differentiation. No sub-task created.

  2. Async readFileSync: Classified as suggestion — proposes switching to async I/O for performance. The project uses readFileSync extensively and the CLI is inherently synchronous. No performance issue demonstrated at current scale. No sub-task created.

@a-oren

a-oren commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

Verification Report for TC-5549 (commit 31e1fc9)

Check Result Details
Review Feedback WARN 1 code change request → sub-task TC-5581 created; 3 suggestions classified (no action)
Root-Cause Investigation SKIPPED Minor defensive coding gap; sub-task addresses the fix directly
Scope Containment PASS All 3 changed files within specified scope
Diff Size PASS 310 lines (under 500 threshold)
Commit Traceability PASS Single commit references TC-5549, Conventional Commits format
Sensitive Patterns PASS No secrets, credentials, or sensitive data found
CI Status PASS All 5 CI checks pass; coverage 91.32% (above 82% threshold)
Acceptance Criteria PASS 8/8 criteria met with dedicated test coverage
Test Quality PASS 8 new tests with doc comments, distinct scenarios, no repetition; Eval Quality: N/A
Test Change Classification ADDITIVE New test suite appended; no existing tests modified
Verification Commands PASS npm test and coverage checks pass in CI

Overall: WARN

One code change request from sourcery-ai review (guard against empty DEP_REGEX parts) resulted in sub-task TC-5581. All other checks pass. Implementation correctly adds SHA-256 hash computation for Maven dependencies with comprehensive test coverage.


This comment was AI-generated by sdlc-workflow/verify-pr v0.13.8.

Skip dependency tree lines where groupId, artifactId, or packaging is
empty after DEP_REGEX matching to prevent malformed .m2 paths.

Implements TC-5581

Assisted-by: Claude Code
@a-oren
a-oren requested review from Strum355 and ruromero August 10, 2026 11:49

@ruromero ruromero left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review — SHA-256 Hash Computation for Maven Dependencies

Overall the feature direction is solid — attaching artifact hashes to SBOM components is valuable. However, the current implementation has correctness bugs that will cause silent hash loss for classified dependencies and several design issues worth addressing before merge.

Summary of findings

Bugs (must fix):

  1. PURL key mismatch on conflict overrides with classifiersparseDep and _buildMavenHashMap construct different PURLs for the same classified dependency when a conflict override is present, so hashMap.get() silently returns undefined.
  2. Dead startsWith('(') guard — Maven tree lines always have tree-drawing prefixes before parentheses, so this guard never fires. The test passes by accident (jar absent from fixture, not because the line is skipped).
  3. Scope list divergenceMAVEN_SCOPES has 6 scopes but parseDep only checks 3 for classifier detection, causing divergent PURL construction.

Efficiency:
4. No deduplication — same artifact can be read and hashed multiple times.
5. readFileSync loads entire files into memory — large jars can cause memory spikes.

Design:
6. Duplicated coordinate parsing will drift silently.
7. Maven-specific hashMap parameter leaks into the shared Base_java abstraction.
8. provideComponent() omits hashes while provideStack() includes them — asymmetric behavior.

See inline comments for details.

🤖 Generated with Claude Code


// Handle conflict overrides the same way parseDep does
const override = rawLine.match(this.CONFLICT_REGEX)
if (override) { version = override[1] }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: PURL key mismatch for classified dependencies with conflict overrides

When a classified dependency has a conflict override (e.g., io.netty:netty-transport:jar:linux-x86_64:4.1.0:compile - omitted for conflict with 4.2.0):

  • _buildMavenHashMap tracks classifier=linux-x86_64, overrides version to 4.2.0, then constructs purlVersion=4.2.0-linux-x86_64 → PURL @4.2.0-linux-x86_64
  • parseDep (in base_java.js) first sets version=4.1.0-linux-x86_64, then CONFLICT_REGEX replaces the entire version string with 4.2.0 → PURL @4.2.0

Since @4.2.0 != @4.2.0-linux-x86_64, hashMap.get(to.toString()) returns undefined and the hash is silently lost.

Fix: Either unify the parsing into a single function (preferred — see duplication comment), or ensure the conflict-override branch in _buildMavenHashMap drops the classifier the same way parseDep does.


for (const rawLine of lines) {
const trimmed = rawLine.trim()
if (!trimmed || trimmed.startsWith('(')) { continue }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dead code: trimmed.startsWith('(') never matches

Maven dependency tree lines for duplicates/conflicts look like:

   \- (org.slf4j:slf4j-api:jar:1.7.36:compile - omitted for duplicate)

After trim(), the line starts with \, not (. This guard never fires.

The test skips parenthesized duplicate entries passes for the wrong reason — org.slf4j:slf4j-api is not in the mock .m2 fixture, so readFileSync throws and the catch block silently skips it. If you added that jar to the fixture, the test would fail (the entry would appear in the hash map), revealing that the guard is dead.

Fix: Strip tree-drawing characters before checking for (, e.g.:

const cleaned = trimmed.replace(/^[|+\\\- ]+/, '')
if (!cleaned || cleaned.startsWith('(')) { continue }

if (packaging === 'pom') { continue }

let version, classifier
if (parts.length >= 6 && Java_maven.MAVEN_SCOPES.includes(parts[5])) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Scope list divergence — 6 scopes here vs 3 in parseDep

MAVEN_SCOPES includes ['compile', 'provided', 'runtime', 'test', 'system', 'import'] (6 scopes), but parseDep in base_java.js:104 only checks ['compile', 'provided', 'runtime'] for classifier detection.

For a classified dependency with scope system (e.g., com.sun:tools:jar:jdk8:1.8.0:system):

  • _buildMavenHashMap detects the classifier → PURL @1.8.0-jdk8
  • parseDep does NOT detect it → PURL @jdk8

The PURL keys diverge and the hash lookup fails silently.

While -Dscope=compile currently filters system-scoped deps, createSbomFileFromTextFormat is public and the filter could change. Both parsers should use the same scope list.

* @param {{}} [opts={}] Options bag (may contain TRUSTIFY_DA_MVN_REPO)
* @returns {Map<string, Array<{alg: string, content: string}>>}
*/
_buildMavenHashMap(depTreeText, opts = {}) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Design: Duplicated coordinate parsing will silently drift

This method re-implements the split-by-colon, DEP_REGEX application, classifier detection, and conflict-override handling that parseDep in base_java.js:98-117 already does. The two implementations already diverge (scope lists, conflict-override classifier handling).

Any future change to parseDep that isn't mirrored here will silently break hash lookups with no test failure, because the test mocks don't cover the cross-function invariant.

Suggestion: Extract a shared parseCoordinate(rawLine) helper that both parseDep and _buildMavenHashMap call, returning {groupId, artifactId, version, classifier, packaging, scope}. This eliminates the entire class of drift bugs.

: `${artifactId}-${version}.${ext}`
const artifactPath = path.join(m2Repo, groupPath, artifactId, version, fileName)

try {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Efficiency: No deduplication guard before file I/O

The same artifact can appear in multiple branches of the dependency tree. Combined with the dead startsWith('(') guard, parenthesized duplicate lines also get processed. Each occurrence triggers a separate fs.readFileSync + SHA-256 computation.

In a multi-module project with 5 modules sharing 50 deps, that's up to 250 redundant file reads.

Fix: Add a check before the try block:

const purl = this.toPurl(groupId, artifactId, purlVersion).toString()
if (hashMap.has(purl)) { continue }

const artifactPath = path.join(m2Repo, groupPath, artifactId, version, fileName)

try {
const fileContent = fs.readFileSync(artifactPath)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Efficiency: readFileSync loads entire file into memory

Large artifacts (e.g., aws-java-sdk-bundle ~300MB) are fully loaded into a Node.js buffer. Combined with the lack of deduplication, peak memory can spike significantly. In memory-constrained CI containers this could trigger OOM kills.

Fix: Use streaming hash:

const stream = fs.createReadStream(artifactPath)
const hash = crypto.createHash('sha256')
for await (const chunk of stream) { hash.update(chunk) }
const digest = hash.digest('hex')

Note: this would make _buildMavenHashMap async.

const purlVersion = classifier ? `${version}-${classifier}` : version
const purl = this.toPurl(groupId, artifactId, purlVersion).toString()
hashMap.set(purl, [{ alg: 'SHA-256', content: digest }])
} catch {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Observability: Silent degradation with incomplete .m2 cache

The catch block swallows all errors unless TRUSTIFY_DA_DEBUG is set. In CI environments with partial caches (ephemeral containers, resolve-only phases), every readFileSync can fail silently, producing an SBOM with zero hashes and no visible signal.

Consider logging a summary warning at the end (e.g., "N of M artifacts could not be hashed") even in non-debug mode, so users have visibility into hash coverage.

* @param {Map<string, Array<{alg: string, content: string}>>} [hashMap] - Optional PURL→hashes map
*/
parseDependencyTree(src, srcDepth, lines, sbom) {
parseDependencyTree(src, srcDepth, lines, sbom, hashMap) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Design: Maven-specific parameter leaks into shared base class

The hashMap parameter is only meaningful for Maven. Gradle extends Base_java but uses its own tree parser and never passes hashMap. This leaks a Maven-specific concern into the shared abstraction.

Consider keeping hash attachment in the Maven subclass (e.g., a post-processing step that walks the SBOM components and attaches hashes) rather than threading it through the base class.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants