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. |
||
|---|---|---|
| .. | ||
| patterns.json | ||
| README.md | ||
| reference-run.json | ||
| suite-counts.json | ||
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 357–404) 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.