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.
1038 lines
68 KiB
Markdown
1038 lines
68 KiB
Markdown
# PLAN — `llm-ingestion-guard`
|
||
|
||
## Retning (godkjent av operatøren 01.11.2026)
|
||
|
||
> «llm-ingestion-guard holder seg på nyeste utgave av hver trusselkatalog over tid,
|
||
> ikke bare én gang. Hvert dokumentert hull som kan lukkes uten nye falske alarmer
|
||
> på rene dokumenter, blir lukket, og resten står med begrunnelse.»
|
||
|
||
Økta foreslo formuleringen, og operatøren svarte 01.11 «Ja til din anbefaling».
|
||
Den er godkjent ordrett. Bakgrunnen var at retningen fra 28.10 var nådd, men to
|
||
ting sto igjen. Katalogkravet var bare sjekket én gang (31.10), og de sju
|
||
dokumenterte hullene var registrert uten at noen hadde prøvd å lukke dem. Samme
|
||
svar avgjorde at de 16 juli-commitene med øktlenker blir stående.
|
||
|
||
Lest slik (antakelser, ikke operatørens ord):
|
||
- «Over tid» betyr en vakt som kjører av seg selv hver uke, pluss en regel som
|
||
stopper en release med en katalogsjekk eldre enn 30 dager. Vakten gjenbrukes fra
|
||
llm-security, slik retningen fra 28.10 ønsket («helst gjenbrukt»).
|
||
- «Dokumentert hull» betyr de sju i LIMITATIONS § «The seven documented gaps», som
|
||
dekningsmatrisen holder. Resten av LIMITATIONS er innrømmelser og målte rester,
|
||
ikke hull, og telles ikke.
|
||
- «Begrunnelse» betyr en målt begrunnelse: regelen som ble prøvd, og hvor mange
|
||
falske alarmer den ga, eller hvorfor ingen regel av denne typen kan se klassen.
|
||
- Ikke tatt med (operatøren valgte bort «bare rydding»): blokk-skalaren `a #b`,
|
||
støyen i ReDoS-sveipet og repo-standard-sjekken. De står under § V.5.
|
||
|
||
**Dekket i:** katalogene over tid → § V.2 rad 1, økt 2 (§ V.7) · hullene → rad 2, økt 1
|
||
(måling) og økt 3 og utover (lukking) · ferdig → siste økt.
|
||
|
||
---
|
||
|
||
# Vedlikehold 2026-11 (v1.8.0 →)
|
||
|
||
## V.1 Målt 01.11.2026 på v1.8.0 (`067e7ae`), før noe er bygd
|
||
|
||
- **Katalogene:** 16 oppføringer i `docs/threat-2026/catalogs.json`. Alle 16 er
|
||
sjekket for hånd 31.10. **0 av 16** sjekkes av en automatisk vakt: llm-security
|
||
sin utgavevakt (`scripts/edition-watch.mjs`, en ukentlig launchd-jobb hver mandag)
|
||
har 58 kilder og 0 av dem er fra dette repoet. Ingen test stopper en release med en
|
||
gammel sjekkdato.
|
||
- **Hullene:** 7, holdt av `tests/test_coverage_matrix.py::test_documented_gap_still_holds`
|
||
(en lukket sak gjør testen rød). Ingen av de sju har en målt begrunnelse. Det som
|
||
står, er «by design» eller «utenfor det token-analyse kan se». Første vurdering
|
||
(ikke målt, bare til å ordne økt 1): 1 (hex-innpakket hemmelighet) og 4 (bare første
|
||
treff per mønster) ser byggbare ut. 5 og 6 (ren beaconing, kort ugjennomsiktig
|
||
URL-ledd) er kostbare i falske alarmer. 2, 3 og 7 (falske fakta, alene-HIGH i
|
||
betrodd prosa, oppgave uten markør) er trolig ikke lukkbare med denne typen regel.
|
||
|
||
## V.2 Ferdig-kriteriet
|
||
|
||
| # | Måling | Nevner | Målt 01.11 (v1.8.0) | Ferdig |
|
||
|---|---|---|---|---|
|
||
| 1 | Kataloger fulgt over tid | de **16** i `catalogs.json`; kan utvides, krymper aldri uten operatørens ja | **0 av 16** under automatisk vakt; ingen release-regel | Hver av de 16 sjekkes av vakten, ELLER (hvis ingen maskin kan svare for den) står den som manuell med begrunnelse. Vakten har kjørt de siste 7 dagene og melder en kjent utdatert kopi (katalogene slik v1.4.1 brukte dem) som bak. En test stopper en release der en manuell sjekk er eldre enn 30 dager før releasedatoen |
|
||
| 2 | Dokumenterte hull | de **7** i LIMITATIONS, frosset i økt 1 (`docs/threat-2026/gaps-frozen.tsv`, sha256 `b1a6d5be…9d22a`) | **0 lukket, 0 med målt begrunnelse** | Hvert hull er lukket (saken fanges, og testen er flyttet fra hull til fanget), eller det står med målt begrunnelse i LIMITATIONS |
|
||
|
||
**Vaktregel, ikke mål (uendret fra § T.2):** ingen nye falske alarmer på de tre
|
||
FP-korpusene og det pinnede OKF-korpuset (6/53). Testsuiten, dekningsmatrisen,
|
||
ReDoS-sveipet og census er grønne på hver release. En regel som lukker et hull
|
||
men gir én ny alarm på rene dokumenter, lukker ikke hullet. Den blir en målt
|
||
begrunnelse.
|
||
|
||
**Ikke framme hvis én:** en katalog er verken fulgt av vakten eller begrunnet som
|
||
manuell · vakten har ikke kjørt de siste 7 dagene · et hull står uten målt
|
||
begrunnelse. Går to økter på rad uten at et av tallene flytter seg, legges det
|
||
fram for operatøren.
|
||
|
||
## V.3 Øktene
|
||
|
||
**Økt 1: måling og frysing.** Rad 1: avgjør for hver av de 16 hvilken probe-klasse i
|
||
vakten som kan svare, eller hvorfor ingen kan. Rad 2: frys de sju (fil + sha256),
|
||
skriv den billigste regelen for hvert hull og mål falske alarmer på korpusene FØR
|
||
noe bygges. Resultatet ordner rekkefølgen for økt 3 og utover. Ingen release.
|
||
|
||
**Økt 2: vakten.** Kildelista for de 16 i vaktens format i dette repoet. Kjent
|
||
positiv: vakten melder en frosset v1.4.1-kopi som bak. Ukentlig kjøring avtales med
|
||
llm-security (det er deres jobb og deres skript, så det avtales med dem og
|
||
endres ikke i deres repo herfra). Release-regelen blir en test. Release 1.9.0
|
||
hvis noe utgitt endres, ellers bare commit.
|
||
|
||
**Økt 3 og utover: ett hull per økt,** i den rekkefølgen økt 1 målte. Hver lukking er
|
||
en minor-release med beskjed til brukerne (ny deteksjon = atferdsendring). Et hull
|
||
som ikke kan lukkes uten falske alarmer får sin målte begrunnelse samme økt.
|
||
|
||
**Siste økt: sluttmåling.** Rad 1 og 2 telles på siste release med de frosne
|
||
nevnerne, og vakten må ha kjørt de siste 7 dagene.
|
||
|
||
## V.4 Hva som er avgjort
|
||
|
||
- Juli-commitene med øktlenker blir stående (operatøren 01.11).
|
||
- Et hull er lukket bare når regelen fanger klassen, ikke bare testsaken: hull 7 med
|
||
2 av 5 omformuleringer får målt begrunnelse (operatøren 01.11, § V.6 rad 2).
|
||
- Vakten gjenbrukes fra llm-security og skrives ikke på nytt i Python. Den er
|
||
vedlikeholdsverktøy, ikke en del av pakken, så stdlib-regelen for kjernen gjelder
|
||
ikke.
|
||
|
||
## V.5 Utenfor retningen
|
||
|
||
- Blokk-skalar `a #b` gir `'a #b'` der YAML gir `'a'` (gammelt avvik).
|
||
- ReDoS-sveipet gir 1–10 tilfeldige tidsflagg per kjøring (exit 0).
|
||
- repo-standard er ikke kjørt på den offentlige flaten.
|
||
- De fire angrepene utenfor (X01–X04) står utenfor med grunn, se § T.3. De er ikke hull.
|
||
|
||
## V.6 Økt 1, målt 01.11.2026 (ingen release)
|
||
|
||
### Rad 1: hvilken probe-klasse som kan svare for hver av de 16
|
||
|
||
Målt ved å kjøre llm-security sin vakt (`scripts/edition-watch.mjs` ved `45d3459`)
|
||
mot en prøveliste i scratch, med `--used` og en egen last-run-fil, så deres
|
||
stempel ikke ble rørt. Ingen fil i llm-security er endret.
|
||
|
||
| Oppføring | Klasse | Hva proben leser | Svar 01.11 |
|
||
|---|---|---|---|
|
||
| owasp-llm-top10 | A | genai.owasp.org sin ressursliste, årstallet i sluggen | ok, 2026 |
|
||
| owasp-agentic-top10 | B | samme liste, agentic-sluggene | ok, 2026 |
|
||
| mitre-atlas | F | GitHub `releases/latest` for atlas-data | ok, v2026.10 |
|
||
| owasp-rag-cheat-sheet | L (bare overvåking) | GitHubs commit-API for fila, `0.sha` | ok, `510ea484` uendret |
|
||
| cve-2025-32711 | L (bare overvåking) | CVE.org-posten i cvelistV5 (raw.githubusercontent.com), `cveMetadata.dateUpdated` | ok, 2026-02-26 |
|
||
| de 7 arXiv-artiklene | O | arXiv-API-et, én oppføring per artikkel | ok, 7 av 7 |
|
||
| unicode | ingen | unicode.org står ikke på vaktens vertsliste, og UCD-lesmeg er tekst, ikke JSON. unicodetools på GitHub har final-17.0 som siste utgivelse (18.0 er gjeldende), og taggene har ikke versjonsform | manuell |
|
||
| rfc-3986 | ingen | rfc-editor.org står ikke på vertslista. JSON-en har `obsoleted_by: []`, så en L-probe ville virke med verten | manuell |
|
||
| llm-security-commons | N | `git ls-remote` mot open/llm-security-commons | `auth-required`: repoet er privat, og proben kjører uten legitimasjon med vilje |
|
||
| willison-dual-llm | S | ingenting (klasse S gjør ingen forespørsel) | ett datert innlegg uten utgaver |
|
||
|
||
**12 av 16** kan svares av en klasse som finnes i dag, og alle 12 svarte `ok`.
|
||
**4 av 16** har ingen maskin som svarer nå: unicode og rfc-3986 trenger en vert på
|
||
llm-securitys liste (unicode også en tekstparser), commons blir maskinlesbar først
|
||
når repoet er offentlig, og Willison-innlegget har ingen utgave å følge.
|
||
|
||
- **Positiv kontroll:** samme liste med det v1.4.1 sto på der det kan uttrykkes
|
||
(OWASP LLM 2025, ATLAS v2026.09, 2509.14285v3) og en nullet RAG-sha. Vakten
|
||
meldte 3 som bak og RAG-arket under overvåking: 4 av 4.
|
||
- **arXiv må stå én per oppføring:** sju id-er i én oppføring gir `parse-failed`.
|
||
Den sammenslåtte verdien blir 90 tegn, og vaktens grense er 80
|
||
(`VERSION_RE`).
|
||
- **CVE-proben leser CNA-posten, ikke NVD.** Registeret siterer MSRC-revisjonen og
|
||
NVDs endringsdato. NVD-API-et og msrc.microsoft.com står ikke på vertslista, og
|
||
OSV har ingen post for denne CVE-en (svarte «not found» 01.11).
|
||
- **Klasse L melder en endring som `watch`, aldri som `behind`.** Det passer for et
|
||
levende dokument og en CVE-post, men release-regelen i økt 2 må lese `watch` også.
|
||
|
||
### Rad 2: de sju frosset, og den billigste regelen per hull målt før noe bygges
|
||
|
||
**Frosset:** `docs/threat-2026/gaps-frozen.tsv`, sha256
|
||
`b1a6d5be5fbfc81263e55135abf11bc6fe349eac82db0d1e84f9e44949b9d22a`. Den holdes av
|
||
`tests/test_gap_register.py`. Et hull som lukkes, føres i testens `CLOSED` i samme
|
||
commit som saken flyttes fra hull til fanget.
|
||
|
||
**Målt med `docs/gap-sweep.py`** (nytt, gjenkjørbart; korpusrøttene er argumenter).
|
||
Grunnlinjen er gjengitt eksakt med `docs/fp-sweep.py` før målingen: 106/198 ·
|
||
196/2918 · 108/389. Vaktpopulasjonene er de tre FP-korpusene og OKF-korpuset hentet
|
||
ved pinnen `3fcbb9f` (78 dokumenter, 0 ikke-WARN), til sammen **3 583 dokumenter**,
|
||
summert bare her for å telle overflaten. I tillegg en README-populasjon på **569**
|
||
pakke-README-er, bygd på nytt 01.11 fra `node_modules` og `dist-info/METADATA` under
|
||
repo-mappa og deduplisert på sha256. Den er ikke den samme som 577-populasjonen fra
|
||
31.10, og den trengs fordi vaktpopulasjonene har **0 eksterne bilder**. Alle åtte
|
||
regler fyrer på sitt hulls egen matrisetekst (positiv kontroll, låst i
|
||
`tests/test_docs_measurement_scripts.py`).
|
||
|
||
«Nye» betyr dokumenter som gaten regelen endrer slipper gjennom i dag (WARN), og som
|
||
regelen ville stoppet.
|
||
|
||
| # | Hull | Billigste regel | Nye alarmer, 3 583 dok. | README-er (569) | Utfall |
|
||
|---|---|---|---|---|---|
|
||
| 1 | Heks-innpakket hemmelighet | Dekod heks-løp på minst 32 tegn som gir skrivbar ASCII, og kjør hemmelighetsmønstrene på klarteksten | **0**. Ingen dokumenter har engang et slikt løp | 0 (0 har løpet) | **lukkbar** |
|
||
| 2 | Falske fakta | En påstand av typen «er laget av» eller «ble fullført i» | 0 | 3 fyrer | **ikke lukkbar med en tokenregel.** Minimalpar: regelen fyrer like mye på den sanne tvillingen («laget av jern og nikkel», «fullført i 1889»), og guarden gir de samme funnene (ingen) på sann og usann setning |
|
||
| 3 | Alene-HIGH i betrodd prosa | HIGH gir ikke WARN under betrodd forhåndsinnstilling heller | **48** på den betrodde gaten (6 + 11 + 31 + 0). Opplastingsgaten endres ikke | 67 | **målt begrunnelse: 48** |
|
||
| 4 | Dedup (`count=1`) | Tell hvert treff (`count` = antall) | **0** fordi `count` aldri når `decide()`. 53 dokumenter ville fått `count` > 1 (23 + 23 + 7 + 0) | 14 med `count` > 1 | **lukkbar** |
|
||
| 5 | Ren beaconing | Ethvert eksternt bilde graderes over LOW | 0, men målingen er **blind** fordi 0 av 3 583 har et eksternt bilde | **23 nye** av 211 som passerer i dag (52 fyrer) | **målt begrunnelse: 23** |
|
||
| 6 | Kort ugjennomsiktig URL-ledd | Et URL-ledd på 8–23 tegn som base64-dekoder til minst 6 skrivbare byte | 0. Blind på bilder, men 0 av 7 382 URL-er fyrer | 1 dokument fyrer, og det er allerede ikke-WARN, så **0 nye**. 4 av 14 943 URL-er | **lukkbar, men regelen må strammes** |
|
||
| 7 | Oppgave uten markør (A12) | 7a: «samle fra private steder». 7b: 7a pluss «legg det inn i dette dokumentet» i samme avsnitt | 7a: **6** (1 + 4 + 1). 7b: **0** | 0 og 0 | 7b lukker saken uten alarmer, men treffer bare **2 av 5** omformuleringer av samme forespørsel (7a også 2 av 5) |
|
||
|
||
Funn som styrer byggingen:
|
||
- **Hull 6:** to av de fire URL-treffene er ekte base64 i godartede adresser:
|
||
Patreon-bilder med størrelsen i stien (`{"w":200}`) og GitHubs `package_id`. Det
|
||
tredje er ordet «Pygments», som dekoder til seks skrivbare tegn. Regelen må kreve
|
||
mer av klarteksten enn «skrivbar» og måles på nytt før den bygges.
|
||
- **Hull 3 og 5 telles på gaten regelen endrer, og bare på en populasjon som har
|
||
klassen.** Det er en antakelse, ikke operatørens ord. Hull 3 gir 0 på
|
||
opplastingsgaten bare fordi regelen ikke rører den. Hull 5 gir 0 på
|
||
vaktpopulasjonene bare fordi de ikke har bilder, og en måling uten positiv kontroll
|
||
teller ikke.
|
||
- **Hull 7 telles ikke som lukket** (operatøren 01.11: «OK til din anbefaling»). En
|
||
regel som fanger testsaken uten alarmer, men bommer på 3 av 5 omformuleringer,
|
||
kjenner én setning, ikke klassen. Hullet får målt begrunnelse.
|
||
|
||
**Rekkefølgen for økt 3 og utover:** 4 (dedup, billigst og 0 per konstruksjon) →
|
||
1 (heks, 0 overflate) → 6 (kort ledd, strammes og måles igjen). De fire begrunnelsene
|
||
(2, 3, 5 og 7) står klare med tallene over. De skrives inn i LIMITATIONS i økt 3,
|
||
så de kommer med i samme release som den første lukkingen.
|
||
|
||
---
|
||
|
||
## V.7 Økt 2, vakten (01.11.2026, ingen release)
|
||
|
||
**Kildelista** er `docs/threat-2026/watch-sources.json`: de 16 i registeret, i vaktens
|
||
egen form (en liste med ett `probe`-objekt per oppføring). Den er generert fra
|
||
`catalogs.json`, og `tests/test_release_rule.py` holder dem like: samme id-er, samme
|
||
`cited`, `latest`, `source` og `checked`. 12 oppføringer har en probe som gjør en
|
||
forespørsel (A, B, F, L×2, O×7, som målt i § V.6). De 4 manuelle (unicode, rfc-3986,
|
||
commons, Willison) står med klasse S. Det er vaktens egen form for «ingen maskin
|
||
kan svare» (llm-security bruker den på 6 av sine 58). Hver har en begrunnelse som
|
||
starter med `manual:`, og sjekkdatoen er registerets `checked`.
|
||
|
||
**Kjørt live 01.11** med vakten ved `45d3459`, med `--used` og en egen last-run-fil
|
||
i scratch. llm-securitys stempel hadde samme sha256 før og etter.
|
||
|
||
| Liste | behind | watch | notVerified | current | Prober ok | exit |
|
||
|---|---|---|---|---|---|---|
|
||
| `watch-sources.json` | 0 | 0 | 0 | 16 | 12 | 0 |
|
||
| `watch-sources-v1.4.1.json` (frosset) | 1: owasp-llm-top10 (2025 → 2026) | 1: owasp-rag-cheat-sheet (41f67299 → 510ea484) | 0 | 12 | 10 | 1 |
|
||
|
||
Utdataene er committet som `watch-last.json` og `watch-v1.4.1-run.json`.
|
||
|
||
**Den frosne v1.4.1-kopien** inneholder bare det v1.4.1 (tagget 2026-09-23) siterte,
|
||
og hver oppføring har utgaven utgiveren serverte den dagen. OWASP Agentic og ATLAS er
|
||
utelatt fordi v1.4.1 ikke siterte dem. RAG-arket sto på `41f67299` (2026-05-06), lest
|
||
fra GitHubs commit-API med `until=2026-09-23`. Uversjonerte arXiv-lenker gir de samme
|
||
versjonene som nå, fordi alle ble publisert før tag-dagen. Fila er låst med sha256 i
|
||
testen.
|
||
|
||
**Funn:** vakten melder Unicode 17.0 som `current`, fordi klasse S ikke spør. Det er
|
||
bare 30-dagersregelen som fanger den. Positiv kontroll med alle seks problemene: 4
|
||
manuelle sjekker fra 23.09, 1 behind og 1 watch.
|
||
|
||
**Release-regelen** (`tests/test_release_rule.py`) leser releasedatoen fra den
|
||
nyeste CHANGELOG-overskriften. Den binder derfor på release-commiten og ikke på en
|
||
vilkårlig dag mellom to releaser. En test holder overskriften lik versjonen i
|
||
`pyproject.toml`. Regelen stopper når:
|
||
- en manuell sjekk er mer enn 30 dager eldre enn releasedatoen;
|
||
- vaktens siste kjøring er mer enn 30 dager eldre (den er sjekkdatoen for de 12);
|
||
- utdataene ikke er en kjøring av denne lista;
|
||
- det finnes en rad i `behind`, `notVerified` eller `watch`. En `watch`-rad teller
|
||
aldri med i vaktens exit-kode, så regelen leser den for seg. Den ryddes av en
|
||
sjekk for hånd som oppdaterer `known_value`, og deretter en ny kjøring.
|
||
|
||
Hver klausul er mutert bort én om gangen, og hver mutasjon gjorde en test rød (5 av 5).
|
||
|
||
**Før en release:** kjør
|
||
`LLM_SECURITY_EDITION_WATCH_LAST_RUN=<scratch>/lr.json node <llm-security>/scripts/edition-watch.mjs --used docs/threat-2026/watch-sources.json > docs/threat-2026/watch-last.json`
|
||
og commit utdataene. Uten den miljøvariabelen stempler kjøringen llm-securitys egen
|
||
last-run-fil.
|
||
|
||
**Ukentlig kjøring:** foreslått til llm-security via coord 01.11. Det er deres jobb
|
||
(`run.sh` i deres tilstandsmappe, launchd mandager 09:15), så den endres ikke herfra.
|
||
Rad 1 er ikke framme før jobben har kjørt lista vår de siste 7 dagene.
|
||
|
||
**Sett, ikke endret:** commons-proben (N) svarer `ok` under llm-securitys jobb, som
|
||
setter en ssh-remote. Deres `last.json` fra 26.10 har 0 i `notVerified`. Commons blir
|
||
likevel stående manuell her. llm-securitys egen liste følger de samme commons-taggene
|
||
hver uke. Under en annen kjøring uten ssh-remote ville proben her svare
|
||
`auth-required`.
|
||
|
||
Ingen release, fordi ingenting i den utgitte pakken er endret.
|
||
|
||
## V.8 Økt 3, hull 4 lukket og fire begrunnelser (01.11.2026, release 1.9.0)
|
||
|
||
**Hull 4 (dedup):** `scan_lexicon` gir fortsatt ett funn per mønster-id med første
|
||
offset, men `count` er nå antall treff. Leksikonet leser flere varianter av teksten
|
||
(rå, dekodet, foldet, rot13), og samme treff vises i flere av dem. Derfor er `count`
|
||
det høyeste antallet i én enkelt variant, og variantene summeres ikke. Det er en nedre
|
||
grense: et rent treff pluss et rot13-skjult teller 1. For orddelingsraden telles bare
|
||
skjøtene raden melder. Matrisesaken står nå som fanget (`lexicon:count`), og
|
||
`tests/test_gap_register.py` har `CLOSED = {"dedup": "count"}`.
|
||
|
||
- **TDD:** fem tester i `tests/test_lexicon.py`. Tre var røde av riktig grunn
|
||
(`count` 1, ventet 2), og to låser det som allerede holdt (ett treff gir 1, og
|
||
rot13 pluss rent treff gir 1). Mutasjonssjekk: med `count=1` igjen ble 5 tester røde.
|
||
- **Vaktregelen:** målt før og etter på samme trær. FP-korpusene er identiske
|
||
linje for linje på 106/198 · 196/2918 · 108/389. Alle 3 583 dokumenter (de tre
|
||
korpusene og de 78 OKF-konseptene ved pinnen) har samme disposisjon under begge
|
||
forhåndsinnstillingene, og samme etiketter og alvorlighetsgrader. OKF-importen står
|
||
på 47 fail-secure / 31 warn, uendret. **53** dokumenter får `count` > 1
|
||
(23 + 23 + 7 + 0), som målt i § V.6, og høyeste `count` er 26. 0 nye alarmer.
|
||
- **Ytelse:** `scan_lexicon` over de 3 505 dokumentene i de tre FP-korpusene tok
|
||
96,0 s før og 97,1 s etter, og de tregeste dokumentene er de samme.
|
||
- **Rad 2:** hull 4 er lukket. 2, 3, 5 og 7 står med målt begrunnelse i LIMITATIONS
|
||
(tallene fra § V.6). 1 og 6 er åpne, og de er neste.
|
||
|
||
**Vakten** er kjørt på nytt 01.11 før release-commiten: 16 current, 0 behind, 0 watch,
|
||
0 notVerified. `watch-last.json` er endret bare i `lastRun`.
|
||
|
||
**llm-security** har ikke svart på forespørselen om ukentlig kjøring. Meldingen
|
||
ligger fortsatt ubehandlet i innboksen deres. Rad 1 er ikke framme før jobben deres
|
||
har kjørt lista vår.
|
||
|
||
## V.9 Økt 4, hull 1 lukket (01.11.2026, release 1.10.0)
|
||
|
||
**Hull 1 (heks-innpakket hemmelighet):** utgangsporten finner løp på minst 32
|
||
heks-sifre (`calibration.HEX_WRAP_MIN_LEN`), dekoder dem og kjører
|
||
hemmelighetsmønstrene på klarteksten. Funnet heter `decoded:egress:*`, som for
|
||
base64, og beviset sier `in hex run @ <offset>`. Matrisesaken `hex-wrapped` står nå
|
||
som fanget og sjekker at funnet kom fra heks-løpet, ikke fra en base64-dekoding.
|
||
`tests/test_gap_register.py` har `CLOSED = {"dedup": "count", "hex-wrapped": "hex-wrapped"}`.
|
||
|
||
**To avvik fra regelen målt i § V.6**, begge for å stå imot en enkel omgåelse:
|
||
- Begge byte-justeringene dekodes. Med bare én ville ett løst heks-siffer foran
|
||
nøkkelen (eller et oddetall-løp) gjemt den.
|
||
- Kravet er minst 80 % skrivbare tegn, samme krav som base64-klarteksten møter (én
|
||
delt hjelper i `entropy`), ikke 100 %. Med 100 % ville én søppelbyte foran nøkkelen
|
||
gjemt den.
|
||
Begge målt som bygget (under), så § V.6-tallet er ikke gjenbrukt.
|
||
|
||
Bare hemmelighetsmønstrene kjøres på klarteksten. En heks-innpakket injeksjon dekodes
|
||
ikke (en base64-innpakket gjør det). Det står i LIMITATIONS.
|
||
|
||
- **TDD:** 14 nye tester i `tests/test_output.py` erstatter den gamle hull-testen. De ti
|
||
som krever et funn var røde av riktig grunn (ingen funn), de fire negative holdt
|
||
allerede. Kalibreringstesten (`HEX_WRAP_MIN_LEN` frosset og lest fra
|
||
`calibration`) ble skrevet før konstanten, men ikke kjørt rød da. Den er rød med
|
||
33. Netto +14 tester. Mutasjonssjekk: bare justering 0, gulv 34, gulv 30, 100 % skrivbart,
|
||
steget fjernet og bevis-suffikset fjernet gir hver minst én rød test (6 av 6).
|
||
Gulv 30 overlevde først, og gulvtesten fikk en sak som dekker den.
|
||
- **Vaktregelen:** målt før og etter på samme trær. Alle 3 583 dokumenter har samme
|
||
disposisjon under begge forhåndsinnstillingene og samme etiketter,
|
||
alvorlighetsgrader, `count` og offset (utdataene er identiske linje for linje).
|
||
OKF-importen står på 47 fail-secure / 31 warn, uendret. 0 nye alarmer.
|
||
- **Overflaten:** de 3 583 og de 569 README-ene har **2 403** heks-løp på minst 32
|
||
tegn i **2 351** dokumenter (5 + 2 311 + 4 + 0 + 31; sjekksummer og id-er). **0**
|
||
dekodes til tekst, så regelen får aldri noe å skanne. Positiv kontroll: matrisetekstens
|
||
løp dekodes og fyrer, og et løp med vanlig tekst dekodes uten å fyre.
|
||
- **Ytelse:** `scan_output` over de 3 583 tok 122,9 s CPU før og 123,4 s etter. Ved
|
||
taket på 1 000 000 tegn koster ett sammenhengende heks-løp 5,8 s mot 4,2 s før,
|
||
og 31-tegns løp (under gulvet) 4,5 s mot 4,2 s.
|
||
- **ReDoS:** sveipet teller 169 mønstre (nytt: `_HEX_RUN_RE`). Fire kjøringer ga 1, 0,
|
||
0 og 0 tidsflagg (den kjente støyen), og output-tabellen alene 0 av 14.
|
||
- **Rad 2:** hull 1 og 4 er lukket, og 2, 3, 5 og 7 står med målt begrunnelse. Hull 6
|
||
er åpent og neste: regelen må kreve mer av klarteksten og måles på nytt (§ V.6).
|
||
|
||
## Forrige retning (godkjent av operatøren 28.10.2026, nådd 31.10)
|
||
|
||
> «llm-ingestion-guard bygger på nyeste utgave av hver trusselkatalog den viser til,
|
||
> hver med dato for siste sjekk og kildelenke. Ingen funn eller tekst bruker et
|
||
> katalognummer med en eldre utgaves betydning. De kjente angrepene fra 2025–2026
|
||
> som kan nå en innhentings-pipeline, er testsaker. Hver av dem fanges eller står
|
||
> som et dokumentert hull.»
|
||
|
||
Operatørens ord: «Dette repoet bør også oppdateres [til de siste
|
||
trusseldefinisjonene], og det er sikkert lurt å hente inspirasjon fra det som gjøres
|
||
med LLM Security.» Hva som er lagt til, og hvorfor:
|
||
- «Siste trusseldefinisjoner» er gjort målbart som tre ting: kataloger, numre og
|
||
angrep. Det er målemetoden llm-security bruker, avgrenset til det som gjelder et
|
||
bibliotek.
|
||
- «Kan nå en innhentings-pipeline» avgrenser hvilke angrep som teller (§ T.3).
|
||
- Ikke tatt med: automatisk varsling om nye katalogutgaver (kan komme senere, helst
|
||
gjenbrukt) og kostnadsregelen for alltid-lastet tekst (gjelder bare en plugin).
|
||
|
||
Operatøren 28.10: «Ja til endringen. Lag en skikkelig plan, vi bruker så mange økter
|
||
som trengs for å gjøre dette skikkelig.» Det samme «ja» godkjente den anbefalte
|
||
merkingen: funn merkes med årstall (`LLM10:2026`, OWASPs egen skrivemåte), og
|
||
endringen gis ut som 1.5.0 med en tydelig merknad.
|
||
|
||
**Publisering (operatøren 28.10):** «Tanken er at når dette repoet er oppgradert så
|
||
skal vi flytte det ut til Forgejo open.» Repoet er privat under oppgraderingen
|
||
(målt 28.10: `open/`-adressen sender videre til en privat kopi). Det flyttes til
|
||
`open/` etter sluttmålingen, se § T.4 «Siste økt». Valget om å publisere testsakene
|
||
åpent gjelder fra da.
|
||
|
||
**Dekket i:** katalogene → § T.4 økt 4 (rad 1) · numrene → økt 3 (rad 2) ·
|
||
angrepene → økt 1 (fryser lista), 2 og 5–7 (rad 3) · ferdig → siste økt.
|
||
|
||
---
|
||
|
||
# Trusselbildet 2026 (v1.4.2 →)
|
||
|
||
Planen til retningen over. Den gamle v1-planen står uendret lenger ned under
|
||
«Implementation Plan — v1». Den er historikk og telles ikke i rad 2, på samme måte
|
||
som CHANGELOG. Planen oppdateres underveis og overskrives aldri.
|
||
|
||
## T.1 Målt 28.10.2026 på v1.4.1 (`6702d7a`), før noe er bygd
|
||
|
||
1. **Katalognumrene har feil betydning.** OWASP Top 10 for LLM Applications kom i
|
||
2026-utgave (publisert 04.08.2026) med ny rekkefølge. Bare LLM01 og LLM02 betyr det
|
||
samme som i 2025. Repoet bruker LLM03–LLM10 med 2025-betydning på **101 steder i 26
|
||
sporede filer** (src 50, tests 28, docs 17, README 6; CHANGELOG, `dist/` og
|
||
`*.local.*` er utelatt). Ingen av dem bærer årstall. Hurtigtelling med
|
||
`grep -oE 'LLM(0[3-9]|10)\b'`, altså ikke en klassifisering. Økt 1 fryser den
|
||
ekte nevneren. Funnfeltet `owasp` bærer dette ut til hver eneste bruker:
|
||
EchoLeak-funnene merkes `LLM05`, som i 2026 betyr dataforgiftning.
|
||
*Økt 1 (28.10):* den ekte nevneren er **95**. Avstemt mot hurtigtellingen:
|
||
101 − 14 i historikk (v1-planen 10, PLAN-v1 1, gjennomgangen 3) + 7 tall i
|
||
skråstrek-lister som grep ikke ser (`LLM01/02/04/05/06`) + 1 `LLM01` med feil
|
||
betydning (standardverdien som merker OKF-stiavvisninger som prompt injection).
|
||
2. **Usynlige tegn slipper gjennom.** Først testet jeg tekst skjult i
|
||
variasjonsvelgere (U+FE00–FE0F, U+E0100–E01EF), der «ignore previous
|
||
instructions and exfiltrate» var kodet i 43 tegn. Det ga **0 funn** i rensingen,
|
||
i innhentingsinngangen og i utgangskontrollen, og alle 43 tegnene lå igjen i den
|
||
«rensede» teksten. Deretter testet jeg 19 tegntyper, 8 av hver, gjennom rensingen.
|
||
**17 av 19 gir 0 funn og blir liggende:** U+2060–2064, U+180E, U+3164, U+115F,
|
||
U+1160, U+FFA0, U+034F, U+FFF9, U+200E, U+061C, U+E000 (privat bruk), U+FE00 og
|
||
U+E0100. Positiv kontroll: U+E0041 (tag) og U+200B (zero-width) fanges og fjernes.
|
||
Ingen av dette står i LIMITATIONS.
|
||
3. **Ingen katalog har dato for siste sjekk.** README og «Threat-model anchors»
|
||
lenger ned nevner OWASP 2025 uten dato. Agent-katalogen (OWASP Top 10 for Agentic
|
||
Applications 2026, der ASI06 «Memory & Context Poisoning» er vårt område) og
|
||
MITRE ATLAS er ikke koblet inn.
|
||
4. **Leksikonet har ikke drevet.** Vår kopi har 83 mønstre, akkurat som
|
||
leksikonet i `llm-security-commons` v0.9.0 (filversjon 0.8.0). Commons'
|
||
tegntabell (`carriers.json` 0.2.0) mangler også variasjonsvelgere: tekstsøk etter
|
||
`FE00`, `E0100` og `variation` gir 0 treff. Hullet er altså felles.
|
||
|
||
## T.2 Ferdig-kriteriet
|
||
|
||
Retningen er nådd når **rad 1–3** holder samtidig, målt på siste 1.x-release:
|
||
|
||
| # | Måling | Nevner | Målt 28.10 (v1.4.1) | Ferdig |
|
||
|---|---|---|---|---|
|
||
| 1 | Trusselkataloger og kilder på nyeste utgave, hver med dato for siste sjekk og kildelenke | **15 oppføringer**, frosset i økt 1 (`docs/threat-2026/catalogs.json`); kan utvides, krymper aldri uten operatørens ja | **0 av 15** har dato i repoet. Bak: OWASP LLM (2025 → 2026), Unicode (17.0 → 18.0). Ikke koblet inn: Agentic, ATLAS. RAG-juksearket står uten lenke og dato | 15 av 15 |
|
||
| 2 | Steder der et katalognummer brukes med en eldre utgaves betydning, eller med en betydning ingen utgave har gitt det | telling over sporede filer, med historikk utenfor (CHANGELOG, v1-planen, utgitte gjennomganger), med `docs/catalog-census.py` | **95** i 23 filer: 82 med gammel betydning, 13 med feil. Frosset ved `6702d7a`, listing-sha256 `da1aa0fa…6f` | 0, og en permanent test holder det der |
|
||
| 3 | Kjente 2025–2026-angrep som kan nå en innhentings-pipeline, som testsaker | **19 rader** innenfor (pluss 4 utenfor med grunn), frosset i økt 1 (`docs/threat-2026/attacks.json`) | **8 fanges**, 1 er et dokumentert hull (PoisonedRAG), **10 er røde** (12 av 31 ankomster) | hver sak fanges, eller står som hull i LIMITATIONS og dekningsmatrisen |
|
||
|
||
**Vaktregel, ikke mål:** ingen nye falske alarmer på korpusene vi allerede måler mot
|
||
(falsk-positiv-korpuset og det pinnede OKF-korpuset på 6/53), og testsuiten,
|
||
dekningsmatrisen og ReDoS-sveipet er grønne på hver release. En regel som strammer inn
|
||
og gir nye alarmer på rene dokumenter, skal måles og legges fram, ikke slippes stille.
|
||
|
||
**Ikke framme hvis én:** en katalog er bak eller mangler dato · ett gammelt
|
||
nummer-sted gjenstår · en angrepssak verken fanges eller er dokumentert.
|
||
Underveis: går to økter på rad uten at et av tallene flytter seg, legges det fram for
|
||
operatøren.
|
||
|
||
## T.3 Hva som teller som «kan nå en innhentings-pipeline»
|
||
|
||
Angrepet teller når nyttelasten kommer fram som **innhold** som en pipeline henter
|
||
eller mottar og skriver til et lager en modell eller agent senere leser. Det teller
|
||
også når nyttelasten rir på modellens transformerte output før den lagres. Det teller
|
||
ikke når angrepet krever kodekjøring på verten, tilgang til modellvektene eller en
|
||
kompromittert pakke (kjernen er stdlib-only). Grensetilfeller legges fram for
|
||
operatøren i økt 1 og avgjøres ikke stille.
|
||
|
||
**Frosset 28.10 (økt 1):** `docs/threat-2026/attacks.json`, med 19 rader innenfor
|
||
og 4 utenfor. Hver rad har primærkilde, dato og mekanisme, og hver ankomst er en
|
||
test i `tests/test_attacks_2026.py`. Operatøren avgjorde grensetilfellene 28.10:
|
||
- Angrep der nyttelasten kommer inn som innhold, men skaden krevde agentverktøy, er
|
||
**med**. Testen er nyttelasten slik den kommer inn: Comment and Control,
|
||
ShadowLeak, Comet og GitHub-issue-flyten.
|
||
- ToxicSkills er med, men **bare tekstdelen**.
|
||
- MemoryTrap er **ute**, fordi det krever kodekjøring via npm postinstall (Cisco
|
||
01.04.2026, verifisert). Unit 42s minneforgiftning via sammendrag er ASI06-raden.
|
||
- Seks tillegg er tatt inn: CopyPasta, Invitation Is All You Need, GeminiJack,
|
||
Unit 42 ×2 og Comet.
|
||
- To kilder som bare finnes fra 2024 er ute (SpAIware og ASCII-smugling mot
|
||
Copilot). PoisonedRAG står.
|
||
|
||
Rettelser fra verifiseringen: CamoLeak har ingen CVE. CopyPasta-nyttelasten ligger i
|
||
README, ikke i en lisensfil. GlassWorm er en utvidelse (pakke) og står utenfor, men
|
||
teknikken er rad A01.
|
||
|
||
Kandidatlista slik den sto før verifiseringen (historikk):
|
||
- Smugling i variasjonsvelgere og andre usynlige tegn. *Målt rød 28.10.*
|
||
- EchoLeak, CVE-2025-32711 (null-klikk-eksfiltrasjon via markdown i output).
|
||
*Dekket i dag.*
|
||
- ShadowLeak, CamoLeak, ForcedLeak, AgentFlayer: eksfiltrasjon eller injeksjon via
|
||
dokumenter, e-post eller kontakter som en agent leser.
|
||
- Forgiftede minnefiler (MemoryTrap-klassen): innhold skrevet til agentens minne,
|
||
som er OWASP ASI06.
|
||
- Skill-innhold med skjulte instruksjoner (ToxicSkills-klassen).
|
||
- Injeksjon via issue- og PR-tekst som en agent henter (Comment and Control).
|
||
- Faktaforgiftning (PoisonedRAG). *Allerede dokumentert som hull. Saken skal bekrefte
|
||
at hullet står.*
|
||
- Skjult tekst i opplastede dokumenter (HTML/CSS-skjult, hvit tekst).
|
||
|
||
**Testsakene publiseres åpent** sammen med koden, slik som dagens korpus (operatøren
|
||
28.10). Grunnen er at brukerne selv skal kunne
|
||
etterprøve hva som fanges og hva som er hull. Nyttelastene er inerte: bare kjente
|
||
teknikker, `*.test`-domener og ingen fungerende kjede mot et navngitt produkt.
|
||
|
||
## T.4 Øktene
|
||
|
||
Hver økt er én leveranse, kan gis ut for seg og følger TDD med rødt før grønt.
|
||
Nummereringen er rekkefølgen, og hver økt starter fra STATE.md.
|
||
|
||
**Økt 1: måling, rød før noe bygges. Ingen produktkode.**
|
||
- Rad 1: Lag katalogregisteret som data i repoet. For hver kilde repoet viser til
|
||
(OWASP LLM Top 10, OWASP Agentic Top 10, MITRE ATLAS, OWASP RAG Security Cheat Sheet,
|
||
Unicode-versjonen som tegntabellene måles mot, og de andre i «Threat-model
|
||
anchors»): utgaven som brukes, nyeste utgave, sjekkdato og kildelenke. Hver utgave
|
||
verifiseres mot utgiveren. En kopi som er verifisert et annet sted, er et premiss
|
||
og ikke et måleresultat.
|
||
- Rad 2: Lag et telleskript som klassifiserer hvert sted som *gammel betydning* (kan
|
||
mappes mekanisk, § T.5) eller *feil betydning* (krever en vurdering; for eksempel
|
||
bruker OKF-avvisninger av YAML-former i dag `LLM10`). Frys nevneren med sha256.
|
||
- Rad 3: Verifiser kandidatene i § T.3, legg fram grensetilfellene og frys lista.
|
||
Skriv hver sak som test, markert som forventet rød der den ikke fanges i dag.
|
||
- Tegnnevneren til økt 2: Unicodes egen `Default_Ignorable_Code_Point`-mengde
|
||
(DerivedCoreProperties.txt, versjonen pinnet i registeret) pluss privat bruk-
|
||
områdene. Det er en prinsipiell nevner i stedet for en håndplukket liste.
|
||
*Gjort 28.10:* `docs/threat-2026/codepoints.json` (Unicode 18.0.0, kildefilene
|
||
sha256-pinnet). Operatøren la til formattegnene (Cf), fordi U+FFF9 ble målt rødt,
|
||
men ikke er DICP. Nevneren er DICP 4174 ∪ Cf 170 ∪ privat bruk 137 468 =
|
||
**141 674** tegn. 32 Cf-tegn ligger utenfor DICP, blant dem arabiske talltegn
|
||
(U+0600–0605) som vises. Økt 2 må unnta dem på kontekst.
|
||
|
||
**Økt 2: usynlige tegn. Gis ut som 1.4.2 (deteksjonsatferd, ingen ny API).**
|
||
- Hvert tegn i nevneren fanges og fjernes, eller er unntatt med en navngitt grunn
|
||
på samme måte som ZWJ i dag: det dømmes på **kontekst**, ikke identitet. Kjente
|
||
legitime bruk som må overleve: VS16 og VS15 etter emoji, ideografiske varianter
|
||
(U+E0100–E01EF etter et CJK-tegn, én velger per tegn), ZWNJ i persisk og indiske
|
||
skrifter (allerede en dokumentert rest), og LRM/RLM/ALM i høyre-til-venstre-tekst.
|
||
En *rekke* av velgere er bærer uansett hva den står etter.
|
||
- Både innhentingssiden og utgangskontrollen, med samme predikat på begge sider
|
||
(som for ZWJ).
|
||
- Vaktregelen er måling på falsk-positiv-korpuset og OKF-korpuset før og etter.
|
||
- Etterpå sendes målingen og kontekstregelen til `llm-security-commons` som innspill
|
||
til tegntabellen. Commons eier sin tabell, og vi skriver aldri dit.
|
||
*Gjort 28.10 (1.4.2):* alle 141 674 tegn i nevneren fanges utenfor kontekst, på
|
||
begge sider med samme predikat (testet tegn for tegn). A01 og A02 er grønne, så
|
||
**rad 3 står på 10 fanget, 1 hull og 8 røde rader (8 av 31 ankomster, var 12)**.
|
||
Operatøren avgjorde de tre åpne unntakene etter anbefaling: privat bruk **fanges
|
||
helt**, arabiske talltegn **unntas foran et siffer**, og planen om **én ideografisk
|
||
velger per CJK-tegn står**. Hva hvert unntak lar stå åpent, står i LIMITATIONS.
|
||
Målt før bygging: av 3 505 dokumenter i falsk-positiv-populasjonene og 78 i
|
||
OKF-korpuset bærer bare 348 VS16 etter emoji og 1 ZWJ mellom emoji noe fra
|
||
nevneren. ZWNJ-regelen er målt mot 9 Wikipedia-artikler (fa, hi, ml, ur, he, ar).
|
||
Der hadde 4 666 av 4 784 skrifttegn på begge sider, og resten sto ved markup eller
|
||
virama. Derfor er regelen «én side», og artikler med funn gikk fra 6/9 til 2/9.
|
||
Ett tillegg til planen: leksikonraden for orddeling dømmer nå ZWNJ med det samme
|
||
predikatet. Ellers ville persisk fortsatt fått MEDIUM på hvert ord, selv med tegnet
|
||
i behold. Selve regex-en er uendret og byte-lik med commons. Vaktregelen er målt
|
||
før og etter: OKF er identisk (6/53, import 47/31), og falsk-positiv-tallene står i
|
||
CHANGELOG for 1.4.2.
|
||
|
||
**Økt 3: katalognumre med årstall (rad 2 → 0). Gis ut som 1.5.0.**
|
||
- Alle funn bærer `LLMxx:2026`. Mapping fra 2025-betydning, verifisert i økt 1:
|
||
LLM04 → LLM05:2026 · LLM05 → LLM10:2026 · LLM06 → LLM03:2026 ·
|
||
LLM08 → LLM09:2026 · LLM09 → LLM07:2026 · LLM10 → LLM06:2026 ·
|
||
LLM01 og LLM02 er uendret i betydning og får årstall.
|
||
- Teknisk valg (kan avvises med grunn): leksikondataene holdes byte-like med
|
||
commons, og årstallet settes på ett sted der funnet lages. Når commons publiserer
|
||
numre med årstall, tas de inn.
|
||
- Stedene med *feil betydning* rettes etter vurdering, hver med begrunnelse i commit.
|
||
- Den permanente testen feiler på et katalognummer uten årstall, eller med et
|
||
årstall som ikke er nyeste, i sporede filer utenom historikk.
|
||
- CHANGELOG-merknaden sier rett ut at en bruker som filtrerer på `"LLM05"`, mister
|
||
treff. Konsumentene varsles.
|
||
*Gjort 31.10 (1.5.0):* **rad 2 står på 0** (var 95). Den frosne nevneren er uendret
|
||
ved `6702d7a` (95, `da1aa0fa…6f`), og tellingen over arbeidstreet gir 0 gammel og 0
|
||
feil. Den permanente testen er `tests/test_catalog_numbers.py`. Den feiler på
|
||
nummer med gammel eller feil betydning, på nummer uten årstall (unntatt
|
||
leksikondataene fra commons) og på funn fra matrisen som mangler nyeste årstall.
|
||
Hva som er «nyeste», leses fra katalogregisteret. Produsentene navngir risikoen,
|
||
ikke nummeret: én tabell (`_owasp.py`) eier numrene. Leksikonet er byte-likt med
|
||
commons og får årstallet når det lastes. Et nummer derfra som har flyttet seg
|
||
mellom utgavene, avvises i stedet for å gjettes.
|
||
De 13 stedene med feil betydning er vurdert slik:
|
||
- Faktaforgiftning (PoisonedRAG) er **LLM05:2026** (2 steder: matriseraden og
|
||
testen som følger den). Det er 2026-tekstens eget eksempel 6, «RAG Knowledge
|
||
Base Poisoning».
|
||
- De andre 11 stedene er fail-secure (README og matrisen), parserflate (4),
|
||
URL-validering av mottatt metadata (4) og standardverdien som merket
|
||
OKF-stiavvisninger som prompt injection (1 sted, 5 rader). De får **ikke noe
|
||
nummer**, fordi ingen utgave beskriver dem. Et tvilsomt nummer ville vært den
|
||
samme feilen på nytt. I matrisen er det 14 rader uten nummer, og hver av dem er
|
||
festet i testen.
|
||
Selvsikkerhet beholder **LLM06:2026**. 2026-teksten er lest hos utgiveren: eksempel
|
||
1 («input flood … exploiting processing inefficiencies … render the system
|
||
unresponsive») og eksempel 3 (applikasjonssidig re-chunking) dekker inputtak og
|
||
ReDoS. README-ankeret «LLM09» (Misinformation) er tatt ut. Det var
|
||
faktaforgiftnings-raden, og grounding-kroken er for den.
|
||
|
||
**Økt 4: katalogregisteret i bruk (rad 1). Ren dokumentasjon, gis ut med neste
|
||
release.**
|
||
- README-delen «What it protects against» skrives om på 2026-utgaven og kobles til
|
||
ASI06 og relevante ATLAS-teknikker. Hver kobling verifiseres mot katalogen, og
|
||
alt som ikke er verifisert, utelates.
|
||
- En test feiler når registeret mangler dato eller lenke. Den feiler ikke på alder:
|
||
automatisk varsling er utenfor retningen.
|
||
- *Gjort 31.10:* **rad 1: 16 av 16.** Alle 15 er sjekket på nytt hos utgiveren.
|
||
Bare ATLAS hadde flyttet seg: v2026.10 kom 28.10 kl. 22:55 UTC, etter økt 1.
|
||
`AML.T0057` finnes ikke lenger, så den er ikke koblet inn. CleanBase sto i BRIEF
|
||
uten å være i registeret, og er lagt til (15 → 16). README kobler ASI06 og sju
|
||
ATLAS-teknikker, hver med det vakta *ikke* stopper. Målt: tekst gjemt med hvit
|
||
eller 1px skrift gir 0 funn, mens null skriftstørrelse, base64 og rot13 fanges.
|
||
Unicode-målingen for ZWJ er tatt på nytt på 18.0 (1614 sekvenser, 122/122).
|
||
«458» kom fra Pythons Unicode 16.0, og riktig tall for 18.0 er 406.
|
||
`tests/test_catalog_register.py` holder rad 1. Utenfor tellingen
|
||
(`candidates`): «AI Agent Traps» (SSRN svarer 403, ikke verifisert) og OKF-
|
||
spesifikasjonen (operatørens beslutning).
|
||
|
||
**Økt 5…N: angrepssakene (rad 3).** Lista fra økt 1 tas i bolker. Hver bolk blir
|
||
enten deteksjon (TDD, vaktregelen holder) eller en dokumentert rest i LIMITATIONS og
|
||
dekningsmatrisen (status `gap`, slik matrisen allerede gjør). Hver bolk er en minor-
|
||
eller patch-release. Antall økter avhenger av lista og fastsettes etter økt 1.
|
||
*Fastsatt av operatøren 28.10 («Ja til din anbefaling»): tre bolker, A01 og A02
|
||
tas i økt 2:*
|
||
- **Økt 5, skjult markup:** A04 (tekst i 1px og hvitt; `zero-font` krever en
|
||
størrelse som starter på 0) og A15 utgang (HTML-kommentar i modellens output gir 0
|
||
funn).
|
||
*Gjort 31.10:* A04 og A15 (begge ankomster) fanges, så rad 3 har nå **12 fanget,
|
||
1 hull og 6 røde** (A05, A08, A09, A12, A16, A19). `html-obfuscation:tiny-font`
|
||
(under 4px) og `html-obfuscation:invisible-colour` (samme farge som bakgrunnen i
|
||
samme attributt, eller gjennomsiktig) bor i `output.py`, fordi leksikonet er
|
||
byte-likt med commons. Hvit tekst alene er bevisst ikke et signal (e-postknapper).
|
||
Det er målt at ren prosa i hvitt gir 0 funn, og det er ført i LIMITATIONS.
|
||
`output:html-comment-present` (MEDIUM) teller bare kommentarer utenfor kode.
|
||
I referansekorpuset står 253 av 254 kommentarer i kode.
|
||
- **Økt 6, rekkefølge-bæring:** A05 utgang. Hvert bilde bærer ingen data, men
|
||
rekkefølgen gjør det. Det graderes LOW og gir WARN.
|
||
*Gjort 31.10 (1.7.0):* A05 fanges, så rad 3 har nå **13 fanget, 1 hull og 5
|
||
røde** (A08, A09, A12, A16, A19). `active:markdown-image-sequence` (MEDIUM) melder
|
||
minst 4 vanlige eksterne bilder på samme URL-mal der bare korte tokener (2 tegn
|
||
eller færre) eller sifre varierer, med unntak for sifre som teller oppover.
|
||
Bildene graderes fortsatt hver for seg; funnet står ved siden av. Målt før
|
||
bygging: de tre falsk-positiv-korpusene har ikke ett eksternt markdown-bilde, så
|
||
de kan ikke prøve regelen. Derfor er den også målt på 411 npm-README-er og 166
|
||
PyPI-beskrivelser (312 med eksterne bilder) og OKF-korpuset: 0 treff for alle
|
||
lengder fra 3 til 8. Vaktregelen holder: falsk-positiv-tallene og OKF-importen er
|
||
identiske. Flaggtabeller og figurer kalt `1a`, `1b` er tvillinger regelen ikke kan
|
||
skille fra en symbolkanal. De går til gjennomsyn og står i LIMITATIONS, sammen med
|
||
de målte omgåelsene (ordbok med ord, symboler på 3 tegn, en teller der stegene
|
||
bærer verdien).
|
||
- **Økt 7, instruksjoner i ren prosa:** A08, A09, A12, A16 og A19. Ingen av dem har
|
||
en markør. Her er det mest sannsynlig at flere ender som dokumenterte hull fordi
|
||
vaktregelen ikke holder. Det skal måles, ikke antas.
|
||
*Gjort 31.10 (1.8.0):* A08, A09, A16 og A19 fanges, og A12 er et dokumentert hull.
|
||
Dermed har rad 3 **17 fanget, 2 hull og 0 røde**. Hver rad fanges eller står som
|
||
hull, så rad 3 holder. `indirect:prose-instruction` (MEDIUM) melder et avsnitt som
|
||
kombinerer to av fire klasser: teksten snakker til modellen, den skal virke senere
|
||
(«from now on», «if the user later says»), den skriver til modellens minne, eller
|
||
den skjuler seg for brukeren. Én klasse alene er vanlig prosa. Målt før bygging:
|
||
ingen av de 5 494 godartede dokumentene har to ulike klasser i samme avsnitt. Det
|
||
gjelder de tre falsk-positiv-korpusene, 577 pakke-README-er og 1 412 skills, agenter
|
||
og kommandoer fra to plugin-samlinger som leverandøren selv publiserer (tekst som
|
||
per definisjon er instruksjoner til en agent). Med hele dokumentet som enhet fyrer
|
||
1 av 5 494. A12 har ingen av klassene: det er en forespørsel en vedlikeholder kunne
|
||
sendt, og regelen som ville fanget den, ville vært tilpasset ordene i nyttelasten.
|
||
Det er ført som sjuende hull i matrisen og i LIMITATIONS, sammen med de målte
|
||
omgåelsene (én klasse alene, omskriving, klassene i hvert sitt avsnitt, et annet
|
||
språk). Ett tillegg: ReDoS-sveipet gikk ikke inn i dict-er, så de fire nye mønstrene
|
||
ble ikke sveipet før det ble rettet. Nå sveipes 168 mønstre.
|
||
|
||
**Siste økt: sluttmåling.** Rad 1–3 telles på siste release med de frosne
|
||
nevnerne. Bare når alle tre holder, er retningen nådd.
|
||
Deretter **flyttes repoet til `open/`**. Operatøren flytter det på serveren. Økta
|
||
gjør tre ting først. Den sjekker den offentlige flaten for private stier,
|
||
koordineringsmetadata og økt-lenker, både i filene og i commit-meldingene. Den
|
||
retter prosjektreglene, slik at de sier at repoet er offentlig igjen. Og den sørger
|
||
for at installasjonsveiledningen peker dit repoet faktisk ligger. Etter flyttingen
|
||
verifiseres README og installasjonen **anonymt**.
|
||
*Målt 31.10 på 1.8.0 (`d43403b`): retningen er nådd.*
|
||
- **Rad 1: 16 av 16.** Ti er sjekket på nytt hos utgiveren i denne økta: ATLAS
|
||
(v2026.10), Unicode (18.0.0), de sju arXiv-versjonene og commons (v0.9.0). De
|
||
seks andre ble sjekket i økt 4 samme dag. `tests/test_catalog_register.py` er grønn.
|
||
- **Rad 2: 0.** Den frosne nevneren er uendret: 95 ved `6702d7a`, sha256
|
||
`da1aa0fa…6f`. Over arbeidstreet er det 0 med gammel og 0 med feil betydning.
|
||
- **Rad 3: 17 fanget, 2 hull (A12, A13), 0 røde.** Det er 19 rader innenfor, og
|
||
ingen av dem står med strict xfail. Matrisen er 144/144, og de 7 dokumenterte
|
||
hullene står.
|
||
- Vaktregelen: 1237 tester passerer, ReDoS-sveipet gir 0 flagg av 168 mønstre, og
|
||
census gir 0.
|
||
- **Den offentlige flaten:** i filene finnes ingen private stier, ingen økt-lenker og
|
||
ingen Keychain- eller postboksreferanser. `llms.txt` installerte v1.1.0. Pinnen er
|
||
rettet, og `tests/test_install_pins.py` holder nå alle installasjonspinner lik
|
||
`__version__`. Commit-meldingene: 16 commits fra 4.–5. juli bærer
|
||
`Claude-Session`-lenker. Alle ligger før `v0.1.0`, så hver eneste tagg bygger på
|
||
dem, og å fjerne dem krever at historikken skrives om. Det legges fram for
|
||
operatøren. Dokumentene nevner også andre av operatørens repoer ved navn
|
||
(commons, wiki-, okf- og linkedin-konsumentene). De fleste er ikke offentlige,
|
||
så for en leser er det navn uten adresse. Det er proveniens, ikke metadata, og
|
||
står uendret.
|
||
- **Etter flyttingen (01.11), anonymt verifisert:** operatøren flyttet repoet til
|
||
`open/` og gjorde det offentlig. Uten innlogging svarer API-et 200, repo-siden 200 og
|
||
README-en 200, og taggen `v1.8.0` peker på `d43403b`. Den gamle `ktg/`-adressen sender
|
||
videre med 301. Begge installasjonslinjene i README installerer 1.8.0 i hver sin rene
|
||
venv, uten git- og pip-oppsett og uten token: git-pinnen og indeksen. Den installerte
|
||
pakken stopper en injeksjon med `FAIL_SECURE` og slipper et rent sammendrag gjennom
|
||
med `WARN` («clean: no findings»), som er gulvet. Commitene fra juli står inntil
|
||
videre, og spørsmålet om historikken ligger hos operatøren.
|
||
|
||
## T.5 OWASP LLM Top 10: 2025 → 2026
|
||
|
||
Kilde: OWASPs 2026-utgave (PDF-innholdsfortegnelse, `2026/final/LLMxx_*.md` og
|
||
README i `GenAI-Security-Project/GenAI-LLM-Top10`). Hentet fra en kopi som ble
|
||
verifisert 02.10.2026. **Økt 1 re-verifiserer mot utgiveren før noe mappes.**
|
||
*Re-verifisert 28.10 mot utgiveren:* alle ti stemmer. Overskriftene i
|
||
`2026/final/*.md` er lest, og fillista er hentet live fra repoets API. Forordet
|
||
bekrefter flyttene. OWASP skriver selv `LLM01:2026`. Detaljer og bevis står i
|
||
`docs/threat-2026/catalogs.json`.
|
||
|
||
| 2025 | 2026 |
|
||
|---|---|
|
||
| LLM01 Prompt Injection | LLM01 Prompt Injection |
|
||
| LLM02 Sensitive Information Disclosure | LLM02 Sensitive Information Disclosure |
|
||
| LLM03 Supply Chain | LLM04 Supply Chain |
|
||
| LLM04 Data and Model Poisoning | LLM05 Data and Model Poisoning |
|
||
| LLM05 Improper Output Handling | LLM10 Improper Output Handling |
|
||
| LLM06 Excessive Agency | LLM03 Excessive Agency |
|
||
| LLM07 System Prompt Leakage | LLM08 Hidden Context Exposure |
|
||
| LLM08 Vector and Embedding Weaknesses | LLM09 Vector and Embedding Weaknesses |
|
||
| LLM09 Misinformation | LLM07 Misinformation |
|
||
| LLM10 Unbounded Consumption | LLM06 Unbounded Consumption |
|
||
|
||
## T.6 Åpne spørsmål
|
||
|
||
1. ~~Publiseres testsakene åpent?~~ **Ja**, operatøren 28.10 (§ T.3).
|
||
2. ~~Grensetilfellene i § T.3.~~ **Avgjort** av operatøren 28.10 (§ T.3).
|
||
3. ~~Tegnnevneren.~~ **Utvidet med Cf**, operatøren 28.10 (§ T.4 økt 1).
|
||
4. ~~Unntakene med uavklart kontekst.~~ **Avgjort** av operatøren 28.10 (§ T.4
|
||
økt 2): privat bruk fanges, talltegn unntas foran siffer, CJK-velgeren står.
|
||
|
||
---
|
||
|
||
# Implementation Plan — `llm-ingestion-guard` v1
|
||
|
||
Derived from `docs/BRIEF.md` plus a 2026-07-04 research pass (competitive landscape,
|
||
novelty/gap refutation, security-coverage gaps). This plan folds the verified gaps
|
||
into the v1 scope. The BRIEF is the design rationale; this is the actionable build.
|
||
|
||
## Resolved decisions (2026-07-04)
|
||
|
||
- **Name:** distribution `llm-ingestion-guard`, import `llm_ingestion_guard`. Repo
|
||
directory stays `llm-ingestion-pipeline-security`.
|
||
- **License:** MIT.
|
||
- **Lexicon storage:** shared JSON data + thin Python loader (single source of truth,
|
||
polyglot-ready for a later TS port).
|
||
- **Publish target:** Forgejo `open/` only (public). No PyPI, no GitHub. Install via
|
||
`pip install git+ssh://…/llm-ingestion-guard.git`.
|
||
- **Language:** Python-first, stdlib-only core; ML/judge detectors behind extras
|
||
(`[ml]`, `[judge]`), never in the core path.
|
||
|
||
## Positioning — A: "Guard the artifact"
|
||
|
||
> Query-time guardrails guard the answer. `llm-ingestion-guard` guards the **artifact**.
|
||
|
||
Lead with the **contract + placement** (write-path, pre-persist) + **failure-semantics**
|
||
(fail-secure toward the artifact, not fail-open toward a user). Detection is the weakest,
|
||
most-evadable layer — defense-in-depth, not the pitch.
|
||
|
||
Defensible claim, every qualifier load-bearing: *a dependency-light,
|
||
framework-agnostic **library** that packages the full write-time
|
||
injection-**containment** contract (quarantine → per-stage capability isolation →
|
||
scan-before-persist → fail-secure disposition) as composable code — the part
|
||
query-time tooling and single-stage detectors leave to the integrator.* Not "the
|
||
first" (an unverifiable temporal claim); the load-bearing qualifier is the *full
|
||
four-part contract*, verified against the 2026-07-15 survey (`docs/BRIEF.md` §11).
|
||
Cite OWASP LLM08:2025 / RAG Security Cheat Sheet for legitimacy; reference Dual-LLM
|
||
(Willison 2023) and CaMeL (DeepMind 2025) as architecture lineage — inspiration,
|
||
**not** equivalence.
|
||
|
||
### Claims we will NOT make (verified overclaim risks)
|
||
|
||
- NOT "prevents RAG/knowledge poisoning" — cannot catch **factual** poisoning
|
||
(PoisonedRAG: plausible false facts, no markers, low entropy).
|
||
- NOT "blocks prompt injection" — pattern/entropy detection is bypassable (arXiv
|
||
2504.11168, up to 100% evasion). Detection is a signal feeding disposition, never a
|
||
sole gate.
|
||
- NOT "invented" tool-less quarantine / capability isolation — prior art (Dual-LLM, CaMeL).
|
||
- NOT "the only" ingestion-security tool — PII-scoped libs (Philter, Presidio) and
|
||
services (AWS, HiddenLayer) exist; we are the minimal-dep *library* for the
|
||
injection-**containment** contract.
|
||
- Positioned as **complementary** to query-time guardrails (LLM Guard, Lakera,
|
||
LlamaFirewall), not a replacement.
|
||
|
||
## Module set (v1)
|
||
|
||
Core (BRIEF §5): `report`, `sanitize`, `fence`, `lexicon`, `entropy`, `contract`,
|
||
`output`, `disposition`.
|
||
|
||
Gap additions folded into v1 (from the security-coverage research):
|
||
|
||
- **`neutralize`** (new) — opt-in **pure** defang helpers for model OUTPUT: markdown
|
||
images, autolinks, `[ref]` links, `data:`/external URIs, raw HTML. Mutation is opt-in
|
||
and kept separate from the report-only gate, so design principle 3 (pure detection) and
|
||
4 (disposition belongs to the caller) hold. Closes EchoLeak-class (CVE-2025-32711)
|
||
persisted zero-click exfil — these are neither injection strings nor high-entropy, so
|
||
lexicon+entropy alone miss them.
|
||
- **`output`** extended — secret/PII egress patterns (OWASP LLM02); **decode-and-rescan**
|
||
(run the lexicon over decoded base64/hex, not just flag blob presence).
|
||
- **`fence`** hardened — randomized, unspoofable per-call delimiter; strip attacker fence
|
||
markers from the payload first.
|
||
- **Scanner self-safety** (OWASP LLM10) — input-size cap, ReDoS-safe patterns (bounded
|
||
quantifiers + per-scan guard), decompression guard. A scanner that hangs on crafted
|
||
input *is* the DoS.
|
||
- **`grounding`** (interface now, impl later) — a `SourceGroundingCheck` protocol in
|
||
core; ML/judge implementation behind the `[judge]` extra. The only structural handle on
|
||
semantic poisoning that lexicon+entropy cannot see. Ship the seam + honest scope note.
|
||
- **Chunk-aware / sliding-window scan** — option for split-payload evasion across chunk
|
||
boundaries.
|
||
- **`disposition` presets** — named source-trust tiers, including a **high-untrust
|
||
user-upload / open-source preset**: QUARANTINE_REVIEW as the default and **hard-fail on
|
||
CRITICAL**, not WARN. An automatic, unattended inbox that ingests arbitrary user uploads
|
||
is the canonical high-untrust consumer (§4.7) — over-blocking one upload is far cheaper
|
||
than persisting a poisoned one.
|
||
|
||
## Build order (TDD — a failing test FIRST for each)
|
||
|
||
1. `report` — findings / Report dataclass (shared return type)
|
||
2. `sanitize` — carrier stripping (zero-width, BIDI, Unicode-tag, HTML comment, `data:`);
|
||
byte-identical invariant on clean input, removes only
|
||
3. `entropy` — shannon / base64-like / hex-blob; decode-and-rescan support
|
||
4. `lexicon` — JSON data + loader + `scan`; ReDoS-safe; size cap; port from the
|
||
`llm-security` JS seed (CRITICAL/HIGH/MEDIUM/HYBRID + normalization/homoglyph/rot13/
|
||
unicode-tag)
|
||
5. `fence` — randomized delimiter; marker-strip
|
||
6. `neutralize` — opt-in defang helpers
|
||
7. `output` — compose lexicon+entropy over output; PII/secret egress; decode-and-rescan
|
||
8. `disposition` — WARN | QUARANTINE_REVIEW | FAIL_SECURE; source-trust + intra-document
|
||
provenance tiering (§4.7); compound-signal fail-secure (§4.6); fail-**closed** when the
|
||
scanner itself errors
|
||
9. `contract` — tool-less assert; per-stage credential allowlist; env-scoping helpers
|
||
10. `grounding` — protocol/interface only in core
|
||
11. Top-level `__init__` wiring the §6 checklist; `pyproject.toml`; `LICENSE`; README
|
||
(positioning + honest limitations); adversarial + false-positive corpora; the
|
||
end-to-end showcase pipeline test (see Test strategy — built LAST)
|
||
|
||
## Reuse map — `llm-security` v7.8.0 (MIT, same author)
|
||
|
||
Maximal reuse: most detection logic is a JS→Python **port**, not new code.
|
||
|
||
| Our module | Seed in `llm-security` | Notes |
|
||
|---|---|---|
|
||
| `lexicon` | `scanners/lib/injection-patterns.mjs` | CRITICAL/HIGH/MEDIUM/HYBRID tables + `scanForInjection` (variant-set: raw / normalized / homoglyph-folded / rot13, dedup by label) + `checkCognitiveLoadTrap`. The load-bearing port. |
|
||
| `sanitize` | `scanners/unicode-scanner.mjs` + `string-utils.mjs` | zero-width / BIDI / Unicode-tag / homoglyph detection + `stripBidiOverrides` / `decodeUnicodeTags` / `containsUnicodeTags`. |
|
||
| normalization | `string-utils.mjs` `normalizeForScan` | decode chain (unicode-tags → bidi → HTML-entities → unicode/hex/url escapes → base64), `foldHomoglyphs`, `rot13`, `collapseLetterSpacing`. Feeds decode-and-rescan. |
|
||
| `entropy` | `scanners/entropy-scanner.mjs` + `string-utils.mjs` | `shannonEntropy` / `isBase64Like` / `isHexBlob` / `tryDecodeBase64`; length-calibrated thresholds; `isFalsePositive` suppression rules; `DATA_URI_PREFIXES`. |
|
||
| `report` | `scanners/lib/output.mjs` + `severity.mjs` | `finding` / `scannerResult` envelope; `SEVERITY`; `riskScore` / `verdict` / `riskBand` / `owaspCategorize`. |
|
||
| `output` secret/PII egress | `knowledge/secrets-patterns.md` | Ready secret regex set (AWS/Azure/GCP/GitHub/npm/OpenAI/Anthropic/PEM/DB-conn/password/JWT) + FP-suppression rules. Seeds the LLM02 egress gap directly. |
|
||
| tests (corpora) | `examples/prompt-injection-showcase/payloads.json` | Labeled adversarial + false-positive corpus (category/severity/expected). Maps ~1:1 to §9. Plus `knowledge/{attack-mutations,attack-scenarios,signatures}.json`. |
|
||
| wiring reference | `hooks/pre-prompt-inject-scan.mjs` (input), `post-mcp-verify.mjs` (output gate) | How the scan is invoked at input vs output — informs our `output` module + the §6 checklist. |
|
||
|
||
**Port caveats (JS→Python, stdlib-only):**
|
||
- `/…/i` → `re.I`; `Buffer.from(s,'base64')` → `base64.b64decode`; `codePointAt`/`fromCodePoint` → `ord`/`chr`.
|
||
- `TOKEN_RE = /[\p{L}\p{N}_]+/gu` — Python `re` has no `\p{L}`; use `\w` under `re.UNICODE` (Py3 default) or `str.isalnum()`. No `regex` dependency.
|
||
- **ReDoS**: some patterns (sub-agent spawn with nested `.*?`) backtrack; Python `re` has no timeout → enforce input-size cap + review/bound these patterns (the self-safety must-have).
|
||
- Lexicon ships as **JSON** (regex source + label + severity), compiled by a thin Python loader — decouples data from engine for the future TS port.
|
||
|
||
## Test strategy (BRIEF §9 + additions)
|
||
|
||
- **Adversarial corpus** — one payload per lexicon class incl. obfuscated + multi-language;
|
||
measure and report *recall*.
|
||
- **False-positive corpus** — content legitimately discussing injection (security docs,
|
||
changelogs); assert WARN-not-block default; hard-fail is an explicit opt-in.
|
||
- **Sanitizer invariant** — clean input returns byte-identical with an all-zero report.
|
||
- **Contract asserters** — a tool-carrying request and a credential-leaking stage env both
|
||
raise; the happy path passes.
|
||
- **Self-safety** — pathological/ReDoS-prone and oversize input return within a bound,
|
||
never hang. Scope, measured 2026-07-31: the *lexicon* path is covered by
|
||
`test_redos_pathological_subagent_input_returns_fast` (crafted against a known-bad
|
||
nested `.*?`). The `output` path is covered by
|
||
`test_crafted_redos_payload_stays_bounded` — 15 crafted payloads plus one through the
|
||
composed gate. Writing them found the defect they were meant to rule out: 19 quadratic
|
||
runs across 17 patterns in `output` (4), `active_content` (5 patterns / 7 runs) and the
|
||
lexicon JSON (8), worst case ~5.7 hours at the 1_000_000-char cap the gate accepts. Note the shape, since it is
|
||
*not* the textbook one: no nested quantifier is involved. A run in front of a required
|
||
literal, reachable from a short anchor, is enough — crafted input repeats the anchor,
|
||
never supplies the literal, and every start position rescans the tail. The earlier
|
||
"output blob is slower than ordinary prose (0.93x/0.96x)" measurement stands and was
|
||
never wrong; it simply measured throughput on a blob, which is a different question
|
||
from what a crafted payload asks.
|
||
- **Neutralize** — active-content output is defanged; clean output is byte-identical.
|
||
- **End-to-end showcase (the FINAL deliverable, built last).** One realistic
|
||
piece of ingested content that carries *many* vulnerabilities at once — visible
|
||
+ zero-width/BIDI/Unicode-tag stego carriers, homoglyph + whole-string-base64 +
|
||
rot13-hidden injection, HTML-obfuscated and cognitive-load-buried payloads,
|
||
secret/PII egress in the enriched output, and active-content exfil links — run
|
||
through a mock ingestion pipeline (sanitize → lexicon+entropy+decode-rescan →
|
||
output gate → disposition). Assert every planted vulnerability is caught and
|
||
disposition fails secure. Doubles as the README's worked example. Inspiration:
|
||
`llm-security/examples/{prompt-injection-showcase,poisoned-claude-md,lethal-trifecta-walkthrough,toxic-agent-demo}`.
|
||
- **No network in any test.**
|
||
|
||
## README "Honest limitations" (shipped as a control)
|
||
|
||
Concede plainly (the concession prevents false assurance, which is itself a control):
|
||
structural unsolvability at the text layer; adversarial-ML evasion survives normalization;
|
||
tokenizer mismatch; semantic/factual poisoning invisible to lexicon+entropy; latent/dormant
|
||
memory poisoning not judgeable at write-time; insider in-place edits; text-only (no
|
||
multimodal).
|
||
|
||
## Out-of-scope (documented boundary)
|
||
|
||
Embedding/vector-layer defenses (OWASP LLM08, downstream of persist); multimodal
|
||
steganography; query-time/runtime guardrails; semantic factuality verification. The
|
||
`contract` tool-less assertion is the write-time analogue of runtime least-privilege.
|
||
|
||
**Text-extraction boundary.** For pipelines that ingest arbitrary user uploads (an upload
|
||
inbox that ingests automatically), the library stays `text -> findings`: it does not parse
|
||
files (no `pypdf`/`python-docx`/archive deps in the core). The pipeline extracts text
|
||
first, then scans the extracted text + the enriched output with provenance tagged as the
|
||
high-untrust upload tier. Binary/multimodal carrier detection (OCR-embedded instructions in
|
||
images/PDFs, stego) is out of scope beyond the sanitizer's character-layer stripping and
|
||
the self-safety size/decompression guard — and must be conceded in the honest-limitations
|
||
section, because a high-untrust, unattended pipeline is exactly where assuming
|
||
uncovered-coverage is most dangerous.
|
||
|
||
## Threat-model anchors
|
||
|
||
OWASP LLM Top-10 2025: LLM01, LLM02, LLM04, LLM05, LLM06 (strongest coverage), LLM08
|
||
(boundary), LLM09, LLM10. Research anchors: PoisonedRAG (arXiv 2402.07867), guardrail
|
||
evasion (2504.11168), EchoLeak (CVE-2025-32711), RAGShield (2604.00387), CaMeL
|
||
(2503.18813), Dual-LLM (Willison), and the litellm supply-chain compromise (corroborates
|
||
the minimal-dependency thesis).
|
||
|
||
## v0.2+ stream sequencing (revised 2026-07-06)
|
||
|
||
v0.1.0 and v0.2.0 are tagged: the format-agnostic text core (modules 1–11,
|
||
`5397ba1` / released `df30c7b`) and the OKF adapter (stream 1, released
|
||
`542ac92`). The forward order was **re-sequenced on 2026-07-06** after a
|
||
ground-truth pass over the intended flagship consumer.
|
||
|
||
**Finding that drove the change.** The named flagship — `portfolio-optimiser`'s
|
||
"OKF-upload-inbox" — does not exist as a seam. Both optimiser siblings are frozen
|
||
at their release milestone, carry their *own* OKF layer (navigate + materialize
|
||
from trusted manifests), receive no *external* bundles, and take no dependency on
|
||
this guard; the operator's own registered future work for them does not include
|
||
it. Wiring an immature guard into two mature, spec-frozen repos would complicate
|
||
them for a consumer that is not asking for it. **Consumer integration is therefore
|
||
deferred until the guard is mature**; consumers are informed at the operator's
|
||
timing, not pushed.
|
||
|
||
Revised streams:
|
||
|
||
1. **OKF v0.2 hardening** — shipped (adapter + brief §8 tasks; T5b/B deferred to a
|
||
consumer that owns corpus storage).
|
||
2. **Mature the guard here, keeping it Node-port-friendly** — the near-term work.
|
||
The lexicon is already shared JSON (polyglot-ready); keep the `text -> findings`
|
||
surface clean and free of Python-only cleverness in the OKF layer, so stream 3
|
||
is a *translation*, not a redesign. The centrepiece is the **OKF inbox
|
||
showcase** (below): the guard demonstrating its own flagship use case
|
||
end-to-end, in-repo.
|
||
3. **Node/polyglot port** — the strategic enabler of painless integration (many
|
||
OKF consumers are Node/JS; the lexicon seed was `.mjs`). One polyglot repo over
|
||
the shared JSON lexicon; never split §13.3. Its API/lexicon contract should be
|
||
**scan-informed** (stream 4), not guessed.
|
||
4. **Pre-adaptation scan (operator-timed)** — at the operator's chosen point, scan
|
||
every repo and plugin that uses or plans Google OKF, then adapt the guard's
|
||
surface in advance so a later integration is painless. The scan is the input to
|
||
the "painless" guarantee: it grounds both the Node port's contract and any
|
||
consumer-specific seams before they are locked.
|
||
|
||
### Re-sequenced toward v1.0 (Fable cross-model review, 2026-07-09)
|
||
|
||
An independent cross-model review re-sequenced the forward path. Stream 2 is extended
|
||
with a set of review-injected hardening + coverage sessions (output-gate coverage,
|
||
OKF adapter hardening, egress decode-rescan, calibration-threshold consolidation,
|
||
docs/version-sync, and a novelty-claim verification) that land **before** a **v1.0
|
||
freeze**. The Node/TS port (stream 3) starts only **after** that freeze — porting a
|
||
frozen, scan-clean surface, never a moving target — over the shared JSON lexicon
|
||
(never split). Streams 4 + consumer integration are operator-timed "ambitious
|
||
extensions", not part of the v1.0 sequence.
|
||
|
||
The session-by-session plan and the review record live in `docs/` on the Forgejo
|
||
`open/` mirror alongside the other design docs — `docs/PLAN-v1.md` (the v1.0 + Node
|
||
session plan) and `docs/review-2026-07.md` (the review) — `open/` being the sole
|
||
sanctioned public surface (never GitHub).
|
||
|
||
### The OKF inbox showcase (next concrete build, TDD)
|
||
|
||
The flagship artifact we hand a consumer later — an in-repo end-to-end
|
||
demonstration of the mode-b receive/quarantine gate, mirroring
|
||
`tests/test_showcase.py` but for a *received external OKF bundle*. It composes the
|
||
public `okf` surface exactly as an "upload inbox" consumer would, so it doubles as
|
||
the README's OKF worked example. Every test is authored by us — the point is to
|
||
prove intent, not to coincidentally pass.
|
||
|
||
- **Composition** (`tests/test_okf_showcase.py`): an `_inbox(bundle)` helper
|
||
calling `okf.import_bundle(bundle, origin=Origin.EXTERNAL, channel=Channel.AUTOMATIC)`
|
||
and mapping the aggregate disposition — `WARN` → ADMIT, `QUARANTINE_REVIEW` →
|
||
HOLD, `FAIL_SECURE` → REJECT, any error → REJECT (fail-secure default).
|
||
- **One poisoned bundle** planting one attack per OKF surface at once, each with a
|
||
label proving it was caught/rejected:
|
||
- body injection (T1 scan) and frontmatter `description` injection (T1
|
||
whole-concept scan);
|
||
- a non-`https` `resource:` URL (T3 → FAIL_SECURE);
|
||
- a path-traversal concept key `../x.md` (T4 → FAIL_SECURE) and a reserved-name
|
||
shadow `index.md` / `log.md` (T4);
|
||
- a dangerous frontmatter value (anchor / alias / explicit tag, T2 → FAIL_SECURE);
|
||
- a dangerous-scheme cross-link `[x](javascript:…)` (T5a → `links.rejected`);
|
||
- a dangling cross-link to an absent concept (§7.2 dormant signal →
|
||
`links.dangling`);
|
||
- a carrier/obfuscation-hidden body injection (zero-width / homoglyph / rot13 /
|
||
whole-string base64) routed through `scan_concept`.
|
||
- **Assertions:** aggregate disposition is FAIL_SECURE; every planted OKF
|
||
vulnerability appears in the per-concept rejects / findings / link graph; a
|
||
**clean** bundle admits (WARN, no rejects, no dangling); `BundleResult.log()`
|
||
renders one line per concept with rejects marked. **Detach-proof:** neutering
|
||
`import_bundle` to always-admit makes the poison assertions fail.
|
||
- **Honest scope:** demonstrates the *structural + known-pattern* OKF surface only
|
||
— semantic/factual poisoning stays out (README honest-limits), consistent with
|
||
the core showcase.
|
||
|
||
#### Realistic upload formats — the two-stage inbox (extract → materialize → guard)
|
||
|
||
A human inbox does not receive tidy `{path: text}` dicts; it receives the files
|
||
people actually drop: `.txt`, `.md`, Word `.docx`, Excel `.xlsx`, PowerPoint
|
||
`.pptx`, `.pdf`, `.csv`, whole **folders**, and **`.zip`** archives. The showcase
|
||
therefore has two stages, honouring the locked text-extraction boundary (§
|
||
"Text-extraction boundary"): the guard core never grows a file parser.
|
||
|
||
1. **Extract & materialize (the inbox front-end).** Reads each dropped file, walks
|
||
folders, and *safely* unpacks archives; extracts text via format libraries
|
||
(`python-docx`, `openpyxl`, `python-pptx`; stdlib `zipfile`/`csv`); materializes
|
||
the result into an OKF bundle `{concept_path: text}` with provenance
|
||
(`origin=EXTERNAL`, source filename + type). **This stage owns the container and
|
||
format threats.** These libraries are **showcase/dev-scoped only** — never core
|
||
`dependencies` (which stays `[]`). Promotion to an optional `[extract]` extra is
|
||
a documented future option if a consumer wants turnkey extraction, not v1.
|
||
2. **Guard (`import_bundle`).** Scans every extracted concept + the OKF structural
|
||
gates → aggregate disposition. **This stage owns the text/structural threats.**
|
||
|
||
**One representative planted vector per format** (the point is where the payload
|
||
hides — the place a human does not look):
|
||
|
||
| Input | Hidden vector planted | Caught at |
|
||
|---|---|---|
|
||
| `.txt` | raw injection + carrier (zero-width / homoglyph / rot13 / base64) | guard scan |
|
||
| `.md` | OKF frontmatter attack (T2) + body injection (T1) | guard |
|
||
| `.docx` | injection in a **comment / hidden (vanish) run / core metadata property** | guard scan of extracted text |
|
||
| `.xlsx` | **formula injection** (`=cmd\|'…'`, `=HYPERLINK(…)`) + **hidden sheet / cell comment** | front-end + guard |
|
||
| `.pptx` | injection in **speaker notes / off-slide text box / image alt-text** | guard scan |
|
||
| `.pdf` | injection in extracted / **white-on-white** text | **conceded** — refused as an unsupported format, not extracted (see honest-scope below) |
|
||
| `.csv` | formula injection (`=`, `+`, `-`, `@` lead) | front-end + guard |
|
||
| **folder** | the OKF bundle-directory shape directly; one member path trips the path gate (T4) | guard |
|
||
| **`.zip`** | **zip-slip** entry `../../x.md` (maps straight onto OKFPathError, T4) + **zip-bomb** (bounded by the size cap / a safe-extract limit, LLM10) + symlink entry | front-end + guard |
|
||
|
||
**Assertions:** each planted vector is caught at the correct stage; the front-end
|
||
refuses zip-slip / zip-bomb / oversize fail-fast; the guard rejects
|
||
injection/carrier/frontmatter/resource/link/path; a **clean** file of every
|
||
*accepted* format admits (WARN). Detach-proof at **both** stages.
|
||
|
||
**Honest scope for uploads (must be conceded — README honest-limits).** What
|
||
survives text extraction is **out of scope**: VBA/macros (`.docm`/`.xlsm`/`.pptm`),
|
||
OLE / embedded objects, image-embedded instructions that need OCR, font/render
|
||
steganography, and encrypted/password-protected files. The guard scans *extracted
|
||
text*; binary-layer carriers need a separate scanner. **`.pdf` is conceded as a
|
||
format:** a top-level `.pdf` drop is refused as unsupported rather than extracted,
|
||
since a PDF parser + `reportlab` (fixtures only) is disproportionate for a dev-scoped
|
||
showcase and its OCR / font-render stego carriers are out of scope regardless. A
|
||
high-untrust, unattended
|
||
inbox is exactly where assuming uncovered-coverage is most dangerous (§4.7), so the
|
||
concession is itself a control.
|
||
|
||
**Deferred, unchanged:** T5b/B (the persisted cross-run link graph) waits for a
|
||
consumer that owns corpus storage; §7.2 dormant cross-run links remain a
|
||
documented residual until then.
|
||
|
||
**Splittable early win (optional):** text-only consumers can be wired at v0.1.0
|
||
today — for them the guard is already complete. Deferred with stream 2 by default;
|
||
pull forward only on explicit request.
|