Skip to content

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.

ComponentMinimum
Gradle7.6 (configuration cache supported from 8.1)
JVM running the build11
Plugin version0.1.0 (current)

The plugin runs on the build JVM only — your project’s toolchains and targets are unaffected.

Apply the plugin in the project you want scanned:

build.gradle.kts
plugins {
id("tech.ringzero.sca") version "0.1.0"
}

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:

settings.gradle.kts
plugins {
id("tech.ringzero.sca") version "0.1.0" apply false
}
// root build.gradle.kts
plugins {
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'

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 covers ringzeroScan uploads today and, once ringzeroCheck ships, polling severity counts. Add findings:read only 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 in ringzeroScan output — 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:

  1. The apiKey property in the ringzero { } DSL (highest precedence)
  2. The RINGZERO_API_KEY environment variable
  3. The ringzero.apiKey Gradle 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"))
}

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"
}

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.

TaskWhat it does
ringzeroScanResolves 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 soonWill depend on ringzeroScan; polls the scan until it completes, evaluates your failOn policy, and fails the build on violation.

Run a one-off scan:

Terminal window
./gradlew ringzeroScan
> Task :ringzeroScan
RingZero: snapshot of 143 dependencies (12 direct, 131 transitive) from 4 projects
RingZero: uploaded — scan scn_7g8h queued
RingZero: https://app.ringzero.tech/scans/scn_7g8h

ringzeroScan 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.

With no API key configured, ringzeroScan still resolves the graph and writes snapshot.json — it just skips the upload and succeeds:

> Task :ringzeroScan
RingZero: snapshot of 143 dependencies (12 direct, 131 transitive) from 4 projects
RingZero: 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 FAILED
RingZero: 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_7g8h

The 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.

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 ringzeroScan

When 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.

build.gradle.kts
plugins {
`java-library`
id("tech.ringzero.sca") version "0.1.0"
}
ringzero {
organization = "org_9f3k"
project = "acme-web"
scan {
configurations = listOf("runtimeClasspath")
includeTransitive = true
}
}

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:

SymptomCauseFix
401 invalid_token on uploadKey missing/revoked/expired, or the env var isn’t reaching the Gradle daemonCheck the resolution order; restart the daemon after changing env; re-issue in Settings → API keys
403 insufficient_scopeKey lacks a scope the task needsringzeroScan needs sca:upload — see recipes
422 unknown_projectSlug/id doesn’t exist or isn’t in the key’s project restrictionPin project to the id from the project’s settings page
Snapshot written but nothing uploadedNo API key resolved — the task ran in report-only modeProvide the key via RINGZERO_API_KEY or ringzero.apiKey; the log line names the mode
Build passes locally, fails in CIDifferent key scopes, or CI resolves different dependency versionsCompare the --info selection output; pin versions or use dependency locking