<!-- Copyright (c) 2026 Pierre Gronau, ndaal in Cologne -->
# Changelog
## v1.2.78 — Unreleased
- **Fix: `VL_ALLOWED_HOSTS` / `--allow-host` accepted only the exact
`Host` string.** A bare hostname never matched `Host: name:8443`, so a named
deployment answered `host not allowed` unless the port was spelled out. A bare
entry now admits that host on any port (case-insensitive); an entry with a
port still pins it. Lookalike names (`evilname`) stay rejected.
- **Fix: `/api/v1/vulnerability/{id}/cvss` and `/cwe` returned nothing for
multi-provider records.** `/cvss` read only an NVD-shaped `metrics` block, so
a record with `nvd`, `mitre` and `opencve` sections gave `{}`; `/cwe` read only
the enrichment table, which is empty until a feeder fills it. Both now fall
back to the extractors the detail page uses. `/cvss` returns a list of
`{version, vectorString, baseScore, baseSeverity, source}` for those records
and keeps the old map for NVD-shaped ones; `/cwe` returns the record's CWE ids.
Unit-tested on a reconstructed record; the live record (CVE-2026-8788) was
not available. Bruno requests 112 to 117 cover both endpoints but have not
been run yet.
- **Four new quality gates: `lsan` (207), `msan` (208), `hardening_check` (209),
`cwe_checker` (210).** `lsan` and `msan` run the tests under
`-Zsanitizer=leak` and `-Zsanitizer=memory` (MSan only over the pure-Rust
`vl-codec` crate, because every dependency must be instrumented). `hardening_check`
runs Debian's `hardening-check` over the Linux ELF release binaries, with PIE,
read-only relocations and immediate binding enforced. `cwe_checker` runs
FKIE's tool over `vl-cli`, from a native binary or an image that is already
present. All four are `--full` only and report-only unless `<GATE>_STRICT=1`.
Checked on Linux, not on macOS where they skip: LSan is clean on `vl-codec`,
and a deliberate 4096-byte leak is reported (exit 23, gate exit 1 under
strict); `hardening-check` on Debian 13 found 0 high findings across the four
release binaries (16 low). MSan is clean on `vl-codec` on Debian 13 and a deliberate uninitialised
read is reported (exit 23); `cwe_checker` has only run its skip paths and
its stub tests. Wired in `quality_gates.sh` and `run_all_tests.sh`,
mirrored under `skills/rust-testing/scripts/`. `elf_hardening` (206) is wired
in the same pass.
- **csaf-crud flyer updated to 1.5.29, with a German edition.** The figures
were counted at the `v1.5.29` tag: 738 tests listed and 738 passed, 39 fuzz
targets, 5 Kani proofs, 6 Loom models. The earlier sheet said 634, 34, 2 and 4.
- **Test-practices deck v1.7.3** and `Test_Sequences.md` section 48: seven slides
reconciled with the gates that exist (MIRAI, benchmarking, sanitizers, race and
heap profiling, binary hardening, fail-point injection, stateful proptest), a
new slide for the reusable quality-gate runners, and the cover and colophon
slide counts replaced by measured ones.
- **Workspace `self_update` 1.3.0 -> 1.3.2** (`vl-updater`). The lock moves
`self_update` to 1.3.2 and `zipsign-api` 0.2.1 -> 0.2.2; upstream lists both
releases as dependency updates with no migration. `vl-updater` checks with
`--all-targets --all-features` and its 29 tests pass; the skill's pin checker
reports 0 findings over 33 manifests. `cargo deny` still fails on two advisories
unrelated to this bump (RUSTSEC-2026-0385 `ctutils` 0.4.2, RUSTSEC-2026-0386
`tokio` 1.53.1), and `cargo vet` reports a `policy.audit-as-crates-io` problem
for `vl-web` 1.2.78; neither is touched here. No CSAF advisory was written for
the bump.
- **`skills/rust-self-update`: baseline `self_update` 1.3.1 -> 1.3.2.** 1.3.2
(2026-10-24, MSRV 1.88, MIT, not yanked) is a dependency update (`quick-xml`
0.42, used only by the `s3` backend); upstream lists no migration. The skill,
its two reference files, `check_self_update_pin.sh` (`MIN_VERSION`) and the
sibling bats (87 tests, all passing; a new case pins that a 1.3.1 pin is below
the floor) all move to 1.3.2. This repository's own workspace pin
(`self_update = "1.3.0"`) is not bumped by this change.
- **New router skill `skills/java`, and `skills/java-jvm-hardening-suite`
committed lint-clean.** The router names the seven `java-*` skills, says which
to load for which question and how conflicts between them resolve; each
`java-*` skill now links back to it, and `CLAUDE.md` lists it. The hardening
suite (JVM flag profiles for Java 8, 11, 17, 21 and 25 with Dockerfiles) passes
`mdformat`, `rumdl`, `markdownlint`, `shfmt`, `shellcheck` and `hadolint`;
`verify-profiles.sh` passes 15 checks and skips 5 because no JDK homes are
configured. The Dockerfiles gained placeholder `LABEL`s (zero git revision,
`info@ndaal.eu` contact) to satisfy the repository label schema; replace them
at build time. `skills/bash-testing` is also committed now.
- **Python gates run for real; strict mypy errors 630 -> 78.** Running
`skills/python-quality-gates` on this repository's 79 first-party Python files
found that `mypy.ini` had never worked (it asked for Python 3.9 and had
comments after values), fixed in `c82b17899`. The strict errors that remained
were fixed by annotation (ruff return types, the repeated `v3_block` /
`v4_block` / `build` signatures in 28 generators, bare generics, test
fixtures) plus two real latent bugs: a `None` match in
`generate_admin_guide_script_inventory.py` and an unhandled missing score in
an untracked generator. The 78 left are `csaf/csaf_hashes.py` (uncommitted
edits of another session) and the calls into it. The runner itself had a
bug the first honest run exposed: it passed `QG_PY_PATHS` as one argument, so
its earlier `ruff`/`bandit` passes were false greens. Real counts on this
tree: ruff 1432 findings under the repo's select-all config, bandit 438 Low,
not fixed here.
- **New skill `skills/python-quality-gates`.** The Python and documentation
sibling of `rust-quality-gates`, distilled from `Test_Sequences.md` section 29
and the Python gates of `scripts/quality_gates.sh`: a generic runner (typos,
lineguard, ruff format and check, bandit, mypy, pytest, mdformat, markdownlint,
rumdl; `--full` adds pip-audit, vulture, radon) with a sibling bats (57 tests),
shdoc README, PlantUML workflow with renders, the validating renderer, and 16
lessons. radon exits 0 whatever it finds (measured), so its gate fails on a
non-empty report. The bats ran green but was not mutation-checked, because
the host load was about 250 and one run took over 13 minutes.
- **Version bump 1.2.77 -> 1.2.78**: the workspace version in
`Cargo.toml`, every `vl-*` path pin beside it, every `vl-*` crate in
`Cargo.lock`, and each other site `scripts/bump_version.sh` knows that
still declares a version. Opens the 1.2.78 development cycle.
- **New skills `skills/html-meta-tags-vulnlookup` and
`skills/html-meta-tags-lagebild`**, siblings of `html-meta-tags-sarif`: the 14
description, author, Open Graph and Twitter card tags for
`vulnlookup.ndaal.eu` and `lagebild.ndaal.eu` (live from 2026-10-24 per the
product owner), the rules, the `/social-preview.png` requirements and the
checks. Both are marked "specified, not implemented": neither product carries
the tags or the image route yet, and the Lagebild local-check port is an
assumption.
- **Skill renamed: `languages-europe-rust` -> `rust-languages-europe`.** Every
reference (`skills/rust`, `languages`, `languages-europe`, `languages-asia`,
`languages-america`, `rust-htmx-multilingual-landing`, two asset comments) and
the checksum sidecars were updated; nothing under `skills/` names the old
directory any more.
## v1.2.77 — Unreleased
- **Guides: fixing "VulnCheck token stored but could not be decrypted".** The
Administrator Guide and the Troubleshooting Guide now carry the fix: the
passphrase `VL_SECRET_PASSPHRASE` is missing from the server process or
differs from the one used with `set-secret`; re-store the token with the
server's passphrase and data home. Seen on vulnlookup.ndaal.eu on 2026-10-23.
- **Release v1.2.77 published.** Artefacts and the GitLab Release are on
`ndaal_public_nvulnlookup_release2` (the main repository is storage-quota
blocked); the `cargo binstall` `pkg-url` of `vl-cli` and `vl-web` now points
there. The quality-gates preflight was skipped at the maintainer's request,
and the guide-PDF step of the pipeline failed (missing `.venv-guides`).
- **New skill `skills/rust-quality-gates`.** A generic, gate-clean
`quality_gates.sh` runner for any Rust project (typos, lineguard, fmt,
clippy, tests, doctests, doc; `--full` adds audit and deny) with a sibling
`quality_gates.bats` (13 tests; a swapped-order mutant is caught), and the
lessons from this repository's runner: strict switches against report-only
false greens, counted skips, widest scope, a workspace in a subdirectory,
literal count pins. Wired into `CLAUDE.md` and `skills/rust`. It also ships
the shdoc sibling README, a PlantUML workflow with its rendered SVG and PNG,
and a copy of the validating renderer; the sibling bats now carries the
structural `skills/bats` tests (54 in all). Known: two tests of the copied
renderer's bats (`bash >= 4.4` guard, `rm -f -v`) fail, and they fail the
same way in `scripts/`.
- **Quality-gates workflow diagram updated.** `scripts/quality_gates.uml` (and
the rendered `.svg`/`.png`) now show typos (0b) and lineguard (0c) ahead of
fmt (1) and clippy (2), with the strict switches each needs to fail the
run. The renderer `render_quality_gates.uml_svg_png.sh` had declared
`readonly SCRIPT_PATH` twice and aborted before drawing anything; the
duplicate is gone and the four-way output validation passes.
- **Version bump 1.2.76 -> 1.2.77**: the workspace version in
`Cargo.toml`, every `vl-*` path pin beside it, every `vl-*` crate in
`Cargo.lock`, and each other site `scripts/bump_version.sh` knows that
still declares a version. Opens the 1.2.77 development cycle.
- **Guides and READMEs updated for this release's work.** The five READMEs get
a "What's new in v1.2.77" section (the VulnCheck page and token, the strict
text gates, the pre-commit hook), mirrored in German, French, Spanish and
Italian; the structural mirror check passes, and so does a comparison of the
section's inline code spans, which that check does not cover. The workspace
README gets the same entry and says plainly that it has not been refreshed
since v1.2.61. Administrator Guide: the `--fast` order lists `typos` and
`lineguard` (it jumped from `ansible_cfg` to `fmt`), and the fmt and clippy
rows and script entries name their strict switches. Developer Guide: typos and
lineguard rows, a pre-commit hook section (order, workspace directory,
`hooks.env`), the rule that a runner whose exit code decides a gate needs a
strict switch, and the checklist. Troubleshooting Guide: three entries (the
empty VulnCheck page, a hook that checks nothing, a red text or format gate).
User Guide: the KEV tabs and why the VulnCheck one can be empty. The embedded
copies are in sync and their sync tests pass. Not touched: the figures in
older sections, and the documents that do not describe this work. The
translations are mine and have not been read by a native speaker.
- **The pre-commit hook now reads the `hooks.env` its lintscout message names.**
The failure message said to raise `RUST_LINTSCOUT_THRESHOLD` "in hooks.env",
and nothing read such a file. The hook now reads `hooks.env` from its own
directory: one `KEY=VALUE` per line, blank lines and `#` comments ignored, one
pair of surrounding quotes removed. The file is data and is never run, so
`$(...)` in a value stays text. Only the hook's own settings are accepted; a
misspelled key, a name such as `PATH`, or a line without `=` stops the commit
with the file and line, because a setting that silently does nothing is the
failure this release has been removing elsewhere. Precedence: environment,
then `git config rusthook.cargodir` (workspace directory only), then
`hooks.env`, then the default. `SKIP_RUST_HOOKS=1` skips the file too. A
versioned `hooks.env` is what lets a team share the workspace directory and
the lintscout ratchet instead of each clone setting them. The workspace suite
went from 23 to 35 tests (a test that every setting the hook reads is in the
allow-list included); five mutants of the loader (environment loses, git
config loses, value run as shell, any key allowed, escape hatch removed) are
each caught. `.githooks/pre-commit` (untracked) is refreshed; no `hooks.env`
is added to this repository, and the hook is still not activated.
- **`quality_gates.sh --fast` runs typos, lineguard, fmt and clippy as real
gates, in the pre-commit order.** The order was already right (adjacent in
`ALL_GATES`) and none of the four is a live gate, so `--fast` kept them. But
three of them could not fail: `test_lineguard.sh`, `test_cargo_fmt.sh` and
`test_cargo_clippy.sh` are report-only by default and exit 0 on findings,
`quality_gates.sh` records PASS or FAIL from the exit code alone, and only
the typos gate asked for a strict verdict. Probed with a crate holding
unformatted code and a `clippy::needless_return` error: both runners exited
0. The runners now take `LINEGUARD_STRICT=1` (existing), `CARGO_FMT_STRICT=1`
and `CARGO_CLIPPY_STRICT=1` (new) and exit 1 on findings, and the gates pass
them; run by hand they stay report-only. Measured before switching: `cargo
fmt --all --check` and the workspace-wide `cargo clippy --all-targets
--all-features -- -D warnings` are clean, and lineguard reports 0 issues over
1081 targets, so the gates are green today. The typos gate's label said
`15.`; it now says `0b.`, as the gate list does. `scripts/quality_gates.bats`
gains 9 tests (order and adjacency, `--fast` membership read from both
arrays, labels, and each gate against a stub runner that fails only on a
strict request), the two runner suites 3 each, all red first.
`Test_Sequences.md` gets §24a (the four gates against the hook's steps) and
§49 (the hook's sequence) and re-derived counts: 275 gates, 154 live,
`--fast` 121, `--rest` 155; the figures in the older sections were not
recounted. The two runners' checksum sidecars are regenerated; the old ones
named a path from another repository.
- **The Rust pre-commit hook no longer runs rust-doctor.** Removed on request:
the verify step that demanded a 100/100 score and a passed gate, the
`cargo clippy --fix` pass over rust-doctor's lint set, `RUST_FIX_DOCTOR`,
`RUST_HOOK_DOCTOR` and the `RUST_DOCTOR_*` settings, the two matching hooks
of the `pre-commit`-framework config, and `rust-doctor.toml` as a file whose
change starts the hook. The hook no longer needs `jq` or `python3` either;
they only read rust-doctor's JSON. The suite grew to 23 tests: a fake
`rust-doctor` on `PATH` that records a call and exits 1 must never be
called, neither file may mention it, and a change to `rust-doctor.toml`
alone starts no check; two mutants that put it back are each caught.
`.githooks/pre-commit` (untracked) is refreshed to match. The skill text and
CLAUDE.md no longer promise 100/100. `rust-doctor` itself and its skill are
untouched, so the score can still be run by hand.
- **The Rust pre-commit hook finds a workspace in a subdirectory.**
`skills/rust-pre-commit/assets/pre-commit` ended with exit 0 whenever the
repository root had no `Cargo.toml`. This repository keeps its workspace in
`vulnerability-lookup-rs/`, so installing the hook would have been active and
silent while checking nothing. `RUST_HOOK_CARGO_DIR` (or, for every commit
in a clone, `git config rusthook.cargodir`; the environment wins) names the
directory: cargo, typos and rust-doctor run there, and only staged files
below it start the checks. Git, the staged-file gate and the work-tree
isolation still work from the repository root. A value that is absolute,
leaves the repository (by `..` or by a symlink), does not exist or holds no
`Cargo.toml` now stops the commit and names the setting. Without the setting
nothing changes. The new suite `scripts/pre_commit_hook_workspace.bats` (19
tests, rustfmt steps only, offline) drives the hook in a throwaway
repository, including the round trip of unstaged edits from a subdirectory
and lineguard's root-relative file list; four mutants of the hook are each
caught by it, after the first run showed that the containment test needed a
real directory outside the repository to be able to fail. The file list now
uses `core.quotePath=off`, so a non-ASCII file name is no longer skipped by
the Rust-file gate. `.githooks/pre-commit` (an untracked file, as it was
before) is refreshed and byte-identical to the asset. **The hook is not
activated:** `core.hooksPath` of this worktree
still points at `.git/hooks`. CLAUDE.md now points at `rust-pre-commit` and
`rust-pre-push`.
- **`/kev?catalog=vulncheck` now says why it is empty, and the VulnCheck API
token is read every cycle.** Two defects sat behind an empty VulnCheck
catalog. The server looked the stored token up once, 35 minutes after
start-up, so a token stored later with `vl-cli set-secret --vulncheck-kev`
was ignored until the next restart. The page also answered every empty
catalog with "The CISA KEV feeder may not have run yet.", whatever had
happened. The token is now looked up at the start of each 6-hour cycle
(`vl-web/src/vulncheck_token.rs`), and a skipped cycle logs its reason: no
token stored, a token that cannot be decrypted (passphrase unset or
different), or an unreadable settings store. The feeder records the outcome
of its latest cycle in memory and the empty page states it in a fixed
sentence: no token, token not decryptable, token refused (HTTP 401 or 403),
another HTTP status, unreachable, failed SHA-256 check, unpackable archive,
unreadable response, a storage failure, or "added N entries". The failure
category is decided where the failure happens; no error text reaches the
page, because a failed download's message can carry the pre-signed URL and
its signature. The status is not persisted, so for 35 minutes after a restart
the page says no cycle has completed yet. The other four catalogs keep the
old hint, and a catalog with rows shows no notice. The Administrator Guide
has a table mapping each sentence to a cause and a fix. Not established: why
the production catalog is empty (1.2.76 shows 0 entries and "never" on
`/stats`); that needs the first cycle's log line or the sentence the page now
shows. The live import itself was verified earlier with a valid token. Tests
cover the token lookup (stored later, replaced, wrong or missing passphrase,
non-UTF-8, unreadable store), the outcome categories and their error kinds,
the page through the real handler, and properties over arbitrary input; the
fuzz entry point now also drives the envelope, digest and catalog parsers.
Mutation testing was not run.
## v1.2.76 — Unreleased
- **The README and its four translations are refreshed for 1.2.76.** The
figures dated from 1.2.66 or older. The README now says 195 feeders (was 135)
from 160 sources, 101 CSAF feeders, 24 OSV ecosystems, 92 dump sources, seven
crates in the architecture block, 2424 tests across 146 targets (was 722
across 26), 1875 Bruno requests holding 5331 assertions, 884 published CSAF
advisories (was 487) and 178 cargo-fuzz targets; each was recomputed from the
tree or from this release's passing gate run. A new "What's new in v1.2.76"
section covers v1.2.68 to v1.2.76, the CWE detail route reads
`/attacks/cwe/{id}`, and the Ansible example names 1.2.76. "0 crashes" is
dropped from the fuzz line because no sweep was run for this refresh. Not
refreshed: the code-health score and the benchmark table (`rust-doctor` 0.7.0
rejects the committed `rust-doctor.toml`, and the README now says so), and the
separate `vulnerability-lookup-rs/README.md`. German, French, Spanish and
Italian were updated in the same change and pass the structural mirror check.
- **`nuclei_fire/nuclei_scanner.sh` now passes its own bats suite.** Ten of the
41 tests failed: the script predated the canonical bash template. Its SPDX
header sat below a ShellCheck block, it had no Bash version guard and no
`REPO_ROOT` anchor, it ended without `exit 0`, and `mkdir`, `chmod`, `rm` and
`mv` ran without `-v`. All of that is added. On a Bash older than 4.4 the
script now re-executes itself under a Homebrew bash, or stops with a clear
message, instead of failing later at `shopt -s inherit_errexit`; the file
operations now print what they touch. One test passes by skipping rather
than by conforming: the function-local `timestamp` variables are renamed
`log_timestamp` and `scan_timestamp`, because the canonical form would put
colons into the result file names and print an Info line on every log call.
The date formats are unchanged. The scanner itself was not run, only its
bats suite, `shfmt` and `shellcheck -o all`.
- **The ndaal-ad-report flyers get a German edition and a recheck against
0.4.0.** The English product sheet and executive brief already named 0.4.0,
the newest version on crates.io; the German product sheet and brief are new.
One unmeasured claim is withdrawn: the executive brief said a run costs
"minutes of controller time", which nobody timed, so it now says the host
collection is heavier than the directory read and needs elevation. The
evidence file records two findings from the source: the updater enables
`self_update`'s `checksums` feature and the release publishes five digest
files, yet `updater::install()` calls no verification, so integrity rests on
TLS alone; and the crate named `ndaal-ad-report` is a library without a
binary, while the installable crate is `ndaal-ad-cli`. Sources, HTML, PDFs
and the evidence are in `documentation/flyer/ad-report/`.
- **The nuclei downloader gets one more community source: the php2shell
templates.** `nuclei_fire/nuclei_scanner.sh` now lists
`Ademking/php2shell-nuclei-templates` in `COMMUNITY_SOURCES`, so
`--install` and `--templates` clone it and `import_nuclei_templates.sh` picks
it up as the folder `Ademking__php2shell-nuclei-templates` with the
community trust score. The repository holds four templates for the PHP
`session.upload_progress` research (CVE-2026-109618, CVE-2026-109619 and two
end-to-end proofs that write session files on the target, which is why it
belongs in the community set and is skipped by `--trusted-only`), plus a lab
folder of vulnerable PHP fixtures. It publishes no licence file. Three new
bats tests pin the entry (exactly once), the absence of duplicate URLs under
case folding, and the owner/repo URL shape the importer's awk relies on.
- **The ms-patch-downloader flyers are rebuilt for 1.5.6, with a German
edition.** The product sheet and the executive brief dated from 1.4.1 and
described a web UI that no longer exists: plain HTTP on port 3000, English
only. 1.5.x serves HTTPS with TLS 1.3 on 8980, HTTP/3 on 8981 and five
languages, and the sheets now say so, along with self-update in every
program, the hardened Linux builds, the 35 fuzz targets and the SBOMs. A
claim that redirects are "never upgraded to HTTPS" is reversed to match the
README. The evidence file also records that the project's own `SECURITY.md`
and `security.txt` expired on 2026-05-09. Sources, HTML, PDFs and the
evidence are in `documentation/flyer/ms-patch-downloader/`.
- **The sarif-viewer flyers get a German edition and a claim-by-claim recheck
against 1.1.12.** The English sheets already named 1.1.12; the executive
brief is re-dated and two unmeasured promises ("minutes", "the same hour") are
replaced by what actually happens. The German product sheet and brief are new.
The evidence file lists what the crate's own README gets wrong: an install
section pinned to 0.1.4, a Rust version of 1.96 against the manifest's 1.93,
and a different legal-entity string in the SPDX header. Sources, HTML, PDFs
and the evidence are in `documentation/flyer/sarif-viewer/`.
- **The simdutf8-cli flyers are rebuilt for 1.3.5, with a German edition.** The
product sheet and the executive brief dated from 1.3.3. They now state the
Rust 1.95 minimum (1.3.2 to 1.3.4 declared 1.93 and did not build on it),
name the five prebuilt targets and the CycloneDX SBOM the release carries
(there is no Windows ARM build), and keep saying that the mutation and
coverage figures are from 1.3.0, because none were published since. The
README mentions neither the binaries nor mimalloc, so the sheets follow the
manifest and the release. Sources, HTML, PDFs and the evidence are in
`documentation/flyer/simdutf8-cli/`.
- **The mimetype-cli flyers are rebuilt for 1.1.5, with a German edition.** The
product sheet and the executive brief dated from 1.1.4. 1.1.5 shipped without
prebuilt binaries: `release/` and `SHA256SUMS` still carry the 1.1.4 builds,
so `cargo binstall` would compile from source and the self-updater has
nothing newer to offer. The technical sheet therefore drops the generated
"prebuilt binary, no compile" install hint and says so. The "598 tests" and
"19 properties" figures, results of the 1.1.4 run, are replaced by a count
(729 test attributes, 12 fuzz targets), and the three open file-name issues
and the Windows allocator exception from the changelog are stated. Sources,
HTML, PDFs and the evidence are in `documentation/flyer/mimetype-cli/`.
- **The archmeld flyers are rebuilt for 1.4.15, with a German edition.** The
product sheet and the executive brief dated from 1.4.14. They now name the
seven advisories 1.4.15 closes (three in `zip`, four in `rustls`) and keep
the warning that a build from crates.io still resolves the vulnerable
quick-xml. One claim is withdrawn: the earlier sheets listed stack canaries
among the Linux hardening, but neither the README nor the repository's linker
configuration sets them. Sources, HTML, PDFs and the evidence are in
`documentation/flyer/archmeld/`.
- **New flyers for the Grundschutz++ OSCAL Viewer 1.6.64, in English and
German.** The product had none. A product sheet and an executive brief per
language, written from the published crate: 1,000 controls in 20 practices,
140 embedded crosswalks (183,353 rows) with 163 frameworks in Settings, no
authentication, and the one network call the README does not mention. Every
start tries to download the YARA Forge rules from GitHub unless
`--no-fetch-yara-rules` is set, so the flyers say "outbound only for
Meilisearch, updates and that download" rather than "fully offline". The
stale README figures (156 frameworks, 8 MB) are not repeated; the binary
measures 43.4 MB. Sources, HTML, PDFs and the evidence are in
`documentation/flyer/grundschutz-oscal-viewer/`.
- **The lagebild flyers are rebuilt for 1.3.8, in English and German.** The
product sheet and the executive brief dated from 1.3.7. They are rewritten
from the published crate and now separate what is true of the listener (TLS
1.3 only) from what is true of the feed client (TLS 1.3 with a TLS 1.2
fallback, because `bundesbank.de` speaks nothing newer), say that the cyber
enrichment is on by default and can be switched off or repointed, and quote
the port range 1.3.8 enforces. The Kani, Miri, Goose and Bruno claims are
marked as living in the repository, since the published crate does not carry
them. Sources, HTML, PDFs and the evidence for every number are in
`documentation/flyer/lagebild/`.
- **The csaf-crud flyers are rebuilt for 1.5.27, in English and German.** The
product sheet and the executive brief dated from 1.5.22. They are rewritten
from the published crates and say what the old ones left out: this release
has no login, so every page and API route is open, and every import and
export is checked against the OASIS CSAF 2.1 suite. The load-test table and
an "afternoon to first advisory" are gone, because nobody measured them for
this version, and the claim that Windows builds ship without mimalloc is
corrected (only aarch64 does). Sources, HTML, PDFs and the evidence for every
number are in `documentation/flyer/csaf-crud/`.
- **The sbom-auditor flyers are rebuilt for 1.6.34, in English and German.**
The product sheet and the executive brief dated from 1.6.27. They are
rewritten from the published crate (nine analyzers with their tested
versions, component search, CSAF drafts for csaf-crud, 49 interface
languages) and each now has a German edition. The privacy line changed: the
old headline said only CVE IDs leave the machine, but **Create CSAF
document** sends a draft whose product tree lists the affected package URLs
to the configured csaf-crud endpoint, so it now says "by default".
`build_flyer.py` localises the `cargo binstall` hint by `lang`. Sources,
HTML, PDFs and the evidence for every number are in
`documentation/flyer/sbom-auditor/`.
- **The nvulnlookup flyers are rebuilt for 1.2.73, and exist in German.** The
product sheet and the executive brief dated from 0.1.52. They are rewritten
from the crates.io packages and the 1.2.73 source (195 feeders, 160 sources,
17 enrichment sources, 166 fuzz targets), and each now has a German edition.
`build_flyer.py` gained a `lang` front-matter key, so a German flyer carries
`lang="de"` and hyphenates. Sources, HTML and PDFs are in
`vulnerability-lookup-rs/documentation/flyer/`, with the evidence for every
number and five inconsistencies found in the product: README counts, the
updater's release host, a binstall URL that returns 404, and two claims the
old sheet made that the code no longer backs.
- **The `cargo vet` exemption list is 58 entries shorter.** 63 new `[[trusted]]`
entries in `supply-chain/audits.toml`, each for a publisher the project
already trusted for other crates (Manishearth 20, rust-lang-owner 11,
kennykerr 8, epage 7, sunfishcode 6, dtolnay 5, BurntSushi, alexcrichton and
cuviper 2 each), valid until 2027-04-21; 417 exemption entries became 359.
This is not an audit: 350 crates are still exempted, of which `cargo vet
suggest` lists 118 as a diff from an audited version (16 of them 50 changed
lines or fewer) and 238 as whole-crate reviews. Importing the last two
registry sources, actix and ariel-os, would remove one more and was not
added. Ten further trust hints exist only because an imported audit set
trusts the publisher; they are not applied.
- **`rustls-webpki`, `webpki-roots` and `webpki-root-certs` updated for the
emSign distrust (RUSTSEC-2026-0382, -0381, -0383).** `rustls-webpki`
0.103.15 -> 0.103.16, `webpki-roots` 1.0.9 -> 1.0.10 and
`webpki-root-certs` 1.0.9 -> 1.0.10, in `Cargo.lock` only. One issue
(CVE-2026-111943): certificate paths through distrusted emSign
intermediates, whose private keys are now well known, could be accepted,
or trusted through the bundled root `emSign ECC Root CA - G3`. The first
reaches every rustls client, the feeders, the self-updater and the
Meilisearch client (ndaal-sa-2026-938). The second reaches only a
Meilisearch client pointed at a remote https URL (ndaal-sa-2026-939).
`webpki-root-certs` is in `Cargo.lock` for wasm32 only, so none of the four
shipped targets links it (ndaal-sa-2026-940). `cargo audit` and `cargo deny`
pass again; the three new versions are covered by regenerated `cargo vet`
exemptions, not by audits.
- **`deny.toml` no longer ignores seven advisories that cannot occur.** The
RUSTSEC-2024-0384, -2025-0057, -2025-0119, -2025-0141, -2026-0098, -2026-0099
and -2026-0104 entries covered the sled / parking_lot 0.11 / rustls 0.21
stack that `rprobe` pulled in. `rprobe` left the dev-dependencies earlier,
`Cargo.lock` has none of those crates, and `cargo deny` reported all seven as
"advisory not encountered". They are gone from `deny.toml` and from the
mirror list in `audit.toml`, so an old entry can no longer hide the same
advisory if one of those crates ever returns.
- **The supply-chain gates stop failing on what cannot be fixed.** `cargo vet
check` aborted on a policy error: the six `publish = true` vl-* crates need
`audit-as-crates-io = false` in `supply-chain/config.toml`, because the
source is ours and the crates.io copy of 1.2.76 does not exist yet. With
that out of the way vet listed 255 crates whose version-pinned exemptions
had drifted from `Cargo.lock`; `cargo vet regenerate exemptions` covers them
again, so the gate now passes by exemption (405 exempted, 201 fully audited,
12 partially), not by new audits. `cargo deny` rejected `libbz2-rs-sys`
(via `sevenz-rust2`) for its `bzip2-1.0.6` licence, a BSD-style text; it is
on the allow list. `basic-toml` (RUSTSEC-2026-0374) and `unsafe-libyaml`
(RUSTSEC-2024-0449) are unmaintained notices that no upstream release
removes: the first runs inside askama's proc-macro at build time, the second
is reached only through the `sql_query_analyzer` dev-dependency of vl-models.
Both are documented ignores in `.cargo/audit.toml` (the file `cargo audit`
reads) and `deny.toml`, published as VEX advisories ndaal-sa-2026-935 and
ndaal-sa-2026-936.
- **The stricter clippy baseline is on.** `vulnerability-lookup-rs/clippy.toml`
now bans `std::env::var`, the `std::process::Command` spawners, the
`std::fs` read/write/create-dir calls and the print macros, as the ndaal
baseline does. The baseline's macro entries (`println`) did not resolve in
clippy, so those bans did nothing; they are `std::println` here. A warn-mode
run flagged 300 sites. The 71 in production code go through new wrappers:
`vl_core::config::env_var` is the one reader of the environment, and
`vl_core::fs::read_to_string_ambient` / `create_dir_all_ambient` the one
plain file read and mkdir, for operator-named paths (a config file may be a
link into `/etc`, which the cap-std `DataDir` refuses). The leaf crates,
tests, binaries, build scripts and examples carry a scoped `#[expect]` with
a reason.
- **zip 9.0.5.** `cargo audit` listed three RustSec advisories against zip
8.6.0 (RUSTSEC-2026-0367 to -0369, published 2026-10-09 and 10). All three
are in extract-to-disk code (`ZipArchive::extract`, `ZipStreamReader`) that
the feeders never call, but the audit and deny gates check the version, so
`vl-feeders` moved from zip 8 to 9. What the migration touched: `name()` now
returns a `Result` (six call sites; an undecodable name counts as not
matching), the test helper in `vulncheck_kev.rs` takes `SimpleFileOptions`,
and zip pulls in `nix` 0.31.3 on Unix. `tests/adoption_zip.rs` pins zip 9
and has five new rows; two show real behaviour changes. Against 8.6.0 a read
past an entry's declared size returned all of it and an empty archive with a
bogus directory offset opened; 9.0.5 fails both. ndaal-sa-2026-919 (CVSS 3.1
and 4.0) records the update. `cargo audit` still lists `basic-toml` and
`unsafe-libyaml`.
- **rustls 0.23.46 and yoke-derive 0.8.4.** `cargo audit` listed four RustSec
advisories against rustls 0.23.45 (RUSTSEC-2026-0346 to -0349, published
2026-10-14) and flagged yoke-derive 0.8.3 as yanked. A lockfile-only
`cargo update -p rustls -p yoke-derive` fixes both; no manifest or source
file changed. Only RUSTSEC-2026-0349 (an unauthenticated ServerHello can pin
later client connections to classical key exchange) reaches nvulnlookup,
through the TLS clients of the feeders and the updater; the other three need
Encrypted Client Hello, a custom `ClientSessionStore` or rustls's `brotli`
feature, and the workspace uses none of them. ndaal-sa-2026-917 (CVSS 3.1
and 4.0) and ndaal-sa-2026-918 record the two bumps. `cargo audit` still
lists `zip` 8.6.0 (RUSTSEC-2026-0367 to -0369), `basic-toml` and
`unsafe-libyaml`; they are not part of this change.
- **Feeders no longer starve each other.** On a fresh data dir the CWE, CAPEC
and Debian feeders could stay unstarted for as long as the first four ran. A
feeder calls the synchronous storage layer straight from its async task, and
every write parks the thread in redb's single-writer slot until the previous
commit's fsync is done. Four feeders in a write held all four workers of the
feeder runtime: a thread sample showed NVD, CISA KEV, KEV ransomware and
EPSS/KEV in `begin_write`. `FeederSpawner` now runs each feeder loop on
its own thread, so a feeder that blocks can only block itself; the workers
serve the feeders' network I/O and timers. On shutdown the parked loops are
dropped at their `.await` instead of each holding the drain for the grace
period. `VL_FEEDER_WORKER_THREADS=32` is no longer needed as a workaround.
Five unit tests pin it, one reproducing the starvation with a plain spawn.
- **The CWE Top 25 table links to the real page, and the enrichment bundle
names itself.** `cwe_dashboard.js` built its row links as
`/attacks/cwe/cwe/{n}`, the doubled path that is only a 308 for old
bookmarks, so every click cost a redirect; it now links
`/attacks/cwe/CWE-{n}`. `GET /api/v1/vulnerability/{id}/enrichment` sent the
vulnerability's link set, so `_links.self` pointed at the vulnerability and
there was no `vulnerability` relation: `self` is now the enrichment document
and `vulnerability` the record it belongs to. A client that followed `self`
to reach the vulnerability must follow `vulnerability` instead. Two offline
tests pin both.
- **A lint suppression is `#[expect(lint, reason = "...")]`, and the gate now
enforces it.** `clippy::allow_attributes` and
`clippy::allow_attributes_without_reason` are `deny` in
`[workspace.lints.clippy]` (inherited by `vl-web` and `vl-feeders`) and in the
own `[lints.clippy]` of the five member crates that cannot inherit that table:
the workspace `unsafe_code = "forbid"` overrules the compile-time transmute
inside the `static_assertions` size macros. All 137 `#[allow]` and `#![allow]`
attributes in the Rust sources (114 files, 117 of them inner) became
`#[expect]`. Because `expect` fails the build once its lint stops firing, 641
of the 837 lint names they listed had to go: they never fired under the gate,
since `unwrap_used`, `panic`, `print_stdout` and the other restriction lints
are not enabled and `clippy.toml` allows them in tests. What is left is 112
`expect` attributes in 86 files, every one with a reason, and 29 files that
carry no suppression at all any more.
- **`vl-updater` stopped suppressing `duration_suboptimal_units`.** It now
writes `Duration::from_mins(10)`; the comment that excused the suppression
said `from_mins` was newer than the MSRV, but the workspace MSRV is 1.95.
- **A test reads the sources as text.**
`crates/vl-web/tests/ test_lint_suppressions.rs` rejects any `allow` and any
`expect` without a `reason`, and checks that every member crate turns the two
lints on. `allow_attributes` ignores an inner `#![allow]`
(`allow_attributes_without_reason` only requires its reason), and both see
only what clippy compiles; the test also covers `cfg`-gated items and the five
standalone `fuzz-harness` crates, which no gate compiles. Its 22 checks were
mutation-tested: 11 of 11 deliberate breaks fail a test, after three weak
samples were strengthened.
- **Known, left as it is.** `vl-web`'s `main()` is one function of about 1,800
lines (clippy: 1804 of 80), and ten `dead_code` expectations remain on fields,
mirror structs and a storage handle that nothing reads. Both are now visible
as `expect` with a reason; deleting or splitting them is separate work.
- **Version bump 1.2.75 -> 1.2.76**: the workspace version in
`Cargo.toml`, the five `vl-*` path pins beside it, all seven `vl-*` crates
in `Cargo.lock`, and both Ansible role defaults. Opens the 1.2.76
development cycle.
- **`bump_version.sh` no longer misses crates.** Its crate lists were written
by hand and left the `vl-codec` pin in `Cargo.toml` and the `vl-codec` and
`vl-updater` entries in `Cargo.lock` at the old version. A stale `version`
on a `path` dependency builds and tests normally and only fails at
`cargo publish`. Both patterns now match every `vl-*` crate, and a new bats
test runs the real bump on copies of the real manifests.
- **vl-web sends a strict Content-Security-Policy.** Scripts, styles and
images may come only from the server's own origin: no `'unsafe-inline'`,
no `'unsafe-eval'`, no `data:`. Before, inline scripts and styles were
allowed, so an injected `<script>` would have run. The scripts of 17 pages
and the inline CSS and JavaScript of `base.html` moved to `/static/js/` and
`/static/css/vl-web.css`; server data reaches the scripts through
`<script type="application/json">` blocks, and the dashboards build their
bars as DOM nodes instead of HTML strings, so feed text is no longer parsed
as HTML. Bootstrap's 23 `data:` icons are served as SVG files
(`scripts/generate_vl_web_bootstrap_local_images.py`), htmx runs with eval,
script tags and injected styles switched off, and the Permissions-Policy
denies 20 browser features. `tests/test_csp_strict_assets.rs` fails on
inline code in a template or script, and `tests/pages/csp.spec.ts` loads 38
pages in a browser and fails on any CSP violation.
- **The `/about` honggfuzz and test-fuzz figures are now pinned to the disk.**
The clause "plus 152 honggfuzz and 124 test-fuzz targets" was typed by hand
and had gone stale: the tree holds **176** honggfuzz targets
(`hfuzz/fuzz_targets/*.rs` in vl-core, vl-web and vl-feeders: 56 + 47 + 73)
and **138** test-fuzz targets (the `#[cfg_attr(test, test_fuzz::test_fuzz)]`
wrappers in those crates' `fuzz-harness`: 51 + 44 + 43). 152 was true when
it was written, so this is drift rather than a wrong rule. A new
`test_about_page` test computes both figures from the disk, like the
cargo-fuzz total beside them, so the next added target fails the build until
the page is updated; a probe showed it counts a new target and ignores a
comment that only names the attribute. The v1.2.75 `/about` entry said these
two figures matched no counting rule. That was a search error: the attribute
sits inside `cfg_attr`, which the first search did not match.
## v1.2.75 — Unreleased
- **`generate_admin_guide_script_inventory.py` previews by default.** It
parsed no arguments, so `--help` performed a real rewrite of the
Administrator Guide's Bash Script Inventory (a 4 900-line diff of unrelated
drift). It now takes `--help`, `--version` and `--apply`; without `--apply`
it reports the size of the change, measured with `git diff --numstat` so it
matches what `git diff --stat` shows afterwards, and writes nothing. A
usage error exits 64. The current tree previews at 377 entries,
+2797/-2139 lines; the guide itself was deliberately not regenerated.
- **Script headers can no longer break the inventory's Markdown.** A header
that quotes its own ```` ```text ```` block closed the ```` ``` ````-fenced
description early; the fence now grows past the longest backtick run it
wraps (three entries get ```` ```` ````, the rest are unchanged). And a
`## Usage` heading inside a description is no longer mistaken for the end
of the chapter: every `--apply` after the first would have left the tail of
the old chapter behind the new one, duplicating most entries. 15 pytest
cases pin both, each against a throwaway git repository.
- **`csaf/csaf_hashes.py` closes every file it touches.** Each sidecar write
and read used a bare `open(...).write()` / `open(...).read()`, leaving six
to seven handles per advisory to the garbage collector. Under pytest's
`filterwarnings = error` each leak surfaced as a `ResourceWarning` and five
tests in `tests/test_csaf_hashes.py` failed. `pathlib` reads and writes now
close deterministically; the five sidecars stay byte-identical (checked
against the previous implementation, including the NBSP-separated
`.sha3-512`), and a new test pins the no-leak contract directly rather than
through `pytest.ini`. Separately, the CSAF coverage gate is still red at
7.3 %: `--cov=csaf` measures 56 one-off `generate_*` advisory scripts that
have no tests. The tested modules together reach 84.8 %.
- **`/about` counts refreshed.** The page claimed 159 cargo-fuzz targets and
624 dependencies; disk has 175 (vl-feeders grew from 58 to 74 with recent
feeders) and `Cargo.lock` 625, so two `test_about_page` tests failed on the
committed tree. The cargo-audit clause now names only RUSTSEC-2026-0253
(`lru` via tantivy, not affected per VEX ndaal-sa-2026-650): RUSTSEC-2026-0246
concerns `sevenz-rust`, which is no longer in the lockfile. Checked with
`cargo audit --deny warnings --deny unsound --deny unmaintained --deny
yanked`, exit 0 over 625 crates. The honggfuzz / test-fuzz figures on the same
line (152 / 124) match no counting rule found in the tree and are unchanged.
- **Search: an excluded word stays excluded.** The full-text search adds
prefix matching so `log4j` finds `Log4j2`, but it took its prefix words
from the raw query with the `-` operator stripped, so `-alpha` returned
every document the parser excluded, and `alpha -unique3` brought the
excluded document back. Prefix terms now come only from positive words, and
the prefix clause carries the query's own exclusions at any depth. `AND` and
`NOT` are no longer treated as search words, which restores prefix matches
beside an explicit `NOT`. A property test checks that no search result
violates an exclusion.
- **Search: a negated phrase no longer panics.** `- " alpha alpha`,
`-"a b"`, `NOT "a b"` and a negated hyphenated id such as `-cve-2024-0003`
tripped a debug assertion inside tantivy 0.26.2 (`phrase_scorer.rs:534`);
release builds compile that assertion out. Every exclusion is now wrapped so
the scorer only seeks forward. 0.26.2 is the latest tantivy, so there is
nothing to upgrade to.
- **Two fuzz targets for the search entry points.** `fuzz_fulltext_search`
and `fuzz_storage_prefix_search` call the production search code, with
seeds and a replay test that runs on plain `cargo test`. With the
exclusion guard switched off, the replay test fails exactly at the tantivy
assertion above. Found and not yet fixed: the id prefix search returns a key
twice when it matches both the prefix and `source:prefix`.
- **NVD feeder: an out-of-range look-back is an error, not a crash.**
`NvdFeeder::fetch` computed its window with `Duration::days`, which panics
for huge values; a negative value gave an inverted window. The window is now
computed with checked arithmetic and rejected with an error instead.
- **Three sources no longer show 0 on `/stats`.** `cwe_enrichment` read rows
as raw JSON although every row is a `vl-codec` frame, so it emptied its
table and imported nothing; it now decodes each row, reads the CVE JSON 5.x
`problemTypes` it never read before, and keeps the old table when nothing
was readable. `debian_rustsec` failed on a `"crate_name": null` entry and
now accepts it. `nvd_cpe_dictionary` and `publish_stats` are counted from
their own tables.
- **The fuzz gate no longer reports a target that never ran as PASS.**
`scripts/run_all_tests.sh` counted a fuzz target that failed to build, or a
missing toolchain, as passed, and a fuzz crate that lists no target ran
nothing and reported nothing. Both are now a FAIL, and the summary counts
crashes, targets that did not run, and crates without targets separately.
- **`fetch_dumps.sh` pins the TLS signature algorithms.** Its curl recipe now
passes the RFC 9846 signature list (no MD5, SHA-224, DSA or SHA-1) wherever
the local curl supports `--sigalgs`.
## v1.2.74 — Unreleased
- **The dead-dashboard fleet: 59 of 160 dashboards were importing nothing.**
Reported as five broken CSAF vendors; a survey of every dashboard found the
scope was twelve times larger, with three distinct causes and not one of them
a broken feeder.
- **OSV bulk import batched — 5 910 advisories went from 1 242 s to 88.6 s.**
`import_osv_ecosystem` stored one advisory per redb write transaction, and
redb fsyncs on every commit: one fsync per advisory. Measured against the
live AlmaLinux ecosystem, the download was 11 s and the remaining ~20 minutes
was fsync. Payload sizes explain exactly which dashboards survived — every
healthy OSV ecosystem was ≤ 4 MiB (Alpine 3.8, BellSoft 0.5), every dead one
larger (AlmaLinux 6, Android 9, Debian 68, Wolfi 260, Ubuntu 704). Wolfi and
Ubuntu could not finish inside their 24 h cycle at all. Now uses
`store_vulnerabilities_batch`, which existed for exactly this reason (its doc
cites fkie_nvd's "363 639 fsyncs") and was never wired into the OSV path.
- **Seeding no longer claims a feeder has run.** `seed_if_empty` wrote a sample
advisory for each of 68 sources *and* `set_last_update(source, now)` for every
one. But `last_updates` means "this source has run at least once" — the
contract `/stats` documents and `/dashboards/<slug>` renders. A source that
had never been fetched therefore showed a fresh timestamp beside its single
seeded row, indistinguishable from a healthy feed. One 2026-09-03 boot stamped
all 68 with the same `Utc::now()`, which is why 34 dashboards still carried the
byte-identical `2026-09-03T08:48:08.203973522` marker three weeks later. They
now read "never", which is true and obvious.
- **Working feeders no longer render as dead ones.** `/dashboards/<slug>` counted
`SOURCE_INDEX`, but the enrichment sets, the dashboard aggregates and
`debian_security_tracker` own their own tables and never touch that index.
`/dashboards/debian-security` showed "0 advisories" while its feeder had
completed successfully hours earlier and written its whole payload. The 23
mapping arms `get_info` already used for `/stats` are now shared with the new
`VulnStorage::count_source_rows`, so the two pages cannot drift apart again.
- **Every transport error now names its cause.** `reqwest::Error`'s `Display` is
`error sending request for url (…)` and names nothing; the real cause lives in
`Error::source()`, which `format!("…: {e}")` discards. **103 sites** did that,
which is why a fleet of feeders could log an error every cycle for weeks and
stay undiagnosable. `error_chain` is now a documented `vl_feeders::http_diag`
module applied at all 103, with `tests/test_error_chain_coverage.rs` guarding
the count at zero — proven to fail on reintroduction.
- **Oracle CSAF feeder (new).** Oracle publishes CSAF 2.0 but cannot be
discovered: no `provider-metadata.json` exists anywhere on `oracle.com`, and all
three CSAF 2.0 discovery mechanisms come back empty. It ships one
self-contained document per release at
`www.oracle.com/docs/tech/security-alerts/<release>csaf.json` — 22 releases
since April 2022, and `cpujul2026csaf.json` alone carries **1460
vulnerabilities**. A dedicated feeder enumerates the quarterly `cpu` and
monthly `cspu` releases; a live run imported 20 releases in 88 s.
- **BSI and Hitachi CSAF URLs corrected**, both verified against the BSI CSAF
aggregator: BSI's feed lives on `wid.cert-bund.de`, not `www.bsi.bund.de`
(which has always 404'd), and the Hitachi advisories that exist in CSAF are
Hitachi *Energy*'s, published through BSI. For the other 25 vendors, all three
discovery mechanisms were probed and the findings recorded as dashboard
`maintenance_note`s, so an empty dashboard explains itself. The seven 403s are
not bot-blocking — they stay 403 under a browser User-Agent and Moxa flips to
404, i.e. a CDN answering for a path that does not exist.
- **Feeder test coverage, measured rather than assumed.** Of 199 feeder modules
119 are thin wrappers whose logic is tested centrally; of the 80 with their own
logic, criterion coverage was 1/80. Adds `benches/bulk_import_paths.rs` for the
four paths with the volume to repeat the OSV outage, 25 new properties, 19 new
`fuzz_drive` seams registered across **all four** engines the fuzzing policy
mandates (libFuzzer 74, AFL 73, honggfuzz 73, plus 43 test-fuzz wrappers), and
a bolero unified harness driving ten invariants across every engine and Kani.
Writing those tests found two real defects: a CVE scanner that matched the
CVE-shaped tail of longer tokens (`NOTACVE-2021-4422`), and a date format
listed as supported that could never parse, silently stamping advisories with
their import time instead of their release date.
- **The workspace clippy gate is green again.** `multiple_crate_versions` had
been red on a stale duplicate-crate allowlist (30 entries against 45 actual
duplicates); refreshed, and the release scripts gained three fixes — workspace
version extraction, `glab` auth without `GITLAB_TOKEN`, and the reftable
`.invalid` HEAD that breaks `cargo publish` from a worktree.
- **`sevenz-rust` replaced by the maintained fork `sevenz-rust2` 0.23.0.**
The old crate carries RUSTSEC-2026-0245, an unfixed CVSS 8.3 path traversal
(CWE-23/CWE-36) in `decompress_impl`, and RUSTSEC-2026-0246 marking it
unmaintained after its repository was deleted upstream. nvulnlookup was not
exploitable: `vl_feeders::safe_extract::safe_decompress_7z` has gated every
entry name since 0.1.49, and all five archive feeders (gsd, capec, moksha,
cwec, sightings) route through it — verified, not assumed. The migration
removes the crate anyway rather than carry an abandoned dependency.
`safe_extract` gains `reject_unsafe_entry_names`, which reads the archive
listing via `Archive::open` (header metadata only, no payload decoded) and
applies `safe_relative` to every name **before** extraction opens the
archive for writing. This keeps the rejection contract ours instead of
depending on the fork's internal `safe_join` or its error-message text, and
makes the module's promise that an unsafe entry costs zero bytes on disk
true by construction. Published as **ndaal-sa-2026-649** (CVSS v3.1 7.4 /
v4.0 8.3).
- **`lru` unsoundness recorded as a documented cargo-audit ignore.**
RUSTSEC-2026-0253 (use-after-free via a panicking `Drop` in
`LruCache::pop()`) reaches the tree transitively through Tantivy. It is not
fixable here — Tantivy 0.26.2, the newest release, pins the `lru` 0.16
series while the fix landed in 0.18.2 — and not reachable, because the cache
keys and values are Tantivy-internal types no nvulnlookup input can
influence. The ignore entry in `.cargo/audit.toml` carries that reasoning
and a re-check trigger (`cargo tree -p lru --depth 0` after every Tantivy
bump). Published as VEX **ndaal-sa-2026-650** (CVSS v3.1 5.6 / v4.0 6.3).
- **`cargo audit` is green again**, exiting 0 across 625 crate dependencies
with `--deny warnings --deny unsound --deny unmaintained --deny yanked`.
The three findings it reported were measured against the pre-update
lockfile first to establish they predated the relock: three before, three
after.
- **Routine dependency relock.** `syn` 3.0.5 to 3.0.6, plus a `cargo update`
sweep moving async-compression, cc, cfg-if, compression-codecs,
find-msvc-tools, hyper-rustls, rand, rustix and unicode-ident by one patch
or minor each — no package added, none removed. The generated
`workspace-hack` was regenerated so `crc32fast` and `base64` are
feature-unified. No security defect fixed or introduced; published as
**ndaal-sa-2026-651**, which states explicitly why it carries no CVSS score
(a CSAF informational advisory has no `/vulnerabilities` section, and there
is no vulnerability to score).
- **Extension-less release binaries are now tracked by Git LFS.** The Ansible
role `files/` trees carry four binaries per role with no file extension
(`vl-cli-linux-amd64`, `vl-web-linux-arm64`, …). `*.deb` and `*.rpm` already
routed through LFS but cannot match a name with no extension, so
`git check-attr filter` reported `unspecified` and all eight were committed
as raw blobs — **185 MB written straight into git history on every release**,
and the largest single contributor to the storage quota this repository is
over. The new patterns stop at the architecture so they match the binary and
not its `.sha-256` sidecar. Existing files converted with
`git add --renormalize`: 133-byte pointers instead of 16–31 MB blobs.
- **`update_ansible_role.sh` gained option parsing.** It previously read no
arguments at all, so `--help` did not print help — it performed a real,
destructive run that cleared and repopulated both role trees. `--help` and
`--version` now exit 0 without touching anything; an unknown option or a
stray operand exits 64.
- **Both Ansible role trees refreshed to the current release**, clearing the
0.1.55, 1.2.57, 1.2.59 and 1.2.63 artefacts that had accumulated.
- **The typos gate can fail now.** `test_typos.sh` captured typos' exit code
(0 clean, 2 findings, anything else an error), logged it, and ended in
`exit 0`, so `gate_typos` recorded PASS whatever typos found. With `--strict`
or `TYPOS_STRICT=1` any non-zero typos exit is now `exit 1`, and
`quality_gates.sh` passes `--strict`. The gate goes red on the current tree:
typos reports **259 findings**, mostly vocabulary `.typos.toml` does not yet
allow (`nd`, `Pn`, `hpe`, British spellings). `--strict` is matched per
argument: under the boilerplate's `IFS=$'\n\t'` a `" ${*} "` match joins the
arguments with a newline and misses `--strict` next to any other argument.
- **`test_typos.sh` and `test_lineguard.sh` pass `shellcheck -o all` without
the repo's `.shellcheckrc`.** That file disables SC2310 and SC2312
project-wide, which hid both findings here; a repository vendoring the gates
without it (ndaal_public_simdutf8_cli) failed their sister bats. Fixed with a
reasoned directive at each lineguard site and a separate `typos --version`
capture.
- **`test_lineguard.sh` takes `REPO_ROOT` from git.** It used the parent of the
workspace directory, which in a flat repository (`<repo>/tests/scripts`) is
the directory above the repository, so the report landed outside it. The
parent derivation stays as the fallback outside a git checkout. The sister
bats' self-skip tests now run on a hermetic `PATH`, so a bats run no longer
scans the tree and writes a report into `documentation/`.
- **The typos gate passes `--strict` on the current tree.** Once `gate_typos`
could fail, typos reported 259 findings over `crates`, `tests`, `scripts`
and `README.md`, and every distinct word was classified. Vocabulary went
into `vulnerability-lookup-rs/typos.toml`, the config the runner's working
directory picks up: whole-token identifiers (`nd`, `Pn`, `fpr`, `mape`,
`MOVEit`, and CNVD's own misspelled field names `vulnerabilitys` and
`serverity`), the vendors HPE, JUMO and Wiesemann & Theis, 19 British
spellings the codebase uses throughout, and German and French words from
the translated READMEs. OpenPGP key files (`*.asc`) are excluded as key
material. Real typos were fixed in source: `synchronous`,
`overridable`, `cloneable`, `shrunk`, `writable`, and two local temp names.
- **Deliberate misspellings get a per-line marker, not an allow-list entry.**
The nuclei severity ladder the importer normalises, a verbatim operator
quote, and example typos cited in comments are data. A new
`extend-ignore-re` skips any line that ends in `spellchecker:disable-line`
(after `#`, `//` or an HTML comment), so the same words still fail
everywhere else. Probes confirm the allow-list stays narrow: an unmarked
corpus typo, or `nd` inside a longer identifier, is still reported.
## v1.2.73 — Unreleased
- **CSAF vendor feeds audited.** Probed all 101 `csaf_*` feeder source URLs:
66 return a usable provider metadata document, 2 serve a valid `index.txt`
feed and 1 clones a git repository, leaving **32 that fetch nothing**. The
generic feeder treats a non-200 or non-JSON metadata response as "nothing to
do", so an empty vendor dashboard (the reported symptom on `/dashboards/qnap`)
looked like a successful run. Root causes are 18 × 404, 8 × 403, 2 × HTTP 200
serving HTML, 1 × 401, 1 × 400, 1 connection failure and 1 URL that spans
source lines. The 403s are not bot protection — they stay 403 under a browser
User-Agent.
- **Authoritative corrections identified** from the BSI CSAF aggregator and
verified to return HTTP 200 with live ROLIE feeds: `siemens`
(`/productcert/csaf/`, not `/.well-known/csaf/`), `cisa`
(`/sites/default/files/csaf/`), `nozomi`
(`csaf.data.security.nozominetworks.com`) and `bsi` (`wid.cert-bund.de`).
- **Release version argument is now validated.** `create_release.sh`
interpolated it straight into a filesystem path, so `../../tmp/pwn` escaped
the release tree and a single-dash typo became the version.
## v1.2.72 — Unreleased
A dependency-currency release. No behaviour change, no security defect fixed
or introduced.
- **`cargo update` relocked 142 packages** to their latest semver-compatible
releases. No manifest requirement changed — only the locked versions. Of
those, **25 are direct workspace dependencies**:
`anyhow` 1.0.103 → 1.0.104
`askama` 0.16.0 → 0.16.1
`async-trait` 0.1.89 → 0.1.92
`atom_syndication` 0.12.9 → 0.12.10
`clap` 4.6.4 → 4.6.7
`flate2` 1.1.9 → 1.1.10
`futures` 0.3.33 → 0.3.34
`http` 1.4.2 → 1.5.0
`http-body-util` 0.1.4 → 0.1.5
`hyper` 1.10.1 → 1.11.1
`mio` 1.2.2 → 1.2.3
`object` 0.37.3 → 0.39.1
`quinn` 0.11.11 → 0.11.12
`rcgen` 0.14.8 → 0.14.10
`redb` 4.1.0 → 4.3.0
`reqwest` 0.13.4 → 0.13.5
`rss` 2.1.0 → 2.1.2
`rustls` 0.23.42 → 0.23.45
`serde_json` 1.0.150 → 1.0.151
`tantivy` 0.26.1 → 0.26.2
`thiserror` 2.0.18 → 2.0.20
`time` 0.3.54 → 0.3.55
`tokio-rustls` 0.26.4 → 0.26.5
`toml` 1.1.3+spec-1.1.0 → 1.1.6+spec-1.1.0
`uuid` 1.24.0 → 1.26.1
`rustls` 0.23.42 → 0.23.45 is the one worth naming: it is the TLS 1.3 backend
the whole service runs on, and it was three patches behind. The AWS-LC
cryptographic backend underneath it (`aws-lc-rs`, `aws-lc-sys`) moved in the
same relock.
- **26 CSAF advisories** record the refresh: one per direct dependency
(ndaal-sa-2026-603 … 627) plus one bulk entry for the 117 transitive moves
(ndaal-sa-2026-628), following the precedent of ndaal-sa-2026-517. Each
carries both CVSS v3.1 and CVSS v4.0 metrics, scored 0.0 (NONE) with every
impact metric NONE — informational, machine-readable, and explicitly not a
vulnerability claim.
- **Script compliance**: `scripts/quality_gates.sh` and
`tests/scripts/test_port_scan.sh` were the last two files in the audited
trees failing `shfmt -d -i 0 -bn -ci -sr`; both are now clean. Across all 310
scripts in `tests/scripts/`, `scripts/` and `release/`, the three blocking
gates now report 0 syntax failures, 0 shfmt failures and **0 SC2250 hits** —
i.e. no bare `$var` remains anywhere in those trees.
- **Version bump 1.2.71 -> 1.2.72**: 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.72 development cycle.
## v1.2.71 — Unreleased
Every CWE weakness detail link on `/attacks/cwe` returned 404.
- **The CWE catalogue could only ever load from a bundled archive.** The
feeder read `dumps_archive/cwec.7z` and failed if it was absent — and it is
absent on every deployed host, because neither the release artefacts nor the
Ansible role carry `dumps_archive/`. Where the file did exist it was an
unresolved Git-LFS pointer stub. CAPEC already had a live-download fallback
and consequently had a populated catalogue in production (615 rows) while
CWE had none, which is why the two sibling dashboards behaved differently.
The CWE feeder now mirrors CAPEC: bundled archive when present, otherwise
download `cwec_latest.xml.zip` from MITRE, unzip with a 64 MiB ceiling
against zip bombs, and parse with quick-xml. A parse that yields **zero**
weaknesses is now an error rather than `Ok(0)` — returning success on an
empty catalogue is precisely how an empty table kept reporting healthy
import cycles while every detail link 404'd.
- **The detail route doubled its path segment.** The page lives at
`/attacks/cwe` and the catalogue namespace is also `cwe`, so the link read
`/attacks/cwe/cwe/79`. `/attacks/cwe/{id}` is now canonical; the doubled form
and the older `/dashboards/cwe/cwe/{id}` both 308 to it, so published links
and bookmarks keep working instead of 404ing.
- **The routing table moved into the library** (`vl_web::routes_table`). This
is the change that makes the defect catchable: `build_router` lived in
`main.rs`, so no test could construct the real router, and **eight** handler
tests for `cwe_detail` passed throughout — they call the handler directly and
can say nothing about whether any URL reaches it.
Tests added, all of which fail without the fix:
- 8 router-level tests driving the real routing table over `handle_parts`,
including the exact `/attacks/cwe/79` shape that 404'd. Verified by removing
the route and confirming 3 of them go red. The load-bearing one asserts a
malformed id returns **400, not 404** — a 404 is ambiguous between "no such
weakness" and "no such route", so only the 400 proves the path is wired.
- 5 Bruno requests covering the dashboard, both id forms, the legacy redirect,
and the malformed-id 400; the dashboard test asserts the doubled link shape
never comes back.
- Playwright: the existing CWE specs were pinned to the doubled path and are
moved to the canonical one, plus a new legacy-alias block asserting the 308
and that the dashboard no longer emits the doubled href.
- **Version bump 1.2.70 -> 1.2.71**: 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.71 development cycle.
## v1.2.70 — Unreleased
A correctness release, and a correction to the previous one. v1.2.69 shipped a
severity counter that was never wrong about the rows it saw — it simply never
saw most of them.
- **Critical Severity reported 112 against a 2,654,274-row corpus.** The count
was not miscomputed; the completeness gate was wrong.
`severity_counts_indexed()` returned the maintained table whenever it was
non-empty, and the sweep only backfills when that read returns `None`. But
the feeders begin writing the moment the process starts and every write
updates the histogram incrementally, so the table was non-empty within
seconds of boot, the sweep took the "already computed" branch, and the full
recount never ran for the lifetime of the database. 112 was the number of
criticals written *since the upgrade*, presented as a corpus-wide figure.
"Populated" and "complete" are different questions, and emptiness could
never answer the second one. Adds `SEVERITY_META` carrying a backfill
generation, written in the **same transaction** as the counts — a crash
between the two would otherwise leave a marker claiming a histogram that
was never written. Bumping `BACKFILL_GENERATION` forces one recount, which
is the only way already-indexed rows pick up a changed label mapping.
A backfilled *empty* corpus now returns `Some(empty)` rather than `None`:
that is a measured zero, and `None` would send the sweep into a full rescan
every cycle forever. **On first deploy this triggers one O(2.65M) recount;
the placeholder persists until it finishes.**
- **Eight-test exhaustive sequence** pins every transition — populated-vs-
complete, replace-not-accumulate, idempotent re-run, measured-empty,
incremental-after-backfill, the batch path the feeders actually use, label
change releasing the old bucket, and survival across reopen. Each
cross-checks the maintained index against a fresh full recount, so drift
fails the suite instead of accumulating. Four pre-existing tests failed on
this change **correctly**: they asserted the indexed read was authoritative
after incremental writes alone, which is precisely the contract that caused
the defect. They now establish a measured baseline first.
- **Nozomi CSAF feeder imported nothing, silently.** Nozomi retired its
Trusted Provider endpoint; `.well-known/csaf/provider-metadata.json` returns
HTTP 404, and a 404 there yields an empty advisory set rather than an error,
so the feeder reported healthy cycles indefinitely. Rewritten on the
`csaf_openeuler` `index.txt` pattern against
`https://security.nozominetworks.com/csaf/`. The filename is **not** the id:
Nozomi publishes `nn-2026_20-01.json` but tracks `NN-2026:20-01`, an
underscore where the id carries a colon — without translating it back the
skip-check can never match a stored row, and every cycle re-downloads and
re-stores the whole corpus while looking healthy. Verified against the live
feed for 2019, 2024 and 2026. The separate `csaf_nozominetworks` feeder uses
a different host that still answers 200 and is untouched.
- **`skills/bash` + `skills/bats` printf convention.** The SKILL.md rule said
format strings take single quotes while the canonical `template.sh` it ships
used 31 double and zero single, and CLAUDE.md prescribes double — the doc
contradicted both the asset and the standard. 804 sites converted; 100 keep
single quotes because conversion would change behaviour (they contain `$`,
a backtick, a literal `"`, or a doubled backslash: `'\n'` prints `\n`,
`"\n"` prints a newline).
- **Version bump 1.2.69 -> 1.2.70**: 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.70 development cycle.
## 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
"ver…") 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.63 — Unreleased
- **New data-integrity gate: sqlite_integrity (174, `--fast`).**
`PRAGMA integrity_check` + `foreign_key_check` + `quick_check` over every
in-scope `*.db` / `*.sqlite` / `*.sqlite3` / `*.db3`, with the contract
PRAGMAs (`journal_mode` WAL, `foreign_keys`) recorded as note-level findings;
jaq-derived, schema-validated SARIF 2.1.0 →
`documentation/sql/sqlite_integrity/`, and a `NOT_APPLICABLE` autofix record
(an integrity check diagnoses, it never repairs — no `VACUUM`, no
`.recover`). It checks a **snapshot copy** (database plus its `-wal`/`-shm`
siblings): a `mode=ro` open cannot open a WAL database, which would have
produced phantom "unable to open database file (14)" errors on exactly the
databases this project ships, and copying is also a stronger non-mutation
guarantee — sqlite3 never opens the original at all. Non-SQLite files with a
matching suffix are skipped by their magic header. Verified live: verdict
`ok` on a WAL database with the original byte-identical afterwards;
idempotent; bats 60/60.
- **Deck: five fuzzing slides promoted PROPOSED → PRODUCTION and rewritten
from `Test_Sequences.md`.** Slides 40 (cargo-afl / AFL++), 42 (bolero), 44
(structure-aware `arbitrary`), 46 (corpus discipline + resource
limits) and 48
(stateful API fuzzing — RESTler, cats) all still described their capability
as future work; each now carries a Gate line and a context bullet stating
what actually shipped: 152 AFL harnesses across
`crates/{vl-core,vl-feeders,vl-web}/afl/` reusing the shared `fuzz_drive`
bodies (gate 150); the `bolero_fuzz.rs` pilot that replays the committed
corpus under `cargo test` and model-checks via `--engine kani`;
`#[derive(Arbitrary)]` typed inputs on the SSVC and CVSS surfaces; the
radamsa gate growing the committed corpora (with coverage measurement and
the `-timeout` / `-rss_limit_mb` oracles named as the remaining increment);
and `test_restler.sh` + `test_cats.sh` driving the expanded `/api/v1`
`/openapi.json`. Body and table-of-contents statuses both updated
(PRODUCTION 75 → 80, PROPOSED 30 → 25).
- **Test-practices deck v1.3.0: 11 new slides, two PROPOSED → PRODUCTION
promotions, and a cross-language tooling matrix.**
`nvulnlookup_test_practices.odt/.pdf` gains concept + flow slides (with
rendered PlantUML diagrams, Skills-involved and Gate lines) for the four
gates built this cycle — blint (208/209), cargo-deadlinks (210/211), vnu
(212/213), stylelint (214/215) — plus three reference slides (216-218): the
12-stage × 6-language quality-gate tooling matrix, the ndaal-specific stages
the matrix has no column for (formal verification, DAST, binary, doc links,
CSS, DB integrity, snapshot, load), and the stage/short-name legend. Cells
the source matrix left blank are completed (Bash + HTML Fuzz = radamsa, HTML
SAST = sighthound, HTML Unit = playwright, HTML Bench = lighthouse, HTML Deps
= osv-scanner, JS/TS Prop = fast-check). Promotions: "Deductive verification
(creusot, prusti, verus, flux)" (slide 54) and "HTML/CSS validation &
accessibility" (slide 146) move PROPOSED → PRODUCTION; the latter also picks
up `skills/html-vnu` + `skills/stylelint`, a Gates line, and a corrected
context bullet (its "no HTML5 validator, no CSS linter" gap is now closed by
gates 172/173). All 427 slide-metadata lines and the 105 status-tagged
table-of-contents rows are renumbered and reconciled against the body
(103/103 match, 0 mismatches); the backup-range note reads "Slides 220 to 427
of 427". Everything uses the deck's own Roboto / Roboto Mono named styles.
Deck version 1.2.0 → 1.3.0; PDF re-rendered.
- **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`.
- **Search results were hard-capped at 50 and the remainder was unreachable
— now paginated with a prev/next navbar on both the HTML `/search` page and
the JSON API.** `/search?q=2026` asked each back-end for a single 50-row
page and reported `total = results.len()`, so the count itself could never
exceed 50 and every match past the first page was unreachable. Fixes:
(1) new `VulnStorage::embedded_search_paged(query, offset, limit)` returns
`(hits, has_more)` — `offset` enables page 2, 3, … navigation and `has_more`
is a count-free "is there a next page?" signal (it looks exactly one match
past the filled page and never totals the multi-million-row store);
`embedded_search` stays a zero-offset wrapper so existing callers are
untouched. (2) `search_exec` grows `embedded_hits_paged` +
`meili_hits_paged`, both bounded by the search budget. (3) `GET /search`
gains a `page` parameter, a `Pager` view-model, and a prev/next navbar in
`partials/search_results.html` — real `href`s work with JavaScript off,
while `hx-get` / `hx-push-url` swap the results in place and keep the URL
shareable; the count line now reads "Showing results N–M (more available)"
instead of a bogus total. (4) `GET /api/v1/vulnerabilities/search` gains a
`page` parameter and a `{query, page, limit, count, has_more, results}`
envelope (was the same 50-cap `total`). TDD: 9 exhaustive
`test_embedded_search_paged` invariants + route / API / helper unit tests;
new Bruno `.bru` files (api-v1 108–111, pages 14) and a
`tests/pages/search-pagination.spec.ts` Playwright suite.
- **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 "ver…") 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`, <!-- spellchecker:disable-line -->
`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 overridable 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)