Changelog

# Changelog

All notable changes to the csaf-crud workspace are documented here.
The format follows [Keep a Changelog](https://keepachangelog.com)
and the workspace uses [Semantic Versioning](https://semver.org).

## [Unreleased]

## [1.5.26] – 2026-10-12

### Security

- **HTTP/3 listener: remote denial of service through `quinn-proto`
  (GHSA-53rm-773f-4q8c).** Advisory `ndaal-sa-2026-753`: CVSS v3.1 7.5
  and v4.0 8.7, both High, scored by ndaal because upstream published no
  vector. No CVE is assigned and RustSec does not list it yet, which is
  why `cargo audit` stayed silent. csaf-crud 1.5.25 bundles `quinn-proto`
  0.11.17, and upstream marks every version up to 0.11.18 as affected.
  After the QUIC handshake, one 13-byte `MAX_STREAM_DATA` frame makes the
  endpoint open up to 2^40 streams and exhaust memory or CPU. The server
  asks for no client certificate, so any peer that completes the
  handshake can send it, and with `panic = "abort"` an out-of-memory abort
  ends the whole process, not only HTTP/3. Fixed in `quinn-proto` 0.11.19
  (with `quinn` 0.11.12). Assessed from the upstream advisory and the
  csaf-crud source; not reproduced against a running server. Workarounds
  before upgrading: bind the HTTP/3 listener to the loopback interface,
  block UDP on its port at the firewall (the TCP listener is not
  affected), or build with `--no-default-features`.
- **macOS builds only: the `quinn-udp` receive loop can spin
  (GHSA-6pp4-4cxf-xf88).** Advisory `ndaal-sa-2026-754`, scored 7.5 and
  8.7 like the one above. A truncated datagram can leave the Unix and
  Apple fallback receive path repeating non-blocking reads before packet
  authentication. Fixed in `quinn-udp` 0.5.16. The Linux build uses the
  batched `recvmmsg` path and the Windows builds their own, which
  upstream states differ from the affected fallback.
- **Four more `quinn-proto` advisories fixed in 0.11.18 and 0.11.19 do
  not reach csaf-crud**, recorded as the VEX statement
  `ndaal-sa-2026-761`: GHSA-3g6h-r5qx-f6hq needs ACK_FREQUENCY, which is
  off by default and unused here; GHSA-ppcp-v39w-8jq2 needs
  `send_datagram`, which no csaf-crud code calls; GHSA-465w-v9q3-7j98
  needs unordered reads, and `h3-quinn` reads in order;
  GHSA-wppq-2f6r-wfvm is processed by clients only, and csaf-crud only
  listens.

### Changed

- **Twelve direct dependencies refreshed in `Cargo.lock`** (the caret
  requirements in `Cargo.toml` already allowed them): `clap` 4.6.7,
  `hyper` 1.12.0, `hyper-util` 0.1.21, `insta` 1.49.0, `proptest`
  1.12.0, `quinn` 0.11.12 with `quinn-proto` 0.11.19, `redb` 2.6.4,
  `serde_json` 1.0.154, `thiserror` 2.0.21, `tokio` 1.53.2,
  `tokio-rustls` 0.26.6 and `uuid` 1.28.0, plus `quinn-udp` 0.5.16. The
  routine ones have one advisory each, scored 0.0 (`ndaal-sa-2026-748`
  to `-752`, `-755` to `-760`). Three are worth knowing about without
  being vulnerabilities: `tokio-rustls` 0.26.6 repairs a 0.26.5
  regression, shipped in 1.5.23 to 1.5.25, where batched reads could
  swallow transport errors; `redb` 2.6.4 fixes unbounded page-cache
  growth under write workloads; `hyper` 1.12.0 hardens header framing.
- `proptest` 1.12.0, a dev-dependency, moves to `rand` 0.10 and needs
  Rust 1.88. The lockfile gains six entries (`borsh`, `borsh-derive`,
  `proc-macro-crate`, `toml_edit`, `toml_datetime`, `toml_parser`) that
  no workspace member builds, and shrinks from 458 to 451 crates.
- Verified before release: `cargo check`, `cargo clippy --workspace
  --all-targets --all-features -- -D warnings` and 634 tests pass. The
  doctest pass finds none, because the workspace has no doctests.
  `cargo audit` shows only the documented dev-only `ttf-parser`
  finding.

## [1.5.25] – 2026-10-12

### Security

- **Open redirect through the `Referer` header (CWE-601).** Advisory
  `ndaal-sa-2026-746` (`csaf/2026/746/`): CVSS v3.1 6.1 and v4.0 5.3, both
  Medium, no CVE assigned. Affects releases 1.4.15 to 1.5.24; **upgrade to
  1.5.25**. After a language switch the server redirects to the path of the
  `Referer` header. It rejected a path starting with `//`, but `http::Uri`
  accepts `/\evil.example` and browsers read `/\` as `//`, so that value left
  the site. A page at `https://attacker.example/\evil.example` that linked to
  `/lang/fr` with `referrerpolicy="unsafe-url"` could send a visitor on to
  another host after one click. A path is now accepted only if it is
  rooted, not protocol-relative, and free of backslashes, control characters
  and whitespace; anything else goes to `/`. Workaround before upgrading:
  strip the `Referer` header on `/lang/` at the reverse proxy. Found by the
  new `fuzz_return_path` target on its first seed, and pinned by a
  regression test, a property test, a Bruno case and a Playwright case. The
  theme route added in this release shares the function and never shipped
  with the flaw.
- **Page titles are HTML-escaped.** `wrap_page` wrote its title into
  `<title>` verbatim, and the detail page passes a tracking id taken from the
  URL, so text such as `a & b </title>` could close the element. The
  Content-Security-Policy added in this release would have blocked an
  injected script; the markup was still wrong. Found by a property test.

### Added

- **The web interface follows the ndaal design system**
  (`skills/ndaal-design` v1.1.0). A white header with a hairline border, the
  skill's measured blue accent, bundled Bootstrap Icons (MIT, self-hosted
  sprite, no third-party request), WCAG 2.2 AA colour contrast in both
  themes, 44 px pointer targets, a dual-ring focus indicator, logical CSS
  properties so Urdu mirrors, and reduced-motion, forced-colours and
  increased-contrast support. Styles and the one script now live in
  `static/css/` and `static/js/csaf-ui.js` instead of an inline `<style>`
  and `<script>`. Recorded divergences from the skill (Roboto Mono, TTF
  fonts, Settings as a page, `<details>` menus, 24 px checkbox box) are in
  the Developer Guide.
- **Six security response headers on every response**: a strict
  `Content-Security-Policy` without `unsafe-inline`,
  `Strict-Transport-Security`, `X-Content-Type-Options`,
  `X-Frame-Options`, `Referrer-Policy` and `Permissions-Policy`. They are
  added in one place
  that the TCP/TLS and the HTTP/3 listener both pass through, so pages,
  static files, the JSON API, redirects, `404` and `405` all carry them. A
  Bruno `headers` collection and `tests/scripts/test_security_headers.sh`
  check them against a running server. See the Administrator Guide for the
  HSTS caveat with the default self-signed certificate.
- **Per-browser theme**: the header's Theme menu offers System, Light and
  Dark, stored in the `csaf_theme` cookie (`POST /preferences/theme`).
- **Message boxes carry an icon and a spoken word**, not colour alone:
  errors are announced (`role="alert"`), results politely
  (`role="status"`), and static facts such as the configured directories
  are no longer styled as successes.
- `tests/e2e/ndaal-design.spec.ts`: axe in both themes, 320 px reflow,
  44 px targets, desktop navigation, theme (unrelated cookies, no-JavaScript
  form, open redirect, removed toggle), menu keyboard handling, the
  confirmation dialog, CSP violations, assets and message boxes. New Bruno
  folders `headers` (4), `preferences` (8) and `ui` (8), a backslash-`Referer`
  case and a language reset in `lang`, and `Referer` open-redirect cases in
  `language-switcher.spec.ts`.
- **Deeper verification of the new UI code**, per the project's testing
  skills: `crates/csaf-crud/tests/proptest_ui.rs` and in-module property
  tests (theme cookie, message boxes, page title, header wrapper, page-title
  transform, redirect path); three cargo-fuzz targets with seed corpora
  (`fuzz_theme_cookie`, `fuzz_return_path`, `fuzz_page_shell`) over shared
  bodies in `fuzz-harness`; two loom models of the per-request settings
  snapshot; and criterion benchmarks (`criterion-harness/benches/
  ui_render.rs`) for the page shell, theme lookup, escaper, message box and
  header wrapper.

### Changed

- **The theme switch is no longer global.** The old toggle rewrote one
  server-wide setting for every visitor and was an open redirect.
  `POST /settings/toggle-theme` is removed. `Settings.theme` stays as the
  default for browsers that have not chosen, so no migration is needed.
- The navigation collapses below 64 rem (1024 px). Settings, Theme,
  Language and Info sit at the end of the bar on every page.
- Deleting an advisory and resetting the settings ask in a native modal
  dialog instead of `window.confirm`; Escape dismisses it.
- Forms: the sidecar checkboxes form a fieldset with a legend, tables carry
  accessible names and header scopes, and the redundant `required` on
  pre-selected `<select>` elements and `type="text"` on inputs are gone.
- The new interface strings are English only; the other 48 languages fall
  back to English until they are translated.

### Fixed

- **Desktop navigation links show again.** The 1.5.23 and 1.5.24
  navigation hid every link above 992 px behind a closed `<details>`.
- **Seven admin texts were a single comma in all 49 languages.** The
  five-language extraction (`bd9e516b5`) dropped the multi-line English
  paragraphs of the Import, Export, Dump and Audit-export pages, so each
  showed a stray `,` and the audit-export result lost its row counts. The
  English text is restored; the other languages fall back to it. A test now
  fails on any translation without a letter or digit.
- **Four keys the code used did not exist** and showed as raw text:
  `csaf.form_section_publisher` (the Publisher heading and detail row, which
  also forced a horizontal scroll at 320 px), `csaf.opt_publisher_category_user`,
  `csaf.list_pagination_previous` and `csaf.form_edit_heading_prefix`. The
  existing guard scanned only two files and one call shape; it now scans
  every route module and the closure form.
- The logo's `alt` text had a bare `&`.

### Removed

- `static/css/custom.css` and `static/js/theme.js`: Bootstrap-era files that
  were served but never linked.

## [1.5.24] – 2026-10-11

### Fixed

- **The `aarch64-pc-windows-msvc` `csaf-crud` ships with HTTP/3 again.**
  `release/create_release.sh` built that one target with
  `--no-default-features`, which drops the default `quic` feature, so the
  Windows ARM64 binary has served HTTP/1.1 and HTTP/2 only. The flag was
  added on the premise that `quinn` pulls in `aws-lc-sys`, whose `-Werror`
  build fails under cargo-xwin's clang CFLAGS (a linker `-L` flag reported
  as `-Wunused-command-line-argument`). That premise no longer holds:
  `cargo tree -p csaf-crud --target all -i aws-lc-sys` is empty, because
  the crate is only an unactivated optional dependency in `Cargo.lock`, and
  a build with `quic` for this triple compiles `quinn`, `quinn-proto`,
  `h3`, `h3-quinn` and `rustls-platform-verifier` cleanly with the existing
  Apple-clang and `lld-link` toolchain. The workaround was never
  re-measured, so it outlived its reason and the 1.5.23 release notes
  repeated that stale reason, since corrected. The 1.5.23 Windows ARM64
  `csaf-crud` asset therefore still lacks HTTP/3; the next release does
  not. A bats test now
  pins the flag's absence so it cannot come back unnoticed. Checked on a
  build of this tree: a valid ARM64 PE whose `quinn` references rose from
  the 5 that come from the embedded documentation to 37. It has not been
  run on Windows ARM64 hardware.

### Changed

- **Documentation brought in line with the code.** The README and its four
  translations, and the User, Administrator, Developer and Troubleshooting
  Guides, were corrected and extended:
  - The hardening table no longer claims Control Flow Guard on Windows.
    Neither Windows binary sets `GUARD_CF` (read from the PE headers of the
    1.5.23 archives); ASLR, high-entropy ASLR and NX are set.
  - The checksum description names five hash families, not three.
  - The release-contents and verify-a-download instructions use the
    aggregate manifests. Since 1.5.21 the per-archive sidecars are not
    attached to a release, so a per-archive `.sha-256` download URL does not
    exist.
  - The Rust 1.99.0 toolchain pin, the deliberately different 1.95.0
    minimum, and the nearest-`clippy.toml` lookup rule are documented.
  - The Troubleshooting Guide gains "No HTTP/3 on Windows ARM64" and "Only
    the logo shows in the navigation bar". The second records what the
    published 1.5.23 binary does at 992 px and wider in Chrome 152: the
    page links, theme toggle and language menu are hidden. Firefox and
    Safari were not tested.

### Known issues

- **The desktop navigation bar is hidden in current browsers.** Since
  1.5.23, a window 992 px wide or wider shows only the logo and "CSAF": the
  page links, theme toggle, language menu and Info menu are missing. The
  controls sit inside a closed `<details>` element that is dissolved with
  `display: contents`, and current browsers keep the content of a closed
  `<details>` hidden through `::details-content`. Below 992 px the menu
  button works. Checked in Chrome 152 against the published 1.5.23 binary;
  the navigation markup and CSS are unchanged in 1.5.24, so it is not fixed
  in this release. Firefox and Safari were not tested. Workaround: narrow
  the window below 992 px or zoom the page in, or open the pages by address
  (see the Troubleshooting Guide). Candidate fix, verified only by injecting
  it into the live page: `content-visibility: visible; display: contents`
  on `nav.primary details.nav-collapse::details-content` at 992 px and
  wider.

## [1.5.23] – 2026-10-08

### Fixed

- **The navigation bar had no mobile layout.** Every control sat on a
  single row, so below roughly 992px the page links, theme toggle,
  language switcher and Info menu overflowed the viewport. The bar is
  now a `<details class="nav-collapse">` whose `<summary>` is the
  hamburger — the same JavaScript-free disclosure pattern as
  `ndaal_public_sbom_auditor`. At `min-width: 992px` both the
  `<details>` and the inner `.nav-group` take `display: contents`,
  which dissolves the wrappers so the desktop layout is unchanged, and
  the summary is hidden. No script is involved, so the menu still
  opens with scripting disabled.
- **Nine checkboxes had no programmatic label.** Their `<label>` was
  only an ancestor element, with no `for`/`id` pair — including all
  five checksum-family export toggles and `verschlusssache_enabled` /
  `nato_enabled`. A screen reader announced them unlabelled. Against
  the WCAG 2.2 AA target the ndaal design standard sets, that is a
  conformance defect rather than a cosmetic one.
- **Six inline `onsubmit=` handlers would have failed open under a
  strict Content-Security-Policy.** A CSP that forbids inline script
  blocks the handler silently while the form still submits, so the
  delete and reset confirmations would have vanished with no error
  anywhere — the destructive action would simply proceed unconfirmed.
  They are now `data-confirm` / `data-busy-text` attributes bound by
  the page script. One test had been pinning the unsafe shape: it
  asserted that `onsubmit="return confirm(` was PRESENT. It now
  asserts the attribute form exists and that no `onsubmit=` remains.

### Changed

- Eighteen inline `style=` attributes became `.u-*` utility classes in
  `layout.rs`, so page styling lives in one stylesheet instead of
  being spread across six Rust string literals. `htmlhint` findings on
  the server-rendered pages went from 42 to 11.
- `PAGE_SCRIPT` moved from `<head>` to the end of `<body>`. It is a
  `DOMContentLoaded` TLP-colouring handler, not an anti-FOUC inline
  script, so nothing depended on it running early.
- The 11 remaining `htmlhint` findings are all `href-abs-or-rel` on
  genuine external links (crates.io, OASIS, Wikipedia) and are left
  as they are. That rule is configured as `true`, which htmlhint does
  not accept as a value — it expects `"abs"` or `"rel"` — so it fails
  unconditionally on any page that mixes internal paths with external
  links. It is a configuration question, not a page defect.

### Added

- `nav.toggle` ("Toggle navigation") as the accessible name and
  tooltip of the mobile navigation hamburger.

## [1.5.22] – 2026-09-16

### Fixed

- **`--self-update` was downloading a checksum sidecar instead of the release
  archive, and had been since 1.4.18.** `self_update` selects a release asset
  with `name.contains(target) && name.contains(identifier)` and takes the
  first match. An archive's name is a strict *prefix* of its five sidecars, so
  six assets satisfied that predicate for every binary+triple; GitLab returns
  links newest-first, the archive sorted last, and the updater fetched a
  175-byte `.shake256-512` file. It then failed with `ChecksumMismatchError`
  against a digest that was **entirely correct** — `SHA256SUMS` was never
  wrong, the downloaded file was. No `contains`-based identifier can separate
  a prefix from its extensions, so the fix is to stop publishing the per-archive
  sidecars as release assets: exactly one asset now matches per binary+triple.
  The five-family integrity guarantee is unchanged — the aggregate
  `SHA-256SUMS.txt`, `SHA-512SUMS.txt`, `SHA3-512SUMS.txt`,
  `BLAKE3-512SUMS.txt` and `SHAKE256-512SUMS.txt` manifests each list every
  artefact, and their names match neither a triple nor a binary name. Two
  regression tests in `csaf-core::updater` pin the hazard so re-adding the
  sidecars fails the build with an explanation.
- **The Info dropdown clipped at the right viewport edge.** It is the rightmost
  nav control and the base `.dropdown-menu` rule is `left: 0`, so the menu grew
  off-screen and Changelog / README / Administrator Guide / User Guide were
  unreadable and unreachable. Both right-hand menus now carry
  `dropdown-menu-end`, matching `ndaal_public_sbom_auditor`.

### Changed

- The Security page listed two of the five checksum families it actually
  writes for exported documents, in all 49 languages. Verified against
  `csaf-core` before correcting: the export emits all five.

## [1.5.21] – 2026-09-15

### Security

- **Patched `rustls` for RUSTSEC-2026-0285** (CVSS v3.1 5.3 / v4.0 6.9,
  MEDIUM). `rustls` `0.23.44` → `0.23.45`. rustls accepted TLS 1.3 handshake
  messages sent at the wrong encryption level when they followed a
  key-changing message in the same record, contrary to RFC 8446 §5.1. rustls
  is this workspace's TLS 1.3 stack, so it is the transport behind every
  HTTPS listener and the in-binary self-updater. The handshake transcript
  remains authenticated — an attacker cannot alter or complete a handshake —
  so the effect is confidentiality-only and LOW: a peer could send in
  plaintext handshake messages that should have been encrypted, without the
  connection being rejected. **1.5.20 is affected**; the advisory was
  published on 2026-09-14, after 1.5.20 was built, and was caught by the
  pre-push `cargo audit` gate on the next change. Recorded as CSAF advisory
  `ndaal-sa-2026-541`.

### Added

- **The About page now documents all five checksum families** it actually
  emits — SHA-256 (`.sha-256`), SHA-512 (`.sha-512`), SHA3-512
  (`.sha3-512`), BLAKE3-512 (`.blake3-512`) and SHAKE256-512
  (`.shake256-512`) — in all 49 languages. It had advertised only two of the
  five, understating a security-relevant feature.
- **`info.about_standards_heading`**, which `routes/info.rs` has always
  called but no language table defined, so the About page rendered the raw
  key string to every user in every language.
- **Four i18n tests**, including a regression guard that walks source →
  `en.tsv`. The existing coverage test walks `en.tsv` outward and is
  structurally blind to a key the code calls that `en.tsv` lacks — which is
  exactly how the missing heading shipped with a green suite.

## [1.5.20] – 2026-09-13

### Changed

- **Raised the declared MSRV from Rust `1.93.0` to `1.95.0`.** The previous
  value was not achievable: every release in the `sysinfo` `0.39` line
  requires rustc `1.95`, so `cargo +1.93.0 check --workspace` failed and the
  declaration published with 1.4.18 was incorrect for consumers. `1.95.0` is
  the real floor; the `rust-toolchain.toml` development pin (`1.97.1`) is a
  separate question and is deliberately left unchanged. Updated in
  `Cargo.toml` and all five README mirrors.

- **Transitive dependency-currency refresh.** `cargo update` moved 13 locked
  packages to their latest semver-compatible releases within the existing
  manifest requirements (no `Cargo.toml` requirement changed) — `uuid`
  `1.26.0` → `1.26.1` (the only one the workspace calls directly, from
  `csaf-core` and `csaf-models`), `zerocopy` and `zerocopy-derive` `0.8.56` →
  `0.8.57`, `encoding_rs` `0.8.40` → `0.8.41`, `smallvec` `1.16.0` → `1.16.1`,
  `hybrid-array` `0.4.14` → `0.4.15`, `bitflags` `2.13.1` → `2.13.2`,
  `console` `0.16.4` → `0.16.6`, `crc32fast` `1.5.1` → `1.5.2`, `cc` `1.4.5` →
  `1.4.6`, and `multiversion` plus `multiversion-macros` `0.8.0` → `0.9.0`
  (which drops `target-features` `0.1.6` from the graph). `reqwest` `0.13.4` →
  `0.13.5` also moved, but in `Cargo.lock` only: this workspace deliberately
  keeps `reqwest` out of the build (`self_update` is pinned `default-features
  = false` with the `ureq` and `rustls` features, per the note at
  `Cargo.toml:87`), so Cargo records it as an unenabled optional dependency
  and `cargo tree --invert reqwest --target all` prints nothing. No compiled
  code changed for it. The `workspace-hack` feature-unification crate needed
  no regeneration — `cargo hakari generate` reported no changes and `cargo
  hakari verify` confirms it still unifies the graph correctly. Verified:
  `cargo check --workspace --all-features` clean; `cargo audit` reports no
  vulnerability. One pre-existing `unmaintained` advisory (RUSTSEC-2026-0192,
  `ttf-parser` `0.25.1`) is unchanged by this refresh — it is a dev-dependency
  of `csaf-crud` only and never reaches a shipped binary.

## [1.4.18] – 2026-09-08

### Changed

- **Transitive dependency-currency refresh.** `cargo update` moved ~137 locked
  packages to their latest semver-compatible releases within the existing
  manifest requirements (no `Cargo.toml` requirement changed) — notably
  `aws-lc-rs` `1.17.3` → `1.18.1` and `aws-lc-sys` `0.43.0` → `0.45.0` (the
  AWS-LC cryptographic backend beneath the rustls TLS 1.3 stack), `cap-std`
  `4.0.2` → `4.0.3` (the capability-based filesystem sandbox), `blake3` `1.8.5`
  → `1.8.7` (one of the five checksum families), `chacha20` `0.10.1` → `0.10.2`
  (cipher for stored-data encryption), `clap` `4.6.1` → `4.6.6`, and `futures`
  `0.3.32` → `0.3.34`. The `workspace-hack` feature-unification crate was
  regenerated with `cargo hakari`. Verified: `cargo hakari verify` clean,
  `cargo check --workspace --all-features` clean, `cargo audit` reports no
  vulnerability. Recorded as CVSS 0.0 CSAF advisory `ndaal-sa-2026-517`.

### Added

- **`example/upload_csaf_documents.sh`** — a bounded-parallel CSAF-document
  uploader (`-j/--jobs`) with a rate-limit throttle (`-D/--delay`), 403
  exponential backoff-retry (`-R/--max-403-retries`), and idempotent
  `409`-as-already-present skipping so a re-run after a partial upload
  converges. Ships a 57-test bats sibling.
- **`example/fetch_csaf_examples.sh`** — a bounded-parallel fetcher for real
  public CSAF + VEX example documents (Red Hat CSAF, the OASIS CSAF 2.0 VEX
  examples, and openSUSE CSAF), each `jaq`-validated. Ships a 55-test bats
  sibling.
- **Rust pre-commit / pre-push hooks** under `.githooks/` (per
  `skills/rust-pre-commit`), preserving Git LFS, plus a `typos.toml` curated
  for the British-English + 49-language tree.

## [1.4.17] – 2026-09-05

### Changed

- **Dependency currency (non-breaking bumps).** `self_update` `1.2.0` →
  `1.3.0` (additive: a `proxy(url)` builder setter and an archive-lookup fix,
  no API breaks — the updater's `--check-update` / `--self-update` contract
  and its checksum + `asset_identifier` verification are unchanged, and the
  archive-lookup fix improves the download path already in use), `uuid`
  `1.24.1` → `1.26.0` (only `Uuid::new_v4` is used), and `hyper` `1.11.0` →
  `1.11.1` (patch; the HTTP server engine). `cargo check --workspace
  --all-features` is clean and the workspace test suite passes on all three.
  Each bump is recorded as a CVSS 0.0 CSAF advisory (ndaal-sa-2026-505 /
  -506 / -507) with dual CVSS 3.1 + 4.0 scores.

### Fixed

- **Dashboard activity badges failed WCAG 2 AA colour contrast in light
  theme.** The "update" badge rendered its text in `#e65100` on `#fff3e0`
  — a 3.46:1 ratio, below the 4.5:1 minimum for normal text — which
  axe-core flagged as a serious `color-contrast` violation on every update
  entry in the recent-activity table. The foreground now uses the design
  system's sanctioned warning colour (`--ndaal-warning`, `#B45309`),
  raising it to 4.58:1; the badge stays the same orange family and every
  other badge/theme pair was already compliant (measured, not estimated).
  A full-page dashboard accessibility spec (`tests/e2e/dashboard-a11y.spec.ts`)
  now seeds real activity and gates light and dark themes on zero
  serious/critical colour-contrast findings, as a shrinking baseline.

## [1.4.16] – 2026-09-04

### Added

- **The UI now speaks 49 languages** (was five: English, German, French,
  Spanish, Italian). The additions cover every EU official language plus the
  Norway set (Norwegian Bokmål and Nynorsk, and Northern, Lule and Southern
  Sámi), Turkish, and the wider European set — Albanian, Armenian,
  Azerbaijani, Belarusian, Bosnian, Bulgarian, Catalan, Croatian, Czech,
  Danish, Dutch, Estonian, Finnish, Georgian, Greek, Hungarian, Icelandic,
  Irish, Latin, Latvian, Lithuanian, Luxembourgish, Macedonian, Maltese,
  Montenegrin, Polish, Portuguese, Romanian, Romansh, Russian, Serbian,
  Slovak, Slovenian, Swedish, Ukrainian — and three Asian languages: Chinese
  (Mandarin), Hindi and Urdu. English, German, French, Spanish and Italian
  stay pinned first in the switcher (unchanged order); the others follow
  alphabetically by English name. The 49-entry menu renders in columns of ten
  on desktop (five columns) and a single scrollable column when the header
  collapses.

  **Urdu is right-to-left.** A `Lang::dir()` accessor feeds `<html dir>`, so
  the whole document — nav, tables, forms, the language menu — mirrors from
  one attribute; every other language stays left-to-right
  (`skills/languages-asia`). Chinese resolves `zh-Hans`/`zh-Hant`/`zh-CN`/
  `zh-TW` all to one Simplified-Han entry (a documented limitation).

  The implementation follows `skills/languages-europe-rust`: a
  pinned-plus-alphabetical `Lang` enum, one embedded `key<TAB>text` TSV per
  language under `crates/csaf-crud/src/i18n_data/`, and English fallback for
  any key a translation has not filled — so a missing string degrades to
  English, never to a blank or a panic. Each switcher link carries its own
  `lang`/`hreflang` and the current language is marked with
  `aria-current="true"` (WCAG 2.2 / EN 301 549, `skills/ndaal-design` §12.4).

  The 44 new languages are **machine-translated, pending native review**
  (each TSV self-labels this in its header). CSAF/VEX/TLP/CVSS/CVE/CWE/SPDX
  and the other protocol vocabulary is deliberately left untranslated, and
  the JSON API (`/api/v1/**`) is unaffected. Irish, Maltese, and the three
  Sámi languages are the lowest-resource of the set and most warrant a
  native pass; coined and adapted terms were recorded per language during
  generation.

  Bundled fonts were re-subset from upstream Roboto (Apache-2.0) to cover
  Latin, Latin Extended, Greek and Cyrillic, so Bulgarian, Greek, Russian,
  Serbian, Ukrainian, Macedonian and Belarusian render offline with no CDN.
  Armenian, Georgian, and the CJK-Han, Devanagari and Perso-Arabic scripts
  (Chinese, Hindi, Urdu) fall through to the platform font stack — Roboto has
  never carried them and bundling CJK would add tens of megabytes; still
  offline-safe, no CDN. A compile-time test (`tests/font_glyph_coverage.rs`)
  fails the build if any translated string needs a glyph the bundled faces
  lack outside those documented fallback scripts.

### Changed

- **`self_update` bumped `1.0.0-rc.6` → `1.2.0`** (first stable of the 1.x
  line; upstream declares the API unchanged since rc.6). Verified against this
  workspace's surfaces — the injected rustls transport, the pinned feature set
  (`gitlab`, `ureq`, `rustls`, `archive-tar`, `compression-tar-gz`,
  `checksums`) and the new `.asset_identifier` call: compile clean, the full
  workspace test suite passes unchanged. Upstream's only behavioural delta
  (rate-limited responses now classify as `Error::RateLimited` instead of
  `Unauthorized`) touches no code path here — neither variant is matched on.
  Recorded as CSAF advisory ndaal-sa-2026-488 (CVSS 0.0, no security impact).

### Fixed

- **The UI language cookie was silently ignored in real browsers over
  HTTP/2.** `i18n::from_parts` read only the first `cookie` header field, but
  HTTP/2 clients (Chrome included) split cookie pairs across several `cookie`
  fields ("cookie crumbling", RFC 9113 §8.2.3). A developer browser on
  `127.0.0.1` carries cookies from every app ever run on that host, so the
  `csaf_lang` cookie routinely arrived in a later field the server never
  read — and no language switch took visible effect, while `curl` and
  single-cookie test contexts stayed falsely green. It now joins every
  `cookie` field before parsing, with a regression test written red-first
  (`from_parts_reads_every_cookie_header_field`).

- **Self-update checksum mismatch (wrong binary's archive selected).**
  `--self-update` failed with `checksum mismatch (expected … computed …)`
  because this workspace ships two binaries (`csaf-crud` and `ndaal-csaf-cli`),
  so every release carries two `.tar.gz` archives per triple, both containing
  the target-triple string. `self_update` selects a download by
  `name.contains(target)`, which matched both — so it could fetch the *other*
  binary's archive, and the digest verification (pinned to the intended asset)
  correctly rejected it. The updater now passes `.asset_identifier(bin_name)`,
  adding the `name.contains(bin_name)` predicate so the CLI updates from the
  CLI archive and the server from the server archive
  (`crates/csaf-core/src/updater.rs`). Meanwhile, existing installs can update
  via `cargo binstall ndaal-csaf-cli`, which uses the exact pkg-url and is
  unaffected.

## [1.4.15] – 2026-08-24

A localization release, plus a fix for a validation-error message that had
been silently discarded, and a batch of internal hardening/quality work
carried over from 1.4.14 development.

### Added

- **Five-language UI: English, German, French, Spanish, Italian.** Every
  server-rendered page — dashboard, CSAF document CRUD, import/export/dump,
  settings, and the info pages — is now fully translated. The active
  language is resolved per request from a `csaf_lang` cookie or, absent
  that, the browser's `Accept-Language` header, falling back to English;
  `GET /lang/{code}` is the switcher, redirecting back to the referring page
  (same-origin path only — no open-redirect surface) and setting a
  one-year `Secure; HttpOnly; SameSite=Lax` cookie.
  The header's control cluster is now Theme → Language → Info (Info
  rightmost), matching `skills/ndaal-design` §12; the language switcher
  marks the active language with both a CSS class and `aria-current="true"`.
  A keyboard skip-link (`Skip to main content` / translated) was added while
  the header markup was already being rewritten.
  The mechanism — cookie + `Accept-Language` resolution, not URL-path
  prefixes — mirrors the identical, independently-developed i18n layer in
  the sibling `ndaal_public_bsi_grundschutz_oscal_viewer` project, rather
  than inventing a new one. 1,159 translation keys across six per-route
  modules (`i18n::{nav,home,csaf,admin,info,settings}`); CSAF/API data
  values, protocol-fixed vocabulary (TLP, Verschlusssache, NATO
  classification labels, CVSS severity words), and the JSON API
  (`/api/v1/**`) are deliberately left untranslated.

### Fixed

- **Creating a CSAF document that failed schema/semantic validation showed
  no error message.** Required-field validation always worked correctly;
  the CSAF-schema-level check (`validation::validate`) built a detailed
  error list and then discarded it through a no-op stub, leaving the user
  looking at an apparently-blank form with no indication why their
  submission was rejected. `update()`'s identical validation branch never
  had this bug — `create()` now builds its error message the same way
  `update()` already did, through one shared `validation_error_summary`
  helper, and the now-genuinely-dead discard shim was removed.

- **`csaf-crud`'s pre-built macOS binaries (`x86_64-apple-darwin`,
  `aarch64-apple-darwin`) crashed immediately with
  `dyld: duplicate linked dylib '/usr/lib/libobjc.A.dylib'`.** Confirmed via
  `otool -L`: both contained two `LC_LOAD_DYLIB libobjc.A.dylib` load
  commands, where a locally-built (non-cross-compiled) binary had exactly
  one. The duplicate traced to `objc2` (pulled in transitively by
  `sysinfo`'s `objc2-io-kit`/`objc2-open-directory` macOS backends),
  declared once via `#[link(name = "objc", kind = "dylib")]` but reached
  through three separate dependency-graph paths; Apple's native linker
  deduplicates the resulting `LC_LOAD_DYLIB` requests into one, but
  `cargo-zigbuild`'s bundled zig linker did not — consistent with a known
  gap in zig's ld64 reimplementation (it doesn't track already-visited
  dylibs across multiple reference paths; see
  [ziglang/zig#9229](https://github.com/ziglang/zig/pull/9229)). Fixed by
  routing `csaf-crud` specifically off `cargo-zigbuild` for both Darwin
  targets in `release/create_release.sh`, building it with the native
  Apple toolchain instead (Xcode's own `clang`/`ld64` cross-compiles for
  any Apple architecture with no extra tooling — the same mechanism behind
  `lipo`/universal binaries). `ndaal-csaf-cli` stays on `cargo-zigbuild`:
  it was never affected (no `sysinfo` dependency, always a single clean
  `libobjc` reference), and its own `redb` dependency's bitcode fails to
  cross-compile via Xcode's native clang specifically for
  `aarch64-apple-darwin` (`failed to load bitcode ...: Callee is not a
  pointer type`) — an unrelated toolchain gap in the other direction.
  Verified on the actual packaged release tarballs (not just the raw
  build output): `otool -L` shows exactly one `libobjc.A.dylib` reference
  on both architectures, the corrected assets were re-uploaded to the
  same `v1.4.15` GitLab release (replacing the two removed broken links
  and the six aggregate checksum manifests, whose content changed), and a
  fresh `cargo binstall csaf-crud` on macOS now downloads and runs a real
  working binary again instead of falling back to a source build.

### Changed

- Hardening and internal-quality work carried out between the 1.4.14 and
  1.4.15 version bumps: ELF PIE/RELRO/NX verified against the actual
  shipped binary (not assumed from build flags), a broken `rust-doctor`
  config that had been silently failing the gate repaired, every
  `#[allow(...)]` given a machine-readable `reason`, several duplicated
  code paths collapsed (CVSS v3/v4 validation, hash/storage read bodies,
  classification-handling extracted from the form builder), a secrets
  allowlist fixed after discovering it suppressed nothing, and stray
  `skills/`/`backup/` directories untracked per the pre-existing
  `.gitignore`.
- A macOS toolchain defect (Apple's `ld-1267` linker segfaulting on debug
  builds with many objects) is now worked around with a scoped,
  macOS-only, debug-only `-Wl,-ld_classic` linker flag; release builds
  were never affected.

## [1.4.14] – 2026-08-21

A correctness and maintenance release. No security fix — 1.4.13 carried that
one — and no change to the API, the storage layer or the CLI contract.

The user-visible fix is in the export UI: `/admin/export` named only three of
the five checksum sidecar families it actually writes, so operators verifying
a download had no reason to look for the other two. The remaining changes are
a dependency bump and two pieces of recorded reasoning that stop the next
reader undoing a deliberate decision.

### Fixed

- **`/admin/export` reported only three of the five sidecar hash families.**
  The Settings page offers five toggles (SHA-256, SHA-512, SHA3-512,
  BLAKE3-512, SHAKE256-512) and `write_sidecar_files` honours all five, but
  all three cards on the export page — Export, Dump Database, Export Audit
  Log — listed only the first three.

  The export itself was always correct; only the reporting was short. The
  practical harm is that an operator who enabled BLAKE3-512 or SHAKE256-512
  saw no mention of them and could reasonably conclude those sidecars were
  not being written, when they were.

  The page body is now a testable `render_export_content` function, matching
  the seam `settings.rs` already uses, with three regression tests. One
  asserts a per-card *count* rather than mere presence: the defect was
  identical in all three cards, so a test checking only that the string
  appears would pass while two cards stayed wrong. That was verified by
  reintroducing the defect in a single card and watching precisely that test
  fail.

### Changed

- **`sysinfo` 0.38.4 → 0.39.6** (`ndaal-sa-2026-444`). Semver-breaking by
  Cargo's rules — for a 0.x crate the minor position carries major semantics
  — so the manifest requirement moved, not just the lockfile. The API in use
  is narrow (`System::new`, `new_all`, `refresh_cpu_list`,
  `CpuRefreshKind::everything`, and the `name`/`os_version`/`kernel_version`/
  `host_name` functions) and compiled unchanged. Pulls two macOS-only
  transitives (`objc2-foundation`, `objc2-open-directory`).

- **`ndaal-sa-2026-434` now mirrors the shared ndaal document.** This tree
  previously carried a csaf-crud-specific advisory under that ID, which
  collided with the canonical shared advisory for the same RUSTSEC finding
  (byte-identical across four other ndaal trees, scoped to
  `grundschutz-oscal-viewer`). The local copy is replaced by the canonical
  one. Cite **RUSTSEC-2026-0258** / **GHSA-q83h-524g-xf6h** for this
  product's exposure; `ndaal-sa-2026-443` is this release line's own
  announcement.

### Notes

- **`crypto-common` deliberately NOT advanced to 0.1.7.** Re-verified against
  the registry rather than taken on trust: 0.1.6 depends on
  `generic-array ^0.14.4`, whereas 0.1.7 hard-pins `generic-array =0.14.7`.
  The workspace pins `generic-array = "0.14.9"` precisely to keep the
  resolver on 0.1.6; taking 0.1.7 would drag `generic-array` back to a stale
  exact version and regress both `cargo install csaf-crud` and
  `cargo publish`'s verify step, each of which re-resolves and ignores the
  committed lockfile.

## [1.4.13] – 2026-08-19

**Contains a security fix.** `h2` is advanced to 0.4.16, patching
RUSTSEC-2026-0258 (unbounded empty HTTP/2 DATA frames, denial of service).
This project serves HTTP/2, so 1.4.12 and earlier are affected — upgrade.

Beyond that: one bug fix in query-parameter decoding, and testing
infrastructure that makes the next change safer to make.

Four fuzzing engines now run over ONE body, and four benchmark harnesses each
answer a question the others cannot. The single user-visible fix — invalid
percent-escapes decoding to a NUL byte — was found by reviewing the decoder
under the `rust-yap` lens while adding that infrastructure, which is the
argument for building it.

### Added

- **`divan-harness/`** — wall-clock benchmarks that also report ALLOCATION
  counts and sizes per benchmark (divan, pinned `=0.1.21`). This is why it
  exists alongside criterion rather than duplicating it: the updater's hot paths
  are string work, so allocation is the interesting axis. It immediately earned
  its place — `asset_name` shows `alloc: 1 / 9 B` followed by `grow: 3 / 75 B`,
  i.e. `format!` reallocating three times to build one 49-byte string.

  The honest cost: `AllocProfiler` replaces the global allocator, so timings
  here are of the profiled allocator and are NOT comparable with criterion's.
  Read allocations from divan and time from criterion.

- **`dhat-harness/`** — heap-usage ASSERTIONS. divan *reports* allocations; this
  *constrains* them, so a regression fails CI instead of sitting unread in a
  benchmark table. Four tests, each in its own file because dhat panics if two
  `Profiler`s run at once and the test runner's own threads allocate into the
  stats. Measured, not guessed:

  - `is_safe_asset_name` allocates **zero** blocks — exact assertion, since the
    function is four `contains` calls on a borrowed `&str`. If it ever starts
    allocating, something has begun copying an attacker-controlled name.
  - `parse_sha256sums` on a miss allocates **zero**; on a hit exactly **one**
    block of **64 bytes** (the digest). More would mean the parser started
    copying the manifest rather than borrowing it — a cost that scales with the
    file instead of the answer.
  - `asset_name` is bounded rather than pinned, because its 4 blocks are `std`'s
    growth policy and a toolchain bump would otherwise turn into a red gate.

- **`tests/scripts/test_hyperfine.sh`** — end-to-end CLI benchmark gate.
  Measures the *process*: spawn, dynamic linking, allocator init, clap
  construction. Warm-run startup for `csaf-crud` is **14.8 ms ± 0.4**, against
  microbenchmarks measured in hundreds of nanoseconds — the gap is exactly why
  this gate exists. Records machine state alongside every number, self-skips
  cleanly when hyperfine is absent, and never benchmarks through `cargo run`.

- **Four fuzzing engines over one body, plus two benchmark harnesses.**
  Every fuzz body now lives in exactly one place: `fuzz-harness/` holds one
  `<target>::fuzz_drive(data: &[u8])` module per target, and the engine crates
  are thin wrappers over it. Previously the 34 cargo-fuzz targets carried their
  assertions inline, so a second engine would have meant a second copy of 949
  lines of assertions, free to drift.

  - `fuzz/` — cargo-fuzz / libFuzzer, 34 targets (unchanged as the PR gate)
  - `hfuzz/` — honggfuzz, 34 targets, `cargo hfuzz run hfuzz_<target>`
  - `afl/` — cargo-afl / AFL++, 34 targets, `cargo afl fuzz afl_<target>`
  - `bolero-harness/` — bolero, 6 harnesses, one body per engine **plus Kani**

  A different engine finds different bugs over the same code: libFuzzer, AFL++
  and honggfuzz use different mutation and coverage strategies, so running all
  three is additive coverage rather than redundancy.

- **`callgrind-harness/`** — instruction-count benchmarks (iai-callgrind,
  pinned `=0.16.1`). Counts barely vary between runs, so they can gate a merge
  on a noisy shared runner. Requires Valgrind, so these execute on **Linux CI
  only** — Valgrind supports neither Windows nor current macOS.

- **`criterion-harness/`** — wall-clock benchmarks (criterion 0.8.2,
  `html_reports` enabled). Covers the same four functions as the callgrind
  suite deliberately: instruction count and wall-clock answer different
  questions, and a change can cut instructions while losing on locality. Use
  criterion to decide, callgrind to gate.

### Changed

- `bolero` moved out of `csaf-core`'s dev-dependencies into the standalone
  `bolero-harness/`. `cargo bolero test` compiles the crate with
  `-Cpasses=sancov-module`, which instruments every target in it — including
  test binaries that never link the libFuzzer runtime, which then fail to link
  with `"___sanitizer_cov_trace_switch", referenced from: test_crate_integrations`.
  The harness cannot live beside ordinary integration tests.

### Fixed

- **SECURITY: `h2` 0.4.15 → 0.4.16 — RUSTSEC-2026-0258 / GHSA-q83h-524g-xf6h**
  (CVSS v3.1 **3.7 LOW** / v4.0 **6.3 MEDIUM**). h2
  accepted and queued empty HTTP/2 DATA frames without limit; where streams are
  not actively drained that can grow memory without bound, or panic if the
  length overflows. Availability only — no confidentiality or integrity impact
  is claimed.

  Reachable, not theoretical: `cargo tree -i h2` shows it entering through
  `hyper`, which `csaf-crud` uses for its HTTP/1.1 + HTTP/2 listener on 8180.
  That is the production request path, so **1.4.12 and earlier are affected**.

  Caught by `cargo audit` during this release's preflight, which failed with
  `error: 1 vulnerability found!`. The release was held until it was fixed
  rather than shipped with a known advisory outstanding. Lockfile-only; the
  same command now exits 0 across 432 dependencies against 1217 advisories.

  The two CVSS versions disagree (3.7 LOW vs 6.3 MEDIUM) because v4.0 weights a
  partial availability impact more heavily. Both are published: quoting only
  the lower understates it to a v4.0 consumer, only the higher overstates it to
  a v3.1 one. Upstream describes it as "Low severity".

  Advisory references: the authoritative identifiers for this finding are
  **RUSTSEC-2026-0258** and **GHSA-q83h-524g-xf6h**. `ndaal-sa-2026-434` is the
  shared ndaal advisory for the same RUSTSEC finding and is mirrored into this
  tree, but it is scoped to a different product (`grundschutz-oscal-viewer`),
  so cite the RUSTSEC/GHSA identifiers rather than the ndaal one when
  describing this product's exposure. The 1.4.13 release announcement is
  `ndaal-sa-2026-442`.

- **Invalid percent-escapes decoded to a NUL byte instead of being left
  literal.** `hex_val` mapped every non-hex byte to `0`, so `decode_percent("%ZZ")`
  returned `"\0"` — a byte the caller never sent, injected into a query
  parameter from attacker-controlled input. `%GG`, `%4Z` and `%Z4` behaved the
  same way.

  RFC 3986 §2.1 requires a `%` not followed by two HEXDIG to survive as the
  literal characters it is. `hex_val` now returns `Option<u8>` and the decoder
  declines the escape when either digit is invalid.

  Not a path-traversal hole — `is_safe_asset_name` and `safe_join` both reject
  NUL — but any other consumer of a decoded parameter (a log line, a database
  column, a rendered page) inherited a NUL it had no reason to expect, and NUL
  is precisely the byte that truncates strings in C-adjacent consumers. The
  existing tests covered *truncated* escapes (`abc%2`) but never *invalid-hex*
  ones, which is how it survived. Both cases are now pinned, including a guard
  that well-formed `%00` still decodes to NUL.

- **Fuzz harness assertion measured the wrong segment.** The
  `updater_download_url` target claimed to check "the asset half" of the
  traversal contract but measured `url[prefix.len()..]`, which spans
  `{tag}/downloads/{asset}`. A fuzzer-supplied tag of `...` therefore tripped an
  assertion about the asset, producing a crash artifact that described a product
  bug that does not exist. The assertion now measures only the asset segment.

  The product path was never affected: `asset_name` embeds the version, so
  `prepare_install` transitively screens the tag — a version carrying `..` or
  `/` makes the *asset* fail `is_safe_asset_name` and returns `UnsafeAssetName`
  before any URL is built. That coupling is now asserted rather than assumed,
  and the offending input is kept as a permanent corpus entry.

### Notes

- **`yap` evaluated and deliberately not adopted.** Against the skill's own
  decision table, `split_sums_line` is not the parser yap is for: 15 lines,
  allocation-free, guard-clause structured, returning borrowed `&str`, and
  already carrying 492 corpus entries plus a proven-equivalent mutant. yap's fit
  is "a hand-written parser that wants structure"; this one does not want for
  structure, and adding a dependency to security-critical code to restyle
  correct std calls would be churn with a real regression budget.

  `urlencoding_decode` *is* the yap-shaped one — byte-level tokenizing with
  escape sequences and backtracking — and reviewing it under that lens is what
  surfaced the NUL defect above. The fix is three lines of `Option`; expressing
  it through a parser combinator would have been a larger change for the same
  behaviour. Recorded here so the evaluation is not silently repeated. yap's
  upstream has also been quiet since late 2023, which the skill asks to be said
  out loud rather than left implicit.

## [1.4.12] – 2026-08-16

A small release: one user-visible addition, one dependency patch, and a
lint sweep over the advisory generators. No behaviour of the server, the
CLI or the storage layer changes.

### Added

- **`/info/about` now carries a Source code section**, mirroring the card
  the Grundschutz++ Viewer shows so both ndaal products present provenance
  the same way. It links the repository, the releases page and all four
  published crates, and gives both `cargo install csaf-crud` and
  `cargo binstall csaf-crud`. The version shown comes from
  `env!("CARGO_PKG_VERSION")`, so the page cannot drift from the build
  serving it.

  The section states what `SHA256SUMS` actually proves — **integrity, not
  authenticity**. The artefacts are not signed, so whoever can rewrite a
  release archive at source can rewrite the manifest beside it. This is the
  same wording the code and the release notes use; the UI deliberately does
  not imply more than the pipeline delivers.

### Changed

- **`uuid` 1.24.0 → 1.24.1** (`ndaal-sa-2026-418`). Lockfile-only; it was
  the only direct workspace dependency behind its latest release. Scored
  CVSS 0.0 / NONE, grounded rather than assumed: `cargo audit` with all four
  deny classes exits clean across 433 crates against 1216 RUSTSEC
  advisories, and no advisory matches `uuid` at either version — so the bump
  fixes nothing and the prior state was not exposed.

  `crypto-common` and `syn` were deliberately **not** bumped. The workspace
  pins `generic-array = "0.14.9"` specifically to stop the resolver
  selecting `crypto-common 0.1.7`, which hard-pins a stale `generic-array`
  and regresses both `cargo install` and `cargo publish`'s verify step.

- **Ruff autofix over the 50 advisory generators**: 1500 → 1362 findings,
  175 fixes. Generator output was compared byte-for-byte against the
  advisories already committed and is identical — these scripts emit
  documents that ship with five checksum sidecars, so a changed string
  literal would silently invalidate published digests.

  `T201` was excluded from the unsafe pass. Ruff "fixes" it by **deleting**
  the `print()` statement, which would have removed all 70 of them —
  including the only line reporting what a generator wrote and at what
  score, making a successful run indistinguishable from a silent no-op.

### Fixed — release matrix restored to six targets

- **`aarch64-pc-windows-msvc` ships again.** 1.4.11 shipped five of six
  targets and its notes called the gap "environmental" and not fixable.
  **That was wrong**, and this entry corrects the record.

  The real blocker in 1.4.11 was mimalloc: cargo-xwin's sysroot carries no
  MSVC C++ standard library, and `mimalloc.h` includes `<cstddef>`. Once
  mimalloc was excluded on MSVC targets the matrix was already capable of
  building, but it was never re-run — the subsequent debugging used
  hand-written `cargo xwin build` commands that omitted the
  `--cross-compiler clang` flag `release/create_release.sh` has always
  passed. Those commands failed with `/imsvc` errors that the real pipeline
  never produced, and that self-inflicted failure was mistaken for an
  environmental one.

  No pipeline change was needed. Windows ARM64 users get a working
  `cargo binstall` again on this version; on 1.4.11 it returns a hard 404.

### Known limitation

- Windows targets still ship **without mimalloc secure mode**, and
  `aarch64-pc-windows-msvc` also ships **without Control Flow Guard**. Both
  are genuine toolchain gaps rather than choices: cargo-xwin provides no
  MSVC C++ standard library for mimalloc, and `-C control-flow-guard=yes`
  leaks into the C compiler as `-mguard=cf`, which clang rejects. The other
  five targets carry the full hardening set.

## [1.4.11] – 2026-08-07

Note: 1.4.10 was never published. The version jumps from 1.4.9 to 1.4.11 so
that no released artefact is ever ambiguous about which number it carries.

### Fixed

- **`updater`: the transport never reached the integrity check.**
  `perform_with` moved its transport into `fetch_release_list` and then
  called `install` with none, so `install` built a real-network client. The
  `SHA256SUMS` digest fetch — the only integrity check that runs on the
  GitLab backend, since GitLab publishes no per-asset digest — was therefore
  unreachable from the offline test suite, and any offline test that served
  a genuinely newer release silently stopped being offline and hung on a
  real download to gitlab.com. `install` now takes the transport (grouped
  into an `InstallRequest` to stay within the four-parameter limit) and the
  single-call-site `fetch_digest` wrapper is gone — supplying `None` was its
  only behaviour and the cause of the bug. Found by mutation testing, which
  reported it three separate ways: one surviving mutant plus two timeouts.

- **`tests/scripts/test_cargo_inspect.sh`: unbounded run could stall every
  gate.** The script shelled out to a nightly
  `cargo rustc -- -Zunpretty=hir` with no time limit. One such process was
  observed wedged for 17.5 hours while holding the cargo build lock,
  blocking every other cargo command in the workspace — unrelated
  `cargo test` runs included. Each per-crate invocation is now bounded
  (`INSPECT_TIMEOUT`, default 300s, `0` disables), escalates to `SIGKILL`
  via `--kill-after` when cargo declines `SIGTERM`, resolves both
  `timeout(1)` and `gtimeout(1)`, and reports a timeout distinctly from a
  compile failure instead of folding both into one counter.

### Testing

- New offline test drives `perform_with` all the way to
  `ChecksumMissing`, which returns before any download is attempted. Its
  kill condition is exact: an unthreaded transport means the manifest
  request never lands in the recorded URLs.
- Seven new bats assertions on the inspect timeout, including a
  behavioural one that proves the host's `timeout(1)` actually honours
  `--kill-after` — busybox ships a `timeout` that rejects it, which would
  silently drop the bound while looking identical to a working one.
- The `split_sums_line` `||`→`&&` mutant is recorded as **equivalent**
  (unkillable, not a test gap) with the proof in `.cargo/mutants.toml`:
  verified over 5630 inputs with zero divergence.

## [1.4.9] – 2026-08-07

Supersedes 1.4.8. Content is identical; the version was rolled forward so
that all four crates carry the same number.

## [1.4.8] – 2026-08-06 — partial publish, superseded by 1.4.9

Published to crates.io for `csaf-models`, `csaf-core` and `csaf-crud` only;
`ndaal-csaf-cli` never shipped at this version, leaving the CLI a version
behind the libraries. Rather than close the gap with a CLI-only 1.4.8, every
crate was rolled forward to 1.4.9 so a single number identifies the whole
workspace. 1.4.8 is left published (crates.io versions cannot be withdrawn,
only yanked) and is functionally equivalent to 1.4.9 for the three crates
that carry it. The full change list is under 1.4.9 below.

### Added

- **In-place self-update for both binaries** (`skills/rust-self-update`).
  `csaf-crud` and `ndaal-csaf-cli` now expose the same three flags:
  `--check-update`, `--self-update`, and the policy opt-out
  `--no-self-update` (env fallback `CSAF_NO_SELF_UPDATE`, accepting
  `1`/`true`/`yes`/`on`). Both actions are terminal — they act and exit
  rather than continuing into the program.
  - `self_update = "1.0.0-rc.6"` is a **mandatory, un-gated** dependency:
    no `optional = true`, no cargo feature, no `#[cfg(feature)]` around
    the updater. There is exactly one binary shape, and opting out is a
    runtime policy decision, never a build-time one.
    `crates/csaf-core/tests/self_update_is_mandatory.rs` fails to
    **compile** if that ever regresses.
  - Feature set `gitlab` + `ureq` + `rustls` + `archive-tar` +
    `compression-tar-gz` + `checksums`, with `default-features = false`.
    `ureq`/`rustls` rather than `reqwest` keeps the tree free of a second
    HTTP client and of any native-tls/OpenSSL; `reqwest` and
    `zipsign-api` appear in `Cargo.lock` only as unactivated optional
    dependencies and never enter the build graph.
  - Exit codes are a documented, script-facing API: `0` up to
    date/updated/unreachable, `1` install failure, `2` argument error,
    `3` refused by policy, `10` a newer release exists. `--check-update`
    **never** fails on a network error — an unreachable host or a
    rate-limit 403 is an `Unreachable` outcome with exit `0`, so checking
    cannot break a boot path or a cron job.
  - Shared logic lives in `csaf_core::updater` and
    `csaf_core::update_cli`, so the hand-rolled parser in `csaf-crud` and
    the clap parser in `ndaal-csaf-cli` cannot drift apart.
- **`SHA256SUMS` in the release bundle**: `release/create_release.sh` now
  emits a sixth, plainly-named manifest alongside the five ndaal
  five-family manifests. This is the file the updater fetches from the
  same release tag to verify a download before it replaces the running
  executable. The release fails if it disagrees with `SHA-256SUMS.txt`.
- **Compile-time invariants**
  (`crates/csaf-core/tests/static_assertions_invariants.rs`, per
  `skills/rust-static-assertions`): `Send`/`Sync` on `CsafStorage`,
  `AppConfig` and `DbPool`; the `Error + Debug + Display + Send + Sync`
  contract on `CsafError` and `UpdateError`; object-safety of
  `HttpClient`/`HttpResponse` (the injection seam the offline updater
  suite depends on); and the `UpdateFlags` field names that *are* the CLI
  contract. These fail the build, not a test run.
- **Mutation testing config** (`skills/rust-cargo-mutants`):
  `.cargo/mutants.toml` plus the required `[profile.mutants]` block.
- **Tests for the updater**, covering the full pyramid in the same change:
  - `updater_pure.rs` — 39 tests over the IO-free surface (unit +
    proptest), including an anti-drift test that renders each binary's real
    `[package.metadata.binstall]` templates and asserts they reproduce
    `asset_name` / `bin_path_in_archive` character for character for every
    shipped triple. Without it, `cargo binstall` (the first install) and
    the updater (every install after) can silently disagree.
  - `update_cli_props.rs` — 7 properties over the shared flag parser:
    exact partition, set-iff-present, order-independence, idempotence.
  - `updater_transport.rs` — 20 offline tests through `self_update`'s
    `HttpClient` seam with a canned GitLab API (scaffolding adapted from
    upstream's MIT-licensed `tests/custom_transport.rs`), covering
    up-to-date / newer / older / empty / malformed / transport-failure /
    rate-limited, every `perform()` branch that decides *whether* to
    install, and the `SHA256SUMS` resolution itself — including the case
    where the manifest lists only the `.sha-256` **sidecar** of the target
    archive, which must be refused rather than verified against another
    file's digest.
  - `prepare_install` was extracted from the download path so its
    `UnsafeAssetName` screen — a security guard on a name derived from a
    remote release tag — is directly testable. Previously it could only be
    reached by performing a real download, so nothing exercised it.
  - Three fuzz targets — `fuzz_sha256sums_parse`,
    `fuzz_updater_asset_name`, `fuzz_cli_parse` — with corpora seeded from
    the real v1.3.7 release manifests and argument vectors. Seeding raised
    `fuzz_sha256sums_parse` coverage from 194 to 230 features.
  - Mutation testing found two genuine gaps that all of the above had
    missed, both now closed: nothing asserted `is_opted_out()` returns
    *false* (self-update could have been permanently disabled in every
    deployment unnoticed), and nothing asserted `--self-update` is actually
    *parsed* (a dropped match arm would make the flag fall through and
    start the server normally — a failure that looks like success).

### Fixed

- **`tests/scripts/test_cargo_tarpaulin.sh` had no way to fail.** It captured
  `cargo tarpaulin`'s exit code, logged a warning and wrote "FAILED" into its
  report — then discarded the status, so `quality_gates.sh --strict` could
  never abort on coverage and the reported number was decorative. It also
  never passed `--fail-under`. Now follows the same contract as the eight
  bash-tool gates in `skills/bash/SKILL.md`: report-only by default (the
  canonical `exit 0` footer is preserved), with `TARPAULIN_STRICT=1` to fail
  the gate and `TARPAULIN_FAIL_UNDER` (default 85) for the threshold. A
  self-skip when cargo-tarpaulin is absent stays rc=0 in both modes.
- **`tests/scripts/test_fuzzing_targets.sh` wrote its reports outside the
  repository.** `OUT_DIR` used `REPO_ROOT`, which in that script is
  deliberately the PARENT of the workspace (it holds the shared CLAUDE.md),
  so every fuzzing artefact landed in `<parent>/documentation/rust/fuzzing/`
  — never committed, and polluting a directory that may hold sibling
  repositories. Now uses `${QG_WORKSPACE}` like every other gate script.
- **The same script misreported every fuzz target as TIMEOUT.** The outer
  wall-clock allowance was 180s on top of the fuzz duration, far below a cold
  `cargo fuzz` build, so unless `--build` was passed the run was killed
  mid-build and bucketed as a fuzzing timeout — making `ok=0` the normal
  outcome and a genuine timeout indistinguishable from a cold cache. The
  allowance is now `FUZZ_BUILD_ALLOWANCE` (default 900s).
- **`skills/rust-self-update/references/self-update-1.0-api.md` documented
  three names that do not exist in 1.0.0-rc.6**, each of which fails to
  compile: `compression-flate2` (really `compression-tar-gz`),
  `identifier(..)` (really `asset_identifier(..)`), and
  `get_latest_releases()` (really `get_latest_release()`, which returns a
  plural `Releases`). The Backends section also still listed `gitee` and
  `manifest`, contradicting the corrected feature list earlier in the same
  file; rc.6 ships exactly `custom`, `gitea`, `github`, `gitlab`, `s3`.
  Corrected against the vendored crate source, and annotated with the fact
  that only `UpdateBuilder` — not `ReleaseListBuilder` — carries the
  `http_client` setter, which is what the offline test suite depends on.

### Changed

- **`cap-std` 3.4.5 → 4.0.2** (workspace-wide). Required by
  `skills/rust-self-update` Rule 5 for the self-update path. No API
  changes were needed in `csaf-core`'s `fs.rs`, `path_security.rs` or
  `dump.rs`; the full suite passed unchanged.
- **Dependency currency** (lockfile only; the caret requirements in
  `[workspace.dependencies]` already permitted all five):
  `hyper` 1.10.1 → 1.11.0, `serde` 1.0.228 → 1.0.229, `serde_json`
  1.0.150 → 1.0.151, `time` 0.3.53 → 0.3.55, `tokio` 1.52.3 → 1.53.1.
  The `tokio` bump removed eleven now-unreferenced Windows shim crates;
  the `serde` bump added `syn` 3.0.3 beside the existing 2.0.119
  (build-time only, so it does not reach the shipped binaries). Recorded
  as CSAF advisories `ndaal-sa-2026-383` … `-387`.

### Security

- The self-updater verifies every download against the `SHA256SUMS`
  published **in the same release**. Stated plainly because it is easy to
  overclaim: this is **integrity, not authenticity**. Anyone able to
  rewrite the release archive at the source can rewrite the manifest
  alongside it. The artifacts are **not signed** — detached signing
  (`self_update`'s `signatures` feature, via zipsign) is deliberately not
  enabled, because this project's release pipeline does not sign today.
  GitLab's releases API publishes no per-asset digest, so the crate's
  automatic release-digest check is a silent no-op on this backend and
  the `SHA256SUMS` manifest is the only integrity check that actually
  runs.

## [1.3.7] – 2026-07-16

### Added

- **Info navbar dropdown + 4 documentation pages**: `/changelog`,
  `/readme`, `/administrator`, `/user` now render the repository's
  `CHANGELOG.md`, `README.md`, `Administrator_Guide.md`, and
  `User_Guide.md` (embedded via `include_str!`) as HTML pages, reachable
  from a new `<details>`/`<summary>` dropdown under the navbar's "Info"
  entry (no JS framework), with a divider separating the existing About/
  License/System Info/Privacy/Security links from the four new doc
  links. Covered by Rust unit tests (embed-matches-on-disk, XSS-escaping,
  dropdown-link presence) and 4 new Bruno API tests
  (`test/bruno/collections/docs/`).
- **Property tests for `SidecarHashes`**: `struct_excessive_bools` allow
  documented with a `WHY:` comment (five independent on/off toggles map
  1:1 to the five mandated hash families and to `Settings`' five
  `sidecar_*` fields).
- **`clippy.toml` for `csaf-cli` and `csaf-core`**: extends the
  test-relaxed-lints policy already in place for `csaf-crud` to the
  other two publishable crates (`allow-*-in-tests` for unwrap/expect/
  indexing/panic/print/dbg/useless_vec).

### Fixed

- **`crates/csaf-core/tests/test_oasis_gating.rs`**: integration test
  files are a separate compilation unit from `lib.rs`, so the
  `#![cfg_attr(test, allow(clippy::expect_used, ...))]` relaxation there
  never covered this file. Added the matching `#![allow(...)]` block
  directly to the file. The full workspace clippy gate
  (`--workspace --all-targets --all-features -- -D warnings`) is now
  clean. (`ndaal-sa-2026-310`)
- **Bruno API test harness**: `test/bruno/run.sh` was missing the `-r`
  (recursive) flag, so `bru run collections` silently discovered 0
  requests and reported a false PASS on every run — possibly since the
  suite's creation. Fixing it exposed 4 stale assertions (sidecar
  extensions still checking the pre-0.3.0 non-hyphenated forms, a
  missing `settings_reset` audit action, and two response bodies
  asserting fields the handlers no longer return), all corrected here,
  plus a new `admin/00-reset-settings.bru` step so the suite no longer
  depends on leftover state from a prior manual session. 107/107 Bruno
  tests now pass. (`ndaal-sa-2026-311`)
- **bats-core sister test suite**: 72 of 85 `tests/scripts/*.bats` files
  had an identical `"mkdir invocations use -p -v"` test block
  copy-pasted twice (once with its explanatory comment, once without).
  Removed the duplicate, comment-free copy after verifying both bodies
  were byte-identical. (`ndaal-sa-2026-312`)
- **`csaf-crud --help` / `-h` / `--version` / `-V`**: the binary treated
  the first positional argument unconditionally as a config-file path,
  so these flags silently started the full TLS server instead of
  printing usage — discovered when it hung a release smoke test for 46
  minutes. Added early handling that prints and exits before any
  config/network setup. Regression-tested via a subprocess-spawning
  integration test bounded by a 5s timeout.
  (`ndaal-sa-2026-313`)

## [0.3.6] – 2026-07-16

### Added

- **Documentation set** (per `skills/documentation`): root `REFERENCE.md`
  (Diátaxis reference — CLI, config, runtime settings, API and web
  routes, errors, exit codes) and `structure.md` (canonical layout),
  plus the `documentation/` guides — `User_Guide`, `Administrator_Guide`,
  `Troubleshooting_Guide` — and a generated `Linkcheck_Report`. A
  Documentation index now links these from the three README mirrors.
- **`documentation/wiki/`**: a self-updating codebase wiki (per
  `skills/documentation-codewiki`) mapping entities, concepts, and
  patterns across the workspace for contributors and coding agents.
- **Fuzz + property-test coverage**: two new cargo-fuzz targets,
  `fuzz_import_file` (the import-directory parser, closing the last gap
  in CLAUDE.md's canonical six-target list) and `fuzz_oasis_validate`
  (the OASIS `csaf-rs` validator wrapper, a new untrusted-JSON surface
  with no prior coverage). `proptest` is now wired into `csaf-core` as a
  dev-dependency for the first time, pairing each new fuzz target with a
  never-panics property test (`import::tests::prop_never_panics_on_arbitrary_file_content`,
  `oasis::tests::prop_never_panics_on_arbitrary_utf8`).
- **CSAF dependency-currency advisories** `ndaal-sa-2026-300`..`308`:
  one advisory per crate bumped in the 2026-07-15 `cargo outdated`
  sweep (`csaf-rs`, `uuid`, `time`, `log`, `memchr`, `quote`,
  `regex-automata`, `syn`, `getrandom`), each carrying the canonical
  0.0/NONE dual CVSS (`cargo audit` reported zero findings against any
  of them) and tri-tool-verified five-family sidecars.

### Changed

- **`csaf-rs` 0.4.1 → 0.5.1** (feature `oasis-validator`, default-on).
  `{csaf2_0,csaf2_1}::loader::load_document_from_str` was renamed
  upstream to `load_document(source: impl JsonSource)`; adapted the two
  call sites in `crates/csaf-core/src/oasis.rs`. No behaviour change —
  `oasis::validate_oasis_json` / `is_oasis_valid` keep their existing
  signatures and the OASIS `"basic"` preset dispatch is unchanged.
- **`uuid` 1.23.3 → 1.24.0**, **`time`/`time-macros`** 0.3.49/0.2.29 →
  0.3.53/0.2.31, and five transitive crates (`log`, `memchr`, `quote`,
  `regex-automata`, `syn`, `getrandom`) bumped to their latest in-range
  versions via `cargo update`; `crates/workspace-hack/Cargo.toml`
  resynced with `cargo hakari generate`. `crypto-common`, `digest`, and
  `hashbrown` were left unbumped — the newer majors are blocked by
  `sha2`/`blake2`/`hashlink`'s own transitive requirements (no stable
  `blake2`/`argon2` release supports `digest` 0.11 yet); see
  `ndaal-sa-2026-300`..`308` for the per-crate detail.

### Fixed

- **ESLint config** (`eslint.config.js`) was non-functional: shell-style
  `#` SPDX comments (invalid JavaScript), a non-existent `security/*`
  rule, type-aware `@typescript-eslint` rules without a tsconfig, and a
  wrong `globals` environment for the browser script. It now loads and
  lints clean. Added a dev-only `package.json` pinning the ESLint
  toolchain (`node_modules` / `package-lock.json` stay gitignored).
- **Windows-ARM64 release build** (`aarch64-pc-windows-msvc`): blake3's
  NEON C intrinsics fail to compile under cargo-xwin/clang (the MSVC SDK
  stubs `vreinterpretq_u32_u8`), so `csaf-core` now selects blake3's
  pure-Rust backend on that target only — native targets keep the SIMD
  path. Restores the sixth release target (absent since v0.3.2).

### Security

- **`quinn-proto` 0.11.14 → 0.11.16** — fixes RUSTSEC-2026-0185 (HIGH,
  7.5), a remote memory-exhaustion vulnerability from unbounded
  out-of-order QUIC stream reassembly.
- **`anyhow` 1.0.102 → 1.0.103** — fixes RUSTSEC-2026-0190, an
  unsoundness in `Error::downcast_mut()`.
  `cargo audit --deny warnings --deny unsound --deny unmaintained --deny yanked`
  is clean after both bumps.
- **`release/create_release.sh`** now emits all five mandated hash
  sidecar families (`.sha-256`, `.sha-512`, `.sha3-512`, `.blake3-512`,
  `.shake256-512`) per CLAUDE.md's cryptographic-hashes convention; it
  previously emitted only three.

## [0.3.5] – 2026-06-15

Dependency-maintenance release. No source changes — in-range
(semver-compatible, non-breaking) updates only.

### Changed

- **Dependency refresh (`cargo update`).** Moved 50 direct and
  transitive crates to their latest in-range versions, including the
  runtime / web / TLS stack: `tokio` 1.52.1 → 1.52.3, `hyper`
  1.9.0 → 1.10.1, `h2` 0.4.13 → 0.4.15, `http` 1.4.0 → 1.4.2,
  `rustls` 0.23.39 → 0.23.40, `rustls-native-certs` 0.8.3 → 0.8.4,
  `rustls-pki-types` 1.14.0 → 1.14.1, `rcgen` 0.14.7 → 0.14.8,
  `serde_json` 1.0.149 → 1.0.150, `time` 0.3.47 → 0.3.49,
  `chrono` 0.4.44 → 0.4.45, `uuid` 1.23.1 → 1.23.3,
  `regex` 1.12.3 → 1.12.4, `zeroize` 1.8.2 → 1.9.0,
  `wasm-bindgen` 0.2.118 → 0.2.125, plus the usual `libc` / `cc` /
  `memchr` / `smallvec` / `hashbrown` / `windows-sys` transitive
  churn. `csaf-rs` deliberately held at 0.4.1 — 0.5.0 is a breaking
  change and is deferred to a dedicated release.
- `workspace-hack` regenerated (`cargo hakari generate`) for the
  refreshed dependency set; `cargo hakari verify` clean.

### Verified

- `cargo check --workspace --all-features` — clean.
- `cargo test --workspace --all-features` — 30 tests pass, no
  failures; the separate `--doc` pass is clean.

## [0.3.4] – 2026-05-25

OASIS conformance + crypto-provider + integrity-sidecar release.

### Added

- **OASIS validator (csaf-rs) — on by default.** New `oasis-validator`
  feature (default) wires the official CSAF 2.1 suite into export and
  import via `csaf_core::oasis`. **Export is a hard gate** — a
  non-conformant advisory is refused before any bytes are written.
  **Import is a soft gate** — findings are logged but the document is
  still admitted (third-party input is not ours to reject). CLI
  `validate` reports OASIS findings and fails on hard errors.
  (+72 transitive crates behind the default feature; disable with
  `--no-default-features`.)
- **Two further hash sidecar families.** `sidecar_blake3_512` and
  `sidecar_shake256_512` settings (default on, restored by
  reset-to-defaults), with `.blake3-512` / `.shake256-512` emitted
  alongside the SHA-2/SHA-3 sidecars — five orthogonal families per
  artefact. New settings-page toggles; new `blake3` dependency;
  `fuzz_blake3_512` / `fuzz_shake256_512` fuzz targets.
- CSAF advisories `ndaal-sa-2026-071` (0.3.3 release note),
  `-072` (test-fixture schema defect), `-073` (cvss_v4 model fix).
- `cargo-vet` supply-chain store (`supply-chain/`).

### Fixed

- **CVSS v4 model serialization.** `CvssV4` emitted the vulnerable-system
  impacts with CVSS v3 names (`confidentialityImpact`/…); renamed to the
  v4 names (`vulnConfidentialityImpact`/…), so exported `cvss_v4` objects
  conform to the CSAF 2.1 schema (advisory `ndaal-sa-2026-073`).
- **Crypto provider — ring only (no aws-lc / OpenSSL-family C).**
  `tokio-rustls`'s default features pulled `rustls/aws_lc_rs`, unifying
  `aws-lc-sys` (BoringSSL-derived) into the whole workspace contrary to
  the documented ring-only posture; pinned to
  `default-features = false, features = ["logging", "ring"]`. Drops
  `aws-lc-sys`/`aws-lc-rs` entirely and unblocks the
  `aarch64-pc-windows-msvc` cross-compile.
- **cargo-deny gate** migrated to the 0.18+ schema.
- CSAF 2.1 test fixtures: corrected `cvss_v4` keys, `cwe` → `cwes[]`,
  and added the required `cwes[].version`.

### Changed

- `csaf_mode = "2.0"` now refuses to export a structurally-2.1 advisory
  (the model emits only 2.1 structures): the export OASIS gate will not
  write a mislabelled, non-conformant document.
- Dropped the legacy non-hyphenated `.sha512` sidecar form.
- Condensed `CLAUDE.md`; `python-testing-patterns` is now a folder skill.

### Verified

- `cargo test -p csaf-core -p csaf-models` — all pass (incl. the new
  `test_oasis_gating` suite); `cargo check --workspace` clean.
- `oxlint` / `fta` clean on the web UI assets.

## [0.3.3] – 2026-05-20

Security release: closes a path-traversal write primitive in CSAF
export, confines all advisory filesystem access with capability
handles, and restricts TLS to 1.3 only.

### Security

- **Path traversal (CWE-22) in CSAF export — fixed.** The export
  path was built from the imported document's attacker-controllable
  `document.tracking.id`: validation only checked the ID was
  non-empty and `parse_tracking_id` did not reject `..` in the
  sequence component, so a crafted tracking ID such as
  `ndaal-sa-2026-../../escaped` resolved outside the configured
  export directory — an arbitrary file write as the service
  account. Documented as
  [`ndaal-sa-2026-047`](csaf/2026/047/ndaal-sa-2026-047.json).
- **Capability-confined filesystem access (defence in depth).**
  New `csaf_core::fs::DataDir` wraps the `cap-std` capability API;
  advisory import, export, dump, and audit-log writes now flow
  through `Dir`-scoped handles, so `..`, absolute paths, drive /
  UNC prefixes, and symlink / TOCTOU escapes are refused at the
  syscall layer (Linux `openat2(RESOLVE_BENEATH)`). A lexical
  guard, `csaf_core::path_security::{safe_join, is_safe_relative_path}`,
  backs every externally-derived path with a clear error before any
  filesystem operation runs.
- **Static-file serving hardened.** Replaced the naive
  `contains("..")` check with the component-based
  `is_safe_relative_path`, which also rejects absolute paths,
  drive / UNC prefixes, and NUL bytes.
- **TLS 1.3 only.** Dropped the `tls12` feature from the rustls
  dependency so the TLS 1.2 implementation is no longer compiled
  in (the server already pinned `TLS13` at runtime).

### Added

- 57-vector path-traversal regression suite
  (`crates/csaf-core/tests/test_path_security.rs`) and a
  `fuzz_safe_join` cargo-fuzz target seeded from the same corpus.
- Quality-gate harness: `scripts/quality_gates.sh` orchestrator
  (89 gates; `--list` / `--strict` / `--fast` / `--only` /
  `--skip`) plus 85 `tests/scripts/test_*.sh` runners with `.bats`
  sisters, and the `scripts/quality_gates.{uml,svg,png}` workflow
  diagram.
- `cargo-hakari` `workspace-hack` crate for build-time
  dependency-feature unification (publish members via
  `cargo hakari publish`).

### Changed

- Sidecar writers (`write_sidecar_files` /
  `write_sidecar_files_for`) take a `SidecarHashes` options object
  instead of three boolean flags.
- Removed the unused `hyper` dev-dependency from `csaf-core`;
  added doc-comment backticks and `Eq` derives across the model
  crates.

### Verified

- `cargo test --workspace --all-features` — 369 passed.
- `cargo clippy --workspace --all-targets --all-features -- -D warnings` — clean.
- `cargo hakari verify` — workspace-hack consistent.

## [0.3.2] – 2026-04-23

### Changed

- `[workspace.dependencies]` gains a direct pin
  `generic-array = "0.14.9"`, consumed from `csaf-core` via a
  hidden `pub use generic_array::typenum::Unsigned as …` shim
  in `sidecar.rs`. 0.3.1 bumped the committed lockfiles but
  `cargo install csaf-crud` / the `cargo publish` verify step
  ignore our lockfile and re-resolve fresh, picking
  `crypto-common 0.1.7` — which hard-pins
  `generic-array = "=0.14.7"`. The direct pin on
  `generic-array 0.14.9+` forces the resolver to pick
  `crypto-common 0.1.6` instead (its `generic-array` requirement
  is the looser `^0.14.4`), so fresh resolves now converge on
  the same versions the lockfiles already use. Ref
  [`ndaal-sa-2026-040`](csaf/2026/040/ndaal-sa-2026-040.json)
  revision 1.3.0.

### Verified

- `cargo test --workspace --all-features` — 302 passed.
- `cargo clippy --workspace --all-targets --all-features -- -D warnings` — clean.
- `cargo update` (both workspaces) — both lockfiles remain on
  `generic-array 0.14.9` + `crypto-common 0.1.6`.
- `cargo publish --dry-run -p csaf-models` — packages cleanly.

## [0.3.1] – 2026-04-23

### Changed

- **generic-array** `0.14.7` → `0.14.9` in the fuzz workspace
  (`fuzz/Cargo.lock`). The main workspace was already on
  `0.14.9`; the fuzz lockfile was held back by
  `crypto-common 0.1.7`, which hard-pins
  `generic-array = "=0.14.7"`.
  `cargo update -p crypto-common --precise 0.1.6` followed by
  `cargo update -p generic-array --precise 0.14.9` brings both
  lockfiles into alignment on the latest patch release (ref
  [`ndaal-sa-2026-040`](csaf/2026/040/ndaal-sa-2026-040.json)).

### Verified

- `cargo test --workspace --all-features` — 302 passed.
- `cargo clippy --workspace --all-targets --all-features -- -D warnings` — clean.
- No source-level edits required.

## [0.3.0] – 2026-04-23

### Added

- **SHA-512 sidecar toggle** — new `Settings.sidecar_sha512`
  (default `true`) alongside the existing SHA-256 and SHA3-512
  toggles. Exports and database dumps now emit the three-way hash
  set (`.sha-256`, `.sha-512`, `.sha3-512`) per payload.
- **Log Directory setting** — new `Settings.log_directory`
  (default `./data_log`). The server installs a
  `tracing-appender` daily rolling file layer under this path
  (filename `csaf-crud.<date>.log`) in addition to the stderr
  subscriber; both sinks honour `RUST_LOG`.
- **Reset to default settings** — `POST /settings/reset`, fronted
  by a confirmation-guarded button at the bottom of `/settings`,
  restores every field to `Settings::default()` and records a
  `settings_reset` audit row. The CHECK constraint on
  `audit_log.action` gains `settings_reset`.
- **Audit Log export** — `/admin/export` grows an **Export Audit
  Log** card (`POST /admin/export-audit`) producing
  `audit-<UTC>.md/.csv/.json/.sarif` with matching hash sidecars
  per payload. SARIF 2.1.0 output is self-validated before write
  (version, schema, tool.driver.name, every result has
  ruleId / message.text / recognised level).
- Three new libfuzzer targets:
  `fuzz_sidecar_hash_triplet`, `fuzz_sidecar_write_ext`,
  `fuzz_audit_sarif`.
- Four new Bruno `.bru` files:
  `settings/03-reset-settings.bru`,
  `settings/04-sidecar-sha512-roundtrip.bru`,
  `settings/05-log-directory.bru`,
  `admin/03-export-audit.bru`.

### Changed

- Workspace version `0.2.1` → `0.3.0`.
- `sidecar::write_sidecar_files` and
  `sidecar::write_sidecar_files_for` gain a `write_sha512`
  parameter between `write_sha256` and `write_sha3_512`. The
  "for" helper now returns a 3-tuple of `Option<PathBuf>`.
- New helper
  `sidecar::generate_all_hashes(data) -> (sha256, sha512, sha3_512)`.
  `generate_hashes` retained as a two-tuple shim for 0.2.x callers.
- `/admin/export` and `/admin/dump` status cards now show the
  SHA-512 sidecar toggle state.

### Fixed

- **SHA-256 sidecar extension** (`.sha256` → `.sha-256`).
  Both sidecar writers were emitting the unhyphenated form,
  violating the `CLAUDE.md` §"Cryptographic hashes" mandate.
  A regression-guard test in `sidecar.rs` now fails the build
  if the legacy form ever reappears.
- `init_tracing` — the daily rolling file layer had no
  `EnvFilter`, so it captured every rustls / quinn / h2 TRACE
  internal (550 KB log file after a handful of curl requests in
  smoke testing). Both sinks now construct their own filter from
  `RUST_LOG`.
- Stale `metadata_version == "2.0"` assertions in
  `csaf_crud_cycle::test_provider_metadata_cycle` and
  `provider_meta::test_deserialize_provider_metadata` updated to
  `"2.1"` to match the in-tree `test/csaf/provider-metadata.json`
  fixture.
- Pre-existing `needless_borrow` lint in
  `fuzz_classification_writeback.rs`.
- `rust-doctor.toml` — `[ignore].rules` entries must NOT carry
  the `clippy::` prefix; four lints not on rust-doctor's
  whitelist were dropped (they remain silenced workspace-wide
  via `[workspace.lints.clippy]` in `Cargo.toml`).
- Unused `chrono` dependency removed from `crates/csaf-crud`.

### Upgrade notes

- Sidecars are derived artefacts — re-run `/admin/export` and
  `/admin/dump` once and the hyphenated extensions land
  alongside the payloads.
- Fresh databases on 0.3.0 accept `settings_reset` as an
  `audit_log.action` value; 0.2.x databases keep the narrower
  CHECK constraint and silently drop those rows. A future
  release will add an online migration if this becomes a
  blocker.

## [0.2.1] – 2026-04-23

### Changed

- **matchit** `0.8.6` → `0.9.2` (ref
  [`ndaal-sa-2026-034`](csaf/2026/034/ndaal-sa-2026-034.json)).
- **rcgen** `0.13.2` → `0.14.7` (ref
  [`ndaal-sa-2026-035`](csaf/2026/035/ndaal-sa-2026-035.json)).
- **sysinfo** `0.32.1` → `0.38.4` (ref
  [`ndaal-sa-2026-038`](csaf/2026/038/ndaal-sa-2026-038.json)).
- **generic-array** `0.14.7` → `0.14.9` — transitive patch
  bump via `cargo update -p generic-array --precise 0.14.9`
  (ref [`ndaal-sa-2026-040`](csaf/2026/040/ndaal-sa-2026-040.json)).

### Verified

- `cargo test --workspace --all-features` — 280 passed,
  0 failed, 0 ignored.
- `cargo check --workspace --all-targets` — clean.
- No source-level edits required for any of the three
  breaking-semver bumps.

## [0.2.0] – 2026-04-23

### Added

- 19 new crate-integration tests covering `askama`,
  `hyper-util`, `http-body-util`, `quinn`, `h3` / `h3-quinn`,
  and `tracing-subscriber`
  (`crates/csaf-core/tests/test_crate_integrations.rs`
  sections 26–30). Total: 129 integration tests.
- Informational advisories
  [`ndaal-sa-2026-032`](csaf/2026/032/ndaal-sa-2026-032.json)
  through
  [`ndaal-sa-2026-039`](csaf/2026/039/ndaal-sa-2026-039.json)
  documenting the planned dependency bumps and the 0.2.0
  release event.

### Changed

- Workspace version bumped from `0.1.0` to `0.2.0`; all four
  member crates publish together via `version.workspace = true`.
- **CLI crate renamed**: `csaf-cli` → `ndaal-csaf-cli`. The
  `csaf-cli` name on crates.io is owned by the unrelated
  `scm-rs/csaf-walker` project at 0.15.1. Users install the
  renamed crate with
  `cargo install ndaal-csaf-cli` and invoke the
  `ndaal-csaf-cli` binary instead of `csaf-cli`.
  See
  [`ndaal-sa-2026-039`](csaf/2026/039/ndaal-sa-2026-039.json).

### Published to crates.io

- [`csaf-models 0.2.0`](https://crates.io/crates/csaf-models/0.2.0)
- [`csaf-core 0.2.0`](https://crates.io/crates/csaf-core/0.2.0)
- [`csaf-crud 0.2.0`](https://crates.io/crates/csaf-crud/0.2.0)
- [`ndaal-csaf-cli 0.2.0`](https://crates.io/crates/ndaal-csaf-cli/0.2.0)

## [0.1.0] – 2026-04

### Added

- Initial workspace layout with four crates: `csaf-models`,
  `csaf-core`, `csaf-crud`, and `csaf-cli`.
- HATEOAS JSON API on TCP `127.0.0.1:8180` (TLS 1.3,
  HTTP/1.1 + HTTP/2) and HTTP/3 on UDP `127.0.0.1:8181`.
- CSAF 2.0 / 2.1 document storage via `redb`, with a SQLite
  sidecar for users, audit log, and settings.
- Sidecar hashes (`.sha-256`, `.sha-512`, `.sha3-512`) for
  every CSAF advisory and release artefact.

Are you sure?