Generate an SBOM
A software bill of materials is the inventory of every component that ships inside your product. This guide covers generating one from a scanned repository, choosing between CycloneDX and SPDX, attaching an SBOM to a product release, and ingesting an SBOM someone else produced.
Why this matters now
An SBOM used to be a nice-to-have artefact. It is now a contractual and regulatory expectation: the EU Cyber Resilience Act requires manufacturers of products with digital elements to maintain a bill of materials covering at least the top-level dependencies, US federal procurement asks for one, and most enterprise supplier questionnaires now include it. Producing one by hand from a lockfile does not scale past a couple of repositories.
OSPulse already resolves your full dependency graph on every scan, so an SBOM is a rendering of data the platform holds rather than a separate analysis. That has a useful consequence: the SBOM is only as fresh as the scan behind it. Generate it after a scan completes, not before.
Prerequisites
You need an API token (see Authentication) and at least one repository imported and scanned — see Scan your first repository.
Formats
OSPulse renders two families. Pick based on who consumes the document, not on preference.
| Format | format value | Media type | Use when |
|---|---|---|---|
| CycloneDX 1.5 | CycloneDX or CycloneDX-1.5 | application/vnd.cyclonedx+json | Security tooling, VEX workflows, the default |
| CycloneDX 1.4 | CycloneDX-1.4 | application/vnd.cyclonedx+json | A consumer pinned to the older schema |
| SPDX 2.3 | SPDX or SPDX-2.3 | application/spdx+json | Licence and compliance review, legal teams |
| SPDX 2.2 | SPDX-2.2 | application/spdx+json | A consumer pinned to the older schema |
Values are matched case-insensitively. The response echoes the resolved format back as an
enum name — CycloneDx15, Spdx23, and so on — so you can confirm what was rendered.
CycloneDX 1.6 and CBOMs
CycloneDX 1.6 is used for two specific artefacts rather than general export: the SBOM attached to a CRA product release, and the cryptographic bill of materials produced by a quantum scan. See Quantum readiness audit.
Generate an SBOM
Choose a scope
An export covers one of three scopes. Repository and Workspace require a scopeId;
Tenant does not and returns the union across everything OSPulse tracks for you.
scopestringRequiredRepository, Workspace, or Tenant.
scopeIduuidOptionalThe repository or workspace id. Required unless scope is Tenant.
formatstringRequiredOne of the format values in the table above.
Request the export
curl -X POST https://api.ospulse.app/api/v1/sbom/export \
-H "Authorization: Bearer $OSPULSE_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"scope": "Repository",
"scopeId": "8f2c1d4e-9a3b-4c5d-8e7f-1a2b3c4d5e6f",
"format": "CycloneDX"
}'{
"sbomExportId": "c31a7e60-5b44-4a1f-9d02-6e8f0b2c9a11",
"scope": "Repository",
"scopeId": "8f2c1d4e-9a3b-4c5d-8e7f-1a2b3c4d5e6f",
"format": "CycloneDx15",
"status": "Completed",
"blobUri": "sbom-exports/…/c31a7e6055b44a1f9d026e8f0b2c9a11.cdx.json",
"integrityHash": "9f2c0b1e7d5a48c3b6e1f0a927d4c5b83e1a6f0d2c9b7a4e5f8d1c0b3a2e9f7d",
"generatedAtUtc": "2026-07-28T09:14:22Z"
}integrityHash is the SHA-256 of the rendered document. Record it alongside the release —
it is what lets a downstream consumer prove the file they received is the file you
generated.
Download the document
curl -L https://api.ospulse.app/api/v1/sbom/exports/c31a7e60-5b44-4a1f-9d02-6e8f0b2c9a11/download \
-H "Authorization: Bearer $OSPULSE_TOKEN" \
-o sbom.cdx.jsonThe response streams the file with the correct media type and repeats the digest in an
X-OSPulse-SBOM-Integrity: sha256=… header, so a CI job can verify the download without a
second API call.
Check status and history
Every export is retained and listable. GET /api/v1/sbom/exports/{exportId} returns the
single record including errorMessage if rendering failed:
curl https://api.ospulse.app/api/v1/sbom/exports \
-H "Authorization: Bearer $OSPULSE_TOKEN"{
"items": [
{
"sbomExportId": "c31a7e60-5b44-4a1f-9d02-6e8f0b2c9a11",
"scope": "Repository",
"scopeId": "8f2c1d4e-9a3b-4c5d-8e7f-1a2b3c4d5e6f",
"format": "CycloneDx15",
"status": "Completed",
"blobUri": "sbom-exports/…/c31a7e6055b44a1f9d026e8f0b2c9a11.cdx.json",
"integrityHash": "9f2c0b1e7d5a…",
"generatedAtUtc": "2026-07-28T09:14:22Z"
}
],
"totalEstimate": 1
}What ends up in the document
The components list is built from the resolved dependency graph for the scope — that is
both direct and transitive dependencies, not just what your manifest declares. Each
component carries its name, version, ecosystem, package URL (pkg:npm/express@4.21.2),
integrity hash where the registry publishes one, and the declared licence expression.
Two consequences worth planning around:
- Transitives dominate. A repository with forty declared dependencies routinely yields several hundred components. That is correct, and it is the point — the components you did not choose are the ones you are least likely to be tracking by hand.
- The graph must exist first. If a repository has never completed a scan, its export is
empty rather than an error. Trigger a scan with
POST /api/v1/repositories/{repositoryId}/scanand wait for it to finish.
Exports require a user principal
POST /api/v1/sbom/export and POST /api/v1/sbom/import both return 403 for a
service-account token. SBOM generation is attributed to a person for audit purposes. Use a
token created by a user account in CI.
Attach an SBOM to a release
For CRA purposes an SBOM belongs to a release of a product, not to a repository. The CRA endpoints model that directly: create a product, create a release pinned to a completed scan, then generate the release SBOM.
# 1. Create the release, pinned to the scan you shipped from
curl -X POST https://api.ospulse.app/api/v1/cra/products/$PRODUCT_ID/releases \
-H "Authorization: Bearer $OSPULSE_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "version": "4.2.0", "sourceScanId": "'"$SCAN_ID"'", "manualUpload": false }'
# 2. Render and attach its CycloneDX 1.6 SBOM
curl -X POST https://api.ospulse.app/api/v1/cra/releases/$RELEASE_ID/sbom \
-H "Authorization: Bearer $OSPULSE_TOKEN"{
"id": "b4d9f012-3c7a-4e51-9f8b-2d6c1a0e5738",
"productReleaseId": "1f0a8c73-6b2d-4e19-a7c5-93b0d4f2e618",
"source": "GeneratedFromScan",
"schemaVersion": "1.6",
"componentCount": 412,
"topLevelComponentCount": 38,
"generatedAt": "2026-07-28T09:20:03Z",
"freshness": "Fresh"
}freshness is the field that matters at audit time. It reports Fresh, Stale, or
Missing against your configured freshness window — a release whose SBOM has gone stale is
a finding, not a formatting problem. Download the attached document any time with
GET /api/v1/cra/releases/{releaseId}/sbom.
If you build outside OSPulse, create the release with "manualUpload": true and push the
document yourself:
curl -X POST "https://api.ospulse.app/api/v1/cra/releases/$RELEASE_ID/sbom/upload?format=SPDX-2.3" \
-H "Authorization: Bearer $OSPULSE_TOKEN" \
-F "file=@build/product.spdx.json"That path accepts CycloneDX, SPDX-2.3, and SPDX-3.0. For an SPDX upload,
GET /api/v1/cra/releases/{releaseId}/sbom/import-record returns per-element provenance —
which SPDX elements mapped to a known component, which were flagged, and why — so you can
see exactly what OSPulse could and could not resolve.
Ingest an SBOM you received
The reverse direction matters just as much: a supplier sends you a bill of materials and
you want it evaluated against the same vulnerability and health intelligence as your own
code. POST /api/v1/sbom/import takes a multipart upload.
curl -X POST https://api.ospulse.app/api/v1/sbom/import \
-H "Authorization: Bearer $OSPULSE_TOKEN" \
-F "file=@vendor-product.cdx.json" \
-F "repositoryId=8f2c1d4e-9a3b-4c5d-8e7f-1a2b3c4d5e6f" \
-F "format=CycloneDX"{
"sbomImportId": "7a1e5c93-2048-4bd6-8f31-c0a95e7b4d12",
"format": "CycloneDx15",
"status": "Completed",
"componentsImportedCount": 287,
"repositoryId": "8f2c1d4e-9a3b-4c5d-8e7f-1a2b3c4d5e6f",
"createdAtUtc": "2026-07-28T09:31:44Z"
}Notes on the import path:
fileis required;repositoryIdandformatare optional. Omitformatand OSPulse sniffs the document — CycloneDX by itsbomFormatkey, SPDX 2.x byspdxVersionor aSPDXVersion:tag-value header, SPDX 3.0 by its JSON-LD@context/@graph.- A non-multipart request is
415. An unrecognised format or a document that fails to parse is422, and the raw upload is still retained for forensic replay — the response carries thesbomImportIdyou can quote to support. GET /api/v1/sbom/importslists what has been ingested.
Automating it
The natural trigger is a release build. Generate the SBOM after the scan step in your pipeline, verify the digest, and publish the file as a build artefact:
- name: Generate SBOM
run: |
EXPORT=$(curl -sf -X POST https://api.ospulse.app/api/v1/sbom/export \
-H "Authorization: Bearer ${{ secrets.OSPULSE_TOKEN }}" \
-H "Content-Type: application/json" \
-d "{\"scope\":\"Repository\",\"scopeId\":\"$OSPULSE_REPO_ID\",\"format\":\"CycloneDX\"}")
ID=$(echo "$EXPORT" | jq -r .sbomExportId)
HASH=$(echo "$EXPORT" | jq -r .integrityHash)
curl -sfL "https://api.ospulse.app/api/v1/sbom/exports/$ID/download" \
-H "Authorization: Bearer ${{ secrets.OSPULSE_TOKEN }}" -o sbom.cdx.json
echo "$HASH sbom.cdx.json" | sha256sum -c -See Scan in CI/CD for the surrounding pipeline, and Errors for the problem-details body returned on failure.
