#!/usr/bin/env node
/**
 * OpenClaw Security — Tool Risk Scorer
 *
 * Four-factor weighted risk assessment before tool execution.
 * Migrated from ECC `ecc2/src/observability/mod.rs` (risk scoring algorithm).
 *
 * Usage:
 *   node src/security/risk-scorer.js '{"tool":"Bash","input":"rm -rf /etc"}'
 *   → { score: 0.70, action: "confirm", factors: [...], explanation: "..." }
 *
 * Thresholds (configurable via env):
 *   BLOCK   ≥ 0.85
 *   CONFIRM ≥ 0.60
 *   REVIEW  ≥ 0.35
 *   ALLOW   <  0.35
 */

// ─── Configuration ──────────────────────────────────────────────

const THRESHOLDS = {
  block:   parseFloat(process.env.RISK_THRESHOLD_BLOCK   || '0.85'),
  confirm: parseFloat(process.env.RISK_THRESHOLD_CONFIRM || '0.60'),
  review:  parseFloat(process.env.RISK_THRESHOLD_REVIEW  || '0.35'),
};

// Base risk per tool type (0.0-1.0 scale)
const BASE_TOOL_RISK = {
  'Bash':        0.20,
  'Write':       0.15,
  'Edit':        0.12,
  'exec':        0.20,
  'process':     0.10,
  'delete':      0.25,
  'Grep':        0.02,
  'Glob':        0.02,
  'Read':        0.02,
  'memory_get':  0.01,
  'memory_search':0.01,
  'web_fetch':   0.05,
  'web_search':  0.02,
  'image':       0.02,
  'cron':        0.08,
  'sessions_send':0.05,
  'sessions_spawn':0.05,
  'notion':      0.05,
  'write':       0.15,
  'edit':        0.12,
  'read':        0.02,
};

// File sensitivity patterns — higher risk
const SENSITIVE_PATTERNS = [
  { pattern: /\.env(\.|\b|$)/i,      score: 0.25, label: 'env file' },
  { pattern: /\/etc\//,               score: 0.20, label: 'system config' },
  { pattern: /\.ssh\//,               score: 0.25, label: 'SSH keys' },
  { pattern: /\.aws\//,               score: 0.22, label: 'AWS credentials' },
  { pattern: /\.git\/config/,         score: 0.15, label: 'git config' },
  { pattern: /id_rsa|id_ed25519/,    score: 0.25, label: 'private key' },
  { pattern: /credentials|secret|token|password/i,
                                       score: 0.20, label: 'credential file' },
  { pattern: /openclaw\.json/,        score: 0.15, label: 'OpenClaw config' },
  { pattern: /\.zshrc|\.bashrc|\.profile/,
                                       score: 0.12, label: 'shell rc' },
  { pattern: /crontab|launchd|systemd/,
                                       score: 0.18, label: 'scheduler config' },
];

// Destructive operation patterns — high blast radius
const DESTRUCTIVE_PATTERNS = [
  { pattern: /\brm\s+-rf?\b/,                  score: 0.35, label: 'recursive delete' },
  { pattern: /\brm\s+-[a-z]*r[a-z]*\b/,        score: 0.30, label: 'recursive removal' },
  { pattern: /\brmdir\b/,                      score: 0.15, label: 'remove directory' },
  { pattern: /\bchmod\s+777\b/,                score: 0.20, label: 'world-writable chmod' },
  { pattern: /\bchmod\s+-R\b/,                 score: 0.18, label: 'recursive chmod' },
  { pattern: /\bchown\b/,                      score: 0.20, label: 'ownership change' },
  { pattern: /\b>(\s*\/dev\/)/,                score: 0.25, label: 'device write' },
  { pattern: /\|.*sh\b/,                       score: 0.10, label: 'piped shell' },
  { pattern: /\bcurl\b.*\|\s*(ba)?sh\b/,      score: 0.25, label: 'curl-to-shell' },
  { pattern: /\bwget\b.*\|\s*(ba)?sh\b/,      score: 0.25, label: 'wget-to-shell' },
  { pattern: /\bkill\b|-9/,                   score: 0.15, label: 'process kill' },
  { pattern: /\bdd\s+if=/,                    score: 0.22, label: 'disk dd' },
  { pattern: /\bmkfs\b/,                      score: 0.30, label: 'filesystem mkfs' },
  { pattern: /\breboot\b|\bshutdown\b/,       score: 0.30, label: 'system reboot/shutdown' },
  { pattern: /\/\*/,                          score: 0.15, label: 'glob wildcard' },
  { pattern: /\bDROP\s+(TABLE|DATABASE)\b/i,  score: 0.30, label: 'SQL DROP' },
  { pattern: /\bTRUNCATE\b/i,                 score: 0.25, label: 'SQL TRUNCATE' },
];

// Irreversibility patterns
const IRREVERSIBLE_PATTERNS = [
  { pattern: /git\s+push\s+--force/,        score: 0.45, label: 'git force push' },
  { pattern: /git\s+push\s+.*--delete/,     score: 0.40, label: 'git delete remote branch' },
  { pattern: /git\s+reset\s+--hard/,        score: 0.35, label: 'git hard reset' },
  { pattern: /git\s+clean\s+-[a-z]*[f][a-z]*/, score: 0.20, label: 'git clean force' },
  { pattern: /npm\s+(unpublish|deprecate)/,  score: 0.35, label: 'npm unpublish' },
  { pattern: /docker\s+(rmi|system\s+prune)/, score: 0.25, label: 'docker image remove' },
  { pattern: /DROP\s+(TABLE|DATABASE|COLUMN)\b/i, score: 0.35, label: 'schema destruction' },
  { pattern: /DELETE\s+FROM\b(?!.*\bWHERE\b)/i, score: 0.25, label: 'SQL unqualified DELETE' },
  { pattern: /ALTER\s+TABLE.*DROP/i,         score: 0.25, label: 'SQL column drop' },
];

// ─── Scoring Engine ──────────────────────────────────────────────

function clamp(v) { return Math.max(0, Math.min(1, v)); }

/**
 * Assess file path sensitivity from tool input
 */
function fileSensitivityScore(input) {
  let score = 0;
  const hits = [];
  for (const { pattern, score: s, label } of SENSITIVE_PATTERNS) {
    if (pattern.test(input)) {
      score += s;
      hits.push(label);
    }
  }
  return { score: clamp(score), hits };
}

/**
 * Assess blast radius from command patterns
 */
function blastRadiusScore(input) {
  let score = 0;
  const hits = [];
  for (const { pattern, score: s, label } of DESTRUCTIVE_PATTERNS) {
    if (pattern.test(input)) {
      score += s;
      hits.push(label);
    }
  }
  return { score: clamp(score), hits };
}

/**
 * Assess irreversibility from operation patterns
 */
function irreversibilityScore(input) {
  let score = 0;
  const hits = [];
  for (const { pattern, score: s, label } of IRREVERSIBLE_PATTERNS) {
    if (pattern.test(input)) {
      score += s;
      hits.push(label);
    }
  }
  return { score: clamp(score), hits };
}

/**
 * Compute full risk assessment for a tool call
 */
function assessRisk(toolName, input) {
  const base = BASE_TOOL_RISK[toolName] || 0.05;
  const fileRisk = fileSensitivityScore(input || '');
  const blastRisk = blastRadiusScore(input || '');
  const irrevRisk = irreversibilityScore(input || '');

  const total = clamp(base + fileRisk.score + blastRisk.score + irrevRisk.score);

  let action;
  if (total >= THRESHOLDS.block)   action = 'BLOCK';
  else if (total >= THRESHOLDS.confirm) action = 'CONFIRM';
  else if (total >= THRESHOLDS.review)  action = 'REVIEW';
  else action = 'ALLOW';

  const factors = [
    { factor: 'base_tool_risk',   tool: toolName, score: base },
    ...(fileRisk.hits.length > 0 ? [{ factor: 'file_sensitivity', patterns: fileRisk.hits, score: fileRisk.score }] : []),
    ...(blastRisk.hits.length > 0 ? [{ factor: 'blast_radius', patterns: blastRisk.hits, score: blastRisk.score }] : []),
    ...(irrevRisk.hits.length > 0 ? [{ factor: 'irreversibility', patterns: irrevRisk.hits, score: irrevRisk.score }] : []),
  ];

  return {
    score: Math.round(total * 100) / 100,
    action,
    factors,
    thresholds: THRESHOLDS,
    explanation: explain(action, total, factors),
  };
}

function explain(action, score, factors) {
  const factorStr = factors
    .filter(f => f.score > 0)
    .map(f => `${f.factor}: ${(f.score*100).toFixed(0)}%`)
    .join(', ');
  return `Risk: ${(score*100).toFixed(0)}% [${factorStr}] → ${action}`;
}

// ─── CLI ─────────────────────────────────────────────────────────

function main() {
  const args = process.argv.slice(2);
  if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
    console.log(JSON.stringify({
      usage: 'node risk-scorer.js <tool> [input]',
      thresholds: THRESHOLDS,
      actions: 'ALLOW | REVIEW | CONFIRM | BLOCK',
      example: 'node risk-scorer.js Bash "rm -rf /tmp/test"',
    }, null, 2));
    return;
  }

  // JSON mode: single argument is JSON object
  if (args.length === 1 && args[0].startsWith('{')) {
    try {
      const obj = JSON.parse(args[0]);
      const result = assessRisk(obj.tool || obj.tool_name || 'unknown', obj.input || obj.command || '');
      console.log(JSON.stringify(result, null, 2));
      return;
    } catch (e) {
      console.log(JSON.stringify({ error: 'Invalid JSON input', detail: e.message }));
      return;
    }
  }

  // Simple mode: tool + input
  const toolName = args[0];
  const input = args.slice(1).join(' ');
  const result = assessRisk(toolName, input);
  console.log(JSON.stringify(result, null, 2));
}

// ─── Module exports (for programmatic use via ctx_execute) ───────

if (typeof module !== 'undefined') {
  module.exports = { assessRisk, THRESHOLDS, BASE_TOOL_RISK };
}

// Run if executed directly
if (require.main === module) {
  main();
}
