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
224 lines
14 KiB
Markdown
224 lines
14 KiB
Markdown
# Changelog
|
|
|
|
All notable changes to this project will be documented in this file.
|
|
|
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this
|
|
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
|
Versioning note: the repository tag versions **the contract** (file set, key names,
|
|
case ids, disposition semantics). Each JSON file additionally carries its own
|
|
`"version"` field, bumped when that file changes.
|
|
|
|
## [Unreleased]
|
|
|
|
Initial extraction, in progress. Runtime-neutral detection data and the finding contract,
|
|
extracted from the `llm-security` Node implementation and a Python guard without behaviour
|
|
change. **Not yet tagged** — see *Not included* below.
|
|
|
|
### Added
|
|
|
|
- `schema/finding.schema.json` — the finding contract plus the SARIF output profile.
|
|
Normative. Closed against the producer in 0.2.0; the JSONL profile is `not applicable`.
|
|
- `signatures/active-content.json` — the EchoLeak class (CVE-2025-32711): 17 patterns,
|
|
severities, opacity floors and pass order, from the Python guard.
|
|
- `lexicon/injection-lexicon.json` — 83 prompt-injection patterns in four families
|
|
(21 critical, 32 high, 22 medium, 8 hybrid).
|
|
- `codepoints/carriers.json` — six carrier tables: zero-width characters, the Unicode Tags
|
|
block, the Supplementary Private Use Areas, BIDI controls, the Cyrillic presence set and
|
|
the 28-entry fold-to-Latin homoglyph map.
|
|
- `signatures/secret-egress.json` — the 18 fixed credential and token shapes. Array order
|
|
is normative.
|
|
- `mapping/owasp-map.json` — four taxonomy maps (LLM, ASI, AST, MCP) over one shared
|
|
16-prefix key set.
|
|
- `calibration/calibration.json` — risk-score tier constants, verdict thresholds, risk-band
|
|
cutoffs, posture grade thresholds.
|
|
- `signatures/malware-signatures.json` — the known-bad-identity table for the `SIG` class:
|
|
seven signatures over four families (`webshell`, `reverse_shell`, `cryptominer`,
|
|
`hacktool`), reproduced verbatim from `knowledge/signatures.json` at `b0de0ca`, key order
|
|
included, with the source file's byte length and SHA-256 pinned in `provenance`.
|
|
|
|
The rules were the easy half. The file's substance is the line between the table and the
|
|
engine, drawn in `engine_behaviour_not_data`: **no rule carries a `flags` field**, because
|
|
the engine compiles every pattern with `i` unconditionally — so a consumer that compiles
|
|
these case-sensitively silently under-matches all seven. Each pattern is also run against
|
|
five decode variants, not just raw bytes; rules are filtered by an enabled-families policy;
|
|
a rule fires once per file; operator rules are merged at scan time; and the loader defaults
|
|
four missing fields rather than rejecting a rule. None of that travels with the data, and
|
|
all of it changes what a consumer sees.
|
|
|
|
Two honesty notes are in `evidence_limits` rather than in prose. 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 papers over this by excluding `knowledge/`, `tests/`, `docs/` and
|
|
`node_modules/` from the scan, which is engine behaviour and does not come with the table.
|
|
|
|
Verified: 7/7 rule objects field-identical to source including key order, no non-ASCII
|
|
bytes, and all seven compile in Node bare, `i` and `iu` (21/21) and in Python `re` (7/7).
|
|
Note the exact family spellings — `reverse_shell`, not `reverse-shell`, and `cryptominer`,
|
|
not `miner`; they are policy keys, and the working note that seeded this file had both wrong.
|
|
|
|
### Verification
|
|
|
|
Every file above except `calibration.json` was proven rather than transcribed: the data was
|
|
rebuilt **from the commons JSON alone** and diffed against the source implementation. Each
|
|
file records its own result and its own limits.
|
|
|
|
`calibration/calibration.json` carries `verified: false`. Its source arrived as a prose
|
|
summary rather than as code, so no differential check was possible, and the file names the
|
|
checks that were not run instead of attaching a caveat to a pass.
|
|
|
|
- `docs/lexicon-port-divergence.md` — informative. A differential comparison of the two
|
|
ports of `injection-patterns.mjs` (this repository's and the Python guard's): 83/83
|
|
patterns correspond, 64 are byte-identical, 6 differ only by escaping and are proven
|
|
equivalent, and **13 behave differently**, with a witness input for each and misses on
|
|
both sides. The cause is two different ReDoS mitigations of one table. **No data file was
|
|
changed** — behaviour preservation holds and the finding is reported to the owning
|
|
repositories.
|
|
|
|
### Changed
|
|
|
|
- `schema/finding.schema.json` **0.1.0 → 0.2.0** — the schema is **closed**. It was seeded
|
|
from `sarif-formatter.mjs`, which *consumes* findings, so its property list could only ever
|
|
be a lower bound and `additionalProperties` had to stay open. The producer is now known —
|
|
`finding()` in `scanners/lib/output.mjs`, line 32 — and it returns an object literal with
|
|
**exactly ten keys and no spread**: `id`, `scanner`, `severity`, `title`, `description`,
|
|
`file`, `line`, `evidence`, `owasp`, `recommendation`. `additionalProperties` is `false`,
|
|
and the two keys the old schema never knew about (`id`, `evidence`) are added.
|
|
|
|
`id` gets its own definition: `DS-<prefix>-<counter>`, pattern `^DS-[A-Za-z]+-[0-9]{3,}$`.
|
|
The `{3,}` is deliberate — `padStart(3, '0')` is a minimum, so a run emitting more than 999
|
|
findings produces four digits. The id comes from a process-global counter, so it is stable
|
|
neither across runs nor across processes, and the definition says so before someone keys on it.
|
|
|
|
Nullability is now evidence rather than convention. Five keys are emitted as `null` rather
|
|
than omitted (`opts.x || null`), so a serialised finding always carries all ten. The
|
|
exception is the four assigned straight from `opts`: omit `description` and the key is
|
|
`undefined` and vanishes from the JSON. Verified by calling the real producer — ten keys in
|
|
memory, nine after serialisation.
|
|
|
|
**`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 emission sites, 13 of
|
|
them multi-code, and **four mix taxonomies inside a single value** (`LLM06, ASI02` and
|
|
friends) with no discriminator saying which is which. That sharpens the edition problem
|
|
`mapping/owasp-map.json` already records, and it has a consequence nobody had written down:
|
|
`sarif-formatter.mjs` builds `tags: [f.owasp]`, so a finding anchored to two taxonomies
|
|
produces **one** SARIF tag with a comma in it. Nothing filtering on `LLM06` will match.
|
|
Reproduced end to end through the real `finding()` and `toSARIF()`, and logged as
|
|
`known_lossiness.owasp-tag-not-split` — consumer behaviour in `llm-security`, not data, so
|
|
it is reported rather than fixed here.
|
|
|
|
The **JSONL profile is `not applicable`, not `unspecified`** — the distinction is the point.
|
|
`unspecified` would claim a profile exists and merely has not been written down. No
|
|
finding-JSONL exists: findings are emitted only inside a single JSON envelope
|
|
(`output.mjs:140`). The one 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. A consumer reading both through one code path will be wrong about one
|
|
of them, so the profile records the trap instead of leaving a TODO.
|
|
|
|
Verified: the schema is valid Draft 2020-12, every finding built by the real producer
|
|
validates against it, and four negative controls (extra property, missing `id`, malformed
|
|
`id`, unknown severity) are all rejected.
|
|
|
|
One new open question, unpatched by design: the producer's JSDoc lists **seventeen** scanner
|
|
prefixes including `IDE`, while all four maps in `mapping/owasp-map.json` are keyed on
|
|
**sixteen** without it. An `IDE` finding has no taxonomy mapping in any map. Adding the key
|
|
would be inventing detection data.
|
|
|
|
- `lexicon/injection-lexicon.json` **0.4.0 → 0.5.0** — the last null in the file is filled and
|
|
the id space is ratified. Two blockers close, no detection data moves.
|
|
|
|
`families[hybrid].severity` was `null`, deliberately, because the seed dump did not supply
|
|
it. It is **`high`** — and the interesting part is where that is written. The hybrid family
|
|
has no severity field anywhere; the engine assigns one by pushing `HYBRID_PATTERNS` matches
|
|
straight into the `high` bucket at `injection-patterns.mjs:274-281`. Both this repository
|
|
and the Python guard had first looked in `severity.mjs`, which contains no injection-family
|
|
severity at all. The guard's port holds the right value behind that wrong citation, so
|
|
`severity_provenance.not_from` records the miss explicitly: a wrong citation to a right
|
|
value is the harder defect to catch later.
|
|
|
|
`pattern_id_space.not_yet_confirmed` is replaced by `ratification`. 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. `id` is now a cross-runtime contract, which is what `conformance/` was waiting
|
|
on to be able to name a finding.
|
|
|
|
`alias_evidence.llm_security` is sharpened rather than upgraded. All 83 alias strings were
|
|
confirmed equal to the module's `label` field, in order — so the alias is certainly the
|
|
pattern's name **in the table**. It is still not established that a finding carries it: the
|
|
producer is `output.mjs:finding()`, which emits `title` and has no `label` key at all.
|
|
Verified at table level, one level short of where it would matter. Match on `id`.
|
|
|
|
- `lexicon/injection-lexicon.json` **0.3.0 → 0.4.0** — verified against the source module
|
|
instead of against the dump it was transcribed from, and **two false provenance claims
|
|
retracted**. The source is now pinned: `b0de0ca` on the public remote, imported in Node
|
|
and compared entry by entry on `source`, `flags` and `label`.
|
|
|
|
The result is **83/83 byte-identical to source**, which is not what the file previously
|
|
claimed. It said two patterns had been rewritten from raw code points into `\uXXXX`
|
|
escapes; the module already writes them escaped, so nothing had been rewritten. The stored
|
|
pattern text was right the whole time — only the account of where it came from was wrong.
|
|
The dump had rendered the module's escapes as the characters they denote, and this
|
|
repository re-escaped them, arriving at the correct bytes by way of an incorrect story.
|
|
|
|
The same inversion ran the other way in `multi-lang:french`, which carried the class
|
|
spelled with a raw accented Latin `e` where the module writes it as the escape
|
|
`\u00e9` inside the same character class.
|
|
That was the one pattern of 83 not byte-identical to source,
|
|
and it is corrected. The two spellings are the same regular expression — verified in Node
|
|
bare and under `u`, and in Python `re`, over accented, unaccented, uppercase and
|
|
non-matching French input, with identical match offsets — so **no behaviour moved**. No
|
|
pattern in the file contains a non-ASCII byte now, matching the module, whose regex
|
|
literals are pure ASCII throughout.
|
|
|
|
Structurally: `normalisations` is now `[]` with a `normalisations_note`, matching the
|
|
convention already used in `signatures/secret-egress.json`, and a new `source_fidelity`
|
|
block carries the counts, the method, the verified class membership, and both retractions
|
|
in full. Retracted claims are recorded rather than deleted — the earlier equivalence
|
|
evidence (692 Node comparisons, 236 Python) remains true, it is simply no longer
|
|
load-bearing.
|
|
|
|
- `lexicon/injection-lexicon.json` **0.2.0 → 0.3.0** — the two aliases are no longer presented
|
|
as equally backed. `pattern_id_space.alias_evidence` now records each one separately:
|
|
`llm_ingestion_guard` is **verified** (the guard's coverage matrix asserts on that exact
|
|
string, so it is demonstrably what a guard finding carries), while `llm_security` is
|
|
**not** — it is the pattern table's own name, and the finding producer was never supplied,
|
|
with the known Node finding shape using `title` rather than `label`. Averaging the two into
|
|
one file-level claim would have repeated the defect this repository corrects per-table
|
|
elsewhere.
|
|
|
|
Also: `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 was added to unify.
|
|
|
|
- `lexicon/injection-lexicon.json` **0.1.0 → 0.2.0** — every pattern gains a commons-owned
|
|
`id` and an `aliases` object naming what each seeding runtime calls it, plus a top-level
|
|
`pattern_id_space` block explaining the field. This exists because a `conformance/`
|
|
fixture has to name a finding and the two runtimes do not name the same pattern the same
|
|
way.
|
|
|
|
The id was **adopted verbatim from the guard's port**, which already carried both names,
|
|
rather than invented here. Matching was by `label` ↔ `desc` with em-dash normalised to
|
|
hyphen: 83/83, one-to-one, ids unique.
|
|
|
|
**No detection data moved.** Labels, patterns and flags are byte-identical in sequence,
|
|
no `flags` key was invented (78 before, 78 after), and stripping the three new fields
|
|
reproduces the previous committed file byte for byte — 23 566 bytes, identical. All 83
|
|
patterns still compile in Node bare and under `u` (166/166) and in Python `re` (83/83).
|
|
|
|
Neither `llm-security` nor the guard has ratified this id space yet; both were asked by
|
|
coord on 2026-08-09, and the file says so rather than implying agreement.
|
|
|
|
### Not included
|
|
|
|
- `spec/decode-pipeline.md` — needs the decode implementation. A normative spec inferred
|
|
from a data dump would be worse than an absent one.
|
|
- `conformance/` — still absent, with half the blocker cleared. 105 of the guard's 134
|
|
coverage cases are convertible to static `input.txt`/`expected.json`; the other 29 assert
|
|
a runtime's API surface, which this repository does not own. Findings can now be **named**
|
|
(see `pattern_id_space` above), but 13 patterns still have no agreed **expected
|
|
behaviour** — the two ports genuinely differ on them — so those fixtures cannot be
|
|
authored until the owning repositories answer.
|
|
|
|
These are named in the README as planned rather than linked, so nothing in the repository
|
|
points at a file that does not exist.
|