SBOM License Inventory: What a Complete SBOM Contains
Almost every security team can produce an SBOM. Far fewer can produce one they would stake an audit answer on. The file validates, the format is right, and it still leaves out half of what the software actually depends on.
An SBOM is only useful if it answers two questions without a follow-up: "are we affected?" when the next Log4Shell lands, and "what are we licensed to ship?" when legal or a customer asks. That second question is where most exports fall down, because a real SBOM license inventory needs more than a list of package names.
This post covers what a complete SBOM contains, the gaps that show up in most exports, and a checklist for judging the one your tooling produces.
What an SBOM is for
A software bill of materials is an inventory of the components your software is built from: each library, its version, where it came from, how it relates to the others, and the license it ships under.
It exists to make three jobs fast:
- Vulnerability response. When a new CVE or malicious package is announced, you search the inventory instead of grepping repositories.
- License compliance. You know which obligations you have taken on before you ship, not after a customer's legal team asks.
- Supplier assurance. Customers, regulators and procurement teams increasingly ask for an SBOM as evidence. US federal guidance has pushed this since Executive Order 14028, and the EU Cyber Resilience Act makes component inventory an explicit expectation for products sold in the EU.
An SBOM that misses components fails all three jobs quietly. You search it, find nothing, and conclude you are not affected.
The two formats: CycloneDX and SPDX
Two formats dominate, and most tools can emit both.
- CycloneDX is an OWASP standard built for security use cases. It models components, dependency relationships, services and vulnerability data, and it is the format most security tooling consumes.
- SPDX is a Linux Foundation standard, published as ISO/IEC 5962:2021. It started with license compliance and carries the SPDX license list and license expression syntax that the rest of the industry uses.
The format matters less than the content. A CycloneDX file that lists direct dependencies only is worse than a complete one in either format. Pick the format your consumers need and spend your effort on completeness.
The minimum elements, and why they are a floor
The US NTIA published the minimum elements for an SBOM in 2021. For each component, the data fields are:
- Supplier name
- Component name
- Version of the component
- Other unique identifiers, such as a package URL (purl)
- Dependency relationship
- Author of the SBOM data
- Timestamp
The same document also sets expectations for automation support, how often SBOMs are generated, depth, and how "known unknowns" are declared.
Two things are missing from that field list that most consumers now expect: license and scope (whether a component ships, or only builds). The minimum elements were written as a floor. Treat them that way.
What most SBOM exports leave out
These are the gaps that show up again and again when you open a generated SBOM and compare it to what is really running.
Transitive dependencies
An SBOM built from a manifest alone lists what you declared. Most of your code is what those packages pulled in. A package.json with 40 entries can resolve to well over a thousand packages. If the tool did not read the lockfile or resolve the graph, the SBOM is a table of contents with most of the chapters missing.
Resolved versions
A range such as ^4.17.0 is not a version. An SBOM entry needs the exact version that was installed, otherwise every vulnerability match is a guess.
License data, in a usable form
This is the most common gap. Exports either omit licenses, copy whatever free-text string the package metadata contained, or record one license where the package declares a choice. A usable SBOM license inventory records an SPDX license expression:
MITfor a single licenseMIT OR Apache-2.0for a dual-licensed package, where you may choose eitherGPL-2.0-only WITH Classpath-exception-2.0for a license with an exception
The difference between OR and AND is the difference between "pick one set of obligations" and "comply with both". A tool that flattens that to a single string makes license policy impossible to evaluate correctly.
The license of the version you actually use
Licenses change between releases. Several well-known projects have moved from permissive licenses to source-available ones in recent years. If your SBOM records the license a package publishes today instead of the license on the version you pinned, it can flag a problem you do not have or miss one you do.
Build-only vs shipped components
Test frameworks, linters and build plugins rarely ship to customers. An SBOM that mixes them in with runtime dependencies overstates exposure and license obligations. One that silently drops them understates your build supply chain. The fix is to record scope, and let the consumer filter.
CI and build tooling
The GitHub Actions and reusable workflows your pipelines run are dependencies with real supply-chain risk. Most SBOMs do not include them at all. Whether they belong in the document you hand a customer is a choice, but you should be able to include them for your own inventory.
First-party packages
Internal libraries published to a private registry are components too. They carry their own dependencies and their own vulnerabilities. Leaving them out hides a real part of the graph.
Multi-module repositories
A repository with five services and five manifests often produces five partial SBOMs, or one that covers only the first manifest the tool found. The useful artifact is one document for the repository, with duplicates removed.
What is actually deployed
A build-time SBOM says what could ship. A runtime SBOM says what is running. They drift apart constantly: a hotfix deployed from a branch, a service that loads an extra plugin, a container base image that adds packages. For incident response, the runtime view is the one that answers "are we affected in production?"
Document metadata
A timestamp, the tool that produced it, a serial number and the root component it describes. Without them, two exports of the same system cannot be told apart and nobody knows how stale a file is.
A checklist for judging your SBOM
Pick one production service and open its SBOM. Then check:
- Count. Does the component count look like the resolved lockfile, or like the manifest? If it is close to the number of lines in
package.json, transitive coverage is missing. - Versions. Are all versions exact? Any range or wildcard is a gap.
- Identifiers. Does each component have a purl or equivalent, so it can be matched to advisories without name guessing?
- Licenses. Are licenses SPDX identifiers or expressions? Pick a known dual-licensed package and see how it was recorded.
- Relationships. Can you trace a transitive component back to the direct dependency that pulled it in?
- Scope. Can you tell shipped components from build-only ones?
- Freshness. Is there a timestamp, and is it from the current release?
- Reality. Pick a package you know is loaded in production. Is it there?
If three or more of these fail, the SBOM is not ready to be used as evidence. It is a starting point.
Generate SBOMs from the graph, not the manifest
The underlying problem is that many SBOMs are produced by reading one file at build time. A complete inventory needs the resolved dependency graph for every module, license data per resolved version, the first-party packages in your private registries, and a view of what is deployed.
Heeler generates CycloneDX SBOMs from that graph at five scopes: global, application, repository, service and service deployment. The service and deployment exports are runtime SBOMs built from what Heeler observes in the deployed environment.
- Repository SBOMs merge every module into one document and remove duplicate components, so a multi-module repository produces one portable file.
- Scope controls let you exclude GitHub Actions, dev and build dependencies, or first-party packages, and restrict the export to open-source, proprietary or unknown license categories.
- Each component carries its name, exact version, supplier, license, dependency relationships and a unique identifier.
- License policy is judged on the version you pinned. A dependency is flagged when the license on that exact version is disallowed, and policy is applied to the resolved SPDX expression.
The docs cover each scope in detail: Software Bill of Materials and License Violations. For how SBOMs feed audit evidence, see compliance and audit readiness, and for the dependency analysis behind them, SCA.
FAQ
What should a complete SBOM contain?
A complete SBOM lists every direct and transitive component with its exact version, supplier, unique identifier, license expression and dependency relationships, plus document metadata such as a timestamp and producer. It should also let you tell shipped components from build-only ones.
Is CycloneDX or SPDX better?
Neither is better in general. CycloneDX is common in security tooling and SPDX is common in license compliance. Completeness matters far more than format, so produce whichever your consumers ask for.
Why does my SBOM have so few components?
It was probably generated from manifests without resolving the dependency graph, so it lists what you declared and not what was installed. Generate it from the lockfile or the resolved graph instead.
What is an SPDX license expression?
An SPDX license expression is a standard way to write a component's license, such as MIT OR Apache-2.0, that preserves choices, combinations and exceptions. License policy tools need expressions, not free-text strings, to evaluate obligations correctly.
Should an SBOM include dev dependencies?
Your internal inventory should include them with their scope recorded, because build tooling is part of your supply chain. The SBOM you hand a customer usually excludes them, since they do not ship.
Check the SBOM you would hand an auditor
Heeler builds SBOMs and license inventories from the resolved graph of every repository, registry and running service, and exports them as CycloneDX at the scope you need. Get a demo.


.jpg)
