Skip to content

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.

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.

Apply and configure the plugin in the modules you want to scan. Module settings remain configuration-cache compatible:

maps/build.gradle.kts
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.

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.

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.

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.

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.

  1. POST /v1/scans/dependency-snapshots/uploads, with an Idempotency-Key header and { "schemaVersion": 2, "project": "acme-web", "ecosystem": "gradle", "partCount": 3 }, returns { "uploadId": "upl_…" }.
  2. 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.
  3. POST /v1/scans/dependency-snapshots/uploads/{uploadId}/complete validates 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.

RecordFields
modulepath, optional teamName, optional string-valued attributes
moduleEdgemodule, dependency
nodeid (name@version), name, version
contextid, module, context (configuration), root
vertexcontextId, id, exactly one of module or node
edgecontextId, from, to
suppressioncve, 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.

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}ScopeQuery fields
/modulesscans:readteam, q, limit, cursor
/teamsscans:readq, limit, cursor
/findingsfindings:readteam, module, attribution, severity, severityMode, suppressed, details, limit, cursor
/findings/{findingId}/attributionsfindings:readteam, module, attribution, limit, cursor
/findings/{findingId}/attributions/{attributionId}/pathfindings:readlimit, 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.

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:

SettingDefaultOperation bounded
SCALABLE_UPLOAD_PART_MAX_BYTES2 MiBOne upload request; the Gradle producer emits parts up to 2 MiB
WORKER_CONTEXT_BATCH_SIZE500Package vertices matched in one checkpoint
WORKER_CONTEXT_ORIGIN_BUDGET10,000Introducing declarations retained for one affected vertex
WORKER_CONTEXT_QUERY_TIMEOUT_MS30,000One context database query
SCALABLE_PATH_VERTEX_BUDGET50,000Vertices visited when deriving path evidence
SCALABLE_PATH_EDGE_BUDGET250,000Edges examined when deriving path evidence
SCALABLE_PATH_QUERY_TIMEOUT_MS10,000One 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.