csaf-core 1.4.14

CSAF storage, validation, sidecar generation, import/export
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
<!-- SPDX-License-Identifier: Apache-2.0 -->
<!-- SPDX-FileCopyrightText: 2026 ndaal GmbH & Co KG, Cologne -->

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

| 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

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

| 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/`]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:

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

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