Skip to main content

Changelog

<!--
SPDX-License-Identifier: Apache-2.0
SPDX-FileCopyrightText: 2026 ndaal Gesellschaft für Sicherheit in der Informationstechnik mbH & Co KG, Cologne
SPDX-FileCopyrightText: Author: Pierre Gronau <Pierre.Gronau@ndaal.eu>
-->

# Changelog

All notable changes to the ndaal SBOM Auditor are documented in this
file. The format is based on
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this
project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.4.11] - 2026-08-07

First 1.x release.

### Added

- **The test harnesses now cover the 0.4.10 surfaces.** `/components` and
  `/sbom/{id}/scorecard` shipped in 0.4.10 with unit and route tests but
  no presence in the out-of-process harnesses, so nothing exercised them
  end to end. Added across all five: two Bruno requests for the scorecard
  (including the 404 path), one for `/components`, and one asserting
  `/about` names all nine analyzers; two Playwright spec files — the
  scorecard spec seeds its own SBOM rather than borrowing one another
  file left behind, which would pass or skip depending on worker
  scheduling; two Goose journey steps, where the component query is
  measured separately from the empty page because it is the only read in
  the mix that `LIKE`-scans `sbom_components` and averaging the two would
  hide how that cost grows with the index; and four scorecard properties
  (`proptest`) pinning that arbitrary bytes always yield exactly
  seventeen elements in the 9/8 split, that `met()` can never exceed what
  was scored met, that zero components can never score a component
  element met, and that every status renders with both a label and a
  colour class.
- **A fifth Loom model** —
  `component_index_rebuild_is_never_observed_half_done`.
  `Store::replace_components` rebuilds an SBOM's component index as
  DELETE-then-INSERT while holding one mutex guard across the whole
  transaction. The model proves, over every interleaving, that a
  concurrent `search_components` sees the old index or the new one and
  never the empty window between them: on a page that answers "which of
  my SBOMs declare this component", a momentary zero reads as *not
  affected* — a false negative with no error and nothing in the log.

### Fixed

- **The workspace Clippy gate was red, and had been since 0.4.10.** Rust
  1.97 tightened several lints and the 0.4.10 modules tripped eighteen of
  them: `derive_partial_eq_without_eq` across the CSAF model and the
  scorecard types, `option_if_let_else` in `product_key` and
  `csaf_artifact_name`, `missing_const_for_fn` on `Scorecard::total`,
  `too_long_first_doc_paragraph` in a findings test, and
  `significant_drop_tightening` on the three new store methods. The store
  fix follows the pattern already established in that file — the bodies
  move to free functions taking `&Connection`, so the guard is a
  temporary that drops at the end of one statement. `replace_components`
  keeps its guard for the whole transaction deliberately, which is
  exactly what the new Loom model proves must happen. The standalone
  `loadtest/` crate tripped the same tightening
  (`needless_question_mark` on both scenario builders) and is clean
  again.
- **The Playwright analyze specs waited on a signal 0.4.10 removed.**
  Both polled for `hx-get` to *disappear* from the run-status fragment as
  the "analysis finished" signal. The 0.4.10 raw-HTML-lint fix made that
  attribute unconditional — a conditional wrapped around an `attr="value"`
  pair is unparseable as a tag — and moved the decision to the sibling
  `hx-trigger`, which flips to `none`. So the assertion could no longer
  ever become true and both specs ran to their deadline whatever the
  server did. They now assert `hx-trigger="none"`. The budgets are also
  raised (8-minute poll inside a 10-minute per-test ceiling): the ensemble
  went from five tools to nine in 0.3.8, and a cold run additionally pays
  for grype's one-shot vulnerability-DB update, which alone can reach its
  own five-minute deadline.
- **`/about` under-reported the analyzer ensemble.** The page hand-listed
  five tools while the ensemble has been nine since 0.3.8 — bomdrift,
  inspektr_cli, shieldbom and provenant were missing, so the page told a
  reader the auditor runs less than it does. It now renders from the
  canonical `Tool::all()`, the same source `/settings` derives from, so
  the two cannot diverge by construction. A route test pins it: every
  analyzer `/settings` can toggle must be described on `/about`, with
  `/settings` treated as canonical because that is where an operator
  enables the ensemble.
- **TLP labels lost their colours.** The CSAF publisher settings rendered
  the TLP label as a plain dropdown, so RED, AMBER, GREEN and CLEAR were
  visually indistinguishable — a poor affordance for a disclosure
  marking, where getting it wrong over-shares. They are now colour-coded
  radio badges following the FIRST TLP 2.0 convention, with the label
  text rendered beside every badge so colour is never the only signal.

## [0.4.10] - 2026-08-06

### Added

- **Component-level SBOM parsing** (`src/sbom_component/`). Until now the
  auditor read three things out of an SBOM — format, spec version and a
  component count — and handed the bytes to subprocesses as an opaque blob.
  It now lifts, per component: name, version, producer, package URLs, CPEs,
  checksums and licences; the dependency edges between components; and the
  document's own metadata (authors, generating tools, timestamp, serial
  number, lifecycle phase, signature presence). All six supported formats
  are covered — `CycloneDX` JSON and XML (recursing into nested
  components), SPDX 2.x JSON and tag:value, SPDX 3.0 JSON-LD, syft JSON.
  `NOASSERTION` is preserved verbatim rather than folded into "absent",
  because "the producer knows it does not know" is different information.
- **CISA 2026 minimum-elements scorecard** (`src/scorecard.rs`, page at
  `/sbom/{id}/scorecard`). Scores an ingested SBOM against all seventeen
  elements — nine SBOM-metadata, eight component-data — answering whether
  a supplier provided what the guidance asks for, which no vulnerability
  scan can. A component element is *met* only when EVERY component carries
  it; an SBOM that is 99% complete is not complete. Also warns on
  deprecated format versions (`CycloneDX` <= 1.2, SPDX 2.1/2.2).
- **Cross-SBOM component search** (`/components`, schema v5). Answers
  "which of my SBOMs ship log4j-core", which no per-document view can.
  Components are indexed at ingest; the page counts SBOMs with no index
  yet and offers a rebuild rather than presenting a partial index as a
  complete answer — in a supply-chain search, "not found" reads as "not
  affected".
- **CSAF 2.1 / 2.2 advisory export** (`src/csaf/`, two new export
  formats). Turns one SBOM's analysis into an OASIS CSAF advisory, with
  findings merged by CVE across tools. Skipped, failed and timed-out
  analyzers become an explicit "Analysis coverage" note naming each tool
  and its reason. Optional per-finding burst via the new
  `csaf_split_per_finding` setting.
- **CVSS vector capture** (`src/cvss.rs`, schema v4). Six of the seven
  finding parsers previously hardcoded no score at all. Score, vector and
  version are now lifted from each tool's real wire shape. A CVSS 4.0
  vector is never derived from a 3.1 one — they are different metric sets
  — and a vector with no numeric base score (OSV) yields no CSAF metric
  rather than a fabricated number.
- New settings: `csaf_publisher_name`, `csaf_publisher_namespace`,
  `csaf_publisher_contact`, `csaf_tlp`, `csaf_split_per_finding`.

### Fixed

- **Migration ladder**: rung 3 stamped `PRAGMA user_version =
  {SCHEMA_VERSION}` interpolated, so bumping `SCHEMA_VERSION` would have
  made rung 3 stamp the new version and silently skip every later rung —
  the new columns would never have been created on an existing database.
  Each rung now stamps its own literal.
- **Raw-HTML lint**: three templates wrapped a conditional around an
  attribute with no separating space, which htmlhint reads as one
  malformed tag. Askama rendered them correctly, so only the lint was red
  — but a permanently-red lint is a lint nobody reads. All 21 templates
  now pass.
- Two pre-existing doc-lint errors in `benches/parsers.rs` that were
  failing `clippy --workspace --all-targets` independently.

### Changed

- The export surface grows from six formats to eight. CSAF is the first
  format that can emit more than one artifact, so rendering now returns a
  list; the other six are unchanged.

### Documentation

- **CISA 2026 SBOM Minimum Elements — conformance assessment** at
  [`CSI_2026_cisa_sbom_minimum_elements_508c.md`](documentation/compliance/CSI_2026_cisa_sbom_minimum_elements_508c.md).
  Assesses `sbom-auditor` 0.3.9 against the CISA publication of
  29 July 2026 (17 data fields, 6 practices and processes), co-authored
  with 17 international organizations including the BSI. Result: 2 of 9
  SBOM metadata fields met, 0 of 8 component data fields, and 1 met /
  3 partial / 2 gap across the practices. The assessment records the
  single root cause — the auditor treats an SBOM as an opaque blob to
  hand to the analyzer ensemble rather than parsing it — alongside the
  eight areas where the tool exceeds the guidance (false-green guard,
  machine-readable non-coverage, five checksum families over four
  constructions, offline operation, dual schema-validated SARIF,
  SPDX 3.0 acceptance), and a ranked gap list `G1`–`G12`. A
  component-level SBOM parser (`G1`) and a minimum-elements scorecard
  (`G2`) are identified as the highest-leverage work: `G1` alone
  converts 15 of the 17 data-field rows.
- The assessment's verdict tables are refreshed for this release: with the
  parser, scorecard and CSAF export in place the auditor now reads 16 of
  the 17 data fields (the seventeenth, SBOM Author Signature, is detected
  but not cryptographically verified) and closes gaps `G1`, `G2`, `G3`
  and `G10`.

## [0.3.9] - 2026-08-05

### Changed

- Version bump to 0.3.9; development continues on top of the 0.3.8 release.
- **Dependency currency:** `base64` 0.23.0 → 0.23.1 (semver-compatible patch;
  the at-rest crypto path in `src/crypto.rs` encodes sealed TEXT-column values
  with it). No source change — `Cargo.toml`'s `base64 = "0.23"` already
  permitted it. Recorded as CSAF advisory `ndaal-sa-2026-379` (CVSS 0.0, NONE).

### Fixed

- **Release pipeline** (`release/create_release_on_gitlab.sh`): push the current
  branch via an explicit `<branch>:<branch>` refspec instead of `HEAD:main`
  (avoids the non-fast-forward rejection and this repo's `.invalid`-HEAD
  resolution), and upload GitLab release assets individually — tolerating
  "already attached" — so a partial upload is idempotent and self-heals on
  re-run rather than aborting on the first collision.

### Documentation

- CSAF advisory `ndaal-sa-2026-380` announces the 0.3.8 release (informational,
  reproducing the 0.3.8 changelog).

### Testing

- Six additional `skills/rust-testing` sequences over the pure-domain parsers,
  each verified green before commit:
  - **bolero + arbitrary** structured fuzzing (`tests/bolero_structured.rs`) —
    three harnesses over `sbom_format::detect`, `findings::parse`,
    `licensing::parse`.
  - **Kani** bit-precise proofs (`kani-harness/`, standalone crate mirroring
    `loom-harness/` so Kani's pinned nightly need not build `libsqlite3-sys`):
    `Severity::from_cvss` matches the documented CVSS bands and is monotonic —
    both `VERIFICATION:- SUCCESSFUL`.
  - **Criterion** parser benchmarks (`benches/parsers.rs`).
  - **cargo-insta** golden snapshots (`src/snapshots/`) pinning the Markdown and
    SARIF 2.1 / 2.2 export shapes, with the `.snap.new` drift gate.
  - **assert_cmd** process-contract tests (`tests/cli_contract.rs`) — exit
    status + stdout/stderr for `--version` / `--help` and rejected invocations.
  - **cucumber** BDD acceptance loop (`tests/bdd.rs`,
    `tests/features/sbom_analysis.feature`) — four Gherkin scenarios,
    `fail_on_skipped()` so no scenario passes vacuously.

## [0.3.8] - 2026-08-05

### Added

- **Four new analyzers, bringing the ensemble to nine.** Each is optional at
  runtime, detected on `PATH`, overridable via `SBA_TOOL_<NAME>_BIN`, and
  self-skips (never fake-green) when absent:
  - **bomdrift** ([crates.io](https://crates.io/crates/bomdrift)) — supply-chain
    drift / typosquat detection. Diffs the SBOM against an empty baseline the
    orchestrator writes to scratch and normalises the SARIF result.
  - **inspektr** ([crates.io](https://crates.io/crates/inspektr)) — SBOM
    vulnerability scanner; its top-level `vulnerabilities` JSON is normalised
    into CVE-shaped findings.
  - **shieldbom** ([crates.io](https://crates.io/crates/shieldbom-core)) —
    offline-first embedded / IoT CVE scanner; SARIF 2.1.0 output (the CVE is
    the `ruleId`, the CVSS-like `security-severity` property drives severity).
  - **provenant** ([crates.io](https://crates.io/crates/provenant-cli)) — a
    ScanCode Toolkit port reporting licence / copyright / provenance rather than
    vulnerabilities.
- **Licence & provenance findings.** provenant's output is not CVE-shaped, so it
  gets its own model (`LicenseFinding`: licence / copyright / holder / author),
  its own store table (additive schema v3, `license_findings`, foreign-keyed to
  the run with `ON DELETE CASCADE`), a "Licence & provenance" section on the
  SBOM detail page (En/De/Fr), and a per-tool section in the Markdown export —
  so provenance data is surfaced instead of discarded.
- **SARIF 2.1.0 finding parser** shared by bomdrift and shieldbom: GitHub's
  `security-severity` property drives severity when present, otherwise the SARIF
  `level` maps (error → high, warning → medium, note → low).

### Testing

- Compile-time invariants (`static_assertions`) for the new types; the
  nine-tool ensemble count is pinned.
- Property tests: both untrusted-output parsers never panic on arbitrary bytes
  and only ever emit store-valid rows; SARIF and ScanCode output are bounded by
  their declared item count (no amplification).
- cargo-fuzz: the findings target now covers the three new sources; a new
  `fuzz_licensing_parse` target covers provenant, each seeded with a valid
  sample (both smoke-ran clean).
- End-to-end integration tests (stub tools) prove each output shape is
  persisted through the whole orchestration, and that provenant lands licence
  findings in their own table and nothing in the vulnerability table.

## [0.3.7] - 2026-08-04

### Fixed

- **grype vulnerability-DB warm-up.** grype's scan runs offline by design
  (`GRYPE_DB_AUTO_UPDATE=false`), so once its DB passed grype's built-in max
  age (5 days) grype hard-failed — surfacing a routine DB refresh as a
  `fail`. The analyzer now runs `grype db update` at most **once per
  process**, out of band, before the first grype scan; the scan itself stays
  offline against the freshened DB. The warm-up is best-effort: on a
  genuinely offline host it fails fast and grype scans against whatever DB it
  has. Verified: with the DB refreshed, all five analyzers (syft / grype /
  trivy / osv-scanner / bomber) now complete `ok` on the CycloneDX log4j
  fixture (grype: 20 findings), where grype previously recorded `fail`.

## [0.3.6] - 2026-08-04

### Fixed

- **osv-scanner invocation modernised.** The analyzer called `osv-scanner
  scan source --sbom <input>`; `--sbom` is deprecated in osv-scanner 2.x and
  `scan source` is a *directory* walker (it logged `Starting filesystem walk
  for root: /` and exited 128 with "could not determine extractor"). The call
  is now `osv-scanner scan --lockfile <input> --format json`, which reads the
  single materialised file and auto-detects its format from the
  server-controlled name (`input.cdx.json`, `input.spdx.json`, the converted
  `scanner-input.cdx.json`, …). Verified: osv-scanner now completes `ok` with
  32 findings on the CycloneDX log4j fixture where it previously failed.

## [0.3.5] - 2026-08-04

### Changed

- **SPDX 3.0 is normalised for the scanners via `syft convert`.** The
  vulnerability scanners (grype / trivy / osv-scanner / bomber) cannot read
  SPDX 3.0 JSON-LD. The analyzer now converts an SPDX 3.0 input to CycloneDX
  **once, up front**, and every scanner reads the conversion; syft itself
  still reads the original. When syft cannot produce the conversion (it is
  not installed), the scanners **skip** with a clear reason instead of
  failing on an input they cannot parse. This supersedes the 0.3.4 note that
  the scanners fail on SPDX 3.0; with syft present, trivy now scans an SPDX
  3.0 SBOM that previously failed.

### Notes

- Vulnerability matching after conversion depends on the SBOM carrying
  package URLs. The upstream SPDX 3.0 examples describe source files and AI
  datasets whose components have no `purl`, so purl-strict scanners
  (grype / osv-scanner / bomber) still find nothing to match on those
  fixtures; a package-manager SPDX 3.0 SBOM (with purls) matches normally.
  This is honest tool behaviour, not a conversion failure — trivy parses the
  converted CycloneDX successfully either way.

## [0.3.4] - 2026-08-04

### Added

- **SPDX 3.0 JSON-LD ingest.** SBOM files in the SPDX 3.0 JSON-LD encoding
  (`@context` + `@graph`, e.g. `*.spdx3.json`) are now detected, counted and
  stored instead of being rejected with `422 Unprocessable Entity`. A new
  `SbomFormat::Spdx3Json` (`spdx3-json`) variant is detected from the SPDX
  3.x `@context` (or a graph `specVersion` beginning `3.`); the spec version
  is read from the graph's `CreationInfo` and the component count is the
  number of package elements (`software_Package` and its profile subclasses
  `ai_AIPackage` / `dataset_DatasetPackage`, all ending in `Package`).

### Changed

- **Store schema v2.** The `sboms.format` CHECK constraint is broadened to
  allow `spdx3-json`. Because `SQLite` cannot alter a CHECK in place, the
  `migrate` ladder gains a v2 rung that rebuilds the `sboms` table with
  foreign keys disabled, so the rebuild does not cascade-delete existing
  `analysis_runs`; an existing v1 database upgrades in place with its data
  intact.

### Notes

- The external scanners handle an SPDX 3.0 input per their own capabilities:
  `syft` parses it; scanner versions that do not yet support SPDX 3.0 fail
  cleanly (a visible `fail`, never a false `ok`), and reports are still
  produced documenting the outcome. Ingest, detection, storage and export do
  not depend on the scanners.

## [0.3.3] - 2026-08-04

Test-fixture release. No change to application behaviour, and no change to
the published crate: the Cargo `include` list scopes the crates.io package
to the crate's own sources, so the example corpus is not part of the
published package.

### Added

- `example/` — a corpus of 651 public test SBOM files for exercising
  ingest, format detection, the analyzer ensemble and the exporters,
  fetched unmodified from four upstream collections:
  anchore/sbom-examples (510 files, Apache-2.0), CycloneDX/bom-examples
  (83, CC0-1.0), spdx/spdx-examples (56, GPL-3.0) and
  sonatype-nexus-community/cyclonedx-sbom-examples (2, Apache-2.0). The
  SBOM data files are tracked via Git LFS (~2.6 GB); `PROVENANCE.md` and a
  per-file `MANIFEST.csv` ship alongside in plain git. See
  `example/PROVENANCE.md`.
- End-to-end test sequences that drive the corpus through the API and the
  browser UI (upload → analyze → export) and assert that reports are
  produced.

## [0.2.1] - 2026-08-03

Packaging and distribution release. No changes to application behaviour;
this version makes the auditor installable as a prebuilt binary.

### Added

- `cargo binstall` support: `[package.metadata.binstall]` points at the
  GitLab release assets, matching the self-updater's raw-binary naming
  (`sbom-auditor-<version>-<triple>`), so `cargo binstall sbom-auditor`
  fetches a prebuilt binary instead of compiling from source.
- Prebuilt release binaries for the supported target triples, each with
  the five cryptographic hash sidecars (`.sha-256`, `.sha-512`,
  `.sha3-512`, `.blake3-512`, `.shake256-512`).

### Changed

- Scoped the published crate with a Cargo `include` list so the package
  contains only the crate itself (sources, embedded assets, build
  script, licence and the info-menu docs) rather than the whole
  repository. The published package is ~832 KiB.
- Updated dependencies to their latest releases: `cap-std` 3.4 → 4.0
  (with `cap-primitives` 4.0 and `io-extras` 0.19 — the
  capability-confined filesystem; the used `Dir` API is unchanged across
  the major, so no code change), `base64` 0.22 → 0.23, `roxmltree`
  0.20 → 0.21, and `time` 0.3.54 → 0.3.55. None of these updates
  remediate a security advisory (`cargo audit` / RUSTSEC report no
  applicable vulnerability for the previous versions); they are routine
  supply-chain currency updates. The dev-only `jsonschema` is
  intentionally held at 0.28.

## [0.1.0] - 2026-08-03

The initial build. A single offline binary that ingests SBOM files,
analyses them with an ensemble of external tools, normalises the
findings and exports them in six formats.

### Added

#### Secure web shell (transport and hardening)

- HTTPS-only listener: TLS 1.3 via rustls (aws-lc-rs provider with
  `prefer-post-quantum`, so the X25519MLKEM768 post-quantum hybrid
  key-exchange group is offered first); self-signed certificate
  regenerated on every start-up, or an operator-supplied
  `--tls-cert` / `--tls-key` pair.
- Loopback-only bind by default (`127.0.0.1:8680`), with an explicit
  `--allow-non-loopback` opt-in and an anti-DNS-rebinding request-Host
  allowlist (`--allowed-host` / `SBA_ALLOWED_HOSTS`).
- Security-header middleware on every response: HSTS, `X-Frame-Options`
  DENY, a hash-pinned Content-Security-Policy, `X-Content-Type-Options`
  nosniff, referrer minimisation, a Permissions-Policy deny list and
  cross-origin isolation headers.
- Same-origin check on every state-changing POST and a request-body
  size limit.
- Hand-rolled hyper 1 + matchit router, shared by the binary and the
  integration tests so both exercise the identical route table.
- Connection admission control (a semaphore cap), TLS-handshake,
  header-read and whole-request timeouts, and graceful shutdown.
- Embedded static assets (Bootstrap, Bootstrap Icons, htmx, Roboto and
  Roboto Mono fonts, logo) — no CDN, no filesystem reads.

#### Persistent store, settings and storage encryption

- Bundled SQLite store (rusqlite with a statically linked, vendored
  SQLite) in WAL mode, holding the SBOM inventory, the analyzer run
  ledger, findings, operator settings and an append-only audit log; a
  `PRAGMA user_version` migration ladder versions the schema.
- Typed operator settings persisted in the database (import and export
  directories, per-run analyzer timeout, analyzer concurrency, the five
  checksum-sidecar toggles, the five analyzer enable toggles and the
  upload size limit) with a settings page and an audited change diff.
- Optional storage encryption (XChaCha20-Poly1305) that seals stored
  SBOM bytes and raw analyzer output while they sit on disk, keyed by
  `--encryption-key-file`; pre-existing plaintext rows still read back.
- Capability-confined filesystem: every data, import and export path is
  opened through a cap-std handle, closing path-traversal, symlink and
  TOCTOU classes at the syscall layer. Stored file names are
  server-derived.

#### SBOM ingest

- Browser upload (`POST /upload`) via a hand-rolled, size-limited
  single-file multipart parser, and bulk import from a configured folder
  (`POST /import`) via a cap-std directory walk.
- Format detection for CycloneDX (JSON/XML), SPDX (JSON/tag-value) and
  syft-json, with the declared spec version and component count captured
  at ingest.
- Content-hash (SHA-256) deduplication: re-ingesting the same bytes
  returns the existing entry rather than a duplicate row.

#### Analyzer orchestration

- An ensemble of five optional, `PATH`-detected tools — syft, grype,
  trivy, osv-scanner and bomber — each overridable with
  `SBA_TOOL_<NAME>_BIN` and individually enableable in settings.
- Each tool runs as an isolated subprocess under a per-run deadline, a
  process-wide concurrency semaphore, bounded stdout/stderr capture and
  `kill_on_drop` on timeout.
- A false-green guard: a run is `ok` only when the tool ran and produced
  parseable JSON (a non-zero exit on findings still counts); a missing
  binary is a visibly distinct `skip`, unparseable output is `fail`, and
  an elapsed deadline is `timeout`. A skipped tool is never shown as
  success.
- The bomber compatibility gate normalises CycloneDX newer than 1.6 via
  `syft convert`, and skips with a documented reason when syft cannot
  convert the input.
- The stored SBOM is decrypted into a per-analysis scratch directory
  (removed on completion) so a tool is never handed the sealed on-disk
  file; captured output is sealed again on disk.
- The detail page polls run status over htmx until every run is
  terminal.

#### Findings normalization and detail UI

- Each `ok` run's JSON is normalised into a common finding shape per
  tool, with the CVE alias resolved from the tool's GHSA/OSV identifier
  and a normalised severity.
- Findings are shown per tool with severity badges, ordered most severe
  first; a live `/tools` inventory page probes each tool's presence,
  version, role and documented caveat.

#### Multi-format export with checksum sidecars

- Six output formats produced in-process by native Rust crates (no
  pandoc, typst or LibreOffice shell-out): SARIF 2.1.0, SARIF 2.2,
  Markdown, ODT, DOCX and PDF (the PDF embeds Roboto for umlaut-safe
  output).
- Every export lists skipped and failed analyzers with their reasons, so
  an incomplete analysis is never presented as clean.
- Up to five checksum sidecars per artifact (`.sha-256`, `.sha-512`,
  `.sha3-512`, `.blake3-512`, `.shake256-512`), each toggleable in
  settings, written into the capability-scoped export directory; the
  download route allowlists the served name against the directory
  listing.

#### Internationalization and operability

- Trilingual UI chrome (English / German / French) with a cookie-pinned
  language switcher (`sba_lang`) and a dark-mode toggle (`sba-theme`).
- Info menu with dialog pages: About, System info, Privacy, Security,
  License, Imprint, Changelog, README, Administrator Guide and User
  Guide.
- Liveness (`/healthz`) and readiness (`/readyz`) probes.
- Structured logging (`--log-format json`) plus an append-only log file
  under `--log-dir` (default `./ndaal/log`).
- Hardened self-update (`--check-update` / `--self-update`) over the
  TLS 1.3 stack, verifying the download against the published checksums
  and refusing on mismatch, with a `--no-self-update` policy opt-out.

#### Test harnesses

- Rust unit and integration suite (`cargo nextest run`) plus doctests.
- `loom-harness/` concurrency-permutation models of the shared state.
- `test/bruno/` API collection exercising every route against a live
  server.
- `test/playwright/` browser E2E of the upload, analyze, findings and
  export journeys.
- `loadtest/` Goose scripted-scenario load test.

[0.3.7]: https://gitlab.com/vPierre/ndaal_public_sbom_auditor/-/releases/v0.3.7
[0.2.1]: https://gitlab.com/vPierre/ndaal_public_sbom_auditor/-/releases/v0.2.1
[0.1.0]: https://gitlab.com/vPierre/ndaal_public_sbom_auditor/-/tags/v0.1.0