Rio normalizes CycloneDX SBOMs, delivers verified outputs, and records the evidence for both.
Two SBOMs consumed → pipeline evidence added → delivered to Dependency-Track → receipts retained.
Here is a JSON excerpt from the actual Rio v0.7.0 record, produced with a synthetic HTTPS receiver
speaking the Dependency-Track upload API. This shows the api artifact; the second artifact,
checks and run details are omitted here. Values and full digests match the complete file.
{
"kind": "rio-run-receipt",
"schemaVersion": 1,
"rioVersion": "0.7.0",
"run": {
"operation": "pipeline",
"outcome": "success"
},
"artifacts": [
{
"id": "api",
"input": {
"path": "api.cdx.json",
"sha256": "1d2b04c0df452a4c2c34d84a6c195bb2805c3f208cf8f6c0bbe90ec54970dc01",
"size": 376
},
"output": {
"path": "api.cdx.json",
"sha256": "55e46a63a8b30ad6082b6c35dd43f0e5bc65d5cdd5ecd91126c09d0327f3d3e4",
"size": 2307
},
"changes": {
"metadata": [
{
"field": "build.id",
"operation": "add",
"before": null,
"after": "42",
"assertion": "producer",
"source": "context-file"
},
{
"field": "build.url",
"operation": "add",
"before": null,
"after": "https://ci.example.org/runs/42",
"assertion": "producer",
"source": "context-file"
}
]
}
}
],
"targets": {
"security": {
"type": "dependency-track",
"url": "https://127.0.0.1:53946"
}
},
"deliveries": [
{
"artifactId": "api",
"target": "security",
"project": {
"name": "api",
"version": "1.0.0"
},
"submitted": [
{
"artifactOutput": "api",
"mediaType": "application/vnd.cyclonedx+json",
"role": "sbom"
}
],
"transport": {
"scheme": "https",
"tlsObserved": true,
"certificateVerification": "enforced"
},
"responses": [
{
"tlsObserved": true,
"kind": "acknowledgment",
"value": "accepted",
"httpStatus": 200,
"observedAt": "2026-09-27T23:54:37.014772Z",
"code": "accepted",
"references": [
{
"kind": "dependency-track:event-token",
"value": "11111111-1111-4111-8111-111111111111"
}
]
}
]
}
]
}Read the full record.json — 5,791 bytes · Download the HTML report (open locally) · Download the runnable example · Reproduce this example
The URL is the destination server, not the Dependency-Track project website. This demo used a
local receiver; your record contains your configured instance URL, such as https://dtrack.example.org.
It also retains the original and delivered SBOM digests, exact payload sizes, project identities,
and response timestamps. Pipeline values are supplied by your build; HTTP 200 records acceptance,
not proof of ingestion.
With intake, pipeline context, and delivery configured in rio.yaml:
rio --receipt record.json
rio record inspect --file record.json
rio record report --file record.json --output report.htmlSet a spec-version floor, attach product and pipeline metadata, and get normalized SBOMs plus an
index.json linking original inputs, output digests, the manifest and check results.
Native delivery checks that the SBOM matches that record, sends the exact verified bytes, and keeps a separate delivery journal: what Rio attempted, what the destination acknowledged, and what later checks observed. Lost responses remain visible as uncertainty, with no automatic resubmission. Normalization stays offline; delivery uses the network explicitly. Both run in one binary, on your workstation or in CI.
Delivery destinations include Dependency-Track and OCI artifact registries, with separate delivery acknowledgments, transport facts and content verification.
First delivery · Try without a server · Verified delivery · OCI registries · One evidence record · Configuration · For agents · Reference
Already generating target/bom.json? Choose one destination below, save its configuration as
rio.yaml, and run rio to normalize and deliver the SBOM. Intake and delivery share this one file.
No server yet? Start with the released, account-free normalization example.
Availability: use Rio v0.7.0 or newer for the one-command pipeline and compact receipts.
With either configuration below:
rioHere is a complete local layout for macOS/Linux. Commit rio.yaml and include target/ in your
.gitignore; keep the API key outside the project:
~/.config/rio/dtrack.env # private local credential file
my-app/ # run Rio from this directory
├── rio.yaml # configuration below
├── .gitignore # includes target/
├── certs/dtrack-ca.pem # optional public company CA certificate
└── target/
├── bom.json # CycloneDX SBOM produced by your build
└── rio/ # Rio creates this output directory
└── runs/<run-id>/ # fresh directory for each invocation
├── app.cdx.json # normalized SBOM
├── index.json # normalization evidence
├── deliveries/ # internal delivery journals
└── record.json # automatic compact receipt
Save this as my-app/rio.yaml:
version: 1
artifacts:
- id: app
sbom: target/bom.json
delivery:
targets:
security: # a target name you choose
type: dependency-track
url: https://dtrack.example.com
apiKeyEnv: DTRACK_API_KEY # environment variable name, never the key itself
autoCreate: truesecurity is a local target label. You could name it company-dtrack instead. It appears in
rio plan, in the --target security filter, and as destinationName in journal evidence.
The journal directory uses a generated hash. The API key determines access rights; the
Dependency-Track project name/version come from the SBOM's subject. Separately, app is Rio's
artifact ID and names the output app.cdx.json.
Replace the URL with your server. Create a private directory for local credentials:
mkdir -p "$HOME/.config/rio"
chmod 700 "$HOME/.config/rio"Using your editor, save ~/.config/rio/dtrack.env with this content:
export DTRACK_API_KEY='replace-with-your-dependency-track-api-key'Restrict that private file to your account, then load it and run from my-app:
chmod 600 "$HOME/.config/rio/dtrack.env"
. "$HOME/.config/rio/dtrack.env"
rioYour shell loads the credential into Rio's environment; Rio does not automatically read this file.
For CI, store the value in a CI secret named DTRACK_API_KEY and inject it as an environment variable.
Rio retains the variable name in evidence, never the API-key value.
For an internal CA, obtain its public certificate from your administrator, save it as
certs/dtrack-ca.pem, and add caFile: certs/dtrack-ca.pem under security. The path is relative to
rio.yaml; certificate and hostname verification remain enabled. This is the preferred option.
Rio v0.5.0 also provides
insecureSkipVerify: true under security for explicitly skipping certificate-chain and hostname
verification. It requires HTTPS and cannot be combined with caFile. The choice is saved in the
journal and portable record alongside observed TLS facts; it does not imply the certificate was
invalid. Protocol failures still fail, with no fallback or automatic replay.
TLS policy and synthetic demo.
This example explicitly enables project creation; omit autoCreate when you provision projects
yourself. Permissions and options.
Use this alternative rio.yaml to retain the same SBOM in a writable OCI repository:
version: 1
artifacts:
- id: app
sbom: target/bom.json
delivery:
targets:
release-registry: # another target name you choose
type: oci
registry: registry.example.com
repository: acme/app-sbom
auth:
usernameEnv: OCI_USERNAME
passwordEnv: OCI_PASSWORDSet the registry host and repository. release-registry is the label used by
--target release-registry; the inner registry field is the server address.
For local use, put these assignments in a separate private ~/.config/rio/registry.env:
export OCI_USERNAME='replace-with-your-registry-user'
export OCI_PASSWORD='replace-with-your-registry-password-or-token'Apply the same private-file permissions, then load it with . "$HOME/.config/rio/registry.env"
before running Rio. In CI, inject the two variables from CI secrets.
This publishes a standalone SBOM and returns an immutable manifest reference. To attach it to
an existing image or image index, use that image's repository and add its exact subject descriptor
(digest, mediaType, size); Rio leaves the image unchanged and requires Referrers API support.
Attachment configuration and current registry scope.
You get normalized bytes, their digest, the receiver's acknowledgment, and an automatic compact
receipt. To preview destinations before sending, run rio plan. To use both destinations, put both
target entries under delivery.targets; plain rio sends each selected artifact to every eligible target.
Each invocation writes its receipt under target/rio/runs/<run-id>/record.json and prints its path.
To choose a public receipt path instead, use a fresh filename:
rio --receipt record.json
rio record inspect --file record.json
rio record report --file record.json --output report.htmlThe receipt retains scope, meaningful changes, effective checks, selected attempts and missing-evidence gaps. Ordinary failures also retain a receipt; an enforced gate failure blocks delivery. Its parent must exist, and Rio refuses to overwrite an existing receipt. See the client evidence workflow and installed-binary examples, including a complete client handoff.
The recipient can inspect record.json without your workspace or credentials, or read the optional
self-contained HTML report. Keep the JSON alongside the report for machine inspection.
What the record establishes.
- Normalize versions and check quality. Raise older SBOMs to your chosen CycloneDX floor, preserve dependency membership, and check required names, versions and package URLs.
- Keep lineage and pipeline context. Record input/output digests and normalization details; attach supplied product, source, build and generator metadata with its origins and changes. Source/build details remain labeled as producer assertions.
- Deliver verified SBOMs with an inspectable history. Upload directly to Dependency-Track by project name/version or UUID. Retain the intended destination, payload digest, acknowledgment and later activity observations in a delivery journal, including unknown outcomes. Publish unchanged SBOM bytes to OCI registries with immutable manifest references and independent read-back evidence.
- Cover every selected module. Include each qualifying module's SBOM automatically, with a failure when a selected module has not produced its output.
- Optionally repair Eclipse/OSGi p2 coordinates. Convert eligible package URLs to Maven coordinates using explicit coordinate evidence and maintained mappings. Repair reference · Focused example.
The records provide evidence for later release controls: which SBOM was processed, what changed, which supplied build assertions belong to it, and what is known about each delivery attempt. Rio's gate enforces configured SBOM handoff checks. The surrounding release system decides whether to publish using these facts and its own policy. SBOM generation, test execution and vulnerability scanning remain separate steps. See project scope.
| Record | What it captures |
|---|---|
index.json |
Normalization inputs, output digests, transforms and gate results |
<artifact>.intoto.json, with rio normalize --attest |
An unsigned in-toto Statement describing the normalization |
record.json (automatic for each invocation) |
Compact input/output identities, meaningful changes, checks, delivery facts and coverage |
<run-directory>/deliveries/<pair-key>/, or an explicit --record directory |
Separate journal events for delivery intent, receipt when available, and later reconciliation observations |
Optional signing and recipient verification adds a per-artifact bundle outside Rio, using a local key with public-log upload disabled.
Delivery history is not added to index.json or the normalization statements. These records
support traceability; they do not authenticate the producer or prove successful ingestion.
rio delivery inspect reads a journal offline; rio delivery reconcile queries the destination
and appends new observations without uploading again.
With Homebrew:
brew install rebaze/tap/rioOr use the installer:
curl -sSL https://raw.githubusercontent.com/rebaze/rio/main/install.sh | shYou can also download a release binary.
See installation options to pin a version or choose the install directory.
Documentation follows main; check feature prerequisites when using an older release.
Testing a fix on Linux? Build and download a temporary test binary from GitHub Actions, including a runnable example, without creating a release.
With Rio v0.7.0 or newer installed, try this synthetic CycloneDX 1.5 input. It needs no project build or account. Download the existing sample and manifest, then normalize and inspect the receipt:
demo_dir=$(mktemp -d "${TMPDIR:-/tmp}/rio-sample.XXXXXXXX") &&
sample_url=https://raw.githubusercontent.com/rebaze/rio/v0.4.0/tools/demo-agent-integration &&
mkdir -p "$demo_dir/desktop/target" &&
curl -fsSL "$sample_url/projects/explicit/seed.cdx.json" -o "$demo_dir/desktop/target/bom.json" &&
curl -fsSL "$sample_url/examples/explicit.yaml" -o "$demo_dir/rio.yaml" &&
rio normalize --manifest "$demo_dir/rio.yaml" --out "$demo_dir/out" --gate fail --receipt "$demo_dir/record.json" &&
rio record inspect --file "$demo_dir/record.json" &&
printf 'Inspect the input and results in: %s\n' "$demo_dir"The index.json in the printed run directory shows the version change (excerpt):
"specVersion": {
"input": "1.5",
"output": "1.6"
}It also records the input and output SHA-256 digests, manifest digest, tool version and gate: "ok".
Compare desktop/target/bom.json with out/runs/<run-id>/desktop.cdx.json: the spec version is normalized and
the dependency inventory is preserved. The original file stays unchanged.
To carry pipeline lineage alongside those records, bind a producer-supplied context file by artifact ID and original SBOM digest. The context example shows source/build metadata appearing in the SBOM and index; Rio does not infer it from this sample.
If rio is not found, use its installed path or add its directory to PATH.
The offline onboarding examples run from a checkout.
Add targets to the existing rio.yaml to include native delivery in the pipeline:
delivery:
targets:
security:
type: dependency-track
url: https://dtrack.example.comInject DTRACK_API_KEY through your secret manager or CI, then run the normal pipeline:
rio plan
rioEvery selected artifact goes to every eligible target, using its verified SBOM subject name and
version as the Dependency-Track project. Project creation is disabled by default; opt in with
autoCreate: true on the target. Rio refuses duplicate project routing and preflights the complete
batch before uploading. Each attempt receives an automatic journal under the current run directory.
A standalone rio deliver --index PATH refuses to reuse existing journals for that indexed output.
A new root rio invocation creates new outputs and may upload them again. An accepted receipt acknowledges submission only;
it does not prove ingestion. A partially completed batch retains each attempt independently.
For the quick-start sample's custom output location, use
rio deliver --manifest "$demo_dir/rio.yaml" --index "$demo_dir/out/runs/RUN_ID/index.json",
replacing RUN_ID with the run printed by normalization.
Changing only delivery settings after normalization is supported; the index retains its original
manifest digest, while each delivery records the current manifest digest.
Use the reported journal path with rio delivery inspect --record PATH or
rio delivery reconcile --record PATH to query saved receipt activity without resubmitting.
See delivery configuration and the runnable demo
for filters, overrides, UUID selectors, deliberate retries, and tested server versions.
With a build that includes OCI delivery, add a registry target to the same rio.yaml:
delivery:
targets:
release-registry:
type: oci
registry: registry.example.com
repository: acme/application-sbom
auth:
usernameEnv: OCI_USERNAME
passwordEnv: OCI_PASSWORDInject the named credentials through your secret manager, then preview and run the pipeline:
rio plan --target release-registry --json
rio --target release-registry --jsonRio uploads the exact verified CycloneDX bytes with a deterministic OCI manifest. It reports a
consumer reference such as registry.example.com/acme/application-sbom@sha256:<manifest-digest>
and uses the generated tag rio-sbom-sha256-<manifest-digest>. A valid persisted receipt is exit 0;
normal publication does not claim that a later read-back has already succeeded.
To attach the SBOM to an existing image, use that image's repository and add its exact
subject descriptor to the target. This example is illustrative; replace all descriptor values
with those from your image-producing pipeline:
repository: acme/application
subject:
digest: sha256:1111111111111111111111111111111111111111111111111111111111111111
mediaType: application/vnd.oci.image.manifest.v1+json
size: 527Rio verifies that subject before writing and requires the OCI Referrers API for attachment. Choose the image index or a specific platform manifest explicitly. The SBOM blob digest, Rio wrapper manifest digest, and subject image digest are different identities. Attachment does not change the subject, prove executable-to-SBOM correspondence, or trigger security analysis.
Use the reported journal with rio delivery reconcile --record PATH for a bounded read-only
content/discovery check. Reconciliation needs no original SBOM or index files. After a lost response
it can report verification: verified while the historical acknowledgment remains unknown.
rio delivery inspect and rio record inspect remain offline. Server tag immutability and retention policy
matter: generated tags avoid human release aliases but are not an atomic protection against racing
external writers. See OCI configuration, recovery, demos and tested registry scope.
Each invocation automatically produces one compact receipt for the work it performed. With your pipeline configured, choose a fresh public filename and inspect it offline:
rio --receipt record.json
rio record inspect --file record.jsonThe receipt records byte identities, meaningful metadata changes, checks and delivery observations;
it does not embed indexes, journals or SBOM inventories. Inspection works after the source workspace
is gone. Separate rio normalize, rio deliver and reconciliation invocations have their own
receipts; retries and reconciliation link to prior attempts rather than copying their history.
Inspection checks internal consistency, without authenticating producers, fetching external SBOM bytes, or proving ingestion. See the receipt field guide and recovery instructions and installed-binary demo.
Once the sample works, point Rio at an SBOM your own build produces. For a project with
target/bom.json, create or adapt rio.yaml:
version: 1
artifacts:
- id: app
sbom: target/bom.jsonThis minimal manifest uses the default 1.6 spec floor and checks the input. Add enrichment for supplied product metadata and context for producer-provided source/build assertions. Their source and change records travel with the outputs.
Preview the selection, then normalize into a fresh directory:
rio plan
out_dir=$(mktemp -d "${TMPDIR:-/tmp}/rio.XXXXXXXX")
rio normalize --out "$out_dir" --gate fail
printf 'Results: %s\n' "$out_dir"You get app.cdx.json, index.json and record.json in the printed run directory. Explicit artifact paths are relative to rio.yaml, and
each must resolve to exactly one SBOM. --gate fail returns exit 1 for failed requirements,
with results still available for inspection. Missing inputs or invalid configuration return exit
2; ordinary failures still retain a failed receipt. See exit codes and what the gate checks.
Use artifactSets when a directory convention defines your deliverable modules:
version: 1
artifactSets:
- modules: "services/*server/pom.xml"
sbom: "target/bom.json"
idFrom: module-directoryThis selects module markers first. services/orders-server/pom.xml produces an orders-server
artifact from that module's target/bom.json; a selected module without an SBOM fails the run.
Rio matches marker paths, not Maven artifact IDs. Explicit artifacts and sets can share a manifest.
Module discovery, enrichment and context are available in v0.4.0 and newer.
Manifest reference covers exclusions, naming rules, paths, transforms and
shared settings. Module discovery explains the full
selection contract. CI examples show build → plan →
run → inspect. Each invocation gets a fresh run directory; index.json defines that run's outputs.
Paste this into your project's coding-agent session:
Integrate Rio into this project. Read
https://raw.githubusercontent.com/rebaze/rio/main/docs/agent-integration.md
first. Inspect our build and existing configuration, ask only about unresolved
decisions, then configure and validate Rio. Report anything still needed.
The integration guide works with any harness; provide the guide and its references locally if
the agent cannot fetch them. Use rio plan --json to inspect resolved inputs and settings.
AGENTS.md is for developing Rio itself.
Reading an extracted release archive? Open these references on GitHub.
| I want to… | Read |
|---|---|
| Look up CLI flags, exit codes or the plan JSON contract | Commands |
| Configure explicit artifacts or module discovery | Manifest |
| Supply product and organization metadata | Enrichment |
| Attach supplied source/build context | Context |
| Read the index, repair records or unsigned statements | Output records |
| Deliver verified SBOMs and inspect delivery journals | Native delivery |
| Repair Eclipse p2 package URLs | p2 repair |
| Run demos, prepare mapping tables or upload to DependencyTrack | Tools and examples |
| Integrate a project with a coding agent | Agent integration |
Maintained by rebaze, licensed under Apache-2.0. Roadmap, scope and contributing · Report an issue