#!/usr/bin/env node
// Hook: pre-bash-destructive.mjs
// Event: PreToolUse (Bash)
// Purpose: Block or warn about destructive shell commands.
//
// Protocol:
//   - Read JSON from stdin: { tool_name, tool_input }
//   - tool_input.command — the shell command string
//   - BLOCK (exit 2): catastrophic/irreversible operations
//   - WARN  (exit 0): risky but recoverable operations — advisory message to stderr
//   - Allow (exit 0): everything else

import { readFileSync } from 'node:fs';
import { normalizeBashExpansion } from '../../scanners/lib/bash-normalize.mjs';
import { getPolicyValue } from '../../scanners/lib/policy-loader.mjs';

// ---------------------------------------------------------------------------
// BLOCK rules — exit 2, command is not executed.
// Each rule: { name, pattern, description }
// ---------------------------------------------------------------------------
const BLOCK_RULES = [
  {
    name: 'Filesystem root destruction (rm -rf /)',
    pattern: /\brm\s+(?:-[a-zA-Z]*f[a-zA-Z]*\s+|--force\s+)*-[a-zA-Z]*r[a-zA-Z]*\s+(?:\/|~|\$HOME)\b/,
    description:
      '`rm -rf /`, `rm -rf ~`, and `rm -rf $HOME` would destroy the entire filesystem ' +
      'or home directory. This command is unconditionally blocked.',
  },
  {
    name: 'World-writable chmod (chmod 777)',
    pattern: /\bchmod\s+(?:-[a-zA-Z]+\s+)*777\b/,
    description:
      '`chmod 777` grants full read/write/execute to all users, creating a severe ' +
      'security vulnerability. Use the minimal permission set required (e.g. 644, 755).',
  },
  {
    name: 'Pipe-to-shell (curl|sh, wget|sh, curl|bash)',
    // Matches: curl ... | sh, curl ... | bash, wget ... | sh, etc.
    // Also catches variations with xargs sh, xargs bash
    pattern: /(?:curl|wget)\b[^|]*\|\s*(?:bash|sh|zsh|ksh|dash)\b/,
    description:
      'Piping remote content directly into a shell interpreter allows ' +
      'arbitrary remote code execution without inspection. Download the script first, ' +
      'review it, then execute explicitly.',
  },
  {
    name: 'Fork bomb',
    pattern: /:\(\)\s*\{\s*:\s*\|\s*:&\s*\}\s*;?\s*:/,
    description:
      'This is a fork bomb that will exhaust system process resources and require a hard reboot. Blocked.',
  },
  {
    name: 'Filesystem format (mkfs)',
    pattern: /\bmkfs(?:\.[a-z0-9]+)?\s/,
    description:
      '`mkfs` formats a filesystem, destroying all data on the target device. ' +
      'This is an irreversible operation and is blocked.',
  },
  {
    name: 'Raw disk overwrite via dd',
    // dd if=... of=/dev/sd* or of=/dev/nvme* or similar block devices
    pattern: /\bdd\b[^&|;]*\bof=\/dev\/(?:sd|nvme|hd|vd|xvd|mmcblk)[a-z0-9]*/,
    description:
      '`dd` writing to a raw block device (/dev/sd*, /dev/nvme*) will destroy partition ' +
      'tables and data on that disk. Blocked to prevent accidental disk wipe.',
  },
  {
    name: 'Direct device write (> /dev/sd* etc.)',
    pattern: />\s*\/dev\/(?:sd|nvme|hd|vd|xvd|mmcblk)[a-z0-9]*/,
    description:
      'Writing directly to a block device via shell redirection destroys disk data. Blocked.',
  },
  {
    name: 'eval with variable/command expansion (potential injection)',
    // eval $VAR, eval $(cmd), eval `cmd`, eval "$VAR"
    pattern: /\beval\s+(?:`|\$[\({]|"[^"]*\$)/,
    description:
      '`eval` with variable or command substitution executes dynamically constructed ' +
      'strings, which is a common code injection vector. Blocked. ' +
      'Refactor to use explicit commands instead.',
  },
  {
    name: 'T8 — base64-pipe-shell idiom (echo BLOB | base64 -d | sh)',
    // Matches: echo|cat|printf <base64-blob> | base64 -d | <shell>
    // Common malware loader pattern that bypasses static name-matching by
    // delivering the destructive command as encoded text.
    pattern: /\b(?:echo|cat|printf)\s+[A-Za-z0-9+/=]+\s*\|\s*base64\s+-d\s*\|\s*(?:bash|sh|zsh|dash|ksh)\b/i,
    description:
      'Decoding a base64 blob and piping it directly into a shell interpreter ' +
      'is a remote-code-execution loader pattern. Decode the blob first, ' +
      'inspect it, then execute explicitly if safe.',
  },
  // Policy-defined additional blocked patterns
  ...getPolicyValue('destructive', 'additional_blocked', []).map(entry => ({
    name: entry.name || 'Custom blocked pattern',
    pattern: new RegExp(entry.pattern),
    description: entry.description || 'Blocked by policy.',
  })),
];

// ---------------------------------------------------------------------------
// WARN rules — exit 0 with advisory message on stderr.
// Command is allowed to proceed but the user/agent is informed.
// ---------------------------------------------------------------------------
const WARN_RULES = [
  {
    name: 'Force push (git push --force)',
    pattern: /\bgit\s+push\b[^|&;]*(?:--force|-f)\b/,
    description:
      'WARNING: `git push --force` rewrites remote history. This can destroy commits ' +
      'for all collaborators on shared branches. Prefer `--force-with-lease`.',
  },
  {
    name: 'Hard reset (git reset --hard)',
    pattern: /\bgit\s+reset\s+--hard\b/,
    description:
      'WARNING: `git reset --hard` permanently discards uncommitted changes and ' +
      'moves the branch pointer. Ensure you have no unsaved work.',
  },
  {
    name: 'Recursive remove (rm -rf, non-root non-home target)',
    // Warn for rm -rf that doesn't hit /, ~, or $HOME (those are BLOCKED above)
    pattern: /\brm\s+(?:-[a-zA-Z]*f[a-zA-Z]*\s+|--force\s+)*-[a-zA-Z]*r[a-zA-Z]*\s+/,
    description:
      'WARNING: `rm -rf` permanently deletes files and directories without recovery. ' +
      'Verify the target path before proceeding.',
  },
  {
    name: 'Docker system prune',
    pattern: /\bdocker\s+system\s+prune\b/,
    description:
      'WARNING: `docker system prune` removes all stopped containers, unused images, ' +
      'networks, and build cache. This may delete data needed for local development.',
  },
  {
    name: 'npm publish',
    pattern: /\bnpm\s+publish\b/,
    description:
      'WARNING: `npm publish` releases a package to the public npm registry. ' +
      'Confirm the version, changelog, and that no secrets are bundled.',
  },
  {
    name: 'DROP TABLE or DROP DATABASE (SQL)',
    pattern: /\bDROP\s+(?:TABLE|DATABASE|SCHEMA)\b/i,
    description:
      'WARNING: SQL DROP statements permanently delete database objects and all their data. ' +
      'Ensure you have a recent backup and are targeting the correct environment.',
  },
  {
    name: 'DELETE without WHERE (SQL)',
    pattern: /\bDELETE\s+FROM\s+\w+(?:\s*;|\s*$)/i,
    description:
      'WARNING: DELETE FROM without a WHERE clause deletes all rows in the table. ' +
      'Ensure this is intentional and backed up.',
  },
];

// ---------------------------------------------------------------------------
// Normalize command: strip ANSI, collapse whitespace, for pattern matching.
// We do NOT strip quotes entirely — patterns are designed to work with raw input.
// ---------------------------------------------------------------------------
function normalizeCommand(cmd) {
  return cmd
    // Remove ANSI escape codes
    .replace(/\x1B\[[0-9;]*m/g, '')
    // Collapse runs of whitespace (including newlines from heredocs) to single space
    .replace(/\s+/g, ' ')
    .trim();
}

// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
let input;
try {
  const raw = readFileSync(0, 'utf-8');
  input = JSON.parse(raw);
} catch {
  // Cannot parse stdin — fail open.
  process.exit(0);
}

const command = input?.tool_input?.command;

if (!command || typeof command !== 'string') {
  process.exit(0);
}

// First strip bash evasion techniques (empty quotes, ${} expansion, backslash splitting),
// then apply standard normalization (ANSI strip, whitespace collapse).
const deobfuscated = normalizeBashExpansion(command);
const normalized = normalizeCommand(deobfuscated);

// Check BLOCK rules first
for (const rule of BLOCK_RULES) {
  if (rule.pattern.test(normalized)) {
    process.stderr.write(
      `BLOCKED: Destructive command detected — ${rule.name}\n` +
      `  Command: ${normalized.slice(0, 200)}${normalized.length > 200 ? '...' : ''}\n` +
      `  ${rule.description}\n`
    );
    process.exit(2);
  }
}

// Check WARN rules (advisory — still exit 0)
const warnings = [];
for (const rule of WARN_RULES) {
  if (rule.pattern.test(normalized)) {
    warnings.push(`  [WARN] ${rule.name}: ${rule.description}`);
  }
}

if (warnings.length > 0) {
  process.stderr.write(
    `SECURITY ADVISORY: Potentially risky command detected.\n` +
    `  Command: ${normalized.slice(0, 200)}${normalized.length > 200 ? '...' : ''}\n` +
    warnings.join('\n') + '\n' +
    `  Proceeding — verify intent before confirming.\n`
  );
}

// Allow (with or without warnings)
process.exit(0);
