Three new self-contained, runnable threat demonstrations under
examples/, continuing the batch started in 583a78c. Each example
has README.md + run-*.mjs + expected-findings.md and uses
state-isolation discipline so the user's real cache/state files
are never polluted.
- examples/supply-chain-attack/ — two-layer demonstration:
pre-install-supply-chain (PreToolUse) blocks compromised
event-stream version 3.3.6 and emits a scope-hop advisory for
the @evilcorp scope; dep-auditor (DEP scanner, offline) flags
5 typosquat dependencies plus a curl-piped install-script
vector in the fixture package.json. Maps to LLM03/LLM05/ASI04.
- examples/poisoned-claude-md/ — all 6 memory-poisoning detectors
fire on a deliberately poisoned CLAUDE.md plus a fixture
agent file under .claude/agents (E15/v7.2.0 surface):
detectInjection, detectShellCommands, detectSuspiciousUrls,
detectCredentialPaths, detectPermissionExpansion,
detectEncodedPayloads. No agent runtime needed — scanner
imported directly. Maps to LLM01/LLM06/ASI04.
- examples/bash-evasion-gallery/ — one disguised variant per
T1 through T9 evasion technique fed through pre-bash-destructive,
verified BLOCK after bash-normalize strips the evasion. T8
base64-pipe-shell uses its own BLOCK_RULE. The canonical
destructive form uses a path token rather than the bare slash
(regex word-boundary requires it). Source-string fragmentation
pattern reused from the e2e attack-chain test. Maps to
LLM06/ASI01/LLM01.
Plugin README "Other runnable examples" section + plugin
CLAUDE.md "Examples" table + CHANGELOG Unreleased/Added
all updated. Marketplace root README unchanged
([skip-docs] for marketplace-level gate — plugin's outward
coverage is unchanged, only demonstrations were added).
126 lines
5.6 KiB
JavaScript
126 lines
5.6 KiB
JavaScript
#!/usr/bin/env node
|
|
// run-memory-poisoning.mjs — Memory-poisoning scanner walkthrough
|
|
// Runs scanners/memory-poisoning-scanner.mjs against a deliberately
|
|
// poisoned CLAUDE.md + .claude/agents/health-checker.md fixture and
|
|
// verifies all six detector categories report at least one finding.
|
|
//
|
|
// Categories (per memory-poisoning-scanner.mjs):
|
|
// 1. detectInjection — prompt-injection patterns
|
|
// 2. detectShellCommands — curl/wget/bash/eval in memory files
|
|
// 3. detectSuspiciousUrls — webhook.site, requestbin, etc.
|
|
// 4. detectCredentialPaths — ~/.ssh/, ~/.aws/, .env, kubeconfig
|
|
// 5. detectPermissionExpansion — allowed-tools, bypassPermissions
|
|
// 6. detectEncodedPayloads — base64 blobs that decode to commands
|
|
//
|
|
// Usage:
|
|
// cd plugins/llm-security
|
|
// node examples/poisoned-claude-md/run-memory-poisoning.mjs
|
|
// node examples/poisoned-claude-md/run-memory-poisoning.mjs --verbose
|
|
|
|
import { resolve, dirname } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const PLUGIN_ROOT = resolve(__dirname, '../..');
|
|
const FIXTURE = resolve(__dirname, 'fixture');
|
|
const VERBOSE = process.argv.includes('--verbose');
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Imports — discoveryFiles + memory-poisoning scanner
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const { discoverFiles } = await import(resolve(PLUGIN_ROOT, 'scanners/lib/file-discovery.mjs'));
|
|
const { scan } = await import(resolve(PLUGIN_ROOT, 'scanners/memory-poisoning-scanner.mjs'));
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Run
|
|
// ---------------------------------------------------------------------------
|
|
|
|
console.log('MEMORY-POISONING SCANNER WALKTHROUGH');
|
|
console.log('====================================\n');
|
|
console.log(`Fixture: ${FIXTURE}`);
|
|
console.log('Files in scope:');
|
|
console.log(' - CLAUDE.md');
|
|
console.log(' - .claude/agents/health-checker.md (E15: agent files are memory surface)\n');
|
|
|
|
const discovery = await discoverFiles(FIXTURE);
|
|
const result = await scan(FIXTURE, discovery);
|
|
const findings = result.findings || [];
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Categorize findings
|
|
// ---------------------------------------------------------------------------
|
|
//
|
|
// memory-poisoning-scanner doesn't tag findings by detector — we infer the
|
|
// category from the title/message text. The contract is: at least one
|
|
// finding from each category.
|
|
|
|
const buckets = {
|
|
injection: [],
|
|
shellCommand: [],
|
|
suspiciousUrl: [],
|
|
credentialPath: [],
|
|
permissionExpansion: [],
|
|
encodedPayload: [],
|
|
};
|
|
|
|
// Order matters: more specific patterns first.
|
|
for (const f of findings) {
|
|
const t = (f.title || '') + ' ' + (f.message || '');
|
|
if (/permission\s+expansion|allowed-tools|bypassPermissions|dangerously|skip-permissions/i.test(t)) buckets.permissionExpansion.push(f);
|
|
else if (/credential\s+path|\.ssh|\.aws|kubeconfig|wallet|service[\s_-]account|sensitive\s+path|credential[s]?\s+reference/i.test(t)) buckets.credentialPath.push(f);
|
|
else if (/suspicious\s+(?:url|domain|exfiltration)|webhook\.site|requestbin|exfiltration\s+(?:url|domain)/i.test(t)) buckets.suspiciousUrl.push(f);
|
|
else if (/base64|encoded\s+payload|payload\s+\(encoded\)/i.test(t)) buckets.encodedPayload.push(f);
|
|
else if (/shell\s+command|shell-command|curl|wget|eval|chmod|npm\s+install|pip\s+install/i.test(t)) buckets.shellCommand.push(f);
|
|
else if (/injection|prompt|spoofed|hidden\s+instruction|override|ignore\s+previous/i.test(t)) buckets.injection.push(f);
|
|
}
|
|
|
|
const expectations = [
|
|
['injection', 'detectInjection — prompt-injection / hidden directive patterns'],
|
|
['shellCommand', 'detectShellCommands — curl/wget/bash/eval/chmod'],
|
|
['suspiciousUrl', 'detectSuspiciousUrls — webhook.site / requestbin / etc'],
|
|
['credentialPath', 'detectCredentialPaths — ~/.ssh/, ~/.aws/, .env, kubeconfig, wallet.dat'],
|
|
['permissionExpansion', 'detectPermissionExpansion — allowed-tools / bypassPermissions / skip-permissions'],
|
|
['encodedPayload', 'detectEncodedPayloads — base64 blob that decodes to a command'],
|
|
];
|
|
|
|
let pass = 0;
|
|
let fail = 0;
|
|
|
|
for (const [key, label] of expectations) {
|
|
const ok = buckets[key].length > 0;
|
|
if (ok) pass++; else fail++;
|
|
console.log(`[${ok ? 'PASS' : 'FAIL'}] ${label}`);
|
|
console.log(` findings: ${buckets[key].length}`);
|
|
for (const f of buckets[key].slice(0, 2)) {
|
|
const sev = (f.severity || '').toUpperCase().padEnd(8);
|
|
const title = (f.title || f.message || '').slice(0, 90);
|
|
console.log(` ${sev} ${title}`);
|
|
}
|
|
console.log();
|
|
}
|
|
|
|
console.log(`Total memory-poisoning findings: ${findings.length}`);
|
|
console.log(`Files scanned: ${result.filesScanned ?? '?'}`);
|
|
console.log(`Scanner status: ${result.status}`);
|
|
|
|
if (VERBOSE) {
|
|
console.log('\nFull findings list:');
|
|
for (const f of findings) {
|
|
const sev = (f.severity || '').toUpperCase().padEnd(8);
|
|
console.log(` ${sev} [${f.file || '?'}:${f.line || '?'}] ${(f.title || f.message || '').slice(0, 110)}`);
|
|
}
|
|
}
|
|
|
|
console.log('\n---');
|
|
console.log(`Result: ${pass} pass, ${fail} fail`);
|
|
|
|
if (fail > 0) {
|
|
console.log('\nFAILURE — at least one detector category had zero findings.');
|
|
console.log('Inspect verbose output (--verbose) to see what was actually returned.');
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log('\nSUCCESS — all 6 detector categories caught the planted signals.');
|
|
console.log('Read examples/poisoned-claude-md/README.md for category mapping.');
|
|
process.exit(0);
|