llm-security/tests/golden
Kjell Tore Guttormsen 359066a3f7 refactor(llm-security): v8 Phase 5 step 4 - swap OWASP_MAP to commons
Second consumer swap of step 4. OWASP_MAP stops being a hardcoded constant in
severity.mjs and is built from the vendored commons artifact
mapping/owasp-map.json by a new scanners/lib/owasp-map.mjs, re-exported from
severity.mjs so the published surface (which the golden gate walks as
severity:OWASP_MAP) is unchanged.

Scope is one of the four maps commons publishes, and the omission is measured,
not incidental. OWASP_MAP has a production consumer: owaspCategorize() reads it
as the per-scanner fallback, and that reaches real report output through
output.mjs's owasp_breakdown. OWASP_AGENTIC_MAP, OWASP_SKILLS_MAP and
OWASP_MCP_MAP have none - every reference tree-wide is a test or a golden
artifact - so they stay source literals, the same call already made for
cyrillic_confusables in the first swap. Porting them would move data no runtime
reads into the load path.

Measured byte-likeness before the swap, all four taxonomies: same 16 prefixes,
same insertion order, same code arrays. Loadable verbatim, unlike the injection
table.

Content preservation proven the same way as the codepoint swap: the golden dump
differs in exactly one record, the sha256 of severity.mjs, which changes by
construction when a table leaves the file. All 83 regex records and all 7 table
records including severity:OWASP_MAP are byte-identical; reference-run.json
unchanged at 61/61. patterns.json re-blessed for the file digest only.

New property, not just preservation: the golden gate now pins the vendored
commons data transitively for this table too. Mutation-proven in both
directions - changing one code value and deleting a whole prefix each turn
three independent gates red (golden table digest, the new owasp-map gate by
name, and the pre-existing severity behaviour tests).

Entries are validated rather than trusted: commons is vendored data, and a
value that is not an array of strings would be spread straight into
owaspCategorize's category list, so a malformed entry is dropped. Graceful-empty
on an unresolvable commons, matching commons-loader's contract - severity.mjs is
on the import path of output.mjs and every orchestrated scanner, so a load throw
would abort a scan rather than degrade it.

Suite 2164 -> 2173, all green.
2026-08-11 12:53:48 +02:00
..
patterns.json refactor(llm-security): v8 Phase 5 step 4 - swap OWASP_MAP to commons 2026-08-11 12:53:48 +02:00
README.md fix(llm-security): golden gate - coverage block measured something else 2026-08-09 13:06:45 +02:00
reference-run.json fix(llm-security): golden gate - coverage block measured something else 2026-08-09 13:06:45 +02:00
suite-counts.json test(llm-security): v8 Phase 5 step 1 - golden baseline before any table swap 2026-08-09 12:57:51 +02:00

Golden baseline — v8 Phase 5

Reference artifacts recorded before any commons extraction, so that each table-by-table swap in Phase 5 can be proven behaviour-preserving (or rolled back). Generated and checked by one code path:

node scripts/golden-baseline.mjs            # check only, exits 1 on drift
node scripts/golden-baseline.mjs --write    # (re-)bless
node scripts/golden-baseline.mjs --write --suite   # also refresh suite counts (slow)

The gate is tests/lib/golden-baseline.test.mjs.

The artifacts

File What it pins Why that layer exists
patterns.json .source + .flags of every RegExp reachable from the walked modules' exports After a swap a pattern is new RegExp(jsonString, flags). The plan's named hazard — JSON backslash-doubling — is visible only on the compiled object.
↳ table records key/value digest of HOMOGLYPH_MAP, TYPOSQUAT_SUSPICIOUS_TOKENS, SEVERITY, the four OWASP maps Most of what Phase 4 moves is not a regex. A regex-only dump is blind to a broken homoglyph or OWASP-map swap, i.e. blind to the bulk of the payload.
↳ file records sha256 of the five source files in the moving set The completeness layer, complete by construction: it covers regexes inlined in function bodies (NAMED at string-utils.mjs:291, the BIDI/tag/PUA ranges at 357404) that no export walk can reach.
reference-run.json the 61 showcase payloads through the real hook entry points, plus a static reachability probe Byte identity over a corpus that trips a handful of patterns would prove almost nothing, so reachability is recorded and unreachable patterns are listed by name.
suite-counts.json per-file pass/fail, each file run alone A total is unattributable, and npm test runs files concurrently where three timing-sensitive files flake. Running each alone is the only reproducible recording.

Why there is no regex enumerator

The obvious design — parse the sources and enumerate every regex literal — was rejected. It needs a JS parser this zero-dependency repo does not have, and a lexical approximation is contaminated by comments and division (severity.mjs scores 4 "regexes" that way and exports none). The two-layer split — export walk for what the scanners actually use, file digest for everything else — answers the same question without a parser.

During a swap

A diff here means the swap changed observable behaviour. Roll the swap back. --write is for deliberately re-blessing a change you have already decided is correct, not for making the gate quiet.

Known coverage gaps — read before quoting a number

coverage is static reachability, NOT observed coverage. It probes the 61 payload strings against the 83 injection patterns in-process: "if you threw every payload string at every regex, how many would match?" It does not measure what the 61 hook invocations evaluated — the pre-bash-destructive payloads never reach injection-patterns at all, yet their strings are in the probe set and can mark a pattern reachable. 47/83 is therefore an upper bound on what the corpus could protect, not a measurement of what it did.

The block is named kind: "static-reachability" and carries that caveat in a note field; the gate asserts both, so the honest label cannot be dropped quietly.

Two gaps follow:

  • 36 unreachable patterns, listed by key under coverage.unreachablePatterns. A swap that breaks one of those is caught by the pattern dump only. Closing it means growing the conformance corpus (Phase 5 step 5).
  • The four OWASP maps have no behavioural coverage at all. They are scanner-side and no hook in this corpus reaches them. They were the reason the dump was widened to table records, and the digest is their only protection — do not read the reference run as backing them.

corpusContains says a payload contains a homoglyph or is altered by normalizeForScan. It does not say the run folded one.