#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const parser = require('@babel/parser');

const args = process.argv.slice(2);
const root = path.resolve(args.find(a => !a.startsWith('--')) || '.');
const useSarif = args.includes('--sarif');
const exts = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs']);
const findings = [];

// .securityignore
const ignorePatterns = ['node_modules', '.git', '.next', 'dist', 'build', 'coverage'];
if (fs.existsSync(path.join(root, '.securityignore'))) {
  const customIgnores = fs.readFileSync(path.join(root, '.securityignore'), 'utf8')
    .split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#'))
    .map(l => l.replace(/\/$/, ''));
  ignorePatterns.push(...customIgnores);
}

function shouldIgnore(p) {
  return ignorePatterns.some(ign => p.includes(ign));
}

function walk(dir, out = []) {
  if (!fs.existsSync(dir)) return out;
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
    const p = path.join(dir, entry.name);
    if (shouldIgnore(p)) continue;
    if (entry.isDirectory()) walk(p, out);
    else out.push(p);
  }
  return out;
}

function addFinding(f) {
  findings.push(f);
}

function scanDependencies() {
  if (fs.existsSync(path.join(root, 'package.json'))) {
    try {
      execSync('npm audit --json', { cwd: root, stdio: ['pipe', 'pipe', 'ignore'] });
    } catch (e) {
      if (e.stdout) {
        try {
          const audit = JSON.parse(e.stdout.toString());
          if (audit.vulnerabilities) {
            for (const [pkg, details] of Object.entries(audit.vulnerabilities)) {
              addFinding({
                title: `Vulnerable dependency: ${pkg}`,
                severity: details.severity === 'critical' || details.severity === 'high' ? 'high' : 'medium',
                confidence: 'high',
                status: 'confirmed',
                evidence: `npm audit reported ${details.severity} vulnerability in ${pkg}`,
                impact: 'May expose application to known CVE exploits',
                affectedFiles: ['package.json'],
                line: 1,
                recommendedFix: `Run npm audit fix or manually update ${pkg}.`
              });
            }
          }
        } catch(err) {}
      }
    }
  }
}

function detectFramework(files) {
  const rels = files.map(f => path.relative(root, f));
  const hasNext = rels.some(f => /(^|\\|\/)next\.config\./.test(f)) || fs.existsSync(path.join(root, 'next.config.js')) || fs.existsSync(path.join(root, 'next.config.ts'));
  const hasApp = rels.some(f => /(^|\\|\/)(src\/)?app\/.+/.test(f));
  const hasPages = rels.some(f => /(^|\\|\/)(src\/)?pages\/.+/.test(f));
  const hasVite = rels.some(f => /vite\.config\./.test(f)) || fs.existsSync(path.join(root, 'vite.config.ts')) || fs.existsSync(path.join(root, 'vite.config.js'));
  if (hasNext && hasApp) return 'nextjs-app-router';
  if (hasNext && hasPages) return 'nextjs-pages-router';
  if (hasNext) return 'nextjs';
  if (hasVite || fs.existsSync(path.join(root, 'public')) || fs.existsSync(path.join(root, 'src'))) return 'react-spa';
  return 'javascript-app';
}

function getIgnoredLines(ast) {
  const ignored = new Set();
  if (ast && ast.comments) {
    for (const comment of ast.comments) {
      if (comment.value.includes('antigravity-disable-next-line')) {
        ignored.add(comment.loc.end.line + 1);
      }
    }
  }
  return ignored;
}

function check(lineText, regex, options) {
  if (regex.test(lineText)) {
    return options; // Returns the finding object delta
  }
  return null;
}

function scanText(file, text) {
  const rel = path.relative(root, file);
  
  let ast;
  let ignoredLines = new Set();
  if (exts.has(path.extname(file))) {
    try {
      ast = parser.parse(text, { sourceType: 'module', plugins: ['jsx', 'typescript'] });
      ignoredLines = getIgnoredLines(ast);
    } catch(e) {}
  }

  const lines = text.split('\n');
  for (let i = 0; i < lines.length; i++) {
    const lineNum = i + 1;
    if (ignoredLines.has(lineNum)) continue;
    const l = lines[i];

    const ctx = l; // keeping it simple, context is the line for regex

    const rules = [
      // antigravity-disable-next-line
      check(ctx, /dangerouslySetInnerHTML/, {
        title: 'Unsafe HTML rendering pattern', severity: 'high', confidence: 'medium', status: 'probable',
        evidence: 'dangerouslySetInnerHTML found', impact: 'Potential XSS if fed untrusted content',
        recommendedFix: 'Sanitize content and restrict sources before rendering HTML.'
      }),
      check(ctx, /(localStorage|sessionStorage)\.(setItem|getItem)\(/, {
        title: 'Token-like data stored in browser storage', severity: 'high', confidence: 'low', status: 'manual-review',
        evidence: 'localStorage/sessionStorage usage', impact: 'Token theft risk via XSS or browser compromise',
        recommendedFix: 'Prefer secure httpOnly cookies for session handling if this is a token.'
      }),
      check(ctx, /process\.env\.[A-Z0-9_]+/, {
        title: 'Potential secret exposure via client environment', severity: 'high', confidence: 'low', status: 'manual-review',
        evidence: 'NEXT_PUBLIC or similar variable found', impact: 'Sensitive values may be exposed to the browser bundle',
        recommendedFix: 'Verify that only public, non-sensitive values use exposed variables.'
      }),
      check(/(SELECT|INSERT|UPDATE|DELETE)[\s\S]{0,120}\$\{/.test(text) ? l : '', /(SELECT|INSERT|UPDATE|DELETE).*\$\{/, { // multi-line approximation via full text
        title: 'Potential SQL injection sink', severity: 'critical', confidence: 'medium', status: 'probable',
        evidence: 'Interpolated SQL or unsafe raw query sink detected', impact: 'Attacker-controlled input may alter database queries',
        recommendedFix: 'Use parameterized queries or safe ORM APIs.'
      }),
      check(ctx, /req\.(body|query)|searchParams|get\(['\"]/, {
        title: 'Server boundary without obvious input validation', severity: 'high', confidence: 'low', status: 'manual-review',
        evidence: 'Request input usage', impact: 'Malformed or hostile input may reach sensitive logic',
        recommendedFix: 'Validate request data at the server boundary with a schema.'
      }),
      check(ctx, /(openai|anthropic|claude|gemini).*(messages\.create|responses\.create|generateContent|chat\.completions)/i, {
        title: 'LLM endpoint with prompt injection exposure', severity: 'high', confidence: 'medium', status: 'probable',
        evidence: 'LLM SDK call', impact: 'Model behavior may be steered to reveal or misuse privileged instructions and tools',
        recommendedFix: 'Separate system instructions, validate tool args, add allowlists, and sanitize user content.'
      }),
      check(ctx, /(fetch|axios\.|got\()/, {
        title: 'Potential SSRF pattern', severity: 'high', confidence: 'low', status: 'manual-review',
        evidence: 'Server-side outbound request', impact: 'Attacker may force calls to internal or unintended hosts',
        recommendedFix: 'Enforce host allowlists and validate URLs before outbound requests.'
      }),
      check(ctx, /(sk_live_|AIza[0-9A-Za-z\-_]{20,}|xox[baprs]-|-----BEGIN (RSA|EC|OPENSSH) PRIVATE KEY-----)/, {
        title: 'Hardcoded credential material', severity: 'critical', confidence: 'high', status: 'confirmed',
        evidence: 'Secret-like token or private key marker detected', impact: 'Credential compromise may permit direct unauthorized access',
        recommendedFix: 'Remove from code, rotate credentials, and load via secure server-side environment management.'
      })
    ];

    for (const res of rules) {
      if (res) {
        addFinding({ ...res, affectedFiles: [rel], line: lineNum });
      }
    }
  }
}

scanDependencies();

const allFiles = walk(root).filter(f => exts.has(path.extname(f)) || path.basename(f) === '.gitignore' || /package(-lock)?\.json$|pnpm-lock\.yaml$|yarn\.lock$|index\.html$|next\.config\./.test(path.basename(f)));
const framework = detectFramework(allFiles);

if (fs.existsSync(path.join(root, '.env')) && fs.existsSync(path.join(root, '.gitignore'))) {
  const gi = fs.readFileSync(path.join(root, '.gitignore'), 'utf8');
  if (!/^\.env(\..*)?$/m.test(gi) && !/\.env/.test(gi)) {
    addFinding({
      title: '.env not ignored by gitignore', severity: 'high', confidence: 'medium', status: 'probable',
      evidence: '.env exists and .gitignore does not obviously ignore it', impact: 'Secrets may be accidentally committed', affectedFiles: ['.env', '.gitignore'], line: 1,
      recommendedFix: 'Ignore .env files and verify they are not tracked in git history.'
    });
  }
}

for (const file of allFiles) {
  try {
    const text = fs.readFileSync(file, 'utf8');
    if (exts.has(path.extname(file))) scanText(file, text);
  } catch (e) {}
}

const summary = findings.reduce((acc, f) => {
  acc.total += 1;
  acc[f.severity] = (acc[f.severity] || 0) + 1;
  return acc;
}, { total: 0, critical: 0, high: 0, medium: 0, low: 0 });

if (useSarif) {
  const sarif = {
    version: "2.1.0",
    $schema: "http://json.schemastore.org/sarif-2.1.0-rtm.5",
    runs: [
      {
        tool: {
          driver: { name: "Security Auditor Antigravity", version: "1.0.0", rules: Object.values(findings.reduce((acc, f) => { acc[f.title] = { id: f.title }; return acc; }, {})) }
        },
        results: findings.map(f => ({
          ruleId: f.title,
          level: f.severity === 'critical' ? 'error' : f.severity === 'high' ? 'error' : f.severity === 'medium' ? 'warning' : 'note',
          message: { text: f.evidence },
          locations: f.affectedFiles.map(file => ({
            physicalLocation: {
              artifactLocation: { uri: file },
              region: { startLine: f.line || 1 }
            }
          }))
        }))
      }
    ]
  };
  console.log(JSON.stringify(sarif, null, 2));
} else {
  console.log(JSON.stringify({ framework, root, summary, findings }, null, 2));
}
