The sbom subcommand¶
When and why to generate an SBOM¶
A Software Bill of Materials (SBOM) is a structured inventory of every component in your project, plus its provenance, licence, and (in adacovex’s case) its SPARK proof level and compliance context. Generate one in these cases:
Auditing or certifying – an SBOM is a standard artifact for safety certification (DO-178C, ISO 26262, IEC 62304). It shows the auditor exactly which Ada/Alire components are in scope and which are proved / not proved.
Shifting left on supply-chain security – the dependency graph exposes transitive dependencies, their licences, and their PURLs so you can review them before a release.
Embedding in CI – the proof-aware SBOM carries the assessed standard and level, so downstream tooling (policy engines, compliance dashboards) can consume it without re-parsing markdown reports.
Reproducible builds – adacovex honours
SOURCE_DATE_EPOCH, so the SBOM timestamp is deterministic when tied to a git commit.
adacovex sbom resolves the target project’s dependency graph from its Alire
manifest (alire.toml / alire-dev.toml), the solved-crate list in
alire/alire.lock, and the root .gpr with clauses, then writes a
proof-aware software bill of materials in CycloneDX 1.5 JSON, SPDX 2.3 JSON,
or Markdown.
How to read the output¶
Choose the format that matches your toolchain:
CycloneDX 1.5 JSON (
--format=cyclonedx-json) – drop into any CycloneDX consumer (Dependency-Track, OWASP Dependency-Check, and more). The root component carriesadacovex:proof_level,adacovex:standard,adacovex:dal_target, andadacovex:level. Every dependency has alanguagefield inferred from file extensions or declared ecosystems.SPDX 2.3 JSON (
--format=spdx-json) – compatible with SPDX-aware tools (FOSSA, Snyk, ScanCode). The adacovex properties appear asattributionTextson the root package.Markdown (
--format=md) – human-readable table, useful for audits and reports. Default path is<target>/docs/compliance/SBOM.md.
Key fields¶
Field |
Where it appears |
Meaning |
|---|---|---|
|
Root component |
Assessed SPARK level ( |
|
Root component |
Compliance standard ( |
|
Root component |
Shared rigour tier ( |
|
Root component |
Standard-specific label ( |
|
Every component |
Implementation language(s) inferred from file extensions |
|
Every component |
Package URL for registry linking |
Dependencies report adacovex:proof_level = "Not proved" because adacovex only
proves the target itself. The SBOM is mutually exclusive with --compare-base
and --coverage-delta.
Usage¶
adacovex sbom [--format=cyclonedx-json|spdx-json|md] [--out=PATH]
[--standard=NAME|--dal=LEVEL|--asil=LEVEL|--class=LEVEL]
Default output:
<target>/sbom.jsonforcyclonedx-json,<target>/sbom.spdx.jsonforspdx-json, and<target>/docs/compliance/SBOM.mdformd. The containing directory is created automatically.Exit code:
0when the SBOM was written,1otherwise. If the target has no Alire manifest the SBOM cannot be generated (the GitHub Action reports this as a warning without failing the job).
Standard-awareness¶
The sbom subcommand accepts the same standard flags as the assessment
(--standard, --dal, --asil, --class) and defaults to all
standards: without an explicit standard flag the SBOM carries the joined
DO-178C / ISO 26262 / IEC 62304 properties at the shared DAL tier.
--standard=iso26262 / --asil=B narrows it to ISO 26262 at ASIL B, and
--class=A to IEC 62304 at Class A. See Standards for the
cross-standard tier mapping.
Properties¶
Only the root component – the project adacovex actually assessed – carries:
adacovex:proof_level–Stone..Platinum, the honest assessed level.adacovex:standard–DO-178C/ISO 26262/IEC 62304.adacovex:dal_target–DAL-A..DAL-D(omitted forDAL-E).adacovex:level– the standard-specific label (DAL-C/ASIL B/Class A, omitted forDAL-E).
Dependency components report adacovex:proof_level = "Not proved" (adacovex
only proves the target itself, never third-party dependencies). Properties are
encoded as attributionTexts in SPDX.
Determinism¶
The metadata.timestamp / creationInfo.created field honors the SOURCE_DATE_EPOCH environment variable (reproducible-builds convention). When set to a Unix epoch second count, the timestamp is derived from it in UTC via pure integer math. As a result, SBOM output is byte-for-byte deterministic across runs and machines. To tie it to a specific git commit, run export SOURCE_DATE_EPOCH=$(git -C <target> log -1 --format=%ct) before adacovex.
The bundled make targets (run-self, run-ada-crdt, prove, release, and Ada_CRDT’s prove/badges) already set it from the target’s git HEAD commit time.
Exclusivity and ordering¶
sbom is mutually exclusive with --compare-base and --coverage-delta. It
scans sources, parses proof/test results, and assesses DAL first. As a
result, the emitted properties reflect the real assessment state.
Both formats validate against the official CycloneDX 1.5 and SPDX 2.3 JSON schemas (see THIRD_PARTY_NOTICES.md).
Language detection¶
Every dependency component carries a language field (CycloneDX JSON /
components[].properties under "name": "adacovex:language", or the
Markdown table’s Language column) describing the implementation language(s)
of that dependency. Detection follows the component’s origin, most specific
first.
Manifest-declared ecosystems¶
A vendored manifest maps directly onto its language and PURL type:
Manifest file |
PURL type |
Language |
|---|---|---|
|
|
JavaScript |
|
|
Rust |
|
|
Go |
|
|
Python |
|
|
Python |
|
|
PHP |
|
|
Ruby |
|
|
Java |
|
|
Swift |
Alire manifest |
|
Ada |
|
|
Ada |
Extension-based inference¶
For every other component – vendored trees, vendor/, node_modules,
resources, loose source drops, and individual files inside resources/,
assets/, and .adacovex/patches/ – adacovex infers the language from the
file extensions actually present. The extension is the source of truth: a
.py file reports Python even when a Cargo.toml sits next to it, and the
manifest language only breaks ties.
Supported extensions:
Ada:
.ads,.adb,.ada,.gprJavaScript:
.js,.mjs,.cjsTypeScript:
.ts,.tsxCSS:
.cssHTML:
.html,.htmPython:
.pyGo:
.goRust:
.rsC:
.c,.hC++:
.cpp,.cc,.cxx,.hpp,.hh,.hxxC#:
.csJava:
.javaRuby:
.rbPHP:
.phpSwift:
.swiftKotlin:
.kt,.ktsScala:
.scalaOCaml:
.ml,.mliLua:
.luaPerl:
.plHaskell:
.hsElixir:
.ex,.exsErlang:
.erl,.hrlClojure:
.clj,.cljsDart:
.dartShell:
.sh,.bashPowerShell:
.ps1SQL:
.sqlFortran:
.f,.f90,.f95,.f03Assembly:
.s,.asmR:
.rJulia:
.jlZig:
.zigVHDL:
.vhd,.vhdlTcl:
.tcl
A directory that mixes languages reports its top 3 languages by file count
(for example "Ada; C; C++"), so a mixed-language vendored drop is summarised
by what it actually contains rather than by a single guess.
Licence resolution¶
Vendored manifest ecosystems report their licence from the local manifest:
package.json (license) for npm/pnpm, Cargo.toml for cargo,
pyproject.toml / composer.json for pypi / composer. When the local
manifest carries no licence, adacovex resolves the version, website, and
licence from the package registry as a best-effort, online fallback. The
resolver dispatches on the ecosystem (the PURL type) through a single static
table, so adding a language is one row rather than a new code path:
npm –
npm view <pkg> version license homepage --json, parsed for the three fields from one JSON object.pnpm –
pnpm show <pkg> version license homepage --json, likewise.cargo (Rust) –
cargo search <pkg>, with the SPDX id read from the(license: ...)token in the output.go and other ecosystems with no portable, reliable registry query keep an empty licence; the vendored manifest scanner still reads any in-repo licence file for them.
The npm and pnpm rows answer for all three fields with a single --json call,
so each component boots node once instead of once per field – a 3x reduction
in subprocess starts that keeps the graph build responsive on vendored
JavaScript trees. The fallback runs only when the offline read finds nothing,
so a vendored package that ships a licence never touches the network. The
resolved licence flows into every SBOM format (CycloneDX licenses, SPDX
licenseConcluded / licenseDeclared, Markdown License column) and the
dashboard detail panel; the resolved version and website appear in the
dashboard detail panel and the /api/deps JSON.
The resolver caches each answer in a per-project store under the project’s
result cache (the same --cache-dir the scan uses), keyed by the target
directory as well as the ecosystem and package name, with a 7-day TTL, the
same scheme as the system-tool version probes. The content-addressed result
cache does not cover these registry calls (each one boots node for npm/pnpm),
so without this layer a warm run still re-paid them; the meta cache makes every
repeat run serve the licence, version, and website from disk with no subprocess
spawn, and two projects that share a cache directory never serve each other’s
resolved licence or version.
Bundled dashboard assets (FlexSearch, nomnoml, graphre) resolve
their licence and website live from the package registry when a loose vendored
copy is scanned, preferring pnpm show <pkg> license and falling back to
npm, yarn, then bun – the same preference chain as every JavaScript
component (see Licence resolution). The SBOM and the
Credits tab therefore track the real upstream licence instead of a hard-coded
copy.
System dependencies¶
Discover_System_Dev_Deps scans the project’s build and dev files (Makefiles,
shell scripts, Python tools, CI workflows, GPR files, Ada sources) for a
curated set of known system binaries, then keeps only the tools that are
installed on PATH. Each becomes a system-scope component of the root with
a pkg:generic/<name> PURL, a resolved version, and no external link or
licence – by design adacovex provisions only the version for system tools
and never guesses a repository or licence for them.
The table is deny-by-default and grouped by category (build drivers, language
implementations and package managers, VCS, documentation tooling, CI and
container plumbing, performance engineering). A tool lands in the SBOM only
when (a) its exact lowercase name appears as a whole word in one of the
scanned build files, and (b) it is installed on PATH; whole-word matching
keeps “makefile” from registering “make”. The table stores only the name and
category: the version-probe flag is inferred at run time by trying
--version, then -v, then the version subcommand, and taking the first
flag that yields a version token – so a subcommand-only tool (go, fossil,
git-lfs) needs no special-cased column and a misconfigured entry cannot
exist.
Resolved versions are never hardcoded. Each probe result is cached per
machine (outside the result cache) together with the identity of the
binary it was probed from: the PATH-resolved executable path plus its size
and mtime. Before a cached version is served, adacovex re-resolves the
tool on PATH and compares the binary identity – an upgraded, replaced,
or PATH-shadowed binary re-probes on the next run, and an unchanged
toolchain serves from cache with no subprocess spawns. A version in the
SBOM therefore always describes the binary that is installed now, not the
binary that happened to be installed when the cache was filled.
system is a first-class dependency scope, distinct from base, dev,
transitive, vendored, and test; the dashboard gives it its own filter
checkbox, badge colour, and legend entry, and the SBOM lists it under
system scope. The dashboard marks these with a system scope badge and a
note in the detail panel.
The result shows up in the dashboard Dependency tab (per-dependency detail
popup) and in every SBOM renderer: CycloneDX components[].language,
SPDX/JSON adacovex:language property, and the Markdown table’s
Language column.
Tools that are really language packages are never registered as system
tools. The root project’s Python requirements (requirements*.txt, for
example sphinx and myst-parser) register as dev-scope pkg:pypi/*
components with the language set to Python. A version pinned in the
requirements line wins; otherwise the package registry answers
(pip index versions <pkg>) when pip is installed and online. A missing
registry or a failing resolve keeps the name-only entry – no version or
licence is ever guessed.
Test dependencies¶
A dependency used only by the project’s tests is classified test (the
adacovex:dep_scope property value "test"). adacovex recognises
test-only declarations in every supported ecosystem’s manifest, in addition
to the Alire [[test-depends-on]] sections and test project files:
Manifest file |
Test label |
|---|---|
|
a dependency section whose key contains |
|
the |
|
no native test-only section: the name heuristic is the signal (a module path whose last path segment starts or ends with |
|
the |
|
gems inside a |
|
|
|
optional-dependencies extras whose name contains |
|
dependencies declared inside a |
|
no native test section: the name heuristic is the signal |
A vendored component (for example a package under node_modules or
vendor/) is classified test when the project manifest that owns the
vendor directory declares it under one of these test labels, or when its
name carries the test label. The name heuristic works across every
supported ecosystem – not just npm: it checks the full name and then the
last segment after any / or :, so @playwright/test, test-case,
github.com/stretchr/testify and org.testng:testng are all
test-labelled. The heuristic also applies to lockfile-resolved names:
pnpm-lock.yaml / package-lock.json / yarn.lock entries next to an
owner package.json, Cargo.lock crate names, and alire.lock crates
that the manifest sets leave transitive. The e2e fixture’s
@playwright/test is the canonical example: it stays a devDependencies
entry of tests/e2e/package.json (and a pnpm-lock.yaml entry) and is
classified test by name.