From f4e89d2885979a67a44c8efe7b28c5b7d1606749 Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Wed, 15 Jul 2026 07:11:29 +0200 Subject: [PATCH] feat(egress): decode-rescan feeds base64 plaintext to secret-egress (review MINOR) Output gate step 3 now runs scan_secret_egress over every decoded base64 blob's plaintext, not only scan_lexicon. A base64-wrapped credential that formerly vanished (decode fed the lexicon, which has no secret patterns) now surfaces as decoded:egress:* carrying the blob offset. Evidence stays length-only, so the decoded finding never leaks the secret value. Hex-wrapped secrets remain a documented honest-limit (entropy exposes decoded plaintext for base64 only). README honest-limits + CLAUDE.md Kontekst updated; 3 tests added (347 passed, was 344). --- CLAUDE.md | 4 ++++ README.md | 6 ++++++ src/llm_ingestion_guard/output.py | 27 +++++++++++++++--------- tests/test_output.py | 35 +++++++++++++++++++++++++++++++ 4 files changed, 62 insertions(+), 10 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 74f5e80..1bd34ff 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,6 +16,10 @@ topp-nivå wiring, showcase + korpus), inkl. OKF-adapter og aktivt-innhold- detektor (EchoLeak-klassen) i output-gaten. Mode-b `import_bundle` skanner reserverte strukturfiler (`index.md`/`log.md`) i mottatte bundles i stedet for å path-avvise dem; upload-front-end beholder shadow-reject (`allow_reserved=False`). +Output-gatens decode-and-rescan mater dekodet base64-klartekst gjennom BÅDE lexicon +og secret-egress (LLM02), så en base64-innpakket credential fanges som +`decoded:egress:*` i stedet for å forsvinne; hex-innpakket er en dokumentert +restgap (entropy eksponerer kun base64-klartekst). Start med `docs/BRIEF.md` for design, `README.md` for bruk, `docs/PLAN.md` for byggerekkefølgen. diff --git a/README.md b/README.md index cabb543..3f877e3 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,12 @@ that a green scan means safe content: collapses to one finding at its first location. This keeps reports readable, but a caller that counts occurrences or needs every offset of a repeated pattern sees only the first: a deliberate readability tradeoff, not full positional coverage. +- **Secret egress: base64-wrapped is caught, hex-wrapped is not.** The output gate + decodes base64 blobs and re-scans the plaintext through the credential/egress set, + so a base64-*wrapped* secret surfaces as `decoded:egress:*` rather than vanishing. + A *hex*-wrapped secret does not: `entropy` exposes decoded plaintext for base64 + only, so hex (and other encodings, or nested wraps) is a deliberate boundary, not + a silent miss — decode the transport layer first if you need it scanned. ## Out-of-scope (documented boundary) diff --git a/src/llm_ingestion_guard/output.py b/src/llm_ingestion_guard/output.py index 9f4de2d..28981b1 100644 --- a/src/llm_ingestion_guard/output.py +++ b/src/llm_ingestion_guard/output.py @@ -15,12 +15,13 @@ input-side scanners do not cover: 2. :func:`~llm_ingestion_guard.entropy.scan_entropy` over the output — encoded / high-entropy carrier blobs. 3. **Decode-and-rescan** — every base64 blob ``entropy`` decoded to printable - text is fed back through ``scan_lexicon``. This is what turns "a blob is - present" into "an injection is hidden *inside* this blob". Findings from the - decoded plaintext are re-labelled ``decoded: