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).
172 lines
6.1 KiB
JavaScript
172 lines
6.1 KiB
JavaScript
#!/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);
|