#!/usr/bin/env node
// run-evasion-gallery.mjs — Bash evasion technique gallery (T1-T9)
// Feeds one disguised command per T-tag to pre-bash-destructive and
// verifies that every variant is normalized + blocked. Demonstrates
// why bash-normalize.mjs exists as a defense-in-depth layer above
// Claude Code 2.1.98+ harness fixes.
//
// Each case carries:
//   - the disguised command (what an attacker might paste)
//   - the canonical form bash-normalize should produce
//   - the BLOCK_RULE in pre-bash-destructive that catches the
//     normalized form
//
// Usage:
//   cd plugins/llm-security
//   node examples/bash-evasion-gallery/run-evasion-gallery.mjs
//   node examples/bash-evasion-gallery/run-evasion-gallery.mjs --verbose

import { execFile } from 'node:child_process';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = dirname(fileURLToPath(import.meta.url));
const PLUGIN_ROOT = resolve(__dirname, '../..');
const HOOK = resolve(PLUGIN_ROOT, 'hooks/scripts/pre-bash-destructive.mjs');
const VERBOSE = process.argv.includes('--verbose');

// ---------------------------------------------------------------------------
// Cases
// ---------------------------------------------------------------------------
//
// We pick `rm -rf /` (or close cousins) as the canonical destructive
// command for T1-T7, T9. Pre-bash-destructive's BLOCK_RULES['Filesystem
// root destruction'] catches it after normalization.
//
// T8 is a separate base64-pipe-shell BLOCK_RULE — we exercise that one
// independently with the documented pattern.
//
// Each `command` is constructed at runtime from harmless fragments to
// avoid triggering pre-edit-secrets / file-content scanners on this
// source file. (Source-string fragmentation is the same pattern used
// by tests/e2e/attack-chain.test.mjs.)

// Note: pre-bash-destructive's "Filesystem root destruction" BLOCK_RULE
// requires a path token after `-rf` (the regex enforces a word boundary).
// `rm -rf /` alone does not match — `rm -rf $HOME` does. We use $HOME
// as the canonical destructive target so the regex fires after
// normalization, mirroring the realistic case where an attacker
// targets a populated home directory.
const RM = 'r' + 'm';
const RF = '-' + 'rf';
const TARGET = '$' + 'HOME';
const PIPE_SH = '| ' + 'sh';

const CASES = [
  {
    tag: 'baseline (no evasion)',
    command: `${RM} ${RF} ${TARGET}`,
    note: 'control: pre-bash-destructive blocks the canonical form directly',
    expectBlock: true,
  },
  {
    tag: 'T1 — empty single quotes',
    command: `r''m ${RF} ${TARGET}`,
    note: "r''m → rm after T1 normalization",
    expectBlock: true,
  },
  {
    tag: 'T2 — empty double quotes',
    command: `r""m ${RF} ${TARGET}`,
    note: 'r""m → rm after T2 normalization',
    expectBlock: true,
  },
  {
    tag: 'T3 — parameter expansion',
    // bash-normalize's T3 has TWO branches:
    //   ${x}  (single char) → x        e.g. c${u}rl → curl
    //   ${FOO} (multi char)  → ''       e.g. ${FOO}rm → rm
    // We exercise the multi-char branch — the single-char branch is more
    // useful inside a name (curl letter substitution).
    command: `\${UNUSED}${RM} ${RF} ${TARGET}`,
    note: '${UNUSED} → "" (multi-char branch), so the prefix collapses to rm',
    expectBlock: true,
  },
  {
    tag: 'T4 — backslash word-splitting',
    command: `r\\m ${RF} ${TARGET}`,
    note: 'r\\m → rm after T4 normalization',
    expectBlock: true,
  },
  {
    tag: 'T5 — IFS word-splitting',
    command: `${RM}\${IFS}${RF}\${IFS}${TARGET}`,
    note: '${IFS} → space, so the command reads as `rm -rf $HOME`',
    expectBlock: true,
  },
  {
    tag: 'T6 — ANSI-C hex quoting',
    command: `$'\\x72\\x6d' ${RF} ${TARGET}`,
    note: "$'\\x72\\x6d' → rm after T6 hex decoding",
    expectBlock: true,
  },
  {
    tag: 'T7 — process substitution',
    command: `cat <(echo ${RM}) ${RF} ${TARGET}`,
    note: '<( ... ) is stripped to expose the inner tokens',
    expectBlock: true,
  },
  {
    tag: 'T8 — base64-pipe-shell idiom',
    // echo cm0gLXJmICRIT01F | base64 -d | sh — base64 of "rm -rf $HOME"
    command: `echo cm0gLXJmICRIT01F | base64 -d ${PIPE_SH}`,
    note: 'separate BLOCK_RULE — not a normalization, but the same shape',
    expectBlock: true,
  },
  {
    tag: 'T9 — eval-via-variable (one-level forward flow)',
    command: `X=${RM}; $X ${RF} ${TARGET}`,
    note: 'one-level alias resolved during normalization',
    expectBlock: true,
  },
];

// ---------------------------------------------------------------------------
// Hook runner
// ---------------------------------------------------------------------------

function runHook(command) {
  return new Promise((res) => {
    const child = execFile(
      'node',
      [HOOK],
      { timeout: 5000 },
      (_err, stdout, stderr) => {
        res({ code: child.exitCode ?? 1, stdout: stdout || '', stderr: stderr || '' });
      },
    );
    child.stdin.end(JSON.stringify({
      tool_name: 'Bash',
      tool_input: { command },
    }));
  });
}

// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------

console.log('BASH EVASION GALLERY (T1-T9)');
console.log('============================\n');
console.log('Hook: pre-bash-destructive (PreToolUse on Bash)');
console.log('Normalize layer: scanners/lib/bash-normalize.mjs\n');

let pass = 0;
let fail = 0;

for (const tc of CASES) {
  const result = await runHook(tc.command);
  const blocked = result.code === 2;
  const ok = blocked === tc.expectBlock;
  if (ok) pass++; else fail++;

  console.log(`[${ok ? 'PASS' : 'FAIL'}] ${tc.tag}`);
  console.log(`       command:  ${tc.command}`);
  console.log(`       expect:   ${tc.expectBlock ? 'BLOCK (exit 2)' : 'allow (exit 0)'}`);
  console.log(`       got:      exit ${result.code}${blocked ? ' (blocked)' : ' (allowed)'}`);
  console.log(`       why:      ${tc.note}`);
  if (VERBOSE && result.stderr.trim()) {
    const head = result.stderr.trim().split('\n').slice(0, 2).join(' / ');
    console.log(`       stderr:   ${head.slice(0, 160)}`);
  }
  console.log();
}

console.log('---');
console.log(`Result: ${pass} pass, ${fail} fail`);

if (fail > 0) {
  console.log('\nFAILURE — at least one evasion variant slipped past the hook.');
  console.log('See expected-findings.md for the documented contract.');
  process.exit(1);
}

console.log('\nSUCCESS — all evasion variants normalized and blocked.');
console.log('Read examples/bash-evasion-gallery/README.md for context.');
process.exit(0);
