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