Skip to content

fix: support Gradle version catalog (libs.versions.toml) in analysis - #668

Merged
Strum355 merged 3 commits into
guacsec:mainfrom
Strum355:TC-6860
Oct 13, 2026
Merged

Strum355 merged 3 commits into
guacsec:mainfrom
Strum355:TC-6860

Conversation

@Strum355

@Strum355 Strum355 commented Oct 12, 2026 •

Copy link
Copy Markdown
Member

Summary

  • Gradle version catalog files (.versions.toml) were discovered by
    findManifests() but rejected by match() — no provider recognised them.
    This caused silent 0 remediations and check-mode hard failures.
  • The Gradle provider now accepts .versions.toml in isSupported() and
    resolves to the parent build.gradle[.kts] for SBOM generation via
    provideStack() / provideComponent().
  • validateLockFile() checks for a parent build file so orphan TOML files
    are rejected at match time rather than failing downstream.

Knock-on effects

  • stackAnalysis() and componentAnalysis() in index.js also flow
    through match(), so they gain TOML support with no code changes.
  • The VS Code extension filters manifests before calling the client
    (only build.gradle[.kts]), so this change has no effect there until
    the extension adds its own TOML support.
  • validateLockFile() now performs two existsSync checks where it
    previously returned true unconditionally. For regular build.gradle
    files the first check trivially passes (the file is in its own directory).

Test plan

  • provideStack with TOML path produces same SBOM as build.gradle path (Groovy + Kotlin)
  • provideStack throws for TOML with no parent build file (Groovy + Kotlin)
  • isSupported returns true for libs.versions.toml (Groovy + Kotlin)
  • All existing provider, remediate, and match tests pass (88 total)

Implements TC-6860

🤖 Generated with Claude Code

Summary by Sourcery

Enable Gradle analysis to accept version catalog manifests by resolving them to their parent build files.

New Features:

  • Support Gradle version catalog files (libs.versions.toml) in stack and component analysis.

Bug Fixes:

  • Prevent unsupported or orphaned version catalog files from causing silent remediation failures or downstream analysis errors.
  • Use the resolved Gradle build file for license checking when analysis starts from a version catalog.

Enhancements:

  • Resolve version catalog paths to nearby build.gradle or build.gradle.kts files and expose the resolved manifest path.

Tests:

  • Add Groovy and Kotlin Gradle provider coverage for version catalog support, build-file resolution, missing parent files, and manifest path reporting.

Chores:

  • Correct JavaScript import annotations and asynchronous return-type documentation.

@sourcery-ai

sourcery-ai Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

Gradle version catalog files are now accepted by both Gradle providers, resolved to a parent build script for SBOM and license analysis, and rejected early when orphaned; provider tests cover Groovy and Kotlin paths, resolution, metadata, and failure cases.

Sequence diagram for Gradle version catalog analysis

sequenceDiagram
    participant Caller
    participant Match
    participant GradleProvider
    participant FileSystem
    participant Analysis
    participant LicenseCheck

    Caller->>Match: match(libs.versions.toml)
    Match->>GradleProvider: isSupported(manifestName)
    GradleProvider-->>Match: true
    Match->>GradleProvider: validateLockFile(manifestDir)
    GradleProvider->>FileSystem: existsSync(build.gradle[.kts])
    FileSystem-->>GradleProvider: parent build file exists
    GradleProvider-->>Match: true
    Caller->>GradleProvider: provideStack(libs.versions.toml)
    GradleProvider->>FileSystem: existsSync(build.gradle.kts)
    FileSystem-->>GradleProvider: path or fallback to build.gradle
    GradleProvider-->>Caller: Provided(SBOM, manifestPath=build.gradle[.kts])
    Caller->>Analysis: requestStack(provider, manifest)
    Analysis->>LicenseCheck: runLicenseCheck(content, manifestPath)
    LicenseCheck-->>Analysis: license summary
Loading

Flow diagram for Gradle version catalog resolution

flowchart TD
    A[libs.versions.toml discovered] --> B[Java_gradle.isSupported]
    B -->|supported| C[Java_gradle.validateLockFile]
    C --> D{Parent build.gradle.kts exists?}
    D -->|yes| E[Resolve build.gradle.kts]
    D -->|no| F{Parent build.gradle exists?}
    F -->|yes| G[Resolve build.gradle]
    F -->|no| H[Reject orphan TOML]
    E --> I[provideStack or provideComponent]
    G --> I
    I --> J[Generate SBOM]
    I --> K[Expose manifestPath]
    K --> L[License check uses resolved build file]
Loading

File-Level Changes

Change Details Files
Add Gradle version catalog files as analyzable manifests by resolving them to a parent build script.
  • Recognize .versions.toml paths for both Groovy and Kotlin Gradle providers.
  • Resolve catalogs to the nearest project-level build.gradle.kts or build.gradle before generating SBOMs.
  • Reject orphan catalogs when no parent build script exists.
  • Expose the resolved build path for downstream license checking.
src/providers/java_gradle.js
src/provider.js
Use the provider’s resolved manifest path when performing license checks.
  • Pass provided.manifestPath to license checking when available, while preserving the original path for regular manifests.
src/analysis.js
Add coverage for version catalog recognition and resolution behavior.
  • Test Groovy and Kotlin support detection.
  • Verify catalog and build-script inputs produce equivalent stack SBOMs.
  • Cover missing-parent errors and resolved manifestPath behavior for stack and component analysis.
test/providers/java_gradle_groovy.test.js
test/providers/java_gradle_kotlin.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

@Strum355
Strum355 requested review from a-oren and ruromero and removed request for ruromero October 12, 2026 11:39

@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 7 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/providers/java_gradle.js" line_range="70" />
<code_context>

 	isSupported(manifestName) {
 		return this._getManifestName() === manifestName
+			|| manifestName.endsWith('.versions.toml')
 	}

</code_context>
<issue_to_address>
**Interrupted writes corrupt catalogs**

When a catalog update is interrupted or the filesystem runs out of space during the write, a remediation that reaches the TOML updater writes the entire catalog directly to its existing path; if the write fails after truncation, such as from a full disk, the catalog is left partial or empty and subsequent Gradle builds fail.

Write the updated content to a temporary file in the same directory and atomically rename it over the catalog only after the write succeeds.
</issue_to_address>

### Comment 2
<location path="src/providers/java_gradle.js" line_range="112-114" />
<code_context>
+		}
+		const projectRoot = path.dirname(path.dirname(manifest))
+		for (const name of ['build.gradle.kts', 'build.gradle']) {
+			const candidate = path.join(projectRoot, name)
+			if (fs.existsSync(candidate)) {
+				return candidate
+			}
+		}
</code_context>
<issue_to_address>
**The wrong Gradle DSL is parsed**

When both `build.gradle` and `build.gradle.kts` exist, `match()` selects the Groovy provider, but `#resolveBuildManifest()` prefers the Kotlin build file. The Groovy provider then parses Kotlin exclusions or ignore directives with Groovy rules, so excluded or ignored dependencies remain in the SBOM.

Resolve the build file that corresponds to the provider selected by `match()`.

Also at `src/providers/java_gradle.js:70`.
</issue_to_address>

### Comment 3
<location path="src/providers/java_gradle.js" line_range="124" />
<code_context>
 		return {
 			ecosystem: ecosystem_gradle,
-			content: await this.#createSbomStackAnalysis(manifest, opts),
+			content: await this.#createSbomStackAnalysis(buildManifest, opts),
 			contentType: 'application/vnd.cyclonedx+json'
 		}
</code_context>
<issue_to_address>
**Uneditable dependencies reported as fixes**

When the Gradle dependency graph contains a vulnerable dependency that has no matching library entry in the selected TOML catalog, the build-file SBOM includes transitive dependencies and dependencies declared outside the version catalog. `runRemediation()` retains those recommendations when `perDependencyChanges` is false, while `updateTomlVersions()` skips entries absent from the catalog; the report therefore shows fixes that were not applied.

Only return a remediation when the TOML updater confirms it can apply that change, including in the default remediation mode.
</issue_to_address>

### Comment 4
<location path="src/providers/java_gradle.js" line_range="140" />
<code_context>
 		return {
 			ecosystem: ecosystem_gradle,
-			content: await this.#getSbomForComponentAnalysis(manifest, opts),
+			content: await this.#getSbomForComponentAnalysis(buildManifest, opts),
 			contentType: 'application/vnd.cyclonedx+json'
 		}
</code_context>
<issue_to_address>
**Project license is missed**

When component analysis or project-license reporting uses a catalog under `gradle/` and the project `LICENSE` is at the root, `requestComponent()` passes the original TOML path to `runLicenseCheck()`, which searches beside the catalog instead of the project root. The project license is missed, so component analysis omits project-license results and the compatibility baseline.

Run the license check using the resolved build path or otherwise search from the project root.

Also at `src/providers/java_gradle.js:70`.
</issue_to_address>

### Comment 5
<location path="src/providers/java_gradle.js" line_range="70" />
<code_context>

 	isSupported(manifestName) {
 		return this._getManifestName() === manifestName
+			|| manifestName.endsWith('.versions.toml')
 	}

</code_context>
<issue_to_address>
**Catalog remediation entries are duplicated**

When a Gradle project has multiple discovered version catalogs, `runRemediation()` analyzes the same parent build once per catalog and appends the project-wide recommendations each time, including fixes for dependencies absent from a catalog. Callers receive duplicate or inapplicable remediation entries.

Avoid appending the same build’s recommendations per catalog and include only fixes applicable to the catalog.
</issue_to_address>

### Comment 6
<location path="src/providers/java_gradle.js" line_range="70" />
<code_context>

 	isSupported(manifestName) {
 		return this._getManifestName() === manifestName
+			|| manifestName.endsWith('.versions.toml')
 	}

</code_context>
<issue_to_address>
**Concurrent fixes overwrite each other**

When two remediation jobs update different dependencies in the same version catalog concurrently, both jobs read the catalog and write full replacements from their in-memory copies. The later write discards the earlier job’s fix, so a requested upgrade is missing from the file.

Serialize catalog updates or merge each update with the latest catalog contents before writing.
</issue_to_address>

### Comment 7
<location path="src/providers/java_gradle.js" line_range="82" />
<code_context>
 	isSupported(manifestName) {
 		return this._getManifestName() === manifestName
+			|| manifestName.endsWith('.versions.toml')
 	}

 	/**
</code_context>
<issue_to_address>
**Orphan catalogs stay invisible**

When an orphan `.versions.toml` is processed through remediation, `match` rejects an orphan catalog, but `runRemediation` catches that error and continues, so the newly detected invalid catalog is silently skipped and remediation can still report no fixes without explaining why.

Surface the match error for discovered version catalogs instead of silently continuing.
</issue_to_address>

Sourcery assessment

Approval pending. 7 findings to address first.

Blocking findings: src/providers/java_gradle.js:70, src/providers/java_gradle.js:114, src/providers/java_gradle.js:124, src/providers/java_gradle.js:140, src/providers/java_gradle.js:70, and 2 more


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread src/providers/java_gradle.js
Comment thread src/providers/java_gradle.js Outdated
Comment thread src/providers/java_gradle.js
Comment thread src/providers/java_gradle.js
Comment thread src/providers/java_gradle.js
Comment thread src/providers/java_gradle.js
Comment thread src/providers/java_gradle.js
@codecov-commenter

codecov-commenter commented Oct 12, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.55172% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.96%. Comparing base (8a87e14) to head (01b779b).

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

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #668      +/-   ##
==========================================
+ Coverage   91.94%   91.96%   +0.01%     
==========================================
  Files          45       45              
  Lines       10996    11045      +49     
  Branches     2002     2019      +17     
==========================================
+ Hits        10110    10157      +47     
- Misses        886      888       +2     
Flag Coverage Δ
unit-tests 91.96% <96.55%> (+0.01%) ⬆️

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

Files with missing lines Coverage Δ
src/provider.js 100.00% <100.00%> (ø)
src/providers/java_gradle.js 94.51% <96.49%> (+0.08%) ⬆️
🚀 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.

@Strum355

Copy link
Copy Markdown
Member Author

@sourcery-ai review

@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

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/providers/java_gradle.js" line_range="70" />
<code_context>

 	isSupported(manifestName) {
 		return this._getManifestName() === manifestName
+			|| manifestName.endsWith('.versions.toml')
 	}

</code_context>
<issue_to_address>
**Shared version updates falsely succeed**

When the analysis report recommends different fixed versions for libraries sharing one `version.ref`, `updateTomlVersions` reads the original version for each change but applies replacements to already-updated content. When two dependencies sharing a `version.ref` have different target versions, the first replacement wins, the next replacement does not match, and the updater still records it as applied; the catalog retains one version while remediation reports both fixes.

Detect conflicting changes per shared ref and either reconcile them before editing or report the conflict without claiming every remediation was applied.
</issue_to_address>

### Comment 2
<location path="test/providers/java_gradle_groovy.test.js" line_range="94" />
<code_context>
+
+			let result = await provider.provideStack(`${dir}/gradle/libs.versions.toml`)
+
+			expect(JSON.parse(result.content)).to.deep.equal(JSON.parse(expectedSbom))
+		}).timeout(process.env.GITHUB_ACTIONS ? 40000 : 10000)
+
</code_context>
<issue_to_address>
**Wrong build path passes SBOM tests**

When the provider invokes Gradle using the original TOML path rather than the resolved build-file path, the TOML-path SBOM tests still pass if `provideStack()` passes the TOML path to Gradle instead of the resolved build file: `mockInvokeCommand` returns the same fixture output regardless of the command arguments or working directory, so the deep-equality assertions do not verify which path was used.

Have the stub assert the Gradle invocation’s working directory or otherwise verify that the resolved build path is used.

Also at `test/providers/java_gradle_kotlin.test.js:94`.
</issue_to_address>

Sourcery assessment

Approval pending. 2 findings to address first.

Blocking findings: src/providers/java_gradle.js:70, test/providers/java_gradle_groovy.test.js:94


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread src/providers/java_gradle.js
Comment thread test/providers/java_gradle_groovy.test.js
Comment thread src/providers/java_gradle.js
Comment thread src/providers/java_gradle.js Outdated
Strum355 and others added 3 commits October 13, 2026 12:41
The Gradle provider's isSupported() only matched build.gradle[.kts],
so match() threw for .versions.toml files — causing silent 0 results
in remediate mode and a hard failure in check mode.

Teach the Gradle provider to recognise .versions.toml files and resolve
them to the parent build.gradle[.kts] for SBOM generation. The updater
layer (updateTomlVersions) already handles writing fixes back to the
TOML file, so no changes are needed outside the provider.

validateLockFile() now checks the parent directory for a build file
when the manifest lives in a subdirectory (the gradle/ convention),
which also means match() correctly rejects orphan TOML files with no
parent build file.

Implements TC-6860

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Assisted-by: Claude Code
When component analysis receives a TOML version catalog path (e.g.
gradle/libs.versions.toml), the license check searched for LICENSE
files next to the catalog instead of the project root. The provider
now returns the resolved build file path via an optional manifestPath
field in the Provided type, and requestComponent uses it for the
license check.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@Strum355
Strum355 merged commit f53e5d1 into guacsec:main Oct 13, 2026
9 checks passed
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