# ndaal CSAF-CRUD
Secure, single-binary Rust CRUD application for managing
**CSAF 2.0 and 2.1** security advisories. Provides both a
server-side rendered web UI (Hyper + Bootstrap 5 + HTMX) and a
RESTful HATEOAS JSON API under `/api/v1/`.
## Features
- **CSAF 2.0 / 2.1** — Full serde types, strict validation, dual
CVSS v3.1 + v4.0 scoring.
- **Classification support** — TLP 2.0 colour-coded selector
(`CLEAR/GREEN/AMBER/AMBER+STRICT/RED`), German Verschlusssache
(`VS-NfD / VS-Vertr. / Geh. / Str. Geh.`), and NATO
(`NR / NC / NS / CTS`). Written into the exported CSAF either
to `document.distribution.text`, to `document.notes[]`, or to
both — configurable per-deployment.
- **Dual embedded database** — `redb` for CSAF documents,
`rusqlite` (bundled) for user management and audit logging.
- **Zero external dependencies** — Single binary deployment.
- **Protocols** — HTTP/1.1, HTTP/2, and HTTP/3 (QUIC) over
TLS 1.3 via rustls, ring crypto, and quinn.
- **Self-signed certificates** — Auto-generated via rcgen with
45-day validity (Let's Encrypt short-lived cert model).
- **Sidecar hashes** — SHA-256 (`.sha-256`), SHA-512 (`.sha-512`),
and SHA3-512 (`.sha3-512`) generated for every export and database
dump, each algorithm individually toggleable via settings. The
hyphenated extension form matches `CLAUDE.md` §"Cryptographic
hashes (release + CSAF)".
- **Rolling file logging** — `tracing-appender` writes a daily
`csaf-crud.<date>.log` under the configurable **Log Directory**
(default `./data_log`); stderr output is preserved. `RUST_LOG`
governs both sinks.
- **Audit Log export** — The `/admin/export` page grows an
**Export Audit Log** card that emits the `audit_log` table as
Markdown, CSV, JSON, and SARIF 2.1.0 — each payload ships the
three hash sidecars above. SARIF output is self-validated before
write (version, schema, tool.driver.name, every result has a
ruleId / message.text / recognised level).
- **Reset to defaults** — The `/settings` page carries a confirmation-
guarded **Reset to default settings** button that restores every
field to `Settings::default()` and records a `settings_reset` row
in the audit log.
- **HATEOAS API** — HAL-like `_links` in every response, RFC 9457
Problem Details error format.
- **Menu structure** — CSAF (CRUD), Administration
(Import/Export), Settings, Info (About/License/System/Privacy/
Security).
- **Audit logging** — Every create/update/delete/import/export
recorded with ISO 8601 timestamps.
- **Argon2id** password hashing per RFC 9106.
## Quick start
Two crates ship binaries: `csaf-crud` (the server) and `ndaal-csaf-cli`
(the CLI).
### Install a prebuilt binary with `cargo binstall` (recommended)
[`cargo-binstall`](https://github.com/cargo-bins/cargo-binstall)
downloads the binary published with each release instead of
compiling the dependency tree on your machine — seconds rather
than minutes. Install it once:
```bash
# Linux / macOS
curl -L --proto '=https' --tlsv1.2 -sSf \
https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh \
| bash
# macOS with Homebrew
brew install cargo-binstall
# From source, if you prefer
cargo install cargo-binstall --locked
```
Then install either binary, or both:
```bash
cargo binstall csaf-crud
cargo binstall ndaal-csaf-cli
# Pin a version, or skip the prompt in scripts and CI
cargo binstall csaf-crud@1.4.14
cargo binstall --no-confirm ndaal-csaf-cli
```
Prebuilt archives are published for six targets: `x86_64` and
`aarch64` for `unknown-linux-gnu` and `apple-darwin`, plus
`x86_64-pc-windows-gnu` and `aarch64-pc-windows-msvc`. Binaries
land in `$HOME/.cargo/bin`; make sure it is on your `PATH`.
### From source with `cargo install`
Works on every target Rust supports, including those with no
prebuilt archive, at the cost of compiling the whole dependency
tree. Requires Rust **1.93.0** or newer. No C toolchain,
`pkg-config` or OpenSSL is needed — SQLite is vendored via
`rusqlite`'s bundled feature and TLS is rustls with the `ring`
provider:
```bash
cargo install csaf-crud --locked
cargo install ndaal-csaf-cli --locked
```
`--locked` builds the exact dependency versions the release was
tested against. Without it Cargo re-resolves, which is how a
stale transitive can reappear — see the `generic-array` pin in
[`Cargo.toml`](Cargo.toml).
### Run it
```bash
# Start the server (TLS on 8180 for TCP, 8181 for QUIC)
csaf-crud
# Import, validate, and report
ndaal-csaf-cli import --directory test/csaf
ndaal-csaf-cli validate \
test/csaf/2026/003/ndaal-sa-2026-003.json
ndaal-csaf-cli stats
```
Then open <https://127.0.0.1:8180>. The server generates its own
45-day certificate on first start, so a browser will warn about
the self-signed chain.
### Building the repository itself
Only needed to develop on csaf-crud rather than run it:
```bash
cargo build --release
cargo run --bin csaf-crud
```
## Supported GUI languages
The web interface ships in 49 languages. **English, Deutsch, Français,
Español and Italiano** are maintained and human-authored, pinned to the
front of the language switcher in that order. Every language after those
five is listed alphabetically by its English name and is
**machine-translated, pending native review**; any string not yet
translated falls back to English automatically. The set covers every
official European Union language, the languages official in Norway
(Norwegian Bokmål and Nynorsk, and Northern, Lule and Southern Sámi),
Turkish, the wider European set, and three Asian languages — Chinese
(中文), Hindi (हिन्दी) and Urdu (اردو).
The switcher labels each language by its own native name (endonym) and is
the authoritative list — it is not repeated here. Right-to-left languages
such as Urdu (اردو) render with `<html dir="rtl">`. This is the web UI's
localisation and is separate from this README, which is delivered in five
languages (see the switcher line at the top).
## Keeping the binaries up to date
Both binaries can update themselves in place. The capability is compiled
into every build — it is never behind a cargo feature — and is disabled at
runtime, not at build time.
```bash
# Is there a newer release? Exit 10 = yes, 0 = up to date or unreachable.
csaf-crud --check-update
ndaal-csaf-cli --check-update
# Download, verify and install the newest release.
csaf-crud --self-update
# Refuse in-place updates (for package-managed or locked-down installs).
csaf-crud --self-update --no-self-update # exit 3
CSAF_NO_SELF_UPDATE=1 csaf-crud --self-update # same, via the environment
```
`--check-update` never fails on a network error: an unreachable host, a
malformed response or a rate-limit all report `Unreachable` and exit `0`, so
it is safe in a boot path or a cron job.
| `0` | Up to date, update installed, host unreachable, `--help`, `--version` |
| `1` | `--self-update` failed (no asset for this triple, checksum mismatch, install error) |
| `2` | Argument parse error |
| `3` | `--self-update` refused by policy |
| `10` | `--check-update` found a newer release |
**Trust model.** Downloads are verified against the `SHA256SUMS` manifest
published in the same release. That is **integrity, not authenticity**:
anyone able to rewrite the release archive can rewrite the manifest too. The
artifacts are **not signed**.
## Workspace layout
```text
crates/
├── csaf-models/ # CSAF 2.0/2.1 serde types, SQLite pool,
│ # User, AuditLog, Settings models
├── csaf-core/ # redb storage, validation, sidecar generation,
│ # import/export, configuration
├── csaf-crud/ # Hyper server, HATEOAS API, HTML routes,
│ # router, static files, TLS 1.3 setup
└── csaf-cli/ # clap-based CLI: import, export, validate,
# stats (published as `ndaal-csaf-cli`)
```
## Documentation
- [User Guide](documentation/User_Guide.md) — using the web UI, API,
and CLI.
- [Administrator Guide](documentation/Administrator_Guide.md) —
deployment, configuration, backup and recovery.
- [Troubleshooting Guide](documentation/Troubleshooting_Guide.md) —
diagnosing common failures.
- [Reference](REFERENCE.md) — CLI, configuration, API, and web routes.
- [Repository structure](structure.md) — canonical layout.
- [Changelog](CHANGELOG.md) — release history.
## Testing
```bash
# All tests (unit + integration + crate integration)
cargo test --workspace
# Just integration tests
cargo test --workspace --test csaf_crud_cycle
cargo test --workspace --test test_crate_integrations
# Load test (oha, HTTP/2 keep-alive, --insecure for dev cert)
OHA_REQUESTS=1000 OHA_CONCURRENCY=20 test/loadtest/run.sh
```
### Load-test results — port 8180, HTTP/2 over TLS 1.3
1000 requests per endpoint × 20 concurrent connections,
`oha --http2 --insecure` against the locally-built release
binary on Apple Silicon (MacBook). All seven endpoints hit
**100 % success**:
| `/api/v1` | 12,037 | 0.74 ms |
| `/api/v1/settings` | 11,844 | 0.77 ms |
| `/api/v1/system/health` | 9,070 | 0.96 ms |
| `/api/v1/csaf?page=1&per_page=5` | 8,781 | 1.64 ms |
| `/` (HTML dashboard) | 7,918 | 2.19 ms |
| `/api/v1/audit-log?page=1&per_page=5` | 7,572 | 2.24 ms |
| `/static/img/logo.png` | 3,076 | 1.50 ms |
Port **8181** (HTTP/3 over QUIC) is not exercised by this
load test — `oha`, `h2load`, and Homebrew `curl` on this
platform do not include an HTTP/3 / QUIC client. A dedicated
QUIC-capable tool (e.g. `h2load` built with `nghttp3` +
`ngtcp2`, or `neqo-client`) is needed to benchmark 8181.
Test coverage includes:
- **Unit tests** per crate (193 tests; +15 for SHA-512, log
directory, reset-to-defaults, and the four audit-log export
formats)
- **Integration tests** — 10 end-to-end CRUD cycle tests,
extended with a post-export assertion that all three
hyphenated hash sidecars (`.sha-256`, `.sha-512`,
`.sha3-512`) land on disk and no unhyphenated legacy form
leaks.
- **Crate integration tests** — 129 tests reusing patterns from
redb, rusqlite, sha2/sha3, argon2, serde_json, chrono, regex,
uuid, matchit, rustls/rcgen, sysinfo, and tracing-appender.
- **Fuzzing — 34 targets, four engines, one body.** Every fuzz
body lives in exactly one place: a `fuzz_drive(&[u8])` module
in `fuzz-harness/`. The engine crates are thin wrappers over
it, so a parser change lands once instead of being copied
three times and drifting:
[`fuzz/`](fuzz/) (cargo-fuzz / libFuzzer, the blocking PR
gate), [`hfuzz/`](hfuzz/) (honggfuzz), and
[`afl/`](afl/) (cargo-afl / AFL++) — 34 targets each — plus
[`bolero-harness/`](bolero-harness/), whose 6 harnesses run
under every engine **and** the Kani model checker.
This is additive, not redundant: libFuzzer, AFL++ and
honggfuzz use different mutation and coverage strategies, so
each finds bugs the others miss over the same code. Parser
changes are gated on 300 s per-target in CI.
- **Property tests** — `proptest` properties over the pure
updater logic, the CLI flag contract, and the `csaf-models`
serialisation contract (round-trip, ordering determinism,
never-panic on arbitrary input).
- **Compile-time invariants** — `static_assertions` suites that
fail the **build**, not a test run: `Send`/`Sync` on handles
shared across the async runtime, the error-type contract, and
trait object-safety.
- **Concurrency permutations** — four
[Loom](https://github.com/tokio-rs/loom) models in the
standalone `loom-harness/` crate, re-running each model once
per legal thread interleaving rather than once per OS
schedule.
- **Model checking** — two `kani` proof harnesses over the
path-traversal guard applied to remote-derived asset names.
- **Benchmarking, two kinds, deliberately over the same
functions** — [`callgrind-harness/`](callgrind-harness/)
counts instructions under Valgrind (iai-callgrind, pinned
`=0.16.1`); counts barely vary between runs, so they can gate
a merge on a noisy shared runner.
[`criterion-harness/`](criterion-harness/) measures
wall-clock time (criterion 0.8.2).
Both exist because fewer instructions is not always faster —
a change can cut instruction count and still lose on locality
or branch prediction. Use criterion to **decide** whether an
optimisation worked, callgrind to **gate** against
regression.
Total: 520 tests passing.
Two limits worth stating rather than leaving to be
rediscovered. **AFL++** builds and links but aborts at runtime
on macOS (`Crash reporter detected`) until ReportCrash is
unloaded, which needs `sudo`. **iai-callgrind** requires
Valgrind, which supports neither Windows nor current macOS, so
those benchmarks execute on Linux CI only.
## Quality toolchain
```bash
cargo fmt --all
cargo clippy --workspace --all-targets --all-features -- \
-D warnings
cargo test --workspace
cargo audit
cargo deny check
cargo machete --with-metadata
cargo llvm-cov --lcov --output-path target/coverage/lcov.info \
--workspace --all-features
rust-doctor
cargo kani --workspace --output-format=terse # nightly
cargo hakari verify # workspace-hack feature unification
```
The `cargo kani` (model checking) and `cargo hakari` (workspace-hack
consistency) gates also ship as dedicated runners,
[`tests/scripts/test_cargo_kani.sh`](tests/scripts/test_cargo_kani.sh)
and
[`tests/scripts/test_cargo_hakari.sh`](tests/scripts/test_cargo_hakari.sh),
wired into [`scripts/quality_gates.sh`](scripts/quality_gates.sh).
Concurrency permutation testing has its own runner,
[`tests/scripts/test_loom.sh`](tests/scripts/test_loom.sh):
```bash
bash tests/scripts/test_loom.sh # self-skips without the toolchain
LOOM_MAX_PREEMPTIONS=4 bash tests/scripts/test_loom.sh
```
It drives the standalone `loom-harness/` crate. That crate is
separate on purpose: `--cfg loom` is set through `RUSTFLAGS` and
propagates to **every** crate in the compilation, and `tokio`
gates its whole `net` module behind `#![cfg(not(loom))]` — so an
in-workspace Loom test cannot build at all. Keeping it apart also
keeps the `loom` dependency out of the main `Cargo.lock`, and
therefore out of `cargo-deny`, `cargo-geiger`, `cargo-machete`
and `cargo-vet`.
Linters applied: `cargo clippy` (pedantic + nursery),
`yamllint`, `ryl`, `markdownlint`, `rumdl`, `hadolint`,
`htmlhint`, `oxlint`, `ruff`, `bandit`.
### rust-doctor — workspace health score
```text
┌────────────────────────────────────────────────────────┐
│ rust-doctor │
│ │
│ 100 / 100 Great │
│ │
│ ████████████████████████████████████████ │
│ │
│ Security: 100 │
│ Reliability: 100 │
│ Maintainability: 100 │
│ Performance: 100 │
│ Dependencies: 100 │
│ │
│ ✓ 0 error(s) ✓ 0 warning(s) ℹ 1 info(s) 38 files │
└────────────────────────────────────────────────────────┘
```
Run `rust-doctor` locally (or `rust-doctor --json` for machine
output) to reproduce. The session-level cache lives in each
crate's `.rust-doctor-cache.json`; any regression greater than
5 points vs the previous run blocks the merge. Configuration
is in [`rust-doctor.toml`](rust-doctor.toml); every ignore rule
carries a justification comment referencing the equivalent
`[workspace.lints.clippy]` entry in
[`Cargo.toml`](Cargo.toml).
## Security
- TLS 1.3 only (no TLS 1.2 fallback)
- rustls with ring crypto provider (no OpenSSL)
- Argon2id for password hashing (RFC 9106)
- Parameterized SQL queries throughout
- No unsafe code (`#![deny(unsafe_code)]`)
- SHA-256, SHA-512, and SHA3-512 integrity sidecars — three
orthogonal hash families for defence in depth
- SARIF 2.1.0 audit-log export with built-in schema validation
### Binary hardening
Release builds layer the mitigations from
`skills/rust-hardening` on top of Rust's own guarantees:
| Secure allocator | `mimalloc` in `secure` mode — guard pages, heap randomisation, encrypted free lists |
| Integer overflow | `overflow-checks = true` in `[profile.release]` — a panic, never a silent wraparound |
| Link-time | Full RELRO, PIE, non-executable stack (Linux); Control Flow Guard (Windows MSVC) |
| Build | Fat LTO, one codegen unit, symbols stripped, `panic = "abort"` |
| Reproducibility | Toolchain pinned in `rust-toolchain.toml`; `Cargo.lock` committed |
Two caveats stated plainly, because a mitigation believed to be
active but silently absent is worse than none:
- Cargo does **not** merge `rustflags` across sources. A CI job
that sets `RUSTFLAGS` replaces everything in
`.cargo/config.toml`, so it must repeat those flags in the
same variable.
- These settings are declarations, not proof. Verify the
artefact you actually ship — `checksec`, `file` (expect *pie
executable*), and `readelf -d … | grep BIND_NOW`. "No canary"
is normal for a Rust binary and is not a misconfiguration.
Why a secure allocator at all, given this workspace is
`unsafe_code = "deny"` with no FFI: the protection is not
against our code, which cannot corrupt the heap, but against the
**dependency tree** — every `unsafe` block below us that
`cargo geiger` counts and we do not own.
## Source code
Canonical repository (used by crates.io and `cargo install`):
- <https://gitlab.com/vPierre/ndaal_public_csaf_crud>
Please open issues and merge requests there. No GitHub mirror is
maintained; treat any third-party GitHub copy as untrusted.
## License
Apache-2.0. See SPDX headers in each source file.
## Author
Pierre Gronau · ndaal Gesellschaft für Sicherheit in der
Informationstechnik mbH & Co KG · Cologne