#!/usr/bin/env node // run-supply-chain.mjs — Supply chain attack demonstration // Two stages: // // Stage A — pre-install-supply-chain (PreToolUse hook): // Feeds three Bash commands to the hook and verifies exit codes: // - "npm install event-stream@3.3.6" → exit 2 (compromised version) // - "npm install @evilcorp/lodash" → exit 0 (advisory only — scope-hop) // - "npm install lodash" → exit 0 (clean) // // Stage B — dep-auditor (offline scanner): // Imports the scanner directly and runs it against fixture/, where // package.json has 4 typosquat dependencies and a curl-piped // postinstall script. Verifies the typosquat + install-script // findings appear. // // No network calls. No real install. The fixture is never executed — // only its declarative package.json is parsed. // // Usage: // cd plugins/llm-security // node examples/supply-chain-attack/run-supply-chain.mjs // node examples/supply-chain-attack/run-supply-chain.mjs --verbose import { execFile } from 'node:child_process'; import { resolve, dirname, join } 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 HOOK = resolve(PLUGIN_ROOT, 'hooks/scripts/pre-install-supply-chain.mjs'); const VERBOSE = process.argv.includes('--verbose'); // --------------------------------------------------------------------------- // Stage A — hook // --------------------------------------------------------------------------- function runHook(command) { return new Promise((res) => { const child = execFile( 'node', [HOOK], { timeout: 10_000 }, (_err, stdout, stderr) => { res({ code: child.exitCode ?? 1, stdout: stdout || '', stderr: stderr || '' }); }, ); child.stdin.end(JSON.stringify({ tool_name: 'Bash', tool_input: { command }, })); }); } const HOOK_CASES = [ { label: 'compromised version (event-stream@3.3.6)', command: 'npm install event-stream@3.3.6', expectExit: 2, expectMatch: /COMPROMISED|known supply chain attack/i, }, { label: 'scope-hopping (@evilcorp/lodash)', command: 'npm install @evilcorp/lodash', // Scope-hop is advisory: hook prints to stderr but does not block. expectExit: 0, expectMatch: /scope|hopping/i, }, { label: 'clean install (lodash)', command: 'npm install lodash', expectExit: 0, expectMatch: null, }, ]; // --------------------------------------------------------------------------- // Stage B — dep-auditor (direct import) // --------------------------------------------------------------------------- async function runDepAuditor() { // Import lazily so the script remains usable even if dep-auditor's deps shift. const { scan } = await import(resolve(PLUGIN_ROOT, 'scanners/dep-auditor.mjs')); return scan(FIXTURE, null); } // --------------------------------------------------------------------------- // Main // --------------------------------------------------------------------------- let pass = 0; let fail = 0; console.log('SUPPLY CHAIN ATTACK WALKTHROUGH'); console.log('================================\n'); console.log('STAGE A — pre-install-supply-chain (PreToolUse hook)'); console.log('----------------------------------------------------'); for (const tc of HOOK_CASES) { const result = await runHook(tc.command); const exitOk = result.code === tc.expectExit; const blob = `${result.stdout}\n${result.stderr}`; const matchOk = tc.expectMatch === null ? !tc.expectMatch || true : tc.expectMatch.test(blob); const ok = exitOk && (tc.expectMatch === null || matchOk); if (ok) pass++; else fail++; const tick = ok ? 'PASS' : 'FAIL'; console.log(`[${tick}] ${tc.label}`); console.log(` command: ${tc.command}`); console.log(` exit: expect ${tc.expectExit} got ${result.code}`); if (tc.expectMatch) { console.log(` match: expect /${tc.expectMatch.source}/ → ${matchOk ? 'yes' : 'no'}`); } if (VERBOSE && result.stderr.trim()) { console.log(` stderr: ${result.stderr.trim().slice(0, 160)}`); } console.log(); } console.log('STAGE B — dep-auditor (offline scanner)'); console.log('---------------------------------------'); const depResult = await runDepAuditor(); const findings = depResult.findings || []; const typosquats = findings.filter(f => /typosquat/i.test(f.title || f.message || '')); const installScripts = findings.filter(f => /install\s*script|postinstall|preinstall/i.test(f.title || f.message || '')); const expectTyposquats = 4; // expresss, loadsh, axois, reaact (chalkk may also trigger) const haveTyposquats = typosquats.length >= expectTyposquats; const haveInstallScripts = installScripts.length >= 1; console.log(`[${haveTyposquats ? 'PASS' : 'FAIL'}] dep-auditor flagged >=${expectTyposquats} typosquats`); console.log(` got: ${typosquats.length}`); for (const f of typosquats.slice(0, 6)) { console.log(` - ${(f.title || f.message || '').slice(0, 100)}`); } if (haveTyposquats) pass++; else fail++; console.log(); console.log(`[${haveInstallScripts ? 'PASS' : 'FAIL'}] dep-auditor flagged install-script vector`); console.log(` got: ${installScripts.length}`); for (const f of installScripts.slice(0, 3)) { console.log(` - ${(f.title || f.message || '').slice(0, 100)}`); } if (haveInstallScripts) pass++; else fail++; console.log(); if (VERBOSE) { console.log(`Total dep-auditor findings: ${findings.length}`); for (const f of findings) { const sev = (f.severity || '').toUpperCase().padEnd(8); console.log(` ${sev} ${f.title || f.message || JSON.stringify(f).slice(0, 120)}`); } console.log(); } console.log('---'); console.log(`Result: ${pass} pass, ${fail} fail`); if (fail > 0) { console.log('\nFAILURE — see expected-findings.md for the documented contract.'); process.exit(1); } console.log('\nSUCCESS — both layers (PreToolUse hook + offline scanner) caught the attack.'); console.log('Read examples/supply-chain-attack/README.md for context.'); process.exit(0);