Large Gradle monorepos
Keep a large Gradle build in one scan. RingZero splits uploads automatically and loads findings, module details, and dependency paths as you need them. Filter by team or module to find where a vulnerable dependency enters the build and who else is affected.
Scan the build
Section titled “Scan the build”Set up the Gradle plugin, then run
./gradlew :ringzeroScan for the root aggregate. No separate large-project mode
is needed. To scan a module independently, apply the plugin there and use its
task path, such as :maps:ringzeroScan.
Configure ownership per module
Section titled “Configure ownership per module”Apply and configure the plugin in the modules you want to scan. Module settings remain configuration-cache compatible:
ringZero { teamName.set("maps") attributes.put("service", "maps-api") attributes.put("domain", "geospatial")}The module’s team and custom attributes travel with its scan metadata. They are snapshot labels; access to scans still follows organization membership, API-key scopes, and project restrictions.
Review ownership and impact
Section titled “Review ownership and impact”Open the completed scan, choose a team or search for a module, and switch between
Introduced by this selection and Affecting this selection. If :maps
declares a library that brings in a vulnerable package and :app consumes
:maps, both are affected; :maps is the introducing module.
Expand a finding to inspect its introducing dependencies and paths. Load more rows or path segments when needed. Custom attributes appear in module details; the finding filters use team and module, not custom attributes.
Modules appear before their consumers. Each attribution retains its resolved configuration and variant, including independent introducing declarations.
Reports and portable artifacts
Section titled “Reports and portable artifacts”Archive the whole build/ringzero/ directory, not just snapshot.json.
Small scans keep a full snapshot and report. For large scans, those files point
to the parts of the bundle.
Report formats and compatibility
ringzeroScan uses multipart upload when the backend supports it. The plugin writes exact request bodies to build/ringzero/multipart/parts/ and an index with sizes, SHA-256 digests, and the session idempotency key to build/ringzero/multipart/manifest.json.
The plugin classifies a scan as large when the combined input graph-fragment files reach 2 MiB (2,097,152 bytes). This is a byte threshold, not a module, configuration, or edge-count threshold, and it is measured before the fragments are packaged into upload requests.
Scans below that threshold keep the full v2 snapshot.json and readable dependencies.md, even when uploaded through multipart. For large scans, snapshot.json indexes the multipart bundle, and dependencies.md links one source report per input graph fragment at multipart/reports/fragment-N.md. A fragment is not guaranteed to correspond to exactly one module. Keep the whole bundle when archiving or handing off a large scan. A manifest alone does not contain the dependency graph.
Offline or missing-API-key runs still produce local reports. If an older backend returns 404 for the multipart begin endpoint, a scan below the 2 MiB input-fragment threshold can fall back to the full v2 single-document upload, subject to that endpoint’s existing limits. Scans at or above the threshold require the multipart backend; they do not use this fallback.
Upload time and graph complexity
Section titled “Upload time and graph complexity”The Gradle upload timeout covers the complete upload, including retries, and defaults to five minutes. Set it to fit your measured bundle size and network speed:
ringZero { upload { timeout.set(java.time.Duration.ofMinutes(30)) }}The producer and partitioned reports work on one source fragment at a time. Legacy report paths within that fragment are still materialized; unusually deep individual configurations can require additional Gradle heap even when the aggregate upload is multipart.
Multipart API
Section titled “Multipart API”The multipart protocol has no repository-wide module, configuration, or package-count ceiling. Per-request and worker resource budgets still apply. The Gradle plugin handles this protocol for you.
Custom producers: upload routes, records, and field limits
All routes use the existing bearer authentication and error envelope. sca:upload is required on every upload operation, with the same project restrictions as single-document uploads.
POST /v1/scans/dependency-snapshots/uploads, with anIdempotency-Keyheader and{ "schemaVersion": 2, "project": "acme-web", "ecosystem": "gradle", "partCount": 3 }, returns{ "uploadId": "upl_…" }.PUT /v1/scans/dependency-snapshots/uploads/{uploadId}/parts/{partIndex}accepts{ "records": [...] }. Indexes start at zero. The default request limit is 2 MiB, including the JSON envelope. Identical bytes may be replayed; different bytes at an accepted index produce a conflict.POST /v1/scans/dependency-snapshots/uploads/{uploadId}/completevalidates references and returns the queued scan. Repeating completion returns the same scan. Missing parts or invalid references never publish a partial scan.
A begin request resumes an open session regardless of its age. Completed sessions keep their idempotency mapping for 24 hours; after that, the same content key begins a new scan so unchanged dependencies can be checked against newly published advisories. Historical upload IDs still support replay of accepted parts and completion.
For an unchanged local build, run ./gradlew ringzeroScan --rerun-tasks to submit again instead of reusing Gradle’s up-to-date task output.
Records are normalized when each part arrives. References may cross parts. Definitions may repeat identically; conflicting definitions are rejected. Each edge is a separate record, so a vertex with many outgoing edges can span parts.
| Record | Fields |
|---|---|
module | path, optional teamName, optional string-valued attributes |
moduleEdge | module, dependency |
node | id (name@version), name, version |
context | id, module, context (configuration), root |
vertex | contextId, id, exactly one of module or node |
edge | contextId, from, to |
suppression | cve, optional package, reason, optional expires |
The part schema describes these records. Each record also has a type equal to its name in the table. A context ID is the lowercase SHA-256 hex digest of UTF-8 module + NUL + configuration. Vertex IDs are local to their context. A context root names a vertex representing that context’s module. Suppressions use the same package-glob and expiry semantics as existing snapshot uploads.
Indexed record strings are limited to 512 UTF-8 bytes, with control characters rejected. Attribute keys allow 128 UTF-8 bytes and string values allow 512. These field-size checks keep composite database indexes valid, including for non-ASCII text. They are independent of the number of modules, attributes, or edges in a scan.
JSON Schema’s maxLength counts characters, not UTF-8 bytes. Passing schema validation alone does not prove a non-ASCII value fits the server’s byte limit; custom producers should also check its encoded UTF-8 length.
Read a large scan incrementally
Section titled “Read a large scan incrementally”API clients: paged metadata, findings, and path evidence
A multipart scan has attributionMode: "paged", moduleCount, and an empty compatibility modules array. Use these routes to fetch the graph metadata and finding evidence as needed:
Route below /v1/scans/{scanId} | Scope | Query fields |
|---|---|---|
/modules | scans:read | team, q, limit, cursor |
/teams | scans:read | q, limit, cursor |
/findings | findings:read | team, module, attribution, severity, severityMode, suppressed, details, limit, cursor |
/findings/{findingId}/attributions | findings:read | team, module, attribution, limit, cursor |
/findings/{findingId}/attributions/{attributionId}/path | findings:read | limit, cursor |
Pages carry nextCursor; continue until it is null. A cursor belongs to its scan, nested resource, and filters. Reset the cursor when changing filters. The default page size is 50, with a maximum of 200.
Use details=summary for the findings list. severityMode=exact selects one severity; the existing default at_least includes higher severities. suppressed=false excludes suppressions, true returns only suppressed findings, and all includes both. The counts object contains unsuppressed severity totals plus a suppressed total for the selected team/module scope, before severity and suppression-visibility filters.
An attribution page contains rows with id, module, context, relationship (direct or transitive), and origin: { module, dependency }. Path evidence is fetched separately and paginated. A path proves the selected introducing declaration, rather than substituting a shorter route through a different declaration. The UI loads more findings, modules, teams, attributions, and path segments only when requested.
Historical inline scans and v1/v2 single-document uploads remain supported. Their existing request-size protections still apply; use multipart for repository-scale uploads.
Worker operation and measurement
Section titled “Worker operation and measurement”Deployment operators: resource budgets and diagnostics
The worker stores progress in PostgreSQL and commits findings, attribution references, and its checkpoint together. Replayed work does not duplicate findings. Leases fence stale workers, and successful checkpoints return work to the queue without consuming the failure retry budget. Findings become visible when the scan completes.
Deployment operators can tune these validated settings:
| Setting | Default | Operation bounded |
|---|---|---|
SCALABLE_UPLOAD_PART_MAX_BYTES | 2 MiB | One upload request; the Gradle producer emits parts up to 2 MiB |
WORKER_CONTEXT_BATCH_SIZE | 500 | Package vertices matched in one checkpoint |
WORKER_CONTEXT_ORIGIN_BUDGET | 10,000 | Introducing declarations retained for one affected vertex |
WORKER_CONTEXT_QUERY_TIMEOUT_MS | 30,000 | One context database query |
SCALABLE_PATH_VERTEX_BUDGET | 50,000 | Vertices visited when deriving path evidence |
SCALABLE_PATH_EDGE_BUDGET | 250,000 | Edges examined when deriving path evidence |
SCALABLE_PATH_QUERY_TIMEOUT_MS | 10,000 | One path database query |
Exceeding the path vertex or edge budget returns path_budget_exceeded; no partial proof is returned. A database query timeout currently returns 500 internal_error. Worker context failures include the context identifier and configured budget in server diagnostics. Tune budgets using measurements of representative configuration graphs. Multipart removes total repository count limits, while an unusually large individual graph can still require operational tuning.
Apply the scalability graph and job-lease database migrations before running the new API and worker. The producer falls back only on a missing multipart endpoint, so deploy the backend first.
The source repository includes tools/scan-benchmark/ and the API’s :app:scalabilityTest task. They exercise the real HTTP upload, PostgreSQL migrations, and worker with a deterministic local advisory service. The workload generator varies repository size, graph density, and individual configuration size independently. Measurements are specific to those workloads and hardware; they are not a universal capacity guarantee.