test(av-surface): probe (e) — payload literals in session-loaded files, red

New probe (e) over agents/**, commands/** and hooks/** — what a Claude Code
session loads at the user's end, where a quarantine breaks the installed
plugin, not just a clone. It fails on a contiguous literal matching the SIG
reverse-shell/webshell/miner rules, a download piped into a shell, or a
base64 decode piped into a shell. Raw text, comments included, per line, so
every hit names file:line.

knowledge/** is measured by the same rule and reported in the diagnostic as
its own number, not gated in S3.

Measured red before any change (v8.1.0 S3, 2026-09-22):
e = 8 hits in 6 of 36 files; knowledge = 17 hits of 22 files.
No SIG rule hit anywhere in the four directories; validated against built
known positives (all three families match).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-22 14:10:33 +02:00
commit e6a7aec971
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q

View file

@ -49,6 +49,17 @@
// payload built at test time from fragments ('ev' + 'al') or rot13 does
// not match here — which is the point. Added in S1 (2026-09-22) because
// (a) keeps .mjs out of its denominator.
// (e) a file under agents/**, commands/** or hooks/** — what a Claude Code
// session loads at the user's end, so a quarantine there breaks the
// installed plugin, not just a clone — whose RAW text holds a contiguous
// literal matching one of the same SIG rules, a pipe into a shell
// (curl/wget ... | sh|bash) or a base64 decode piped into a shell. Raw
// text, comments included, one line at a time, every file type: a hook
// script's comment sits on disk like any other byte. knowledge/** is
// measured by the same rule and REPORTED as its own number in the
// diagnostic, not gated (S3, 2026-09-22): part of it is a scanner's or
// the attack simulator's input, and S3 records which literal stays and
// why. Added in S3 (2026-09-22).
//
// This test was written RED on purpose (order S0, 2026-09-22): it is the
// failing test for sessions S1-S3 of the v8.1.0 plan. It is expected to fail
@ -81,6 +92,11 @@ const COMMAND_SHAPE = /^\s*(?:curl|wget|bash|sh|python[0-9.]*|eval|nc)\b|\|\s*(?
const CARRIER = /[\u{E0000}-\u{E007F}\u200B-\u200D\u2060\uFEFF\u202A-\u202E\u2066-\u2069]/u;
const SESSION_LOADED = /^(?:agents|commands|hooks)\//;
const KNOWLEDGE = /^knowledge\//;
const PIPE_TO_SHELL = /\b(?:curl|wget)\b[^\n|]*\|\s*(?:sudo\s+)?(?:ba|z)?sh\b/i;
const BASE64_PIPE_TO_SHELL = /\bbase64\s+(?:-d|-D|--decode)\b[^\n|]*\|\s*(?:sudo\s+)?(?:ba|z)?sh\b/i;
const PAYLOAD_TREES = [
'tests/fixtures/signature-scan/poisoned/',
'tests/fixtures/memory-scan/poisoned-project/',
@ -107,8 +123,34 @@ function measureAvSurface() {
const b = [];
const c = [];
const a2 = [];
const e = [];
const eKnowledge = [];
let testSources = 0;
let textFiles = 0;
let sessionLoaded = 0;
let knowledgeFiles = 0;
// (e): every hit is `rel:line [rule]`, so the report names the exact literal.
const loadedSurfaceHits = (rel, text) => {
const hits = [];
text.split('\n').forEach((line, i) => {
const ids = rules.filter(r => r.re.test(line)).map(r => r.id);
if (PIPE_TO_SHELL.test(line)) ids.push('pipe-to-shell');
if (BASE64_PIPE_TO_SHELL.test(line)) ids.push('base64-pipe-to-shell');
if (ids.length > 0) hits.push(`${rel}:${i + 1} [${ids.join(', ')}]`);
});
return hits;
};
for (const rel of files) {
if (SESSION_LOADED.test(rel)) {
sessionLoaded++;
e.push(...loadedSurfaceHits(rel, readFileSync(resolve(ROOT, rel), 'utf8')));
} else if (KNOWLEDGE.test(rel)) {
knowledgeFiles++;
eKnowledge.push(...loadedSurfaceHits(rel, readFileSync(resolve(ROOT, rel), 'utf8')));
}
}
for (const rel of files) {
const buf = readFileSync(resolve(ROOT, rel));
@ -159,6 +201,10 @@ function measureAvSurface() {
bFiles: new Set(b.map(x => x.split(' :: ')[0])).size,
c,
d,
e,
eKnowledge,
sessionLoaded,
knowledgeFiles,
};
}
@ -192,4 +238,11 @@ describe('av-surface: tracked tree carries no AV-triggering payloads', () => {
t.diagnostic(`d=${m.d.length}`);
assert.deepEqual(lines, [], report('payload trees', lines));
});
it(`(e) no payload literal in what a session loads (of ${m.sessionLoaded} files in agents/commands/hooks)`, (t) => {
t.diagnostic(`e=${m.e.length}`);
// Reported, not gated: see the header.
t.diagnostic(report(`knowledge (of ${m.knowledgeFiles} files, not gated)`, m.eKnowledge));
assert.deepEqual(m.e, [], report('payload literals in session-loaded files', m.e));
});
});