Gradle plugin
The RingZero Gradle plugin resolves your build’s real dependency graph — direct
and transitive, after Gradle’s version conflict resolution — and uploads a
dependency snapshot to RingZero for advisory matching. Build gating (failing
check when the scan reports findings above a severity threshold) ships with
the upcoming ringzeroCheck task — see
Policy gates.
Requirements
Section titled “Requirements”| Component | Minimum |
|---|---|
| Gradle | 7.6 (configuration cache supported from 8.1) |
| JVM running the build | 11 |
| Plugin version | 0.1.0 (current) |
The plugin runs on the build JVM only — your project’s toolchains and targets are unaffected.
Installation
Section titled “Installation”Apply the plugin in the project you want scanned:
plugins { id("tech.ringzero.sca") version "0.1.0"}plugins { id 'tech.ringzero.sca' version '0.1.0'}Multi-project builds
Section titled “Multi-project builds”For a multi-project build, declare the version once in settings and apply the plugin to the root project only. The plugin aggregates the resolved graph of every subproject into a single snapshot — one snapshot, one scan — so transitive conflicts across subprojects are reported the way Gradle actually resolves them:
plugins { id("tech.ringzero.sca") version "0.1.0" apply false}
// root build.gradle.ktsplugins { id("tech.ringzero.sca")}plugins { id 'tech.ringzero.sca' version '0.1.0' apply false}
// root build.gradleplugins { id 'tech.ringzero.sca'}Root-only application is enforced: applying the plugin to more than one
project in the same build is a configuration error — ringzeroScan fails at
configuration time naming both projects. There are no per-subproject
ringzero { } override blocks; scope subprojects with
scan.excludeProjects and
path-qualified configuration names (":benchmark:runtimeClasspath").
If you can’t use the plugins block (legacy builds), the artifact coordinates
are tech.ringzero:ringzero-gradle-plugin:0.1.0 on the Gradle Plugin Portal
and Maven Central:
buildscript { repositories { gradlePluginPortal() } dependencies { classpath 'tech.ringzero:ringzero-gradle-plugin:0.1.0' }}apply plugin: 'tech.ringzero.sca'Provide your API key — securely
Section titled “Provide your API key — securely”Uploads authenticate with a scoped API key (rz_key_…), issued once by an
org admin in Settings → API keys. Create a dedicated key for this build
with the minimum it needs:
- Scopes:
sca:upload+scans:read. That coversringzeroScanuploads today and, onceringzeroCheckships, polling severity counts. Addfindings:readonly if you want the per-CVE table in build output — without it, a blocked build still fails correctly and prints counts plus a link to the scan in the app. CVE detail never appears inringzeroScanoutput — the snapshot upload carries no vulnerability data; advisory matching happens server-side. - Projects: restricted to just this project.
See Authentication & permissions for the full scope and role model.
The plugin looks for a key in this order:
- The
apiKeyproperty in theringzero { }DSL (highest precedence) - The
RINGZERO_API_KEYenvironment variable - The
ringzero.apiKeyGradle property (e.g.~/.gradle/gradle.properties)
Local development — put the key in your user-level Gradle properties file, which lives outside any repository:
# ~/.gradle/gradle.properties (never committed)ringzero.apiKey=rz_key_5f2a…CI — inject the key from your secret store as RINGZERO_API_KEY (see the
GitHub Actions example below). The plugin never logs the key
and masks it in --info/--debug output.
Explicit wiring — if you need to wire the key yourself (custom resolution order, a different env var, a file-based secret), use the Provider API so the value stays lazy and configuration-cache-friendly:
ringzero { apiKey = providers.environmentVariable("RINGZERO_API_KEY") .orElse(providers.gradleProperty("ringzero.apiKey"))}ringzero { apiKey = providers.environmentVariable('RINGZERO_API_KEY') .orElse(providers.gradleProperty('ringzero.apiKey'))}Associate the build with your org and project
Section titled “Associate the build with your org and project”Tell RingZero which project the snapshot belongs to. Find both ids in the app — the org id under Settings → General, the project id on the project’s settings page:
ringzero { organization = "org_9f3k" project = "prj_6h2m" // or the project slug, e.g. "acme-web"}ringzero { organization = 'org_9f3k' project = 'prj_6h2m' // or the project slug, e.g. 'acme-web'}If project is omitted, the plugin uses the root project name — fine for a
first run, but pin it explicitly so renames don’t fork your scan history. A
slug that doesn’t exist server-side fails the upload with
422 unknown_project (the plugin prints the slug it tried and where to find
the real id) — RingZero never auto-creates projects from uploads.
| Task | What it does |
|---|---|
ringzeroScan | Resolves the configured configurations (default: runtimeClasspath of every project), builds the dependency snapshot, and uploads it. Prints the scan id and a link to the scan in the app. |
ringzeroCheck Coming soon | Will depend on ringzeroScan; polls the scan until it completes, evaluates your failOn policy, and fails the build on violation. |
Run a one-off scan:
./gradlew ringzeroScan> Task :ringzeroScanRingZero: snapshot of 143 dependencies (12 direct, 131 transitive) from 4 projectsRingZero: uploaded — scan scn_7g8h queuedRingZero: https://app.ringzero.tech/scans/scn_7g8hringzeroScan never blocks on scan results: the upload carries a pure
dependency graph (no vulnerability data), advisory matching runs server-side,
and the findings appear in the app. Your build finishes as soon as the upload
is accepted.
Report-only mode (no API key)
Section titled “Report-only mode (no API key)”With no API key configured, ringzeroScan still resolves the graph and
writes snapshot.json — it just skips the upload and succeeds:
> Task :ringzeroScanRingZero: snapshot of 143 dependencies (12 direct, 131 transitive) from 4 projectsRingZero: no API key configured — snapshot written to build/ringzero/snapshot.json, upload skipped. Set RINGZERO_API_KEY (or ringzero.apiKey) to upload.This is the zero-friction first run: inspect exactly what would be uploaded
before creating a key. The same behavior applies under
gradle --offline, and
scan.json records why nothing was uploaded
({ "uploaded": false, "reason": "no-api-key" | "offline" }).
Fail the build on policy violations (coming soon)
Section titled “Fail the build on policy violations (coming soon)”Once shipped, you declare a threshold (failOn { severity = "high" }), wire
ringzeroCheck onto check, and a blocked build reports exactly what tripped
the gate:
> Task :ringzeroCheck FAILEDRingZero: scan scn_7g8h completed — 1 high, 3 medium, 9 low
HIGH CVE-2025-4410 com.alibaba:fastjson 1.2.68 → fix: 1.2.83 via: build.gradle > acme-web-core > fastjson fastjson deserialization of untrusted input can lead to RCE
FAILURE: Build failed with an exception.* What went wrong:Execution failed for task ':ringzeroCheck'.> 1 finding at or above severity 'high' (policy: failOn.severity=high) Details: https://app.ringzero.tech/scans/scn_7g8hThe per-CVE lines require findings:read; with only scans:read the failure
shows the counts and the scan link (see the
key recipes for the trade-off). Either way, CVE
detail comes from RingZero’s API after the server finishes advisory matching —
never from the build itself.
To silence a triaged finding without losing sight of it — with a required
reason and an optional expiry — use a committed
suppressions file
(ringzero-suppressions.yml): suppressed findings drop out of the severity
counts and the gate but stay visible in the app and API.
The full policy surface (CVSS thresholds, fixableOnly/reachableOnly
filters, grace periods for freshly published advisories, behavior when the
API is unreachable) is in the
configuration reference.
Running in CI
Section titled “Running in CI”GitHub Actions example — key from the repository’s encrypted secrets, a fresh snapshot uploaded on every PR:
name: Build
on: pull_request:
jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-java@v4 with: distribution: temurin java-version: 21 - uses: gradle/actions/setup-gradle@v4 - name: Build and scan env: RINGZERO_API_KEY: ${{ secrets.RINGZERO_API_KEY }} run: ./gradlew build ringzeroScanWhen ringzeroCheck ships, swapping ringzeroScan for ringzeroCheck in
this step turns the upload into a policy gate.
The same pattern applies to any CI system: store the key in the platform’s
secret store and expose it to the build as RINGZERO_API_KEY. Never echo it,
and never pass it as a -P command-line property (command lines end up in
process listings and CI logs).
Concurrent jobs are safe: uploads are idempotent on the snapshot’s content hash, so two jobs building the same commit converge on one scan instead of creating duplicates.
A complete example
Section titled “A complete example”plugins { `java-library` id("tech.ringzero.sca") version "0.1.0"}
ringzero { organization = "org_9f3k" project = "acme-web"
scan { configurations = listOf("runtimeClasspath") includeTransitive = true }}plugins { id 'java-library' id 'tech.ringzero.sca' version '0.1.0'}
ringzero { organization = 'org_9f3k' project = 'acme-web'
scan { configurations = ['runtimeClasspath'] includeTransitive = true }}Troubleshooting & diagnostics
Section titled “Troubleshooting & diagnostics”Run with --info to see what the plugin actually did:
- the projects and configurations selected (after
excludedConfigurations/excludeProjects), - counts of excluded modules (matched/expired
suppression counts arrive
with
ringzeroCheck), - the snapshot file path and byte size,
- the API base URL in use and which precedence source supplied it (DSL, env, or gradle.properties),
- request ids (
X-Request-Id) for every API call — quote these to support, - retry/backoff events.
The API key is never logged and is masked in --info/--debug output.
Common failures:
| Symptom | Cause | Fix |
|---|---|---|
401 invalid_token on upload | Key missing/revoked/expired, or the env var isn’t reaching the Gradle daemon | Check the resolution order; restart the daemon after changing env; re-issue in Settings → API keys |
403 insufficient_scope | Key lacks a scope the task needs | ringzeroScan needs sca:upload — see recipes |
422 unknown_project | Slug/id doesn’t exist or isn’t in the key’s project restriction | Pin project to the id from the project’s settings page |
| Snapshot written but nothing uploaded | No API key resolved — the task ran in report-only mode | Provide the key via RINGZERO_API_KEY or ringzero.apiKey; the log line names the mode |
| Build passes locally, fails in CI | Different key scopes, or CI resolves different dependency versions | Compare the --info selection output; pin versions or use dependency locking |
Next steps
Section titled “Next steps”- Every DSL block and property — exclusions, suppressions, policy gates, offline behavior: Configuration reference
- The API the plugin calls — for direct integrations and debugging: REST API reference