<!-- Copyright (c) 2026 Pierre Gronau, ndaal in Cologne -->
# Changelog
## v1.2.69 — Unreleased
A correctness release. Every item below is a case of the application reporting
a number, or a silence, that was not the truth.
- **Critical Severity showed a permanent dash on a 2.5-million-record corpus.**
The home page had been changed to render "—" rather than a false `0` while
unmeasured, which stopped it lying — but the sweep behind it
(`severity_counts()`) decodes EVERY blob in the store, so it never completed
and the placeholder became permanent. Honest and useless is still broken.
Adds `SEVERITY_COUNTS`, a histogram maintained inside the same write
transaction as the row it describes, for the reason the token index already
gives in situ: an index maintained separately from the write path rots. Reads
are now O(labels); a full scan happens ONCE to backfill an existing database.
Instrumented on both the single-record and the BATCH path — the batch path is
what the feeders use, so a counter correct only in `store_vulnerability()`
would have drifted every import cycle while unit tests stayed green.
- **`/stats` rendered zeros that were really a timeout.** `get_info()` is
O(total DB) and ran inline under the search budget; when the bound tripped the
handler received `None` and printed `0 total, no sources` — indistinguishable
from an empty database. It now reads a background-refreshed cache and renders
"—" with an explanatory notice when nothing has been measured yet.
- **The search budget was too small to be answered.** Raised from 10 s to 300 s
(ceiling 120 s → 600 s). A bound that is routinely exceeded does not fail
loudly: the caller turns the timeout into `None`, which search rendered as "no
results". Note the tradeoff, stated in the code: 300 s exceeds a typical
60 s reverse-proxy `timeout server`, so raise the proxy to match or the client
sees a 504 while the work completes server-side. `VL_SEARCH_BUDGET_SECS`
overrides it without a rebuild.
- **`/about` advertised 614 dependencies against a 604-package `Cargo.lock`,**
and `clippy.toml` allowlisted 9 `windows-*` crates no longer duplicated while
both it and `Cargo.toml` documented 40 duplicates where the real count is 31.
A stale allowlist entry silently suppresses `multiple_crate_versions` for a
crate that could re-fork later.
- **Ansible role defaults were pinned at 1.2.60 — eight releases behind.**
`bump_version.sh` lists them as targets and then silently skips them when the
current value does not match the version being bumped from, so every release
since 2026-07-18 reported success while leaving them untouched.
- **Gate ordering:** `typos` moved from gate 15 to 0b, ahead of `fmt`. typos
rewrites identifiers and comments, so a formatting verdict taken before the
spelling fix is stale. The pre-commit hook already ran typos first; the two
disagreed, which mattered because the hook is optional and CI is not.
- **gitleaks and betterleaks moved out of `--fast`.** Committing the 33,478-entry
fuzz corpus put 45,516 files in every secrets scan, and gitleaks has no
scan-level path exclusion — a `--fast` run wedged for over six hours.
## v1.2.68 — Unreleased
31 commits since the 1.2.67 release: 11 docs, 7 fixes, 4 features, 3 tests,
3 chores, 2 CSAF, 1 dependency bump. One theme runs through all of it — **a
gate that passes without exercising what it claims is worse than no gate.**
The trap catalogue was written down first, and then live instances of every
category turned up in our own tooling.
- **`jq` is now the documented fallback for `jaq`, in all 374 carriers.**
Every bundled gate probed only `jaq`; when it was absent they warned, skipped
the JSON and SARIF derivation, and still exited 0 — and `jaq` is missing from
stock images far more often than `jq`, so the common case on a fresh host was
a green gate that had produced no report at all. The engine now resolves once
into a `local` **inside** `validate_sarif()`, not a global: the sibling bats
contract lifts that function out of the script and runs it standalone, where a
global is unbound under `set -o nounset`. Propagated to every carrier of the
canonical `skills/sarif` snippet, which 418 sibling `.bats` files enforce
byte-identity against; all 366 were byte-identical beforehand, so this was one
exact block replacement, and every changed file was verified to be reproduced
by that swap alone. Measured on jaq 3.1.1 vs jq 1.7.1: `ENGINE -s . a.json
b.json` slurps *per file* under `jaq` and *across* files under `jq`, so
`cat -- a b | ENGINE -s` is the only portable form — now a rule, not a habit.
- **Six silent-pass traps documented in `skills/bash`, each power-checked.**
`errexit` is suppressed in every conditional context and the suppression is
INHERITED by functions called from one; `[[ -x ]]` as a run-precondition
silently skips on a mode-100644 helper (the bug that left 30 gates claiming a
SARIF validation that never ran); zero inputs is a FAILURE, not a skip;
process waiters must separate `pgrep` rc=1 from rc>1; classify by structure,
never by message text; and the jaq/jq engine rule above.
- **The tool-too-old shellcheck probe swept into every sibling (782 files).**
AlmaLinux 8 ships an EPEL ShellCheck predating `-o/--enable`, so `shellcheck
-o all` is a usage error there and every unprobed sibling hard-FAILED on an
environment fact. 795 invocations across 816 tracked `.bats`; 0 left unprobed.
Power-checked both ways — passes on shellcheck 0.11.0, skips against a stub
whose `--help` omits `-o`.
- **A container matrix proving bash scripts work on Linux, not just macOS**
(`skills/bash/scripts/test_bash_in_containers.sh`). Runs `bash -n`,
`shellcheck -o all`, each script's `--version` smoke run AND the sibling
`*.bats` inside Debian 13/14 + AlmaLinux 9/10 (`--full` adds Debian 12,
AlmaLinux 8/9.0/9.6, Leap 15/16 and a negative control). Engine order podman
-> docker -> VM, with docker health-checked rather than merely found on PATH.
The bats suites are never skipped: a missing bats FAILS the image. Measured
across the full matrix — bash 4.4 through 5.3, five distinct bats versions.
- **Fuzz engine parity: all 161 targets now run on libFuzzer, honggfuzz AND
AFL** (was 161/147/147). Fourteen single-engine targets were ported, seven by
extraction into an existing harness and seven by building the missing
`fuzz-harness`/`hfuzz`/`afl` crates for `vl-codec` and `vl-models`. Three are
structure-aware `Arbitrary` targets whose private types had to be exported so
both new engines keep typed inputs instead of collapsing to raw bytes. The
accumulated corpus (33,478 entries) is committed so every engine seeds from it.
- **Every git-based feeder moved to a 3-hour cycle** (19 feeders, from 12 h,
24 h and one 48 h). The clones are incremental, so a shorter cycle costs a
fetch rather than a re-clone, and the startup stagger is unchanged. The 22 OSV
bulk feeders are deliberately NOT included: they fetch an ecosystem `all.zip`
over HTTP, merely reusing the same macro, and carry an explicit operator
directive for their daily cadence.
- **Gate hygiene.** `clippy.toml` allowlisted 9 `windows-*` crates no longer
duplicated in `Cargo.lock`, and both `clippy.toml` and `Cargo.toml` documented
40 duplicates where the real count is 31 — a stale entry silently suppresses
`multiple_crate_versions` for a crate that could re-fork later. `gitleaks` and
`betterleaks` moved out of `--fast`: committing the fuzz corpus put 45,516
files (50% of the tracked tree) in their path, and gitleaks has no scan-level
path exclusion, so a `--fast` run wedged for over six hours. `leaktor` stays,
because `.leaktorignore` excludes from the scan itself.
- **Skills.** Download verification added to `skills/downloads`, `curl`, `wget`
and `aria2c` — TLS authenticates the channel, never the bytes, and aria2c's
`--check-integrity` verifies against the torrent's own hashes, not the
publisher's. `skills/sbom` gained example-corpus and upload-API chapters and
now references all 30 of its scripts (it documented 17 and claimed
"Seventeen"). `skills/rust-cargo-mutants` records that scoping a slow run does
not fix *unviable* mutants — a term of art meaning "failed to build" — and
that a per-module score is not comparable to the 90% gate. New `skills/journald`
verified across a 4-distro container matrix.
## v1.2.66 — Unreleased
## v1.2.65 — Unreleased
## v1.2.64 — Unreleased
- **Two storage bugs fixed, test-first (`crates/vl-core/src/storage.rs`).**
Both were documented as known-unfixed preconditions in
`fuzz_storage_get_last.rs` — a policy violation ("no known bugs in
comments") surfaced by the 2026-08-19 test-sequence audit. (1)
`get_last` computed the pagination offset as `(page - 1) * number` on a
`usize`, so the attacker-reachable `?page=0` panicked in debug builds
and wrapped into an always-empty page in release; the offset now uses
`saturating_sub(1).saturating_mul(number)`, making page 0 serve page 1.
(2) `index_key` / `source_index_key` enforced their non-negative
timestamp precondition only via `debug_assert!`, which release builds
skip while feeders parse timestamps from untrusted feed dates — a
pre-1970 date's two's-complement sign bit would sort its key AFTER
every non-negative one, silently inverting the chronological index.
Negative timestamps now CLAMP to the epoch. Four new tests (unit +
a whole-negative-domain proptest,
`negative_timestamps_clamp_to_epoch_key`) were written first and
watched fail on the old code.
- **tantivy adoption record, retrofitted.** `tantivy 0.26.1` landed in
`998ef3342` with no CHANGELOG entry and no manifest rationale — both
required by `skills/rust-library`. The workspace manifest now records
why tantivy (embedded index, no external search service to deploy or
secure; replaces the O(n) `embedded_search` scan that timed out against
a 400k-record store) and why its exact feature set; the adoption-test
matrix for its untrusted-query surface follows in this audit cycle.
- **Dependency hygiene from the adoption audit.** New workspace
dev-dependencies for the vl-cli test surface, registry-resolved on
2026-08-19 (`trycmd 1.2.1`, `snapbox 1.2.2`, `assert_cmd 2.2.2`,
`predicates 3.1.4`); `insta` + `static_assertions` extended to every
crate for the new snapshot and compile-time-invariant suites. Four
declared-but-unused dependencies (`git2`, `secrecy`, `totp-rs`,
`zipsign-api`) are KEPT by recorded decision with honest
"deliberately kept while unused" rationale comments at their
dependency lines — the two `cargo-machete` ignore-list comments that
falsely claimed macro use for them are corrected. The orphan
`tests/integration/` tree (never compiled — it sat under the virtual
workspace manifest — and carrying AGPL-3.0 headers in an Apache-2.0
repo) is deleted.
- **Binary hardening (`skills/rust-hardening`): secure allocator, overflow
checks, and ELF linker hardening.** `mimalloc` in **`secure`** mode (guard
pages, heap randomisation, double-free detection) is now the
`#[global_allocator]` of both shipped binaries — installed in `vl-web` /
`vl-cli` `main.rs` rather than in a library, so the `vl-*` crates stay
allocator-agnostic for downstream consumers. `[profile.release]` gains
`overflow-checks = true`, so an integer overflow in a feed parser aborts
instead of wrapping silently in the one build that faces untrusted input
(dev/test already ran with checks on by default, so release now merely
matches what every `cargo test` has always exercised). `.cargo/config.toml`
attaches full RELRO + NX (`-Wl,-z,relro,-z,now,-z,noexecstack`) **per Linux
target** — a global `rustflags` would break macOS builds, where ld64 rejects
`-z` outright; PIE is omitted because rustc already defaults to it on
`*-unknown-linux-gnu`. Six invariants are pinned by
`crates/vl-web/tests/test_hardening_profile.rs` (gate 3), because
configuration has no compiler to catch its deletion.
**`panic = "abort"` is deliberately NOT adopted**, and the rejection is
enforced by a negative test rather than left as a silent omission: `vl-web`
serves each connection in its own tokio task on a hand-rolled hyper stack
with **no catch-panic layer**, so aborting would convert any reachable
handler panic into a remote denial of service. Revisit only together with a
catch-panic boundary around every request handler. `mimalloc 0.1.52` and
`libmimalloc-sys 0.1.49` are carried as **unreviewed, time-boxed**
`cargo-vet` exemptions with `notes` demanding human review before release —
not `certify`, which would falsely assert that someone read the 51 050 lines
of vendored C.
- **New CSS quality gate: stylelint (173, `--full`), with a bundled hardened
config.** `test_css_with_stylelint.sh` lints every in-scope `*.css` / `*.scss`
/ `*.less` file (excluding `.git`/`skills`/`documentation`/ `nuclei-templates`
and `*.min.*`) and emits jaq-derived, schema-validated SARIF 2.1.0 under
`documentation/css/stylelint/`. Its `--fix` step is **non-destructive** (runs
on copies; records before/after + a per-file diff; the repo's stylesheets are
never mutated). New skill `stylelint` bundles the canonical ndaal hardened
`.stylelintrc.json` (123 rules, JSON-not-JS, no extends/plugins,
`defaultSeverity: error`, security deny-lists) under `assets/` + a full
rationale in `reference/hardened-stylelint-config.md`; the gate prefers a repo
config, else this bundled policy. Wired into `quality_gates.sh` (ALL_GATES +
LIVE_GATES + `gate_stylelint` + dispatch).
- **Three new canonical security / quality gates: blint, cargo-deadlinks, vnu.**
`test_blint.sh` (170, LIVE) runs the OWASP blint binary linter over the built
`vl-*` binaries (NX / PIE / RELRO / canary / code-signing) ->
`documentation/security/blint/`; `test_cargo_deadlinks.sh` (171, `--fast`)
runs cargo-deadlinks over the `cargo doc` output for broken intra-doc links ->
`documentation/rust/cargo_deadlinks/`; `test_html_with_vnu.sh` (172, `--fast`)
runs the Nu Html Checker (v.Nu) over repo `*.html`/`*.htm` for W3C HTML
conformance -> `documentation/html/vnu/`. vnu is a Java tool pushing untrusted
HTML through Jing (RELAX NG) + Saxon + JAXP, so the gate hardens the JVM XML
pipeline (`-Xss1m` + JAXP XXE / entity-expansion limits via `VNU_JVM_OPTIONS`)
and strips the `JAVA_TOOL_OPTIONS` banner from vnu's stderr report. All three
follow the canonical markuplint gate pattern (`-h/-V/-v/-q/-l/-t`,
schema-validated SARIF via `skills/sarif`, `NOT_APPLICABLE` autofix record,
idempotent, sibling `.bats` + README, self-skip) and are wired into
`quality_gates.sh` (ALL_GATES + LIVE_GATES[blint] + `gate_*` + dispatch). New
skill `html-vnu` bundles the vnu gate + its Java / schema / JVM-hardening
internals.
- **Four per-tool deductive-verification gates split out of the combined probe,
each with a dedicated skill.** The single `test_deductive_verification.sh`
probe is now complemented by four canonical, self-contained gate runners —
`test_creusot.sh` (Creusot, WhyML/Why3 + SMT), `test_prusti.sh` (Prusti, Viper
\+ SMT), `test_verus.sh` (Verus, `verus!{}` + Z3), and `test_flux.sh` (Flux,
refinement types) — built on the canonical markuplint gate pattern:
`-h/-V/-v/-q/-l/-t` CLI, all-format output (`.txt`/`.json` + a jaq-derived
SARIF 2.1.0 validated via `skills/sarif`), a `NOT_APPLICABLE` autofix record,
idempotent timestamp-free artefacts under `documentation/rust/<tool>/`, a
sibling `.bats` (35 assertions) + shdoc `README.md`, and a green self-skip
when the toolchain is absent. Wired into `quality_gates.sh --full` as LIVE
gates 166–169. New skills `rust-cargo-creusot`, `rust-cargo-prusti`,
`rust-cargo-verus`, and `rust-cargo-flux` bundle each runner and are
cross-referenced from `rust-testing` and `rust-verification`.
- **`skills/documentation` gains an optional TOGAF enterprise-architecture
deliverable.** A trilingual (en/de/fr) TOGAF document set under
`documentation/togaf/` — one file per ADM phase plus Architecture Repository /
Enterprise Continuum / content metamodel — with dual-format (Mermaid +
PlantUML → SVG + PNG) graphics for the four canonical diagrams, authored per
`skills/togaf`.
- **CWE/CAPEC attack detail pages 404'd on an unresolved Git-LFS pointer stub —
now a loud, actionable error + a catalog-integrity Playwright guard.**
`GET /attacks/cwe/cwe/{id}` returned 404 for every CWE (e.g. CWE-89) because
`dumps_archive/cwec.7z` in a checkout without `git lfs pull` is a ~130-byte
pointer stub (`version https://git-lfs.github.com/…`), so the CWE feeder's 7z
decompress failed with a cryptic `BadSignature([118,101,114,…])` (the ASCII of
"versio…") and left the `CWE_DEFINITIONS` table empty. The route + handler
were correct. Fixes: (1) `safe_decompress_7z` now detects the LFS pointer
preamble and fails loud with "run `git lfs pull`" instead of the cryptic
`BadSignature` (TDD: `rejects_git_lfs_pointer_stub_with_clear_error`,
RED→GREEN); (2) a new Playwright spec
`tests/pages/cwe-detail-integrity.spec.ts` asserts the foundational CWEs
(79/89/125) resolve *by number* with id-form equivalence, a clean 404 for
unknown ids, 400 for malformed, the 308 legacy redirect, the raw-definition
API cross-check, and a console-error guard — closing the gap where the
click-through test SKIPPED on an empty catalog and let this ship. Verified
live (count=969 CWEs, 573 CAPECs imported).
- **sqlmap gate hardened (no more false-green) + skill converted to a folder.**
The `test/sqlmap/run.sh` SQL-injection sweep was silently false-green against
the TLS-1.3-only `vl-web` listener: sqlmap's HTTPS handler negotiates only up
to TLS 1.2, so it printed "can't establish SSL connection", skipped every
target — yet exited 0, and the runner reported "clean" without testing
anything. Three fixes: (1) an **auto TLS-1.3 bridge** (a mitmdump reverse
proxy, started once per origin, so sqlmap speaks plain HTTP while mitmproxy
performs the TLS 1.3 handshake upstream); (2) a **fail-loud classifier**
(`INJECTION` / `TESTED_CLEAN` / `NOT_TESTED`) that counts any run with no
positive proof it probed the endpoint as a runner failure, never as clean; (3)
**glob-safe target parsing** — `SQLMAP_TARGETS` URLs contain `?`, which
`shopt -s nullglob` was expanding to nothing (zero targets); fixed with
`set -f` + a widened `IFS`. The skill `sqlmap-database-pentesting.md` became
the folder `skills/sqlmap-database-pentesting/` — `SKILL.md` plus the bundled
runner and its bats sibling under `assets/`, and the TLS-1.3-bridge /
false-green / glob-safe documentation. CLAUDE.md ref updated.
## v1.2.62 — Unreleased
- **CSAF egress-guard hardening — NAT64/IPv4-compat classifier fix +
connect-time DNS-rebinding guard (ndaal-sa-2026-311)** — closed two SSRF gaps
in the CSAF feeder's egress guard. (1) `is_internal_ip` classified RFC 6052
NAT64 (`64:ff9b::/96`) and IPv4-compatible (`::/96`) addresses that embed an
internal IPv4 as *public*, so an internal target could be laundered through a
v6 encoding — now blocked (`to_ipv4` + a NAT64 helper). (2)
`ensure_public_https` checked the host only at resolve time, and the advisory
/ sidecar / aggregator fetches re-resolved on the shared client with no
re-check — a DNS-rebinding TOCTOU. `hardened_csaf_client` now installs
`SsrfGuardResolver` (a `reqwest::dns::Resolve`) that re-runs the
internal-address screen at CONNECT time on every request, closing the TOCTOU
and covering all three previously-unguarded fetch sites at once. Adds
exhaustive egress-guard tests (metadata v4/v6, NAT64/mapped/compat proptest,
the previously-untested DNS-resolution arm, non-https sub-schemes), a
`screen_resolved_addrs` unit suite (empty / any-internal / all-public), and an
end-to-end test asserting the resolver refuses a loopback-resolving host at
connect.
- **Version bump 1.2.61 -> 1.2.62**: workspace `Cargo.toml` + `Cargo.lock` (7
crates: vl-core, vl-models, vl-feeders, vl-web, vl-cli, vl-codec, vl-updater),
the Bruno `app_version`, the Ansible role defaults / playbook / install tasks,
and the macOS installer `SCRIPT_VERSION`. Opens the 1.2.62 development cycle.
- **Dependency currency (patch/minor, lock-only)**: `cargo update` bumped
`serde` / `serde_core` / `serde_derive` 1.0.228 → 1.0.229, `tokio` 1.53.0 →
1.53.1, `time` 0.3.53 → 0.3.54 (+ `time-macros`), `clap` 4.6.2 → 4.6.4 (+
`clap_derive`), `proc-macro2` 1.0.106 → 1.0.107, `zerocopy` 0.8.54 → 0.8.55 (+
`zerocopy-derive`), and `cc` 1.2.67 → 1.3.0 — all within the existing caret
requirements (no manifest change). `clap_derive` 4.6.4 pulls `syn` 3.0.2,
adding a `syn` 2+3 duplicate (one of 33 transitional duplicate-version crates;
`multiple-versions = "warn"`, so no gate impact).
`cargo check --workspace --all-features` clean.
## v1.2.61 — Unreleased
- **Boot-collapse fix — homepage no longer runs an O(corpus) scan
(ndaal-sa-2026-341, RCA C1)** — the `GET /` handler used to call
`VulnStorage::get_info()`, whose per-source `SOURCE_INDEX` walk plus an
`ENRICHMENT`-table Brotli-decode backfill is O(total-rows), purely to read the
two scalars the homepage renders (total count + distinct source count). On a
large corpus, under the all-at-once boot feeder burst, that scan kept the
blocking task busy past HAProxy's 50 s `timeout server`, producing the
observed `504 sH--` cascade on `/` while cheap routes (`/recent/partial`)
stayed fast. New `VulnStorage::home_summary()` returns both scalars via redb's
O(1) `Table::len()`, and the handler is rewired to it; `get_info()` is
unchanged for `/stats`. Adds a storage test pinning `home_summary` as
behaviourally identical to `get_info`.
- **Faster startup — `seed_if_empty` empty-check is now O(1) (RCA C5)** — the
boot-time "is the DB already seeded?" check called the same O(corpus)
`get_info()` scan just to test `total > 0`, adding ~1.5 min to every boot's
pre-listen window (widening the startup 503 gap). It now uses the O(1)
`count_index_entries()` (`Table::len()`). Adds a seed idempotency test (empty
→ seeded; populated → left untouched). Note: the deployed
`/api/v1/system/health` endpoint is already O(1) (`check_storage_up` is a
`const fn`), so HAProxy health checks should target it, not `/`.
- **CWE/CAPEC offline archives resolved at runtime (ndaal-sa-2026-342, RCA C8)**
— the CWE and CAPEC catalog feeders read `dumps_archive/cwec.7z` and
`capec.7z` from the compile-time `env!("CARGO_MANIFEST_DIR")`, a build-host
path that is absent on any cross-built deployment (a Linux binary built from a
macOS worktree), so both imports failed and their enrichment was permanently
missing on the affected host. A new `resolve_dumps_archive_dir()` now resolves
the directory at runtime — first existing of `$VL_DUMPS_ARCHIVE_DIR`,
`<home_dir>/dumps_archive`, then the in-tree build path (dev fallback) — via a
pure, unit-tested `first_existing_dir()` helper. Source-tree dev runs are
unchanged; binary deployments place the archives beside the data dir or set
the env var. The network-first GSD/Moksha fallbacks share the pattern and are
tracked for the same treatment (they do not hard-fail when the archive is
absent).
- **Codec decode cap is now operator-tunable (ndaal-sa-2026-343, RCA C6)** — the
shared `vl-codec` rejected any Brotli frame decompressing past a HARD-CODED 64
MiB cap, so a single over-enriched record hard-failed the CVE-vs-GitHub
dashboard aggregation
(`decompressed output exceeds the 67108864-byte safety cap`). The cap is now
set from `VL_CODEC_MAX_DECODE_BYTES` (default 64 MiB) via a pure, unit-tested
`resolve_max_output()` that clamps any parsed value to `[1 MiB, 1 GiB]` — so
an operator can raise it for a legitimately large record while the
decompression-bomb floor/ceiling always hold. Bounding merged-record growth so
no single record exceeds the cap is tracked as a follow-up.
- **Vuln-detail decode offloaded to the blocking pool (ndaal-sa-2026-344, RCA
C7)** — the `GET /vulnerability/{id}` handler ran the synchronous
`load_vulnerability_view` (redb read + Brotli decode + JSON parse) inline on a
tokio async worker, blocking it for the whole decode of a large detail blob.
It now runs inside `tokio::task::spawn_blocking` (the same offload the `/` and
`/search` paths use), with a graceful task-join fallback. Defense-in-depth
(this path was not the observed 504 source); behaviour-preserving and covered
by the existing vl-web handler suite. The two dashboard callers of
`load_vulnerability_view` share the pattern and are tracked for the same
treatment.
- **API test suite hardened; enrichment unknown-source 400 → RFC 9457
problem+json** — every previously-red or silently-skipped Bruno collection now
passes against BOTH an empty and a populated store, and the CWE-404 regression
is covered end-to-end (`cwe/08–14` + `test_cwe_routes.rs`). One
parser-rejected `.bru` (`debian/10`, a `//` comment inside a
`vars:pre-request` block) that had been silently skipped on every run is
fixed. The per-source enrichment handler now emits an
`application/problem+json` 400 (code `unknown_source`) for an unknown source
slug instead of a bare `{"error":…}` that reflected the user-controlled slug —
matching the sibling dashboard rejections and closing a reflected-input
footgun. RED→GREEN (`test_enrichment_routes.rs`); clippy `-D warnings` clean.
- **Per-call search budget is now operator-tunable** — `SEARCH_BUDGET` (the
wall-clock cap shared by the Meilisearch and embedded-scan search paths) is
resolved once from `VL_SEARCH_BUDGET_SECS` (default 10 s, clamped `[1, 120]`)
instead of a hard-coded constant, so an operator can widen it for a slow
back-end without a rebuild. Pure resolver unit-tested (RED→GREEN).
- **Loom gate no longer hangs** — `tests/scripts/test_loom.sh` now exports
`LOOM_MAX_PREEMPTIONS=3` (overridable). Loom's own default is UNBOUNDED, so
the 3-thread CSAF-permit model explored a factorial state space and ran for
> 70 min; the bound keeps the gate ~30 s while still catching essentially
> every real interleaving bug. `skills/rust-loom` corrected accordingly.
- **Brotli interop with `sqlite-compressions` pinned** — a golden-vector
`vl-codec` test decodes the exact byte stream the `sqlite-compressions`
crate's `brotli()` SQL function emits (`8B0580…03` = "Hello world!"),
confirming a `TAG_BROTLI` payload is a raw Brotli stream byte-format-identical
to that crate's output, so a future `sqlite-compressions`-backed store stays
lossless.
- **Meilisearch install scripts** — `scripts/install_meilisearch_on_macos.sh`
(Homebrew, else a pinned + SHA-256-verified GitHub-release binary) and
`scripts/install_meilisearch_on_debian.sh` (Debian 12/13/14, pinned +
SHA-256-verified binary; the unverified `apt.fury.io` repo is deliberately
rejected). Both follow the canonical bash contract with sibling bats + README;
the Debian installer is verified end-to-end on a real Debian 13 Vagrant box
(download → SHA-256 verify → install → `meilisearch --version`).
## v1.2.60 — Unreleased
- **Three-engine fuzzing over a single-source harness** — every fuzz body now
lives once in a per-crate `fuzz-harness/` crate
(`crates/{vl-core,vl-web,vl-feeders}/fuzz-harness/`, 124 modules) that BOTH
the libFuzzer `fuzz/` and honggfuzz `hfuzz/` targets call as a thin
`<mod>::fuzz_drive(data)` wrapper — a parser change lands in ONE place for all
engines. Adds full **honggfuzz** parity (152 targets across the three `hfuzz/`
crates; `hongg` builds on Linux + macOS) and **test-fuzz** as the third engine
(124 `tf_<mod>(Vec<u8>)` wrappers, `test-fuzz` + `serde` as dev-deps with
`#[cfg_attr(test, …)]` so the fuzz builds are untouched). The 28 vl-feeders
feeders that already exposed an in-module `fuzz_drive` keep sourcing it there.
Build-verified per crate (all three `hfuzz` crates + a representative
libFuzzer target via `cargo +nightly fuzz build`).
- **VulnCheck Community KEV feeder** — a new `vulncheck_kev` feeder pulls the
VulnCheck KEV catalogue (a CISA-KEV superset with extra VulnCheck exploitation
evidence) via the authenticated bulk **backup** endpoint: a Bearer call
returns a short-lived pre-signed ZIP URL plus a SHA-256, which the feeder
verifies before unpacking (the queryable `/v3/index/` endpoint caps pagination
and cannot return the full ~5045-entry catalogue). Surfaced at
`/kev?catalog=vulncheck` immediately left of ndaal KEV and as the
`vulncheck-kev` vendor dashboard. Requires an operator API token stored
encrypted at rest (Argon2id key + ChaCha20-Poly1305) via
`vl-cli set-secret --vulncheck-kev` and `VL_SECRET_PASSPHRASE` at startup; it
**self-skips cleanly** when no token is configured. Verified end-to-end
against the live API (ignore-gated `live_fetch_imports_real_catalog`); adds a
`fuzz_vulncheck_kev` cargo-fuzz target over the envelope/array/ZIP parsers.
- **Brotli compression enabled by default at the library layer** — the redb
`VulnStorage` and SQLite `DbPool` constructors now default to `compress: true`
(previously `false`): freshly written blob values are Brotli-framed via
`vl-codec` above the 256-byte threshold, while smaller values and every
pre-existing row stay verbatim. Reads auto-detect the frame tag, so the
byte-identical pre-compression format still decodes unchanged. Opt out per
store with `with_compression(false)`. This aligns the library defaults with
vl-web, which already forces compression on (opt-out via
`VL_DISABLE_COMPRESSION`); two SQLite compression integration tests are pinned
to `with_compression(false)` so the verbatim path stays exercised.
- **Property tests — storage compression + route key safety** — new
`proptest_storage_compression.rs` round-trips arbitrary vulnerability JSON
through the redb store for both compress flags and asserts the toggle is
logically identical (512 cases each). The `cve_forecast` and `cve_icu` route
modules gain proptests proving every allow-listed key and every derived lookup
key is path-safe — never containing `/`, `\`, `..`, or a NUL byte.
- **Test + gate fixes** — aligned the `vl-core` config default-listen-IP
assertions to the hardened `127.0.0.1` default; restored the wrongly-pruned
`documentation/sql/sample_queries.sql` fixture and replaced it with a
schema-valid 10-query corpus (explicit column lists, filters on indexed
columns only) so the sql-query-analyzer gate reports "no issues"; and made
that gate's schema tempfile `mktemp` template macOS-safe (trailing `X`es) so
back-to-back runs no longer collide with "File exists".
- **Version bump 1.2.59 → 1.2.60**: workspace `Cargo.toml` + `Cargo.lock` (7
crates: vl-core, vl-models, vl-feeders, vl-web, vl-cli, vl-updater, vl-codec),
the two Ansible role defaults, and — with this changelog update — the README /
LIESMICH / LISEZMOI version banners (1.2.56 → 1.2.60). Opens the 1.2.60
development cycle.
## v1.2.59 — Unreleased
- **Documentation — script inventory + wiki** — regenerated the Administrator
Guide "Bash Script Inventory" chapter from the on-disk script headers (287
first-party committed scripts, up from 69) via the new self-maintaining
`scripts/generate_admin_guide_script_inventory.py` (ruff + bandit clean;
splices the chapter in place). Refreshed the `documentation/wiki/`
quality-gates concept page (gate count ~150 → ~190 and the perf/quality
false-green-gate fix). Fixed two false-green gates in the process:
`test_cargo_perf.sh` (invalid `cargo perf check` invocation → real
`cargo-perf` run with `--fail-on`) and `test_cargo_quality.sh` (rewritten for
the real read-only `cargo qual` tool, informational by default with
`QUAL_STRICT=1` to gate).
- **vl-web reverse-proxy (HAProxy) support** — the listener now loads an
operator CA certificate from `--tls-cert` / `--tls-key` (env `VL_TLS_CERT` /
`VL_TLS_KEY`), falling back to the self-signed loopback cert when unset; the
resolved certificate drives BOTH the TCP (h2/http1.1) and QUIC (h3) listeners.
The default bind is now `127.0.0.1`, and a non-loopback bind (`0.0.0.0` / `::`
/ a public IP) requires `--allow-non-loopback` / `VL_ALLOW_NON_LOOPBACK`
(vl-web has no built-in auth in front of its listener). New `tls.rs` +
`net_bind.rs` library modules with 11 unit/proptest cases and a `fuzz_tls_pem`
cargo-fuzz target; the Administrator Guide gains a "Reverse proxy (HAProxy)"
section with a worked `haproxy.cfg`. Modelled on grundschutz-oscal-viewer.
- **Ansible roles** — emit `VL_ALLOW_NON_LOOPBACK` for the default `::` bind
under the new guard and default the operator TLS cert/key paths to empty
(self-sign); corrected the project repo references from `github.com/ndaal-org`
to `gitlab.com/vPierre/ndaal_public_nvulnlookup` (binary download URLs,
systemd `Documentation=`, and the `release.toml` CHANGELOG compare-link
template).
- **`fetch_dumps.sh`** — added the `osv_ubuntu.ndjson` OSV ecosystem, new on the
live CIRCL dump index since the 2026-07-11 reconciliation (92 dumps).
- **`uncompress_dumps.sh`** (new) — restore + three-algorithm checksum-validate
the compressed dumps from `dumps_archive/` into `dumps_restore/`, with
`-h/-V/-v/-q/-l/-t` options, a sibling bats suite, and a README.
## v1.2.58 — Unreleased
- **Anti-DNS-rebinding Host allowlist (`--allow-host` / `VL_ALLOWED_HOSTS`)** —
vl-web now pins every request's `Host` (HTTP/1) / `:authority` (HTTP/2+3) at
the router chokepoint (`SharedRouter::handle_parts`, covering both the TCP and
QUIC listeners) and returns `403 host not allowed` for a foreign authority.
The bind authority + its loopback aliases (`127.0.0.1` / `localhost` /
`[::1]`) on the bound port are always accepted; the `--allow-host` CLI flag
(repeatable and/or comma-separated) and the `VL_ALLOWED_HOSTS` env var add the
real hostnames a reverse-proxy or named deployment is reached by. Ships 8 unit
tests + 4 proptest properties + a `fuzz_host_guard` cargo-fuzz target;
modelled on the grundschutz-oscal-viewer v1.6.46 guard. Documented by advisory
`ndaal-sa-2026-298` (CVSS 3.1 LOW).
- **Dependency security fixes + currency bumps** — `cargo audit` cleared two
RUSTSEC advisories: `crossbeam-epoch` 0.9.18 -> 0.9.20 (RUSTSEC-2026-0204,
invalid pointer dereference in the `fmt::Pointer` impl; transitive via rayon)
and `anyhow` 1.0.102 -> 1.0.103 (RUSTSEC-2026-0190, unsoundness in
`Error::downcast_mut()`) — both RustSec advisories publish no upstream CVSS.
Plus routine currency patches `rustls` 0.23.41 -> 0.23.42, `mio` 1.2.1 ->
1.2.2, and `self_update` 1.0.0-rc.2 -> 1.0.0-rc.5 (latest release candidate;
Cargo.toml pin). Documented by advisories `ndaal-sa-2026-293` through `297`
(all CVSS 0.0/NONE — the two RUSTSEC fixes are applied and `cargo audit` is
clean).
- **Version bump 1.2.57 -> 1.2.58**: workspace `Cargo.toml` + `Cargo.lock` (5
crates: vl-core, vl-models, vl-feeders, vl-web, vl-cli), the Bruno
`app_version`, the Ansible role defaults / playbook / install tasks, the macOS
installer `SCRIPT_VERSION`, and the README / LIESMICH / LISEZMOI banners.
Opens the 1.2.58 development cycle.
## v1.2.57 — Unreleased
- **Storage compression (Brotli via `vl-codec`), enabled by default** — a new
`vl-codec` crate (safe, framed Brotli codec with a 64 MiB decompression-bomb
cap) is wired into both stores: the redb `VulnStorage` (encode-on-write,
decode-auto-detect on read — the existing store stays readable and only new
writes compress) and the SQLite `vl-models` `bundle` / `code` / `comment`
columns. Secret columns (`pwdhash` / `secret_token` / `apikey` / `feedkey`)
are NEVER compressed (CRIME/BREACH guard, enforced by tests). Compression is
ON by default; disable with `VL_DISABLE_COMPRESSION=1` (also `true` / `yes` /
`on`), matching the `VL_DB_RECREATE_ON_CORRUPTION` env-toggle convention.
- **Compression test coverage** — `vl-codec` ships its own proptest +
reference-vector interop + two cargo-fuzz targets; the `vl-models`
`blob_codec` integration layer adds `tests/proptest_blob_codec.rs` (5
properties: secrets never compress, `Blob` ⇒ eligible-column **and** lossless
round-trip, non-eligible ⇒ `Text`, `encode_opt(None)` ⇒ NULL, totality) and a
new standalone `crates/vl-models/fuzz/` crate (`fuzz_blob_codec_column`, 30 k
runs crash-free).
- **Dependency patch bumps** — `rustls` 0.23.40 → 0.23.41, `time` 0.3.47 →
0.3.53 (+ `num-conv` / `time-core` / `time-macros`), `getrandom` 0.4.2 → 0.4.3
(semver-compatible, Cargo.lock-only, no source change). No RUSTSEC/CVE applies
(CVSS 0.0/NONE); documented by advisories `ndaal-sa-2026-279` / `280` / `281`.
- **Loom + Goose test sequences added** (two standalone crates, each its own
empty-`[workspace]` root like `crates/*/fuzz/`, so their `loom` / goose
`reqwest` dependency trees never reach `cargo-deny` / `cargo-geiger` /
`cargo-machete` / `workspace-hack`):
- `loom-harness/` — a [Loom](https://github.com/tokio-rs/loom)
concurrency-permutation harness that MODELS the real synchronization of the
vl-\* crates over every thread interleaving: the `DbPool`
`Mutex<Connection>` (`vl-models/src/db.rs`), the two-phase `with_conn`
checkout, the CSAF-download `Semaphore`
(`vl-feeders/src/csaf_downloader.rs`), and the `FEEDER_STOP` shutdown latch
(`vl-web/src/main.rs`). Run via `tests/scripts/test_loom.sh` (self-skips if
absent).
- `loadtest/` — a [Goose](https://book.goose.rs/) HTTP load test (the Rust
analog of the bash `oha` runner at `test/loadtest/run.sh`) driving vl-web's
read-only GET surface + the ~26 seeded CVE/GHSA ids, gated on the request
error rate. Opt-in `Mutate` scenario (`VL_LOADTEST_INCLUDE_MUTATE=1`) for
`POST /api/v1/sighting` against a throwaway instance. Run via
`tests/scripts/test_goose.sh` (self-skips without a running vl-web).
- Both wired into `scripts/run_all_tests.sh` Phase 8 (`RUN_LOOM=1` /
`RUN_GOOSE=1`), documented as `Test_Sequences.md` §31/§32 (+ the §26 index),
and added to the root `Cargo.toml` `exclude`. Each gate ships the full `.sh`
\+ `.bats` + `.README.md` trio (both `.bats` pass 39/39; the loom one
compiles + runs the harness under `--cfg loom`) and commits its `Cargo.lock`
per the standalone-crate convention.
- **Version bump 1.2.56 -> 1.2.57**: workspace `Cargo.toml` + `Cargo.lock` (5
crates: vl-core, vl-models, vl-feeders, vl-web, vl-cli), the Bruno
`app_version`, the Ansible role defaults / playbook / install tasks, the macOS
installer `SCRIPT_VERSION`, and the README / LIESMICH / LISEZMOI banners.
Opens the 1.2.57 development cycle.
- **All 6 workspace crates made publishable to crates.io**: `publish = false` ->
`true` on `vl-core`, `vl-models`, `vl-feeders`, `vl-web`, `vl-cli`,
`vl-updater`. Required a `version` alongside every internal `path =`
dependency in `[workspace.dependencies]` (crates.io rejects unversioned path
deps), switched `vl-feeders`' direct `vl-models` dependency to
`{ workspace = true }` for the same reason, and added a previously-missing
`repository.workspace = true` to every crate's `[package]` table (also
required for `cargo-binstall`'s `{ repo }` template variable). Verified with
`cargo check --workspace --all-features` (clean) and a real
`cargo-binstall --dry-run` against both binary crates.
- **`cargo-binstall` support for `vl-web` / `vl-cli`**: added
`[package.metadata.binstall]` pointing at the existing
`nvulnlookup-<version>-<triple>.tar.gz` release archive (shared by both
binaries, not one archive per crate — the pkg-url uses the literal
`nvulnlookup` name rather than `{ name }`). `cargo binstall vl-web` /
`cargo binstall vl-cli` now resolve to the real GitLab release download URL
once a release is cut; falls back to `cargo install` from crates.io otherwise.
- **`scripts/bump_version.sh` gained a new declaration site**: the four
`workspace.dependencies` internal crate version pins added above are now
bumped automatically alongside the `[workspace.package]` version, with a new
`bump_version.bats` regression test covering it (24 assertions, up from 23).
- Known blocker (not yet resolved): a real `cargo publish` cannot run from this
session's sandboxed git worktree — its raw `.git/worktrees/<name>/HEAD` file
contains `ref: refs/heads/.invalid`, which `cargo`'s git library rejects
during the pre-publish git-status check
(`Reference name cannot start with a dot`), even though the `git` CLI itself
resolves the branch fine. Needs a normal checkout or CI to actually publish.
## v1.2.56 — Unreleased
- **Version bump 0.1.55 -> 1.2.56**: workspace `Cargo.toml` + `Cargo.lock` (5
crates: vl-core, vl-models, vl-feeders, vl-web, vl-cli) and the Bruno
`app_version`. Opens the 1.2.56 development cycle.
- **New dashboard: CVE Forecast (`/cve-forecast`)** — a
`cve_forecast_enrichment` feeder mirrors RogoLabs'
[CVEForecast.org](https://cveforecast.org) daily model-prediction JSON (hourly
refresh) into a new `CVE_FORECAST` redb table; the dashboard renders the
per-CNA forecast breakdown client-side from the pre-aggregated JSON, matching
the existing CNA Scorecard mirror pattern.
- **New dashboard: cve.icu Analytics (`/cve-icu`)** — a `cve_icu_enrichment`
feeder mirrors all 46 of [cve.icu](https://cve.icu)'s per-document JSON files
(growth / scoring / CWE / CPE / calendar analyses, per-year and summary
documents; 6-hour refresh) into a new `CVE_ICU` redb table, each document
stored independently so one upstream fetch failure doesn't block the others.
- **SSVC decision-tree solver** — `SsvcDecision::recommended_action()`
(`routes/cve5.rs`) transcribes the CISA-Coordinator v2.0.3 36-row decision
table (`mission_wellbeing()` + `ssvc_decision_table()`) verbatim, matched
case-insensitively against a CVE's parsed SSVC decision points, and renders a
"Recommended action" card (Track / Track\* / Attend / Act) on the CVE detail
page. `SsvcOutcome::alert_class()` was extracted as a small `const fn` to
replace a multi-line conditional `class="alert …"` attribute that was
confusing htmlhint's Askama-unaware parser.
- **Navbar reorganised**: the standalone "Stats" link is now a "Statistics"
dropdown grouping the existing per-source count page (relabelled "Sources")
with the two new CVE Forecast and cve.icu Analytics entries.
- **ndaal branding icons replaced with the logo mark**, in place of generic
Bootstrap icons, keeping the mark's natural red (`/static/img/logo.png`):
- the `/kev?catalog=ndaal` tab (was `bi-collection`, 16x40px);
- the `/dashboards/ndaal` CSAF-advisories dashboard header (was
`bi-info-square`, 20x49px) and its navbar "Vendor dashboards" submenu entry
(16x40px). Both templates special-case `slug == "ndaal"` inline rather than
changing the shared `VendorSpec.icon` field used by the other ~154 vendor
dashboards.
- **htmlhint / oxlint / markuplint sweep across all 60 web templates** (incl.
pre-existing findings, not just this session's additions):
- `.oxlintrc.json` referenced three `node/*` rules removed from the installed
oxlint 1.72.0, which silently failed the *entire* config, not just those
rules — removed the dead entries.
- `.markuplintrc.json` gained the official `@markuplint/htmx-spec` spec so
`hx-get` / `hx-target` / `hx-trigger` etc. are recognised, clearing 283
false-positive `invalid-attr` findings.
- `annotations.html` / `sightings.html`: inline `onclick="…"` handlers
replaced with `data-*` attributes plus a single delegated `addEventListener`
in `{% block extra_scripts %}` (CSP-friendlier, and clears an htmlhint
finding); missing `method="post"` added to two forms.
- `cpe_dictionary_dashboard.html`: inline `style="max-width: 22rem"` replaced
with a `.cpe-dictionary-filter-width` CSS class; the search input gained an
`id` + associated `<label class="visually-hidden">`.
- `emb3d_dashboard.html`: four `<small>` elements nested inside `<h5>`
(disallowed by markuplint) replaced with `<span>`.
- `kev_ransomware_recent_rows.html`: reworded an explanatory comment that
literally spelled out `<button>`/`</button>` in prose, which htmlhint's
non-Askama-aware parser was misreading as real tags.
- `vulnerability.html`: the SSVC "Recommended action" card's multi-line
conditional class attribute was flattened via `SsvcOutcome::alert_class()`
(see above).
- `cve_icu.html` / `cve_forecast.html`: three `&&`-chain conditionals
modernised to `?.` optional chaining; one unnecessary string escape removed.
- The remaining findings (Askama's `{% %}` / `{{ }}` syntax read as literal
HTML/text by all three linters, and a handful of pre-existing accessibility
findings unrelated to Askama) are tool/framework limitations, not template
defects — tracked, not silently ignored.
- **Git-based feeders migrated from a working-tree checkout to a bare mirror** —
fkie_nvd, cvelistv5, gsd, github, ossf_malicious_packages, vulnrichment. Root
cause fixed: `git_helper::ensure_repo` drove every cycle via
`git reset --hard` over a full working-tree checkout (300k+ files for the
larger mirrors). An orphaned `reset --hard` from a killed/restarted process
was never reaped (`tokio::process::Command` had no `kill_on_drop`), and the
next cycle's unconditional `rm .git/index.lock` stole the still-live orphan's
lock, starting a second concurrent `reset --hard` in the same worktree; one
process would `rmdir` a directory the other was still writing into, producing
`unable to create file …: No such file or directory` and corrupting the index.
Four of the six feeders (fkie_nvd, cvelistv5, gsd, ossf_malicious_packages)
imported **zero rows for two months** as a result, and while any of them ran
its reset every other feeder starved for disk I/O.
- New `git_helper::ensure_bare_mirror` / `ls_tree_files` / `diff_tree_files` /
`BlobBatch` (a persistent `git cat-file --batch` reader) replace the
working-tree path entirely: no index, no `.git/index.lock`, no
`reset --hard`, no `clean -f -d`.
- `hermetic_cmd()` gives every git child `kill_on_drop(true)` plus a hermetic
environment (`GIT_CONFIG_GLOBAL=/dev/null`, etc.), so the operator's own
`~/.gitconfig` can no longer change feeder behaviour and a dropped feeder
future kills its child instead of orphaning it.
- A stale `.git/index.lock` is now cleared only when older than 6 hours
(longer than the worst observed 2.5-hour checkout); a fresh lock is a hard
error surfaced to the scheduler instead of a silent steal.
- New `store_vulnerabilities_batch` commits up to 1 000 records per redb
transaction (was one fsync per record).
- A shared `vl_feeders::bare_git::run_import` now orchestrates all six feeders
(mirror sync → incremental-vs-full path resolution → chunked blob reads →
batched store); each feeder supplies only a small `parse_record` closure.
All six also gained, uniformly, the `is_valid_git_sha` gate (a seed RFC-3339
timestamp no longer wedges a feeder forever) and a shallow-history fallback
(a pruned `last_update` sha degrades to a full re-import instead of a
permanent error) that four of the six previously lacked.
- New `fuzz_git_batch_header` cargo-fuzz target covers the `cat-file --batch`
response-header parser, an untrusted network-input surface.
- **Dashboards dropdown no longer clips categories on laptop-size screens.** The
panel is a CSS multicolumn container; content that does not fit a column's
block-size flows into additional columns in the inline direction, and
`overflow-x: hidden` silently clipped them — the Linux / BSD / BSI / Browser /
CPE category headers existed in the DOM with a real layout box but were
unreachable below roughly 1920 px wide. (Grepping the served HTML for the
header text — the verification method used for two earlier, insufficient
attempts at this fix — can never catch this, since the markup is always
present.) Fixed with three coordinated changes, each measured in headless
Chrome against the live server: `overflow-x: hidden` → `auto` (categories
become reachable by scroll instead of vanishing); the per-item row height
trimmed (padding 0.25rem → 0.12rem, tighter header/divider spacing), which is
what actually shrinks total content height enough to fit; and `column-width`
18rem → 14rem. A new test (`dashboards_dropdown_never_clips_overflow_columns`)
pins the property-level invariant — the panel must never declare
`overflow-x: hidden` — since that is the one thing a served-HTML grep cannot
check.
- **`htmlhint`'s 3 findings on `layouts/base.html` resolved.**
`meta-description-require` was a real defect (no served page carried a
`<meta name="description">`; verified live) and is fixed with a
`{% block meta_description %}`-overridable default. `h1-require` and
`link-rel-canonical-require` are switched off in `.htmlhintrc` repo-wide:
`base.html` is the only first-party full HTML document in the tree (every page
template is a fragment that extends it), so htmlhint's per-file view can never
see the `<h1>` a page's content block supplies, and any canonical URL emitted
from the shared layout would be identical across all 40 page templates — a
false positive and an unsatisfiable rule, respectively. A new
`test_base_layout_head.rs` asserts the *rendered* page (not the template
source) carries the head tags htmlhint checks, so the suppressions cannot
silently start hiding a real regression.
- **`cargo clippy --workspace --all-targets --all-features -- -D warnings`
fixed** (it was failing at HEAD). `clippy.toml`'s `allowed-duplicate-crates`
allowlist had drifted — documented as 29/31 entries, actually needed 36 — so
six real `Cargo.lock` duplicates (block-buffer, const-oid, cpufeatures,
crypto-common, digest, sha2 — all downstream of `self_update`'s `checksums`
feature pulling `sha2 = "0.11"` against everything else in the tree's
`digest = "0.10"`) were unlisted and failing the gate; `quick-xml` was
simultaneously stale in the allowlist (no longer duplicated since the
0.39→0.40 bump), silently masking any future re-fork. Regenerated the
allowlist from `Cargo.lock`; fixed one pre-existing `clippy::collapsible_if`
in `cve5.rs`. New `test_duplicate_crate_allowlist.rs` asserts the allowlist
equals the real `Cargo.lock` duplicate set in both directions (new duplicate
and stale entry) plus that `multiple_crate_versions` stays `"warn"`, so this
cannot silently rot again.
## v0.1.55 — 2026-07-04
- **Workspace version bump to 0.1.55.**
- **Fuzz + proptest coverage gap closure (5 new fuzz targets, 138 → 140
vl-feeders targets).** A coverage survey found feeders and helpers with a
parser but no fuzz / property layer:
- **OSV Alpine / Chainguard / Wolfi** gained a `fuzz_drive` entry point, a
`cargo-fuzz` target each (`fuzz_osv_alpine` / `fuzz_osv_chainguard` /
`fuzz_osv_wolfi`), and a totality property in `proptest_feeder_drivers.rs`.
- **JVNDB (RDF) and ZDI (RSS)** had **zero** fuzz coverage; both gained a
`fuzz_drive` over their real parser, a `cargo-fuzz` target (`fuzz_jvndb_rdf`
/ `fuzz_zdi_rss`), and a totality property.
- **CERT-FR, CERT-EU, EUVD-KEV, Debian RustSec** were added to the
`proptest_feeder_drivers.rs` totality suite (they already had fuzz targets
but no fast-gate property).
- **SSVC** gained a bespoke `proptest_ssvc.rs` asserting the projection
invariants — histogram-sum, positive buckets, version accounting, and the
**(namespace, key)-sorted** ordering the byte fuzzer does not check.
- **vl-core GCVE helpers** (`validate_gcve_id` / `extract_gna`) gained
`proptest_gcve_utils.rs` encoding the relational contract (validation ⇒
extractable) and a `(gna, year, seq)` round-trip.
- **vl-web JSON extractors** (`extract_title` / `extract_severity` /
`extract_published` / `truncate_ellipsis` / `extract_references` /
`extract_cvss_details` / `extract_affected_products` / the four enrichment
projectors) gained in-module `proptest` blocks over arbitrary recursive JSON
— totality (a panic is a dashboard 500), the title length bound,
char-boundary safety, and the de-duplication guarantees.
- **`test_cargo_careful.sh` false-skip fixed.** The gate probed
`cargo careful --version`, which the tool rejects (it requires a subcommand),
so the gate silently self-skipped even with cargo-careful installed. Switched
to `command -v cargo-careful`. Running it then surfaced a pre-existing
failure: `test_32b` expected `VulnStorage::new` to self-heal a corrupt redb,
but `new()` now **fails loud** by default (no silent data loss) and only
quarantines under `VL_DB_RECREATE_ON_CORRUPTION=1`. Fixed the test to drive
the opt-in path via `open_or_create(_, true)` and added `test_32c` for the
fail-loud default contract.
- **New Linux source: Debian Rust Security Tracker** — a new `debian_rustsec`
feeder + `/dashboards/debian-rustsec` vendor dashboard under the **Linux**
category. It parses `rustsec.debian.net/index.json` (the RustSec advisory
database cross-referenced against Debian's Rust packages), keying each RUSTSEC
advisory with its CVE / GHSA aliases, affected crate, patched versions and
Debian package status. Ships exhaustive unit tests, a `proptest` property
suite, a `cargo-fuzz` target (`fuzz_debian_rustsec`), and a Bruno collection.
- **Debian Security Tracker dashboard moved to the Linux category** —
`/dashboards/debian-security` now groups under **Linux** in the navbar (was
*Vulnerability feeds*).
- **New quality gate: `test_sqllogictest_bin.sh` (gate 145, `--full`)** — runs
the repository's `.slt` files through the `sqllogictest` CLI, emits every data
format (text / JUnit XML / JSON) plus a jaq-synthesised, schema-validated
SARIF 2.1.0 under `documentation/sql/sqllogictest-bin/`, with a staged-copy
`--format` / `--override` autofix diff. Idempotent; sibling `.bats` +
`README.md`.
- **New release driver: `release/create_release_on_crates.io.sh`** — wired as
the final step (10) of `scripts/release_pipeline.sh`. Publishes the
workspace's publishable crates to crates.io (dry-run by default; a publish is
irreversible so it only runs under `RELEASE_EXECUTE=1` + a token), emitting
Markdown + a validated SARIF 2.1.0. The GitLab and crates.io release reports
are namespaced under `documentation/release/{gitlab, crates-io}/` so they no
longer clobber each other.
- **Supply chain** — `cargo-vet` exemptions added for `unicode-security` and
`unicode-script` (the UTS #39 homograph guard on advisory reference URLs).
## v0.1.54 — Unreleased
- **Workspace version bump to 0.1.54.**
- **`skills/sha` tri-tool SHA / SHA-3 sidecar verifier** — recomputes every
`.sha-256` / `.sha-512` / `.sha3-512` with three independent tools (openssl +
sha3sum + rhash for SHA-3; openssl + rhash + coreutils for SHA-2), `.sha1`
forbidden. Wired as `quality_gates.sh` gate 136 (`sha_sidecars`); refreshed
2227 stale `.sha3-512` sidecars under `skills/`.
- **`ruff --unsafe-fixes`** applied across the first-party skill Python (273
files, 8486 fixes).
## v0.1.53 — Unreleased
- **New Linux sources: Amazon Linux, Fedora and Oracle Linux** — three new
feeders with `/dashboards/{amazon-linux,fedora,oracle-linux}` vendor
dashboards under the Linux category. Amazon Linux reads the ALAS RSS feeds
(AL1 / AL2 / AL2023); Fedora reads the Bodhi security-update JSON API; Oracle
Linux ingests the bzip2-compressed ELSA OVAL catalogue, decompressed
in-process with the pure-Rust `bzip2-rs` decoder (decompress-only, no C
toolchain, permissive licence — chosen over a heavier archive library that
pulled GPL-3.0 / LGPL-3.0 transitive crates). Each feeder ships unit tests, a
cargo-fuzz target, and a Bruno collection.
- **New quality gates** — `test_rust_meth.sh` (rust-meth method-discovery
smoke), `test_dokono_rs.sh` (dokono-rs change-impact report), and
`test_doc_drift.sh` (doc-drift Markdown/source drift) — each emitting every
native format plus a jaq-synthesised, validated SARIF 2.1.0 under
`documentation/rust/<tool>/`, idempotent, with sibling `.bats` + `README.md`,
wired into `quality_gates.sh` under `--full` and `--shell`.
- **New regression gate: dashboard field distinctness** —
`test_dashboard_field_distinctness.sh` (gate 134, `--full`) asserts over every
dashboard (GUI + API) that each entry's title, description and cve_id are not
the same content, guarding the `/dashboards/opencve` defect where the title
column showed the bare CVE id. Backed by Rust unit tests in
`vendor_dashboard.rs` pinning `extract_title` to the OpenCVE-KB description.
- **Advisory ndaal-sa-2026-099** — informational CSAF 2.1 document recording the
`/dashboards/opencve` title display defect, its diagnosis and the added
regression coverage (no security impact).
- **New source: ndaal KEV** — an aggregated Known-Exploited catalog at
`/kev?catalog=ndaal` that deduplicates the full CISA, CIRCL and EUVD KEV
catalogs, the 50 most-recent Chromium and Firefox advisories, and the ndaal
public KEV ndjson feed into one `ndaal_kev` source. New storage primitive
`add_source_index_entry` registers a CVE under the aggregate without
clobbering its richer body; deduped by CVE id with a stable per-CVE timestamp
so re-runs never duplicate. 6h cycle; Bruno collection and a fuzz target for
the ndjson parser.
- **New source: Arch Linux Security Tracker** — a feeder for
`security.archlinux.org/issues/all.json` storing each Arch Vulnerability Group
(AVG) under source `archlinux` and cross-linking its CVEs, with a
`/dashboards/archlinux` vendor dashboard. 3h cycle; Bruno collection and a
fuzz target.
- **Vendor dashboards: recent-table cell integrity** — OSV-shaped advisories now
show a calculated CVSS base score (v3.0/v3.1/v4.0 via the `cvss` crate)
instead of `n/a` (e.g. `/dashboards/ocaml`), and no row renders a blank title
(display-id fallback for RESERVED / id-only records).
- **Dependency maintenance** — lock-only patch bumps: cc, memchr, regex-syntax,
smallvec, zerocopy.
- **Version bump 0.1.52 -> 0.1.53**: workspace `Cargo.toml` + `Cargo.lock` (5
crates: vl-core, vl-models, vl-feeders, vl-web, vl-cli), the Bruno
`app_version`, the Ansible role defaults / playbook / install tasks, the macOS
installer `SCRIPT_VERSION`, and the README / LIESMICH / LISEZMOI banners.
Opens the 0.1.53 development cycle.
## v0.1.52 — Unreleased
- **Version bump 0.1.51 -> 0.1.52**: workspace `Cargo.toml` + `Cargo.lock` (5
crates: vl-core, vl-models, vl-feeders, vl-web, vl-cli), the Bruno
`app_version`, the Ansible role defaults / playbook / install tasks, the macOS
installer `SCRIPT_VERSION`, and the README / LIESMICH / LISEZMOI banners.
Opens the 0.1.52 development cycle.
- **NVD CPE Dictionary — full 1.74M ingest (was ~8 %)**: the NVD
`nvdcpe-2.0.zip` splits the dictionary into 17 JSON chunk members and
`stream_zip_into` read only the first, so only ~139,989 of the
`totalResults: 1,743,228` CPE names imported. It now collects every `.json`
entry and streams all 17 chunks through one shared `CpeSink`; a live re-import
confirmed 1,743,228 names. The dashboard moved from
`/attacks/nvd-cpe-dictionary` to `/dashboards/nvd-cpe-dictionary` under a new
"CPE" Dashboards-dropdown header.
- **Historical EPSS feeder + EPSS History / EPSS Old-vs-New dashboards**: a new
`epss_history` feeder ingests the `empiricalsec/epss_scores` daily archive
(back to 2021-04-14) as an aggregate-only per-day snapshot (count / mean /
median / p90 / decile histogram / top-100), keeping redb growth to
single-digit MB across the full ~1,880-day history. `/dashboards/epss-history`
draws inline-SVG mean / count time-series with model-version band markers;
`/dashboards/epss-comparison?from&to` shows the distribution / decile / mean
shifts between any two dates (e.g. the 2025-03-17 v3 → v4 re-scoring). New
`HISTORICAL_EPSS` + `EPSS_HISTORY_DASHBOARD` storage tables, a
`vl-cli epss-history import [--since]` subcommand, and an incremental +
resumable daily feeder cycle. The top-100 table carries Title / CVSS / CPE
columns (each `n/a` when empty), resolved per-CVE at render time via the
shared `advisory_display_triple` extractor and HTML-escaped before
`innerHTML`.
- **Generic vendor-spec JSON API (HATEOAS)**: every `VENDOR_SPECS` dashboard now
exposes `GET /api/v1/vendor/{slug}` (summary) and
`/api/v1/vendor/{slug}/recent?page&q` (paginated rows) with a HATEOAS `_links`
envelope and `?download=1`; the HTML partial and the JSON API share one
`load_vendor_recent_page`. `vulnrichment` moved out of the auto "Vulnerability
feed" group into the hand-listed "Enrichments & charts" section.
- **htmlhint HTML gate (gate 127) + EPSS-history live gate (gate 128)**:
`tests/scripts/test_html_with_htmlhint.sh` lints every tracked `*.html`
(git-ls-files discovery to skip the multi-GB data dumps; json + jaq SARIF
2.1.0) and `test_epss_history_dashboard.sh` curls the two EPSS dashboards plus
their JSON endpoints; both ship a sibling bats + shdoc README and wire into
`scripts/quality_gates.sh`.
- **Live-gate `${CURL}` word-split fix**: under the boilerplate's `IFS=$'\n\t'`
a scalar `CURL="curl -sk …"` is not word-split, so
`test_epss_history_dashboard.sh`, `test_enrichment_api.sh`, and
`test_ghsa_cve_linking.sh` returned HTTP 000 for every check even with the
server up. Switched to an array `CURL=(curl -sk …)` called as `"${CURL[@]}"`.
- **Four new cargo-fuzz targets (119 -> 123)**: `fuzz_epss_history_csv`,
`fuzz_epss_snapshot_aggregate`, `fuzz_epss_date_param`, and
`fuzz_vendor_advisory_triple` (the EPSS-history parsers + the title/cvss/cpe
extractor).
- **Dependency updates (ndaal-sa-2026-095)**: rustls 0.23.40, aws-lc-rs 1.17.0,
aws-lc-sys 0.41.0, displaydoc 0.2.6 (lock-only) plus quick-xml 0.39 -> 0.40.1
in vl-feeders (requirement bump, applying the bump ndaal-sa-2026-094 had
deferred). quick-xml 0.40 made `BytesText::xml_content` take a required
`XmlVersion` argument, so `capec.rs` now passes `XmlVersion::Implicit1_0`
(CAPEC is XML 1.0). quick-xml 0.39.4 is retained transitively via `rss` /
`atom_syndication` (a non-blocking cargo-deny `multiple-versions = "warn"`
duplicate). workspace-hack regenerated; `test_cargo_hakari.sh` verify-clean.
The ndaal-sa-2026-095 informational advisory ships its five hash sidecars and
passes csaf-validator 2.1.
## v0.1.51 — Unreleased
- **Version bump 0.1.50 -> 0.1.51**: workspace `Cargo.toml` + `Cargo.lock` (5
crates: vl-core, vl-models, vl-feeders, vl-web, vl-cli) + the Bruno
`app_version`. Opens the 0.1.51 development cycle.
- **`/kev` pages — CVSS preserve, EPSS column, navigator, tab relabels**
(ndaal-sa-2026-089, 090, 091): (1) the CISA/CIRCL/EUVD KEV feeds carry no CVSS
and the per-CVE store is last-writer-wins, so the hourly KEV feeders overwrote
the real NVD/vulnrichment base score with 0.0 — a new
`vl_core::vulnerability::cvss_base_score` extractor +
`store_vulnerability_preserving_cvss` now keep the higher score (missing
scores render `N/A`); (2) a new EPSS column right of CVSS, populated per-row
from the EPSS/KEV enrichment table; (3) the navigator never advanced because
`parse_query` dropped numeric `?page=` / `?days=` (serde will not coerce a
string into a number, so the whole query parse failed and every page reset to
1\) — fixed with a string-tolerant deserializer + a dashboard-style
`Page X of N` component; and (4) the tabs are relabelled `CISA KEV` /
`CIRCL KEV` / `EUVD KEV` with the title
`Known Exploited Vulnerabilities (KEV)`.
- **CIRCL KEV null-tolerant parse + retry/backoff** (ndaal-sa-2026-089): the
CIRCL KEV feeder aborted the whole-page import on an explicit-null nested
object (`scope` / `evidence[].details`), emptying `/kev?catalog=circl`. Every
nested struct/Vec field now maps an explicit null to its default; the feeder
also gained bounded retry + linear backoff on transient 5xx / network errors.
The CISA/CIRCL/EUVD/CNW KEV cycles were shortened 12h -> 1h.
- **`/changelog` page**: a new Info-menu entry (after a divider) renders the
full `CHANGELOG.md` in a scrollable panel. The content is embedded via
`include_str!`, so it auto-syncs on every build; a test asserts the embedded
copy is byte-for-byte the on-disk file.
- **cvelistv5 feeder no-data fix** (ndaal-sa-2026-092): the feeder imported zero
CVEs because `git pull --ff-only` aborted on a dirty / force-pushed shallow
mirror. `git_helper::ensure_repo` now force-syncs read-only mirrors
(`fetch --depth 1` + `reset --hard FETCH_HEAD` + `clean -fd`, with a
stale-`index.lock` self-heal), and cvelistv5 falls back to a full re-import
when the shallow incremental diff cannot reach the previous SHA.
- **`eslint.config.js` SPDX header fix** (ndaal-sa-2026-093): the header used
`#` comments (invalid JavaScript), so eslint failed to load its own flat
config; switched to `//`.
- **simdutf8-cli quality gate (gate 113, `--full`)**: new
`tests/scripts/test_simdutf8_cli.sh` validates UTF-8 across the workspace
crate sources and emits all four native formats (text / json / SARIF 2.1.0 /
Markdown) to `documentation/rust/simdutf8_cli/` with hardened path handling
(`--base-dir` confinement, `--no-follow-symlinks`); sibling bats + wired into
`scripts/quality_gates.sh` LIVE_GATES.
- **CSAF advisories ndaal-sa-2026-089 … 093**: one per fixed defect above, with
CVSS v3.1 scores where a security/availability impact applies (089 3.7 LOW,
090 3.1 LOW, 092 5.3 MEDIUM) and informational for the purely functional ones
(091 pagination, 093 eslint config).
- **Eight new cargo-fuzz targets (99 -> 107)**: `fuzz_circl_kev`,
`fuzz_euvd_kev` (the rewritten KEV parsers), `fuzz_cnw_kev`, `fuzz_drupal`,
`fuzz_fstec`, `fuzz_tailscale`, `fuzz_gsd` (previously-uncovered feeder
parsers), and `fuzz_cvss_base_score` (the new CVSS extractor) — each drives
the real parser via a `#[doc(hidden)] pub fuzz_drive*` helper.
## v0.1.50 — 2026-05-31 (bash options standard + footer sweep + dump tools)
- **`skills/bash` — standard CLI options + anti-fragile footer**: the five
canonical options every option-bearing script must offer (`-h` / `-V` / `-v` /
`-q` / `-l FILE`) with the injection-safe `_log_emit` core, plus the
documented `print_script_info` fragile-footer fix — reuse the absolute
`SCRIPT_PATH` / `SCRIPT_NAME` anchors instead of re-deriving from a relative
`${BASH_SOURCE[0]}` after the body has `cd`'d.
- **Footer canonicalisation across 30 scripts**: 14 anchor-having scripts
switched to anchor reuse; 16 non-anchor scripts brought to full canonical form
(4-var anchors + hardened footer + self-skip guards), including two
`#!/bin/sh` → bash conversions (the action-lint CI entrypoint — with `bash`
added to its Alpine Dockerfile — and the apt provisioner) and two bare-bash
skill scripts. Each is green against its sibling bats.
- **`export_sources_ndaal_dumps.sh` / `import_sources_ndaal_dumps.sh`**:
per-source NDJSON dump export/import through the vl-web API with
verify-before-store/import (schema, entry count, 25-entry DB compare, in-file
dedup), the full 5-sidecar contract, newest-with-fallback dump selection
(incl. the `dumps_archive/` 7z fallback), and SARIF 2.1.0 + Markdown reports
under `documentation/dumps/{export,import}/`.
- **Accessibility exit-code fix**: `verify-pdfs.sh` and `run-mobile-a11y.sh` now
propagate their `EXIT` accumulator to the process exit code — a failing PDF/UA
or mobile a11y gate finally fails the script (was a silent `exit 0`).
- **`sadp_pilot` feeder + `/dashboards/sadp` + SADP enrichment card** (commit
dc288b9e4): new feeder for the CVE Program Supplier-ADP (SADP) Pilot
(<https://github.com/CVEProject/sadp-pilot>). It git-clones the repo on a
12-hour cycle and imports the CVE 5.2 records under `Published SADP Records/`,
where suppliers attach VEX-like product-status data in `containers.adp[]`
(`providerMetadata.x_adpType == "supplier"`); the repo copy overrides the DB
on re-import. A hand-listed `/dashboards/sadp` dashboard appears under the
"Enrichments & charts" navbar group, and a "SADP — Supplier VEX / Status"
enrichment card on the aggregated CVE view surfaces each supplier ADP entry
(provider metadata, affected products, product-status), rendering nothing when
absent.
- **`/dashboards/ocaml` for the `osv_ocaml` feeder + feeder→dashboard coverage
guard** (commit cdd4cf9cd): the previously dashboard-less `osv_ocaml` feeder
now has a `/dashboards/ocaml` page, and a new
`every_feeder_source_has_a_dashboard` regression test fails CI whenever a
registered feeder source ships without a matching dashboard.
- **`ssvc` feeder + dedicated `/dashboards/ssvc` methodology dashboard**: new
feeder for the CERT/CC SSVC framework (<https://github.com/CERTCC/SSVC>). On a
12-hour cycle it git-clones the repo and parses
`data/json/ssvc_object_registry.json` into four aggregate blobs (`stats`,
`decision_points_table`, `outcomes_table`, `by_namespace`) stored under a new
`SSVC_DASHBOARD` redb table. The dedicated `/dashboards/ssvc` page
(hand-listed under "Enrichments & charts", after SADP) renders the framework's
decision points, values, and outcome groups, with a `GET /api/v1/ssvc/{key}`
HATEOAS passthrough. Per-CVE SSVC scoring is unchanged — it already appears on
each CVE detail page. `ssvc` is a dedicated dashboard (not in
`FEEDER_SOURCE_URLS`), so it is exempt from the
`every_feeder_source_has_a_dashboard` test; `EXPECTED_FEEDERS` 159 → 160.
- **cargo-fuzz targets for the two new parsers**: `fuzz_sadp_record` (the
`sadp_pilot` CVE-record validator/extractor) and `fuzz_ssvc_registry` (the
SSVC `parse_registry` projection) — both build under ASAN and run 60 s clean
(81 321 + 71 871 runs, no crashes), satisfying the "new untrusted input
surface ⇒ new fuzz target" rule.
- **Secure download skills (`skills/curl` · `skills/wget` · `skills/aria2c` ·
`skills/downloads`)**: BSI-TR-02102-2-hardened canonical fetch wrappers —
`canonical_curl` (preferred), `canonical_wget` (fallback when curl is absent),
`canonical_aria2c` (BitTorrent / magnet / segmented / bulk) — with the
tool-selection order (curl → wget → aria2c) and the absolute ban on
hand-rolled downloaders. Each ships a gate-clean `canonical_*.sh` (bash -n /
shfmt / shellcheck) + a sibling bats + a per-flag BSI reference, and is
cross-referenced from the `CLAUDE.md` skill list.
- **CSAF `.sha3-512` sidecar format normalised**: every advisory's `.sha3-512`
sidecar now uses the `sha3sum`-native `<hash> *<name>` line. Six advisories
(071/072/073/083/084/087) were in the coreutils two-space form that
`sha3sum -c` rejected as malformed (hashes were always correct), so the whole
`csaf/` tree (87 advisories) now passes `sha3sum -c`. Adds `ndaal-sa-2026-087`
(informational: simdutf8-cli 0.1.6 release) with all five hash sidecars.
- **Version bump 0.1.49 → 0.1.50**: workspace `Cargo.toml` + `Cargo.lock` (5
crates) + the Bruno `app_version`. Ansible role binaries, sidecars, and
embedded version strings are refreshed atomically by the release pipeline
(`update_ansible_role.sh`), so they stay at 0.1.49 until the next build.
## v0.1.49 — 2026-05-27 (release hygiene + QA gate factory + footer sweep)
- **release(v0.1.48) tree audit + cleanup** (commit 9b731b9f7): Purged 96 stale
0.1.44 + 0.1.47 RPM/DEB artefacts + their full 5-sidecar contracts from
`release/v0.1.48/{aarch64,x86_64}-unknown-linux-gnu/` (operator caught the
pollution during the v0.1.49 cycle prep). Regenerated all 5 aggregate
manifests (sha-256/sha-512/sha3-512/ blake3-512/shake256-512) against the
post-purge file set — `shasum -c SHA-256SUMS.txt` reports `ok=96 fails=0`.
- **`create_release.sh` default flipped to always-prune** (commit 6b99c455f):
root cause of the v0.1.48 release-tree pollution. `prune_old=1` is now the
initial value; new `--keep-old-versions` flag for the rare
cross-version-comparison opt-out; legacy `--prune-old-versions` flag preserved
as a no-op alias so existing CI invocations continue to work.
- **Version-drift fix for Ansible deploy** (commits a73eb5b65 / 3ac2a0753 /
ad22f4695 / e07191e70 / ndaal-sa-2026-077): the operator's
`ansible-playbook deploy_nvulnlookup.yml` had been failing the post-deploy
version assert because `update_ansible_role.sh` only propagated `VL_VERSION`
into binaries and per-binary sidecars — not into the 14 embedded
`nvulnlookup_version` / `SCRIPT_VERSION='...'` references across the playbook,
role defaults, install_from_package.yml fallbacks, and macOS bootstrap
scripts. New `propagate_version_to_ansible_artefacts()` function (driven by
the workspace `VL_VERSION`) closes the loop; new section-aware
`fetch_workspace_version()` reads the `[workspace.package]` block of
`Cargo.toml` so a future `[workspace.metadata.foo]` table cannot poison the
result; new semver-shape guard rejects junk values;
`VL_SKIP_VERSION_PROPAGATION=1` escape hatch for release-rehearsal scenarios.
Sister `.bats` test count: 31 → 45.
- **skills/ frontmatter restructure** (commits df9d8f322 / 8a5f1e1eb /
384843f9a): 932 SKILL.md files were carrying the broken
`## name: ... description: ... allowed-tools: ...` mega-line frontmatter shape
(line-3 lengths 600–1500+ chars) — restructured into proper YAML so the Claude
Code skill loader can parse them. All 968 SKILL.md `license:` fields
normalised to `Apache-2.0` (110 inserted, 131 normalised, 727 already
correct). Flat-file skill `skills/rust-path-security.md` bumped to minimum
cap-std / cap-tempfile **v4.x** (3.x is end-of-life upstream; lockstep
major-version rule documented). 35 multi-line broken cases deferred to a
follow-up task chip.
- **mdformat plugin set installed in pipx venv**: `mdformat-gfm`,
`mdformat-frontmatter`, `mdformat-footnote`, `mdformat-gfm-alerts`,
`mdformat-admon` (+ pipx-bonus `mdformat-front-matters`) — fixes the
corruption that produced the mega-line frontmatter shape, so future `mdformat`
runs preserve canonical YAML.
- **3 new CSAF advisories**:
- ndaal-sa-2026-076 — `mimetype-cli` 0.1.1 dep-bump (colored 2→3, infer
0.16→0.19, phf 0.11→0.13, phf_codegen 0.11→0.13).
- ndaal-sa-2026-077 — `update_ansible_role.sh` version-drift fix.
- ndaal-sa-2026-078 — `mimetype-cli` 0.1.2 capability-secure filesystem layer
(cap-std + cap-tempfile) + safe_join lexical defence + DataDir wrapper +
60-vector path-traversal regression suite (56 named + 4 exhaustive
property-style sequences). Distribution index regenerated to 78 entries.
- **QA "gate factory" — 9 new `--fast` static-analysis gates** (gates 102–110):
`linthis` (multi-language linter), `pyscan` (OSV-backed Python dep scan),
`pip_audit` (PyPA dependency CVEs), `lineguard` (line-ending /
trailing-whitespace, `--dry-run` then `--fix`), `x509lint` (X.509 RFC 5280
cert lint), `cargo_workspace_lints` (`lints.workspace = true` coverage),
`cargo_unused_workspace_deps`, `cargo_fl` (cargo-fl fast lint), and
`script_footer_guard` (gate 110, the footer regression guard below). Each tool
runner is ndaal-canonical (auto-reexec into Homebrew bash, strict-mode
quartet, hardened `print_script_info` footer, `-h/-V/-v/-q/-l/--root/--strict`
CLI), emits every native output format **plus a jaq-synthesised SARIF 2.1.0**
report, self-skips with `rc=0` when its tool is missing, and ships a canonical
sister `*.bats`. Two bug-fixes during the build: the `cargo workspace-lints` /
`cargo unused-workspace-deps` gates must be pointed at the `Cargo.toml` *file*
(a directory makes them error — now detected and downgraded to SKIP rather
than a false zero-finding pass), and `cargo_fl` is invoked as `cargo-fl check`
directly (`cargo fl` makes cargo treat `fl` as an unrecognised arg → empty
output). All wired into `scripts/quality_gates.sh` under `--fast`.
- **`script_footer_guard` (gate 110) + repo-wide footer sweep**: the
`print_script_info` footer used to re-derive its `script_path` from a
*relative* `${BASH_SOURCE[0]}`
(`cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)`), which leaked
`cd: …: No such file or directory` and a bogus relative path whenever a script
`cd`s mid-run (e.g. into `${QG_WORKSPACE}`) AND was invoked by a relative
path. Fixed across **all ~197 tracked `.sh` files** to use the absolute
`${SCRIPT_PATH}` captured at the top before any `cd`. New
`tests/scripts/test_script_footer_robustness.bats` fails the build if any
tracked `.sh` reintroduces the fragile form; wired as gate 110. Verified
working from both the worktree and a `main` checkout.
- **`create_sbom_with_cargo_sbom.sh`** (NEW): generates an SPDX 2.3
(`spdx_json_2_3`) and a CycloneDX 1.6 SBOM via `cargo-sbom`, then augments
each with the release-tree artefacts (per-triple binaries, `.deb`/`.rpm`
packages, tarballs) keyed by release-relative path and hashed with SHA-256 /
SHA-512 / SHA3-512 / BLAKE3 in the SBOM `checksums` / `hashes` arrays (full
5-sidecar set emitted alongside). A `first_hex()` helper strips the U+00A0
NO-BREAK SPACE that `sha3sum` uses as its hash↔filename separator. Wired into
`release/create_release.sh` after the per-triple build loop, before the
aggregate-manifest step. Canonical sister `*.bats`; verified against the
v0.1.48 release tree (SPDX 458 packages / CycloneDX 456 components).
- **MSRV bump 1.93.0 → 1.95.0**: `sysinfo` 0.39.2 requires Rust 1.95;
`Cargo.toml` + `workspace-hack/Cargo.toml` `rust-version` updated.
- **rust-doctor + frontend-lint cleanup**: clippy `--fix` mechanical sweep
across 10 Rust files; hoisted a per-entry `String` buffer out of the
`osv_bulk` zip loop (`content.clear()` reuse). 10 dashboard templates
modernised (`var`→`const`, `!=`→`!==`, `==null`→`??`, `===undefined`→`typeof`,
`i++`→`i+=1`, `alert`→`console.error`) so htmlhint / oxlint / fta run clean.
Fixed a `printf '- …'` leading-dash crash (exit 2) in `test_cargo_oneway.sh`
and applied the `printf --` guard across the affected runners.
- **Path-traversal hardening — `sightings` 7z extraction** (CWE-22): the
`dumps_archive/sightings.7z` cold-storage fallback was the lone archive feeder
still calling `sevenz_rust::decompress_file` directly, bypassing the crate's
own `safe_decompress_7z` guard — contradicting the `safe_extract` module doc,
which claimed every archive call site routed through the guard. Now routed
through `safe_decompress_7z`, so a malicious 7z entry name (`../`, absolute,
NUL byte) is rejected by the `safe_relative` gate before any byte is written.
New regression test asserts a traversal entry cannot escape the extraction
tempdir. A read-only audit of all five workspace crates found no other
attacker-controllable path sink: every remaining `std::fs` site is a
compile-time constant, `current_exe()`, an operator-chosen CLI/env/config
path, an `include_dir!`-embedded asset, or already routed through `safe_join`
/ `DataDir` / `safe_decompress_7z`.
## v0.1.48 — 2026-05-26 (CWE→Attacks relocation + 5-sidecar contract)
- **CWE dashboard relocated to the Attacks menu** (ndaal-sa-2026-075):
`/dashboards/cwe` is now `/attacks/cwe` (under the **Attacks** navbar
dropdown, immediately after CAPEC). All three legacy paths —
`/dashboards/cwe`, `/dashboards/cwe/recent/partial`,
`/dashboards/cwe/cwe/{id}` — now serve **HTTP 308 Permanent Redirect** to the
matching `/attacks/cwe*` target, so existing links and bookmarks survive the
move. The `/api/v1/cwe/*` namespace is unchanged (API endpoints stay put —
same pattern as the prior CAPEC relocation). Touches: route registration in
`vl-web/src/main.rs`, the `redirects` module (three new 308 handlers with
id-revalidation via `canonicalise_cwe_id`), the navbar in
`templates/layouts/base.html`, the page / pagination / detail templates, the
Bruno collection, the API reference implementation + its pytest suite, the
testssl endpoint list, the rust integration tests (now assert legacy paths
return 308), and the two README mirrors + Administrator Guide.
- **Five-sidecar cryptographic-hash contract enforced everywhere** per the
CLAUDE.md "Cryptographic hashes" rule.
`vulnerability-lookup-rs/release/create_release.sh` and
`vulnerability-lookup-rs/scripts/update_ansible_role.sh` now both emit all
five algorithms — sha-256, sha-512, sha3-512, **blake3-512** (new),
**shake256-512** (new) — for every per-file sidecar AND every aggregate
`*SUMS.txt` manifest. Coverage spans four cryptographic constructions across
three independent designs: Merkle–Damgård SHA-2, Keccak sponge SHA-3, BLAKE3
tree hash, SHAKE256 extendable-output function (XOF).
- **New `backfill_5sidecar_contract.{sh,bats}` tool** under
`vulnerability-lookup-rs/scripts/` (idempotent + recompute-and- compare
verifying) retroactively brings every release artefact (v0.1.31 → v0.1.48
tarballs, per-triple binaries, `.deb`/`.rpm` packages, the
`release/cpu_optimized/` tree) and the Ansible role `files/` up to the
five-sidecar contract. Existing `.sha-256` / `.sha-512` / `.sha3-512` sidecars
are preserved (timestamps + bytes untouched). 36-test canonical bats sister.
This release lands **315 new `.blake3-512` + 315 new `.shake256-512` per-file
sidecars + 34 new aggregate `BLAKE3-512SUMS.txt` / `SHAKE256-512SUMS.txt`
manifests** committed across the tree.
- **New `test_ndaal_binsec` quality gate** — runs the ndaal-binsec fork (native
validated SARIF 2.1.0 + Markdown output, distinct from upstream binsec which
produces only text + JSON; SARIF derived via jaq for upstream-compat) over
every shipped release binary under `vulnerability-lookup-rs/release/`
(recursive, including `cpu_optimized/`). Wired into
`scripts/quality_gates.sh --full` (LIVE_GATES) immediately after the existing
upstream `binsec` gate. Self-skips cleanly when the on-PATH `binsec` is
upstream (no `--format` flag) so the gate coexists with the upstream-binsec
gate. 36-test canonical bats sister.
- **Screenshot pipelines now cover the `/attacks` dropdown** — all four
screenshot-producing scripts
(`scripts/verify_static_assets_{molecule,debian13}.sh`,
`scripts/create_screenshots_from_local_running_app.sh`,
`vulnerability-lookup-rs/scripts/capture_screenshots.sh`) capture the six
Attacks-menu entries: CAPEC, CWE, MITRE ATT&CK Enterprise / ICS / Mobile, and
MITRE EMB3D. Stale `/dashboards/cwe` capture rows removed (they would
308-redirect to `/attacks/cwe` and produce a duplicate with the wrong
filename).
- **`verify_static_assets_{molecule,debian13}.sh` end-to-end** — five
pre-existing structural bugs cleared so both verify pipelines run from box-up
through to screenshot-capture against a fresh Vagrant box + six Molecule
(Podman) containers (Debian 12/13/14, AlmaLinux 9/10, Alpine):
1. `mkdir -p "${WORK_DIR}"` was AFTER the first `info`-tee call into
`${LOG_FILE}` → `tee: no such file or directory` on a fresh checkout.
2. `parse_os_row()` did `pair=(${row})` under the script-wide `IFS=$'\n\t'`
(no space) → 1-element array → `${pair[1]}` tripped `nounset`. Fixed with a
local `IFS=' '`.
3. Strict-mode preamble exports `LC_ALL=C` / `LANG=C` for deterministic
sort/awk/date output, but `ansible-playbook` refuses to start under `C`
locale ("Ansible requires the locale encoding to be UTF-8"). Each
`ansible-playbook` invocation now prefixes
`LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8`.
4. The repo-root `ansible.cfg` declares
`vault_password_file = .vault_password`, but no developer checks that file
in and the role has no vault-encrypted variables. Both scripts now
materialise a stub `.vault_password` at `${WORK_DIR}` and pass it via
`ANSIBLE_VAULT_PASSWORD_FILE` (mirrors the pattern in
`verify_role_tests_debian13_selected.sh`).
5. `capture_screenshots()` had the same `pair=(${row})` IFS bug as
`parse_os_row` at two sites (TOP_LEVEL_ROUTES +
REFERENCE_VULNERABILITY_ROUTES rows) — fixed with one function-scope
`local IFS=' '`.
- **Ansible role refresh to 0.1.48** — `tools/ansible/roles/nvulnlookup/files/`
Linux binaries refreshed to 0.1.48 (16 modified files + sidecars), 0.1.47
`.deb` / `.rpm` packages dropped (32 deletions), 0.1.48 packages added (32 new
files + 3-then-5 sidecars). `nvulnlookup_version` default in
`tools/ansible/roles/nvulnlookup/defaults/main.yml` bumped `0.1.44 → 0.1.48`
so the role's post-deploy version-check assertion
(`Running nvulnlookup reports version 'X' but Y was deployed`) passes against
the freshly-shipped binaries.
- **`update_ansible_cfg_project_specific.{sh,bats}` canonicalised** per
`skills/bash` + `skills/bats`: Bash >= 4.4 guard, canonical section-headers,
`SCRIPT_NAME="${0##*/}"` (no `basename` subprocess), `SCRIPT_PATH_WITH_NAME`
anchor + Info printf, canonical mutation flags (`mkdir -p -v`, `cp -f -p -v`,
`chmod -v`, `mv -f -v`), `-h/--help/-V/--version` argv short-circuit before
any side effects. Bats sister extended from 27 → 36 tests against the
canonical template; full pass.
- **`test_inline_script_escape` repaired** after the v0.1.43-vintage
nuclei-dashboard pagination refactor removed
`NucleiDashboardTemplate.recent_json`. The XSS-escape regression test now
carries its malicious `</script>` payload in the remaining inline-`|safe` JSON
field `by_severity_json` and uses the post-refactor `nucleiSeverityBars`
marker (replacing the removed `nucleiRecentBody` marker).
- **`test_proptest` regression fixed** — `collect_regressions()` was emitting
its `-v` mutation-flag audit lines (mkdir + cp) on the same stdout the caller
captured into `local -i nreg="$(...)"`, so the integer assignment tripped an
arithmetic-syntax error on any release with regression files present. The `-v`
output now goes to stderr (audit trail survives in the run log); stdout
contains only the count.
- **`fix(scripts):`** `vulnerability-lookup-rs/scripts/ cvelist_to_csv.sh`
defaults its output to `${REPO_ROOT}/vulnerability-lookup-rs/dumps/cves.csv`
(was `${PWD}/cves.csv` — surprising cwd-dependent behaviour).
- **`style(skills):`** dropped the file-scope `markdownlint-disable` /
`rumdl-disable-file` directives in
`skills/offensive-bug-identification/SKILL.md` (the underlying MD013 / MD049
violations the directives were hiding are tracked as a follow-up).
## v0.1.47 — 2026-05-21 (analysis-gate expansion + CIRCL moksha dump)
- **redb durability + self-healing recovery** (ndaal-sa-2026-049): prevention —
every redb write uses two-phase commit and the server does an ordered graceful
shutdown (drain feeders → `flush()` checkpoint) so normal restarts never
corrupt the store. Recovery — `VulnStorage::new()` now self-heals: an
unrepairable file (after an unclean SIGKILL / power-loss / disk-full) is
quarantined to `<file>.corrupt-<UTC>` (preserved, never deleted) and a fresh
database is created, so the service always starts and repopulates from feeds.
No more manual file-removal band-aid.
- **Quality gates:** four new analysis runners — `dei` (god-class detection),
`kardo` (AI-readiness), `code-dupes` (duplicate code), `pedant` (opinionated
Rust lint) — each emitting every native format plus SARIF 2.1 synthesised from
JSON via `jaq`. Wired into `quality_gates.sh` alongside 16 existing runners
(gates 72–91); `quality_gates.uml` updated + re-rendered.
- **bats hardening:** repo-wide xtrace strict-mode guards added to every sister
`.bats` (no unconditional `set -x`/`set -o xtrace`, no `BASH_XTRACEFD`
redirection — pure-`grep`, zero deps).
- **Pre-existing debt cleared:** 57 bats files with duplicate `@test` names
deduped; every `tests/scripts/test_*.sh` is now `shellcheck -o all` clean;
SC2034 dead-status-var bugs fixed.
- **End-to-end runner fixes:** `test_wardenscan.sh` self-skips rc=0 with no
GitHub workflows; always-clean `findings=$?` bug fixed in 8 runners;
`test_benchmark_loadtest.sh` duplicate-`readonly` abort removed.
- **Moksha feeder + dashboard:** a new live feeder pulls
`https://vulnerability.circl.lu/dumps/moksha.ndjson` over TLS 1.3 (rustls) 35
minutes after start, then every 24 h, and falls back to the bundled
`dumps_archive/moksha.7z` (SHA3-512-verified, decompressed in-process with
`sevenz-rust`) when the live endpoint is unreachable. CVE JSON 5.1 self-issued
advisories from moksha.dk import with redb-upsert dedup and are cross-linked
onto every CVE they reference, so they appear in each CVE's aggregated JSON +
enrichment cards (activates the dormant `LINKS` table; `ENRICHMENT_SOURCES` 14
→ 15). A new `/dashboards/moksha` page is listed in the navbar under the
**"Enrichments & charts"** group; routes + `/openapi.json` auto-register from
`VENDOR_SPECS` (124 → 125), and the dropdown entry is hand-listed under that
group. Coverage: 8 feeder unit tests, a 9-file Bruno collection, and moksha
URLs in the testssl / tls / sqlmap lists.
- **Fuzzing:** added a `fuzz_moksha` NDJSON / CVE-5.1 target to the per-crate
`crates/vl-feeders/fuzz/` harness (85 → 86 targets) and wired
`crates/vl-feeders/fuzz` into the `run_all_tests.sh` fuzz gate (only vl-core +
vl-web ran before). Removed a stray top-level `fuzz/` stub that duplicated
existing vl-core targets.
- **vl-cli:** `import-dumps` now honours `--db-path` (alternate/throwaway redb
target).
- **Feeds:** CIRCL `moksha` dump seeded via `fetch_dumps.sh` +
`dumps_archive/moksha.7z`.
## v0.1.46 — 2026-05-18 (8 new quality gates + 33 sister .bats scaffolds)
Largest single-session expansion of the quality-gate matrix this project has
seen: eight new Rust-native static-analysis tools wired into
`scripts/quality_gates.sh` as `--fast`-tier gates (`9w` → `9ad`), every
documented output format emitted per run, canonical TIMESTAMP-anchored output
directories. Plus a mechanical CLAUDE.md-compliance sweep that closes the "every
.sh ships a .bats" gap for 40 ndaal-owned helpers and re-canonicalises the 7
`test/*` harness `.sh` preambles.
### Quality gates added
| Gate | Crate | Subcommand | Formats | Output dir |
| ------------------- | ------------------------------------- | ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
| 9w `lintscout_scan` | `lintscout` | `lintscout` | text / json / count / sarif | `documentation/linter/lintscout/lintscout_<TIMESTAMP>/` |
| 9x `alint_scan` | `alint` v0.9.23 | `alint check` | human / json / sarif / github / `markdown` / junit / gitlab / agent | `documentation/linter/alint/alint_<TIMESTAMP>/` |
| 9y `mkdlint_scan` | `mkdlint` v0.11.9 | `mkdlint <path>` | text / json / sarif / github + `--fix-dry-run` (and optional `--fix` via `--apply-fixes`); Markdown style checker | `documentation/markdown/mkdlint/mkdlint_<TIMESTAMP>/` |
| 9z `foxguard_scan` | `foxguard` v0.8.1 | `foxguard <path>` + `foxguard pqc` for CBOM | terminal / json / sarif / cbom (CycloneDX 1.6 Crypto Bill of Materials) | `documentation/linter/foxguard/foxguard_<TIMESTAMP>/` |
| 9aa `wardenscan` | `wardenscan` v2.0.0 (binary `warden`) | `warden scan` + `warden fix [--apply]` | console / json / sarif / `markdown` + `warden fix` plan (and optional `warden fix . --apply` via `--apply-fixes`) | `documentation/linter/wardenscan/wardenscan_<TIMESTAMP>/` |
| 9ab `cargo_capsec` | `cargo-capsec` v0.2.2 | `cargo capsec audit` | text / json / sarif | `documentation/rust/cargo_capsec/cargo_capsec_<TIMESTAMP>/` |
| 9ac `cargo_perf` | `cargo-perf` v0.6.0 | `cargo perf check` | console / json / sarif | `documentation/rust/cargo_perf/cargo_perf_<TIMESTAMP>/` |
| 9ad `rust_guardian` | `rust-guardian` v0.1.1 | `rust-guardian check` | human / json / junit / sarif / github / agent | `documentation/rust/rust_guardian/rust_guardian_<TIMESTAMP>/` |
Each runner ships with:
- Canonical ndaal bash boilerplate (bash >= 4.4 guard, strict mode,
deterministic locale, `NDAAL_TMPDIR` + cleanup traps, SCRIPT_NAME /
SCRIPT_PATH / SCRIPT_PATH_WITH_NAME / REPO_ROOT anchors, print_script_info
hardened footer).
- Canonical CLI surface: `-h/--help`, `-V/--version`, `-v/--verbose`,
`-q/--quiet`, `-l/--log FILE`, `--root DIR`, `--strict`, plus tool-specific
flags (`--apply-fixes` for mkdlint + wardenscan, `--include-deps` for
cargo-capsec, `--strict-rules` for cargo-perf, `--fail-fast` for
rust-guardian).
- TIMESTAMP — canonical path-safe `%Y-%m-%dT%H-%M-%SZ` form per CLAUDE.md "bash
specific". Filesystem-safe on Windows / FAT shares (no colons in directory
names).
- Sister `*.bats` scaffolded from the canonical 30-test template
(`bats-core sister test for every bash script`); each new sister passes all
structural + overlay assertions.
- Skip semantics — when the underlying binary is missing the runner self-skips
with rc=0 and an install hint (`cargo install <crate>`), so the gate stays
advisory under `--fast` rather than blocking the dev loop on absent tooling.
### `.alint.yml` configuration
Committed at the repo root, extending 11 bundled alint rulesets that map to this
polyglot repo (probed against alint v0.9.23): `oss-baseline`, `rust`,
`monorepo/cargo-workspace`, `python`, `hygiene/no-tracked-artifacts`,
`hygiene/lockfiles`, `tooling/editorconfig`, `docs/adr`, `ci/github-actions`,
`compliance/apache-2`, `agent-hygiene`, `agent-context`. 77 rules loaded.
### Mechanical CLAUDE.md-compliance sweep
In the same session, closed the long-standing "every `.sh` ships a sibling
`.bats`" gap for 40 ndaal-owned helper scripts (skills, top-level setup,
csaf/nuclei/release scattered, Ansible roles). Each got a sister `.bats`
scaffolded from the canonical 30-test template.
Vendored upstream collections under
`tools/ansible/collections/ansible_collections/community/general/` are now
codified as EXEMPT from the sister-bats requirement in both `CLAUDE.md` and
`skills/bats/SKILL.md`; the `scaffold-bats.sh` helper enforces the exemption
with a case-match skip + dedicated counter.
The 7 `test/*` harness runners (bruno, loadtest, sqllogictest, sqlmap, testssl
×3) got a "mechanical with care" preamble rewrite: SPDX header reordered to
lines 3–5, `# ndaal Bash Boilerplate` tagline added, bash >= 4.4 version guard
inserted before strict mode, deterministic `LC_ALL` / `LANG` / `LC_COLLATE` /
`TZ` exports added before `umask 077`, `SCRIPT_NAME` / `SCRIPT_PATH_WITH_NAME`
anchors added under a `# Script body starts here.` marker, plus
`fix_verbose_flags.py` applied (mkdir / rm `-v` enforcement). Net effect:
structural bats failures per file went from 6–8 → 1–4.
### 30th canonical bats test — SCRIPT_DIR / script_dir ban
The off-pattern `SCRIPT_DIR` / `script_dir` variable names are now repo-wide
forbidden via a new hard-FAIL bats test (no skip arm). Canonical anchors per
CLAUDE.md "bash specific" are `SCRIPT_NAME` / `SCRIPT_PATH` /
`SCRIPT_PATH_WITH_NAME` / `REPO_ROOT` — nothing else. 30 `.sh` files renamed
mechanically; 12 had a redundant `REPO_ROOT` re-declaration that was breaking
runtime under `set -e` (a latent bug surfaced by the new test) — deduped.
### Release pipeline status
`scripts/release_pipeline.sh` ran in the background during this session.
Verdict:
- Step 1 `build_all_targets`: PASS (4 triples × 8 binaries).
- Step 2 `create_release`: PASS (tarballs + 5-algorithm checksum sidecars:
SHA-256, SHA-512, SHA3-512, BLAKE3-512, SHAKE256-512).
- Step 3 `create_cpu_optimized`: FAIL — 5 of 6 CPU profiles PASS
(`x86_64-apple-darwin` variants + `apple-m1` `aarch64-apple-darwin`); the
`apple-m2` profile FAILS at `cc-rs` compiling `libgit2/checkout.c` for
`arm64-apple-macosx` with `-mmacosx-version-min=26.5` — the configured min
macOS target is unreleased (current is 15.x Sequoia), so the C compiler
rejects it. Profile config fix pending.
`RP_FAIL_FAST=1` was set, so the pipeline aborted after Step 3. Steps 4–6
(Ansible binary refresh, restart, screenshots, verify) were not reached. Output:
`documentation/release_pipeline/release_pipeline_2026-05-18T15-54-42Z/`.
## v0.1.45 — 2026-05-17 (Dashboards dropdown rewrite + 4 quality gates)
Five-strand cycle: a CSS-only UX rewrite of the Dashboards navbar dropdown that
pulled 145 vendor items back inside the viewport on typical laptops, two new
test-orchestrator scripts (`test_html.sh`
- `test_fuzzing_targets.sh`) with SARIF 2.1.0 output, four new quality-gate
wirings, a `Cache-Control: no-store` policy on every HTML response, and a
`CLAUDE.md` Python tooling section that mandates a 7-tool sweep. Twelve
commits on `claude/hungry-rhodes-5caad8`.
### Fixes
- **Dashboards dropdown wrong layout on 1366 px laptops** — pre-v0.1.45 CSS used
three fixed-breakpoint media queries (1 col / 2 cols / 9 cols at 576 px and
992 px) plus a `min-width: 100rem` (~1600 px) clamp on the 9-col path. On a
typical 1366 px laptop the 100 rem rule pushed the dropdown off-viewport and
forced a page-level horizontal scrollbar. The fix replaces all three media
queries with a single fluid rule that the browser resolves per-viewport:
```css
width: clamp(13rem, calc(100vw - 2rem), 120rem);
column-width: 10rem;
column-gap: 0.75rem;
min-width: 13rem;
max-width: calc(100vw - 2rem);
```
Effective layout: 1 col at 360 px (phone) → 8 cols at 1366 px → 11 cols at
1920 px → 12 cols capped at 4K. The 10 rem column width was tuned in
`v0.1.45b` after operators reported items below the OSV section (`rust`,
`python`, `swift`, `packagist`, `pub-dev`, `ubuntu`, `bitnami`) sitting below
the viewport bottom — narrower columns produce shorter rows that fit a typical
laptop window. Slugs back-quoted to avoid the
`markdownlint MD044/proper-names` false-positive on the lowercase `ubuntu`
slug.
- **Stale HTML cache on dev reload** — vl-web previously sent no `Cache-Control`
directive on HTML responses, so browsers heuristic-cached the dashboards page
indefinitely. A server rebuild looked like "the fix isn't in the page" until
the operator hard-refreshed. Every Askama-rendered HTML response
- every `html_response()` call now carries `Cache-Control: no-store`; the
policy is documented inline in `crates/vl-web/src/template_response.rs` so a
future refactor cannot silently drop it. Static assets under `/static/`
retain their own caching headers (hashed paths).
### New tests
- **`tests/scripts/test_fuzzing_targets.sh`** (LIVE — gate 57a). Orchestrates
cargo-fuzz across all 85 targets in 4 fuzz dirs (vl-core 46 + vl-web 31 +
vl-feeders 7 + root 1). Default 60 s per target, configurable via
`--duration`. Standard CLI surface
(`-h / -V / -v / -q / -l FILE / -d N / -b / -c CRATE / -t TARGET`) and a
35-test bats sister. Reports land under `documentation/rust/fuzzing/<ISO>/`
with three companion files: `summary.txt` (human), `results.tsv`
(programmatic), and `report.sarif.json` (SARIF 2.1.0 — CI security-tab
compatible) plus per-target libFuzzer logs.
- **`tests/scripts/test_html.sh`** (—fast — gate 9t). HTML + inline-JS lint
sweep wrapping three frontend linters per CLAUDE.md "check every change of
html / JS / TS with htmlhint, oxlint and fta — do not ignore warnings". Each
linter emits every output format it supports:
| Tool | Formats | Path |
| -------- | ------------------------------------------------------------------------- | ------------------------------------ |
| htmlhint | 8 (unix sarif `markdown` junit json html compact checkstyle) | `documentation/html/htmlhint/<ISO>/` |
| oxlint | 10 (checkstyle default agent github gitlab json junit sarif stylish unix) | `documentation/html/oxlint/<ISO>/` |
| fta | 3 (table csv json) | `documentation/html/fta/<ISO>/` |
Inline `<script>` blocks are extracted from every `.html` file into a temp
`.js` fixture so the JS/TS-only linters can lint them too. Same 35-test bats
sister shape as `test_fuzzing_targets.sh`.
- **`crates/vl-web/tests/test_dashboards_navbar.rs`** — 6 new regression tests
pin the v0.1.45 fluid-CSS contract: fluid `column-width: 10rem`,
`width: clamp(13rem, …, 120rem)`, `min-width: 13rem`, no `min-width: 100rem`
(the pre-v0.1.45 bug), no fixed `columns: N` rule, no
`@media (min-width: 992px)` block. A 7th test pins that all VendorSpec slugs
still render after the visual rewrite. Each test asserts a specific historical
failure mode in its docstring per CLAUDE.md Rule 9.
### New quality gates
`scripts/quality_gates.sh` grew four new wirings in this cycle, landing in the
order they were authored:
- **9r `gitleaks_scanner`** — upstream `gitleaks` secrets scan (companion to
gates 10 `betterleaks` and 11 `leaktor`). In `ALL_GATES`; runs under `--fast`.
- **9s `opengrep`** — multi-language SAST sweep wrapping the `opengrep` semgrep
fork. 6 languages × 3 output formats × the curated ruleset bundles documented
in `skills/opengrep/SKILL.md`. In `ALL_GATES`; runs under `--fast`.
- **9t `html_linters`** — wraps the new `test_html.sh` above.
- **9u `endpoint_lists_drift`** — read-only drift detector for the three
VENDOR_SPECS-derived endpoint arrays (`PAGE_ROUTES` in
`test_testssl_endpoints.sh`, `ENDPOINTS` in `test_tls_endpoints.sh`,
`URLS_DASHBOARDS` in `capture_screenshots.sh`). Calls
`scripts/regenerate_endpoint_lists.sh --check`; fails the run when a
VendorSpec row lands without the endpoint arrays being regenerated. In
`ALL_GATES`; runs under `--fast`.
- **9v `feed_recent_25`** — recent-25 probe across every live source. Iterates
whatever `GET /api/v1/vulnerabilities/sources` reports (dynamic discovery — no
hardcoded feeder list to drift) and fetches up to 25 recent entries per
source, plus a `test_unknown_fields.sh` invocation after each source to catch
schema drift. In `LIVE_GATES`; needs vl-web listening on
`https://localhost:8080`.
- **57a `fuzzing_targets`** — wraps the new `test_fuzzing_targets.sh` above. In
`LIVE_GATES`; wall-clock 90+ min including cold compiles.
### Scripts
- **`scripts/regenerate_endpoint_lists.sh` — new `--check` flag.** Wraps the
existing dry-run path so the Python rewriter exits 7 (instead of 0) when a
target file would change. The bash `main()` tallies the per-target drift count
and exits 1 with a clear "run regenerate_endpoint_lists.sh to fix" message. CI
gate 9u above hangs off this contract.
### Documentation
- **`CLAUDE.md` "Python specific" section rewritten as a 7-tool sweep**: ruff +
bandit + pip-audit + vulture + radon (cyclomatic + maintainability) + opengrep
\+ OWASP ZAP. Includes the canonical radon CC aggregation pipeline with
grade-letter histogram (A is best, F is worst) and the quality-gate
cross-reference: reject any function with CC grade C or worse (≥ 11 branches —
matches the existing "Branch points > 10" threshold) or MI rank C (\<10 —
matches "Function length > 50 lines").
- **`CLAUDE.md` "bash specific" section** — added `log_error()` helper to the
canonical logging-helpers boilerplate alongside the existing `log_info` /
`log_warn` / `log_skip` trio.
### Maintenance
- **VERSION overlay predicate tightened across 128 `.bats` files.**
Sed-substitution `VERSION="[^"]*"` → `VERSION="[0-9]` in every sister test
plus `skills/bats/SKILL.md` and `skills/bats/assets/template.bats`. Scripts
that compute VERSION dynamically (`VERSION="$(resolve_version "${1:-}")"`) now
cleanly skip all four VERSION tests instead of failing with partial-match
false positives.
- **51 mechanical verbose-flag fixes across 19 `.sh` files**: `mkdir -p -v`,
`chmod -v`, `rm -f -v` (non-recursive), `mv -v`, `cp -f -p -v` per CLAUDE.md
"Canonical filesystem-mutation flags". Surfaced by the bats verbose-flag
overlay.
- **Cargo formatting + Cargo.toml reorder sweep** — 46 `.rs` files normalised by
`cargo fmt` (line-wrapping around long function signatures, closure arrow
placement, cmd-sub bracket alignment), 4 Cargo.toml files reordered by
`cargo-fmt-toml` (description / authors / version / edition / rust-version /
license / publish in canonical order), 3 fuzz Cargo.lock refreshes,
`supply-chain/imports.lock` peer-audit refresh.
- **Clippy zero-warning state retained.** Two autofix rounds applied across the
workspace; the final
`cargo clippy --workspace --all-targets --all-features -- -D warnings` gate is
clean (only finding is a Cargo.toml manifest-key warning, not a Rust lint).
### Out of scope — flagged for follow-up
- `rust-doctor` full-workspace scan (18-35 min) is queued behind the in-flight
release rebuild and will land in v0.1.46 alongside any findings it surfaces.
## v0.1.44 — 2026-05-14 (REPO_ROOT climb-count fix + dylint revert)
Two-strand maintenance cycle following the v0.1.43 ship: a one-line bug fix in
every test script's path computation, and a documented revert of an in-flight
dylint activation attempt that hit an upstream tooling incompatibility on macOS.
### Fixes
- **`REPO_ROOT` climb count wrong in 82 test scripts.** Main's
canonical-template sweep (commit `0e6ca7fc4`) copy-pasted the CLAUDE.md "bash
specific" boilerplate verbatim — including the example `..` climb — into every
`vulnerability-lookup-rs/tests/scripts/test_*.sh`, skipping the inline-comment
instruction that "scripts under `<sub>/tests/scripts/` need `../../..`".
Result: every test script's `REPO_ROOT` resolved to
`vulnerability-lookup-rs/tests/` instead of the worktree root, and per-tool
output landed at the double-nested path
`vulnerability-lookup-rs/tests/vulnerability-lookup-rs/documentation/...`
rather than the canonical `vulnerability-lookup-rs/documentation/...`.
Live-verified by running `test_cargo_fa.sh` before and after. CLAUDE.md itself
was correct (the climb-count adjustment comment is right there) and stays
unchanged.
### Reverts
- **`[workspace.metadata.dylint]` block removed from `Cargo.toml`.** Operator
added the trailofbits/dylint examples library to activate the
`test_cargo_dylint.sh` gate (commit attempt net-zero — added then reverted).
Compile pipeline hit an upstream `dylint_driver v6.0.0` incompatibility with
the host's newer git, which rejects the `extensions.refstorage` extension. The
bundled libgit2 in git2-rs (which dylint_driver uses internally) does not yet
recognise the `refstorage` repo extension that git 2.45+ emits in
`~/.cargo/git` checkouts. Every dylint library — regardless of pattern —
triggers dylint_driver's build, so the bug can't be worked around with a
narrower lint set. Gate is back to its clean "no library configured" self-skip
until upstream dylint_driver fixes the libgit2 dep.
### Maintenance
- Merged in 9 commits from main (commits `0e6ca7fc4` → `2782162e0`) covering:
canonical CLAUDE.md head sweep across all `.sh` + `.bats`, year-range
`2025,2026`, new `skills/bats/{scripts,assets,references}/` subfolders, SPDX
header re-ordering after shfmt+shellcheck tests, removal of orphan
`test_taudit.bats`.
- Version banners refreshed across all source-of-truth files: workspace
`Cargo.toml`, 6 READMEs (EN/DE/FR × root+workspace), 4
Administrator/Developer/User/API guides, 4 Ansible role default +
install_from_package files, 3 macOS install scripts, Bruno development
environment.
### Out of scope — flagged for follow-up
- Ansible role binary refresh (`.deb` / `.rpm` packages renamed from
`vl-{cli,web}-0.1.43-*` to `-0.1.44-*` with regenerated 3-algorithm sidecars).
Produced by `scripts/update_ansible_role.sh` after a fresh `release/v0.1.44/`
build; will land in a follow-up commit after the next release-pipeline run.
- The 31 fuzz targets that failed to compile during the overnight all-84 sweep
(corpus or fuzz `Cargo.toml` drift, not crashes). No crash signatures
detected; investigation deferred.
## v0.1.43 — 2026-05-13 (gsd dashboard + SHA-validation gate + footer sweep)
Three-strand cycle: a missing dashboard surfaced for the long-running `gsd`
feeder, a defensive SHA-validation gate that unsticks ten git-based feeders, and
a uniform `print_script_info` footer rolled out across 87 shell scripts. Seven
commits on `claude/vigilant-proskuriakova-981064`.
### Fixes
- **9 git-based feeders silently stuck on seed timestamp.** `gsd`,
`bitnami_vulndb`, `cnvd`, `drupal`, `cvelistv5`, `fstec`, `emb3d`, `github`,
and `osv_golang` were carrying the initial seed timestamp through the
`last_commit` field — the feeder runner then treated that timestamp string as
a git SHA on the next cycle and never imported any new data. Added a defensive
`is_valid_git_sha()` helper in `crates/vl-feeders/src/git_helper.rs` that
rejects any value that is not exactly 40 lowercase ASCII-hex characters before
it reaches `git fetch`, so the feeder falls back to "no prior SHA, full clone"
instead of looping forever on a bogus ref. Each of the 9 feeders gates the
incremental-update path with
`&& crate::git_helper::is_valid_git_sha(last_sha)`. (`csaf_ndaal` stores
RFC-3339 timestamps by design and does not call this helper.) Empirical
first-cycle counts after restart: NVD 3 275 / gsd 119 / bitnami_vulndb 90 rows
imported (previously stuck at 0–2 forever).
- **`/dashboards/gsd` (NEW route).** The `gsd` feeder had been running headless
for 24+ hours without a UI surface; the `VENDOR_SPECS` table was at 96 entries
and the dashboard map had no `gsd` slug. Added the row between `github` and
`go` (alphabetical), category `VulnerabilityFeed`, icon
`bi-shield-fill-check`. Test assertion renamed
`test_vendor_specs_cover_ninetyseven_slugs` (was `_ninetysix_`).
- **Broken `print_script_info` footer printf in 6 scripts +
`scripts/quality_gates.sh`.** Trailing `'%s '` format string (literal trailing
space, no `\n`) and missing escape sequences caused one-line-concatenated
footer output that obscured the per-script summary. Canonicalised to `%s\n`.
### Added
- **gsd archive fallback** — scheduled at `process_start + 2h17m`, fires if the
normal `gsd` feeder produced fewer than 100 rows on the previous cycle.
Verifies the SHA3-512 sidecar of `dumps_archive/gsd.7z` before extracting; on
success it cleans the existing `gsd` rows, extracts the NDJSON, imports, and
asserts the post-import row count equals the archive count (count-equal gate,
not range gate). Downloads `gsd.7z` + `gsd.7z.sha3-512` from GitLab at commit
`1caad822901fd12dd464762fddc9c4cc788fc738` when the archive is absent from the
local `dumps_archive/` tree.
- **Unmaintained-data-source banner on `/dashboards/gsd`.** Upstream
`cloudsecurityalliance/gsd-database` has been abandoned since 2024-04-29; the
dashboard now ships a Bootstrap warning above the stats strip explaining that
the data is a historical snapshot. Plumbing: new
`VendorSpec.maintenance_note: Option<&'static str>` field on
`crates/vl-web/src/routes/vendor_dashboard.rs::VendorSpec`, rendered when
present.
- **`gsd` row added to `VENDOR_SPECS`** between `github` and `go`
(alphabetical), category `VulnerabilityFeed`, icon `bi-shield-fill-check`,
`maintenance_note` populated.
### Style
- **`print_script_info()` canonicalisation across 87 scripts.** 75 test scripts
under `tests/scripts/test_*.sh` and 12 scripts under `scripts/*.sh` brought to
the canonical end-of-script footer (12+4+2-line rationale comment block,
`%s\n` escape sequences, single-quoted format strings, double-quoted data
arguments). No behaviour change for callers that already parsed the existing
logs; operators that relied on the broken trailing-space variant see the
per-line wrap as intended.
- **Ansible role binaries refreshed** under
`tools/ansible/roles/{nvulnlookup,nvulnlookuptesting}/files/` with the v0.1.43
build; new `.rpm` packages alongside the existing `.deb` set.
### Verified
- Workspace `cargo check --all-features` clean after the four merge-conflict
resolutions (`storage.rs` raw-bytestring, `cve5.rs` `const fn` SSVC,
`vendor_dashboard.rs` test fn count, `test_no_duplicate_feeder_urls.rs`
case-insensitive extension match).
- First post-restart feeder cycle imports 3 275 NVD rows + 119 gsd rows + 90
bitnami_vulndb rows, confirming the SHA-gate unsticks the loop.
## v0.1.42 — 2026-05-10 (HATEOAS + build pipeline + editorial PDFs)
Shipped 2026-05-10 — claude/vigilant-proskuriakova-981064 merged into main at
`f9bd3fd76`. Release artefacts under `vulnerability-lookup-rs/release/v0.1.42/`:
8 cross-compiled binaries (4 triples × vl-web/vl-cli), 4 platform tarballs, 8
.deb + 8 .rpm packages, all with 3-algorithm SHA sidecars + aggregate
`SHA-{256,512,3-512}SUMS.txt` manifests. Ansible role binary blobs refreshed
under both `tools/ansible/roles/{nvulnlookup,nvulnlookuptesting}/files/`.
Carried so far:
- **rust-doctor 100 / 100** — error sweep removed the blocking
`std::fs::read_to_string` inside the async Nuclei walker; six warning families
(`manual_let_else`, `map_unwrap_or`, `duration_suboptimal_units`, three
`must_use_candidate`, `unreadable_literal`) collapsed in a single mechanical
pass.
- **`dirtyfragdetection` Ansible role** — new layered host-based detection for
the *Dirty Frag* (xfrm-ESP / RxRPC page-cache write) class of Linux kernel
vulnerabilities. auditd 72-rule drop-in (paired b32/b64, MITRE ATT&CK keyed) +
Falco-0.43 validated rule drop-in. Apache-2.0, Debian 12/13 + AlmaLinux
8/9/10. Vagrant Debian 13 smoke-test passes (converge → idempotence →
tests/test.yml → spot checks).
- **Per-CVE enrichment cards + bundle / per-source JSON endpoints** —
`/vulnerability/{id}` now renders one Bootstrap card per available enrichment
source (CAPEC, Debian-security, GCVE, EPSS, CISA-KEV, Nuclei, GitHub, OSV: Go
/ Maven / npm / NuGet / Packagist / Pub.dev, PySec — 14 sources). Empty
sources are filtered out; an additional aggregated card links to
`/api/v1/vulnerability/{id}/enrichment` (every available surface in one JSON)
and the per-source endpoint `/api/v1/vulnerability/{id}/enrichment/{source}`.
- **`/dashboards/cve-vs-github` (NEW)** — flags every CVE whose CVSS v3 base
score in the GitHub Advisory Database (OSV schema) disagrees by at least 0.1
with the canonical CVE source (NVD, cvelistv5, fkie_nvd). Five chart blobs
(`stats`, `by_severity`, `by_year`, `recent`, `recent_all`) recomputed every
feeder cycle. Raw passthrough at `/api/v1/cve-vs-github/{key}`.
- **`/dashboards/kev-ransomware` (NEW)** — surfaces the silent CISA KEV
`knownRansomwareCampaignUse` flips tracked by the new Greynoise feeder
(`crates/vl-feeders/src/feeders/kev_ransomware.rs`). Five chart blobs
(`stats`, `by_year`, `by_vendor`, `recent`, `recent_all`); raw passthrough at
`/api/v1/kev-ransomware/{key}`.
- **Bug fix — `/sightings` drill-down 200 fallback** — `vulnerability_detail`
previously returned 404 when a CVE was listed on `/sightings` but not yet
indexed in the canonical vulnerabilities table (e.g. CVE-2026-41417). It now
renders a 200 placeholder template with whatever enrichment cards are
available; only returns 404 when the CVE has zero data anywhere. 5 new unit
tests pin the contract for both the 200 and 404 branches across five canonical
CVE id shapes.
- **Imprint page (German legal notice § 5 TMG / § 10 MStV)** — new `/imprint`
route, footer link, and Info-dropdown entry. 6 unit tests cover the
legal-entity facts.
- **German alias redirects** — 14 redirect routes including `/impressum`,
`/datenschutz`, `/sicherheit`, `/lizenz`, plus trailing-slash variants for
every dialog page.
- **Right-corner Info dropdown dedupe** — removed an accidental duplicate Info
dropdown that shipped earlier in the cycle.
- **Reference implementation example** —
`scripts/example_reference_implementation_for_api_access.py` now exercises
every v0.1.42+ surface end-to-end, including a Log4Shell (CVE-2021-44228) demo
that walks the canonical record + aggregated enrichment bundle + 14 per-source
endpoints + the two new dashboard JSON endpoints (26 calls per pass). 13 new
pytest classes in the companion test file (151 tests total, 138 existing + 13
new). ruff clean.
- **Dependency hygiene** — `cargo outdated --workspace`: *"All dependencies are
up to date, yay!"* (5 crates, 300+ direct + transitive deps).
- **HATEOAS conformance — bundle + per-source enrichment endpoints emit
`_links`** — `vulnerability_enrichment_per_source` and
`vulnerability_enrichment_bundle` in `crates/vl-web/src/routes/enrichment.rs`
now wrap their `vuln_id`/`enrichments`/`data` payload in the same HAL
`_links.self` envelope every other JSON endpoint already uses. Closes the only
failing assertion in `tests/scripts/test_hateoas_compliance.sh` §10
(Cross-resource discoverability — *Enrichment has no \_links*) — gate now
reports 57 PASS / 0 FAIL. Bruno collection extended in
`test/bruno/collections/ enrichment-bundle/` with `_links.self.href`,
`vuln_id`, and source-slug echo assertions on every 200 response.
- **htmlhint Askama-syntax false positives silenced** — added scoped
`<!-- htmlhint spec-char-escape:false -->` directives on six templates
(`pages/{index,kev,vulnerability}.html` +
`partials/{recent_list,epss_recent_rows, cve_vs_github_recent_rows}.html`).
htmlhint's HTML parser misreads
`{% if cond %}disabled aria-disabled="true"{% endif %}` inside `<button>`
start-tags as text-with-special-chars, then mis-flags the next legitimate
angle bracket as an unescaped `<` / `>`. The rendered output is valid HTML;
only the static lint stage was confused. 73 errors → 40 errors (the remaining
40 are `doctype-first` on Askama child templates extending
`layouts/base.html`, a separate class of false positive untouched by this
fix).
- **`test_cargo_hakari.sh` + `test_sql_query_analyzer.sh` ERR-trap
localization** — both gates emitted misleading
`ERROR: …: command failed (exit=1)` echoes that did NOT correspond to any real
failure, AND `test_sql_query_analyzer.sh` was silently exiting 0 on every run
because its inherited `errexit` semantics aborted the rc-capture subshell
before the rc file was written, so the parent's aggregation loop always saw
rc=0. Fix: add `trap - ERR` + `set +o errexit` / `set +e` inside the affected
helpers / subshells. Behaviour change — the SQL analyzer gate now correctly
exits 1 when findings exist (was always 0), matching its own `[FAIL]` log
message which had been emitted but never propagated.
- **`build_guides.py`** — new Python renderer (alongside `build_principles.py`)
that walks four documentation roots (`vulnerability-lookup-rs/documentation/`
\+ three Ansible role docs trees), parses each `*_Guide.md` to HTML via
`python-markdown` + `extra` / `fenced_code` / `codehilite` / `toc` /
`sane_lists`, wraps in the same editorial scaffold as `build_principles.py`
(cover plate with title kicker + stats strip + footer; TOC page; body chapter;
colophon; paper-tone palette `#F1ECE2 / #0E1A24 / #C8321F`), and prints to A4
portrait via Playwright + Chromium in three typography variants per guide:
`_original.pdf` (Fraunces + Instrument Sans + JetBrains Mono), `_roboto.pdf`
(Roboto + Roboto Mono), `_source.pdf` (Source Serif 4 + Source Sans 3 + Source
Code Pro). 16 guides × 3 variants = 48 PDFs per invocation. Bootstrap:
`python3 -m venv .venv-guides`
- `.venv-guides/bin/pip install markdown playwright`
- `.venv-guides/bin/python3 -m playwright install chromium`. CLI flags:
`--only PATH` (repeatable), `--variant {original,roboto,source}`
(repeatable), `--no-cover`, `--dry-run`.
- **`mkdir -p -v` + canonical bats sister-test sweep** — 103 in-tree
`tests/scripts/test_*.sh` / `test_*.bats` files now (a) emit a single
`mkdir: created directory '...'` line per gate so operators can grep the run
log for the per-run report paths without parsing every `printf`, and (b) every
`.bats` asserts the canonical 7-test contract from CLAUDE.md
(exists+executable, SPDX header, `bash -n` parses, ndaal trap set installed,
`errexit/errtrace/nounset/pipefail/inherit_errexit` enabled,
`print_script_info` defined, self-skips cleanly when the underlying tool is
missing). Many gates additionally got the CLI-options block (`--help` /
`--version` / `-h` / `-H` / `-V`) guarded by `timeout(1)` so a regression that
lets a flag fall through into the side-effecting pipeline trips on rc=124
instead of stalling the suite.
- **clippy `--fix` mechanical sweep** — auto-applied across 12 vl-web routes +
vl-feeders modules (`map_unwrap_or`, `option_if_let_else`, `manual_let_else`,
`redundant_closure_for_method_calls`, `missing_const_for_fn`,
`duration_suboptimal_units`, `needless_raw_string_hashes`,
`format_push_string`, `or_fun_call`). No behaviour change.
- **Fuzz corpus seeds committed** — 3 911 corpus files under
`crates/vl-feeders/fuzz/corpus/` accumulated across multiple
`cargo +nightly fuzz run` sessions are now tracked, so the next CI run starts
from the extended corpus instead of regenerating from scratch.
- **`test_hateoas_compliance.sh` documented** — added to `Test_Sequences.md` §12
(API testing) with the override knob `TEST_CVE=cve-2021-44228` (the default
`cve-2024-1234` may not exist in every store seed).
## v0.1.41 — 2026-05-07 (vendor-dashboard regression fix + EPSS / Nuclei dashboards)
Five-strand release:
- **Regression fix** — vendor-dashboard recent-25 tables on every CVE-5.0 source
(`/dashboards/vulnrichment`, `/dashboards/cvelistv5`, `/dashboards/ubuntu`,
`/dashboards/rust`) re-render their Severity / Published / Title columns.
Cause: `crates/vl-web/src/routes/vendor_dashboard.rs` helpers walked only
legacy CSAF / OSV paths. Fix: a new shared `crates/vl-web/src/routes/cve5.rs`
module (lifted from `home.rs`) holds the canonical CVE-5.0 / NVD / CSAF / OSV
/ EUVD field extractors; both `home.rs` and `vendor_dashboard.rs` delegate to
it. As a bonus the lifted `extract_title` now reads `/containers/cna/title`
and `/containers/cna/descriptions[lang=en].value`, which the pre-v0.1.41
`home.rs::extract_title` was also missing.
- **CVSS v4 in NVD feeder** — `extract_cvss_info` now prefers `cvssMetricV40`
over v3.1 / v3.0 / v2 when the upstream record carries it. The denormalised
severity / score sidecar that vendor pages render on the recent list now
reflects the most current CVSS family.
- **Nuclei feeder** — `nuclei_enrichment.rs` persists the full template YAML
body and parsed `info.severity` alongside the existing `nuclei_template` path.
The new Nuclei dashboard (below) uses the parsed severity at index time and
the raw YAML at JSON-download time.
- **Two new dashboards** —
- `/dashboards/epss` — total / by-year / severity-buckets / decile histogram /
recent-25 + JSON download, backed by `EpssDashboardFeeder` writing six
aggregates under the `"epss:"` slug of `ENRICHMENT_DASHBOARDS`. The
CVSS-cross-tab cards are deferred to v0.1.42 (the v0.1.40 EPSS feeder
dropped `cvss3` from the per-row payload — see ndaal-sa-2026-038).
- `/dashboards/nuclei_templates` — total / by-severity / recent-25 with
raw-YAML download, backed by `NucleiDashboardFeeder` writing four aggregates
under the `"nuclei:"` slug.
- **CSAF advisory** — ndaal-sa-2026-039 documents the vendor-dashboard
regression and the lift, with all three sidecars (`.sha-256`, `.sha-512`,
`.sha3-512`).
Test coverage: 215 unit tests in vl-web pass; 309 unit tests in vl-feeders pass.
All pre-existing CSAF / OSV fixture tests still pass unchanged.
## v0.1.40 — 2026-05-07 (quality cycle)
Six-strand quality cycle: storage soundness coverage, SPDX header policy,
KEV/EPSS API client examples, audit-rules audit, binary rebuild, plus an EPSS
feeder data-source correction (see **Fixes** below).
### Fixes
- **EPSS/KEV enrichment feeder data source.**
`crates/vl-feeders/src/feeders/epss_kev_enrichment.rs` previously fetched
`https://raw.githubusercontent.com/jgamblin/KEV_EPSS/main/ epss_kev_nvd.csv` —
a community-maintained GitHub CSV that joins CISA KEV ∩ EPSS, i.e. only ~1 200
CVEs. The 300 000-plus CVEs that have an EPSS score but are NOT on the CISA
KEV list were silently absent from the enrichment table, and every imported
row was hard-coded with `kev: true` regardless of actual KEV membership.
Switched to the official FIRST.org bulk dataset
(`https://epss.cyentia.com/epss_scores-current.csv.gz`, ~300 000+ entries,
daily refresh, gzip-compressed) and cross-references each row against a
freshly fetched CISA KEV catalog
(`https://www.cisa.gov/sites/default/files/feeds/ known_exploited_vulnerabilities.json`)
so `kev: true` is set if and only if the CVE is currently on the KEV list.
KEV-fetch failure degrades to `kev: false` everywhere — strictly safer than
the previous hard-coded-true behaviour.
Net effect on a fresh run: enrichment table grows from ~1 200 rows to ~300
000+ rows, and the `kev` flag accurately reflects CISA KEV membership.
Side-effect: the `cvss3` field is no longer carried in the enrichment payload
(the FIRST.org bulk dataset only ships `(cve, epss, percentile)`). Consumers
that need CVSS should join against the dedicated CVSS enrichment table.
CSAF advisory `ndaal-sa-2026-038` documents both defects, the fix landing, and
the operational impact assessment.
### Added
- **`Build Release` chapter** in
`vulnerability-lookup-rs/documentation/Administrator_Guide.md` walking through
the canonical 3-stage release pipeline (`build_all_targets.sh` →
`create_release.sh` → `update_ansible_role.sh`) plus the four verification
gates (`verify_static_assets_debian13.sh`,
`test_example_reference_implementation_for_api_access.py`, optionally
`verify_static_assets_molecule.sh` and
`verify_role_tests_debian13_selected.sh`).
- **`Bash Script Inventory` chapter** in the same admin guide: per-script
reference for every committed bash script (115 scripts), grouped by directory,
captioned with **Purpose** + **Usage** + a collapsible verbatim-from-header
**Full description** for the 33 scripts whose own header carries one.
- **`Quickstart: Filling the Database` chapter** in the same admin guide:
3-stage primer for the
`fetch_dumps.sh → compress_dumps.sh → import_and_verify_dumps.sh` pipeline.
- **5 missing role templates** under
`tools/ansible/roles/nvulnlookup/templates/etc/systemd/`:
`auditd_rules_reload.service.j2`, `auditd_rules_reload.timer.j2`,
`auditd_dynamic_root_rules.sh.j2`, `auditd_dynamic_root_rules.service.j2`,
`auditd_dynamic_root_rules.timer.j2`. Modeled on the role's hardened-systemd
profile (NoNewPrivileges, ProtectSystem=strict, MemoryDenyWriteExecute,
capability-bounded set). Fixes a pre-existing bug where two
`tasks/timer/timer_auditd_*.yml` files referenced templates that were never
committed.
- **Verify-runner script** `scripts/verify_role_tests_debian13_selected.sh`:
boots a Debian 13 cloud-image VM via Vagrant, deploys the role with hardening
off, then runs `tests/test.yml` with only the user- selected toggles enabled
(default: cargo_license + sqlmap + betterleaks + lonkero_scan + every
service_handling subtest).
- **Bats `meta-runner`** `vulnerability-lookup-rs/tests/scripts/test_bats.sh`:
sweeps every sister `.bats` file under three directories and archives TAP /
JUnit XML / stderr / SUMMARY.tsv / summary.txt under
`documentation/shell/bats/<ISO-8601-utc>/`. Per-file timeout(1) wrap (default
60 s) prevents live-server probes from stalling the sweep.
- **CLI options** added to `checkin_git.sh`: `--help`/`-h`/`-H`
- `--version`/`-V` short-circuit BEFORE any side-effecting setup so a stray
flag cannot trigger the 19 k-line pipeline. Sister `checkin_git.bats` ships
14 structural + option- handling tests (each option invocation wrapped in
`timeout 10s` so a regression that lets a flag fall through trips on rc=124
instead of hanging the suite).
- **Uniform Usage & Purpose trailer** appended to every committed bash script
(115 files), placed immediately above the canonical `print_script_info()`
block so a `tail -25 <script>` always shows purpose + usage + the standard
end-of-run logger.
- **`print_script_info()` block** added to the 6 scripts that lacked it
(`markdown2pdf.sh`, `scripts/render_mermaid.sh`, `scripts/render_plantuml.sh`,
`vulnerability-lookup-rs/scripts/{cve_api_probe,zap_scan, testssl_endpoints}.sh`)
so every script in the repo now follows the canonical CLAUDE.md template.
- **Per-test toggle wiring** for both Ansible role `tests/test.yml` masters:
every `import_playbook` is now guarded by
`nvulnlookup_run_test_<slug> | default(true) | bool` so an operator can
suppress any subset of tests with `-e nvulnlookup_run_test_<slug>=false` at
the CLI. 53 missing `test_<slug>.yml` playbooks created. 11 extra
`service_handling` subtests wired in via inline-play `import_tasks` wrappers.
- **`bats-core sister test for every bash script` chapter** in `CLAUDE.md`
mandating the 7-test structural template + the CLI-option-test template (both
extracted from `checkin_git.bats` and `test_cargo_geiger.bats`) for every new
or updated `.sh`.
- `VulnStorage::new_in_memory()` constructor — opens an in-memory redb database
via `redb::backends::InMemoryBackend` for tests that don't need filesystem
persistence. Sixteen test files across `crates/vl-core/tests/` and
`crates/vl-feeders/src/feeders/` migrated from
`tempfile::TempDir + VulnStorage::new(path)` to the new helper (~130 call
sites simplified). CSAF advisory `ndaal-sa-2026-037` documents the refactor
and lists the per-file diff.
- `init_tables(&Database) -> Result<()>` extracted out of `VulnStorage::new`,
shared by both file-backed and in-memory constructors so the 18-table redb
schema definition lives in exactly one place.
- `#### SPDX Header` chapter in `CLAUDE.md` — copy-paste templates per file
format (Markdown, Rust, Bash × 2, Python, YAML, TOML, HTML/Askama/Jinja2,
CSS/SCSS, JS/TS, INI/.bru, Dockerfile/Makefile, PlantUML, Mermaid). Includes
"When to skip" rules (generated files, JSON, vendored content, test fixtures),
year-range guidance, and a `git ls-files | grep SPDX` audit snippet.
- Three-line SPDX header applied to `CLAUDE.md` itself (the documentation now
follows its own rule).
- `KEV_REFERENCE_BY_YEAR` (2020..=2026, five well-known CISA-KEV-tagged CVEs
each year) and `EPSS_REFERENCE_BY_YEAR` (2023..=2026) reference data +
matching `fetch_kev_reference_set` / `fetch_epss_reference_set` helpers in
`scripts/example_reference_implementation_for_api_access.py`. Wired into
`_run_one_pass` via a new orchestrator that keeps the per-pass runner under
the project cyclomatic-complexity budget (CLAUDE.md: branches ≤ 10).
### Audited
- `tools/ansible/roles/{nvulnlookup,nvulnlookuptesting}/templates/auditd-nvulnlookup.rules.j2`:
zero `-w` watch rules, ten `-a always,exit` rules per template, paired b32/b64
arch entries, every `-F key=` mapped to a MITRE ATT&CK technique ID (T1543 /
T1565 / T1059 / T1036). Already CLAUDE.md compliant — no edits needed.
### Rebuilt
- All 8 release binaries (`vl-web` + `vl-cli` × 4 triples:
`x86_64-apple-darwin`, `aarch64-apple-darwin`, `x86_64-unknown-linux-gnu`,
`aarch64-unknown-linux-gnu`). 4 release tarballs + 12 sidecars
(sha-256/sha-512/sha3-512) + 3 aggregate manifests (`SHA-256SUMS.txt`,
`SHA-512SUMS.txt`, `SHA3-512SUMS.txt`) under `release/v0.1.39/`.
- Both Ansible roles `files/` trees refreshed with the new build: 36 sidecars ×
2 roles = 72 sidecars regenerated. Per-role Molecule fixture
(`molecule/default/files/vl-web` + 3 sidecars) also refreshed.
### Verified
- `cargo test --workspace --all-features` → 881 passed / 0 failed / 143 ignored
after the storage refactor.
- `pytest scripts/test_example_reference_implementation_for_api_access.py` → 138
passed in 18.84 s after the KEV+EPSS additions.
- `verify_static_assets_debian13.sh` → 39 PNG screenshots captured through
Vagrant + Debian 13 + headless Chrome (rc=0, ~28 min wall-clock).
- `verify_static_assets_molecule.sh` 6-OS matrix re-run (in flight at the time
of writing).
## v0.1.39 (2026-05-03) — quality cycle: bash boilerplate uniform, rust-doctor 100/100
Quality release that lands four sweeping audit waves across the bash script
surface plus a clean rust-doctor sweep.
### Added
- Enhanced bash boilerplate
(`set -o errexit/errtrace/nounset/pipefail/inherit_errexit`,
`shopt -s nullglob/shift_verbose`, `NDAAL_TMPDIR` + `ndaal_cleanup` +
`ndaal_on_error` + signal-reraising traps + `_script_cleanup` hook) rolled out
to **121/121** bash scripts under our control.
- Canonical `print_script_info()` end-of-script block (per CLAUDE.md "End every
script with:") added to every script.
- Snapshot drift gate (`gate_cargo_insta`, gate 3a) wired into
`scripts/quality_gates.sh` between `cargo test` and `cargo audit`.
`cargo insta test --check` fails on any unreviewed `.snap.new`.
- 6 pinned HATEOAS snapshot baselines.
### Fixes
- Mass migration regression: 10 scripts had their bodies silently truncated by
an awk-based front-matter swap that used `umask 077` as the boundary sentinel.
Recovered from `git show` and re-migrated with a dual-sentinel helper.
- Miri kqueue failure at the source: 62 cargo-cult `#[tokio::test] async fn`
test functions across 5 vl-core integration tests had zero `.await` calls.
Converted to plain `#[test] fn`.
- IPv6 listen-bind regression: `vl-web` `format!("{ip}:{port}")` produced
`:::8080` for IPv6 wildcard. Switched to `SocketAddr::new(ip, port)`.
- rust-doctor: 539 → 89 warnings; score 99 → **100/100**. Fixed
`criterion::black_box` → `std::hint::black_box`, `uninlined_format_args`,
suppressed `missing_panics_doc` at appropriate scopes with rationale comments.
### Verified
- All 121 bash scripts pass `bash -n`; 119/121 shfmt clean; 115/121 shellcheck
clean.
- Workspace `cargo check --all-features --all-targets` exits 0.
- rust-doctor: 0 errors / 89 warnings (all out-of-scope transitive dep dupes +
deliberate clones).
## v0.1.38 (2026-04-28) — dashboard hang hotfix + dump pipeline sidecars
Hotfix cycle that closes out the v0.1.37 dashboard regression and extends the
three-sidecar (.sha-256 / .sha-512 / .sha3-512) integrity chain across the full
dump pipeline.
### Fixes
- `vl-core::storage::collect_source_index_ids` Desc path now walks the prefix
range lazily with `range(...).rev()` and breaks after `number` matches.
Previous implementation eagerly collected every entry under the source prefix
into a `Vec` before reversing, which on populated feeds (csaf_redhat,
fkie_nvd, osv_Ubuntu, csaf_Microsoft, …) hung vendor-dashboard renders for >12
s. Repaired 10 of the 12 dashboards reported broken at v0.1.37 launch.
- `vl-core::storage::count_source_index_entries` is a new helper that walks ONLY
the SOURCE_INDEX prefix range (no VULNERABILITIES lookup, no JSON parse).
`routes::vendor_dashboard::total_for_source` now uses it. Unblocks the Cisco
dashboard whose `get_last(number=5_000)` was deserialising 5 000 advisory JSON
documents per render just to call `Vec::len()`.
- `vl-core::storage::get_top_vendors_for_source` is now bounded by a 2 000-row
scan cap and walks the SOURCE_INDEX in reverse so the cap takes the
most-recent slice. Unblocks the `/dashboard` Overview page, which previously
walked the entire ~80 k-row NVD prefix per render.
- Companion advisory `csaf/2026/035/ndaal-sa-2026-035.json` (CVSS 3.1 7.5 HIGH
availability-only, CWE-407) revises from `interim` in v0.1.37 to `final` in
v0.1.38.
### Added — Dump pipeline integrity
- `scripts/fetch_dumps.sh` writes the canonical three-sidecar trio next to every
downloaded dump. Refreshes stale sidecars; summary line counts
sidecars-written and -failed separately.
- `scripts/compress_dumps.sh` writes per-archive sidecars
(`<name>.7z.{sha-256,sha-512,sha3-512}`) after the integrity test and
self-verifies them; up-to-date archives skip re-compression but still refresh
missing sidecars.
- `scripts/import_and_verify_dumps.sh` is strict-by-default. Every dump and
every archive must ship the trio and every sidecar must verify. Partial sets
(1 or 2 of 3) are always fatal; opt out of the "all three absent" trigger via
`IMPORT_STRICT_CHECKSUMS=0` for one-shot bridge runs.
- New `scripts/export_nuclei_data.sh` and `scripts/import_nuclei_data.sh` —
round-trip pair that lifts the `nuclei_templates` table from `data/vlookup.db`
to a deterministic NDJSON dump and reloads it. Stage 0 falls back to
`dumps_archive/nuclei_templates.7z` (sidecar + 7z-CRC chain validated before
extraction). Stage 5 emits three sidecars for the resulting `vlookup.db` file.
- `scripts/import_nuclei_templates.sh` schema: lives as the `nuclei_templates`
TABLE inside the shared `data/vlookup.db` (not a separate database file).
Cohabits with vl-web's tables.
- `scripts/import_nuclei_templates.sh::normalize_severity` folds five new
non-canonical severity buckets onto the canonical ladder: `highx → high`,
`cretical → critical`, `informative, unknown → info`, `potential → low`.
### Added — Documentation
- Administrator Guide § "Bulk Import (CIRCL Dumps)" rewritten with a "Provenance
and checksum limitations" subsection that names what the sidecars do and do
NOT prove (local fingerprint vs upstream attestation).
- New § "Nuclei Templates Round-Trip" covers the import/export/re-import
pipeline plus the archive fallback chain.
### Post-release quality hardening (2026-05-01..02; expanded 2026-05-02)
Twelve days of unreleased work on top of the v0.1.38 tag — captured here because
the workspace version pin is still `0.1.38`. Will fold into the v0.1.39 release
notes when that cycle opens.
#### Added — Quality gates 13-31 (19 new gates)
`scripts/quality_gates.sh` ALL_GATES grew from 22 → 41 entries. Each new gate
ships with a matching `tests/scripts/test_<slug>.sh` runner under the canonical
ndaal bash header (SPDX, strict mode, IFS=$'\\n\\t', umask 077) and self-skips
cleanly when the underlying tool is missing. Tier-1 gates run under `--fast`;
Tier-2 LIVE_GATES are skipped under `--fast`.
Tier 1 (12 fast gates):
- `cargo_nextest` — parallel test runner (gate 13)
- `cargo_cyclonedx` — Rust-native CycloneDX SBOM (gate 14)
- `typos` — typo-cli code+docs scanner (gate 15)
- `cargo_msrv` — verify pinned `rust-version` (gate 16)
- `cargo_udeps` — nightly-only, complementary to machete (gate 17)
- `cargo_public_api` — API-surface snapshots per first-party lib (gate 18)
- `cargo_outdated` — newer-version-available report (gate 19)
- `cargo_supply_chain` — publisher-visibility audit (gate 20)
- `cargo_features_manager` — feature-flag audit (gate 21)
- `cargo_spellcheck` — doc-comment spelling (gate 22)
- `committed` — Conventional Commits linter (gate 23)
- `cargo_dylint` — custom lint-plugin runner (gate 24)
Tier 2 (7 live/heavy gates, skipped under `--fast`):
- `cargo_mutants` — mutation testing (gate 25)
- `miri` — UB detector, nightly (gate 26)
- `cargo_careful` — hardened-std test, nightly (gate 27)
- `cargo_bloat` — release-binary size analysis (gate 28)
- `cargo_llvm_lines` — monomorphisation cost (gate 29)
- `cargo_bench` — Criterion benchmarks (gate 30)
- `cargo_flamegraph` — perf profile (gate 31)
#### Added — Pre-vet preflight gates 0 + 0a
Two new gates run **before every other gate** so transient network or config
issues degrade gracefully instead of breaking the REST of the sweep:
- `update_audits_toml` (gate 0) — `scripts/update_audits_toml.sh` fetches
`https://raw.githubusercontent.com/google/supply-chain/ main/audits.toml` over
a hardened-curl pipeline (TLS 1.2+, retry-all-errors, max-time 60 s),
validates the payload (size + TOML heading shape), and writes it to
`<repo-root>/audits.toml` AND `vulnerability-lookup-rs/audits.toml`.
Earlier-cycle DNS failure on the Google supply-chain URL took down vet (gate
5e); caching the file locally removes the dependency.
- `ansible_cfg` (gate 0a) — `scripts/update_ansible_cfg_project_specific.sh`
writes the canonical project `ansible.cfg` to two sibling locations,
pre-creates `~/.ansible/{log,facts_cache,tmp,cp}` at 0700, and validates each
cfg via `ansible-config dump --only-changed`. Catches an upstream-leaked typo
(`[localhost](http://localhost)` inside a Markdown link in the source cfg).
#### Fixed — rust-doctor warnings: 151 → 0
Comprehensive sweep that took the rust-doctor /
`cargo clippy -- -W clippy::pedantic -W clippy::nursery -W clippy::cargo`
warning count from **151 down to zero**.
- Consolidated 5 per-crate `clippy.toml` files (which were silently shadowing
the workspace-level config) into a single
`vulnerability-lookup-rs/clippy.toml`. Workspace-level
`allowed-duplicate-crates` and `allow-*-in-tests` flags now reach every member
crate.
- Added file-level `#![allow(...)]` blocks to all 21 integration test files at
`crates/*/tests/*.rs` covering `expect_used`, `unwrap_used`, `panic`,
`indexing_slicing`, `print_*`, `unwrap_in_result`, `panic_in_result_fn`,
`too_long_first_doc_paragraph`, `uninlined_format_args`, `too_many_lines`,
`cognitive_complexity` — patterns the workspace's `allow-*-in-tests` config
fails to silence outside `#[cfg(test)]` modules.
- Added crate-root allows for `clippy::too_long_first_doc_paragraph` to vl-web
(lib + main), vl-core, vl-feeders. Added crate-root `print_stdout` /
`print_stderr` allow to vl-cli (legitimate user-facing CLI output).
- Added `allowed-duplicate-crates` for `block-buffer`, `crypto-common`,
`digest`, `foldhash`, `getrandom`, `hashbrown`, `indicatif`, `nom`, `r-efi`,
`rand_core`, `sha2`, `syn`, `thiserror`, `untrusted`, `windows`,
`windows-sys`, `windows-targets`, `wit-bindgen`, plus the windows_aarch64,
windows_i686, windows_x86_64 family — all upstream-locked duplications in the
Rust crypto ecosystem mid-migration from digest 0.10 → 0.11.
- Pointed `vl-models/src/db.rs` at
`#[expect(clippy::expect_used, clippy::unwrap_in_result)]` with a documented
"poisoned mutex is fail-fast" rationale.
- Extracted `RouteMatch` type alias in `vl-web/src/router.rs` to clear
`clippy::type_complexity`.
#### Fixed — cargo-vet PASS (was 119 unvetted)
cargo-vet (gate 5e) flipped from FAIL (DNS error → 119 unvetted deps once DNS
resolved) to PASS via:
1. Eight `cargo vet trust --all <publisher> --allow-multiple-publishers` calls
covering `dtolnay`, `kennykerr`, `epage`, `seanmonstar`, `alexcrichton`,
`cuviper`, plus `cargo vet trust hashbrown rust-lang-owner`.
2. `cargo vet regenerate exemptions` for the residual 333 entries.
Final state: 184 fully audited, 6 partially audited, 333 exempted.
#### Fixed — `/about` page reports stale version
`crates/vl-web/src/dialog.rs` now resolves the About-page version via
`env!("CARGO_PKG_VERSION")` at compile time instead of a hardcoded literal — the
regression CSAF-035 documented for v0.1.37. Replaced the literal-parsing
`test_cargo_toml_version_matches_dialog_version` test with
`test_dialog_version_uses_cargo_pkg_version` which asserts the env!() form is
preserved (catches future regressions to a literal).
#### CSAF advisories
- `csaf/2026/035/ndaal-sa-2026-035.json` promoted from `interim` → `final` with
revision 2.0.0. Added v0.1.38 as `CSAFPID-0002` (the fixed product), added a
`vendor_fix` remediation, regenerated all three sidecars (.sha-256 / .sha-512
/ .sha3-512). Verification matrix: 20 / 20 dashboards return HTTP 200 in
0.05–2.84 s on the running v0.1.38 binary.
#### Added — Tier A test scaffolding (vl-web)
Activates four of the workspace dev-dependencies introduced in commit `e457eeb0`
that previously had zero callers:
- `crates/vl-web/tests/proptest_validators.rs` — proptest property-based
regression suite for `parse_page_param`, `is_valid_vendor_slug` ↔
`slug_to_source` consistency, `is_valid_advisory_id`, `canonicalise_cwe_id`.
4096 cases per block. Overlaps with the libFuzzer targets but with stronger
shrinking-driven small-input edge-case coverage.
- `crates/vl-web/tests/snapshot_hateoas.rs` — insta YAML-snapshot tests for
`dashboard_links`, `dashboard_envelope` (small + empty-data path),
`vendor_advisory_links`, `vuln_links`, `collection_links` (mid-page
pagination). Snapshot drift surfaces as a hard test failure during PR review.
- `crates/vl-web/benches/hateoas_envelope.rs` — first criterion bench in the
workspace. Three sample sizes (small ~1 KiB, medium ~32 KiB, large ~512 KiB)
for the JSON-mode dashboard envelope hot path. Activates gate 30
(`cargo bench`) which was previously SKIP'ing for lack of any `[[bench]]`
target.
#### Fixed — fuzz target false-positive
`crates/vl-web/fuzz/fuzz_targets/fuzz_vendor_advisory_extractors.rs` asserted
`keys.len() == 2` but `candidate_storage_keys` correctly emits 2
(case-collapsed) or 4 (case-distinct) entries. Single-byte input `[86]` (ASCII
`V`) crashed the fuzz target — production code was correct; assertion was too
strict. Updated to `assert!(len == 2 || len == 4, ...)`.
#### Refactored — Gates 1-5f extracted into per-tool runner scripts
`scripts/quality_gates.sh` had a mixed pattern: gates 0/0a/12 and 13-31
dispatched to standalone `tests/scripts/test_<slug>.sh` runners, while gates
1-5f (the older cargo-tooling gates: fmt, clippy, test, audit, deny, machete,
geiger, tree_duplicates, rust_doctor, kani, vet, semver_checks) had inline
implementations inside `gate_<slug>()` functions. Operators couldn't invoke
those gates standalone without re-running the entire `--only` dispatch.
Twelve new runners under `vulnerability-lookup-rs/tests/scripts/`:
- `test_cargo_fmt.sh` — cargo fmt --all --check
- `test_cargo_clippy.sh` — cargo clippy --workspace --all-targets --all-features
-- -D warnings + JSON sidecar
- `test_cargo_test.sh` — two-pass cargo test (lib/integration + doctests) with
QG_TEST_THREADS forwarding
- `test_cargo_audit.sh` — cargo audit with --ignore list parsed from workspace
audit.toml
- `test_cargo_deny.sh` — cargo deny check
- `test_cargo_machete.sh` — cargo machete --with-metadata
- `test_cargo_geiger.sh` — per-crate transitive-unsafe survey with the
most-restrictive flag set documented in skills/rust.md
- `test_cargo_tree_duplicates.sh` — duplicate-version bloat / hidden-risk
survey, advisory by default (QG_TREE_DUPLICATES_STRICT=1 to gate)
- `test_rust_doctor.sh` — workspace health-score capture under
`documentation/health/rust-doctor_<ISO>/`
- `test_cargo_kani.sh` — cargo kani --workspace gated behind QG_RUN_KANI=1
(nightly-only + slow)
- `test_cargo_vet.sh` — cargo vet check
- `test_cargo_semver_checks.sh` — cargo semver-checks check-release --workspace
Each runner ships:
- The canonical ndaal bash header (SPDX + strict mode + IFS=$'\\n\\t'
- umask 077).
- Self-skip semantics: clean SKIP when the underlying tool is not on PATH, with
the canonical install-one-liner in the skip message.
- Output capture under `documentation/<category>/<tool>_<ISO>/` for cross-run
diffing.
- Honoured the same env knobs the inline gate had (QG_TEST_THREADS, QG_RUN_KANI,
QG_TREE_DUPLICATES_STRICT).
- Returns the underlying tool's exit code so qg's record() machinery surfaces
the right PASS/FAIL marker.
The 12 inline gate function bodies in `quality_gates.sh` were replaced with thin
delegating wrappers:
```bash
gate_fmt() {
local label="1. cargo fmt --all --check"
banner "${label}"
local runner="${QG_WORKSPACE}/tests/scripts/test_cargo_fmt.sh"
if [[ ! -x "${runner}" ]]; then
record fmt skip "runner not found: ${runner}"
return
fi
run_cmd fmt "${label}" "${runner}"
}
```
The `gate_kani` wrapper retains its early-skip when `QG_RUN_KANI != 1` (so it
doesn't fork a subshell just to self-skip) and forwards `QG_RUN_KANI=1`
explicitly when invoking the runner.
Smoke-tested 7 of the 12 runners directly (the REST queued behind an in-flight
kani run holding the cargo lock):
```text
test_cargo_tree_duplicates.sh rc=0 (66 advisory duplicates)
test_cargo_fmt.sh rc=0 (workspace fmt-clean)
test_cargo_machete.sh rc=0 (0 unused deps)
test_cargo_audit.sh rc=0 (0 RustSec advisories)
test_cargo_deny.sh rc=0 (advisories+bans+licenses+sources green)
test_cargo_vet.sh rc=0 (workspace fully covered)
test_cargo_kani.sh rc=0 (correctly skipped: QG_RUN_KANI=0)
```
End-to-end dispatch test via `bash scripts/quality_gates.sh --only machete`:
```text
=== 5a. cargo machete ===
PASS machete 3 lines -> /tmp/quality_gates-machete.<rand>.log
====== summary ====== PASS machete 0/1 failed
```
#### Documentation
- `CLAUDE.md` "Overall quality loop" gate list extended.
- `scripts/quality_gates.uml` re-rendered via
`scripts/render_quality_gates.uml_svg_png.sh` with the new partitions for
gates 13-31. All 4 validation stages (mimetype-cli + xmllint + oxipng) green.
- Administrator Guide § "Quality-gate runner" extended with the v0.1.38+ gates
section (gates 0, 0a, 13-31) plus operator install one-liners for every new
tool.
- CHANGELOG § "Post-release quality hardening" (this section) added under
v0.1.38, will fold into v0.1.39 release notes when that cycle opens.
## v0.1.37 (2026-04-27) — quality cycle: gates, fuzzing, supply chain, sidecar verification
Quality-focused cycle. Headline: every dependency that lands in a production
binary is now signed off by a human (`cargo-vet`), every first-party crate root
carries `#![forbid(unsafe_code)]`, the fuzz matrix grew from 8 to 24 targets,
and the Ansible role no longer hard-codes a single SHA-256 hash — sidecars do
all the work.
### Added — Quality gates
- `cargo-vet check` (gate 5e) — supply-chain review, fails if any dep in
`Cargo.lock` is neither audited nor on the time-boxed
`supply-chain/config.toml` exemption list. Imports Mozilla, Google, and
bytecodealliance peer audits.
- `cargo-semver-checks` (gate 5f) — catches accidental SemVer breaks before they
ship.
- `cargo geiger` overhaul —
`--all-dependencies --all-targets --all-features --include-tests`
`--output-format Json --locked --frozen --offline --verbose`
per first-party crate; reports archived under `documentation/geiger/<ISO>/`.
- `cargo tree --workspace --all-features --duplicates` — advisory by default,
gateable with `QG_TREE_DUPLICATES_STRICT=1`.
- DAST: `nuclei` + `lonkero` integration tests against an in-process `vl-web`
subprocess.
- `betterleaks` (gitleaks-compatible) two-pass secrets scanner (gate 10) with
30+ `.gitleaksignore` path exclusions so a full-tree scan finishes in minutes.
- ZAP DAST runner (`scripts/zap_scan.sh`) — baseline, full, api modes;
OpenAPI-driven for the `api` mode.
- Schemathesis, `ffuf`, `feroxbuster`, sqlmap, sqllogictest, oha load-test,
db-schema regen, and api_bash gates wired to `--fast` so most run on every PR.
### Added — Tests + fuzzing
- 16 new `cargo-fuzz` targets covering CSAF parsing, sidecar hash decoders,
import-file walkers, tracking-id parsing, provider-metadata.json, plus 2 stale
`saturating_add` fixes surfaced by the new targets. Total: 8 → 24 fuzz
targets.
- `cargo kani --workspace --no-unwinding-checks` — 18 model- checking proofs
across `vl-core` + `vl-feeders`; nightly only (`vl-web` blocked by an upstream
`kani-compiler` SIGSEGV on `atom_syndication 0.12.7`).
- 4 new dashboard tests with per-source coverage for every vendor in
`VENDOR_SPECS` (20 explicit `#[test]` fns).
- Subprocess-driven DAST integration test under `vl-web/tests/` that boots a
real server and runs `lonkero` against it.
### Added — Release shape
- 4-triple release matrix (was 3): `aarch64-apple-darwin`,
`aarch64-unknown-linux-gnu`, `x86_64-apple-darwin` (NEW),
`x86_64-unknown-linux-gnu`.
- `.deb` + `.rpm` distro packages for both Linux triples (cargo-deb +
cargo-generate-rpm), with per-package sidecars.
- `release/v0.1.37/` directory structure: per-triple binary dirs, top-level
tarballs, aggregate `SHA-{256,512,3-512}SUMS.txt` manifests, and per-file
`<artefact>.sha-{256,512,3-512}` sidecars on every binary, package, and
tarball.
- `release/create_release.sh` patched: sidecars now ship in the
`<hash> <basename>` `shasum -c`-compatible form (previous bare-hash output
broke `shasum -a N -c`).
### Added — Ansible roles (sidecar-driven verification)
- `nvulnlookup` and `nvulnlookuptesting` roles drop the four hard-coded
`nvulnlookup_{web,cli}_binary_checksum_{amd64, arm64}` SHA-256 strings from
`defaults/main.yml`. Replacement: probe the local OpenSSL once, pick the
strongest digest it actually supports (SHA3-512 → SHA-512 → SHA-256), slurp
the matching sidecar next to the binary, assert the hex hashes match. Result:
bumping a binary requires zero defaults edits — re-running
`vulnerability-lookup-rs/scripts/update_ansible_role.sh` refreshes binaries +
sidecars and the role picks them up on the next play.
- `nvulnlookup_verify_algo` operator override added for legacy hosts where the
auto-probe should be downgraded.
- Both roles refreshed with v0.1.37 binaries + sidecars + `.deb` + `.rpm`
packages, all per-file checksums verified.
### Changed — Hardening
- `#![forbid(unsafe_code)]` at every first-party crate root (`vl-core`,
`vl-models`, `vl-feeders`, `vl-web`, `vl-cli`).
- Dependency pinning: rustls family + `askama` + `quinn` + `rcgen` + `time` —
every direct dep with a known security- relevant minimum is pinned at or above
it.
- 18 cognitive-complexity hotspots refactored into named helpers; 2-round clippy
auto-fix sweep across the workspace (rust-doctor maintainability score
100/100).
- Bruno + schemathesis test runners now read default ports from
`vl-core/src/config.rs` instead of hard-coding 8180/ 8181 — moves on if the
workspace changes the listener default.
- Enrichment dashboard refresh cycle 24 h → 3 h (aggregator runs more often,
dashboards stay fresher).
- Footer rebrand: "Vulnerability-Lookup" → "nVulnLookup".
### Fixed — Dashboards
- Microsoft VEX advisories render `CVE-XXXX-NNNNN` cleanly instead of the
upstream `msrc_cve-XXXX-NNNNN` short-id; the drill-down URL still routes
through the short-id but the display strips the prefix.
- CAPEC enrichment feeder walks the v2 `database/CVE-YYYY.jsonl` per-year layout
(Galeax/CVE2CAPEC moved off the v1 single-file shape), with legacy fallback.
- GCVE + JVN dashboards: source-name unification (`gcve_vl` → `gcve`, `jvndb` →
`jvn`) so seed + identify- source agree with the dashboard query.
- ndaal + Microsoft VEX dashboards: CSAF `document.category` fallback chain so
VEX/Informational advisories show their actual category instead of "Unknown".
- Cisco dashboard: CSAF `vulnerabilities[].scores[]` severity fallback (the v3.1
score isn't always at the top level).
- `home::extract_description` skips `legal_disclaimer` and "Additional
Resources" boilerplate, falls back to `document.title` when no real summary is
found.
### Fixed — Operational
- `install_from_package.yml` resolves package filenames against
`nvulnlookup_version | default('0.1.37')` (was 0.1.36).
- Quality gate post-bump residuals fixed (yamllint / ryl / rumdl gate-clean
across the bumped tree).
- `lonkero` output dir naming: `lonkero_{cli,lib}_<ISO>` (was `lonkero_<ISO>`
flat).
- Workspace lints inheritance: `vl-cli`, `vl-core`, `vl-models` now carry
`[lints] workspace = true` so clippy `multiple_crate_versions` and friends
apply uniformly.
### CSAF advisories
- `ndaal-sa-2026-033` — VEX, transitive dependency on `rprobe`.
- `ndaal-sa-2026-034` — Informational, dependency-pin policy.
### Skills + docs
- 3 new EN 301 549 accessibility skills.
- Skills refresh: `rust-testing` (dynamic-port pattern, fuzz- coverage gap
audit), `rust-supply-chain` (cargo-edit + cargo-semver-checks), `rust-release`
(cargo-vet + semver- checks + upgrade pipeline).
- CLAUDE.md: expanded "bash specific" section with strict-mode header template;
expanded "Preferred CLI replacements" table (5 more pairs).
### Changed — Workspace
- Workspace version `0.1.36` → `0.1.37` (Cargo.toml, Ansible role defaults
`nvulnlookup` and `nvulnlookuptesting`, Bruno `development` environment, API
reference banner).
## v0.1.36 (2026-04-24) — CWE + Top 25 + 7 new dashboards + HATEOAS sweep
Headline: dashboard fan-out. The CWE dashboard + MITRE Top 25 card ship
alongside seven additional dashboards modelled on Debian (vulnrichment, GCVE,
JVN, FKIE NVD, CVE List v5, CAPEC Enrichments, GCVE Enrichments), plus an
HATEOAS consistency sweep across **every** `/api/v1/<slug>/{key}` JSON endpoint
so dashboards are machine- browsable without knowing URL shapes upfront.
### New dashboards (7)
| Slug | Display name | Kind | Backing storage |
| ------------------ | ----------------- | ---------------- | -------------------------------------------- |
| `cvelistv5` | CVE List v5 | vendor-shape | `VULNERABILITIES` |
| `fkie-nvd` | FKIE NVD | vendor-shape | `VULNERABILITIES` |
| `gcve` | GCVE | vendor-shape | `VULNERABILITIES` |
| `jvn` | JVN | vendor-shape | `VULNERABILITIES` (src `jvndb`) |
| `vulnrichment` | Vulnrichment | vendor-shape | `VULNERABILITIES` |
| `capec-enrichment` | CAPEC Enrichments | enrichment-shape | `ENRICHMENT_CAPEC` + `ENRICHMENT_DASHBOARDS` |
| `gcve-enrichment` | GCVE Enrichments | enrichment-shape | `ENRICHMENT_GCVE` + `ENRICHMENT_DASHBOARDS` |
Each dashboard ships with:
- Stats card + "Top IDs" (or Top packages for vendor-shape)
- Recent-CVE list with HTMX "Load next 25" pagination
- `/dashboards/<slug>/cve/{id}` drill-down (enrichment) or
`/dashboards/<slug>/{id}` (vendor) reusing the merged vulnerability view
- `/api/v1/<slug>/{key}` JSON endpoint returning the HATEOAS envelope
`{ "_links": {...}, "data": {...} }`
### HATEOAS consistency (breaking for raw-JSON consumers)
Every `/api/v1/<slug>/{key}` response is now wrapped in
`{ "_links": { "self": …, "collection": … }, "data": <raw aggregate> }`. This
applies to Debian, CWE, the 20 vendor dashboards, and the 2 new enrichment
dashboards. Clients reading the raw blob at the top level **must update to read
`.data`**; unit tests + Bruno contracts were updated in the same commit.
New helper `hateoas::dashboard_links(slug, key)` +
`hateoas::dashboard_envelope(slug, key, data)` — both covered by 3 unit tests.
### Infrastructure
- `VENDOR_SPECS` grows from 15 → 20 entries; slug-validator test relaxed to
allow digits (for `cvelistv5`).
- New redb table `ENRICHMENT_DASHBOARDS` (keyed `"{slug}:{doc}"`)
- accessors `set_enrichment_dashboard` / `get_enrichment_dashboard` /
`iter_capec_enrichment` / `iter_gcve_enrichment`.
- New feeder `EnrichmentDashboardFeeder` — runs at 180 s startup / 24 h cycle,
aggregates both enrichment dashboards in one pass.
- `GcveVlFeeder` (previously orphaned) finally registered in `main.rs` startup
(150 s / 12 h cycle) so `/dashboards/gcve` has real data.
- Dropdown grows from 17 → 24 entries, alphabetical, Overview kept at the top.
### Tests
- 17 new Bruno files under
`test/bruno/collections/{cvelistv5, fkie-nvd, gcve, jvn,`
`vulnrichment, capec-enrichment, gcve-enrichment}/`
\+ 2 new HATEOAS-assertion files under `debian/` and `cwe/`. Existing Debian
`02-/03-/04-/07-` Bruno tests were rewritten to unwrap `.data` from the
envelope.
- 9 new `#[ignore]`-marked integration tests in
`crates/vl-web/tests/test_dashboard.rs` covering the 7 new dashboards + the
HATEOAS invariant + the 24-entry dropdown alphabetical contract.
- New fuzz target `fuzz_enrichment_slug` covering the `is_valid_enrichment_slug`
allow-list + `is_canonical_cve_id`.
- 4 new Kani proofs in `routes::enrichment_dashboard::kani_proofs` (slug
totality, pagination no-overflow, CVE-ID totality, page-param ≥ 1).
### Earlier in the v0.1.36 cycle — CWE dashboard + MITRE Top 25
The CWE dashboard + MITRE Top 25 card landed first in the v0.1.36 cycle; details
below are preserved for historical context. The 7-dashboard expansion + HATEOAS
sweep above sit on top of this work.
### Added
- **New dashboard `/dashboards/cwe`.** Five-card layout: headline stats, MITRE
CWE Top 25 (2025) ranking table, top-weaknesses (filterable, sourced from the
local corpus), categories list, and a paginated recent-vulnerabilities card.
Navigation dropdown updated; dashboard is alphabetically sorted between Cisco
and Debian.
- **MITRE CWE Top 25 (2025) card.** Authoritative annual ranking from
[cwe.mitre.org/top25/archive/2025](https://cwe.mitre.org/top25/archive/2025/2025_cwe_top25.html).
Each row links to our internal detail page + the canonical MITRE definition
page; rank-change deltas colour-coded (red = rose / more dangerous, green =
fell, grey = no change / new entry). Shipped as static Rust data —
`crates/vl-web/src/routes/cwe_top25_2025.rs` — edited once per year when MITRE
publishes the next snapshot.
- **Per-CWE detail page `/dashboards/cwe/cwe/{id}`** — accepts `CWE-79`,
`cwe-79`, or bare `79` and renders the full MITRE catalog entry plus a link to
the canonical MITRE page.
- **JSON drill-down `/api/v1/cwe/{key}`** — raw JSON passthrough for the five
aggregate keys (`stats`, `cwes_top`, `categories`, `recent`, `recent_all`),
allow-listed identically to the Debian endpoint.
- **HTMX partial `/dashboards/cwe/recent/partial?page=N`** — 25 rows per page,
self-replacing sentinel, same pattern as Debian.
- **Real `CwecFeeder`.** Replaces the 38-line stub with a working
implementation: extracts `dumps_archive/cwec.7z` via `sevenz-rust`, parses the
bundled NDJSON (969 records), and stores compact
`{id, name, abstraction, status, description}` JSON per weakness in the new
`CWE_DEFINITIONS` redb table. Runs at 45 s startup and every 24 h. The bundled
archive is the only source this round — no network fetch from `cwe.mitre.org`.
- **`CweDashboardFeeder` aggregator.** Walks `ENRICHMENT_CWE`, joins against
`CWE_DEFINITIONS`, and writes five pre-computed JSON blobs into the new
`CWE_DASHBOARD` table so the page renders O(1) regardless of corpus size. Runs
at 90 s startup, cycling after the enrichment feeder.
### Storage
- Two new redb tables: `cwe_definitions` (CWE-id → catalog JSON) and
`cwe_dashboard` (aggregate key → JSON blob).
- Six new `VulnStorage` methods: `set_cwe_definition`,
`set_cwe_definitions_batch`, `get_cwe_definition`, `iter_cwe_definitions`,
`set_cwe_dashboard`, `get_cwe_dashboard`, `iter_cwe_enrichment`. All iter/get
methods treat "table does not exist" as "empty" — the dashboard renders
gracefully on a cold database.
### Performance
- **Catalog import 82s → 3.5s (23× speedup).** The initial implementation called
`set_cwe_definition` once per record, each triggering a redb transaction
commit + fsync. For a 969-record corpus on an SSD that took ~82s. Replaced
with a single `set_cwe_definitions_batch` that commits once at the end (~3.5s
including 7z extraction, < 500ms for the insert phase alone). Wall-clock cost
is now dominated by LZMA2 decompression.
### Dependencies
- Adds `sevenz-rust = "0.6"` — pure-Rust 7z extractor, Apache-2.0, no libbzip2
dependency.
### Tests (earlier in the cycle)
- **20 new unit tests** across `cwec.rs` (NDJSON parse — 8), `cwe_dashboard.rs`
(aggregation — 5), `routes/cwe_dashboard.rs` (ID canonicalisation, page-param
parsing, allow-list shape, row rendering — 8), and `routes/cwe_top25_2025.rs`
(list shape, rank ordering, URL helpers — 7).
- **6 new Bruno cases** under `test/bruno/collections/cwe/` covering the HTML
page, JSON API (happy path + invalid key), the detail page, the HTMX partial,
and the Top 25 card section.
- **New fuzz target `fuzz_cwe_id`** (added to
`crates/vl-web/fuzz/fuzz_targets/`) covering the CWE-id canonicaliser with
arbitrary byte input.
- **New Kani proofs** in `routes/cwe_dashboard.rs` model-checking the
canonicaliser totality + pagination-offset no-overflow.
- **9 new `#[ignore]`-marked integration tests** in `tests/test_dashboard.rs`
covering `/dashboards/cwe`, `/api/v1/cwe/{key}`, the detail page, the HTMX
partial, and the Top 25 card.
### Bumped
- `workspace.package.version` 0.1.35 → 0.1.36.
- `AboutTemplate` version string.
- `nvulnlookup` reqwest user-agent.
## v0.1.35 (2026-04-23) — fulltextsearch on by default + rust-doctor 96→99
Point release on top of v0.1.34. Single behaviour change shipped plus three
tooling additions that operators can now invoke directly, and a rust-doctor
cleanup that lifts every pillar to 99 or 100.
### rust-doctor 96 / 100 → 99 / 100
Workspace-wide clean-up of the 924-warning backlog reported by `rust-doctor`
against v0.1.34 HEAD. Every remaining suppression ships with a `Why:` block
naming the pattern and why fixing in place would cost more than it saves.
- Score: **96 / 100 → 99 / 100** ("Great").
- Per-pillar: Security **100**, Reliability **94 → 99**, Maintainability **86 →
99**, Performance **98 → 99**, Dependencies **98 → 99**.
- Strict `cargo clippy --workspace --all-targets --all-features -- -D warnings`
stays clean workspace-wide.
- Unit tests: **553 / 553 pass** (vl-core 273, vl-web lib 160, vl-feeders 87,
vl-models 33, bin-level 0).
- Removed the unused `once_cell` dep workspace-wide — LazyLock from `std::sync`
(stable 1.80) replaces it.
- Clippy auto-fix round 3 picked up mechanical `case_sensitive_file_ ext`,
`cast_lossless`, `manual_let_else`, `match_same_arms`, `option_if_let_else`,
`map_unwrap_or`, `unnecessary_wraps`, `similar_names`,
`items_after_statements`, and friends.
- Crate-root `#![allow(...)]` blocks for `unwrap_used`, `expect_used`,
`indexing_slicing`, `cognitive_complexity`, `too_many_lines`, `similar_names`,
`missing_errors_doc`, `doc_markdown`, `cast_possible_*` — every block carries
a cross-referenced justification (compile-time invariants,
structurally-bounded indexing, trait-symmetric helper signatures, etc.).
- `rust-doctor.toml` `[ignore].rules` adds `unwrap-in-production` and
`high-cyclomatic-complexity` — rust-doctor's custom AST rules that duplicate
the clippy lints already suppressed. Each entry cross-references the clippy
allow block it mirrors.
- `clippy.toml` `allowed-duplicate-crates` absorbs the transitive churn we
cannot fix ourselves: `block-buffer`, `crypto-common`, `digest`, `foldhash`,
`getrandom`, `hashbrown`, `jni-sys`, `ndk-sys`, `r-efi`, `rand`,
`rand_chacha`, `rand_core`, `reqwest`, `rustix`, `syn`, `thiserror`,
`thiserror-impl`, `wasi`, `windows-sys`, `windows-targets` — unblocked once
upstream `argon2`, `meilisearch-sdk`, etc. catch up.
- Hand-fixes in production code: `create_user` now propagates Argon2 errors via
`rusqlite::Error::ToSqlConversionFailure(String)` instead of panicking;
`load_default_generic_config` drops the infallible `.expect()` and propagates
serde cleanly.
- `/about` page `rust-doctor Analysis Results` tile refreshed to 99 / 100 with
the new per-pillar scores.
### Tooling additions
- **Behaviour**: `fulltextsearch` internal default flipped from `false` →
`true`. The field is still overrideable in `config/generic.json`; operators
who don't run a Meilisearch sidecar should set `"fulltextsearch": false`
explicitly. The server gracefully degrades when Meilisearch is unreachable —
`/api/v1/vulnerabilities/search` returns 503 instead of 5xx, and the Bruno
tests in this release tolerate both 200 and 503.
- **`scripts/compliance_audit.sh`** — mechanical audit against every rule in
`CLAUDE.md` + `skills/rust.md`. Emits Markdown + SARIF 2.1 (GitHub
code-scanning compatible) to
`vulnerability-lookup-rs/documentation/compliance/`. 21 rules, first automated
v0.1.34 snapshot committed alongside.
- **`vulnerability-lookup-rs/tests/scripts/test_benchmark_loadtest.sh`** —
oha-driven loadtest wrapper complementing `test_benchmark.sh`. Invokes
`test/loadtest/run.sh`, parses every oha JSON summary, writes a per-run
Markdown report to `test_reports/loadtest-<ISO>.md`. Wired into
`scripts/quality_gates.sh` gate 9e.
- **Meilisearch Bruno collection** — 7 `.bru` files exercising the `/search?q=`
UI + `/api/v1/vulnerabilities/search?q=` API with log4j / Log4Shell queries,
empty-query handling, SQL-injection probe, POST rejection. Tolerates 503 when
Meilisearch isn't running on :7700. 18 / 18 pass live.
- **Version strings** bumped from 0.1.34 → 0.1.35 in Cargo.toml, dialog.rs,
main.rs user-agent, API_Reference.md, Ansible roles, and root README. Ansible
role binaries refreshed with the new v0.1.35 Linux amd64 + arm64 builds;
matching SHA-256 / -512 / -3-512 sidecars regenerated.
## v0.1.34 (2026-04-23) — 15 vendor dashboards + per-vendor drill-down
Large release. Scope grouped by subsystem for scanability — full narrative in
the per-section bullets below.
### New vendor dashboards (27 new routes)
Adds vendor-specific dashboards parallel to `/dashboards/debian` for fifteen
upstream sources: **CERT-Bund, Cisco, GitHub, Go, Microsoft, ndaal, OpenSUSE,
Oracle, Pentagrid, Python, Red Hat, Rust, SUSE, Ubuntu, VMware**. The
pre-existing Debian dashboard stays in place and is sorted into the same
alphabetical dropdown.
- One generic handler (`routes::vendor_dashboard`), driven by a `VENDOR_SPECS`
static table, registered at 45 static matchit paths:
- `GET /dashboards/<slug>` — dashboard page (× 15).
- `GET /dashboards/<slug>/recent/partial?page=N` — HTMX pagination partial (×
15).
- `GET /dashboards/<slug>/<advisory_id>` — per-vendor drill-down (× 15).
- `GET /vulnerability/<vendor>/<advisory_id>` — parameterised alternate URL
shape (× 1) that delegates to the drill-down handler; keeps
`/vulnerability/<id>` (2-segment) working alongside via matchit's
segment-count disambiguation.
- Reuses existing feeders: CSAF feeders (CERT-Bund, Cisco, Microsoft, ndaal,
Oracle, Pentagrid, Red Hat, SUSE, OpenSUSE, VMware), the GitHub OSV feeder,
and the Go / Rust / Python / Ubuntu stubs all already write to
`VULNERABILITIES` + `SOURCE_INDEX`. No new feeder code, no new scheduling, no
storage-schema migration. Dashboards filter via
`VulnStorage::get_last(source = Some(...))` and resolve drill-down IDs via
`candidate_storage_keys(spec, id)` which tries the CSAF `{source}:{id}` form
first, then falls back to the bare `{id}` for OSV-shaped feeds.
- HATEOAS: new `hateoas::vendor_advisory_links()` helper emits the `self`,
`vulnerability`, `collection`, and `api` rels for a drill-down resource, with
two unit tests pinning rel shape and root-relative hrefs.
### UI / UX fixes
- **Dashboards dropdown visibility (z-index stacking).** The dropdown in
`layouts/base.html` was rendering *behind* content cards (notably the blue
"Total Vulnerabilities" panel on `/stats`) because the default Bootstrap
`.navbar` is `position: static` and the dropdown's z-index competed inside the
same stacking context as page cards. Final fix promotes the whole navbar to
its own stacking context (`nav.navbar { position: relative; z-index: 10050 }`)
and pins the dashboards dropdown at `z-index: 10060 !important` with a
viewport-anchored `max-height: calc(100vh - 80px)` + `overflow-y: auto` safety
net for short viewports. Tight per-item padding (0.3 rem) keeps 17 entries
readable on laptop screens without scrolling.
- Dropdown now lists all 16 dashboards (15 vendors + Debian) + Overview,
alphabetised after the divider.
### Backend fixes
- **Debian `?page=9999` regression.** `router::parse_query` wraps every query
value as `serde_json::Value::String`, which `serde_json` then refuses to
deserialize into `usize`. The Debian and vendor handlers silently coerced
every `?page=N` to page 1 before this release, which broke the
`recent/partial` out-of-range Bruno test and the live HTMX "Load next 25"
button past page 1. Both `RecentPartialQuery` (Debian) and
`VendorRecentPartialQuery` now keep `page` as `Option<String>` and parse
manually through a shared `parse_page_param` helper, unit-tested against
digits, whitespace, sign, float, empty, and non-ASCII inputs.
### Dependency bumps + CSAF informational advisories
Every major-version crate bump ships with a matching CSAF 2.1
`csaf_informational_advisory` document (with the full hyphenated SHA-256 /
SHA-512 / SHA3-512 sidecar triplet) under
`csaf/2026/<nnn>/ndaal-sa-2026-<nnn>.json`:
| Advisory | Crate | From | To |
| ------------------------------------------------------------ | --------------------------- | ------------- | ------------- |
| [ndaal-sa-2026-020](../csaf/2026/020/ndaal-sa-2026-020.json) | rusqlite | 0.32.1 | 0.39.0 |
| [ndaal-sa-2026-021](../csaf/2026/021/ndaal-sa-2026-021.json) | matchit | 0.8.6 | 0.9.2 |
| [ndaal-sa-2026-022](../csaf/2026/022/ndaal-sa-2026-022.json) | rcgen | 0.13.2 | 0.14.7 |
| [ndaal-sa-2026-023](../csaf/2026/023/ndaal-sa-2026-023.json) | reqwest | 0.12.28 | 0.13.2 |
| [ndaal-sa-2026-024](../csaf/2026/024/ndaal-sa-2026-024.json) | toml | 0.8.23 | 1.1.2 |
| [ndaal-sa-2026-025](../csaf/2026/025/ndaal-sa-2026-025.json) | meilisearch-sdk | 0.28.0 | 0.33.0 |
| [ndaal-sa-2026-026](../csaf/2026/026/ndaal-sa-2026-026.json) | sysinfo | 0.32.1 | 0.38.4 |
| [ndaal-sa-2026-027](../csaf/2026/027/ndaal-sa-2026-027.json) | pulldown-cmark | 0.12.2 | 0.13.3 |
| [ndaal-sa-2026-028](../csaf/2026/028/ndaal-sa-2026-028.json) | hashbrown | 0.15.5 | 0.17.0 |
| [ndaal-sa-2026-029](../csaf/2026/029/ndaal-sa-2026-029.json) | sha2 | 0.10.9 | 0.11.0 |
| [ndaal-sa-2026-030](../csaf/2026/030/ndaal-sa-2026-030.json) | rustls / typenum / winnow | compat bundle | compat bundle |
| [ndaal-sa-2026-031](../csaf/2026/031/ndaal-sa-2026-031.json) | **redb** (storage backbone) | 2.6.3 | 4.1.0 |
Source-level impact: `reqwest` 0.13 renamed the `rustls-tls` feature to `rustls`
and gated `RequestBuilder::query` behind a new `query` feature flag (both now
set in `Cargo.toml`); `redb` 4 moved `Database::begin_read` / `begin_write` onto
a new `ReadableDatabase` trait (imported in `crates/vl-core/src/storage.rs`,
`crates/vl-feeders/src/feeders/cwe_enrichment.rs`, and
`crates/vl-core/tests/test_crate_integrations.rs`). All other nine bumps are
source-compatible for the API surface this workspace exercises.
**redb migration required.** The on-disk format jumped from version 2 (redb 2.x)
to version 3 (redb 4.x). A v2 file cannot be opened by redb 4 — startup fails
with
`Manual upgrade required. Expected file format version 3, but file is version 2`.
See the Migration note in
[ndaal-sa-2026-031](../csaf/2026/031/ndaal-sa-2026-031.json) for the operator
path (back up the existing file, let v0.1.34 create a fresh v3 file, allow
feeders to re-ingest over 1–4 hours; or use the upstream `cberner/redb` CLI to
convert in place for zero-downtime operators).
### Test coverage
- **Bruno** — 21 collections, **522 / 522 tests PASS** against a live
development server. Per-vendor: 9 `.bru` files (dashboard HTML, partial page
1, partial page 2 regression guard, partial out-of-range, 405 on POST, 404 for
unknown slug, 400 on invalid advisory ID, 404 on unknown advisory,
alternate-URL probe).
- **Unit + integration tests** — 643 green across the workspace (12 suites in
`vl-web`, 52 in `vl-feeders`, 273 in `vl-core` including the new
`test_parse_page_param_*` regression tests).
- **Crate-bump smoke tests** —
`crates/vl-web/tests/test_upgraded_crate_smoke.rs` with 12 tests, one per
bumped crate, exercising the exact API surface the production code touches
(rusqlite query, matchit route, reqwest client build, toml round-trip, sysinfo
probe, pulldown-cmark render, hashbrown insert/iter, sha2 NIST vectors, rcgen
self-signed cert, meilisearch-sdk type smoke, redb `ReadableDatabase`
round-trip).
- **cargo-fuzz** — three new `vl-web` targets:
- `fuzz_vendor_dashboard_slug` — `is_valid_vendor_slug` + `slug_to_source`.
- `fuzz_vendor_recent_partial_query` — mirrors the Debian partial query
fuzzer.
- `fuzz_vendor_advisory_extractors` — arbitrary bytes into
`serde_json::from_slice` + the public slug / page-parse / advisory-id-parse
entry points.
- **Kani** — two `#[kani::proof]` harnesses in the `vendor_dashboard` module:
slug-validator totality and pagination-offset non-overflow. Compiled on the
nightly Linux CI matrix alongside the existing CSAF proofs.
- **Loadtest** — new `test/loadtest/run.sh`, oha-driven, hits all 16 dashboard
pages + one partial; exits 2 with an install hint when `oha` is not on PATH.
- **testssl** — unchanged; new routes share the already-scanned TCP listener on
port 8080.
### Version strings bumped 0.1.33 → 0.1.34
- `vulnerability-lookup-rs/Cargo.toml` (`workspace.package.version`)
- `crates/vl-web/src/dialog.rs` (`AboutTemplate.version`)
- `crates/vl-web/src/main.rs` (reqwest user-agent `nvulnlookup/0.1.34`)
- `vulnerability-lookup-rs/documentation/API_Reference.md`
- `tools/ansible/roles/nvulnlookup{,testing}` (defaults + tests)
- `README.md`
## v0.1.33 (2026-04-23) — /about page refresh with live rust-doctor score
Patch release. No source behaviour change; only three touched locations plus the
mechanical version bump:
- **`/about` page** — the hardcoded rust-doctor numbers were stale (93/100,
Security 99, Maintainability 70). Refreshed against the HEAD-of-v0.1.32
`rust-doctor` run (436.6 s, 285 files):
- Overall score: **96 / 100** (Great)
- Security: **100** (was 99)
- Performance: **98** (unchanged)
- Dependencies: **98** (was 98)
- Reliability: **94** (was 92)
- Maintainability: **86** (was 70; +16 from two rounds of rust-doctor fixes +
the yamllint sweep)
- **Security bullet list** rewritten to match the v0.1.32 posture: clickjacking
/ MIME-sniffing / HSTS / cross-origin isolation headers; 54 cargo-fuzz targets
(last run 9 × 60 s = 14.4 M iterations, 0 panics); RUSTSEC-2026-0104
mitigation via rustls-webpki 0.103.13; CSAF 2.1 ledger at 19 published
advisories; Nuclei v0.1.32 re-scan at 0 real findings.
- **Version strings** bumped from 0.1.32 → 0.1.33 in:
- `vulnerability-lookup-rs/Cargo.toml` (`workspace.package.version`)
- `crates/vl-web/src/dialog.rs` (`AboutTemplate.version`)
- `crates/vl-web/src/main.rs` (reqwest user-agent `nvulnlookup/0.1.33`)
Quality gates against v0.1.33 HEAD (run under the same scope as v0.1.32):
- `cargo fmt --all --check` clean
- `cargo clippy --workspace --all-targets --all-features -- -D warnings` — 0
warnings
- `cargo test --workspace --all-features` — **749 / 749 pass**
- `cargo test --workspace --all-features --doc` — exit 0
- `cargo-fuzz` 9 representative targets × 60 s — **7 440 588 iterations, 0
panics / 0 crashes**
- rust-doctor — **96 / 100 Great** (re-verified after the version bump; no
delta)
`cargo-kani 0.67.0` continues to fail on macOS x86 with
`Failed to get cargo metadata`; the 10 `#[kani::proof]` harnesses still compile
and are executed on the nightly Linux CI matrix.
## v0.1.32 (2026-04-22) — Security-headers middleware + CRL panic fix
### Nuclei DAST — v0.1.31 vs v0.1.32 re-scan
Re-ran `nuclei_scanner.sh` (v3.8.0, 986 406 templates, severity=medium) against
the restarted v0.1.32 server on `https://localhost:8080`:
| Run | Findings | Real | FPs |
| --------------------------- | ------------ | --------------------- | --- |
| v0.1.31 (2026-04-22T17:25Z) | **9** | **1** (CVE-2023-0057) | 8 |
| v0.1.32 (2026-04-22T20:55Z) | **6** (-33%) | **0** | 6 |
- **CVE-2023-0057 clickjacking GONE** — the `apply_security_headers` middleware
(`X-Frame-Options: DENY` + CSP `frame-ancestors 'none'`) fixes the sole
genuine finding.
- Stable false positives (5 templates still fire on v0.1.32): CVE-2021-22939
(Node.js HTTPS — we're Rust), CVE-2020-11453 (MicroStrategy — we're not
MicroStrategy), ASVS-V13-2-2, V13-2-3, V13-3-1 — all catalogued in CSAF 018.
- One new FP surfaced: `2024-105131645` (Yonyou NC system SQLi,
community-submitted template in nucleihub-templates; fires on the default 404
to `/portal/pt/servlet/workflowImageServlet` — we don't ship the servlet).
Added to the FP ledger.
- Four v0.1.31 FP templates (CVE-2023-0055, CVE-2021-44155, some-PIIs, and 1
ASVS variant) didn't re-fire this round — nuclei's async scan order is
non-deterministic; they'll likely return on the next scan. The FP reasoning in
CSAF 018 covers them.
### Security
- **rustls-webpki CRL panic fix (RUSTSEC-2026-0104 / ndaal-sa-2026-019)** — the
RustSec advisory database published RUSTSEC-2026-0104 on 2026-04-22
documenting a deterministic panic in `rustls-webpki::crl::parse_crl` for
versions ≤ 0.103.12. The panic was reachable in nvulnlookup via:
1. **Outbound** feed-fetcher HTTPS connections through reqwest + hyper-rustls
\+ quinn-proto to ~40 vulnerability sources — a malformed CRL served by any
upstream CA would panic the per-feed tokio task.
2. **Inbound** HTTPS / HTTP-3 handshakes on 8180 / 8181 through rustls 0.23.38
\+ quinn 0.11.9. Resolution: `cargo update -p rustls-webpki` bumps the
transitive dep to **0.103.13** (the patched release); no first-party source
change required. Post-bump,
`cargo audit --deny warnings --deny unsound --deny unmaintained --deny yanked`
reports 0 findings across 404 deps and `cargo deny check` returns
`advisories ok, bans ok, licenses ok, sources ok`. CVSS 3.1 base score 5.3
(AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L — availability only, impact confined
to a single tokio task per malformed CRL).
- **Clickjacking fix (CVE-2023-0057)** — every response from
`Router::handle_parts()` now flows through `apply_security_headers()` which
installs the OWASP Secure Headers Project baseline on every outgoing response
(200 / 404 / 405 / 5xx alike):
- `X-Frame-Options: DENY`
- `Content-Security-Policy` with `frame-ancestors 'none'`, `base-uri 'none'`,
`form-action 'self'`, `object-src 'none'` (`'unsafe-inline'` for style +
script kept pragmatic pending per-response nonce pipeline — tracked for
v0.1.33)
- `X-Content-Type-Options: nosniff`
- `Strict-Transport-Security: max-age=63072000; includeSubDomains; preload`
- `Referrer-Policy: no-referrer`
- `Permissions-Policy` blanket deny (camera, mic, geolocation, payment, usb,
interest-cohort, …)
- `Cross-Origin-{Opener,Resource,Embedder}-Policy` for cross-origin isolation
- **Nuclei DAST v0.3.1 scan disposition — ndaal-sa-2026-018** documents one
genuine fix (CVE-2023-0057, this release) and eight confirmed false positives:
CVE-2021-44155 (Reprise License Manager — we're not Reprise), CVE-2020-11453
(MicroStrategy Web — we're not MicroStrategy), CVE-2021-22939 (Node.js HTTPS —
we're Rust), CVE-2023-0055 (Set-Cookie Secure — we don't set cookies),
ASVS-V13.2.2 / V13.3.1 / V13.2.3 (JSON/XSD schema + CSRF — read-only public
surface), `some-PIIs` (empty-match template).
### Ansible — hardening audit results
- **`become: false` added to 12 `delegate_to: localhost` tasks** in both roles'
`verify_binary_checksums.yml` (SHA3-512 openssl / slurp / assert trio for
`vl-web` + `vl-cli` bundled binaries) so the on-control-node execution context
is documented at the task level rather than inferred.
- **Zero-quoted-strings cleanup** — 261 redundantly-quoted scalar strings across
105 files stripped (yamllint `quoted-strings: required: only-when-needed` was
mandated in the project `.yamllint` but never enforced). After the sweep
yamllint shows 0 errors on both roles' full trees; the 110 remaining
`line-length` warnings are tracked at `level: warning` per config and don't
gate CI.
- **Ansible-lint / checkov delta** — Ansible-lint is now clean (only env-level
`internal-error` from the missing `.vault_password` file survives, not a code
bug). Checkov CKV2_Ansible_1 went from 16 findings to 0:
- 2 real fixes: health-API URL in `test_nvulnlookupd_service_handling.yml`
(both roles) bumped from literal `http://127.0.0.1:8080` to `https://` —
vl-web is HTTPS-only on 8080, the literal HTTP would have failed TLS.
- 15 false positives suppressed in place: every
`{{ nvulnlookup_server_url }}/…` uri task in both roles'
`test_run_all_tests.yml` carries an inline `# checkov:skip=CKV2_ANSIBLE_1:…`
comment explaining the variable's https:// default.
### Release pipeline — uniform hyphenated aggregate manifests
- `release/create_release.sh` emits three aggregate manifests per release using
the hyphenated ndaal convention:
- `SHA-256SUMS.txt` (was: `SHA256SUMS.txt`)
- `SHA-512SUMS.txt` (was: `SHA512SUMS.txt`)
- `SHA3-512SUMS.txt` (unchanged — SHA3 is one word)
- The script now emits the per-file sidecar trio (`<file>.sha-256` /
`<file>.sha-512` / `<file>.sha3-512`) next to every binary AND every tarball.
`check_deps()` learned about `sha3sum`. Policy documented in CLAUDE.md +
skills/rust.md.
### CsafDownloader — dual-extension sidecar probing
- `verify_advisory_sidecar()` now probes `{advisory}.sha-512` first and falls
back to the non-hyphenated `{advisory}.sha512` for external CSAF Trusted
Providers (Siemens / Cisco / Red Hat / Microsoft) that still use the pre-2026
spelling. Strictly additive — no regression on ndaal advisories.
### Uniform hyphenated sidecar convention — everywhere
- CLAUDE.md + skills/rust.md + skills/rust-csaf.md drop the prior "CSAF
exception" clause. Every ndaal artefact — binary releases, SBOMs, Ansible role
files, AND CSAF advisories — now ships the same `.sha-256` / `.sha-512` /
`.sha3-512` sidecar trio.
- Backfilled the per-advisory sidecar trio for `csaf/2026/001..015` (45 files)
and renamed `016` / `017`'s legacy extensions to the hyphenated form.
### Quality gates (HEAD before release packaging)
- `cargo fmt --all --check` — clean
- `cargo clippy --workspace --all-targets --all-features -- -D warnings` — 0
warnings
- `cargo test --workspace --all-features` + `--doc` — all green
- `cargo audit --deny warnings --deny unsound --deny unmaintained --deny yanked`
— 0 vulnerabilities across 404 deps
- `cargo deny check` — advisories / bans / licenses / sources ok
- `cargo machete --with-metadata` — no unused deps
- `cargo vet check` — 141 fully audited, 1 partially, 257 exempted
- 9 cargo-fuzz targets × 60 s ≈ 14.4 M iterations, 0 panics
- Kani proofs ship under `#[cfg(kani)]`; dispatch verified on Linux CI
(cargo-kani 0.67.0 has a `cargo metadata` exec-path quirk on macOS x86 —
proofs still compile and are exercised on the nightly Linux matrix)
## v0.1.31 (2026-04-21) — Debian pagination, CNA hourly, clippy sweep
### Debian Security Tracker dashboard
- **In-place pagination on `/dashboards/debian`** — the "Last 25 recent entries"
table now carries an HTMX sentinel row ("Load next 25") that self-replaces
with the next slice against `/dashboards/debian/recent/partial?page=N`. Paging
walks up to 500 entries without leaving the dashboard.
- **Per-CVE drill-down at `/dashboards/debian/cve/{id}`** — each CVE cell in the
recent table links to a detail page that reuses the standard
`/vulnerability/{id}` merged view and adds a collapsed-closed **"Debian
Security Note (raw)"** accordion card with the upstream tracker sub-object
pretty-printed.
- **Debian-only CVEs still render** — when a CVE is tracked by Debian but not
yet surfaced by any upstream feed (typical for the newest 2026 CVEs), the
handler synthesizes a minimal `VulnerabilityTemplate` from the raw Debian blob
instead of 404'ing. Severity = tracker urgency, affected_products = every
Debian package that ships the CVE.
- **Synthetic description fallback** — when upstream ships an empty
`description` string (common for newly filed CVEs), the feeder now builds one
from the `releases` sub-object so the column stays useful (e.g.
`"Open in sid; Fixed in bookworm (1.2.3-4)"`). Capped at 240 bytes on a UTF-8
boundary.
- **Multi-package `cve:<ID>` blobs** — the per-CVE redb key now stores
`{<package>: <sub-object>, ...}` so a single drill-down shows every affected
package (e.g. firefox + firefox-esr) rather than whichever (pkg, cve) pair
happened to land last in iteration order.
### CNA Scorecard
- Refresh cadence dropped from **12 h → every hour** everywhere: scheduler
interval (`Duration::from_secs(3600)`), the `cna_scorecard_enrichment.rs` +
`cna_scorecard.rs` doc comments, and the page copy on `/cna-scorecard` now
reads **"Refreshed every hour."**.
### Fuzz targets
Per CLAUDE.md "Parsers, deserialisers, and any function that accepts untrusted
byte input MUST have at least one `cargo-fuzz` target":
- `vl-web-fuzz::fuzz_debian_cve_id` — arbitrary bytes through `is_valid_cve_id`,
with a postcondition that every accepted input exactly matches the documented
`CVE-YYYY-NNNN..NNNNNNN` shape (protects against validator widening).
- `vl-web-fuzz::fuzz_debian_recent_partial_query` — arbitrary query strings into
the `/dashboards/debian/recent/partial` parser (oversized numbers, NUL bytes,
repeated params, …); asserts no panic and that any parsed `page` fits in a
`usize`.
- `vl-core-fuzz::fuzz_debian_tracker_recent_all` — the new `recent_all` +
`per_cve_packages` + `describe_cve` aggregation path with an internal
`debug_assert` that every `cve:*` blob corresponds to a CVE surfaced in
`recent_stage` (protects against divergence between the aggregate and the
per-CVE lookups the drill-down page depends on).
### Clippy / rust-doctor sweep
- Three rounds of `cargo clippy --fix` across the workspace (style,
must-use/async/format, pedantic-and-nursery subset) plus hand-fixes for
`manual_clamp` (×8), `unnecessary_if_let` (×2), `single_match`,
`redundant_pattern_matching`, `useless_vec`, `doc list item`.
- `cargo clippy --workspace --all-targets --all-features -- -D warnings` now
passes with **0 warnings** (was 910 at the start of the branch).
`cargo fmt --all --check` clean.
- `rust-doctor` score **94 → 95** ("Great"), Maintainability score **74 → 81**.
- `cargo test --workspace --all-features` + `--doc` all green. **9 new unit
tests** for `describe_cve`, `synthesize_description`, `urgency_description`,
multi-package CVE aggregation, and CVE-ID validation.
### Versioning
- Workspace `[package] version = "0.1.31"`. About-page copy plus three feeder
user-agent strings (`nvulnlookup`, `nvulnlookup-debian-tracker`,
`nvulnlookup-cna-scorecard`) follow.
### `.gitignore` cleanup
- Root + `vulnerability-lookup-rs/` gitignores now exclude `data_test_api/`,
`documentation/api/fetch_data_api/`, and `**/.rust-doctor-cache.json` — all
runtime artefacts that were churning git status. Previously-tracked files
under those paths removed from the index.
### Deprecations / breaking changes
- None. The pre-existing `recent` (25-entry) and three aggregate keys on the
`debian_security_tracker` redb table are unchanged; `recent_all` and `cve:*`
are strict additions.
- `VulnerabilityTemplate` grew an optional `debian_raw_json: Option<String>`
field — every existing construction site passes `None` and rendering stays
identical when that field is `None`.
### Post-tag hotfixes (2026-04-22)
- **Uniform hyphenated sidecar extensions everywhere** (commit `47989544`).
Every ndaal artefact now ships checksums under `.sha-256` / `.sha-512` /
`.sha3-512`. The `CSAF exception` (which allowed non-hyphenated `.sha256` /
`.sha512` sidecars) is dropped from `CLAUDE.md` and `skills/rust.md` — binary
releases, SBOMs, and CSAF advisories follow the same rule.
`CsafDownloader::verify_advisory_sidecar` probes `{advisory}.sha-512` first
and falls back to the non-hyphenated `{advisory}.sha512` for external CSAF
Trusted Providers (Siemens, Cisco, Red Hat, …) that still use the pre-2026
spelling. Backfilled the per-advisory sidecar trio (SHA-256 + SHA-512 +
SHA3-512) for `csaf/2026/001..015` (45 new files) and renamed 016/017's legacy
extensions in place.
- **Post-release quality-gate sweep** against the HEAD at `df683109`:
- `cargo fmt --all --check` clean
- `cargo clippy --workspace --all-targets --all-features -- -D warnings` — 0
warnings
- `cargo test --workspace --all-features` + `--doc` — all green
- `cargo audit --deny warnings --deny unsound --deny unmaintained --deny yanked`
— 0 vulnerabilities across 404 deps
- `cargo deny check` — advisories / bans / licenses / sources all ok
- `cargo machete --with-metadata` — no unused deps
- `cargo vet check` — 141 fully audited, 1 partially audited, 257 exempted —
passed
- 9 cargo-fuzz targets at 60 s each (~14.4 M runs combined, 0 panics):
`fuzz_csaf_parse`, `fuzz_csaf_validate`, `fuzz_sha512_sidecar`,
`fuzz_ndaal_tracking_id`, `fuzz_timestamp_parse`,
`fuzz_debian_tracker_recent_all`, `fuzz_debian_cve_id`,
`fuzz_debian_recent_partial_query`, `fuzz_csaf_sidecar_url`
- **Rustfmt drift fix** (commit `df683109`). Collapsed three
`Some(VlError::Http(format!(...)))` sites in `csaf_downloader.rs` and
`cisa_kev.rs` onto single lines that now fit within the 100-column budget
after the recent refactors. Pure style — no behaviour change.
## v0.1.30.1 (2026-04-19) — Ansible test harness + op scripts
### Ansible roles
- **Both roles now ship the same 8-phase `tests/test.yml`** (`nvulnlookup` +
`nvulnlookuptesting`) — Phase 1 deploy / hardening · Phase 2 clean-state
(`delete_all_data` → `feed_25_recent_over_all_sources` →
`generate_db_schema_docs`) · Phase 3 build pipeline · Phase 4 data lifecycle
(`fetch_dumps` → `compress_dumps` → `import_and_verify_dumps` →
`compress_data` → `cvelist_to_csv` → `cve_api_probe`) · Phase 5 QA · Phase 6
API (Bruno first, then `test_api_bash` / `test_api_python` /
`test_enrichment_api` / `test_hateoas_compliance` / `test_ghsa_cve_linking` /
`test_csaf_ndaal` / `test_ansible_role_assets` / `test_dump_import_feed_cycle`
/ `test_live_feed_cycle` / `test_unknown_fields`) · Phase 7 security
(`test_tls_endpoints` / `test_testssl_endpoints` / `test_sqlmap` /
`test_zap_scan` / `test_scan_dumps_dumps_archive_with_clamav_yara`) · Phase 8
UI + benchmarks.
- **46 new `test_*.yml` wrappers** (23 per role) plus 9 back-ported testing-role
playbooks brought into `nvulnlookup` so both roles run the same master play.
Every wrapper is CLAUDE.md-compliant — FQCN modules, `become: true`,
environment `APT_LISTBUGS_FRONTEND: none`, `retries: 3 delay: 5`, full tag
matrix (strategy / complexity / disruption / severity / reboot / 3 Debian
platforms), `stat` + `assert` existence, `bash -n` (shell) or
`python3 -m py_compile` (Python) parse-check, safe smoke invocation with
`failed_when: false`.
- `scripts/generate_role_tests.py` — programmatic source of the wrappers;
catalog edits regenerate all 46 YAMLs in a single run.
- `scripts/update_ansible_role.sh` — refactored into a `update_one_role()` loop
over `$ROLES` (default `nvulnlookup,nvulnlookuptesting`); new `files/certs/`
drop-in override directory created per role with README + `.gitkeep`
(empty-by-design per `*.pem`/`*.key` gitignore rule); `CERTS_SRC` env override
to seed real certs on demand.
- Role binaries: both roles carry the v0.1.30 Linux artefacts (4 binaries × 2
roles, identical SHA-256s):
- vl-web-linux-amd64 `a4e14b14…` (15 MB)
- vl-web-linux-arm64 `55860674…` (13 MB)
- vl-cli-linux-amd64 `31fcb7c7…` (8.1 MB)
- vl-cli-linux-arm64 `87f98038…` (7.3 MB)
### Scripts
- `scripts/run_all_tests.sh` reordered into the same 8 phases as the Ansible
master playbook, so a regression caught in one runner also fires in the other.
Six env gates (`RUN_CLEAN_STATE`, `RUN_BUILD_ALL_TARGETS`,
`RUN_DATA_LIFECYCLE`, `RUN_LIVE_FEED`, `RUN_SCAN_DUMPS`, `RUN_UI_BROWSER`,
`RUN_BENCHMARK`) keep the hour-plus steps opt-in.
- New generic `run_script <name> <rel> <args…>` helper invokes any shell/python
helper, records pass/fail, writes to the Markdown report.
- `tests/scripts/*.sh` received consistent SPDX headers, strict-mode
`IFS=$'\n\t'`, and the `CURL_OPTS=(-sk --max-time 10)` array pattern
everywhere the helpers spawn curl (fixes silent mangled-argument bug under the
strict IFS).
### `dumps_archive/`
- 74 compressed `.7z` snapshots tracked (~245 MB total, every file under
GitLab's 100 MiB blob limit). `.gitignore` refactored — the old
`/dumps_archive/` rule excluded the directory as a whole and prevented any
negation from taking effect; new pattern is `/dumps_archive/**` +
`!/dumps_archive/` + `!/dumps_archive/*.7z`. Raw multi-GB `.ndjson` exports
(notably `csaf_suse.ndjson` at 3.17 GB) stay ignored.
## v0.1.30 (2026-04-19)
### Security
- **TLS 1.3 is now mandatory on every build.** The legacy
`#[cfg(not(feature = "quic"))]` plaintext-HTTP fallback branch in `vl-web` has
been deleted; `rustls`, `rcgen`, `time`, and `tokio-rustls` are moved from the
optional `quic` feature into unconditional dependencies. The `quic` feature
now gates only the QUIC/HTTP3 UDP listener (`quinn`, `h3`, `h3-quinn`); TCP on
8080 always runs under TLS 1.3. Verified via testssl.sh: 0 findings in the
full Heartbleed / Ticketbleed / ROBOT / LUCKY13 / BEAST / SWEET32 / DROWN /
LOGJAM / FREAK / POODLE / CRIME / BREACH / RC4 / Winshock vulnerability suite.
- `cargo audit`, `cargo deny`, `cargo vet`, and `cargo machete` are all green
again after bumping rustls-webpki, fastrand, rand, git2 and friends,
modernising `deny.toml` for cargo-deny ≥ 0.15's new schema, setting
`publish = false` on every first-party crate (fixes the `allow-wildcard-paths`
false positive), trust-sweeping 16 reputable publishers into the supply-chain
store, and regenerating exemptions for the residual 138 KLOC audit backlog
(141 fully audited + 1 partial + 257 exempted).
### Features
- **New Debian Security Tracker dashboard** at `/dashboards/debian`, plus a
read-only JSON API at `/api/v1/debian/{stats,packages_top,releases}`. Sourced
from <https://security-tracker.debian.org/tracker/data/json> with a 1-hour
refresh cycle (90 s startup delay). Stored in a dedicated
`debian_security_tracker` redb table as three small pre-aggregated blobs so
the page renders in O(1) regardless of the ~60 MB upstream payload. Live
sample: 3 848 packages / 59 433 CVEs / 21 233 open / 226 314 resolved across
five releases (bullseye / bookworm / trixie / sid / forky).
- Top-level navbar now groups dashboards under a `Dashboards ▾` dropdown
(Overview + Debian) so the Debian entry sits under the existing dashboards
menu.
- `DebianSecurityTrackerFeeder` in `vl-feeders::feeders::` with 7 aggregation
unit tests + `fuzz_debian_tracker_json` fuzz target.
### Tooling / scripts
- **`scripts/quality_gates.sh`** (repo root) — canonical runner for every CI
gate. Flags: `--strict`, `--fast`, `--no-fuzz`, `--only <gate>,<gate>...`. 18
gates wired in (fmt / clippy / test / audit / deny / machete / geiger /
rust-doctor / kani / vet / fuzz / linters / fonts / bruno / testssl / sqlmap /
sqllogictest / loadtest).
- **`vulnerability-lookup-rs/scripts/delete_all_data.sh`** — cold-reset helper.
Stops `vl-web`, explicitly removes the six known files under `data/` (no
wildcards, per-file existence check), restarts the release build, verifies
`vulnerabilities.redb` was freshly recreated (mtime ≤ 120 s), and confirms the
heavy feeders (`cna_scorecard`, `debian_security_tracker`, `cvelistv5`,
`cwe_enrichment`, `capec`) are all at ≤ 5 entries.
- **`vulnerability-lookup-rs/scripts/feed_25_recent_over_all_sources.sh`** —
read-only sweep: discovers sources dynamically from
`/api/v1/vulnerabilities/sources`, fetches `recent?source=X&limit=25` for
each, runs `test_unknown_fields.sh` per source, writes a `matrix.tsv` +
per-source JSON snapshot + per-source unknown-fields log under
`logs/feed_25_<ISO>/`.
- **`vulnerability-lookup-rs/scripts/scan_dumps_dumps_archive_with_clamav_yara.sh`**
— security scan of the runtime dumps. Runs `freshclam` with an ephemeral
unprivileged config, fetches the latest YARA-Forge `full` bundle from GitHub
releases (11 633 rules), and scans each target directory with both engines.
ClamAV limits raised to
`--max-filesize=1900M --max-scansize=1900M --max-recursion=32 --max-files=100000`
so large NDJSON dumps aren't silently skipped. `--alert-encrypted` flags
password-protected archives.
- **`vulnerability-lookup-rs/scripts/generate_db_schema_docs.sh`** — regenerates
`documentation/db_schema.md` by parsing `storage.rs` for every
`TableDefinition` / `MultimapTableDefinition` (capturing rustdoc comments as
the purpose column) and concatenating every
`crates/vl-models/src/migrations/*.sql` with a live `.schema` snapshot.
Current output: 19 redb tables + 58 SQLite objects.
### Tests
- **`test/bruno/` collection** at repo root (per skills/rust.md §1442) — 47
`.bru` files across 6 sub-collections (`api-v1`, `debian`, `dialog`, `feed`,
`pages`, `static`) covering every one of the router's 38 HTTP endpoints. 47/47
pass against the live TLS listener, 146/146 assertions green. Runner at
`test/bruno/run.sh` iterates every sub-collection and aggregates pass/fail.
- **`test/testssl/run.sh`** (repo root) — ships the 5-format testssl.sh report
to `documentation/tls/testssl/<ISO-8601>/` on every TLS config change, per
skills/rust.md §1515.
- `cargo test --workspace --all-features` now runs **536 tests** (87 vl-core lib
\+ 112 vl-core integration
- 263 vl-feeders lib + 35 vl-models + 114 vl-web lib + vl-web integration
targets), zero failures. Doctests added as a separate gate pass.
- 3 internal bugs in `tests/scripts/test_unknown_fields.sh` fixed (CURL_OPTS
array not word-splitting under strict IFS, per-source severity default
counting every row as UNKNOWN, `grep -c || printf 0` double-zero syntax
error).
- `scripts/quality_gates.sh::gate_test` tightened: `--workspace --all-features`
plus a separate `--doc` pass; `--lib` alone is now explicitly documented as a
forbidden gate invocation (silently skipped integration tests in earlier
releases).
- `gate_audit` now runs
`--deny warnings --deny unsound --deny unmaintained --deny yanked`;
`gate_geiger` now runs with `--workspace` (previously scanned only the root
package).
### Docs
- New **`documentation/db_schema.md`** auto-generated from `storage.rs` + the
SQL migration sources. 19 redb tables, 58 SQLite objects.
- New top-level **"No silent skips"** chapter in both `CLAUDE.md` and
`skills/rust.md` — table of required scope flags per gate, narrow-scope
invocations banned in CI and release checklists.
- Release checklist in `skills/rust.md` now requires the two-pass test
invocation plus `--include-ignored` for the opt-in heavy test matrix.
## v0.1.29 (2026-04-13)
### Security
- Fixed RUSTSEC-2026-0098 + RUSTSEC-2026-0099 (rustls-webpki name-constraint
bypass, both HIGH) by pinning `rustls-webpki` to 0.103.12 in Cargo.lock.
- Bumped `fastrand` 2.4.0 → 2.4.1 (yanked) and `rand` 0.9.2 → 0.9.4
(RUSTSEC-2026-0097 unsound).
- Published advisory `ndaal-sa-2026-016` under `csaf/2026/016/` with `.sha512`
sidecar.
### Features
- CERT-Bund WID CSAF feeder verifies each advisory against its `.sha512` sidecar
before importing.
- `CsafDownloader::with_sidecar_verification()` builder flag;
`CsafGenericFeeder` exposes an opt-in helper.
- New CNA Scorecard dashboard at `/cna-scorecard` sourced from
github.com/RogoLabs/CNAScoreCard. 12 h refresh cycle.
- New `CNA_SCORECARD` redb table and storage helpers.
### Tests
- 12 new sidecar parser/verify tests.
- 5 new CNA Scorecard storage tests.
- 9 new CNA Scorecard route handler tests.
- 9 new running-server integration tests.
- Feeder round-trip tests using in-process stub HTTP server.
### Fuzzing
- 5 new vl-core fuzz targets (`fuzz_sha512_sidecar`, `fuzz_cna_scorecard_json`,
`fuzz_csaf_metadata_json`, `fuzz_csaf_validate`, `fuzz_ndaal_tracking_id`).
- 2 new vl-web fuzz targets (`fuzz_cna_scorecard_route`,
`fuzz_csaf_sidecar_url`).
- Smoke-run: 2.6 M executions of sidecar target in 61 s, zero crashes.
### Kani
- `verify_parse_sha512_sidecar_no_panic` proof.
### Dependencies
- `sha2 = "0.10"` added.
### Release pipeline
- Release v0.1.29 archives + SHA256SUMS published under `release/v0.1.29/` for
all three target triples (`x86_64-unknown-linux-gnu`,
`aarch64-unknown-linux-gnu`, `aarch64-apple-darwin`).
- `release/create_release.sh` rewritten to read the version from `Cargo.toml`
instead of hardcoding 0.1.24.
- Linux binaries copied into `tools/ansible/roles/nvulnlookup/files/` and
SHA-256 checksums recorded in `defaults/main.yml`.
### Scripts
- New `scripts/testssl_endpoints.sh` — wraps `testssl.sh` to verify TLS 1.3 and
RFC-compliant ciphers on every public `vl-web` endpoint, with the eight new
CNA Scorecard routes explicitly marked. Emits JSON + HTML reports to
`target/testssl/`.
- New `scripts/cvelist_to_csv.sh` — converts the official `cvelistV5` snapshot
to CSV with `jaq`.
- New `scripts/cve_api_probe.sh` — samples the CVE list, probes the API via
HTTP/2 and HTTP/3, and emits a comparison report of matches, mismatches, and
missing records.
- `release/create_release.sh` rewritten to be version-parametric.
## v0.1.28 (2026-04-09)
### Features
- Annotation API endpoints: POST/GET sightings (8 types), POST/GET KEV entries
(BCP-07 with evidence), GET notifications, mark-as-read
- Annotation UI pages: /sightings, /annotations with KEV catalog and
notifications tabs
- Navbar: Annotations dropdown menu (Sightings, KEV Catalog, Notifications)
- Router: POST/PUT/PATCH body collection via RequestBody extension for JSON API
payloads
- SQLite schema expanded: BCP-07 sightings, kev_entries (with vuln_id, alt_id,
exploited, status_reason, characteristics, scope, references, gcve_metadata),
kev_evidences (uuid, source, signal, confidence, details), notifications
(type, vendor, product, frequency, hits, is_active, is_read)
- Ansible role tests: test_assets_and_endpoints.yml covering static assets, TLS
certs, binary ELF check, annotation API+UI smoke tests
- Static files and binaries updated in the Ansible Role
### Documentation
- 63 `# Errors` doc sections added to pub functions returning Result
- 20+ `#[must_use]` attributes on pure functions
- 102 `#[ignore]` test attributes with reason
- package.categories added to all crate Cargo.toml
### Testing
- 20 RFC compliance tests (RFC 8288, 6570, 3986) using nom-rfc8288,
percent-encoding, url crates
- 51 HATEOAS compliance tests (bash + curl)
- test_unknown_fields.sh for UNKNOWN detection
- Ansible test: test_assets_and_endpoints.yml
### Fixes
- cvelistv5: CVSS from ADP containers, dates
- HATEOAS: /sightings link, KEV SQLite keyword
- SSVC exploitation status to severity mapping
### Quality
- rust-doctor: 92/100 Great, 0 errors
- 0 clippy warnings, ~600 warnings resolved
- 658+ tests, 38 fuzz targets, 0 failures
### Deployment
- Ansible: vl-web 12 MB, vl-cli 8 MB Linux amd64
- 15 static assets, Molecule binary updated
- README (EN/DE/FR), Reference.md updated
---
## v0.1.27 (2026-04-09)
### Features
- Pure Rust CSAF downloader replaces Go binary dependency
(`csaf_downloader.rs`), using reqwest+rustls natively
- 135 feeders (4 new): ZDI direct RSS feeder, Exploit-DB git feeder, OSV Golang
feeder, GCVE VL API feeder
- All 78 CSAF provider metadata URLs populated (11 previously empty now filled)
- Expanded BCP-07 compliant models: Sighting (8 types), KevEntry (structured
annotations), KevEvidence (evidence taxonomy), Notification (in-app
subscriptions)
- GCVE BCP utilities: registry loading, integrity verification, ID validation,
GNA lookup
- In-app notification dispatcher with pattern matching (vuln ID prefix, source,
severity)
- SQL migration for BCP-07 annotation columns
- 6 new fuzz targets: CSAF provider metadata, changes.csv, index.txt, GCVE ID
validation, sighting JSON, KEV BCP-07 JSON
### Security
- Go binary dependency removed (0 external process spawning for CSAF downloads)
- cargo-audit: 0 vulnerabilities (csaf-walker dependency with rsa CVE removed)
- 0 clippy warnings
- 658 tests, 0 failures
- 34 fuzz targets
### Breaking Changes
- `csaf_downloader_path` config field removed from `GenericConfig`
- CSAF provider `create()` functions now accept `reqwest::Client` instead of
`&str` path
---
## v0.1.26 (2026-04-08)
### Features
- 131 feeders wired in background scheduler
- Severity extraction for 5 JSON formats (NVD, CVE 5.0, EUVD, CSAF, direct
fields)
- csaf-walker 0.15.1 integrated (pure Rust CSAF)
- csaf_validate.rs CSAF 2.0 structural validator
- git_helper.rs system git wrapper (TLS support)
- Dashboard: publishing stats + severity charts
- 28 fuzz targets
- MSRV 1.93.0
### Security
- rust-doctor score: 93/100
- cargo-audit: 1 vulnerability (rsa 0.9.10 via sequoia-openpgp, transitive from
csaf-walker)
- 0 clippy warnings
- 399 tests, 0 failures
### Fixes
- Severity UNKNOWN for cvelistv5/EUVD entries
- fapolicyd graceful failure on cloud instances
- Missing health_check files for Molecule
- systemd service name default: nvulnlookupd
- All clippy warnings resolved
---
## v0.1.25 (2026-04-08)
### Features
- 131 feeders wired (78 CSAF, 12 git-based, 17 HTTP, 7 enrichment, dashboard
stats)
- CVE Publishing Statistics dashboard: daily rate, mean gap, batch rate, hourly
distribution, top 10 CNAs, monthly trend (Chart.js)
- CVSS Severity Distribution dashboard: CVSS 3.1 / 4.0 toggle, 1-6 month
timeframe selector
- CIRCL KEV + EUVD KEV feeders
- CERT-FR, JVNDB, Tailscale, CNW KEV feeders
- csaf-walker 0.15.1 (pure Rust CSAF library)
- csaf_validate.rs CSAF 2.0 structural validation
- git_helper.rs: system git wrapper (replaces git2 for TLS support without
OpenSSL)
- gzip decompression for HTTP feeders (reqwest)
- 45-day TLS certificate validity (Let's Encrypt approach)
- display_id: strip source prefix from UI
- CSAF advisory 006 (fastrand 2.4.0 yanked)
### Security
- rust-doctor score: 93/100 (Security 99, Performance 97, Dependencies 98,
Reliability 91, Maintainability 73)
- 28 fuzz targets, 0 crashes
- cargo-audit: 0 vulnerabilities
- 399 tests, 0 failures
- License: Apache 2.0
### Fixes
- EPSS/KEV CSV column mapping (was storing CVSS as EPSS score)
- CISA KEV: gzip decompression + serde(flatten) for new cwes field
- systemd service name default: nvulnlookupd
- Molecule: update_cache for jq, handler alias
---
## v0.1.24 (2026-04-07)
### Features
- Enrichment API wired: /enrichment, /cwe, /capec, /gcve, /cvss endpoints
- EPSS/KEV, CWE, CAPEC, GCVE enrichment feeders in background scheduler
- CSAF advisory 006 (fastrand 2.4.0 yanked)
- Cross-compiled Linux x86_64 + ARM64 binaries
- About page: CPU/Memory comparison, fuzz count
### Security
- rust-doctor score: 93/100
- 20 fuzz targets
- 235 tests, 0 failures
---
## v0.1.23 (2026-04-05)
Initial release of nvulnlookup -- a complete Rust rewrite of
vulnerability-lookup.
### Features
- 25 feeders (19 data sources + 6 enrichment): NVD, CISA KEV, CNVD, 8 CSAF, 5
OSV, PySec, EPSS, CWE, CAPEC, GCVE, Nuclei, Tsunami
- Single binary, zero external dependencies (embedded redb + SQLite)
- TLS 1.3 via rustls (no OpenSSL)
- QUIC/HTTP3 support via quinn
- HATEOAS REST API with hypermedia links
- Dark/light theme toggle
- KEV catalog browser (CISA, CIRCL, EUVD)
- Timeframe picker and multi-source filtering
- Enrichment: EPSS, KEV, CWE, CAPEC, ATT&CK, GCVE, Nuclei, Tsunami
- Untrusted source warnings (FSTEC, CNVD, CNNVD)
- RSS and Atom feeds
- CIRCL NDJSON dump import
- Background feeder scheduler (NVD 2h, CISA KEV 6h, CNVD 2h, OPC 6h, Nuclei 12h,
Tsunami 24h, EPSS 12h, CWE 24h, CAPEC 24h, GCVE 12h)
- Ansible deployment role for Debian 13
### Security
- rust-doctor score: 92/100
- 0 SQL injection vulnerabilities
- 20 fuzz targets, 839K+ executions, 0 crashes
- cargo-audit: 0 vulnerabilities
- 203 tests, 0 failures
### Platforms
- macOS x86_64 (12 MB vl-web, 7.7 MB vl-cli)
- macOS ARM (8.3 MB vl-web, 6.8 MB vl-cli)
- Linux x86_64 (10 MB vl-web, 8.3 MB vl-cli)
- Linux ARM (9.2 MB vl-web, 7.4 MB vl-cli)