1
0
Fork 0
Commit graph

27 commits

Author SHA1 Message Date
0eab4345bf
feat(output): decode hex-wrapped credentials at the persist gate
Gap 1 closed. The output gate finds runs of 32+ hex digits
(calibration.HEX_WRAP_MIN_LEN), decodes both byte alignments, keeps text
that is at least 80% printable (the base64 bar, now one shared helper in
entropy) and runs the credential patterns on it. A hex-wrapped AWS key
surfaces as decoded:egress:aws-access-key-id with "in hex run @ N" as
evidence; the value is never in it. Credentials only: a hex-wrapped
injection is not decoded.

Measured before and after on the same trees: all 3583 clean documents
(three FP populations and the 78 pinned OKF concepts) are identical line
for line under both presets (disposition, labels, severity, count,
offset); OKF import stays 47/31. They and 569 READMEs hold 2403 hex runs
of 32+ digits, none of which decodes to text. scan_output CPU 122.9 s
before, 123.4 s after; one solid hex run at the 1M cap 4.2 s -> 5.8 s.

14 new tests (10 red for the right reason first), 6 of 6 mutations red.
Matrix 146/146 + 5/5 gaps, suite 1274 passed, ReDoS sweep 169 patterns.
2026-11-01 17:06:44 +01:00
736bf7882f
feat(lexicon): count every match of a pattern, and give four gaps a measured reason
Gap 4 (count=1) is closed. scan_lexicon still gives one finding per
pattern id at the first offset, and its count is now the number of
matches: the most matches any one view of the text shows (raw, decoded,
folded, rot13). Views are not summed, so count is a lower bound; the
joiner row counts only the splits it reports. The matrix case moved from
gap to caught (lexicon:count), and the gap register records it closed.

Measured before and after on the same trees: the FP populations are
identical line for line at 106/198, 196/2918, 108/389; all 3583 documents
(with the 78 pinned OKF concepts) keep their disposition under both
presets and the same labels and severities; OKF import stays 47/31.
53 documents now carry count > 1 (23 + 23 + 7 + 0), as measured before
building. scan_lexicon over 3505 documents: 96.0 s before, 97.1 s after.
Mutating count back to 1 turns 5 tests red.

docs/LIMITATIONS.md: gaps 2, 3, 5 and 7 now state the rule tried and its
cost (session 1 numbers): 2 fires on the true twin, 3 costs 48 on the
trusted gate, 5 costs 23 of 569 READMEs, 7 catches 2 of 5 rewordings.
Matrix 145/145 + 6/6 gaps, suite 1260 passed.
2026-11-01 14:57:11 +01:00
c4ecf0dba6
feat(watch): source list for the 16 catalogs and the 30-day release rule
docs/threat-2026/watch-sources.json is the catalog register in the edition
watch's input format: 12 entries probed (A, B, F, L x2, O x7), 4 manual as
class S with a reason. Run live on 2026-11-01: 16 current, 12 probes ok.

A frozen copy of the catalogs as v1.4.1 used them is reported behind
(OWASP LLM 2025) and on watch (RAG cheat sheet); the watch calls its Unicode
17.0 current, which only the 30-day clause catches.

tests/test_release_rule.py stops a release whose manual check or watch run
is more than 30 days older than the newest CHANGELOG date, or whose watch
output has a behind, notVerified or watch row. Each clause mutated away
turns a test red (5 of 5). No release: the package is unchanged.
2026-11-01 12:50:51 +01:00
56fcc844d6
docs(plan): record the operator's decision that gap 7 stands with a measured reason 2026-11-01 12:39:08 +01:00
0ed305e979
docs(plan): measure which watch probe answers each catalog and freeze the seven gaps
Row 1: 12 of 16 register entries answer through an existing probe class of
the reused edition watch (positive control 4 of 4); unicode and rfc-3986 need
a host on its list, the commons repository is private, and the Dual LLM post
has no edition.

Row 2: the seven documented gaps are frozen in gaps-frozen.tsv and held by
tests/test_gap_register.py. docs/gap-sweep.py measures the cheapest rule per
gap on the three false-positive corpora and the pinned OKF corpus before
anything is built: gaps 4, 1 and 6 are closable without new alarms; 2, 3, 5
and 7 get measured reasons.
2026-11-01 11:33:52 +01:00
136d9311e5
docs(plan): new direction - keep catalogs current over time and close the documented gaps 2026-11-01 08:36:54 +01:00
067e7aee89
docs(plan): record the anonymous verification after the move to open/ 2026-11-01 08:08:47 +01:00
f34a424447
docs(plan): record the final measurement on 1.8.0; the repo is public again
Rows 1-3 hold on 1.8.0: 16/16 catalogues current, 0 stale catalogue numbers
(frozen denominator 95 at 6702d7a unchanged), 17 attacks caught and 2
documented gaps with none red. The project rules now say the remote is the
public open/ repository, and .claude/ is ignored so local session state cannot
be added by accident.
2026-10-31 21:57:43 +01:00
61ee8d3445
feat(output): report an instruction in plain prose
Session 7 of the 2026 threat plan (attack row 3): A08, A09, A12, A16 and
A19 were red. Their payloads are ordinary English with no marker.

- indirect:prose-instruction (MEDIUM): a paragraph that combines two of
  four classes: it speaks to the model, it is meant to act later, it
  writes to the model's memory, it hides from the user. One class alone is
  ordinary prose. A keyword prefilter skips text where no non-address
  class can match. Catches A08, A09, A16 and A19.
- A12 (a task with no marker) is the seventh documented gap; a rule that
  caught it would be fitted to the payload's words.
- redos-sweep walked no dicts, so the new class patterns were swept by
  nothing. It walks dicts now: 168 patterns, 0 flagged; positive control
  flagged at x4.0.

Measured before building: 0 of 5494 benign documents fire (the three FP
corpora, 577 package READMEs, 1412 vendor-published agent skills). Guard
rule before/after: FP 106/198, 196/2918, 108/389 identical line for line;
OKF import 47/31 identical. Matrix 144/144 + 7/7 gaps, suite 1233 passed.
Row 3: 17 caught, 2 gaps, 0 open.
2026-10-31 21:20:32 +01:00
eeb7bf07f6
feat(active): report a run of images whose order carries a value
Session 6 of the 2026 threat plan (attack row 3): A05 output was red.
CamoLeak renders a secret as one image per character from a URL alphabet;
each URL is ordinary, so each image graded LOW and the gate WARNed.

- active:markdown-image-sequence (MEDIUM): at least 4 ordinary external
  images, inline or reference-style, on one URL template whose varying
  tokens are 2 chars or fewer, or digit runs. Digits that count up in
  document order are a counter and carry nothing. The per-image grade is
  unchanged; the finding sits beside it.
- Accepted twins (flag tables, figures 1a/1b) go to review; measured
  evasions (word dictionary, 3-char symbols, counter increments, run of 3,
  a template per symbol) are in LIMITATIONS.

Guard rule before/after: FP 106/198, 196/2918, 108/389 identical. Those
corpora hold no external markdown image, so the rule was also run on 577
package READMEs (312 with external images): 0 firings at run lengths 3-8.
OKF import 47/31 identical. Matrix 140/140, ReDoS 163 patterns 0 flagged,
suite 1179 passed + 5 xfailed. Row 3: 13 caught, 1 gap, 5 open.
2026-10-31 19:33:51 +01:00
0ebe8abdbd
feat(output): report hidden markup — tiny font, invisible colour, comments outside code
Session 5 of the 2026 threat plan (attack row 3): A04 and A15 were red.

- html-obfuscation:tiny-font (HIGH): font below 4px. ShadowLeak's 1px passed
  because the commons zero-font row only matches a leading 0.
- html-obfuscation:invisible-colour (HIGH): text within 8 per channel of the
  background in the same style attribute, or transparent. White alone is not
  a signal (email buttons); measured residual in LIMITATIONS.
- output:html-comment-present (MEDIUM): a comment in the artifact outside
  fenced code and inline code. 253/254 comments in the reference corpus sit
  in code.

Local to output.py; the vendored lexicon stays byte-equal with commons.
Guard rule before/after: FP 106/198, 196/2918, 108/389 identical; OKF 6/53
and 47/31 identical. Matrix 139/139, ReDoS 161 patterns 0 flagged, suite
1142 passed + 6 xfailed. Row 3: 12 caught, 1 gap, 6 open.
2026-10-31 17:01:39 +01:00
8f08b1906e
docs(catalogs): wire ASI06 and ATLAS v2026.10 into the README, date every source
Row 1 of the 2026 done-criterion: 16 of 16 register entries on the newest
edition, each with a check date and a source link, re-read at the publisher
on 2026-10-31.

- README: new subsection mapping OWASP ASI06 and seven MITRE ATLAS v2026.10
  techniques to what the guard stops at the write and what it does not;
  a dated catalog table; the threat-model anchors are linked.
- ATLAS moved to v2026.10 (2026-10-28) after session 1; AML.T0057 no longer
  exists there and is not linked.
- Register 1.1.0: `cited` beside `latest` on every entry; CleanBase added
  (cited in BRIEF, unregistered); unverifiable sources kept as candidates.
- tests/test_catalog_register.py fails on a missing date or link, an older
  cited edition, or a README ATLAS/ASI id the register has not verified.
- ZWJ measurement re-taken on Unicode 18.0 (1614 sequences, 122/122); the
  "458 unassigned" figure came from Python's Unicode 16.0, 406 in 18.0.
2026-10-31 16:29:35 +01:00
1c6816cdfe
fix(owasp): qualify every catalog number with the 2026 edition
OWASP's Top 10 for LLM Applications 2026 renumbered the list; only LLM01
and LLM02 kept their meaning. Every finding's `owasp` was written against
2025 with no year, so EchoLeak findings said LLM05, which in 2026 means
Data and Model Poisoning. Row 2 of the 2026 done-criterion counted 95
such sites at 6702d7a (82 old meaning, 13 wrong); it is now 0.

- Producers name the risk, never the number: one table (`_owasp.py`)
  owns `LLMnn:2026`. The vendored lexicon stays byte-equal with commons
  and gets its year where the finding is made; a commons number whose
  meaning moved between editions raises instead of being guessed.
- Mechanical map from the 2025 meaning (verified against the publisher
  in session 1): LLM05 -> LLM10:2026, LLM06 -> LLM03:2026,
  LLM10 -> LLM06:2026, LLM08 -> LLM09:2026, LLM04 -> LLM05:2026.

The 13 wrong-meaning sites, judged:
- Factual poisoning (PoisonedRAG gap, and the test that follows it):
  LLM05:2026. The 2026 text names "RAG Knowledge Base Poisoning" as its
  example 6. It said LLM09, which was Misinformation.
- Fail-secure (README row, matrix row): no number. It is the guard's own
  failure semantics; no edition describes it. Was LLM10.
- Parser-surface refusals (nested mapping, block scalar, quoted flow
  element, off-allowlist key): no number. Refusing a shape the parser
  would have to interpret is not resource consumption. Was LLM10. The
  anchor and merge-key rows stay LLM06:2026 (alias expansion is DoS).
- `resource` URL and cross-link scheme validation (4): no number. They
  validate received bundle metadata; LLM10:2026 is about model output
  and says input validation belongs to LLM01. Was LLM05.
- `_raise_case` default (OKF path traversal, absolute path, reserved
  name, non-.md, bundle escape): no number. None is prompt injection.
  Was LLM01.
A doubtful number would be the misreading the year exists to prevent,
so `Case.owasp` is now Optional and the matrix prints "-".

Self-safety (input caps, ReDoS) keeps LLM06:2026: the 2026 text, read at
the publisher, covers "input flood ... exploiting processing
inefficiencies" and application-side rechunking. The README's LLM09
anchor (Misinformation) is dropped; it was the factual-poisoning row.

The permanent test (tests/test_catalog_numbers.py) fails on an old or
wrong number, on a number without the year outside the vendored
lexicon, and on any finding the coverage matrix drives whose `owasp`
lacks the newest year; "newest" is read from the catalog register. The
census holds its wrong-rules to their counts at the frozen revision
only, so the frozen listing (da1aa0fa...6f) is unchanged.
2026-10-31 14:20:35 +01:00
c72e648a94
fix(sanitize): catch every invisible codepoint, exempt only on context
Variation-selector smuggling (A01) and Sneaky Bits (A02) gave 0 findings
on 1.4.1 and stayed in the sanitized text. The denominator is now the
frozen Unicode 18.0 set (DICP | Cf | Co, 141 674 codepoints); each one is
stripped and flagged unless a known legitimate use exempts it on context:
one VS15/16 after an emoji base, one ideographic selector after a CJK
ideograph, ZWNJ/ZWJ beside Arabic-script or Indic letters, LRM/RLM/ALM
in a document with RTL text, an Arabic number sign before a digit. A run
is always a carrier; private use is caught in full (operator decisions
2026-10-28). One predicate serves sanitize, the output gate and the
lexicon's word-split row, whose regex stays byte-identical with commons.

A01 and A02 are green; the FP populations and the pinned OKF corpus are
identical before and after.
2026-10-29 08:31:43 +01:00
08ca8f61bc
docs(plan): fix the attack batches at three sessions, 5-7
The operator approved the proposed split on 2026-10-28: hidden markup,
sequence-borne exfiltration, and instructions in plain prose.
2026-10-28 21:30:06 +01:00
69e80f26a1
docs(plan): record session 1 measurements and the scope decisions
Rows 1-3 of the done-criterion now carry frozen denominators: 15
catalogs (0 dated), 95 number sites, 19 attacks (8 caught, 1 documented
gap, 10 red). The 2025 -> 2026 mapping is re-verified against OWASP,
and the attack batches for sessions 5-7 are proposed.
2026-10-28 21:27:33 +01:00
61475686bf
docs(plan): keep the repository private until the upgrade, then move it to open/ 2026-10-28 21:06:14 +01:00
f19f434b54
docs(plan): plan the move to the 2026 threat catalogs
The approved direction heads the plan: every cited catalog on its newest
edition with a check date and source link, no catalog ID carrying an older
edition's meaning, and the known 2025-2026 attacks that can reach an
ingestion pipeline as test cases, each caught or documented as a gap.

Measured on v1.4.1 before anything is built: LLM03-LLM10 carry their 2025
meaning in 101 places across 26 tracked files, and 17 of 19 invisible or
deceptive code-point types (variation selectors among them) pass sanitize
with no finding.
2026-10-28 20:55:41 +01:00
cff043787d fix(output): 19 quadratic regex runs on the output path, worst ~5.7h at the cap
The output gate claimed LLM10 self-safety on the grounds that its patterns have
no nested quantifiers. True, and irrelevant: nesting is not what makes these
blow up. A run in front of a REQUIRED literal, reachable from a short anchor, is
enough -- crafted input repeats the anchor and never supplies the literal, so
every start position rescans the tail. Quadratic, not exponential, and the
max_scan_chars cap does not help: it bounds the input, and quadratic work on a
bounded input is still hours.

Measured, not argued. `<a:` x 100_000 took 23.4s in AUTOLINK_RE alone; the
composed gate on that payload took 458.7s, extrapolating to ~5.7 hours at the
1_000_000-char input the gate itself accepts. Size-matched ordinary prose runs
0.31s, so the separation is 18x-660x -- unlike the blob in the neighbouring
test, which is the *faster* side of prose and never exercised backtracking.

Two fixes, chosen per pattern rather than uniformly:

- active_content + lexicon JSON (15 runs): exclude the character that opens the
  pattern's own anchor (`[` for markdown, `<` for tags), so a run cannot reach
  past the next start position and the per-start costs telescope. Verified to
  cost no recall: long URLs, long alt text, and `<` inside a quoted attribute
  all still match. Bounding instead would have been linear too but wrong here --
  the content is attacker-controlled, so padding past a bound would be a
  one-line bypass of the EchoLeak class this table exists to catch.
- connstr egress (4 runs): bound the password at MAX_CONNSTR_VALUE. The
  exclusion fix is unavailable -- the anchor character is `/` and passwords
  containing `/` are the common case (measured: they match today). The residual
  miss is a credential over 256 chars; a token that long is still caught by
  egress:jwt-token.

hybrid-xss:script-tag had neither option: its run is the script BODY, which may
legitimately contain `<`. It now matches the opening tag and drops the
`</script>` requirement. That also closes a fail-open -- `<script>alert(1)`
unclosed was silently missed -- at the cost of flagging prose that merely
mentions `<script>`, now documented.

Found by the composed-gate test staying red after every individual scanner was
already linear: the lexicon's six html-obfuscation patterns were the remaining
813x. A per-scanner test alone would have shipped that.

662 passed (was 642), and faster than before the fix.
2026-07-31 18:31:58 +02:00
8deca93ee1 docs(plan): scope the self-safety claim -- ReDoS duty is the lexicon path, not output
PLAN.md's test-plan line read as if both scan paths carried ReDoS coverage.
f74245f measured that the output path's "pathological" payload is the FASTER
side against size-matched ordinary prose (0.93x / 0.96x, order swapped), so it
does not exercise catastrophic backtracking at all.

Leaving the line unscoped would be the same defect c0899e9 just retracted in
LIMITATIONS.md: a committed claim resting on a premise now known to be false.

Names which path carries which duty and says plainly that a crafted payload for
the output regexes does not exist. No new limitation bullet -- 27 stays 27.
2026-07-31 17:23:52 +02:00
66f3cbf4f5 docs(pdf): concede .pdf as a deliberate design boundary (Session F1)
Reframe .pdf from a 'known gap'/TODO to a deliberate concession across the
honest-limits and OKF-upload docs. A top-level .pdf drop is already refused as
an unsupported format (inbox_frontend.py raises on the else branch), not
half-scanned; adding a PDF parser + reportlab (solely to author white-on-white
test fixtures) is disproportionate for a dev-scoped showcase, and the OCR /
font-render stego carriers a PDF would smuggle are out of scope regardless.

- README honest-limits: .pdf = concession, not TODO; only the numeric CSV FP
  remains a known gap.
- docs/PLAN.md: upload table .pdf row marked 'conceded'; honest-scope paragraph
  names .pdf; assertions tightened to 'every accepted format'.

Closes the last format gap before v1.0 freeze (Session G). No code touched;
357 tests green.
2026-07-15 10:15:39 +02:00
4a9cfd2bbe docs: reframe novelty claim to composite write-time contract (review MAJOR #3)
Replace the unverified/absolute novelty statement with the defensible
four-part-contract form, verified against a focused adversarial PyPI+GitHub
survey (2026-07-15):

- BRIEF §11: 'assumed, not verified' -> verified survey with sources. Names
  aig-guardian (real, query-time; blurs only the minimal-dep-library
  differentiator), GuardLLM (nearest neighbour, runtime hardening, no
  scan-before-persist / capability isolation / fail-secure), and ipi-scanner
  (orphaned placeholder repo, recorded for honesty not as prior art).
- README: differentiator moved from 'library vs hosted/model' to the full
  four-part write-time contract.
- PLAN §27-31: drop the unverifiable 'the first' superlative.

Also promotes the v1.0 session plan (PLAN-v1.md) and the cross-model review
(review-2026-07.md) into docs/ on the open/ mirror, referenced by PLAN.md's
re-sequencing addendum.
2026-07-15 09:22:38 +02:00
896ab4034a docs(plan): showcase covers realistic upload formats — two-stage extract→guard, dev-scoped parsers, honest binary-layer limits 2026-07-06 10:18:43 +02:00
ba7fc68c05 docs(plan): re-sequence v0.2+ streams — defer consumer integration, add OKF inbox showcase as next build
Ground-truth pass over the named flagship consumer (portfolio-optimiser's
'OKF-upload-inbox') found it does not exist as a seam: both optimiser siblings
are frozen at release, carry their own OKF layer, receive no external bundles,
and take no dependency on this guard. Defer consumer integration (stream 2)
until the guard is mature; mature it here first, Node-port-friendly. Next
concrete build is the in-repo OKF inbox showcase (mode-b import_bundle as a
receive/quarantine gate), spec'd under 'The OKF inbox showcase'.
2026-07-06 10:12:50 +02:00
43368e9684 docs(plan): lock v0.2+ stream sequencing (OKF->consumers->Node) + dependency rationale 2026-07-06 07:30:09 +02:00
a55404460a docs(plan): add end-to-end showcase pipeline test as the final deliverable
One realistic content sample carrying many vulnerabilities at once, run through
a mock ingestion pipeline (sanitize -> lexicon+entropy+decode-rescan -> output
gate -> disposition); assert every planted vuln is caught and disposition fails
secure. Doubles as the README worked example. Added to build-order step 11 and
the test strategy; tracked as task #11. Inspiration: llm-security/examples/*.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K8GmKRCdsPjWYAKWsNgeQS
2026-07-04 17:23:17 +02:00
a9c4ccd8c7 feat: scaffold package + report and sanitize modules (TDD)
Build order steps 1-2 of docs/PLAN.md:
- pyproject.toml (llm-ingestion-guard, stdlib-only core, extras [ml]/[judge]/[dev]), LICENSE (MIT)
- report: Finding/Report/Severity/Source shared type (pure data)
- sanitize: carrier stripping (zero-width, BIDI, Unicode-tag, HTML comment,
  data: URI) with the byte-identical / removes-only invariant
- docs/PLAN.md: v1 implementation plan (positioning A, gap-expanded scope,
  llm-security reuse map)

15 tests passing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K8GmKRCdsPjWYAKWsNgeQS
2026-07-04 09:24:20 +02:00