Commit graph

13 commits

Author SHA1 Message Date
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
0ffee85a4b fix(conformance): name the fixture field pattern_id, not id — the schema already owns that word
schema/finding.schema.json defines a finding `id` as DS-<scanner>-<counter>,
built from a process-global counter: stable across neither runs nor
processes, and the schema says so itself. The corpus keys its comparison on
the lexicon's stable rule identity. Two normative documents in one
repository using one word for both would produce runtimes failing every case
for a reason unrelated to detection.

Also adds spec section 3.1, which publishes the bridge a consumer actually
needs and which neither normative document named: a runtime's own label
reaches a pattern_id through the lexicon's `aliases` object, and a runtime
absent from that object has no published way to be compared -- a mapping
kept privately in a consumer is the drift this repository exists to prevent.

Regenerated all 83 fixtures; re-verified from the corpus alone against both
runtimes, 83 cases, 0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WhXDL82FRrQWedEmUg12Pj
2026-08-10 04:44:59 +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
47760d2264 feat(signatures): add the malware identity table, drawn from source not memory
The last missing data file. Seven known-bad-identity signatures over four
families - webshell, reverse_shell, cryptominer, hacktool - reproduced
verbatim from llm-security/knowledge/signatures.json at b0de0ca, key order
included. The file was generated from the parsed source rather than typed,
and provenance pins the source's byte length (2494) and SHA-256 so the
claim is checkable rather than asserted.

Note the family spellings: reverse_shell with an underscore, and
cryptominer rather than miner. The working note this file was planned from
had both wrong. They are policy keys - the engine filters on them and
interpolates them into every finding title - so a rename is a breaking
change, which is exactly why the table was read instead of recalled.

The rules were the easy half. The substance is engine_behaviour_not_data,
which draws the line between the table and the runtime around it. No rule
carries a flags field, because the engine compiles every pattern with `i`
unconditionally at signature-scanner.mjs:48 - so a consumer compiling these
case-sensitively silently under-matches all seven, and the dialect block
records that where a reader will hit it. Also engine, not data: matching
against five decode variants rather than raw bytes, the enabled-families
policy filter, per-file rule dedup, custom-rule merging, and a loader that
defaults four missing fields instead of rejecting a rule.

Two limits are stated as evidence limits rather than left implied. Seven
signatures are not malware coverage; a clean SIG result is not "no
malware", and the seed runtime's own header calls the table deliberately
tight. And three of the seven match on names - xmrig, mimikatz,
meterpreter - so a document discussing those tools matches. The seed
runtime hides that by excluding knowledge/, tests/, docs/ and
node_modules/, which is scan scoping and does not travel with the table.

Verified: 7/7 rule objects field-identical to source including key order,
no non-ASCII bytes, all seven compile in Node bare, i and iu (21/21) and in
Python re (7/7). Charter guard clean - no executable code in the repository.

README and CHANGELOG updated: the file moves out of "planned, not in
v0.1.0" and out of "not included".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SNMcqrfNyoLRQ7qXUFZnb9
2026-08-09 22:49:21 +02:00
7e2e92eecf feat(schema): close the finding contract against its producer
The schema was seeded from sarif-formatter.mjs, which consumes findings.
That could only ever establish a lower bound on the property set, so
additionalProperties had to stay open. The producer is now readable:
finding() in scanners/lib/output.mjs line 32, at b0de0ca. It returns an
object literal with exactly ten keys and no spread, so the set is complete
and the schema closes.

Added: id and evidence, the two keys a consumer-side reading could not see.
id gets its own definition, DS-<prefix>-<counter>, with pattern
^DS-[A-Za-z]+-[0-9]{3,} - the {3,} because padStart(3) is a minimum, so a
run past 999 findings produces four digits. It comes from a process-global
counter and is stable neither across runs nor across processes; the
definition says so before someone keys on it.

Nullability is evidence now, not convention. Five keys are emitted as null
rather than omitted, so a serialised finding always carries all ten. The
four assigned straight from opts are the exception: omit description and
the key is undefined and disappears from the JSON. Reproduced against the
real producer - ten keys in memory, nine serialised.

owasp is a string, not an array, and not one code. Multiple codes are
joined with ", ". Measured across the seed runtime: 31 distinct values over
157 sites, 13 multi-code, and four that mix taxonomies inside a single
value with no discriminator. That has a consequence nobody had written
down: sarif-formatter builds tags: [f.owasp], so "LLM06, ASI02" becomes ONE
tag with a comma in it and nothing filtering on LLM06 matches. Reproduced
end to end through the real finding() and toSARIF(), logged as
known_lossiness.owasp-tag-not-split. It is consumer behaviour, not data, so
it is reported rather than fixed here.

The JSONL profile is set to "not applicable" rather than "unspecified".
The distinction carries weight: unspecified would assert a profile exists
and has merely not been written down. There is no finding-JSONL - findings
are emitted only inside one JSON envelope. The single module that does
write JSONL, audit-trail.mjs, writes audit events under a different schema
where owasp is an ARRAY. Same field name, different type, same repository.
The profile records that trap instead of leaving a TODO.

Verified: valid Draft 2020-12, every finding built by the real producer
validates, and four negative controls are rejected.

One new open question left unpatched: the producer's JSDoc lists seventeen
scanner prefixes including IDE, while all four maps in owasp-map.json are
keyed on sixteen without it. An IDE finding has no taxonomy mapping
anywhere. Adding the key would be inventing detection data.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SNMcqrfNyoLRQ7qXUFZnb9
2026-08-09 22:46:13 +02:00
be84121604 feat(lexicon): set hybrid severity to high, record the id space as ratified
Two things the file was waiting on arrived, and both are now data rather
than open questions.

families[hybrid].severity was null by design - the seed dump supplied the
array but not the severity. It is "high". The value is not stored anywhere
as a field: the engine assigns it by pushing HYBRID_PATTERNS matches
straight into the high bucket at injection-patterns.mjs:274-281, under the
comment "Hybrid patterns are HIGH severity (v5.0 S6)".

severity_provenance names the file it is NOT in. Both this repository and
the Python guard first looked in severity.mjs, which contains no injection-
family severity at all - grep it for any of the four pattern constants and
you get nothing. The guard's port carries the right value while citing that
file, so a consumer following its citation finds no answer. A wrong citation
to a right value survives review in a way a wrong value does not, so it is
recorded rather than quietly fixed.

pattern_id_space.not_yet_confirmed is replaced by a ratification block.
Both seeding runtimes agreed on 2026-08-09: llm-security ratified the 0.2.0
proposal as-is and treats an id change as breaking on the same terms, and
the guard confirmed the space its own port supplied. The id is a
cross-runtime contract now, which is what conformance/ needed before a
fixture could name a finding at all.

alias_evidence.llm_security is sharpened, not upgraded. All 83 alias
strings were confirmed equal to the module's label field in array order, so
the alias is certainly the pattern's name in the table. It is still not
established that a FINDING carries it: output.mjs:finding() emits title and
has no label key. Verified at table level, one level short of where it
would matter.

No detection data moved: all 83 patterns still byte-identical to the module
at b0de0ca, and no pattern entry differs from HEAD.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SNMcqrfNyoLRQ7qXUFZnb9
2026-08-09 22:41:06 +02:00
344bc52adf fix(lexicon): retract two false provenance claims, reach 83/83 to source
The lexicon was seeded from a transcribed dump. With the source module now
readable read-only at b0de0ca on the public remote, it was imported in Node
and compared entry by entry - RegExp source, flags, label, family, array
order. That check contradicted the file twice, in mirror-image ways.

The file claimed it had rewritten raw code points into \uXXXX escapes in
homoglyph:cyrillic-latin-mix and unicode:zero-width-in-word, and carried
byte_identical_to_source: false for both. The module already writes them
escaped. Nothing was ever rewritten. The stored pattern text was correct
throughout - only the account of its origin was wrong, because the dump had
rendered the module's escapes as the characters they denote and this
repository escaped them back.

The same inversion ran the other way in multi-lang:french, which stored a raw
accented Latin e where the module writes the é escape. That was the one
pattern of 83 not byte-identical to source, and it is corrected here, making
it 83/83. Both spellings are the same regular expression: compiled in Node
bare and under u, and in Python re, over accented, unaccented, uppercase and
non-matching French input, with identical match offsets. No behaviour moved.

Exactly one pattern entry differs from HEAD, and no pattern in the file
contains a non-ASCII byte any more - which matches the module, whose regex
literals are ASCII throughout. A third claim fell with them: the translation
note asserting that the leetspeak and multi-language patterns embed accented
Latin directly was wrong in both halves.

normalisations is now [] with a normalisations_note, the convention already
in signatures/secret-egress.json. A new source_fidelity block carries the
counts, the method, the verified class membership, and both retractions in
full - retracted, not deleted. The equivalence evidence behind the withdrawn
claim (692 Node comparisons, 236 Python) is still a true statement about raw
versus escaped; it is just no longer load-bearing.

Provenance upgraded accordingly: source_commit pinned, and the caveat saying
these checks only proved agreement with a dump is gone, because they no
longer do.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SNMcqrfNyoLRQ7qXUFZnb9
2026-08-09 22:39:32 +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
4742c067a8 docs: unlink the three files that do not exist yet, correct the changelog
repo-standard's gate flagged 10 dead internal README links. Seven now
resolve because the files landed this session. The remaining three are
unlinked and marked "Planned, not in v0.1.0" rather than invented:

- signatures/malware-signatures.json — seed data not delivered
- spec/decode-pipeline.md — needs the decode implementation, which is engine
  code and was not supplied. A normative spec guessed from a data dump would
  be worse than an absent one: it would carry a Status: normative marker and
  pass every convention check while saying nothing anyone verified.
- conformance/ — corpus form is still an open decision

The changelog claimed 0.1.0 shipped "a conformance corpus". It does not.
Rewritten to enumerate what is actually present, to state which files were
proven differentially and which one (calibration) carries verified: false,
and to name what is not included. Moved back under [Unreleased] because the
tag has not been cut.

README also now tells a reader that each data file records its own provenance
and verification strength, and which file currently says false.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FaYqid3mejFmd9ZHsiHgp3
2026-08-09 21:15:54 +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