Skip to content

feat(providers): add SHA-256 hash computation for Gradle providers - #614

Open
a-oren wants to merge 7 commits into
guacsec:mainfrom
a-oren:TC-5550
Open

feat(providers): add SHA-256 hash computation for Gradle providers#614
a-oren wants to merge 7 commits into
guacsec:mainfrom
a-oren:TC-5550

Conversation

@a-oren

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

Copy link
Copy Markdown
Contributor

Description

Adds SHA-256 hash computation for Gradle dependency artifacts (both Groovy and Kotlin variants). Rather than scanning the local Gradle cache layout, the provider asks Gradle itself for the resolved artifact files via an init script, then hashes those files with the Node.js crypto module — the same technique the CycloneDX Gradle plugin uses.

What changed

  • GRADLE_HASH_INIT_SCRIPT — an allprojects { task daListHashes } block mirroring the existing GRADLE_INIT_SCRIPT. It iterates resolvable configurations via a lenient artifactView and emits ::DA_HASH::group:name:version::<absolute-file-path> per resolved module artifact. Fileless artifacts (BOM/platform()) are skipped.
  • parseGradleHashes(manifest, opts) — writes the init script to a temp file (crypto.randomUUID() name, removed in finally), invokes Gradle with --init-script … daListHashes, parses the output, and computes SHA-256 per file. Returns a Map<"group:name@version", [{alg, content}]>.
  • The hash map is threaded through #buildSbom() and #buildDirectDependenciesSbom() into sbom.addDependency(source, target, scope, hashes) at both the transitive and direct add-dependency sites.
  • Graceful degradation: an uninvokable Gradle, a failing init script, or an artifact with no readable file omits the hash rather than throwing.

Testing

  • Groovy and Kotlin test suites converted to the SBOM_CASES deep-equal pattern against regenerated golden fixtures (which now include hashes).
  • Added targeted tests: exact SHA-256 digest of a resolved artifact, hash omitted for a fileless artifact (siblings unaffected), and graceful degradation when the init script fails.
  • npm test: all 28 Gradle tests pass. npm run coverage: 90.8% (above the 82% threshold). Verified end-to-end against a real Gradle project — the SBOM digest matched an independent sha256sum of the resolved jar.

Implements TC-5550

🤖 Generated with Claude Code

Summary by Sourcery

Add resolved artifact hashing to Gradle-generated SBOMs.

New Features:

  • Add SHA-256 hashes for resolved Gradle dependency artifacts in both Groovy and Kotlin provider SBOMs.

Enhancements:

  • Obtain artifact files through Gradle resolution and gracefully omit hashes when Gradle or artifact access fails.

Tests:

  • Expand Gradle provider coverage for artifact hashing, fileless dependencies, conflict resolution, warnings, and graceful degradation, with updated SBOM fixtures.

@sourcery-ai

sourcery-ai Bot commented Aug 16, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds SHA-256 hash computation for Gradle (Groovy and Kotlin) providers by invoking a Gradle init script to list resolved artifacts, hashing their files, and threading those hashes into SBOM generation, along with corresponding tests and fixtures updates.

Sequence diagram for Gradle hash computation and SBOM enrichment

sequenceDiagram
    participant Java_gradle
    participant Gradle
    participant Node_fs
    participant Node_crypto
    participant Sbom

    Java_gradle->>Java_gradle: parseGradleHashes(manifest, opts)
    Java_gradle->>Java_gradle: selectToolBinary(manifest, opts)
    Java_gradle->>Node_fs: writeFileSync(initScriptPath, GRADLE_HASH_INIT_SCRIPT)
    Java_gradle->>Gradle: _invokeCommand(gradle, [--init-script, daListHashes])
    Gradle-->>Java_gradle: ::DA_HASH:: lines
    Java_gradle->>Java_gradle: parseGradleHashScriptOutput(output)
    loop for each artifact
        Java_gradle->>Java_gradle: hashKeyFromComponentId(id)
        Java_gradle->>Node_fs: readFileSync(file)
        Node_fs-->>Java_gradle: artifact bytes
        Java_gradle->>Node_crypto: createHash('sha256').update(bytes).digest('hex')
        Node_crypto-->>Java_gradle: digest
        Java_gradle->>Java_gradle: hashMap.set(key, [{alg: SHA-256, content: digest}])
    end

    Java_gradle->>Java_gradle: #buildSbom(content, properties, manifestPath, opts, hashMap)
    Java_gradle->>Sbom: addDependency(currentParent, purl, scope, #lookupHashes(purl, hashMap))
    Java_gradle->>Java_gradle: #buildDirectDependenciesSbom(content, properties, manifestPath, opts, hashMap)
    Java_gradle->>Sbom: addDependency(rootPurl, purl, scope, #lookupHashes(purl, hashMap))
Loading

File-Level Changes

Change Details Files
Compute SHA-256 hashes for Gradle artifacts and attach them to SBOM dependencies via a Gradle init script–driven hash map.
  • Introduce GRADLE_HASH_INIT_SCRIPT and supporting helpers (parseGradleHashScriptOutput, hashKeyFromComponentId) to have Gradle emit resolved artifact coordinates and file paths.
  • Add parseGradleHashes() in the Java_gradle provider to invoke Gradle with the hash init script, compute SHA-256 digests with Node.js crypto for each artifact file, and build a Map keyed by group:name@version.
  • Plumb the hash map through #buildSbom, #buildDirectDependenciesSbom, and #processDependencyTree so sbom.addDependency receives optional hashes via #lookupHashes.
  • Extend Groovy and Kotlin Gradle provider tests to deep-equal SBOM fixtures (including hashes) and add targeted tests for correct hash computation, fileless artifact behavior, and graceful degradation when the init script fails.
  • Refactor Gradle test helpers to mock _invokeCommand for dependencies/properties/hash tasks and add deterministic hash fixtures plus updated expected SBOM JSONs with hashes.
src/providers/java_gradle.js
test/providers/java_gradle_groovy.test.js
test/providers/java_gradle_kotlin.test.js
test/providers/tst_manifests/gradle/deps_with_ignore_full_specification/expected_component_sbom.json
test/providers/tst_manifests/gradle/deps_with_ignore_full_specification/expected_stack_sbom.json
test/providers/tst_manifests/gradle/deps_with_ignore_named_params/expected_component_sbom.json
test/providers/tst_manifests/gradle/deps_with_ignore_named_params/expected_stack_sbom.json
test/providers/tst_manifests/gradle/deps_with_ignore_notations/expected_component_sbom.json
test/providers/tst_manifests/gradle/deps_with_ignore_notations/expected_stack_sbom.json
test/providers/tst_manifests/gradle/deps_with_no_ignore_common_paths/expected_component_sbom.json
test/providers/tst_manifests/gradle/deps_with_no_ignore_common_paths/expected_stack_sbom.json

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 left some high level feedback:

  • The helper logic for HASH_FIXTURE_DIR, extractCoordinates, artifactFileFor, buildHashScriptOutput, and mockInvokeCommand is duplicated between the Groovy and Kotlin Gradle tests; consider extracting these into a shared test utility to keep the behavior in sync and reduce maintenance overhead.
  • In parseGradleHashes, the broad try { ... } catch { return hashMap } blocks will swallow unexpected programming errors as silent hash omissions; consider narrowing the catch scope or at least logging under a debug flag (similar to TRUSTIFY_DA_DEBUG) so genuine failures are observable.
  • The temp artifact files created in artifactFileFor under the OS temp directory are never cleaned up; it may be worth deleting them at suite teardown (or using a per-test temporary directory) to avoid unbounded growth of test artifacts on long-lived CI agents.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The helper logic for `HASH_FIXTURE_DIR`, `extractCoordinates`, `artifactFileFor`, `buildHashScriptOutput`, and `mockInvokeCommand` is duplicated between the Groovy and Kotlin Gradle tests; consider extracting these into a shared test utility to keep the behavior in sync and reduce maintenance overhead.
- In `parseGradleHashes`, the broad `try { ... } catch { return hashMap }` blocks will swallow unexpected programming errors as silent hash omissions; consider narrowing the catch scope or at least logging under a debug flag (similar to `TRUSTIFY_DA_DEBUG`) so genuine failures are observable.
- The temp artifact files created in `artifactFileFor` under the OS temp directory are never cleaned up; it may be worth deleting them at suite teardown (or using a per-test temporary directory) to avoid unbounded growth of test artifacts on long-lived CI agents.

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.

@codecov-commenter

codecov-commenter commented Aug 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.71795% with 22 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.45%. Comparing base (a27acdd) to head (8fe8ae8).

Files with missing lines Patch % Lines
src/providers/java_gradle.js 88.71% 22 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #614      +/-   ##
==========================================
- Coverage   91.52%   91.45%   -0.07%     
==========================================
  Files          44       44              
  Lines        9989    10169     +180     
  Branches     1811     1839      +28     
==========================================
+ Hits         9142     9300     +158     
- Misses        847      869      +22     
Flag Coverage Δ
unit-tests 91.45% <88.71%> (-0.07%) ⬇️

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

Files with missing lines Coverage Δ
src/providers/java_gradle.js 94.38% <88.71%> (-2.03%) ⬇️
🚀 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 16, 2026

Copy link
Copy Markdown
Contributor Author

[sdlc-workflow/verify-pr] Re: @sourcery-ai[bot] review — the review body contained three suggestions, each classified as suggestion. No sub-tasks created.

  1. Extract duplicated test helpers into a shared utility (java_gradle_groovy.test.js / java_gradle_kotlin.test.js) — suggestion. CONVENTIONS.md has no test-utility-extraction convention, and shared test-util usage is not an established codebase pattern (every sibling provider test — java_maven, golang_gomodules, python_pip, rust_cargo, oci_* — inlines its own setup; test/providers/test-utils.js has zero importers). Not upgraded.

  2. Narrow the broad try/catch in parseGradleHashes / log under a debug flag (src/providers/java_gradle.js) — suggestion. The broad catch is intentional: acceptance criterion test: better integration tests including new cli script #5 requires graceful degradation ("artifacts with no resolvable file or an uninvokable Gradle result in omitted hashes, not errors"). CONVENTIONS.md §Error Handling scopes "no blanket try-catch" to async error bubbling; parseGradleHashes is synchronous, so the convention does not apply. Not upgraded.

  3. Clean up temp artifact files created by artifactFileForsuggestion. No CONVENTIONS.md section or counted codebase pattern covers temp-file teardown. Not upgraded.

Classified by sdlc-workflow/verify-pr v0.13.8.

@a-oren

a-oren commented Aug 16, 2026

Copy link
Copy Markdown
Contributor Author

Verification Report for TC-5550 (commit 2fa8a41)

Check Result Details
Review Feedback PASS 1 review body (@sourcery-ai[bot]) with 3 suggestions; all classified as suggestion, none upgraded, no code change requests.
Root-Cause Investigation N/A No sub-tasks created — nothing to investigate.
Scope Containment PASS All 11 changed files map exactly onto the task spec (3 named + 8 golden fixtures); no out-of-scope or unimplemented files.
Diff Size PASS +7,686/-1,375 dominated by mechanical golden-JSON regeneration; source/test logic diff is small and proportionate.
Commit Traceability PASS The single commit body references Implements TC-5550.
Sensitive Patterns PASS No secrets/credentials/keys in 7,686 added lines; SHA-256 content values are artifact integrity hashes, not secrets.
CI Status PASS All 5 GitHub checks pass (Node 22 & 24 lint/test, Sourcery, PR title, commit messages).
Acceptance Criteria PASS 6 of 6 criteria met.
Test Quality PASS Repetitive Test Detection: PASS; Test Documentation: PASS; Eval Quality: N/A (no eval reviews).
Test Change Classification ADDITIVE Lenient compareSboms replaced with full deep.equal (now also checks hashes) + new artifact hash computation suite; zero reductive signals.
Verification Commands PASS All 28 gradle tests green; coverage 90.8% overall (java_gradle.js 94.84%), well above the 82% threshold.

Overall: PASS

No issues require attention. The implementation reuses the existing --init-script / structured-output pattern (no cache scanning), computes CycloneDX {alg: "SHA-256", content: hexDigest} hashes for both Groovy and Kotlin variants, and degrades gracefully on uninvokable Gradle / failing init script / fileless artifacts. The three Sourcery suggestions are optional maintainability improvements and were not upgraded to required changes.

Note: 17 pre-existing test failures in the OCI/python-pip/python-poetry suites are environmental (missing skopeo/docker/poetry, local package-version mismatches) and unrelated to this change — no gradle test is among them.


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

@a-oren
a-oren requested a review from Strum355 August 20, 2026 07:01
Comment thread src/providers/java_gradle.js Outdated
Comment on lines +339 to +342
console.warn('Gradle could not be invoked to compute artifact hashes, SBOM will be generated without hashes')
if (debug) {
console.error(`Gradle hash: selectToolBinary failed => ${error.stack || error.message}`)
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think the error message should be included in the console.warn and no need for the extra console.error

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Comment thread src/providers/java_gradle.js Outdated
Comment thread src/providers/java_gradle.js Outdated
@a-oren
a-oren force-pushed the TC-5550 branch 2 times, most recently from ae9d806 to 87669ea Compare August 25, 2026 13:03
@a-oren
a-oren requested a review from Strum355 August 25, 2026 13:12
@a-oren
a-oren force-pushed the TC-5550 branch 2 times, most recently from a6c4c71 to f630d1b Compare August 27, 2026 11:57
a-oren and others added 7 commits August 27, 2026 14:58
Compute SHA-256 hashes for Gradle dependency artifacts by asking Gradle
itself for the resolved artifact files via an init script, then hashing
those files with the Node.js crypto module. This mirrors the existing
init-script pattern (GRADLE_INIT_SCRIPT / discoverGradleSubprojects /
parseGradleInitScriptOutput) rather than scanning the local Gradle cache.

A new GRADLE_HASH_INIT_SCRIPT emits a `::DA_HASH::group:name:version::<file>`
line per resolved module artifact using a lenient artifact view, so fileless
artifacts (BOM/platform() dependencies) are skipped. parseGradleHashes builds
a Map<"group:name@version", [{alg, content}]> that is threaded through
Both Groovy and Kotlin variants inherit this behaviour.

Degrades gracefully: an uninvokable Gradle, a failing init script, or an
artifact with no readable file omits the hash rather than throwing.

Golden SBOM fixtures and the Groovy/Kotlin tests are updated to the
SBOM_CASES deep-equal pattern, with targeted tests for the exact digest
and graceful degradation.

Implements TC-5550

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Apply two lessons from the Maven SHA PR (guacsec#612) to the Gradle provider:

Key drift (lesson guacsec#1): derive both the stored hash-map key and the lookup
key from the canonical PURL (`toPurl(...).toString()` / `purl.toString()`),
the same builder parseDep uses, so the two keys cannot drift in formatting.
Refactor hashKeyFromComponentId into parseComponentId (parse/validate only);
the class method builds the canonical key.

Degradation warning (lesson guacsec#2): parseGradleHashes previously degraded
completely silently on every failure path. Emit a console.warn when gradle
cannot be invoked, the init script fails, hashing fails, or some resolved
artifacts cannot be read (with an attempted/missed count summary), mirroring
the pip/cargo providers so incomplete hash coverage is visible without
TRUSTIFY_DA_DEBUG.

Add regression tests (groovy + kotlin): canonical-key round trip for a
conflict-resolved (`->`) transitive dependency, and warning emission on the
partial-miss and failing-init-script paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The broad catch blocks in parseGradleHashes degrade to an SBOM without
hashes, which is correct for expected failures (gradle missing, init
script failing, unreadable artifacts) but also silently swallows genuine
programming errors as ordinary graceful degradation.

Capture the caught error on every path and log it (with stack for the
unexpected-failure path) when TRUSTIFY_DA_DEBUG is set, so real bugs are
observable during debugging without changing the graceful-degradation
behavior required by the feature.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tures

The HASH_FIXTURE_DIR constant and the extractCoordinates, artifactFileFor,
buildHashScriptOutput, getStubbedResponse and mockInvokeCommand helpers were
duplicated byte-for-byte between the Groovy and Kotlin Gradle test suites.
Extract them into a shared gradle_hash_test_utils.js so the two suites cannot
drift and are maintained in one place.

Also add cleanupHashFixtures() and call it from each suiteTeardown so the
stand-in artifact files written under the OS temp dir are removed after the
run, avoiding unbounded accumulation on long-lived CI agents.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…print

Replace readFileSync with createReadStream + event listeners when computing
SHA-256 hashes. Large artifacts (e.g., AWS SDK bundles ~300MB) now process
in chunks instead of loading entirely into memory, preventing OOM in
memory-constrained CI containers.

Additional simplifications:
- Remove #lookupHashes helper, inline hashMap?.get() at call-sites
- Consolidate error logging into single console.warn with message

Mirrors Maven streaming approach from PR guacsec#612.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…dlock

The Gradle hash map now streams artifact reads via createReadStream +
event listeners (the OOM fix for large jars). Sinon's default
useFakeTimers() also fakes process.nextTick/setImmediate, which Node
streams depend on, so the reader deadlocks. Restrict faking to Date
across all Gradle suites.

Additional fixes:
- Remove unused throws import
- Add await to all async provider method calls (provideStack,
  provideComponent, parseGradleHashes)
- Replace throws() with expect(...).to.be.rejected for async assertions
- Make all affected test functions async

Mirrors Maven streaming fix from commit eb00a42.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Include the error detail in the console.warn on the init-script and
unexpected-failure degradation paths, dropping the duplicate
debug-gated console.error (per PR review).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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