Changelog

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

# Changelog

All notable changes to the BSI Grundschutz++ OSCAL Viewer are documented
in this file. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.1.16] - 2026-06-15

### Tests

- `tests/test_export_pdf.rs`: assert the injected section-heading labels
  (notably `Guidance (Erläuterung)`, rendered in the bold heading font) keep
  their umlaut even for controls whose own text contains none — closing a gap
  in the exhaustive per-control umlaut sweep (which only checked umlauts that
  appear in each control's source text).

### Note

- The faithful-Unicode PDF export (umlauts `ä ö ü ß`) landed in **0.1.15**.
  If a PDF still shows dropped umlauts (e.g. `Erläuterung` → `Erluterung`), it
  was generated by an older binary — re-export with 0.1.15 or later (or
  `cargo install grundschutz-oscal-viewer --force`). The packaged crate is
  otherwise unchanged from 0.1.15.

## [0.1.15] - 2026-06-15

### Added

- **Export menu** (`/export`, beside *Metadata*): export the whole
  Grundschutz++ catalog, a single practice, or a single control as
  **JSON, Markdown, ODT or PDF**, each accompanied by selectable checksum
  **sidecars** (`.sha-256`, `.sha-512`, `.sha3-512`, `.blake3-512`,
  `.shake256-512`, GNU `shasum` format). The document formats are rendered
  in-process by `lo_writer` (the pure-Rust libreoffice-rs document model —
  no external LibreOffice, no shelling out); JSON is the raw OSCAL. Files
  are written into a server-side directory through a single
  capability-scoped `cap_std` handle with `create_new` semantics (existing
  files are never overwritten).
- `--export-dir <DIR>` / `GSV_EXPORT_DIR` configure the Export target
  directory. The default is a per-user `grundschutz-oscal-viewer/export`
  folder under the home directory (`$HOME` on Linux/macOS, `%USERPROFILE%`
  on Windows — never a world-writable temp location), created on first use.
- The Export form has an **Overwrite existing files** checkbox (ticked by
  default) so re-running an export replaces its previous files; unticked,
  the export refuses to clobber an existing file and says so clearly
  instead of surfacing a raw `File exists (os error 17)`.
- **PDF export now renders German umlauts faithfully.** `lo_writer`'s PDF
  backend uses a base-14 font with StandardEncoding and silently drops
  non-ASCII characters (`Geschäftsprozesse` → `Geschftsprozesse`). PDF is
  now rendered directly (`src/export_pdf.rs`) with the bundled Roboto
  TrueType font embedded as a Type0 / CIDFontType2 plus a ToUnicode CMap,
  so `ä ö ü ß` render correctly and stay selectable / extractable. Uses
  only `ttf-parser` (zero-dependency, `forbid(unsafe_code)`); Markdown and
  ODT (which already preserved umlauts) stay on `lo_writer`. Covered by
  `tests/test_export_pdf.rs` (an exhaustive sweep that decodes every
  exported control's text back through its ToUnicode CMap) and by
  `scripts/verify_pdf_umlauts.sh`, an independent cross-check that extracts
  the PDF text with `unpdf` (PDF.js) and asserts all seven umlauts survive.
- `tests/test_export.rs` — an exhaustive export suite driven through the
  real router: the form, every format, the five sidecars (content checked
  against the digests), every practice as JSON, the validation paths, the
  unknown-token handling and the overwrite refusal.
- Fuzz targets `fuzz_sidecar` (digest width/charset/determinism + token
  selection) and `fuzz_export` (format-token parsing + scope resolution +
  JSON building, asserting sanitised single-segment filenames — no
  traversal).

### Changed

- **Meilisearch is now enabled by default** at `http://localhost:7700`.
  Pass an empty `--meili-url=` (or `MEILI_URL=`) to disable it. The client
  is rustls-protected: HTTPS for any non-loopback host, plain HTTP only for
  loopback.
- **TLS 1.2 removed everywhere.** The server and the Meilisearch client now
  negotiate **TLS 1.3 only** (the rustls `tls12` feature is no longer
  enabled); every older protocol is refused outright.

## [0.1.14] - 2026-06-13

### Added

- Control pages now offer a **View JSON / Download JSON** action below the
  raw-OSCAL card (the same widget the ndaal advisory drill-downs use).
  `GET /control/{id}/raw.json?download=1` serves the control as a file
  attachment — `Content-Disposition: attachment; filename="<id>.json"`
  with the id lowercased and sanitised (`KONF.2.1` → `konf.2.1.json`) —
  while the plain endpoint stays inline for in-browser viewing.
- Control-list and practice pages now surface the **mapped control**, not
  just the framework name: each framework badge reads e.g.
  `github-security-controls · GH-CHG-01 +6`, with the full reference list
  in its `title` tooltip.
- `scripts/quality_gates.sh` — a canonical quality-gate runner adapted for
  this single-crate, no-database app (native fmt / clippy / test /
  doctest / doc / htmlhint / oxlint / fuzz / live-sweep plus a convention
  dispatcher to `tests/scripts/test_<slug>.sh`), with the reference's
  `--strict / --fast / --rest / --only / --list` profiles.
- `tests/scripts/test_settings_toggle_combinatorics.sh` — a headless-Chrome
  (DevTools-Protocol) test that combinatorially toggles the Settings
  frameworks and asserts each mapping row's visibility and each toggle's
  checked state.

### Changed

- The Settings framework panel is now a multi-column grid that shows every
  framework at once, instead of a single scrolling column.

### Fixed

- Dropped the yanked `time 0.3.48` / `time-core 0.1.9` (transitively via
  `rcgen`); `cargo deny check` is clean again.
- `run_exhaustive_tests.sh`: the HSTS live-sweep check no longer false-fails
  under `set -o pipefail` — `curl -I` (HEAD) exits 16 over HTTP/2 after
  printing the headers, which masked the present header; switched to a
  body-discarding GET + `-D -`.
- `frameworks::parse` moves the parsed CSV fields into each toggle instead
  of cloning them inside the loop.

## [0.1.13] - 2026-06-13

### Added

- Full framework toggle universe: the Settings menu now lists ~150
  compliance frameworks (the CISO-Assistant framework set, with flags),
  embedded as `data/frameworks.csv` (slug, display, default flag). The
  dropdown is a fixed-height scrollable panel with a search box and
  All / None / Reset buttons; a specific subset (ISO 27001, NIS2,
  PCI DSS 4.0.1, GDPR, DORA, CRA, BSI Kompendium, BSI C5, BSI
  Mindeststandard Cloud, NIS2 IR, OWASP ASVS 5, and our GitHub
  crosswalk) is enabled by default, every other framework off.
- Runtime mapping pack: `--mappings-dir <DIR>` / `GSV_MAPPINGS_DIR`
  loads extra mapping CSVs at startup through a capability-based cap-std
  handle, merged with the embedded crosswalk. This keeps any
  separately-licensed (e.g. auto-generated, AGPL) mapping data out of
  the Apache-2.0 binary while still letting the viewer display it.
- Each control page now shows, per framework, **the corresponding
  control** (its reference id and name) plus a similarity-score badge
  and an "auto-generated — verify before use" caveat. The CSV format
  gains a sixth `score` column; `framework` is now a stable slug
  matching `data/frameworks.csv`. See
  `documentation/framework_mappings_pack.md` for the pack contract and
  generator design.

### Changed

- Mapping framework keys are slugs (e.g. `iso27001`) rather than display
  strings; the embedded GitHub crosswalk and `scripts/import_mappings.py`
  were re-keyed accordingly. Default-off framework rows/badges render
  with `d-none` (hidden until enabled); per-framework visibility is
  remembered per browser as overrides on top of each framework's default.

## [0.1.12] - 2026-06-12

### Added

- Exhaustive test sequence: `tests/test_exhaustive.rs` sweeps the whole
  surface in-process (every practice page, every control's `raw.json`,
  every practice × modal-verb × security-level filter combination, the
  full pagination range, every embedded static asset including the
  icon-font aliases, `HEAD` on every registered route, a search per
  practice id and every mapped control's mapping table).
- `scripts/run_exhaustive_tests.sh` (`just exhaustive`) chains the full
  sequence: QA gates → test suite → optional fuzz smoke → release
  build → live HTTPS sweep against a running instance → **testssl.sh
  port check**, which asserts the served port offers TLS 1.3 and
  refuses every older protocol (TLS 1.2/1.1/1.0, SSLv2/v3).

### Changed

- **The viewer now serves HTTPS only.** A TLS 1.3 listener built on
  rustls (aws-lc-rs provider with `prefer-post-quantum`, so the
  X25519MLKEM768 post-quantum hybrid key-exchange group is offered
  first) replaces the previous plaintext HTTP listener — same design as
  ndaal `vulnerability-lookup-rs`. A self-signed certificate for
  localhost is generated with `rcgen` at every start-up (45-day
  validity), HTTP/1.1 and HTTP/2 are offered via ALPN, and a 10 s
  handshake timeout guards the accept loop. `Strict-Transport-Security`
  is sent on every response again (it was dropped while the listener
  was plain HTTP). Browsers show a one-time self-signed-certificate
  warning; scripted clients use `curl -k`. TLS listener pins ported
  from vl-web `test_pqc_kex.rs`, plus a live in-process handshake test
  asserting TLS 1.3 + ALPN h2 + the negotiated PQC group.

## [0.1.11] - 2026-06-12

### Added

- Pre-built release binaries for all six supported platforms committed
  under `release/`, with `SHA256SUMS` and a usage/verification README.
- `scripts/import_mappings.py` (`just import-mappings`) converts the
  GitHub Security Control Catalog under `import/` into viewer mappings:
  it pivots on the `map_bsi_grundschutz_pp` column and attaches, per
  pinned Grundschutz++ id, the GitHub control plus that row's NIS2,
  DORA, ISO/IEC 27001, C5 and Mindeststandard references. Placeholder
  values (`TBD`, `to pin`) are skipped, unknown control ids are warned
  about — a no-op until ids are pinned in the import CSV.
- Cross-framework mappings imported from CSV: every
  `data/mappings/*.csv` (format
  `control_id,framework,reference,title,url`) is embedded at build time
  and shown on every level of the control hierarchy — a "Framework
  mappings" table on each control page (nested sub-controls included)
  and framework badges on the controls list and practice pages. A new
  navbar **Settings** menu (left of the theme toggle and Info) offers a
  checkbox per framework — all enabled by default; disabling one hides
  that framework's mappings (remembered per browser). Ships with
  clearly-marked illustrative example mappings for DORA and
  ISO/IEC 27001:2022 — replace them with reviewed mappings.
- Published on crates.io as `grundschutz-oscal-viewer`
  (`cargo install grundschutz-oscal-viewer`); the package whitelist
  ships only sources, templates, assets and the BSI catalog — release
  binaries, docs and tooling stay out. The crate licence is declared as
  the composite of the packaged contents
  (`Apache-2.0 AND CC-BY-SA-4.0 AND MIT AND 0BSD AND OFL-1.1`).
- Fuzzing harness (`fuzz/`, cargo-fuzz, libFuzzer) with eight targets
  covering every untrusted-input surface: percent decoding, query
  parsing, static-path lookup (traversal property), OSCAL parameter
  substitution, catalog JSON parsing, built-in search, filter +
  pagination invariants and the encode/decode round-trip used by all
  filter links. `just fuzz-build` / `just fuzz <target>` /
  `just fuzz-smoke`.
- Info-menu unit tests adapted from ndaal `vulnerability-lookup-rs`:
  the Impressum's legally required sections, entity facts, disclaimers
  and external profiles; navbar/footer reachability of `/imprint` and
  `/changelog`; full, untruncated changelog rendering in lock-step with
  the on-disk `CHANGELOG.md`.

### Changed

- Real framework mappings: the empty `map_bsi_grundschutz_pp` column of
  the GitHub Security Control Catalog (`import/`) is now populated for
  all 91 GitHub controls (domain-level crosswalk to validated
  Grundschutz++ control IDs), and `import/generate_catalog_md.py` reads
  and writes under `import/` (it previously pointed at non-existent
  `data/` and `documentation/` paths). Running
  `scripts/import_mappings.py` embeds **211 mappings** across five
  frameworks (GitHub Security Controls, NIS2, ISO/IEC 27001:2022, BSI
  Kompendium 2023, BSI Mindeststandard Cloud); the illustrative
  DORA / ISO toy example CSVs are removed.
- Settings dropdown layout fixed: each framework toggle is now a clean
  row with the name left and the switch right-aligned, on a dropdown
  wide enough to keep long framework names on one line.
- Requirement-level colours swapped: MUSS badges, bars and stat cards
  are now green, KANN red (SOLLTE stays yellow).

### Fixed

- Navbar dropdowns (Info, Practices, mobile menu) did nothing: the
  vendored `bootstrap.bundle.min.js` was a re-prettified variant whose
  broken minification threw at load time, so Bootstrap never
  initialised. All vendored text assets (Bootstrap 5.3.3 CSS/JS,
  Bootstrap Icons 1.11.3 CSS, htmx 2.0.4) are now byte-identical to the
  official distributions (verified against published SRI hashes) and an
  integrity test pins their SHA-256 values.
- `/static/` asset URLs now carry a `?v=<version>` cache-buster so a
  binary upgrade can no longer leave stale CSS/JS in browsers
  (assets are cached for an hour).

## [0.1.10] - 2026-06-12

### Added

- Single-binary viewer for the official BSI Grundschutz++ OSCAL catalog
  (`BSI-Bund/Stand-der-Technik-Bibliothek`, Anwenderkatalog, OSCAL 1.1.3).
- Complete catalog JSON embedded at compile time — a binary copy is a
  full installation; no network access, database or install step needed.
- Web UI served on localhost (hyper + Askama + Bootstrap 5 + HTMX),
  design and Info menu carried over from ndaal `vulnerability-lookup-rs`:
  About, System info, Privacy, Security, License, Imprint, Changelog.
- Control list with filters for practice (domain), modal verb
  (MUSS / SOLLTE / KANN), security level (`normal-SdT` / `erhöht`),
  effort level (0–5), tag and free text.
- Control detail pages showing every JSON field: statement (with OSCAL
  parameter substitution), guidance, props with namespaces, params,
  tags, nested sub-controls and the raw control JSON.
- Practice overview and per-practice pages with the full group tree.
- Statistics page with catalog-wide distributions.
- Catalog metadata page (raw OSCAL metadata + back-matter) and
  `/catalog.json` download of the embedded catalog.
- Full-text search: built-in engine (always available, offline) plus
  optional Meilisearch integration (`--meili-url` / `MEILI_URL`); the
  index is populated automatically at startup. Results report the true
  match count ("showing the first 50"); query length and token count
  are capped so a single request cannot buy unbounded CPU time.
- `--export <dir>` writes the embedded catalog through capability-based
  cap-std directory handles with create-new semantics;
  `scripts/update_catalog.sh` refreshes the vendored catalog.
- Security headers on every response: X-Frame-Options DENY, a CSP whose
  script-src pins the two inline scripts by SHA-256 hash (no
  `unsafe-inline` for scripts), nosniff, Referrer-Policy,
  Permissions-Policy and COOP/CORP/COEP.
- Server hardening: connection cap (512) and a 30 s header-read timeout
  against slowloris; `HEAD` served wherever `GET` is (RFC 9110), `405`
  responses carry the `Allow` header.
- REUSE licensing: `.license` sidecars for the embedded catalog and all
  vendored UI assets, full license texts under `LICENSES/`.
- Cross-platform release builds via cargo-zigbuild
  (`scripts/build_release_targets.sh`, `just build-all`): Linux
  (x86_64/aarch64, static musl), Windows (x86_64/aarch64) and macOS
  (x86_64/aarch64), with SHA-256 checksums in `dist/SHA256SUMS`.