Gradle remediation pull request development
- Status: Implemented (PR creation verified in a validation repository, and Gradle builds verified on the PR branches)
- Started: November 2, 2026
- Last updated: November 2, 2026
Purpose
Create remediation PRs for Gradle projects that have gradle.lockfile by updating build definitions and gradle.lockfile in mapped directories from vulnerabilities that Trivy reports as jar for jar files in running images.
For supported inputs, see Gradle dependency files, and for update rules, see Gradle candidate selection.
Approach
- Regenerate the lockfile, which records Gradle's dependency resolution results, by running official Gradle
- Run Gradle in a worker container, using a worker Pod in Kubernetes
- Have KestreLynx rewrite version declarations and allow the worker container to change only
gradle.lockfile - Accept that the user's build scripts and plugin code run in the worker container
- Limit automatic updates to project forms that KestreLynx can verify by reading their files
- Have KestreLynx inspect the worker container's results without trusting them
- Scan
gradle.lockfilewith Trivy before and after the update and compare resolved, remaining, and new vulnerabilities
Decisions
Project forms eligible for automatic updates
Because reports from Gradle inside the worker container can be falsified, KestreLynx automatically updates only projects for which it can verify all the following conditions by reading the files, and requires manual action for the others.
| Condition | Files to check | What happens without the condition |
|---|---|---|
gradle.lockfile exists |
gradle.lockfile |
There is no lockfile to update and compare |
Locking is enabled for all configurations and the lock mode is STRICT |
lockAllConfigurations() and lockMode = LockMode.STRICT in build.gradle or build.gradle.kts |
Dependencies omitted from the lockfile do not stop the build and are not detected by Trivy comparison |
| No code capable of changing the locking settings runs (only allowed statements and plugin version combinations) | Build scripts, settings, gradle.properties, and the catalog |
Changes to the locking settings inside the worker container can go unnoticed |
settings-gradle.lockfile exists when a version catalog is used |
gradle/libs.versions.toml and settings-gradle.lockfile |
Gradle creates settings-gradle.lockfile during the update, failing the check that only gradle.lockfile may change |
For projects using a version catalog, the user must create settings-gradle.lockfile with --write-locks and commit it beforehand. In the verification application, empty=incomingCatalogForLibs0 was recorded in this file.
For allowed statements and forms requiring manual action, see Build script forms, and for plugin version combinations, see Allowed plugins.
Updating versions
Choose where to make the edit according to where the version is determined.
| Where the version is determined | KestreLynx edit | Verification example | Guidance in the PR body |
|---|---|---|---|
| Catalog or coordinate string in the build script | Change the declared version value | commons-text 1.9 → 1.10.0 | None |
| Spring Boot BOM property | Change the value in gradle.properties or the build script's extra or ext (append to gradle.properties if unset) |
tomcat.version 11.0.5 → 11.0.25 |
Remove the override after upgrading the Spring Boot plugin to a version containing the fix |
| Transitive dependency with no version declaration anywhere | Append constraints to the build script | Specify 3.18.0 for commons-lang3 | Remove the constraints after the dependency that brings it in is upgraded to a version that requires the fixed version |
Changing a BOM property or catalog value also changes other packages whose versions are determined by the same value, so verify that those versions exist on Maven Central as well. Verification confirmed that version 11.0.25 existed for the six packages whose versions were determined by tomcat.version.
For target version rules, see Target versions, and for rewrite rules, see Version declarations and edits.
Worker containers
Pin the official gradle image by digest and select Gradle and JDK versions that match the project.
| Item | Selection rule | Verification example |
|---|---|---|
| Gradle version | Use gradle from an official image with the same version as the wrapper |
9.8.2 |
| JDK version | Use a version matching the toolchain | JDK 17 for the application with a Java 17 toolchain |
| Allowed combinations | Only combinations that have passed integration tests on the execution platform | Three combinations on amd64 |
| Automatic JDK downloads | Disable | — |
The official image declares /home/gradle/.gradle as a VOLUME, and leaving it uncovered creates an anonymous volume in Docker or a volume on the node's disk in Kubernetes, so cover it as follows for each execution environment.
| Execution environment | How /home/gradle/.gradle is covered |
|---|---|
| Docker | Cover with tmpfs |
| Kubernetes | Cover with an empty read-only memory volume with a 1 MiB size limit |
For execution settings, see Gradle execution settings.
Checking the returned results
Check the worker container's results in the following order before creating a PR.
| Order | What to check | If the check fails |
|---|---|---|
| 1 | All input files are returned, and the worker container changed only gradle.lockfile |
Reject the results |
| 2 | The lockfile's configuration set and empty= set are unchanged, no entries are removed from any configuration, targets reach at least their target versions, and added or changed coordinates exist on Maven Central |
Reject the results (configuration changes and entry removals require manual action) |
| 3 | Trivy comparison of gradle.lockfile before and after the update shows that target findings confirmed before the update are gone, no new findings appear, and all entries appear in the respective Trivy results |
Reject the results, or mark as not comparable if entries are missing |
| 4 | The PR body lists packages whose versions changed outside the targets and newly added packages | Not applicable |
In the verification application using a version catalog and constraints, the rewritten catalog and constraints were returned unchanged, and commons-text and commons-lang3 in the lockfile reached their target versions.
For acceptance conditions, see Gradle update validation, and for exclusion conditions, see Conditions for excluding targets and updating the rest.
Remaining limitations
KestreLynx's checks cannot prevent or detect the following three issues, so address them through other measures.
| Limitation | What happens | Mitigation |
|---|---|---|
| The user's code runs in the worker container | Gradle executes build scripts and plugin code, so malicious code also runs inside the worker container | Limit the damage by providing no secrets, running as a non-root user, and restricting writes to temporary storage with size limits |
| Dependencies remain at older versions | Versions in the lockfile act as fixed constraints, so the build passes with an older locked version even when an updated package requires a newer dependency version (for example, commons-text 1.10.0 requires commons-lang3 3.12.0, but the build passes with 3.11 from the lockfile) | Advise in the PR body to build and test in CI using the lockfile unchanged (./gradlew dependencies && ./gradlew build) |
| Components bundled into artifacts by plugins | The Spring Boot plugin bundles launcher classes and similar components into the jar at the plugin's own version, so they do not appear in the lockfile and remain unchanged when the lockfile is updated | Report them as findings from scanning the running image and require manual action |
Running --write-locks or --update-locks first in CI replaces the checked lockfile, so state in the PR body that these must not be run beforehand.
Verification method
Unit tests
- Verified reading of Groovy and Kotlin build scripts, catalogs,
gradle.properties, wrappers, and lockfiles - Verified that rewriting version declarations preserves formatting and comments
- Verified inspection of results returned by worker containers and the contents of PR bodies
Integration tests
- Verified lockfile creation and updates with
--update-locksusing officialgradleimages - Verified Gradle 8.14.6 with JDK 21, Gradle 9.8.2 with JDK 17, and Gradle 9.8.2 with JDK 21 on amd64
Applications used for verification
flowchart LR
subgraph A["gradleapp (Groovy DSL)"]
AC["gradle/libs.versions.toml<br/>commons-text = 1.9"]
AB["build.gradle<br/>application plugin<br/>implementation libs.commons.text"]
AC -->|"Version declaration"| AB
AB -->|"Direct dependency"| AT["commons-text 1.9"]
AT -->|"Transitive dependency"| AL["commons-lang3 3.11"]
AB -->|"gradle installDist"| AJ["gradleapp image<br/>Library jars in the distribution's lib/"]
end
subgraph B["gradleboot (Kotlin DSL)"]
BB["build.gradle.kts<br/>Spring Boot 4.1.2<br/>dependency-management 1.1.7"]
BP["tomcat.version = 11.0.5 in extra"]
BB -->|"Direct dependency"| BT["commons-text 1.9"]
BP -->|"Override BOM property"| BC["tomcat-embed-core 11.0.5"]
BB -->|"Uses spring-boot-starter-webmvc"| BC
BB -->|"gradle bootJar"| BJ["gradleboot image<br/>Library jars in BOOT-INF/lib/"]
end
Both applications used Gradle 9.8.2 for the wrapper and configured locking for all configurations and STRICT in dependencyLocking.
| Library | Application | Running version | Where the version is determined | Fixed version |
|---|---|---|---|---|
| commons-text | Both | 1.9 | Catalog [versions] in gradleapp; a coordinate string in the build script in gradleboot |
1.10.0 |
| commons-lang3 | gradleapp | 3.11 | No version in the build definition because commons-text brings it in transitively | 3.18.0 |
| tomcat-embed-core | gradleboot | 11.0.5 | The Spring Boot BOM property tomcat.version, overridden in the build script's extra |
11.0.25 |
Real environment
- Verified the workflow for both applications from remediation planning through PR creation and builds on the PR branches with local Docker
- Verified lockfile updates in worker Pods and PR creation on K3s (v1.36.5) on a validation host
Verification results
Scanned both applications with Trivy 0.71.2 on November 2, 2026.
Remediation plans and PR creation
Created both PRs in the validation repository from remediation plans, and both worker containers exited with code 0.
| Application | Execution environment | Main changes | Scans before and after the update (resolved/remaining/new) |
|---|---|---|---|
| gradleboot | Official image with Gradle 9.8.2 and JDK 17 | Changed commons-text in build.gradle.kts from 1.9 → 1.10.0 and tomcat.version from 11.0.5 → 11.0.25, and updated gradle.lockfile |
25/0/0 |
| gradleapp | Official image with Gradle 9.8.2 and JDK 21 | Changed commons-text in the catalog from 1.9 → 1.10.0, added constraints for commons-lang3 3.18.0 to build.gradle, and updated gradle.lockfile |
2/0/0 |
- gradleboot resolved CVE-2022-42889 in commons-text and 24 vulnerabilities in tomcat-embed-core
- Also updated
tomcat-embed-elandtomcat-embed-websocketfrom 11.0.5 → 11.0.25 - Verified that the 6 packages whose versions are determined by the same property exist at 11.0.25 on Maven Central, and advised in the PR body to remove the override when upgrading the Spring Boot plugin to a version that includes the fix
- gradleapp resolved CVE-2025-48924 and CVE-2022-42889
Checks on the PR branches
Used official gradle:9.8.2 images with the files on the PR branches unchanged.
- gradleboot passed
gradle dependencies buildwith JDK 17 - Verified
commons-text-1.10.0.jarandtomcat-embed-core-11.0.25.jarinbootJar'sBOOT-INF/lib/ - gradleapp passed
gradle build installDistwith JDK 21 - Verified
commons-text-1.10.0.jarandcommons-lang3-3.18.0.jarin the distribution'slib/ - Neither application had lock state errors
Checks in Kubernetes
- Ran the 2 applications as Deployments on K3s (v1.36.5); worker Pod updates and PR creation succeeded for both, with 0 requiring manual action
- Admission policy checks in each cycle and the startup check for the read-only memory volume covering
/home/gradle/.gradlein worker Pods passed - Changed files, diffs, and resolved/remaining/new counts matched local Docker for both applications
Unverified areas and next steps
- Add supported worker container combinations
- Verify execution on arm64 and combinations of other Spring Boot and dependency-management versions with other Gradle versions, and add verified combinations to the table
- Expand the project forms eligible for automatic updates
- Verify the toolchain auto-resolution plugin
org.gradle.toolchains.foojay-resolver-convention, for which all versions currently require manual action, and consider support for projects with multiple projects - Check lower bounds on dependency versions required by new versions
- Add the checks already performed for Maven after examining differences involving Gradle Module Metadata
Update history
November 2, 2026
- Design and implementation
- Verified PR creation in Docker and Kubernetes