SBOM
SBOM examples
The fastest way to understand an SBOM is to read one. Here is the same small application in CycloneDX and in SPDX, with what each field means, what a weak SBOM looks like and how to check your own.
Updated
The application in these examples
Both files describe example-api 2.4.0, an illustrative Node.js service with one direct dependency,
express. Express in turn depends on accepts, so the examples show one transitive
dependency as well. A real SBOM of an Express application lists many more components, but the structure is the same.
CycloneDX SBOM example
{
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
"version": 1,
"metadata": {
"timestamp": "2026-10-30T09:00:00Z",
"lifecycles": [{ "phase": "build" }],
"tools": {
"components": [
{ "type": "application", "name": "example-sbom-tool", "version": "1.0.0" }
]
},
"component": {
"type": "application",
"bom-ref": "example-api",
"name": "example-api",
"version": "2.4.0"
}
},
"components": [
{
"type": "library",
"bom-ref": "pkg:npm/[email protected]",
"name": "express",
"version": "4.21.2",
"purl": "pkg:npm/[email protected]",
"licenses": [{ "license": { "id": "MIT" } }]
},
{
"type": "library",
"bom-ref": "pkg:npm/[email protected]",
"name": "accepts",
"version": "1.3.8",
"purl": "pkg:npm/[email protected]",
"licenses": [{ "license": { "id": "MIT" } }]
}
],
"dependencies": [
{ "ref": "example-api", "dependsOn": ["pkg:npm/[email protected]"] },
{ "ref": "pkg:npm/[email protected]", "dependsOn": ["pkg:npm/[email protected]"] }
]
}
| Field | What it tells you |
|---|---|
specVersion |
Which version of the format the file uses, so that tools can parse it. |
serialNumber, version |
Which SBOM this is, and which revision of it. |
metadata.lifecycles |
When the SBOM was made. Here, during the build. |
metadata.tools |
Which tool produced it. Two tools can report the same commit differently. |
metadata.component |
The product the SBOM describes. |
purl |
The package URL, the identifier that lets a tool match the component to advisories. |
licenses |
The component's declared license. |
dependencies |
Who pulls in whom. This is what makes accepts visible as a transitive dependency. |
SPDX SBOM example
{
"spdxVersion": "SPDX-2.3",
"dataLicense": "CC0-1.0",
"SPDXID": "SPDXRef-DOCUMENT",
"name": "example-api-2.4.0",
"documentNamespace": "https://example.com/spdx/example-api-2.4.0",
"creationInfo": {
"created": "2026-10-30T09:00:00Z",
"creators": ["Tool: example-sbom-tool-1.0.0"]
},
"packages": [
{
"name": "example-api",
"SPDXID": "SPDXRef-Package-example-api",
"versionInfo": "2.4.0",
"downloadLocation": "NOASSERTION"
},
{
"name": "express",
"SPDXID": "SPDXRef-Package-express",
"versionInfo": "4.21.2",
"downloadLocation": "NOASSERTION",
"licenseDeclared": "MIT",
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": "pkg:npm/[email protected]"
}
]
},
{
"name": "accepts",
"SPDXID": "SPDXRef-Package-accepts",
"versionInfo": "1.3.8",
"downloadLocation": "NOASSERTION",
"licenseDeclared": "MIT",
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": "pkg:npm/[email protected]"
}
]
}
],
"relationships": [
{
"spdxElementId": "SPDXRef-DOCUMENT",
"relationshipType": "DESCRIBES",
"relatedSpdxElement": "SPDXRef-Package-example-api"
},
{
"spdxElementId": "SPDXRef-Package-example-api",
"relationshipType": "DEPENDS_ON",
"relatedSpdxElement": "SPDXRef-Package-express"
},
{
"spdxElementId": "SPDXRef-Package-express",
"relationshipType": "DEPENDS_ON",
"relatedSpdxElement": "SPDXRef-Package-accepts"
}
]
}
The information is the same, arranged differently. SPDX keeps a packages list and states every link
between items in relationships. licenseDeclared is what the package says about itself, and
downloadLocation may be NOASSERTION when the tool does not know it. For how the two formats
differ beyond layout, see CycloneDX vs SPDX.
What a weak SBOM looks like
A file can be valid and still be nearly useless. Watch for these:
- No dependency relationships. A flat list tells you that a package is present, not what pulled it in, which is what you need in order to fix a transitive dependency.
- No package URLs. Without an identifier such as a purl, matching a component to advisories falls back on guessing from names.
-
Missing licenses. Many fields left as
NOASSERTIONmean the SBOM cannot answer license questions. - Suspiciously few components. A handful of components for a large application usually means only direct dependencies, or only what one lockfile declared.
- No word on when or how. Without a tool and a generation context, you cannot tell what the SBOM can be trusted to cover.
- Silence about gaps. A good SBOM says what it could not see. Most files simply stop, and a reader assumes they are complete.
How to check your own SBOM
- Validate the format. Run it through a schema validator for its format and version, because a tool that cannot parse the file cannot use it.
- Check the fields. Compare it with the 2026 Minimum Elements or the list your customer asked for.
- Search for something you know. Pick a transitive dependency, a package from your base image and a build tool, and see which of them appear.
- Compare two sources. Generate a second SBOM with a different method and look at the difference. Each extra component in one file is a question about the other.
A build-time SBOM from CRACI
CRACI exports the same formats. What differs is where the content comes from: it records what each GitHub Actions job fetched, so every component in the SBOM was actually pulled into the build, and packages restored from CI caches are marked as such. GitHub Actions the workflow ran appear as components too, pinned to the commit used. Each job's SBOM also carries a completeness state, so the gaps are stated instead of left for you to find. See build-time SBOM generation, or how SBOM tools compare.
SBOM examples: frequently asked questions
What does an SBOM look like?
It is usually a JSON file, sometimes XML. It starts with data about the SBOM itself, such as the tool and timestamp, then lists each component with its name, version, identifier and license, and finally describes how the components depend on each other.
Is an SBOM a JSON file?
Usually, because JSON is the most widely used encoding of both CycloneDX and SPDX. Both formats have other encodings too, and SPDX also has a tag-value text form.
What is a purl in an SBOM?
A purl, or package URL, is a standard identifier for a package, such as pkg:npm/[email protected]. It names the ecosystem, package and version in one string, which lets tools match the component to vulnerability data reliably.
How many components should an SBOM have?
There is no right number, but it should include transitive dependencies, which usually far outnumber the direct ones. Very few components for a large application usually means the tool only saw part of it.
Can you see an SBOM for an open source project?
Sometimes. Some projects publish SBOMs with their releases, and GitHub can export one from a repository's dependency graph. That export reflects what the repository declares rather than what a specific build used.
Compare your SBOM with a recorded one
Run one GitHub Actions workflow on CRACI and see which components your current SBOM does not list.
Book a demo