SBOM
The portal generates Software Bill of Materials (SBOM) artifacts from the latest successful scan. Four interchange formats are supported, plus an attribution NOTICE file.
This page is about exporting an SBOM that TRUSCA generated from a scan. To go the other way — upload an SBOM your own tooling already produced (CycloneDX or SPDX) so TRUSCA matches CVEs and scores its conformance — see Upload an SBOM and Scans → SBOM upload.

Engineers shipping releases, compliance leads filing artifacts, customers fulfilling SBOM requests under EO 14028. Read access via team membership.
Supported formats
| Format | Query value (format=) | MIME | Use case |
|---|---|---|---|
| CycloneDX 1.6 (JSON) | cyclonedx-json | application/vnd.cyclonedx+json | Modern de-facto standard for SCA tooling. Includes VEX. |
| CycloneDX 1.6 (XML) | cyclonedx-xml | application/vnd.cyclonedx+xml | Same data; XML for legacy tooling. |
| SPDX 2.3 (JSON) | spdx-json | application/spdx+json | NTIA minimum elements; broadly accepted in regulated industries. |
| SPDX 2.3 (Tag-Value) | spdx-tv | text/spdx | The original SPDX line-based format. |
Both formats are produced from the same internal model, so component lists are identical (modulo format-specific fields).
What's included per component
Each component carries its name, version, Package URL (PURL), and the licenses detected for it:
- Licenses — populated from the scan's license findings. CycloneDX uses the
per-component
licensesarray (preferring the concluded verdict, then declared, then detected); SPDX fillslicenseDeclaredandlicenseConcludedas SPDX license expressions. Components with no detected license — and licenses with no SPDX identifier (ORTLicenseRef-*) — emit the spec sentinelNOASSERTIONin SPDX (CycloneDX still carries the license name).copyrightTextis currently alwaysNOASSERTION. - Top-level version —
metadata.component.versionreflects the scanned release: if the scan was submitted with areleaselabel (e.g.v1.2.3), that label is used; otherwise the scan id is used as a stable fallback.
Byte-stable output
All four exports are byte-stable: re-exporting the same scan produces identical bytes. This makes diffing, signing, and caching trivial.
The portal achieves byte-stability by:
- Sorting components by
purl(lexicographic). - Sorting license expressions alphabetically within each component.
- Pinning
serialNumber(CycloneDX) /documentNamespace(SPDX) to a deterministic value derived from(project_id, scan_id). - Omitting timestamps from the body (the SBOM's metadata records the scan finish time, which is stable per scan).
Download from the UI
- Open the project.
- Click the SBOM tab.
- Click one of the four format buttons (CycloneDX JSON, CycloneDX XML, SPDX JSON, SPDX Tag-Value) to download.

The file name is sbom-<project-slug>.<ext>.
Download from the API
The API serves the SBOM in CycloneDX JSON:
…and in SPDX JSON (same endpoint, different format):
# CycloneDX JSON
curl -sS -L -OJ \
-H "Authorization: Bearer ${TRUSTEDOSS_API_KEY}" \
"https://trustedoss.example.com/v1/projects/${PROJECT_ID}/sbom?format=cyclonedx-json"
# SPDX JSON
curl -sS -L -OJ \
-H "Authorization: Bearer ${TRUSTEDOSS_API_KEY}" \
"https://trustedoss.example.com/v1/projects/${PROJECT_ID}/sbom?format=spdx-json"
format accepts: cyclonedx-json, cyclonedx-xml, spdx-json, spdx-tv.
Endpoint always exports the latest succeeded scan's SBOM; pinning to a specific historical scan id is on the roadmap.
The SBOM export always reflects the latest succeeded scan. External auditors typically ask for the SBOM at a specific release point (e.g. "what shipped on 2026-01-15?"). Until historical-scan pinning lands, capture the SBOM artifact at each release boundary and store it in your release archive. Treat the portal as the current SBOM, not the historical one.
Policy profiles
By default the export is the raw inventory. An optional policy profile applies your team's license policy to the export, so a distribution SBOM reflects your organization's rules:
- Policy annotated — every violating component is flagged in place
without removing anything. CycloneDX components gain
trusca:policy:category/trusca:policy:spdxproperties; SPDX packages gain aREVIEWannotation. Forbidden and conditional components are annotated, so a reviewer sees why each is a concern inside the SBOM. - Policy filtered — forbidden components are dropped from the export
(along with the vulnerability entries that referenced them), and the number
removed is recorded on the document
(
trusca:policy:excluded_count/ a document annotation).
When no team or organization policy is configured, the profile falls back to the built-in classification catalog, so a forbidden GPL is still flagged.
Select the profile on the SBOM tab, or pass profile on the API:
curl -sS -L -OJ \
-H "Authorization: Bearer ${TRUSTEDOSS_API_KEY}" \
"https://trustedoss.example.com/v1/projects/${PROJECT_ID}/sbom?format=cyclonedx-json&profile=policy-annotated"
profile accepts: policy-annotated, policy-filtered. A profiled download
is named distinctly (sbom-<project>-<profile>.<ext>) so it is never mistaken
for the canonical export.
The cosign signature covers only
the default SBOM — the exact bytes produced at scan time. A policy profile
is generated on demand and
therefore has no signature; cosign verify-blob against a profiled export
will fail. Use the default export when you need a verifiable, signed artifact,
and treat profiled exports as review / distribution conveniences.
NOTICE file
For Apache-2.0 §4(d) compliance and similar attribution obligations, the portal auto-generates a NOTICE attribution body from the project's latest scan.
The file contains:
- A header with the project name and generation timestamp.
- One section per detected license, listing the components (
name @ version) under that license, each with a copyright line. - Each license section's attribution obligations (e.g. attribution, no-endorsement) with a short description and a policy reference link.
- A closing License Texts section with the full text of every license observed in the project.
- A License review needed section listing any component whose license carries an AI-specific restriction flag (behavioral-use or non-commercial); see AI license review flags.
Copyright lines
Each component entry carries the copyright statement the scan's SBOM recorded
for it (cdxgen reads it from package metadata). When the SBOM recorded no
copyright holder, the line is never left blank: it falls back to an explicit
note that the SBOM carries no copyright holder, pointing at the component's
registry URL so you can retrieve the statement from the source. A manual
per-component override in the component drawer is on the roadmap.
License texts
The document closes with a License Texts section reproducing the full
canonical text of each license that appears in the project. The portal bundles
the SPDX (Software Package Data Exchange) text for 32 well-known licenses —
MIT, Apache-2.0, and the BSD, GPL / LGPL / AGPL, MPL, EPL, and CDDL families,
among others. A license whose text is not bundled is not silently dropped: its
entry falls back to the license's reference-URL link to the canonical text.
With this section the NOTICE artifact itself satisfies the catalog's
license_text_inclusion_required obligation — see the
obligation catalog.
Supported formats
The NOTICE endpoint accepts a format query value (default text):
| Format | Query value (format=) | MIME | Extension | Use case |
|---|---|---|---|---|
| Plain text | text | text/plain | .txt | Drop into a release tarball's NOTICE file. The default. |
| Markdown | markdown | text/markdown | .md | Render in a docs site or PR description. |
| HTML | html | text/html | .html | A self-contained document (inline <style>, no scripts) for an attribution page. |
The output is byte-stable across exports for a given scan and format — diffable across releases.
Download
-
UI: Project → Obligations tab → pick a format (text or HTML) → Download NOTICE. The browser saves
NOTICE-<project>.<ext>. The markdown variant is available from the API. -
API:
# Plain text (default)curl -sS -L -OJ \-H "Authorization: Bearer ${TRUSTEDOSS_API_KEY}" \"https://trustedoss.example.com/v1/projects/${PROJECT_ID}/notice?format=text&download=true"# HTMLcurl -sS -L -OJ \-H "Authorization: Bearer ${TRUSTEDOSS_API_KEY}" \"https://trustedoss.example.com/v1/projects/${PROJECT_ID}/notice?format=html&download=true"formatacceptstext,markdown,html. Passdownload=trueso the response carriesContent-Disposition: attachmentand-OJsaves it under the server-supplied file name (NOTICE-<project>.<ext>); omit it to stream the body inline.
VEX exports
CycloneDX SBOMs include the project's VEX state for every finding. SPDX does not have a native VEX representation, so SPDX exports omit per-finding state; pair an SPDX export with a separate CycloneDX VEX document if your downstream consumer expects it.
Each entry in the SBOM's vulnerabilities[] array carries the CVE id, its
source database, the VEX analysis, and affects[].ref pointing at the
affected component's bom-ref within the same document (so consumers can
join findings to components without parsing PURLs).
The VEX states map directly to CycloneDX's analysis.state; the analyst's
free-text note (when present) is carried in analysis.detail:
| Portal state | CycloneDX VEX state | analysis.detail |
|---|---|---|
New | in_triage | (none) |
Analyzing | in_triage | analyst note |
Exploitable | exploitable | analyst note |
Not affected | not_affected | analyst note |
False positive | false_positive | analyst note |
Suppressed | not_affected | analyst note |
Fixed | resolved | analyst note |
The closed CycloneDX analysis.justification enum (code_not_present, …) is
never emitted: its members have a precise meaning that cannot be inferred
from free-form analyst prose, so the note stays in analysis.detail.
Verify it worked
-
The downloaded SBOM passes a validator — for CycloneDX, run
cyclonedx validate:cyclonedx validate --input-file checkout-service.sbom.json
-
SPDX validates with
spdx-tools:pyspdxtools -i checkout-service.sbom.json
-
Re-downloading the same scan produces a byte-identical file:
sha256sum checkout-service.sbom.json checkout-service.sbom.json.again# → identical hashes
Troubleshooting
Empty SBOM when no scan has succeeded yet
If the project has no succeeded scan yet, the export still returns a valid SBOM document with empty components/packages lists (HTTP 200) so downstream tooling can parse it.
422 from /sbom?format=…
The query string used a value the API does not accept. Use one of the four canonical query values from the table above — in particular, the SPDX Tag-Value format is spdx-tv (not spdx-tag-value).
404 for a project you cannot access
The SBOM and NOTICE endpoints existence-hide: a caller who is not a member
of the project's team receives 404 (not 403), the same response as a
project id that does not exist. This is deliberate — unlike the project-detail
endpoint (which returns 403), the SBOM/NOTICE bodies expose structural detail
(component names, versions), so the endpoints refuse to confirm a project even
exists to a non-member. Join the owning team to get access.
A copyright line shows a registry link instead of a holder
The scan's SBOM recorded no copyright holder for that component — cdxgen
found none in the package metadata. The NOTICE never leaves the line blank; it
notes the missing holder and points at the component's registry URL instead,
so you can retrieve the copyright statement from the upstream source directly.
A manual per-component override in the component drawer is on the
roadmap. The SPDX export is a separate surface and still emits
copyrightText: NOASSERTION.
Compliance evidence trail
External auditors typically ask portal operators five questions. This table tells you which are answerable today and which require workarounds.
| Auditor question | v0.10.0 answer source | Limitation |
|---|---|---|
| "Show me the SBOM as of release X" | Manual archive; portal only retains latest | Historical pinning on the roadmap |
| "Who downloaded the SBOM / NOTICE in the last quarter?" | structlog (Loki / journald) — not audit_logs | Audit-row promotion on the roadmap |
| "Show me when GPL was first detected on project X" | audit_logs on scans.create + per-scan vulnerability_findings.create | Yes — full evidence chain |
| "Show me every approval verdict in 2026 Q1" | audit_logs on component_approvals.update + decision_note | Yes — full evidence chain |
| "Prove no audit row was tampered with" | Append-only trigger (migration 0012) | Super-admin role still has bypass — review audit-log hardening |
Supplier submission compatibility
The export satisfies common corporate supplier SBOM requirements (e.g. SK Telecom's supplier guide): standard format/version (CycloneDX, SPDX 2.3), ISO-8601 timestamp, tool metadata, per-component name + version + PURL, licenses, and transitive dependencies (when the scanned source includes or can resolve lockfiles).
Two caveats to be aware of before submitting:
pkg:generic/PURLs are rejected by some programs. A generic PURL means the scanner could not classify the component's ecosystem; supply lockfiles / build artifacts so cdxgen can assign an ecosystem-specific type.- Licenses without an SPDX id appear as
NOASSERTIONin SPDX expressions (the CycloneDXlicense.namestill carries the label).
Roadmap
Items the manual previously promised that are not in this release; tracked for later releases.
- The vulnerability report is implemented — as PDF, and since v0.13.0 also as Excel (
.xlsx); see Vulnerabilities → Download a report (PDF or Excel) (GET /v1/projects/{id}/vulnerability-report.pdf/.xlsx). Still not implemented: a standalone Components Excel export and the Compliance PDF; those will land in a later release. Stakeholders who need a components table today should consume the SBOM (CycloneDX JSON) via their preferred tooling. - Manual copyright override in the component drawer for NOTICE assembly — planned.
- Historical-scan pinning on the SBOM and NOTICE exports — planned.
- Promote SBOM / NOTICE downloads from
structlogevents toaudit_logsrows — planned.
See also
- Verify SBOM signatures (cosign) — prove the SBOM is intact and signed by this deployment
- Components & licenses
- Vulnerabilities
- API overview