Skip to content

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

Open
a-oren wants to merge 4 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 4 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 SHA-256 hash computation for Gradle dependency artifacts and propagate these hashes into SBOMs for both stack and component analysis while ensuring graceful degradation and updated test coverage.

New Features:

  • Compute and attach SHA-256 hashes for Gradle dependency artifacts in generated SBOMs for both Groovy and Kotlin providers.

Enhancements:

  • Introduce a Gradle init script and parsing utilities to obtain resolved artifact files directly from Gradle and map them to CycloneDX hash entries.
  • Refactor Gradle provider tests to use direct deep-equality assertions for SBOM fixtures and to share command-stubbing helpers across cases.

Tests:

  • Add Gradle provider tests covering SHA-256 hash computation, omission of hashes for fileless artifacts, and graceful degradation when hash collection fails.

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
#buildSbom() and #buildDirectDependenciesSbom() into sbom.addDependency().
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>
@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 89.56044% with 19 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.18%. Comparing base (61444d4) to head (3c483e5).

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

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #614      +/-   ##
==========================================
- Coverage   91.22%   91.18%   -0.04%     
==========================================
  Files          43       43              
  Lines        9558     9731     +173     
  Branches     1717     1739      +22     
==========================================
+ Hits         8719     8873     +154     
- Misses        839      858      +19     
Flag Coverage Δ
unit-tests 91.18% <89.56%> (-0.04%) ⬇️

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.72% <89.56%> (-1.69%) ⬇️
🚀 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 and others added 3 commits August 17, 2026 14:46
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>
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.

2 participants