csaf-crud 1.4.14

CSAF 2.0 / 2.1 advisory CRUD server with HATEOAS JSON API and HTML UI (TLS 1.3, HTTP/1.1 + HTTP/2 + HTTP/3)
docs.rs failed to build csaf-crud-1.4.14
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.
Visit the last successful build: csaf-crud-0.3.3

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 databaseredb 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 loggingtracing-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 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
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:

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:

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.

Run it

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

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.

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

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

Testing

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

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 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/ (cargo-fuzz / libFuzzer, the blocking PR gate), hfuzz/ (honggfuzz), and afl/ (cargo-afl / AFL++) — 34 targets each — plus 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 testsproptest 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 invariantsstatic_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 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 functionscallgrind-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

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 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:

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

┌────────────────────────────────────────────────────────┐
│ 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 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):

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