Commit graph

21 commits

Author SHA1 Message Date
2d86151e3b fix(divergence): our own iframe number read 3x low, and the reported cause was not the cause
No data file changed and no pattern moved. A published figure was wrong.

llm-ingestion-pipeline-security flagged it (coord, 2026-08-11T19:51:55Z) by
measuring the hybrid-xss:iframe-src row themselves instead of citing ours, and
got roughly 4x our number. They attributed the gap to measurement surface -
their composed scan_lexicon() against our standalone regex - and said no
re-measure was needed.

Re-measured anyway, because the claim was about OUR number. Their diagnosis does
not hold: our standalone 100 000-char figure is 7.86 s against their composed
8.95 s, so the two surfaces differ by far less than the error did. Standalone,
Python 3.14.0, same unit the document claims:

  iframe-src [^>]*    822.7 ms @ 32k     51 477.4 ms @ 256k
  published           119.6 ms @ 32k     16 857    ms @ 256k
  script-tag [^>]*     87.4 ms @ 32k      5 222.6 ms @ 256k
  published            21    ms @ 32k      5 440    ms @ 256k

So the Python script-tag figure at 256k reproduces and the one at 32k does not,
and the iframe-src pair reproduces at neither point. Error ratios are not
constant, so a single mis-sized input does not explain it, and the original
harness lived in a previous session's scratchpad and is gone. Recorded as NOT
DIAGNOSABLE rather than given a plausible cause - a guessed cause would read
like a finding.

Superseded figures are struck in place rather than overwritten. Anyone who cited
the old number needs to be able to discover that they did.

The 0.4.0 decision does not depend on this. Every corrected figure is larger, the
shape is unchanged (quadratic, x4 per doubling), and both [^><]* forms stay flat.
The 0.4.0 CHANGELOG section still quotes the old figure and is left alone: that
section is the record of what was released, not a live claim.

Verified: JSON well-formed, specs normative, charter clean, README vendoring
examples and prose moved to v0.4.1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JLEZ4XCSnSrQUFA8SzkQB4
2026-08-11 22:02:15 +02:00
2eee7e19ee feat(lexicon): both unbounded rows narrow to [^><]*, and the mechanism is new here
injection-lexicon.json 0.7.0 -> 0.8.0. hybrid-xss:script-tag and
hybrid-xss:iframe-src drop the `>`-only negated class for `[^><]*`.

Both were quadratic in scan length on input repeating the tag prefix without
ever supplying a `>`: every occurrence is a match start and `[^>]*` runs to end
of input from each one. Node v25.8.2, 16k/32k/64k/128k/256k chars - script-tag
32.65/113.36/479.02/1988.83/7772.25 ms, iframe-src
39.23/131.76/574.94/2469.55/9449.94 ms, x4 per doubling. Under `[^><]*` the same
inputs cost 0.08-0.66 ms and 0.10-1.00 ms. Flat, not merely faster.

This is NOT re-extraction and it is not commons correcting seed data. The
dependency direction inverted: at llm-security be14867 their four injection
tables are built from this file and hold zero local regex literals - measured on
their published surface at 47905da, their vendored lexicon confirmed
byte-identical to 0.7.0. There is no upstream literal left to re-read, so
re-extraction was unavailable as a mechanism. They re-measured rather than
accepting our report, rejected [^>]{0,256} because a bound is paddable and
[^>]{1,256} because it drops bare `<script>` along with two corpus cases, chose
[^><]*, and asked us to carry it.

Recorded in a new source_fidelity.owner_directed_changes block, deliberately not
in post_extraction_drift - that block's shape asserts the source moved and
commons followed, which would be false in the one way that matters now that the
source reads commons.

Not majority. The guard reached [^><] first and independently (cff0437), so all
three agree, but 3-of-3 is not what moved this value. The justification is the
one that kept us on [^>] through 0.7.0 and is quoted in the divergence doc: this
file tracks its declared source, and the declared source chose.

Cost, stated: `<script <x>` and `<script<div>` stop matching. Measured over all
90 conformance cases rather than the 4 citing these ids - 0 lost, 0 gained. The
dropped class is real and unwitnessed by the corpus.

count_against_llm_security_head is now undefined rather than 83: its comparison
target no longer exists. A diff against their vendored copy is a subtree
freshness check, not an independent agreement measurement, and the field says so.

Verified against the SHIPPED file, patterns read from the JSON and never from
literals in the harness: 83/83 compile in Node bare and `u`; whole-table ReDoS
budget at the guard's 512 KB cap, per-pattern unit derived from each pattern's
own literal prefix, slowest of 83 is 1.90 ms against a 250 ms budget; all 4
corpus cases citing the changed ids still match. Gate proven discriminating, not
assumed: the pre-0.8.0 forms measure 37 374 ms and 44 514 ms on that same check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JLEZ4XCSnSrQUFA8SzkQB4
2026-08-11 21:52:43 +02:00
4187715ab7 docs(divergence): our own form has a number now, and it is quadratic
The guard answered the span-byte question with evidence rather than preference:
their [^><] is a measured ReDoS fix (cff0437, 2026-07-31), and they asked us to
measure our form rather than take their word for it. We did.

Node v25.8.2 and Python 3.14.0, patterns read out of the lexicon, run over
'<script ' repeated — the unit that denies the pattern the > it needs:
25.5ms at 16k chars, 6681.6ms at 256k. x4.0 per doubling against the guard
form's x2. Quadratic against linear, reproduced in both engines. It is
scan-position quadratic, not catastrophic backtracking: <script matches at ~n
positions and [^>]* scans to end of input at each one.

Counted across the whole file instead of stopping at the row we were told
about: 8 of 83 patterns carry a bare [^>], none carries [^><]. Six are the
bounded html-obfuscation family — measured linear, so bounding does work, at
the recall cost this file already records. Two are unbounded, and the second
one, hybrid-xss:iframe-src, had not been named by any party: 16.9s at 256k
chars. That row is ours, found by counting rather than by being told.

No data file touched. Both patterns stay byte-identical to llm-security's
source, because behaviour preservation is not suspended when a measurement is
unwelcome — commons carries [^>] on provenance and would carry [^><] had the
source been the guard's. The finding is routed to the owning runtime instead,
which is exactly the path SECURITY.md promised an outsider three commits ago.
First time that route is walked from the inside.

Also recorded: the guard's own disclosure that their suite passes under both
forms (717 green), because their DoS row repeats '<script>' — which supplies
the > and never exercises the run.

Verified: JSON well-formed, every spec still normative, charter clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T85QqeiWBEoBMjWaMiBnxD
2026-08-11 14:13:29 +02:00
d96fbbf791 docs(divergence): the span row had one witness; llm-security measured five more
Their coord message of 2026-08-11T11:01:59Z measured the three <script> forms as
totally ordered by what they match — [^>] strictly contains [^><] strictly contains
the pre-0.7.0 closing-tag form — and named two input classes the guard's narrower
class drops that commons had not recorded: `<script a="<" >x` and `<script<div>`.

Reproduced independently before writing it down, including the strict-superset
property in both directions. Their table came with a claim attached; the claim is
now a measurement here rather than a relayed assertion.

Their argument for [^>] — the narrower class buys an empty false-positive surface,
since anything reaching [^>]-and-not-[^><] already contains a literal <script tag —
is recorded as THEIRS and marked explicitly as not what decided commons' form.
Commons carries [^>] on provenance and would have carried [^><] had the source been
the guard's. Keeping that distinction visible matters more than the regex: if merit
ever becomes the reason a commons pattern moves, behaviour-preservation is gone.

Also records honestly that they asked to hear the guard's reason for [^><] before
commons shipped, and commons shipped first — with why that order is defensible
rather than an oversight. A reason that beats their argument is a reason for the
guard's own port; commons would still track its declared source.

Informative file only. v0.3.0 unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HuudQLGMyMenuyeZk8fKs5
2026-08-11 13:44:28 +02:00
c1b2385de6 fix(lexicon): converge script-tag on its source — re-extraction, not revision
hybrid-xss:script-tag drops the closing-tag requirement, becoming byte-identical
to llm-security's RegExp.source at b1ba1fb (injection-patterns.mjs:170). Verified
by compiling both, not by reading them.

Why this does not breach behaviour-preservation: that invariant forbids commons
from CORRECTING seed data it believes is wrong. It does not forbid re-reading the
source after the source itself moved and its owner asked. This file's declared
provenance is llm-security's injection table, and being loadable verbatim by that
table's owner is the one thing it exists to do. The standing alternative was a
per-pattern override in their loader — a published core its own source repository
could not load as published.

Direction is what makes it safe: the new form matches a strict superset of the
old, so it can add matches and cannot introduce a false negative. The reverse
change would not have been adoptable on this reasoning.

NOT decided by the 2-of-3 majority across the three ports. A count of
implementations is not a mandate over detection data; the provenance argument
would hold with the guard on either side. Operator decision, 2026-08-11, on
llm-security's blocking coord request (their 10:46:28Z message, step-4 swap
parked behind it).

Measured collateral: none. Full corpus run under both patterns — 84/84
lexicon-scoped cases pass under 0.7.0, and exactly one case's finding set differs
between the forms. The widening added no finding to any other case's input.

source_fidelity keeps patterns_byte_identical_to_source at 83 and gains the field
it was missing: byte_identical_against_commit: b1ba1fb. Against the original
extraction commit b0de0ca the count is now 82/83, recorded explicitly.
post_extraction_drift is marked resolved and carries before/after pattern text,
so a consumer diffing against either commit has a coordinate for what it finds.

The divergence row is closed on the closing-tag axis after reversing twice in
three days. What stays open is the one-byte span difference: the guard excludes
`<` from its negated class and the other two do not, so `<script <x>` matches
commons and llm-security but not the guard. Measured by compiling all three.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HuudQLGMyMenuyeZk8fKs5
2026-08-11 13:39:01 +02:00
6f79a6ea57 fix(lexicon,docs): the script-tag row reversed — commons is now the sole diverger
llm-security commit 90f576f (2026-08-10) dropped the closing-</script>-tag
requirement from hybrid-xss:script-tag, matching what the guard has always had.
Both now agree in shape (no closing tag required), still not byte-identical
([^><] vs [^>]). Commons' own extracted pattern is unchanged under
behaviour-preservation, so it is the only one of the three still requiring a
closing tag — the opposite of what the divergence doc said when it was written.

injection-lexicon.json gains source_fidelity.post_extraction_drift: the
existing 83/83-at-b0de0ca claim is untouched (extraction-commit fidelity never
decays), and the new 82/83-at-b1ba1fb count carries its own commit and date so
it doesn't read as a bare, decaying "vs HEAD" claim.

Verified directly against both upstream trees this session, not relayed:
llm-security's public remote re-fetched to b1ba1fb, guard read at HEAD 0dce50f.
2026-08-11 12:45:44 +02:00
f082a91995 fix(lexicon,docs): retract the claim that the guard's port cites severity.mjs
The guard asked twice which file:line carried it. Tracing it produced an
uncomfortable answer: no file of ours measured it. The claim arrived from
llm-security (coord 20260809T201048Z) and was written down here as a commons
finding in three places.

Measured against the guard's own tree: severity.mjs has never appeared in
src/llm_ingestion_guard/injection_lexicon.json at any point in that file's
history (git log -S: no commits), and at 0bf0729 - the commit our manifest
pins - the only tree-wide occurrence is docs/PLAN.md:114, correctly about the
report module. Their only lexicon source statement is the note at line 3, and
it names injection-patterns.mjs. The right file.

No detection data moves. hybrid severity is still high, still sourced to
injection-patterns.mjs:274-281 at b0de0ca; severity.mjs still has zero
occurrences of the four pattern arrays, re-measured today. Only the sentence
about the other repository falls.

Marked in place, not edited away. The claim survived review because it came
bundled with a correct measurement of the same question - a wrong citation to
a right value, which is exactly what the field it lived in was written to warn
against.

lexicon/injection-lexicon.json 0.5.0 -> 0.5.1 (provenance metadata only).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DWKkmKQBsWtrkod4MusUBC
2026-08-10 20:37:53 +02:00
49e1e79807 feat(conformance): build the 83-case corpus; the 13-pattern blocker dissolves under measurement
The corpus was blocked on a decision nobody had to take. The 13 divergent
lexicon patterns were measured on witness inputs -- an attribute run padded
past 256 characters, an interior '<', an unclosed <script> -- and the corpus
payloads contain none of those shapes. Run through both runtimes' public
entry points, all 83 patterns produce identical lexicon finding sets, the 13
included. No winner picked, because the question was never reachable from
these inputs.

Measured at each runtime's entry point (scanForInjection() at b0de0ca,
scan_output(source=OUTPUT) at 0bf0729), never at a rebuilt regex table --
the layer mistake the divergence document already had to retract once.

- conformance/<case>/{input.txt,expected.json} x83, plus manifest.json
- spec/conformance-corpus.md, normative: bytes not text, id-only findings,
  exact-within-scope, and observed_out_of_scope as evidence not expectation

Verified by a harness that does not share the generator's knowledge: reads
only the case directories, re-runs both runtimes, checks every field
including digests -- 83 cases, 0 failures. Severity agrees with what commons
publishes 83/83. Deleting the middle third of each input breaks 76 of 83
expectations; the 7 survivors are the shortest payloads, a weak mutation
rather than a weak fixture, and are recorded as such.

Scope is 83 and not 94 for a different reason than expected: the 11
non-lexicon convertible cases have no ratified cross-runtime finding id, and
writing them would mint a contract unilaterally in the same stroke as the
tag. Named in manifest.json under scope_planned.

Docs corrected in place rather than edited away: extraction-plan and
lexicon-port-divergence both claimed the 13 blocked the corpus.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WhXDL82FRrQWedEmUg12Pj
2026-08-10 04:40:56 +02:00
ff18768b71 docs: retract the "neither runtime misses an attack" claim, fix three counts
Four corrections across the two informative documents, all of them to
claims this repository made and none of them to a data file.

The retraction is the important one. lexicon-port-divergence.md said
neither runtime misses an attack: every witness payload still produced a
finding, so the divergence was only in which labels were raised. That was
wrong in method rather than in arithmetic. The payloads had been run
against the UNION of every pattern table this repository holds - 111 rules
across the lexicon, active-content.json and secret-egress.json - and the
hit that rescued them came from active-content.json, which is the Python
guard's table. llm-security has no active-content table at all. Unioning
two runtimes' tables and reading the result as a statement about each of
them turns combined reach into individual coverage.

Re-measured through llm-security's own entry point scanForInjection() at
b0de0ca - the whole engine, normalisation, homoglyph folding, rot13 and all
four arrays - all three witness payloads return found: false. Not a
different label set: no finding at all. Controls in the same run behave
normally, so the {0,256} window is a real evasion window and the <script>
pattern really does require a closing tag. llm-security reached the same
three results independently and has logged them as a v8.x task.

Second: one of the 13 divergences does not reach report level. On the
iframe witness the guard misses hybrid-xss:iframe-src but its
hybrid-xss:javascript-uri fires at the same severity and the same OWASP
anchor, so a reader of its report still sees the payload flagged. Both
sides were re-measured to confirm. The report-level number is 12; the 13
stands as the number that blocks conformance/, because a fixture names
labels.

Third: extraction-plan.md said six of the confusables have no entry in the
fold map. It is zero - all 13 are keys in the 28-entry map, and
carriers.json has said so correctly since it was written. The prose, not
the data, carried the error, and the number six most likely came from
running the comparison the other way (five fold-map Cyrillic keys are not
confusables). It also called the diverging sets deliberately distinct;
llm-security states the U+0456 / U+0445 divergence is drift, not design,
and has logged it for v8.x. Those two readings imply opposite things about
whether the sets should be expected to converge.

Fourth: the seed-sources section still asserted that sessions here have no
read access to llm-security and that content arrives only as a dump. The
operator changed that on 2026-08-09. It now records the read-only pinned-
commit boundary, the still-absolute prohibition on the sibling working copy
and on writing, and the reason the change matters - four attribution
defects caught so far, every one a transcription artefact.

Status blocks updated: all eight JSON artefacts now exist, seven verified
differentially and calibration.json still honestly verified: false; the
conformance blocker is half-cleared, with naming ratified and the 13
divergent patterns still unresolved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SNMcqrfNyoLRQ7qXUFZnb9
2026-08-09 22:54:41 +02:00
f68f1e19d5 docs(lexicon): split the alias evidence, correct a false absence claim
Advisor review found six gaps the session's own checks did not cover. The one
that mattered was outward-facing: the divergence report told the guard repo
that two sub-agent bounds were undocumented. That was wrong. lexicon.py's
module docstring documents them explicitly under "Bounded token gaps", and
git log -S dates {0,12} to the original port commit f397cd9 and [^><] to the
ReDoS fix cff0437. Every divergence on the guard's side is documented and
traceable. The claim rested on two sed slices of one file; an absence claim
needs a search over the whole repository. Corrected here and by coord.

lexicon/injection-lexicon.json 0.2.0 -> 0.3.0:

- pattern_id_space.alias_evidence records the two aliases separately instead
  of averaging them. llm_ingestion_guard is verified — coverage.py asserts on
  that exact string, so it is demonstrably what a guard finding carries.
  llm_security is not: it is the pattern table's name, the finding producer
  was never supplied, and the known Node finding shape uses title, not label.
- normalisations[].affects now keys on id, with the prose names kept beside it
  as affects_labels. An internal cross-reference on label was a second
  identity space inside the file the id exists to unify.

Detection data unmoved again: labels, patterns, flags and the ids and aliases
added in 7b70f5b are all byte-identical in sequence; 166/166 Node compiles.

Also: the README lexicon row described thematic families the file does not
have (they are severity families; the theme is the id prefix), and the
conformance convertibility table gained its missing second condition — a case
is buildable only if the label it asserts maps to data this repository
publishes. Eleven cases fail that test (entropy, decoded, the sanitize rows,
the OKF scans), so the buildable set is ~94, not 105.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FaYqid3mejFmd9ZHsiHgp3
2026-08-09 21:56:45 +02:00
7b70f5b52f feat(lexicon): publish a commons-owned pattern id with per-runtime aliases
A conformance fixture has to name a finding. The two runtimes that seed this
repository do not name the same pattern the same way -- llm-security says
"override: ignore previous instructions", the guard says
"override:ignore-previous" -- so until now no expected.json could be written
at all, including for the 64 patterns whose regex is byte-identical in both.

Each pattern now carries `id` plus an `aliases` object naming what each
seeding runtime calls it, and a top-level pattern_id_space block explains the
field, its provenance and its stability contract.

The id is adopted verbatim from the guard's port rather than invented here:
that port already carries both names, so the mapping is read from source data.
Matched by label <-> desc with em-dash normalised to hyphen, 83/83 one-to-one,
83 unique ids.

No detection data moved, and that is proven rather than asserted: labels,
patterns and flags are byte-identical in sequence, no flags key was invented
(78 before, 78 after), and stripping id + aliases + pattern_id_space
reproduces the previous committed file byte for byte -- 23566 bytes. All 83
patterns still compile in Node bare and under u (166/166) and in Python re
(83/83).

Neither consumer has ratified this id space; both were asked by coord today,
and the file records it as a proposal rather than implying agreement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FaYqid3mejFmd9ZHsiHgp3
2026-08-09 21:46:48 +02:00
22d3a78591 docs: measure the lexicon port divergence, correct the case count
The guard's coverage.py turned out to be readable from a session here — the
read boundary covers llm-security only. Counting it instead of estimating
corrected two published numbers and surfaced the real conformance blocker.

Counted, not estimated: CORE_CASES holds 134 cases (128 caught, 6 gap), not
the "126 classes + 4 gaps-must-hold" the extraction plan claimed. 105 are
convertible to static input/expected; 29 assert a runtime API surface this
repository does not own, and are named as out of scope rather than faked.

Measured while counting, and the reason the corpus is still blocked: the
commons lexicon and the guard's are two ports of one source file, and they
disagree. 83/83 patterns correspond, 64 byte-identical, 6 differ only by
escaping and are proven equivalent by string identity, and 13 behave
differently — with a witness input for each and misses in both directions.
Cause is two different ReDoS mitigations of the same table: the Node side
bounds the run ({0,256}), the guard excludes the anchor character ([^><]).
Each has a recall cost the other does not.

No data file changed. Behaviour preservation is a v0.1.0 invariant, so the
divergence is reported to the owning repositories, not fixed here. The 8
hybrid severities the guard's port assigns are deliberately NOT copied in —
a second-hand port is not the producing module.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FaYqid3mejFmd9ZHsiHgp3
2026-08-09 21:40:26 +02:00
93f7bc024f docs: close three honesty gaps the validation pass missed
1. The layout block still described calibration.json as holding "entropy
   floors, scan caps, disposition ranks" — the same never-delivered content
   the seed-source row and the README row were already corrected for. Third
   copy, same defect. Grepped the repo to confirm no fourth: the remaining
   hits either describe the absence or belong to active-content.json's own
   Shannon-entropy floor, which is a real thing from a different source.
   Tightened the calibration log entry so it cannot be read as denying that.

2. Definition of Done item 2 claimed every file is present or "explicitly and
   visibly stubbed". Three are neither — they are absent and marked Planned.
   Item 2 now records that it is unmet, which three files are missing, why
   absent beats stubbed (an empty conformance/ and a contentless normative
   spec would both PASS the mechanical checks in this document while making
   the repo look more finished than it is), and that the tag decision is the
   operator's. The DoD is what a future session reads to decide whether
   v0.1.0 is done; it must not assert a bar the repo does not clear.

3. The lexicon's "83/83 compile in both runtimes" claim had been produced
   against the raw dump, not against the committed JSON — this repo's stated
   standard is that a fidelity claim comes from a command over the artifact.
   Re-run against lexicon/injection-lexicon.json: 83/83 in Python re, 83/83
   in Node bare and under `u`. Also confirmed the \uXXXX escapes survive the
   JSON round-trip with the right semantics — the zero-width class matches
   U+200B and rejects U+00AD, the Cyrillic class matches U+0430 and rejects
   U+0445. The claim stands as written.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FaYqid3mejFmd9ZHsiHgp3
2026-08-09 21:20:06 +02:00
8ee297c585 feat(calibration): add calibration.json, marked transcribed-only
The risk-score tier constants, verdict thresholds, risk-band cutoffs and
posture grade thresholds. Constants only — the log scaling and the if/else
chains that consume them are engine and stay in the consumer.

This file is the exception in this repository and is marked as such in its
own verification block. Every other data file here was rebuilt from its
commons JSON and diffed against an imported module. This one arrived as a
human-written PROSE SUMMARY of severity.mjs, so there was nothing to import
and nothing to diff. verified: false, with the two skipped checks named
rather than a caveat attached to a pass.

The consequence inverts this repository's central rule and the file says so:
for every other file a disagreeing consumer is wrong; for this one, until the
module arrives in executable form, a disagreement is not automatically the
consumer's bug.

What COULD be checked was: bands are contiguous and non-overlapping across
0-100, the BLOCK trigger (65) is exactly the Critical band floor, the WARNING
trigger (15) is exactly the Medium band floor, and the per-tier reachable
minima recompute exactly (80/48/20/4, exact because log2(2) is 1).

Corrects README and the extraction plan in the same commit: both promised
entropy floors, scan caps and disposition ranks. None arrived — 0 occurrences
each across the whole dump. Named in the file under not_supplied so the
absence is visible rather than inferred, and both rows now describe what is
actually present.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FaYqid3mejFmd9ZHsiHgp3
2026-08-09 21:14:13 +02:00
dd6ae51d60 feat(mapping): add owasp-map.json from verified llm-security dump
Four parallel taxonomy maps — LLM, ASI (agentic), AST (skills) and MCP —
each keyed by the same 16 scanner prefixes, so a finding can be placed in
whichever taxonomy a report is written against.

Proven, not transcribed: each exported object was rebuilt from the commons
JSON alone and compared against the imported dump module — 4/4 identical on
keys, order, values and empty arrays. The shared 16-key order was verified
across all eight objects rather than assumed, and the count is the counted
one (the dump's own aside says 14).

Empty arrays are data and are preserved as arrays: agentic TRG/AST, skills
WFL/SIG, mcp WFL/TRG/SIG/AST all mean "deliberately mapped to nothing", not
"gap to fill".

Recorded as an open question in the file rather than papered over: the dump
does not state which EDITION of each taxonomy the codes belong to. OWASP's
LLM Top 10 was renumbered between editions — LLM06 is Excessive Agency in the
2025 list, with earlier entries consolidated and LLM07/LLM08 newly added — so
a bare LLM06 does not identify a risk. Two runtimes can match this map
perfectly and still disagree about what a finding means, which is the exact
failure this repository exists to prevent. taxonomy_name is left null rather
than guessed; the question goes to llm-security.

Scanner prefix meanings were not supplied and are reproduced as opaque keys.
Verification log in docs/extraction-plan.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FaYqid3mejFmd9ZHsiHgp3
2026-08-09 21:11:50 +02:00
c3852c9148 feat(signatures): add secret-egress.json from verified llm-security dump
The 18 fixed credential and token shapes a pre-write guard matches before
content is persisted: cloud keys, vendor tokens, PEM blocks, connection
strings, JWTs.

Proven, not transcribed: the table was rebuilt from the commons JSON alone
and diffed against the imported dump module — 18/18 identical on name, source
and flags, and all 18 byte-identical, so no normalisation was needed. All 18
compile in Node bare, Node under `u`, and Python `re`.

Array order is normative and is tested as such, not merely asserted: a Bearer
header containing a JWT must be labelled "Authorization header with token"
rather than "JWT (three-part token)", which is why the source puts the bare
JWT entry last. Reproduced from the commons order, and shown to change under a
reversed table. Every entry carries an explicit `order` field so a JSON
round-trip cannot reorder the contract silently.

Corrects the extraction plan's seed-source row in the same commit: it named
knowledge/secrets-patterns.md, but the dump named hooks/scripts/
pre-edit-secrets.mjs and stated the two are different tables. Recording a
source file that was never delivered is the defect class this repository
already caught once in finding.schema.json.

Neither severity nor disposition was supplied, so neither is invented — the
source table carries a name and a pattern and nothing else. Verification log
in docs/extraction-plan.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FaYqid3mejFmd9ZHsiHgp3
2026-08-09 21:10:01 +02:00
f4aa8b66f4 feat(codepoints): add carriers.json from verified llm-security dump
Six independent carrier tables: zero-width characters (5), the Unicode Tags
block with its subtraction decode rule, the two Supplementary Private Use
Areas, BIDI controls (9, the Trojan Source class CVE-2021-42574), the
Cyrillic presence set (13), and the fold-to-Latin homoglyph map (28).

Proven, not transcribed: five of the six tables were rebuilt from the commons
JSON alone and diffed against the imported dump module — every constant
identical, and the homoglyph map identical down to insertion order. Folding a
corpus through the rebuilt table and the source table gives identical results.

The tables overlap but are NOT merged, and cross_table_notes states each
divergence as fact: the zero-width carrier set includes U+00AD while the
lexicon's pattern class does not; the lexicon class holds U+0456 which
CYRILLIC_CONFUSABLES lacks, and CYRILLIC_CONFUSABLES holds U+0445 which the
class lacks. Reporting that to llm-security, not fixing it here.

Two honest limits, marked in the file rather than smoothed over: the private
use ranges arrived as a source comment with no constant behind them and carry
verified: false, and the dump's own "~25 entries" estimate for the homoglyph
map is wrong — counted mechanically it is 28.

Character names resolved through Python unicodedata against the Unicode
character database, not written from recollection. Verification log in
docs/extraction-plan.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FaYqid3mejFmd9ZHsiHgp3
2026-08-09 21:08:01 +02:00
23cf3e383a feat(lexicon): add injection-lexicon.json from verified llm-security dump
The four prompt-injection pattern families — 21 critical, 32 high, 22 medium,
8 hybrid — with per-pattern label, ECMAScript source and declared flags.

Data only. The dump names checkCognitiveLoadTrap and scanForInjection's
variant building (normalise → fold → rot13 → unicode-tag escalation) as
ENGINE logic that never moves here; they are absent by intent, not omission.

Proven, not transcribed: all four arrays were rebuilt from the commons JSON
alone and diffed against the imported dump module — 83/83 identical on label,
source and flags, 81 of them byte-identical. All 83 compile in Node bare, Node
under `u`, and Python `re`.

Two deliberate deviations from byte-identity, declared in the file: the
zero-width and Cyrillic character classes carried raw code points, four of
them invisible. Escaped to \uXXXX (the convention forbids raw invisible code
points outside conformance inputs — an unreviewable class is where a silent
false negative hides) and proven equivalent by differential match-set
comparison over 208 adversarial inputs, 832 comparisons, 0 differences.
Class membership was counted from the dump bytes: the zero-width class holds
4 code points and does NOT include U+00AD.

HYBRID_PATTERNS' severity was not supplied and is left null rather than
inferred from its siblings. Verification log in docs/extraction-plan.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FaYqid3mejFmd9ZHsiHgp3
2026-08-09 21:05:15 +02:00
0433240b14 feat(schema): add finding.schema.json with normative SARIF profile
The finding contract plus the SARIF 2.1.0 output profile, modelled on
llm-security's sarif-formatter.mjs (operator dump). Draft 2020-12, valid
against the metaschema.

The SARIF profile is proven, not transcribed: the mapping was re-implemented
from the commons JSON alone and diffed against the real toSARIF over 10
envelope shapes (all severities incl. unknown/undefined, slug edge cases,
rule-id collision, every optional-field combination, multi-scanner, explicit
version) — 0 differences.

Three things left honest rather than closed:
- additionalProperties stays open. The formatter CONSUMES findings, so fields
  it ignores are invisible in this evidence; the property list is a lower
  bound until the producer is supplied.
- The JSONL profile is status=unspecified with a TODO. "One finding per line"
  is inference, and a guessed normative contract is worse than a missing one.
- Node and the Python guard share only severity+owasp; the rest of the field
  names diverge (scanner/title vs detector/label). Recorded as an open
  question for v0.2.0, not settled silently here.

One published claim was wrong and is corrected: rule-id collision does NOT
cover punctuation, only case and whitespace runs. Caught by executing the
claim instead of asserting it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0191AKc2qW6tmXDFSx1xn53q
2026-08-09 14:52:53 +02:00
43f682bedd feat(signatures): add active-content.json from verified guard source
The EchoLeak class (CVE-2025-32711): markdown image/link/refdef/autolink,
data: URIs and active HTML, plus the URL-shape analysis that separates a URL
naming a remote document from one carrying bytes outward.

Seeded from llm-ingestion-pipeline-security v0.3.4 @ 0bf0729 (read-only) —
active_content.py's pattern table and calibration.py's severities/floors.
That module documents itself as the canonical home of this table, which is
why the guard is the source here rather than llm-security.

One deliberate deviation from byte-identity, recorded in the file: five
patterns carried `\"` from Python raw strings, which is a SyntaxError under
ECMAScript's u/v flags. Normalised to `"` and proven equivalent by
differential match-set comparison (5 patterns x 30 adversarial inputs, 0
differences). Verification log in docs/extraction-plan.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0191AKc2qW6tmXDFSx1xn53q
2026-08-09 14:37:48 +02:00
ef80288c57 feat: initialize llm-security-commons (charter, license, extraction plan)
Data+contract+fixtures repository for runtime-neutral LLM/agent security
detection. Charter is hard: no engine code, no dependencies, no runtime —
JSON data, normative specs and conformance fixtures only.

Scaffolding only; the seed data files land in follow-up commits from
verified source dumps (never from recollection).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0191AKc2qW6tmXDFSx1xn53q
2026-08-09 14:29:16 +02:00