1
0
Fork 0
llm-ingestion-pipeline-secu.../README.md
Kjell Tore Guttormsen 3dda1f68fa docs(readme): add concrete 'What it protects against' catalogue; split limitations
Answers the gap that the README said what it does NOT stop (a long limitations
section) but never plainly listed what it DOES. Add a 'What it protects against'
section high up: attack classes grouped by OWASP anchor (LLM01 injection + 83
lexicon classes + carriers, LLM02 egress, LLM05 EchoLeak, LLM06 agency, LLM10
fail-secure, OKF T1-T6, container front-end), each driven by a live coverage-matrix
payload. Move the full honest-limitations list to docs/LIMITATIONS.md; README keeps
a high-impact summary + link. Net: protection and limits read in balance, 261 -> 216
lines. Coverage 126/126 and 522 tests unchanged; every class listed is real.
2026-07-15 19:09:04 +02:00

216 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# llm-ingestion-guard
![Version](https://img.shields.io/badge/version-0.2.0-blue)
![Status](https://img.shields.io/badge/status-alpha-orange)
![Python](https://img.shields.io/badge/python-3.10%2B-purple)
![Tests](https://img.shields.io/badge/tests-522_passing-green)
![License](https://img.shields.io/badge/license-MIT-lightgrey)
**Write-time ingestion is the trust boundary that query-time guardrails
structurally cannot see.** When untrusted content passes through an LLM
enrichment/summarization/extraction step into a *persisted* artifact — a RAG
corpus, a knowledge base, an LLM wiki — the poisoned result is read *later* by a
downstream agent as **trusted context**. That agent's guardrail never sees where
the content came from. The only place the provenance still exists is the write.
This library packages that write-time contract — sanitize → fence → tool-less
quarantined transform → per-stage capability isolation → scan-before-commit →
fail-secure — as composable, stdlib-first, framework-agnostic code. It is the
write-time **sibling** of query-time tools (LLM Guard, NeMo Guardrails, Rebuff,
Vigil), not a competitor: those harden material as it enters the model; this
hardens it as it is committed for a *later* reader. Where existing OSS tooling is
mostly single-stage *detectors* — a risk verdict, with quarantine, capability
isolation, scan-before-persist, and fail-secure left to the integrator — this
packages the full contract as code. (Neighbours surveyed in
[`docs/BRIEF.md`](docs/BRIEF.md) §11.)
**Why an LLM wiki (e.g. Google OKF) needs this specifically.** OKF and
second-brain formats have no schema registry, no central authority, and no
signing — a bundle's claimed origin is not verifiable at the format level. So
*your ingestion pipeline is the trust boundary*: provenance must be stamped by you
at write time, never assumed from the format. Any pipeline ingesting external data
into an agent-read store has this shape; an OKF wiki is its canonical form — which
is why the guard ships a first-class OKF adapter (below).
**Status:** `v0.2`, alpha. The stdlib-only core — its detector, contract, and
OKF-adapter modules plus the top-level wiring — is built and tested, exercised by
an end-to-end showcase and adversarial + false-positive corpora. The public API
may still change. There are real limitations, stated plainly below; read them.
## Install
```bash
pip install llm-ingestion-guard # stdlib-only core, zero dependencies
```
Optional ML/judge detectors live behind extras (`[ml]`, `[judge]`) and are not
required — the core is deterministic and dependency-free.
## Quickstart — the two bookends
The library never makes the model call itself. It gives you the two library-side
halves around your own **tool-less** transform:
```python
from llm_ingestion_guard import (
prepare_input, screen_output, Disposition, PRESET_USER_UPLOAD,
)
prepared = prepare_input(untrusted_content) # sanitize + fence
enriched = your_model(prepared.fenced) # tool-less — YOUR call
decision = screen_output(enriched, PRESET_USER_UPLOAD) # scan + dispose
if decision.disposition is Disposition.FAIL_SECURE:
alert(gate_code=decision.reasons) # minimal payload, no content
raise SystemExit # halt — never persist
```
`screen_output` fails **closed**: if the scanner itself errors on crafted input,
the disposition is `FAIL_SECURE`, never a silent persist. Pass
`transform_failed=True` when your model call raised or fell back — a scan hit
together with a transform failure is treated as a probable forced-fallback attack
and halts regardless of trust tier.
Every primitive is also exported for pipelines that compose the checklist
themselves — `sanitize`, `scan_lexicon`, `scan_entropy`, `scan_output`,
`scan_active_content`, `neutralize`, the `decide` / `guard` disposition
machinery, and the contract asserters `assert_tool_less` /
`assert_credential_allowlist` / `scoped_env`. See
[the end-to-end showcase](tests/test_showcase.py) for a full worked pipeline.
## OKF / LLM-wiki support (shipped)
For a bundle-shaped store, the `okf` submodule sits **on top of** the
format-agnostic core: it knows OKF structure (frontmatter, paths, links,
`resource`, bundles) and feeds scannable regions into the same `sanitize` /
`scan_output` / disposition machinery — no YAML/format awareness leaks into the
core. Two ingestion modes:
- **(a) Own enrichment output** — your agent writes concepts. Run the two bookends
above per concept before commit.
- **(b) Received external bundle** — you merge a whole third-party OKF bundle.
`import_bundle` iterates concept-by-concept and runs the full per-concept gate:
```python
from llm_ingestion_guard.okf import import_bundle, Origin, Channel
# bundle: {concept_path -> raw document text}, e.g. {"tables/users.md": "---\n..."}
result = import_bundle(bundle, origin=Origin.EXTERNAL, channel=Channel.AUTOMATIC)
for c in result.concepts:
if c.error: # hard reject: bad path, unsafe frontmatter, non-https resource
skip(c.path) # disposition is FAIL_SECURE; do not merge this concept
# result.disposition = most-severe across concepts
# result.links = the cross-link graph (dangling/rejected/resolved edges)
# result.log() = the log.md body (one provenance-stamped line per concept)
```
Per-concept gates: **path / reserved-name** (rejects `..` traversal and reserved
`index.md`/`log.md` shadowing); **frontmatter parse-safety** — a strict,
reject-by-default loader that refuses anchors, aliases, and explicit tags *by
construction*, so a billion-laughs alias expansion or a `!!python/object` coercion
cannot occur (it is deliberately **not** a general YAML engine, whose own features
are the attack surface); **`resource` https-allowlist** (hard-rejects
`data:`/`javascript:`/`file:` before commit — a reject-gate, not defang);
**whole-concept scan** (frontmatter *values* + body through `scan_output`);
**cross-link graph** (surfaces dangling targets, the dormant-injection signal, and
rejects bundle-escaping links); **provenance stamping** (`Origin` × `Channel`
tier + disposition, emitted to `log.md`).
## What it protects against
Concrete attack classes, grouped by OWASP LLM Top-10 (2025) anchor. Every row is
driven by a **live payload** in the coverage matrix — run it to watch all 126 pass
in your own environment:
```bash
python -m llm_ingestion_guard.coverage # 126/126 classes; exit 0 = all as documented
```
| Anchor | Attack classes it stops (representative) |
|---|---|
| **LLM01 · prompt injection** | 83 instruction-override lexicon classes (ignore / forget / disregard / suspend-constraints, role-play, jailbreak, …); hidden carriers — zero-width stego, BIDI override, Unicode-tag stego, HTML-comment, `data:` URI — stripped on input **and** re-checked at the persist gate; high-entropy base64/hex blobs; an injection hidden inside a base64 blob (decoded, then re-scanned) |
| **LLM02 · sensitive-info disclosure** | Secret egress in output — AWS keys, tokens, private keys, the credential set; a base64-*wrapped* secret (decoded → `decoded:egress:*`) |
| **LLM05 · improper output handling** | Zero-click exfil carriers (EchoLeak, CVE-2025-32711) — markdown-image auto-fetch, inline / reference / autolink links, raw active HTML, standalone `data:` URIs; non-`https` `resource` URLs |
| **LLM06 · excessive agency** | A tool surface on the quarantined transform; credential use beyond the per-stage allowlist; capability isolation (`scoped_env`) |
| **LLM10 · fail-secure** | Forced-fallback attack (scan hit + transform failure → halt); compound weak-signal escalation; an un-scannable artifact fails **closed** — never a silent persist |
| **OKF structure (T1T6)** | Body + frontmatter-value injection; YAML anchor / merge / nested / block DoS (parse-safety); `resource` allowlist; path traversal / reserved-name shadow; cross-link graph; provenance stamping |
| **Container / upload layer** | CSV/XLSX formula-injection (lead `= + - @`); zip-slip path escape; zip-bomb size cap; symlink refusal *(upload front-end — see the showcase)* |
The manifest ([`coverage.py`](src/llm_ingestion_guard/coverage.py)) is the single
source of truth for
[`tests/test_coverage_matrix.py`](tests/test_coverage_matrix.py), which asserts
total recall over every class, that every documented gap still holds (a closed gap
fails the test), and that every lexicon pattern has a case — so the matrix cannot
fall behind the code. The four classes it deliberately does **not** stop are called
out in [Honest limitations](#honest-limitations).
## The reusable contract (adopt-this checklist)
The actual product is this checklist, encoded as code you wire in order:
1. **Sanitize before fence.** Strip carrier classes (zero-width, BIDI,
Unicode-tag, HTML comment, `data:`) from untrusted input first.
2. **Fence untrusted input.** Spotlight-mark it in a randomized per-call
delimiter; strip attacker fence markers from the payload.
3. **Tool-less transform.** Call the model with zero tools. A successful
injection then has nothing to act with.
4. **Per-stage capability isolation.** The enrichment stage holds only the model
key; the publish stage holds only the publish credential; no stage holds both.
5. **Treat output as data.** Parse to a frozen schema; reject on structural
violation. The output never reaches a shell, git, or a filesystem path.
6. **Scan output before persist.** Run the lexicon + entropy + active-content
scan over the emitted text. Verbatim-carried payloads, model-emitted
instructions, and zero-click exfil carriers (EchoLeak-class markdown
images/links, raw active HTML, `data:` URIs) are caught here; `neutralize`
additionally defangs them, opt-in.
7. **Fail-secure on compound signals.** Injection hit + transform failure = halt
+ alert, never a silent verbatim commit.
8. **Minimal alert payloads.** Alert with a gate code + run ID, never content.
Steps 1-2 are `prepare_input`; steps 6-7 are `screen_output`; steps 3-5 are
yours; the contract asserters harden step 3-4.
## Honest limitations
Conceding these plainly is itself a control — it prevents the false assurance that
a green scan means safe content. The highest-impact items:
- **The contract carries the security, not the lexicon.** Pattern detection is
bypassable in isolation (character-injection, novel phrasings); the tool-less
transform + capability isolation + fail-secure are the wall.
- **Semantic / factual poisoning is invisible.** A false claim in clean prose
carries no suspicious token — the highest-impact gap for a wiki. Needs a `[judge]`
plugged into the `grounding` seam.
- **Text-only, extracted-text-only.** The core parses no files; OCR-embedded
instructions, macros, and multimodal stego are out of scope. `.pdf` is refused as
unsupported, not half-scanned.
- **A lone HIGH in *trusted* prose disposes to WARN**, and **insider in-place
edits** are outside the untrusted-content threat model — run genuinely untrusted
sources as untrusted.
- **Four documented gaps** the coverage matrix keeps honest: hex-wrapped secret
egress, semantic poisoning, trusted-prose lone-HIGH, lexicon dedup (`count=1`).
**Full list — 15 items, each with the mechanism, plus the out-of-scope boundary:**
[`docs/LIMITATIONS.md`](docs/LIMITATIONS.md).
## Out-of-scope (documented boundary)
Embedding/vector-layer defenses (OWASP LLM08, downstream of persist); multimodal
steganography; query-time / runtime guardrails; semantic factuality verification.
## Design & threat model
- [Design brief](docs/BRIEF.md) — what this repo contains and why.
- [Build plan](docs/PLAN.md) — module build order and the reuse map.
- [Adoption brief](docs/ADOPTION-BRIEF.md) — wiring the guard into an OKF
second-brain / LLM wiki, and a checklist for *when* to include it.
The contract is extracted from a working reference implementation (the
`claude-code-llm-wiki` Stage B enrichment pipeline). Threat-model anchors: OWASP
LLM Top-10 2025 (LLM01/02/04/05/06 strongest, LLM08 boundary, LLM09/10),
PoisonedRAG, guardrail-evasion (arXiv 2504.11168), EchoLeak (CVE-2025-32711).
## License
MIT — see [LICENSE](LICENSE).