#!/usr/bin/env node
// Hook: pre-install-supply-chain.mjs
// Event: PreToolUse (Bash)
// Purpose: Analyze ALL package installs BEFORE execution.
//
// Covers: npm, yarn, pnpm, npx, pip, pip3, uv, brew, docker, go, cargo, gem
//
// Checks per manager:
//   npm/yarn/pnpm: blocklist, npm audit, npm view (scripts + age gate)
//   pip/pip3/uv:   blocklist, PyPI API (age gate + metadata)
//   brew:          third-party tap warning, cask verification
//   docker:        unpinned tags, unverified images, known malicious
//   go install:    age gate via proxy.golang.org
//   cargo:         blocklist
//   gem:           blocklist
//
// Protocol:
//   - BLOCK (exit 2): known compromised, critical CVEs, new + install scripts
//   - WARN  (exit 0): high CVEs, install scripts on established packages
//   - Allow (exit 0): everything else

import { readFileSync, existsSync } from 'node:fs';
import {
  AGE_THRESHOLD_HOURS,
  NPM_COMPROMISED, PIP_COMPROMISED, CARGO_COMPROMISED, GEM_COMPROMISED,
  DOCKER_SUSPICIOUS, POPULAR_PIP,
  isCompromised, parseSpec, parsePipSpec, execSafe,
  queryOSV, extractOSVSeverity, checkScopeHop,
} from '../../scanners/lib/supply-chain-data.mjs';
import { normalizeBashExpansion } from '../../scanners/lib/bash-normalize.mjs';
import { getPolicyValue } from '../../scanners/lib/policy-loader.mjs';

// Policy-defined additional blocked packages (merged with built-in lists)
const POLICY_BLOCKED = new Set(getPolicyValue('supply_chain', 'additional_blocked_packages', []));

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

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

// First strip bash evasion techniques, then collapse whitespace
const normalized = normalizeBashExpansion(command).replace(/\s+/g, ' ').trim();
// ===========================================================================
// Quick gate — detect any package install command
// ===========================================================================
const GATES = {
  npm:    /\b(?:npm\s+(?:install|i|ci|add)|yarn\s+(?:add|install)|pnpm\s+(?:add|install|i))\b/,
  npx:    /\b(?:npx|pnpx)\s+\S/,
  pip:    /\b(?:pip3?\s+install|python3?\s+-m\s+pip\s+install|uv\s+pip\s+install|uv\s+add)\b/,
  brew:   /\b(?:brew\s+(?:install|tap))\b/,
  docker: /\b(?:docker\s+(?:pull|run))\b/,
  go:     /\bgo\s+install\b/,
  cargo:  /\bcargo\s+install\b/,
  gem:    /\bgem\s+install\b/,
};

const detectedManager = Object.entries(GATES).find(([, re]) => re.test(normalized))?.[0];
if (!detectedManager) {
  process.exit(0); // Not a package install command
}

// ===========================================================================
// Utility functions (only hook-specific ones remain; shared ones imported above)
// ===========================================================================

function extractArgs(cmd, installRegex) {
  const match = cmd.match(installRegex);
  if (!match) return [];
  return match[1].split(/\s+/).filter(a => a && !a.startsWith('-') && !['true', 'false'].includes(a));
}

// ===========================================================================
// NPM checks
// ===========================================================================

async function checkNpm() {
  const blocks = [];
  const warnings = [];

  const packages = extractNpmPackages(normalized);
  const isBareInstall = packages.length === 0 && !GATES.npx.test(normalized);

  if (isBareInstall) {
    // Scan lockfile for known compromised
    const lockFindings = scanNpmLockfile();
    for (const f of lockFindings) {
      blocks.push(
        `COMPROMISED in lockfile (${f.source}): ${f.name}@${f.version}\n` +
        `  This package/version is on the known-compromised list.\n` +
        `  Remove it from your lockfile and package.json before installing.`
      );
    }

    // npm audit
    const audit = runNpmAudit();
    if (audit.critical.length > 0) {
      const list = audit.critical.map(v => `    - ${v.name} (${v.severity}): ${v.title}`).join('\n');
      blocks.push(
        `npm audit: ${audit.critical.length} CRITICAL vulnerabilities\n${list}\n` +
        `  Run \`npm audit fix\` or update affected packages before installing.`
      );
    }
    if (audit.high.length > 0) {
      const list = audit.high.map(v => `    - ${v.name} (${v.severity}): ${v.title}`).join('\n');
      warnings.push(
        `npm audit: ${audit.high.length} HIGH vulnerabilities\n${list}\n` +
        `  Consider running \`npm audit fix\` to resolve.`
      );
    }
  }

  for (const spec of packages) {
    const { name, version } = parseSpec(spec);

    // E13: scope-hopping advisory — '@evilcorp/lodash' where '@evilcorp'
    // is not on the official-scopes allowlist and 'lodash' matches a top
    // npm package. MEDIUM advisory only, never blocks.
    const allowedScopes = getPolicyValue('supply_chain', 'allowed_scopes', []);
    const scopeHop = checkScopeHop(name, allowedScopes);
    if (scopeHop) {
      warnings.push(
        `SCOPE-HOPPING SUSPECTED: "${scopeHop.spec}"\n` +
        `  Unscoped name "${scopeHop.unscoped}" matches a top-100 npm package, but\n` +
        `  scope "${scopeHop.scope}" is not on the official-scopes allowlist.\n` +
        `  Verify the publisher before installing. Add "${scopeHop.scope}" to\n` +
        `  policy.json supply_chain.allowed_scopes to silence this advisory.`
      );
    }

    if (isCompromised(NPM_COMPROMISED, name, version) || POLICY_BLOCKED.has(name)) {
      blocks.push(
        `COMPROMISED: ${name}${version ? '@' + version : ''}\n` +
        `  ${POLICY_BLOCKED.has(name) ? 'Blocked by policy.' : 'Known supply chain attack.'} See: https://socket.dev/npm/package/${name}`
      );
      continue;
    }

    const meta = inspectNpmPackage(name, version);
    if (!meta) continue;

    const resolvedVersion = meta.version;

    // --- Advisory check (OSV.dev) — catches compromised established packages ---
    const advisories = await queryOSV('npm', name, resolvedVersion);
    if (advisories.critical.length > 0) {
      blocks.push(
        `KNOWN VULNERABILITY: ${name}@${resolvedVersion}\n` +
        advisories.critical.map(a => `  - [${a.severity}] ${a.id}: ${a.summary}`).join('\n') + '\n' +
        `  This version has critical advisories. Use a patched version.`
      );
      continue;
    }
    if (advisories.high.length > 0) {
      warnings.push(
        `VULNERABILITY ADVISORY: ${name}@${resolvedVersion}\n` +
        advisories.high.map(a => `  - [${a.severity}] ${a.id}: ${a.summary}`).join('\n') + '\n' +
        `  Consider using a version without known vulnerabilities.`
      );
    }

    // --- Git provenance check — catches hijacked publishes like axios ---
    const provenance = checkNpmProvenance(meta);
    if (provenance === 'suspicious') {
      warnings.push(
        `PROVENANCE WARNING: ${name}@${resolvedVersion}\n` +
        `  This version was published without matching git tag or CI attestation.\n` +
        `  It may have been published directly to npm (bypass CI) — as in the axios attack.\n` +
        `  Verify at: https://www.npmjs.com/package/${name}/v/${resolvedVersion}`
      );
    }

    // --- Install scripts check ---
    const scriptNames = ['preinstall', 'install', 'postinstall'].filter(s => meta.scripts?.[s]);
    if (scriptNames.length === 0) continue;

    const ageHours = getNpmPublishAge(meta);
    const versionCount = meta.versions?.length || (meta.time ? Object.keys(meta.time).length - 2 : 0);
    const isEstablished = versionCount >= 10;

    if (ageHours !== null && ageHours < AGE_THRESHOLD_HOURS && !isEstablished) {
      blocks.push(
        `NEW PACKAGE WITH INSTALL SCRIPTS: ${name}@${resolvedVersion}\n` +
        `  Has: ${scriptNames.join(', ')}\n` +
        `  Published: ${Math.round(ageHours)}h ago, ${versionCount} version(s) total\n` +
        `  New packages with install scripts are the #1 supply chain attack vector.`
      );
    } else {
      warnings.push(
        `INSTALL SCRIPTS: ${name}@${resolvedVersion}\n` +
        `  Has: ${scriptNames.join(', ')}\n` +
        `  Note: ~/.npmrc has ignore-scripts=true, so these won't run.`
      );
    }
  }

  return { blocks, warnings };
}

function extractNpmPackages(cmd) {
  const npxMatch = cmd.match(/\b(?:npx|pnpx)\s+(.+)/);
  if (npxMatch) {
    const args = npxMatch[1].split(/\s+/).filter(a => !a.startsWith('-'));
    return args.length > 0 ? [args[0]] : [];
  }
  if (/\bnpm\s+ci\b/.test(cmd)) return [];
  if (/\b(?:npm|yarn|pnpm)\s+(?:install|i)\s*$/.test(cmd.replace(/\s+--?\S+/g, '').trim())) return [];

  const match = cmd.match(/\b(?:npm|yarn|pnpm)\s+(?:install|i|add)\s+(.*)/);
  if (!match) return [];
  return match[1].split(/\s+/).filter(a => a && !a.startsWith('-'));
}

// ---------------------------------------------------------------------------
// npm provenance check — detect publishes that bypassed CI
// If a package has .attestations but this version doesn't, or if the repo
// field exists but the version has no corresponding git tag, flag it.
// ---------------------------------------------------------------------------
function checkNpmProvenance(meta) {
  if (!meta) return 'unknown';

  // Check if package normally has attestations (npm provenance)
  // Packages with sigstore attestations went through CI. Absence is suspicious.
  const hasGitRepo = meta.repository?.url || meta.repository;
  const hasAttestations = meta._attestations || meta.attestations;

  // If the package declares a git repo but this specific version
  // has no attestations AND was published very recently, flag it
  if (hasGitRepo && !hasAttestations) {
    const ageHours = getNpmPublishAge(meta);
    // Only flag very recent publishes (< 24h) from packages that normally use CI
    if (ageHours !== null && ageHours < 24) {
      // Check if previous versions had attestations by checking dist.attestations
      // This is a heuristic — not all packages use provenance yet
      return 'suspicious';
    }
  }

  return 'ok';
}

function inspectNpmPackage(name, version) {
  const spec = version ? `${name}@${version}` : name;
  const raw = execSafe(`npm view ${spec} --json`);
  if (!raw) return null;
  try { return JSON.parse(raw); } catch { return null; }
}

function getNpmPublishAge(meta) {
  const timeField = meta?.time;
  if (!timeField) return null;
  const publishDate = typeof timeField === 'string' ? timeField : timeField[meta.version] || timeField.modified;
  if (!publishDate) return null;
  return (Date.now() - new Date(publishDate).getTime()) / (1000 * 60 * 60);
}

function scanNpmLockfile() {
  const findings = [];
  const cwd = process.env.CLAUDE_WORKING_DIR || process.cwd();

  const lockPath = `${cwd}/package-lock.json`;
  if (existsSync(lockPath)) {
    try {
      const lock = JSON.parse(readFileSync(lockPath, 'utf-8'));
      for (const [key, info] of Object.entries(lock.packages || lock.dependencies || {})) {
        const name = key.replace(/^node_modules\//, '');
        if (name && isCompromised(NPM_COMPROMISED, name, info.version)) {
          findings.push({ name, version: info.version, source: 'package-lock.json' });
        }
      }
    } catch { /* ignore */ }
  }

  const yarnLock = `${cwd}/yarn.lock`;
  if (existsSync(yarnLock)) {
    try {
      const content = readFileSync(yarnLock, 'utf-8');
      for (const [pkg, versions] of Object.entries(NPM_COMPROMISED)) {
        for (const v of versions) {
          if (v === '*' ? content.includes(`${pkg}@`) : content.includes(`version "${v}"`) && content.includes(`${pkg}@`)) {
            findings.push({ name: pkg, version: v === '*' ? '(any)' : v, source: 'yarn.lock' });
          }
        }
      }
    } catch { /* ignore */ }
  }

  return findings;
}

function runNpmAudit() {
  const cwd = process.env.CLAUDE_WORKING_DIR || process.cwd();
  if (!existsSync(`${cwd}/package-lock.json`)) return { critical: [], high: [] };

  const raw = execSafe('npm audit --json', 15000);
  if (!raw) return { critical: [], high: [] };

  const critical = [];
  const high = [];
  try {
    const audit = JSON.parse(raw);
    for (const [name, info] of Object.entries(audit.vulnerabilities || {})) {
      const title = Array.isArray(info.via) ? info.via.map(v => typeof v === 'string' ? v : v.title).join(', ') : String(info.via);
      const entry = { name, severity: info.severity, title };
      if (info.severity === 'critical') critical.push(entry);
      else if (info.severity === 'high') high.push(entry);
    }
  } catch { /* ignore */ }
  return { critical, high };
}

// ===========================================================================
// PIP checks
// ===========================================================================

async function checkPip() {
  const blocks = [];
  const warnings = [];

  const packages = extractPipPackages(normalized);

  // pip install (bare, from requirements.txt) — scan requirements for known bad
  if (packages.length === 0) {
    const reqFindings = scanRequirementsTxt();
    for (const f of reqFindings) {
      blocks.push(
        `COMPROMISED in requirements: ${f.name}${f.version ? '==' + f.version : ''}\n` +
        `  This package is on the known-compromised list (typosquat/malware).`
      );
    }
    return { blocks, warnings };
  }

  for (const spec of packages) {
    const { name, version } = parsePipSpec(spec);

    if (isCompromised(PIP_COMPROMISED, name, version) || POLICY_BLOCKED.has(name)) {
      blocks.push(
        `COMPROMISED: ${name} (PyPI)\n` +
        `  ${POLICY_BLOCKED.has(name) ? 'Blocked by policy.' : 'Known malicious package (likely typosquat).'}\n` +
        `  See: https://pypi.org/project/${name}/`
      );
      continue;
    }

    // Check PyPI API for age and metadata
    const meta = await inspectPyPIPackage(name, version);
    if (!meta) continue;

    const resolvedVersion = version || meta.info?.version;

    // --- Advisory check (OSV.dev) — catches compromised established packages ---
    const advisories = await queryOSV('pip', name, resolvedVersion);
    if (advisories.critical.length > 0) {
      blocks.push(
        `KNOWN VULNERABILITY: ${name}==${resolvedVersion} (PyPI)\n` +
        advisories.critical.map(a => `  - [${a.severity}] ${a.id}: ${a.summary}`).join('\n') + '\n' +
        `  This version has critical advisories. Use a patched version.`
      );
      continue;
    }
    if (advisories.high.length > 0) {
      warnings.push(
        `VULNERABILITY ADVISORY: ${name}==${resolvedVersion} (PyPI)\n` +
        advisories.high.map(a => `  - [${a.severity}] ${a.id}: ${a.summary}`).join('\n') + '\n' +
        `  Consider using a version without known vulnerabilities.`
      );
    }

    const ageHours = getPyPIPublishAge(meta, version);
    const releaseCount = Object.keys(meta.releases || {}).length;
    const isEstablished = releaseCount >= 10;

    // Age gate only for genuinely new packages (few releases).
    // Established packages (10+ releases) with a new version are normal — don't block.
    if (ageHours !== null && ageHours < AGE_THRESHOLD_HOURS && !isEstablished) {
      blocks.push(
        `NEW PyPI PACKAGE: ${name}${version ? '==' + version : ''}\n` +
        `  Published: ${Math.round(ageHours)}h ago (threshold: ${AGE_THRESHOLD_HOURS}h)\n` +
        `  Only ${releaseCount} release(s) — this looks like a genuinely new package.\n` +
        `  New PyPI packages may contain malicious setup.py scripts.\n` +
        `  Wait ${AGE_THRESHOLD_HOURS}h or verify manually first.`
      );
    }

    // Typosquat detection — Levenshtein distance to popular packages
    const typosquatOf = checkTyposquat(name);
    if (typosquatOf) {
      warnings.push(
        `POSSIBLE TYPOSQUAT: "${name}" is suspiciously similar to "${typosquatOf}"\n` +
        `  Verify this is the intended package before installing.`
      );
    }
  }

  return { blocks, warnings };
}

function extractPipPackages(cmd) {
  // Handle: pip install pkg, pip3 install pkg, python -m pip install pkg, uv pip install pkg, uv add pkg
  const match = cmd.match(/\b(?:pip3?\s+install|python3?\s+-m\s+pip\s+install|uv\s+pip\s+install|uv\s+add)\s+(.*)/);
  if (!match) return [];

  return match[1].split(/\s+/)
    .filter(a => a && !a.startsWith('-') && !a.startsWith('/') && !a.endsWith('.txt') && !a.endsWith('.whl') && !a.endsWith('.tar.gz'));
}

async function inspectPyPIPackage(name, version) {
  const url = version
    ? `https://pypi.org/pypi/${name}/${version}/json`
    : `https://pypi.org/pypi/${name}/json`;
  try {
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), 10000);
    const res = await fetch(url, { signal: controller.signal });
    clearTimeout(timer);
    if (!res.ok) return null;
    return await res.json();
  } catch { return null; }
}

function getPyPIPublishAge(meta, requestedVersion) {
  // PyPI returns upload_time per release
  const version = requestedVersion || meta?.info?.version;
  if (!version || !meta?.releases?.[version]) return null;
  const files = meta.releases[version];
  if (!files.length) return null;
  const uploadTime = files[0].upload_time_iso_8601 || files[0].upload_time;
  if (!uploadTime) return null;
  return (Date.now() - new Date(uploadTime).getTime()) / (1000 * 60 * 60);
}

function scanRequirementsTxt() {
  const findings = [];
  const cwd = process.env.CLAUDE_WORKING_DIR || process.cwd();

  for (const reqFile of ['requirements.txt', 'requirements-dev.txt', 'requirements.lock']) {
    const path = `${cwd}/${reqFile}`;
    if (!existsSync(path)) continue;
    try {
      const lines = readFileSync(path, 'utf-8').split('\n');
      for (const line of lines) {
        const trimmed = line.trim();
        if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('-')) continue;
        const { name, version } = parsePipSpec(trimmed);
        if (isCompromised(PIP_COMPROMISED, name, version)) {
          findings.push({ name, version });
        }
      }
    } catch { /* ignore */ }
  }
  return findings;
}

// levenshtein and checkTyposquat imported via POPULAR_PIP from supply-chain-data.mjs
// Local wrapper preserving hook's original behavior (normalizes differently than scanner)
function checkTyposquat(name) {
  const lower = name.toLowerCase().replace(/[_.-]/g, '');
  for (const popular of POPULAR_PIP) {
    const popLower = popular.toLowerCase().replace(/[_.-]/g, '');
    if (lower === popLower) continue;
    const dist = levenshteinLocal(lower, popLower);
    if (dist === 1 && lower.length > 3) return popular;
    if (lower.length === popLower.length && dist <= 2 && lower.length > 5) {
      const diffs = [...lower].filter((c, i) => c !== popLower[i]).length;
      if (diffs <= 1) return popular;
    }
  }
  return null;
}

// Hook-local levenshtein (O(m*n) matrix variant preserved for zero-dependency guarantee)
function levenshteinLocal(a, b) {
  const m = a.length, n = b.length;
  const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
  for (let i = 0; i <= m; i++) dp[i][0] = i;
  for (let j = 0; j <= n; j++) dp[0][j] = j;
  for (let i = 1; i <= m; i++) {
    for (let j = 1; j <= n; j++) {
      dp[i][j] = a[i - 1] === b[j - 1]
        ? dp[i - 1][j - 1]
        : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
    }
  }
  return dp[m][n];
}

// ===========================================================================
// BREW checks
// ===========================================================================

function checkBrew() {
  const blocks = [];
  const warnings = [];

  // brew tap — warn about third-party taps
  if (/\bbrew\s+tap\s+/.test(normalized)) {
    const tapMatch = normalized.match(/\bbrew\s+tap\s+(\S+)/);
    if (tapMatch) {
      const tap = tapMatch[1];
      if (!tap.startsWith('homebrew/')) {
        warnings.push(
          `THIRD-PARTY TAP: ${tap}\n` +
          `  Only official Homebrew taps (homebrew/*) are curated.\n` +
          `  Third-party taps can contain arbitrary formulae. Verify the source.`
        );
      }
    }
  }

  // brew install --cask — warn about cask source
  if (/\bbrew\s+install\s+.*--cask/.test(normalized) || /\bbrew\s+install\s+--cask/.test(normalized)) {
    warnings.push(
      `CASK INSTALL: Casks install full macOS applications.\n` +
      `  Verify the publisher and download source before proceeding.`
    );
  }

  return { blocks, warnings };
}

// ===========================================================================
// DOCKER checks
// ===========================================================================

function checkDocker() {
  const blocks = [];
  const warnings = [];

  const imageMatch = normalized.match(/\bdocker\s+(?:pull|run)\s+(?:--[^\s]+\s+)*(\S+)/);
  if (!imageMatch) return { blocks, warnings };

  const image = imageMatch[1];

  // Check for known malicious patterns
  for (const pattern of DOCKER_SUSPICIOUS) {
    if (pattern.test(image)) {
      blocks.push(
        `SUSPICIOUS DOCKER IMAGE: ${image}\n` +
        `  Matches known malicious pattern (cryptominer/malware).`
      );
      return { blocks, warnings };
    }
  }

  // Unpinned tag (using :latest or no tag)
  if (!image.includes(':') || image.endsWith(':latest')) {
    warnings.push(
      `UNPINNED DOCKER IMAGE: ${image}\n` +
      `  Using :latest or no tag means the image can change without notice.\n` +
      `  Pin to a specific digest: docker pull ${image.split(':')[0]}@sha256:<digest>`
    );
  }

  // Unofficial image (no / means Docker Hub library, but user images have owner/)
  if (image.includes('/') && !image.startsWith('library/')) {
    const owner = image.split('/')[0];
    // Not a known registry
    if (!['docker.io', 'ghcr.io', 'gcr.io', 'mcr.microsoft.com', 'registry.k8s.io', 'quay.io', 'public.ecr.aws'].some(r => image.startsWith(r))) {
      warnings.push(
        `COMMUNITY DOCKER IMAGE: ${image}\n` +
        `  This is not an official Docker Hub image.\n` +
        `  Verify the publisher "${owner}" before running.`
      );
    }
  }

  return { blocks, warnings };
}

// ===========================================================================
// GO checks
// ===========================================================================

async function checkGo() {
  const blocks = [];
  const warnings = [];

  const match = normalized.match(/\bgo\s+install\s+(\S+)/);
  if (!match) return { blocks, warnings };

  const pkg = match[1];

  // Check module age via proxy.golang.org
  const modPath = pkg.replace(/@.*$/, '');
  const version = pkg.includes('@') ? pkg.split('@').pop() : null;
  if (version && version !== 'latest') {
    try {
      const controller = new AbortController();
      const timer = setTimeout(() => controller.abort(), 8000);
      const res = await fetch(`https://proxy.golang.org/${modPath}/@v/${version}.info`, { signal: controller.signal });
      clearTimeout(timer);
      if (res.ok) {
        const info = await res.json();
        if (info.Time) {
          const ageHours = (Date.now() - new Date(info.Time).getTime()) / (1000 * 60 * 60);
          if (ageHours < AGE_THRESHOLD_HOURS) {
            blocks.push(
              `NEW GO MODULE: ${pkg}\n` +
              `  Published: ${Math.round(ageHours)}h ago (threshold: ${AGE_THRESHOLD_HOURS}h)\n` +
              `  go install compiles and runs code. Wait or verify manually.`
            );
          }
        }
      }
    } catch { /* network error — fail open */ }
  }

  return { blocks, warnings };
}

// ===========================================================================
// CARGO checks
// ===========================================================================

async function checkCargo() {
  const blocks = [];
  const warnings = [];

  const match = normalized.match(/\bcargo\s+install\s+(\S+)/);
  if (!match) return { blocks, warnings };

  const crate = match[1].replace(/^--.*/, '').trim();
  if (!crate) return { blocks, warnings };

  if (isCompromised(CARGO_COMPROMISED, crate, null)) {
    blocks.push(
      `COMPROMISED CRATE: ${crate}\n` +
      `  Known malicious Rust crate. See: https://crates.io/crates/${crate}`
    );
  } else {
    // Check OSV for known vulns
    const vMatch = normalized.match(/--version\s+(\S+)/);
    const version = vMatch ? vMatch[1] : null;
    if (version) {
      const advisories = await queryOSV('cargo', crate, version);
      if (advisories.critical.length > 0) {
        blocks.push(
          `KNOWN VULNERABILITY: ${crate}@${version} (crates.io)\n` +
          advisories.critical.map(a => `  - [${a.severity}] ${a.id}: ${a.summary}`).join('\n')
        );
      }
    }
  }

  return { blocks, warnings };
}

// ===========================================================================
// GEM checks
// ===========================================================================

async function checkGem() {
  const blocks = [];
  const warnings = [];

  const match = normalized.match(/\bgem\s+install\s+(\S+)/);
  if (!match) return { blocks, warnings };

  const spec = match[1];
  const dashV = normalized.match(/-v\s+['"]?([0-9][0-9a-zA-Z._-]*)['"]?/);
  const version = dashV ? dashV[1] : null;

  if (isCompromised(GEM_COMPROMISED, spec, version)) {
    blocks.push(
      `COMPROMISED GEM: ${spec}${version ? '@' + version : ''}\n` +
      `  Known backdoored version. See: https://rubygems.org/gems/${spec}`
    );
  } else if (version) {
    const advisories = await queryOSV('gem', spec, version);
    if (advisories.critical.length > 0) {
      blocks.push(
        `KNOWN VULNERABILITY: ${spec}@${version} (RubyGems)\n` +
        advisories.critical.map(a => `  - [${a.severity}] ${a.id}: ${a.summary}`).join('\n')
      );
    }
  }

  return { blocks, warnings };
}

// ===========================================================================
// Main — dispatch to correct checker
// ===========================================================================

const checkers = {
  npm: checkNpm,
  npx: checkNpm,    // npx uses the same npm ecosystem
  pip: checkPip,
  brew: checkBrew,
  docker: checkDocker,
  go: checkGo,
  cargo: checkCargo,
  gem: checkGem,
};

const checker = checkers[detectedManager];
if (!checker) process.exit(0);

const { blocks, warnings } = await checker();

if (blocks.length > 0) {
  process.stderr.write(
    `\n🛑 BLOCKED: Supply chain risk detected [${detectedManager}]\n` +
    `  Command: ${normalized.slice(0, 200)}${normalized.length > 200 ? '...' : ''}\n\n` +
    blocks.map(b => `  ${b}`).join('\n\n') + '\n\n' +
    `  The command was NOT executed.\n`
  );
  process.exit(2);
}

if (warnings.length > 0) {
  process.stderr.write(
    `\n⚠️  Supply chain advisory [${detectedManager}]:\n` +
    warnings.map(w => `  ${w}`).join('\n\n') + '\n\n'
  );
}

process.exit(0);
