<!--
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`.