Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

mikronova/modgunn

.nornir/verdict-reasoning.md

verdict (Engine A) — reasoning

Class: reasoning. Why Engine A is shaped this way, and the forks not taken. The descriptive shape is in verdict-design.md, which mentions none of this.

Why a max()-fold over an ordered Decision

Both Decision (Pass < Warn < Block) and Severity (Info…Critical) derive Ord in declaration order so folding many findings into one verdict is a single max() with Pass as the identity for a clean artifact. The alternative — a hand-written precedence table or an "if any Block then Block, else if any Warn…" ladder — is what Ord already expresses for free, and it would drift the moment a severity is added. Encoding precedence in the enum order makes "strictest wins" a type-level fact, and it is why Engine B can reuse the exact same fold.

Why exploit-intel gating lives in vuln_finding, not in the feed

KEV/EPSS/VEX change a finding's severity and presence, not the underlying match. Applying them at scan time (against the live Policy) — rather than baking an escalated severity into the stored Advisory — means the same imported advisory generation can be scored under different policies (one consumer blocks on KEV, another only warns) without re-importing. Advisory.kev/epss are raw facts from the feed; the decision those facts drive is policy, so it belongs at the gate.

Why VEX suppression is a policy list, not a deletion

A VEX not_affected assertion ("this CVE's vulnerable path is unreachable here") is a consumer claim about this artifact, not a fact about the advisory. Dropping the advisory from the db would hide it from every other artifact; keeping it and suppressing per-scan via policy.vex_not_affected (mirrored by the warehouse-scan --vex flag) preserves the advisory for everyone else and keeps the suppression auditable. Engine B's reachability analysis (A8) is the automated version of the same idea — see worlddelta-reasoning.md.

Why the verdict is content-addressed by (blob_sha256, policy_version, advisory_db_rev)

A verdict is expensive relative to a cache hit, and it is a pure function of exactly those three inputs: the bytes scanned, the policy applied, and the advisory generation matched against. Keying the cache on all three means a consumer (holger) invalidates precisely when one moves — a new policy version, a new imported db_rev, or a different artifact — and never serves a stale verdict. policy_version is a plain string the operator bumps; advisory_db_rev is the import generation the warehouse already tracks.

Rejected: expand advisory ranges into exact versions at import

The first cve_advisories schema stored exact affected_versions and matched by equality, so Advisory::matches was set-membership. Importing a range-only OSV/NVD entry then meant expanding it against the full published-version universe per package. Rejected because it explodes row counts, is lossy for versions published after import, and needs an offline registry catalog we do not have. The scanner now matches range-native via versions::affected_by at scan time; the exact-list path survives only as a fallback for tiny/explicit advisories. Full argument + migration: cve-warehouse-ingest.md §0.

Why Engine A carries no I/O

OsvScanner::scan takes an AdvisoryDb and an Artifact and returns a Verdict — no file, no network, no warehouse. That is what lets holger's gate and static-holger link it with zero heavy dependencies (the whole point of the leaf; see reasoning.md). Getting advisories in is the feeds/warehouse functions' job, kept on the other side of a feature wall.