OSPulse

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.

Formatformat valueMedia typeUse when
CycloneDX 1.5CycloneDX or CycloneDX-1.5application/vnd.cyclonedx+jsonSecurity tooling, VEX workflows, the default
CycloneDX 1.4CycloneDX-1.4application/vnd.cyclonedx+jsonA consumer pinned to the older schema
SPDX 2.3SPDX or SPDX-2.3application/spdx+jsonLicence and compliance review, legal teams
SPDX 2.2SPDX-2.2application/spdx+jsonA 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.

scopestringRequired

Repository, Workspace, or Tenant.

scopeIduuidOptional

The repository or workspace id. Required unless scope is Tenant.

formatstringRequired

One of the format values in the table above.

Request the export

bash
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"
  }'
json
{
  "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

bash
curl -L https://api.ospulse.app/api/v1/sbom/exports/c31a7e60-5b44-4a1f-9d02-6e8f0b2c9a11/download \
  -H "Authorization: Bearer $OSPULSE_TOKEN" \
  -o sbom.cdx.json

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

bash
curl https://api.ospulse.app/api/v1/sbom/exports \
  -H "Authorization: Bearer $OSPULSE_TOKEN"
json
{
  "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}/scan and 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.

bash
# 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"
json
{
  "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:

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

bash
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"
json
{
  "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:

  • file is required; repositoryId and format are optional. Omit format and OSPulse sniffs the document — CycloneDX by its bomFormat key, SPDX 2.x by spdxVersion or a SPDXVersion: 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 is 422, and the raw upload is still retained for forensic replay — the response carries the sbomImportId you can quote to support.
  • GET /api/v1/sbom/imports lists 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:

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

Next steps