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 carries adacovex:proof_level, adacovex:standard, adacovex:dal_target, and adacovex:level. Every dependency has a language field 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 as attributionTexts on 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

adacovex:proof_level

Root component

Assessed SPARK level (Stone .. Platinum)

adacovex:standard

Root component

Compliance standard (DO-178C, ISO 26262, IEC 62304)

adacovex:dal_target

Root component

Shared rigour tier (DAL-A .. DAL-E)

adacovex:level

Root component

Standard-specific label (DAL-C, ASIL B, Class A)

language

Every component

Implementation language(s) inferred from file extensions

purl

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.json for cyclonedx-json, <target>/sbom.spdx.json for spdx-json, and <target>/docs/compliance/SBOM.md for md. The containing directory is created automatically.

  • Exit code: 0 when the SBOM was written, 1 otherwise. 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_levelStone..Platinum, the honest assessed level.

  • adacovex:standardDO-178C / ISO 26262 / IEC 62304.

  • adacovex:dal_targetDAL-A..DAL-D (omitted for DAL-E).

  • adacovex:level – the standard-specific label (DAL-C / ASIL B / Class A, omitted for DAL-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

package.json

pkg:npm

JavaScript

Cargo.toml

pkg:cargo

Rust

go.mod

pkg:golang

Go

pyproject.toml

pkg:pypi

Python

requirements*.txt

pkg:pypi

Python

composer.json

pkg:composer

PHP

Gemfile

pkg:gem

Ruby

pom.xml

pkg:maven

Java

Package.swift

pkg:swift

Swift

Alire manifest

pkg:alire

Ada

.gpr project file

pkg:gpr

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, .gpr

  • JavaScript: .js, .mjs, .cjs

  • TypeScript: .ts, .tsx

  • CSS: .css

  • HTML: .html, .htm

  • Python: .py

  • Go: .go

  • Rust: .rs

  • C: .c, .h

  • C++: .cpp, .cc, .cxx, .hpp, .hh, .hxx

  • C#: .cs

  • Java: .java

  • Ruby: .rb

  • PHP: .php

  • Swift: .swift

  • Kotlin: .kt, .kts

  • Scala: .scala

  • OCaml: .ml, .mli

  • Lua: .lua

  • Perl: .pl

  • Haskell: .hs

  • Elixir: .ex, .exs

  • Erlang: .erl, .hrl

  • Clojure: .clj, .cljs

  • Dart: .dart

  • Shell: .sh, .bash

  • PowerShell: .ps1

  • SQL: .sql

  • Fortran: .f, .f90, .f95, .f03

  • Assembly: .s, .asm

  • R: .r

  • Julia: .jl

  • Zig: .zig

  • VHDL: .vhd, .vhdl

  • Tcl: .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:

  • npmnpm view <pkg> version license homepage --json, parsed for the three fields from one JSON object.

  • pnpmpnpm 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

package.json

a dependency section whose key contains test (for example testDependencies or devTestDependencies); a name whose last segment starts or ends with test (for example @playwright/test, vitest)

Cargo.toml

the [dev-dependencies] section (Cargo’s test-only section); any section whose name contains test (for example [target.'cfg(test)'.dependencies])

go.mod

no native test-only section: the name heuristic is the signal (a module path whose last path segment starts or ends with test, for example github.com/stretchr/testify)

composer.json

the require-dev section

Gemfile

gems inside a group :test block (any group name containing test)

pom.xml

<dependency> blocks whose <scope> is test

pyproject.toml

optional-dependencies extras whose name contains test (for example the test extra); Poetry sections such as [tool.poetry.group.test.dependencies]

Package.swift

dependencies declared inside a .testTarget(...) block

requirements*.txt

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.