# 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.