SBOM
SBOM generation across engineering teams: one policy, every pipeline
Most organizations do not have an SBOM problem; they have six SBOM processes. Fix the output first: one format, one set of fields, one moment of generation, one store. Then move each team's pipeline to meet it, preferring generation inside the release build. Customers, auditors and regulators such as the CRA look at the SBOM and at the process behind it, not at which scanner you bought.
Updated
Why per-team scanners do not add up to an SBOM programme
Point five tools at the same product and you get five SBOMs. The differences are structural, not bugs:
- Different inputs, read at different times. One tool reads the lockfile, another the installed dependency tree, another the container image, another the binary. Each sees a different slice of the product.
- Different names for the same thing. One identifies components by package URL, another by CPE, another by file name only. Two SBOMs that describe the same library cannot be joined on a common key.
- Different depth. Some list only direct dependencies, some the full transitive tree, and some also list build tools. Why generators disagree on the same repository is shown in open source SBOM generators.
- Some tools answer a different question. Infrastructure vulnerability scanners report what is installed on running hosts. That is useful for operations, and it is not a description of the product you ship.
The consequence: you cannot merge, deduplicate or compare SBOMs across products, and you cannot show an auditor one process, because there is none.
| Method | Input it reads | When | What it misses | Typical identifier |
|---|---|---|---|---|
| Lockfile or manifest scanner | Lockfiles and manifests in the checkout | Before or after the build | Anything no lockfile lists: install hooks, downloads by build scripts, base images | Package URL |
| Container image scanner | Package databases and files in the image | After the build | Build-only tools, compiled-in code, anything without a package record | Package URL for OS and language packages |
| Binary or firmware scanner | Strings and signatures in the binary | After the build | Certainty: versions and components are inferred | CPE or inferred name |
| Host vulnerability scanner | Installed software on running servers | In operation | The product as shipped to customers | CPE or vendor product name |
| Build-time recording | What the release build fetched | During the build | Software you did not build; what it could not see is flagged as incomplete | Package URL |
The SBOM policy: what to standardize
Standardize the SBOM, not the scanner. The policy fixes the output and the process; each pipeline meets it with the best generator it can run. Every row below is something you can check.
| Requirement | The rule | Why | How to check it |
|---|---|---|---|
| Scope | Every product release that ships to a customer, including firmware and container images. Internal tools optional. | Customers and regulators ask about what you ship. | Each release in the release register has an SBOM. |
| Format and version | One primary format, CycloneDX or SPDX, with a minimum version. Accept the other on request. | One format means one parser, one validator and one store. BSI TR-03183-2 asks for CycloneDX 1.6 or later or SPDX 3.0.1 or later, a sensible floor for EU customers. | Schema validation reports format and version. |
| Minimum fields | A package URL on every component that has one, a CPE only where none exists, supplier, version, declared license, dependency relationships, tool name and version, timestamp and generation context. | These are what matching, license review and audits use. Align them with the minimum elements on what is an SBOM. | A field check in the pipeline counts missing values. |
| When generated | In the CI build that produces the release, every time. Not at audit time, not on a laptop. | A rebuild is not the same build; see build-time SBOMs. | The SBOM's build ID matches the release's build ID. |
| Depth | The full transitive tree, not top-level only. | The CRA's minimum is the top-level dependencies; vulnerabilities and customers go deeper. | Dependency relationships exist below the first level. |
| Where stored | With the release artifact, retrievable by product and version. | An SBOM nobody can find when an advisory lands is not evidence. | Fetch the SBOM for a random release by product and version. |
| Retention | At least the product's support period plus the documentation retention your regulations require. | The CRA sets a retention period for technical documentation, and the SBOM is part of it. | Compare the policy period with each tool's retention. |
| Validation | Every SBOM is schema-validated in the pipeline; an invalid one fails the build or the release gate. | Invalid SBOMs fail silently in the customer's tooling. | The release gate log shows a validation step. |
| Ownership | The product team owns generation; product security owns the policy, the validation rules and coverage reporting. | Generation needs the pipeline owner; consistency needs one owner of the rules. | Each pipeline names an owner. |
| Exceptions | A named, time-boxed exception register: team, reason, end date. | The policy survives teams that cannot comply yet. | No exception is past its end date. |
The checklist in SBOM best practices covers the rules for a single SBOM; this policy is the layer above them. Each team declares how its pipeline meets the policy, in a file next to the pipeline:
# sbom-policy.yaml, one per release pipeline
product: gateway-firmware
owner: team-gateway
format: cyclonedx
spec_version: "1.6"
generated_in: release-build
generator: build-time-recording
store: artifact-registry
validation: schema, fail-on-errors
exception: none
Generate in the build, not per team after the fact
Build-time generation is the standard to aim for. It describes the build that shipped, it includes hidden dependencies that no lockfile lists, and one mechanism produces the same shape of SBOM for every team on that pipeline. The difference is easiest to explain with a kitchen: a scanner tastes the soup and guesses the recipe; a recorder watches what goes into the kitchen.
A scanner is still the right tool for images you did not build, for third-party binaries, and for legacy products with no CI. Say so in the policy, and record the generation context, so a reader knows what each SBOM saw.
| Situation | Generation method | Generation context to record |
|---|---|---|
| Release built in CI you control | Record the build, or run a generator inside the release job | "Recorded during build" or "after install, from files" |
| Container image built in CI | Record the image build; scan the result for its OS packages if the recorder does not cover them | Build ID and image digest |
| Third-party image or appliance | Image scanner, plus the vendor's SBOM if there is one | "Scanned output, not built by us" |
| Supplier binary | Ask for the supplier's build SBOM; binary analysis as a check | Supplier, version, how it was obtained |
| Legacy product with no pipeline | Scan the shipped artifact; plan a pipeline | "Scanned output" and an exception with an end date |
Mixed CI: GitHub Actions, GitLab and Jenkins in one organization
The policy is CI-agnostic by design: it defines the output, so a GitLab or Jenkins pipeline can meet it with a generator step, and a GitHub Actions pipeline can use build-time recording. Rules for a generator step:
- Run it in the release job, after the install step, not in a separate job that installs again.
- Pin the generator's version, and pin CI actions to a commit, not a tag. Why that matters for scanners in particular is covered in open source SBOM generators.
- Record the generation context as "after install, from files", so readers know what it saw.
- Validate the output against the same schema as every other pipeline.
Open source generators such as Syft, Trivy and cdxgen can all write CycloneDX and SPDX, so the format rule does not force a tool choice. To compare them, see SBOM tools compared.
CRACI today supports GitHub Actions; GitLab CI and Jenkins are on the roadmap, with no date. The rollout consequence: start build-time recording where GitHub Actions is used, keep generator steps elsewhere, and track both in the same coverage report.
When a customer wants a specific format version
A customer asks for CycloneDX 1.6, and your tooling writes 1.7. It happens because their ingestion tool or their own policy is pinned to a version; BSI's floor is 1.6, so EU customers often name it. Your options:
- Ask whether the newer version is accepted. Often it is.
- Convert with the CycloneDX CLI, then validate against the target version, and check what was lost: fields that exist only in the newer specification are dropped.
- For an SPDX request, the same CLI converts to and from SPDX JSON 2.3. Its documentation warns that converting between SPDX and CycloneDX "can result in the loss of some information".
cyclonedx convert --input-file bom.cdx.json --input-format json \
--output-file bom-1.6.cdx.json --output-format json --output-version v1_6
cyclonedx validate --input-file bom-1.6.cdx.json --input-format json \
--input-version v1_6 --fail-on-errors
The validator checks against CycloneDX 1.7 unless you pass --input-version. Policy rule: keep the
original SBOM as the record. A converted copy is a derived delivery; store the derivation (tool, version, date) with
it. More on answering these requests in customer SBOM requests, and on the two
formats in CycloneDX vs SPDX.
License data for legal
The same SBOM can serve the legal team, if the policy asks for the right fields. Legal needs the license of every component, the difference between a declared license (what the package says) and a concluded one (what someone verified), copyleft obligations, and a list of components with no license. CycloneDX 1.6 marks licenses as declared or concluded, and SPDX has separate fields for both.
- Require SPDX license identifiers or expressions, such as
MITorApache-2.0 OR MIT. -
Flag
NOASSERTIONand missing licenses for review. In SPDX,NOASSERTIONmeans no determination was made or no information was given; it is not a license. - Route copyleft licenses to legal before release, not after.
An SBOM carries declared licenses. A license compliance review, with concluded licenses, notices and obligations, is legal work or a job for a specialist tool; see open source license compliance.
Rolling it out team by team
| Phase | Owner | Exit criterion | Typical blocker |
|---|---|---|---|
| 1. Inventory: products, pipelines, CI systems and current generators, one row per release pipeline | Product security | Every shipped product has a row | Nobody knows which pipeline builds a release |
| 2. Publish the policy with the exception register | Product security | Policy approved and published | Debate over which tool to mandate (the policy does not mandate one) |
| 3. Pilot with one or two teams that have a reason: a customer request, a deadline, a product the current tool cannot handle | Pilot teams | Pilot releases ship with compliant SBOMs | Teams without a burning reason deprioritize it |
| 4. Add validation gates in the pilot pipelines | Pilot teams | An invalid SBOM fails the release | Fear of blocking releases; start with a warning |
| 5. Expand by CI system; retire a per-team scanner only when the replacement meets the policy | Product teams | Coverage target met per CI system | Pipelines nobody owns |
| 6. Report coverage monthly | Product security | The report goes to management every month | Metrics computed by hand |
The inventory spreadsheet in phase 1 should be the last one; moving off spreadsheets is covered in SBOM automation. A Yocto-based product is a common pilot, because general-purpose scanners struggle with it; see why SBOM scanners miss Yocto builds.
Measuring coverage
The numbers management and auditors ask for, each defined as a ratio:
- Release coverage: releases shipped with a policy-compliant SBOM ÷ releases shipped.
- Product coverage: products with at least one compliant SBOM in the current support period ÷ products in support.
- Validation pass rate: SBOMs that pass validation ÷ SBOMs generated.
- Identifier coverage: components with a package URL (or a CPE) ÷ components.
- License coverage: components with a declared license ÷ components.
- Completeness: SBOMs whose generator states they are complete ÷ SBOMs. Only possible where the tool reports it.
- Time to SBOM: time from release to SBOM available.
A sample monthly report (illustrative figures):
| Metric | GitHub Actions pipelines | GitLab and Jenkins pipelines | All |
|---|---|---|---|
| Release coverage | 46 / 48 | 19 / 27 | 65 / 75 |
| Validation pass rate | 100% | 92% | 97% |
| Identifier coverage | 99% | 88% | 95% |
| Open exceptions | 1 | 4 | 5 |
As evidence for a regulator or an auditor, the set is the policy, the exception register, the coverage report and a few sample SBOMs. It shows one process, applied and measured. For the CRA side, see the CRA guide.
How CRACI fits
For GitHub Actions pipelines, CRACI is the runner: you set runs-on: craci, and every job gets a recorded
SBOM without a generator step. Each SBOM carries a completeness state, the transitive dependencies, a package URL per
package and each component's declared license, and exports as CycloneDX or SPDX. License policy rules can block a
build, monitored SBOMs are re-evaluated continuously, and build history shows which builds contained a vulnerable
dependency. That gives those teams the policy's "generated in the release build", "identifier" and "license" rows by
default. Third-party SBOMs can be added, so bought-in components are monitored alongside your builds.
See SBOM generation on CRACI and SBOMs in GitHub Actions.
When CRACI is not the answer
- Teams on GitLab CI or Jenkins: not today, it is on the roadmap. Use a generator step that meets the policy.
- GitHub Enterprise Server: not supported today.
- Infrastructure and host scanning, meaning what is installed on servers and misconfigurations: a different job; keep those tools.
- Binary or firmware analysis of supplier images you do not build: CRACI records builds and does not scan binaries.
- Long retention: CRACI keeps build data for 180 days by default, with custom retention on Enterprise. If your policy needs SBOMs for longer, export them to your own store.
SBOM generation across teams: frequently asked questions
How do we standardize SBOM generation across teams that use different tools?
Standardize the output, not the tool. Write one policy that fixes the format and version, minimum fields, when the SBOM is generated, where it is stored, how long it is kept and who validates it. Let each pipeline meet it with the best generator it can run, and measure coverage against the policy.
What should an SBOM policy include?
Scope, format and minimum version, minimum fields (package URL, supplier, version, declared license, dependencies, tool, timestamp, generation context), generation in the release build, full transitive depth, storage with the release, retention, validation in the pipeline, ownership, and a time-boxed exception register.
Should every team run its own scanner, or should SBOMs come from the build?
From the build, where you can. A build-time SBOM describes the build that shipped and gives every team on that pipeline the same shape of SBOM. Keep scanners for what you did not build: third-party images, supplier binaries and legacy products with no pipeline.
A customer wants our SBOM in an older CycloneDX version. What do we do?
Ask first whether the newer version is accepted. If not, convert a copy with the CycloneDX CLI (cyclonedx convert with --output-version v1_6), validate it against that version, and check which fields were dropped. Keep the original as the record and store how the copy was derived.
How do we get license data to legal from our SBOMs?
Require SPDX license identifiers or expressions in the policy, flag NOASSERTION and missing licenses for review, and route copyleft licenses to legal before release. The SBOM carries declared licenses; concluded licenses, notices and obligations need a license review.
How do we measure SBOM coverage across products?
As ratios: releases shipped with a compliant SBOM, products in support with a compliant SBOM, validation pass rate, components with a package URL, components with a declared license, complete SBOMs where the tool reports it, and time from release to SBOM. Report them monthly by CI system.
What evidence do we need to show an auditor that SBOM generation is consistent?
The policy, the exception register, the monthly coverage report and a few sample SBOMs with their generation context. Together they show one process, applied to every release and measured, which is what an auditor or a customer checks.
Pilot it on one GitHub Actions pipeline
Book a demo and run one release pipeline on CRACI. Get an SBOM recorded from the build and check it against your policy.
Book a demo