Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.
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 todocument.distribution.text, todocument.notes[], or to both — configurable per-deployment. - Dual embedded database —
redbfor 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 matchesCLAUDE.md§"Cryptographic hashes (release + CSAF)". - Rolling file logging —
tracing-appenderwrites a dailycsaf-crud.<date>.logunder the configurable Log Directory (default./data_log); stderr output is preserved.RUST_LOGgoverns both sinks. - Audit Log export — The
/admin/exportpage grows an Export Audit Log card that emits theaudit_logtable 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
/settingspage carries a confirmation- guarded Reset to default settings button that restores every field toSettings::default()and records asettings_resetrow in the audit log. - HATEOAS API — HAL-like
_linksin 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
downloads the binary published with each release instead of
compiling the dependency tree on your machine — seconds rather
than minutes. Install it once:
# Linux / macOS
|
# macOS with Homebrew
# From source, if you prefer
Then install either binary, or both:
# Pin a version, or skip the prompt in scripts and CI
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:
--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.
Run it
# Start the server (TLS on 8180 for TCP, 8181 for QUIC)
# Import, validate, and report
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:
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.
# Is there a newer release? Exit 10 = yes, 0 = up to date or unreachable.
# Download, verify and install the newest release.
# Refuse in-place updates (for package-managed or locked-down installs).
CSAF_NO_SELF_UPDATE=1
--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.
| Exit | Meaning |
|---|---|
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
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 — using the web UI, API, and CLI.
- Administrator Guide — deployment, configuration, backup and recovery.
- Troubleshooting Guide — diagnosing common failures.
- Reference — CLI, configuration, API, and web routes.
- Repository structure — canonical layout.
- Changelog — release history.
Testing
# All tests (unit + integration + crate integration)
# Just integration tests
# Load test (oha, HTTP/2 keep-alive, --insecure for dev cert)
OHA_REQUESTS=1000 OHA_CONCURRENCY=20
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:
| Endpoint | req/s | p95 latency |
|---|---|---|
/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 infuzz-harness/. The engine crates are thin wrappers over it, so a parser change lands once instead of being copied three times and drifting:fuzz/(cargo-fuzz / libFuzzer, the blocking PR gate),hfuzz/(honggfuzz), andafl/(cargo-afl / AFL++) — 34 targets each — plusbolero-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 —
proptestproperties over the pure updater logic, the CLI flag contract, and thecsaf-modelsserialisation contract (round-trip, ordering determinism, never-panic on arbitrary input). -
Compile-time invariants —
static_assertionssuites that fail the build, not a test run:Send/Syncon handles shared across the async runtime, the error-type contract, and trait object-safety. -
Concurrency permutations — four 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
kaniproof harnesses over the path-traversal guard applied to remote-derived asset names. -
Benchmarking, two kinds, deliberately over the same functions —
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/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
The cargo kani (model checking) and cargo hakari (workspace-hack
consistency) gates also ship as dedicated runners,
tests/scripts/test_cargo_kani.sh
and
tests/scripts/test_cargo_hakari.sh,
wired into scripts/quality_gates.sh.
Concurrency permutation testing has its own runner,
tests/scripts/test_loom.sh:
LOOM_MAX_PREEMPTIONS=4
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
┌────────────────────────────────────────────────────────┐
│ 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; every ignore rule
carries a justification comment referencing the equivalent
[workspace.lints.clippy] entry in
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:
| Mitigation | How |
|---|---|
| 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
rustflagsacross sources. A CI job that setsRUSTFLAGSreplaces 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), andreadelf -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):
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