ktg-plugin-marketplace/plugins/llm-security/examples/pre-compact-poisoning/expected-findings.md
Kjell Tore Guttormsen b6d912200e feat(llm-security): add pre-compact-poisoning example for PreCompact hook [skip-docs]
Runnable demonstration of hooks/scripts/pre-compact-scan.mjs (the
only PreCompact hook in the plugin) detecting both a CRITICAL
injection pattern and an AWS-shaped credential inside a synthetic
JSONL transcript, exercised across all three values of
LLM_SECURITY_PRECOMPACT_MODE plus a benign-transcript control case
in block mode that proves the gate is not a brick wall.

The transcript is generated at runtime in a per-invocation tempdir
under os.tmpdir() and the directory is removed in a finally block,
so the user's real ~/.claude/projects/.../transcripts/ are never
touched. The AWS-shaped key uses the same 'AK' + 'IA' + ...
fragmentation idiom as tests/e2e/attack-chain.test.mjs so this
source contains no literal credentials and pre-edit-secrets does
not block writes during development.

Nine independent assertions (9/9 must pass):
- block mode + poisoned: exit 2, decision=block JSON, reason text
  covers both injection and AWS labels (3 assertions)
- warn mode + poisoned: exit 0, systemMessage JSON, no decision
  field (2 assertions)
- off mode + poisoned: exit 0, no JSON on stdout (2 assertions)
- block mode + benign: exit 0, no decision=block JSON (2 assertions)

OWASP / framework mapping: LLM01, LLM02, ASI01, AT-1, AT-3.

Docs updated: plugin README "Other runnable examples", plugin
CLAUDE.md "Examples" tabellen, CHANGELOG [Unreleased] Added.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-05 15:23:10 +02:00

88 lines
3.4 KiB
Markdown

# Expected findings — pre-compact-poisoning
This is the testable contract enforced by
`run-pre-compact-poisoning.mjs`. Nine independent assertions
across four scenarios. Any drift = hook regression or fixture rot.
## Required assertions (9 / 9 must pass)
### Scenario A — block mode + poisoned transcript
The poisoned transcript embeds two distinct triggers:
- An "ignore all previous instructions" phrase inside a synthetic
`tool_result` block (matches `CRITICAL_PATTERNS` in
`injection-patterns.mjs`).
- An AWS-shaped key built at runtime via string concatenation
(matches `SECRET_PATTERNS` regex `/AKIA[0-9A-Z]{16}/`).
A.1 Hook exits with code `2`.
A.2 Stdout is JSON `{ "decision": "block", "reason": "..." }`.
A.3 The `reason` string mentions both:
- an injection label (`/ignore previous|override/i`), AND
- the AWS key label (`/AWS Access Key/i`).
If A.3 fails, either the injection-patterns regex set or the
SECRET_PATTERNS table changed in a way that dropped one of these
labels.
### Scenario B — warn mode + poisoned transcript
B.1 Hook exits with code `0` (advisory, not block).
B.2 Stdout is JSON `{ "systemMessage": "..." }` with no
`decision` field. The `systemMessage` summary is the same as
the block-mode `reason` text.
### Scenario C — off mode + poisoned transcript
C.1 Hook exits with code `0`.
C.2 Stdout is empty (no JSON). The `off` branch returns at the
top of the script before reading the transcript at all,
which is the documented "fully disabled" semantic.
### Scenario D — block mode + benign transcript
This is the brick-wall control: it proves the hook does not
reflexively block all compactions.
D.1 Hook exits with code `0`.
D.2 Stdout has no `decision: "block"` JSON. (Either no JSON or
a non-block payload — the assertion only fails on a literal
block decision, which would indicate a false positive.)
## Total finding shape (block mode)
```
pre-compact-scan (auto): 3 finding(s) in transcript. Compaction
may preserve poisoned content in condensed form. Top: override:
ignore previous instructions, indirect: instruction addressed
to AI/assistant, AWS Access Key ID.
```
The "3 finding(s)" count covers:
1. CRITICAL — `override: ignore previous instructions`
2. MEDIUM — `indirect: instruction addressed to AI/assistant`
(the synthetic tool-result text frames the injection as a
"Note to assistant", which trips the indirect-address pattern)
3. SECRET — `AWS Access Key ID`
If `injection-patterns.mjs` adds new MEDIUM rules that match the
fixture text, the count and `Top: ...` ordering may shift. The
contract only asserts the *labels* in the reason string, not the
finding count or order — that flexibility is intentional.
## Out of scope (intentionally)
- The other secret labels in `SECRET_PATTERNS`
(GitHub / npm / PEM / bearer / generic). Demonstrating those
would require either growing the fixture or building each at
runtime; the AWS key alone is sufficient to prove the
credential-finding path activates.
- The 512 KB tail cap (`LLM_SECURITY_PRECOMPACT_MAX_BYTES`) — not
exercised because the synthetic transcript is small.
- The leetspeak / homoglyph / multi-language MEDIUM patterns —
exercised by `examples/prompt-injection-showcase/`.
- The `compaction_trigger` legacy field name (the hook reads
both `trigger` and `compaction_trigger`) — only `trigger` is
exercised here.