#!/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);