v0.21.0 — The 2026 SBOM minimum elements, outbound licence conflicts, and scans that describe the right thing
A feature release, and an unusually corrective one. Two new capabilities arrive — the 2026 SBOM minimum elements and outbound licence-conflict verdicts — alongside a set of fixes for scans that completed successfully while describing something other than what was asked for. Those are worth reading even if you skip the rest: a passing scan is not evidence when it measured the wrong branch, and an empty vulnerability list is not evidence when nothing was matched.
The full machine-readable changelog lives in
CHANGELOG.md.
Read this before upgrading
Two changes alter what you see for data you already have.
Container inventories change ecosystem on the next scan. Container scans
recorded every package as an Alpine package regardless of the image, so a Rocky
image's rpms and a Debian image's debs were inventoried as pkg:apk/*. Nothing
failed and no vulnerability was missed — the counts were right and only the
identity was wrong — which is why it went unnoticed. Existing rows are left as
they were stored; re-scanning an rpm or deb image corrects it, and that
re-scan resets the project's vulnerability triage, because analyst verdicts
and the first-detected clock are keyed on the component and the corrected
package is a different component. Deployments that only scanned Alpine images
are unaffected. See Upgrade.
Vulnerability counts can rise on SBOMs you already uploaded. If a supplier SBOM listed OS packages without naming the operating system, it previously matched nothing at all. Re-ingesting it now produces findings — in one measured case, a five-package CentOS document went from 0 to 306.
Highlights
Every SBOM is measured against the 2026 minimum elements
The 2026 Minimum Elements for a Software Bill of Materials (v2.1, 2026-07-29, published by CISA, NSA and FBI with fifteen international partners) replace the NTIA elements of 2021. They apply to all software rather than a subset, so the baseline carries no condition and is measured on every CycloneDX document: 17 data fields and 6 practices, 23 checks.
Every check is advisory and none moves the pass/fail verdict. That is deliberate and follows the guidance itself, which accepts an explicit statement that a value is unknown in place of the value — promoting a check would fail an SBOM for a value it may legitimately not have. Four of the six practices describe how an organisation operates rather than what a document contains, so they carry a note saying what to establish instead of a score.
The regulatory crosswalk's US framework moves onto these elements. Its rollup now
states failed explicitly rather than leaving it to be inferred from the
remainder, and the four counters partition the total.
The conformance panel was reordered to match: it leads with what blocks the SBOM — mandatory failures, advisory shortfalls, rows needing a person — and gives each baseline its own section, so 23 advisory rows cannot push the mandatory checks down the page. Where a check is short, the panel now shows the CycloneDX fragment that would satisfy it, in a fold-away block, for both baselines.
See SBOM conformance.
Declare the licence you ship under, and every dependency is judged against it
A licence conflict exists only relative to an outbound licence, so the policy axis could never express one — a policy can say AGPL-3.0 is forbidden, but not that it conflicts with the MIT licence this project ships under.
Projects now declare an outbound licence in their settings. A compatibility matrix judges every licence in the scan against it, and the Compliance grid gains a verdict column, a filter and a summary, with the reasoning shown in the drawer.
No declaration means no verdicts. An empty column is not a clean result, and the summary distinguishes "not assessed" from "assessed, none found".
Migration 0050 adds the column.
Exports say who made them, when, and with what
The 2026 elements ask an SBOM to state the lifecycle phase it was generated at,
who generated it, with which tool at which version, and whether an empty field is
unknown or withheld. None of the four was recorded, and the tool version was the
literal 0.0.1.
The phase now follows from the scan kind, using the distinction the elements draw
themselves: source manifests are pre-build, a built image is post-build. An
ingested supplier document is re-exported with no phase at all, because
converting someone else's document does not make us its author. SBOM_AUTHOR
declares the author; when it is unset the field is omitted rather than filled
with a placeholder that would satisfy the element while answering nothing.
These statements now land on both documents TRUSCA produces — the export rebuilt on request and the scan document that is signed and offered for download. They reached the export route first, which left the SBOM a consumer actually receives as the silent one.
Fixes worth knowing about
A pull request's scan described the base branch
Every source scan ran git clone --depth 1, which takes the remote's default
branch. The ref travelled alongside as a retention key only, so a PR that added a
vulnerable dependency passed, and a critical CVE on main blocked PRs that had
nothing to do with it. Branch-scoped verdicts named the wrong code.
The worker now fetches the ref and checks it out. A ref that has since vanished
falls back to the default branch and records metadata.ref_fallback, because a
verdict from a substituted target has to be distinguishable from one that ran
where it was asked to.
A supplier SBOM full of OS packages matched nothing
Trivy chooses a distro advisory database from an operating-system component in
the document, not from the package PURLs. An SBOM listing every rpm on an image,
each PURL well formed, therefore reported zero vulnerabilities.
The distro is now inferred from the packages themselves and an enriched copy is scanned. The upload is never edited — it backs the conformance verdict and the signature bundle — and an unrecognised distro contributes nothing, so a document this cannot place is scanned as it arrived rather than guessed at.
A second defect sat behind it: Trivy labels an OS-package result with the distro name, which no PURL reconstruction maps, so findings it had matched were discarded before they reached the database. Those now resolve through the PURL Trivy attaches to each finding.
CI could not run a container scan at all
Neither CI client could send image_ref, which the worker requires, so every
container scan they triggered queued, waited for a worker and failed there. The
action and the GitLab template now take the image, and the request is rejected at
trigger time with a clear message instead.
A second CI run against the same ref failed the build
The portal allows one active scan per (project, ref) and answered a second
trigger with 409, which all three CI clients treated as fatal. GitHub's own
recommended cancel-in-progress workflow therefore broke itself: the runner
dies, its scan keeps going server-side, and the replacement run collides with a
scan nobody is watching. The 409 now names the scan holding the ref and the
clients attach to it. Polling also survives a transient blip rather than
discarding a nearly-finished scan, and 429 honours Retry-After.
A push could go unscanned while the delivery log said otherwise
A delivery skipped because the ref already had a running scan was reported as
duplicate — this API's word for a replayed delivery — so an operator reading
the SCM's delivery log saw a commit as handled when nothing had scanned it. It
now says skipped_active_scan. That path also answered 500, which the Git host
retried.
Pull-request events carry no top-level ref, so webhook-triggered PR scans were
stored ref-less and grouped with nothing; they are now keyed pr-N / mr-N like
the CI clients' scans. The receiver also asks the capacity guards before
recording a delivery, so it no longer bypasses the team concurrency cap and the
disk limit.
Nested components were invisible
CycloneDX lets a component declare its own components array — a bundled
dependency, an image layer's packages — and only the top level was read. Three
places each looked correct alone: persistence dropped the nested entries,
conformance scored a denominator without them, and a Trivy finding against a
nested component was resolved by PURL to a row that was never written, so the
finding disappeared with nothing logged.
Smaller corrections
- File entries were counted against the package fields. A file carries no package version and no PURL type, so a document was reported as short on what its entries cannot hold. Files are now asked for the identifier they can carry.
- Every SBOM asserted a release that does not exist. The builder version
defaulted to
2.3.0-devand no deployment setTRUSTEDOSS_VERSION. Release images inject the tag at build time and the default is nowunknown. - The regulatory crosswalk's disclaimer named the wrong product — the sibling project the data was vendored from, rather than the product showing it. It now reads TRUSCA in both languages.
- The CI guides promised things that were not there: a webhook secret recipe importing a function that does not exist, a Jenkins quickstart grading a failed scan against an older one, and three guides pointing at a portal page and API key scopes that do not exist.
Upgrading
Follow the standard upgrade procedure. Migration 0050 is forward-only, like every migration in this project.
Two post-upgrade actions are worth planning for:
- Re-scan any rpm or deb based container image whose inventory you rely on, and expect that project's vulnerability triage to reset.
- Re-ingest any supplier SBOM that lists OS packages, which may not have matched anything before.