xploitscan 1.0.14 → 1.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/commands/scan.ts","../src/utils/files.ts","../src/utils/config.ts","../src/scanners/custom-rules.ts","../src/scanners/semgrep.ts","../src/scanners/gitleaks.ts","../src/scanners/ai-analyzer.ts","../src/scanners/ai-fp-filter.ts","../src/scanners/ast-analyzer.ts","../src/scanners/osv.ts","../src/scanners/dependency-scanner.ts","../src/scanners/entropy-scanner.ts","../src/scanners/config-analyzer.ts","../src/scanners/multi-file-analyzer.ts","../src/reporters/terminal.ts","../src/reporters/json.ts","../src/reporters/sarif.ts","../src/commands/auth.ts","../src/commands/hook.ts","../src/commands/cursor.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport { scanCommand } from \"./commands/scan.js\";\nimport { loginCommand, logoutCommand, whoamiCommand } from \"./commands/auth.js\";\nimport { installHookCommand, uninstallHookCommand } from \"./commands/hook.js\";\nimport { cursorInstallCommand } from \"./commands/cursor.js\";\n\nconst program = new Command();\n\nprogram\n .name(\"xploitscan\")\n .description(\n \"AI security scanner for vibe-coded apps. Find vulnerabilities before attackers do.\",\n )\n .version(\"1.0.8\");\n\nprogram\n .command(\"scan\")\n .description(\"Scan a directory for security vulnerabilities\")\n .argument(\"[directory]\", \"Directory to scan\", \".\")\n .option(\"--no-ai\", \"Skip AI-powered analysis\")\n .option(\"-f, --format <format>\", \"Output format: terminal, json, sarif\", \"terminal\")\n .option(\"-v, --verbose\", \"Show detailed output\", false)\n .option(\"--diff [base]\", \"Scan only files changed vs base branch (default: main)\")\n .option(\"-w, --watch\", \"Watch for file changes and re-scan automatically\", false)\n .action(async (directory: string, opts: { ai: boolean; format: string; verbose: boolean; diff?: string | boolean; watch: boolean }) => {\n await scanCommand(directory, {\n directory,\n aiAnalysis: opts.ai,\n format: opts.format as \"terminal\" | \"json\" | \"sarif\",\n verbose: opts.verbose,\n diff: opts.diff,\n watch: opts.watch,\n });\n });\n\n// Auth commands\nconst auth = program\n .command(\"auth\")\n .description(\"Manage authentication\");\n\nauth\n .command(\"login\")\n .description(\"Log in to your XploitScan account\")\n .action(loginCommand);\n\nauth\n .command(\"logout\")\n .description(\"Log out of your XploitScan account\")\n .action(logoutCommand);\n\nauth\n .command(\"whoami\")\n .description(\"Show current logged-in user\")\n .action(whoamiCommand);\n\n// Hook commands\nconst hook = program\n .command(\"hook\")\n .description(\"Manage git pre-commit hooks for automatic scanning\");\n\nhook\n .command(\"install\")\n .description(\"Install a git pre-commit hook that runs XploitScan on every commit\")\n .option(\"-f, --force\", \"Overwrite existing XploitScan hook without prompting\", false)\n .action(async (opts: { force?: boolean }) => {\n await installHookCommand({ force: opts.force });\n });\n\nhook\n .command(\"uninstall\")\n .description(\"Remove the XploitScan pre-commit hook\")\n .action(uninstallHookCommand);\n\n// Cursor integration\nconst cursor = program\n .command(\"cursor\")\n .description(\"Manage XploitScan integration with Cursor IDE\");\n\ncursor\n .command(\"install\")\n .description(\"Drop XploitScan security rules into .cursor/rules and .cursorrules so Cursor enforces them at write-time\")\n .option(\"-f, --force\", \"Overwrite existing rule files\", false)\n .option(\"--legacy-only\", \"Only install the legacy .cursorrules file (skip .cursor/rules/*.mdc)\", false)\n .action(async (opts: { force?: boolean; legacyOnly?: boolean }) => {\n await cursorInstallCommand({ force: opts.force, legacyOnly: opts.legacyOnly });\n });\n\n// Upgrade command (shortcut)\nprogram\n .command(\"upgrade\")\n .description(\"Upgrade to XploitScan Pro for unlimited scans\")\n .action(async () => {\n const { getStoredToken, getCheckoutUrl } = await import(\"./utils/api.js\");\n const chalk = (await import(\"chalk\")).default;\n\n const token = getStoredToken();\n if (!token) {\n console.log(chalk.yellow(\"Please log in first: xploitscan auth login\"));\n return;\n }\n\n console.log(chalk.cyan(\"Creating checkout session...\"));\n const url = await getCheckoutUrl();\n if (url) {\n console.log(chalk.green(`\\nOpen this URL to upgrade:`));\n console.log(chalk.bold.underline(url));\n const { execFile } = await import(\"node:child_process\");\n const openCmd = process.platform === \"darwin\" ? \"open\" : process.platform === \"win32\" ? \"start\" : \"xdg-open\";\n execFile(openCmd, [url], () => {});\n } else {\n console.log(chalk.red(\"Failed to create checkout session. Please try again.\"));\n }\n });\n\nprogram.parse();\n","import { resolve, join, relative } from \"node:path\";\nimport { watch as fsWatch } from \"node:fs\";\nimport ora from \"ora\";\nimport chalk from \"chalk\";\nimport type { Finding, ScanOptions, ScanResult } from \"../types.js\";\nimport { collectFiles, readFileContents } from \"../utils/files.js\";\nimport { loadConfig } from \"../utils/config.js\";\nimport { runCustomRules } from \"../scanners/custom-rules.js\";\nimport { runSemgrep } from \"../scanners/semgrep.js\";\nimport { runGitleaks } from \"../scanners/gitleaks.js\";\nimport { analyzeWithAI } from \"../scanners/ai-analyzer.js\";\nimport { filterFalsePositives } from \"../scanners/ai-fp-filter.js\";\nimport { buildASTContext } from \"../scanners/ast-analyzer.js\";\nimport { scanDependencies, scanDependenciesOsv } from \"../scanners/dependency-scanner.js\";\nimport { scanEntropy } from \"../scanners/entropy-scanner.js\";\nimport { scanConfigs } from \"../scanners/config-analyzer.js\";\nimport { scanMultiFile } from \"../scanners/multi-file-analyzer.js\";\nimport { renderTerminalReport } from \"../reporters/terminal.js\";\nimport { renderJsonReport } from \"../reporters/json.js\";\nimport { renderSarifReport } from \"../reporters/sarif.js\";\nimport { checkUsage, incrementUsage, uploadScanResults, isAuthenticated, loadCachedProRules, downloadProRulesBundle } from \"../utils/api.js\";\nimport type { CustomRule } from \"../types.js\";\n\nexport async function scanCommand(\n directory: string,\n options: Partial<ScanOptions>,\n): Promise<void> {\n const dir = resolve(directory || \".\");\n const format = options.format ?? \"terminal\";\n const verbose = options.verbose ?? false;\n const startTime = Date.now();\n\n // Load config\n const config = await loadConfig(dir);\n const useAI = (options.aiAnalysis ?? config.ai ?? true) && !!process.env.ANTHROPIC_API_KEY;\n\n const isSilent = format !== \"terminal\";\n\n // Step 0: Check usage limits and determine rule tier\n let tier: \"free\" | \"pro\" = \"free\";\n let userPlan = \"anonymous\";\n\n if (isAuthenticated()) {\n const usage = await checkUsage();\n userPlan = usage.plan;\n if (usage.plan === \"pro\") {\n tier = \"pro\";\n }\n if (!usage.allowed) {\n console.log(chalk.red(\"\\nDaily scan limit reached.\"));\n console.log(chalk.yellow(\"Upgrade to Pro for unlimited scans: \") + chalk.bold(\"xploitscan upgrade\"));\n console.log(chalk.gray(`Resets tomorrow. Plan: ${usage.plan}\\n`));\n process.exitCode = 1;\n return;\n }\n if (usage.plan === \"free\" && usage.remaining > 0 && usage.remaining <= 2 && !isSilent) {\n console.log(chalk.gray(` ${usage.remaining} free scan${usage.remaining === 1 ? \"\" : \"s\"} remaining today\\n`));\n }\n }\n\n // Step 0b: Load Pro rules from cache or server\n let proRulesExtra: CustomRule[] = [];\n if (tier === \"pro\") {\n const cached = loadCachedProRules() as CustomRule[] | null;\n if (cached) {\n proRulesExtra = cached;\n } else {\n if (!isSilent) console.log(chalk.gray(\" Downloading Pro rules...\"));\n const downloaded = await downloadProRulesBundle();\n if (downloaded) {\n proRulesExtra = (loadCachedProRules() as CustomRule[] | null) || [];\n }\n if (proRulesExtra.length === 0 && !isSilent) {\n console.log(chalk.yellow(\" Could not load Pro rules — scanning with free rules only\"));\n }\n }\n }\n\n // Step 1: Collect files\n const spinner = ora({\n text: \"Scanning files...\",\n color: \"cyan\",\n isSilent,\n }).start();\n\n let files: string[];\n try {\n files = await collectFiles(dir);\n } catch (error) {\n spinner.fail(\"Failed to scan directory\");\n console.error(chalk.red(`Error: ${error instanceof Error ? error.message : error}`));\n process.exit(1);\n }\n\n // Diff mode: filter to only changed files\n if (options.diff) {\n const base = typeof options.diff === \"string\" ? options.diff : \"main\";\n try {\n const { execSync } = await import(\"node:child_process\");\n const changedFiles = execSync(`git diff --name-only ${base}`, { cwd: dir, encoding: \"utf-8\" })\n .trim().split(\"\\n\").filter(Boolean);\n files = files.filter(f => changedFiles.some(cf => f.endsWith(cf) || cf.endsWith(f)));\n if (!isSilent) {\n spinner.info(chalk.gray(` Diff mode: scanning ${files.length} changed files vs ${base}`));\n }\n } catch {\n spinner.warn(\"Could not run git diff — scanning all files\");\n }\n }\n\n if (files.length === 0) {\n spinner.warn(\"No source files found in this directory\");\n return;\n }\n\n spinner.text = `Found ${files.length} files. Running security rules...`;\n\n // Step 2: Run all static scanners in parallel\n const allFindings: Finding[] = [];\n\n // 2a: Custom rules with AST context (always runs, instant)\n const fileContentsForAnalysis: { path: string; content: string }[] = [];\n for (const filePath of files) {\n const content = readFileContents(dir, filePath);\n if (!content) continue;\n fileContentsForAnalysis.push({ path: filePath, content });\n\n // Build AST context for false positive reduction\n const astCtx = buildASTContext(content, filePath);\n\n // Skip scanner files scanning themselves\n if (astCtx.isScannerFile) continue;\n\n // Run custom rules with AST-stripped content for comment-heavy files\n const findings = runCustomRules(content, filePath, config.disableRules, tier, proRulesExtra);\n\n // Add confidence scores based on AST context\n for (const f of findings) {\n if (astCtx.isTestFile) {\n f.confidence = \"low\";\n } else if (astCtx.isConfigFile) {\n f.confidence = \"medium\";\n } else {\n f.confidence = \"high\";\n }\n }\n\n allFindings.push(...findings);\n }\n\n const customCount = allFindings.length;\n if (verbose && customCount > 0) {\n spinner.info(`Custom rules found ${customCount} issues`);\n }\n\n // 2b: Dependency vulnerability scanning (static allowlist + live OSV.dev)\n spinner.text = \"Scanning dependencies...\";\n const depFindings = scanDependencies(fileContentsForAnalysis);\n for (const f of depFindings) { f.confidence = \"high\"; }\n allFindings.push(...depFindings);\n\n // Track which (package, CVE) pairs the static scanner already caught so the\n // OSV pass doesn't double-report\n const dedupeKeys = new Set<string>();\n for (const f of depFindings) {\n // Static findings use rule = CVE id. Extract the package name from the title.\n const match = f.title.match(/^(\\S+)/);\n if (match) dedupeKeys.add(`${match[1].toLowerCase()}:${f.rule}`);\n }\n\n spinner.text = \"Checking dependencies against OSV.dev...\";\n try {\n const osvFindings = await scanDependenciesOsv(fileContentsForAnalysis, dedupeKeys);\n for (const f of osvFindings) { f.confidence = \"high\"; }\n allFindings.push(...osvFindings);\n if (verbose && osvFindings.length > 0) {\n spinner.info(`OSV.dev found ${osvFindings.length} additional vulnerable dependencies`);\n }\n } catch (err) {\n if (verbose) spinner.info(`OSV.dev lookup skipped: ${err instanceof Error ? err.message : \"unknown error\"}`);\n }\n\n if (verbose && depFindings.length > 0) {\n spinner.info(`Dependency scanner found ${depFindings.length} issues`);\n }\n\n // 2c: Entropy-based secret detection\n spinner.text = \"Scanning for high-entropy secrets...\";\n const entropyFindings = scanEntropy(fileContentsForAnalysis);\n for (const f of entropyFindings) { f.confidence = \"medium\"; }\n allFindings.push(...entropyFindings);\n if (verbose && entropyFindings.length > 0) {\n spinner.info(`Entropy scanner found ${entropyFindings.length} potential secrets`);\n }\n\n // 2d: Configuration file deep analysis\n spinner.text = \"Analyzing configuration files...\";\n const configFindings = scanConfigs(fileContentsForAnalysis);\n for (const f of configFindings) { f.confidence = \"high\"; }\n allFindings.push(...configFindings);\n if (verbose && configFindings.length > 0) {\n spinner.info(`Config analyzer found ${configFindings.length} issues`);\n }\n\n // 2e: Multi-file cross-reference analysis\n spinner.text = \"Running cross-file analysis...\";\n const multiFileFindings = scanMultiFile(fileContentsForAnalysis);\n for (const f of multiFileFindings) { f.confidence = \"medium\"; }\n allFindings.push(...multiFileFindings);\n if (verbose && multiFileFindings.length > 0) {\n spinner.info(`Multi-file analyzer found ${multiFileFindings.length} issues`);\n }\n\n // 2f: Semgrep + Gitleaks (run in parallel, gracefully skip if not installed)\n spinner.text = \"Running external scanners...\";\n spinner.color = \"yellow\";\n\n // Resolve custom rules directory (shipped with xploitscan)\n const rulesDir = resolve(join(import.meta.dirname, \"../../rules\"));\n const fallbackRulesDir = resolve(join(dir, \"../rules\"));\n\n const [semgrepResult, gitleaksResult] = await Promise.allSettled([\n runSemgrep(dir, rulesDir).catch(() => runSemgrep(dir, fallbackRulesDir)).catch(() => ({ findings: [] as Finding[], available: false })),\n runGitleaks(dir).catch(() => ({ findings: [] as Finding[], available: false })),\n ]);\n\n const semgrep = semgrepResult.status === \"fulfilled\" ? semgrepResult.value : { findings: [], available: false };\n const gitleaks = gitleaksResult.status === \"fulfilled\" ? gitleaksResult.value : { findings: [], available: false };\n\n allFindings.push(...semgrep.findings);\n allFindings.push(...gitleaks.findings);\n\n if (verbose) {\n if (semgrep.available) {\n spinner.info(`Semgrep found ${semgrep.findings.length} issues`);\n } else {\n spinner.info(chalk.gray(\"Semgrep not installed — install with: pip install semgrep\"));\n }\n if (gitleaks.available) {\n spinner.info(`Gitleaks found ${gitleaks.findings.length} issues`);\n } else {\n spinner.info(chalk.gray(\"Gitleaks not installed — install with: brew install gitleaks\"));\n }\n }\n\n // Show install hints for missing tools (non-verbose, terminal only)\n if (!isSilent && !verbose) {\n const missing: string[] = [];\n if (!semgrep.available) missing.push(\"semgrep (pip install semgrep)\");\n if (!gitleaks.available) missing.push(\"gitleaks (brew install gitleaks)\");\n if (missing.length > 0) {\n spinner.info(chalk.gray(`Optional: install ${missing.join(\" and \")} for deeper scanning`));\n }\n }\n\n const staticCount = allFindings.length;\n spinner.text = `Static analysis found ${staticCount} issue${staticCount !== 1 ? \"s\" : \"\"}`;\n\n // Step 3: AI analysis (if enabled)\n if (useAI) {\n spinner.text = \"Running AI analysis...\";\n spinner.color = \"magenta\";\n\n try {\n const priorityFiles = files\n .map((path) => ({\n path,\n content: readFileContents(dir, path) ?? \"\",\n }))\n .filter((f) => f.content.length > 0 && f.content.length < 30_000)\n .filter((f) => {\n const isHighPriority =\n /(?:api|server|route|auth|middleware|webhook|payment|stripe|supabase)/i.test(f.path) ||\n allFindings.some((finding) => finding.file === f.path) ||\n /(?:query|execute|fetch|prisma|drizzle|mongoose)/i.test(f.content);\n return isHighPriority;\n })\n .slice(0, 20);\n\n if (priorityFiles.length > 0) {\n const aiFindings = await analyzeWithAI(priorityFiles, allFindings);\n allFindings.push(...aiFindings);\n\n if (verbose && aiFindings.length > 0) {\n spinner.info(`AI analysis found ${aiFindings.length} additional issues`);\n }\n }\n } catch (error) {\n if (error instanceof Error) {\n spinner.warn(`AI analysis skipped: ${error.message}`);\n }\n }\n } else if (!process.env.ANTHROPIC_API_KEY && !isSilent) {\n spinner.info(\n chalk.gray(\"Tip: Set ANTHROPIC_API_KEY for AI-powered contextual analysis\"),\n );\n }\n\n // Step 3b: AI false positive filter (if API key present)\n // Reviews regex findings with Claude Haiku to remove false positives.\n // Separate from --no-ai (which disables finding generation). This filter\n // only reviews existing findings, it doesn't generate new ones.\n let aiReviewed = false;\n let aiRemovedCount = 0;\n let aiTotalBefore = 0;\n let aiFilteredFindings: Array<{ finding: Finding; reason: string }> = [];\n if (process.env.ANTHROPIC_API_KEY && allFindings.length > 0) {\n spinner.text = \"AI reviewing findings for false positives...\";\n spinner.color = \"cyan\";\n try {\n const fileContentsMap = new Map<string, string>();\n for (const f of allFindings) {\n if (!fileContentsMap.has(f.file)) {\n const content = readFileContents(dir, f.file);\n if (content) fileContentsMap.set(f.file, content);\n }\n }\n const result = await filterFalsePositives(allFindings, fileContentsMap);\n aiReviewed = result.aiReviewed;\n aiRemovedCount = result.removedCount;\n aiTotalBefore = result.totalBefore;\n aiFilteredFindings = result.filteredFindings;\n if (result.removedCount > 0) {\n allFindings.length = 0;\n allFindings.push(...result.findings);\n }\n } catch {\n // AI filter error — keep all findings (graceful degradation)\n }\n }\n\n spinner.stop();\n\n // Step 4: Deduplicate findings (same file + line + similar rule)\n const seen = new Set<string>();\n const dedupedFindings = allFindings.filter((f) => {\n // Normalize: group by file:line and a simplified rule key\n const ruleKey = f.source === \"gitleaks\" ? `secret:${f.file}:${f.line}` : `${f.rule}:${f.file}:${f.line}`;\n if (seen.has(ruleKey)) return false;\n seen.add(ruleKey);\n return true;\n });\n\n // Step 5: Render results\n const result: ScanResult = {\n findings: dedupedFindings,\n filesScanned: files.length,\n duration: Date.now() - startTime,\n timestamp: new Date().toISOString(),\n directory: dir,\n };\n\n switch (format) {\n case \"json\":\n renderJsonReport(result);\n break;\n case \"sarif\":\n renderSarifReport(result);\n break;\n default:\n renderTerminalReport(result, fileContentsForAnalysis);\n break;\n }\n\n // Step 5b: Show AI review status\n if (aiReviewed && !isSilent) {\n console.log(\"\");\n if (aiRemovedCount > 0) {\n if (dedupedFindings.length > 0) {\n console.log(chalk.cyan(\"🤖 AI Review: \") + chalk.white(`${aiRemovedCount} false positive${aiRemovedCount !== 1 ? \"s\" : \"\"} removed · `) + chalk.green(`${dedupedFindings.length} real issue${dedupedFindings.length !== 1 ? \"s\" : \"\"} confirmed`));\n } else {\n console.log(chalk.cyan(\"🤖 AI Review: \") + chalk.green(`clean scan`) + chalk.gray(` (${aiRemovedCount} false positive${aiRemovedCount !== 1 ? \"s\" : \"\"} removed)`));\n }\n } else {\n console.log(chalk.cyan(\"🤖 AI Review: \") + chalk.green(`all ${dedupedFindings.length} finding${dedupedFindings.length !== 1 ? \"s\" : \"\"} confirmed real`));\n }\n // Show filtered findings if --verbose or if user wants detail\n if (verbose && aiFilteredFindings.length > 0) {\n console.log(chalk.gray(`\\n Filtered findings (AI determined these are false positives):`));\n for (const { finding: f, reason } of aiFilteredFindings) {\n console.log(chalk.gray(` ${f.severity.toUpperCase().padEnd(8)} ${f.rule} ${f.file}:${f.line}`));\n console.log(chalk.gray(` Reason: ${reason}`));\n }\n }\n }\n\n // Step 5c: Show upsell for free tier\n if (tier === \"free\" && !isSilent) {\n console.log(\"\");\n if (userPlan === \"anonymous\") {\n console.log(chalk.gray(\" Scanned with 30 free rules.\") + chalk.cyan(\" Log in to unlock all 158 rules →\") + chalk.bold(\" xploitscan auth login\"));\n } else {\n console.log(chalk.gray(\" Scanned with 30 rules.\") + chalk.cyan(\" Upgrade to Pro for all 158 rules →\") + chalk.bold(\" xploitscan upgrade\"));\n }\n console.log(\"\");\n }\n\n // Step 6: Upload results and increment usage (if authenticated)\n if (isAuthenticated()) {\n await Promise.allSettled([\n incrementUsage(),\n uploadScanResults({\n directory: dir,\n filesScanned: files.length,\n findings: dedupedFindings,\n duration: Date.now() - startTime,\n }),\n ]);\n }\n\n // Exit with error code if critical/high findings exist\n const hasCritical = dedupedFindings.some(\n (f) => f.severity === \"critical\" || f.severity === \"high\",\n );\n if (hasCritical) {\n process.exitCode = 1;\n }\n\n // Step 7: Watch mode\n if (options.watch) {\n const watchExclude = new Set([\"node_modules\", \".git\", \"dist\", \"build\", \".next\"]);\n const debounceMs = 500;\n let debounceTimer: ReturnType<typeof setTimeout> | null = null;\n let isScanning = false;\n\n console.log(chalk.cyan(\"\\nWatching for changes... (press Ctrl+C to stop)\"));\n\n fsWatch(dir, { recursive: true }, (_event, filename) => {\n if (!filename) return;\n\n // Skip excluded directories\n const parts = filename.split(\"/\");\n if (parts.some((p) => watchExclude.has(p))) return;\n\n console.log(chalk.gray(`File changed: ${filename}`));\n\n if (debounceTimer) clearTimeout(debounceTimer);\n\n debounceTimer = setTimeout(async () => {\n if (isScanning) return;\n isScanning = true;\n\n console.log(chalk.cyan(\"\\nRe-scanning...\\n\"));\n try {\n await scanCommand(directory, {\n ...options,\n watch: false, // prevent recursive watch\n });\n } catch (error) {\n console.error(chalk.red(`Re-scan error: ${error instanceof Error ? error.message : error}`));\n } finally {\n isScanning = false;\n console.log(chalk.cyan(\"\\nWatching for changes... (press Ctrl+C to stop)\"));\n }\n }, debounceMs);\n });\n\n // Keep the process alive\n await new Promise(() => {});\n }\n}\n","import fg from \"fast-glob\";\nimport ignore from \"ignore\";\nimport { readFileSync, existsSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\n\nconst SOURCE_EXTENSIONS = [\n \"js\", \"jsx\", \"ts\", \"tsx\", \"mjs\", \"cjs\",\n \"vue\", \"svelte\", \"astro\",\n \"py\", \"rb\", \"go\", \"rs\", \"java\", \"php\",\n \"swift\", \"kt\", \"kts\", \"dart\", \"cs\",\n \"c\", \"cpp\", \"h\",\n \"sh\", \"bash\", \"zsh\",\n \"env\", \"yaml\", \"yml\", \"toml\", \"json\", \"xml\",\n \"html\", \"htm\", \"sql\",\n \"properties\", \"ini\", \"cfg\", \"conf\",\n \"tf\", \"hcl\", \"dockerfile\",\n \"erb\", \"jinja\", \"j2\",\n \"gradle\",\n \"r\", \"lua\", \"pl\", \"pm\", \"ex\", \"exs\",\n \"ipynb\", \"md\",\n \"prisma\", \"plist\", \"pbxproj\", \"entitlements\", \"rules\", \"csv\",\n];\n\nconst SOURCE_FILENAMES = [\n \"Dockerfile\", \"Makefile\", \"Gemfile\", \"Rakefile\",\n \"package.json\", \"Cargo.toml\", \"go.mod\", \"requirements.txt\", \"Pipfile\",\n \"next.config.js\", \"next.config.mjs\", \"next.config.ts\", \"vercel.json\",\n \"firebase.json\", \".firebaserc\", \"firestore.rules\",\n \"app.json\", \"app.config.js\", \"eas.json\",\n \"wrangler.toml\", \"netlify.toml\",\n \"drizzle.config.ts\", \"drizzle.config.js\",\n \"Procfile\", \"Caddyfile\", \"nginx.conf\",\n \"AndroidManifest.xml\",\n];\n\nconst ALWAYS_IGNORE = [\n \"node_modules\",\n \".git\",\n \"dist\",\n \"build\",\n \".next\",\n \".nuxt\",\n \".svelte-kit\",\n \"vendor\",\n \"__pycache__\",\n \".venv\",\n \"venv\",\n \"coverage\",\n \".turbo\",\n \"*.min.js\",\n \"*.min.css\",\n \"*.map\",\n \"package-lock.json\",\n \"pnpm-lock.yaml\",\n \"yarn.lock\",\n];\n\nexport async function collectFiles(directory: string): Promise<string[]> {\n const ig = ignore.default();\n\n // Load .gitignore if present\n const gitignorePath = join(directory, \".gitignore\");\n if (existsSync(gitignorePath)) {\n const gitignoreContent = readFileSync(gitignorePath, \"utf-8\");\n ig.add(gitignoreContent);\n }\n\n // Load .xploitscanignore if present\n const xploitscanIgnorePath = join(directory, \".xploitscanignore\");\n if (existsSync(xploitscanIgnorePath)) {\n const xploitscanIgnoreContent = readFileSync(xploitscanIgnorePath, \"utf-8\");\n ig.add(xploitscanIgnoreContent);\n }\n\n // Always ignore these\n ig.add(ALWAYS_IGNORE);\n\n const patterns = SOURCE_EXTENSIONS.map((ext) => `**/*.${ext}`);\n // Also grab dotfiles like .env, .env.local, etc.\n patterns.push(\"**/.env*\");\n // Also grab files matched by name (Dockerfile, Makefile, etc.)\n for (const name of SOURCE_FILENAMES) {\n patterns.push(`**/${name}`);\n }\n\n const files = await fg(patterns, {\n cwd: directory,\n absolute: false,\n dot: true,\n onlyFiles: true,\n ignore: ALWAYS_IGNORE.map((p) => `**/${p}`),\n });\n\n // Apply .gitignore filtering\n return files.filter((file) => !ig.ignores(file));\n}\n\nexport function readFileContents(\n directory: string,\n filePath: string,\n): string | null {\n try {\n const fullPath = resolve(join(directory, filePath));\n if (!fullPath.startsWith(resolve(directory))) {\n throw new Error(\"Path traversal detected\");\n }\n return readFileSync(fullPath, \"utf-8\");\n } catch {\n return null;\n }\n}\n\nexport function getSnippet(\n content: string,\n line: number,\n contextLines = 2,\n): string {\n const lines = content.split(\"\\n\");\n const start = Math.max(0, line - 1 - contextLines);\n const end = Math.min(lines.length, line + contextLines);\n\n return lines\n .slice(start, end)\n .map((l, i) => {\n const lineNum = start + i + 1;\n const marker = lineNum === line ? \">\" : \" \";\n return `${marker} ${lineNum.toString().padStart(4)} | ${l}`;\n })\n .join(\"\\n\");\n}\n","import { cosmiconfig } from \"cosmiconfig\";\nimport { readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\nexport interface XploitScanRcConfig {\n rules?: {\n disable?: string[];\n severityOverride?: Record<string, string>;\n };\n scan?: {\n exclude?: string[];\n maxFileSize?: number;\n extensions?: string[];\n };\n output?: {\n format?: \"terminal\" | \"json\" | \"sarif\";\n verbose?: boolean;\n };\n watch?: {\n debounce?: number;\n exclude?: string[];\n };\n}\n\nexport interface XploitScanConfig {\n /** Glob patterns to exclude from scanning */\n exclude?: string[];\n /** Enable/disable AI analysis (requires ANTHROPIC_API_KEY) */\n ai?: boolean;\n /** Severity threshold to report: only show findings at or above this level */\n severity?: \"critical\" | \"high\" | \"medium\" | \"low\" | \"info\";\n /** Custom rules to disable by ID */\n disableRules?: string[];\n /** Severity overrides by rule ID */\n severityOverride?: Record<string, string>;\n /** Max file size in bytes */\n maxFileSize?: number;\n /** Additional file extensions to scan */\n extensions?: string[];\n /** Output format */\n format?: \"terminal\" | \"json\" | \"sarif\";\n /** Verbose output */\n verbose?: boolean;\n /** Watch mode config */\n watch?: {\n debounce?: number;\n exclude?: string[];\n };\n}\n\nconst defaults: XploitScanConfig = {\n exclude: [],\n ai: true,\n severity: \"low\",\n disableRules: [],\n};\n\nfunction loadXploitscanRc(directory: string): XploitScanRcConfig | null {\n try {\n const rcPath = join(directory, \".xploitscanrc\");\n const content = readFileSync(rcPath, \"utf-8\");\n return JSON.parse(content) as XploitScanRcConfig;\n } catch {\n return null;\n }\n}\n\nfunction mergeRcIntoConfig(\n base: XploitScanConfig,\n rc: XploitScanRcConfig,\n): XploitScanConfig {\n const merged = { ...base };\n\n // Merge rules\n if (rc.rules) {\n if (rc.rules.disable) {\n merged.disableRules = [\n ...new Set([...(merged.disableRules ?? []), ...rc.rules.disable]),\n ];\n }\n if (rc.rules.severityOverride) {\n merged.severityOverride = {\n ...(merged.severityOverride ?? {}),\n ...rc.rules.severityOverride,\n };\n }\n }\n\n // Merge scan settings\n if (rc.scan) {\n if (rc.scan.exclude) {\n merged.exclude = [\n ...new Set([...(merged.exclude ?? []), ...rc.scan.exclude]),\n ];\n }\n if (rc.scan.maxFileSize !== undefined) {\n merged.maxFileSize = rc.scan.maxFileSize;\n }\n if (rc.scan.extensions) {\n merged.extensions = [\n ...new Set([...(merged.extensions ?? []), ...rc.scan.extensions]),\n ];\n }\n }\n\n // Merge output settings (rc takes priority)\n if (rc.output) {\n if (rc.output.format !== undefined) {\n merged.format = rc.output.format;\n }\n if (rc.output.verbose !== undefined) {\n merged.verbose = rc.output.verbose;\n }\n }\n\n // Merge watch settings\n if (rc.watch) {\n merged.watch = {\n ...(merged.watch ?? {}),\n ...rc.watch,\n };\n }\n\n return merged;\n}\n\nexport async function loadConfig(\n directory: string,\n): Promise<XploitScanConfig> {\n const explorer = cosmiconfig(\"xploitscan\");\n\n let config = { ...defaults };\n\n try {\n const result = await explorer.search(directory);\n if (result && result.config) {\n config = { ...defaults, ...result.config };\n }\n } catch {\n // Config loading failed, use defaults\n }\n\n // Load .xploitscanrc and merge (rc takes priority)\n const rc = loadXploitscanRc(directory);\n if (rc) {\n config = mergeRcIntoConfig(config, rc);\n }\n\n return config;\n}\n","import type { CustomRule, Finding, RuleMatch } from \"../types.js\";\nimport { readFileContents, getSnippet } from \"../utils/files.js\";\n\n// ────────────────────────────────────────────\n// GLOBAL PRE-FILTERS\n// Reduces false positives before any rule runs\n// ────────────────────────────────────────────\n\n// Broad test/mock/fixture file detection\nconst TEST_FILE_PATTERN = /(?:\\.test\\.|\\.spec\\.|__tests__|__mocks__|\\.stories\\.|\\.story\\.|\\/test\\/|\\/tests\\/|\\/fixtures?\\/|\\/mocks?\\/|\\.mock\\.|test-utils|testing|\\.cy\\.|\\.e2e\\.)/i;\n\nfunction isTestFile(filePath: string): boolean {\n return TEST_FILE_PATTERN.test(filePath);\n}\n\n// Check if a match falls on a comment line (JS/TS/Python/Ruby/YAML/HTML)\nfunction isCommentLine(content: string, matchIndex: number): boolean {\n const lineStart = content.lastIndexOf(\"\\n\", matchIndex - 1) + 1;\n const lineText = content.substring(lineStart, content.indexOf(\"\\n\", matchIndex)).trimStart();\n return (\n lineText.startsWith(\"//\") ||\n lineText.startsWith(\"#\") ||\n lineText.startsWith(\"*\") ||\n lineText.startsWith(\"/*\") ||\n lineText.startsWith(\"<!--\") ||\n lineText.startsWith(\"'\") && lineText.length > 1 && /\\.(vb|bas)$/i.test(\"\") // VB comments\n );\n}\n\n// Check if a match is inside a string literal that's a fix/description message\n// (e.g., inside a findMatches callback or rule description)\nfunction isInsideFixMessage(content: string, matchIndex: number): boolean {\n const lineStart = content.lastIndexOf(\"\\n\", matchIndex - 1) + 1;\n const lineText = content.substring(lineStart, content.indexOf(\"\\n\", matchIndex));\n // Skip if the line looks like a fix suggestion string or rule description\n return /(?:fix|description|message|suggestion|hint|help|example|doc|comment)\\s*[:=(]/i.test(lineText) ||\n /return\\s*[\"'`].*(?:Use|Replace|Add|Move|Set|Enable|Disable|Never|Don't|Do not|Instead)/i.test(lineText);\n}\n\n// Context-aware matching: checks if mitigation exists within N lines after the match\nfunction hasMitigationNearby(content: string, matchIndex: number, mitigationPattern: RegExp, linesAhead: number = 5): boolean {\n const lines = content.split(\"\\n\");\n const matchLine = content.substring(0, matchIndex).split(\"\\n\").length - 1;\n const endLine = Math.min(matchLine + linesAhead, lines.length - 1);\n const nearbyContent = lines.slice(matchLine, endLine + 1).join(\"\\n\");\n return mitigationPattern.test(nearbyContent);\n}\n\n// Helper to find all regex matches with line numbers\n// Automatically skips matches on comment lines and fix messages\nfunction findMatches(\n content: string,\n pattern: RegExp,\n rule: Omit<CustomRule, \"check\">,\n filePath: string,\n fixTemplate?: (match: RegExpExecArray) => string,\n): RuleMatch[] {\n const matches: RuleMatch[] = [];\n const lines = content.split(\"\\n\");\n let m: RegExpExecArray | null;\n const re = new RegExp(pattern.source, pattern.flags.includes(\"g\") ? pattern.flags : `${pattern.flags}g`);\n\n while ((m = re.exec(content)) !== null) {\n // Skip matches on comment lines\n if (isCommentLine(content, m.index)) continue;\n // Skip matches inside fix/description strings\n if (isInsideFixMessage(content, m.index)) continue;\n\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n matches.push({\n rule: rule.id,\n title: rule.title,\n severity: rule.severity,\n category: rule.category,\n file: filePath,\n line: lineNum,\n snippet: getSnippet(content, lineNum),\n fix: fixTemplate?.(m),\n });\n }\n\n return matches;\n}\n\n// ────────────────────────────────────────────\n// RULE DEFINITIONS\n// ────────────────────────────────────────────\n\nexport const hardcodedSecrets: CustomRule = {\n id: \"VC001\",\n title: \"Hardcoded API Key or Secret\",\n severity: \"critical\",\n category: \"Secrets\",\n description: \"API keys, tokens, or secrets hardcoded in source code can be extracted by anyone with access to the code.\",\n check(content, filePath) {\n // Skip .env.example, template, test, and documentation files\n if (filePath.endsWith(\".example\") || filePath.endsWith(\".template\")) return [];\n if (isTestFile(filePath)) return [];\n if (filePath.match(/\\.(md|txt|rst|adoc)$/)) return [];\n\n const patterns = [\n // Generic API key patterns — require actual value assignment, not variable declarations\n /(?:api[_-]?key|apikey|api[_-]?secret)\\s*[:=]\\s*[\"'`]([a-zA-Z0-9_\\-]{20,})[\"'`]/gi,\n // AWS keys\n /(?:AKIA|ABIA|ACCA|ASIA)[A-Z0-9]{16}/g,\n // Stripe keys\n /(?:sk_live|pk_live|sk_test|pk_test)_[a-zA-Z0-9]{20,}/g,\n // Supabase anon/service keys (JWT format)\n /(?:supabase[_-]?(?:anon|service)[_-]?key|SUPABASE_(?:ANON|SERVICE_ROLE)_KEY)\\s*[:=]\\s*[\"'`](eyJ[a-zA-Z0-9_-]{50,})[\"'`]/gi,\n // OpenAI keys\n /sk-[a-zA-Z0-9]{20,}T3BlbkFJ[a-zA-Z0-9]{20,}/g,\n // Generic tokens in assignments — require standalone word and longer min length\n /(?:^|[\\s,({])(?:token|secret|password|passwd|pwd)\\s*[:=]\\s*[\"'`]([a-zA-Z0-9_\\-!@#$%^&*]{20,})[\"'`]/gim,\n // Private keys\n /-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----/g,\n // Database URLs with credentials\n /(?:postgres|mysql|mongodb(?:\\+srv)?):\\/\\/[^:]+:[^@]+@[^/\\s\"'`]+/gi,\n ];\n\n // Pattern-specific fix messages with risk context\n const fixMessages: string[] = [\n \"Move this API key to an environment variable. If this key has been committed, rotate it immediately — it may have already been scraped by bots.\",\n \"AWS access key detected — may grant full account access (EC2, S3, IAM, billing). Rotate immediately in AWS Console → IAM → Security Credentials. Use IAM roles or environment variables instead.\",\n \"Stripe key detected. sk_live_ keys can process real charges, issue refunds, and access customer payment data. sk_test_ keys are lower risk but should still not be committed. Rotate in Stripe Dashboard → Developers → API Keys.\",\n \"Supabase key detected. Service role keys bypass Row Level Security and grant full database read/write access. Move to a server-side environment variable immediately.\",\n \"OpenAI API key detected — grants full API access and can incur charges. Rotate at platform.openai.com → API Keys.\",\n \"Hardcoded token or password detected. Move to an environment variable and rotate the credential if it has been committed to version control.\",\n \"Private key found in source code. If this has been committed to version control, consider the key compromised — generate a new key pair and revoke the old one.\",\n \"Database credentials in connection string. An attacker with this URL has full database access. Move to an environment variable, restrict network access, and rotate the password.\",\n ];\n\n const matches: RuleMatch[] = [];\n for (let pi = 0; pi < patterns.length; pi++) {\n const pattern = patterns[pi];\n const rawMatches = findMatches(content, pattern, hardcodedSecrets, filePath, () => fixMessages[pi]);\n // Skip matches where the matched secret value is less than 12 characters\n for (const rm of rawMatches) {\n const lineText = content.split(\"\\n\")[rm.line - 1] || \"\";\n // Skip comment lines (additional guard beyond findMatches)\n const trimmed = lineText.trimStart();\n if (trimmed.startsWith(\"//\") || trimmed.startsWith(\"#\")) continue;\n // Extract the secret value from the match and skip short ones\n const secretMatch = lineText.match(/[:=]\\s*[\"'`]([^\"'`]*)[\"'`]/);\n if (secretMatch && secretMatch[1].length < 12) continue;\n matches.push(rm);\n }\n }\n return matches;\n },\n};\n\nexport const exposedEnvFile: CustomRule = {\n id: \"VC002\",\n title: \"Environment File May Be Committed\",\n severity: \"high\",\n category: \"Secrets\",\n description: \".env files containing secrets may be committed to version control.\",\n check(content, filePath) {\n // Only applies to .env files (not .env.example)\n if (!filePath.match(/\\.env(?:\\.[a-z]+)?$/) || filePath.includes(\"example\")) return [];\n\n const hasSecrets = /(?:KEY|SECRET|TOKEN|PASSWORD|PRIVATE|DATABASE_URL)\\s*=/i.test(content);\n if (!hasSecrets) return [];\n\n return [{\n rule: \"VC002\",\n title: exposedEnvFile.title,\n severity: \"high\",\n category: \"Secrets\",\n file: filePath,\n line: 1,\n snippet: getSnippet(content, 1),\n fix: 'Add \".env*\" to your .gitignore file and remove this file from git history with: git rm --cached ' + filePath,\n }];\n },\n};\n\nexport const missingAuthMiddleware: CustomRule = {\n id: \"VC003\",\n title: \"API Route Missing Authentication\",\n severity: \"high\",\n category: \"Authentication\",\n description: \"API routes without authentication checks allow unauthorized access.\",\n check(content, filePath) {\n // Only check API route files\n const isApiRoute = /(?:\\/api\\/|routes?\\/|controllers?\\/|endpoints?\\/)/.test(filePath) ||\n filePath.includes(\"server.\");\n if (!isApiRoute) return [];\n\n // Look for route handlers without auth checks\n const routePatterns = [\n // Express/Hono style\n /\\.(get|post|put|patch|delete)\\s*\\(\\s*[\"'`][^\"'`]+[\"'`]\\s*,\\s*(?:async\\s+)?\\(?(?:req|c|ctx)/gi,\n // Next.js API routes\n /export\\s+(?:async\\s+)?function\\s+(?:GET|POST|PUT|PATCH|DELETE)\\s*\\(/gi,\n ];\n\n // Skip test files\n if (isTestFile(filePath)) return [];\n // Skip non-code files\n if (filePath.match(/\\.(md|txt|rst|html|css|json|yaml|yml)$/)) return [];\n\n // Skip routes that are intentionally unauthenticated\n const isAuthRoute = /\\/auth\\/|\\/login|\\/signup|\\/register|\\/logout|\\/password\\/|\\/forgot|\\/reset/i.test(filePath);\n if (isAuthRoute) return [];\n\n // Skip webhook receivers (they use their own auth: HMAC, shared secrets, etc.)\n const isWebhookRoute = /\\/webhook/i.test(filePath);\n if (isWebhookRoute) return [];\n\n const authPatterns = [\n /auth/i, /session/i, /jwt/i, /bearer/i, /middleware/i,\n /getUser/i, /currentUser/i, /isAuthenticated/i, /requireAuth/i,\n /requireUser/i, /requireUserForApi/i,\n /clerk/i, /supabase\\.auth/i, /getServerSession/i, /getToken/i,\n /protect/i, /guard/i, /verifyToken/i, /validateToken/i, /withAuth/i,\n /passport/i, /firebase\\.auth/i, /cognito/i,\n /verifyCronSecret/i, /verifySecret/i, /checkApiKey/i,\n ];\n\n const hasAuth = authPatterns.some((p) => p.test(content));\n if (hasAuth) return [];\n\n const matches: RuleMatch[] = [];\n for (const pattern of routePatterns) {\n matches.push(\n ...findMatches(content, pattern, missingAuthMiddleware, filePath, () =>\n \"Add authentication middleware to protect this route. Check the user's session/token before processing the request.\"\n ),\n );\n }\n return matches;\n },\n};\n\nexport const supabaseNoRLS: CustomRule = {\n id: \"VC004\",\n title: \"Supabase Client Without Row Level Security\",\n severity: \"critical\",\n category: \"Authorization\",\n description: \"Using Supabase with the service role key or bypassing RLS exposes all database rows to any user.\",\n check(content, filePath) {\n const matches: RuleMatch[] = [];\n\n // Service role key used in client-side code\n if (\n /supabase_service_role|service_role_key/i.test(content) &&\n (/[\"']use client[\"']/.test(content) || filePath.match(/\\.(jsx|tsx|vue|svelte)$/))\n ) {\n matches.push(\n ...findMatches(\n content,\n /service_role/gi,\n supabaseNoRLS,\n filePath,\n () => \"Never expose the service_role key in client-side code. Use the anon key with RLS policies instead.\",\n ),\n );\n }\n\n // .rpc() or direct table access without .auth\n if (/createClient/i.test(content) && /\\.from\\(/.test(content)) {\n const hasRLSBypass = /\\.rpc\\(|auth\\.admin|service_role/i.test(content);\n if (hasRLSBypass) {\n matches.push(\n ...findMatches(\n content,\n /\\.rpc\\(|auth\\.admin/gi,\n { ...supabaseNoRLS, title: \"Supabase RLS Bypass Detected\" },\n filePath,\n () => \"Ensure RLS policies are enabled on all tables and avoid bypassing them with service_role or admin methods in user-facing code.\",\n ),\n );\n }\n }\n\n return matches;\n },\n};\n\nexport const stripeWebhookUnprotected: CustomRule = {\n id: \"VC005\",\n title: \"Unprotected Stripe Webhook Endpoint\",\n severity: \"critical\",\n category: \"Payment Security\",\n description: \"Stripe webhook endpoints without signature verification allow attackers to fake payment events.\",\n check(content, filePath) {\n // Only scan code files, never docs/README/markdown/lock files\n if (!filePath.match(/\\.(js|ts|jsx|tsx|py|rb|go|java|php)$/)) return [];\n if (isTestFile(filePath)) return [];\n\n // Must actually reference Stripe (not just generic \"webhook\")\n if (!/stripe/i.test(content)) return [];\n\n // Must have a webhook handler pattern (route definition or event handling)\n const hasStripeWebhookHandler =\n /(?:stripe.*webhook|webhook.*stripe)/i.test(content) ||\n /(?:checkout\\.session\\.completed|invoice\\.paid|payment_intent|customer\\.subscription)/i.test(content);\n if (!hasStripeWebhookHandler) return [];\n\n // Already has signature verification — safe\n if (/constructEvent|verifyHeader|stripe[_-]?signature|webhook[_-]?secret|STRIPE_WEBHOOK_SECRET/i.test(content)) return [];\n\n // Find the actual route handler, not every mention of \"webhook\"\n const handlerPatterns = [\n // POST handler that processes Stripe events\n /export\\s+(?:async\\s+)?function\\s+POST\\s*\\(/g,\n // Express-style Stripe webhook route\n /\\.(post|all)\\s*\\(\\s*[\"'`][^\"'`]*(?:stripe|webhook)[^\"'`]*[\"'`]/gi,\n // Event type checking without prior verification\n /(?:event\\.type|req\\.body\\.type)\\s*===?\\s*[\"'`](?:checkout|invoice|payment|customer)\\./g,\n ];\n\n const matches: RuleMatch[] = [];\n for (const pattern of handlerPatterns) {\n matches.push(\n ...findMatches(content, pattern, stripeWebhookUnprotected, filePath, () =>\n \"Verify the Stripe webhook signature using stripe.webhooks.constructEvent(body, sig, webhookSecret) to prevent forged payment events.\"\n ),\n );\n }\n return matches;\n },\n};\n\nexport const sqlInjection: CustomRule = {\n id: \"VC006\",\n title: \"Potential SQL Injection\",\n severity: \"critical\",\n category: \"Injection\",\n description: \"String concatenation or template literals in SQL queries allow attackers to execute arbitrary database commands.\",\n check(content, filePath) {\n const patterns = [\n // Template literals in SQL\n /(?:query|execute|raw|sql)\\s*\\(\\s*`[^`]*\\$\\{/gi,\n // String concatenation in SQL\n /(?:query|execute)\\s*\\(\\s*[\"'][^\"']*[\"']\\s*\\+/gi,\n // Direct variable interpolation\n /(?:SELECT|INSERT|UPDATE|DELETE|WHERE)\\s+.*\\$\\{(?!.*parameterized)/gi,\n ];\n\n const matches: RuleMatch[] = [];\n\n // Skip if using parameterized queries / prepared statements\n const usesParams = /\\?\\s*,|\\$\\d+|:[\\w]+|\\bprepare\\b|\\bplaceholder\\b/i.test(content);\n if (usesParams) return [];\n\n for (const pattern of patterns) {\n matches.push(\n ...findMatches(content, pattern, sqlInjection, filePath, () =>\n \"Use parameterized queries or prepared statements instead of string interpolation. Example: db.query('SELECT * FROM users WHERE id = ?', [userId])\"\n ),\n );\n }\n return matches;\n },\n};\n\nexport const xssVulnerability: CustomRule = {\n id: \"VC007\",\n title: \"Potential Cross-Site Scripting (XSS)\",\n severity: \"high\",\n category: \"Injection\",\n description: \"Rendering user input without sanitization allows attackers to inject malicious scripts.\",\n check(content, filePath) {\n // Skip if file is a sanitizer utility or already uses DOMPurify\n if (/(?:sanitize|purify|escape|xss)/i.test(filePath)) return [];\n if (/DOMPurify\\.sanitize|sanitizeHtml|xss\\(|escapeHtml/i.test(content)) return [];\n // Skip if the file imports or requires DOMPurify or a sanitize library anywhere\n if (/(?:import|require)\\s*\\(?.*(?:DOMPurify|dompurify|sanitize|sanitizer)/i.test(content)) return [];\n const patterns = [\n // React dangerouslySetInnerHTML\n /dangerouslySetInnerHTML\\s*=\\s*\\{\\s*\\{\\s*__html\\s*:/g,\n // Direct innerHTML assignment\n /\\.innerHTML\\s*=\\s*(?![\"'`]\\s*$)/gm,\n // document.write\n /document\\.write\\s*\\(/g,\n // v-html in Vue\n /v-html\\s*=/g,\n // {@html} in Svelte\n /\\{@html\\s/g,\n ];\n\n const matches: RuleMatch[] = [];\n for (const pattern of patterns) {\n const raw = findMatches(content, pattern, xssVulnerability, filePath, () =>\n \"Sanitize user input before rendering as HTML. Use a library like DOMPurify: DOMPurify.sanitize(userInput)\"\n );\n // Filter out innerHTML assignments that use only static strings (no user input)\n for (const m of raw) {\n const lineText = content.split(\"\\n\")[m.line - 1] || \"\";\n // Skip if innerHTML is assigned a pure string literal with no interpolation\n if (/\\.innerHTML\\s*=\\s*['\"]/.test(lineText) && !/\\$\\{/.test(lineText)) continue;\n // Skip if innerHTML is assigned a string concatenation of only literals (no variables from user input)\n if (/\\.innerHTML\\s*=\\s*['\"][^'\"]*['\"]\\s*$/.test(lineText)) continue;\n matches.push(m);\n }\n }\n return matches;\n },\n};\n\nexport const noRateLimiting: CustomRule = {\n id: \"VC008\",\n title: \"API Endpoint Without Rate Limiting\",\n severity: \"medium\",\n category: \"Availability\",\n description: \"API endpoints without rate limiting are vulnerable to abuse and denial-of-service attacks.\",\n check(content, filePath) {\n // Only check main server/app entry files\n const isEntryFile = /(?:server|app|index|main)\\.[jt]sx?$/.test(filePath) ||\n filePath.includes(\"middleware\");\n if (!isEntryFile) return [];\n\n // Check if this is a server file\n const isServer = /(?:express|hono|fastify|koa|next|createServer|listen\\()/i.test(content);\n if (!isServer) return [];\n\n // Check for rate limiting\n const hasRateLimit = /rate.?limit|throttle|express-rate-limit|@elysiajs\\/rate-limit|hono.*limiter/i.test(content);\n if (hasRateLimit) return [];\n\n return [{\n rule: \"VC008\",\n title: noRateLimiting.title,\n severity: \"medium\",\n category: \"Availability\",\n file: filePath,\n line: 1,\n snippet: getSnippet(content, 1),\n fix: \"Add rate limiting middleware to your server. For Express: npm install express-rate-limit. For other frameworks, check their rate limiting plugins.\",\n }];\n },\n};\n\nexport const corsWildcard: CustomRule = {\n id: \"VC009\",\n title: \"CORS Allows All Origins\",\n severity: \"medium\",\n category: \"Configuration\",\n description: \"Wildcard CORS (*) allows any website to make requests to your API, potentially exposing user data.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n const patterns = [\n /cors\\(\\s*\\)/g, // cors() with no options = allow all\n /origin\\s*:\\s*[\"'`]\\*[\"'`]/g,\n /[\"'`]Access-Control-Allow-Origin[\"'`]\\s*,\\s*[\"'`]\\*[\"'`]/g,\n /origin\\s*:\\s*true/g,\n ];\n\n const matches: RuleMatch[] = [];\n for (const pattern of patterns) {\n matches.push(\n ...findMatches(content, pattern, corsWildcard, filePath, () =>\n \"Restrict CORS to your specific frontend domain(s): cors({ origin: 'https://yourdomain.com' })\"\n ),\n );\n }\n return matches;\n },\n};\n\nexport const clientSideAuth: CustomRule = {\n id: \"VC010\",\n title: \"Client-Side Only Authorization\",\n severity: \"high\",\n category: \"Authorization\",\n description: \"Hiding UI elements based on roles without server-side checks lets attackers bypass restrictions using DevTools.\",\n check(content, filePath) {\n // Only check frontend component files\n if (!filePath.match(/\\.(jsx|tsx|vue|svelte)$/)) return [];\n\n const matches: RuleMatch[] = [];\n\n // Pattern: conditional rendering based on role/admin without server check\n const rolePatterns = [\n /\\{.*(?:isAdmin|role\\s*===?\\s*[\"'`]admin[\"'`]|user\\.role).*&&/gi,\n /v-if\\s*=\\s*[\"'`].*(?:isAdmin|role\\s*===?\\s*'admin')/gi,\n ];\n\n for (const pattern of rolePatterns) {\n // Only flag if the file has no server-side fetch for auth verification\n const hasServerCheck = /getServerSession|getUser|server|api\\/auth|middleware/i.test(content);\n if (hasServerCheck) continue;\n\n matches.push(\n ...findMatches(content, pattern, clientSideAuth, filePath, () =>\n \"Client-side role checks only hide UI — they don't prevent access. Always verify permissions on the server/API side too.\"\n ),\n );\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC011 – Secret in NEXT_PUBLIC_ env var\n// ────────────────────────────────────────────\n\nexport const nextPublicSecret: CustomRule = {\n id: \"VC011\",\n title: \"Secret in NEXT_PUBLIC_ Environment Variable\",\n severity: \"critical\",\n category: \"Secrets\",\n description: \"NEXT_PUBLIC_ variables are exposed to the browser. Secrets placed here are visible to anyone.\",\n check(content, filePath) {\n if (!filePath.match(/\\.env/) && !filePath.match(/next\\.config/)) return [];\n const patterns = [\n /NEXT_PUBLIC_[A-Z_]*(?:SECRET|KEY|TOKEN|PASSWORD|PRIVATE)[A-Z_]*\\s*=\\s*.+/gi,\n /NEXT_PUBLIC_[A-Z_]*(?:SUPABASE_SERVICE|CLERK_SECRET|STRIPE_SECRET)[A-Z_]*\\s*=\\s*.+/gi,\n ];\n const matches: RuleMatch[] = [];\n for (const p of patterns) {\n const raw = findMatches(content, p, nextPublicSecret, filePath, () =>\n \"Remove the NEXT_PUBLIC_ prefix. Only use NEXT_PUBLIC_ for values safe to expose in the browser.\"\n );\n // Filter out publishable/public keys that are DESIGNED to be client-side\n for (const m of raw) {\n const lineText = content.split(\"\\n\")[m.line - 1] || \"\";\n if (/PUBLISHABLE|ANON_KEY|PUBLIC_KEY/i.test(lineText)) continue;\n // Skip Clerk publishable keys (pk_test_, pk_live_)\n if (/CLERK_PUBLISHABLE/i.test(lineText)) continue;\n // Skip Stripe publishable keys\n if (/STRIPE_PUBLISHABLE/i.test(lineText)) continue;\n // Skip if the value is a placeholder (empty, pk_test_, etc.)\n if (/=\\s*[\"']?\\s*$|=\\s*[\"']?pk_(?:test|live)_[\"']?\\s*$/.test(lineText)) continue;\n matches.push(m);\n }\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC012 – Firebase config in client code\n// ────────────────────────────────────────────\n\nexport const firebaseClientConfig: CustomRule = {\n id: \"VC012\",\n title: \"Firebase Config with API Key in Client Code\",\n severity: \"medium\",\n category: \"Configuration\",\n description: \"Firebase config objects in client code expose your API key. While Firebase API keys aren't secret, they should be restricted in the Firebase console.\",\n check(content, filePath) {\n if (!/firebase/i.test(content)) return [];\n const patterns = [\n /firebaseConfig\\s*=\\s*\\{[^}]*apiKey\\s*:/gi,\n /initializeApp\\s*\\(\\s*\\{[^}]*apiKey\\s*:/gi,\n ];\n const matches: RuleMatch[] = [];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, firebaseClientConfig, filePath, () =>\n \"Move Firebase config to environment variables. Restrict the API key in Firebase Console > Project Settings > API restrictions.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC013 – Supabase anon key for admin ops\n// ────────────────────────────────────────────\n\nexport const supabaseAnonAdmin: CustomRule = {\n id: \"VC013\",\n title: \"Supabase Anon Key Used for Admin Operations\",\n severity: \"high\",\n category: \"Authorization\",\n description: \"Using the Supabase anon key for operations that require elevated privileges is insecure.\",\n check(content, filePath) {\n if (!/supabase/i.test(content)) return [];\n if (!/anon/i.test(content)) return [];\n if (/service_role/i.test(content)) return [];\n const patterns = [\n /supabase[^.]*\\.auth\\.admin/gi,\n /supabase[^.]*\\.rpc\\s*\\(/gi,\n ];\n const matches: RuleMatch[] = [];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, supabaseAnonAdmin, filePath, () =>\n \"Use the service_role key on the server side for admin operations. Never expose it to the client.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC014 – .env not in .gitignore\n// ────────────────────────────────────────────\n\nexport const envNotGitignored: CustomRule = {\n id: \"VC014\",\n title: \".env File Not in .gitignore\",\n severity: \"high\",\n category: \"Secrets\",\n description: \"If .env is not listed in .gitignore, secrets will be committed to version control.\",\n check(content, filePath) {\n if (!filePath.endsWith(\".gitignore\")) return [];\n if (/\\.env/i.test(content)) return [];\n return [{\n rule: \"VC014\", title: envNotGitignored.title, severity: \"high\" as const, category: \"Secrets\",\n file: filePath, line: 1, snippet: getSnippet(content, 1),\n fix: 'Add \".env*\" to your .gitignore file to prevent committing secrets.',\n }];\n },\n};\n\n// ────────────────────────────────────────────\n// VC015 – eval() / new Function()\n// ────────────────────────────────────────────\n\nexport const evalUsage: CustomRule = {\n id: \"VC015\",\n title: \"Use of eval() or Function Constructor\",\n severity: \"high\",\n category: \"Injection\",\n description: \"eval() and new Function() execute arbitrary code, creating severe injection risks. Common in AI-generated code.\",\n check(content, filePath) {\n if (filePath.includes(\"node_modules\") || filePath.includes(\".min.\")) return [];\n if (filePath.match(/(?:webpack|rollup|vite|jest|babel|tsup|esbuild)\\.config/i)) return [];\n if (isTestFile(filePath)) return [];\n // Skip files that are linters/scanners/rule engines (they reference eval in detection patterns)\n if (filePath.match(/(?:rules?|scanner|lint|check|detect|analyz)/i) && /\\.check\\s*\\(|findMatches/i.test(content)) return [];\n const patterns = [\n // Actual eval() calls — not in strings or regex patterns\n /\\beval\\s*\\(\\s*(?)/g,\n /new\\s+Function\\s*\\(\\s*(?![\"'`])/g,\n ];\n // Skip if eval is only in a string (e.g., devtool: 'eval-source-map')\n const hasEvalInString = /[\"'`]eval(?:-source-map|[\"'`])/i.test(content);\n if (hasEvalInString && !/\\beval\\s*\\([^)]*(?:req\\.|body\\.|input|params|user|data)/i.test(content)) return [];\n const matches: RuleMatch[] = [];\n for (const p of patterns) {\n const rawMatches = findMatches(content, p, evalUsage, filePath, () =>\n \"Replace eval() with JSON.parse() for data, or a proper parser for expressions. Never pass user input to eval().\"\n );\n // Skip matches on lines containing devtool or source-map config\n for (const rm of rawMatches) {\n const lineStart = content.lastIndexOf(\"\\n\", content.split(\"\\n\").slice(0, rm.line - 1).join(\"\\n\").length) + 1;\n const lineEnd = content.indexOf(\"\\n\", lineStart + 1);\n const lineText = content.substring(lineStart, lineEnd === -1 ? content.length : lineEnd);\n if (/devtool|source-map/i.test(lineText)) continue;\n // Skip if eval appears on a line with description/title/message keys (string literal context)\n if (/(?:description|title|message)\\s*[:=]/i.test(lineText)) continue;\n matches.push(rm);\n }\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC016 – Unvalidated redirect\n// ────────────────────────────────────────────\n\nexport const unvalidatedRedirect: CustomRule = {\n id: \"VC016\",\n title: \"Unvalidated Redirect\",\n severity: \"high\",\n category: \"Injection\",\n description: \"Redirecting users to URLs from untrusted input enables phishing attacks.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n // Skip if the file already validates redirect URLs\n if (/isAllowedRedirect|validateRedirect|isSafeRedirect|allowedDomains|trustedDomains|whitelist.*url|allowlist.*url/i.test(content)) return [];\n const patterns = [\n /window\\.location\\s*=\\s*(?![\"'`]https?:\\/\\/)/g,\n /window\\.location\\.href\\s*=\\s*(?![\"'`]https?:\\/\\/)/g,\n /window\\.location\\.assign\\s*\\(\\s*(?![\"'`]https?:\\/\\/)/g,\n /window\\.location\\.replace\\s*\\(\\s*(?![\"'`]https?:\\/\\/)/g,\n /res\\.redirect\\s*\\(\\s*(?:req\\.|params\\.|query\\.)/gi,\n ];\n const matches: RuleMatch[] = [];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, unvalidatedRedirect, filePath, () =>\n \"Validate redirect URLs against an allowlist of trusted domains. Never redirect to user-supplied URLs directly.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC017 – Insecure cookie settings\n// ────────────────────────────────────────────\n\nexport const insecureCookies: CustomRule = {\n id: \"VC017\",\n title: \"Insecure Cookie Settings\",\n severity: \"medium\",\n category: \"Configuration\",\n description: \"Cookies without httpOnly, secure, or sameSite flags are vulnerable to theft and CSRF attacks.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!/cookie/i.test(content)) return [];\n const setCookiePattern = /(?:set-cookie|setCookie|cookie\\s*=|res\\.cookie\\s*\\()/gi;\n if (!setCookiePattern.test(content)) return [];\n const hasHttpOnly = /httpOnly\\s*:\\s*true|httponly/i.test(content);\n const hasSecure = /secure\\s*:\\s*true|;\\s*secure/i.test(content);\n const hasSameSite = /sameSite\\s*:|samesite/i.test(content);\n const matches: RuleMatch[] = [];\n if (!hasHttpOnly || !hasSecure || !hasSameSite) {\n const missing: string[] = [];\n if (!hasHttpOnly) missing.push(\"httpOnly\");\n if (!hasSecure) missing.push(\"secure\");\n if (!hasSameSite) missing.push(\"sameSite\");\n matches.push(...findMatches(content, /(?:set-cookie|setCookie|cookie\\s*=|res\\.cookie\\s*\\()/gi, insecureCookies, filePath, () =>\n `Add missing cookie flags: ${missing.join(\", \")}. Example: { httpOnly: true, secure: true, sameSite: 'lax' }`\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC018 – Exposed auth provider secret key\n// ────────────────────────────────────────────\n\nexport const exposedAuthSecret: CustomRule = {\n id: \"VC018\",\n title: \"Exposed Clerk/Auth Secret Key\",\n severity: \"critical\",\n category: \"Secrets\",\n description: \"Auth provider secret keys (Clerk, Auth0, NextAuth) must never be in client-side code or NEXT_PUBLIC_ variables.\",\n check(content, filePath) {\n const isClientFile = filePath.match(/\\.(jsx|tsx|vue|svelte)$/) || /[\"']use client[\"']/.test(content);\n const isEnvFile = filePath.match(/\\.env/);\n if (!isClientFile && !isEnvFile) return [];\n const patterns: RegExp[] = [];\n if (isClientFile) {\n patterns.push(\n /CLERK_SECRET_KEY/g,\n /AUTH0_CLIENT_SECRET/g,\n /NEXTAUTH_SECRET/g,\n );\n }\n if (isEnvFile) {\n patterns.push(\n /NEXT_PUBLIC_CLERK_SECRET/gi,\n /NEXT_PUBLIC_AUTH0_SECRET/gi,\n /NEXT_PUBLIC_NEXTAUTH_SECRET/gi,\n );\n }\n const matches: RuleMatch[] = [];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, exposedAuthSecret, filePath, () =>\n \"Move this secret to a server-side environment variable (without the NEXT_PUBLIC_ prefix). Never expose auth secrets to the browser.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC019 – Insecure Electron BrowserWindow\n// ────────────────────────────────────────────\n\nexport const insecureElectronWindow: CustomRule = {\n id: \"VC019\",\n title: \"Insecure Electron BrowserWindow Configuration\",\n severity: \"high\",\n category: \"Configuration\",\n description: \"Electron BrowserWindow with nodeIntegration enabled, contextIsolation disabled, or sandbox disabled allows renderer processes to access Node.js APIs, enabling remote code execution.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!/BrowserWindow/i.test(content)) return [];\n const matches: RuleMatch[] = [];\n const patterns = [\n /nodeIntegration\\s*:\\s*true/g,\n /contextIsolation\\s*:\\s*false/g,\n /sandbox\\s*:\\s*false/g,\n /webSecurity\\s*:\\s*false/g,\n /allowRunningInsecureContent\\s*:\\s*true/g,\n ];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, insecureElectronWindow, filePath, (m) =>\n `Set ${m[0].split(\":\")[0].trim()}: ${m[0].includes(\"true\") ? \"false\" : \"true\"}. Enable contextIsolation, sandbox, and webSecurity; disable nodeIntegration and allowRunningInsecureContent.`\n ));\n }\n // Check for BrowserWindow without sandbox/webSecurity set at all\n if (/new\\s+BrowserWindow\\s*\\(/g.test(content)) {\n if (!/sandbox\\s*:/i.test(content)) {\n matches.push(...findMatches(content, /new\\s+BrowserWindow\\s*\\(/g, { ...insecureElectronWindow, title: \"Electron BrowserWindow Missing sandbox:true\" }, filePath, () =>\n \"Add sandbox: true to BrowserWindow webPreferences for defense in depth.\"\n ));\n }\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC020 – Missing Content Security Policy\n// ────────────────────────────────────────────\n\nexport const missingCSP: CustomRule = {\n id: \"VC020\",\n title: \"Missing Content Security Policy (CSP)\",\n severity: \"high\",\n category: \"Configuration\",\n description: \"Without a Content-Security-Policy header or meta tag, your app is vulnerable to XSS and data injection attacks.\",\n check(content, filePath) {\n // Check HTML files for missing CSP meta tag\n if (filePath.match(/\\.(html|htm)$/)) {\n if (!/Content-Security-Policy/i.test(content)) {\n return [{\n rule: \"VC020\", title: missingCSP.title, severity: \"high\" as const, category: \"Configuration\",\n file: filePath, line: 1, snippet: getSnippet(content, 1),\n fix: 'Add a CSP meta tag: <meta http-equiv=\"Content-Security-Policy\" content=\"default-src \\'self\\'; script-src \\'self\\'\">'\n }];\n }\n }\n // Skip Electron main process — CSP is typically set in the HTML file (already checked above)\n // Flagging main.ts creates false positives since CSP belongs in index.html for Electron apps\n return [];\n },\n};\n\n// ────────────────────────────────────────────\n// VC021 – IPC Handler Without Path Validation\n// ────────────────────────────────────────────\n\nexport const ipcPathTraversal: CustomRule = {\n id: \"VC021\",\n title: \"IPC/File Handler Without Path Validation\",\n severity: \"medium\",\n category: \"Injection\",\n description: \"IPC handlers that read or write files based on renderer-supplied paths without validation allow path traversal attacks, potentially exposing sensitive files like .ssh keys or .env files.\",\n check(content, filePath) {\n if (!/ipcMain\\.handle|ipcMain\\.on/i.test(content)) return [];\n const matches: RuleMatch[] = [];\n // Look for file read/write in IPC handlers without path validation\n const hasFileOps = /readFile|writeFile|readFileSync|writeFileSync|createReadStream|createWriteStream/i.test(content);\n if (!hasFileOps) return [];\n const hasPathValidation = /(?:path\\.resolve|path\\.normalize|startsWith|isAbsolute|\\.includes\\s*\\(\\s*[\"'`]\\.\\.[\"'`]\\s*\\)|allowedPaths|safePath|validatePath|sanitizePath)/i.test(content);\n if (!hasPathValidation) {\n matches.push(...findMatches(content, /ipcMain\\.(?:handle|on)\\s*\\(\\s*[\"'`][^\"'`]*(?:read|write|file|save|load|open|export)[^\"'`]*[\"'`]/gi, ipcPathTraversal, filePath, () =>\n \"Validate file paths in IPC handlers: ensure paths are within an allowed directory (e.g., app.getPath('userData')), reject paths containing '..', and block access to sensitive directories (.ssh, .env, etc).\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC022 – HTML Export Without Sanitization\n// ────────────────────────────────────────────\n\nexport const unsanitizedHTMLExport: CustomRule = {\n id: \"VC022\",\n title: \"HTML Export/Render Without Sanitization\",\n severity: \"critical\",\n category: \"Injection\",\n description: \"Generating HTML from user content without sanitization (e.g., DOMPurify) allows stored XSS attacks. Malicious content saved in documents could execute scripts when exported or previewed.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n // Skip if DOMPurify is imported or used anywhere in the file\n if (/DOMPurify|dompurify/i.test(content)) return [];\n // Skip React/Vue component files — JSX is not unsafe HTML concatenation\n if (filePath.match(/\\.(jsx|tsx|vue|svelte)$/) && !/innerHTML|document\\.write|\\.html\\s*=/i.test(content)) return [];\n // Skip files that have any sanitizer\n const hasSanitizer = /sanitize|escapeHtml|escapeHTML|xss|htmlEncode|purify/i.test(content);\n if (hasSanitizer) return [];\n // Only flag files that actually export/write HTML (not just template rendering)\n if (!/(?:export|download|save|write|send).*(?:html|HTML)|\\.innerHTML\\s*=|document\\.write|res\\.send\\s*\\(/i.test(content)) return [];\n const matches: RuleMatch[] = [];\n const htmlBuildPatterns = [\n /`<[^`]*\\$\\{[^}]*(?:content|title|body|text|name|message|description|input|value|data)[^}]*\\}[^`]*>`/gi,\n /[\"']<[^\"']*['\"]\\s*\\+\\s*(?:content|title|body|text|message|data|doc\\.|post\\.|article\\.)/gi,\n ];\n for (const p of htmlBuildPatterns) {\n matches.push(...findMatches(content, p, unsanitizedHTMLExport, filePath, () =>\n \"Sanitize user content before embedding in HTML. Use DOMPurify: DOMPurify.sanitize(content). For plain text, use a function to escape HTML entities (<, >, &, quotes).\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC023 – Prototype Pollution via Storage\n// ────────────────────────────────────────────\n\nexport const prototypePollution: CustomRule = {\n id: \"VC023\",\n title: \"Prototype Pollution Risk\",\n severity: \"high\",\n category: \"Injection\",\n description: \"Parsing JSON from localStorage, URL params, or external sources and merging it into objects without validation can lead to prototype pollution, allowing attackers to inject __proto__ or constructor properties.\",\n check(content, filePath) {\n const matches: RuleMatch[] = [];\n // JSON.parse from localStorage/sessionStorage without validation\n const storageParsePatterns = [\n /JSON\\.parse\\s*\\(\\s*(?:localStorage|sessionStorage)\\.getItem/g,\n /JSON\\.parse\\s*\\(\\s*window\\.localStorage/g,\n ];\n const hasValidation = /schema|validate|sanitize|whitelist|allowedKeys|pick\\(|Object\\.freeze|zod|yup|joi|ajv/i.test(content);\n if (hasValidation) return [];\n // Check for object spread/assign from parsed storage\n const hasUnsafeMerge = /Object\\.assign\\s*\\([^)]*JSON\\.parse|\\.\\.\\.JSON\\.parse|\\{.*\\.\\.\\.(?:stored|saved|cached|parsed|data)/i.test(content);\n if (hasUnsafeMerge) {\n matches.push(...findMatches(content, /Object\\.assign\\s*\\([^)]*JSON\\.parse|\\.\\.\\.JSON\\.parse/g, prototypePollution, filePath, () =>\n \"Validate parsed data against an expected schema before merging into objects. Use Object.freeze(), a validation library (Zod, Yup), or manually check for __proto__ and constructor keys.\"\n ));\n }\n for (const p of storageParsePatterns) {\n matches.push(...findMatches(content, p, prototypePollution, filePath, () =>\n \"Validate localStorage data against an expected schema before using it. Malicious extensions or XSS can modify localStorage values.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC024 – Missing File Size Limits\n// ────────────────────────────────────────────\n\nexport const missingFileSizeLimits: CustomRule = {\n id: \"VC024\",\n title: \"File Write/Save Without Size Limit\",\n severity: \"medium\",\n category: \"Availability\",\n description: \"File save or upload handlers without size validation can lead to denial-of-service via disk exhaustion or memory exhaustion.\",\n check(content, filePath) {\n if (!/(?:writeFile|save|upload|export)/i.test(filePath) && !/(?:writeFile|writeFileSync|createWriteStream)/i.test(content)) return [];\n // Look for file write operations in handlers\n const hasWriteOps = /(?:ipcMain|app\\.(?:post|put)|router\\.(?:post|put)).*(?:writeFile|save|export)/is.test(content) ||\n /(?:writeFile|writeFileSync)\\s*\\(/g.test(content);\n if (!hasWriteOps) return [];\n const hasSizeCheck = /(?:size|length|byteLength|bytes)\\s*(?:>|>=|<|<=|===)\\s*\\d|maxSize|MAX_SIZE|sizeLimit|content-length/i.test(content);\n if (hasSizeCheck) return [];\n return findMatches(content, /(?:writeFile|writeFileSync)\\s*\\(/g, missingFileSizeLimits, filePath, () =>\n \"Add file size validation before writing. Check content.length or Buffer.byteLength() against a maximum (e.g., 10MB) to prevent disk exhaustion.\"\n );\n },\n};\n\n// ────────────────────────────────────────────\n// VC025 – Unsanitized Export Filenames\n// ────────────────────────────────────────────\n\nexport const unsanitizedFilenames: CustomRule = {\n id: \"VC025\",\n title: \"Unsanitized Filename in File Operations\",\n severity: \"medium\",\n category: \"Injection\",\n description: \"Using user-supplied filenames without sanitization in file operations can enable path traversal, overwriting system files, or executing commands via special characters.\",\n check(content, filePath) {\n const matches: RuleMatch[] = [];\n // Look for file operations using variables as filenames\n const patterns = [\n /(?:writeFile|writeFileSync|createWriteStream|rename|copyFile)\\s*\\(\\s*(?:`[^`]*\\$\\{|[^\"'`\\s,]+\\s*\\+)/g,\n /(?:dialog\\.showSaveDialog|saveDialog).*(?:defaultPath|fileName)\\s*:\\s*(?![\"'`])/g,\n /\\.download\\s*=\\s*(?![\"'`])/g,\n ];\n const hasSanitization = /sanitize|cleanFilename|safeFilename|replace\\s*\\(\\s*\\/\\[.*\\]\\//i.test(content);\n if (hasSanitization) return [];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, unsanitizedFilenames, filePath, () =>\n \"Sanitize filenames before use: strip path separators (/ \\\\), special chars, and '..' sequences. Example: name.replace(/[^a-zA-Z0-9._-]/g, '_')\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC026 – Electron Navigation Not Restricted\n// ────────────────────────────────────────────\n\nexport const electronNavigationUnrestricted: CustomRule = {\n id: \"VC026\",\n title: \"Electron: External Navigation Not Blocked\",\n severity: \"medium\",\n category: \"Configuration\",\n description: \"Electron apps that don't block navigation to external URLs or new window creation are vulnerable to phishing and drive-by downloads. Malicious links in app content can redirect the entire app to an attacker's site.\",\n check(content, filePath) {\n if (!/BrowserWindow|electron/i.test(content)) return [];\n if (!/main|index/i.test(filePath)) return [];\n const hasNavBlock = /will-navigate|new-window|setWindowOpenHandler|webContents\\.on.*navigate/i.test(content);\n if (hasNavBlock) return [];\n if (/new\\s+BrowserWindow/i.test(content)) {\n return findMatches(content, /new\\s+BrowserWindow\\s*\\(/g, electronNavigationUnrestricted, filePath, () =>\n \"Block external navigation: win.webContents.on('will-navigate', (e, url) => { if (!url.startsWith('file://')) e.preventDefault(); }); and use setWindowOpenHandler to block new windows.\"\n );\n }\n return [];\n },\n};\n\n// ────────────────────────────────────────────\n// VC027 – Missing Security Meta Tags\n// ────────────────────────────────────────────\n\nexport const missingSecurityMeta: CustomRule = {\n id: \"VC027\",\n title: \"Missing Security Meta Tags / Headers\",\n severity: \"medium\",\n category: \"Configuration\",\n description: \"HTML pages without X-Content-Type-Options, referrer policy, or other security meta tags are more susceptible to MIME-sniffing attacks and information leakage.\",\n check(content, filePath) {\n if (!filePath.match(/\\.(html|htm)$/)) return [];\n const matches: RuleMatch[] = [];\n if (!/X-Content-Type-Options/i.test(content) && !/<meta[^>]*nosniff/i.test(content)) {\n matches.push({\n rule: \"VC027\", title: \"Missing X-Content-Type-Options Header\", severity: \"medium\" as const,\n category: \"Configuration\", file: filePath, line: 1, snippet: getSnippet(content, 1),\n fix: 'Add <meta http-equiv=\"X-Content-Type-Options\" content=\"nosniff\"> to prevent MIME-type sniffing.'\n });\n }\n if (!/referrer/i.test(content)) {\n matches.push({\n rule: \"VC027\", title: \"Missing Referrer Policy\", severity: \"medium\" as const,\n category: \"Configuration\", file: filePath, line: 1, snippet: getSnippet(content, 1),\n fix: 'Add <meta name=\"referrer\" content=\"no-referrer\"> or \"strict-origin-when-cross-origin\" to limit referrer leakage.'\n });\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC028 – Unvalidated API Parameters\n// ────────────────────────────────────────────\n\nexport const unvalidatedAPIParams: CustomRule = {\n id: \"VC028\",\n title: \"Unvalidated API Request Parameters\",\n severity: \"high\",\n category: \"Injection\",\n description: \"API requests constructed with unvalidated user input (API keys, model names, URLs) can be exploited for injection attacks or unauthorized access to different API models/endpoints.\",\n check(content, filePath) {\n const matches: RuleMatch[] = [];\n // API key passed without format validation\n const apiKeyPatterns = [\n /(?:apiKey|api_key|authorization)\\s*[:=]\\s*(?:req\\.body|req\\.query|params|input|formData|body)\\./gi,\n /headers\\s*:\\s*\\{[^}]*Authorization\\s*:\\s*(?![\"'`]Bearer\\s)/gi,\n ];\n const hasValidation = /validate|sanitize|regex|test\\(|match\\(|pattern|allowList|whitelist|enum|includes\\(/i.test(content);\n if (hasValidation) return [];\n // Model selection without allowlist\n if (/model\\s*[:=]\\s*(?:req\\.body|params|input|body)\\./i.test(content) || /model\\s*[:=]\\s*(?![\"'`])[a-z]/i.test(content)) {\n const hasModelValidation = /allowedModels|validModels|models\\s*\\.\\s*includes|model.*(?:===|!==|includes)/i.test(content);\n if (!hasModelValidation && /(?:openai|anthropic|claude|gpt|llm)/i.test(content)) {\n matches.push(...findMatches(content, /model\\s*[:=]\\s*(?:req\\.body|params|input|body)\\./gi, unvalidatedAPIParams, filePath, () =>\n \"Validate model selection against an allowlist of approved models. Example: const ALLOWED_MODELS = ['gpt-4', 'claude-3']; if (!ALLOWED_MODELS.includes(model)) throw new Error('Invalid model');\"\n ));\n }\n }\n for (const p of apiKeyPatterns) {\n matches.push(...findMatches(content, p, unvalidatedAPIParams, filePath, () =>\n \"Validate API key format before using it (e.g., check prefix and length). Never pass user-supplied API keys directly to third-party services without validation.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC029 – Unvalidated Event/Message Data\n// ────────────────────────────────────────────\n\nexport const unvalidatedEventData: CustomRule = {\n id: \"VC029\",\n title: \"Unvalidated Event or PostMessage Data\",\n severity: \"medium\",\n category: \"Injection\",\n description: \"Custom events, postMessage, or IPC message data used without type-checking can lead to injection attacks or unexpected behavior when malicious data is sent through event channels.\",\n check(content, filePath) {\n const matches: RuleMatch[] = [];\n // addEventListener('message') without origin check\n if (/addEventListener\\s*\\(\\s*[\"'`]message[\"'`]/i.test(content)) {\n if (!/event\\.origin|e\\.origin|message\\.origin/i.test(content)) {\n matches.push(...findMatches(content, /addEventListener\\s*\\(\\s*[\"'`]message[\"'`]/g, unvalidatedEventData, filePath, () =>\n \"Always verify event.origin in message event handlers to prevent cross-origin attacks. Example: if (event.origin !== 'https://trusted.com') return;\"\n ));\n }\n }\n // dispatchEvent with custom data inserted without validation\n if (/new\\s+CustomEvent\\s*\\(/i.test(content) || /ipcRenderer\\.send/i.test(content)) {\n const hasTypeCheck = /typeof\\s|instanceof|z\\.|schema|validate|Number\\.isFinite|parseInt|parseFloat/i.test(content);\n if (!hasTypeCheck) {\n matches.push(...findMatches(content, /new\\s+CustomEvent\\s*\\(/g, unvalidatedEventData, filePath, () =>\n \"Type-check custom event data before using it. Validate that data.detail contains expected types to prevent injection.\"\n ));\n }\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC030 – Insecure Deserialization\n// ────────────────────────────────────────────\n\nexport const insecureDeserialization: CustomRule = {\n id: \"VC030\",\n title: \"Insecure Deserialization\",\n severity: \"critical\",\n category: \"Injection\",\n description: \"Deserializing untrusted data (pickle, unserialize, yaml.load) can execute arbitrary code. Attackers craft malicious payloads to gain remote code execution.\",\n check(content, filePath) {\n const matches: RuleMatch[] = [];\n const patterns = [\n // Python pickle\n /pickle\\.loads?\\s*\\(/g,\n /cPickle\\.loads?\\s*\\(/g,\n // PHP unserialize\n /unserialize\\s*\\(/g,\n // Ruby Marshal\n /Marshal\\.load\\s*\\(/g,\n // YAML unsafe load (Python)\n /yaml\\.load\\s*\\([^)]*(?!Loader\\s*=\\s*yaml\\.SafeLoader)/g,\n /yaml\\.unsafe_load\\s*\\(/g,\n // Java ObjectInputStream\n /ObjectInputStream\\s*\\(/g,\n // Node.js node-serialize\n /serialize\\.unserialize\\s*\\(/g,\n ];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, insecureDeserialization, filePath, () =>\n \"Never deserialize untrusted data. Use JSON instead of pickle/Marshal/unserialize. For YAML, use yaml.safe_load(). Validate and sanitize all input before deserialization.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC031 – Hardcoded JWT Secret\n// ────────────────────────────────────────────\n\nexport const hardcodedJWTSecret: CustomRule = {\n id: \"VC031\",\n title: \"Hardcoded JWT Secret\",\n severity: \"critical\",\n category: \"Secrets\",\n description: \"JWT tokens signed with a hardcoded string secret can be forged by anyone who reads the source code.\",\n check(content, filePath) {\n if (filePath.endsWith(\".example\") || filePath.endsWith(\".template\") || filePath.includes(\"test\")) return [];\n const patterns = [\n /jwt\\.sign\\s*\\([^,]+,\\s*[\"'`][^\"'`]{3,}[\"'`]/g,\n /jwt\\.verify\\s*\\([^,]+,\\s*[\"'`][^\"'`]{3,}[\"'`]/g,\n /jsonwebtoken.*secret\\s*[:=]\\s*[\"'`][^\"'`]{3,}[\"'`]/gi,\n /JWT_SECRET\\s*[:=]\\s*[\"'`][^\"'`]{3,}[\"'`]/g,\n ];\n const matches: RuleMatch[] = [];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, hardcodedJWTSecret, filePath, () =>\n \"Move JWT secret to an environment variable: jwt.sign(payload, process.env.JWT_SECRET). Use a strong, random secret (256+ bits).\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC032 – Missing HTTPS Enforcement\n// ────────────────────────────────────────────\n\nexport const missingHTTPS: CustomRule = {\n id: \"VC032\",\n title: \"Missing HTTPS Enforcement\",\n severity: \"high\",\n category: \"Configuration\",\n description: \"HTTP URLs in production code, missing HSTS headers, or insecure redirect configurations expose data to man-in-the-middle attacks.\",\n check(content, filePath) {\n if (filePath.endsWith(\".example\") || filePath.includes(\"test\") || filePath.includes(\"README\")) return [];\n if (filePath.match(/\\.(md|txt)$/)) return [];\n const matches: RuleMatch[] = [];\n // Skip standard XML/HTML doctypes, DTDs, schema URIs, namespace URLs\n if (/<!DOCTYPE|xmlns|\\.dtd|\\.xsd/i.test(content) && !/fetch|axios|request|http\\.get/i.test(content)) return [];\n // Hardcoded http:// URLs to non-local hosts (excluding standard schema/namespace URIs)\n const httpPattern = /[\"'`]http:\\/\\/(?!localhost|127\\.0\\.0\\.1|0\\.0\\.0\\.0|192\\.168\\.|10\\.|172\\.(?:1[6-9]|2\\d|3[01])\\.|www\\.w3\\.org|www\\.apple\\.com\\/DTDs|schemas?\\.|xml\\.org|purl\\.org|ns\\.adobe|xmlpull\\.org|java\\.sun\\.com)[^\"'`\\s]+[\"'`]/g;\n const rawMatches = findMatches(content, httpPattern, missingHTTPS, filePath, () =>\n \"Use https:// instead of http:// for all production URLs. Add HSTS header: Strict-Transport-Security: max-age=31536000; includeSubDomains\"\n );\n // Skip DOCTYPE/DTD URLs\n for (const rm of rawMatches) {\n const lineStart = content.lastIndexOf(\"\\n\", content.split(\"\\n\").slice(0, rm.line - 1).join(\"\\n\").length) + 1;\n const lineEnd = content.indexOf(\"\\n\", lineStart + 1);\n const matchText = content.substring(lineStart, lineEnd === -1 ? content.length : lineEnd);\n if (/DTD|DOCTYPE|w3\\.org|apple\\.com\\/DTDs/i.test(matchText)) continue;\n matches.push(rm);\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC033 – Exposed Debug/Dev Mode\n// ────────────────────────────────────────────\n\nexport const exposedDebugMode: CustomRule = {\n id: \"VC033\",\n title: \"Debug/Development Mode Exposed\",\n severity: \"high\",\n category: \"Configuration\",\n description: \"Debug mode, verbose logging, or development configuration left in production code exposes internal details and may enable debug endpoints.\",\n check(content, filePath) {\n if (filePath.includes(\"test\") || filePath.endsWith(\".example\") || filePath.includes(\"node_modules\")) return [];\n if (filePath.match(/\\.env\\.development$/)) return []; // Expected in dev env files\n const matches: RuleMatch[] = [];\n const patterns = [\n // Debug flags set to true\n /DEBUG\\s*[:=]\\s*(?:true|1|[\"'`]true[\"'`]|[\"'`]\\*[\"'`])/g,\n // Django DEBUG\n /DEBUG\\s*=\\s*True/g,\n // Flask/Express debug mode\n /app\\.debug\\s*=\\s*True/g,\n /app\\.run\\s*\\([^)]*debug\\s*=\\s*True/g,\n // Source maps in production\n /devtool\\s*:\\s*[\"'`](?:eval|cheap|source-map|inline-source-map)[\"'`]/g,\n ];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, exposedDebugMode, filePath, () =>\n \"Disable debug mode in production. Use environment variables: DEBUG = process.env.NODE_ENV !== 'production'. Remove source maps from production builds.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC034 – Insecure Randomness\n// ────────────────────────────────────────────\n\nexport const insecureRandomness: CustomRule = {\n id: \"VC034\",\n title: \"Insecure Randomness for Security-Sensitive Values\",\n severity: \"high\",\n category: \"Cryptography\",\n description: \"Math.random() is not cryptographically secure. Using it for tokens, session IDs, passwords, or OTPs makes them predictable.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n // Only flag if Math.random is actually used (not just mentioned)\n if (!/Math\\.random\\s*\\(\\s*\\)/i.test(content)) return [];\n const matches: RuleMatch[] = [];\n // Math.random used in security-critical assignments (narrower: require direct assignment on same line)\n const securityContext = /(?:token|secret|session|password|otp|nonce|salt|csrf|auth)\\s*[:=]\\s*.*Math\\.random/gi;\n const rawMatches = findMatches(content, securityContext, insecureRandomness, filePath, () =>\n \"Use crypto.randomUUID() or crypto.getRandomValues() for security-sensitive values. Math.random() is predictable.\"\n );\n // Skip non-security uses of Math.random (e.g., UI-related randomness)\n const nonSecurityVarNames = /(?:id|key|color|index|delay|position|size|width|height|offset|opacity|rotation|animation|random(?!.*(?:token|secret|key|password)))\\s*[:=]\\s*.*Math\\.random/i;\n for (const rm of rawMatches) {\n const lineText = content.split(\"\\n\")[rm.line - 1] || \"\";\n if (nonSecurityVarNames.test(lineText)) continue;\n matches.push(rm);\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC035 – Open Redirect via URL Params\n// ────────────────────────────────────────────\n\nexport const openRedirectParams: CustomRule = {\n id: \"VC035\",\n title: \"Open Redirect via URL Parameters\",\n severity: \"high\",\n category: \"Injection\",\n description: \"Redirect parameters like ?redirect_url=, ?return_to=, ?next= passed directly to redirects enable phishing attacks.\",\n check(content, filePath) {\n const matches: RuleMatch[] = [];\n const patterns = [\n // Reading redirect-like query params and using in redirect\n /(?:redirect_url|redirect_uri|return_to|return_url|next|callback_url|continue|goto|target|dest|destination|forward|redir)\\s*(?:=|:)\\s*(?:req\\.query|req\\.params|searchParams|query|params)\\./gi,\n /redirect\\s*\\(\\s*(?:req\\.query|req\\.params|searchParams\\.get)\\s*\\(\\s*[\"'`](?:redirect|return|next|callback|url|goto)/gi,\n ];\n const hasValidation = /allowedUrls|allowedDomains|allowedHosts|validUrl|safeDomain|whitelist|startsWith.*https|new URL.*hostname/i.test(content);\n if (hasValidation) return [];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, openRedirectParams, filePath, () =>\n \"Validate redirect URLs against an allowlist of trusted domains. Use: const url = new URL(input); if (!ALLOWED_HOSTS.includes(url.hostname)) reject.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC036 – Missing Error Boundary (React)\n// ────────────────────────────────────────────\n\nexport const missingErrorBoundary: CustomRule = {\n id: \"VC036\",\n title: \"React App Missing Error Boundary\",\n severity: \"medium\",\n category: \"Configuration\",\n description: \"React apps without error boundaries display raw stack traces and component tree info to users when crashes occur, leaking internal details.\",\n check(content, filePath) {\n // Only check App.tsx, App.jsx, app.tsx, app.jsx files\n const basename = filePath.split(\"/\").pop() || \"\";\n if (!/^[Aa]pp\\.[jt]sx$/i.test(basename)) return [];\n // Skip .ts files (non-JSX)\n if (filePath.match(/\\.ts$/)) return [];\n // Only trigger if file contains JSX (< followed by an uppercase letter)\n if (!/<[A-Z]/.test(content)) return [];\n // Skip Electron main process files\n if (/(?:BrowserWindow|electron|ipcMain|app\\.on\\s*\\(\\s*[\"']ready)/i.test(content)) return [];\n if (/\\/main\\//.test(filePath) && !/react-dom|createRoot/i.test(content)) return [];\n // Must have React imports and render calls\n if (!/(?:import.*react|from\\s+['\"]react|require.*react)/i.test(content)) return [];\n if (!/(?:createRoot|ReactDOM\\.render)/i.test(content)) return [];\n const hasErrorBoundary = /ErrorBoundary|componentDidCatch|getDerivedStateFromError|error-boundary/i.test(content);\n if (hasErrorBoundary) return [];\n if (/createRoot|ReactDOM\\.render/i.test(content)) {\n return [{\n rule: \"VC036\", title: missingErrorBoundary.title, severity: \"medium\" as const, category: \"Configuration\",\n file: filePath, line: 1, snippet: getSnippet(content, 1),\n fix: \"Wrap your app in an ErrorBoundary component to catch rendering errors gracefully. Use react-error-boundary or create a class component with componentDidCatch.\"\n }];\n }\n return [];\n },\n};\n\n// ────────────────────────────────────────────\n// VC037 – Exposed Stack Traces in API\n// ────────────────────────────────────────────\n\nexport const exposedStackTraces: CustomRule = {\n id: \"VC037\",\n title: \"Stack Traces Exposed in API Responses\",\n severity: \"medium\",\n category: \"Information Leakage\",\n description: \"Returning error.stack or detailed error messages in API responses reveals internal code paths, file structure, and dependencies to attackers.\",\n check(content, filePath) {\n const isApiFile = /(?:\\/api\\/|routes?\\/|controllers?\\/|server\\.|middleware)/i.test(filePath);\n if (!isApiFile) return [];\n const matches: RuleMatch[] = [];\n const patterns = [\n // Sending stack trace in response\n /(?:res\\.(?:json|send|status)|c\\.json|return.*json)\\s*\\([^)]*(?:err\\.stack|error\\.stack|e\\.stack)/gi,\n /(?:res\\.(?:json|send|status)|c\\.json)\\s*\\([^)]*(?:err\\.message|error\\.message|e\\.message)/gi,\n // Express-style error with stack\n /(?:message|error)\\s*:\\s*(?:err|error|e)\\.(?:stack|message)/gi,\n ];\n const hasEnvCheck = /process\\.env\\.NODE_ENV\\s*(?:===|!==)\\s*[\"'`]production[\"'`]|NODE_ENV/i.test(content);\n if (hasEnvCheck) return []; // They're conditionally showing errors\n for (const p of patterns) {\n matches.push(...findMatches(content, p, exposedStackTraces, filePath, () =>\n \"Never expose error.stack or error.message to clients in production. Return generic error messages: { error: 'Something went wrong' }. Log details server-side only.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC038 – Insecure File Upload Type\n// ────────────────────────────────────────────\n\nexport const insecureFileUpload: CustomRule = {\n id: \"VC038\",\n title: \"Insecure File Upload Validation\",\n severity: \"high\",\n category: \"Injection\",\n description: \"File uploads validated only by extension (not MIME type or content) allow attackers to upload executable files disguised as images or documents.\",\n check(content, filePath) {\n if (!/upload|multer|formidable|busboy|multipart/i.test(content)) return [];\n const matches: RuleMatch[] = [];\n // Check for extension-only validation\n const hasExtCheck = /\\.(?:endsWith|match|test)\\s*\\([^)]*(?:\\.jpg|\\.png|\\.pdf|\\.doc|ext)/i.test(content);\n const hasMimeCheck = /mimetype|content-type|file\\.type|mime|magic\\.detect|file-type/i.test(content);\n if (hasExtCheck && !hasMimeCheck) {\n matches.push(...findMatches(content, /upload|multer|formidable|busboy/gi, insecureFileUpload, filePath, () =>\n \"Validate file uploads by MIME type AND magic bytes, not just extension. Use the 'file-type' package to detect actual file type from content. Also enforce size limits.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC039 – Missing Dependency Lock File\n// ────────────────────────────────────────────\n\nexport const missingLockFile: CustomRule = {\n id: \"VC039\",\n title: \"Missing Dependency Lock File\",\n severity: \"medium\",\n category: \"Supply Chain\",\n description: \"Without a lockfile (package-lock.json, pnpm-lock.yaml, yarn.lock), dependency versions are unpinned and vulnerable to supply chain attacks via version substitution.\",\n check(content, filePath) {\n // Only check .gitignore for lock files being ignored\n if (!filePath.endsWith(\".gitignore\")) return [];\n const ignoresLock = /package-lock\\.json|pnpm-lock\\.yaml|yarn\\.lock/i.test(content);\n if (ignoresLock) {\n return findMatches(content, /(?:package-lock\\.json|pnpm-lock\\.yaml|yarn\\.lock)/gi, missingLockFile, filePath, () =>\n \"Remove the lockfile from .gitignore. Lockfiles should be committed to prevent supply chain attacks. They ensure exact versions are installed across all environments.\"\n );\n }\n return [];\n },\n};\n\n// ────────────────────────────────────────────\n// VC040 – Exposed .git Directory\n// ────────────────────────────────────────────\n\nexport const exposedGitDir: CustomRule = {\n id: \"VC040\",\n title: \"Exposed .git Directory via Web Server\",\n severity: \"critical\",\n category: \"Information Leakage\",\n description: \"Web server configs that don't block access to .git directories expose your entire source code, commit history, secrets, and credentials.\",\n check(content, filePath) {\n // Check web server configs\n if (!filePath.match(/(?:nginx|apache|httpd|caddy|\\.htaccess|vercel\\.json|netlify\\.toml|server\\.[jt]s)/i)) return [];\n // For static file servers, check they block .git\n if (/(?:static|serve|express\\.static|serveStatic|public)/i.test(content)) {\n const blocksGit = /\\.git|dotfiles|hidden/i.test(content);\n if (!blocksGit) {\n return findMatches(content, /(?:static|serve|express\\.static|serveStatic)\\s*\\(/g, exposedGitDir, filePath, () =>\n \"Block access to .git and other dotfiles in your static file server config. For Express: app.use('/.git', (req, res) => res.status(403).end()). For Nginx: location ~ /\\\\.git { deny all; }\"\n );\n }\n }\n return [];\n },\n};\n\n// ────────────────────────────────────────────\n// VC041 – Server-Side Request Forgery (SSRF)\n// ────────────────────────────────────────────\n\nexport const ssrfVulnerability: CustomRule = {\n id: \"VC041\",\n title: \"Potential Server-Side Request Forgery (SSRF)\",\n severity: \"critical\",\n category: \"Injection\",\n description: \"Fetching URLs from user input without validation allows attackers to access internal services, cloud metadata endpoints (169.254.169.254), and private networks.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n // Only check server-side files\n const isServerFile = /(?:\\/api\\/|routes?\\/|controllers?\\/|server\\.|handler|middleware)/i.test(filePath);\n if (!isServerFile) return [];\n const matches: RuleMatch[] = [];\n // Only flag direct user input passed to fetch — narrower pattern\n const patterns = [\n /(?:fetch|axios\\.get|axios\\.post|axios|got|request|http\\.get|https\\.get)\\s*\\(\\s*(?:req\\.(?:body|query|params))\\./gi,\n ];\n const hasValidation = /allowedHosts|allowedDomains|allowedUrls|safeDomain|whitelist|urlValidator|new URL.*hostname.*includes|isAllowedUrl|validateUrl|isValidUrl/i.test(content);\n if (hasValidation) return [];\n for (const p of patterns) {\n const rawMatches = findMatches(content, p, ssrfVulnerability, filePath, () =>\n \"Validate URLs against an allowlist before fetching. Block internal IPs: 127.0.0.1, 10.x, 172.16-31.x, 192.168.x, 169.254.169.254 (cloud metadata). Use: const url = new URL(input); if (!ALLOWED_HOSTS.includes(url.hostname)) throw new Error('Blocked');\"\n );\n // Skip if the variable passed to fetch is defined as a constant string earlier in the file\n for (const rm of rawMatches) {\n const lineText = content.split(\"\\n\")[rm.line - 1] || \"\";\n const varMatch = lineText.match(/(?:fetch|axios\\.\\w+|got|request|https?\\.get)\\s*\\(\\s*(\\w+)/);\n if (varMatch) {\n const varName = varMatch[1];\n // Check if this variable is defined as a const string literal earlier in the file\n const constDef = new RegExp(`const\\\\s+${varName}\\\\s*=\\\\s*[\"'\\`]https?://`, \"i\");\n if (constDef.test(content)) continue;\n }\n matches.push(rm);\n }\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC042 – Mass Assignment\n// ────────────────────────────────────────────\n\nexport const massAssignment: CustomRule = {\n id: \"VC042\",\n title: \"Mass Assignment Vulnerability\",\n severity: \"high\",\n category: \"Authorization\",\n description: \"Spreading or assigning request body directly into database models allows attackers to set fields they shouldn't (e.g., isAdmin, role, verified).\",\n check(content, filePath) {\n const isApiFile = /(?:\\/api\\/|routes?\\/|controllers?\\/|server\\.|handler)/i.test(filePath);\n if (!isApiFile) return [];\n const matches: RuleMatch[] = [];\n const patterns = [\n // Object.assign(model, req.body)\n /Object\\.assign\\s*\\(\\s*(?:user|account|profile|record|doc|model|entity)[^,]*,\\s*(?:req\\.body|body|input|data)\\s*\\)/gi,\n // Spread req.body into create/update\n /(?:create|update|insert|save|findOneAndUpdate|updateOne|upsert)\\s*\\(\\s*\\{[^}]*\\.\\.\\.(?:req\\.body|body|input|data)/gi,\n // Direct req.body into DB\n /(?:create|insert|save)\\s*\\(\\s*(?:req\\.body|body)\\s*\\)/gi,\n ];\n const hasSanitization = /pick\\(|omit\\(|allowedFields|sanitize|whitelist|permit|strong_params/i.test(content);\n if (hasSanitization) return [];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, massAssignment, filePath, () =>\n \"Never pass req.body directly to database operations. Explicitly pick allowed fields: const { name, email } = req.body; await db.create({ name, email });\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC043 – Timing Attack on Comparison\n// ────────────────────────────────────────────\n\nexport const timingAttack: CustomRule = {\n id: \"VC043\",\n title: \"Timing-Unsafe Secret Comparison\",\n severity: \"medium\",\n category: \"Cryptography\",\n description: \"Using === to compare secrets, tokens, or hashes leaks information via timing side-channels. Attackers can determine the correct value one character at a time.\",\n check(content, filePath) {\n if (filePath.includes(\"test\") || filePath.includes(\"mock\")) return [];\n const matches: RuleMatch[] = [];\n // Direct comparison of secrets/tokens/hashes\n const patterns = [\n /(?:token|secret|hash|digest|signature|hmac|apiKey|api_key)\\s*(?:===|!==)\\s*(?:req\\.|body\\.|params\\.|query\\.|input)/gi,\n /(?:req\\.|body\\.|params\\.|query\\.|input)[\\w.]*(?:token|secret|hash|digest|signature|hmac)\\s*(?:===|!==)/gi,\n ];\n const hasTimingSafe = /timingSafeEqual|constantTimeEqual|safeCompare|secureCompare/i.test(content);\n if (hasTimingSafe) return [];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, timingAttack, filePath, () =>\n \"Use crypto.timingSafeEqual() for comparing secrets: crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b)). This prevents timing-based side-channel attacks.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC044 – Log Injection\n// ────────────────────────────────────────────\n\nexport const logInjection: CustomRule = {\n id: \"VC044\",\n title: \"Potential Log Injection\",\n severity: \"medium\",\n category: \"Injection\",\n description: \"Logging unsanitized user input allows attackers to forge log entries, inject malicious content, or exploit log aggregation systems via newlines and special characters.\",\n check(content, filePath) {\n if (filePath.includes(\"test\") || filePath.includes(\"mock\")) return [];\n const isServerFile = /(?:\\/api\\/|routes?\\/|controllers?\\/|server\\.|middleware|handler)/i.test(filePath);\n if (!isServerFile) return [];\n const matches: RuleMatch[] = [];\n // console.log/warn/error with req.body/query/params directly\n const patterns = [\n /console\\.(?:log|warn|error|info)\\s*\\([^)]*(?:req\\.body|req\\.query|req\\.params|req\\.headers)\\s*\\)/gi,\n /(?:logger|log)\\.(?:info|warn|error|debug)\\s*\\([^)]*(?:req\\.body|req\\.query|req\\.params)\\s*\\)/gi,\n ];\n const hasSanitization = /sanitize|escape|JSON\\.stringify|replace.*\\\\n/i.test(content);\n if (hasSanitization) return [];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, logInjection, filePath, () =>\n \"Sanitize user input before logging: strip newlines and control characters. Use JSON.stringify() or a structured logger (e.g., pino, winston) that escapes values automatically.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC045 – Weak Password Requirements\n// ────────────────────────────────────────────\n\nexport const weakPasswordRequirements: CustomRule = {\n id: \"VC045\",\n title: \"Weak Password Requirements\",\n severity: \"high\",\n category: \"Authentication\",\n description: \"Registration or password-change endpoints without minimum length or complexity validation allow weak passwords that are easily brute-forced.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!/(?:password|passwd|pwd)/i.test(content)) return [];\n if (!/(?:register|signup|sign.up|createUser|create.user|changePassword|resetPassword|set.password)/i.test(content) &&\n !/(?:\\/api\\/|routes?\\/|controllers?\\/)/i.test(filePath)) return [];\n // Check for validation anywhere in the file — broader detection of mitigation\n const hasValidation = /(?:password|pwd).*(?:\\.length|minLength|minlength|min_length)\\s*(?:>=?|<|>)\\s*\\d|(?:password|pwd).*(?:match|test|regex|pattern)|zxcvbn|password-validator|passwordStrength|isStrongPassword|joi\\.|yup\\.|zod\\.|validate|schema/i.test(content);\n if (hasValidation) return [];\n const hasPasswordHandling = /(?:password|pwd)\\s*[:=]\\s*(?:req\\.body|body|input|params|args)\\./i.test(content);\n if (!hasPasswordHandling) return [];\n const rawMatches = findMatches(content, /(?:password|pwd)\\s*[:=]\\s*(?:req\\.body|body|input|params|args)\\./gi, weakPasswordRequirements, filePath, () =>\n \"Enforce minimum password requirements: at least 8 characters, mix of letters/numbers/symbols. Use a library like zxcvbn for strength estimation.\"\n );\n // Skip if validation logic exists within 10 lines of the password assignment\n const lines = content.split(\"\\n\");\n const validationPattern = /\\.length|minLength|minlength|min_length|match|test|regex|pattern|validate|schema|zxcvbn|isStrongPassword/i;\n return rawMatches.filter((rm) => {\n const start = Math.max(0, rm.line - 1 - 10);\n const end = Math.min(lines.length, rm.line - 1 + 10);\n const nearby = lines.slice(start, end).join(\"\\n\");\n return !validationPattern.test(nearby);\n });\n },\n};\n\n// ────────────────────────────────────────────\n// VC046 – Session Fixation\n// ────────────────────────────────────────────\n\nexport const sessionFixation: CustomRule = {\n id: \"VC046\",\n title: \"Session Fixation Risk\",\n severity: \"high\",\n category: \"Authentication\",\n description: \"Not regenerating session IDs after login allows attackers to pre-set a session ID and hijack the authenticated session.\",\n check(content, filePath) {\n // Only applies to server-side code files, not iOS/Swift, docs, or tests\n if (!filePath.match(/\\.(js|ts|jsx|tsx|py|rb|go|java|php)$/)) return [];\n if (isTestFile(filePath)) return [];\n if (!/(?:login|signin|sign.in|authenticate)/i.test(content)) return [];\n if (!/session/i.test(content)) return [];\n // Check if session is regenerated after auth\n const hasRegenerate = /regenerate|destroy.*create|req\\.session\\.id\\s*=|session\\.regenerateId|rotateSession|clearCookies/i.test(content);\n if (hasRegenerate) return [];\n // Must have a login function definition, not just a reference or import\n const hasLogin = /(?:function\\s+(?:login|signin|authenticate)|(?:login|signin|authenticate)\\s*(?:=\\s*(?:async\\s*)?\\(|:\\s*(?:async\\s*)?\\())/i.test(content);\n if (!hasLogin) return [];\n return findMatches(content, /(?:function\\s+(?:login|signin|authenticate)|(?:login|signin|authenticate)\\s*(?:=\\s*(?:async\\s*)?\\())/gi, sessionFixation, filePath, () =>\n \"Regenerate the session ID after successful login: req.session.regenerate() (Express) or equivalent. This prevents session fixation attacks.\"\n );\n },\n};\n\n// ────────────────────────────────────────────\n// VC047 – Missing Brute Force Protection\n// ────────────────────────────────────────────\n\nexport const missingBruteForce: CustomRule = {\n id: \"VC047\",\n title: \"Login Without Brute Force Protection\",\n severity: \"high\",\n category: \"Authentication\",\n description: \"Login endpoints without rate limiting, account lockout, or progressive delays are vulnerable to credential stuffing and brute force attacks.\",\n check(content, filePath) {\n const isLoginFile = /(?:login|signin|sign.in|auth)/i.test(filePath) || /(?:login|signin|authenticate).*(?:post|handler|route)/i.test(content);\n if (!isLoginFile) return [];\n if (!/(?:password|credential)/i.test(content)) return [];\n const hasBruteForce = /rate.?limit|throttle|lockout|maxAttempts|max_attempts|failedAttempts|loginAttempts|brute|express-brute|express-rate-limit|slowDown/i.test(content);\n if (hasBruteForce) return [];\n return findMatches(content, /\\.(post|handler)\\s*\\([^)]*(?:login|signin|auth)/gi, missingBruteForce, filePath, () =>\n \"Add brute force protection to login endpoints: rate limiting (5 attempts/minute), progressive delays, or account lockout after N failures. Use express-rate-limit or similar.\"\n );\n },\n};\n\n// ────────────────────────────────────────────\n// VC048 – NoSQL Injection\n// ────────────────────────────────────────────\n\nexport const nosqlInjection: CustomRule = {\n id: \"VC048\",\n title: \"Potential NoSQL Injection\",\n severity: \"critical\",\n category: \"Injection\",\n description: \"Passing unsanitized user input directly into MongoDB/NoSQL queries allows attackers to bypass authentication, extract data, or modify queries using operators like $gt, $ne, $regex.\",\n check(content, filePath) {\n if (!/(?:mongo|mongoose|findOne|findById|find\\(|collection|aggregate)/i.test(content)) return [];\n const matches: RuleMatch[] = [];\n const patterns = [\n // Direct req.body in MongoDB queries\n /\\.find(?:One)?\\s*\\(\\s*(?:req\\.body|body|input|params)\\s*\\)/gi,\n /\\.find(?:One)?\\s*\\(\\s*\\{[^}]*:\\s*(?:req\\.body|body|input|params)\\./gi,\n // $where with user input\n /\\$where\\s*:\\s*(?![\"'`])/g,\n // Direct variable in query without sanitization\n /\\.(?:findOne|findById|deleteOne|updateOne|findOneAndUpdate)\\s*\\(\\s*\\{[^}]*:\\s*(?:req\\.(?:body|query|params))\\./gi,\n ];\n const hasSanitization = /sanitize|escape|mongo-sanitize|express-mongo-sanitize|validator|typeof.*===.*string/i.test(content);\n if (hasSanitization) return [];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, nosqlInjection, filePath, () =>\n \"Sanitize MongoDB query inputs: use express-mongo-sanitize, validate types (ensure strings aren't objects), and avoid $where. Example: if (typeof input !== 'string') throw new Error('Invalid input');\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC049 – Exposed DB Credentials in Config\n// ────────────────────────────────────────────\n\nexport const exposedDBCredentials: CustomRule = {\n id: \"VC049\",\n title: \"Database Credentials in Config File\",\n severity: \"critical\",\n category: \"Secrets\",\n description: \"Database connection strings with embedded usernames and passwords in committed config files expose credentials to anyone with repo access.\",\n check(content, filePath) {\n if (filePath.endsWith(\".example\") || filePath.endsWith(\".template\")) return [];\n if (!filePath.match(/(?:config|setting|database|db|knexfile|sequelize|drizzle|prisma)/i) && !filePath.match(/\\.(json|yaml|yml|toml|js|ts)$/)) return [];\n if (filePath.match(/\\.env/)) return []; // Handled by VC002\n const patterns = [\n // Connection strings with credentials\n /(?:host|server|database|db).*(?:password|passwd|pwd)\\s*[:=]\\s*[\"'`][^\"'`]{3,}[\"'`]/gi,\n // Inline connection URLs with credentials\n /(?:connection|database|db).*(?:postgres|mysql|mongodb|redis):\\/\\/[^:]+:[^@]+@/gi,\n ];\n const matches: RuleMatch[] = [];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, exposedDBCredentials, filePath, () =>\n \"Move database credentials to environment variables. Use: process.env.DATABASE_URL instead of hardcoding connection strings in config files.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC050 – Missing DB Connection Encryption\n// ────────────────────────────────────────────\n\nexport const missingDBEncryption: CustomRule = {\n id: \"VC050\",\n title: \"Database Connection Without SSL/TLS\",\n severity: \"high\",\n category: \"Configuration\",\n description: \"Database connections without SSL/TLS encryption transmit credentials and data in plaintext, allowing eavesdropping on the network.\",\n check(content, filePath) {\n if (!/(?:createConnection|createPool|createClient|connect|new.*Client|knex|sequelize|drizzle)/i.test(content)) return [];\n if (!/(?:postgres|mysql|mariadb|pg|mongo)/i.test(content)) return [];\n const matches: RuleMatch[] = [];\n // SSL explicitly disabled\n const sslDisabled = [\n /ssl\\s*:\\s*false/gi,\n /sslmode\\s*[:=]\\s*[\"'`]?disable[\"'`]?/gi,\n /rejectUnauthorized\\s*:\\s*false/gi,\n ];\n for (const p of sslDisabled) {\n matches.push(...findMatches(content, p, missingDBEncryption, filePath, () =>\n \"Enable SSL/TLS for database connections: { ssl: { rejectUnauthorized: true } }. In production, always verify server certificates.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC051 – GraphQL Introspection Enabled\n// ────────────────────────────────────────────\n\nexport const graphqlIntrospection: CustomRule = {\n id: \"VC051\",\n title: \"GraphQL Introspection Enabled in Production\",\n severity: \"medium\",\n category: \"Information Leakage\",\n description: \"GraphQL introspection exposes your entire API schema, types, queries, and mutations to attackers, making it easy to find attack vectors.\",\n check(content, filePath) {\n if (!/graphql/i.test(content) && !/graphql/i.test(filePath)) return [];\n const matches: RuleMatch[] = [];\n // Introspection explicitly enabled or not disabled\n if (/introspection\\s*:\\s*true/i.test(content)) {\n matches.push(...findMatches(content, /introspection\\s*:\\s*true/gi, graphqlIntrospection, filePath, () =>\n \"Disable GraphQL introspection in production: introspection: process.env.NODE_ENV !== 'production'. This prevents schema exposure.\"\n ));\n }\n // GraphQL server setup without introspection config\n if (/(?:ApolloServer|GraphQLServer|createYoga|buildSchema|makeExecutableSchema)\\s*\\(/i.test(content)) {\n if (!/introspection/i.test(content)) {\n matches.push(...findMatches(content, /(?:ApolloServer|GraphQLServer|createYoga)\\s*\\(/gi, graphqlIntrospection, filePath, () =>\n \"Explicitly disable introspection in production: new ApolloServer({ introspection: process.env.NODE_ENV !== 'production' })\"\n ));\n }\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC052 – Missing Request Size Limit\n// ────────────────────────────────────────────\n\nexport const missingRequestSizeLimit: CustomRule = {\n id: \"VC052\",\n title: \"Missing Request Body Size Limit\",\n severity: \"medium\",\n category: \"Availability\",\n description: \"Express/Hono/Fastify servers without request body size limits are vulnerable to denial-of-service via oversized payloads that exhaust memory.\",\n check(content, filePath) {\n if (!/(?:server|app|index|main)\\.[jt]sx?$/.test(filePath)) return [];\n if (!/(?:express|hono|fastify|koa)/i.test(content)) return [];\n const matches: RuleMatch[] = [];\n // express.json() without limit\n if (/express\\.json\\s*\\(\\s*\\)/g.test(content)) {\n matches.push(...findMatches(content, /express\\.json\\s*\\(\\s*\\)/g, missingRequestSizeLimit, filePath, () =>\n \"Set a body size limit: express.json({ limit: '1mb' }). Without this, attackers can send huge payloads to crash your server.\"\n ));\n }\n // bodyParser without limit\n if (/bodyParser\\.json\\s*\\(\\s*\\)/g.test(content)) {\n matches.push(...findMatches(content, /bodyParser\\.json\\s*\\(\\s*\\)/g, missingRequestSizeLimit, filePath, () =>\n \"Set a body size limit: bodyParser.json({ limit: '1mb' }).\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC053 – Hardcoded IP/Host Allowlist\n// ────────────────────────────────────────────\n\nexport const hardcodedIPAllowlist: CustomRule = {\n id: \"VC053\",\n title: \"Hardcoded IP or Host Allowlist\",\n severity: \"medium\",\n category: \"Configuration\",\n description: \"Hardcoded IP addresses or hostnames in allowlists are brittle and hard to update. They should be in environment variables or configuration files.\",\n check(content, filePath) {\n if (filePath.includes(\"test\") || filePath.includes(\"mock\") || filePath.match(/\\.(md|txt)$/)) return [];\n const matches: RuleMatch[] = [];\n // Arrays of IPs used in access control\n const patterns = [\n /(?:allowedIPs|allowed_ips|whitelist|allowlist|trustedHosts)\\s*[:=]\\s*\\[\\s*[\"'`]\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}/gi,\n /(?:allowedIPs|allowed_ips|whitelist|allowlist|trustedHosts)\\s*[:=]\\s*\\[\\s*[\"'`][\\w.-]+\\.(?:com|net|org|io)/gi,\n ];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, hardcodedIPAllowlist, filePath, () =>\n \"Move IP/host allowlists to environment variables or a config file: const allowed = process.env.ALLOWED_IPS?.split(',') || [];\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC054 – Sensitive Data in localStorage\n// ────────────────────────────────────────────\n\nexport const sensitiveLocalStorage: CustomRule = {\n id: \"VC054\",\n title: \"Sensitive Data in localStorage\",\n severity: \"high\",\n category: \"Secrets\",\n description: \"Storing tokens, passwords, or secrets in localStorage is insecure — it's accessible to any JavaScript on the page (XSS) and persists indefinitely. Use httpOnly cookies instead.\",\n check(content, filePath) {\n if (!filePath.match(/\\.(jsx?|tsx?|vue|svelte)$/)) return [];\n if (isTestFile(filePath)) return [];\n // Skip test-related files (mock, spec in the path)\n if (/mock|spec/i.test(filePath)) return [];\n // Skip if localStorage.removeItem is used (cleanup code, not storage)\n if (!/localStorage\\.setItem|localStorage\\[/i.test(content)) return [];\n const matches: RuleMatch[] = [];\n const patterns = [\n /localStorage\\.setItem\\s*\\(\\s*[\"'`](?:token|access_token|auth_token|jwt|session|refresh_token|api_key|password|secret)/gi,\n /localStorage\\s*\\[\\s*[\"'`](?:token|access_token|auth_token|jwt|session|refresh_token|api_key|password|secret)/gi,\n ];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, sensitiveLocalStorage, filePath, () =>\n \"Don't store tokens/secrets in localStorage — use httpOnly cookies instead. localStorage is accessible to any XSS attack. For session tokens, set them as httpOnly, secure, sameSite cookies.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC055 – Exposed Source Maps in Production\n// ────────────────────────────────────────────\n\nexport const exposedSourceMaps: CustomRule = {\n id: \"VC055\",\n title: \"Source Maps Exposed in Production\",\n severity: \"medium\",\n category: \"Information Leakage\",\n description: \"Source map files (.map) in production expose your original source code, comments, and internal logic to anyone who downloads them.\",\n check(content, filePath) {\n // Check build configs for source maps in production\n if (!filePath.match(/(?:webpack|vite|rollup|next)\\.config|tsconfig/i)) return [];\n const matches: RuleMatch[] = [];\n // Source maps enabled without environment check\n if (/(?:sourceMap|source-map|sourcemap)\\s*[:=]\\s*true/i.test(content)) {\n const hasEnvCheck = /process\\.env\\.NODE_ENV|NODE_ENV|production/i.test(content);\n if (!hasEnvCheck) {\n matches.push(...findMatches(content, /(?:sourceMap|source-map|sourcemap)\\s*[:=]\\s*true/gi, exposedSourceMaps, filePath, () =>\n \"Disable source maps in production builds: sourceMap: process.env.NODE_ENV !== 'production'. Or use 'hidden-source-map' to generate maps without exposing them.\"\n ));\n }\n }\n // productionSourceMap in Vue\n if (/productionSourceMap\\s*:\\s*true/i.test(content)) {\n matches.push(...findMatches(content, /productionSourceMap\\s*:\\s*true/gi, exposedSourceMaps, filePath, () =>\n \"Set productionSourceMap: false to avoid exposing source code in production.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC056 – Clickjacking / Missing X-Frame-Options\n// ────────────────────────────────────────────\n\nexport const clickjacking: CustomRule = {\n id: \"VC056\",\n title: \"Clickjacking — Missing X-Frame-Options\",\n severity: \"medium\",\n category: \"Configuration\",\n description: \"Without X-Frame-Options or frame-ancestors CSP directive, your page can be embedded in an attacker's iframe for UI redress (clickjacking) attacks.\",\n check(content, filePath) {\n // Check HTML files\n if (filePath.match(/\\.(html|htm)$/)) {\n if (!/X-Frame-Options|frame-ancestors/i.test(content)) {\n return [{\n rule: \"VC056\", title: clickjacking.title, severity: \"medium\" as const, category: \"Configuration\",\n file: filePath, line: 1, snippet: getSnippet(content, 1),\n fix: 'Add <meta http-equiv=\"X-Frame-Options\" content=\"DENY\"> or set frame-ancestors in CSP to prevent clickjacking.'\n }];\n }\n }\n // Check server configs\n if (/(?:server|app|index|main)\\.[jt]sx?$/.test(filePath)) {\n if (/(?:express|hono|fastify|koa)/i.test(content)) {\n if (!/X-Frame-Options|frame-ancestors|helmet/i.test(content)) {\n return findMatches(content, /(?:express|hono|fastify|koa)\\s*\\(/gi, clickjacking, filePath, () =>\n \"Add X-Frame-Options header: res.setHeader('X-Frame-Options', 'DENY'). Or use helmet: app.use(helmet()) which sets this and other security headers.\"\n );\n }\n }\n }\n return [];\n },\n};\n\n// ────────────────────────────────────────────\n// VC057 – Overly Permissive IAM/Cloud Roles\n// ────────────────────────────────────────────\n\nexport const overlyPermissiveIAM: CustomRule = {\n id: \"VC057\",\n title: \"Overly Permissive IAM/Cloud Permissions\",\n severity: \"critical\",\n category: \"Authorization\",\n description: \"Wildcard (*) permissions in AWS IAM, GCP, or Terraform configs grant unrestricted access, violating the principle of least privilege.\",\n check(content, filePath) {\n if (!filePath.match(/\\.(tf|hcl|json|yaml|yml)$/) && !filePath.match(/(?:iam|policy|role|permission)/i)) return [];\n const matches: RuleMatch[] = [];\n const patterns = [\n // AWS IAM wildcard\n /[\"'`]Action[\"'`]\\s*:\\s*[\"'`]\\*[\"'`]/g,\n /[\"'`]Resource[\"'`]\\s*:\\s*[\"'`]\\*[\"'`]/g,\n // Terraform aws_iam\n /actions\\s*=\\s*\\[\\s*[\"'`]\\*[\"'`]\\s*\\]/g,\n /resources\\s*=\\s*\\[\\s*[\"'`]\\*[\"'`]\\s*\\]/g,\n // GCP bindings\n /role\\s*[:=]\\s*[\"'`]roles\\/(?:owner|editor)[\"'`]/g,\n ];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, overlyPermissiveIAM, filePath, () =>\n \"Follow the principle of least privilege: replace wildcard (*) with specific actions and resources. Example: 'Action': 's3:GetObject' instead of '*'.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC058 – Docker Running as Root\n// ────────────────────────────────────────────\n\nexport const dockerRunAsRoot: CustomRule = {\n id: \"VC058\",\n title: \"Docker Container Running as Root\",\n severity: \"high\",\n category: \"Configuration\",\n description: \"Containers running as root give attackers full system access if they escape the container. Always run as a non-root user.\",\n check(content, filePath) {\n if (!filePath.match(/Dockerfile$/i)) return [];\n const hasUser = /^\\s*USER\\s+/m.test(content);\n if (hasUser) return [];\n return [{\n rule: \"VC058\", title: dockerRunAsRoot.title, severity: \"high\" as const, category: \"Configuration\",\n file: filePath, line: 1, snippet: getSnippet(content, 1),\n fix: \"Add a USER directive: RUN addgroup -S app && adduser -S app -G app\\\\nUSER app. Place it after installing dependencies but before COPY/CMD.\"\n }];\n },\n};\n\n// ────────────────────────────────────────────\n// VC059 – Exposed Ports in Docker Compose\n// ────────────────────────────────────────────\n\nexport const exposedDockerPorts: CustomRule = {\n id: \"VC059\",\n title: \"Docker Compose Binding to All Interfaces\",\n severity: \"medium\",\n category: \"Configuration\",\n description: \"Binding ports to 0.0.0.0 (default) in Docker Compose exposes services to the entire network. Bind to 127.0.0.1 for local-only access.\",\n check(content, filePath) {\n if (!filePath.match(/docker-compose|compose\\.(yaml|yml)$/i)) return [];\n const matches: RuleMatch[] = [];\n // ports: \"3000:3000\" or \"8080:80\" without binding to 127.0.0.1\n const portPattern = /ports:\\s*\\n(?:\\s*-\\s*[\"'`]?\\d+:\\d+[\"'`]?\\s*\\n?)+/g;\n if (portPattern.test(content) && !/127\\.0\\.0\\.1:/i.test(content)) {\n matches.push(...findMatches(content, /^\\s*-\\s*[\"'`]?\\d+:\\d+[\"'`]?/gm, exposedDockerPorts, filePath, () =>\n \"Bind to localhost only: '127.0.0.1:3000:3000' instead of '3000:3000'. This prevents external network access to the service.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC060 – Weak Hashing Algorithm\n// ────────────────────────────────────────────\n\nexport const weakHashing: CustomRule = {\n id: \"VC060\",\n title: \"Weak Hashing Algorithm for Passwords\",\n severity: \"critical\",\n category: \"Cryptography\",\n description: \"MD5 and SHA1/SHA256 are too fast for password hashing — they can be brute-forced at billions of attempts per second. Use bcrypt, scrypt, or argon2 instead.\",\n check(content, filePath) {\n if (filePath.includes(\"test\") || filePath.includes(\"mock\")) return [];\n // Skip if file also uses strong hashing (indicates migration or comparison code)\n if (/bcrypt|scrypt|argon2/i.test(content)) return [];\n const matches: RuleMatch[] = [];\n // MD5/SHA used with password context\n const patterns = [\n /(?:md5|sha1|sha256|sha512)\\s*\\([^)]*(?:password|passwd|pwd)/gi,\n /createHash\\s*\\(\\s*[\"'`](?:md5|sha1|sha256)[\"'`]\\).*(?:password|passwd|pwd)/gi,\n /(?:password|passwd|pwd).*createHash\\s*\\(\\s*[\"'`](?:md5|sha1|sha256)[\"'`]\\)/gi,\n /hashlib\\.(?:md5|sha1|sha256)\\s*\\([^)]*(?:password|passwd|pwd)/gi,\n /Digest::(?:MD5|SHA1|SHA256).*(?:password|passwd|pwd)/gi,\n ];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, weakHashing, filePath, () =>\n \"Use bcrypt, scrypt, or argon2 for password hashing — they're intentionally slow. Example: const hash = await bcrypt.hash(password, 12);\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC061 – Disabled TLS Certificate Verification\n// ────────────────────────────────────────────\n\nexport const disabledTLSVerification: CustomRule = {\n id: \"VC061\",\n title: \"Disabled TLS Certificate Verification\",\n severity: \"critical\",\n category: \"Cryptography\",\n description: \"Disabling TLS certificate verification (NODE_TLS_REJECT_UNAUTHORIZED=0 or rejectUnauthorized:false) makes all HTTPS connections vulnerable to man-in-the-middle attacks.\",\n check(content, filePath) {\n const matches: RuleMatch[] = [];\n const patterns = [\n /NODE_TLS_REJECT_UNAUTHORIZED\\s*[:=]\\s*[\"'`]?0[\"'`]?/g,\n /rejectUnauthorized\\s*:\\s*false/g,\n /verify\\s*[:=]\\s*false.*(?:ssl|tls|cert|https)/gi,\n /PYTHONHTTPSVERIFY\\s*[:=]\\s*[\"'`]?0[\"'`]?/g,\n /ssl_verify\\s*[:=]\\s*false/gi,\n /InsecureSkipVerify\\s*:\\s*true/g,\n ];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, disabledTLSVerification, filePath, () =>\n \"Never disable TLS certificate verification in production. Fix the root cause: install the correct CA certificate, or use NODE_EXTRA_CA_CERTS for custom CAs.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC062 – Hardcoded Encryption Key/IV\n// ────────────────────────────────────────────\n\nexport const hardcodedEncryptionKey: CustomRule = {\n id: \"VC062\",\n title: \"Hardcoded Encryption Key or IV\",\n severity: \"critical\",\n category: \"Cryptography\",\n description: \"Hardcoded encryption keys and initialization vectors (IVs) in source code can be extracted to decrypt all data. IVs must be random per encryption operation.\",\n check(content, filePath) {\n if (filePath.endsWith(\".example\") || filePath.endsWith(\".template\")) return [];\n if (isTestFile(filePath)) return [];\n // Skip HTML/XML/config files — meta tags and config values are not encryption keys\n if (filePath.match(/\\.(html|htm|xml|plist|svg|xhtml)$/)) return [];\n // Skip files without any crypto-related code\n if (!/(?:cipher|encrypt|decrypt|crypto|aes|createCipher)/i.test(content)) return [];\n const matches: RuleMatch[] = [];\n const patterns = [\n // Encryption key as string literal\n /(?:encryption_key|encryptionKey|cipher_key|cipherKey|aes_key|AES_KEY|ENCRYPTION_KEY)\\s*[:=]\\s*[\"'`][^\"'`]{8,}[\"'`]/g,\n // createCipheriv with hardcoded key\n /createCipher(?:iv)?\\s*\\(\\s*[\"'`][^\"'`]+[\"'`]\\s*,\\s*[\"'`][^\"'`]+[\"'`]/g,\n // Buffer.from with hardcoded key near cipher context\n /(?:key|iv|nonce)\\s*[:=]\\s*Buffer\\.from\\s*\\(\\s*[\"'`][^\"'`]{8,}[\"'`]/gi,\n // Static IV (should be random) — only in crypto context\n /(?:^|[\\s,({])(?:iv|nonce|initialVector)\\s*[:=]\\s*[\"'`][0-9a-fA-F]{16,}[\"'`]/gi,\n ];\n for (const p of patterns) {\n const rawMatches = findMatches(content, p, hardcodedEncryptionKey, filePath, () =>\n \"Move encryption keys to environment variables. Generate IVs randomly per operation: crypto.randomBytes(16). Never reuse IVs.\"\n );\n // Skip matches on lines containing CSP meta tags or security headers\n for (const rm of rawMatches) {\n const lineStart = content.lastIndexOf(\"\\n\", content.split(\"\\n\").slice(0, rm.line - 1).join(\"\\n\").length) + 1;\n const lineEnd = content.indexOf(\"\\n\", lineStart + 1);\n const lineText = content.substring(lineStart, lineEnd === -1 ? content.length : lineEnd);\n if (/meta\\s+http-equiv|Content-Security-Policy|X-Frame-Options|X-Content-Type/i.test(lineText)) continue;\n matches.push(rm);\n }\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC063 – dangerouslySetInnerHTML\n// ────────────────────────────────────────────\n\nexport const dangerousInnerHTML: CustomRule = {\n id: \"VC063\",\n title: \"Unsanitized dangerouslySetInnerHTML\",\n severity: \"critical\",\n category: \"Injection\",\n description: \"Using dangerouslySetInnerHTML without sanitization (DOMPurify) enables XSS attacks. User-controlled content injected as raw HTML can execute arbitrary JavaScript.\",\n check(content, filePath) {\n if (!filePath.match(/\\.(jsx|tsx)$/)) return [];\n if (isTestFile(filePath)) return [];\n if (!/dangerouslySetInnerHTML/i.test(content)) return [];\n const hasSanitize = /DOMPurify|sanitize|purify|xss|sanitizeHtml|isomorphic-dompurify/i.test(content);\n if (hasSanitize) return [];\n // Find all dangerouslySetInnerHTML usages, but skip if the value is a\n // static constant (all-caps like THEME_INIT_SCRIPT), a string literal,\n // or a marked safe value — these are not user-controlled input\n const findings: RuleMatch[] = [];\n const re = /dangerouslySetInnerHTML\\s*=\\s*\\{\\s*\\{\\s*__html\\s*:\\s*([^}]+)\\}/g;\n let m: RegExpExecArray | null;\n while ((m = re.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n const value = m[1].trim();\n // Skip static constants (UPPER_CASE), string literals, or template strings without interpolation\n if (/^[A-Z][A-Z0-9_]+$/.test(value)) continue;\n if (/^[\"'`][^$]*[\"'`]$/.test(value)) continue;\n // Skip JSON-LD structured data (standard SEO practice, developer-controlled)\n if (/JSON\\.stringify/i.test(value)) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC063\", title: dangerousInnerHTML.title,\n severity: \"critical\" as const, category: \"Injection\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Sanitize HTML before using dangerouslySetInnerHTML: dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(content) }}. Install: npm install dompurify\",\n });\n }\n return findings;\n },\n};\n\n// ────────────────────────────────────────────\n// VC064 – Exposed Next.js Server Actions\n// ────────────────────────────────────────────\n\nexport const exposedServerActions: CustomRule = {\n id: \"VC064\",\n title: \"Next.js Server Action Without Auth Check\",\n severity: \"high\",\n category: \"Authorization\",\n description: \"Next.js Server Actions ('use server') are publicly callable endpoints. Without authentication checks, anyone can invoke them directly.\",\n check(content, filePath) {\n if (!filePath.match(/\\.(jsx?|tsx?)$/)) return [];\n if (!/[\"']use server[\"']/i.test(content)) return [];\n const hasAuth = /getServerSession|auth\\(\\)|currentUser|getUser|requireAuth|requireUser|requireUserForApi|session|clerk|getAuth|verifyCronSecret|checkApiKey/i.test(content);\n if (hasAuth) return [];\n // Check if there are exported async functions (server actions)\n if (/export\\s+async\\s+function/i.test(content)) {\n return findMatches(content, /export\\s+async\\s+function\\s+\\w+/g, exposedServerActions, filePath, () =>\n \"Add authentication to Server Actions: const session = await getServerSession(); if (!session) throw new Error('Unauthorized');\"\n );\n }\n return [];\n },\n};\n\n// ────────────────────────────────────────────\n// VC065 – Unprotected Next.js API Routes\n// ────────────────────────────────────────────\n\nexport const unprotectedAPIRoutes: CustomRule = {\n id: \"VC065\",\n title: \"Unprotected Next.js API Route\",\n severity: \"high\",\n category: \"Authorization\",\n description: \"Next.js API routes under /api/ without authentication middleware can be called by anyone, exposing data or mutations.\",\n check(content, filePath) {\n if (!filePath.match(/\\/api\\/.*\\.(jsx?|tsx?)$/) && !filePath.match(/\\/app\\/api\\/.*route\\.(jsx?|tsx?)$/)) return [];\n // Skip health/public endpoints\n if (/health|status|public|webhook/i.test(filePath)) return [];\n const hasAuth = /getServerSession|auth\\(\\)|currentUser|getUser|requireAuth|session|clerk|getAuth|verifyToken|authenticate|middleware/i.test(content);\n if (hasAuth) return [];\n const hasHandler = /export\\s+(?:async\\s+)?function\\s+(?:GET|POST|PUT|DELETE|PATCH)|export\\s+default/i.test(content);\n if (hasHandler) {\n return findMatches(content, /export\\s+(?:async\\s+)?function\\s+(?:GET|POST|PUT|DELETE|PATCH)/g, unprotectedAPIRoutes, filePath, () =>\n \"Add authentication to API routes: const session = await getServerSession(authOptions); if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });\"\n );\n }\n return [];\n },\n};\n\n// ────────────────────────────────────────────\n// VC066 – Client Component Using Secrets\n// ────────────────────────────────────────────\n\nexport const clientComponentSecret: CustomRule = {\n id: \"VC066\",\n title: \"Secret Used in Client Component\",\n severity: \"critical\",\n category: \"Secrets\",\n description: \"Using server-side secrets (process.env without NEXT_PUBLIC_) in 'use client' components exposes them in the browser bundle.\",\n check(content, filePath) {\n if (!filePath.match(/\\.(jsx?|tsx?)$/)) return [];\n if (!/[\"']use client[\"']/i.test(content)) return [];\n // process.env without NEXT_PUBLIC_ prefix in a client component\n const pattern = /process\\.env\\.(?!NEXT_PUBLIC_)[A-Z_]{3,}/g;\n if (pattern.test(content)) {\n return findMatches(content, /process\\.env\\.(?!NEXT_PUBLIC_)[A-Z_]{3,}/g, clientComponentSecret, filePath, () =>\n \"Server-side env vars are not available in client components and may leak in builds. Move this logic to a Server Component or API route.\"\n );\n }\n return [];\n },\n};\n\n// ────────────────────────────────────────────\n// VC067 – Insecure Deep Link Handling\n// ────────────────────────────────────────────\n\nexport const insecureDeepLink: CustomRule = {\n id: \"VC067\",\n title: \"Insecure Deep Link Handling\",\n severity: \"high\",\n category: \"Injection\",\n description: \"Deep links that navigate or execute actions without validating the URL scheme, host, or parameters can be exploited for phishing or unauthorized actions.\",\n check(content, filePath) {\n if (!/(?:Linking|DeepLinking|deep.?link|handleURL|openURL|url.?scheme)/i.test(content)) return [];\n const matches: RuleMatch[] = [];\n // React Native Linking without validation\n const patterns = [\n /Linking\\.addEventListener\\s*\\([^)]*(?:url|link)/gi,\n /Linking\\.getInitialURL\\s*\\(\\)/g,\n /handleOpenURL|handleDeepLink|onDeepLink/gi,\n ];\n const hasValidation = /allowedSchemes|allowedHosts|validateURL|isAllowedURL|whitelist|URL.*hostname.*includes/i.test(content);\n if (hasValidation) return [];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, insecureDeepLink, filePath, () =>\n \"Validate deep link URLs: check the scheme and host against an allowlist before navigating or executing actions.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC068 – Sensitive Data in AsyncStorage\n// ────────────────────────────────────────────\n\nexport const sensitiveAsyncStorage: CustomRule = {\n id: \"VC068\",\n title: \"Sensitive Data in AsyncStorage\",\n severity: \"high\",\n category: \"Secrets\",\n description: \"React Native AsyncStorage is unencrypted. Storing tokens, passwords, or secrets there makes them readable by other apps or anyone with device access.\",\n check(content, filePath) {\n if (!/AsyncStorage/i.test(content)) return [];\n const patterns = [\n /AsyncStorage\\.setItem\\s*\\(\\s*[\"'`](?:token|access_token|auth_token|jwt|session|refresh_token|api_key|password|secret|private_key)/gi,\n ];\n const matches: RuleMatch[] = [];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, sensitiveAsyncStorage, filePath, () =>\n \"Use react-native-keychain or expo-secure-store instead of AsyncStorage for sensitive data. These use the OS keychain (encrypted).\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC069 – Missing Certificate Pinning\n// ────────────────────────────────────────────\n\nexport const missingCertPinning: CustomRule = {\n id: \"VC069\",\n title: \"Missing Certificate Pinning in Mobile App\",\n severity: \"medium\",\n category: \"Cryptography\",\n description: \"Mobile apps without SSL certificate pinning are vulnerable to MITM attacks via compromised or rogue CAs. Pin your API server's certificate.\",\n check(content, filePath) {\n // Only for mobile project files\n if (!/(?:react.native|expo|android|ios|mobile)/i.test(filePath) && !/(?:React.*Native|expo)/i.test(content)) return [];\n if (!/(?:fetch|axios|http|api|request)/i.test(content)) return [];\n // Check for HTTP client setup without pinning\n if (/axios\\.create|new\\s+(?:HttpClient|ApiClient)/i.test(content)) {\n const hasPinning = /pinning|certificate|cert|ssl|TrustKit|react-native-ssl-pinning|cert-pinner/i.test(content);\n if (!hasPinning) {\n return findMatches(content, /axios\\.create|new\\s+(?:HttpClient|ApiClient)/gi, missingCertPinning, filePath, () =>\n \"Add SSL certificate pinning: use react-native-ssl-pinning or TrustKit. This prevents MITM attacks via rogue certificates.\"\n );\n }\n }\n return [];\n },\n};\n\n// ────────────────────────────────────────────\n// VC070 – Android Debuggable Flag\n// ────────────────────────────────────────────\n\nexport const androidDebuggable: CustomRule = {\n id: \"VC070\",\n title: \"Android App Debuggable in Production\",\n severity: \"high\",\n category: \"Configuration\",\n description: \"android:debuggable='true' in AndroidManifest.xml allows attackers to attach debuggers, inspect memory, and bypass security controls.\",\n check(content, filePath) {\n if (!filePath.match(/AndroidManifest\\.xml$/i)) return [];\n if (/android:debuggable\\s*=\\s*[\"']true[\"']/i.test(content)) {\n return findMatches(content, /android:debuggable\\s*=\\s*[\"']true[\"']/gi, androidDebuggable, filePath, () =>\n \"Remove android:debuggable='true' or set it to false. Debug builds should use build variants, not manifest flags.\"\n );\n }\n return [];\n },\n};\n\n// ────────────────────────────────────────────\n// VC071 – Django DEBUG=True\n// ────────────────────────────────────────────\n\nexport const djangoDebug: CustomRule = {\n id: \"VC071\",\n title: \"Django DEBUG Mode Enabled\",\n severity: \"critical\",\n category: \"Configuration\",\n description: \"Django with DEBUG=True exposes detailed error pages with source code, database queries, environment variables, and installed apps to anyone.\",\n check(content, filePath) {\n if (!filePath.match(/settings\\.py$/i) && !filePath.match(/config.*\\.py$/i)) return [];\n if (/^\\s*DEBUG\\s*=\\s*True\\s*$/m.test(content)) {\n const hasEnvCheck = /os\\.environ|env\\(|config\\(|getenv/i.test(content);\n if (!hasEnvCheck) {\n return findMatches(content, /^\\s*DEBUG\\s*=\\s*True/gm, djangoDebug, filePath, () =>\n \"Use environment variable: DEBUG = os.environ.get('DEBUG', 'False') == 'True'. Never hardcode DEBUG=True.\"\n );\n }\n }\n return [];\n },\n};\n\n// ────────────────────────────────────────────\n// VC072 – Flask Hardcoded Secret Key\n// ────────────────────────────────────────────\n\nexport const flaskSecretKey: CustomRule = {\n id: \"VC072\",\n title: \"Flask Secret Key Hardcoded\",\n severity: \"critical\",\n category: \"Secrets\",\n description: \"Hardcoded Flask secret_key allows attackers to forge sessions, CSRF tokens, and signed cookies. Must be a random value from environment variables.\",\n check(content, filePath) {\n if (!filePath.match(/\\.py$/)) return [];\n const patterns = [\n /(?:app\\.)?secret_key\\s*=\\s*[\"'`][^\"'`]{3,}[\"'`]/g,\n /(?:app\\.config)\\s*\\[\\s*[\"']SECRET_KEY[\"']\\s*\\]\\s*=\\s*[\"'`][^\"'`]{3,}[\"'`]/g,\n ];\n const hasEnv = /os\\.environ|env\\(|config\\(|getenv/i.test(content);\n if (hasEnv) return [];\n const matches: RuleMatch[] = [];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, flaskSecretKey, filePath, () =>\n \"Use environment variable: app.secret_key = os.environ['SECRET_KEY']. Generate with: python -c \\\"import secrets; print(secrets.token_hex(32))\\\"\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC073 – Pickle Deserialization\n// ────────────────────────────────────────────\n\nexport const pickleDeserialization: CustomRule = {\n id: \"VC073\",\n title: \"Unsafe Pickle Deserialization\",\n severity: \"critical\",\n category: \"Injection\",\n description: \"pickle.loads() on untrusted data allows arbitrary code execution. An attacker can craft a pickle payload that runs system commands on your server.\",\n check(content, filePath) {\n if (!filePath.match(/\\.py$/)) return [];\n const matches: RuleMatch[] = [];\n const patterns = [\n /pickle\\.loads?\\s*\\(/g,\n /cPickle\\.loads?\\s*\\(/g,\n /shelve\\.open\\s*\\(/g,\n /yaml\\.load\\s*\\([^)]*(?!Loader\\s*=\\s*yaml\\.SafeLoader)/g,\n ];\n const hasSafe = /restricted_loads|SafeUnpickler|safe_load|yaml\\.safe_load/i.test(content);\n if (hasSafe) return [];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, pickleDeserialization, filePath, () =>\n \"Never unpickle untrusted data — it allows arbitrary code execution. Use JSON for data exchange, or yaml.safe_load() for YAML.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC074 – Missing CSRF Protection (Django)\n// ────────────────────────────────────────────\n\nexport const missingCSRF: CustomRule = {\n id: \"VC074\",\n title: \"CSRF Protection Disabled\",\n severity: \"high\",\n category: \"Authorization\",\n description: \"Using @csrf_exempt on state-changing views (POST/PUT/DELETE) allows attackers to forge requests from other sites, performing actions as authenticated users.\",\n check(content, filePath) {\n if (!filePath.match(/\\.py$/)) return [];\n const matches: RuleMatch[] = [];\n if (/csrf_exempt/i.test(content)) {\n matches.push(...findMatches(content, /@csrf_exempt/g, missingCSRF, filePath, () =>\n \"Remove @csrf_exempt and use proper CSRF tokens. For APIs, use token-based auth (JWT) instead of session cookies.\"\n ));\n }\n // Also check for CsrfViewMiddleware removal\n if (/MIDDLEWARE.*=.*\\[/s.test(content) && !/CsrfViewMiddleware/i.test(content) && /django/i.test(content)) {\n matches.push(...findMatches(content, /MIDDLEWARE\\s*=/g, missingCSRF, filePath, () =>\n \"Re-add 'django.middleware.csrf.CsrfViewMiddleware' to MIDDLEWARE. CSRF protection is essential for session-based auth.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC075 – GitHub Actions Script Injection\n// ────────────────────────────────────────────\n\nexport const githubActionsInjection: CustomRule = {\n id: \"VC075\",\n title: \"GitHub Actions Script Injection\",\n severity: \"critical\",\n category: \"Injection\",\n description: \"Using ${{ github.event.* }} directly in 'run:' steps allows attackers to inject shell commands via PR titles, issue bodies, or branch names.\",\n check(content, filePath) {\n if (!filePath.match(/\\.github\\/workflows\\/.*\\.(yml|yaml)$/i)) return [];\n const matches: RuleMatch[] = [];\n // Direct interpolation in run steps\n const patterns = [\n /run:.*\\$\\{\\{\\s*github\\.event\\.(?:issue|pull_request|comment|review|head_commit)\\.(?:title|body|message)/gi,\n /run:.*\\$\\{\\{\\s*github\\.event\\.(?:inputs|head_ref|base_ref)/gi,\n ];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, githubActionsInjection, filePath, () =>\n \"Never use ${{ github.event.* }} directly in 'run:'. Pass it as an environment variable: env: TITLE: ${{ github.event.issue.title }} then use $TITLE in the script.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC076 – Secrets in CI Config\n// ────────────────────────────────────────────\n\nexport const secretsInCI: CustomRule = {\n id: \"VC076\",\n title: \"Hardcoded Secrets in CI/CD Config\",\n severity: \"critical\",\n category: \"Secrets\",\n description: \"Hardcoded tokens, passwords, or API keys in CI/CD workflow files are visible to anyone with repo access. Use encrypted secrets instead.\",\n check(content, filePath) {\n if (!filePath.match(/\\.github\\/workflows\\/|\\.gitlab-ci|Jenkinsfile|\\.circleci|bitbucket-pipelines/i)) return [];\n const matches: RuleMatch[] = [];\n // Hardcoded values that look like secrets (not using ${{ secrets.* }})\n const patterns = [\n /(?:password|token|key|secret|api_key|apikey)\\s*[:=]\\s*[\"'`][A-Za-z0-9+/=_-]{20,}[\"'`]/gi,\n /(?:DOCKER_PASSWORD|NPM_TOKEN|AWS_SECRET_ACCESS_KEY|GH_TOKEN|GITHUB_TOKEN)\\s*[:=]\\s*[\"'`][^\"'`$]+[\"'`]/gi,\n ];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, secretsInCI, filePath, () =>\n \"Use repository secrets: ${{ secrets.MY_TOKEN }} (GitHub) or CI/CD variable settings. Never hardcode credentials in workflow files.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC077 – CORS Wildcard in Serverless Config\n// ────────────────────────────────────────────\n\nexport const corsServerless: CustomRule = {\n id: \"VC077\",\n title: \"CORS Wildcard in Serverless Config\",\n severity: \"high\",\n category: \"Configuration\",\n description: \"Setting Access-Control-Allow-Origin: * in serverless.yml, vercel.json, or similar configs allows any website to make authenticated requests to your API.\",\n check(content, filePath) {\n if (!filePath.match(/serverless\\.(yml|yaml)|vercel\\.json|netlify\\.toml|amplify\\.yml/i)) return [];\n const matches: RuleMatch[] = [];\n if (/(?:Access-Control-Allow-Origin|allowOrigin|cors).*['\"]\\*['\"]/i.test(content)) {\n matches.push(...findMatches(content, /(?:Access-Control-Allow-Origin|allowOrigin|cors).*['\"]\\*['\"]/gi, corsServerless, filePath, () =>\n \"Replace wildcard CORS with specific origins: allowOrigin: ['https://yourdomain.com']. Wildcard allows any site to call your API.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC078 – Kubernetes Privileged Container\n// ────────────────────────────────────────────\n\nexport const k8sPrivileged: CustomRule = {\n id: \"VC078\",\n title: \"Kubernetes Privileged Container\",\n severity: \"critical\",\n category: \"Configuration\",\n description: \"Running containers with privileged: true or as root in Kubernetes gives full host access, making container escapes trivial.\",\n check(content, filePath) {\n if (!filePath.match(/\\.(yaml|yml)$/) || !/(?:kind|apiVersion|container)/i.test(content)) return [];\n const matches: RuleMatch[] = [];\n const patterns = [\n /privileged\\s*:\\s*true/g,\n /runAsUser\\s*:\\s*0\\b/g,\n /runAsNonRoot\\s*:\\s*false/g,\n /allowPrivilegeEscalation\\s*:\\s*true/g,\n ];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, k8sPrivileged, filePath, () =>\n \"Set securityContext: { privileged: false, runAsNonRoot: true, allowPrivilegeEscalation: false }. Never run containers as root in production.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// FRAMEWORK DETECTION\n// ────────────────────────────────────────────\n\nexport type DetectedFramework = \"next.js\" | \"react\" | \"react-native\" | \"express\" | \"hono\" | \"fastify\" | \"django\" | \"flask\" | \"electron\" | \"vue\" | \"svelte\" | \"unknown\";\n\nexport function detectFramework(files: { path: string; content: string }[]): DetectedFramework[] {\n const frameworks: Set<DetectedFramework> = new Set();\n for (const { path, content } of files) {\n if (path.match(/next\\.config/i) || /from\\s+[\"']next/i.test(content)) frameworks.add(\"next.js\");\n if (/from\\s+[\"']react-native/i.test(content) || path.match(/react-native\\.config/i)) frameworks.add(\"react-native\");\n else if (/from\\s+[\"']react/i.test(content) || /import\\s+React/i.test(content)) frameworks.add(\"react\");\n if (/from\\s+[\"']express/i.test(content) || /require\\s*\\(\\s*[\"']express/i.test(content)) frameworks.add(\"express\");\n if (/from\\s+[\"']hono/i.test(content)) frameworks.add(\"hono\");\n if (/from\\s+[\"']fastify/i.test(content)) frameworks.add(\"fastify\");\n if (/from\\s+[\"']electron/i.test(content) || path.match(/electron/i)) frameworks.add(\"electron\");\n if (path.match(/settings\\.py$/) || /from\\s+django/i.test(content)) frameworks.add(\"django\");\n if (/from\\s+flask/i.test(content) || /Flask\\s*\\(/i.test(content)) frameworks.add(\"flask\");\n if (/from\\s+[\"']vue/i.test(content) || path.match(/vue\\.config/i)) frameworks.add(\"vue\");\n if (/from\\s+[\"']svelte/i.test(content) || path.match(/svelte\\.config/i)) frameworks.add(\"svelte\");\n }\n if (frameworks.size === 0) frameworks.add(\"unknown\");\n return [...frameworks];\n}\n\n// ────────────────────────────────────────────\n// SEVERITY GRADING\n// ────────────────────────────────────────────\n\nexport type SecurityGrade = \"A+\" | \"A\" | \"B\" | \"C\" | \"D\" | \"F\";\n\nexport interface GradeResult {\n grade: SecurityGrade;\n score: number;\n summary: string;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nexport function calculateGrade(findings: Finding[], _totalFiles: number): GradeResult {\n if (findings.length === 0) {\n return { grade: \"A+\", score: 100, summary: \"No security issues detected. Excellent.\" };\n }\n\n // Single source of truth for severity weights. Matches the dashboard\n // scoring modal and the GitHub Action action.yml so all three surfaces\n // produce the same grade for the same input.\n let critical = 0, high = 0, medium = 0, low = 0;\n for (const f of findings) {\n if (f.severity === \"critical\") critical++;\n else if (f.severity === \"high\") high++;\n else if (f.severity === \"medium\") medium++;\n else if (f.severity === \"low\") low++;\n }\n\n const deductions = critical * 15 + high * 7 + medium * 3 + low * 1;\n const rawScore = Math.max(0, 100 - deductions);\n\n // Score-based grade thresholds\n let grade: SecurityGrade;\n if (rawScore >= 97) grade = \"A+\";\n else if (rawScore >= 90) grade = \"A\";\n else if (rawScore >= 80) grade = \"B\";\n else if (rawScore >= 70) grade = \"C\";\n else if (rawScore >= 60) grade = \"D\";\n else grade = \"F\";\n\n // Severity caps — a scan with serious findings cannot earn an A regardless\n // of what the score math says. Prevents the 'strong security with minor\n // concerns' bug where 2 High findings were rated A.\n const capGrade = (cap: SecurityGrade): SecurityGrade => {\n const order: SecurityGrade[] = [\"A+\", \"A\", \"B\", \"C\", \"D\", \"F\"];\n return order.indexOf(grade) < order.indexOf(cap) ? cap : grade;\n };\n if (critical >= 1) grade = capGrade(\"D\"); // any critical → max D\n else if (high >= 3) grade = capGrade(\"D\"); // 3+ high → max D\n else if (high >= 1) grade = capGrade(\"B\"); // 1–2 high → max B\n else if (medium >= 5) grade = capGrade(\"B\"); // 5+ medium → max B\n else if (medium >= 1) grade = capGrade(\"A\"); // any medium → max A (not A+)\n\n // Summary references what's actually wrong, not just the score bucket.\n let summary: string;\n if (critical > 0) {\n summary = `${critical} critical ${critical === 1 ? \"vulnerability\" : \"vulnerabilities\"} require immediate attention.`;\n } else if (high >= 3) {\n summary = `${high} high-severity issues require urgent attention.`;\n } else if (high > 0) {\n summary = `${high} high-severity ${high === 1 ? \"issue needs\" : \"issues need\"} attention.`;\n } else if (medium >= 5) {\n summary = `${medium} medium-severity issues to address.`;\n } else if (medium > 0) {\n summary = `Clean of critical and high issues. ${medium} medium-severity ${medium === 1 ? \"issue\" : \"issues\"} to review.`;\n } else if (low > 0) {\n summary = `Clean of critical, high, and medium issues. ${low} low-severity best-practice ${low === 1 ? \"note\" : \"notes\"}.`;\n } else {\n summary = \"No security issues detected.\";\n }\n\n return { grade, score: rawScore, summary };\n}\n\n// ────────────────────────────────────────────\n// VC079 – JWT Algorithm Confusion\n// ────────────────────────────────────────────\n\nexport const jwtAlgConfusion: CustomRule = {\n id: \"VC079\",\n title: \"JWT Algorithm Confusion (alg:none)\",\n severity: \"critical\",\n category: \"Authentication\",\n description: \"Accepting 'none' as a JWT algorithm allows attackers to forge tokens by removing the signature entirely.\",\n check(content, filePath) {\n if (!/jwt|jsonwebtoken|jose/i.test(content)) return [];\n const matches: RuleMatch[] = [];\n const patterns = [\n /algorithms\\s*:\\s*\\[.*[\"']none[\"']/gi,\n /algorithm\\s*[:=]\\s*[\"']none[\"']/gi,\n ];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, jwtAlgConfusion, filePath, () =>\n \"Never allow algorithm 'none'. Explicitly specify: algorithms: ['RS256'] or algorithms: ['HS256']. Reject tokens with alg:none.\"\n ));\n }\n // Also check for missing algorithm restriction\n if (/jwt\\.verify\\s*\\([^)]*\\)\\s*(?!.*algorithms)/i.test(content) && !/algorithms/i.test(content)) {\n matches.push(...findMatches(content, /jwt\\.verify\\s*\\(/g, jwtAlgConfusion, filePath, () =>\n \"Specify allowed algorithms in jwt.verify: jwt.verify(token, secret, { algorithms: ['HS256'] }).\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC080 – Regex DoS (ReDoS)\n// ────────────────────────────────────────────\n\nexport const regexDos: CustomRule = {\n id: \"VC080\",\n title: \"Potential Regular Expression DoS (ReDoS)\",\n severity: \"high\",\n category: \"Availability\",\n description: \"Nested quantifiers like (a+)+ or (a*){2,} cause catastrophic backtracking, allowing attackers to freeze your server with crafted input.\",\n check(content, filePath) {\n if (filePath.includes(\"test\") || filePath.includes(\"mock\")) return [];\n const matches: RuleMatch[] = [];\n // Detect nested quantifiers in regex\n const patterns = [\n /new\\s+RegExp\\s*\\(\\s*[\"'`].*\\([^)]*[+*]\\)[+*{]/g,\n /\\/.*\\([^)]*[+*]\\)[+*{].*\\//g,\n ];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, regexDos, filePath, () =>\n \"Avoid nested quantifiers in regex. Use atomic groups, possessive quantifiers, or the 're2' library for safe regex execution.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC081 – XML External Entity (XXE)\n// ────────────────────────────────────────────\n\nexport const xxeVulnerability: CustomRule = {\n id: \"VC081\",\n title: \"XML External Entity (XXE) Injection\",\n severity: \"critical\",\n category: \"Injection\",\n description: \"XML parsers that process external entities allow attackers to read files, perform SSRF, or cause DoS via billion-laughs attacks.\",\n check(content, filePath) {\n if (!/xml|parseXML|DOMParser|SAXParser|etree|lxml/i.test(content)) return [];\n const matches: RuleMatch[] = [];\n const patterns = [\n /\\.parseXML\\s*\\(/g,\n /new\\s+DOMParser\\s*\\(\\)/g,\n /etree\\.parse\\s*\\(/g,\n /lxml\\.etree/g,\n /SAXParserFactory/g,\n /XMLReaderFactory/g,\n ];\n const hasProtection = /noent.*false|resolveExternals.*false|FEATURE_EXTERNAL.*false|defusedxml|disallow-doctype-decl/i.test(content);\n if (hasProtection) return [];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, xxeVulnerability, filePath, () =>\n \"Disable external entities: set noent: false, or use defusedxml (Python). For Java: factory.setFeature('http://apache.org/xml/features/disallow-doctype-decl', true).\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC082 – Server-Side Template Injection\n// ────────────────────────────────────────────\n\nexport const ssti: CustomRule = {\n id: \"VC082\",\n title: \"Server-Side Template Injection (SSTI)\",\n severity: \"critical\",\n category: \"Injection\",\n description: \"Rendering templates from user-controlled strings allows attackers to execute arbitrary code on the server.\",\n check(content, filePath) {\n if (filePath.includes(\"test\") || filePath.includes(\"mock\")) return [];\n const matches: RuleMatch[] = [];\n const patterns = [\n /render_template_string\\s*\\(\\s*(?![\"'`])/g,\n /Template\\s*\\(\\s*(?:req\\.|body\\.|input|params|args|user)/gi,\n /engine\\.render\\s*\\(\\s*(?:req\\.|body\\.|input)/gi,\n /nunjucks\\.renderString\\s*\\(\\s*(?:req\\.|body\\.|input)/gi,\n /ejs\\.render\\s*\\(\\s*(?:req\\.|body\\.|input)/gi,\n ];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, ssti, filePath, () =>\n \"Never render templates from user input. Use pre-defined templates and pass data as context variables: render_template('template.html', data=user_data).\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC083 – Insecure Java Deserialization\n// ────────────────────────────────────────────\n\nexport const javaDeserialization: CustomRule = {\n id: \"VC083\",\n title: \"Insecure Java Deserialization\",\n severity: \"critical\",\n category: \"Injection\",\n description: \"ObjectInputStream.readObject() on untrusted data allows arbitrary code execution via gadget chains in the classpath.\",\n check(content, filePath) {\n if (!filePath.match(/\\.java$|\\.kt$/)) return [];\n const matches: RuleMatch[] = [];\n const patterns = [\n /ObjectInputStream\\s*\\(/g,\n /\\.readObject\\s*\\(\\)/g,\n /XMLDecoder\\s*\\(/g,\n /XStream\\s*\\(\\)/g,\n ];\n const hasSafe = /ValidatingObjectInputStream|ObjectInputFilter|SerialKiller|NotSerializableException/i.test(content);\n if (hasSafe) return [];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, javaDeserialization, filePath, () =>\n \"Use ValidatingObjectInputStream with an allowlist of classes, or avoid Java serialization entirely. Use JSON instead.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC084 – Missing Subresource Integrity (SRI)\n// ────────────────────────────────────────────\n\nexport const missingSRI: CustomRule = {\n id: \"VC084\",\n title: \"Missing Subresource Integrity (SRI)\",\n severity: \"medium\",\n category: \"Configuration\",\n description: \"External scripts and stylesheets loaded without integrity= attributes can be tampered with if the CDN is compromised.\",\n check(content, filePath) {\n if (!filePath.match(/\\.(html|htm|jsx|tsx|ejs|hbs)$/)) return [];\n const matches: RuleMatch[] = [];\n // Script tags with external src but no integrity\n const scriptPattern = /<script\\s+[^>]*src\\s*=\\s*[\"']https?:\\/\\/[^\"']+[\"'][^>]*>/gi;\n let m: RegExpExecArray | null;\n const re = new RegExp(scriptPattern.source, scriptPattern.flags);\n while ((m = re.exec(content)) !== null) {\n if (!m[0].includes(\"integrity\")) {\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n matches.push({\n rule: \"VC084\", title: missingSRI.title, severity: \"medium\", category: \"Configuration\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: 'Add integrity and crossorigin attributes: <script src=\"...\" integrity=\"sha384-...\" crossorigin=\"anonymous\">'\n });\n }\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC085 – Exposed Admin/Debug Routes\n// ────────────────────────────────────────────\n\nexport const exposedAdminRoutes: CustomRule = {\n id: \"VC085\",\n title: \"Exposed Admin or Debug Route\",\n severity: \"high\",\n category: \"Information Leakage\",\n description: \"Routes like /admin, /debug, /phpinfo, or /actuator without authentication expose sensitive controls and information to attackers.\",\n check(content, filePath) {\n if (!/(?:\\/api\\/|routes?\\/|server\\.|app\\.|index\\.[jt]s)/i.test(filePath)) return [];\n const matches: RuleMatch[] = [];\n const patterns = [\n /[.'\"]\\s*(?:get|use|all)\\s*\\(\\s*[\"'`]\\/(?:admin|debug|_debug|__debug__|phpinfo|actuator|graphiql|playground|swagger|reset|seed|test|dev|mock)[\"'`]/gi,\n ];\n const hasAuth = /auth|requireAuth|isAdmin|requireAdmin|authenticate|middleware.*admin|requireUser/i.test(content);\n if (hasAuth) return [];\n // Skip if there's a dev-only guard\n if (/NODE_ENV\\s*!==\\s*[\"']development[\"']|NODE_ENV\\s*===\\s*[\"']production[\"']/i.test(content)) return [];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, exposedAdminRoutes, filePath, () =>\n \"Protect admin/debug routes with authentication middleware. In production, disable debug endpoints entirely.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC086 – Insecure WebSocket\n// ────────────────────────────────────────────\n\nexport const insecureWebSocket: CustomRule = {\n id: \"VC086\",\n title: \"Insecure WebSocket Connection (ws://)\",\n severity: \"medium\",\n category: \"Configuration\",\n description: \"Using ws:// instead of wss:// transmits data in plaintext, vulnerable to eavesdropping and man-in-the-middle attacks.\",\n check(content, filePath) {\n if (filePath.includes(\"test\") || filePath.includes(\"mock\")) return [];\n return findMatches(content, /new\\s+WebSocket\\s*\\(\\s*[\"'`]ws:\\/\\//g, insecureWebSocket, filePath, () =>\n \"Use wss:// (WebSocket Secure) instead of ws:// for encrypted connections: new WebSocket('wss://...').\"\n );\n },\n};\n\n// ────────────────────────────────────────────\n// VC087 – Missing HSTS\n// ────────────────────────────────────────────\n\nexport const missingHSTS: CustomRule = {\n id: \"VC087\",\n title: \"Missing HTTP Strict Transport Security (HSTS)\",\n severity: \"medium\",\n category: \"Configuration\",\n description: \"Without HSTS headers, browsers allow downgrade attacks from HTTPS to HTTP, exposing traffic to interception.\",\n check(content, filePath) {\n if (!/(?:server|app|index|main)\\.[jt]sx?$/.test(filePath)) return [];\n if (!/(?:express|hono|fastify|koa)/i.test(content)) return [];\n if (/Strict-Transport-Security|hsts|helmet/i.test(content)) return [];\n return findMatches(content, /(?:express|hono|fastify|koa)\\s*\\(/gi, missingHSTS, filePath, () =>\n \"Add HSTS header: res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'). Or use helmet().\"\n );\n },\n};\n\n// ────────────────────────────────────────────\n// VC088 – Sensitive Data in URL Parameters\n// ────────────────────────────────────────────\n\nexport const sensitiveURLParams: CustomRule = {\n id: \"VC088\",\n title: \"Sensitive Data in URL Parameters\",\n severity: \"high\",\n category: \"Information Leakage\",\n description: \"Passing passwords, tokens, or API keys in URL query parameters exposes them in server logs, browser history, and referrer headers.\",\n check(content, filePath) {\n // Only scan code files, never docs/README/examples/configs\n if (!filePath.match(/\\.(js|ts|jsx|tsx|py|rb|go|java|php)$/)) return [];\n if (isTestFile(filePath)) return [];\n const matches: RuleMatch[] = [];\n const patterns = [\n // Actual code building URLs with sensitive params (template literals or concat)\n /[&?](?:password|secret|api_key|apiKey|access_token|ssn|credit_card)\\s*=\\s*\\$\\{/gi,\n /[&?](?:password|secret|api_key|apiKey|access_token)\\s*=[\"']\\s*\\+/gi,\n ];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, sensitiveURLParams, filePath, () =>\n \"Never pass sensitive data in URL parameters. Use request headers (Authorization: Bearer ...) or POST body instead.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC089 – Missing Content-Disposition\n// ────────────────────────────────────────────\n\nexport const missingContentDisposition: CustomRule = {\n id: \"VC089\",\n title: \"File Download Missing Content-Disposition\",\n severity: \"medium\",\n category: \"Configuration\",\n description: \"File download endpoints without Content-Disposition headers may render files inline, leading to XSS if the file contains HTML/JS.\",\n check(content, filePath) {\n if (!/(?:download|sendFile|send_file|pipe|createReadStream)/i.test(content)) return [];\n if (!/(?:\\/api\\/|routes?\\/|controllers?\\/|server\\.|handler)/i.test(filePath)) return [];\n if (/Content-Disposition|attachment|download/i.test(content)) return [];\n return findMatches(content, /(?:sendFile|send_file|createReadStream|\\.pipe)\\s*\\(/gi, missingContentDisposition, filePath, () =>\n \"Set Content-Disposition: attachment header on file downloads: res.setHeader('Content-Disposition', 'attachment; filename=\\\"file.pdf\\\"').\"\n );\n },\n};\n\n// ────────────────────────────────────────────\n// VC090 – Open Redirect via Host Header\n// ────────────────────────────────────────────\n\nexport const hostHeaderRedirect: CustomRule = {\n id: \"VC090\",\n title: \"Open Redirect via Host Header\",\n severity: \"high\",\n category: \"Injection\",\n description: \"Using req.headers.host to construct redirect URLs allows attackers to inject a malicious host header, redirecting users to phishing sites.\",\n check(content, filePath) {\n if (filePath.includes(\"test\") || filePath.includes(\"mock\")) return [];\n const matches: RuleMatch[] = [];\n if (/req\\.headers\\.host|req\\.get\\s*\\(\\s*[\"']host[\"']\\)/i.test(content) && /redirect|location/i.test(content)) {\n matches.push(...findMatches(content, /req\\.headers\\.host|req\\.get\\s*\\(\\s*[\"']host[\"']\\)/gi, hostHeaderRedirect, filePath, () =>\n \"Don't use req.headers.host for redirects — it's attacker-controlled. Use a hardcoded domain or environment variable.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC091 – Race Condition / TOCTOU\n// ────────────────────────────────────────────\n\nexport const raceCondition: CustomRule = {\n id: \"VC091\",\n title: \"Potential Race Condition (TOCTOU)\",\n severity: \"high\",\n category: \"Authorization\",\n description: \"Check-then-act patterns (e.g., checking if a file exists then writing to it) are vulnerable to race conditions where state changes between the check and the action.\",\n check(content, filePath) {\n if (filePath.includes(\"test\") || filePath.includes(\"mock\")) return [];\n const matches: RuleMatch[] = [];\n const patterns = [\n /(?:existsSync|exists)\\s*\\([^)]+\\)[\\s\\S]{0,50}(?:writeFileSync|writeFile|unlinkSync|unlink|renameSync)\\s*\\(/g,\n /os\\.path\\.exists\\s*\\([^)]+\\)[\\s\\S]{0,50}open\\s*\\(/g,\n /File\\.exists\\?\\s*\\([^)]+\\)[\\s\\S]{0,50}File\\.(?:write|delete)/g,\n ];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, raceCondition, filePath, () =>\n \"Use atomic operations instead of check-then-act. For files: use fs.open with 'wx' flag (exclusive create), or use file locks.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC092 – Unsafe Object.assign from User Input\n// ────────────────────────────────────────────\n\nexport const unsafeObjectAssign: CustomRule = {\n id: \"VC092\",\n title: \"Unsafe Object Spread from User Input\",\n severity: \"medium\",\n category: \"Injection\",\n description: \"Spreading request body into a new object can copy __proto__, constructor, or other dangerous properties, enabling prototype pollution.\",\n check(content, filePath) {\n if (filePath.includes(\"test\") || filePath.includes(\"mock\")) return [];\n const matches: RuleMatch[] = [];\n const patterns = [\n /Object\\.assign\\s*\\(\\s*\\{\\s*\\}\\s*,\\s*(?:req\\.(?:body|query|params)|body|input)\\s*\\)/gi,\n /\\{\\s*\\.\\.\\.(?:req\\.(?:body|query|params)|body|input)\\s*\\}/gi,\n ];\n const hasSafe = /omit.*__proto__|sanitize|pick\\(|lodash\\.pick|stripProto/i.test(content);\n if (hasSafe) return [];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, unsafeObjectAssign, filePath, () =>\n \"Explicitly pick allowed properties instead of spreading: const { name, email } = req.body; const safe = { name, email };\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC093 – Unprotected File Download Endpoint\n// ────────────────────────────────────────────\n\nexport const unprotectedDownload: CustomRule = {\n id: \"VC093\",\n title: \"File Download Without Path Validation\",\n severity: \"medium\",\n category: \"Authorization\",\n description: \"File download endpoints that accept user-controlled filenames without path validation allow directory traversal to read arbitrary files.\",\n check(content, filePath) {\n if (!/(?:download|sendFile|send_file)/i.test(content)) return [];\n if (!/(?:\\/api\\/|routes?\\/|controllers?\\/|server\\.|handler)/i.test(filePath)) return [];\n const matches: RuleMatch[] = [];\n // sendFile/download with user input\n if (/(?:sendFile|download|send_file)\\s*\\([^)]*(?:req\\.|params\\.|query\\.|body\\.)/i.test(content)) {\n const hasValidation = /path\\.resolve|path\\.normalize|path\\.join.*__dirname|realpath|includes\\s*\\(\\s*[\"']\\.\\./i.test(content);\n if (!hasValidation) {\n matches.push(...findMatches(content, /(?:sendFile|download|send_file)\\s*\\(/gi, unprotectedDownload, filePath, () =>\n \"Validate file paths: const safePath = path.resolve(DOWNLOAD_DIR, filename); if (!safePath.startsWith(DOWNLOAD_DIR)) throw new Error('Invalid path');\"\n ));\n }\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC094 – Command Injection\n// ────────────────────────────────────────────\n\nexport const commandInjection: CustomRule = {\n id: \"VC094\",\n title: \"Potential Command Injection\",\n severity: \"critical\",\n category: \"Injection\",\n description: \"Passing user input to shell commands (exec, system, child_process) allows attackers to execute arbitrary system commands.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!filePath.match(/\\.(js|ts|jsx|tsx|py|rb)$/)) return [];\n const matches: RuleMatch[] = [];\n const patterns = [\n // Node.js — require standalone exec/execSync, not db.exec() or conn.exec()\n /(?<![.\\w])(?:exec|execSync)\\s*\\(\\s*(?:`[^`]*\\$\\{|[\"'][^\"']*\\+\\s*(?:req\\.|body\\.|input|params|args|user))/gi,\n /child_process.*exec\\s*\\(\\s*(?![\"'`])/g,\n // Python\n /os\\.system\\s*\\(\\s*(?![\"'`].*[\"'`]\\s*\\))/g,\n /subprocess\\.(?:call|run|Popen)\\s*\\([^)]*shell\\s*=\\s*True/gi,\n // Ruby\n /system\\s*\\(\\s*[\"'].*#\\{/g,\n ];\n const hasSafe = /execFile|spawn|escapeshellarg|shlex\\.quote|shellEscape/i.test(content);\n if (hasSafe) return [];\n for (const p of patterns) {\n const raw = findMatches(content, p, commandInjection, filePath, () =>\n \"Use execFile/spawn instead of exec (avoids shell). Never concatenate user input into shell commands. Use parameterized arguments.\"\n );\n // Filter out database .exec() calls (better-sqlite3, knex, sequelize, etc.)\n for (const m of raw) {\n const lineText = content.split(\"\\n\")[m.line - 1] || \"\";\n if (/(?:conn|db|database|knex|sequelize|prisma|sqlite|pool|client)\\s*\\.exec/i.test(lineText)) continue;\n if (/ALTER\\s+TABLE|CREATE\\s+TABLE|DROP\\s+TABLE|INSERT\\s+INTO/i.test(lineText)) continue;\n matches.push(m);\n }\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC095 – Hardcoded CORS Origin Localhost\n// ────────────────────────────────────────────\n\nexport const corsLocalhost: CustomRule = {\n id: \"VC095\",\n title: \"Hardcoded Localhost CORS Origin\",\n severity: \"medium\",\n category: \"Configuration\",\n description: \"Hardcoded localhost CORS origins in production code allow any local process to make authenticated requests to your API.\",\n check(content, filePath) {\n if (filePath.includes(\"test\") || filePath.includes(\"mock\") || filePath.includes(\".env\")) return [];\n if (!/origin/i.test(content)) return [];\n return findMatches(content, /origin\\s*[:=]\\s*[\"'`]http:\\/\\/localhost/gi, corsLocalhost, filePath, () =>\n \"Use environment variables for CORS origins: origin: process.env.ALLOWED_ORIGIN. Remove localhost from production configs.\"\n );\n },\n};\n\n// ────────────────────────────────────────────\n// VC096 – Unencrypted gRPC\n// ────────────────────────────────────────────\n\nexport const insecureGRPC: CustomRule = {\n id: \"VC096\",\n title: \"Unencrypted gRPC Channel\",\n severity: \"medium\",\n category: \"Configuration\",\n description: \"Using insecure gRPC channels transmits data including credentials in plaintext.\",\n check(content, filePath) {\n if (!/grpc/i.test(content)) return [];\n return findMatches(content, /(?:insecure_channel|createInsecure|grpc\\.Insecure)/gi, insecureGRPC, filePath, () =>\n \"Use encrypted gRPC channels: grpc.ssl_channel_credentials() or grpc.credentials.createSsl().\"\n );\n },\n};\n\n// ────────────────────────────────────────────\n// COMPLIANCE MAPPING (OWASP Top 10 + CWE)\n// ────────────────────────────────────────────\n\nexport const complianceMap: Record<string, { owasp: string; cwe: string }> = {\n VC001: { owasp: \"A07:2021\", cwe: \"CWE-798\" },\n VC002: { owasp: \"A05:2021\", cwe: \"CWE-200\" },\n VC003: { owasp: \"A01:2021\", cwe: \"CWE-862\" },\n VC004: { owasp: \"A01:2021\", cwe: \"CWE-284\" },\n VC005: { owasp: \"A08:2021\", cwe: \"CWE-345\" },\n VC006: { owasp: \"A03:2021\", cwe: \"CWE-89\" },\n VC007: { owasp: \"A03:2021\", cwe: \"CWE-79\" },\n VC008: { owasp: \"A04:2021\", cwe: \"CWE-770\" },\n VC009: { owasp: \"A05:2021\", cwe: \"CWE-942\" },\n VC010: { owasp: \"A01:2021\", cwe: \"CWE-602\" },\n VC011: { owasp: \"A07:2021\", cwe: \"CWE-798\" },\n VC012: { owasp: \"A05:2021\", cwe: \"CWE-200\" },\n VC013: { owasp: \"A01:2021\", cwe: \"CWE-269\" },\n VC014: { owasp: \"A05:2021\", cwe: \"CWE-538\" },\n VC015: { owasp: \"A03:2021\", cwe: \"CWE-95\" },\n VC016: { owasp: \"A01:2021\", cwe: \"CWE-601\" },\n VC017: { owasp: \"A05:2021\", cwe: \"CWE-614\" },\n VC018: { owasp: \"A07:2021\", cwe: \"CWE-798\" },\n VC019: { owasp: \"A05:2021\", cwe: \"CWE-693\" },\n VC020: { owasp: \"A05:2021\", cwe: \"CWE-1021\" },\n VC021: { owasp: \"A01:2021\", cwe: \"CWE-22\" },\n VC022: { owasp: \"A03:2021\", cwe: \"CWE-79\" },\n VC023: { owasp: \"A08:2021\", cwe: \"CWE-1321\" },\n VC024: { owasp: \"A04:2021\", cwe: \"CWE-770\" },\n VC025: { owasp: \"A03:2021\", cwe: \"CWE-22\" },\n VC026: { owasp: \"A05:2021\", cwe: \"CWE-693\" },\n VC027: { owasp: \"A05:2021\", cwe: \"CWE-693\" },\n VC028: { owasp: \"A07:2021\", cwe: \"CWE-20\" },\n VC029: { owasp: \"A08:2021\", cwe: \"CWE-20\" },\n VC030: { owasp: \"A08:2021\", cwe: \"CWE-502\" },\n VC031: { owasp: \"A02:2021\", cwe: \"CWE-321\" },\n VC032: { owasp: \"A05:2021\", cwe: \"CWE-319\" },\n VC033: { owasp: \"A05:2021\", cwe: \"CWE-215\" },\n VC034: { owasp: \"A02:2021\", cwe: \"CWE-338\" },\n VC035: { owasp: \"A01:2021\", cwe: \"CWE-601\" },\n VC036: { owasp: \"A04:2021\", cwe: \"CWE-755\" },\n VC037: { owasp: \"A09:2021\", cwe: \"CWE-209\" },\n VC038: { owasp: \"A04:2021\", cwe: \"CWE-434\" },\n VC039: { owasp: \"A06:2021\", cwe: \"CWE-1104\" },\n VC040: { owasp: \"A05:2021\", cwe: \"CWE-538\" },\n VC041: { owasp: \"A10:2021\", cwe: \"CWE-918\" },\n VC042: { owasp: \"A01:2021\", cwe: \"CWE-915\" },\n VC043: { owasp: \"A02:2021\", cwe: \"CWE-208\" },\n VC044: { owasp: \"A09:2021\", cwe: \"CWE-117\" },\n VC045: { owasp: \"A07:2021\", cwe: \"CWE-521\" },\n VC046: { owasp: \"A07:2021\", cwe: \"CWE-384\" },\n VC047: { owasp: \"A07:2021\", cwe: \"CWE-307\" },\n VC048: { owasp: \"A03:2021\", cwe: \"CWE-943\" },\n VC049: { owasp: \"A07:2021\", cwe: \"CWE-798\" },\n VC050: { owasp: \"A02:2021\", cwe: \"CWE-319\" },\n VC051: { owasp: \"A05:2021\", cwe: \"CWE-200\" },\n VC052: { owasp: \"A04:2021\", cwe: \"CWE-770\" },\n VC053: { owasp: \"A05:2021\", cwe: \"CWE-798\" },\n VC054: { owasp: \"A07:2021\", cwe: \"CWE-922\" },\n VC055: { owasp: \"A05:2021\", cwe: \"CWE-540\" },\n VC056: { owasp: \"A05:2021\", cwe: \"CWE-1021\" },\n VC057: { owasp: \"A01:2021\", cwe: \"CWE-269\" },\n VC058: { owasp: \"A05:2021\", cwe: \"CWE-250\" },\n VC059: { owasp: \"A05:2021\", cwe: \"CWE-284\" },\n VC060: { owasp: \"A02:2021\", cwe: \"CWE-328\" },\n VC061: { owasp: \"A02:2021\", cwe: \"CWE-295\" },\n VC062: { owasp: \"A02:2021\", cwe: \"CWE-321\" },\n VC063: { owasp: \"A03:2021\", cwe: \"CWE-79\" },\n VC064: { owasp: \"A01:2021\", cwe: \"CWE-862\" },\n VC065: { owasp: \"A01:2021\", cwe: \"CWE-862\" },\n VC066: { owasp: \"A07:2021\", cwe: \"CWE-798\" },\n VC067: { owasp: \"A01:2021\", cwe: \"CWE-601\" },\n VC068: { owasp: \"A07:2021\", cwe: \"CWE-922\" },\n VC069: { owasp: \"A02:2021\", cwe: \"CWE-295\" },\n VC070: { owasp: \"A05:2021\", cwe: \"CWE-489\" },\n VC071: { owasp: \"A05:2021\", cwe: \"CWE-215\" },\n VC072: { owasp: \"A07:2021\", cwe: \"CWE-798\" },\n VC073: { owasp: \"A08:2021\", cwe: \"CWE-502\" },\n VC074: { owasp: \"A01:2021\", cwe: \"CWE-352\" },\n VC075: { owasp: \"A03:2021\", cwe: \"CWE-78\" },\n VC076: { owasp: \"A07:2021\", cwe: \"CWE-798\" },\n VC077: { owasp: \"A05:2021\", cwe: \"CWE-942\" },\n VC078: { owasp: \"A05:2021\", cwe: \"CWE-250\" },\n VC079: { owasp: \"A02:2021\", cwe: \"CWE-327\" },\n VC080: { owasp: \"A04:2021\", cwe: \"CWE-1333\" },\n VC081: { owasp: \"A03:2021\", cwe: \"CWE-611\" },\n VC082: { owasp: \"A03:2021\", cwe: \"CWE-94\" },\n VC083: { owasp: \"A08:2021\", cwe: \"CWE-502\" },\n VC084: { owasp: \"A06:2021\", cwe: \"CWE-353\" },\n VC085: { owasp: \"A01:2021\", cwe: \"CWE-862\" },\n VC086: { owasp: \"A02:2021\", cwe: \"CWE-319\" },\n VC087: { owasp: \"A05:2021\", cwe: \"CWE-311\" },\n VC088: { owasp: \"A07:2021\", cwe: \"CWE-598\" },\n VC089: { owasp: \"A05:2021\", cwe: \"CWE-430\" },\n VC090: { owasp: \"A01:2021\", cwe: \"CWE-601\" },\n VC091: { owasp: \"A04:2021\", cwe: \"CWE-367\" },\n VC092: { owasp: \"A08:2021\", cwe: \"CWE-1321\" },\n VC093: { owasp: \"A01:2021\", cwe: \"CWE-22\" },\n VC094: { owasp: \"A03:2021\", cwe: \"CWE-78\" },\n VC095: { owasp: \"A05:2021\", cwe: \"CWE-942\" },\n VC096: { owasp: \"A02:2021\", cwe: \"CWE-319\" },\n VC097: { owasp: \"A09:2021\", cwe: \"CWE-532\" },\n VC098: { owasp: \"A04:2021\", cwe: \"CWE-400\" },\n VC099: { owasp: \"A04:2021\", cwe: \"CWE-401\" },\n VC100: { owasp: \"A04:2021\", cwe: \"CWE-400\" },\n VC101: { owasp: \"A04:2021\", cwe: \"CWE-400\" },\n VC102: { owasp: \"A04:2021\", cwe: \"CWE-400\" },\n VC103: { owasp: \"A04:2021\", cwe: \"CWE-710\" },\n VC104: { owasp: \"A04:2021\", cwe: \"CWE-390\" },\n VC105: { owasp: \"A04:2021\", cwe: \"CWE-710\" },\n VC106: { owasp: \"A04:2021\", cwe: \"CWE-710\" },\n VC107: { owasp: \"A02:2021\", cwe: \"CWE-311\" },\n VC108: { owasp: \"A01:2021\", cwe: \"CWE-284\" },\n VC109: { owasp: \"A01:2021\", cwe: \"CWE-284\" },\n VC110: { owasp: \"A09:2021\", cwe: \"CWE-778\" },\n VC111: { owasp: \"A05:2021\", cwe: \"CWE-16\" },\n VC112: { owasp: \"A06:2021\", cwe: \"CWE-1104\" },\n VC113: { owasp: \"A05:2021\", cwe: \"CWE-200\" },\n VC114: { owasp: \"A05:2021\", cwe: \"CWE-16\" },\n VC115: { owasp: \"A02:2021\", cwe: \"CWE-311\" },\n VC116: { owasp: \"A05:2021\", cwe: \"CWE-770\" },\n VC117: { owasp: \"A03:2021\", cwe: \"CWE-22\" },\n VC118: { owasp: \"A09:2021\", cwe: \"CWE-532\" },\n VC119: { owasp: \"A07:2021\", cwe: \"CWE-798\" },\n VC120: { owasp: \"A07:2021\", cwe: \"CWE-352\" },\n VC121: { owasp: \"A06:2021\", cwe: \"CWE-1104\" },\n VC122: { owasp: \"A02:2021\", cwe: \"CWE-326\" },\n VC123: { owasp: \"A02:2021\", cwe: \"CWE-326\" },\n VC124: { owasp: \"A02:2021\", cwe: \"CWE-327\" },\n VC125: { owasp: \"A07:2021\", cwe: \"CWE-640\" },\n VC126: { owasp: \"A05:2021\", cwe: \"CWE-200\" },\n VC127: { owasp: \"A01:2021\", cwe: \"CWE-862\" },\n VC128: { owasp: \"A05:2021\", cwe: \"CWE-444\" },\n VC129: { owasp: \"A02:2021\", cwe: \"CWE-311\" },\n VC130: { owasp: \"A07:2021\", cwe: \"CWE-307\" },\n VC131: { owasp: \"A06:2021\", cwe: \"CWE-1104\" },\n // VC132–VC145: Service-specific API key detection\n VC132: { owasp: \"A07:2021\", cwe: \"CWE-798\" },\n VC133: { owasp: \"A07:2021\", cwe: \"CWE-798\" },\n VC134: { owasp: \"A07:2021\", cwe: \"CWE-798\" },\n VC135: { owasp: \"A07:2021\", cwe: \"CWE-798\" },\n VC136: { owasp: \"A07:2021\", cwe: \"CWE-798\" },\n VC137: { owasp: \"A07:2021\", cwe: \"CWE-798\" },\n VC138: { owasp: \"A07:2021\", cwe: \"CWE-798\" },\n VC139: { owasp: \"A07:2021\", cwe: \"CWE-798\" },\n VC140: { owasp: \"A07:2021\", cwe: \"CWE-798\" },\n VC141: { owasp: \"A07:2021\", cwe: \"CWE-798\" },\n VC142: { owasp: \"A07:2021\", cwe: \"CWE-798\" },\n VC143: { owasp: \"A07:2021\", cwe: \"CWE-798\" },\n VC144: { owasp: \"A07:2021\", cwe: \"CWE-798\" },\n VC145: { owasp: \"A07:2021\", cwe: \"CWE-798\" },\n // VC146–VC151: Secret exposure path analysis\n VC146: { owasp: \"A07:2021\", cwe: \"CWE-598\" },\n VC147: { owasp: \"A09:2021\", cwe: \"CWE-532\" },\n VC148: { owasp: \"A09:2021\", cwe: \"CWE-209\" },\n VC149: { owasp: \"A07:2021\", cwe: \"CWE-798\" },\n VC150: { owasp: \"A07:2021\", cwe: \"CWE-615\" },\n VC151: { owasp: \"A07:2021\", cwe: \"CWE-214\" },\n // VC152–VC158: New rules\n VC152: { owasp: \"A08:2021\", cwe: \"CWE-345\" },\n VC153: { owasp: \"A05:2021\", cwe: \"CWE-942\" },\n VC154: { owasp: \"A03:2021\", cwe: \"CWE-20\" },\n VC155: { owasp: \"A04:2021\", cwe: \"CWE-770\" },\n VC156: { owasp: \"A04:2021\", cwe: \"CWE-770\" },\n VC157: { owasp: \"A05:2021\", cwe: \"CWE-16\" },\n VC158: { owasp: \"A01:2021\", cwe: \"CWE-639\" },\n};\n\n// ────────────────────────────────────────────\n// VC097 – Console.log in Production\n// ────────────────────────────────────────────\n\nexport const consoleLogProduction: CustomRule = {\n id: \"VC097\",\n title: \"Console.log Left in Production Code\",\n severity: \"low\",\n category: \"Performance\",\n description: \"console.log statements left in production code can leak sensitive data, slow down rendering, and clutter browser consoles.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n // Skip server-side scripts, migration files, CLI tools, and seed files\n if (/(?:migrate|seed|script|cli|setup|dev)\\./i.test(filePath)) return [];\n if (!/console\\.log\\s*\\(/g.test(content)) return [];\n const lines = content.split(\"\\n\");\n const logCount = lines.filter(l => /console\\.log\\s*\\(/.test(l.trim()) && !l.trim().startsWith(\"//\")).length;\n // If a file has many console.logs, it's intentional logging, not forgotten debug statements\n if (logCount > 5) return [];\n const matches: RuleMatch[] = [];\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i].trim();\n if (/console\\.log\\s*\\(/.test(line) && !line.startsWith(\"//\") && !line.startsWith(\"*\") && !/if\\s*\\(\\s*(?:debug|process\\.env)/i.test(lines[Math.max(0, i-1)] + line)) {\n matches.push({ rule: \"VC097\", title: consoleLogProduction.title, severity: \"low\" as const, category: \"Performance\", file: filePath, line: i + 1, snippet: getSnippet(content, i + 1), fix: \"Remove console.log or use a structured logger that can be disabled in production.\" });\n }\n }\n return matches.slice(0, 1); // Max 1 per file to avoid noise\n },\n};\n\n// ────────────────────────────────────────────\n// VC098 – Synchronous File Operations\n// ────────────────────────────────────────────\n\nexport const syncFileOps: CustomRule = {\n id: \"VC098\",\n title: \"Synchronous File Operations\",\n severity: \"medium\",\n category: \"Performance\",\n description: \"Synchronous file operations (readFileSync, writeFileSync) block the event loop, causing all other requests to wait.\",\n check(content, filePath) {\n if (filePath.match(/test|spec|mock|__tests__|fixture|config|\\.config\\./i)) return [];\n return findMatches(content, /(?:readFileSync|writeFileSync|appendFileSync|mkdirSync|rmdirSync|statSync)\\s*\\(/g, syncFileOps, filePath, () =>\n \"Use async file operations (readFile, writeFile) to avoid blocking the event loop.\"\n );\n },\n};\n\n// ────────────────────────────────────────────\n// VC099 – Memory Leak: Event Listener\n// ────────────────────────────────────────────\n\nexport const eventListenerLeak: CustomRule = {\n id: \"VC099\",\n title: \"Memory Leak: Event Listener Not Cleaned Up\",\n severity: \"high\",\n category: \"Performance\",\n description: \"Adding event listeners in React useEffect without a cleanup function causes memory leaks as listeners accumulate on re-renders.\",\n check(content, filePath) {\n if (!filePath.match(/\\.(jsx|tsx)$/)) return [];\n if (!/addEventListener/i.test(content)) return [];\n if (/removeEventListener/i.test(content)) return [];\n if (!/useEffect/i.test(content)) return [];\n return findMatches(content, /addEventListener\\s*\\(/g, eventListenerLeak, filePath, () =>\n \"Return a cleanup function from useEffect: useEffect(() => { window.addEventListener('resize', fn); return () => window.removeEventListener('resize', fn); }, []);\"\n );\n },\n};\n\n// ────────────────────────────────────────────\n// VC100 – N+1 Query Pattern\n// ────────────────────────────────────────────\n\nexport const nPlusOneQuery: CustomRule = {\n id: \"VC100\",\n title: \"N+1 Query Pattern Detected\",\n severity: \"medium\",\n category: \"Performance\",\n description: \"Database queries inside loops cause N+1 performance problems — one query per iteration instead of a single batch query.\",\n check(content, filePath) {\n if (filePath.match(/test|spec|mock/i)) return [];\n const hasLoopWithQuery = /(?:for\\s*\\(|\\.forEach\\s*\\(|\\.map\\s*\\(|while\\s*\\()[^}]*(?:\\.find\\(|\\.findOne\\(|\\.findById\\(|\\.query\\(|\\.execute\\(|SELECT\\s)/is.test(content);\n if (!hasLoopWithQuery) return [];\n return findMatches(content, /(?:for\\s*\\(|\\.forEach\\s*\\(|\\.map\\s*\\(|while\\s*\\()/g, nPlusOneQuery, filePath, () =>\n \"Fetch all data in a single query using WHERE IN, JOIN, or batch operations instead of querying per item in a loop.\"\n ).slice(0, 2);\n },\n};\n\n// ────────────────────────────────────────────\n// VC101 – Large Bundle Import\n// ────────────────────────────────────────────\n\nexport const largeBundleImport: CustomRule = {\n id: \"VC101\",\n title: \"Importing Entire Library (Large Bundle)\",\n severity: \"medium\",\n category: \"Performance\",\n description: \"Importing entire libraries like lodash or moment.js adds hundreds of KB to your bundle. Import only the functions you need.\",\n check(content, filePath) {\n if (!filePath.match(/\\.(jsx?|tsx?)$/)) return [];\n const matches: RuleMatch[] = [];\n const patterns = [\n /import\\s+_\\s+from\\s+['\"]lodash['\"]/g,\n /import\\s+\\*\\s+as\\s+_\\s+from\\s+['\"]lodash['\"]/g,\n /import\\s+moment\\s+from\\s+['\"]moment['\"]/g,\n /const\\s+_\\s*=\\s*require\\s*\\(\\s*['\"]lodash['\"]\\s*\\)/g,\n /const\\s+moment\\s*=\\s*require\\s*\\(\\s*['\"]moment['\"]\\s*\\)/g,\n ];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, largeBundleImport, filePath, () =>\n \"Import only what you need: import { debounce } from 'lodash/debounce'. Or switch to lighter alternatives like date-fns instead of moment.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC102 – Blocking Main Thread\n// ────────────────────────────────────────────\n\nexport const blockingMainThread: CustomRule = {\n id: \"VC102\",\n title: \"Blocking Main Thread with Heavy Computation\",\n severity: \"medium\",\n category: \"Performance\",\n description: \"Infinite loops or deeply nested iterations on the main thread freeze the UI and cause unresponsiveness.\",\n check(content, filePath) {\n if (filePath.match(/worker|test|spec|mock/i)) return [];\n const matches: RuleMatch[] = [];\n if (/while\\s*\\(\\s*true\\s*\\)/g.test(content)) {\n matches.push(...findMatches(content, /while\\s*\\(\\s*true\\s*\\)/g, blockingMainThread, filePath, () =>\n \"Avoid while(true) on the main thread. Use Web Workers for heavy computation or requestIdleCallback for non-urgent work.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC103 – TODO/FIXME Left in Code\n// ────────────────────────────────────────────\n\nexport const todoLeftInCode: CustomRule = {\n id: \"VC103\",\n title: \"TODO/FIXME Left in Code\",\n severity: \"low\",\n category: \"Code Quality\",\n description: \"TODO, FIXME, HACK, and XXX comments indicate unfinished work that should be resolved before shipping to production.\",\n check(content, filePath) {\n if (filePath.match(/test|spec|mock|__tests__|fixture|node_modules/i)) return [];\n return findMatches(content, /\\/\\/\\s*(?:TODO|FIXME|HACK|XXX)\\b/gi, todoLeftInCode, filePath, () =>\n \"Resolve TODO/FIXME comments before shipping. If it's intentional tech debt, track it in your issue tracker instead.\"\n ).slice(0, 5);\n },\n};\n\n// ────────────────────────────────────────────\n// VC104 – Empty Catch Block\n// ────────────────────────────────────────────\n\nexport const emptyCatchBlock: CustomRule = {\n id: \"VC104\",\n title: \"Empty Catch Block\",\n severity: \"medium\",\n category: \"Code Quality\",\n description: \"Empty catch blocks silently swallow errors, making bugs impossible to diagnose. At minimum, log the error.\",\n check(content, filePath) {\n if (filePath.match(/test|spec|mock/i)) return [];\n return findMatches(content, /catch\\s*(?:\\([^)]*\\))?\\s*\\{\\s*\\}/g, emptyCatchBlock, filePath, () =>\n \"Handle errors in catch blocks: catch(err) { console.error('Operation failed:', err); } or re-throw if appropriate.\"\n );\n },\n};\n\n// ────────────────────────────────────────────\n// VC105 – Callback Hell\n// ────────────────────────────────────────────\n\nexport const callbackHell: CustomRule = {\n id: \"VC105\",\n title: \"Deeply Nested Callbacks (Promise Chain)\",\n severity: \"medium\",\n category: \"Code Quality\",\n description: \"Long .then() chains or deeply nested callbacks are hard to read, debug, and maintain. Refactor to async/await.\",\n check(content, filePath) {\n if (filePath.match(/test|spec|mock/i)) return [];\n return findMatches(content, /\\.then\\s*\\([^)]*\\)\\s*\\.then\\s*\\([^)]*\\)\\s*\\.then/g, callbackHell, filePath, () =>\n \"Refactor .then() chains to async/await for cleaner, more readable code.\"\n );\n },\n};\n\n// ────────────────────────────────────────────\n// VC106 – Magic Numbers\n// ────────────────────────────────────────────\n\nexport const magicNumbers: CustomRule = {\n id: \"VC106\",\n title: \"Magic Numbers in Code\",\n severity: \"low\",\n category: \"Code Quality\",\n description: \"Unnamed numeric constants in conditions or calculations make code hard to understand. Extract them into named constants.\",\n check(content, filePath) {\n if (filePath.match(/test|spec|mock|config|\\.config\\.|constant|enum|migration/i)) return [];\n if (!filePath.match(/\\.(jsx?|tsx?)$/)) return [];\n const matches: RuleMatch[] = [];\n const lines = content.split(\"\\n\");\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i].trim();\n if (line.startsWith(\"//\") || line.startsWith(\"*\")) continue;\n // Look for conditions with magic numbers > 1 (skip 0, 1, -1, 2)\n if (/(?:===|!==|>=?|<=?)\\s*\\d{3,}/.test(line) || /(?:setTimeout|setInterval)\\s*\\([^,]+,\\s*\\d{4,}/.test(line)) {\n matches.push({ rule: \"VC106\", title: magicNumbers.title, severity: \"low\" as const, category: \"Code Quality\", file: filePath, line: i + 1, snippet: getSnippet(content, i + 1), fix: \"Extract magic numbers into named constants: const MAX_RETRIES = 3; const TIMEOUT_MS = 5000;\" });\n }\n }\n return matches.slice(0, 3);\n },\n};\n\n// ────────────────────────────────────────────\n// VC107 – S3 Bucket Without Encryption\n// ────────────────────────────────────────────\n\nexport const s3BucketNoEncryption: CustomRule = {\n id: \"VC107\",\n title: \"S3 Bucket Without Encryption\",\n severity: \"high\",\n category: \"Infrastructure\",\n description: \"AWS S3 buckets without server-side encryption leave data at rest unprotected. Enable encryption to protect sensitive data.\",\n check(content, filePath) {\n if (!filePath.match(/\\.tf$/)) return [];\n if (!/resource\\s+\"aws_s3_bucket\"/.test(content)) return [];\n const matches: RuleMatch[] = [];\n // Find S3 bucket resources without encryption configuration nearby\n const bucketPattern = /resource\\s+\"aws_s3_bucket\"\\s+\"(\\w+)\"/g;\n let m: RegExpExecArray | null;\n while ((m = bucketPattern.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n // Check if there's a server_side_encryption_configuration within a reasonable range\n const blockEnd = Math.min(m.index + 2000, content.length);\n const blockContent = content.substring(m.index, blockEnd);\n if (!/server_side_encryption/.test(blockContent)) {\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n matches.push({\n rule: \"VC107\", title: s3BucketNoEncryption.title, severity: \"high\" as const, category: \"Infrastructure\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Enable S3 bucket encryption: server_side_encryption_configuration { rule { apply_server_side_encryption_by_default { sse_algorithm = 'aws:kms' } } }\",\n });\n }\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC108 – Security Group Allows All Inbound\n// ────────────────────────────────────────────\n\nexport const securityGroupAllInbound: CustomRule = {\n id: \"VC108\",\n title: \"Security Group Allows All Inbound\",\n severity: \"critical\",\n category: \"Infrastructure\",\n description: \"Security groups allowing all inbound traffic (0.0.0.0/0 on all ports) expose resources to the entire internet.\",\n check(content, filePath) {\n if (!filePath.match(/\\.(tf|json|yaml|yml)$/)) return [];\n const matches: RuleMatch[] = [];\n // Terraform: ingress with 0.0.0.0/0 and port 0 or no port restriction\n if (filePath.match(/\\.tf$/)) {\n const ingressPattern = /ingress\\s*\\{[^}]*cidr_blocks\\s*=\\s*\\[\\s*\"0\\.0\\.0\\.0\\/0\"\\s*\\][^}]*\\}/gs;\n let m: RegExpExecArray | null;\n while ((m = ingressPattern.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n // Check for unrestricted ports (from_port = 0 or no port spec)\n if (/from_port\\s*=\\s*0/.test(m[0]) || !/from_port/.test(m[0])) {\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n matches.push({\n rule: \"VC108\", title: securityGroupAllInbound.title, severity: \"critical\" as const, category: \"Infrastructure\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Restrict security group ingress to specific IP ranges and ports.\",\n });\n }\n }\n }\n // CloudFormation: AWS::EC2::SecurityGroup with CidrIp: 0.0.0.0/0\n if (filePath.match(/\\.(json|yaml|yml)$/)) {\n const cfnPattern = /AWS::EC2::SecurityGroup/g;\n if (cfnPattern.test(content) && /CidrIp\\s*:\\s*[\"']?0\\.0\\.0\\.0\\/0/.test(content)) {\n matches.push(...findMatches(content, /CidrIp\\s*:\\s*[\"']?0\\.0\\.0\\.0\\/0/g, securityGroupAllInbound, filePath, () =>\n \"Restrict security group ingress to specific IP ranges and ports.\"\n ));\n }\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC109 – RDS Instance Publicly Accessible\n// ────────────────────────────────────────────\n\nexport const rdsPubliclyAccessible: CustomRule = {\n id: \"VC109\",\n title: \"RDS Instance Publicly Accessible\",\n severity: \"critical\",\n category: \"Infrastructure\",\n description: \"RDS instances with publicly_accessible = true are exposed to the internet, risking unauthorized database access.\",\n check(content, filePath) {\n if (!filePath.match(/\\.tf$/)) return [];\n if (!/resource\\s+\"aws_db_instance\"/.test(content)) return [];\n return findMatches(content, /publicly_accessible\\s*=\\s*true/g, rdsPubliclyAccessible, filePath, () =>\n \"Set publicly_accessible = false. Access RDS through VPC private subnets.\"\n );\n },\n};\n\n// ────────────────────────────────────────────\n// VC110 – Missing CloudTrail Logging\n// ────────────────────────────────────────────\n\nexport const missingCloudTrail: CustomRule = {\n id: \"VC110\",\n title: \"Missing CloudTrail Logging\",\n severity: \"high\",\n category: \"Infrastructure\",\n description: \"AWS environments without CloudTrail lack audit logging of API calls, making it difficult to detect unauthorized activity.\",\n check(content, filePath) {\n if (!filePath.match(/\\.tf$/)) return [];\n // Only flag if the file uses AWS provider but has no cloudtrail resource\n if (!/provider\\s+\"aws\"/.test(content)) return [];\n if (/aws_cloudtrail/.test(content)) return [];\n const lineNum = content.substring(0, content.search(/provider\\s+\"aws\"/)).split(\"\\n\").length;\n return [{\n rule: \"VC110\", title: missingCloudTrail.title, severity: \"high\" as const, category: \"Infrastructure\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Enable CloudTrail for audit logging of all AWS API calls.\",\n }];\n },\n};\n\n// ────────────────────────────────────────────\n// VC111 – Lambda Without VPC\n// ────────────────────────────────────────────\n\nexport const lambdaWithoutVPC: CustomRule = {\n id: \"VC111\",\n title: \"Lambda Without VPC\",\n severity: \"medium\",\n category: \"Infrastructure\",\n description: \"Lambda functions not placed in a VPC lack network isolation and cannot access VPC-only resources securely.\",\n check(content, filePath) {\n if (!filePath.match(/\\.tf$/)) return [];\n if (!/resource\\s+\"aws_lambda_function\"/.test(content)) return [];\n const matches: RuleMatch[] = [];\n const lambdaPattern = /resource\\s+\"aws_lambda_function\"\\s+\"(\\w+)\"/g;\n let m: RegExpExecArray | null;\n while ((m = lambdaPattern.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n const blockEnd = Math.min(m.index + 2000, content.length);\n const blockContent = content.substring(m.index, blockEnd);\n if (!/vpc_config\\s*\\{/.test(blockContent)) {\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n matches.push({\n rule: \"VC111\", title: lambdaWithoutVPC.title, severity: \"medium\" as const, category: \"Infrastructure\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Place Lambda functions in a VPC for network isolation.\",\n });\n }\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC112 – Docker Image Using Latest Tag\n// ────────────────────────────────────────────\n\nexport const dockerLatestTag: CustomRule = {\n id: \"VC112\",\n title: \"Docker Image Using Latest Tag\",\n severity: \"high\",\n category: \"Configuration\",\n description: \"Using :latest or no tag in Docker FROM directives leads to non-reproducible builds and potential security regressions.\",\n check(content, filePath) {\n if (!filePath.match(/Dockerfile$/i)) return [];\n const matches: RuleMatch[] = [];\n // Match FROM image:latest or FROM image (no tag, no AS, no scratch)\n const fromPattern = /^FROM\\s+(?!scratch)(\\S+?)(?:\\s+AS\\s+\\S+)?\\s*$/gm;\n let m: RegExpExecArray | null;\n while ((m = fromPattern.exec(content)) !== null) {\n const image = m[1];\n // Flag if using :latest or no tag at all (no colon)\n if (image.endsWith(\":latest\") || (!image.includes(\":\") && !image.startsWith(\"$\"))) {\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n matches.push({\n rule: \"VC112\", title: dockerLatestTag.title, severity: \"high\" as const, category: \"Configuration\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Pin Docker image versions: FROM node:20-alpine instead of FROM node:latest\",\n });\n }\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC113 – Docker COPY With Sensitive Files\n// ────────────────────────────────────────────\n\nexport const dockerCopySensitive: CustomRule = {\n id: \"VC113\",\n title: \"Docker COPY With Sensitive Files\",\n severity: \"high\",\n category: \"Configuration\",\n description: \"Using COPY . . or ADD . . without a .dockerignore can leak .env files, .git history, and other sensitive data into the Docker image.\",\n check(content, filePath) {\n if (!filePath.match(/Dockerfile$/i)) return [];\n // Check for COPY . . or ADD . .\n if (!/(?:COPY|ADD)\\s+\\.\\s+\\./.test(content)) return [];\n return findMatches(content, /(?:COPY|ADD)\\s+\\.\\s+\\./g, dockerCopySensitive, filePath, () =>\n \"Use .dockerignore to exclude .env, .git, node_modules, and sensitive files from the Docker build context.\"\n );\n },\n};\n\n// ────────────────────────────────────────────\n// VC114 – Docker Exposing Too Many Ports\n// ────────────────────────────────────────────\n\nexport const dockerTooManyPorts: CustomRule = {\n id: \"VC114\",\n title: \"Docker Exposing Too Many Ports\",\n severity: \"medium\",\n category: \"Configuration\",\n description: \"Exposing many ports or wide port ranges increases the attack surface of a container.\",\n check(content, filePath) {\n if (!filePath.match(/Dockerfile$/i)) return [];\n const matches: RuleMatch[] = [];\n // Detect port ranges like EXPOSE 3000-9000\n const rangePattern = /^EXPOSE\\s+\\d+-\\d+/gm;\n matches.push(...findMatches(content, rangePattern, dockerTooManyPorts, filePath, () =>\n \"Only expose necessary ports. Each EXPOSE should have a clear purpose.\"\n ));\n // Detect multiple EXPOSE directives (more than 3)\n const exposeLines = content.split(\"\\n\").filter(l => /^\\s*EXPOSE\\s+/.test(l));\n if (exposeLines.length > 3) {\n const firstExpose = content.search(/^\\s*EXPOSE\\s+/m);\n if (firstExpose >= 0) {\n const lineNum = content.substring(0, firstExpose).split(\"\\n\").length;\n matches.push({\n rule: \"VC114\", title: dockerTooManyPorts.title, severity: \"medium\" as const, category: \"Configuration\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Only expose necessary ports. Each EXPOSE should have a clear purpose.\",\n });\n }\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC115 – Kubernetes Secret Not Encrypted\n// ────────────────────────────────────────────\n\nexport const k8sSecretNotEncrypted: CustomRule = {\n id: \"VC115\",\n title: \"Kubernetes Secret Not Encrypted\",\n severity: \"high\",\n category: \"Infrastructure\",\n description: \"Kubernetes Secrets stored as plain base64 in manifests are not encrypted and can be trivially decoded. Use sealed-secrets or external-secrets.\",\n check(content, filePath) {\n if (!filePath.match(/\\.(yaml|yml)$/)) return [];\n if (!/kind\\s*:\\s*Secret/i.test(content)) return [];\n // Skip if using sealed-secrets or external-secrets annotations\n if (/sealedsecrets\\.bitnami\\.com|external-secrets\\.io/i.test(content)) return [];\n return findMatches(content, /kind\\s*:\\s*Secret/g, k8sSecretNotEncrypted, filePath, () =>\n \"Use sealed-secrets or external-secrets-operator. Never store plain secrets in manifests.\"\n );\n },\n};\n\n// ────────────────────────────────────────────\n// VC116 – Kubernetes Pod Without Resource Limits\n// ────────────────────────────────────────────\n\nexport const k8sNoResourceLimits: CustomRule = {\n id: \"VC116\",\n title: \"Kubernetes Pod Without Resource Limits\",\n severity: \"medium\",\n category: \"Infrastructure\",\n description: \"Pods or deployments without resource limits can consume excessive CPU/memory, causing cluster instability.\",\n check(content, filePath) {\n if (!filePath.match(/\\.(yaml|yml)$/)) return [];\n if (!/kind\\s*:\\s*(?:Pod|Deployment|StatefulSet|DaemonSet|Job|CronJob)/i.test(content)) return [];\n // Check if any container spec has resources.limits\n if (/resources\\s*:\\s*\\n\\s+limits\\s*:/m.test(content)) return [];\n // Also accept inline resources: { limits: ... }\n if (/resources\\s*:.*limits/i.test(content)) return [];\n const kindMatch = content.match(/kind\\s*:\\s*(?:Pod|Deployment|StatefulSet|DaemonSet|Job|CronJob)/i);\n if (!kindMatch) return [];\n const lineNum = content.substring(0, kindMatch.index).split(\"\\n\").length;\n return [{\n rule: \"VC116\", title: k8sNoResourceLimits.title, severity: \"medium\" as const, category: \"Infrastructure\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Set resource limits to prevent pods from consuming excessive CPU/memory.\",\n }];\n },\n};\n\n// ────────────────────────────────────────────\n// VC117 – Path Traversal Vulnerability\n// ────────────────────────────────────────────\n\nexport const pathTraversal: CustomRule = {\n id: \"VC117\",\n title: \"Path Traversal Vulnerability\",\n severity: \"critical\",\n category: \"Injection\",\n description: \"User input is used to construct file paths without sanitization, allowing attackers to read/write arbitrary files (e.g., ../../etc/passwd).\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!filePath.match(/\\.(js|ts|jsx|tsx|py|rb|go|php|java)$/)) return [];\n const findings: RuleMatch[] = [];\n // Pattern: file operations using user input (req.params, req.query, req.body) in path\n const patterns = [\n /(?:readFile|readFileSync|createReadStream|writeFile|writeFileSync|appendFile|unlink|unlinkSync|access|stat|statSync|existsSync)\\s*\\(\\s*(?:req\\.|request\\.|ctx\\.|params\\.|query\\.)/gi,\n /(?:path\\.join|path\\.resolve)\\s*\\([^)]*(?:req\\.|request\\.|ctx\\.|params\\.|query\\.)[^)]*\\)/gi,\n /(?:res\\.sendFile|res\\.download)\\s*\\([^)]*(?:req\\.|request\\.)/gi,\n /open\\s*\\(\\s*(?:request\\.|params\\[|args\\.)/gi,\n ];\n for (const pat of patterns) {\n let m;\n while ((m = pat.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n // Skip if path.normalize or sanitization is near\n const surrounding = content.substring(Math.max(0, m.index - 200), m.index + 200);\n if (/path\\.normalize|sanitize|\\.replace\\(\\s*['\"]\\.\\./i.test(surrounding)) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC117\", title: pathTraversal.title, severity: \"critical\" as const, category: \"Injection\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Sanitize file paths: use path.normalize(), reject paths containing '..', and validate against an allowed base directory.\",\n });\n }\n }\n return findings;\n },\n};\n\n// ────────────────────────────────────────────\n// VC118 – PII in Log Output\n// ────────────────────────────────────────────\n\nexport const piiInLogs: CustomRule = {\n id: \"VC118\",\n title: \"Personally Identifiable Information in Logs\",\n severity: \"high\",\n category: \"Information Leakage\",\n description: \"Logging statements that include email addresses, passwords, SSNs, credit card numbers, or other PII can leak sensitive data to log aggregation systems.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!filePath.match(/\\.(js|ts|jsx|tsx|py|rb|go|java|php)$/)) return [];\n const findings: RuleMatch[] = [];\n // Match console.log/logger.info etc. that reference sensitive fields\n const logPattern = /(?:console\\.(?:log|info|warn|error|debug)|logger\\.(?:info|warn|error|debug|log)|log\\.(?:info|warn|error|debug)|logging\\.(?:info|warn|error|debug))\\s*\\([^)]*(?:password|passwd|secret|ssn|social.?security|credit.?card|cardNumber|cvv|token|bearer|authorization)\\b/gi;\n let m;\n while ((m = logPattern.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n if (isInsideFixMessage(content, m.index)) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC118\", title: piiInLogs.title, severity: \"high\" as const, category: \"Information Leakage\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Never log sensitive data. Redact or mask PII before logging: log('user login', { email: maskEmail(email) }).\",\n });\n }\n return findings;\n },\n};\n\n// ────────────────────────────────────────────\n// VC119 – Hardcoded OAuth Client Secret\n// ────────────────────────────────────────────\n\nexport const hardcodedOAuthSecret: CustomRule = {\n id: \"VC119\",\n title: \"Hardcoded OAuth Client Secret\",\n severity: \"critical\",\n category: \"Secrets\",\n description: \"OAuth client secrets hardcoded in source code can be extracted and used to impersonate your application.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!filePath.match(/\\.(js|ts|jsx|tsx|py|rb|go|java|php|env|json|yaml|yml)$/)) return [];\n // Skip lockfiles and generated files\n if (/package-lock|yarn\\.lock|pnpm-lock|composer\\.lock/i.test(filePath)) return [];\n const findings: RuleMatch[] = [];\n const patterns = [\n /client[_-]?secret\\s*[:=]\\s*[\"'][a-zA-Z0-9_\\-]{20,}[\"']/gi,\n /GOOGLE_CLIENT_SECRET\\s*[:=]\\s*[\"'][^\"']{10,}[\"']/gi,\n /GITHUB_CLIENT_SECRET\\s*[:=]\\s*[\"'][^\"']{10,}[\"']/gi,\n /FACEBOOK_APP_SECRET\\s*[:=]\\s*[\"'][^\"']{10,}[\"']/gi,\n /AUTH0_CLIENT_SECRET\\s*[:=]\\s*[\"'][^\"']{10,}[\"']/gi,\n ];\n for (const pat of patterns) {\n let m;\n while ((m = pat.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n // Skip if value is a placeholder or env ref\n if (/process\\.env|os\\.environ|ENV\\[|getenv|\\$\\{|<your|CHANGE_ME|xxx|placeholder/i.test(m[0])) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC119\", title: hardcodedOAuthSecret.title, severity: \"critical\" as const, category: \"Secrets\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Move OAuth client secrets to environment variables. Never commit secrets to source control.\",\n });\n }\n }\n return findings;\n },\n};\n\n// ────────────────────────────────────────────\n// VC120 – Missing OAuth State Parameter\n// ────────────────────────────────────────────\n\nexport const missingOAuthState: CustomRule = {\n id: \"VC120\",\n title: \"OAuth Flow Missing State Parameter\",\n severity: \"high\",\n category: \"Authentication\",\n description: \"OAuth authorization requests without a state parameter are vulnerable to CSRF attacks, allowing attackers to link their account to a victim's session.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!filePath.match(/\\.(js|ts|jsx|tsx|py|rb|go|java|php)$/)) return [];\n const findings: RuleMatch[] = [];\n // OAuth authorization URL construction without state param\n const oauthUrlPattern = /(?:authorize\\?|\\/oauth\\/authorize|\\/auth\\?|authorization_endpoint)[^}]*(?:client_id|response_type)/gi;\n let m;\n while ((m = oauthUrlPattern.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n const surrounding = content.substring(m.index, Math.min(content.length, m.index + 500));\n if (/state\\s*[=:]/i.test(surrounding)) continue; // Has state param\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC120\", title: missingOAuthState.title, severity: \"high\" as const, category: \"Authentication\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Always include a cryptographically random 'state' parameter in OAuth authorization requests and validate it on callback.\",\n });\n }\n return findings;\n },\n};\n\n// ────────────────────────────────────────────\n// VC121 – Unpinned GitHub Actions Version\n// ────────────────────────────────────────────\n\nexport const unpinnedGitHubAction: CustomRule = {\n id: \"VC121\",\n title: \"Unpinned GitHub Actions Version\",\n severity: \"high\",\n category: \"Supply Chain\",\n description: \"GitHub Actions using branch references (@main, @master) instead of commit SHAs can be compromised via supply-chain attacks.\",\n check(content, filePath) {\n if (!filePath.match(/\\.github\\/workflows\\/.*\\.(yml|yaml)$/)) return [];\n const findings: RuleMatch[] = [];\n // Match uses: org/action@branch (not @sha or @v1.2.3)\n const usesPattern = /uses\\s*:\\s*[\\w\\-]+\\/[\\w\\-]+@(main|master|dev|develop|latest)\\b/gi;\n let m;\n while ((m = usesPattern.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC121\", title: unpinnedGitHubAction.title, severity: \"high\" as const, category: \"Supply Chain\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Pin GitHub Actions to a specific commit SHA instead of a branch name: uses: owner/action@<full-sha>\",\n });\n }\n return findings;\n },\n};\n\n// ────────────────────────────────────────────\n// VC122 – Deprecated TLS Version\n// ────────────────────────────────────────────\n\nexport const deprecatedTLS: CustomRule = {\n id: \"VC122\",\n title: \"Deprecated TLS Version Configured\",\n severity: \"high\",\n category: \"Cryptography\",\n description: \"TLS 1.0 and 1.1 are deprecated and have known vulnerabilities. Only TLS 1.2+ should be used.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!filePath.match(/\\.(js|ts|py|rb|go|java|yaml|yml|json|conf|cfg|xml)$/)) return [];\n const findings: RuleMatch[] = [];\n const patterns = [\n /(?:TLSv1_METHOD|TLSv1\\.0|TLSv1\\.1|ssl\\.PROTOCOL_TLSv1|SSLv3|SSLv2|TLS_1_0|TLS_1_1|minVersion\\s*[:=]\\s*[\"']TLSv1(?:\\.1)?[\"'])/gi,\n /(?:tls\\.DEFAULT_MIN_VERSION|secureProtocol)\\s*[:=]\\s*[\"'](?:TLSv1_method|TLSv1_1_method|SSLv3_method)[\"']/gi,\n /ssl_protocols\\s+.*(?:TLSv1(?:\\.1)?(?:\\s|;))/gi,\n ];\n for (const pat of patterns) {\n let m;\n while ((m = pat.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC122\", title: deprecatedTLS.title, severity: \"high\" as const, category: \"Cryptography\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Use TLS 1.2 or higher. Set minVersion to 'TLSv1.2' and remove support for TLS 1.0/1.1.\",\n });\n }\n }\n return findings;\n },\n};\n\n// ────────────────────────────────────────────\n// VC123 – Weak RSA Key Size\n// ────────────────────────────────────────────\n\nexport const weakRSAKeySize: CustomRule = {\n id: \"VC123\",\n title: \"Weak RSA Key Size\",\n severity: \"high\",\n category: \"Cryptography\",\n description: \"RSA keys smaller than 2048 bits are considered insecure and can be factored with modern hardware.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!filePath.match(/\\.(js|ts|py|rb|go|java|php)$/)) return [];\n const findings: RuleMatch[] = [];\n // Match RSA key generation with explicit small sizes\n const patterns = [\n /generateKeyPair\\s*\\(\\s*[\"']rsa[\"']\\s*,\\s*\\{[^}]*modulusLength\\s*:\\s*(512|768|1024)\\b/gi,\n /RSA\\.generate\\s*\\(\\s*(512|768|1024)\\b/gi,\n /rsa\\.GenerateKey\\s*\\([^,]*,\\s*(512|768|1024)\\b/gi,\n /KeyPairGenerator.*initialize\\s*\\(\\s*(512|768|1024)\\b/gi,\n ];\n for (const pat of patterns) {\n let m;\n while ((m = pat.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC123\", title: weakRSAKeySize.title, severity: \"high\" as const, category: \"Cryptography\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Use RSA key sizes of at least 2048 bits. 4096 bits is recommended for long-term security.\",\n });\n }\n }\n return findings;\n },\n};\n\n// ────────────────────────────────────────────\n// VC124 – ECB Mode Encryption\n// ────────────────────────────────────────────\n\nexport const ecbModeEncryption: CustomRule = {\n id: \"VC124\",\n title: \"Insecure ECB Mode Encryption\",\n severity: \"high\",\n category: \"Cryptography\",\n description: \"ECB (Electronic Codebook) mode encrypts identical plaintext blocks to identical ciphertext, leaking patterns in the data.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!filePath.match(/\\.(js|ts|py|rb|go|java|php)$/)) return [];\n const findings: RuleMatch[] = [];\n const patterns = [\n /createCipher(?:iv)?\\s*\\(\\s*[\"'](?:aes-(?:128|192|256)-ecb|des-ecb)[\"']/gi,\n /AES\\.(?:new|MODE_ECB)|MODE_ECB/gi,\n /Cipher\\.getInstance\\s*\\(\\s*[\"']AES\\/ECB/gi,\n /aes\\.NewCipher|cipher\\.NewECBEncrypter/gi,\n ];\n for (const pat of patterns) {\n let m;\n while ((m = pat.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC124\", title: ecbModeEncryption.title, severity: \"high\" as const, category: \"Cryptography\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Use AES-GCM or AES-CBC with HMAC instead of ECB mode. GCM provides both encryption and authentication.\",\n });\n }\n }\n return findings;\n },\n};\n\n// ────────────────────────────────────────────\n// VC125 – Insecure Password Reset Flow\n// ────────────────────────────────────────────\n\nexport const insecurePasswordReset: CustomRule = {\n id: \"VC125\",\n title: \"Insecure Password Reset Implementation\",\n severity: \"high\",\n category: \"Authentication\",\n description: \"Password reset using predictable tokens, no expiration, or user-enumeration leaks can be exploited to take over accounts.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!filePath.match(/\\.(js|ts|jsx|tsx|py|rb|go|java|php)$/)) return [];\n if (!/(?:reset|forgot).*(?:password|passwd)/i.test(content)) return [];\n const findings: RuleMatch[] = [];\n // Predictable reset tokens (using Date.now, Math.random, uuid without crypto)\n const weakTokens = /(?:reset|forgot).*(?:token|code)\\s*[:=].*(?:Date\\.now|Math\\.random|uuid\\(\\)|nanoid\\(\\))/gi;\n let m;\n while ((m = weakTokens.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC125\", title: insecurePasswordReset.title, severity: \"high\" as const, category: \"Authentication\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Use crypto.randomBytes(32).toString('hex') for reset tokens. Set expiration (15-60 minutes) and single-use enforcement.\",\n });\n }\n // User enumeration: different responses for valid vs invalid emails\n const enumeration = /(?:user|email|account)\\s*(?:not\\s*found|does\\s*not\\s*exist|invalid)/gi;\n while ((m = enumeration.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n // Only flag if in a password reset context\n const surrounding = content.substring(Math.max(0, m.index - 500), m.index);\n if (!/(?:reset|forgot).*password/i.test(surrounding)) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC125\", title: insecurePasswordReset.title, severity: \"high\" as const, category: \"Authentication\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Always return the same response regardless of whether the email exists. Say 'If an account exists, a reset link was sent.'\",\n });\n }\n return findings;\n },\n};\n\n// ────────────────────────────────────────────\n// VC126 – Terraform State File Exposed\n// ────────────────────────────────────────────\n\nexport const terraformStateExposed: CustomRule = {\n id: \"VC126\",\n title: \"Terraform State File Committed\",\n severity: \"critical\",\n category: \"Secrets\",\n description: \"Terraform state files contain sensitive infrastructure details, secrets, and access credentials in plaintext. They must never be committed to version control.\",\n check(content, filePath) {\n const findings: RuleMatch[] = [];\n // Direct detection of terraform state files\n if (/terraform\\.tfstate(\\.backup)?$/.test(filePath)) {\n findings.push({\n rule: \"VC126\", title: terraformStateExposed.title, severity: \"critical\" as const, category: \"Secrets\",\n file: filePath, line: 1, snippet: getSnippet(content, 1),\n fix: \"Add '*.tfstate' and '*.tfstate.backup' to .gitignore. Use remote state backends (S3, GCS, Terraform Cloud) instead.\",\n });\n }\n // Check .gitignore for missing tfstate\n if (filePath.endsWith(\".gitignore\") && !content.includes(\"tfstate\")) {\n // Only flag if the repo has terraform files\n if (/\\.tf$/.test(filePath) || content.includes(\".tf\")) {\n // This is a weak signal, skip\n }\n }\n return findings;\n },\n};\n\n// ────────────────────────────────────────────\n// VC127 – Insecure HTTP Method Handling\n// ────────────────────────────────────────────\n\nexport const insecureHTTPMethods: CustomRule = {\n id: \"VC127\",\n title: \"Dangerous HTTP Methods Without Auth\",\n severity: \"high\",\n category: \"Authorization\",\n description: \"DELETE, PUT, and PATCH endpoints without authentication checks allow unauthorized data modification or deletion.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!filePath.match(/\\.(js|ts|jsx|tsx|py|rb|go|java|php)$/)) return [];\n const findings: RuleMatch[] = [];\n // Express/Hono/Fastify route handlers for dangerous methods\n const routePatterns = [\n /(?:app|router|server)\\.(delete|put|patch)\\s*\\(\\s*[\"']/gi,\n ];\n for (const pat of routePatterns) {\n let m;\n while ((m = pat.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n // Check the handler for auth middleware or auth checks\n const handlerBlock = content.substring(m.index, Math.min(content.length, m.index + 500));\n if (/auth|authenticate|authorize|requireAuth|isAuthenticated|protect|guard|middleware|session|jwt|verify|clerk|getAuth/i.test(handlerBlock)) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC127\", title: insecureHTTPMethods.title, severity: \"high\" as const, category: \"Authorization\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Add authentication middleware to DELETE, PUT, and PATCH routes. Verify the user has permission to modify the resource.\",\n });\n }\n }\n return findings;\n },\n};\n\n// ────────────────────────────────────────────\n// VC128 – HTTP Request Smuggling Risk\n// ────────────────────────────────────────────\n\nexport const httpRequestSmuggling: CustomRule = {\n id: \"VC128\",\n title: \"HTTP Request Smuggling Risk\",\n severity: \"high\",\n category: \"Configuration\",\n description: \"Manually parsing Content-Length or Transfer-Encoding headers can lead to request smuggling when behind a reverse proxy.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!filePath.match(/\\.(js|ts|py|rb|go|java|php)$/)) return [];\n const findings: RuleMatch[] = [];\n const patterns = [\n /(?:headers?\\[?|getHeader\\s*\\(\\s*)[\"'](?:content-length|transfer-encoding)[\"']\\s*\\]?\\s*\\)/gi,\n /parseInt\\s*\\(\\s*(?:req|request)\\.headers?\\[?\\s*[\"']content-length[\"']/gi,\n ];\n for (const pat of patterns) {\n let m;\n while ((m = pat.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC128\", title: httpRequestSmuggling.title, severity: \"high\" as const, category: \"Configuration\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Let your framework handle Content-Length and Transfer-Encoding headers. Do not manually parse or trust these values.\",\n });\n }\n }\n return findings;\n },\n};\n\n// ────────────────────────────────────────────\n// VC129 – Unencrypted PII in Database Schema\n// ────────────────────────────────────────────\n\nexport const unencryptedPII: CustomRule = {\n id: \"VC129\",\n title: \"Sensitive Data Stored Without Encryption\",\n severity: \"high\",\n category: \"Information Leakage\",\n description: \"Database schemas storing PII (SSN, credit cards, health records) in plaintext violate compliance requirements and expose data in breaches.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!filePath.match(/\\.(sql|prisma|py|ts|js|rb)$/)) return [];\n const findings: RuleMatch[] = [];\n // Schema definitions with sensitive field names stored as plain text/varchar\n const schemaPatterns = [\n /(?:column|field|Column|Field)\\s*\\(\\s*[\"'](?:ssn|social_security|tax_id|credit_card|card_number|cvv|passport|driver_license|bank_account|routing_number)[\"']\\s*,\\s*(?:String|TEXT|VARCHAR|Text|text)/gi,\n /(?:ssn|social_security|tax_id|credit_card|card_number|cvv|passport_number|driver_license|bank_account|routing_number)\\s+(?:TEXT|VARCHAR|String|text|character varying)/gi,\n ];\n for (const pat of schemaPatterns) {\n let m;\n while ((m = pat.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC129\", title: unencryptedPII.title, severity: \"high\" as const, category: \"Information Leakage\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Encrypt sensitive fields at the application level before storing. Use field-level encryption with a KMS for SSN, credit cards, and health data.\",\n });\n }\n }\n return findings;\n },\n};\n\n// ────────────────────────────────────────────\n// VC130 – Missing Rate Limit on Auth Endpoints\n// ────────────────────────────────────────────\n\nexport const missingAuthRateLimit: CustomRule = {\n id: \"VC130\",\n title: \"Authentication Endpoint Without Rate Limiting\",\n severity: \"high\",\n category: \"Authentication\",\n description: \"Login, registration, and password reset endpoints without rate limiting are vulnerable to credential stuffing and brute-force attacks.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!filePath.match(/\\.(js|ts|jsx|tsx|py|rb|go|java|php)$/)) return [];\n const findings: RuleMatch[] = [];\n // Routes that handle auth operations\n const authRoutePattern = /(?:app|router|server)\\.(?:post|put)\\s*\\(\\s*[\"'](?:\\/(?:api\\/)?(?:auth\\/)?(?:login|signin|sign-in|register|signup|sign-up|forgot-password|reset-password))[\"']/gi;\n let m;\n while ((m = authRoutePattern.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n // Check surrounding code for rate limiting\n const surrounding = content.substring(Math.max(0, m.index - 300), Math.min(content.length, m.index + 300));\n if (/rateLimit|rateLimiter|throttle|slowDown|express-rate-limit|rate_limit|RateLimiter|limiter/i.test(surrounding)) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC130\", title: missingAuthRateLimit.title, severity: \"high\" as const, category: \"Authentication\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Add rate limiting to authentication endpoints. Limit to 5-10 attempts per minute per IP. Use express-rate-limit or similar.\",\n });\n }\n return findings;\n },\n};\n\n// ────────────────────────────────────────────\n// VC131 – Known Vulnerable Dependency Pattern\n// ────────────────────────────────────────────\n\nexport const vulnerableDependencies: CustomRule = {\n id: \"VC131\",\n title: \"Potentially Vulnerable Dependency\",\n severity: \"high\",\n category: \"Supply Chain\",\n description: \"Dependencies with known security issues that are commonly found in AI-generated codebases.\",\n check(content, filePath) {\n if (!filePath.endsWith(\"package.json\")) return [];\n // Skip nested package.json (node_modules, etc)\n if (/node_modules|\\.next|dist|build/.test(filePath)) return [];\n const findings: RuleMatch[] = [];\n // Known problematic packages and version ranges\n const vulnerablePackages: [RegExp, string][] = [\n [/\"jsonwebtoken\"\\s*:\\s*\"[\\^~]?[0-7]\\./g, \"jsonwebtoken < 8.x has signature bypass vulnerabilities\"],\n [/\"lodash\"\\s*:\\s*\"[\\^~]?[0-3]\\./g, \"lodash < 4.x has prototype pollution vulnerabilities\"],\n [/\"minimist\"\\s*:\\s*\"[\\^~]?[01]\\.[01]\\./g, \"minimist < 1.2.6 has prototype pollution\"],\n [/\"node-fetch\"\\s*:\\s*\"[\\^~]?[12]\\./g, \"node-fetch < 3.x has data exposure issues\"],\n [/\"express\"\\s*:\\s*\"[\\^~]?[0-3]\\./g, \"express < 4.x has multiple known vulnerabilities\"],\n [/\"axios\"\\s*:\\s*\"[\\^~]?0\\.[0-9]\\./g, \"axios < 0.21 has SSRF vulnerabilities\"],\n [/\"tar\"\\s*:\\s*\"[\\^~]?[0-5]\\./g, \"tar < 6.x has path traversal vulnerabilities\"],\n [/\"glob-parent\"\\s*:\\s*\"[\\^~]?[0-4]\\./g, \"glob-parent < 5.1.2 has ReDoS vulnerability\"],\n ];\n for (const [pat, message] of vulnerablePackages) {\n let m;\n while ((m = pat.exec(content)) !== null) {\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC131\", title: vulnerableDependencies.title, severity: \"high\" as const, category: \"Supply Chain\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: `${message}. Update to the latest version and run 'npm audit' regularly.`,\n });\n }\n }\n return findings;\n },\n};\n\n// ────────────────────────────────────────────\n// VC132–VC145: SERVICE-SPECIFIC API KEY DETECTION\n// Each rule targets a service with a known key prefix/format.\n// Descriptions include permission context (what the key can do).\n// ────────────────────────────────────────────\n\nconst SECRET_FILE_EXT = /\\.(js|ts|jsx|tsx|py|rb|go|java|php|env|json|yaml|yml|toml|cfg|conf|ini|sh|bash|zsh)$/;\nconst PLACEHOLDER_RE = /process\\.env|os\\.environ|ENV\\[|getenv|System\\.getenv|env\\(|CHANGE_ME|YOUR_|xxx|placeholder|example|TODO|FIXME|<your|dummy|fake_?key|test_?key/i;\nconst LOCK_FILE_RE = /package-lock|yarn\\.lock|pnpm-lock|Gemfile\\.lock|Pipfile\\.lock|composer\\.lock/i;\n\nfunction secretRuleCheck(\n content: string,\n filePath: string,\n pattern: RegExp,\n ruleId: string,\n title: string,\n severity: \"critical\" | \"high\",\n fix: string,\n): RuleMatch[] {\n if (isTestFile(filePath)) return [];\n if (!SECRET_FILE_EXT.test(filePath)) return [];\n if (LOCK_FILE_RE.test(filePath)) return [];\n const findings: RuleMatch[] = [];\n const re = new RegExp(pattern.source, pattern.flags.includes(\"g\") ? pattern.flags : `${pattern.flags}g`);\n let m: RegExpExecArray | null;\n while ((m = re.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n if (isInsideFixMessage(content, m.index)) continue;\n // Skip if this looks like an env var reference, not a literal\n const lineStart = content.lastIndexOf(\"\\n\", m.index - 1) + 1;\n const lineText = content.substring(lineStart, content.indexOf(\"\\n\", m.index));\n if (PLACEHOLDER_RE.test(lineText)) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: ruleId, title, severity, category: \"Secrets\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum), fix,\n });\n }\n return findings;\n}\n\nexport const hardcodedAnthropicKey: CustomRule = {\n id: \"VC132\",\n title: \"Hardcoded Anthropic API Key\",\n severity: \"critical\",\n category: \"Secrets\",\n description: \"Anthropic API keys (sk-ant-*) grant full API access: running models, incurring charges, and accessing usage history. A leaked key can be used to make unlimited API calls at your expense.\",\n check(content, filePath) {\n return secretRuleCheck(content, filePath, /sk-ant-api03-[A-Za-z0-9_\\-]{80,}/g, \"VC132\", this.title, \"critical\",\n \"Move the Anthropic API key to an environment variable (ANTHROPIC_API_KEY). Rotate the exposed key immediately at console.anthropic.com → API Keys.\");\n },\n};\n\nexport const hardcodedGitHubPAT: CustomRule = {\n id: \"VC133\",\n title: \"Hardcoded GitHub Personal Access Token\",\n severity: \"critical\",\n category: \"Secrets\",\n description: \"GitHub tokens (ghp_, gho_, ghu_, ghs_, ghr_) grant repository access: reading/writing code, managing issues, triggering Actions, and accessing private repos depending on scopes.\",\n check(content, filePath) {\n return secretRuleCheck(content, filePath, /(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36,}/g, \"VC133\", this.title, \"critical\",\n \"Move the GitHub token to an environment variable (GITHUB_TOKEN). Rotate immediately at github.com → Settings → Developer settings → Personal access tokens. Consider using fine-grained tokens with minimal permissions.\");\n },\n};\n\nexport const hardcodedSendGridKey: CustomRule = {\n id: \"VC134\",\n title: \"Hardcoded SendGrid API Key\",\n severity: \"high\",\n category: \"Secrets\",\n description: \"SendGrid API keys (SG.*) can send emails as your domain, access contact lists, view email activity, and modify sender reputation. A leaked key enables phishing from your verified domain.\",\n check(content, filePath) {\n return secretRuleCheck(content, filePath, /SG\\.[A-Za-z0-9_\\-]{22}\\.[A-Za-z0-9_\\-]{43}/g, \"VC134\", this.title, \"high\",\n \"Move the SendGrid API key to an environment variable (SENDGRID_API_KEY). Rotate at app.sendgrid.com → Settings → API Keys.\");\n },\n};\n\nexport const hardcodedSlackToken: CustomRule = {\n id: \"VC135\",\n title: \"Hardcoded Slack Token\",\n severity: \"high\",\n category: \"Secrets\",\n description: \"Slack tokens (xoxb-, xoxp-, xapp-) can read/send messages, access channels, list users, and manage workspace settings depending on scopes. Bot tokens (xoxb) have app-level access; user tokens (xoxp) act as the installing user.\",\n check(content, filePath) {\n return secretRuleCheck(content, filePath, /xox[bpas]-[0-9A-Za-z\\-]{10,}/g, \"VC135\", this.title, \"high\",\n \"Move the Slack token to an environment variable (SLACK_BOT_TOKEN). Rotate at api.slack.com → Your Apps → OAuth & Permissions → Reinstall.\");\n },\n};\n\nexport const hardcodedGCPServiceAccount: CustomRule = {\n id: \"VC136\",\n title: \"Hardcoded GCP Service Account Key\",\n severity: \"critical\",\n category: \"Secrets\",\n description: \"GCP service account JSON keys grant project-level access: compute, storage, databases, IAM, and billing depending on assigned roles. A leaked key can compromise your entire Google Cloud project.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!filePath.match(/\\.(json|js|ts|py|yaml|yml)$/)) return [];\n if (LOCK_FILE_RE.test(filePath)) return [];\n // Look for the combination of service_account type + private key in the same file\n if (!/\"type\"\\s*:\\s*\"service_account\"/.test(content)) return [];\n if (!/-----BEGIN (?:RSA )?PRIVATE KEY-----/.test(content)) return [];\n const findings: RuleMatch[] = [];\n const m = content.match(/\"type\"\\s*:\\s*\"service_account\"/);\n if (m && m.index !== undefined) {\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC136\", title: this.title, severity: \"critical\", category: \"Secrets\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"GCP service account keys should never be committed. Use Workload Identity Federation or store the key in a secrets manager. Delete this key in Google Cloud Console → IAM → Service Accounts and generate a new one.\",\n });\n }\n return findings;\n },\n};\n\nexport const hardcodedShopifyToken: CustomRule = {\n id: \"VC137\",\n title: \"Hardcoded Shopify Access Token\",\n severity: \"critical\",\n category: \"Secrets\",\n description: \"Shopify access tokens (shpat_, shpca_, shppa_) grant full store access: orders, customers, products, and payment information. A leaked token can expose customer PII and modify store data.\",\n check(content, filePath) {\n return secretRuleCheck(content, filePath, /shp(?:at|ca|pa)_[a-fA-F0-9]{32,}/g, \"VC137\", this.title, \"critical\",\n \"Move the Shopify token to an environment variable. Rotate in Shopify Admin → Apps → Develop apps → API credentials.\");\n },\n};\n\nexport const hardcodedGitLabToken: CustomRule = {\n id: \"VC138\",\n title: \"Hardcoded GitLab Token\",\n severity: \"critical\",\n category: \"Secrets\",\n description: \"GitLab personal/project tokens (glpat-) grant repository and CI/CD access: reading/writing code, triggering pipelines, and managing project settings.\",\n check(content, filePath) {\n return secretRuleCheck(content, filePath, /glpat-[A-Za-z0-9_\\-]{20,}/g, \"VC138\", this.title, \"critical\",\n \"Move the GitLab token to an environment variable. Revoke and regenerate at gitlab.com → Preferences → Access Tokens.\");\n },\n};\n\nexport const hardcodedTwilioKey: CustomRule = {\n id: \"VC139\",\n title: \"Hardcoded Twilio API Key\",\n severity: \"high\",\n category: \"Secrets\",\n description: \"Twilio API key SIDs (SK*) can send SMS/calls, access call recordings and logs, and modify account configuration. A leaked key enables toll fraud and unauthorized communications.\",\n check(content, filePath) {\n return secretRuleCheck(content, filePath, /SK[0-9a-fA-F]{32}/g, \"VC139\", this.title, \"high\",\n \"Move the Twilio API key to an environment variable (TWILIO_API_KEY). Rotate at twilio.com → Console → API Keys.\");\n },\n};\n\nexport const hardcodedMailgunKey: CustomRule = {\n id: \"VC140\",\n title: \"Hardcoded Mailgun API Key\",\n severity: \"high\",\n category: \"Secrets\",\n description: \"Mailgun API keys (key-*) can send emails as your domain, access email logs, and manage routes. A leaked key enables phishing from your verified sending domain.\",\n check(content, filePath) {\n return secretRuleCheck(content, filePath, /key-[0-9a-zA-Z]{32}/g, \"VC140\", this.title, \"high\",\n \"Move the Mailgun key to an environment variable (MAILGUN_API_KEY). Rotate at app.mailgun.com → Settings → API Security.\");\n },\n};\n\nexport const hardcodedDatadogKey: CustomRule = {\n id: \"VC141\",\n title: \"Hardcoded Datadog API Key\",\n severity: \"high\",\n category: \"Secrets\",\n description: \"Datadog API/app keys grant access to all monitoring data and can modify dashboards, alerts, and integrations. A leaked key exposes your entire infrastructure topology.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!SECRET_FILE_EXT.test(filePath)) return [];\n if (LOCK_FILE_RE.test(filePath)) return [];\n // Look for DD_API_KEY or DD_APP_KEY assignments with hex values\n const pattern = /(?:DD_API_KEY|DD_APP_KEY|datadog[_-]?(?:api|app)[_-]?key)\\s*[:=]\\s*[\"'`]([a-f0-9]{32})[\"'`]/gi;\n const findings: RuleMatch[] = [];\n let m: RegExpExecArray | null;\n while ((m = pattern.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n if (isInsideFixMessage(content, m.index)) continue;\n const lineStart = content.lastIndexOf(\"\\n\", m.index - 1) + 1;\n const lineText = content.substring(lineStart, content.indexOf(\"\\n\", m.index));\n if (PLACEHOLDER_RE.test(lineText)) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC141\", title: this.title, severity: \"high\", category: \"Secrets\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Move the Datadog key to an environment variable (DD_API_KEY). Rotate at app.datadoghq.com → Organization Settings → API Keys.\",\n });\n }\n return findings;\n },\n};\n\nexport const hardcodedVercelToken: CustomRule = {\n id: \"VC142\",\n title: \"Hardcoded Vercel Token\",\n severity: \"high\",\n category: \"Secrets\",\n description: \"Vercel tokens can deploy projects, manage environment variables (which may contain other secrets), and access project settings. A leaked token can modify your production deployments.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!SECRET_FILE_EXT.test(filePath)) return [];\n if (LOCK_FILE_RE.test(filePath)) return [];\n const pattern = /(?:VERCEL_TOKEN|vercel[_-]?token|vercel[_-]?api[_-]?token)\\s*[:=]\\s*[\"'`]([A-Za-z0-9]{24,})[\"'`]/gi;\n const findings: RuleMatch[] = [];\n let m: RegExpExecArray | null;\n while ((m = pattern.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n if (isInsideFixMessage(content, m.index)) continue;\n const lineStart = content.lastIndexOf(\"\\n\", m.index - 1) + 1;\n const lineText = content.substring(lineStart, content.indexOf(\"\\n\", m.index));\n if (PLACEHOLDER_RE.test(lineText)) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC142\", title: this.title, severity: \"high\", category: \"Secrets\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Move the Vercel token to an environment variable (VERCEL_TOKEN). Rotate at vercel.com → Settings → Tokens.\",\n });\n }\n return findings;\n },\n};\n\nexport const hardcodedSupabaseServiceRole: CustomRule = {\n id: \"VC143\",\n title: \"Hardcoded Supabase Service Role Key\",\n severity: \"critical\",\n category: \"Secrets\",\n description: \"Supabase service role keys bypass Row Level Security and grant full database read/write access, auth admin, and storage admin. A leaked service role key exposes all user data.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!SECRET_FILE_EXT.test(filePath)) return [];\n if (LOCK_FILE_RE.test(filePath)) return [];\n // Look for service_role key assignments (JWT format near service_role context)\n const pattern = /(?:service[_-]?role[_-]?key|SUPABASE_SERVICE_ROLE_KEY|supabaseServiceRole)\\s*[:=]\\s*[\"'`](eyJ[A-Za-z0-9_\\-]{50,})[\"'`]/gi;\n const findings: RuleMatch[] = [];\n let m: RegExpExecArray | null;\n while ((m = pattern.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n if (isInsideFixMessage(content, m.index)) continue;\n const lineStart = content.lastIndexOf(\"\\n\", m.index - 1) + 1;\n const lineText = content.substring(lineStart, content.indexOf(\"\\n\", m.index));\n if (PLACEHOLDER_RE.test(lineText)) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC143\", title: this.title, severity: \"critical\", category: \"Secrets\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Move the Supabase service role key to a server-side environment variable. This key bypasses RLS — it must NEVER be exposed to the client. Rotate at supabase.com → Project Settings → API.\",\n });\n }\n return findings;\n },\n};\n\nexport const hardcodedVaultToken: CustomRule = {\n id: \"VC144\",\n title: \"Hardcoded HashiCorp Vault Token\",\n severity: \"critical\",\n category: \"Secrets\",\n description: \"Vault tokens (hvs.*, s.*) grant access to secrets stored in HashiCorp Vault, potentially exposing database credentials, API keys, and certificates across your entire infrastructure.\",\n check(content, filePath) {\n return secretRuleCheck(content, filePath, /(?:hvs\\.[A-Za-z0-9_\\-]{24,}|(?:VAULT_TOKEN|vault[_-]?token)\\s*[:=]\\s*[\"'`]s\\.[A-Za-z0-9]{24,}[\"'`])/g, \"VC144\", this.title, \"critical\",\n \"Move the Vault token to an environment variable (VAULT_TOKEN). Revoke the exposed token immediately with `vault token revoke`. Use short-lived tokens with minimal policies.\");\n },\n};\n\nexport const hardcodedPineconeKey: CustomRule = {\n id: \"VC145\",\n title: \"Hardcoded Pinecone API Key\",\n severity: \"high\",\n category: \"Secrets\",\n description: \"Pinecone API keys can read, write, and delete all vector data and manage indexes. A leaked key exposes your entire vector database and the embeddings stored in it.\",\n check(content, filePath) {\n return secretRuleCheck(content, filePath, /pcsk_[A-Za-z0-9_\\-]{50,}/g, \"VC145\", this.title, \"high\",\n \"Move the Pinecone API key to an environment variable (PINECONE_API_KEY). Rotate at app.pinecone.io → API Keys.\");\n },\n};\n\n// ────────────────────────────────────────────\n// VC146–VC151: SECRET EXPOSURE PATH ANALYSIS\n// Detects when secrets are used in ways that amplify exposure.\n// ────────────────────────────────────────────\n\nconst SECRET_VAR_RE = /(?:api[_-]?key|apikey|api[_-]?secret|secret[_-]?key|access[_-]?token|auth[_-]?token|private[_-]?key|password|passwd|pwd|credentials|client[_-]?secret|app[_-]?secret|master[_-]?key)/i;\n\nexport const secretInURLParam: CustomRule = {\n id: \"VC146\",\n title: \"Secret Passed in URL Query Parameter\",\n severity: \"critical\",\n category: \"Secrets\",\n description: \"API keys or tokens in URL query parameters are logged in server access logs, browser history, referrer headers, and proxy logs. Secrets in URLs are one of the most common causes of credential exposure.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!filePath.match(/\\.(js|ts|jsx|tsx|py|rb|go|java|php)$/)) return [];\n const findings: RuleMatch[] = [];\n const patterns = [\n // Template literals: `?api_key=${secret}` or `&token=${token}`\n /[?&](?:api_key|apikey|api_secret|token|access_token|secret|key|password|auth|authorization)=\\$\\{/gi,\n // String concat: '?key=' + apiKey\n /[?&](?:api_key|apikey|api_secret|token|access_token|secret|key|password|auth)=[\"']\\s*\\+/gi,\n // Python f-strings: f\"?token={token}\"\n /[?&](?:api_key|apikey|token|access_token|secret|key|password)=\\{[a-zA-Z_]/g,\n ];\n for (const p of patterns) {\n let m: RegExpExecArray | null;\n const re = new RegExp(p.source, p.flags.includes(\"g\") ? p.flags : `${p.flags}g`);\n while ((m = re.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n if (isInsideFixMessage(content, m.index)) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC146\", title: this.title, severity: \"critical\", category: \"Secrets\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Pass secrets in the Authorization header (Bearer token) or request body, never in URL parameters. URL parameters are logged in server access logs, browser history, and referrer headers.\",\n });\n }\n }\n return findings;\n },\n};\n\nexport const secretLoggedToConsole: CustomRule = {\n id: \"VC147\",\n title: \"Secret Logged to Console\",\n severity: \"high\",\n category: \"Secrets\",\n description: \"Logging variables that appear to contain secrets (key, token, password, secret) writes credentials to stdout, log files, and monitoring services where they can be harvested.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!filePath.match(/\\.(js|ts|jsx|tsx)$/)) return [];\n const findings: RuleMatch[] = [];\n // console.log/debug/info/warn/error with a secret-named variable\n const pattern = /console\\.(?:log|debug|info|warn|error)\\s*\\([^)]*\\b(api[_-]?key|apikey|secret|token|password|passwd|credentials|private[_-]?key|auth[_-]?token|access[_-]?token)\\b/gi;\n let m: RegExpExecArray | null;\n while ((m = pattern.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n if (isInsideFixMessage(content, m.index)) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC147\", title: this.title, severity: \"high\", category: \"Secrets\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Remove this console statement or redact the secret before logging. Use structured logging with a secret-redaction filter in production.\",\n });\n }\n return findings;\n },\n};\n\nexport const secretInErrorResponse: CustomRule = {\n id: \"VC148\",\n title: \"Secret Leaked in Error Response\",\n severity: \"critical\",\n category: \"Secrets\",\n description: \"Returning secrets or secret-named variables in API error responses exposes credentials to anyone who can trigger the error, including unauthenticated users.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!filePath.match(/\\.(js|ts|jsx|tsx)$/)) return [];\n const findings: RuleMatch[] = [];\n // res.json/send/status().json containing secret-named variables in catch/error blocks\n const pattern = /(?:res\\.(?:json|send|status\\s*\\([^)]*\\)\\s*\\.json))\\s*\\(\\s*\\{[^}]*\\b(api[_-]?key|secret|token|password|credentials|private[_-]?key|process\\.env\\.[A-Z_]*(?:KEY|SECRET|TOKEN|PASSWORD))\\b/gi;\n let m: RegExpExecArray | null;\n while ((m = pattern.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n if (isInsideFixMessage(content, m.index)) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC148\", title: this.title, severity: \"critical\", category: \"Secrets\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Never include secrets in API responses. Return a generic error message and log the detailed error server-side only.\",\n });\n }\n return findings;\n },\n};\n\nexport const secretInBundleConfig: CustomRule = {\n id: \"VC149\",\n title: \"Secret in Client-Side Bundle Configuration\",\n severity: \"critical\",\n category: \"Secrets\",\n description: \"Secrets defined in Webpack DefinePlugin, Vite define, or Next.js publicRuntimeConfig are embedded into the client-side JavaScript bundle and visible to anyone viewing the page source.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!filePath.match(/(?:webpack|vite|next)\\.config\\.|\\.config\\.(js|ts|mjs)$/)) return [];\n const findings: RuleMatch[] = [];\n const patterns = [\n // Webpack DefinePlugin with secret-named key\n /DefinePlugin\\s*\\(\\s*\\{[^}]*\\b(?:API_KEY|SECRET|TOKEN|PASSWORD|PRIVATE_KEY|CLIENT_SECRET)\\b/gi,\n // Vite define with secret-named key\n /define\\s*:\\s*\\{[^}]*\\b(?:API_KEY|SECRET|TOKEN|PASSWORD|PRIVATE_KEY|CLIENT_SECRET)\\b/gi,\n // Next.js publicRuntimeConfig with secret-named key\n /publicRuntimeConfig\\s*:\\s*\\{[^}]*\\b(?:apiKey|secret|token|password|privateKey|clientSecret)\\b/gi,\n ];\n for (const p of patterns) {\n let m: RegExpExecArray | null;\n const re = new RegExp(p.source, p.flags.includes(\"g\") ? p.flags : `${p.flags}g`);\n while ((m = re.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n if (isInsideFixMessage(content, m.index)) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC149\", title: this.title, severity: \"critical\", category: \"Secrets\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Move secrets to server-side environment variables. Only expose public configuration (like API base URLs) in client-side bundle config. Use serverRuntimeConfig (Next.js) or server-only modules.\",\n });\n }\n }\n return findings;\n },\n};\n\nexport const secretInHTMLAttribute: CustomRule = {\n id: \"VC150\",\n title: \"Secret in HTML Meta Tag or Data Attribute\",\n severity: \"high\",\n category: \"Secrets\",\n description: \"API keys or tokens embedded in HTML meta tags or data-* attributes are visible in the page source to anyone who visits the page, including search engine crawlers.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!filePath.match(/\\.(html|htm|jsx|tsx|vue|svelte|ejs|hbs|pug)$/)) return [];\n const findings: RuleMatch[] = [];\n const patterns = [\n // <meta name=\"api-key\" content=\"...\">\n /<meta\\s[^>]*name\\s*=\\s*[\"'](?:api[_-]?key|secret|token|password)[^>]*>/gi,\n // data-api-key=\"actual-value\" (not a template/binding)\n /data-(?:api[_-]?key|secret|token|password|auth)\\s*=\\s*[\"'][a-zA-Z0-9_\\-]{12,}[\"']/gi,\n ];\n for (const p of patterns) {\n let m: RegExpExecArray | null;\n const re = new RegExp(p.source, p.flags.includes(\"g\") ? p.flags : `${p.flags}g`);\n while ((m = re.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n if (isInsideFixMessage(content, m.index)) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC150\", title: this.title, severity: \"high\", category: \"Secrets\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Never embed secrets in HTML attributes or meta tags. Load configuration from server-side APIs or use server-rendered environment injection with non-secret values only.\",\n });\n }\n }\n return findings;\n },\n};\n\nexport const secretInCLIArgument: CustomRule = {\n id: \"VC151\",\n title: \"Secret Passed as Command-Line Argument\",\n severity: \"high\",\n category: \"Secrets\",\n description: \"Secrets interpolated into exec/spawn/execSync command strings are visible in process listings (ps aux), shell history, and system audit logs to any user on the same machine.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!filePath.match(/\\.(js|ts|jsx|tsx|py|rb)$/)) return [];\n const findings: RuleMatch[] = [];\n // exec/spawn/execSync with secret-named variable interpolated\n const patterns = [\n /(?:exec|execSync|spawn|spawnSync|child_process)\\s*\\([^)]*\\$\\{[^}]*(?:api[_-]?key|secret|token|password|credentials)/gi,\n /(?:exec|execSync|spawn|spawnSync|child_process)\\s*\\([^)]*[\"']\\s*\\+\\s*(?:api[_-]?key|secret|token|password|credentials)/gi,\n /(?:subprocess|os\\.system|os\\.popen)\\s*\\([^)]*\\{[^}]*(?:api[_-]?key|secret|token|password|credentials)/gi,\n ];\n for (const p of patterns) {\n let m: RegExpExecArray | null;\n const re = new RegExp(p.source, p.flags.includes(\"g\") ? p.flags : `${p.flags}g`);\n while ((m = re.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n if (isInsideFixMessage(content, m.index)) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC151\", title: this.title, severity: \"high\", category: \"Secrets\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Pass secrets via environment variables (env option in spawn/exec) instead of command-line arguments. CLI arguments are visible to all users via `ps aux`.\",\n });\n }\n }\n return findings;\n },\n};\n\n// ────────────────────────────────────────────\n// VC152–VC158: NEW RULES (Webhook Verification, CORS, Validation, etc.)\n// ────────────────────────────────────────────\n\nexport const webhookSignatureVerification: CustomRule = {\n id: \"VC152\",\n title: \"Missing Webhook Signature Verification (Non-Stripe)\",\n severity: \"critical\",\n category: \"Payment Security\",\n description: \"Webhook endpoints for Clerk, GitHub, Resend, SendGrid, Slack, or Twilio that don't verify the request signature allow attackers to forge events.\",\n check(content, filePath) {\n if (!filePath.match(/\\.(js|ts|jsx|tsx)$/)) return [];\n if (isTestFile(filePath)) return [];\n if (!/\\/api\\/|\\/webhook/i.test(filePath)) return [];\n // Must have a POST handler\n if (!/export\\s+(?:async\\s+)?function\\s+POST/i.test(content)) return [];\n\n const services = [\n { name: \"Clerk\", content: /clerk/i, events: /user\\.created|user\\.updated|user\\.deleted|session\\./i, verify: /svix|Webhook\\(\\)\\.verify|webhook-id|wh_secret/i },\n { name: \"GitHub\", content: /github/i, events: /push|pull_request|issues|installation/i, verify: /createHmac|x-hub-signature|verify.*signature|GITHUB_WEBHOOK_SECRET/i },\n { name: \"Resend\", content: /resend/i, events: /email\\.sent|email\\.delivered|email\\.bounced/i, verify: /svix|webhook.*verify|x-webhook-signature|RESEND_WEBHOOK_SECRET/i },\n { name: \"SendGrid\", content: /sendgrid/i, events: /inbound.*parse|event.*webhook/i, verify: /EventWebhook|x-twilio-email|verifySignature|SENDGRID_WEBHOOK_VERIFICATION/i },\n { name: \"Slack\", content: /slack/i, events: /event_callback|challenge|url_verification/i, verify: /signing_secret|createHmac|x-slack-signature|SLACK_SIGNING_SECRET/i },\n { name: \"Twilio\", content: /twilio/i, events: /sms|voice|StatusCallback|MessagingResponse/i, verify: /validateRequest|X-Twilio-Signature|authToken|TWILIO_AUTH_TOKEN/i },\n ];\n\n const findings: RuleMatch[] = [];\n for (const svc of services) {\n if (!svc.content.test(content)) continue;\n if (!svc.events.test(content)) continue;\n if (svc.verify.test(content)) continue;\n // Found a handler for this service without verification\n const m = content.match(/export\\s+(?:async\\s+)?function\\s+POST/);\n if (!m || m.index === undefined) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC152\", title: `${svc.name} Webhook Missing Signature Verification`,\n severity: \"critical\", category: \"Payment Security\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: `Verify the ${svc.name} webhook signature before processing events. Check the ${svc.name} documentation for their signature verification method.`,\n });\n break; // Max 1 finding per file\n }\n return findings;\n },\n};\n\nexport const reflectedCORSOrigin: CustomRule = {\n id: \"VC153\",\n title: \"Reflected CORS Origin with Credentials\",\n severity: \"high\",\n category: \"Configuration\",\n description: \"Echoing req.headers.origin into Access-Control-Allow-Origin with credentials enabled lets any website read authenticated API responses. This is worse than wildcard CORS because browsers actually send cookies.\",\n check(content, filePath) {\n if (!filePath.match(/\\.(js|ts|jsx|tsx)$/)) return [];\n if (isTestFile(filePath)) return [];\n // Must have credentials enabled\n if (!/credentials\\s*[=:]\\s*true|Allow-Credentials.*true/i.test(content)) return [];\n // Must have origin reflection pattern\n if (!/req\\.headers\\.origin|callback\\s*\\(\\s*null\\s*,\\s*origin\\s*\\)|origin:\\s*true/i.test(content)) return [];\n // Skip if there's an allowlist check\n if (/ALLOWED_ORIGINS|allowedOrigins|whitelist|isAllowed|\\.includes\\s*\\(\\s*origin|\\.has\\s*\\(\\s*origin/i.test(content)) return [];\n\n const patterns = [\n /(?:Allow-Origin[\"'],\\s*req\\.headers\\.origin)/gi,\n /origin\\s*:\\s*\\(\\s*origin\\s*,\\s*callback\\s*\\)\\s*=>\\s*callback\\s*\\(\\s*null\\s*,\\s*origin\\s*\\)/gi,\n /origin\\s*:\\s*true/gi,\n ];\n const matches: RuleMatch[] = [];\n for (const p of patterns) {\n const raw = findMatches(content, p, reflectedCORSOrigin, filePath, () =>\n \"Use an explicit origin allowlist instead of reflecting the request origin. Only allow origins you control.\"\n );\n if (raw.length > 0) { matches.push(raw[0]); break; } // Max 1 per file\n }\n return matches;\n },\n};\n\nexport const missingRequestValidation: CustomRule = {\n id: \"VC154\",\n title: \"API Route Without Request Body Validation\",\n severity: \"medium\",\n category: \"Authorization\",\n description: \"API route reads the request body without any schema validation (zod, joi, yup, etc.). Unvalidated input can lead to unexpected behavior, injection, or data corruption.\",\n check(content, filePath) {\n if (!filePath.match(/\\.(js|ts|jsx|tsx)$/)) return [];\n if (isTestFile(filePath)) return [];\n if (!/\\/api\\//i.test(filePath)) return [];\n if (/\\/webhook/i.test(filePath)) return []; // Webhooks validate via signature\n // Must have a mutating handler\n if (!/export\\s+(?:async\\s+)?function\\s+(?:POST|PUT|PATCH)/i.test(content)) return [];\n // Must read the body\n if (!/req\\.body|request\\.json\\(\\)|c\\.req\\.json\\(\\)|await.*\\.json\\(\\)/i.test(content)) return [];\n // Skip if any validation library or manual checks exist\n if (/zod|joi|yup|ajv|class-validator|superstruct|valibot|\\.safeParse|\\.validate\\(|z\\.object|Joi\\.object|yup\\.object|schema\\.parse|zodResolver/i.test(content)) return [];\n if (/if\\s*\\(\\s*!(?:body|parsed|data|payload)\\.|throw.*(?:missing|invalid|required)/i.test(content)) return [];\n const m = content.match(/export\\s+(?:async\\s+)?function\\s+(?:POST|PUT|PATCH)/);\n if (!m || m.index === undefined) return [];\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n return [{\n rule: \"VC154\", title: this.title, severity: \"medium\" as const, category: \"Authorization\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Validate request bodies with a schema library like zod: const parsed = schema.safeParse(await request.json()); if (!parsed.success) return Response.json({ error: 'Invalid input' }, { status: 400 });\",\n }];\n },\n};\n\nexport const missingAIRateLimit: CustomRule = {\n id: \"VC155\",\n title: \"Missing Rate Limiting on AI/LLM API Call\",\n severity: \"high\",\n category: \"Availability\",\n description: \"API route calls an AI/LLM service (OpenAI, Anthropic, Cohere) without rate limiting. A single user can burn through your entire API budget in minutes.\",\n check(content, filePath) {\n if (!filePath.match(/\\.(js|ts|jsx|tsx)$/)) return [];\n if (isTestFile(filePath)) return [];\n // Must have a route handler\n if (!/export\\s+(?:async\\s+)?function\\s+(?:GET|POST|PUT|PATCH|DELETE)/i.test(content) &&\n !/\\.(get|post|put|patch|delete)\\s*\\(/i.test(content)) return [];\n // Must reference an AI SDK\n if (!/openai|anthropic|@anthropic-ai|cohere|@google\\/generative-ai|chat\\.completions|messages\\.create/i.test(content)) return [];\n // Skip if rate limiting exists\n if (/rateLimit|rateLimiter|throttle|checkRateLimit|limiter|slowDown|express-rate-limit/i.test(content)) return [];\n const m = content.match(/export\\s+(?:async\\s+)?function\\s+(?:POST|GET)/i) || content.match(/\\.(post|get)\\s*\\(/i);\n if (!m || m.index === undefined) return [];\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n return [{\n rule: \"VC155\", title: this.title, severity: \"high\" as const, category: \"Availability\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Add rate limiting to AI/LLM endpoints to prevent budget abuse. Use express-rate-limit or a custom limiter with per-user and per-IP limits.\",\n }];\n },\n};\n\nexport const missingPagination: CustomRule = {\n id: \"VC156\",\n title: \"Missing Pagination on List Endpoint\",\n severity: \"medium\",\n category: \"Availability\",\n description: \"API endpoint returns database records without a LIMIT or pagination. A single request can dump the entire table, crash the server, or cause an out-of-memory error.\",\n check(content, filePath) {\n if (!filePath.match(/\\.(js|ts|jsx|tsx)$/)) return [];\n if (isTestFile(filePath)) return [];\n if (!/\\/api\\//i.test(filePath)) return [];\n // Must have a GET handler (list endpoints)\n if (!/export\\s+(?:async\\s+)?function\\s+GET/i.test(content)) return [];\n // Must have a database query\n if (!/findMany|\\.find\\(\\s*\\{|\\.find\\(\\s*\\)|SELECT\\s.*FROM|\\.all\\(\\)|\\.query\\(/i.test(content)) return [];\n // Skip if any pagination exists\n if (/LIMIT|\\.take\\(|\\.limit\\(|\\.skip\\(|offset|cursor|page|perPage|pageSize|paginate|\\.slice\\(/i.test(content)) return [];\n // Skip if it filters by specific ID (not a list endpoint)\n if (/findUnique|findFirst|findById|\\.findOne|WHERE.*id\\s*=/i.test(content)) return [];\n const m = content.match(/export\\s+(?:async\\s+)?function\\s+GET/);\n if (!m || m.index === undefined) return [];\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n return [{\n rule: \"VC156\", title: this.title, severity: \"medium\" as const, category: \"Availability\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Add pagination to list endpoints: .findMany({ take: limit, skip: offset }) or SELECT ... LIMIT ? OFFSET ?\",\n }];\n },\n};\n\nexport const exposedDatabaseStudio: CustomRule = {\n id: \"VC157\",\n title: \"Database Studio Exposed in Production\",\n severity: \"high\",\n category: \"Configuration\",\n description: \"Prisma Studio or Drizzle Studio is configured in a production script or bound to 0.0.0.0, giving anyone with the URL full database admin access.\",\n check(content, filePath) {\n const basename = filePath.split(\"/\").pop() || \"\";\n if (basename !== \"package.json\" && !/docker-compose/i.test(basename)) return [];\n // Check for studio in production-facing scripts\n if (!/prisma\\s+studio|drizzle-kit\\s+studio/i.test(content)) return [];\n // Skip if only in dev/db:studio scripts (that's fine)\n const lines = content.split(\"\\n\");\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n if (!/prisma\\s+studio|drizzle-kit\\s+studio/i.test(line)) continue;\n // Flag if in start/production script or bound to 0.0.0.0\n if (/\"start\"|\"production\"|\"build.*&&.*studio\"|0\\.0\\.0\\.0/i.test(line)) {\n return [{\n rule: \"VC157\", title: this.title, severity: \"high\" as const, category: \"Configuration\",\n file: filePath, line: i + 1, snippet: getSnippet(content, i + 1),\n fix: \"Only run database studio in development. Move the studio command to a dev-only script and never bind to 0.0.0.0 in production.\",\n }];\n }\n }\n return [];\n },\n};\n\nexport const insecureDirectObjectReference: CustomRule = {\n id: \"VC158\",\n title: \"Potential Insecure Direct Object Reference (IDOR)\",\n severity: \"high\",\n category: \"Authorization\",\n description: \"API route fetches a record by ID from URL parameters without checking if the requesting user owns that record. An attacker can access other users' data by guessing or enumerating IDs.\",\n check(content, filePath) {\n if (!filePath.match(/\\.(js|ts|jsx|tsx)$/)) return [];\n if (isTestFile(filePath)) return [];\n if (!/\\/api\\//i.test(filePath)) return [];\n // Must have a route handler\n if (!/export\\s+(?:async\\s+)?function\\s+(?:GET|PUT|PATCH|DELETE)/i.test(content)) return [];\n // Must read an ID from params\n if (!/params\\.id|params\\.(?:slug|uuid)|req\\.params\\./i.test(content)) return [];\n // Must query a database with that param\n if (!/findUnique|findFirst|findById|\\.findOne|WHERE.*=.*params|\\.get\\(.*params/i.test(content)) return [];\n // Skip if ownership check exists\n if (/userId|ownerId|createdBy|user_id|owner_id|AND.*user|where.*user|authorize|canAccess|checkPermission|isOwner|belongsTo/i.test(content)) return [];\n // Skip admin routes\n if (/admin/i.test(filePath)) return [];\n // Skip if auth is present (may be checking ownership via middleware)\n if (/requireUser|requireUserForApi|getServerSession|auth\\(\\)/i.test(content)) return [];\n const m = content.match(/params\\.id|params\\.(?:slug|uuid)|req\\.params\\./);\n if (!m || m.index === undefined) return [];\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n return [{\n rule: \"VC158\", title: this.title, severity: \"high\" as const, category: \"Authorization\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Add an ownership check: verify the requesting user owns the record before returning it. Example: WHERE id = params.id AND userId = session.userId\",\n }];\n },\n};\n\n// ────────────────────────────────────────────\n// RULE TIERS: FREE (30 rules) + PRO (all 158)\n// VC152-VC158 are Pro rules (not in freeRules)\n// ────────────────────────────────────────────\n\n// Free tier: core security rules available to everyone (30 of 151)\nexport const freeRules: CustomRule[] = [\n hardcodedSecrets, // VC001\n exposedEnvFile, // VC002\n missingAuthMiddleware, // VC003\n supabaseNoRLS, // VC004\n stripeWebhookUnprotected, // VC005\n sqlInjection, // VC006\n xssVulnerability, // VC007\n noRateLimiting, // VC008\n corsWildcard, // VC009\n clientSideAuth, // VC010\n nextPublicSecret, // VC011\n envNotGitignored, // VC014\n evalUsage, // VC015\n unvalidatedRedirect, // VC016\n insecureCookies, // VC017\n exposedAuthSecret, // VC018\n missingCSP, // VC020\n hardcodedJWTSecret, // VC031\n missingHTTPS, // VC032\n exposedDebugMode, // VC033\n insecureRandomness, // VC034\n missingErrorBoundary, // VC036\n exposedStackTraces, // VC037\n missingLockFile, // VC039\n dangerousInnerHTML, // VC063\n consoleLogProduction, // VC097\n emptyCatchBlock, // VC104\n todoLeftInCode, // VC103\n weakHashing, // VC060\n disabledTLSVerification, // VC061\n];\n\n// Pro rules are delivered server-side — not shipped in npm package\n// See: /api/cli/rules-bundle endpoint\nexport const allRules = freeRules;\n\nexport function runCustomRules(\n content: string,\n filePath: string,\n disabledRules: string[] = [],\n tier: \"free\" | \"pro\" = \"free\",\n extraRules: CustomRule[] = [],\n): Finding[] {\n const findings: Finding[] = [];\n\n // Skip files that ARE security scanners (avoid scanning ourselves)\n if (/function runScan\\(files\\)|export function runCustomRules/.test(content) && /const (?:rules|allRules)\\s*[:=]/.test(content) && /findMatches/.test(content)) {\n return findings;\n }\n\n // Skip compiled bundles (pro-rules-bundle.cjs, webpack output, etc.)\n if (/pro-rules-bundle|\\.bundle\\./i.test(filePath)) return findings;\n\n // Skip files that contain intentional example/demo vulnerable code\n // (onboarding tutorials, blog code samples, documentation examples)\n if (/onboarding|demo-data|example-vulnerable|code-sample/i.test(filePath)) return findings;\n\n // Free tier uses bundled rules; Pro tier uses free + server-delivered extra rules\n const ruleset = tier === \"pro\" && extraRules.length > 0\n ? [...freeRules, ...extraRules]\n : freeRules;\n for (const rule of ruleset) {\n if (disabledRules.includes(rule.id)) continue;\n\n const matches = rule.check(content, filePath);\n const compliance = complianceMap[rule.id];\n for (const match of matches) {\n findings.push({\n id: `${match.rule}-${match.file}:${match.line}`,\n rule: match.rule,\n severity: match.severity,\n title: match.title,\n description: rule.description,\n file: match.file,\n line: match.line,\n column: match.column,\n snippet: match.snippet,\n fix: match.fix,\n category: match.category,\n source: \"custom\",\n owasp: compliance?.owasp,\n cwe: compliance?.cwe,\n });\n }\n }\n\n return findings;\n}\n","import { execFile } from \"node:child_process\";\nimport { existsSync } from \"node:fs\";\nimport { readFile, writeFile, mkdtemp, rm } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { tmpdir } from \"node:os\";\nimport type { Finding, Severity } from \"../types.js\";\n\ninterface SemgrepSarifResult {\n runs?: Array<{\n results?: Array<{\n ruleId?: string;\n level?: string;\n message?: { text?: string };\n locations?: Array<{\n physicalLocation?: {\n artifactLocation?: { uri?: string };\n region?: {\n startLine?: number;\n startColumn?: number;\n snippet?: { text?: string };\n };\n };\n }>;\n }>;\n }>;\n}\n\nconst SEVERITY_MAP: Record<string, Severity> = {\n error: \"high\",\n warning: \"medium\",\n note: \"low\",\n none: \"info\",\n};\n\nfunction semgrepSeverityToXploitscan(level: string): Severity {\n return SEVERITY_MAP[level] ?? \"medium\";\n}\n\nasync function isSemgrepInstalled(): Promise<boolean> {\n return new Promise((resolve) => {\n execFile(\"semgrep\", [\"--version\"], (error) => {\n resolve(!error);\n });\n });\n}\n\nexport async function runSemgrep(\n directory: string,\n customRulesDir?: string,\n): Promise<{ findings: Finding[]; available: boolean }> {\n const installed = await isSemgrepInstalled();\n if (!installed) {\n return { findings: [], available: false };\n }\n\n const findings: Finding[] = [];\n\n // Create a temp directory for SARIF output\n const tmpDir = await mkdtemp(join(tmpdir(), \"xploitscan-semgrep-\"));\n const sarifPath = join(tmpDir, \"results.sarif\");\n\n try {\n // Build semgrep args\n const args = [\n \"scan\",\n \"--sarif\",\n \"--output\", sarifPath,\n \"--quiet\",\n \"--no-git-ignore\", // We handle .gitignore ourselves\n \"--timeout\", \"30\",\n \"--max-target-bytes\", \"1000000\",\n ];\n\n // Use auto config (community rules) + custom rules if available\n args.push(\"--config\", \"auto\");\n\n if (customRulesDir && existsSync(customRulesDir)) {\n args.push(\"--config\", customRulesDir);\n }\n\n args.push(directory);\n\n // Run semgrep\n await new Promise<void>((resolve, reject) => {\n const proc = execFile(\n \"semgrep\",\n args,\n { timeout: 120_000, maxBuffer: 10 * 1024 * 1024 },\n (error, _stdout, stderr) => {\n // Semgrep returns exit code 1 when findings exist — that's fine\n if (error && error.code !== 1) {\n reject(new Error(`Semgrep failed: ${stderr || error.message}`));\n } else {\n resolve();\n }\n },\n );\n });\n\n // Parse SARIF output\n if (!existsSync(sarifPath)) return { findings, available: true };\n\n const sarifContent = await readFile(sarifPath, \"utf-8\");\n const sarif: SemgrepSarifResult = JSON.parse(sarifContent);\n\n for (const run of sarif.runs ?? []) {\n for (const result of run.results ?? []) {\n const location = result.locations?.[0]?.physicalLocation;\n const filePath = location?.artifactLocation?.uri ?? \"unknown\";\n const line = location?.region?.startLine ?? 1;\n const snippet = location?.region?.snippet?.text ?? \"\";\n\n // Determine category from rule ID\n const ruleId = result.ruleId ?? \"semgrep\";\n const category = categorizeSemgrepRule(ruleId);\n\n findings.push({\n id: `SG-${filePath}:${line}:${ruleId}`,\n rule: ruleId,\n severity: semgrepSeverityToXploitscan(result.level ?? \"warning\"),\n title: truncate(result.message?.text ?? ruleId, 100),\n description: result.message?.text ?? \"\",\n file: filePath.replace(/^file:\\/\\//, \"\"),\n line,\n column: location?.region?.startColumn,\n snippet: formatSnippet(snippet, line),\n category,\n source: \"semgrep\",\n });\n }\n }\n } finally {\n // Clean up temp directory\n await rm(tmpDir, { recursive: true, force: true }).catch(() => {});\n }\n\n return { findings, available: true };\n}\n\nfunction categorizeSemgrepRule(ruleId: string): string {\n const id = ruleId.toLowerCase();\n if (id.includes(\"sql\") || id.includes(\"injection\") || id.includes(\"xss\") || id.includes(\"command\")) return \"Injection\";\n if (id.includes(\"auth\") || id.includes(\"session\")) return \"Authentication\";\n if (id.includes(\"crypto\") || id.includes(\"hash\") || id.includes(\"random\")) return \"Cryptography\";\n if (id.includes(\"cors\") || id.includes(\"header\") || id.includes(\"config\")) return \"Configuration\";\n if (id.includes(\"secret\") || id.includes(\"key\") || id.includes(\"password\") || id.includes(\"credential\")) return \"Secrets\";\n if (id.includes(\"path\") || id.includes(\"traversal\") || id.includes(\"file\")) return \"Path Traversal\";\n if (id.includes(\"deserial\")) return \"Deserialization\";\n return \"Security\";\n}\n\nfunction formatSnippet(text: string, line: number): string {\n if (!text) return \"\";\n const lines = text.split(\"\\n\");\n return lines\n .map((l, i) => {\n const num = line + i;\n return ` ${num.toString().padStart(4)} | ${l}`;\n })\n .join(\"\\n\");\n}\n\nfunction truncate(str: string, max: number): string {\n return str.length > max ? str.substring(0, max - 3) + \"...\" : str;\n}\n","import { execFile } from \"node:child_process\";\nimport { readFile, mkdtemp, rm } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { tmpdir } from \"node:os\";\nimport type { Finding, Severity } from \"../types.js\";\nimport { getSnippet, readFileContents } from \"../utils/files.js\";\n\ninterface GitleaksResult {\n Description: string;\n File: string;\n StartLine: number;\n EndLine: number;\n StartColumn: number;\n EndColumn: number;\n Match: string;\n Secret: string;\n RuleID: string;\n Entropy: number;\n Tags?: string[];\n}\n\nconst RULE_SEVERITY: Record<string, Severity> = {\n \"aws-access-token\": \"critical\",\n \"aws-secret-access-key\": \"critical\",\n \"stripe-access-token\": \"critical\",\n \"github-pat\": \"critical\",\n \"private-key\": \"critical\",\n \"generic-api-key\": \"high\",\n \"slack-webhook\": \"high\",\n \"twilio-api-key\": \"high\",\n \"sendgrid-api-key\": \"high\",\n \"shopify-access-token\": \"high\",\n \"gcp-api-key\": \"critical\",\n \"heroku-api-key\": \"high\",\n \"npm-access-token\": \"critical\",\n \"pypi-upload-token\": \"critical\",\n \"telegram-bot-api-token\": \"high\",\n \"discord-bot-token\": \"high\",\n \"firebase-api-key\": \"high\",\n};\n\nasync function isGitleaksInstalled(): Promise<boolean> {\n return new Promise((resolve) => {\n execFile(\"gitleaks\", [\"version\"], (error) => {\n resolve(!error);\n });\n });\n}\n\nexport async function runGitleaks(\n directory: string,\n): Promise<{ findings: Finding[]; available: boolean }> {\n const installed = await isGitleaksInstalled();\n if (!installed) {\n return { findings: [], available: false };\n }\n\n const findings: Finding[] = [];\n const tmpDir = await mkdtemp(join(tmpdir(), \"xploitscan-gitleaks-\"));\n const reportPath = join(tmpDir, \"results.json\");\n\n try {\n const args = [\n \"detect\",\n \"--source\", directory,\n \"--report-path\", reportPath,\n \"--report-format\", \"json\",\n \"--no-git\",\n \"--exit-code\", \"0\", // Don't fail on findings\n ];\n\n await new Promise<void>((resolve, reject) => {\n execFile(\n \"gitleaks\",\n args,\n { timeout: 60_000, maxBuffer: 10 * 1024 * 1024 },\n (error, _stdout, stderr) => {\n if (error) {\n const sanitizedError = (stderr || error.message).slice(0, 200);\n reject(new Error(`Gitleaks failed: ${sanitizedError}`));\n } else {\n resolve();\n }\n },\n );\n });\n\n // Parse results\n if (!existsSync(reportPath)) return { findings, available: true };\n\n const reportContent = await readFile(reportPath, \"utf-8\");\n if (!reportContent.trim()) return { findings, available: true };\n\n const results: GitleaksResult[] = JSON.parse(reportContent);\n\n for (const result of results) {\n const filePath = result.File;\n const line = result.StartLine + 1; // Gitleaks uses 0-based lines\n const severity = RULE_SEVERITY[result.RuleID] ?? \"high\";\n\n // Read file content for snippet\n const content = readFileContents(directory, filePath);\n const snippet = content ? getSnippet(content, line) : ` ${result.Match}`;\n\n // Redact the actual secret in the description\n const redactedSecret = result.Secret.length > 8\n ? result.Secret.substring(0, 4) + \"...\" + result.Secret.substring(result.Secret.length - 4)\n : \"****\";\n\n findings.push({\n id: `GL-${filePath}:${line}:${result.RuleID}`,\n rule: `GL:${result.RuleID}`,\n severity,\n title: `${result.Description} (detected by Gitleaks)`,\n description: `A secret matching \"${result.RuleID}\" pattern was found: ${redactedSecret}. If this is a real credential, it may already be compromised. Rotate it immediately and move it to environment variables.`,\n file: filePath,\n line,\n column: result.StartColumn,\n snippet,\n fix: `1. Rotate this credential immediately (it may be in git history)\\n2. Move it to a .env file: ${result.RuleID.toUpperCase().replace(/-/g, \"_\")}=<new-value>\\n3. Add .env to .gitignore\\n4. Remove from git history: git filter-branch or BFG Repo Cleaner`,\n category: \"Secrets\",\n source: \"gitleaks\",\n });\n }\n } finally {\n await rm(tmpDir, { recursive: true, force: true }).catch(() => {});\n }\n\n return { findings, available: true };\n}\n","import Anthropic from \"@anthropic-ai/sdk\";\nimport type { Finding, Severity } from \"../types.js\";\n\nconst SYSTEM_PROMPT = `You are a security auditor specializing in code generated by AI tools (Cursor, Lovable, Bolt, Replit, Claude). Your audience is non-expert developers who may not understand security jargon.\n\nAnalyze the provided code for security vulnerabilities. Focus on issues that AI code generators commonly introduce:\n- Missing authentication/authorization\n- Exposed secrets or credentials\n- SQL injection, XSS, and other injection flaws\n- Insecure direct object references (IDOR)\n- Missing input validation\n- Insecure defaults (permissive CORS, no rate limiting)\n- Client-side security checks without server-side enforcement\n- Supabase RLS misconfigurations\n- Unprotected payment/webhook endpoints\n\nFor each vulnerability found, respond with a JSON array of objects:\n{\n \"title\": \"Short, clear title\",\n \"severity\": \"critical|high|medium|low\",\n \"line\": <approximate line number>,\n \"description\": \"Plain-English explanation a non-developer can understand. What's the risk? What could an attacker do?\",\n \"fix\": \"Step-by-step fix instructions with code example if helpful\",\n \"category\": \"Secrets|Authentication|Authorization|Injection|Configuration|Payment Security|Data Exposure\"\n}\n\nIf no vulnerabilities are found, return an empty array: []\n\nRules:\n- Only report real, exploitable issues — no theoretical concerns\n- Be specific about what an attacker could do\n- Explain fixes in beginner-friendly language\n- Reference specific line numbers`;\n\nexport async function analyzeWithAI(\n files: { path: string; content: string }[],\n existingFindings: Finding[],\n): Promise<Finding[]> {\n const apiKey = process.env.ANTHROPIC_API_KEY;\n if (!apiKey) return [];\n\n const client = new Anthropic();\n\n // Build context: send files with existing findings for dedup\n const existingRules = new Set(\n existingFindings.map((f) => `${f.file}:${f.line}:${f.rule}`),\n );\n\n // Batch files into chunks to stay within token limits (~50KB per chunk)\n const chunks = chunkFiles(files, 50_000);\n const allFindings: Finding[] = [];\n\n for (const chunk of chunks) {\n const fileContext = chunk\n .map((f) => `--- ${f.path} ---\\n${f.content}`)\n .join(\"\\n\\n\");\n\n const existingNote = existingFindings.length > 0\n ? `\\n\\nThe following issues have already been found by static rules (do NOT duplicate these):\\n${existingFindings.map((f) => `- ${f.file}:${f.line} — ${f.title}`).join(\"\\n\")}`\n : \"\";\n\n try {\n const response = await client.messages.create({\n model: \"claude-sonnet-4-5-20250514\",\n max_tokens: 4096,\n messages: [\n {\n role: \"user\",\n content: `Analyze these source files for security vulnerabilities:${existingNote}\\n\\n${fileContext}`,\n },\n ],\n system: SYSTEM_PROMPT,\n });\n\n const text = response.content\n .filter((block): block is Anthropic.TextBlock => block.type === \"text\")\n .map((block) => block.text)\n .join(\"\");\n\n // Extract JSON from response (handles markdown code blocks)\n const jsonMatch = text.match(/\\[[\\s\\S]*\\]/);\n if (!jsonMatch) continue;\n\n const parsed = JSON.parse(jsonMatch[0]) as Array<{\n title: string;\n severity: Severity;\n line: number;\n description: string;\n fix: string;\n category: string;\n }>;\n\n for (const item of parsed) {\n // Find which file this finding belongs to\n const matchFile = chunk.find((f) => {\n const lines = f.content.split(\"\\n\");\n return item.line <= lines.length;\n });\n const file = matchFile?.path ?? chunk[0].path;\n\n const key = `${file}:${item.line}:AI`;\n if (existingRules.has(key)) continue;\n\n const content = chunk.find((f) => f.path === file)?.content ?? \"\";\n const lines = content.split(\"\\n\");\n const snippetStart = Math.max(0, item.line - 3);\n const snippetEnd = Math.min(lines.length, item.line + 2);\n const snippet = lines\n .slice(snippetStart, snippetEnd)\n .map((l, i) => {\n const num = snippetStart + i + 1;\n const marker = num === item.line ? \">\" : \" \";\n return `${marker} ${num.toString().padStart(4)} | ${l}`;\n })\n .join(\"\\n\");\n\n allFindings.push({\n id: `AI-${file}:${item.line}`,\n rule: \"AI\",\n severity: item.severity,\n title: item.title,\n description: item.description,\n file,\n line: item.line,\n snippet,\n fix: item.fix,\n category: item.category,\n source: \"ai\",\n });\n }\n } catch (error) {\n // AI analysis failed for this chunk — continue with others\n if (error instanceof Error && error.message.includes(\"API key\")) {\n throw new Error(\n \"Invalid ANTHROPIC_API_KEY. Get one at https://console.anthropic.com/\",\n );\n }\n }\n }\n\n return allFindings;\n}\n\nfunction chunkFiles(\n files: { path: string; content: string }[],\n maxChars: number,\n): { path: string; content: string }[][] {\n const chunks: { path: string; content: string }[][] = [];\n let current: { path: string; content: string }[] = [];\n let currentSize = 0;\n\n for (const file of files) {\n // Truncate individual files that exceed the chunk limit\n const truncatedContent = file.content.length > maxChars\n ? file.content.slice(0, maxChars) + \"\\n// ... truncated for analysis\"\n : file.content;\n const entry = { path: file.path, content: truncatedContent };\n\n if (currentSize + entry.content.length > maxChars && current.length > 0) {\n chunks.push(current);\n current = [];\n currentSize = 0;\n }\n current.push(entry);\n currentSize += entry.content.length;\n }\n\n if (current.length > 0) chunks.push(current);\n return chunks;\n}\n","import Anthropic from \"@anthropic-ai/sdk\";\nimport type { Finding } from \"../types.js\";\n\n/**\n * AI-powered false positive filter.\n *\n * Takes findings from the regex/entropy scanners and asks Claude Haiku\n * to classify each one as \"real\" or \"false positive\" based on the\n * surrounding code context. Findings marked FP are removed before the\n * user sees them, but preserved in filteredFindings so users can\n * review the AI's decisions.\n */\n\nconst REVIEW_SYSTEM_PROMPT = `You are reviewing security scan findings for false positives. Your job is to look at each finding and the surrounding code to determine if it's a REAL security vulnerability or a FALSE POSITIVE.\n\nCommon false positive patterns you should catch:\n- Auth check exists inside the function body (requireUser, requireUserForApi, getSession, etc.) but the scanner only checked the function signature\n- The flagged pattern is in example/documentation/tutorial code, not production code\n- The variable is developer-controlled (constants, config values, static strings), not user input\n- The flagged function is a database method (conn.exec, db.exec, prisma.$executeRaw) not a shell command (child_process.exec)\n- The comparison is a type check (typeof x === \"string\") not a secret comparison\n- The file is a test, mock, or fixture file\n- The \"secret\" is a publishable/public key (pk_test_, NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY) designed to be client-side\n- The innerHTML/dangerouslySetInnerHTML uses a static constant or JSON.stringify, not user input\n- The redirect URL has already been validated (isAllowedRedirect, validateRedirect)\n- The webhook endpoint is for a non-Stripe service but flagged as \"Stripe webhook\"\n- The \"sensitive data in URL\" is in a comment or documentation, not actual code\n- Package-lock.json URLs flagged as secrets (they're npm registry URLs, not secrets)\n\nFor each finding, respond ONLY with a JSON array. No other text.\nEach element: {\"index\": <number>, \"verdict\": \"real\" or \"fp\", \"reason\": \"<1 sentence>\"}`;\n\nconst MAX_FINDINGS_PER_BATCH = 15;\nconst MAX_CONTEXT_LINES = 10;\nconst MAX_TOTAL_FINDINGS = 50;\n\ninterface ReviewResult {\n index: number;\n verdict: \"real\" | \"fp\";\n reason: string;\n}\n\nexport interface FilteredFinding {\n finding: Finding;\n reason: string;\n}\n\nexport interface AIFilterResult {\n findings: Finding[];\n filteredFindings: FilteredFinding[];\n aiReviewed: boolean;\n removedCount: number;\n totalBefore: number;\n}\n\nfunction getExpandedContext(content: string, line: number, contextLines: number = MAX_CONTEXT_LINES): string {\n const lines = content.split(\"\\n\");\n const start = Math.max(0, line - 1 - contextLines);\n const end = Math.min(lines.length, line + contextLines);\n return lines.slice(start, end).map((l, i) => {\n const lineNum = start + i + 1;\n const marker = lineNum === line ? \">>>\" : \" \";\n return `${marker} ${lineNum} | ${l}`;\n }).join(\"\\n\");\n}\n\nfunction buildReviewPrompt(findings: Finding[], fileContent: string): string {\n const parts: string[] = [];\n for (let i = 0; i < findings.length; i++) {\n const f = findings[i];\n const context = getExpandedContext(fileContent, f.line);\n parts.push(`--- Finding ${i} ---\nRule: ${f.rule} (${f.title})\nSeverity: ${f.severity}\nFile: ${f.file}\nLine: ${f.line}\nDescription: ${f.description}\nSuggested fix: ${f.fix || \"N/A\"}\n\nCode context:\n${context}\n`);\n }\n return `Review these ${findings.length} security scan findings. For each one, determine if it's a real vulnerability or a false positive based on the code context.\\n\\n${parts.join(\"\\n\")}`;\n}\n\nfunction parseReviewResponse(text: string): ReviewResult[] {\n try {\n const cleaned = text.replace(/```json\\n?/g, \"\").replace(/```\\n?/g, \"\").trim();\n const parsed = JSON.parse(cleaned);\n if (!Array.isArray(parsed)) return [];\n return parsed.filter(\n (r: unknown): r is ReviewResult =>\n typeof r === \"object\" && r !== null &&\n \"index\" in r && \"verdict\" in r &&\n ((r as ReviewResult).verdict === \"real\" || (r as ReviewResult).verdict === \"fp\")\n );\n } catch {\n return [];\n }\n}\n\n/**\n * Filter false positives from findings using Claude Haiku.\n *\n * Returns filtered findings, the removed findings with AI reasons,\n * and metadata about what the AI did.\n */\nexport async function filterFalsePositives(\n findings: Finding[],\n fileContents: Map<string, string>,\n): Promise<AIFilterResult> {\n const empty: AIFilterResult = { findings, filteredFindings: [], aiReviewed: false, removedCount: 0, totalBefore: findings.length };\n if (!process.env.ANTHROPIC_API_KEY) return empty;\n if (findings.length === 0) return empty;\n\n const toReview = findings.slice(0, MAX_TOTAL_FINDINGS);\n const overflow = findings.slice(MAX_TOTAL_FINDINGS);\n const totalBefore = findings.length;\n\n const byFile = new Map<string, Finding[]>();\n for (const f of toReview) {\n const group = byFile.get(f.file) || [];\n group.push(f);\n byFile.set(f.file, group);\n }\n\n let client: Anthropic;\n try {\n client = new Anthropic();\n } catch {\n return empty;\n }\n\n // Track which findings are FP and why\n const fpMap = new Map<number, string>(); // globalIndex → reason\n\n for (const [file, fileFindings] of byFile) {\n const content = fileContents.get(file);\n if (!content) continue;\n\n for (let i = 0; i < fileFindings.length; i += MAX_FINDINGS_PER_BATCH) {\n const batch = fileFindings.slice(i, i + MAX_FINDINGS_PER_BATCH);\n const prompt = buildReviewPrompt(batch, content);\n\n try {\n const response = await client.messages.create({\n model: \"claude-haiku-4-5-20251001\",\n max_tokens: 1024,\n system: REVIEW_SYSTEM_PROMPT,\n messages: [{ role: \"user\", content: prompt }],\n });\n\n const text = response.content\n .filter((b): b is Anthropic.TextBlock => b.type === \"text\")\n .map((b) => b.text)\n .join(\"\");\n\n const results = parseReviewResponse(text);\n\n for (const r of results) {\n if (r.verdict === \"fp\" && r.index >= 0 && r.index < batch.length) {\n const globalIndex = toReview.indexOf(batch[r.index]);\n if (globalIndex !== -1) {\n fpMap.set(globalIndex, r.reason);\n }\n }\n }\n } catch {\n continue;\n }\n }\n }\n\n const filtered = toReview.filter((_, i) => !fpMap.has(i));\n const filteredFindings: FilteredFinding[] = [];\n for (const [idx, reason] of fpMap) {\n filteredFindings.push({ finding: toReview[idx], reason });\n }\n\n return {\n findings: [...filtered, ...overflow],\n filteredFindings,\n aiReviewed: true,\n removedCount: fpMap.size,\n totalBefore,\n };\n}\n","/**\n * AST-based JavaScript/TypeScript analyzer\n * Uses regex-based pseudo-AST parsing (no external deps) to understand code structure\n * and reduce false positives from comments, strings, and non-executable code.\n */\n\nimport type { Finding, Severity } from \"../types.js\";\n\n// ────────────────────────────────────────────\n// Code Structure Detection\n// ────────────────────────────────────────────\n\ninterface CodeContext {\n isComment: boolean;\n isString: boolean;\n isImport: boolean;\n isTestFile: boolean;\n isTypeDefinition: boolean;\n functionScope: string | null;\n nearestAssignment: string | null;\n}\n\n/** Strip all comments from JS/TS/Python code to prevent false positives */\nexport function stripComments(content: string): string {\n let result = \"\";\n let i = 0;\n let inSingleQuote = false;\n let inDoubleQuote = false;\n let inTemplate = false;\n let inSingleLineComment = false;\n let inMultiLineComment = false;\n\n while (i < content.length) {\n const ch = content[i];\n const next = content[i + 1];\n\n if (inSingleLineComment) {\n if (ch === \"\\n\") {\n inSingleLineComment = false;\n result += ch;\n }\n i++;\n continue;\n }\n\n if (inMultiLineComment) {\n if (ch === \"*\" && next === \"/\") {\n inMultiLineComment = false;\n i += 2;\n } else {\n if (ch === \"\\n\") result += ch; // preserve line numbers\n i++;\n }\n continue;\n }\n\n if (inSingleQuote) {\n result += ch;\n if (ch === \"'\" && content[i - 1] !== \"\\\\\") inSingleQuote = false;\n i++;\n continue;\n }\n\n if (inDoubleQuote) {\n result += ch;\n if (ch === '\"' && content[i - 1] !== \"\\\\\") inDoubleQuote = false;\n i++;\n continue;\n }\n\n if (inTemplate) {\n result += ch;\n if (ch === \"`\" && content[i - 1] !== \"\\\\\") inTemplate = false;\n i++;\n continue;\n }\n\n // Check for comment starts\n if (ch === \"/\" && next === \"/\") {\n inSingleLineComment = true;\n i += 2;\n continue;\n }\n if (ch === \"/\" && next === \"*\") {\n inMultiLineComment = true;\n i += 2;\n continue;\n }\n // Python/YAML comments\n if (ch === \"#\" && !inSingleQuote && !inDoubleQuote) {\n inSingleLineComment = true;\n i++;\n continue;\n }\n\n // Check for string starts\n if (ch === \"'\") inSingleQuote = true;\n if (ch === '\"') inDoubleQuote = true;\n if (ch === \"`\") inTemplate = true;\n\n result += ch;\n i++;\n }\n\n return result;\n}\n\n/** Detect if a line index falls within a function body */\nexport function getFunctionScope(content: string, lineIndex: number): string | null {\n const lines = content.split(\"\\n\");\n // Walk backwards from the match line to find enclosing function\n for (let i = lineIndex; i >= 0; i--) {\n const line = lines[i];\n const funcMatch = line.match(/(?:function\\s+(\\w+)|(?:const|let|var)\\s+(\\w+)\\s*=\\s*(?:async\\s+)?(?:function|\\([^)]*\\)\\s*=>)|(\\w+)\\s*\\([^)]*\\)\\s*\\{)/);\n if (funcMatch) {\n return funcMatch[1] || funcMatch[2] || funcMatch[3] || \"anonymous\";\n }\n }\n return null;\n}\n\n/** Check if content between two positions has a validation/sanitization pattern */\nexport function hasValidationBetween(content: string, startLine: number, endLine: number): boolean {\n const lines = content.split(\"\\n\");\n const segment = lines.slice(startLine, endLine + 1).join(\"\\n\");\n return /(?:validate|sanitize|escape|check|verify|assert|ensure|guard|protect|filter|whitelist|allowlist|isValid|isAllowed|isSafe)/i.test(segment);\n}\n\n/** Detect data flow: does user input reach a dangerous sink? */\nexport function tracesUserInput(content: string, sinkLine: number, linesBack: number = 15): boolean {\n const lines = content.split(\"\\n\");\n const start = Math.max(0, sinkLine - linesBack);\n const segment = lines.slice(start, sinkLine + 1).join(\"\\n\");\n\n const userInputSources = /(?:req\\.(?:body|query|params|headers)|body\\.|input\\.|params\\.|args\\.|request\\.|formData|searchParams|useSearchParams|URLSearchParams)/i;\n return userInputSources.test(segment);\n}\n\n/** Extract all import/require statements to understand dependencies */\nexport function extractImports(content: string): string[] {\n const imports: string[] = [];\n const patterns = [\n /import\\s+.*?from\\s+[\"'`]([^\"'`]+)[\"'`]/g,\n /require\\s*\\(\\s*[\"'`]([^\"'`]+)[\"'`]\\s*\\)/g,\n ];\n for (const p of patterns) {\n let m;\n while ((m = p.exec(content)) !== null) {\n imports.push(m[1]);\n }\n }\n return imports;\n}\n\n/** Check if a file imports/uses a specific security middleware */\nexport function hasSecurityMiddleware(content: string): { auth: boolean; rateLimit: boolean; helmet: boolean; cors: boolean; csrf: boolean } {\n return {\n auth: /(?:requireAuth|isAuthenticated|authenticate|passport|jwt\\.verify|clerk|auth0|nextauth|session\\.user|getServerSession|getAuth|withAuth|authMiddleware)/i.test(content),\n rateLimit: /(?:rateLimit|rate-limit|express-rate-limit|throttle|slowDown|limiter)/i.test(content),\n helmet: /(?:helmet|security-headers|securityHeaders)/i.test(content),\n cors: /(?:cors\\(|corsMiddleware|allowedOrigins)/i.test(content),\n csrf: /(?:csrf|csurf|csrfToken|_csrf)/i.test(content),\n };\n}\n\n// ────────────────────────────────────────────\n// AST-Enhanced Rule Runner\n// ────────────────────────────────────────────\n\nexport interface ASTContext {\n strippedContent: string; // Code with comments removed\n imports: string[]; // All imports/requires\n middleware: ReturnType<typeof hasSecurityMiddleware>;\n isTestFile: boolean;\n isConfigFile: boolean;\n isScannerFile: boolean; // Self-detection: is this a security scanner?\n}\n\nexport function buildASTContext(content: string, filePath: string): ASTContext {\n return {\n strippedContent: stripComments(content),\n imports: extractImports(content),\n middleware: hasSecurityMiddleware(content),\n isTestFile: /(?:\\.test\\.|\\.spec\\.|__tests__|__mocks__|\\.stories\\.|fixtures?\\/|mocks?\\/|\\.cy\\.|\\.e2e\\.)/i.test(filePath),\n isConfigFile: /(?:\\.config\\.|config\\/|\\.rc$|tsconfig|next\\.config|vite\\.config|webpack\\.config)/i.test(filePath),\n isScannerFile: /(?:custom-rules|scanner|security-check|eslint-plugin|lint)/i.test(filePath) &&\n /(?:findMatches|check\\s*\\(content|rule\\.check|severity|category)/i.test(content),\n };\n}\n","/**\n * OSV.dev client — queries the public Open Source Vulnerabilities database\n * for known CVEs affecting a list of dependencies. Augments the hand-curated\n * allowlist in dependency-scanner.ts with live data so we catch the long tail\n * of vulnerable packages we haven't manually added.\n *\n * API: https://google.github.io/osv.dev/post-v1-querybatch/\n * Batch endpoint accepts up to 1000 package queries per request so a typical\n * project's full dependency list resolves in one HTTP round trip.\n */\n\ninterface OsvQuery {\n package: {\n name: string;\n ecosystem: \"npm\" | \"PyPI\" | \"RubyGems\" | \"Go\" | \"crates.io\" | \"Maven\" | \"Packagist\" | \"NuGet\";\n };\n version: string;\n}\n\ninterface OsvVulnSummary {\n id: string; // e.g. GHSA-xxxx-xxxx-xxxx or CVE-2023-12345\n summary?: string;\n details?: string;\n aliases?: string[];\n severity?: Array<{ type: string; score: string }>;\n}\n\ninterface OsvBatchResponse {\n results: Array<{\n vulns?: Array<{ id: string }>;\n }>;\n}\n\nexport interface OsvMatch {\n ecosystem: string;\n name: string;\n version: string;\n vulns: OsvVulnSummary[];\n}\n\nconst OSV_BATCH_URL = \"https://api.osv.dev/v1/querybatch\";\nconst OSV_VULN_URL = \"https://api.osv.dev/v1/vulns\";\nconst NETWORK_TIMEOUT_MS = 5000;\n\nasync function fetchWithTimeout(url: string, init: RequestInit, timeoutMs: number): Promise<Response> {\n const controller = new AbortController();\n const id = setTimeout(() => controller.abort(), timeoutMs);\n try {\n return await fetch(url, { ...init, signal: controller.signal });\n } finally {\n clearTimeout(id);\n }\n}\n\n/**\n * Query OSV.dev for every given (ecosystem, name, version) tuple and return\n * any vulnerabilities found. Fails soft on network error / timeout so offline\n * scans still work — the caller just gets an empty array and continues.\n *\n * Rate limit aware: the batch endpoint lets us send the whole dependency\n * list in one request, then we only fetch details for packages that\n * actually matched (avoiding the N+1 lookup pattern).\n */\nexport async function queryOsvBatch(queries: OsvQuery[]): Promise<OsvMatch[]> {\n if (queries.length === 0) return [];\n try {\n // Step 1: batch query returns only vuln IDs per package\n const batchRes = await fetchWithTimeout(\n OSV_BATCH_URL,\n {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ queries }),\n },\n NETWORK_TIMEOUT_MS,\n );\n if (!batchRes.ok) return [];\n const batch = (await batchRes.json()) as OsvBatchResponse;\n if (!Array.isArray(batch.results)) return [];\n\n // Collect unique vuln IDs across all matched packages so we only fetch\n // each one once (many packages can share a CVE)\n const uniqueVulnIds = new Set<string>();\n for (const result of batch.results) {\n for (const v of result.vulns ?? []) uniqueVulnIds.add(v.id);\n }\n\n // Step 2: fetch full details for each unique vuln (sequential with\n // per-request timeout — parallelizing would hammer their rate limiter)\n const vulnDetails = new Map<string, OsvVulnSummary>();\n for (const vulnId of uniqueVulnIds) {\n try {\n const detailRes = await fetchWithTimeout(\n `${OSV_VULN_URL}/${encodeURIComponent(vulnId)}`,\n { method: \"GET\" },\n NETWORK_TIMEOUT_MS,\n );\n if (!detailRes.ok) continue;\n const detail = (await detailRes.json()) as OsvVulnSummary;\n vulnDetails.set(vulnId, detail);\n } catch {\n // Skip individual vuln detail failures\n }\n }\n\n // Step 3: zip results back into the caller's query order\n const matches: OsvMatch[] = [];\n batch.results.forEach((result, i) => {\n const ids = (result.vulns ?? []).map((v) => v.id);\n if (ids.length === 0) return;\n const q = queries[i];\n matches.push({\n ecosystem: q.package.ecosystem,\n name: q.package.name,\n version: q.version,\n vulns: ids.map((id) => vulnDetails.get(id) ?? { id }),\n });\n });\n return matches;\n } catch {\n // Network error, timeout, DNS failure — fail soft so scans still work\n // offline. The manual allowlist in dependency-scanner.ts is the fallback.\n return [];\n }\n}\n\n/**\n * Classify an OSV vuln summary into a Finding severity. OSV ships several\n * severity vectors (CVSS v3, CVSS v4, ecosystem-specific) and most entries\n * have at least one. Falls back to 'medium' when unclassified.\n */\nexport function osvSeverity(vuln: OsvVulnSummary): \"critical\" | \"high\" | \"medium\" | \"low\" {\n const vectors = vuln.severity ?? [];\n for (const v of vectors) {\n // CVSS scores are embedded in a vector string — easiest to yank the first\n // numeric \"base score\" value out of it\n const match = v.score.match(/\\b(\\d+(?:\\.\\d+)?)\\b/);\n if (!match) continue;\n const score = parseFloat(match[1]);\n if (Number.isFinite(score)) {\n if (score >= 9) return \"critical\";\n if (score >= 7) return \"high\";\n if (score >= 4) return \"medium\";\n return \"low\";\n }\n }\n return \"medium\";\n}\n","/**\n * Dependency Vulnerability Scanner\n * Parses package.json, requirements.txt, Gemfile, etc. and checks\n * for known vulnerable packages and risky dependency patterns.\n */\n\nimport type { Finding } from \"../types.js\";\nimport { queryOsvBatch, osvSeverity } from \"./osv.js\";\n\n// Known vulnerable packages and their details\n// Format: package -> { minSafeVersion, cve, severity, description }\ninterface VulnEntry {\n minSafe: string;\n cve: string;\n severity: \"critical\" | \"high\" | \"medium\" | \"low\";\n title: string;\n description: string;\n fix: string;\n}\n\n// Well-known vulnerable npm packages (manually curated, high-signal)\nconst KNOWN_VULN_NPM: Record<string, VulnEntry[]> = {\n \"lodash\": [{\n minSafe: \"4.17.21\", cve: \"CVE-2021-23337\", severity: \"critical\",\n title: \"Lodash Prototype Pollution\",\n description: \"lodash before 4.17.21 is vulnerable to prototype pollution via set, setWith, and zipObjectDeep.\",\n fix: \"Upgrade lodash to >= 4.17.21: npm install lodash@latest\"\n }],\n \"axios\": [{\n minSafe: \"1.6.0\", cve: \"CVE-2023-45857\", severity: \"high\",\n title: \"Axios CSRF Vulnerability\",\n description: \"axios before 1.6.0 inadvertently leaks XSRF-TOKEN cookie in cross-site requests.\",\n fix: \"Upgrade axios to >= 1.6.0: npm install axios@latest\"\n }],\n \"express\": [{\n minSafe: \"4.19.2\", cve: \"CVE-2024-29041\", severity: \"medium\",\n title: \"Express Open Redirect\",\n description: \"Express before 4.19.2 is vulnerable to open redirect via crafted URLs.\",\n fix: \"Upgrade express to >= 4.19.2: npm install express@latest\"\n }],\n \"jsonwebtoken\": [{\n minSafe: \"9.0.0\", cve: \"CVE-2022-23529\", severity: \"high\",\n title: \"jsonwebtoken Insecure Key Handling\",\n description: \"jsonwebtoken before 9.0.0 allows attackers to set secretOrPublicKey to a malicious object.\",\n fix: \"Upgrade jsonwebtoken to >= 9.0.0: npm install jsonwebtoken@latest\"\n }],\n \"node-fetch\": [{\n minSafe: \"2.6.7\", cve: \"CVE-2022-0235\", severity: \"high\",\n title: \"node-fetch Header Exposure\",\n description: \"node-fetch before 2.6.7 exposes authorization headers on redirect to different origin.\",\n fix: \"Upgrade node-fetch to >= 2.6.7 or switch to native fetch (Node 18+).\"\n }],\n \"minimatch\": [{\n minSafe: \"3.0.5\", cve: \"CVE-2022-3517\", severity: \"high\",\n title: \"Minimatch ReDoS\",\n description: \"minimatch before 3.0.5 is vulnerable to Regular Expression Denial of Service.\",\n fix: \"Upgrade minimatch to >= 3.0.5: npm install minimatch@latest\"\n }],\n \"moment\": [{\n minSafe: \"999.0.0\", cve: \"N/A\", severity: \"medium\",\n title: \"Moment.js is Deprecated\",\n description: \"moment.js is in maintenance mode with known path traversal issues. Consider alternatives.\",\n fix: \"Migrate to date-fns, dayjs, or Temporal API. See: https://momentjs.com/docs/#/-project-status/\"\n }],\n \"request\": [{\n minSafe: \"999.0.0\", cve: \"N/A\", severity: \"medium\",\n title: \"Request Package Deprecated\",\n description: \"The 'request' package is deprecated and no longer receives security updates.\",\n fix: \"Migrate to node-fetch, axios, got, or native fetch (Node 18+).\"\n }],\n \"underscore\": [{\n minSafe: \"1.13.6\", cve: \"CVE-2021-23358\", severity: \"high\",\n title: \"Underscore.js Arbitrary Code Execution\",\n description: \"underscore before 1.13.6 allows arbitrary code execution via the template function.\",\n fix: \"Upgrade underscore to >= 1.13.6 or migrate to lodash.\"\n }],\n \"tar\": [{\n minSafe: \"6.2.1\", cve: \"CVE-2024-28863\", severity: \"high\",\n title: \"Tar Path Traversal\",\n description: \"tar before 6.2.1 is vulnerable to denial of service via crafted archives.\",\n fix: \"Upgrade tar to >= 6.2.1: npm install tar@latest\"\n }],\n \"semver\": [{\n minSafe: \"7.5.2\", cve: \"CVE-2022-25883\", severity: \"medium\",\n title: \"Semver ReDoS\",\n description: \"semver before 7.5.2 is vulnerable to Regular Expression Denial of Service.\",\n fix: \"Upgrade semver to >= 7.5.2: npm install semver@latest\"\n }],\n \"xml2js\": [{\n minSafe: \"0.5.0\", cve: \"CVE-2023-0842\", severity: \"medium\",\n title: \"xml2js Prototype Pollution\",\n description: \"xml2js before 0.5.0 is vulnerable to prototype pollution when parsing XML.\",\n fix: \"Upgrade xml2js to >= 0.5.0: npm install xml2js@latest\"\n }],\n};\n\n// Known vulnerable Python packages\nconst KNOWN_VULN_PYTHON: Record<string, VulnEntry[]> = {\n \"django\": [{\n minSafe: \"4.2.11\", cve: \"CVE-2024-27351\", severity: \"high\",\n title: \"Django ReDoS in Truncator\",\n description: \"Django before 4.2.11 is vulnerable to Regular Expression Denial of Service.\",\n fix: \"Upgrade Django to >= 4.2.11: pip install Django --upgrade\"\n }],\n \"flask\": [{\n minSafe: \"2.3.2\", cve: \"CVE-2023-30861\", severity: \"high\",\n title: \"Flask Session Cookie Vulnerability\",\n description: \"Flask before 2.3.2 may set permanent session cookies on redirect responses.\",\n fix: \"Upgrade Flask to >= 2.3.2: pip install Flask --upgrade\"\n }],\n \"pillow\": [{\n minSafe: \"10.2.0\", cve: \"CVE-2023-50447\", severity: \"critical\",\n title: \"Pillow Arbitrary Code Execution\",\n description: \"Pillow before 10.2.0 is vulnerable to arbitrary code execution via crafted images.\",\n fix: \"Upgrade Pillow to >= 10.2.0: pip install Pillow --upgrade\"\n }],\n \"requests\": [{\n minSafe: \"2.31.0\", cve: \"CVE-2023-32681\", severity: \"medium\",\n title: \"Requests Proxy Header Leak\",\n description: \"requests before 2.31.0 leaks Proxy-Authorization headers to destination servers.\",\n fix: \"Upgrade requests to >= 2.31.0: pip install requests --upgrade\"\n }],\n \"pyyaml\": [{\n minSafe: \"6.0.1\", cve: \"CVE-2022-41316\", severity: \"high\",\n title: \"PyYAML Unsafe Loading\",\n description: \"PyYAML has known deserialization vulnerabilities. Always use yaml.safe_load().\",\n fix: \"Upgrade PyYAML to >= 6.0.1 and use yaml.safe_load() instead of yaml.load().\"\n }],\n \"jinja2\": [{\n minSafe: \"3.1.3\", cve: \"CVE-2024-22195\", severity: \"medium\",\n title: \"Jinja2 XSS Vulnerability\",\n description: \"Jinja2 before 3.1.3 is vulnerable to XSS via xmlattr filter.\",\n fix: \"Upgrade Jinja2 to >= 3.1.3: pip install Jinja2 --upgrade\"\n }],\n};\n\n// Risky patterns in dependency configs\ninterface DepPattern {\n id: string;\n title: string;\n severity: \"critical\" | \"high\" | \"medium\" | \"low\";\n description: string;\n fix: string;\n test: (content: string, filePath: string) => boolean;\n}\n\nconst DEP_PATTERNS: DepPattern[] = [\n {\n id: \"DEP001\", title: \"Wildcard Dependency Version\", severity: \"high\",\n description: \"Using '*' or 'latest' as a dependency version allows any version to be installed, including malicious ones.\",\n fix: \"Pin dependency versions: use exact (1.2.3) or caret (^1.2.3) versions, never '*' or 'latest'.\",\n test: (content, filePath) => filePath.endsWith(\"package.json\") && /[\"']\\*[\"']|[\"']latest[\"']/.test(content),\n },\n {\n id: \"DEP002\", title: \"Git Dependency Without Commit Hash\", severity: \"medium\",\n description: \"Git dependencies without a pinned commit hash can be replaced with malicious code.\",\n fix: \"Pin git dependencies to a specific commit: git+https://github.com/user/repo#commit-hash\",\n test: (content, filePath) => filePath.endsWith(\"package.json\") && /git\\+https?:\\/\\/[^#\"]+[\"']/.test(content),\n },\n {\n id: \"DEP003\", title: \"Postinstall Script in Package\", severity: \"medium\",\n description: \"postinstall scripts run automatically after npm install and can execute arbitrary code.\",\n fix: \"Audit postinstall scripts carefully. Use --ignore-scripts flag for untrusted packages.\",\n test: (content, filePath) => filePath.endsWith(\"package.json\") && /\"postinstall\"\\s*:/.test(content),\n },\n {\n id: \"DEP004\", title: \"No Package Lock File\", severity: \"medium\",\n description: \"Without a lockfile, npm install may resolve different versions on different machines.\",\n fix: \"Commit your lockfile (package-lock.json, pnpm-lock.yaml, or yarn.lock).\",\n test: (content, filePath) => filePath.endsWith(\".gitignore\") && /package-lock\\.json|pnpm-lock\\.yaml|yarn\\.lock/.test(content),\n },\n {\n id: \"DEP005\", title: \"HTTP Registry URL\", severity: \"high\",\n description: \"Using HTTP (not HTTPS) for npm registry allows man-in-the-middle attacks on package downloads.\",\n fix: \"Use HTTPS for registry: registry=https://registry.npmjs.org/\",\n test: (content, filePath) => filePath.endsWith(\".npmrc\") && /registry\\s*=\\s*http:\\/\\//.test(content),\n },\n];\n\nfunction compareVersions(v1: string, v2: string): number {\n const parts1 = v1.replace(/^[^0-9]*/, \"\").split(\".\").map(Number);\n const parts2 = v2.replace(/^[^0-9]*/, \"\").split(\".\").map(Number);\n for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {\n const a = parts1[i] || 0;\n const b = parts2[i] || 0;\n if (a < b) return -1;\n if (a > b) return 1;\n }\n return 0;\n}\n\nfunction getSnippet(content: string, line: number): string {\n const lines = content.split(\"\\n\");\n const start = Math.max(0, line - 2);\n const end = Math.min(lines.length, line + 2);\n return lines.slice(start, end).map((l, i) => {\n const lineNum = start + i + 1;\n const prefix = lineNum === line ? \">\" : \" \";\n return `${prefix} ${String(lineNum).padStart(5)} | ${l}`;\n }).join(\"\\n\");\n}\n\nexport function scanDependencies(files: { path: string; content: string }[]): Finding[] {\n const findings: Finding[] = [];\n\n for (const { path: filePath, content } of files) {\n // npm package.json\n if (filePath.endsWith(\"package.json\") && !filePath.includes(\"node_modules\")) {\n try {\n const pkg = JSON.parse(content);\n const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };\n\n for (const [name, version] of Object.entries(allDeps)) {\n const vulns = KNOWN_VULN_NPM[name];\n if (!vulns) continue;\n\n const versionStr = String(version).replace(/^[\\^~>=<]*/g, \"\");\n for (const vuln of vulns) {\n if (compareVersions(versionStr, vuln.minSafe) < 0) {\n const line = content.split(\"\\n\").findIndex(l => l.includes(`\"${name}\"`)) + 1;\n findings.push({\n id: `${vuln.cve}-${filePath}:${line}`,\n rule: vuln.cve,\n severity: vuln.severity,\n title: vuln.title,\n description: vuln.description,\n file: filePath,\n line,\n snippet: getSnippet(content, line),\n fix: vuln.fix,\n category: \"Dependencies\",\n source: \"custom\",\n owasp: \"A06:2021\",\n cwe: \"CWE-1395\",\n });\n }\n }\n }\n } catch {\n // Invalid JSON — skip\n }\n }\n\n // Python requirements.txt\n if (filePath.match(/requirements.*\\.txt$/i)) {\n const lines = content.split(\"\\n\");\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i].trim();\n const match = line.match(/^([a-zA-Z0-9_-]+)(?:[=<>!~]+(.+))?/);\n if (!match) continue;\n\n const name = match[1].toLowerCase();\n const version = match[2] || \"0.0.0\";\n const vulns = KNOWN_VULN_PYTHON[name];\n if (!vulns) continue;\n\n for (const vuln of vulns) {\n if (compareVersions(version, vuln.minSafe) < 0) {\n findings.push({\n id: `${vuln.cve}-${filePath}:${i + 1}`,\n rule: vuln.cve,\n severity: vuln.severity,\n title: vuln.title,\n description: vuln.description,\n file: filePath,\n line: i + 1,\n snippet: getSnippet(content, i + 1),\n fix: vuln.fix,\n category: \"Dependencies\",\n source: \"custom\",\n owasp: \"A06:2021\",\n cwe: \"CWE-1395\",\n });\n }\n }\n }\n }\n\n // Dependency pattern checks\n for (const pattern of DEP_PATTERNS) {\n if (pattern.test(content, filePath)) {\n findings.push({\n id: `${pattern.id}-${filePath}:1`,\n rule: pattern.id,\n severity: pattern.severity,\n title: pattern.title,\n description: pattern.description,\n file: filePath,\n line: 1,\n snippet: getSnippet(content, 1),\n fix: pattern.fix,\n category: \"Dependencies\",\n source: \"custom\",\n owasp: \"A06:2021\",\n cwe: \"CWE-1395\",\n });\n }\n }\n }\n\n return findings;\n}\n\n/**\n * Async companion to scanDependencies() that augments the hand-curated\n * allowlist with a live OSV.dev lookup of every package version. Fails\n * soft on network error — offline scans still run the static checks.\n *\n * Returns ONLY the extra findings from OSV so callers can merge with the\n * sync results without double-counting:\n * const findings = [...scanDependencies(files), ...await scanDependenciesOsv(files)];\n *\n * Deduplication is by (package name + CVE id) so a package already caught\n * by the static allowlist doesn't show up twice.\n */\nexport async function scanDependenciesOsv(\n files: { path: string; content: string }[],\n alreadyFoundByRule: Set<string>,\n): Promise<Finding[]> {\n interface PendingLookup {\n ecosystem: \"npm\" | \"PyPI\" | \"RubyGems\" | \"Go\";\n name: string;\n version: string;\n file: string;\n line: number;\n content: string;\n }\n\n const lookups: PendingLookup[] = [];\n\n for (const { path: filePath, content } of files) {\n // npm package.json\n if (filePath.endsWith(\"package.json\") && !filePath.includes(\"node_modules\")) {\n try {\n const pkg = JSON.parse(content);\n const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };\n for (const [name, rawVersion] of Object.entries(allDeps)) {\n const version = String(rawVersion).replace(/^[\\^~>=<]*/g, \"\").trim();\n if (!version || version === \"*\" || version === \"latest\") continue;\n const line = content.split(\"\\n\").findIndex((l) => l.includes(`\"${name}\"`)) + 1;\n lookups.push({ ecosystem: \"npm\", name, version, file: filePath, line: line || 1, content });\n }\n } catch {\n /* invalid JSON */\n }\n }\n\n // Python requirements.txt (exact version pins only — ranges are tricky\n // without a resolver and we'd rather report nothing than guess wrong)\n if (filePath.match(/requirements.*\\.txt$/i)) {\n const lines = content.split(\"\\n\");\n for (let i = 0; i < lines.length; i++) {\n const m = lines[i].trim().match(/^([a-zA-Z0-9_-]+)==([\\d.]+)/);\n if (!m) continue;\n lookups.push({\n ecosystem: \"PyPI\",\n name: m[1].toLowerCase(),\n version: m[2],\n file: filePath,\n line: i + 1,\n content,\n });\n }\n }\n\n // Ruby Gemfile.lock (exact pins)\n if (filePath.endsWith(\"Gemfile.lock\")) {\n const lines = content.split(\"\\n\");\n for (let i = 0; i < lines.length; i++) {\n const m = lines[i].match(/^\\s{4}([a-z0-9_-]+)\\s+\\(([\\d.]+)\\)/);\n if (!m) continue;\n lookups.push({\n ecosystem: \"RubyGems\",\n name: m[1],\n version: m[2],\n file: filePath,\n line: i + 1,\n content,\n });\n }\n }\n }\n\n if (lookups.length === 0) return [];\n\n // Chunk into batches of 500 to stay well under OSV's 1000-query limit\n const CHUNK = 500;\n const findings: Finding[] = [];\n for (let start = 0; start < lookups.length; start += CHUNK) {\n const chunk = lookups.slice(start, start + CHUNK);\n const matches = await queryOsvBatch(\n chunk.map((l) => ({ package: { name: l.name, ecosystem: l.ecosystem }, version: l.version })),\n );\n // Zip matches back to their lookup rows by index within the chunk\n const matchByKey = new Map<string, (typeof matches)[0]>();\n for (const m of matches) matchByKey.set(`${m.ecosystem}:${m.name}:${m.version}`, m);\n\n for (const lookup of chunk) {\n const key = `${lookup.ecosystem}:${lookup.name}:${lookup.version}`;\n const match = matchByKey.get(key);\n if (!match) continue;\n for (const vuln of match.vulns) {\n // Skip if the static allowlist already reported this CVE for this package\n const dedupeKey = `${lookup.name}:${vuln.id}`;\n if (alreadyFoundByRule.has(dedupeKey)) continue;\n alreadyFoundByRule.add(dedupeKey);\n\n const severity = osvSeverity(vuln);\n const title = vuln.summary || `${lookup.name} vulnerability ${vuln.id}`;\n const description =\n vuln.details ||\n vuln.summary ||\n `${lookup.name}@${lookup.version} is affected by ${vuln.id}. See https://osv.dev/vulnerability/${vuln.id} for details.`;\n findings.push({\n id: `${vuln.id}-${lookup.file}:${lookup.line}`,\n rule: vuln.id,\n severity,\n title,\n description: description.slice(0, 500),\n file: lookup.file,\n line: lookup.line,\n snippet: getSnippet(lookup.content, lookup.line),\n fix: `Upgrade ${lookup.name} to a version that resolves ${vuln.id}. See https://osv.dev/vulnerability/${vuln.id}`,\n category: \"Dependencies\",\n source: \"custom\",\n owasp: \"A06:2021\",\n cwe: \"CWE-1395\",\n });\n }\n }\n }\n\n return findings;\n}\n","/**\n * Secret Entropy Detection Scanner\n * Detects high-entropy strings that are likely secrets/keys/tokens\n * regardless of their naming pattern. Uses Shannon entropy calculation.\n */\n\nimport type { Finding } from \"../types.js\";\n\n// ────────────────────────────────────────────\n// Shannon Entropy Calculation\n// ────────────────────────────────────────────\n\nfunction shannonEntropy(str: string): number {\n const freq: Record<string, number> = {};\n for (const ch of str) {\n freq[ch] = (freq[ch] || 0) + 1;\n }\n const len = str.length;\n let entropy = 0;\n for (const count of Object.values(freq)) {\n const p = count / len;\n entropy -= p * Math.log2(p);\n }\n return entropy;\n}\n\n// ────────────────────────────────────────────\n// Known Safe Patterns (allowlist)\n// ────────────────────────────────────────────\n\nconst SAFE_PATTERNS = [\n // UUIDs\n /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,\n // Git commit hashes\n /^[0-9a-f]{40}$/i,\n // Short git hashes\n /^[0-9a-f]{7,8}$/i,\n // Hex colors\n /^#?[0-9a-fA-F]{3,8}$/,\n // Base64 encoded small data (< 20 chars)\n /^[A-Za-z0-9+/]{1,19}={0,2}$/,\n // Package versions\n /^\\d+\\.\\d+\\.\\d+/,\n // File hashes (integrity)\n /^sha\\d+-/i,\n // URLs without credentials\n /^https?:\\/\\/[^:@]*$/,\n // Date/time strings\n /^\\d{4}-\\d{2}-\\d{2}/,\n // Locale strings\n /^[a-z]{2}-[A-Z]{2}$/,\n // Common encodings\n /^utf-?8|ascii|latin|iso-8859/i,\n // MIME types\n /^(?:application|text|image|audio|video)\\//,\n // CSS/HTML values\n /^(?:inherit|none|auto|block|flex|grid|absolute|relative|fixed|px|em|rem|%)/,\n // Common placeholder/test values\n /^(?:test|example|sample|demo|placeholder|temp|tmp|foo|bar|baz|lorem|ipsum)/i,\n // DOCTYPE/DTD URLs\n /DTD|DOCTYPE|w3\\.org|apple\\.com\\/DTDs/i,\n // XML namespaces\n /xmlns|schema|xsd|xsi/i,\n // npm/package registry URLs\n /^https?:\\/\\/registry\\.npmjs\\.org\\//,\n /^https?:\\/\\/registry\\.yarnpkg\\.com\\//,\n // Package integrity hashes (sha512-..., sha256-...)\n /^sha\\d+-[A-Za-z0-9+/=]+$/,\n // Resolved package URLs (.tgz)\n /\\.tgz$/,\n // npm resolved URLs (any registry URL with package tarball)\n /registry.*\\/-\\/.*\\.tgz$/,\n];\n\n// File types that shouldn't be scanned for entropy\nconst SKIP_FILES = /\\.(css|scss|less|svg|md|txt|html?|xml|yml|yaml|toml|lock|map|woff2?|ttf|eot|ico|png|jpg|gif|webp)$/i;\n\n// File names that should be completely excluded from entropy scanning\nconst SKIP_FILENAMES = /(?:package-lock\\.json|pnpm-lock\\.yaml|yarn\\.lock|composer\\.lock|Gemfile\\.lock|Cargo\\.lock|poetry\\.lock|Pipfile\\.lock|shrinkwrap\\.json)$/i;\n\n// Variable names that commonly hold non-secret long strings\nconst SAFE_VAR_NAMES = /(?:description|message|text|label|title|content|template|html|svg|css|style|class|query|mutation|schema|regex|pattern|format|placeholder|comment|url|path|route|endpoint|href|src|alt|name|type|version|encoding|charset)/i;\n\n// ────────────────────────────────────────────\n// Scanner\n// ────────────────────────────────────────────\n\ninterface EntropyMatch {\n value: string;\n line: number;\n entropy: number;\n context: string;\n}\n\nfunction getSnippet(content: string, line: number): string {\n const lines = content.split(\"\\n\");\n const start = Math.max(0, line - 2);\n const end = Math.min(lines.length, line + 2);\n return lines.slice(start, end).map((l, i) => {\n const lineNum = start + i + 1;\n const prefix = lineNum === line ? \">\" : \" \";\n return `${prefix} ${String(lineNum).padStart(5)} | ${l}`;\n }).join(\"\\n\");\n}\n\nexport function scanEntropy(files: { path: string; content: string }[]): Finding[] {\n const findings: Finding[] = [];\n\n for (const { path: filePath, content } of files) {\n // Skip non-source files\n if (SKIP_FILES.test(filePath)) continue;\n if (SKIP_FILENAMES.test(filePath)) continue;\n // Also check the basename in case the path has a prefix (e.g., zip extraction)\n const basename = filePath.split(\"/\").pop() || \"\";\n if (SKIP_FILENAMES.test(basename)) continue;\n if (filePath.includes(\"node_modules\")) continue;\n if (filePath.includes(\".min.\")) continue;\n // Skip compiled bundles and generated files\n if (/pro-rules-bundle|\\.bundle\\.|\\.chunk\\./i.test(filePath)) continue;\n if (/(?:\\.test\\.|\\.spec\\.|__tests__|__mocks__|fixtures?\\/)/i.test(filePath)) continue;\n\n const lines = content.split(\"\\n\");\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n\n // Skip comment lines\n const trimmed = line.trimStart();\n if (trimmed.startsWith(\"//\") || trimmed.startsWith(\"#\") || trimmed.startsWith(\"*\") || trimmed.startsWith(\"/*\")) continue;\n\n // Find string assignments (quotes or backticks)\n const stringPattern = /(?:[:=]\\s*)([\"'`])([^\"'`\\n]{20,120})\\1/g;\n let match;\n\n while ((match = stringPattern.exec(line)) !== null) {\n const value = match[2];\n\n // Skip known safe patterns\n if (SAFE_PATTERNS.some(p => p.test(value))) continue;\n\n // Skip if the variable name suggests non-secret content\n const beforeAssign = line.substring(0, match.index);\n if (SAFE_VAR_NAMES.test(beforeAssign)) continue;\n\n // Skip if it looks like a URL without credentials\n if (/^https?:\\/\\/[^:@]*$/.test(value)) continue;\n\n // Skip if it contains spaces (likely a sentence/message)\n if ((value.match(/\\s/g) || []).length > 2) continue;\n\n // Calculate entropy\n const entropy = shannonEntropy(value);\n\n // High entropy thresholds:\n // - Hex strings (charset 16): entropy > 3.0\n // - Base64 strings (charset 64): entropy > 4.5\n // - General strings: entropy > 4.0\n const isHex = /^[0-9a-fA-F]+$/.test(value);\n const isBase64 = /^[A-Za-z0-9+/]+=*$/.test(value);\n\n let threshold = 4.0;\n if (isHex) threshold = 3.0;\n else if (isBase64) threshold = 4.5;\n\n // Minimum length for detection\n if (value.length < 20) continue;\n\n if (entropy >= threshold) {\n // Additional context checks to reduce false positives\n const varName = beforeAssign.match(/(\\w+)\\s*[:=]\\s*$/)?.[1] || \"\";\n\n // Boost confidence if variable name suggests secret\n const isLikelySecret = /(?:key|secret|token|password|passwd|pwd|api_?key|auth|credential|private|signing)/i.test(varName);\n\n // Only report if entropy is very high OR variable name suggests secret\n if (entropy >= 4.5 || isLikelySecret) {\n // Mask the value for display\n const masked = value.substring(0, 6) + \"...\" + value.substring(value.length - 4);\n\n findings.push({\n id: `ENTROPY-${filePath}:${i + 1}`,\n rule: \"ENTROPY\",\n severity: isLikelySecret ? \"critical\" : \"high\",\n title: \"High-Entropy String Detected (Possible Secret)\",\n description: `Found a high-entropy string (${entropy.toFixed(1)} bits) that may be a hardcoded secret or API key: \"${masked}\"`,\n file: filePath,\n line: i + 1,\n snippet: getSnippet(content, i + 1),\n fix: \"If this is a secret, move it to an environment variable. If it's not a secret (e.g., hash, encoded data), add it to .xploitscanignore.\",\n category: \"Secrets\",\n source: \"custom\",\n owasp: \"A02:2021\",\n cwe: \"CWE-798\",\n });\n }\n }\n }\n }\n }\n\n return findings;\n}\n","/**\n * Configuration File Deep Analyzer\n * Analyzes tsconfig, next.config, Dockerfile, GitHub Actions,\n * and other config files for security misconfigurations.\n */\n\nimport type { Finding } from \"../types.js\";\n\nfunction getSnippet(content: string, line: number): string {\n const lines = content.split(\"\\n\");\n const start = Math.max(0, line - 2);\n const end = Math.min(lines.length, line + 2);\n return lines.slice(start, end).map((l, i) => {\n const lineNum = start + i + 1;\n const prefix = lineNum === line ? \">\" : \" \";\n return `${prefix} ${String(lineNum).padStart(5)} | ${l}`;\n }).join(\"\\n\");\n}\n\ninterface ConfigCheck {\n id: string;\n title: string;\n severity: \"critical\" | \"high\" | \"medium\" | \"low\";\n category: string;\n description: string;\n fix: string;\n owasp: string;\n cwe: string;\n filePattern: RegExp;\n check: (content: string, filePath: string) => { line: number; snippet: string } | null;\n}\n\nconst CONFIG_CHECKS: ConfigCheck[] = [\n // ── TypeScript Config ──\n {\n id: \"CFG001\", title: \"TypeScript Strict Mode Disabled\", severity: \"medium\",\n category: \"Configuration\", description: \"TypeScript without strict mode misses type errors that can cause runtime bugs and security issues.\",\n fix: 'Enable strict mode in tsconfig.json: \"strict\": true',\n owasp: \"A05:2021\", cwe: \"CWE-710\",\n filePattern: /tsconfig\\.json$/,\n check(content) {\n try {\n if (/\"strict\"\\s*:\\s*false/.test(content)) {\n const line = content.split(\"\\n\").findIndex(l => /strict/.test(l)) + 1;\n return { line, snippet: getSnippet(content, line) };\n }\n if (!/\"strict\"\\s*:\\s*true/.test(content) && /\"compilerOptions\"/.test(content)) {\n return { line: 1, snippet: getSnippet(content, 1) };\n }\n } catch {}\n return null;\n },\n },\n {\n id: \"CFG002\", title: \"TypeScript allowJs Without checkJs\", severity: \"low\",\n category: \"Configuration\", description: \"allowJs without checkJs means JavaScript files bypass type checking entirely.\",\n fix: 'Add \"checkJs\": true alongside \"allowJs\": true in tsconfig.json.',\n owasp: \"A05:2021\", cwe: \"CWE-710\",\n filePattern: /tsconfig\\.json$/,\n check(content) {\n if (/\"allowJs\"\\s*:\\s*true/.test(content) && !/\"checkJs\"\\s*:\\s*true/.test(content)) {\n const line = content.split(\"\\n\").findIndex(l => /allowJs/.test(l)) + 1;\n return { line, snippet: getSnippet(content, line) };\n }\n return null;\n },\n },\n\n // ── Next.js Config ──\n {\n id: \"CFG003\", title: \"Next.js Missing Security Headers\", severity: \"medium\",\n category: \"Configuration\", description: \"Next.js app without security headers in next.config is missing important protections.\",\n fix: \"Add a headers() function to next.config.js with X-Frame-Options, X-Content-Type-Options, Referrer-Policy, and Content-Security-Policy.\",\n owasp: \"A05:2021\", cwe: \"CWE-693\",\n filePattern: /next\\.config\\.(js|mjs|ts)$/,\n check(content) {\n if (!/headers\\s*\\(/.test(content) && !/securityHeaders|security-headers/.test(content)) {\n return { line: 1, snippet: getSnippet(content, 1) };\n }\n return null;\n },\n },\n {\n id: \"CFG004\", title: \"Next.js Powered-By Header Not Disabled\", severity: \"low\",\n category: \"Configuration\", description: \"Next.js exposes X-Powered-By header by default, revealing your tech stack.\",\n fix: 'Add poweredByHeader: false to next.config.js.',\n owasp: \"A05:2021\", cwe: \"CWE-200\",\n filePattern: /next\\.config\\.(js|mjs|ts)$/,\n check(content) {\n if (!/poweredByHeader\\s*:\\s*false/.test(content)) {\n return { line: 1, snippet: getSnippet(content, 1) };\n }\n return null;\n },\n },\n\n // ── Dockerfile ──\n {\n id: \"CFG005\", title: \"Dockerfile Using Latest Tag\", severity: \"medium\",\n category: \"Configuration\", description: \"Using :latest tag in FROM makes builds non-reproducible and may pull vulnerable images.\",\n fix: \"Pin to a specific version: FROM node:20-alpine instead of FROM node:latest.\",\n owasp: \"A06:2021\", cwe: \"CWE-1395\",\n filePattern: /Dockerfile$/i,\n check(content) {\n const lines = content.split(\"\\n\");\n for (let i = 0; i < lines.length; i++) {\n if (/^FROM\\s+\\S+:latest/i.test(lines[i].trim()) || /^FROM\\s+\\w+\\s*$/i.test(lines[i].trim())) {\n return { line: i + 1, snippet: getSnippet(content, i + 1) };\n }\n }\n return null;\n },\n },\n {\n id: \"CFG006\", title: \"Dockerfile Not Using Multi-Stage Build\", severity: \"low\",\n category: \"Configuration\", description: \"Single-stage Docker builds include build tools in the final image, increasing attack surface.\",\n fix: \"Use multi-stage builds: first stage for building, second stage (FROM alpine) for running.\",\n owasp: \"A05:2021\", cwe: \"CWE-1059\",\n filePattern: /Dockerfile$/i,\n check(content) {\n const fromCount = (content.match(/^FROM\\s/gmi) || []).length;\n if (fromCount <= 1 && content.includes(\"npm\") && content.length > 200) {\n return { line: 1, snippet: getSnippet(content, 1) };\n }\n return null;\n },\n },\n {\n id: \"CFG007\", title: \"Dockerfile Copies .env File\", severity: \"critical\",\n category: \"Configuration\", description: \"COPY that includes .env files bakes secrets into the Docker image layer.\",\n fix: \"Add .env to .dockerignore. Use build args or runtime env vars instead.\",\n owasp: \"A02:2021\", cwe: \"CWE-312\",\n filePattern: /Dockerfile$/i,\n check(content) {\n const lines = content.split(\"\\n\");\n for (let i = 0; i < lines.length; i++) {\n if (/^COPY\\s.*\\.env\\b/i.test(lines[i].trim())) {\n return { line: i + 1, snippet: getSnippet(content, i + 1) };\n }\n }\n return null;\n },\n },\n\n // ── GitHub Actions ──\n {\n id: \"CFG008\", title: \"GitHub Actions with Broad Permissions\", severity: \"high\",\n category: \"Configuration\", description: \"GitHub Actions with write-all or broad permissions can be exploited if a dependency is compromised.\",\n fix: \"Use minimal permissions: permissions: { contents: read }. Only grant what's needed.\",\n owasp: \"A01:2021\", cwe: \"CWE-250\",\n filePattern: /\\.github\\/workflows\\/.*\\.(yml|yaml)$/,\n check(content) {\n const lines = content.split(\"\\n\");\n for (let i = 0; i < lines.length; i++) {\n if (/permissions\\s*:\\s*write-all/i.test(lines[i])) {\n return { line: i + 1, snippet: getSnippet(content, i + 1) };\n }\n }\n return null;\n },\n },\n {\n id: \"CFG009\", title: \"GitHub Actions Using Unpinned Action\", severity: \"medium\",\n category: \"Configuration\", description: \"Using actions with @main or @master instead of a pinned SHA allows supply chain attacks.\",\n fix: \"Pin actions to a specific SHA: uses: actions/checkout@abc123... instead of @main.\",\n owasp: \"A06:2021\", cwe: \"CWE-1395\",\n filePattern: /\\.github\\/workflows\\/.*\\.(yml|yaml)$/,\n check(content) {\n const lines = content.split(\"\\n\");\n for (let i = 0; i < lines.length; i++) {\n if (/uses:\\s*\\S+@(?:main|master|dev|latest)\\s*$/i.test(lines[i])) {\n return { line: i + 1, snippet: getSnippet(content, i + 1) };\n }\n }\n return null;\n },\n },\n\n // ── Vite Config ──\n {\n id: \"CFG010\", title: \"Vite Server Open to Network\", severity: \"medium\",\n category: \"Configuration\", description: \"Vite dev server with host: true exposes it to the entire network.\",\n fix: \"Remove host: true or use host: '127.0.0.1' for local-only access during development.\",\n owasp: \"A05:2021\", cwe: \"CWE-668\",\n filePattern: /vite\\.config\\.(js|ts|mjs)$/,\n check(content) {\n const lines = content.split(\"\\n\");\n for (let i = 0; i < lines.length; i++) {\n if (/host\\s*:\\s*true/.test(lines[i]) || /host\\s*:\\s*['\"]0\\.0\\.0\\.0['\"]/.test(lines[i])) {\n return { line: i + 1, snippet: getSnippet(content, i + 1) };\n }\n }\n return null;\n },\n },\n\n // ── .npmrc ──\n {\n id: \"CFG011\", title: \"NPM Auth Token in .npmrc\", severity: \"critical\",\n category: \"Secrets\", description: \"NPM auth tokens in committed .npmrc files expose registry credentials.\",\n fix: \"Remove the token and add .npmrc to .gitignore. Use NPM_TOKEN env var instead.\",\n owasp: \"A02:2021\", cwe: \"CWE-798\",\n filePattern: /\\.npmrc$/,\n check(content) {\n const lines = content.split(\"\\n\");\n for (let i = 0; i < lines.length; i++) {\n if (/_authToken|_auth=|\\/\\/registry.*:_password/.test(lines[i])) {\n return { line: i + 1, snippet: getSnippet(content, i + 1) };\n }\n }\n return null;\n },\n },\n\n // ── ESLint Config ──\n {\n id: \"CFG012\", title: \"Security ESLint Rules Disabled\", severity: \"medium\",\n category: \"Configuration\", description: \"Disabling security-related ESLint rules removes an important safety net.\",\n fix: \"Re-enable security rules or use eslint-plugin-security for automated checks.\",\n owasp: \"A05:2021\", cwe: \"CWE-710\",\n filePattern: /\\.eslint(rc)?(\\.(js|json|yml|yaml|cjs|mjs))?$/,\n check(content) {\n const lines = content.split(\"\\n\");\n for (let i = 0; i < lines.length; i++) {\n if (/no-eval|no-implied-eval|no-new-func|no-script-url|security\\/detect/i.test(lines[i]) && /[\"']off[\"']|:\\s*0/.test(lines[i])) {\n return { line: i + 1, snippet: getSnippet(content, i + 1) };\n }\n }\n return null;\n },\n },\n];\n\nexport function scanConfigs(files: { path: string; content: string }[]): Finding[] {\n const findings: Finding[] = [];\n\n for (const { path: filePath, content } of files) {\n for (const check of CONFIG_CHECKS) {\n if (!check.filePattern.test(filePath)) continue;\n\n const result = check.check(content, filePath);\n if (result) {\n findings.push({\n id: `${check.id}-${filePath}:${result.line}`,\n rule: check.id,\n severity: check.severity,\n title: check.title,\n description: check.description,\n file: filePath,\n line: result.line,\n snippet: result.snippet,\n fix: check.fix,\n category: check.category,\n source: \"custom\",\n owasp: check.owasp,\n cwe: check.cwe,\n });\n }\n }\n }\n\n return findings;\n}\n","/**\n * Multi-File Analysis Scanner\n * Analyzes relationships between files to detect cross-file vulnerabilities\n * that single-file scanners miss.\n */\n\nimport type { Finding } from \"../types.js\";\n\nfunction getSnippet(content: string, line: number): string {\n const lines = content.split(\"\\n\");\n const start = Math.max(0, line - 2);\n const end = Math.min(lines.length, line + 2);\n return lines.slice(start, end).map((l, i) => {\n const lineNum = start + i + 1;\n const prefix = lineNum === line ? \">\" : \" \";\n return `${prefix} ${String(lineNum).padStart(5)} | ${l}`;\n }).join(\"\\n\");\n}\n\ninterface FileInfo {\n path: string;\n content: string;\n}\n\nexport function scanMultiFile(files: FileInfo[]): Finding[] {\n const findings: Finding[] = [];\n\n // Build indexes\n const fileMap = new Map<string, string>();\n for (const f of files) {\n fileMap.set(f.path, f.content);\n }\n\n // ── Check 1: API routes without middleware protection ──\n // If there's a middleware file that handles auth, routes are protected.\n // But if routes exist WITHOUT middleware, they're exposed.\n const hasGlobalMiddleware = files.some(f =>\n /middleware\\.(ts|js)$/.test(f.path) &&\n /(?:auth|session|clerk|nextauth|getToken)/i.test(f.content)\n );\n const hasAuthModule = files.some(f =>\n /(?:auth|middleware)\\.(ts|js)$/i.test(f.path) &&\n /(?:requireAuth|isAuthenticated|authenticate|verify)/i.test(f.content)\n );\n\n // Check API route files for auth\n for (const f of files) {\n if (!/(?:\\/api\\/|routes?\\/|controllers?\\/)/i.test(f.path)) continue;\n if (/middleware/i.test(f.path)) continue;\n\n // Skip health/status endpoints\n if (/(?:health|status|ping|ready|live)\\.(?:ts|js)/i.test(f.path)) continue;\n\n const hasLocalAuth = /(?:requireAuth|isAuthenticated|authenticate|verify|getAuth|getSession|getServerSession|withAuth|jwt\\.verify|clerk)/i.test(f.content);\n const hasAuthImport = /import.*(?:auth|session|middleware|verify|clerk)/i.test(f.content);\n\n if (!hasLocalAuth && !hasAuthImport && !hasGlobalMiddleware) {\n // Only flag if the file has actual route handlers\n if (/\\.(get|post|put|delete|patch)\\s*\\(|export\\s+(?:async\\s+)?function\\s+(?:GET|POST|PUT|DELETE|PATCH)/i.test(f.content)) {\n const line = f.content.split(\"\\n\").findIndex(l =>\n /\\.(get|post|put|delete|patch)\\s*\\(|export\\s+(?:async\\s+)?function\\s+(?:GET|POST|PUT|DELETE|PATCH)/i.test(l)\n ) + 1;\n findings.push({\n id: `MFA001-${f.path}:${line}`,\n rule: \"MFA001\",\n severity: \"high\",\n title: \"API Route Without Auth (Cross-File Check)\",\n description: \"This API route file has no authentication checks and no auth middleware was detected in the project.\",\n file: f.path,\n line,\n snippet: getSnippet(f.content, line),\n fix: \"Add authentication middleware or import your auth module. If using Next.js, create middleware.ts in your app root.\",\n category: \"Authentication\",\n source: \"custom\",\n owasp: \"A01:2021\",\n cwe: \"CWE-862\",\n });\n }\n }\n }\n\n // ── Check 2: .env file exists but .gitignore doesn't exclude it ──\n const hasEnvFile = files.some(f => /^\\.env$|\\/\\.env$/.test(f.path));\n const gitignore = files.find(f => f.path.endsWith(\".gitignore\"));\n if (hasEnvFile && gitignore) {\n if (!/^\\.env$/m.test(gitignore.content) && !/^\\*\\.env$/m.test(gitignore.content)) {\n findings.push({\n id: \"MFA002-.gitignore:1\",\n rule: \"MFA002\",\n severity: \"critical\",\n title: \".env File Not in .gitignore\",\n description: \"An .env file exists in the project but .gitignore doesn't exclude it. Secrets may be committed.\",\n file: \".gitignore\",\n line: 1,\n snippet: getSnippet(gitignore.content, 1),\n fix: \"Add .env to .gitignore: echo '.env' >> .gitignore\",\n category: \"Secrets\",\n source: \"custom\",\n owasp: \"A02:2021\",\n cwe: \"CWE-312\",\n });\n }\n }\n\n // ── Check 3: Database client without connection pooling ──\n const dbFiles = files.filter(f =>\n /(?:db|database|prisma|drizzle|sequelize|mongoose|knex)\\.(ts|js)$/i.test(f.path) ||\n /(?:createClient|createPool|createConnection|PrismaClient)/i.test(f.content)\n );\n for (const f of dbFiles) {\n // Check if DB client is created at module level (not in a function)\n if (/new PrismaClient/i.test(f.content)) {\n // Good pattern: globalThis.prisma = globalThis.prisma || new PrismaClient()\n if (!/globalThis|global\\./i.test(f.content)) {\n const line = f.content.split(\"\\n\").findIndex(l => /new PrismaClient/.test(l)) + 1;\n findings.push({\n id: `MFA003-${f.path}:${line}`,\n rule: \"MFA003\",\n severity: \"medium\",\n title: \"Database Client Not Cached (Connection Leak)\",\n description: \"Creating a new PrismaClient on every request leaks database connections. Use a singleton pattern.\",\n file: f.path,\n line,\n snippet: getSnippet(f.content, line),\n fix: \"Use singleton: globalThis.prisma = globalThis.prisma || new PrismaClient(). This prevents connection exhaustion in serverless.\",\n category: \"Configuration\",\n source: \"custom\",\n owasp: \"A05:2021\",\n cwe: \"CWE-400\",\n });\n }\n }\n }\n\n // ── Check 4: CORS origin mismatch ──\n // Check if CORS is configured but the origin doesn't match APP_URL or frontend URL\n for (const f of files) {\n if (!/(?:server|app|index|main)\\.(ts|js)$/i.test(f.path)) continue;\n const corsMatch = f.content.match(/origin\\s*:\\s*[\"'`](https?:\\/\\/[^\"'`]+)[\"'`]/);\n if (corsMatch) {\n const corsOrigin = corsMatch[1];\n if (corsOrigin.includes(\"localhost\") || corsOrigin.includes(\"127.0.0.1\")) {\n const line = f.content.split(\"\\n\").findIndex(l => l.includes(corsOrigin)) + 1;\n findings.push({\n id: `MFA004-${f.path}:${line}`,\n rule: \"MFA004\",\n severity: \"medium\",\n title: \"CORS Origin Set to Localhost\",\n description: \"CORS origin is hardcoded to localhost. This will block requests from your production frontend.\",\n file: f.path,\n line,\n snippet: getSnippet(f.content, line),\n fix: \"Use environment variable: origin: process.env.FRONTEND_URL || 'http://localhost:3000'\",\n category: \"Configuration\",\n source: \"custom\",\n owasp: \"A05:2021\",\n cwe: \"CWE-942\",\n });\n }\n }\n }\n\n // ── Check 5: Secret referenced but not in .env.example ──\n const envExample = files.find(f => /\\.env\\.example$|\\.env\\.sample$|\\.env\\.template$/.test(f.path));\n if (envExample) {\n const envVarsUsed = new Set<string>();\n for (const f of files) {\n if (/\\.(ts|js|tsx|jsx)$/i.test(f.path)) {\n const matches = f.content.matchAll(/process\\.env\\.([A-Z_][A-Z0-9_]*)/g);\n for (const m of matches) {\n envVarsUsed.add(m[1]);\n }\n }\n }\n const envVarsDocumented = new Set<string>();\n const exampleMatches = envExample.content.matchAll(/^([A-Z_][A-Z0-9_]*)=/gm);\n for (const m of exampleMatches) {\n envVarsDocumented.add(m[1]);\n }\n\n const undocumented = [...envVarsUsed].filter(v => !envVarsDocumented.has(v) && !v.startsWith(\"NODE_\") && v !== \"npm_\");\n if (undocumented.length > 0) {\n findings.push({\n id: `MFA005-${envExample.path}:1`,\n rule: \"MFA005\",\n severity: \"low\",\n title: \"Env Vars Used But Not in .env.example\",\n description: `These environment variables are used in code but missing from .env.example: ${undocumented.slice(0, 5).join(\", \")}${undocumented.length > 5 ? ` (+${undocumented.length - 5} more)` : \"\"}`,\n file: envExample.path,\n line: 1,\n snippet: getSnippet(envExample.content, 1),\n fix: \"Add missing variables to .env.example so team members know which env vars are needed.\",\n category: \"Configuration\",\n source: \"custom\",\n owasp: \"A05:2021\",\n cwe: \"CWE-1188\",\n });\n }\n }\n\n // ── Check 6: Frontend fetching HTTP API in production code ──\n for (const f of files) {\n if (!/\\.(tsx|jsx|ts|js)$/i.test(f.path)) continue;\n if (/(?:test|spec|mock|fixture)/i.test(f.path)) continue;\n\n const httpFetches = f.content.matchAll(/fetch\\s*\\(\\s*[\"'`](http:\\/\\/(?!localhost|127\\.0\\.0\\.1)[^\"'`]+)[\"'`]/g);\n for (const m of httpFetches) {\n const line = f.content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n id: `MFA006-${f.path}:${line}`,\n rule: \"MFA006\",\n severity: \"high\",\n title: \"Frontend Fetching Over HTTP (Not HTTPS)\",\n description: `Fetching from ${m[1].substring(0, 40)}... over HTTP exposes data to interception.`,\n file: f.path,\n line,\n snippet: getSnippet(f.content, line),\n fix: \"Use HTTPS for all API calls in production.\",\n category: \"Configuration\",\n source: \"custom\",\n owasp: \"A02:2021\",\n cwe: \"CWE-319\",\n });\n }\n }\n\n return findings;\n}\n","import chalk from \"chalk\";\nimport Table from \"cli-table3\";\nimport type { Finding, ScanResult, Severity } from \"../types.js\";\nimport { calculateGrade, detectFramework, type SecurityGrade } from \"../scanners/custom-rules.js\";\n\nconst SEVERITY_COLORS: Record<Severity, (text: string) => string> = {\n critical: chalk.bgRed.white.bold,\n high: chalk.red.bold,\n medium: chalk.yellow.bold,\n low: chalk.blue,\n info: chalk.gray,\n};\n\nconst SEVERITY_ICONS: Record<Severity, string> = {\n critical: \"!!!\",\n high: \" !! \",\n medium: \" ! \",\n low: \" - \",\n info: \" i \",\n};\n\nconst SEVERITY_ORDER: Record<Severity, number> = {\n critical: 0,\n high: 1,\n medium: 2,\n low: 3,\n info: 4,\n};\n\nconst GRADE_COLORS: Record<SecurityGrade, (text: string) => string> = {\n \"A+\": chalk.green.bold,\n \"A\": chalk.green.bold,\n \"B\": chalk.cyan.bold,\n \"C\": chalk.yellow.bold,\n \"D\": chalk.red.bold,\n \"F\": chalk.bgRed.white.bold,\n};\n\nexport function renderTerminalReport(result: ScanResult, files?: { path: string; content: string }[]): void {\n const { findings, filesScanned, duration } = result;\n\n // Header\n console.log(\"\");\n console.log(chalk.bold.cyan(\" xploitscan\") + chalk.gray(\" — security scan results\"));\n console.log(chalk.gray(\" \" + \"─\".repeat(50)));\n console.log(\"\");\n\n // Framework detection\n if (files && files.length > 0) {\n const frameworks = detectFramework(files);\n if (frameworks.length > 0 && frameworks[0] !== \"unknown\") {\n console.log(chalk.gray(\" Frameworks: \") + chalk.white(frameworks.join(\", \")));\n }\n }\n\n // Grade + Benchmark\n const GRADE_PERCENTILES: Record<string, number> = { \"A+\": 98, \"A\": 90, \"B\": 70, \"C\": 45, \"D\": 20, \"F\": 5 };\n const { grade, score, summary } = calculateGrade(findings, filesScanned);\n const gradeColor = GRADE_COLORS[grade];\n const percentile = GRADE_PERCENTILES[grade] ?? 50;\n console.log(chalk.gray(\" Security Grade: \") + gradeColor(` ${grade} `) + chalk.gray(` (${score}/100) — ${summary}`));\n console.log(chalk.gray(` Benchmark: More secure than ${percentile}% of projects scanned`));\n console.log(\"\");\n\n if (findings.length === 0) {\n console.log(chalk.green.bold(\" No vulnerabilities found!\"));\n console.log(chalk.gray(` Scanned ${filesScanned} files in ${(duration / 1000).toFixed(1)}s`));\n console.log(\"\");\n return;\n }\n\n // Sort by severity\n const sorted = [...findings].sort(\n (a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity],\n );\n\n // Summary table\n const counts: Record<Severity, number> = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };\n for (const f of findings) counts[f.severity]++;\n\n const summaryParts: string[] = [];\n if (counts.critical > 0) summaryParts.push(chalk.bgRed.white.bold(` ${counts.critical} CRITICAL `));\n if (counts.high > 0) summaryParts.push(chalk.red.bold(`${counts.high} high`));\n if (counts.medium > 0) summaryParts.push(chalk.yellow.bold(`${counts.medium} medium`));\n if (counts.low > 0) summaryParts.push(chalk.blue(`${counts.low} low`));\n if (counts.info > 0) summaryParts.push(chalk.gray(`${counts.info} info`));\n\n console.log(` Found ${chalk.bold(findings.length.toString())} issues: ${summaryParts.join(chalk.gray(\" | \"))}`);\n console.log(chalk.gray(` Scanned ${filesScanned} files in ${(duration / 1000).toFixed(1)}s`));\n console.log(\"\");\n\n // Individual findings\n for (const finding of sorted) {\n const severityLabel = SEVERITY_COLORS[finding.severity](\n ` ${finding.severity.toUpperCase()} `,\n );\n const sourceLabel = finding.source === \"ai\"\n ? chalk.magenta(\" [AI] \")\n : finding.source === \"dependency\"\n ? chalk.cyan(\" [DEP] \")\n : finding.source === \"entropy\"\n ? chalk.yellow(\" [ENTROPY] \")\n : finding.source === \"config\"\n ? chalk.blue(\" [CFG] \")\n : finding.source === \"multi-file\"\n ? chalk.green(\" [CROSS-FILE] \")\n : chalk.gray(` [${finding.rule}] `);\n const confidenceLabel = finding.confidence\n ? chalk.gray(` (${finding.confidence} confidence)`)\n : \"\";\n\n const complianceTags = [\n finding.owasp ? chalk.yellow(`[${finding.owasp}]`) : \"\",\n finding.cwe ? chalk.blue(`[${finding.cwe}]`) : \"\",\n ].filter(Boolean).join(\" \");\n console.log(` ${severityLabel}${sourceLabel}${chalk.bold(finding.title)} ${complianceTags}${confidenceLabel}`);\n console.log(chalk.gray(` ${finding.file}:${finding.line}`));\n console.log(\"\");\n\n // Description\n console.log(chalk.white(` ${finding.description}`));\n console.log(\"\");\n\n // Code snippet\n if (finding.snippet) {\n const snippetLines = finding.snippet.split(\"\\n\");\n for (const line of snippetLines) {\n if (line.startsWith(\">\")) {\n console.log(chalk.red(` ${line}`));\n } else {\n console.log(chalk.gray(` ${line}`));\n }\n }\n console.log(\"\");\n }\n\n // Fix suggestion\n if (finding.fix) {\n console.log(chalk.green(` Fix: ${finding.fix}`));\n console.log(\"\");\n }\n\n console.log(chalk.gray(\" \" + \"─\".repeat(50)));\n console.log(\"\");\n }\n\n // OWASP Top 10 Summary\n const owaspFindings = findings.filter(f => f.owasp);\n if (owaspFindings.length > 0) {\n const owaspCats: Record<string, { name: string; count: number }> = {\n \"A01:2021\": { name: \"Broken Access Control\", count: 0 },\n \"A02:2021\": { name: \"Cryptographic Failures\", count: 0 },\n \"A03:2021\": { name: \"Injection\", count: 0 },\n \"A04:2021\": { name: \"Insecure Design\", count: 0 },\n \"A05:2021\": { name: \"Security Misconfiguration\", count: 0 },\n \"A06:2021\": { name: \"Vulnerable Components\", count: 0 },\n \"A07:2021\": { name: \"Auth Failures\", count: 0 },\n \"A08:2021\": { name: \"Data Integrity\", count: 0 },\n \"A09:2021\": { name: \"Logging Failures\", count: 0 },\n \"A10:2021\": { name: \"SSRF\", count: 0 },\n };\n for (const f of owaspFindings) {\n if (f.owasp && owaspCats[f.owasp]) owaspCats[f.owasp].count++;\n }\n console.log(chalk.bold(\" OWASP Top 10 Compliance\"));\n for (const [id, { name, count }] of Object.entries(owaspCats)) {\n const status = count > 0 ? chalk.red(`${count} issue${count > 1 ? \"s\" : \"\"}`) : chalk.green(\"PASS\");\n console.log(chalk.gray(` ${id} ${name}: `) + status);\n }\n console.log(\"\");\n }\n\n // Footer\n if (counts.critical > 0) {\n console.log(\n chalk.bgRed.white.bold(\" ACTION REQUIRED \") +\n chalk.red.bold(` ${counts.critical} critical issue${counts.critical > 1 ? \"s\" : \"\"} found. Fix these before deploying.`),\n );\n } else if (counts.high > 0) {\n console.log(\n chalk.yellow.bold(` Recommendation: Address the ${counts.high} high-severity issue${counts.high > 1 ? \"s\" : \"\"} before going to production.`),\n );\n }\n\n console.log(\"\");\n}\n","import type { ScanResult } from \"../types.js\";\n\nexport function renderJsonReport(result: ScanResult): void {\n console.log(JSON.stringify(result, null, 2));\n}\n","import type { Finding, ScanResult, Severity } from \"../types.js\";\n\nconst SEVERITY_TO_SARIF: Record<Severity, string> = {\n critical: \"error\",\n high: \"error\",\n medium: \"warning\",\n low: \"note\",\n info: \"none\",\n};\n\nconst SEVERITY_TO_LEVEL: Record<Severity, number> = {\n critical: 10.0,\n high: 8.0,\n medium: 5.0,\n low: 3.0,\n info: 1.0,\n};\n\ninterface SarifOutput {\n $schema: string;\n version: string;\n runs: Array<{\n tool: {\n driver: {\n name: string;\n version: string;\n informationUri: string;\n rules: Array<{\n id: string;\n shortDescription: { text: string };\n fullDescription: { text: string };\n defaultConfiguration: { level: string };\n properties: { security_severity: string; tags: string[] };\n }>;\n };\n };\n results: Array<{\n ruleId: string;\n ruleIndex: number;\n level: string;\n message: { text: string };\n locations: Array<{\n physicalLocation: {\n artifactLocation: { uri: string };\n region: {\n startLine: number;\n startColumn?: number;\n };\n };\n }>;\n }>;\n }>;\n}\n\nexport function renderSarifReport(result: ScanResult): void {\n // Collect unique rules\n const ruleMap = new Map<string, Finding>();\n for (const f of result.findings) {\n if (!ruleMap.has(f.rule)) {\n ruleMap.set(f.rule, f);\n }\n }\n\n const rules = Array.from(ruleMap.entries()).map(([id, f]) => ({\n id,\n shortDescription: { text: f.title },\n fullDescription: { text: f.description },\n defaultConfiguration: { level: SEVERITY_TO_SARIF[f.severity] },\n properties: {\n security_severity: SEVERITY_TO_LEVEL[f.severity].toFixed(1),\n tags: [\"security\", f.category.toLowerCase().replace(/\\s+/g, \"-\")],\n },\n }));\n\n const ruleIndex = new Map(rules.map((r, i) => [r.id, i]));\n\n const sarif: SarifOutput = {\n $schema: \"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json\",\n version: \"2.1.0\",\n runs: [\n {\n tool: {\n driver: {\n name: \"xploitscan\",\n version: \"0.1.0\",\n informationUri: \"https://github.com/bgage72590/xploitscan\",\n rules,\n },\n },\n results: result.findings.map((f) => {\n // SARIF schema requires that any `fixes[]` entry include an\n // `artifactChanges` array, which we don't have because findings\n // are pattern-based, not AST rewrites. Inline the fix text into\n // the message instead so GitHub still surfaces it.\n const messageText = f.fix\n ? `${f.title}: ${f.description}\\nSuggested fix: ${f.fix}`\n : `${f.title}: ${f.description}`;\n return {\n ruleId: f.rule,\n ruleIndex: ruleIndex.get(f.rule) ?? 0,\n level: SEVERITY_TO_SARIF[f.severity],\n message: { text: messageText },\n locations: [\n {\n physicalLocation: {\n artifactLocation: { uri: f.file },\n region: {\n startLine: f.line,\n ...(f.column ? { startColumn: f.column } : {}),\n },\n },\n },\n ],\n };\n }),\n },\n ],\n };\n\n console.log(JSON.stringify(sarif, null, 2));\n}\n","import { createServer } from \"node:http\";\nimport { URL } from \"node:url\";\nimport { randomUUID } from \"node:crypto\";\nimport { execFile } from \"node:child_process\";\nimport chalk from \"chalk\";\nimport ora from \"ora\";\nimport { storeToken, clearToken, getStoredToken, syncUser, clearProRulesCache } from \"../utils/api.js\";\n\nconst CLERK_PUBLISHABLE_KEY = process.env.CLERK_PUBLISHABLE_KEY ?? \"\";\n\n/**\n * Opens a browser-based login flow.\n * 1. Starts a local HTTP server on a random port\n * 2. Opens the Clerk sign-in page with redirect back to local server\n * 3. Receives the token via redirect callback\n * 4. Stores the token locally\n */\nexport async function loginCommand(): Promise<void> {\n const existing = getStoredToken();\n if (existing) {\n console.log(chalk.yellow(`Already logged in as ${existing.email}`));\n console.log(chalk.gray(\"Run `xploitscan auth logout` first to switch accounts.\"));\n return;\n }\n\n const spinner = ora(\"Waiting for browser login...\").start();\n\n // Start local callback server\n const { token, email, userId } = await waitForBrowserLogin();\n\n spinner.text = \"Syncing account...\";\n\n // Store token\n storeToken({\n token,\n userId,\n email,\n expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000, // 7 days\n });\n\n // Sync user with API\n const user = await syncUser();\n spinner.stop();\n\n console.log(chalk.green(`Logged in as ${email}`));\n if (user) {\n console.log(chalk.gray(`Plan: ${user.plan}`));\n }\n}\n\nexport async function logoutCommand(): Promise<void> {\n const existing = getStoredToken();\n if (!existing) {\n console.log(chalk.gray(\"Not logged in.\"));\n return;\n }\n\n clearToken();\n clearProRulesCache();\n console.log(chalk.green(\"Logged out successfully.\"));\n}\n\nexport async function whoamiCommand(): Promise<void> {\n const token = getStoredToken();\n if (!token) {\n console.log(chalk.gray(\"Not logged in. Run `xploitscan auth login` to authenticate.\"));\n return;\n }\n\n console.log(chalk.cyan(`Email: ${token.email}`));\n console.log(chalk.gray(`User ID: ${token.userId}`));\n\n const user = await syncUser();\n if (user) {\n const planBadge = user.plan === \"pro\"\n ? chalk.bgGreen.black(\" PRO \")\n : chalk.bgGray.white(\" FREE \");\n console.log(`Plan: ${planBadge}`);\n }\n}\n\nasync function waitForBrowserLogin(): Promise<{\n token: string;\n email: string;\n userId: string;\n}> {\n return new Promise((resolve, reject) => {\n const expectedState = randomUUID();\n\n const server = createServer((req, res) => {\n if (!req.url) {\n res.writeHead(400);\n res.end(\"Bad request\");\n return;\n }\n\n const url = new URL(req.url, `http://localhost`);\n\n if (url.pathname === \"/callback\") {\n const token = url.searchParams.get(\"token\");\n const email = url.searchParams.get(\"email\");\n const userId = url.searchParams.get(\"user_id\");\n const state = url.searchParams.get(\"state\");\n\n if (!state || state !== expectedState) {\n res.writeHead(403);\n res.end(\"Invalid state parameter — possible CSRF attack.\");\n return;\n }\n\n if (token && email && userId) {\n res.writeHead(200, { \"Content-Type\": \"text/html\" });\n res.end(`\n <html>\n <body style=\"font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #0a0a0f; color: #e0e0e0;\">\n <div style=\"text-align: center;\">\n <h1 style=\"color: #00d4ff;\">xploitscan</h1>\n <p style=\"color: #4ade80; font-size: 1.2rem;\">Login successful!</p>\n <p style=\"color: #888;\">You can close this tab and return to your terminal.</p>\n </div>\n </body>\n </html>\n `);\n\n server.close();\n resolve({ token, email, userId });\n } else {\n res.writeHead(400);\n res.end(\"Missing parameters\");\n }\n } else if (url.pathname === \"/login\") {\n // Serve a simple login page that redirects to Clerk\n const callbackUrl = `http://localhost:${(server.address() as { port: number }).port}/callback`;\n\n res.writeHead(200, { \"Content-Type\": \"text/html\" });\n res.end(`\n <html>\n <body style=\"font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #0a0a0f; color: #e0e0e0;\">\n <div style=\"text-align: center;\">\n <h1 style=\"color: #00d4ff;\">xploitscan</h1>\n <p>Redirecting to login...</p>\n <p style=\"color: #888; font-size: 0.85rem;\">If not redirected, <a href=\"#\" style=\"color: #00d4ff;\">click here</a>.</p>\n </div>\n </body>\n </html>\n `);\n } else {\n res.writeHead(302, { Location: \"/login\" });\n res.end();\n }\n });\n\n // Listen on random available port\n server.listen(0, \"127.0.0.1\", () => {\n const addr = server.address() as { port: number };\n const loginUrl = `http://localhost:${addr.port}/login`;\n\n console.log(chalk.cyan(`\\nOpen this URL in your browser to log in:`));\n console.log(chalk.bold.underline(loginUrl));\n console.log(\"\");\n\n // Try to open browser automatically\n const openCmd = process.platform === \"darwin\" ? \"open\" : process.platform === \"win32\" ? \"start\" : \"xdg-open\";\n execFile(openCmd, [loginUrl], () => {});\n });\n\n // Timeout after 5 minutes\n setTimeout(() => {\n server.close();\n reject(new Error(\"Login timed out. Please try again.\"));\n }, 5 * 60 * 1000);\n });\n}\n","import { execSync } from \"node:child_process\";\nimport {\n existsSync,\n readFileSync,\n writeFileSync,\n chmodSync,\n unlinkSync,\n} from \"node:fs\";\nimport { join } from \"node:path\";\nimport chalk from \"chalk\";\n\nconst HOOK_START_MARKER = \"# xploitscan-hook-start\";\nconst HOOK_END_MARKER = \"# xploitscan-hook-end\";\n\nconst HOOK_CONTENT = `${HOOK_START_MARKER}\n# Installed by XploitScan CLI — do not edit this block manually\necho \"\"\necho \"🛡 Running XploitScan security check...\"\nnpx --yes xploitscan scan . --no-ai --diff HEAD\nHOOK_EXIT=$?\nif [ $HOOK_EXIT -ne 0 ]; then\n echo \"\"\n echo \"❌ XploitScan found security issues. Commit blocked.\"\n echo \" Fix the issues above, or use 'git commit --no-verify' to skip.\"\n exit 1\nfi\n${HOOK_END_MARKER}`;\n\nfunction getGitRoot(): string | null {\n try {\n const root = execSync(\"git rev-parse --show-toplevel\", {\n encoding: \"utf-8\",\n stdio: [\"pipe\", \"pipe\", \"ignore\"],\n }).trim();\n return root || null;\n } catch {\n return null;\n }\n}\n\nfunction removeExistingBlock(content: string): string {\n const startIdx = content.indexOf(HOOK_START_MARKER);\n const endIdx = content.indexOf(HOOK_END_MARKER);\n if (startIdx === -1 || endIdx === -1 || endIdx < startIdx) {\n return content;\n }\n const before = content.slice(0, startIdx).replace(/\\n+$/, \"\");\n const after = content.slice(endIdx + HOOK_END_MARKER.length).replace(/^\\n+/, \"\");\n if (before && after) return `${before}\\n\\n${after}\\n`;\n if (before) return `${before}\\n`;\n if (after) return `${after}\\n`;\n return \"\";\n}\n\nexport async function installHookCommand(options: { force?: boolean }): Promise<void> {\n const gitRoot = getGitRoot();\n if (!gitRoot) {\n console.log(chalk.red(\"✗ Not a git repository.\"));\n console.log(chalk.gray(\" Run this command from inside a git repo.\"));\n process.exit(1);\n }\n\n const hooksDir = join(gitRoot, \".git\", \"hooks\");\n const hookPath = join(hooksDir, \"pre-commit\");\n\n let existingContent = \"\";\n let hadExisting = false;\n if (existsSync(hookPath)) {\n existingContent = readFileSync(hookPath, \"utf-8\");\n hadExisting = true;\n\n const alreadyInstalled =\n existingContent.includes(HOOK_START_MARKER) && existingContent.includes(HOOK_END_MARKER);\n\n if (alreadyInstalled && !options.force) {\n console.log(chalk.yellow(\"⚠ XploitScan hook is already installed.\"));\n console.log(chalk.gray(\" Use --force to reinstall, or `xploitscan hook uninstall` to remove it.\"));\n return;\n }\n\n if (alreadyInstalled && options.force) {\n existingContent = removeExistingBlock(existingContent);\n }\n }\n\n // Compose new hook file\n let newContent: string;\n if (hadExisting && existingContent.trim()) {\n // Preserve existing hook content, append our block\n const trimmed = existingContent.replace(/\\n+$/, \"\");\n newContent = `${trimmed}\\n\\n${HOOK_CONTENT}\\n`;\n } else {\n newContent = `#!/bin/sh\\n\\n${HOOK_CONTENT}\\n`;\n }\n\n try {\n writeFileSync(hookPath, newContent, \"utf-8\");\n chmodSync(hookPath, 0o755);\n } catch (err) {\n console.log(chalk.red(`✗ Failed to write pre-commit hook: ${(err as Error).message}`));\n process.exit(1);\n }\n\n console.log(chalk.green(\"✓ XploitScan pre-commit hook installed.\"));\n console.log(\"\");\n console.log(chalk.gray(\" XploitScan will now scan your code before every commit.\"));\n console.log(chalk.gray(\" To skip a scan, use: \") + chalk.cyan(\"git commit --no-verify\"));\n console.log(chalk.gray(\" To remove: \") + chalk.cyan(\"xploitscan hook uninstall\"));\n}\n\nexport async function uninstallHookCommand(): Promise<void> {\n const gitRoot = getGitRoot();\n if (!gitRoot) {\n console.log(chalk.red(\"✗ Not a git repository.\"));\n console.log(chalk.gray(\" Run this command from inside a git repo.\"));\n process.exit(1);\n }\n\n const hookPath = join(gitRoot, \".git\", \"hooks\", \"pre-commit\");\n\n if (!existsSync(hookPath)) {\n console.log(chalk.yellow(\"⚠ No pre-commit hook found. Nothing to uninstall.\"));\n return;\n }\n\n const content = readFileSync(hookPath, \"utf-8\");\n if (!content.includes(HOOK_START_MARKER)) {\n console.log(chalk.yellow(\"⚠ XploitScan hook not found in pre-commit file.\"));\n console.log(chalk.gray(\" Nothing to remove.\"));\n return;\n }\n\n const stripped = removeExistingBlock(content).trim();\n const shebangOnly = stripped === \"#!/bin/sh\" || stripped === \"#!/usr/bin/env sh\" || stripped === \"\";\n\n try {\n if (shebangOnly) {\n unlinkSync(hookPath);\n console.log(chalk.green(\"✓ XploitScan hook removed. Pre-commit file deleted (was empty after removal).\"));\n } else {\n writeFileSync(hookPath, stripped + \"\\n\", \"utf-8\");\n chmodSync(hookPath, 0o755);\n console.log(chalk.green(\"✓ XploitScan hook removed.\"));\n console.log(chalk.gray(\" Other hook content was preserved.\"));\n }\n } catch (err) {\n console.log(chalk.red(`✗ Failed to uninstall hook: ${(err as Error).message}`));\n process.exit(1);\n }\n}\n","import chalk from \"chalk\";\nimport { mkdirSync, existsSync, writeFileSync, readFileSync } from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\ninterface InstallOptions {\n force?: boolean;\n legacyOnly?: boolean;\n}\n\n/**\n * Install XploitScan security rules into a project so Cursor enforces them\n * at write-time. Drops:\n * - .cursor/rules/xploitscan-security.mdc (modern context-aware format)\n * - .cursorrules (legacy fallback for older Cursor versions)\n *\n * Idempotent: if files exist, won't overwrite without --force.\n */\nexport async function cursorInstallCommand(opts: InstallOptions = {}): Promise<void> {\n const cwd = process.cwd();\n\n // Resolve template paths relative to this file (works after tsup bundling\n // because we copy templates into dist via tsup config)\n const here = dirname(fileURLToPath(import.meta.url));\n const candidates = [\n join(here, \"templates\"),\n join(here, \"..\", \"templates\"),\n join(here, \"..\", \"src\", \"templates\"),\n ];\n const templatesDir = candidates.find((p) => existsSync(join(p, \"cursor-security.mdc\")));\n if (!templatesDir) {\n console.error(chalk.red(\"Could not locate XploitScan rule templates. Try reinstalling: npm i -g xploitscan@latest\"));\n process.exit(1);\n }\n\n const mdcSrc = readFileSync(join(templatesDir, \"cursor-security.mdc\"), \"utf-8\");\n const legacySrc = readFileSync(join(templatesDir, \"cursorrules-legacy.txt\"), \"utf-8\");\n\n // Modern format\n if (!opts.legacyOnly) {\n const mdcDir = join(cwd, \".cursor\", \"rules\");\n const mdcPath = join(mdcDir, \"xploitscan-security.mdc\");\n if (existsSync(mdcPath) && !opts.force) {\n console.log(chalk.yellow(`Skipping ${mdcPath} (already exists, use --force to overwrite)`));\n } else {\n mkdirSync(mdcDir, { recursive: true });\n writeFileSync(mdcPath, mdcSrc);\n console.log(chalk.green(`Installed ${mdcPath}`));\n }\n }\n\n // Legacy format\n const legacyPath = join(cwd, \".cursorrules\");\n if (existsSync(legacyPath) && !opts.force) {\n console.log(chalk.yellow(`Skipping ${legacyPath} (already exists, use --force to overwrite)`));\n } else {\n writeFileSync(legacyPath, legacySrc);\n console.log(chalk.green(`Installed ${legacyPath}`));\n }\n\n console.log(\"\");\n console.log(chalk.cyan(\"Cursor will pick up these rules automatically the next time you open the project.\"));\n console.log(chalk.gray(\"Run a scan any time to catch what slipped through: npx xploitscan scan .\"));\n}\n"],"mappings":"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","names":["resolve","join","chalk","readFileSync","join","rm","rm","rm","existsSync","join","resolve","execFile","readFile","mkdtemp","rm","existsSync","join","tmpdir","resolve","execFile","mkdtemp","join","tmpdir","existsSync","readFile","rm","lines","Anthropic","getSnippet","getSnippet","getSnippet","getSnippet","resolve","chalk","execSync","join","result","execFile","chalk","ora","chalk","ora","resolve","execFile","existsSync","readFileSync","join","chalk","chalk","existsSync","writeFileSync","readFileSync","join","getStoredToken","chalk","execFile"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/commands/scan.ts","../src/utils/files.ts","../src/utils/config.ts","../src/scanners/custom-rules.ts","../src/scanners/semgrep.ts","../src/scanners/gitleaks.ts","../src/scanners/ai-analyzer.ts","../src/scanners/ai-fp-filter.ts","../src/scanners/ast-analyzer.ts","../src/scanners/osv.ts","../src/scanners/dependency-scanner.ts","../src/scanners/entropy-scanner.ts","../src/scanners/config-analyzer.ts","../src/scanners/multi-file-analyzer.ts","../src/reporters/terminal.ts","../src/reporters/json.ts","../src/reporters/sarif.ts","../src/commands/auth.ts","../src/commands/hook.ts","../src/commands/cursor.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport { scanCommand } from \"./commands/scan.js\";\nimport { loginCommand, logoutCommand, whoamiCommand } from \"./commands/auth.js\";\nimport { installHookCommand, uninstallHookCommand } from \"./commands/hook.js\";\nimport { cursorInstallCommand } from \"./commands/cursor.js\";\n\nconst program = new Command();\n\nprogram\n .name(\"xploitscan\")\n .description(\n \"AI security scanner for vibe-coded apps. Find vulnerabilities before attackers do.\",\n )\n .version(\"1.0.8\");\n\nprogram\n .command(\"scan\")\n .description(\"Scan a directory for security vulnerabilities\")\n .argument(\"[directory]\", \"Directory to scan\", \".\")\n .option(\"--no-ai\", \"Skip AI-powered analysis\")\n .option(\"-f, --format <format>\", \"Output format: terminal, json, sarif\", \"terminal\")\n .option(\"-v, --verbose\", \"Show detailed output\", false)\n .option(\"--diff [base]\", \"Scan only files changed vs base branch (default: main)\")\n .option(\"-w, --watch\", \"Watch for file changes and re-scan automatically\", false)\n .action(async (directory: string, opts: { ai: boolean; format: string; verbose: boolean; diff?: string | boolean; watch: boolean }) => {\n await scanCommand(directory, {\n directory,\n aiAnalysis: opts.ai,\n format: opts.format as \"terminal\" | \"json\" | \"sarif\",\n verbose: opts.verbose,\n diff: opts.diff,\n watch: opts.watch,\n });\n });\n\n// Auth commands\nconst auth = program\n .command(\"auth\")\n .description(\"Manage authentication\");\n\nauth\n .command(\"login\")\n .description(\"Log in to your XploitScan account\")\n .action(loginCommand);\n\nauth\n .command(\"logout\")\n .description(\"Log out of your XploitScan account\")\n .action(logoutCommand);\n\nauth\n .command(\"whoami\")\n .description(\"Show current logged-in user\")\n .action(whoamiCommand);\n\n// Hook commands\nconst hook = program\n .command(\"hook\")\n .description(\"Manage git pre-commit hooks for automatic scanning\");\n\nhook\n .command(\"install\")\n .description(\"Install a git pre-commit hook that runs XploitScan on every commit\")\n .option(\"-f, --force\", \"Overwrite existing XploitScan hook without prompting\", false)\n .action(async (opts: { force?: boolean }) => {\n await installHookCommand({ force: opts.force });\n });\n\nhook\n .command(\"uninstall\")\n .description(\"Remove the XploitScan pre-commit hook\")\n .action(uninstallHookCommand);\n\n// Cursor integration\nconst cursor = program\n .command(\"cursor\")\n .description(\"Manage XploitScan integration with Cursor IDE\");\n\ncursor\n .command(\"install\")\n .description(\"Drop XploitScan security rules into .cursor/rules and .cursorrules so Cursor enforces them at write-time\")\n .option(\"-f, --force\", \"Overwrite existing rule files\", false)\n .option(\"--legacy-only\", \"Only install the legacy .cursorrules file (skip .cursor/rules/*.mdc)\", false)\n .action(async (opts: { force?: boolean; legacyOnly?: boolean }) => {\n await cursorInstallCommand({ force: opts.force, legacyOnly: opts.legacyOnly });\n });\n\n// Upgrade command (shortcut)\nprogram\n .command(\"upgrade\")\n .description(\"Upgrade to XploitScan Pro for unlimited scans\")\n .action(async () => {\n const { getStoredToken, getCheckoutUrl } = await import(\"./utils/api.js\");\n const chalk = (await import(\"chalk\")).default;\n\n const token = getStoredToken();\n if (!token) {\n console.log(chalk.yellow(\"Please log in first: xploitscan auth login\"));\n return;\n }\n\n console.log(chalk.cyan(\"Creating checkout session...\"));\n const url = await getCheckoutUrl();\n if (url) {\n console.log(chalk.green(`\\nOpen this URL to upgrade:`));\n console.log(chalk.bold.underline(url));\n const { execFile } = await import(\"node:child_process\");\n const openCmd = process.platform === \"darwin\" ? \"open\" : process.platform === \"win32\" ? \"start\" : \"xdg-open\";\n execFile(openCmd, [url], () => {});\n } else {\n console.log(chalk.red(\"Failed to create checkout session. Please try again.\"));\n }\n });\n\nprogram.parse();\n","import { resolve, join, relative } from \"node:path\";\nimport { watch as fsWatch } from \"node:fs\";\nimport ora from \"ora\";\nimport chalk from \"chalk\";\nimport type { Finding, ScanOptions, ScanResult } from \"../types.js\";\nimport { collectFiles, readFileContents } from \"../utils/files.js\";\nimport { loadConfig } from \"../utils/config.js\";\nimport { runCustomRules } from \"../scanners/custom-rules.js\";\nimport { runSemgrep } from \"../scanners/semgrep.js\";\nimport { runGitleaks } from \"../scanners/gitleaks.js\";\nimport { analyzeWithAI } from \"../scanners/ai-analyzer.js\";\nimport { filterFalsePositives } from \"../scanners/ai-fp-filter.js\";\nimport { buildASTContext } from \"../scanners/ast-analyzer.js\";\nimport { scanDependencies, scanDependenciesOsv } from \"../scanners/dependency-scanner.js\";\nimport { scanEntropy } from \"../scanners/entropy-scanner.js\";\nimport { scanConfigs } from \"../scanners/config-analyzer.js\";\nimport { scanMultiFile } from \"../scanners/multi-file-analyzer.js\";\nimport { renderTerminalReport } from \"../reporters/terminal.js\";\nimport { renderJsonReport } from \"../reporters/json.js\";\nimport { renderSarifReport } from \"../reporters/sarif.js\";\nimport { checkUsage, incrementUsage, uploadScanResults, isAuthenticated, loadCachedProRules, downloadProRulesBundle } from \"../utils/api.js\";\nimport type { CustomRule } from \"../types.js\";\n\nexport async function scanCommand(\n directory: string,\n options: Partial<ScanOptions>,\n): Promise<void> {\n const dir = resolve(directory || \".\");\n const format = options.format ?? \"terminal\";\n const verbose = options.verbose ?? false;\n const startTime = Date.now();\n\n // Load config\n const config = await loadConfig(dir);\n const useAI = (options.aiAnalysis ?? config.ai ?? true) && !!process.env.ANTHROPIC_API_KEY;\n\n const isSilent = format !== \"terminal\";\n\n // Step 0: Check usage limits and determine rule tier\n let tier: \"free\" | \"pro\" = \"free\";\n let userPlan = \"anonymous\";\n\n if (isAuthenticated()) {\n const usage = await checkUsage();\n userPlan = usage.plan;\n if (usage.plan === \"pro\") {\n tier = \"pro\";\n }\n if (!usage.allowed) {\n console.log(chalk.red(\"\\nDaily scan limit reached.\"));\n console.log(chalk.yellow(\"Upgrade to Pro for unlimited scans: \") + chalk.bold(\"xploitscan upgrade\"));\n console.log(chalk.gray(`Resets tomorrow. Plan: ${usage.plan}\\n`));\n process.exitCode = 1;\n return;\n }\n if (usage.plan === \"free\" && usage.remaining > 0 && usage.remaining <= 2 && !isSilent) {\n console.log(chalk.gray(` ${usage.remaining} free scan${usage.remaining === 1 ? \"\" : \"s\"} remaining today\\n`));\n }\n }\n\n // Step 0b: Load Pro rules from cache or server\n let proRulesExtra: CustomRule[] = [];\n if (tier === \"pro\") {\n const cached = loadCachedProRules() as CustomRule[] | null;\n if (cached) {\n proRulesExtra = cached;\n } else {\n if (!isSilent) console.log(chalk.gray(\" Downloading Pro rules...\"));\n const downloaded = await downloadProRulesBundle();\n if (downloaded) {\n proRulesExtra = (loadCachedProRules() as CustomRule[] | null) || [];\n }\n if (proRulesExtra.length === 0 && !isSilent) {\n console.log(chalk.yellow(\" Could not load Pro rules — scanning with free rules only\"));\n }\n }\n }\n\n // Step 1: Collect files\n const spinner = ora({\n text: \"Scanning files...\",\n color: \"cyan\",\n isSilent,\n }).start();\n\n let files: string[];\n try {\n files = await collectFiles(dir);\n } catch (error) {\n spinner.fail(\"Failed to scan directory\");\n console.error(chalk.red(`Error: ${error instanceof Error ? error.message : error}`));\n process.exit(1);\n }\n\n // Diff mode: filter to only changed files\n if (options.diff) {\n const base = typeof options.diff === \"string\" ? options.diff : \"main\";\n try {\n const { execSync } = await import(\"node:child_process\");\n const changedFiles = execSync(`git diff --name-only ${base}`, { cwd: dir, encoding: \"utf-8\" })\n .trim().split(\"\\n\").filter(Boolean);\n files = files.filter(f => changedFiles.some(cf => f.endsWith(cf) || cf.endsWith(f)));\n if (!isSilent) {\n spinner.info(chalk.gray(` Diff mode: scanning ${files.length} changed files vs ${base}`));\n }\n } catch {\n spinner.warn(\"Could not run git diff — scanning all files\");\n }\n }\n\n if (files.length === 0) {\n spinner.warn(\"No source files found in this directory\");\n return;\n }\n\n spinner.text = `Found ${files.length} files. Running security rules...`;\n\n // Step 2: Run all static scanners in parallel\n const allFindings: Finding[] = [];\n\n // 2a: Custom rules with AST context (always runs, instant)\n const fileContentsForAnalysis: { path: string; content: string }[] = [];\n for (const filePath of files) {\n const content = readFileContents(dir, filePath);\n if (!content) continue;\n fileContentsForAnalysis.push({ path: filePath, content });\n\n // Build AST context for false positive reduction\n const astCtx = buildASTContext(content, filePath);\n\n // Skip scanner files scanning themselves\n if (astCtx.isScannerFile) continue;\n\n // Run custom rules with AST-stripped content for comment-heavy files\n const findings = runCustomRules(content, filePath, config.disableRules, tier, proRulesExtra);\n\n // Add confidence scores based on AST context\n for (const f of findings) {\n if (astCtx.isTestFile) {\n f.confidence = \"low\";\n } else if (astCtx.isConfigFile) {\n f.confidence = \"medium\";\n } else {\n f.confidence = \"high\";\n }\n }\n\n allFindings.push(...findings);\n }\n\n const customCount = allFindings.length;\n if (verbose && customCount > 0) {\n spinner.info(`Custom rules found ${customCount} issues`);\n }\n\n // 2b: Dependency vulnerability scanning (static allowlist + live OSV.dev)\n spinner.text = \"Scanning dependencies...\";\n const depFindings = scanDependencies(fileContentsForAnalysis);\n for (const f of depFindings) { f.confidence = \"high\"; }\n allFindings.push(...depFindings);\n\n // Track which (package, CVE) pairs the static scanner already caught so the\n // OSV pass doesn't double-report\n const dedupeKeys = new Set<string>();\n for (const f of depFindings) {\n // Static findings use rule = CVE id. Extract the package name from the title.\n const match = f.title.match(/^(\\S+)/);\n if (match) dedupeKeys.add(`${match[1].toLowerCase()}:${f.rule}`);\n }\n\n spinner.text = \"Checking dependencies against OSV.dev...\";\n try {\n const osvFindings = await scanDependenciesOsv(fileContentsForAnalysis, dedupeKeys);\n for (const f of osvFindings) { f.confidence = \"high\"; }\n allFindings.push(...osvFindings);\n if (verbose && osvFindings.length > 0) {\n spinner.info(`OSV.dev found ${osvFindings.length} additional vulnerable dependencies`);\n }\n } catch (err) {\n if (verbose) spinner.info(`OSV.dev lookup skipped: ${err instanceof Error ? err.message : \"unknown error\"}`);\n }\n\n if (verbose && depFindings.length > 0) {\n spinner.info(`Dependency scanner found ${depFindings.length} issues`);\n }\n\n // 2c: Entropy-based secret detection\n spinner.text = \"Scanning for high-entropy secrets...\";\n const entropyFindings = scanEntropy(fileContentsForAnalysis);\n for (const f of entropyFindings) { f.confidence = \"medium\"; }\n allFindings.push(...entropyFindings);\n if (verbose && entropyFindings.length > 0) {\n spinner.info(`Entropy scanner found ${entropyFindings.length} potential secrets`);\n }\n\n // 2d: Configuration file deep analysis\n spinner.text = \"Analyzing configuration files...\";\n const configFindings = scanConfigs(fileContentsForAnalysis);\n for (const f of configFindings) { f.confidence = \"high\"; }\n allFindings.push(...configFindings);\n if (verbose && configFindings.length > 0) {\n spinner.info(`Config analyzer found ${configFindings.length} issues`);\n }\n\n // 2e: Multi-file cross-reference analysis\n spinner.text = \"Running cross-file analysis...\";\n const multiFileFindings = scanMultiFile(fileContentsForAnalysis);\n for (const f of multiFileFindings) { f.confidence = \"medium\"; }\n allFindings.push(...multiFileFindings);\n if (verbose && multiFileFindings.length > 0) {\n spinner.info(`Multi-file analyzer found ${multiFileFindings.length} issues`);\n }\n\n // 2f: Semgrep + Gitleaks (run in parallel, gracefully skip if not installed)\n spinner.text = \"Running external scanners...\";\n spinner.color = \"yellow\";\n\n // Resolve custom rules directory (shipped with xploitscan)\n const rulesDir = resolve(join(import.meta.dirname, \"../../rules\"));\n const fallbackRulesDir = resolve(join(dir, \"../rules\"));\n\n const [semgrepResult, gitleaksResult] = await Promise.allSettled([\n runSemgrep(dir, rulesDir).catch(() => runSemgrep(dir, fallbackRulesDir)).catch(() => ({ findings: [] as Finding[], available: false })),\n runGitleaks(dir).catch(() => ({ findings: [] as Finding[], available: false })),\n ]);\n\n const semgrep = semgrepResult.status === \"fulfilled\" ? semgrepResult.value : { findings: [], available: false };\n const gitleaks = gitleaksResult.status === \"fulfilled\" ? gitleaksResult.value : { findings: [], available: false };\n\n allFindings.push(...semgrep.findings);\n allFindings.push(...gitleaks.findings);\n\n if (verbose) {\n if (semgrep.available) {\n spinner.info(`Semgrep found ${semgrep.findings.length} issues`);\n } else {\n spinner.info(chalk.gray(\"Semgrep not installed — install with: pip install semgrep\"));\n }\n if (gitleaks.available) {\n spinner.info(`Gitleaks found ${gitleaks.findings.length} issues`);\n } else {\n spinner.info(chalk.gray(\"Gitleaks not installed — install with: brew install gitleaks\"));\n }\n }\n\n // Show install hints for missing tools (non-verbose, terminal only)\n if (!isSilent && !verbose) {\n const missing: string[] = [];\n if (!semgrep.available) missing.push(\"semgrep (pip install semgrep)\");\n if (!gitleaks.available) missing.push(\"gitleaks (brew install gitleaks)\");\n if (missing.length > 0) {\n spinner.info(chalk.gray(`Optional: install ${missing.join(\" and \")} for deeper scanning`));\n }\n }\n\n const staticCount = allFindings.length;\n spinner.text = `Static analysis found ${staticCount} issue${staticCount !== 1 ? \"s\" : \"\"}`;\n\n // Step 3: AI analysis (if enabled)\n if (useAI) {\n spinner.text = \"Running AI analysis...\";\n spinner.color = \"magenta\";\n\n try {\n const priorityFiles = files\n .map((path) => ({\n path,\n content: readFileContents(dir, path) ?? \"\",\n }))\n .filter((f) => f.content.length > 0 && f.content.length < 30_000)\n .filter((f) => {\n const isHighPriority =\n /(?:api|server|route|auth|middleware|webhook|payment|stripe|supabase)/i.test(f.path) ||\n allFindings.some((finding) => finding.file === f.path) ||\n /(?:query|execute|fetch|prisma|drizzle|mongoose)/i.test(f.content);\n return isHighPriority;\n })\n .slice(0, 20);\n\n if (priorityFiles.length > 0) {\n const aiFindings = await analyzeWithAI(priorityFiles, allFindings);\n allFindings.push(...aiFindings);\n\n if (verbose && aiFindings.length > 0) {\n spinner.info(`AI analysis found ${aiFindings.length} additional issues`);\n }\n }\n } catch (error) {\n if (error instanceof Error) {\n spinner.warn(`AI analysis skipped: ${error.message}`);\n }\n }\n } else if (!process.env.ANTHROPIC_API_KEY && !isSilent) {\n spinner.info(\n chalk.gray(\"Tip: Set ANTHROPIC_API_KEY for AI-powered contextual analysis\"),\n );\n }\n\n // Step 3b: AI false positive filter (if API key present)\n // Reviews regex findings with Claude Haiku to remove false positives.\n // Separate from --no-ai (which disables finding generation). This filter\n // only reviews existing findings, it doesn't generate new ones.\n let aiReviewed = false;\n let aiRemovedCount = 0;\n let aiTotalBefore = 0;\n let aiFilteredFindings: Array<{ finding: Finding; reason: string }> = [];\n if (process.env.ANTHROPIC_API_KEY && allFindings.length > 0) {\n spinner.text = \"AI reviewing findings for false positives...\";\n spinner.color = \"cyan\";\n try {\n const fileContentsMap = new Map<string, string>();\n for (const f of allFindings) {\n if (!fileContentsMap.has(f.file)) {\n const content = readFileContents(dir, f.file);\n if (content) fileContentsMap.set(f.file, content);\n }\n }\n const result = await filterFalsePositives(allFindings, fileContentsMap);\n aiReviewed = result.aiReviewed;\n aiRemovedCount = result.removedCount;\n aiTotalBefore = result.totalBefore;\n aiFilteredFindings = result.filteredFindings;\n if (result.removedCount > 0) {\n allFindings.length = 0;\n allFindings.push(...result.findings);\n }\n } catch {\n // AI filter error — keep all findings (graceful degradation)\n }\n }\n\n spinner.stop();\n\n // Step 4: Deduplicate findings (same file + line + similar rule)\n const seen = new Set<string>();\n const dedupedFindings = allFindings.filter((f) => {\n // Normalize: group by file:line and a simplified rule key\n const ruleKey = f.source === \"gitleaks\" ? `secret:${f.file}:${f.line}` : `${f.rule}:${f.file}:${f.line}`;\n if (seen.has(ruleKey)) return false;\n seen.add(ruleKey);\n return true;\n });\n\n // Step 5: Render results\n const result: ScanResult = {\n findings: dedupedFindings,\n filesScanned: files.length,\n duration: Date.now() - startTime,\n timestamp: new Date().toISOString(),\n directory: dir,\n };\n\n switch (format) {\n case \"json\":\n renderJsonReport(result);\n break;\n case \"sarif\":\n renderSarifReport(result);\n break;\n default:\n renderTerminalReport(result, fileContentsForAnalysis);\n break;\n }\n\n // Step 5b: Show AI review status\n if (aiReviewed && !isSilent) {\n console.log(\"\");\n if (aiRemovedCount > 0) {\n if (dedupedFindings.length > 0) {\n console.log(chalk.cyan(\"🤖 AI Review: \") + chalk.white(`${aiRemovedCount} false positive${aiRemovedCount !== 1 ? \"s\" : \"\"} removed · `) + chalk.green(`${dedupedFindings.length} real issue${dedupedFindings.length !== 1 ? \"s\" : \"\"} confirmed`));\n } else {\n console.log(chalk.cyan(\"🤖 AI Review: \") + chalk.green(`clean scan`) + chalk.gray(` (${aiRemovedCount} false positive${aiRemovedCount !== 1 ? \"s\" : \"\"} removed)`));\n }\n } else {\n console.log(chalk.cyan(\"🤖 AI Review: \") + chalk.green(`all ${dedupedFindings.length} finding${dedupedFindings.length !== 1 ? \"s\" : \"\"} confirmed real`));\n }\n // Show filtered findings if --verbose or if user wants detail\n if (verbose && aiFilteredFindings.length > 0) {\n console.log(chalk.gray(`\\n Filtered findings (AI determined these are false positives):`));\n for (const { finding: f, reason } of aiFilteredFindings) {\n console.log(chalk.gray(` ${f.severity.toUpperCase().padEnd(8)} ${f.rule} ${f.file}:${f.line}`));\n console.log(chalk.gray(` Reason: ${reason}`));\n }\n }\n }\n\n // Step 5c: Show upsell for free tier\n if (tier === \"free\" && !isSilent) {\n console.log(\"\");\n if (userPlan === \"anonymous\") {\n console.log(chalk.gray(\" Scanned with 30 free rules.\") + chalk.cyan(\" Log in to unlock all 158 rules →\") + chalk.bold(\" xploitscan auth login\"));\n } else {\n console.log(chalk.gray(\" Scanned with 30 rules.\") + chalk.cyan(\" Upgrade to Pro for all 158 rules →\") + chalk.bold(\" xploitscan upgrade\"));\n }\n console.log(\"\");\n }\n\n // Step 6: Upload results and increment usage (if authenticated)\n if (isAuthenticated()) {\n await Promise.allSettled([\n incrementUsage(),\n uploadScanResults({\n directory: dir,\n filesScanned: files.length,\n findings: dedupedFindings,\n duration: Date.now() - startTime,\n }),\n ]);\n }\n\n // Exit with error code if critical/high findings exist\n const hasCritical = dedupedFindings.some(\n (f) => f.severity === \"critical\" || f.severity === \"high\",\n );\n if (hasCritical) {\n process.exitCode = 1;\n }\n\n // Step 7: Watch mode\n if (options.watch) {\n const watchExclude = new Set([\"node_modules\", \".git\", \"dist\", \"build\", \".next\"]);\n const debounceMs = 500;\n let debounceTimer: ReturnType<typeof setTimeout> | null = null;\n let isScanning = false;\n\n console.log(chalk.cyan(\"\\nWatching for changes... (press Ctrl+C to stop)\"));\n\n fsWatch(dir, { recursive: true }, (_event, filename) => {\n if (!filename) return;\n\n // Skip excluded directories\n const parts = filename.split(\"/\");\n if (parts.some((p) => watchExclude.has(p))) return;\n\n console.log(chalk.gray(`File changed: ${filename}`));\n\n if (debounceTimer) clearTimeout(debounceTimer);\n\n debounceTimer = setTimeout(async () => {\n if (isScanning) return;\n isScanning = true;\n\n console.log(chalk.cyan(\"\\nRe-scanning...\\n\"));\n try {\n await scanCommand(directory, {\n ...options,\n watch: false, // prevent recursive watch\n });\n } catch (error) {\n console.error(chalk.red(`Re-scan error: ${error instanceof Error ? error.message : error}`));\n } finally {\n isScanning = false;\n console.log(chalk.cyan(\"\\nWatching for changes... (press Ctrl+C to stop)\"));\n }\n }, debounceMs);\n });\n\n // Keep the process alive\n await new Promise(() => {});\n }\n}\n","import fg from \"fast-glob\";\nimport ignore from \"ignore\";\nimport { readFileSync, existsSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\n\nconst SOURCE_EXTENSIONS = [\n \"js\", \"jsx\", \"ts\", \"tsx\", \"mjs\", \"cjs\",\n \"vue\", \"svelte\", \"astro\",\n \"py\", \"rb\", \"go\", \"rs\", \"java\", \"php\",\n \"swift\", \"kt\", \"kts\", \"dart\", \"cs\",\n \"c\", \"cpp\", \"h\",\n \"sh\", \"bash\", \"zsh\",\n \"env\", \"yaml\", \"yml\", \"toml\", \"json\", \"xml\",\n \"html\", \"htm\", \"sql\",\n \"properties\", \"ini\", \"cfg\", \"conf\",\n \"tf\", \"hcl\", \"dockerfile\",\n \"erb\", \"jinja\", \"j2\",\n \"gradle\",\n \"r\", \"lua\", \"pl\", \"pm\", \"ex\", \"exs\",\n \"ipynb\", \"md\",\n \"prisma\", \"plist\", \"pbxproj\", \"entitlements\", \"rules\", \"csv\",\n];\n\nconst SOURCE_FILENAMES = [\n \"Dockerfile\", \"Makefile\", \"Gemfile\", \"Rakefile\",\n \"package.json\", \"Cargo.toml\", \"go.mod\", \"requirements.txt\", \"Pipfile\",\n \"next.config.js\", \"next.config.mjs\", \"next.config.ts\", \"vercel.json\",\n \"firebase.json\", \".firebaserc\", \"firestore.rules\",\n \"app.json\", \"app.config.js\", \"eas.json\",\n \"wrangler.toml\", \"netlify.toml\",\n \"drizzle.config.ts\", \"drizzle.config.js\",\n \"Procfile\", \"Caddyfile\", \"nginx.conf\",\n \"AndroidManifest.xml\",\n];\n\nconst ALWAYS_IGNORE = [\n \"node_modules\",\n \".git\",\n \"dist\",\n \"build\",\n \".next\",\n \".nuxt\",\n \".svelte-kit\",\n \"vendor\",\n \"__pycache__\",\n \".venv\",\n \"venv\",\n \"coverage\",\n \".turbo\",\n \"*.min.js\",\n \"*.min.css\",\n \"*.map\",\n \"package-lock.json\",\n \"pnpm-lock.yaml\",\n \"yarn.lock\",\n];\n\nexport async function collectFiles(directory: string): Promise<string[]> {\n const ig = ignore.default();\n\n // Load .gitignore if present\n const gitignorePath = join(directory, \".gitignore\");\n if (existsSync(gitignorePath)) {\n const gitignoreContent = readFileSync(gitignorePath, \"utf-8\");\n ig.add(gitignoreContent);\n }\n\n // Load .xploitscanignore if present\n const xploitscanIgnorePath = join(directory, \".xploitscanignore\");\n if (existsSync(xploitscanIgnorePath)) {\n const xploitscanIgnoreContent = readFileSync(xploitscanIgnorePath, \"utf-8\");\n ig.add(xploitscanIgnoreContent);\n }\n\n // Always ignore these\n ig.add(ALWAYS_IGNORE);\n\n const patterns = SOURCE_EXTENSIONS.map((ext) => `**/*.${ext}`);\n // Also grab dotfiles like .env, .env.local, etc.\n patterns.push(\"**/.env*\");\n // Also grab files matched by name (Dockerfile, Makefile, etc.)\n for (const name of SOURCE_FILENAMES) {\n patterns.push(`**/${name}`);\n }\n\n const files = await fg(patterns, {\n cwd: directory,\n absolute: false,\n dot: true,\n onlyFiles: true,\n ignore: ALWAYS_IGNORE.map((p) => `**/${p}`),\n });\n\n // Apply .gitignore filtering\n return files.filter((file) => !ig.ignores(file));\n}\n\nexport function readFileContents(\n directory: string,\n filePath: string,\n): string | null {\n try {\n const fullPath = resolve(join(directory, filePath));\n if (!fullPath.startsWith(resolve(directory))) {\n throw new Error(\"Path traversal detected\");\n }\n return readFileSync(fullPath, \"utf-8\");\n } catch {\n return null;\n }\n}\n\nexport function getSnippet(\n content: string,\n line: number,\n contextLines = 2,\n): string {\n const lines = content.split(\"\\n\");\n const start = Math.max(0, line - 1 - contextLines);\n const end = Math.min(lines.length, line + contextLines);\n\n return lines\n .slice(start, end)\n .map((l, i) => {\n const lineNum = start + i + 1;\n const marker = lineNum === line ? \">\" : \" \";\n return `${marker} ${lineNum.toString().padStart(4)} | ${l}`;\n })\n .join(\"\\n\");\n}\n","import { cosmiconfig } from \"cosmiconfig\";\nimport { readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\nexport interface XploitScanRcConfig {\n rules?: {\n disable?: string[];\n severityOverride?: Record<string, string>;\n };\n scan?: {\n exclude?: string[];\n maxFileSize?: number;\n extensions?: string[];\n };\n output?: {\n format?: \"terminal\" | \"json\" | \"sarif\";\n verbose?: boolean;\n };\n watch?: {\n debounce?: number;\n exclude?: string[];\n };\n}\n\nexport interface XploitScanConfig {\n /** Glob patterns to exclude from scanning */\n exclude?: string[];\n /** Enable/disable AI analysis (requires ANTHROPIC_API_KEY) */\n ai?: boolean;\n /** Severity threshold to report: only show findings at or above this level */\n severity?: \"critical\" | \"high\" | \"medium\" | \"low\" | \"info\";\n /** Custom rules to disable by ID */\n disableRules?: string[];\n /** Severity overrides by rule ID */\n severityOverride?: Record<string, string>;\n /** Max file size in bytes */\n maxFileSize?: number;\n /** Additional file extensions to scan */\n extensions?: string[];\n /** Output format */\n format?: \"terminal\" | \"json\" | \"sarif\";\n /** Verbose output */\n verbose?: boolean;\n /** Watch mode config */\n watch?: {\n debounce?: number;\n exclude?: string[];\n };\n}\n\nconst defaults: XploitScanConfig = {\n exclude: [],\n ai: true,\n severity: \"low\",\n disableRules: [],\n};\n\nfunction loadXploitscanRc(directory: string): XploitScanRcConfig | null {\n try {\n const rcPath = join(directory, \".xploitscanrc\");\n const content = readFileSync(rcPath, \"utf-8\");\n return JSON.parse(content) as XploitScanRcConfig;\n } catch {\n return null;\n }\n}\n\nfunction mergeRcIntoConfig(\n base: XploitScanConfig,\n rc: XploitScanRcConfig,\n): XploitScanConfig {\n const merged = { ...base };\n\n // Merge rules\n if (rc.rules) {\n if (rc.rules.disable) {\n merged.disableRules = [\n ...new Set([...(merged.disableRules ?? []), ...rc.rules.disable]),\n ];\n }\n if (rc.rules.severityOverride) {\n merged.severityOverride = {\n ...(merged.severityOverride ?? {}),\n ...rc.rules.severityOverride,\n };\n }\n }\n\n // Merge scan settings\n if (rc.scan) {\n if (rc.scan.exclude) {\n merged.exclude = [\n ...new Set([...(merged.exclude ?? []), ...rc.scan.exclude]),\n ];\n }\n if (rc.scan.maxFileSize !== undefined) {\n merged.maxFileSize = rc.scan.maxFileSize;\n }\n if (rc.scan.extensions) {\n merged.extensions = [\n ...new Set([...(merged.extensions ?? []), ...rc.scan.extensions]),\n ];\n }\n }\n\n // Merge output settings (rc takes priority)\n if (rc.output) {\n if (rc.output.format !== undefined) {\n merged.format = rc.output.format;\n }\n if (rc.output.verbose !== undefined) {\n merged.verbose = rc.output.verbose;\n }\n }\n\n // Merge watch settings\n if (rc.watch) {\n merged.watch = {\n ...(merged.watch ?? {}),\n ...rc.watch,\n };\n }\n\n return merged;\n}\n\nexport async function loadConfig(\n directory: string,\n): Promise<XploitScanConfig> {\n const explorer = cosmiconfig(\"xploitscan\");\n\n let config = { ...defaults };\n\n try {\n const result = await explorer.search(directory);\n if (result && result.config) {\n config = { ...defaults, ...result.config };\n }\n } catch {\n // Config loading failed, use defaults\n }\n\n // Load .xploitscanrc and merge (rc takes priority)\n const rc = loadXploitscanRc(directory);\n if (rc) {\n config = mergeRcIntoConfig(config, rc);\n }\n\n return config;\n}\n","import type { CustomRule, Finding, RuleMatch } from \"../types.js\";\nimport { readFileContents, getSnippet } from \"../utils/files.js\";\n\n// ────────────────────────────────────────────\n// GLOBAL PRE-FILTERS\n// Reduces false positives before any rule runs\n// ────────────────────────────────────────────\n\n// Broad test/mock/fixture file detection\nconst TEST_FILE_PATTERN = /(?:\\.test\\.|\\.spec\\.|__tests__|__mocks__|\\.stories\\.|\\.story\\.|\\/test\\/|\\/tests\\/|\\/fixtures?\\/|\\/mocks?\\/|\\.mock\\.|test-utils|testing|\\.cy\\.|\\.e2e\\.)/i;\n\nfunction isTestFile(filePath: string): boolean {\n return TEST_FILE_PATTERN.test(filePath);\n}\n\n// Check if a match falls on a comment line (JS/TS/Python/Ruby/YAML/HTML)\nfunction isCommentLine(content: string, matchIndex: number): boolean {\n const lineStart = content.lastIndexOf(\"\\n\", matchIndex - 1) + 1;\n const lineText = content.substring(lineStart, content.indexOf(\"\\n\", matchIndex)).trimStart();\n return (\n lineText.startsWith(\"//\") ||\n lineText.startsWith(\"#\") ||\n lineText.startsWith(\"*\") ||\n lineText.startsWith(\"/*\") ||\n lineText.startsWith(\"<!--\") ||\n lineText.startsWith(\"'\") && lineText.length > 1 && /\\.(vb|bas)$/i.test(\"\") // VB comments\n );\n}\n\n// Check if a match is inside a string literal that's a fix/description message\n// (e.g., inside a findMatches callback or rule description)\nfunction isInsideFixMessage(content: string, matchIndex: number): boolean {\n const lineStart = content.lastIndexOf(\"\\n\", matchIndex - 1) + 1;\n const lineText = content.substring(lineStart, content.indexOf(\"\\n\", matchIndex));\n // Skip if the line looks like a fix suggestion string or rule description\n return /(?:fix|description|message|suggestion|hint|help|example|doc|comment)\\s*[:=(]/i.test(lineText) ||\n /return\\s*[\"'`].*(?:Use|Replace|Add|Move|Set|Enable|Disable|Never|Don't|Do not|Instead)/i.test(lineText);\n}\n\n// Context-aware matching: checks if mitigation exists within N lines after the match\nfunction hasMitigationNearby(content: string, matchIndex: number, mitigationPattern: RegExp, linesAhead: number = 5): boolean {\n const lines = content.split(\"\\n\");\n const matchLine = content.substring(0, matchIndex).split(\"\\n\").length - 1;\n const endLine = Math.min(matchLine + linesAhead, lines.length - 1);\n const nearbyContent = lines.slice(matchLine, endLine + 1).join(\"\\n\");\n return mitigationPattern.test(nearbyContent);\n}\n\n// Helper to find all regex matches with line numbers\n// Automatically skips matches on comment lines and fix messages\nfunction findMatches(\n content: string,\n pattern: RegExp,\n rule: Omit<CustomRule, \"check\">,\n filePath: string,\n fixTemplate?: (match: RegExpExecArray) => string,\n): RuleMatch[] {\n const matches: RuleMatch[] = [];\n const lines = content.split(\"\\n\");\n let m: RegExpExecArray | null;\n const re = new RegExp(pattern.source, pattern.flags.includes(\"g\") ? pattern.flags : `${pattern.flags}g`);\n\n while ((m = re.exec(content)) !== null) {\n // Skip matches on comment lines\n if (isCommentLine(content, m.index)) continue;\n // Skip matches inside fix/description strings\n if (isInsideFixMessage(content, m.index)) continue;\n\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n matches.push({\n rule: rule.id,\n title: rule.title,\n severity: rule.severity,\n category: rule.category,\n file: filePath,\n line: lineNum,\n snippet: getSnippet(content, lineNum),\n fix: fixTemplate?.(m),\n });\n }\n\n return matches;\n}\n\n// ────────────────────────────────────────────\n// RULE DEFINITIONS\n// ────────────────────────────────────────────\n\nexport const hardcodedSecrets: CustomRule = {\n id: \"VC001\",\n title: \"Hardcoded API Key or Secret\",\n severity: \"critical\",\n category: \"Secrets\",\n description: \"API keys, tokens, or secrets hardcoded in source code can be extracted by anyone with access to the code.\",\n check(content, filePath) {\n // Skip .env.example, template, test, and documentation files\n if (filePath.endsWith(\".example\") || filePath.endsWith(\".template\")) return [];\n if (isTestFile(filePath)) return [];\n if (filePath.match(/\\.(md|txt|rst|adoc)$/)) return [];\n\n const patterns = [\n // Generic API key patterns — require actual value assignment, not variable declarations\n /(?:api[_-]?key|apikey|api[_-]?secret)\\s*[:=]\\s*[\"'`]([a-zA-Z0-9_\\-]{20,})[\"'`]/gi,\n // AWS keys\n /(?:AKIA|ABIA|ACCA|ASIA)[A-Z0-9]{16}/g,\n // Stripe keys\n /(?:sk_live|pk_live|sk_test|pk_test)_[a-zA-Z0-9]{20,}/g,\n // Supabase anon/service keys (JWT format)\n /(?:supabase[_-]?(?:anon|service)[_-]?key|SUPABASE_(?:ANON|SERVICE_ROLE)_KEY)\\s*[:=]\\s*[\"'`](eyJ[a-zA-Z0-9_-]{50,})[\"'`]/gi,\n // OpenAI keys\n /sk-[a-zA-Z0-9]{20,}T3BlbkFJ[a-zA-Z0-9]{20,}/g,\n // Generic tokens in assignments — require standalone word and longer min length\n /(?:^|[\\s,({])(?:token|secret|password|passwd|pwd)\\s*[:=]\\s*[\"'`]([a-zA-Z0-9_\\-!@#$%^&*]{20,})[\"'`]/gim,\n // Private keys\n /-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----/g,\n // Database URLs with credentials\n /(?:postgres|mysql|mongodb(?:\\+srv)?):\\/\\/[^:]+:[^@]+@[^/\\s\"'`]+/gi,\n ];\n\n // Pattern-specific fix messages with risk context\n const fixMessages: string[] = [\n \"Move this API key to an environment variable. If this key has been committed, rotate it immediately — it may have already been scraped by bots.\",\n \"AWS access key detected — may grant full account access (EC2, S3, IAM, billing). Rotate immediately in AWS Console → IAM → Security Credentials. Use IAM roles or environment variables instead.\",\n \"Stripe key detected. sk_live_ keys can process real charges, issue refunds, and access customer payment data. sk_test_ keys are lower risk but should still not be committed. Rotate in Stripe Dashboard → Developers → API Keys.\",\n \"Supabase key detected. Service role keys bypass Row Level Security and grant full database read/write access. Move to a server-side environment variable immediately.\",\n \"OpenAI API key detected — grants full API access and can incur charges. Rotate at platform.openai.com → API Keys.\",\n \"Hardcoded token or password detected. Move to an environment variable and rotate the credential if it has been committed to version control.\",\n \"Private key found in source code. If this has been committed to version control, consider the key compromised — generate a new key pair and revoke the old one.\",\n \"Database credentials in connection string. An attacker with this URL has full database access. Move to an environment variable, restrict network access, and rotate the password.\",\n ];\n\n const matches: RuleMatch[] = [];\n for (let pi = 0; pi < patterns.length; pi++) {\n const pattern = patterns[pi];\n const rawMatches = findMatches(content, pattern, hardcodedSecrets, filePath, () => fixMessages[pi]);\n // Skip matches where the matched secret value is less than 12 characters\n for (const rm of rawMatches) {\n const lineText = content.split(\"\\n\")[rm.line - 1] || \"\";\n // Skip comment lines (additional guard beyond findMatches)\n const trimmed = lineText.trimStart();\n if (trimmed.startsWith(\"//\") || trimmed.startsWith(\"#\")) continue;\n // Extract the secret value from the match and skip short ones\n const secretMatch = lineText.match(/[:=]\\s*[\"'`]([^\"'`]*)[\"'`]/);\n if (secretMatch && secretMatch[1].length < 12) continue;\n matches.push(rm);\n }\n }\n return matches;\n },\n};\n\nexport const exposedEnvFile: CustomRule = {\n id: \"VC002\",\n title: \"Environment File May Be Committed\",\n severity: \"high\",\n category: \"Secrets\",\n description: \".env files containing secrets may be committed to version control.\",\n check(content, filePath) {\n // Only applies to .env files (not .env.example)\n if (!filePath.match(/\\.env(?:\\.[a-z]+)?$/) || filePath.includes(\"example\")) return [];\n\n const hasSecrets = /(?:KEY|SECRET|TOKEN|PASSWORD|PRIVATE|DATABASE_URL)\\s*=/i.test(content);\n if (!hasSecrets) return [];\n\n return [{\n rule: \"VC002\",\n title: exposedEnvFile.title,\n severity: \"high\",\n category: \"Secrets\",\n file: filePath,\n line: 1,\n snippet: getSnippet(content, 1),\n fix: 'Add \".env*\" to your .gitignore file and remove this file from git history with: git rm --cached ' + filePath,\n }];\n },\n};\n\nexport const missingAuthMiddleware: CustomRule = {\n id: \"VC003\",\n title: \"API Route Missing Authentication\",\n severity: \"high\",\n category: \"Authentication\",\n description: \"API routes without authentication checks allow unauthorized access.\",\n check(content, filePath) {\n // Only check API route files\n const isApiRoute = /(?:\\/api\\/|routes?\\/|controllers?\\/|endpoints?\\/)/.test(filePath) ||\n filePath.includes(\"server.\");\n if (!isApiRoute) return [];\n\n // Look for route handlers without auth checks\n const routePatterns = [\n // Express/Hono style\n /\\.(get|post|put|patch|delete)\\s*\\(\\s*[\"'`][^\"'`]+[\"'`]\\s*,\\s*(?:async\\s+)?\\(?(?:req|c|ctx)/gi,\n // Next.js API routes\n /export\\s+(?:async\\s+)?function\\s+(?:GET|POST|PUT|PATCH|DELETE)\\s*\\(/gi,\n ];\n\n // Skip test files\n if (isTestFile(filePath)) return [];\n // Skip non-code files\n if (filePath.match(/\\.(md|txt|rst|html|css|json|yaml|yml)$/)) return [];\n\n // Skip routes that are intentionally unauthenticated\n const isAuthRoute = /\\/auth\\/|\\/login|\\/signup|\\/register|\\/logout|\\/password\\/|\\/forgot|\\/reset/i.test(filePath);\n if (isAuthRoute) return [];\n\n // Skip webhook receivers (they use their own auth: HMAC, shared secrets, etc.)\n const isWebhookRoute = /\\/webhook/i.test(filePath);\n if (isWebhookRoute) return [];\n\n const authPatterns = [\n /auth/i, /session/i, /jwt/i, /bearer/i, /middleware/i,\n /getUser/i, /currentUser/i, /isAuthenticated/i, /requireAuth/i,\n /requireUser/i, /requireUserForApi/i,\n /clerk/i, /supabase\\.auth/i, /getServerSession/i, /getToken/i,\n /protect/i, /guard/i, /verifyToken/i, /validateToken/i, /withAuth/i,\n /passport/i, /firebase\\.auth/i, /cognito/i,\n /verifyCronSecret/i, /verifySecret/i, /checkApiKey/i,\n ];\n\n const hasAuth = authPatterns.some((p) => p.test(content));\n if (hasAuth) return [];\n\n const matches: RuleMatch[] = [];\n for (const pattern of routePatterns) {\n matches.push(\n ...findMatches(content, pattern, missingAuthMiddleware, filePath, () =>\n \"Add authentication middleware to protect this route. Check the user's session/token before processing the request.\"\n ),\n );\n }\n return matches;\n },\n};\n\nexport const supabaseNoRLS: CustomRule = {\n id: \"VC004\",\n title: \"Supabase Client Without Row Level Security\",\n severity: \"critical\",\n category: \"Authorization\",\n description: \"Using Supabase with the service role key or bypassing RLS exposes all database rows to any user.\",\n check(content, filePath) {\n const matches: RuleMatch[] = [];\n\n // Service role key used in client-side code\n if (\n /supabase_service_role|service_role_key/i.test(content) &&\n (/[\"']use client[\"']/.test(content) || filePath.match(/\\.(jsx|tsx|vue|svelte)$/))\n ) {\n matches.push(\n ...findMatches(\n content,\n /service_role/gi,\n supabaseNoRLS,\n filePath,\n () => \"Never expose the service_role key in client-side code. Use the anon key with RLS policies instead.\",\n ),\n );\n }\n\n // .rpc() or direct table access without .auth\n if (/createClient/i.test(content) && /\\.from\\(/.test(content)) {\n const hasRLSBypass = /\\.rpc\\(|auth\\.admin|service_role/i.test(content);\n if (hasRLSBypass) {\n matches.push(\n ...findMatches(\n content,\n /\\.rpc\\(|auth\\.admin/gi,\n { ...supabaseNoRLS, title: \"Supabase RLS Bypass Detected\" },\n filePath,\n () => \"Ensure RLS policies are enabled on all tables and avoid bypassing them with service_role or admin methods in user-facing code.\",\n ),\n );\n }\n }\n\n return matches;\n },\n};\n\nexport const stripeWebhookUnprotected: CustomRule = {\n id: \"VC005\",\n title: \"Unprotected Stripe Webhook Endpoint\",\n severity: \"critical\",\n category: \"Payment Security\",\n description: \"Stripe webhook endpoints without signature verification allow attackers to fake payment events.\",\n check(content, filePath) {\n // Only scan code files, never docs/README/markdown/lock files\n if (!filePath.match(/\\.(js|ts|jsx|tsx|py|rb|go|java|php)$/)) return [];\n if (isTestFile(filePath)) return [];\n\n // Must actually reference Stripe (not just generic \"webhook\")\n if (!/stripe/i.test(content)) return [];\n\n // Must have a webhook handler pattern (route definition or event handling)\n const hasStripeWebhookHandler =\n /(?:stripe.*webhook|webhook.*stripe)/i.test(content) ||\n /(?:checkout\\.session\\.completed|invoice\\.paid|payment_intent|customer\\.subscription)/i.test(content);\n if (!hasStripeWebhookHandler) return [];\n\n // Already has signature verification — safe\n if (/constructEvent|verifyHeader|stripe[_-]?signature|webhook[_-]?secret|STRIPE_WEBHOOK_SECRET/i.test(content)) return [];\n\n // Find the actual route handler, not every mention of \"webhook\"\n const handlerPatterns = [\n // POST handler that processes Stripe events\n /export\\s+(?:async\\s+)?function\\s+POST\\s*\\(/g,\n // Express-style Stripe webhook route\n /\\.(post|all)\\s*\\(\\s*[\"'`][^\"'`]*(?:stripe|webhook)[^\"'`]*[\"'`]/gi,\n // Event type checking without prior verification\n /(?:event\\.type|req\\.body\\.type)\\s*===?\\s*[\"'`](?:checkout|invoice|payment|customer)\\./g,\n ];\n\n const matches: RuleMatch[] = [];\n for (const pattern of handlerPatterns) {\n matches.push(\n ...findMatches(content, pattern, stripeWebhookUnprotected, filePath, () =>\n \"Verify the Stripe webhook signature using stripe.webhooks.constructEvent(body, sig, webhookSecret) to prevent forged payment events.\"\n ),\n );\n }\n return matches;\n },\n};\n\nexport const sqlInjection: CustomRule = {\n id: \"VC006\",\n title: \"Potential SQL Injection\",\n severity: \"critical\",\n category: \"Injection\",\n description: \"String concatenation or template literals in SQL queries allow attackers to execute arbitrary database commands.\",\n check(content, filePath) {\n const patterns = [\n // Template literals in SQL\n /(?:query|execute|raw|sql)\\s*\\(\\s*`[^`]*\\$\\{/gi,\n // String concatenation in SQL\n /(?:query|execute)\\s*\\(\\s*[\"'][^\"']*[\"']\\s*\\+/gi,\n // Direct variable interpolation\n /(?:SELECT|INSERT|UPDATE|DELETE|WHERE)\\s+.*\\$\\{(?!.*parameterized)/gi,\n ];\n\n const matches: RuleMatch[] = [];\n\n // Skip if using parameterized queries / prepared statements\n const usesParams = /\\?\\s*,|\\$\\d+|:[\\w]+|\\bprepare\\b|\\bplaceholder\\b/i.test(content);\n if (usesParams) return [];\n\n for (const pattern of patterns) {\n matches.push(\n ...findMatches(content, pattern, sqlInjection, filePath, () =>\n \"Use parameterized queries or prepared statements instead of string interpolation. Example: db.query('SELECT * FROM users WHERE id = ?', [userId])\"\n ),\n );\n }\n return matches;\n },\n};\n\nexport const xssVulnerability: CustomRule = {\n id: \"VC007\",\n title: \"Potential Cross-Site Scripting (XSS)\",\n severity: \"high\",\n category: \"Injection\",\n description: \"Rendering user input without sanitization allows attackers to inject malicious scripts.\",\n check(content, filePath) {\n // Skip if file is a sanitizer utility or already uses DOMPurify\n if (/(?:sanitize|purify|escape|xss)/i.test(filePath)) return [];\n if (/DOMPurify\\.sanitize|sanitizeHtml|xss\\(|escapeHtml/i.test(content)) return [];\n // Skip if the file imports or requires DOMPurify or a sanitize library anywhere\n if (/(?:import|require)\\s*\\(?.*(?:DOMPurify|dompurify|sanitize|sanitizer)/i.test(content)) return [];\n const patterns = [\n // React dangerouslySetInnerHTML\n /dangerouslySetInnerHTML\\s*=\\s*\\{\\s*\\{\\s*__html\\s*:/g,\n // Direct innerHTML assignment\n /\\.innerHTML\\s*=\\s*(?![\"'`]\\s*$)/gm,\n // document.write\n /document\\.write\\s*\\(/g,\n // v-html in Vue\n /v-html\\s*=/g,\n // {@html} in Svelte\n /\\{@html\\s/g,\n ];\n\n const matches: RuleMatch[] = [];\n for (const pattern of patterns) {\n const raw = findMatches(content, pattern, xssVulnerability, filePath, () =>\n \"Sanitize user input before rendering as HTML. Use a library like DOMPurify: DOMPurify.sanitize(userInput)\"\n );\n // Filter out innerHTML assignments that use only static strings (no user input)\n for (const m of raw) {\n const lineText = content.split(\"\\n\")[m.line - 1] || \"\";\n // Skip if innerHTML is assigned a pure string literal with no interpolation\n if (/\\.innerHTML\\s*=\\s*['\"]/.test(lineText) && !/\\$\\{/.test(lineText)) continue;\n // Skip if innerHTML is assigned a string concatenation of only literals (no variables from user input)\n if (/\\.innerHTML\\s*=\\s*['\"][^'\"]*['\"]\\s*$/.test(lineText)) continue;\n matches.push(m);\n }\n }\n return matches;\n },\n};\n\nexport const noRateLimiting: CustomRule = {\n id: \"VC008\",\n title: \"API Endpoint Without Rate Limiting\",\n severity: \"medium\",\n category: \"Availability\",\n description: \"API endpoints without rate limiting are vulnerable to abuse and denial-of-service attacks.\",\n check(content, filePath) {\n // Only check main server/app entry files\n const isEntryFile = /(?:server|app|index|main)\\.[jt]sx?$/.test(filePath) ||\n filePath.includes(\"middleware\");\n if (!isEntryFile) return [];\n\n // Check if this is a server file\n const isServer = /(?:express|hono|fastify|koa|next|createServer|listen\\()/i.test(content);\n if (!isServer) return [];\n\n // Check for rate limiting\n const hasRateLimit = /rate.?limit|throttle|express-rate-limit|@elysiajs\\/rate-limit|hono.*limiter/i.test(content);\n if (hasRateLimit) return [];\n\n return [{\n rule: \"VC008\",\n title: noRateLimiting.title,\n severity: \"medium\",\n category: \"Availability\",\n file: filePath,\n line: 1,\n snippet: getSnippet(content, 1),\n fix: \"Add rate limiting middleware to your server. For Express: npm install express-rate-limit. For other frameworks, check their rate limiting plugins.\",\n }];\n },\n};\n\nexport const corsWildcard: CustomRule = {\n id: \"VC009\",\n title: \"CORS Allows All Origins\",\n severity: \"medium\",\n category: \"Configuration\",\n description: \"Wildcard CORS (*) allows any website to make requests to your API, potentially exposing user data.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n const patterns = [\n /cors\\(\\s*\\)/g, // cors() with no options = allow all\n /origin\\s*:\\s*[\"'`]\\*[\"'`]/g,\n /[\"'`]Access-Control-Allow-Origin[\"'`]\\s*,\\s*[\"'`]\\*[\"'`]/g,\n /origin\\s*:\\s*true/g,\n ];\n\n const matches: RuleMatch[] = [];\n for (const pattern of patterns) {\n matches.push(\n ...findMatches(content, pattern, corsWildcard, filePath, () =>\n \"Restrict CORS to your specific frontend domain(s): cors({ origin: 'https://yourdomain.com' })\"\n ),\n );\n }\n return matches;\n },\n};\n\nexport const clientSideAuth: CustomRule = {\n id: \"VC010\",\n title: \"Client-Side Only Authorization\",\n severity: \"high\",\n category: \"Authorization\",\n description: \"Hiding UI elements based on roles without server-side checks lets attackers bypass restrictions using DevTools.\",\n check(content, filePath) {\n // Only check frontend component files\n if (!filePath.match(/\\.(jsx|tsx|vue|svelte)$/)) return [];\n\n const matches: RuleMatch[] = [];\n\n // Pattern: conditional rendering based on role/admin without server check\n const rolePatterns = [\n /\\{.*(?:isAdmin|role\\s*===?\\s*[\"'`]admin[\"'`]|user\\.role).*&&/gi,\n /v-if\\s*=\\s*[\"'`].*(?:isAdmin|role\\s*===?\\s*'admin')/gi,\n ];\n\n for (const pattern of rolePatterns) {\n // Only flag if the file has no server-side fetch for auth verification\n const hasServerCheck = /getServerSession|getUser|server|api\\/auth|middleware/i.test(content);\n if (hasServerCheck) continue;\n\n matches.push(\n ...findMatches(content, pattern, clientSideAuth, filePath, () =>\n \"Client-side role checks only hide UI — they don't prevent access. Always verify permissions on the server/API side too.\"\n ),\n );\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC011 – Secret in NEXT_PUBLIC_ env var\n// ────────────────────────────────────────────\n\nexport const nextPublicSecret: CustomRule = {\n id: \"VC011\",\n title: \"Secret in NEXT_PUBLIC_ Environment Variable\",\n severity: \"critical\",\n category: \"Secrets\",\n description: \"NEXT_PUBLIC_ variables are exposed to the browser. Secrets placed here are visible to anyone.\",\n check(content, filePath) {\n if (!filePath.match(/\\.env/) && !filePath.match(/next\\.config/)) return [];\n const patterns = [\n /NEXT_PUBLIC_[A-Z_]*(?:SECRET|KEY|TOKEN|PASSWORD|PRIVATE)[A-Z_]*\\s*=\\s*.+/gi,\n /NEXT_PUBLIC_[A-Z_]*(?:SUPABASE_SERVICE|CLERK_SECRET|STRIPE_SECRET)[A-Z_]*\\s*=\\s*.+/gi,\n ];\n const matches: RuleMatch[] = [];\n for (const p of patterns) {\n const raw = findMatches(content, p, nextPublicSecret, filePath, () =>\n \"Remove the NEXT_PUBLIC_ prefix. Only use NEXT_PUBLIC_ for values safe to expose in the browser.\"\n );\n // Filter out publishable/public keys that are DESIGNED to be client-side\n for (const m of raw) {\n const lineText = content.split(\"\\n\")[m.line - 1] || \"\";\n if (/PUBLISHABLE|ANON_KEY|PUBLIC_KEY/i.test(lineText)) continue;\n // Skip Clerk publishable keys (pk_test_, pk_live_)\n if (/CLERK_PUBLISHABLE/i.test(lineText)) continue;\n // Skip Stripe publishable keys\n if (/STRIPE_PUBLISHABLE/i.test(lineText)) continue;\n // Skip if the value is a placeholder (empty, pk_test_, etc.)\n if (/=\\s*[\"']?\\s*$|=\\s*[\"']?pk_(?:test|live)_[\"']?\\s*$/.test(lineText)) continue;\n matches.push(m);\n }\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC012 – Firebase config in client code\n// ────────────────────────────────────────────\n\nexport const firebaseClientConfig: CustomRule = {\n id: \"VC012\",\n title: \"Firebase Config with API Key in Client Code\",\n severity: \"medium\",\n category: \"Configuration\",\n description: \"Firebase config objects in client code expose your API key. While Firebase API keys aren't secret, they should be restricted in the Firebase console.\",\n check(content, filePath) {\n if (!/firebase/i.test(content)) return [];\n const patterns = [\n /firebaseConfig\\s*=\\s*\\{[^}]*apiKey\\s*:/gi,\n /initializeApp\\s*\\(\\s*\\{[^}]*apiKey\\s*:/gi,\n ];\n const matches: RuleMatch[] = [];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, firebaseClientConfig, filePath, () =>\n \"Move Firebase config to environment variables. Restrict the API key in Firebase Console > Project Settings > API restrictions.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC013 – Supabase anon key for admin ops\n// ────────────────────────────────────────────\n\nexport const supabaseAnonAdmin: CustomRule = {\n id: \"VC013\",\n title: \"Supabase Anon Key Used for Admin Operations\",\n severity: \"high\",\n category: \"Authorization\",\n description: \"Using the Supabase anon key for operations that require elevated privileges is insecure.\",\n check(content, filePath) {\n if (!/supabase/i.test(content)) return [];\n if (!/anon/i.test(content)) return [];\n if (/service_role/i.test(content)) return [];\n const patterns = [\n /supabase[^.]*\\.auth\\.admin/gi,\n /supabase[^.]*\\.rpc\\s*\\(/gi,\n ];\n const matches: RuleMatch[] = [];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, supabaseAnonAdmin, filePath, () =>\n \"Use the service_role key on the server side for admin operations. Never expose it to the client.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC014 – .env not in .gitignore\n// ────────────────────────────────────────────\n\nexport const envNotGitignored: CustomRule = {\n id: \"VC014\",\n title: \".env File Not in .gitignore\",\n severity: \"high\",\n category: \"Secrets\",\n description: \"If .env is not listed in .gitignore, secrets will be committed to version control.\",\n check(content, filePath) {\n if (!filePath.endsWith(\".gitignore\")) return [];\n if (/\\.env/i.test(content)) return [];\n return [{\n rule: \"VC014\", title: envNotGitignored.title, severity: \"high\" as const, category: \"Secrets\",\n file: filePath, line: 1, snippet: getSnippet(content, 1),\n fix: 'Add \".env*\" to your .gitignore file to prevent committing secrets.',\n }];\n },\n};\n\n// ────────────────────────────────────────────\n// VC015 – eval() / new Function()\n// ────────────────────────────────────────────\n\nexport const evalUsage: CustomRule = {\n id: \"VC015\",\n title: \"Use of eval() or Function Constructor\",\n severity: \"high\",\n category: \"Injection\",\n description: \"eval() and new Function() execute arbitrary code, creating severe injection risks. Common in AI-generated code.\",\n check(content, filePath) {\n if (filePath.includes(\"node_modules\") || filePath.includes(\".min.\")) return [];\n if (filePath.match(/(?:webpack|rollup|vite|jest|babel|tsup|esbuild)\\.config/i)) return [];\n if (isTestFile(filePath)) return [];\n // Skip files that are linters/scanners/rule engines (they reference eval in detection patterns)\n if (filePath.match(/(?:rules?|scanner|lint|check|detect|analyz)/i) && /\\.check\\s*\\(|findMatches/i.test(content)) return [];\n const patterns = [\n // Actual eval() calls — not in strings or regex patterns\n /\\beval\\s*\\(\\s*(?)/g,\n /new\\s+Function\\s*\\(\\s*(?![\"'`])/g,\n ];\n // Skip if eval is only in a string (e.g., devtool: 'eval-source-map')\n const hasEvalInString = /[\"'`]eval(?:-source-map|[\"'`])/i.test(content);\n if (hasEvalInString && !/\\beval\\s*\\([^)]*(?:req\\.|body\\.|input|params|user|data)/i.test(content)) return [];\n const matches: RuleMatch[] = [];\n for (const p of patterns) {\n const rawMatches = findMatches(content, p, evalUsage, filePath, () =>\n \"Replace eval() with JSON.parse() for data, or a proper parser for expressions. Never pass user input to eval().\"\n );\n // Skip matches on lines containing devtool or source-map config\n for (const rm of rawMatches) {\n const lineStart = content.lastIndexOf(\"\\n\", content.split(\"\\n\").slice(0, rm.line - 1).join(\"\\n\").length) + 1;\n const lineEnd = content.indexOf(\"\\n\", lineStart + 1);\n const lineText = content.substring(lineStart, lineEnd === -1 ? content.length : lineEnd);\n if (/devtool|source-map/i.test(lineText)) continue;\n // Skip if eval appears on a line with description/title/message keys (string literal context)\n if (/(?:description|title|message)\\s*[:=]/i.test(lineText)) continue;\n matches.push(rm);\n }\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC016 – Unvalidated redirect\n// ────────────────────────────────────────────\n\nexport const unvalidatedRedirect: CustomRule = {\n id: \"VC016\",\n title: \"Unvalidated Redirect\",\n severity: \"high\",\n category: \"Injection\",\n description: \"Redirecting users to URLs from untrusted input enables phishing attacks.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n // Skip if the file already validates redirect URLs\n if (/isAllowedRedirect|validateRedirect|isSafeRedirect|allowedDomains|trustedDomains|whitelist.*url|allowlist.*url/i.test(content)) return [];\n const patterns = [\n /window\\.location\\s*=\\s*(?![\"'`]https?:\\/\\/)/g,\n /window\\.location\\.href\\s*=\\s*(?![\"'`]https?:\\/\\/)/g,\n /window\\.location\\.assign\\s*\\(\\s*(?![\"'`]https?:\\/\\/)/g,\n /window\\.location\\.replace\\s*\\(\\s*(?![\"'`]https?:\\/\\/)/g,\n /res\\.redirect\\s*\\(\\s*(?:req\\.|params\\.|query\\.)/gi,\n ];\n const matches: RuleMatch[] = [];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, unvalidatedRedirect, filePath, () =>\n \"Validate redirect URLs against an allowlist of trusted domains. Never redirect to user-supplied URLs directly.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC017 – Insecure cookie settings\n// ────────────────────────────────────────────\n\nexport const insecureCookies: CustomRule = {\n id: \"VC017\",\n title: \"Insecure Cookie Settings\",\n severity: \"medium\",\n category: \"Configuration\",\n description: \"Cookies without httpOnly, secure, or sameSite flags are vulnerable to theft and CSRF attacks.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!/cookie/i.test(content)) return [];\n const setCookiePattern = /(?:set-cookie|setCookie|cookie\\s*=|res\\.cookie\\s*\\()/gi;\n if (!setCookiePattern.test(content)) return [];\n const hasHttpOnly = /httpOnly\\s*:\\s*true|httponly/i.test(content);\n const hasSecure = /secure\\s*:\\s*true|;\\s*secure/i.test(content);\n const hasSameSite = /sameSite\\s*:|samesite/i.test(content);\n const matches: RuleMatch[] = [];\n if (!hasHttpOnly || !hasSecure || !hasSameSite) {\n const missing: string[] = [];\n if (!hasHttpOnly) missing.push(\"httpOnly\");\n if (!hasSecure) missing.push(\"secure\");\n if (!hasSameSite) missing.push(\"sameSite\");\n matches.push(...findMatches(content, /(?:set-cookie|setCookie|cookie\\s*=|res\\.cookie\\s*\\()/gi, insecureCookies, filePath, () =>\n `Add missing cookie flags: ${missing.join(\", \")}. Example: { httpOnly: true, secure: true, sameSite: 'lax' }`\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC018 – Exposed auth provider secret key\n// ────────────────────────────────────────────\n\nexport const exposedAuthSecret: CustomRule = {\n id: \"VC018\",\n title: \"Exposed Clerk/Auth Secret Key\",\n severity: \"critical\",\n category: \"Secrets\",\n description: \"Auth provider secret keys (Clerk, Auth0, NextAuth) must never be in client-side code or NEXT_PUBLIC_ variables.\",\n check(content, filePath) {\n const isClientFile = filePath.match(/\\.(jsx|tsx|vue|svelte)$/) || /[\"']use client[\"']/.test(content);\n const isEnvFile = filePath.match(/\\.env/);\n if (!isClientFile && !isEnvFile) return [];\n const patterns: RegExp[] = [];\n if (isClientFile) {\n patterns.push(\n /CLERK_SECRET_KEY/g,\n /AUTH0_CLIENT_SECRET/g,\n /NEXTAUTH_SECRET/g,\n );\n }\n if (isEnvFile) {\n patterns.push(\n /NEXT_PUBLIC_CLERK_SECRET/gi,\n /NEXT_PUBLIC_AUTH0_SECRET/gi,\n /NEXT_PUBLIC_NEXTAUTH_SECRET/gi,\n );\n }\n const matches: RuleMatch[] = [];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, exposedAuthSecret, filePath, () =>\n \"Move this secret to a server-side environment variable (without the NEXT_PUBLIC_ prefix). Never expose auth secrets to the browser.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC019 – Insecure Electron BrowserWindow\n// ────────────────────────────────────────────\n\nexport const insecureElectronWindow: CustomRule = {\n id: \"VC019\",\n title: \"Insecure Electron BrowserWindow Configuration\",\n severity: \"high\",\n category: \"Configuration\",\n description: \"Electron BrowserWindow with nodeIntegration enabled, contextIsolation disabled, or sandbox disabled allows renderer processes to access Node.js APIs, enabling remote code execution.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!/BrowserWindow/i.test(content)) return [];\n const matches: RuleMatch[] = [];\n const patterns = [\n /nodeIntegration\\s*:\\s*true/g,\n /contextIsolation\\s*:\\s*false/g,\n /sandbox\\s*:\\s*false/g,\n /webSecurity\\s*:\\s*false/g,\n /allowRunningInsecureContent\\s*:\\s*true/g,\n ];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, insecureElectronWindow, filePath, (m) =>\n `Set ${m[0].split(\":\")[0].trim()}: ${m[0].includes(\"true\") ? \"false\" : \"true\"}. Enable contextIsolation, sandbox, and webSecurity; disable nodeIntegration and allowRunningInsecureContent.`\n ));\n }\n // Check for BrowserWindow without sandbox/webSecurity set at all\n if (/new\\s+BrowserWindow\\s*\\(/g.test(content)) {\n if (!/sandbox\\s*:/i.test(content)) {\n matches.push(...findMatches(content, /new\\s+BrowserWindow\\s*\\(/g, { ...insecureElectronWindow, title: \"Electron BrowserWindow Missing sandbox:true\" }, filePath, () =>\n \"Add sandbox: true to BrowserWindow webPreferences for defense in depth.\"\n ));\n }\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC020 – Missing Content Security Policy\n// ────────────────────────────────────────────\n\nexport const missingCSP: CustomRule = {\n id: \"VC020\",\n title: \"Missing Content Security Policy (CSP)\",\n severity: \"high\",\n category: \"Configuration\",\n description: \"Without a Content-Security-Policy header or meta tag, your app is vulnerable to XSS and data injection attacks.\",\n check(content, filePath) {\n // Check HTML files for missing CSP meta tag\n if (filePath.match(/\\.(html|htm)$/)) {\n if (!/Content-Security-Policy/i.test(content)) {\n return [{\n rule: \"VC020\", title: missingCSP.title, severity: \"high\" as const, category: \"Configuration\",\n file: filePath, line: 1, snippet: getSnippet(content, 1),\n fix: 'Add a CSP meta tag: <meta http-equiv=\"Content-Security-Policy\" content=\"default-src \\'self\\'; script-src \\'self\\'\">'\n }];\n }\n }\n // Skip Electron main process — CSP is typically set in the HTML file (already checked above)\n // Flagging main.ts creates false positives since CSP belongs in index.html for Electron apps\n return [];\n },\n};\n\n// ────────────────────────────────────────────\n// VC021 – IPC Handler Without Path Validation\n// ────────────────────────────────────────────\n\nexport const ipcPathTraversal: CustomRule = {\n id: \"VC021\",\n title: \"IPC/File Handler Without Path Validation\",\n severity: \"medium\",\n category: \"Injection\",\n description: \"IPC handlers that read or write files based on renderer-supplied paths without validation allow path traversal attacks, potentially exposing sensitive files like .ssh keys or .env files.\",\n check(content, filePath) {\n if (!/ipcMain\\.handle|ipcMain\\.on/i.test(content)) return [];\n const matches: RuleMatch[] = [];\n // Look for file read/write in IPC handlers without path validation\n const hasFileOps = /readFile|writeFile|readFileSync|writeFileSync|createReadStream|createWriteStream/i.test(content);\n if (!hasFileOps) return [];\n const hasPathValidation = /(?:path\\.resolve|path\\.normalize|startsWith|isAbsolute|\\.includes\\s*\\(\\s*[\"'`]\\.\\.[\"'`]\\s*\\)|allowedPaths|safePath|validatePath|sanitizePath)/i.test(content);\n if (!hasPathValidation) {\n matches.push(...findMatches(content, /ipcMain\\.(?:handle|on)\\s*\\(\\s*[\"'`][^\"'`]*(?:read|write|file|save|load|open|export)[^\"'`]*[\"'`]/gi, ipcPathTraversal, filePath, () =>\n \"Validate file paths in IPC handlers: ensure paths are within an allowed directory (e.g., app.getPath('userData')), reject paths containing '..', and block access to sensitive directories (.ssh, .env, etc).\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC022 – HTML Export Without Sanitization\n// ────────────────────────────────────────────\n\nexport const unsanitizedHTMLExport: CustomRule = {\n id: \"VC022\",\n title: \"HTML Export/Render Without Sanitization\",\n severity: \"critical\",\n category: \"Injection\",\n description: \"Generating HTML from user content without sanitization (e.g., DOMPurify) allows stored XSS attacks. Malicious content saved in documents could execute scripts when exported or previewed.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n // Skip if DOMPurify is imported or used anywhere in the file\n if (/DOMPurify|dompurify/i.test(content)) return [];\n // Skip React/Vue component files — JSX is not unsafe HTML concatenation\n if (filePath.match(/\\.(jsx|tsx|vue|svelte)$/) && !/innerHTML|document\\.write|\\.html\\s*=/i.test(content)) return [];\n // Skip files that have any sanitizer\n const hasSanitizer = /sanitize|escapeHtml|escapeHTML|xss|htmlEncode|purify/i.test(content);\n if (hasSanitizer) return [];\n // Only flag files that actually export/write HTML (not just template rendering)\n if (!/(?:export|download|save|write|send).*(?:html|HTML)|\\.innerHTML\\s*=|document\\.write|res\\.send\\s*\\(/i.test(content)) return [];\n const matches: RuleMatch[] = [];\n const htmlBuildPatterns = [\n /`<[^`]*\\$\\{[^}]*(?:content|title|body|text|name|message|description|input|value|data)[^}]*\\}[^`]*>`/gi,\n /[\"']<[^\"']*['\"]\\s*\\+\\s*(?:content|title|body|text|message|data|doc\\.|post\\.|article\\.)/gi,\n ];\n for (const p of htmlBuildPatterns) {\n matches.push(...findMatches(content, p, unsanitizedHTMLExport, filePath, () =>\n \"Sanitize user content before embedding in HTML. Use DOMPurify: DOMPurify.sanitize(content). For plain text, use a function to escape HTML entities (<, >, &, quotes).\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC023 – Prototype Pollution via Storage\n// ────────────────────────────────────────────\n\nexport const prototypePollution: CustomRule = {\n id: \"VC023\",\n title: \"Prototype Pollution Risk\",\n severity: \"high\",\n category: \"Injection\",\n description: \"Parsing JSON from localStorage, URL params, or external sources and merging it into objects without validation can lead to prototype pollution, allowing attackers to inject __proto__ or constructor properties.\",\n check(content, filePath) {\n const matches: RuleMatch[] = [];\n // JSON.parse from localStorage/sessionStorage without validation\n const storageParsePatterns = [\n /JSON\\.parse\\s*\\(\\s*(?:localStorage|sessionStorage)\\.getItem/g,\n /JSON\\.parse\\s*\\(\\s*window\\.localStorage/g,\n ];\n const hasValidation = /schema|validate|sanitize|whitelist|allowedKeys|pick\\(|Object\\.freeze|zod|yup|joi|ajv/i.test(content);\n if (hasValidation) return [];\n // Check for object spread/assign from parsed storage\n const hasUnsafeMerge = /Object\\.assign\\s*\\([^)]*JSON\\.parse|\\.\\.\\.JSON\\.parse|\\{.*\\.\\.\\.(?:stored|saved|cached|parsed|data)/i.test(content);\n if (hasUnsafeMerge) {\n matches.push(...findMatches(content, /Object\\.assign\\s*\\([^)]*JSON\\.parse|\\.\\.\\.JSON\\.parse/g, prototypePollution, filePath, () =>\n \"Validate parsed data against an expected schema before merging into objects. Use Object.freeze(), a validation library (Zod, Yup), or manually check for __proto__ and constructor keys.\"\n ));\n }\n for (const p of storageParsePatterns) {\n matches.push(...findMatches(content, p, prototypePollution, filePath, () =>\n \"Validate localStorage data against an expected schema before using it. Malicious extensions or XSS can modify localStorage values.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC024 – Missing File Size Limits\n// ────────────────────────────────────────────\n\nexport const missingFileSizeLimits: CustomRule = {\n id: \"VC024\",\n title: \"File Write/Save Without Size Limit\",\n severity: \"medium\",\n category: \"Availability\",\n description: \"File save or upload handlers without size validation can lead to denial-of-service via disk exhaustion or memory exhaustion.\",\n check(content, filePath) {\n if (!/(?:writeFile|save|upload|export)/i.test(filePath) && !/(?:writeFile|writeFileSync|createWriteStream)/i.test(content)) return [];\n // Look for file write operations in handlers\n const hasWriteOps = /(?:ipcMain|app\\.(?:post|put)|router\\.(?:post|put)).*(?:writeFile|save|export)/is.test(content) ||\n /(?:writeFile|writeFileSync)\\s*\\(/g.test(content);\n if (!hasWriteOps) return [];\n const hasSizeCheck = /(?:size|length|byteLength|bytes)\\s*(?:>|>=|<|<=|===)\\s*\\d|maxSize|MAX_SIZE|sizeLimit|content-length/i.test(content);\n if (hasSizeCheck) return [];\n return findMatches(content, /(?:writeFile|writeFileSync)\\s*\\(/g, missingFileSizeLimits, filePath, () =>\n \"Add file size validation before writing. Check content.length or Buffer.byteLength() against a maximum (e.g., 10MB) to prevent disk exhaustion.\"\n );\n },\n};\n\n// ────────────────────────────────────────────\n// VC025 – Unsanitized Export Filenames\n// ────────────────────────────────────────────\n\nexport const unsanitizedFilenames: CustomRule = {\n id: \"VC025\",\n title: \"Unsanitized Filename in File Operations\",\n severity: \"medium\",\n category: \"Injection\",\n description: \"Using user-supplied filenames without sanitization in file operations can enable path traversal, overwriting system files, or executing commands via special characters.\",\n check(content, filePath) {\n const matches: RuleMatch[] = [];\n // Look for file operations using variables as filenames\n const patterns = [\n /(?:writeFile|writeFileSync|createWriteStream|rename|copyFile)\\s*\\(\\s*(?:`[^`]*\\$\\{|[^\"'`\\s,]+\\s*\\+)/g,\n /(?:dialog\\.showSaveDialog|saveDialog).*(?:defaultPath|fileName)\\s*:\\s*(?![\"'`])/g,\n /\\.download\\s*=\\s*(?![\"'`])/g,\n ];\n const hasSanitization = /sanitize|cleanFilename|safeFilename|replace\\s*\\(\\s*\\/\\[.*\\]\\//i.test(content);\n if (hasSanitization) return [];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, unsanitizedFilenames, filePath, () =>\n \"Sanitize filenames before use: strip path separators (/ \\\\), special chars, and '..' sequences. Example: name.replace(/[^a-zA-Z0-9._-]/g, '_')\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC026 – Electron Navigation Not Restricted\n// ────────────────────────────────────────────\n\nexport const electronNavigationUnrestricted: CustomRule = {\n id: \"VC026\",\n title: \"Electron: External Navigation Not Blocked\",\n severity: \"medium\",\n category: \"Configuration\",\n description: \"Electron apps that don't block navigation to external URLs or new window creation are vulnerable to phishing and drive-by downloads. Malicious links in app content can redirect the entire app to an attacker's site.\",\n check(content, filePath) {\n if (!/BrowserWindow|electron/i.test(content)) return [];\n if (!/main|index/i.test(filePath)) return [];\n const hasNavBlock = /will-navigate|new-window|setWindowOpenHandler|webContents\\.on.*navigate/i.test(content);\n if (hasNavBlock) return [];\n if (/new\\s+BrowserWindow/i.test(content)) {\n return findMatches(content, /new\\s+BrowserWindow\\s*\\(/g, electronNavigationUnrestricted, filePath, () =>\n \"Block external navigation: win.webContents.on('will-navigate', (e, url) => { if (!url.startsWith('file://')) e.preventDefault(); }); and use setWindowOpenHandler to block new windows.\"\n );\n }\n return [];\n },\n};\n\n// ────────────────────────────────────────────\n// VC027 – Missing Security Meta Tags\n// ────────────────────────────────────────────\n\nexport const missingSecurityMeta: CustomRule = {\n id: \"VC027\",\n title: \"Missing Security Meta Tags / Headers\",\n severity: \"medium\",\n category: \"Configuration\",\n description: \"HTML pages without X-Content-Type-Options, referrer policy, or other security meta tags are more susceptible to MIME-sniffing attacks and information leakage.\",\n check(content, filePath) {\n if (!filePath.match(/\\.(html|htm)$/)) return [];\n const matches: RuleMatch[] = [];\n if (!/X-Content-Type-Options/i.test(content) && !/<meta[^>]*nosniff/i.test(content)) {\n matches.push({\n rule: \"VC027\", title: \"Missing X-Content-Type-Options Header\", severity: \"medium\" as const,\n category: \"Configuration\", file: filePath, line: 1, snippet: getSnippet(content, 1),\n fix: 'Add <meta http-equiv=\"X-Content-Type-Options\" content=\"nosniff\"> to prevent MIME-type sniffing.'\n });\n }\n if (!/referrer/i.test(content)) {\n matches.push({\n rule: \"VC027\", title: \"Missing Referrer Policy\", severity: \"medium\" as const,\n category: \"Configuration\", file: filePath, line: 1, snippet: getSnippet(content, 1),\n fix: 'Add <meta name=\"referrer\" content=\"no-referrer\"> or \"strict-origin-when-cross-origin\" to limit referrer leakage.'\n });\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC028 – Unvalidated API Parameters\n// ────────────────────────────────────────────\n\nexport const unvalidatedAPIParams: CustomRule = {\n id: \"VC028\",\n title: \"Unvalidated API Request Parameters\",\n severity: \"high\",\n category: \"Injection\",\n description: \"API requests constructed with unvalidated user input (API keys, model names, URLs) can be exploited for injection attacks or unauthorized access to different API models/endpoints.\",\n check(content, filePath) {\n const matches: RuleMatch[] = [];\n // API key passed without format validation\n const apiKeyPatterns = [\n /(?:apiKey|api_key|authorization)\\s*[:=]\\s*(?:req\\.body|req\\.query|params|input|formData|body)\\./gi,\n /headers\\s*:\\s*\\{[^}]*Authorization\\s*:\\s*(?![\"'`]Bearer\\s)/gi,\n ];\n const hasValidation = /validate|sanitize|regex|test\\(|match\\(|pattern|allowList|whitelist|enum|includes\\(/i.test(content);\n if (hasValidation) return [];\n // Model selection without allowlist\n if (/model\\s*[:=]\\s*(?:req\\.body|params|input|body)\\./i.test(content) || /model\\s*[:=]\\s*(?![\"'`])[a-z]/i.test(content)) {\n const hasModelValidation = /allowedModels|validModels|models\\s*\\.\\s*includes|model.*(?:===|!==|includes)/i.test(content);\n if (!hasModelValidation && /(?:openai|anthropic|claude|gpt|llm)/i.test(content)) {\n matches.push(...findMatches(content, /model\\s*[:=]\\s*(?:req\\.body|params|input|body)\\./gi, unvalidatedAPIParams, filePath, () =>\n \"Validate model selection against an allowlist of approved models. Example: const ALLOWED_MODELS = ['gpt-4', 'claude-3']; if (!ALLOWED_MODELS.includes(model)) throw new Error('Invalid model');\"\n ));\n }\n }\n for (const p of apiKeyPatterns) {\n matches.push(...findMatches(content, p, unvalidatedAPIParams, filePath, () =>\n \"Validate API key format before using it (e.g., check prefix and length). Never pass user-supplied API keys directly to third-party services without validation.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC029 – Unvalidated Event/Message Data\n// ────────────────────────────────────────────\n\nexport const unvalidatedEventData: CustomRule = {\n id: \"VC029\",\n title: \"Unvalidated Event or PostMessage Data\",\n severity: \"medium\",\n category: \"Injection\",\n description: \"Custom events, postMessage, or IPC message data used without type-checking can lead to injection attacks or unexpected behavior when malicious data is sent through event channels.\",\n check(content, filePath) {\n const matches: RuleMatch[] = [];\n // addEventListener('message') without origin check\n if (/addEventListener\\s*\\(\\s*[\"'`]message[\"'`]/i.test(content)) {\n if (!/event\\.origin|e\\.origin|message\\.origin/i.test(content)) {\n matches.push(...findMatches(content, /addEventListener\\s*\\(\\s*[\"'`]message[\"'`]/g, unvalidatedEventData, filePath, () =>\n \"Always verify event.origin in message event handlers to prevent cross-origin attacks. Example: if (event.origin !== 'https://trusted.com') return;\"\n ));\n }\n }\n // dispatchEvent with custom data inserted without validation\n if (/new\\s+CustomEvent\\s*\\(/i.test(content) || /ipcRenderer\\.send/i.test(content)) {\n const hasTypeCheck = /typeof\\s|instanceof|z\\.|schema|validate|Number\\.isFinite|parseInt|parseFloat/i.test(content);\n if (!hasTypeCheck) {\n matches.push(...findMatches(content, /new\\s+CustomEvent\\s*\\(/g, unvalidatedEventData, filePath, () =>\n \"Type-check custom event data before using it. Validate that data.detail contains expected types to prevent injection.\"\n ));\n }\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC030 – Insecure Deserialization\n// ────────────────────────────────────────────\n\nexport const insecureDeserialization: CustomRule = {\n id: \"VC030\",\n title: \"Insecure Deserialization\",\n severity: \"critical\",\n category: \"Injection\",\n description: \"Deserializing untrusted data (pickle, unserialize, yaml.load) can execute arbitrary code. Attackers craft malicious payloads to gain remote code execution.\",\n check(content, filePath) {\n const matches: RuleMatch[] = [];\n const patterns = [\n // Python pickle\n /pickle\\.loads?\\s*\\(/g,\n /cPickle\\.loads?\\s*\\(/g,\n // PHP unserialize\n /unserialize\\s*\\(/g,\n // Ruby Marshal\n /Marshal\\.load\\s*\\(/g,\n // YAML unsafe load (Python)\n /yaml\\.load\\s*\\([^)]*(?!Loader\\s*=\\s*yaml\\.SafeLoader)/g,\n /yaml\\.unsafe_load\\s*\\(/g,\n // Java ObjectInputStream\n /ObjectInputStream\\s*\\(/g,\n // Node.js node-serialize\n /serialize\\.unserialize\\s*\\(/g,\n ];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, insecureDeserialization, filePath, () =>\n \"Never deserialize untrusted data. Use JSON instead of pickle/Marshal/unserialize. For YAML, use yaml.safe_load(). Validate and sanitize all input before deserialization.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC031 – Hardcoded JWT Secret\n// ────────────────────────────────────────────\n\nexport const hardcodedJWTSecret: CustomRule = {\n id: \"VC031\",\n title: \"Hardcoded JWT Secret\",\n severity: \"critical\",\n category: \"Secrets\",\n description: \"JWT tokens signed with a hardcoded string secret can be forged by anyone who reads the source code.\",\n check(content, filePath) {\n if (filePath.endsWith(\".example\") || filePath.endsWith(\".template\") || filePath.includes(\"test\")) return [];\n const patterns = [\n /jwt\\.sign\\s*\\([^,]+,\\s*[\"'`][^\"'`]{3,}[\"'`]/g,\n /jwt\\.verify\\s*\\([^,]+,\\s*[\"'`][^\"'`]{3,}[\"'`]/g,\n /jsonwebtoken.*secret\\s*[:=]\\s*[\"'`][^\"'`]{3,}[\"'`]/gi,\n /JWT_SECRET\\s*[:=]\\s*[\"'`][^\"'`]{3,}[\"'`]/g,\n ];\n const matches: RuleMatch[] = [];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, hardcodedJWTSecret, filePath, () =>\n \"Move JWT secret to an environment variable: jwt.sign(payload, process.env.JWT_SECRET). Use a strong, random secret (256+ bits).\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC032 – Missing HTTPS Enforcement\n// ────────────────────────────────────────────\n\nexport const missingHTTPS: CustomRule = {\n id: \"VC032\",\n title: \"Missing HTTPS Enforcement\",\n severity: \"high\",\n category: \"Configuration\",\n description: \"HTTP URLs in production code, missing HSTS headers, or insecure redirect configurations expose data to man-in-the-middle attacks.\",\n check(content, filePath) {\n if (filePath.endsWith(\".example\") || filePath.includes(\"test\") || filePath.includes(\"README\")) return [];\n if (filePath.match(/\\.(md|txt)$/)) return [];\n const matches: RuleMatch[] = [];\n // Skip standard XML/HTML doctypes, DTDs, schema URIs, namespace URLs\n if (/<!DOCTYPE|xmlns|\\.dtd|\\.xsd/i.test(content) && !/fetch|axios|request|http\\.get/i.test(content)) return [];\n // Hardcoded http:// URLs to non-local hosts (excluding standard schema/namespace URIs)\n const httpPattern = /[\"'`]http:\\/\\/(?!localhost|127\\.0\\.0\\.1|0\\.0\\.0\\.0|192\\.168\\.|10\\.|172\\.(?:1[6-9]|2\\d|3[01])\\.|www\\.w3\\.org|www\\.apple\\.com\\/DTDs|schemas?\\.|xml\\.org|purl\\.org|ns\\.adobe|xmlpull\\.org|java\\.sun\\.com)[^\"'`\\s]+[\"'`]/g;\n const rawMatches = findMatches(content, httpPattern, missingHTTPS, filePath, () =>\n \"Use https:// instead of http:// for all production URLs. Add HSTS header: Strict-Transport-Security: max-age=31536000; includeSubDomains\"\n );\n // Skip DOCTYPE/DTD URLs\n for (const rm of rawMatches) {\n const lineStart = content.lastIndexOf(\"\\n\", content.split(\"\\n\").slice(0, rm.line - 1).join(\"\\n\").length) + 1;\n const lineEnd = content.indexOf(\"\\n\", lineStart + 1);\n const matchText = content.substring(lineStart, lineEnd === -1 ? content.length : lineEnd);\n if (/DTD|DOCTYPE|w3\\.org|apple\\.com\\/DTDs/i.test(matchText)) continue;\n matches.push(rm);\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC033 – Exposed Debug/Dev Mode\n// ────────────────────────────────────────────\n\nexport const exposedDebugMode: CustomRule = {\n id: \"VC033\",\n title: \"Debug/Development Mode Exposed\",\n severity: \"high\",\n category: \"Configuration\",\n description: \"Debug mode, verbose logging, or development configuration left in production code exposes internal details and may enable debug endpoints.\",\n check(content, filePath) {\n if (filePath.includes(\"test\") || filePath.endsWith(\".example\") || filePath.includes(\"node_modules\")) return [];\n if (filePath.match(/\\.env\\.development$/)) return []; // Expected in dev env files\n const matches: RuleMatch[] = [];\n const patterns = [\n // Debug flags set to true\n /DEBUG\\s*[:=]\\s*(?:true|1|[\"'`]true[\"'`]|[\"'`]\\*[\"'`])/g,\n // Django DEBUG\n /DEBUG\\s*=\\s*True/g,\n // Flask/Express debug mode\n /app\\.debug\\s*=\\s*True/g,\n /app\\.run\\s*\\([^)]*debug\\s*=\\s*True/g,\n // Source maps in production\n /devtool\\s*:\\s*[\"'`](?:eval|cheap|source-map|inline-source-map)[\"'`]/g,\n ];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, exposedDebugMode, filePath, () =>\n \"Disable debug mode in production. Use environment variables: DEBUG = process.env.NODE_ENV !== 'production'. Remove source maps from production builds.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC034 – Insecure Randomness\n// ────────────────────────────────────────────\n\nexport const insecureRandomness: CustomRule = {\n id: \"VC034\",\n title: \"Insecure Randomness for Security-Sensitive Values\",\n severity: \"high\",\n category: \"Cryptography\",\n description: \"Math.random() is not cryptographically secure. Using it for tokens, session IDs, passwords, or OTPs makes them predictable.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n // Only flag if Math.random is actually used (not just mentioned)\n if (!/Math\\.random\\s*\\(\\s*\\)/i.test(content)) return [];\n const matches: RuleMatch[] = [];\n // Math.random used in security-critical assignments (narrower: require direct assignment on same line)\n const securityContext = /(?:token|secret|session|password|otp|nonce|salt|csrf|auth)\\s*[:=]\\s*.*Math\\.random/gi;\n const rawMatches = findMatches(content, securityContext, insecureRandomness, filePath, () =>\n \"Use crypto.randomUUID() or crypto.getRandomValues() for security-sensitive values. Math.random() is predictable.\"\n );\n // Skip non-security uses of Math.random (e.g., UI-related randomness)\n const nonSecurityVarNames = /(?:id|key|color|index|delay|position|size|width|height|offset|opacity|rotation|animation|random(?!.*(?:token|secret|key|password)))\\s*[:=]\\s*.*Math\\.random/i;\n for (const rm of rawMatches) {\n const lineText = content.split(\"\\n\")[rm.line - 1] || \"\";\n if (nonSecurityVarNames.test(lineText)) continue;\n matches.push(rm);\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC035 – Open Redirect via URL Params\n// ────────────────────────────────────────────\n\nexport const openRedirectParams: CustomRule = {\n id: \"VC035\",\n title: \"Open Redirect via URL Parameters\",\n severity: \"high\",\n category: \"Injection\",\n description: \"Redirect parameters like ?redirect_url=, ?return_to=, ?next= passed directly to redirects enable phishing attacks.\",\n check(content, filePath) {\n const matches: RuleMatch[] = [];\n const patterns = [\n // Reading redirect-like query params and using in redirect\n /(?:redirect_url|redirect_uri|return_to|return_url|next|callback_url|continue|goto|target|dest|destination|forward|redir)\\s*(?:=|:)\\s*(?:req\\.query|req\\.params|searchParams|query|params)\\./gi,\n /redirect\\s*\\(\\s*(?:req\\.query|req\\.params|searchParams\\.get)\\s*\\(\\s*[\"'`](?:redirect|return|next|callback|url|goto)/gi,\n ];\n const hasValidation = /allowedUrls|allowedDomains|allowedHosts|validUrl|safeDomain|whitelist|startsWith.*https|new URL.*hostname/i.test(content);\n if (hasValidation) return [];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, openRedirectParams, filePath, () =>\n \"Validate redirect URLs against an allowlist of trusted domains. Use: const url = new URL(input); if (!ALLOWED_HOSTS.includes(url.hostname)) reject.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC036 – Missing Error Boundary (React)\n// ────────────────────────────────────────────\n\nexport const missingErrorBoundary: CustomRule = {\n id: \"VC036\",\n title: \"React App Missing Error Boundary\",\n severity: \"medium\",\n category: \"Configuration\",\n description: \"React apps without error boundaries display raw stack traces and component tree info to users when crashes occur, leaking internal details.\",\n check(content, filePath) {\n // Only check App.tsx, App.jsx, app.tsx, app.jsx files\n const basename = filePath.split(\"/\").pop() || \"\";\n if (!/^[Aa]pp\\.[jt]sx$/i.test(basename)) return [];\n // Skip .ts files (non-JSX)\n if (filePath.match(/\\.ts$/)) return [];\n // Only trigger if file contains JSX (< followed by an uppercase letter)\n if (!/<[A-Z]/.test(content)) return [];\n // Skip Electron main process files\n if (/(?:BrowserWindow|electron|ipcMain|app\\.on\\s*\\(\\s*[\"']ready)/i.test(content)) return [];\n if (/\\/main\\//.test(filePath) && !/react-dom|createRoot/i.test(content)) return [];\n // Must have React imports and render calls\n if (!/(?:import.*react|from\\s+['\"]react|require.*react)/i.test(content)) return [];\n if (!/(?:createRoot|ReactDOM\\.render)/i.test(content)) return [];\n const hasErrorBoundary = /ErrorBoundary|componentDidCatch|getDerivedStateFromError|error-boundary/i.test(content);\n if (hasErrorBoundary) return [];\n if (/createRoot|ReactDOM\\.render/i.test(content)) {\n return [{\n rule: \"VC036\", title: missingErrorBoundary.title, severity: \"medium\" as const, category: \"Configuration\",\n file: filePath, line: 1, snippet: getSnippet(content, 1),\n fix: \"Wrap your app in an ErrorBoundary component to catch rendering errors gracefully. Use react-error-boundary or create a class component with componentDidCatch.\"\n }];\n }\n return [];\n },\n};\n\n// ────────────────────────────────────────────\n// VC037 – Exposed Stack Traces in API\n// ────────────────────────────────────────────\n\nexport const exposedStackTraces: CustomRule = {\n id: \"VC037\",\n title: \"Stack Traces Exposed in API Responses\",\n severity: \"medium\",\n category: \"Information Leakage\",\n description: \"Returning error.stack or detailed error messages in API responses reveals internal code paths, file structure, and dependencies to attackers.\",\n check(content, filePath) {\n const isApiFile = /(?:\\/api\\/|routes?\\/|controllers?\\/|server\\.|middleware)/i.test(filePath);\n if (!isApiFile) return [];\n const matches: RuleMatch[] = [];\n const patterns = [\n // Sending stack trace in response\n /(?:res\\.(?:json|send|status)|c\\.json|return.*json)\\s*\\([^)]*(?:err\\.stack|error\\.stack|e\\.stack)/gi,\n /(?:res\\.(?:json|send|status)|c\\.json)\\s*\\([^)]*(?:err\\.message|error\\.message|e\\.message)/gi,\n // Express-style error with stack\n /(?:message|error)\\s*:\\s*(?:err|error|e)\\.(?:stack|message)/gi,\n ];\n const hasEnvCheck = /process\\.env\\.NODE_ENV\\s*(?:===|!==)\\s*[\"'`]production[\"'`]|NODE_ENV/i.test(content);\n if (hasEnvCheck) return []; // They're conditionally showing errors\n for (const p of patterns) {\n matches.push(...findMatches(content, p, exposedStackTraces, filePath, () =>\n \"Never expose error.stack or error.message to clients in production. Return generic error messages: { error: 'Something went wrong' }. Log details server-side only.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC038 – Insecure File Upload Type\n// ────────────────────────────────────────────\n\nexport const insecureFileUpload: CustomRule = {\n id: \"VC038\",\n title: \"Insecure File Upload Validation\",\n severity: \"high\",\n category: \"Injection\",\n description: \"File uploads validated only by extension (not MIME type or content) allow attackers to upload executable files disguised as images or documents.\",\n check(content, filePath) {\n if (!/upload|multer|formidable|busboy|multipart/i.test(content)) return [];\n const matches: RuleMatch[] = [];\n // Check for extension-only validation\n const hasExtCheck = /\\.(?:endsWith|match|test)\\s*\\([^)]*(?:\\.jpg|\\.png|\\.pdf|\\.doc|ext)/i.test(content);\n const hasMimeCheck = /mimetype|content-type|file\\.type|mime|magic\\.detect|file-type/i.test(content);\n if (hasExtCheck && !hasMimeCheck) {\n matches.push(...findMatches(content, /upload|multer|formidable|busboy/gi, insecureFileUpload, filePath, () =>\n \"Validate file uploads by MIME type AND magic bytes, not just extension. Use the 'file-type' package to detect actual file type from content. Also enforce size limits.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC039 – Missing Dependency Lock File\n// ────────────────────────────────────────────\n\nexport const missingLockFile: CustomRule = {\n id: \"VC039\",\n title: \"Missing Dependency Lock File\",\n severity: \"medium\",\n category: \"Supply Chain\",\n description: \"Without a lockfile (package-lock.json, pnpm-lock.yaml, yarn.lock), dependency versions are unpinned and vulnerable to supply chain attacks via version substitution.\",\n check(content, filePath) {\n // Only check .gitignore for lock files being ignored\n if (!filePath.endsWith(\".gitignore\")) return [];\n const ignoresLock = /package-lock\\.json|pnpm-lock\\.yaml|yarn\\.lock/i.test(content);\n if (ignoresLock) {\n return findMatches(content, /(?:package-lock\\.json|pnpm-lock\\.yaml|yarn\\.lock)/gi, missingLockFile, filePath, () =>\n \"Remove the lockfile from .gitignore. Lockfiles should be committed to prevent supply chain attacks. They ensure exact versions are installed across all environments.\"\n );\n }\n return [];\n },\n};\n\n// ────────────────────────────────────────────\n// VC040 – Exposed .git Directory\n// ────────────────────────────────────────────\n\nexport const exposedGitDir: CustomRule = {\n id: \"VC040\",\n title: \"Exposed .git Directory via Web Server\",\n severity: \"critical\",\n category: \"Information Leakage\",\n description: \"Web server configs that don't block access to .git directories expose your entire source code, commit history, secrets, and credentials.\",\n check(content, filePath) {\n // Check web server configs\n if (!filePath.match(/(?:nginx|apache|httpd|caddy|\\.htaccess|vercel\\.json|netlify\\.toml|server\\.[jt]s)/i)) return [];\n // For static file servers, check they block .git\n if (/(?:static|serve|express\\.static|serveStatic|public)/i.test(content)) {\n const blocksGit = /\\.git|dotfiles|hidden/i.test(content);\n if (!blocksGit) {\n return findMatches(content, /(?:static|serve|express\\.static|serveStatic)\\s*\\(/g, exposedGitDir, filePath, () =>\n \"Block access to .git and other dotfiles in your static file server config. For Express: app.use('/.git', (req, res) => res.status(403).end()). For Nginx: location ~ /\\\\.git { deny all; }\"\n );\n }\n }\n return [];\n },\n};\n\n// ────────────────────────────────────────────\n// VC041 – Server-Side Request Forgery (SSRF)\n// ────────────────────────────────────────────\n\nexport const ssrfVulnerability: CustomRule = {\n id: \"VC041\",\n title: \"Potential Server-Side Request Forgery (SSRF)\",\n severity: \"critical\",\n category: \"Injection\",\n description: \"Fetching URLs from user input without validation allows attackers to access internal services, cloud metadata endpoints (169.254.169.254), and private networks.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n // Only check server-side files\n const isServerFile = /(?:\\/api\\/|routes?\\/|controllers?\\/|server\\.|handler|middleware)/i.test(filePath);\n if (!isServerFile) return [];\n const matches: RuleMatch[] = [];\n // Only flag direct user input passed to fetch — narrower pattern\n const patterns = [\n /(?:fetch|axios\\.get|axios\\.post|axios|got|request|http\\.get|https\\.get)\\s*\\(\\s*(?:req\\.(?:body|query|params))\\./gi,\n ];\n const hasValidation = /allowedHosts|allowedDomains|allowedUrls|safeDomain|whitelist|urlValidator|new URL.*hostname.*includes|isAllowedUrl|validateUrl|isValidUrl/i.test(content);\n if (hasValidation) return [];\n for (const p of patterns) {\n const rawMatches = findMatches(content, p, ssrfVulnerability, filePath, () =>\n \"Validate URLs against an allowlist before fetching. Block internal IPs: 127.0.0.1, 10.x, 172.16-31.x, 192.168.x, 169.254.169.254 (cloud metadata). Use: const url = new URL(input); if (!ALLOWED_HOSTS.includes(url.hostname)) throw new Error('Blocked');\"\n );\n // Skip if the variable passed to fetch is defined as a constant string earlier in the file\n for (const rm of rawMatches) {\n const lineText = content.split(\"\\n\")[rm.line - 1] || \"\";\n const varMatch = lineText.match(/(?:fetch|axios\\.\\w+|got|request|https?\\.get)\\s*\\(\\s*(\\w+)/);\n if (varMatch) {\n const varName = varMatch[1];\n // Check if this variable is defined as a const string literal earlier in the file\n const constDef = new RegExp(`const\\\\s+${varName}\\\\s*=\\\\s*[\"'\\`]https?://`, \"i\");\n if (constDef.test(content)) continue;\n }\n matches.push(rm);\n }\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC042 – Mass Assignment\n// ────────────────────────────────────────────\n\nexport const massAssignment: CustomRule = {\n id: \"VC042\",\n title: \"Mass Assignment Vulnerability\",\n severity: \"high\",\n category: \"Authorization\",\n description: \"Spreading or assigning request body directly into database models allows attackers to set fields they shouldn't (e.g., isAdmin, role, verified).\",\n check(content, filePath) {\n const isApiFile = /(?:\\/api\\/|routes?\\/|controllers?\\/|server\\.|handler)/i.test(filePath);\n if (!isApiFile) return [];\n const matches: RuleMatch[] = [];\n const patterns = [\n // Object.assign(model, req.body)\n /Object\\.assign\\s*\\(\\s*(?:user|account|profile|record|doc|model|entity)[^,]*,\\s*(?:req\\.body|body|input|data)\\s*\\)/gi,\n // Spread req.body into create/update\n /(?:create|update|insert|save|findOneAndUpdate|updateOne|upsert)\\s*\\(\\s*\\{[^}]*\\.\\.\\.(?:req\\.body|body|input|data)/gi,\n // Direct req.body into DB\n /(?:create|insert|save)\\s*\\(\\s*(?:req\\.body|body)\\s*\\)/gi,\n ];\n const hasSanitization = /pick\\(|omit\\(|allowedFields|sanitize|whitelist|permit|strong_params/i.test(content);\n if (hasSanitization) return [];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, massAssignment, filePath, () =>\n \"Never pass req.body directly to database operations. Explicitly pick allowed fields: const { name, email } = req.body; await db.create({ name, email });\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC043 – Timing Attack on Comparison\n// ────────────────────────────────────────────\n\nexport const timingAttack: CustomRule = {\n id: \"VC043\",\n title: \"Timing-Unsafe Secret Comparison\",\n severity: \"medium\",\n category: \"Cryptography\",\n description: \"Using === to compare secrets, tokens, or hashes leaks information via timing side-channels. Attackers can determine the correct value one character at a time.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n const matches: RuleMatch[] = [];\n // Direct comparison of secrets/tokens/hashes\n const patterns = [\n /(?:token|secret|hash|digest|signature|hmac|apiKey|api_key)\\s*(?:===|!==)\\s*(?:req\\.|body\\.|params\\.|query\\.|input)/gi,\n /(?:^|[^.\\w])(?:req\\.|body\\.|params\\.|query\\.|input)[\\w.]*(?:token|secret|hash|digest|signature|hmac)\\s*(?:===|!==)/gim,\n ];\n const hasTimingSafe = /timingSafeEqual|constantTimeEqual|safeCompare|secureCompare/i.test(content);\n if (hasTimingSafe) return [];\n for (const p of patterns) {\n const raw = findMatches(content, p, timingAttack, filePath, () =>\n \"Use crypto.timingSafeEqual() for comparing secrets: crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b)). This prevents timing-based side-channel attacks.\"\n );\n // Skip typeof type guards — they're not secret comparisons\n for (const m of raw) {\n const lineText = content.split(\"\\n\")[m.line - 1] || \"\";\n if (/typeof\\s/.test(lineText)) continue;\n matches.push(m);\n }\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC044 – Log Injection\n// ────────────────────────────────────────────\n\nexport const logInjection: CustomRule = {\n id: \"VC044\",\n title: \"Potential Log Injection\",\n severity: \"medium\",\n category: \"Injection\",\n description: \"Logging unsanitized user input allows attackers to forge log entries, inject malicious content, or exploit log aggregation systems via newlines and special characters.\",\n check(content, filePath) {\n if (filePath.includes(\"test\") || filePath.includes(\"mock\")) return [];\n const isServerFile = /(?:\\/api\\/|routes?\\/|controllers?\\/|server\\.|middleware|handler)/i.test(filePath);\n if (!isServerFile) return [];\n const matches: RuleMatch[] = [];\n // console.log/warn/error with req.body/query/params directly\n const patterns = [\n /console\\.(?:log|warn|error|info)\\s*\\([^)]*(?:req\\.body|req\\.query|req\\.params|req\\.headers)\\s*\\)/gi,\n /(?:logger|log)\\.(?:info|warn|error|debug)\\s*\\([^)]*(?:req\\.body|req\\.query|req\\.params)\\s*\\)/gi,\n ];\n const hasSanitization = /sanitize|escape|JSON\\.stringify|replace.*\\\\n/i.test(content);\n if (hasSanitization) return [];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, logInjection, filePath, () =>\n \"Sanitize user input before logging: strip newlines and control characters. Use JSON.stringify() or a structured logger (e.g., pino, winston) that escapes values automatically.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC045 – Weak Password Requirements\n// ────────────────────────────────────────────\n\nexport const weakPasswordRequirements: CustomRule = {\n id: \"VC045\",\n title: \"Weak Password Requirements\",\n severity: \"high\",\n category: \"Authentication\",\n description: \"Registration or password-change endpoints without minimum length or complexity validation allow weak passwords that are easily brute-forced.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!/(?:password|passwd|pwd)/i.test(content)) return [];\n if (!/(?:register|signup|sign.up|createUser|create.user|changePassword|resetPassword|set.password)/i.test(content) &&\n !/(?:\\/api\\/|routes?\\/|controllers?\\/)/i.test(filePath)) return [];\n // Check for validation anywhere in the file — broader detection of mitigation\n const hasValidation = /(?:password|pwd).*(?:\\.length|minLength|minlength|min_length)\\s*(?:>=?|<|>)\\s*\\d|(?:password|pwd).*(?:match|test|regex|pattern)|zxcvbn|password-validator|passwordStrength|isStrongPassword|joi\\.|yup\\.|zod\\.|validate|schema/i.test(content);\n if (hasValidation) return [];\n const hasPasswordHandling = /(?:password|pwd)\\s*[:=]\\s*(?:req\\.body|body|input|params|args)\\./i.test(content);\n if (!hasPasswordHandling) return [];\n const rawMatches = findMatches(content, /(?:password|pwd)\\s*[:=]\\s*(?:req\\.body|body|input|params|args)\\./gi, weakPasswordRequirements, filePath, () =>\n \"Enforce minimum password requirements: at least 8 characters, mix of letters/numbers/symbols. Use a library like zxcvbn for strength estimation.\"\n );\n // Skip if validation logic exists within 10 lines of the password assignment\n const lines = content.split(\"\\n\");\n const validationPattern = /\\.length|minLength|minlength|min_length|match|test|regex|pattern|validate|schema|zxcvbn|isStrongPassword/i;\n return rawMatches.filter((rm) => {\n const start = Math.max(0, rm.line - 1 - 10);\n const end = Math.min(lines.length, rm.line - 1 + 10);\n const nearby = lines.slice(start, end).join(\"\\n\");\n return !validationPattern.test(nearby);\n });\n },\n};\n\n// ────────────────────────────────────────────\n// VC046 – Session Fixation\n// ────────────────────────────────────────────\n\nexport const sessionFixation: CustomRule = {\n id: \"VC046\",\n title: \"Session Fixation Risk\",\n severity: \"high\",\n category: \"Authentication\",\n description: \"Not regenerating session IDs after login allows attackers to pre-set a session ID and hijack the authenticated session.\",\n check(content, filePath) {\n // Only applies to server-side code files, not iOS/Swift, docs, or tests\n if (!filePath.match(/\\.(js|ts|jsx|tsx|py|rb|go|java|php)$/)) return [];\n if (isTestFile(filePath)) return [];\n if (!/(?:login|signin|sign.in|authenticate)/i.test(content)) return [];\n if (!/session/i.test(content)) return [];\n // Check if session is regenerated after auth\n const hasRegenerate = /regenerate|destroy.*create|req\\.session\\.id\\s*=|session\\.regenerateId|rotateSession|clearCookies/i.test(content);\n if (hasRegenerate) return [];\n // Must have a login function definition, not just a reference or import\n const hasLogin = /(?:function\\s+(?:login|signin|authenticate)|(?:login|signin|authenticate)\\s*(?:=\\s*(?:async\\s*)?\\(|:\\s*(?:async\\s*)?\\())/i.test(content);\n if (!hasLogin) return [];\n return findMatches(content, /(?:function\\s+(?:login|signin|authenticate)|(?:login|signin|authenticate)\\s*(?:=\\s*(?:async\\s*)?\\())/gi, sessionFixation, filePath, () =>\n \"Regenerate the session ID after successful login: req.session.regenerate() (Express) or equivalent. This prevents session fixation attacks.\"\n );\n },\n};\n\n// ────────────────────────────────────────────\n// VC047 – Missing Brute Force Protection\n// ────────────────────────────────────────────\n\nexport const missingBruteForce: CustomRule = {\n id: \"VC047\",\n title: \"Login Without Brute Force Protection\",\n severity: \"high\",\n category: \"Authentication\",\n description: \"Login endpoints without rate limiting, account lockout, or progressive delays are vulnerable to credential stuffing and brute force attacks.\",\n check(content, filePath) {\n const isLoginFile = /(?:login|signin|sign.in|auth)/i.test(filePath) || /(?:login|signin|authenticate).*(?:post|handler|route)/i.test(content);\n if (!isLoginFile) return [];\n if (!/(?:password|credential)/i.test(content)) return [];\n const hasBruteForce = /rate.?limit|throttle|lockout|maxAttempts|max_attempts|failedAttempts|loginAttempts|brute|express-brute|express-rate-limit|slowDown/i.test(content);\n if (hasBruteForce) return [];\n return findMatches(content, /\\.(post|handler)\\s*\\([^)]*(?:login|signin|auth)/gi, missingBruteForce, filePath, () =>\n \"Add brute force protection to login endpoints: rate limiting (5 attempts/minute), progressive delays, or account lockout after N failures. Use express-rate-limit or similar.\"\n );\n },\n};\n\n// ────────────────────────────────────────────\n// VC048 – NoSQL Injection\n// ────────────────────────────────────────────\n\nexport const nosqlInjection: CustomRule = {\n id: \"VC048\",\n title: \"Potential NoSQL Injection\",\n severity: \"critical\",\n category: \"Injection\",\n description: \"Passing unsanitized user input directly into MongoDB/NoSQL queries allows attackers to bypass authentication, extract data, or modify queries using operators like $gt, $ne, $regex.\",\n check(content, filePath) {\n if (!/(?:mongo|mongoose|findOne|findById|find\\(|collection|aggregate)/i.test(content)) return [];\n const matches: RuleMatch[] = [];\n const patterns = [\n // Direct req.body in MongoDB queries\n /\\.find(?:One)?\\s*\\(\\s*(?:req\\.body|body|input|params)\\s*\\)/gi,\n /\\.find(?:One)?\\s*\\(\\s*\\{[^}]*:\\s*(?:req\\.body|body|input|params)\\./gi,\n // $where with user input\n /\\$where\\s*:\\s*(?![\"'`])/g,\n // Direct variable in query without sanitization\n /\\.(?:findOne|findById|deleteOne|updateOne|findOneAndUpdate)\\s*\\(\\s*\\{[^}]*:\\s*(?:req\\.(?:body|query|params))\\./gi,\n ];\n const hasSanitization = /sanitize|escape|mongo-sanitize|express-mongo-sanitize|validator|typeof.*===.*string/i.test(content);\n if (hasSanitization) return [];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, nosqlInjection, filePath, () =>\n \"Sanitize MongoDB query inputs: use express-mongo-sanitize, validate types (ensure strings aren't objects), and avoid $where. Example: if (typeof input !== 'string') throw new Error('Invalid input');\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC049 – Exposed DB Credentials in Config\n// ────────────────────────────────────────────\n\nexport const exposedDBCredentials: CustomRule = {\n id: \"VC049\",\n title: \"Database Credentials in Config File\",\n severity: \"critical\",\n category: \"Secrets\",\n description: \"Database connection strings with embedded usernames and passwords in committed config files expose credentials to anyone with repo access.\",\n check(content, filePath) {\n if (filePath.endsWith(\".example\") || filePath.endsWith(\".template\")) return [];\n if (!filePath.match(/(?:config|setting|database|db|knexfile|sequelize|drizzle|prisma)/i) && !filePath.match(/\\.(json|yaml|yml|toml|js|ts)$/)) return [];\n if (filePath.match(/\\.env/)) return []; // Handled by VC002\n const patterns = [\n // Connection strings with credentials\n /(?:host|server|database|db).*(?:password|passwd|pwd)\\s*[:=]\\s*[\"'`][^\"'`]{3,}[\"'`]/gi,\n // Inline connection URLs with credentials\n /(?:connection|database|db).*(?:postgres|mysql|mongodb|redis):\\/\\/[^:]+:[^@]+@/gi,\n ];\n const matches: RuleMatch[] = [];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, exposedDBCredentials, filePath, () =>\n \"Move database credentials to environment variables. Use: process.env.DATABASE_URL instead of hardcoding connection strings in config files.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC050 – Missing DB Connection Encryption\n// ────────────────────────────────────────────\n\nexport const missingDBEncryption: CustomRule = {\n id: \"VC050\",\n title: \"Database Connection Without SSL/TLS\",\n severity: \"high\",\n category: \"Configuration\",\n description: \"Database connections without SSL/TLS encryption transmit credentials and data in plaintext, allowing eavesdropping on the network.\",\n check(content, filePath) {\n if (!/(?:createConnection|createPool|createClient|connect|new.*Client|knex|sequelize|drizzle)/i.test(content)) return [];\n if (!/(?:postgres|mysql|mariadb|pg|mongo)/i.test(content)) return [];\n const matches: RuleMatch[] = [];\n // SSL explicitly disabled\n const sslDisabled = [\n /ssl\\s*:\\s*false/gi,\n /sslmode\\s*[:=]\\s*[\"'`]?disable[\"'`]?/gi,\n /rejectUnauthorized\\s*:\\s*false/gi,\n ];\n for (const p of sslDisabled) {\n matches.push(...findMatches(content, p, missingDBEncryption, filePath, () =>\n \"Enable SSL/TLS for database connections: { ssl: { rejectUnauthorized: true } }. In production, always verify server certificates.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC051 – GraphQL Introspection Enabled\n// ────────────────────────────────────────────\n\nexport const graphqlIntrospection: CustomRule = {\n id: \"VC051\",\n title: \"GraphQL Introspection Enabled in Production\",\n severity: \"medium\",\n category: \"Information Leakage\",\n description: \"GraphQL introspection exposes your entire API schema, types, queries, and mutations to attackers, making it easy to find attack vectors.\",\n check(content, filePath) {\n if (!/graphql/i.test(content) && !/graphql/i.test(filePath)) return [];\n const matches: RuleMatch[] = [];\n // Introspection explicitly enabled or not disabled\n if (/introspection\\s*:\\s*true/i.test(content)) {\n matches.push(...findMatches(content, /introspection\\s*:\\s*true/gi, graphqlIntrospection, filePath, () =>\n \"Disable GraphQL introspection in production: introspection: process.env.NODE_ENV !== 'production'. This prevents schema exposure.\"\n ));\n }\n // GraphQL server setup without introspection config\n if (/(?:ApolloServer|GraphQLServer|createYoga|buildSchema|makeExecutableSchema)\\s*\\(/i.test(content)) {\n if (!/introspection/i.test(content)) {\n matches.push(...findMatches(content, /(?:ApolloServer|GraphQLServer|createYoga)\\s*\\(/gi, graphqlIntrospection, filePath, () =>\n \"Explicitly disable introspection in production: new ApolloServer({ introspection: process.env.NODE_ENV !== 'production' })\"\n ));\n }\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC052 – Missing Request Size Limit\n// ────────────────────────────────────────────\n\nexport const missingRequestSizeLimit: CustomRule = {\n id: \"VC052\",\n title: \"Missing Request Body Size Limit\",\n severity: \"medium\",\n category: \"Availability\",\n description: \"Express/Hono/Fastify servers without request body size limits are vulnerable to denial-of-service via oversized payloads that exhaust memory.\",\n check(content, filePath) {\n if (!/(?:server|app|index|main)\\.[jt]sx?$/.test(filePath)) return [];\n if (!/(?:express|hono|fastify|koa)/i.test(content)) return [];\n const matches: RuleMatch[] = [];\n // express.json() without limit\n if (/express\\.json\\s*\\(\\s*\\)/g.test(content)) {\n matches.push(...findMatches(content, /express\\.json\\s*\\(\\s*\\)/g, missingRequestSizeLimit, filePath, () =>\n \"Set a body size limit: express.json({ limit: '1mb' }). Without this, attackers can send huge payloads to crash your server.\"\n ));\n }\n // bodyParser without limit\n if (/bodyParser\\.json\\s*\\(\\s*\\)/g.test(content)) {\n matches.push(...findMatches(content, /bodyParser\\.json\\s*\\(\\s*\\)/g, missingRequestSizeLimit, filePath, () =>\n \"Set a body size limit: bodyParser.json({ limit: '1mb' }).\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC053 – Hardcoded IP/Host Allowlist\n// ────────────────────────────────────────────\n\nexport const hardcodedIPAllowlist: CustomRule = {\n id: \"VC053\",\n title: \"Hardcoded IP or Host Allowlist\",\n severity: \"medium\",\n category: \"Configuration\",\n description: \"Hardcoded IP addresses or hostnames in allowlists are brittle and hard to update. They should be in environment variables or configuration files.\",\n check(content, filePath) {\n if (filePath.includes(\"test\") || filePath.includes(\"mock\") || filePath.match(/\\.(md|txt)$/)) return [];\n const matches: RuleMatch[] = [];\n // Arrays of IPs used in access control\n const patterns = [\n /(?:allowedIPs|allowed_ips|whitelist|allowlist|trustedHosts)\\s*[:=]\\s*\\[\\s*[\"'`]\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}/gi,\n /(?:allowedIPs|allowed_ips|whitelist|allowlist|trustedHosts)\\s*[:=]\\s*\\[\\s*[\"'`][\\w.-]+\\.(?:com|net|org|io)/gi,\n ];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, hardcodedIPAllowlist, filePath, () =>\n \"Move IP/host allowlists to environment variables or a config file: const allowed = process.env.ALLOWED_IPS?.split(',') || [];\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC054 – Sensitive Data in localStorage\n// ────────────────────────────────────────────\n\nexport const sensitiveLocalStorage: CustomRule = {\n id: \"VC054\",\n title: \"Sensitive Data in localStorage\",\n severity: \"high\",\n category: \"Secrets\",\n description: \"Storing tokens, passwords, or secrets in localStorage is insecure — it's accessible to any JavaScript on the page (XSS) and persists indefinitely. Use httpOnly cookies instead.\",\n check(content, filePath) {\n if (!filePath.match(/\\.(jsx?|tsx?|vue|svelte)$/)) return [];\n if (isTestFile(filePath)) return [];\n // Skip test-related files (mock, spec in the path)\n if (/mock|spec/i.test(filePath)) return [];\n // Skip if localStorage.removeItem is used (cleanup code, not storage)\n if (!/localStorage\\.setItem|localStorage\\[/i.test(content)) return [];\n const matches: RuleMatch[] = [];\n const patterns = [\n /localStorage\\.setItem\\s*\\(\\s*[\"'`](?:token|access_token|auth_token|jwt|session|refresh_token|api_key|password|secret)/gi,\n /localStorage\\s*\\[\\s*[\"'`](?:token|access_token|auth_token|jwt|session|refresh_token|api_key|password|secret)/gi,\n ];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, sensitiveLocalStorage, filePath, () =>\n \"Don't store tokens/secrets in localStorage — use httpOnly cookies instead. localStorage is accessible to any XSS attack. For session tokens, set them as httpOnly, secure, sameSite cookies.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC055 – Exposed Source Maps in Production\n// ────────────────────────────────────────────\n\nexport const exposedSourceMaps: CustomRule = {\n id: \"VC055\",\n title: \"Source Maps Exposed in Production\",\n severity: \"medium\",\n category: \"Information Leakage\",\n description: \"Source map files (.map) in production expose your original source code, comments, and internal logic to anyone who downloads them.\",\n check(content, filePath) {\n // Check build configs for source maps in production\n if (!filePath.match(/(?:webpack|vite|rollup|next)\\.config|tsconfig/i)) return [];\n const matches: RuleMatch[] = [];\n // Source maps enabled without environment check\n if (/(?:sourceMap|source-map|sourcemap)\\s*[:=]\\s*true/i.test(content)) {\n const hasEnvCheck = /process\\.env\\.NODE_ENV|NODE_ENV|production/i.test(content);\n if (!hasEnvCheck) {\n matches.push(...findMatches(content, /(?:sourceMap|source-map|sourcemap)\\s*[:=]\\s*true/gi, exposedSourceMaps, filePath, () =>\n \"Disable source maps in production builds: sourceMap: process.env.NODE_ENV !== 'production'. Or use 'hidden-source-map' to generate maps without exposing them.\"\n ));\n }\n }\n // productionSourceMap in Vue\n if (/productionSourceMap\\s*:\\s*true/i.test(content)) {\n matches.push(...findMatches(content, /productionSourceMap\\s*:\\s*true/gi, exposedSourceMaps, filePath, () =>\n \"Set productionSourceMap: false to avoid exposing source code in production.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC056 – Clickjacking / Missing X-Frame-Options\n// ────────────────────────────────────────────\n\nexport const clickjacking: CustomRule = {\n id: \"VC056\",\n title: \"Clickjacking — Missing X-Frame-Options\",\n severity: \"medium\",\n category: \"Configuration\",\n description: \"Without X-Frame-Options or frame-ancestors CSP directive, your page can be embedded in an attacker's iframe for UI redress (clickjacking) attacks.\",\n check(content, filePath) {\n // Check HTML files\n if (filePath.match(/\\.(html|htm)$/)) {\n if (!/X-Frame-Options|frame-ancestors/i.test(content)) {\n return [{\n rule: \"VC056\", title: clickjacking.title, severity: \"medium\" as const, category: \"Configuration\",\n file: filePath, line: 1, snippet: getSnippet(content, 1),\n fix: 'Add <meta http-equiv=\"X-Frame-Options\" content=\"DENY\"> or set frame-ancestors in CSP to prevent clickjacking.'\n }];\n }\n }\n // Check server configs\n if (/(?:server|app|index|main)\\.[jt]sx?$/.test(filePath)) {\n if (/(?:express|hono|fastify|koa)/i.test(content)) {\n if (!/X-Frame-Options|frame-ancestors|helmet/i.test(content)) {\n return findMatches(content, /(?:express|hono|fastify|koa)\\s*\\(/gi, clickjacking, filePath, () =>\n \"Add X-Frame-Options header: res.setHeader('X-Frame-Options', 'DENY'). Or use helmet: app.use(helmet()) which sets this and other security headers.\"\n );\n }\n }\n }\n return [];\n },\n};\n\n// ────────────────────────────────────────────\n// VC057 – Overly Permissive IAM/Cloud Roles\n// ────────────────────────────────────────────\n\nexport const overlyPermissiveIAM: CustomRule = {\n id: \"VC057\",\n title: \"Overly Permissive IAM/Cloud Permissions\",\n severity: \"critical\",\n category: \"Authorization\",\n description: \"Wildcard (*) permissions in AWS IAM, GCP, or Terraform configs grant unrestricted access, violating the principle of least privilege.\",\n check(content, filePath) {\n if (!filePath.match(/\\.(tf|hcl|json|yaml|yml)$/) && !filePath.match(/(?:iam|policy|role|permission)/i)) return [];\n const matches: RuleMatch[] = [];\n const patterns = [\n // AWS IAM wildcard\n /[\"'`]Action[\"'`]\\s*:\\s*[\"'`]\\*[\"'`]/g,\n /[\"'`]Resource[\"'`]\\s*:\\s*[\"'`]\\*[\"'`]/g,\n // Terraform aws_iam\n /actions\\s*=\\s*\\[\\s*[\"'`]\\*[\"'`]\\s*\\]/g,\n /resources\\s*=\\s*\\[\\s*[\"'`]\\*[\"'`]\\s*\\]/g,\n // GCP bindings\n /role\\s*[:=]\\s*[\"'`]roles\\/(?:owner|editor)[\"'`]/g,\n ];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, overlyPermissiveIAM, filePath, () =>\n \"Follow the principle of least privilege: replace wildcard (*) with specific actions and resources. Example: 'Action': 's3:GetObject' instead of '*'.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC058 – Docker Running as Root\n// ────────────────────────────────────────────\n\nexport const dockerRunAsRoot: CustomRule = {\n id: \"VC058\",\n title: \"Docker Container Running as Root\",\n severity: \"high\",\n category: \"Configuration\",\n description: \"Containers running as root give attackers full system access if they escape the container. Always run as a non-root user.\",\n check(content, filePath) {\n if (!filePath.match(/Dockerfile$/i)) return [];\n const hasUser = /^\\s*USER\\s+/m.test(content);\n if (hasUser) return [];\n return [{\n rule: \"VC058\", title: dockerRunAsRoot.title, severity: \"high\" as const, category: \"Configuration\",\n file: filePath, line: 1, snippet: getSnippet(content, 1),\n fix: \"Add a USER directive: RUN addgroup -S app && adduser -S app -G app\\\\nUSER app. Place it after installing dependencies but before COPY/CMD.\"\n }];\n },\n};\n\n// ────────────────────────────────────────────\n// VC059 – Exposed Ports in Docker Compose\n// ────────────────────────────────────────────\n\nexport const exposedDockerPorts: CustomRule = {\n id: \"VC059\",\n title: \"Docker Compose Binding to All Interfaces\",\n severity: \"medium\",\n category: \"Configuration\",\n description: \"Binding ports to 0.0.0.0 (default) in Docker Compose exposes services to the entire network. Bind to 127.0.0.1 for local-only access.\",\n check(content, filePath) {\n if (!filePath.match(/docker-compose|compose\\.(yaml|yml)$/i)) return [];\n const matches: RuleMatch[] = [];\n // ports: \"3000:3000\" or \"8080:80\" without binding to 127.0.0.1\n const portPattern = /ports:\\s*\\n(?:\\s*-\\s*[\"'`]?\\d+:\\d+[\"'`]?\\s*\\n?)+/g;\n if (portPattern.test(content) && !/127\\.0\\.0\\.1:/i.test(content)) {\n matches.push(...findMatches(content, /^\\s*-\\s*[\"'`]?\\d+:\\d+[\"'`]?/gm, exposedDockerPorts, filePath, () =>\n \"Bind to localhost only: '127.0.0.1:3000:3000' instead of '3000:3000'. This prevents external network access to the service.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC060 – Weak Hashing Algorithm\n// ────────────────────────────────────────────\n\nexport const weakHashing: CustomRule = {\n id: \"VC060\",\n title: \"Weak Hashing Algorithm for Passwords\",\n severity: \"critical\",\n category: \"Cryptography\",\n description: \"MD5 and SHA1/SHA256 are too fast for password hashing — they can be brute-forced at billions of attempts per second. Use bcrypt, scrypt, or argon2 instead.\",\n check(content, filePath) {\n if (filePath.includes(\"test\") || filePath.includes(\"mock\")) return [];\n // Skip if file also uses strong hashing (indicates migration or comparison code)\n if (/bcrypt|scrypt|argon2/i.test(content)) return [];\n const matches: RuleMatch[] = [];\n // MD5/SHA used with password context\n const patterns = [\n /(?:md5|sha1|sha256|sha512)\\s*\\([^)]*(?:password|passwd|pwd)/gi,\n /createHash\\s*\\(\\s*[\"'`](?:md5|sha1|sha256)[\"'`]\\).*(?:password|passwd|pwd)/gi,\n /(?:password|passwd|pwd).*createHash\\s*\\(\\s*[\"'`](?:md5|sha1|sha256)[\"'`]\\)/gi,\n /hashlib\\.(?:md5|sha1|sha256)\\s*\\([^)]*(?:password|passwd|pwd)/gi,\n /Digest::(?:MD5|SHA1|SHA256).*(?:password|passwd|pwd)/gi,\n ];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, weakHashing, filePath, () =>\n \"Use bcrypt, scrypt, or argon2 for password hashing — they're intentionally slow. Example: const hash = await bcrypt.hash(password, 12);\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC061 – Disabled TLS Certificate Verification\n// ────────────────────────────────────────────\n\nexport const disabledTLSVerification: CustomRule = {\n id: \"VC061\",\n title: \"Disabled TLS Certificate Verification\",\n severity: \"critical\",\n category: \"Cryptography\",\n description: \"Disabling TLS certificate verification (NODE_TLS_REJECT_UNAUTHORIZED=0 or rejectUnauthorized:false) makes all HTTPS connections vulnerable to man-in-the-middle attacks.\",\n check(content, filePath) {\n const matches: RuleMatch[] = [];\n const patterns = [\n /NODE_TLS_REJECT_UNAUTHORIZED\\s*[:=]\\s*[\"'`]?0[\"'`]?/g,\n /rejectUnauthorized\\s*:\\s*false/g,\n /verify\\s*[:=]\\s*false.*(?:ssl|tls|cert|https)/gi,\n /PYTHONHTTPSVERIFY\\s*[:=]\\s*[\"'`]?0[\"'`]?/g,\n /ssl_verify\\s*[:=]\\s*false/gi,\n /InsecureSkipVerify\\s*:\\s*true/g,\n ];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, disabledTLSVerification, filePath, () =>\n \"Never disable TLS certificate verification in production. Fix the root cause: install the correct CA certificate, or use NODE_EXTRA_CA_CERTS for custom CAs.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC062 – Hardcoded Encryption Key/IV\n// ────────────────────────────────────────────\n\nexport const hardcodedEncryptionKey: CustomRule = {\n id: \"VC062\",\n title: \"Hardcoded Encryption Key or IV\",\n severity: \"critical\",\n category: \"Cryptography\",\n description: \"Hardcoded encryption keys and initialization vectors (IVs) in source code can be extracted to decrypt all data. IVs must be random per encryption operation.\",\n check(content, filePath) {\n if (filePath.endsWith(\".example\") || filePath.endsWith(\".template\")) return [];\n if (isTestFile(filePath)) return [];\n // Skip HTML/XML/config files — meta tags and config values are not encryption keys\n if (filePath.match(/\\.(html|htm|xml|plist|svg|xhtml)$/)) return [];\n // Skip files without any crypto-related code\n if (!/(?:cipher|encrypt|decrypt|crypto|aes|createCipher)/i.test(content)) return [];\n const matches: RuleMatch[] = [];\n const patterns = [\n // Encryption key as string literal\n /(?:encryption_key|encryptionKey|cipher_key|cipherKey|aes_key|AES_KEY|ENCRYPTION_KEY)\\s*[:=]\\s*[\"'`][^\"'`]{8,}[\"'`]/g,\n // createCipheriv with hardcoded key\n /createCipher(?:iv)?\\s*\\(\\s*[\"'`][^\"'`]+[\"'`]\\s*,\\s*[\"'`][^\"'`]+[\"'`]/g,\n // Buffer.from with hardcoded key near cipher context\n /(?:key|iv|nonce)\\s*[:=]\\s*Buffer\\.from\\s*\\(\\s*[\"'`][^\"'`]{8,}[\"'`]/gi,\n // Static IV (should be random) — only in crypto context\n /(?:^|[\\s,({])(?:iv|nonce|initialVector)\\s*[:=]\\s*[\"'`][0-9a-fA-F]{16,}[\"'`]/gi,\n ];\n for (const p of patterns) {\n const rawMatches = findMatches(content, p, hardcodedEncryptionKey, filePath, () =>\n \"Move encryption keys to environment variables. Generate IVs randomly per operation: crypto.randomBytes(16). Never reuse IVs.\"\n );\n // Skip matches on lines containing CSP meta tags or security headers\n for (const rm of rawMatches) {\n const lineStart = content.lastIndexOf(\"\\n\", content.split(\"\\n\").slice(0, rm.line - 1).join(\"\\n\").length) + 1;\n const lineEnd = content.indexOf(\"\\n\", lineStart + 1);\n const lineText = content.substring(lineStart, lineEnd === -1 ? content.length : lineEnd);\n if (/meta\\s+http-equiv|Content-Security-Policy|X-Frame-Options|X-Content-Type/i.test(lineText)) continue;\n matches.push(rm);\n }\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC063 – dangerouslySetInnerHTML\n// ────────────────────────────────────────────\n\nexport const dangerousInnerHTML: CustomRule = {\n id: \"VC063\",\n title: \"Unsanitized dangerouslySetInnerHTML\",\n severity: \"critical\",\n category: \"Injection\",\n description: \"Using dangerouslySetInnerHTML without sanitization (DOMPurify) enables XSS attacks. User-controlled content injected as raw HTML can execute arbitrary JavaScript.\",\n check(content, filePath) {\n if (!filePath.match(/\\.(jsx|tsx)$/)) return [];\n if (isTestFile(filePath)) return [];\n if (!/dangerouslySetInnerHTML/i.test(content)) return [];\n const hasSanitize = /DOMPurify|sanitize|purify|xss|sanitizeHtml|isomorphic-dompurify/i.test(content);\n if (hasSanitize) return [];\n // Find all dangerouslySetInnerHTML usages, but skip if the value is a\n // static constant (all-caps like THEME_INIT_SCRIPT), a string literal,\n // or a marked safe value — these are not user-controlled input\n const findings: RuleMatch[] = [];\n const re = /dangerouslySetInnerHTML\\s*=\\s*\\{\\s*\\{\\s*__html\\s*:\\s*([^}]+)\\}/g;\n let m: RegExpExecArray | null;\n while ((m = re.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n const value = m[1].trim();\n // Skip static constants (UPPER_CASE), string literals, or template strings without interpolation\n if (/^[A-Z][A-Z0-9_]+$/.test(value)) continue;\n if (/^[\"'`][^$]*[\"'`]$/.test(value)) continue;\n // Skip JSON-LD structured data (standard SEO practice, developer-controlled)\n if (/JSON\\.stringify/i.test(value)) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC063\", title: dangerousInnerHTML.title,\n severity: \"critical\" as const, category: \"Injection\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Sanitize HTML before using dangerouslySetInnerHTML: dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(content) }}. Install: npm install dompurify\",\n });\n }\n return findings;\n },\n};\n\n// ────────────────────────────────────────────\n// VC064 – Exposed Next.js Server Actions\n// ────────────────────────────────────────────\n\nexport const exposedServerActions: CustomRule = {\n id: \"VC064\",\n title: \"Next.js Server Action Without Auth Check\",\n severity: \"high\",\n category: \"Authorization\",\n description: \"Next.js Server Actions ('use server') are publicly callable endpoints. Without authentication checks, anyone can invoke them directly.\",\n check(content, filePath) {\n if (!filePath.match(/\\.(jsx?|tsx?)$/)) return [];\n if (!/[\"']use server[\"']/i.test(content)) return [];\n const hasAuth = /getServerSession|auth\\(\\)|currentUser|getUser|requireAuth|requireUser|requireUserForApi|session|clerk|getAuth|verifyCronSecret|checkApiKey/i.test(content);\n if (hasAuth) return [];\n // Check if there are exported async functions (server actions)\n if (/export\\s+async\\s+function/i.test(content)) {\n return findMatches(content, /export\\s+async\\s+function\\s+\\w+/g, exposedServerActions, filePath, () =>\n \"Add authentication to Server Actions: const session = await getServerSession(); if (!session) throw new Error('Unauthorized');\"\n );\n }\n return [];\n },\n};\n\n// ────────────────────────────────────────────\n// VC065 – Unprotected Next.js API Routes\n// ────────────────────────────────────────────\n\nexport const unprotectedAPIRoutes: CustomRule = {\n id: \"VC065\",\n title: \"Unprotected Next.js API Route\",\n severity: \"high\",\n category: \"Authorization\",\n description: \"Next.js API routes under /api/ without authentication middleware can be called by anyone, exposing data or mutations.\",\n check(content, filePath) {\n if (!filePath.match(/\\/api\\/.*\\.(jsx?|tsx?)$/) && !filePath.match(/\\/app\\/api\\/.*route\\.(jsx?|tsx?)$/)) return [];\n // Skip health/public endpoints\n if (/health|status|public|webhook/i.test(filePath)) return [];\n const hasAuth = /getServerSession|auth\\(\\)|currentUser|getUser|requireAuth|session|clerk|getAuth|verifyToken|authenticate|middleware/i.test(content);\n if (hasAuth) return [];\n const hasHandler = /export\\s+(?:async\\s+)?function\\s+(?:GET|POST|PUT|DELETE|PATCH)|export\\s+default/i.test(content);\n if (hasHandler) {\n return findMatches(content, /export\\s+(?:async\\s+)?function\\s+(?:GET|POST|PUT|DELETE|PATCH)/g, unprotectedAPIRoutes, filePath, () =>\n \"Add authentication to API routes: const session = await getServerSession(authOptions); if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });\"\n );\n }\n return [];\n },\n};\n\n// ────────────────────────────────────────────\n// VC066 – Client Component Using Secrets\n// ────────────────────────────────────────────\n\nexport const clientComponentSecret: CustomRule = {\n id: \"VC066\",\n title: \"Secret Used in Client Component\",\n severity: \"critical\",\n category: \"Secrets\",\n description: \"Using server-side secrets (process.env without NEXT_PUBLIC_) in 'use client' components exposes them in the browser bundle.\",\n check(content, filePath) {\n if (!filePath.match(/\\.(jsx?|tsx?)$/)) return [];\n if (!/[\"']use client[\"']/i.test(content)) return [];\n // process.env without NEXT_PUBLIC_ prefix in a client component\n const pattern = /process\\.env\\.(?!NEXT_PUBLIC_)[A-Z_]{3,}/g;\n if (pattern.test(content)) {\n return findMatches(content, /process\\.env\\.(?!NEXT_PUBLIC_)[A-Z_]{3,}/g, clientComponentSecret, filePath, () =>\n \"Server-side env vars are not available in client components and may leak in builds. Move this logic to a Server Component or API route.\"\n );\n }\n return [];\n },\n};\n\n// ────────────────────────────────────────────\n// VC067 – Insecure Deep Link Handling\n// ────────────────────────────────────────────\n\nexport const insecureDeepLink: CustomRule = {\n id: \"VC067\",\n title: \"Insecure Deep Link Handling\",\n severity: \"high\",\n category: \"Injection\",\n description: \"Deep links that navigate or execute actions without validating the URL scheme, host, or parameters can be exploited for phishing or unauthorized actions.\",\n check(content, filePath) {\n if (!/(?:Linking|DeepLinking|deep.?link|handleURL|openURL|url.?scheme)/i.test(content)) return [];\n const matches: RuleMatch[] = [];\n // React Native Linking without validation\n const patterns = [\n /Linking\\.addEventListener\\s*\\([^)]*(?:url|link)/gi,\n /Linking\\.getInitialURL\\s*\\(\\)/g,\n /handleOpenURL|handleDeepLink|onDeepLink/gi,\n ];\n const hasValidation = /allowedSchemes|allowedHosts|validateURL|isAllowedURL|whitelist|URL.*hostname.*includes/i.test(content);\n if (hasValidation) return [];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, insecureDeepLink, filePath, () =>\n \"Validate deep link URLs: check the scheme and host against an allowlist before navigating or executing actions.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC068 – Sensitive Data in AsyncStorage\n// ────────────────────────────────────────────\n\nexport const sensitiveAsyncStorage: CustomRule = {\n id: \"VC068\",\n title: \"Sensitive Data in AsyncStorage\",\n severity: \"high\",\n category: \"Secrets\",\n description: \"React Native AsyncStorage is unencrypted. Storing tokens, passwords, or secrets there makes them readable by other apps or anyone with device access.\",\n check(content, filePath) {\n if (!/AsyncStorage/i.test(content)) return [];\n const patterns = [\n /AsyncStorage\\.setItem\\s*\\(\\s*[\"'`](?:token|access_token|auth_token|jwt|session|refresh_token|api_key|password|secret|private_key)/gi,\n ];\n const matches: RuleMatch[] = [];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, sensitiveAsyncStorage, filePath, () =>\n \"Use react-native-keychain or expo-secure-store instead of AsyncStorage for sensitive data. These use the OS keychain (encrypted).\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC069 – Missing Certificate Pinning\n// ────────────────────────────────────────────\n\nexport const missingCertPinning: CustomRule = {\n id: \"VC069\",\n title: \"Missing Certificate Pinning in Mobile App\",\n severity: \"medium\",\n category: \"Cryptography\",\n description: \"Mobile apps without SSL certificate pinning are vulnerable to MITM attacks via compromised or rogue CAs. Pin your API server's certificate.\",\n check(content, filePath) {\n // Only for mobile project files\n if (!/(?:react.native|expo|android|ios|mobile)/i.test(filePath) && !/(?:React.*Native|expo)/i.test(content)) return [];\n if (!/(?:fetch|axios|http|api|request)/i.test(content)) return [];\n // Check for HTTP client setup without pinning\n if (/axios\\.create|new\\s+(?:HttpClient|ApiClient)/i.test(content)) {\n const hasPinning = /pinning|certificate|cert|ssl|TrustKit|react-native-ssl-pinning|cert-pinner/i.test(content);\n if (!hasPinning) {\n return findMatches(content, /axios\\.create|new\\s+(?:HttpClient|ApiClient)/gi, missingCertPinning, filePath, () =>\n \"Add SSL certificate pinning: use react-native-ssl-pinning or TrustKit. This prevents MITM attacks via rogue certificates.\"\n );\n }\n }\n return [];\n },\n};\n\n// ────────────────────────────────────────────\n// VC070 – Android Debuggable Flag\n// ────────────────────────────────────────────\n\nexport const androidDebuggable: CustomRule = {\n id: \"VC070\",\n title: \"Android App Debuggable in Production\",\n severity: \"high\",\n category: \"Configuration\",\n description: \"android:debuggable='true' in AndroidManifest.xml allows attackers to attach debuggers, inspect memory, and bypass security controls.\",\n check(content, filePath) {\n if (!filePath.match(/AndroidManifest\\.xml$/i)) return [];\n if (/android:debuggable\\s*=\\s*[\"']true[\"']/i.test(content)) {\n return findMatches(content, /android:debuggable\\s*=\\s*[\"']true[\"']/gi, androidDebuggable, filePath, () =>\n \"Remove android:debuggable='true' or set it to false. Debug builds should use build variants, not manifest flags.\"\n );\n }\n return [];\n },\n};\n\n// ────────────────────────────────────────────\n// VC071 – Django DEBUG=True\n// ────────────────────────────────────────────\n\nexport const djangoDebug: CustomRule = {\n id: \"VC071\",\n title: \"Django DEBUG Mode Enabled\",\n severity: \"critical\",\n category: \"Configuration\",\n description: \"Django with DEBUG=True exposes detailed error pages with source code, database queries, environment variables, and installed apps to anyone.\",\n check(content, filePath) {\n if (!filePath.match(/settings\\.py$/i) && !filePath.match(/config.*\\.py$/i)) return [];\n if (/^\\s*DEBUG\\s*=\\s*True\\s*$/m.test(content)) {\n const hasEnvCheck = /os\\.environ|env\\(|config\\(|getenv/i.test(content);\n if (!hasEnvCheck) {\n return findMatches(content, /^\\s*DEBUG\\s*=\\s*True/gm, djangoDebug, filePath, () =>\n \"Use environment variable: DEBUG = os.environ.get('DEBUG', 'False') == 'True'. Never hardcode DEBUG=True.\"\n );\n }\n }\n return [];\n },\n};\n\n// ────────────────────────────────────────────\n// VC072 – Flask Hardcoded Secret Key\n// ────────────────────────────────────────────\n\nexport const flaskSecretKey: CustomRule = {\n id: \"VC072\",\n title: \"Flask Secret Key Hardcoded\",\n severity: \"critical\",\n category: \"Secrets\",\n description: \"Hardcoded Flask secret_key allows attackers to forge sessions, CSRF tokens, and signed cookies. Must be a random value from environment variables.\",\n check(content, filePath) {\n if (!filePath.match(/\\.py$/)) return [];\n const patterns = [\n /(?:app\\.)?secret_key\\s*=\\s*[\"'`][^\"'`]{3,}[\"'`]/g,\n /(?:app\\.config)\\s*\\[\\s*[\"']SECRET_KEY[\"']\\s*\\]\\s*=\\s*[\"'`][^\"'`]{3,}[\"'`]/g,\n ];\n const hasEnv = /os\\.environ|env\\(|config\\(|getenv/i.test(content);\n if (hasEnv) return [];\n const matches: RuleMatch[] = [];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, flaskSecretKey, filePath, () =>\n \"Use environment variable: app.secret_key = os.environ['SECRET_KEY']. Generate with: python -c \\\"import secrets; print(secrets.token_hex(32))\\\"\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC073 – Pickle Deserialization\n// ────────────────────────────────────────────\n\nexport const pickleDeserialization: CustomRule = {\n id: \"VC073\",\n title: \"Unsafe Pickle Deserialization\",\n severity: \"critical\",\n category: \"Injection\",\n description: \"pickle.loads() on untrusted data allows arbitrary code execution. An attacker can craft a pickle payload that runs system commands on your server.\",\n check(content, filePath) {\n if (!filePath.match(/\\.py$/)) return [];\n const matches: RuleMatch[] = [];\n const patterns = [\n /pickle\\.loads?\\s*\\(/g,\n /cPickle\\.loads?\\s*\\(/g,\n /shelve\\.open\\s*\\(/g,\n /yaml\\.load\\s*\\([^)]*(?!Loader\\s*=\\s*yaml\\.SafeLoader)/g,\n ];\n const hasSafe = /restricted_loads|SafeUnpickler|safe_load|yaml\\.safe_load/i.test(content);\n if (hasSafe) return [];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, pickleDeserialization, filePath, () =>\n \"Never unpickle untrusted data — it allows arbitrary code execution. Use JSON for data exchange, or yaml.safe_load() for YAML.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC074 – Missing CSRF Protection (Django)\n// ────────────────────────────────────────────\n\nexport const missingCSRF: CustomRule = {\n id: \"VC074\",\n title: \"CSRF Protection Disabled\",\n severity: \"high\",\n category: \"Authorization\",\n description: \"Using @csrf_exempt on state-changing views (POST/PUT/DELETE) allows attackers to forge requests from other sites, performing actions as authenticated users.\",\n check(content, filePath) {\n if (!filePath.match(/\\.py$/)) return [];\n const matches: RuleMatch[] = [];\n if (/csrf_exempt/i.test(content)) {\n matches.push(...findMatches(content, /@csrf_exempt/g, missingCSRF, filePath, () =>\n \"Remove @csrf_exempt and use proper CSRF tokens. For APIs, use token-based auth (JWT) instead of session cookies.\"\n ));\n }\n // Also check for CsrfViewMiddleware removal\n if (/MIDDLEWARE.*=.*\\[/s.test(content) && !/CsrfViewMiddleware/i.test(content) && /django/i.test(content)) {\n matches.push(...findMatches(content, /MIDDLEWARE\\s*=/g, missingCSRF, filePath, () =>\n \"Re-add 'django.middleware.csrf.CsrfViewMiddleware' to MIDDLEWARE. CSRF protection is essential for session-based auth.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC075 – GitHub Actions Script Injection\n// ────────────────────────────────────────────\n\nexport const githubActionsInjection: CustomRule = {\n id: \"VC075\",\n title: \"GitHub Actions Script Injection\",\n severity: \"critical\",\n category: \"Injection\",\n description: \"Using ${{ github.event.* }} directly in 'run:' steps allows attackers to inject shell commands via PR titles, issue bodies, or branch names.\",\n check(content, filePath) {\n if (!filePath.match(/\\.github\\/workflows\\/.*\\.(yml|yaml)$/i)) return [];\n const matches: RuleMatch[] = [];\n // Direct interpolation in run steps\n const patterns = [\n /run:.*\\$\\{\\{\\s*github\\.event\\.(?:issue|pull_request|comment|review|head_commit)\\.(?:title|body|message)/gi,\n /run:.*\\$\\{\\{\\s*github\\.event\\.(?:inputs|head_ref|base_ref)/gi,\n ];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, githubActionsInjection, filePath, () =>\n \"Never use ${{ github.event.* }} directly in 'run:'. Pass it as an environment variable: env: TITLE: ${{ github.event.issue.title }} then use $TITLE in the script.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC076 – Secrets in CI Config\n// ────────────────────────────────────────────\n\nexport const secretsInCI: CustomRule = {\n id: \"VC076\",\n title: \"Hardcoded Secrets in CI/CD Config\",\n severity: \"critical\",\n category: \"Secrets\",\n description: \"Hardcoded tokens, passwords, or API keys in CI/CD workflow files are visible to anyone with repo access. Use encrypted secrets instead.\",\n check(content, filePath) {\n if (!filePath.match(/\\.github\\/workflows\\/|\\.gitlab-ci|Jenkinsfile|\\.circleci|bitbucket-pipelines/i)) return [];\n const matches: RuleMatch[] = [];\n // Hardcoded values that look like secrets (not using ${{ secrets.* }})\n const patterns = [\n /(?:password|token|key|secret|api_key|apikey)\\s*[:=]\\s*[\"'`][A-Za-z0-9+/=_-]{20,}[\"'`]/gi,\n /(?:DOCKER_PASSWORD|NPM_TOKEN|AWS_SECRET_ACCESS_KEY|GH_TOKEN|GITHUB_TOKEN)\\s*[:=]\\s*[\"'`][^\"'`$]+[\"'`]/gi,\n ];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, secretsInCI, filePath, () =>\n \"Use repository secrets: ${{ secrets.MY_TOKEN }} (GitHub) or CI/CD variable settings. Never hardcode credentials in workflow files.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC077 – CORS Wildcard in Serverless Config\n// ────────────────────────────────────────────\n\nexport const corsServerless: CustomRule = {\n id: \"VC077\",\n title: \"CORS Wildcard in Serverless Config\",\n severity: \"high\",\n category: \"Configuration\",\n description: \"Setting Access-Control-Allow-Origin: * in serverless.yml, vercel.json, or similar configs allows any website to make authenticated requests to your API.\",\n check(content, filePath) {\n if (!filePath.match(/serverless\\.(yml|yaml)|vercel\\.json|netlify\\.toml|amplify\\.yml/i)) return [];\n const matches: RuleMatch[] = [];\n if (/(?:Access-Control-Allow-Origin|allowOrigin|cors).*['\"]\\*['\"]/i.test(content)) {\n matches.push(...findMatches(content, /(?:Access-Control-Allow-Origin|allowOrigin|cors).*['\"]\\*['\"]/gi, corsServerless, filePath, () =>\n \"Replace wildcard CORS with specific origins: allowOrigin: ['https://yourdomain.com']. Wildcard allows any site to call your API.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC078 – Kubernetes Privileged Container\n// ────────────────────────────────────────────\n\nexport const k8sPrivileged: CustomRule = {\n id: \"VC078\",\n title: \"Kubernetes Privileged Container\",\n severity: \"critical\",\n category: \"Configuration\",\n description: \"Running containers with privileged: true or as root in Kubernetes gives full host access, making container escapes trivial.\",\n check(content, filePath) {\n if (!filePath.match(/\\.(yaml|yml)$/) || !/(?:kind|apiVersion|container)/i.test(content)) return [];\n const matches: RuleMatch[] = [];\n const patterns = [\n /privileged\\s*:\\s*true/g,\n /runAsUser\\s*:\\s*0\\b/g,\n /runAsNonRoot\\s*:\\s*false/g,\n /allowPrivilegeEscalation\\s*:\\s*true/g,\n ];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, k8sPrivileged, filePath, () =>\n \"Set securityContext: { privileged: false, runAsNonRoot: true, allowPrivilegeEscalation: false }. Never run containers as root in production.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// FRAMEWORK DETECTION\n// ────────────────────────────────────────────\n\nexport type DetectedFramework = \"next.js\" | \"react\" | \"react-native\" | \"express\" | \"hono\" | \"fastify\" | \"django\" | \"flask\" | \"electron\" | \"vue\" | \"svelte\" | \"unknown\";\n\nexport function detectFramework(files: { path: string; content: string }[]): DetectedFramework[] {\n const frameworks: Set<DetectedFramework> = new Set();\n for (const { path, content } of files) {\n if (path.match(/next\\.config/i) || /from\\s+[\"']next/i.test(content)) frameworks.add(\"next.js\");\n if (/from\\s+[\"']react-native/i.test(content) || path.match(/react-native\\.config/i)) frameworks.add(\"react-native\");\n else if (/from\\s+[\"']react/i.test(content) || /import\\s+React/i.test(content)) frameworks.add(\"react\");\n if (/from\\s+[\"']express/i.test(content) || /require\\s*\\(\\s*[\"']express/i.test(content)) frameworks.add(\"express\");\n if (/from\\s+[\"']hono/i.test(content)) frameworks.add(\"hono\");\n if (/from\\s+[\"']fastify/i.test(content)) frameworks.add(\"fastify\");\n if (/from\\s+[\"']electron/i.test(content) || path.match(/electron/i)) frameworks.add(\"electron\");\n if (path.match(/settings\\.py$/) || /from\\s+django/i.test(content)) frameworks.add(\"django\");\n if (/from\\s+flask/i.test(content) || /Flask\\s*\\(/i.test(content)) frameworks.add(\"flask\");\n if (/from\\s+[\"']vue/i.test(content) || path.match(/vue\\.config/i)) frameworks.add(\"vue\");\n if (/from\\s+[\"']svelte/i.test(content) || path.match(/svelte\\.config/i)) frameworks.add(\"svelte\");\n }\n if (frameworks.size === 0) frameworks.add(\"unknown\");\n return [...frameworks];\n}\n\n// ────────────────────────────────────────────\n// SEVERITY GRADING\n// ────────────────────────────────────────────\n\nexport type SecurityGrade = \"A+\" | \"A\" | \"B\" | \"C\" | \"D\" | \"F\";\n\nexport interface GradeResult {\n grade: SecurityGrade;\n score: number;\n summary: string;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nexport function calculateGrade(findings: Finding[], _totalFiles: number): GradeResult {\n if (findings.length === 0) {\n return { grade: \"A+\", score: 100, summary: \"No security issues detected. Excellent.\" };\n }\n\n // Single source of truth for severity weights. Matches the dashboard\n // scoring modal and the GitHub Action action.yml so all three surfaces\n // produce the same grade for the same input.\n let critical = 0, high = 0, medium = 0, low = 0;\n for (const f of findings) {\n if (f.severity === \"critical\") critical++;\n else if (f.severity === \"high\") high++;\n else if (f.severity === \"medium\") medium++;\n else if (f.severity === \"low\") low++;\n }\n\n const deductions = critical * 15 + high * 7 + medium * 3 + low * 1;\n const rawScore = Math.max(0, 100 - deductions);\n\n // Score-based grade thresholds\n let grade: SecurityGrade;\n if (rawScore >= 97) grade = \"A+\";\n else if (rawScore >= 90) grade = \"A\";\n else if (rawScore >= 80) grade = \"B\";\n else if (rawScore >= 70) grade = \"C\";\n else if (rawScore >= 60) grade = \"D\";\n else grade = \"F\";\n\n // Severity caps — a scan with serious findings cannot earn an A regardless\n // of what the score math says. Prevents the 'strong security with minor\n // concerns' bug where 2 High findings were rated A.\n const capGrade = (cap: SecurityGrade): SecurityGrade => {\n const order: SecurityGrade[] = [\"A+\", \"A\", \"B\", \"C\", \"D\", \"F\"];\n return order.indexOf(grade) < order.indexOf(cap) ? cap : grade;\n };\n if (critical >= 1) grade = capGrade(\"D\"); // any critical → max D\n else if (high >= 3) grade = capGrade(\"D\"); // 3+ high → max D\n else if (high >= 1) grade = capGrade(\"B\"); // 1–2 high → max B\n else if (medium >= 5) grade = capGrade(\"B\"); // 5+ medium → max B\n else if (medium >= 1) grade = capGrade(\"A\"); // any medium → max A (not A+)\n\n // Summary references what's actually wrong, not just the score bucket.\n let summary: string;\n if (critical > 0) {\n summary = `${critical} critical ${critical === 1 ? \"vulnerability\" : \"vulnerabilities\"} require immediate attention.`;\n } else if (high >= 3) {\n summary = `${high} high-severity issues require urgent attention.`;\n } else if (high > 0) {\n summary = `${high} high-severity ${high === 1 ? \"issue needs\" : \"issues need\"} attention.`;\n } else if (medium >= 5) {\n summary = `${medium} medium-severity issues to address.`;\n } else if (medium > 0) {\n summary = `Clean of critical and high issues. ${medium} medium-severity ${medium === 1 ? \"issue\" : \"issues\"} to review.`;\n } else if (low > 0) {\n summary = `Clean of critical, high, and medium issues. ${low} low-severity best-practice ${low === 1 ? \"note\" : \"notes\"}.`;\n } else {\n summary = \"No security issues detected.\";\n }\n\n return { grade, score: rawScore, summary };\n}\n\n// ────────────────────────────────────────────\n// VC079 – JWT Algorithm Confusion\n// ────────────────────────────────────────────\n\nexport const jwtAlgConfusion: CustomRule = {\n id: \"VC079\",\n title: \"JWT Algorithm Confusion (alg:none)\",\n severity: \"critical\",\n category: \"Authentication\",\n description: \"Accepting 'none' as a JWT algorithm allows attackers to forge tokens by removing the signature entirely.\",\n check(content, filePath) {\n if (!/jwt|jsonwebtoken|jose/i.test(content)) return [];\n const matches: RuleMatch[] = [];\n const patterns = [\n /algorithms\\s*:\\s*\\[.*[\"']none[\"']/gi,\n /algorithm\\s*[:=]\\s*[\"']none[\"']/gi,\n ];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, jwtAlgConfusion, filePath, () =>\n \"Never allow algorithm 'none'. Explicitly specify: algorithms: ['RS256'] or algorithms: ['HS256']. Reject tokens with alg:none.\"\n ));\n }\n // Also check for missing algorithm restriction\n if (/jwt\\.verify\\s*\\([^)]*\\)\\s*(?!.*algorithms)/i.test(content) && !/algorithms/i.test(content)) {\n matches.push(...findMatches(content, /jwt\\.verify\\s*\\(/g, jwtAlgConfusion, filePath, () =>\n \"Specify allowed algorithms in jwt.verify: jwt.verify(token, secret, { algorithms: ['HS256'] }).\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC080 – Regex DoS (ReDoS)\n// ────────────────────────────────────────────\n\nexport const regexDos: CustomRule = {\n id: \"VC080\",\n title: \"Potential Regular Expression DoS (ReDoS)\",\n severity: \"high\",\n category: \"Availability\",\n description: \"Nested quantifiers like (a+)+ or (a*){2,} cause catastrophic backtracking, allowing attackers to freeze your server with crafted input.\",\n check(content, filePath) {\n if (filePath.includes(\"test\") || filePath.includes(\"mock\")) return [];\n const matches: RuleMatch[] = [];\n // Detect nested quantifiers in regex\n const patterns = [\n /new\\s+RegExp\\s*\\(\\s*[\"'`].*\\([^)]*[+*]\\)[+*{]/g,\n /\\/.*\\([^)]*[+*]\\)[+*{].*\\//g,\n ];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, regexDos, filePath, () =>\n \"Avoid nested quantifiers in regex. Use atomic groups, possessive quantifiers, or the 're2' library for safe regex execution.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC081 – XML External Entity (XXE)\n// ────────────────────────────────────────────\n\nexport const xxeVulnerability: CustomRule = {\n id: \"VC081\",\n title: \"XML External Entity (XXE) Injection\",\n severity: \"critical\",\n category: \"Injection\",\n description: \"XML parsers that process external entities allow attackers to read files, perform SSRF, or cause DoS via billion-laughs attacks.\",\n check(content, filePath) {\n if (!/xml|parseXML|DOMParser|SAXParser|etree|lxml/i.test(content)) return [];\n const matches: RuleMatch[] = [];\n const patterns = [\n /\\.parseXML\\s*\\(/g,\n /new\\s+DOMParser\\s*\\(\\)/g,\n /etree\\.parse\\s*\\(/g,\n /lxml\\.etree/g,\n /SAXParserFactory/g,\n /XMLReaderFactory/g,\n ];\n const hasProtection = /noent.*false|resolveExternals.*false|FEATURE_EXTERNAL.*false|defusedxml|disallow-doctype-decl/i.test(content);\n if (hasProtection) return [];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, xxeVulnerability, filePath, () =>\n \"Disable external entities: set noent: false, or use defusedxml (Python). For Java: factory.setFeature('http://apache.org/xml/features/disallow-doctype-decl', true).\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC082 – Server-Side Template Injection\n// ────────────────────────────────────────────\n\nexport const ssti: CustomRule = {\n id: \"VC082\",\n title: \"Server-Side Template Injection (SSTI)\",\n severity: \"critical\",\n category: \"Injection\",\n description: \"Rendering templates from user-controlled strings allows attackers to execute arbitrary code on the server.\",\n check(content, filePath) {\n if (filePath.includes(\"test\") || filePath.includes(\"mock\")) return [];\n const matches: RuleMatch[] = [];\n const patterns = [\n /render_template_string\\s*\\(\\s*(?![\"'`])/g,\n /Template\\s*\\(\\s*(?:req\\.|body\\.|input|params|args|user)/gi,\n /engine\\.render\\s*\\(\\s*(?:req\\.|body\\.|input)/gi,\n /nunjucks\\.renderString\\s*\\(\\s*(?:req\\.|body\\.|input)/gi,\n /ejs\\.render\\s*\\(\\s*(?:req\\.|body\\.|input)/gi,\n ];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, ssti, filePath, () =>\n \"Never render templates from user input. Use pre-defined templates and pass data as context variables: render_template('template.html', data=user_data).\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC083 – Insecure Java Deserialization\n// ────────────────────────────────────────────\n\nexport const javaDeserialization: CustomRule = {\n id: \"VC083\",\n title: \"Insecure Java Deserialization\",\n severity: \"critical\",\n category: \"Injection\",\n description: \"ObjectInputStream.readObject() on untrusted data allows arbitrary code execution via gadget chains in the classpath.\",\n check(content, filePath) {\n if (!filePath.match(/\\.java$|\\.kt$/)) return [];\n const matches: RuleMatch[] = [];\n const patterns = [\n /ObjectInputStream\\s*\\(/g,\n /\\.readObject\\s*\\(\\)/g,\n /XMLDecoder\\s*\\(/g,\n /XStream\\s*\\(\\)/g,\n ];\n const hasSafe = /ValidatingObjectInputStream|ObjectInputFilter|SerialKiller|NotSerializableException/i.test(content);\n if (hasSafe) return [];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, javaDeserialization, filePath, () =>\n \"Use ValidatingObjectInputStream with an allowlist of classes, or avoid Java serialization entirely. Use JSON instead.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC084 – Missing Subresource Integrity (SRI)\n// ────────────────────────────────────────────\n\nexport const missingSRI: CustomRule = {\n id: \"VC084\",\n title: \"Missing Subresource Integrity (SRI)\",\n severity: \"medium\",\n category: \"Configuration\",\n description: \"External scripts and stylesheets loaded without integrity= attributes can be tampered with if the CDN is compromised.\",\n check(content, filePath) {\n if (!filePath.match(/\\.(html|htm|jsx|tsx|ejs|hbs)$/)) return [];\n const matches: RuleMatch[] = [];\n // Script tags with external src but no integrity\n const scriptPattern = /<script\\s+[^>]*src\\s*=\\s*[\"']https?:\\/\\/[^\"']+[\"'][^>]*>/gi;\n let m: RegExpExecArray | null;\n const re = new RegExp(scriptPattern.source, scriptPattern.flags);\n while ((m = re.exec(content)) !== null) {\n if (!m[0].includes(\"integrity\")) {\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n matches.push({\n rule: \"VC084\", title: missingSRI.title, severity: \"medium\", category: \"Configuration\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: 'Add integrity and crossorigin attributes: <script src=\"...\" integrity=\"sha384-...\" crossorigin=\"anonymous\">'\n });\n }\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC085 – Exposed Admin/Debug Routes\n// ────────────────────────────────────────────\n\nexport const exposedAdminRoutes: CustomRule = {\n id: \"VC085\",\n title: \"Exposed Admin or Debug Route\",\n severity: \"high\",\n category: \"Information Leakage\",\n description: \"Routes like /admin, /debug, /phpinfo, or /actuator without authentication expose sensitive controls and information to attackers.\",\n check(content, filePath) {\n if (!/(?:\\/api\\/|routes?\\/|server\\.|app\\.|index\\.[jt]s)/i.test(filePath)) return [];\n const matches: RuleMatch[] = [];\n const patterns = [\n /[.'\"]\\s*(?:get|use|all)\\s*\\(\\s*[\"'`]\\/(?:admin|debug|_debug|__debug__|phpinfo|actuator|graphiql|playground|swagger|reset|seed|test|dev|mock)[\"'`]/gi,\n ];\n const hasAuth = /auth|requireAuth|isAdmin|requireAdmin|authenticate|middleware.*admin|requireUser/i.test(content);\n if (hasAuth) return [];\n // Skip if there's a dev-only guard\n if (/NODE_ENV\\s*!==\\s*[\"']development[\"']|NODE_ENV\\s*===\\s*[\"']production[\"']/i.test(content)) return [];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, exposedAdminRoutes, filePath, () =>\n \"Protect admin/debug routes with authentication middleware. In production, disable debug endpoints entirely.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC086 – Insecure WebSocket\n// ────────────────────────────────────────────\n\nexport const insecureWebSocket: CustomRule = {\n id: \"VC086\",\n title: \"Insecure WebSocket Connection (ws://)\",\n severity: \"medium\",\n category: \"Configuration\",\n description: \"Using ws:// instead of wss:// transmits data in plaintext, vulnerable to eavesdropping and man-in-the-middle attacks.\",\n check(content, filePath) {\n if (filePath.includes(\"test\") || filePath.includes(\"mock\")) return [];\n return findMatches(content, /new\\s+WebSocket\\s*\\(\\s*[\"'`]ws:\\/\\//g, insecureWebSocket, filePath, () =>\n \"Use wss:// (WebSocket Secure) instead of ws:// for encrypted connections: new WebSocket('wss://...').\"\n );\n },\n};\n\n// ────────────────────────────────────────────\n// VC087 – Missing HSTS\n// ────────────────────────────────────────────\n\nexport const missingHSTS: CustomRule = {\n id: \"VC087\",\n title: \"Missing HTTP Strict Transport Security (HSTS)\",\n severity: \"medium\",\n category: \"Configuration\",\n description: \"Without HSTS headers, browsers allow downgrade attacks from HTTPS to HTTP, exposing traffic to interception.\",\n check(content, filePath) {\n if (!/(?:server|app|index|main)\\.[jt]sx?$/.test(filePath)) return [];\n if (!/(?:express|hono|fastify|koa)/i.test(content)) return [];\n if (/Strict-Transport-Security|hsts|helmet/i.test(content)) return [];\n return findMatches(content, /(?:express|hono|fastify|koa)\\s*\\(/gi, missingHSTS, filePath, () =>\n \"Add HSTS header: res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'). Or use helmet().\"\n );\n },\n};\n\n// ────────────────────────────────────────────\n// VC088 – Sensitive Data in URL Parameters\n// ────────────────────────────────────────────\n\nexport const sensitiveURLParams: CustomRule = {\n id: \"VC088\",\n title: \"Sensitive Data in URL Parameters\",\n severity: \"high\",\n category: \"Information Leakage\",\n description: \"Passing passwords, tokens, or API keys in URL query parameters exposes them in server logs, browser history, and referrer headers.\",\n check(content, filePath) {\n // Only scan code files, never docs/README/examples/configs\n if (!filePath.match(/\\.(js|ts|jsx|tsx|py|rb|go|java|php)$/)) return [];\n if (isTestFile(filePath)) return [];\n const matches: RuleMatch[] = [];\n const patterns = [\n // Actual code building URLs with sensitive params (template literals or concat)\n /[&?](?:password|secret|api_key|apiKey|access_token|ssn|credit_card)\\s*=\\s*\\$\\{/gi,\n /[&?](?:password|secret|api_key|apiKey|access_token)\\s*=[\"']\\s*\\+/gi,\n ];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, sensitiveURLParams, filePath, () =>\n \"Never pass sensitive data in URL parameters. Use request headers (Authorization: Bearer ...) or POST body instead.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC089 – Missing Content-Disposition\n// ────────────────────────────────────────────\n\nexport const missingContentDisposition: CustomRule = {\n id: \"VC089\",\n title: \"File Download Missing Content-Disposition\",\n severity: \"medium\",\n category: \"Configuration\",\n description: \"File download endpoints without Content-Disposition headers may render files inline, leading to XSS if the file contains HTML/JS.\",\n check(content, filePath) {\n if (!/(?:download|sendFile|send_file|pipe|createReadStream)/i.test(content)) return [];\n if (!/(?:\\/api\\/|routes?\\/|controllers?\\/|server\\.|handler)/i.test(filePath)) return [];\n if (/Content-Disposition|attachment|download/i.test(content)) return [];\n return findMatches(content, /(?:sendFile|send_file|createReadStream|\\.pipe)\\s*\\(/gi, missingContentDisposition, filePath, () =>\n \"Set Content-Disposition: attachment header on file downloads: res.setHeader('Content-Disposition', 'attachment; filename=\\\"file.pdf\\\"').\"\n );\n },\n};\n\n// ────────────────────────────────────────────\n// VC090 – Open Redirect via Host Header\n// ────────────────────────────────────────────\n\nexport const hostHeaderRedirect: CustomRule = {\n id: \"VC090\",\n title: \"Open Redirect via Host Header\",\n severity: \"high\",\n category: \"Injection\",\n description: \"Using req.headers.host to construct redirect URLs allows attackers to inject a malicious host header, redirecting users to phishing sites.\",\n check(content, filePath) {\n if (filePath.includes(\"test\") || filePath.includes(\"mock\")) return [];\n const matches: RuleMatch[] = [];\n if (/req\\.headers\\.host|req\\.get\\s*\\(\\s*[\"']host[\"']\\)/i.test(content) && /redirect|location/i.test(content)) {\n matches.push(...findMatches(content, /req\\.headers\\.host|req\\.get\\s*\\(\\s*[\"']host[\"']\\)/gi, hostHeaderRedirect, filePath, () =>\n \"Don't use req.headers.host for redirects — it's attacker-controlled. Use a hardcoded domain or environment variable.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC091 – Race Condition / TOCTOU\n// ────────────────────────────────────────────\n\nexport const raceCondition: CustomRule = {\n id: \"VC091\",\n title: \"Potential Race Condition (TOCTOU)\",\n severity: \"high\",\n category: \"Authorization\",\n description: \"Check-then-act patterns (e.g., checking if a file exists then writing to it) are vulnerable to race conditions where state changes between the check and the action.\",\n check(content, filePath) {\n if (filePath.includes(\"test\") || filePath.includes(\"mock\")) return [];\n const matches: RuleMatch[] = [];\n const patterns = [\n /(?:existsSync|exists)\\s*\\([^)]+\\)[\\s\\S]{0,50}(?:writeFileSync|writeFile|unlinkSync|unlink|renameSync)\\s*\\(/g,\n /os\\.path\\.exists\\s*\\([^)]+\\)[\\s\\S]{0,50}open\\s*\\(/g,\n /File\\.exists\\?\\s*\\([^)]+\\)[\\s\\S]{0,50}File\\.(?:write|delete)/g,\n ];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, raceCondition, filePath, () =>\n \"Use atomic operations instead of check-then-act. For files: use fs.open with 'wx' flag (exclusive create), or use file locks.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC092 – Unsafe Object.assign from User Input\n// ────────────────────────────────────────────\n\nexport const unsafeObjectAssign: CustomRule = {\n id: \"VC092\",\n title: \"Unsafe Object Spread from User Input\",\n severity: \"medium\",\n category: \"Injection\",\n description: \"Spreading request body into a new object can copy __proto__, constructor, or other dangerous properties, enabling prototype pollution.\",\n check(content, filePath) {\n if (filePath.includes(\"test\") || filePath.includes(\"mock\")) return [];\n const matches: RuleMatch[] = [];\n const patterns = [\n /Object\\.assign\\s*\\(\\s*\\{\\s*\\}\\s*,\\s*(?:req\\.(?:body|query|params)|body|input)\\s*\\)/gi,\n /\\{\\s*\\.\\.\\.(?:req\\.(?:body|query|params)|body|input)\\s*\\}/gi,\n ];\n const hasSafe = /omit.*__proto__|sanitize|pick\\(|lodash\\.pick|stripProto/i.test(content);\n if (hasSafe) return [];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, unsafeObjectAssign, filePath, () =>\n \"Explicitly pick allowed properties instead of spreading: const { name, email } = req.body; const safe = { name, email };\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC093 – Unprotected File Download Endpoint\n// ────────────────────────────────────────────\n\nexport const unprotectedDownload: CustomRule = {\n id: \"VC093\",\n title: \"File Download Without Path Validation\",\n severity: \"medium\",\n category: \"Authorization\",\n description: \"File download endpoints that accept user-controlled filenames without path validation allow directory traversal to read arbitrary files.\",\n check(content, filePath) {\n if (!/(?:download|sendFile|send_file)/i.test(content)) return [];\n if (!/(?:\\/api\\/|routes?\\/|controllers?\\/|server\\.|handler)/i.test(filePath)) return [];\n const matches: RuleMatch[] = [];\n // sendFile/download with user input\n if (/(?:sendFile|download|send_file)\\s*\\([^)]*(?:req\\.|params\\.|query\\.|body\\.)/i.test(content)) {\n const hasValidation = /path\\.resolve|path\\.normalize|path\\.join.*__dirname|realpath|includes\\s*\\(\\s*[\"']\\.\\./i.test(content);\n if (!hasValidation) {\n matches.push(...findMatches(content, /(?:sendFile|download|send_file)\\s*\\(/gi, unprotectedDownload, filePath, () =>\n \"Validate file paths: const safePath = path.resolve(DOWNLOAD_DIR, filename); if (!safePath.startsWith(DOWNLOAD_DIR)) throw new Error('Invalid path');\"\n ));\n }\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC094 – Command Injection\n// ────────────────────────────────────────────\n\nexport const commandInjection: CustomRule = {\n id: \"VC094\",\n title: \"Potential Command Injection\",\n severity: \"critical\",\n category: \"Injection\",\n description: \"Passing user input to shell commands (exec, system, child_process) allows attackers to execute arbitrary system commands.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!filePath.match(/\\.(js|ts|jsx|tsx|py|rb)$/)) return [];\n const matches: RuleMatch[] = [];\n const patterns = [\n // Node.js — require standalone exec/execSync, not db.exec() or conn.exec()\n /(?<![.\\w])(?:exec|execSync)\\s*\\(\\s*(?:`[^`]*\\$\\{|[\"'][^\"']*\\+\\s*(?:req\\.|body\\.|input|params|args|user))/gi,\n /child_process.*exec\\s*\\(\\s*(?![\"'`])/g,\n // Python\n /os\\.system\\s*\\(\\s*(?![\"'`].*[\"'`]\\s*\\))/g,\n /subprocess\\.(?:call|run|Popen)\\s*\\([^)]*shell\\s*=\\s*True/gi,\n // Ruby\n /system\\s*\\(\\s*[\"'].*#\\{/g,\n ];\n const hasSafe = /execFile|spawn|escapeshellarg|shlex\\.quote|shellEscape/i.test(content);\n if (hasSafe) return [];\n for (const p of patterns) {\n const raw = findMatches(content, p, commandInjection, filePath, () =>\n \"Use execFile/spawn instead of exec (avoids shell). Never concatenate user input into shell commands. Use parameterized arguments.\"\n );\n // Filter out database .exec() calls (better-sqlite3, knex, sequelize, etc.)\n for (const m of raw) {\n const lineText = content.split(\"\\n\")[m.line - 1] || \"\";\n if (/(?:conn|db|database|knex|sequelize|prisma|sqlite|pool|client)\\s*\\.exec/i.test(lineText)) continue;\n if (/ALTER\\s+TABLE|CREATE\\s+TABLE|DROP\\s+TABLE|INSERT\\s+INTO/i.test(lineText)) continue;\n matches.push(m);\n }\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC095 – Hardcoded CORS Origin Localhost\n// ────────────────────────────────────────────\n\nexport const corsLocalhost: CustomRule = {\n id: \"VC095\",\n title: \"Hardcoded Localhost CORS Origin\",\n severity: \"medium\",\n category: \"Configuration\",\n description: \"Hardcoded localhost CORS origins in production code allow any local process to make authenticated requests to your API.\",\n check(content, filePath) {\n if (filePath.includes(\"test\") || filePath.includes(\"mock\") || filePath.includes(\".env\")) return [];\n if (!/origin/i.test(content)) return [];\n return findMatches(content, /origin\\s*[:=]\\s*[\"'`]http:\\/\\/localhost/gi, corsLocalhost, filePath, () =>\n \"Use environment variables for CORS origins: origin: process.env.ALLOWED_ORIGIN. Remove localhost from production configs.\"\n );\n },\n};\n\n// ────────────────────────────────────────────\n// VC096 – Unencrypted gRPC\n// ────────────────────────────────────────────\n\nexport const insecureGRPC: CustomRule = {\n id: \"VC096\",\n title: \"Unencrypted gRPC Channel\",\n severity: \"medium\",\n category: \"Configuration\",\n description: \"Using insecure gRPC channels transmits data including credentials in plaintext.\",\n check(content, filePath) {\n if (!/grpc/i.test(content)) return [];\n return findMatches(content, /(?:insecure_channel|createInsecure|grpc\\.Insecure)/gi, insecureGRPC, filePath, () =>\n \"Use encrypted gRPC channels: grpc.ssl_channel_credentials() or grpc.credentials.createSsl().\"\n );\n },\n};\n\n// ────────────────────────────────────────────\n// COMPLIANCE MAPPING (OWASP Top 10 + CWE)\n// ────────────────────────────────────────────\n\nexport const complianceMap: Record<string, { owasp: string; cwe: string }> = {\n VC001: { owasp: \"A07:2021\", cwe: \"CWE-798\" },\n VC002: { owasp: \"A05:2021\", cwe: \"CWE-200\" },\n VC003: { owasp: \"A01:2021\", cwe: \"CWE-862\" },\n VC004: { owasp: \"A01:2021\", cwe: \"CWE-284\" },\n VC005: { owasp: \"A08:2021\", cwe: \"CWE-345\" },\n VC006: { owasp: \"A03:2021\", cwe: \"CWE-89\" },\n VC007: { owasp: \"A03:2021\", cwe: \"CWE-79\" },\n VC008: { owasp: \"A04:2021\", cwe: \"CWE-770\" },\n VC009: { owasp: \"A05:2021\", cwe: \"CWE-942\" },\n VC010: { owasp: \"A01:2021\", cwe: \"CWE-602\" },\n VC011: { owasp: \"A07:2021\", cwe: \"CWE-798\" },\n VC012: { owasp: \"A05:2021\", cwe: \"CWE-200\" },\n VC013: { owasp: \"A01:2021\", cwe: \"CWE-269\" },\n VC014: { owasp: \"A05:2021\", cwe: \"CWE-538\" },\n VC015: { owasp: \"A03:2021\", cwe: \"CWE-95\" },\n VC016: { owasp: \"A01:2021\", cwe: \"CWE-601\" },\n VC017: { owasp: \"A05:2021\", cwe: \"CWE-614\" },\n VC018: { owasp: \"A07:2021\", cwe: \"CWE-798\" },\n VC019: { owasp: \"A05:2021\", cwe: \"CWE-693\" },\n VC020: { owasp: \"A05:2021\", cwe: \"CWE-1021\" },\n VC021: { owasp: \"A01:2021\", cwe: \"CWE-22\" },\n VC022: { owasp: \"A03:2021\", cwe: \"CWE-79\" },\n VC023: { owasp: \"A08:2021\", cwe: \"CWE-1321\" },\n VC024: { owasp: \"A04:2021\", cwe: \"CWE-770\" },\n VC025: { owasp: \"A03:2021\", cwe: \"CWE-22\" },\n VC026: { owasp: \"A05:2021\", cwe: \"CWE-693\" },\n VC027: { owasp: \"A05:2021\", cwe: \"CWE-693\" },\n VC028: { owasp: \"A07:2021\", cwe: \"CWE-20\" },\n VC029: { owasp: \"A08:2021\", cwe: \"CWE-20\" },\n VC030: { owasp: \"A08:2021\", cwe: \"CWE-502\" },\n VC031: { owasp: \"A02:2021\", cwe: \"CWE-321\" },\n VC032: { owasp: \"A05:2021\", cwe: \"CWE-319\" },\n VC033: { owasp: \"A05:2021\", cwe: \"CWE-215\" },\n VC034: { owasp: \"A02:2021\", cwe: \"CWE-338\" },\n VC035: { owasp: \"A01:2021\", cwe: \"CWE-601\" },\n VC036: { owasp: \"A04:2021\", cwe: \"CWE-755\" },\n VC037: { owasp: \"A09:2021\", cwe: \"CWE-209\" },\n VC038: { owasp: \"A04:2021\", cwe: \"CWE-434\" },\n VC039: { owasp: \"A06:2021\", cwe: \"CWE-1104\" },\n VC040: { owasp: \"A05:2021\", cwe: \"CWE-538\" },\n VC041: { owasp: \"A10:2021\", cwe: \"CWE-918\" },\n VC042: { owasp: \"A01:2021\", cwe: \"CWE-915\" },\n VC043: { owasp: \"A02:2021\", cwe: \"CWE-208\" },\n VC044: { owasp: \"A09:2021\", cwe: \"CWE-117\" },\n VC045: { owasp: \"A07:2021\", cwe: \"CWE-521\" },\n VC046: { owasp: \"A07:2021\", cwe: \"CWE-384\" },\n VC047: { owasp: \"A07:2021\", cwe: \"CWE-307\" },\n VC048: { owasp: \"A03:2021\", cwe: \"CWE-943\" },\n VC049: { owasp: \"A07:2021\", cwe: \"CWE-798\" },\n VC050: { owasp: \"A02:2021\", cwe: \"CWE-319\" },\n VC051: { owasp: \"A05:2021\", cwe: \"CWE-200\" },\n VC052: { owasp: \"A04:2021\", cwe: \"CWE-770\" },\n VC053: { owasp: \"A05:2021\", cwe: \"CWE-798\" },\n VC054: { owasp: \"A07:2021\", cwe: \"CWE-922\" },\n VC055: { owasp: \"A05:2021\", cwe: \"CWE-540\" },\n VC056: { owasp: \"A05:2021\", cwe: \"CWE-1021\" },\n VC057: { owasp: \"A01:2021\", cwe: \"CWE-269\" },\n VC058: { owasp: \"A05:2021\", cwe: \"CWE-250\" },\n VC059: { owasp: \"A05:2021\", cwe: \"CWE-284\" },\n VC060: { owasp: \"A02:2021\", cwe: \"CWE-328\" },\n VC061: { owasp: \"A02:2021\", cwe: \"CWE-295\" },\n VC062: { owasp: \"A02:2021\", cwe: \"CWE-321\" },\n VC063: { owasp: \"A03:2021\", cwe: \"CWE-79\" },\n VC064: { owasp: \"A01:2021\", cwe: \"CWE-862\" },\n VC065: { owasp: \"A01:2021\", cwe: \"CWE-862\" },\n VC066: { owasp: \"A07:2021\", cwe: \"CWE-798\" },\n VC067: { owasp: \"A01:2021\", cwe: \"CWE-601\" },\n VC068: { owasp: \"A07:2021\", cwe: \"CWE-922\" },\n VC069: { owasp: \"A02:2021\", cwe: \"CWE-295\" },\n VC070: { owasp: \"A05:2021\", cwe: \"CWE-489\" },\n VC071: { owasp: \"A05:2021\", cwe: \"CWE-215\" },\n VC072: { owasp: \"A07:2021\", cwe: \"CWE-798\" },\n VC073: { owasp: \"A08:2021\", cwe: \"CWE-502\" },\n VC074: { owasp: \"A01:2021\", cwe: \"CWE-352\" },\n VC075: { owasp: \"A03:2021\", cwe: \"CWE-78\" },\n VC076: { owasp: \"A07:2021\", cwe: \"CWE-798\" },\n VC077: { owasp: \"A05:2021\", cwe: \"CWE-942\" },\n VC078: { owasp: \"A05:2021\", cwe: \"CWE-250\" },\n VC079: { owasp: \"A02:2021\", cwe: \"CWE-327\" },\n VC080: { owasp: \"A04:2021\", cwe: \"CWE-1333\" },\n VC081: { owasp: \"A03:2021\", cwe: \"CWE-611\" },\n VC082: { owasp: \"A03:2021\", cwe: \"CWE-94\" },\n VC083: { owasp: \"A08:2021\", cwe: \"CWE-502\" },\n VC084: { owasp: \"A06:2021\", cwe: \"CWE-353\" },\n VC085: { owasp: \"A01:2021\", cwe: \"CWE-862\" },\n VC086: { owasp: \"A02:2021\", cwe: \"CWE-319\" },\n VC087: { owasp: \"A05:2021\", cwe: \"CWE-311\" },\n VC088: { owasp: \"A07:2021\", cwe: \"CWE-598\" },\n VC089: { owasp: \"A05:2021\", cwe: \"CWE-430\" },\n VC090: { owasp: \"A01:2021\", cwe: \"CWE-601\" },\n VC091: { owasp: \"A04:2021\", cwe: \"CWE-367\" },\n VC092: { owasp: \"A08:2021\", cwe: \"CWE-1321\" },\n VC093: { owasp: \"A01:2021\", cwe: \"CWE-22\" },\n VC094: { owasp: \"A03:2021\", cwe: \"CWE-78\" },\n VC095: { owasp: \"A05:2021\", cwe: \"CWE-942\" },\n VC096: { owasp: \"A02:2021\", cwe: \"CWE-319\" },\n VC097: { owasp: \"A09:2021\", cwe: \"CWE-532\" },\n VC098: { owasp: \"A04:2021\", cwe: \"CWE-400\" },\n VC099: { owasp: \"A04:2021\", cwe: \"CWE-401\" },\n VC100: { owasp: \"A04:2021\", cwe: \"CWE-400\" },\n VC101: { owasp: \"A04:2021\", cwe: \"CWE-400\" },\n VC102: { owasp: \"A04:2021\", cwe: \"CWE-400\" },\n VC103: { owasp: \"A04:2021\", cwe: \"CWE-710\" },\n VC104: { owasp: \"A04:2021\", cwe: \"CWE-390\" },\n VC105: { owasp: \"A04:2021\", cwe: \"CWE-710\" },\n VC106: { owasp: \"A04:2021\", cwe: \"CWE-710\" },\n VC107: { owasp: \"A02:2021\", cwe: \"CWE-311\" },\n VC108: { owasp: \"A01:2021\", cwe: \"CWE-284\" },\n VC109: { owasp: \"A01:2021\", cwe: \"CWE-284\" },\n VC110: { owasp: \"A09:2021\", cwe: \"CWE-778\" },\n VC111: { owasp: \"A05:2021\", cwe: \"CWE-16\" },\n VC112: { owasp: \"A06:2021\", cwe: \"CWE-1104\" },\n VC113: { owasp: \"A05:2021\", cwe: \"CWE-200\" },\n VC114: { owasp: \"A05:2021\", cwe: \"CWE-16\" },\n VC115: { owasp: \"A02:2021\", cwe: \"CWE-311\" },\n VC116: { owasp: \"A05:2021\", cwe: \"CWE-770\" },\n VC117: { owasp: \"A03:2021\", cwe: \"CWE-22\" },\n VC118: { owasp: \"A09:2021\", cwe: \"CWE-532\" },\n VC119: { owasp: \"A07:2021\", cwe: \"CWE-798\" },\n VC120: { owasp: \"A07:2021\", cwe: \"CWE-352\" },\n VC121: { owasp: \"A06:2021\", cwe: \"CWE-1104\" },\n VC122: { owasp: \"A02:2021\", cwe: \"CWE-326\" },\n VC123: { owasp: \"A02:2021\", cwe: \"CWE-326\" },\n VC124: { owasp: \"A02:2021\", cwe: \"CWE-327\" },\n VC125: { owasp: \"A07:2021\", cwe: \"CWE-640\" },\n VC126: { owasp: \"A05:2021\", cwe: \"CWE-200\" },\n VC127: { owasp: \"A01:2021\", cwe: \"CWE-862\" },\n VC128: { owasp: \"A05:2021\", cwe: \"CWE-444\" },\n VC129: { owasp: \"A02:2021\", cwe: \"CWE-311\" },\n VC130: { owasp: \"A07:2021\", cwe: \"CWE-307\" },\n VC131: { owasp: \"A06:2021\", cwe: \"CWE-1104\" },\n // VC132–VC145: Service-specific API key detection\n VC132: { owasp: \"A07:2021\", cwe: \"CWE-798\" },\n VC133: { owasp: \"A07:2021\", cwe: \"CWE-798\" },\n VC134: { owasp: \"A07:2021\", cwe: \"CWE-798\" },\n VC135: { owasp: \"A07:2021\", cwe: \"CWE-798\" },\n VC136: { owasp: \"A07:2021\", cwe: \"CWE-798\" },\n VC137: { owasp: \"A07:2021\", cwe: \"CWE-798\" },\n VC138: { owasp: \"A07:2021\", cwe: \"CWE-798\" },\n VC139: { owasp: \"A07:2021\", cwe: \"CWE-798\" },\n VC140: { owasp: \"A07:2021\", cwe: \"CWE-798\" },\n VC141: { owasp: \"A07:2021\", cwe: \"CWE-798\" },\n VC142: { owasp: \"A07:2021\", cwe: \"CWE-798\" },\n VC143: { owasp: \"A07:2021\", cwe: \"CWE-798\" },\n VC144: { owasp: \"A07:2021\", cwe: \"CWE-798\" },\n VC145: { owasp: \"A07:2021\", cwe: \"CWE-798\" },\n // VC146–VC151: Secret exposure path analysis\n VC146: { owasp: \"A07:2021\", cwe: \"CWE-598\" },\n VC147: { owasp: \"A09:2021\", cwe: \"CWE-532\" },\n VC148: { owasp: \"A09:2021\", cwe: \"CWE-209\" },\n VC149: { owasp: \"A07:2021\", cwe: \"CWE-798\" },\n VC150: { owasp: \"A07:2021\", cwe: \"CWE-615\" },\n VC151: { owasp: \"A07:2021\", cwe: \"CWE-214\" },\n // VC152–VC158: New rules\n VC152: { owasp: \"A08:2021\", cwe: \"CWE-345\" },\n VC153: { owasp: \"A05:2021\", cwe: \"CWE-942\" },\n VC154: { owasp: \"A03:2021\", cwe: \"CWE-20\" },\n VC155: { owasp: \"A04:2021\", cwe: \"CWE-770\" },\n VC156: { owasp: \"A04:2021\", cwe: \"CWE-770\" },\n VC157: { owasp: \"A05:2021\", cwe: \"CWE-16\" },\n VC158: { owasp: \"A01:2021\", cwe: \"CWE-639\" },\n};\n\n// ────────────────────────────────────────────\n// VC097 – Console.log in Production\n// ────────────────────────────────────────────\n\nexport const consoleLogProduction: CustomRule = {\n id: \"VC097\",\n title: \"Console.log Left in Production Code\",\n severity: \"low\",\n category: \"Performance\",\n description: \"console.log statements left in production code can leak sensitive data, slow down rendering, and clutter browser consoles.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n // Skip server-side scripts, migration files, CLI tools, and seed files\n if (/(?:migrate|seed|script|cli|setup|dev)\\./i.test(filePath)) return [];\n if (!/console\\.log\\s*\\(/g.test(content)) return [];\n const lines = content.split(\"\\n\");\n const logCount = lines.filter(l => /console\\.log\\s*\\(/.test(l.trim()) && !l.trim().startsWith(\"//\")).length;\n // If a file has many console.logs, it's intentional logging, not forgotten debug statements\n if (logCount > 5) return [];\n const matches: RuleMatch[] = [];\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i].trim();\n if (/console\\.log\\s*\\(/.test(line) && !line.startsWith(\"//\") && !line.startsWith(\"*\") && !/if\\s*\\(\\s*(?:debug|process\\.env)/i.test(lines[Math.max(0, i-1)] + line)) {\n matches.push({ rule: \"VC097\", title: consoleLogProduction.title, severity: \"low\" as const, category: \"Performance\", file: filePath, line: i + 1, snippet: getSnippet(content, i + 1), fix: \"Remove console.log or use a structured logger that can be disabled in production.\" });\n }\n }\n return matches.slice(0, 1); // Max 1 per file to avoid noise\n },\n};\n\n// ────────────────────────────────────────────\n// VC098 – Synchronous File Operations\n// ────────────────────────────────────────────\n\nexport const syncFileOps: CustomRule = {\n id: \"VC098\",\n title: \"Synchronous File Operations\",\n severity: \"medium\",\n category: \"Performance\",\n description: \"Synchronous file operations (readFileSync, writeFileSync) block the event loop, causing all other requests to wait.\",\n check(content, filePath) {\n if (filePath.match(/test|spec|mock|__tests__|fixture|config|\\.config\\./i)) return [];\n return findMatches(content, /(?:readFileSync|writeFileSync|appendFileSync|mkdirSync|rmdirSync|statSync)\\s*\\(/g, syncFileOps, filePath, () =>\n \"Use async file operations (readFile, writeFile) to avoid blocking the event loop.\"\n );\n },\n};\n\n// ────────────────────────────────────────────\n// VC099 – Memory Leak: Event Listener\n// ────────────────────────────────────────────\n\nexport const eventListenerLeak: CustomRule = {\n id: \"VC099\",\n title: \"Memory Leak: Event Listener Not Cleaned Up\",\n severity: \"high\",\n category: \"Performance\",\n description: \"Adding event listeners in React useEffect without a cleanup function causes memory leaks as listeners accumulate on re-renders.\",\n check(content, filePath) {\n if (!filePath.match(/\\.(jsx|tsx)$/)) return [];\n if (!/addEventListener/i.test(content)) return [];\n if (/removeEventListener/i.test(content)) return [];\n if (!/useEffect/i.test(content)) return [];\n return findMatches(content, /addEventListener\\s*\\(/g, eventListenerLeak, filePath, () =>\n \"Return a cleanup function from useEffect: useEffect(() => { window.addEventListener('resize', fn); return () => window.removeEventListener('resize', fn); }, []);\"\n );\n },\n};\n\n// ────────────────────────────────────────────\n// VC100 – N+1 Query Pattern\n// ────────────────────────────────────────────\n\nexport const nPlusOneQuery: CustomRule = {\n id: \"VC100\",\n title: \"N+1 Query Pattern Detected\",\n severity: \"medium\",\n category: \"Performance\",\n description: \"Database queries inside loops cause N+1 performance problems — one query per iteration instead of a single batch query.\",\n check(content, filePath) {\n if (filePath.match(/test|spec|mock/i)) return [];\n const hasLoopWithQuery = /(?:for\\s*\\(|\\.forEach\\s*\\(|\\.map\\s*\\(|while\\s*\\()[^}]*(?:\\.find\\(|\\.findOne\\(|\\.findById\\(|\\.query\\(|\\.execute\\(|SELECT\\s)/is.test(content);\n if (!hasLoopWithQuery) return [];\n return findMatches(content, /(?:for\\s*\\(|\\.forEach\\s*\\(|\\.map\\s*\\(|while\\s*\\()/g, nPlusOneQuery, filePath, () =>\n \"Fetch all data in a single query using WHERE IN, JOIN, or batch operations instead of querying per item in a loop.\"\n ).slice(0, 2);\n },\n};\n\n// ────────────────────────────────────────────\n// VC101 – Large Bundle Import\n// ────────────────────────────────────────────\n\nexport const largeBundleImport: CustomRule = {\n id: \"VC101\",\n title: \"Importing Entire Library (Large Bundle)\",\n severity: \"medium\",\n category: \"Performance\",\n description: \"Importing entire libraries like lodash or moment.js adds hundreds of KB to your bundle. Import only the functions you need.\",\n check(content, filePath) {\n if (!filePath.match(/\\.(jsx?|tsx?)$/)) return [];\n const matches: RuleMatch[] = [];\n const patterns = [\n /import\\s+_\\s+from\\s+['\"]lodash['\"]/g,\n /import\\s+\\*\\s+as\\s+_\\s+from\\s+['\"]lodash['\"]/g,\n /import\\s+moment\\s+from\\s+['\"]moment['\"]/g,\n /const\\s+_\\s*=\\s*require\\s*\\(\\s*['\"]lodash['\"]\\s*\\)/g,\n /const\\s+moment\\s*=\\s*require\\s*\\(\\s*['\"]moment['\"]\\s*\\)/g,\n ];\n for (const p of patterns) {\n matches.push(...findMatches(content, p, largeBundleImport, filePath, () =>\n \"Import only what you need: import { debounce } from 'lodash/debounce'. Or switch to lighter alternatives like date-fns instead of moment.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC102 – Blocking Main Thread\n// ────────────────────────────────────────────\n\nexport const blockingMainThread: CustomRule = {\n id: \"VC102\",\n title: \"Blocking Main Thread with Heavy Computation\",\n severity: \"medium\",\n category: \"Performance\",\n description: \"Infinite loops or deeply nested iterations on the main thread freeze the UI and cause unresponsiveness.\",\n check(content, filePath) {\n if (filePath.match(/worker|test|spec|mock/i)) return [];\n const matches: RuleMatch[] = [];\n if (/while\\s*\\(\\s*true\\s*\\)/g.test(content)) {\n matches.push(...findMatches(content, /while\\s*\\(\\s*true\\s*\\)/g, blockingMainThread, filePath, () =>\n \"Avoid while(true) on the main thread. Use Web Workers for heavy computation or requestIdleCallback for non-urgent work.\"\n ));\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC103 – TODO/FIXME Left in Code\n// ────────────────────────────────────────────\n\nexport const todoLeftInCode: CustomRule = {\n id: \"VC103\",\n title: \"TODO/FIXME Left in Code\",\n severity: \"low\",\n category: \"Code Quality\",\n description: \"TODO, FIXME, HACK, and XXX comments indicate unfinished work that should be resolved before shipping to production.\",\n check(content, filePath) {\n if (filePath.match(/test|spec|mock|__tests__|fixture|node_modules/i)) return [];\n return findMatches(content, /\\/\\/\\s*(?:TODO|FIXME|HACK|XXX)\\b/gi, todoLeftInCode, filePath, () =>\n \"Resolve TODO/FIXME comments before shipping. If it's intentional tech debt, track it in your issue tracker instead.\"\n ).slice(0, 5);\n },\n};\n\n// ────────────────────────────────────────────\n// VC104 – Empty Catch Block\n// ────────────────────────────────────────────\n\nexport const emptyCatchBlock: CustomRule = {\n id: \"VC104\",\n title: \"Empty Catch Block\",\n severity: \"medium\",\n category: \"Code Quality\",\n description: \"Empty catch blocks silently swallow errors, making bugs impossible to diagnose. At minimum, log the error.\",\n check(content, filePath) {\n if (filePath.match(/test|spec|mock/i)) return [];\n return findMatches(content, /catch\\s*(?:\\([^)]*\\))?\\s*\\{\\s*\\}/g, emptyCatchBlock, filePath, () =>\n \"Handle errors in catch blocks: catch(err) { console.error('Operation failed:', err); } or re-throw if appropriate.\"\n );\n },\n};\n\n// ────────────────────────────────────────────\n// VC105 – Callback Hell\n// ────────────────────────────────────────────\n\nexport const callbackHell: CustomRule = {\n id: \"VC105\",\n title: \"Deeply Nested Callbacks (Promise Chain)\",\n severity: \"medium\",\n category: \"Code Quality\",\n description: \"Long .then() chains or deeply nested callbacks are hard to read, debug, and maintain. Refactor to async/await.\",\n check(content, filePath) {\n if (filePath.match(/test|spec|mock/i)) return [];\n return findMatches(content, /\\.then\\s*\\([^)]*\\)\\s*\\.then\\s*\\([^)]*\\)\\s*\\.then/g, callbackHell, filePath, () =>\n \"Refactor .then() chains to async/await for cleaner, more readable code.\"\n );\n },\n};\n\n// ────────────────────────────────────────────\n// VC106 – Magic Numbers\n// ────────────────────────────────────────────\n\nexport const magicNumbers: CustomRule = {\n id: \"VC106\",\n title: \"Magic Numbers in Code\",\n severity: \"low\",\n category: \"Code Quality\",\n description: \"Unnamed numeric constants in conditions or calculations make code hard to understand. Extract them into named constants.\",\n check(content, filePath) {\n if (filePath.match(/test|spec|mock|config|\\.config\\.|constant|enum|migration/i)) return [];\n if (!filePath.match(/\\.(jsx?|tsx?)$/)) return [];\n const matches: RuleMatch[] = [];\n const lines = content.split(\"\\n\");\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i].trim();\n if (line.startsWith(\"//\") || line.startsWith(\"*\")) continue;\n // Look for conditions with magic numbers > 1 (skip 0, 1, -1, 2)\n if (/(?:===|!==|>=?|<=?)\\s*\\d{3,}/.test(line) || /(?:setTimeout|setInterval)\\s*\\([^,]+,\\s*\\d{4,}/.test(line)) {\n matches.push({ rule: \"VC106\", title: magicNumbers.title, severity: \"low\" as const, category: \"Code Quality\", file: filePath, line: i + 1, snippet: getSnippet(content, i + 1), fix: \"Extract magic numbers into named constants: const MAX_RETRIES = 3; const TIMEOUT_MS = 5000;\" });\n }\n }\n return matches.slice(0, 3);\n },\n};\n\n// ────────────────────────────────────────────\n// VC107 – S3 Bucket Without Encryption\n// ────────────────────────────────────────────\n\nexport const s3BucketNoEncryption: CustomRule = {\n id: \"VC107\",\n title: \"S3 Bucket Without Encryption\",\n severity: \"high\",\n category: \"Infrastructure\",\n description: \"AWS S3 buckets without server-side encryption leave data at rest unprotected. Enable encryption to protect sensitive data.\",\n check(content, filePath) {\n if (!filePath.match(/\\.tf$/)) return [];\n if (!/resource\\s+\"aws_s3_bucket\"/.test(content)) return [];\n const matches: RuleMatch[] = [];\n // Find S3 bucket resources without encryption configuration nearby\n const bucketPattern = /resource\\s+\"aws_s3_bucket\"\\s+\"(\\w+)\"/g;\n let m: RegExpExecArray | null;\n while ((m = bucketPattern.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n // Check if there's a server_side_encryption_configuration within a reasonable range\n const blockEnd = Math.min(m.index + 2000, content.length);\n const blockContent = content.substring(m.index, blockEnd);\n if (!/server_side_encryption/.test(blockContent)) {\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n matches.push({\n rule: \"VC107\", title: s3BucketNoEncryption.title, severity: \"high\" as const, category: \"Infrastructure\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Enable S3 bucket encryption: server_side_encryption_configuration { rule { apply_server_side_encryption_by_default { sse_algorithm = 'aws:kms' } } }\",\n });\n }\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC108 – Security Group Allows All Inbound\n// ────────────────────────────────────────────\n\nexport const securityGroupAllInbound: CustomRule = {\n id: \"VC108\",\n title: \"Security Group Allows All Inbound\",\n severity: \"critical\",\n category: \"Infrastructure\",\n description: \"Security groups allowing all inbound traffic (0.0.0.0/0 on all ports) expose resources to the entire internet.\",\n check(content, filePath) {\n if (!filePath.match(/\\.(tf|json|yaml|yml)$/)) return [];\n const matches: RuleMatch[] = [];\n // Terraform: ingress with 0.0.0.0/0 and port 0 or no port restriction\n if (filePath.match(/\\.tf$/)) {\n const ingressPattern = /ingress\\s*\\{[^}]*cidr_blocks\\s*=\\s*\\[\\s*\"0\\.0\\.0\\.0\\/0\"\\s*\\][^}]*\\}/gs;\n let m: RegExpExecArray | null;\n while ((m = ingressPattern.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n // Check for unrestricted ports (from_port = 0 or no port spec)\n if (/from_port\\s*=\\s*0/.test(m[0]) || !/from_port/.test(m[0])) {\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n matches.push({\n rule: \"VC108\", title: securityGroupAllInbound.title, severity: \"critical\" as const, category: \"Infrastructure\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Restrict security group ingress to specific IP ranges and ports.\",\n });\n }\n }\n }\n // CloudFormation: AWS::EC2::SecurityGroup with CidrIp: 0.0.0.0/0\n if (filePath.match(/\\.(json|yaml|yml)$/)) {\n const cfnPattern = /AWS::EC2::SecurityGroup/g;\n if (cfnPattern.test(content) && /CidrIp\\s*:\\s*[\"']?0\\.0\\.0\\.0\\/0/.test(content)) {\n matches.push(...findMatches(content, /CidrIp\\s*:\\s*[\"']?0\\.0\\.0\\.0\\/0/g, securityGroupAllInbound, filePath, () =>\n \"Restrict security group ingress to specific IP ranges and ports.\"\n ));\n }\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC109 – RDS Instance Publicly Accessible\n// ────────────────────────────────────────────\n\nexport const rdsPubliclyAccessible: CustomRule = {\n id: \"VC109\",\n title: \"RDS Instance Publicly Accessible\",\n severity: \"critical\",\n category: \"Infrastructure\",\n description: \"RDS instances with publicly_accessible = true are exposed to the internet, risking unauthorized database access.\",\n check(content, filePath) {\n if (!filePath.match(/\\.tf$/)) return [];\n if (!/resource\\s+\"aws_db_instance\"/.test(content)) return [];\n return findMatches(content, /publicly_accessible\\s*=\\s*true/g, rdsPubliclyAccessible, filePath, () =>\n \"Set publicly_accessible = false. Access RDS through VPC private subnets.\"\n );\n },\n};\n\n// ────────────────────────────────────────────\n// VC110 – Missing CloudTrail Logging\n// ────────────────────────────────────────────\n\nexport const missingCloudTrail: CustomRule = {\n id: \"VC110\",\n title: \"Missing CloudTrail Logging\",\n severity: \"high\",\n category: \"Infrastructure\",\n description: \"AWS environments without CloudTrail lack audit logging of API calls, making it difficult to detect unauthorized activity.\",\n check(content, filePath) {\n if (!filePath.match(/\\.tf$/)) return [];\n // Only flag if the file uses AWS provider but has no cloudtrail resource\n if (!/provider\\s+\"aws\"/.test(content)) return [];\n if (/aws_cloudtrail/.test(content)) return [];\n const lineNum = content.substring(0, content.search(/provider\\s+\"aws\"/)).split(\"\\n\").length;\n return [{\n rule: \"VC110\", title: missingCloudTrail.title, severity: \"high\" as const, category: \"Infrastructure\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Enable CloudTrail for audit logging of all AWS API calls.\",\n }];\n },\n};\n\n// ────────────────────────────────────────────\n// VC111 – Lambda Without VPC\n// ────────────────────────────────────────────\n\nexport const lambdaWithoutVPC: CustomRule = {\n id: \"VC111\",\n title: \"Lambda Without VPC\",\n severity: \"medium\",\n category: \"Infrastructure\",\n description: \"Lambda functions not placed in a VPC lack network isolation and cannot access VPC-only resources securely.\",\n check(content, filePath) {\n if (!filePath.match(/\\.tf$/)) return [];\n if (!/resource\\s+\"aws_lambda_function\"/.test(content)) return [];\n const matches: RuleMatch[] = [];\n const lambdaPattern = /resource\\s+\"aws_lambda_function\"\\s+\"(\\w+)\"/g;\n let m: RegExpExecArray | null;\n while ((m = lambdaPattern.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n const blockEnd = Math.min(m.index + 2000, content.length);\n const blockContent = content.substring(m.index, blockEnd);\n if (!/vpc_config\\s*\\{/.test(blockContent)) {\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n matches.push({\n rule: \"VC111\", title: lambdaWithoutVPC.title, severity: \"medium\" as const, category: \"Infrastructure\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Place Lambda functions in a VPC for network isolation.\",\n });\n }\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC112 – Docker Image Using Latest Tag\n// ────────────────────────────────────────────\n\nexport const dockerLatestTag: CustomRule = {\n id: \"VC112\",\n title: \"Docker Image Using Latest Tag\",\n severity: \"high\",\n category: \"Configuration\",\n description: \"Using :latest or no tag in Docker FROM directives leads to non-reproducible builds and potential security regressions.\",\n check(content, filePath) {\n if (!filePath.match(/Dockerfile$/i)) return [];\n const matches: RuleMatch[] = [];\n // Match FROM image:latest or FROM image (no tag, no AS, no scratch)\n const fromPattern = /^FROM\\s+(?!scratch)(\\S+?)(?:\\s+AS\\s+\\S+)?\\s*$/gm;\n let m: RegExpExecArray | null;\n while ((m = fromPattern.exec(content)) !== null) {\n const image = m[1];\n // Flag if using :latest or no tag at all (no colon)\n if (image.endsWith(\":latest\") || (!image.includes(\":\") && !image.startsWith(\"$\"))) {\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n matches.push({\n rule: \"VC112\", title: dockerLatestTag.title, severity: \"high\" as const, category: \"Configuration\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Pin Docker image versions: FROM node:20-alpine instead of FROM node:latest\",\n });\n }\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC113 – Docker COPY With Sensitive Files\n// ────────────────────────────────────────────\n\nexport const dockerCopySensitive: CustomRule = {\n id: \"VC113\",\n title: \"Docker COPY With Sensitive Files\",\n severity: \"high\",\n category: \"Configuration\",\n description: \"Using COPY . . or ADD . . without a .dockerignore can leak .env files, .git history, and other sensitive data into the Docker image.\",\n check(content, filePath) {\n if (!filePath.match(/Dockerfile$/i)) return [];\n // Check for COPY . . or ADD . .\n if (!/(?:COPY|ADD)\\s+\\.\\s+\\./.test(content)) return [];\n return findMatches(content, /(?:COPY|ADD)\\s+\\.\\s+\\./g, dockerCopySensitive, filePath, () =>\n \"Use .dockerignore to exclude .env, .git, node_modules, and sensitive files from the Docker build context.\"\n );\n },\n};\n\n// ────────────────────────────────────────────\n// VC114 – Docker Exposing Too Many Ports\n// ────────────────────────────────────────────\n\nexport const dockerTooManyPorts: CustomRule = {\n id: \"VC114\",\n title: \"Docker Exposing Too Many Ports\",\n severity: \"medium\",\n category: \"Configuration\",\n description: \"Exposing many ports or wide port ranges increases the attack surface of a container.\",\n check(content, filePath) {\n if (!filePath.match(/Dockerfile$/i)) return [];\n const matches: RuleMatch[] = [];\n // Detect port ranges like EXPOSE 3000-9000\n const rangePattern = /^EXPOSE\\s+\\d+-\\d+/gm;\n matches.push(...findMatches(content, rangePattern, dockerTooManyPorts, filePath, () =>\n \"Only expose necessary ports. Each EXPOSE should have a clear purpose.\"\n ));\n // Detect multiple EXPOSE directives (more than 3)\n const exposeLines = content.split(\"\\n\").filter(l => /^\\s*EXPOSE\\s+/.test(l));\n if (exposeLines.length > 3) {\n const firstExpose = content.search(/^\\s*EXPOSE\\s+/m);\n if (firstExpose >= 0) {\n const lineNum = content.substring(0, firstExpose).split(\"\\n\").length;\n matches.push({\n rule: \"VC114\", title: dockerTooManyPorts.title, severity: \"medium\" as const, category: \"Configuration\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Only expose necessary ports. Each EXPOSE should have a clear purpose.\",\n });\n }\n }\n return matches;\n },\n};\n\n// ────────────────────────────────────────────\n// VC115 – Kubernetes Secret Not Encrypted\n// ────────────────────────────────────────────\n\nexport const k8sSecretNotEncrypted: CustomRule = {\n id: \"VC115\",\n title: \"Kubernetes Secret Not Encrypted\",\n severity: \"high\",\n category: \"Infrastructure\",\n description: \"Kubernetes Secrets stored as plain base64 in manifests are not encrypted and can be trivially decoded. Use sealed-secrets or external-secrets.\",\n check(content, filePath) {\n if (!filePath.match(/\\.(yaml|yml)$/)) return [];\n if (!/kind\\s*:\\s*Secret/i.test(content)) return [];\n // Skip if using sealed-secrets or external-secrets annotations\n if (/sealedsecrets\\.bitnami\\.com|external-secrets\\.io/i.test(content)) return [];\n return findMatches(content, /kind\\s*:\\s*Secret/g, k8sSecretNotEncrypted, filePath, () =>\n \"Use sealed-secrets or external-secrets-operator. Never store plain secrets in manifests.\"\n );\n },\n};\n\n// ────────────────────────────────────────────\n// VC116 – Kubernetes Pod Without Resource Limits\n// ────────────────────────────────────────────\n\nexport const k8sNoResourceLimits: CustomRule = {\n id: \"VC116\",\n title: \"Kubernetes Pod Without Resource Limits\",\n severity: \"medium\",\n category: \"Infrastructure\",\n description: \"Pods or deployments without resource limits can consume excessive CPU/memory, causing cluster instability.\",\n check(content, filePath) {\n if (!filePath.match(/\\.(yaml|yml)$/)) return [];\n if (!/kind\\s*:\\s*(?:Pod|Deployment|StatefulSet|DaemonSet|Job|CronJob)/i.test(content)) return [];\n // Check if any container spec has resources.limits\n if (/resources\\s*:\\s*\\n\\s+limits\\s*:/m.test(content)) return [];\n // Also accept inline resources: { limits: ... }\n if (/resources\\s*:.*limits/i.test(content)) return [];\n const kindMatch = content.match(/kind\\s*:\\s*(?:Pod|Deployment|StatefulSet|DaemonSet|Job|CronJob)/i);\n if (!kindMatch) return [];\n const lineNum = content.substring(0, kindMatch.index).split(\"\\n\").length;\n return [{\n rule: \"VC116\", title: k8sNoResourceLimits.title, severity: \"medium\" as const, category: \"Infrastructure\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Set resource limits to prevent pods from consuming excessive CPU/memory.\",\n }];\n },\n};\n\n// ────────────────────────────────────────────\n// VC117 – Path Traversal Vulnerability\n// ────────────────────────────────────────────\n\nexport const pathTraversal: CustomRule = {\n id: \"VC117\",\n title: \"Path Traversal Vulnerability\",\n severity: \"critical\",\n category: \"Injection\",\n description: \"User input is used to construct file paths without sanitization, allowing attackers to read/write arbitrary files (e.g., ../../etc/passwd).\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!filePath.match(/\\.(js|ts|jsx|tsx|py|rb|go|php|java)$/)) return [];\n const findings: RuleMatch[] = [];\n // Pattern: file operations using user input (req.params, req.query, req.body) in path\n const patterns = [\n /(?:readFile|readFileSync|createReadStream|writeFile|writeFileSync|appendFile|unlink|unlinkSync|access|stat|statSync|existsSync)\\s*\\(\\s*(?:req\\.|request\\.|ctx\\.|params\\.|query\\.)/gi,\n /(?:path\\.join|path\\.resolve)\\s*\\([^)]*(?:req\\.|request\\.|ctx\\.|params\\.|query\\.)[^)]*\\)/gi,\n /(?:res\\.sendFile|res\\.download)\\s*\\([^)]*(?:req\\.|request\\.)/gi,\n /open\\s*\\(\\s*(?:request\\.|params\\[|args\\.)/gi,\n ];\n for (const pat of patterns) {\n let m;\n while ((m = pat.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n // Skip if path.normalize or sanitization is near\n const surrounding = content.substring(Math.max(0, m.index - 200), m.index + 200);\n if (/path\\.normalize|sanitize|\\.replace\\(\\s*['\"]\\.\\./i.test(surrounding)) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC117\", title: pathTraversal.title, severity: \"critical\" as const, category: \"Injection\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Sanitize file paths: use path.normalize(), reject paths containing '..', and validate against an allowed base directory.\",\n });\n }\n }\n return findings;\n },\n};\n\n// ────────────────────────────────────────────\n// VC118 – PII in Log Output\n// ────────────────────────────────────────────\n\nexport const piiInLogs: CustomRule = {\n id: \"VC118\",\n title: \"Personally Identifiable Information in Logs\",\n severity: \"high\",\n category: \"Information Leakage\",\n description: \"Logging statements that include email addresses, passwords, SSNs, credit card numbers, or other PII can leak sensitive data to log aggregation systems.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!filePath.match(/\\.(js|ts|jsx|tsx|py|rb|go|java|php)$/)) return [];\n const findings: RuleMatch[] = [];\n // Match console.log/logger.info etc. that reference sensitive fields\n const logPattern = /(?:console\\.(?:log|info|warn|error|debug)|logger\\.(?:info|warn|error|debug|log)|log\\.(?:info|warn|error|debug)|logging\\.(?:info|warn|error|debug))\\s*\\([^)]*(?:password|passwd|secret|ssn|social.?security|credit.?card|cardNumber|cvv|token|bearer|authorization)\\b/gi;\n let m;\n while ((m = logPattern.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n if (isInsideFixMessage(content, m.index)) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC118\", title: piiInLogs.title, severity: \"high\" as const, category: \"Information Leakage\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Never log sensitive data. Redact or mask PII before logging: log('user login', { email: maskEmail(email) }).\",\n });\n }\n return findings;\n },\n};\n\n// ────────────────────────────────────────────\n// VC119 – Hardcoded OAuth Client Secret\n// ────────────────────────────────────────────\n\nexport const hardcodedOAuthSecret: CustomRule = {\n id: \"VC119\",\n title: \"Hardcoded OAuth Client Secret\",\n severity: \"critical\",\n category: \"Secrets\",\n description: \"OAuth client secrets hardcoded in source code can be extracted and used to impersonate your application.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!filePath.match(/\\.(js|ts|jsx|tsx|py|rb|go|java|php|env|json|yaml|yml)$/)) return [];\n // Skip lockfiles and generated files\n if (/package-lock|yarn\\.lock|pnpm-lock|composer\\.lock/i.test(filePath)) return [];\n const findings: RuleMatch[] = [];\n const patterns = [\n /client[_-]?secret\\s*[:=]\\s*[\"'][a-zA-Z0-9_\\-]{20,}[\"']/gi,\n /GOOGLE_CLIENT_SECRET\\s*[:=]\\s*[\"'][^\"']{10,}[\"']/gi,\n /GITHUB_CLIENT_SECRET\\s*[:=]\\s*[\"'][^\"']{10,}[\"']/gi,\n /FACEBOOK_APP_SECRET\\s*[:=]\\s*[\"'][^\"']{10,}[\"']/gi,\n /AUTH0_CLIENT_SECRET\\s*[:=]\\s*[\"'][^\"']{10,}[\"']/gi,\n ];\n for (const pat of patterns) {\n let m;\n while ((m = pat.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n // Skip if value is a placeholder or env ref\n if (/process\\.env|os\\.environ|ENV\\[|getenv|\\$\\{|<your|CHANGE_ME|xxx|placeholder/i.test(m[0])) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC119\", title: hardcodedOAuthSecret.title, severity: \"critical\" as const, category: \"Secrets\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Move OAuth client secrets to environment variables. Never commit secrets to source control.\",\n });\n }\n }\n return findings;\n },\n};\n\n// ────────────────────────────────────────────\n// VC120 – Missing OAuth State Parameter\n// ────────────────────────────────────────────\n\nexport const missingOAuthState: CustomRule = {\n id: \"VC120\",\n title: \"OAuth Flow Missing State Parameter\",\n severity: \"high\",\n category: \"Authentication\",\n description: \"OAuth authorization requests without a state parameter are vulnerable to CSRF attacks, allowing attackers to link their account to a victim's session.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!filePath.match(/\\.(js|ts|jsx|tsx|py|rb|go|java|php)$/)) return [];\n const findings: RuleMatch[] = [];\n // OAuth authorization URL construction without state param\n const oauthUrlPattern = /(?:authorize\\?|\\/oauth\\/authorize|\\/auth\\?|authorization_endpoint)[^}]*(?:client_id|response_type)/gi;\n let m;\n while ((m = oauthUrlPattern.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n const surrounding = content.substring(m.index, Math.min(content.length, m.index + 500));\n if (/state\\s*[=:]/i.test(surrounding)) continue; // Has state param\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC120\", title: missingOAuthState.title, severity: \"high\" as const, category: \"Authentication\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Always include a cryptographically random 'state' parameter in OAuth authorization requests and validate it on callback.\",\n });\n }\n return findings;\n },\n};\n\n// ────────────────────────────────────────────\n// VC121 – Unpinned GitHub Actions Version\n// ────────────────────────────────────────────\n\nexport const unpinnedGitHubAction: CustomRule = {\n id: \"VC121\",\n title: \"Unpinned GitHub Actions Version\",\n severity: \"high\",\n category: \"Supply Chain\",\n description: \"GitHub Actions using branch references (@main, @master) instead of commit SHAs can be compromised via supply-chain attacks.\",\n check(content, filePath) {\n if (!filePath.match(/\\.github\\/workflows\\/.*\\.(yml|yaml)$/)) return [];\n const findings: RuleMatch[] = [];\n // Match uses: org/action@branch (not @sha or @v1.2.3)\n const usesPattern = /uses\\s*:\\s*[\\w\\-]+\\/[\\w\\-]+@(main|master|dev|develop|latest)\\b/gi;\n let m;\n while ((m = usesPattern.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC121\", title: unpinnedGitHubAction.title, severity: \"high\" as const, category: \"Supply Chain\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Pin GitHub Actions to a specific commit SHA instead of a branch name: uses: owner/action@<full-sha>\",\n });\n }\n return findings;\n },\n};\n\n// ────────────────────────────────────────────\n// VC122 – Deprecated TLS Version\n// ────────────────────────────────────────────\n\nexport const deprecatedTLS: CustomRule = {\n id: \"VC122\",\n title: \"Deprecated TLS Version Configured\",\n severity: \"high\",\n category: \"Cryptography\",\n description: \"TLS 1.0 and 1.1 are deprecated and have known vulnerabilities. Only TLS 1.2+ should be used.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!filePath.match(/\\.(js|ts|py|rb|go|java|yaml|yml|json|conf|cfg|xml)$/)) return [];\n const findings: RuleMatch[] = [];\n const patterns = [\n /(?:TLSv1_METHOD|TLSv1\\.0|TLSv1\\.1|ssl\\.PROTOCOL_TLSv1|SSLv3|SSLv2|TLS_1_0|TLS_1_1|minVersion\\s*[:=]\\s*[\"']TLSv1(?:\\.1)?[\"'])/gi,\n /(?:tls\\.DEFAULT_MIN_VERSION|secureProtocol)\\s*[:=]\\s*[\"'](?:TLSv1_method|TLSv1_1_method|SSLv3_method)[\"']/gi,\n /ssl_protocols\\s+.*(?:TLSv1(?:\\.1)?(?:\\s|;))/gi,\n ];\n for (const pat of patterns) {\n let m;\n while ((m = pat.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC122\", title: deprecatedTLS.title, severity: \"high\" as const, category: \"Cryptography\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Use TLS 1.2 or higher. Set minVersion to 'TLSv1.2' and remove support for TLS 1.0/1.1.\",\n });\n }\n }\n return findings;\n },\n};\n\n// ────────────────────────────────────────────\n// VC123 – Weak RSA Key Size\n// ────────────────────────────────────────────\n\nexport const weakRSAKeySize: CustomRule = {\n id: \"VC123\",\n title: \"Weak RSA Key Size\",\n severity: \"high\",\n category: \"Cryptography\",\n description: \"RSA keys smaller than 2048 bits are considered insecure and can be factored with modern hardware.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!filePath.match(/\\.(js|ts|py|rb|go|java|php)$/)) return [];\n const findings: RuleMatch[] = [];\n // Match RSA key generation with explicit small sizes\n const patterns = [\n /generateKeyPair\\s*\\(\\s*[\"']rsa[\"']\\s*,\\s*\\{[^}]*modulusLength\\s*:\\s*(512|768|1024)\\b/gi,\n /RSA\\.generate\\s*\\(\\s*(512|768|1024)\\b/gi,\n /rsa\\.GenerateKey\\s*\\([^,]*,\\s*(512|768|1024)\\b/gi,\n /KeyPairGenerator.*initialize\\s*\\(\\s*(512|768|1024)\\b/gi,\n ];\n for (const pat of patterns) {\n let m;\n while ((m = pat.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC123\", title: weakRSAKeySize.title, severity: \"high\" as const, category: \"Cryptography\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Use RSA key sizes of at least 2048 bits. 4096 bits is recommended for long-term security.\",\n });\n }\n }\n return findings;\n },\n};\n\n// ────────────────────────────────────────────\n// VC124 – ECB Mode Encryption\n// ────────────────────────────────────────────\n\nexport const ecbModeEncryption: CustomRule = {\n id: \"VC124\",\n title: \"Insecure ECB Mode Encryption\",\n severity: \"high\",\n category: \"Cryptography\",\n description: \"ECB (Electronic Codebook) mode encrypts identical plaintext blocks to identical ciphertext, leaking patterns in the data.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!filePath.match(/\\.(js|ts|py|rb|go|java|php)$/)) return [];\n const findings: RuleMatch[] = [];\n const patterns = [\n /createCipher(?:iv)?\\s*\\(\\s*[\"'](?:aes-(?:128|192|256)-ecb|des-ecb)[\"']/gi,\n /AES\\.(?:new|MODE_ECB)|MODE_ECB/gi,\n /Cipher\\.getInstance\\s*\\(\\s*[\"']AES\\/ECB/gi,\n /aes\\.NewCipher|cipher\\.NewECBEncrypter/gi,\n ];\n for (const pat of patterns) {\n let m;\n while ((m = pat.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC124\", title: ecbModeEncryption.title, severity: \"high\" as const, category: \"Cryptography\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Use AES-GCM or AES-CBC with HMAC instead of ECB mode. GCM provides both encryption and authentication.\",\n });\n }\n }\n return findings;\n },\n};\n\n// ────────────────────────────────────────────\n// VC125 – Insecure Password Reset Flow\n// ────────────────────────────────────────────\n\nexport const insecurePasswordReset: CustomRule = {\n id: \"VC125\",\n title: \"Insecure Password Reset Implementation\",\n severity: \"high\",\n category: \"Authentication\",\n description: \"Password reset using predictable tokens, no expiration, or user-enumeration leaks can be exploited to take over accounts.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!filePath.match(/\\.(js|ts|jsx|tsx|py|rb|go|java|php)$/)) return [];\n if (!/(?:reset|forgot).*(?:password|passwd)/i.test(content)) return [];\n const findings: RuleMatch[] = [];\n // Predictable reset tokens (using Date.now, Math.random, uuid without crypto)\n const weakTokens = /(?:reset|forgot).*(?:token|code)\\s*[:=].*(?:Date\\.now|Math\\.random|uuid\\(\\)|nanoid\\(\\))/gi;\n let m;\n while ((m = weakTokens.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC125\", title: insecurePasswordReset.title, severity: \"high\" as const, category: \"Authentication\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Use crypto.randomBytes(32).toString('hex') for reset tokens. Set expiration (15-60 minutes) and single-use enforcement.\",\n });\n }\n // User enumeration: different responses for valid vs invalid emails\n const enumeration = /(?:user|email|account)\\s*(?:not\\s*found|does\\s*not\\s*exist|invalid)/gi;\n while ((m = enumeration.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n // Only flag if in a password reset context\n const surrounding = content.substring(Math.max(0, m.index - 500), m.index);\n if (!/(?:reset|forgot).*password/i.test(surrounding)) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC125\", title: insecurePasswordReset.title, severity: \"high\" as const, category: \"Authentication\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Always return the same response regardless of whether the email exists. Say 'If an account exists, a reset link was sent.'\",\n });\n }\n return findings;\n },\n};\n\n// ────────────────────────────────────────────\n// VC126 – Terraform State File Exposed\n// ────────────────────────────────────────────\n\nexport const terraformStateExposed: CustomRule = {\n id: \"VC126\",\n title: \"Terraform State File Committed\",\n severity: \"critical\",\n category: \"Secrets\",\n description: \"Terraform state files contain sensitive infrastructure details, secrets, and access credentials in plaintext. They must never be committed to version control.\",\n check(content, filePath) {\n const findings: RuleMatch[] = [];\n // Direct detection of terraform state files\n if (/terraform\\.tfstate(\\.backup)?$/.test(filePath)) {\n findings.push({\n rule: \"VC126\", title: terraformStateExposed.title, severity: \"critical\" as const, category: \"Secrets\",\n file: filePath, line: 1, snippet: getSnippet(content, 1),\n fix: \"Add '*.tfstate' and '*.tfstate.backup' to .gitignore. Use remote state backends (S3, GCS, Terraform Cloud) instead.\",\n });\n }\n // Check .gitignore for missing tfstate\n if (filePath.endsWith(\".gitignore\") && !content.includes(\"tfstate\")) {\n // Only flag if the repo has terraform files\n if (/\\.tf$/.test(filePath) || content.includes(\".tf\")) {\n // This is a weak signal, skip\n }\n }\n return findings;\n },\n};\n\n// ────────────────────────────────────────────\n// VC127 – Insecure HTTP Method Handling\n// ────────────────────────────────────────────\n\nexport const insecureHTTPMethods: CustomRule = {\n id: \"VC127\",\n title: \"Dangerous HTTP Methods Without Auth\",\n severity: \"high\",\n category: \"Authorization\",\n description: \"DELETE, PUT, and PATCH endpoints without authentication checks allow unauthorized data modification or deletion.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!filePath.match(/\\.(js|ts|jsx|tsx|py|rb|go|java|php)$/)) return [];\n const findings: RuleMatch[] = [];\n // Express/Hono/Fastify route handlers for dangerous methods\n const routePatterns = [\n /(?:app|router|server)\\.(delete|put|patch)\\s*\\(\\s*[\"']/gi,\n ];\n for (const pat of routePatterns) {\n let m;\n while ((m = pat.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n // Check the handler for auth middleware or auth checks\n const handlerBlock = content.substring(m.index, Math.min(content.length, m.index + 500));\n if (/auth|authenticate|authorize|requireAuth|isAuthenticated|protect|guard|middleware|session|jwt|verify|clerk|getAuth/i.test(handlerBlock)) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC127\", title: insecureHTTPMethods.title, severity: \"high\" as const, category: \"Authorization\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Add authentication middleware to DELETE, PUT, and PATCH routes. Verify the user has permission to modify the resource.\",\n });\n }\n }\n return findings;\n },\n};\n\n// ────────────────────────────────────────────\n// VC128 – HTTP Request Smuggling Risk\n// ────────────────────────────────────────────\n\nexport const httpRequestSmuggling: CustomRule = {\n id: \"VC128\",\n title: \"HTTP Request Smuggling Risk\",\n severity: \"high\",\n category: \"Configuration\",\n description: \"Manually parsing Content-Length or Transfer-Encoding headers can lead to request smuggling when behind a reverse proxy.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!filePath.match(/\\.(js|ts|py|rb|go|java|php)$/)) return [];\n const findings: RuleMatch[] = [];\n const patterns = [\n /(?:headers?\\[?|getHeader\\s*\\(\\s*)[\"'](?:content-length|transfer-encoding)[\"']\\s*\\]?\\s*\\)/gi,\n /parseInt\\s*\\(\\s*(?:req|request)\\.headers?\\[?\\s*[\"']content-length[\"']/gi,\n ];\n for (const pat of patterns) {\n let m;\n while ((m = pat.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC128\", title: httpRequestSmuggling.title, severity: \"high\" as const, category: \"Configuration\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Let your framework handle Content-Length and Transfer-Encoding headers. Do not manually parse or trust these values.\",\n });\n }\n }\n return findings;\n },\n};\n\n// ────────────────────────────────────────────\n// VC129 – Unencrypted PII in Database Schema\n// ────────────────────────────────────────────\n\nexport const unencryptedPII: CustomRule = {\n id: \"VC129\",\n title: \"Sensitive Data Stored Without Encryption\",\n severity: \"high\",\n category: \"Information Leakage\",\n description: \"Database schemas storing PII (SSN, credit cards, health records) in plaintext violate compliance requirements and expose data in breaches.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!filePath.match(/\\.(sql|prisma|py|ts|js|rb)$/)) return [];\n const findings: RuleMatch[] = [];\n // Schema definitions with sensitive field names stored as plain text/varchar\n const schemaPatterns = [\n /(?:column|field|Column|Field)\\s*\\(\\s*[\"'](?:ssn|social_security|tax_id|credit_card|card_number|cvv|passport|driver_license|bank_account|routing_number)[\"']\\s*,\\s*(?:String|TEXT|VARCHAR|Text|text)/gi,\n /(?:ssn|social_security|tax_id|credit_card|card_number|cvv|passport_number|driver_license|bank_account|routing_number)\\s+(?:TEXT|VARCHAR|String|text|character varying)/gi,\n ];\n for (const pat of schemaPatterns) {\n let m;\n while ((m = pat.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC129\", title: unencryptedPII.title, severity: \"high\" as const, category: \"Information Leakage\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Encrypt sensitive fields at the application level before storing. Use field-level encryption with a KMS for SSN, credit cards, and health data.\",\n });\n }\n }\n return findings;\n },\n};\n\n// ────────────────────────────────────────────\n// VC130 – Missing Rate Limit on Auth Endpoints\n// ────────────────────────────────────────────\n\nexport const missingAuthRateLimit: CustomRule = {\n id: \"VC130\",\n title: \"Authentication Endpoint Without Rate Limiting\",\n severity: \"high\",\n category: \"Authentication\",\n description: \"Login, registration, and password reset endpoints without rate limiting are vulnerable to credential stuffing and brute-force attacks.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!filePath.match(/\\.(js|ts|jsx|tsx|py|rb|go|java|php)$/)) return [];\n const findings: RuleMatch[] = [];\n // Routes that handle auth operations\n const authRoutePattern = /(?:app|router|server)\\.(?:post|put)\\s*\\(\\s*[\"'](?:\\/(?:api\\/)?(?:auth\\/)?(?:login|signin|sign-in|register|signup|sign-up|forgot-password|reset-password))[\"']/gi;\n let m;\n while ((m = authRoutePattern.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n // Check surrounding code for rate limiting\n const surrounding = content.substring(Math.max(0, m.index - 300), Math.min(content.length, m.index + 300));\n if (/rateLimit|rateLimiter|throttle|slowDown|express-rate-limit|rate_limit|RateLimiter|limiter/i.test(surrounding)) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC130\", title: missingAuthRateLimit.title, severity: \"high\" as const, category: \"Authentication\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Add rate limiting to authentication endpoints. Limit to 5-10 attempts per minute per IP. Use express-rate-limit or similar.\",\n });\n }\n return findings;\n },\n};\n\n// ────────────────────────────────────────────\n// VC131 – Known Vulnerable Dependency Pattern\n// ────────────────────────────────────────────\n\nexport const vulnerableDependencies: CustomRule = {\n id: \"VC131\",\n title: \"Potentially Vulnerable Dependency\",\n severity: \"high\",\n category: \"Supply Chain\",\n description: \"Dependencies with known security issues that are commonly found in AI-generated codebases.\",\n check(content, filePath) {\n if (!filePath.endsWith(\"package.json\")) return [];\n // Skip nested package.json (node_modules, etc)\n if (/node_modules|\\.next|dist|build/.test(filePath)) return [];\n const findings: RuleMatch[] = [];\n // Known problematic packages and version ranges\n const vulnerablePackages: [RegExp, string][] = [\n [/\"jsonwebtoken\"\\s*:\\s*\"[\\^~]?[0-7]\\./g, \"jsonwebtoken < 8.x has signature bypass vulnerabilities\"],\n [/\"lodash\"\\s*:\\s*\"[\\^~]?[0-3]\\./g, \"lodash < 4.x has prototype pollution vulnerabilities\"],\n [/\"minimist\"\\s*:\\s*\"[\\^~]?[01]\\.[01]\\./g, \"minimist < 1.2.6 has prototype pollution\"],\n [/\"node-fetch\"\\s*:\\s*\"[\\^~]?[12]\\./g, \"node-fetch < 3.x has data exposure issues\"],\n [/\"express\"\\s*:\\s*\"[\\^~]?[0-3]\\./g, \"express < 4.x has multiple known vulnerabilities\"],\n [/\"axios\"\\s*:\\s*\"[\\^~]?0\\.[0-9]\\./g, \"axios < 0.21 has SSRF vulnerabilities\"],\n [/\"tar\"\\s*:\\s*\"[\\^~]?[0-5]\\./g, \"tar < 6.x has path traversal vulnerabilities\"],\n [/\"glob-parent\"\\s*:\\s*\"[\\^~]?[0-4]\\./g, \"glob-parent < 5.1.2 has ReDoS vulnerability\"],\n ];\n for (const [pat, message] of vulnerablePackages) {\n let m;\n while ((m = pat.exec(content)) !== null) {\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC131\", title: vulnerableDependencies.title, severity: \"high\" as const, category: \"Supply Chain\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: `${message}. Update to the latest version and run 'npm audit' regularly.`,\n });\n }\n }\n return findings;\n },\n};\n\n// ────────────────────────────────────────────\n// VC132–VC145: SERVICE-SPECIFIC API KEY DETECTION\n// Each rule targets a service with a known key prefix/format.\n// Descriptions include permission context (what the key can do).\n// ────────────────────────────────────────────\n\nconst SECRET_FILE_EXT = /\\.(js|ts|jsx|tsx|py|rb|go|java|php|env|json|yaml|yml|toml|cfg|conf|ini|sh|bash|zsh)$/;\nconst PLACEHOLDER_RE = /process\\.env|os\\.environ|ENV\\[|getenv|System\\.getenv|env\\(|CHANGE_ME|YOUR_|xxx|placeholder|example|TODO|FIXME|<your|dummy|fake_?key|test_?key/i;\nconst LOCK_FILE_RE = /package-lock|yarn\\.lock|pnpm-lock|Gemfile\\.lock|Pipfile\\.lock|composer\\.lock/i;\n\nfunction secretRuleCheck(\n content: string,\n filePath: string,\n pattern: RegExp,\n ruleId: string,\n title: string,\n severity: \"critical\" | \"high\",\n fix: string,\n): RuleMatch[] {\n if (isTestFile(filePath)) return [];\n if (!SECRET_FILE_EXT.test(filePath)) return [];\n if (LOCK_FILE_RE.test(filePath)) return [];\n const findings: RuleMatch[] = [];\n const re = new RegExp(pattern.source, pattern.flags.includes(\"g\") ? pattern.flags : `${pattern.flags}g`);\n let m: RegExpExecArray | null;\n while ((m = re.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n if (isInsideFixMessage(content, m.index)) continue;\n // Skip if this looks like an env var reference, not a literal\n const lineStart = content.lastIndexOf(\"\\n\", m.index - 1) + 1;\n const lineText = content.substring(lineStart, content.indexOf(\"\\n\", m.index));\n if (PLACEHOLDER_RE.test(lineText)) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: ruleId, title, severity, category: \"Secrets\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum), fix,\n });\n }\n return findings;\n}\n\nexport const hardcodedAnthropicKey: CustomRule = {\n id: \"VC132\",\n title: \"Hardcoded Anthropic API Key\",\n severity: \"critical\",\n category: \"Secrets\",\n description: \"Anthropic API keys (sk-ant-*) grant full API access: running models, incurring charges, and accessing usage history. A leaked key can be used to make unlimited API calls at your expense.\",\n check(content, filePath) {\n return secretRuleCheck(content, filePath, /sk-ant-api03-[A-Za-z0-9_\\-]{80,}/g, \"VC132\", this.title, \"critical\",\n \"Move the Anthropic API key to an environment variable (ANTHROPIC_API_KEY). Rotate the exposed key immediately at console.anthropic.com → API Keys.\");\n },\n};\n\nexport const hardcodedGitHubPAT: CustomRule = {\n id: \"VC133\",\n title: \"Hardcoded GitHub Personal Access Token\",\n severity: \"critical\",\n category: \"Secrets\",\n description: \"GitHub tokens (ghp_, gho_, ghu_, ghs_, ghr_) grant repository access: reading/writing code, managing issues, triggering Actions, and accessing private repos depending on scopes.\",\n check(content, filePath) {\n return secretRuleCheck(content, filePath, /(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36,}/g, \"VC133\", this.title, \"critical\",\n \"Move the GitHub token to an environment variable (GITHUB_TOKEN). Rotate immediately at github.com → Settings → Developer settings → Personal access tokens. Consider using fine-grained tokens with minimal permissions.\");\n },\n};\n\nexport const hardcodedSendGridKey: CustomRule = {\n id: \"VC134\",\n title: \"Hardcoded SendGrid API Key\",\n severity: \"high\",\n category: \"Secrets\",\n description: \"SendGrid API keys (SG.*) can send emails as your domain, access contact lists, view email activity, and modify sender reputation. A leaked key enables phishing from your verified domain.\",\n check(content, filePath) {\n return secretRuleCheck(content, filePath, /SG\\.[A-Za-z0-9_\\-]{22}\\.[A-Za-z0-9_\\-]{43}/g, \"VC134\", this.title, \"high\",\n \"Move the SendGrid API key to an environment variable (SENDGRID_API_KEY). Rotate at app.sendgrid.com → Settings → API Keys.\");\n },\n};\n\nexport const hardcodedSlackToken: CustomRule = {\n id: \"VC135\",\n title: \"Hardcoded Slack Token\",\n severity: \"high\",\n category: \"Secrets\",\n description: \"Slack tokens (xoxb-, xoxp-, xapp-) can read/send messages, access channels, list users, and manage workspace settings depending on scopes. Bot tokens (xoxb) have app-level access; user tokens (xoxp) act as the installing user.\",\n check(content, filePath) {\n return secretRuleCheck(content, filePath, /xox[bpas]-[0-9A-Za-z\\-]{10,}/g, \"VC135\", this.title, \"high\",\n \"Move the Slack token to an environment variable (SLACK_BOT_TOKEN). Rotate at api.slack.com → Your Apps → OAuth & Permissions → Reinstall.\");\n },\n};\n\nexport const hardcodedGCPServiceAccount: CustomRule = {\n id: \"VC136\",\n title: \"Hardcoded GCP Service Account Key\",\n severity: \"critical\",\n category: \"Secrets\",\n description: \"GCP service account JSON keys grant project-level access: compute, storage, databases, IAM, and billing depending on assigned roles. A leaked key can compromise your entire Google Cloud project.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!filePath.match(/\\.(json|js|ts|py|yaml|yml)$/)) return [];\n if (LOCK_FILE_RE.test(filePath)) return [];\n // Look for the combination of service_account type + private key in the same file\n if (!/\"type\"\\s*:\\s*\"service_account\"/.test(content)) return [];\n if (!/-----BEGIN (?:RSA )?PRIVATE KEY-----/.test(content)) return [];\n const findings: RuleMatch[] = [];\n const m = content.match(/\"type\"\\s*:\\s*\"service_account\"/);\n if (m && m.index !== undefined) {\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC136\", title: this.title, severity: \"critical\", category: \"Secrets\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"GCP service account keys should never be committed. Use Workload Identity Federation or store the key in a secrets manager. Delete this key in Google Cloud Console → IAM → Service Accounts and generate a new one.\",\n });\n }\n return findings;\n },\n};\n\nexport const hardcodedShopifyToken: CustomRule = {\n id: \"VC137\",\n title: \"Hardcoded Shopify Access Token\",\n severity: \"critical\",\n category: \"Secrets\",\n description: \"Shopify access tokens (shpat_, shpca_, shppa_) grant full store access: orders, customers, products, and payment information. A leaked token can expose customer PII and modify store data.\",\n check(content, filePath) {\n return secretRuleCheck(content, filePath, /shp(?:at|ca|pa)_[a-fA-F0-9]{32,}/g, \"VC137\", this.title, \"critical\",\n \"Move the Shopify token to an environment variable. Rotate in Shopify Admin → Apps → Develop apps → API credentials.\");\n },\n};\n\nexport const hardcodedGitLabToken: CustomRule = {\n id: \"VC138\",\n title: \"Hardcoded GitLab Token\",\n severity: \"critical\",\n category: \"Secrets\",\n description: \"GitLab personal/project tokens (glpat-) grant repository and CI/CD access: reading/writing code, triggering pipelines, and managing project settings.\",\n check(content, filePath) {\n return secretRuleCheck(content, filePath, /glpat-[A-Za-z0-9_\\-]{20,}/g, \"VC138\", this.title, \"critical\",\n \"Move the GitLab token to an environment variable. Revoke and regenerate at gitlab.com → Preferences → Access Tokens.\");\n },\n};\n\nexport const hardcodedTwilioKey: CustomRule = {\n id: \"VC139\",\n title: \"Hardcoded Twilio API Key\",\n severity: \"high\",\n category: \"Secrets\",\n description: \"Twilio API key SIDs (SK*) can send SMS/calls, access call recordings and logs, and modify account configuration. A leaked key enables toll fraud and unauthorized communications.\",\n check(content, filePath) {\n return secretRuleCheck(content, filePath, /SK[0-9a-fA-F]{32}/g, \"VC139\", this.title, \"high\",\n \"Move the Twilio API key to an environment variable (TWILIO_API_KEY). Rotate at twilio.com → Console → API Keys.\");\n },\n};\n\nexport const hardcodedMailgunKey: CustomRule = {\n id: \"VC140\",\n title: \"Hardcoded Mailgun API Key\",\n severity: \"high\",\n category: \"Secrets\",\n description: \"Mailgun API keys (key-*) can send emails as your domain, access email logs, and manage routes. A leaked key enables phishing from your verified sending domain.\",\n check(content, filePath) {\n return secretRuleCheck(content, filePath, /key-[0-9a-zA-Z]{32}/g, \"VC140\", this.title, \"high\",\n \"Move the Mailgun key to an environment variable (MAILGUN_API_KEY). Rotate at app.mailgun.com → Settings → API Security.\");\n },\n};\n\nexport const hardcodedDatadogKey: CustomRule = {\n id: \"VC141\",\n title: \"Hardcoded Datadog API Key\",\n severity: \"high\",\n category: \"Secrets\",\n description: \"Datadog API/app keys grant access to all monitoring data and can modify dashboards, alerts, and integrations. A leaked key exposes your entire infrastructure topology.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!SECRET_FILE_EXT.test(filePath)) return [];\n if (LOCK_FILE_RE.test(filePath)) return [];\n // Look for DD_API_KEY or DD_APP_KEY assignments with hex values\n const pattern = /(?:DD_API_KEY|DD_APP_KEY|datadog[_-]?(?:api|app)[_-]?key)\\s*[:=]\\s*[\"'`]([a-f0-9]{32})[\"'`]/gi;\n const findings: RuleMatch[] = [];\n let m: RegExpExecArray | null;\n while ((m = pattern.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n if (isInsideFixMessage(content, m.index)) continue;\n const lineStart = content.lastIndexOf(\"\\n\", m.index - 1) + 1;\n const lineText = content.substring(lineStart, content.indexOf(\"\\n\", m.index));\n if (PLACEHOLDER_RE.test(lineText)) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC141\", title: this.title, severity: \"high\", category: \"Secrets\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Move the Datadog key to an environment variable (DD_API_KEY). Rotate at app.datadoghq.com → Organization Settings → API Keys.\",\n });\n }\n return findings;\n },\n};\n\nexport const hardcodedVercelToken: CustomRule = {\n id: \"VC142\",\n title: \"Hardcoded Vercel Token\",\n severity: \"high\",\n category: \"Secrets\",\n description: \"Vercel tokens can deploy projects, manage environment variables (which may contain other secrets), and access project settings. A leaked token can modify your production deployments.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!SECRET_FILE_EXT.test(filePath)) return [];\n if (LOCK_FILE_RE.test(filePath)) return [];\n const pattern = /(?:VERCEL_TOKEN|vercel[_-]?token|vercel[_-]?api[_-]?token)\\s*[:=]\\s*[\"'`]([A-Za-z0-9]{24,})[\"'`]/gi;\n const findings: RuleMatch[] = [];\n let m: RegExpExecArray | null;\n while ((m = pattern.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n if (isInsideFixMessage(content, m.index)) continue;\n const lineStart = content.lastIndexOf(\"\\n\", m.index - 1) + 1;\n const lineText = content.substring(lineStart, content.indexOf(\"\\n\", m.index));\n if (PLACEHOLDER_RE.test(lineText)) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC142\", title: this.title, severity: \"high\", category: \"Secrets\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Move the Vercel token to an environment variable (VERCEL_TOKEN). Rotate at vercel.com → Settings → Tokens.\",\n });\n }\n return findings;\n },\n};\n\nexport const hardcodedSupabaseServiceRole: CustomRule = {\n id: \"VC143\",\n title: \"Hardcoded Supabase Service Role Key\",\n severity: \"critical\",\n category: \"Secrets\",\n description: \"Supabase service role keys bypass Row Level Security and grant full database read/write access, auth admin, and storage admin. A leaked service role key exposes all user data.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!SECRET_FILE_EXT.test(filePath)) return [];\n if (LOCK_FILE_RE.test(filePath)) return [];\n // Look for service_role key assignments (JWT format near service_role context)\n const pattern = /(?:service[_-]?role[_-]?key|SUPABASE_SERVICE_ROLE_KEY|supabaseServiceRole)\\s*[:=]\\s*[\"'`](eyJ[A-Za-z0-9_\\-]{50,})[\"'`]/gi;\n const findings: RuleMatch[] = [];\n let m: RegExpExecArray | null;\n while ((m = pattern.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n if (isInsideFixMessage(content, m.index)) continue;\n const lineStart = content.lastIndexOf(\"\\n\", m.index - 1) + 1;\n const lineText = content.substring(lineStart, content.indexOf(\"\\n\", m.index));\n if (PLACEHOLDER_RE.test(lineText)) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC143\", title: this.title, severity: \"critical\", category: \"Secrets\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Move the Supabase service role key to a server-side environment variable. This key bypasses RLS — it must NEVER be exposed to the client. Rotate at supabase.com → Project Settings → API.\",\n });\n }\n return findings;\n },\n};\n\nexport const hardcodedVaultToken: CustomRule = {\n id: \"VC144\",\n title: \"Hardcoded HashiCorp Vault Token\",\n severity: \"critical\",\n category: \"Secrets\",\n description: \"Vault tokens (hvs.*, s.*) grant access to secrets stored in HashiCorp Vault, potentially exposing database credentials, API keys, and certificates across your entire infrastructure.\",\n check(content, filePath) {\n return secretRuleCheck(content, filePath, /(?:hvs\\.[A-Za-z0-9_\\-]{24,}|(?:VAULT_TOKEN|vault[_-]?token)\\s*[:=]\\s*[\"'`]s\\.[A-Za-z0-9]{24,}[\"'`])/g, \"VC144\", this.title, \"critical\",\n \"Move the Vault token to an environment variable (VAULT_TOKEN). Revoke the exposed token immediately with `vault token revoke`. Use short-lived tokens with minimal policies.\");\n },\n};\n\nexport const hardcodedPineconeKey: CustomRule = {\n id: \"VC145\",\n title: \"Hardcoded Pinecone API Key\",\n severity: \"high\",\n category: \"Secrets\",\n description: \"Pinecone API keys can read, write, and delete all vector data and manage indexes. A leaked key exposes your entire vector database and the embeddings stored in it.\",\n check(content, filePath) {\n return secretRuleCheck(content, filePath, /pcsk_[A-Za-z0-9_\\-]{50,}/g, \"VC145\", this.title, \"high\",\n \"Move the Pinecone API key to an environment variable (PINECONE_API_KEY). Rotate at app.pinecone.io → API Keys.\");\n },\n};\n\n// ────────────────────────────────────────────\n// VC146–VC151: SECRET EXPOSURE PATH ANALYSIS\n// Detects when secrets are used in ways that amplify exposure.\n// ────────────────────────────────────────────\n\nconst SECRET_VAR_RE = /(?:api[_-]?key|apikey|api[_-]?secret|secret[_-]?key|access[_-]?token|auth[_-]?token|private[_-]?key|password|passwd|pwd|credentials|client[_-]?secret|app[_-]?secret|master[_-]?key)/i;\n\nexport const secretInURLParam: CustomRule = {\n id: \"VC146\",\n title: \"Secret Passed in URL Query Parameter\",\n severity: \"critical\",\n category: \"Secrets\",\n description: \"API keys or tokens in URL query parameters are logged in server access logs, browser history, referrer headers, and proxy logs. Secrets in URLs are one of the most common causes of credential exposure.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!filePath.match(/\\.(js|ts|jsx|tsx|py|rb|go|java|php)$/)) return [];\n const findings: RuleMatch[] = [];\n const patterns = [\n // Template literals: `?api_key=${secret}` or `&token=${token}`\n /[?&](?:api_key|apikey|api_secret|token|access_token|secret|key|password|auth|authorization)=\\$\\{/gi,\n // String concat: '?key=' + apiKey\n /[?&](?:api_key|apikey|api_secret|token|access_token|secret|key|password|auth)=[\"']\\s*\\+/gi,\n // Python f-strings: f\"?token={token}\"\n /[?&](?:api_key|apikey|token|access_token|secret|key|password)=\\{[a-zA-Z_]/g,\n ];\n for (const p of patterns) {\n let m: RegExpExecArray | null;\n const re = new RegExp(p.source, p.flags.includes(\"g\") ? p.flags : `${p.flags}g`);\n while ((m = re.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n if (isInsideFixMessage(content, m.index)) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC146\", title: this.title, severity: \"critical\", category: \"Secrets\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Pass secrets in the Authorization header (Bearer token) or request body, never in URL parameters. URL parameters are logged in server access logs, browser history, and referrer headers.\",\n });\n }\n }\n return findings;\n },\n};\n\nexport const secretLoggedToConsole: CustomRule = {\n id: \"VC147\",\n title: \"Secret Logged to Console\",\n severity: \"high\",\n category: \"Secrets\",\n description: \"Logging variables that appear to contain secrets (key, token, password, secret) writes credentials to stdout, log files, and monitoring services where they can be harvested.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!filePath.match(/\\.(js|ts|jsx|tsx)$/)) return [];\n const findings: RuleMatch[] = [];\n // console.log/debug/info/warn/error with a secret-named variable\n const pattern = /console\\.(?:log|debug|info|warn|error)\\s*\\([^)]*\\b(api[_-]?key|apikey|secret|token|password|passwd|credentials|private[_-]?key|auth[_-]?token|access[_-]?token)\\b/gi;\n let m: RegExpExecArray | null;\n while ((m = pattern.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n if (isInsideFixMessage(content, m.index)) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC147\", title: this.title, severity: \"high\", category: \"Secrets\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Remove this console statement or redact the secret before logging. Use structured logging with a secret-redaction filter in production.\",\n });\n }\n return findings;\n },\n};\n\nexport const secretInErrorResponse: CustomRule = {\n id: \"VC148\",\n title: \"Secret Leaked in Error Response\",\n severity: \"critical\",\n category: \"Secrets\",\n description: \"Returning secrets or secret-named variables in API error responses exposes credentials to anyone who can trigger the error, including unauthenticated users.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!filePath.match(/\\.(js|ts|jsx|tsx)$/)) return [];\n const findings: RuleMatch[] = [];\n // res.json/send/status().json containing secret-named variables in catch/error blocks\n const pattern = /(?:res\\.(?:json|send|status\\s*\\([^)]*\\)\\s*\\.json))\\s*\\(\\s*\\{[^}]*\\b(api[_-]?key|secret|token|password|credentials|private[_-]?key|process\\.env\\.[A-Z_]*(?:KEY|SECRET|TOKEN|PASSWORD))\\b/gi;\n let m: RegExpExecArray | null;\n while ((m = pattern.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n if (isInsideFixMessage(content, m.index)) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC148\", title: this.title, severity: \"critical\", category: \"Secrets\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Never include secrets in API responses. Return a generic error message and log the detailed error server-side only.\",\n });\n }\n return findings;\n },\n};\n\nexport const secretInBundleConfig: CustomRule = {\n id: \"VC149\",\n title: \"Secret in Client-Side Bundle Configuration\",\n severity: \"critical\",\n category: \"Secrets\",\n description: \"Secrets defined in Webpack DefinePlugin, Vite define, or Next.js publicRuntimeConfig are embedded into the client-side JavaScript bundle and visible to anyone viewing the page source.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!filePath.match(/(?:webpack|vite|next)\\.config\\.|\\.config\\.(js|ts|mjs)$/)) return [];\n const findings: RuleMatch[] = [];\n const patterns = [\n // Webpack DefinePlugin with secret-named key\n /DefinePlugin\\s*\\(\\s*\\{[^}]*\\b(?:API_KEY|SECRET|TOKEN|PASSWORD|PRIVATE_KEY|CLIENT_SECRET)\\b/gi,\n // Vite define with secret-named key\n /define\\s*:\\s*\\{[^}]*\\b(?:API_KEY|SECRET|TOKEN|PASSWORD|PRIVATE_KEY|CLIENT_SECRET)\\b/gi,\n // Next.js publicRuntimeConfig with secret-named key\n /publicRuntimeConfig\\s*:\\s*\\{[^}]*\\b(?:apiKey|secret|token|password|privateKey|clientSecret)\\b/gi,\n ];\n for (const p of patterns) {\n let m: RegExpExecArray | null;\n const re = new RegExp(p.source, p.flags.includes(\"g\") ? p.flags : `${p.flags}g`);\n while ((m = re.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n if (isInsideFixMessage(content, m.index)) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC149\", title: this.title, severity: \"critical\", category: \"Secrets\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Move secrets to server-side environment variables. Only expose public configuration (like API base URLs) in client-side bundle config. Use serverRuntimeConfig (Next.js) or server-only modules.\",\n });\n }\n }\n return findings;\n },\n};\n\nexport const secretInHTMLAttribute: CustomRule = {\n id: \"VC150\",\n title: \"Secret in HTML Meta Tag or Data Attribute\",\n severity: \"high\",\n category: \"Secrets\",\n description: \"API keys or tokens embedded in HTML meta tags or data-* attributes are visible in the page source to anyone who visits the page, including search engine crawlers.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!filePath.match(/\\.(html|htm|jsx|tsx|vue|svelte|ejs|hbs|pug)$/)) return [];\n const findings: RuleMatch[] = [];\n const patterns = [\n // <meta name=\"api-key\" content=\"...\">\n /<meta\\s[^>]*name\\s*=\\s*[\"'](?:api[_-]?key|secret|token|password)[^>]*>/gi,\n // data-api-key=\"actual-value\" (not a template/binding)\n /data-(?:api[_-]?key|secret|token|password|auth)\\s*=\\s*[\"'][a-zA-Z0-9_\\-]{12,}[\"']/gi,\n ];\n for (const p of patterns) {\n let m: RegExpExecArray | null;\n const re = new RegExp(p.source, p.flags.includes(\"g\") ? p.flags : `${p.flags}g`);\n while ((m = re.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n if (isInsideFixMessage(content, m.index)) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC150\", title: this.title, severity: \"high\", category: \"Secrets\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Never embed secrets in HTML attributes or meta tags. Load configuration from server-side APIs or use server-rendered environment injection with non-secret values only.\",\n });\n }\n }\n return findings;\n },\n};\n\nexport const secretInCLIArgument: CustomRule = {\n id: \"VC151\",\n title: \"Secret Passed as Command-Line Argument\",\n severity: \"high\",\n category: \"Secrets\",\n description: \"Secrets interpolated into exec/spawn/execSync command strings are visible in process listings (ps aux), shell history, and system audit logs to any user on the same machine.\",\n check(content, filePath) {\n if (isTestFile(filePath)) return [];\n if (!filePath.match(/\\.(js|ts|jsx|tsx|py|rb)$/)) return [];\n const findings: RuleMatch[] = [];\n // exec/spawn/execSync with secret-named variable interpolated\n const patterns = [\n /(?:exec|execSync|spawn|spawnSync|child_process)\\s*\\([^)]*\\$\\{[^}]*(?:api[_-]?key|secret|token|password|credentials)/gi,\n /(?:exec|execSync|spawn|spawnSync|child_process)\\s*\\([^)]*[\"']\\s*\\+\\s*(?:api[_-]?key|secret|token|password|credentials)/gi,\n /(?:subprocess|os\\.system|os\\.popen)\\s*\\([^)]*\\{[^}]*(?:api[_-]?key|secret|token|password|credentials)/gi,\n ];\n for (const p of patterns) {\n let m: RegExpExecArray | null;\n const re = new RegExp(p.source, p.flags.includes(\"g\") ? p.flags : `${p.flags}g`);\n while ((m = re.exec(content)) !== null) {\n if (isCommentLine(content, m.index)) continue;\n if (isInsideFixMessage(content, m.index)) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC151\", title: this.title, severity: \"high\", category: \"Secrets\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Pass secrets via environment variables (env option in spawn/exec) instead of command-line arguments. CLI arguments are visible to all users via `ps aux`.\",\n });\n }\n }\n return findings;\n },\n};\n\n// ────────────────────────────────────────────\n// VC152–VC158: NEW RULES (Webhook Verification, CORS, Validation, etc.)\n// ────────────────────────────────────────────\n\nexport const webhookSignatureVerification: CustomRule = {\n id: \"VC152\",\n title: \"Missing Webhook Signature Verification (Non-Stripe)\",\n severity: \"critical\",\n category: \"Payment Security\",\n description: \"Webhook endpoints for Clerk, GitHub, Resend, SendGrid, Slack, or Twilio that don't verify the request signature allow attackers to forge events.\",\n check(content, filePath) {\n if (!filePath.match(/\\.(js|ts|jsx|tsx)$/)) return [];\n if (isTestFile(filePath)) return [];\n if (!/\\/api\\/|\\/webhook/i.test(filePath)) return [];\n // Must have a POST handler\n if (!/export\\s+(?:async\\s+)?function\\s+POST/i.test(content)) return [];\n\n const services = [\n { name: \"Clerk\", content: /clerk/i, events: /user\\.created|user\\.updated|user\\.deleted|session\\./i, verify: /svix|Webhook\\(\\)\\.verify|webhook-id|wh_secret/i },\n { name: \"GitHub\", content: /github/i, events: /push|pull_request|issues|installation/i, verify: /createHmac|x-hub-signature|verify.*signature|GITHUB_WEBHOOK_SECRET/i },\n { name: \"Resend\", content: /resend/i, events: /email\\.sent|email\\.delivered|email\\.bounced/i, verify: /svix|webhook.*verify|x-webhook-signature|RESEND_WEBHOOK_SECRET/i },\n { name: \"SendGrid\", content: /sendgrid/i, events: /inbound.*parse|event.*webhook/i, verify: /EventWebhook|x-twilio-email|verifySignature|SENDGRID_WEBHOOK_VERIFICATION/i },\n { name: \"Slack\", content: /slack/i, events: /event_callback|challenge|url_verification/i, verify: /signing_secret|createHmac|x-slack-signature|SLACK_SIGNING_SECRET/i },\n { name: \"Twilio\", content: /twilio/i, events: /sms|voice|StatusCallback|MessagingResponse/i, verify: /validateRequest|X-Twilio-Signature|authToken|TWILIO_AUTH_TOKEN/i },\n ];\n\n const findings: RuleMatch[] = [];\n for (const svc of services) {\n if (!svc.content.test(content)) continue;\n if (!svc.events.test(content)) continue;\n if (svc.verify.test(content)) continue;\n // Found a handler for this service without verification\n const m = content.match(/export\\s+(?:async\\s+)?function\\s+POST/);\n if (!m || m.index === undefined) continue;\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n rule: \"VC152\", title: `${svc.name} Webhook Missing Signature Verification`,\n severity: \"critical\", category: \"Payment Security\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: `Verify the ${svc.name} webhook signature before processing events. Check the ${svc.name} documentation for their signature verification method.`,\n });\n break; // Max 1 finding per file\n }\n return findings;\n },\n};\n\nexport const reflectedCORSOrigin: CustomRule = {\n id: \"VC153\",\n title: \"Reflected CORS Origin with Credentials\",\n severity: \"high\",\n category: \"Configuration\",\n description: \"Echoing req.headers.origin into Access-Control-Allow-Origin with credentials enabled lets any website read authenticated API responses. This is worse than wildcard CORS because browsers actually send cookies.\",\n check(content, filePath) {\n if (!filePath.match(/\\.(js|ts|jsx|tsx)$/)) return [];\n if (isTestFile(filePath)) return [];\n // Must have credentials enabled\n if (!/credentials\\s*[=:]\\s*true|Allow-Credentials.*true/i.test(content)) return [];\n // Must have origin reflection pattern\n if (!/req\\.headers\\.origin|callback\\s*\\(\\s*null\\s*,\\s*origin\\s*\\)|origin:\\s*true/i.test(content)) return [];\n // Skip if there's an allowlist check\n if (/ALLOWED_ORIGINS|allowedOrigins|whitelist|isAllowed|\\.includes\\s*\\(\\s*origin|\\.has\\s*\\(\\s*origin/i.test(content)) return [];\n\n const patterns = [\n /(?:Allow-Origin[\"'],\\s*req\\.headers\\.origin)/gi,\n /origin\\s*:\\s*\\(\\s*origin\\s*,\\s*callback\\s*\\)\\s*=>\\s*callback\\s*\\(\\s*null\\s*,\\s*origin\\s*\\)/gi,\n /origin\\s*:\\s*true/gi,\n ];\n const matches: RuleMatch[] = [];\n for (const p of patterns) {\n const raw = findMatches(content, p, reflectedCORSOrigin, filePath, () =>\n \"Use an explicit origin allowlist instead of reflecting the request origin. Only allow origins you control.\"\n );\n if (raw.length > 0) { matches.push(raw[0]); break; } // Max 1 per file\n }\n return matches;\n },\n};\n\nexport const missingRequestValidation: CustomRule = {\n id: \"VC154\",\n title: \"API Route Without Request Body Validation\",\n severity: \"medium\",\n category: \"Authorization\",\n description: \"API route reads the request body without any schema validation (zod, joi, yup, etc.). Unvalidated input can lead to unexpected behavior, injection, or data corruption.\",\n check(content, filePath) {\n if (!filePath.match(/\\.(js|ts|jsx|tsx)$/)) return [];\n if (isTestFile(filePath)) return [];\n if (!/\\/api\\//i.test(filePath)) return [];\n if (/\\/webhook/i.test(filePath)) return []; // Webhooks validate via signature\n // Must have a mutating handler\n if (!/export\\s+(?:async\\s+)?function\\s+(?:POST|PUT|PATCH)/i.test(content)) return [];\n // Must read the body\n if (!/req\\.body|request\\.json\\(\\)|c\\.req\\.json\\(\\)|await.*\\.json\\(\\)/i.test(content)) return [];\n // Skip if any validation library or manual checks exist\n if (/zod|joi|yup|ajv|class-validator|superstruct|valibot|\\.safeParse|\\.validate\\(|z\\.object|Joi\\.object|yup\\.object|schema\\.parse|zodResolver/i.test(content)) return [];\n if (/if\\s*\\(\\s*!(?:body|parsed|data|payload)\\.|throw.*(?:missing|invalid|required)/i.test(content)) return [];\n const m = content.match(/export\\s+(?:async\\s+)?function\\s+(?:POST|PUT|PATCH)/);\n if (!m || m.index === undefined) return [];\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n return [{\n rule: \"VC154\", title: this.title, severity: \"medium\" as const, category: \"Authorization\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Validate request bodies with a schema library like zod: const parsed = schema.safeParse(await request.json()); if (!parsed.success) return Response.json({ error: 'Invalid input' }, { status: 400 });\",\n }];\n },\n};\n\nexport const missingAIRateLimit: CustomRule = {\n id: \"VC155\",\n title: \"Missing Rate Limiting on AI/LLM API Call\",\n severity: \"high\",\n category: \"Availability\",\n description: \"API route calls an AI/LLM service (OpenAI, Anthropic, Cohere) without rate limiting. A single user can burn through your entire API budget in minutes.\",\n check(content, filePath) {\n if (!filePath.match(/\\.(js|ts|jsx|tsx)$/)) return [];\n if (isTestFile(filePath)) return [];\n // Must have a route handler\n if (!/export\\s+(?:async\\s+)?function\\s+(?:GET|POST|PUT|PATCH|DELETE)/i.test(content) &&\n !/\\.(get|post|put|patch|delete)\\s*\\(/i.test(content)) return [];\n // Must reference an AI SDK\n if (!/openai|anthropic|@anthropic-ai|cohere|@google\\/generative-ai|chat\\.completions|messages\\.create/i.test(content)) return [];\n // Skip if rate limiting exists\n if (/rateLimit|rateLimiter|throttle|checkRateLimit|limiter|slowDown|express-rate-limit/i.test(content)) return [];\n const m = content.match(/export\\s+(?:async\\s+)?function\\s+(?:POST|GET)/i) || content.match(/\\.(post|get)\\s*\\(/i);\n if (!m || m.index === undefined) return [];\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n return [{\n rule: \"VC155\", title: this.title, severity: \"high\" as const, category: \"Availability\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Add rate limiting to AI/LLM endpoints to prevent budget abuse. Use express-rate-limit or a custom limiter with per-user and per-IP limits.\",\n }];\n },\n};\n\nexport const missingPagination: CustomRule = {\n id: \"VC156\",\n title: \"Missing Pagination on List Endpoint\",\n severity: \"medium\",\n category: \"Availability\",\n description: \"API endpoint returns database records without a LIMIT or pagination. A single request can dump the entire table, crash the server, or cause an out-of-memory error.\",\n check(content, filePath) {\n if (!filePath.match(/\\.(js|ts|jsx|tsx)$/)) return [];\n if (isTestFile(filePath)) return [];\n if (!/\\/api\\//i.test(filePath)) return [];\n // Must have a GET handler (list endpoints)\n if (!/export\\s+(?:async\\s+)?function\\s+GET/i.test(content)) return [];\n // Must have a database query\n if (!/findMany|\\.find\\(\\s*\\{|\\.find\\(\\s*\\)|SELECT\\s.*FROM|\\.all\\(\\)|\\.query\\(/i.test(content)) return [];\n // Skip if any pagination exists\n if (/LIMIT|\\.take\\(|\\.limit\\(|\\.skip\\(|offset|cursor|page|perPage|pageSize|paginate|\\.slice\\(/i.test(content)) return [];\n // Skip if it filters by specific ID (not a list endpoint)\n if (/findUnique|findFirst|findById|\\.findOne|WHERE.*id\\s*=/i.test(content)) return [];\n const m = content.match(/export\\s+(?:async\\s+)?function\\s+GET/);\n if (!m || m.index === undefined) return [];\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n return [{\n rule: \"VC156\", title: this.title, severity: \"medium\" as const, category: \"Availability\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Add pagination to list endpoints: .findMany({ take: limit, skip: offset }) or SELECT ... LIMIT ? OFFSET ?\",\n }];\n },\n};\n\nexport const exposedDatabaseStudio: CustomRule = {\n id: \"VC157\",\n title: \"Database Studio Exposed in Production\",\n severity: \"high\",\n category: \"Configuration\",\n description: \"Prisma Studio or Drizzle Studio is configured in a production script or bound to 0.0.0.0, giving anyone with the URL full database admin access.\",\n check(content, filePath) {\n const basename = filePath.split(\"/\").pop() || \"\";\n if (basename !== \"package.json\" && !/docker-compose/i.test(basename)) return [];\n // Check for studio in production-facing scripts\n if (!/prisma\\s+studio|drizzle-kit\\s+studio/i.test(content)) return [];\n // Skip if only in dev/db:studio scripts (that's fine)\n const lines = content.split(\"\\n\");\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n if (!/prisma\\s+studio|drizzle-kit\\s+studio/i.test(line)) continue;\n // Flag if in start/production script or bound to 0.0.0.0\n if (/\"start\"|\"production\"|\"build.*&&.*studio\"|0\\.0\\.0\\.0/i.test(line)) {\n return [{\n rule: \"VC157\", title: this.title, severity: \"high\" as const, category: \"Configuration\",\n file: filePath, line: i + 1, snippet: getSnippet(content, i + 1),\n fix: \"Only run database studio in development. Move the studio command to a dev-only script and never bind to 0.0.0.0 in production.\",\n }];\n }\n }\n return [];\n },\n};\n\nexport const insecureDirectObjectReference: CustomRule = {\n id: \"VC158\",\n title: \"Potential Insecure Direct Object Reference (IDOR)\",\n severity: \"high\",\n category: \"Authorization\",\n description: \"API route fetches a record by ID from URL parameters without checking if the requesting user owns that record. An attacker can access other users' data by guessing or enumerating IDs.\",\n check(content, filePath) {\n if (!filePath.match(/\\.(js|ts|jsx|tsx)$/)) return [];\n if (isTestFile(filePath)) return [];\n if (!/\\/api\\//i.test(filePath)) return [];\n // Must have a route handler\n if (!/export\\s+(?:async\\s+)?function\\s+(?:GET|PUT|PATCH|DELETE)/i.test(content)) return [];\n // Must read an ID from params\n if (!/params\\.id|params\\.(?:slug|uuid)|req\\.params\\./i.test(content)) return [];\n // Must query a database with that param\n if (!/findUnique|findFirst|findById|\\.findOne|WHERE.*=.*params|\\.get\\(.*params/i.test(content)) return [];\n // Skip if ownership check exists\n if (/userId|ownerId|createdBy|user_id|owner_id|AND.*user|where.*user|authorize|canAccess|checkPermission|isOwner|belongsTo/i.test(content)) return [];\n // Skip admin routes\n if (/admin/i.test(filePath)) return [];\n // Skip if auth is present (may be checking ownership via middleware)\n if (/requireUser|requireUserForApi|getServerSession|auth\\(\\)/i.test(content)) return [];\n const m = content.match(/params\\.id|params\\.(?:slug|uuid)|req\\.params\\./);\n if (!m || m.index === undefined) return [];\n const lineNum = content.substring(0, m.index).split(\"\\n\").length;\n return [{\n rule: \"VC158\", title: this.title, severity: \"high\" as const, category: \"Authorization\",\n file: filePath, line: lineNum, snippet: getSnippet(content, lineNum),\n fix: \"Add an ownership check: verify the requesting user owns the record before returning it. Example: WHERE id = params.id AND userId = session.userId\",\n }];\n },\n};\n\n// ────────────────────────────────────────────\n// RULE TIERS: FREE (30 rules) + PRO (all 158)\n// VC152-VC158 are Pro rules (not in freeRules)\n// ────────────────────────────────────────────\n\n// Free tier: core security rules available to everyone (30 of 151)\nexport const freeRules: CustomRule[] = [\n hardcodedSecrets, // VC001\n exposedEnvFile, // VC002\n missingAuthMiddleware, // VC003\n supabaseNoRLS, // VC004\n stripeWebhookUnprotected, // VC005\n sqlInjection, // VC006\n xssVulnerability, // VC007\n noRateLimiting, // VC008\n corsWildcard, // VC009\n clientSideAuth, // VC010\n nextPublicSecret, // VC011\n envNotGitignored, // VC014\n evalUsage, // VC015\n unvalidatedRedirect, // VC016\n insecureCookies, // VC017\n exposedAuthSecret, // VC018\n missingCSP, // VC020\n hardcodedJWTSecret, // VC031\n missingHTTPS, // VC032\n exposedDebugMode, // VC033\n insecureRandomness, // VC034\n missingErrorBoundary, // VC036\n exposedStackTraces, // VC037\n missingLockFile, // VC039\n dangerousInnerHTML, // VC063\n consoleLogProduction, // VC097\n emptyCatchBlock, // VC104\n todoLeftInCode, // VC103\n weakHashing, // VC060\n disabledTLSVerification, // VC061\n];\n\n// Pro rules are delivered server-side — not shipped in npm package\n// See: /api/cli/rules-bundle endpoint\nexport const allRules = freeRules;\n\nexport function runCustomRules(\n content: string,\n filePath: string,\n disabledRules: string[] = [],\n tier: \"free\" | \"pro\" = \"free\",\n extraRules: CustomRule[] = [],\n): Finding[] {\n const findings: Finding[] = [];\n\n // Skip files that ARE security scanners (avoid scanning ourselves)\n if (/function runScan\\(files\\)|export function runCustomRules/.test(content) && /const (?:rules|allRules)\\s*[:=]/.test(content) && /findMatches/.test(content)) {\n return findings;\n }\n\n // Skip compiled bundles (pro-rules-bundle.cjs, webpack output, etc.)\n if (/pro-rules-bundle|\\.bundle\\./i.test(filePath)) return findings;\n\n // Skip files that contain intentional example/demo vulnerable code\n // (onboarding tutorials, blog code samples, documentation examples)\n if (/onboarding|demo-data|example-vulnerable|code-sample/i.test(filePath)) return findings;\n\n // Free tier uses bundled rules; Pro tier uses free + server-delivered extra rules\n const ruleset = tier === \"pro\" && extraRules.length > 0\n ? [...freeRules, ...extraRules]\n : freeRules;\n for (const rule of ruleset) {\n if (disabledRules.includes(rule.id)) continue;\n\n const matches = rule.check(content, filePath);\n const compliance = complianceMap[rule.id];\n for (const match of matches) {\n findings.push({\n id: `${match.rule}-${match.file}:${match.line}`,\n rule: match.rule,\n severity: match.severity,\n title: match.title,\n description: rule.description,\n file: match.file,\n line: match.line,\n column: match.column,\n snippet: match.snippet,\n fix: match.fix,\n category: match.category,\n source: \"custom\",\n owasp: compliance?.owasp,\n cwe: compliance?.cwe,\n });\n }\n }\n\n return findings;\n}\n","import { execFile } from \"node:child_process\";\nimport { existsSync } from \"node:fs\";\nimport { readFile, writeFile, mkdtemp, rm } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { tmpdir } from \"node:os\";\nimport type { Finding, Severity } from \"../types.js\";\n\ninterface SemgrepSarifResult {\n runs?: Array<{\n results?: Array<{\n ruleId?: string;\n level?: string;\n message?: { text?: string };\n locations?: Array<{\n physicalLocation?: {\n artifactLocation?: { uri?: string };\n region?: {\n startLine?: number;\n startColumn?: number;\n snippet?: { text?: string };\n };\n };\n }>;\n }>;\n }>;\n}\n\nconst SEVERITY_MAP: Record<string, Severity> = {\n error: \"high\",\n warning: \"medium\",\n note: \"low\",\n none: \"info\",\n};\n\nfunction semgrepSeverityToXploitscan(level: string): Severity {\n return SEVERITY_MAP[level] ?? \"medium\";\n}\n\nasync function isSemgrepInstalled(): Promise<boolean> {\n return new Promise((resolve) => {\n execFile(\"semgrep\", [\"--version\"], (error) => {\n resolve(!error);\n });\n });\n}\n\nexport async function runSemgrep(\n directory: string,\n customRulesDir?: string,\n): Promise<{ findings: Finding[]; available: boolean }> {\n const installed = await isSemgrepInstalled();\n if (!installed) {\n return { findings: [], available: false };\n }\n\n const findings: Finding[] = [];\n\n // Create a temp directory for SARIF output\n const tmpDir = await mkdtemp(join(tmpdir(), \"xploitscan-semgrep-\"));\n const sarifPath = join(tmpDir, \"results.sarif\");\n\n try {\n // Build semgrep args\n const args = [\n \"scan\",\n \"--sarif\",\n \"--output\", sarifPath,\n \"--quiet\",\n \"--no-git-ignore\", // We handle .gitignore ourselves\n \"--timeout\", \"30\",\n \"--max-target-bytes\", \"1000000\",\n ];\n\n // Use auto config (community rules) + custom rules if available\n args.push(\"--config\", \"auto\");\n\n if (customRulesDir && existsSync(customRulesDir)) {\n args.push(\"--config\", customRulesDir);\n }\n\n args.push(directory);\n\n // Run semgrep\n await new Promise<void>((resolve, reject) => {\n const proc = execFile(\n \"semgrep\",\n args,\n { timeout: 120_000, maxBuffer: 10 * 1024 * 1024 },\n (error, _stdout, stderr) => {\n // Semgrep returns exit code 1 when findings exist — that's fine\n if (error && error.code !== 1) {\n reject(new Error(`Semgrep failed: ${stderr || error.message}`));\n } else {\n resolve();\n }\n },\n );\n });\n\n // Parse SARIF output\n if (!existsSync(sarifPath)) return { findings, available: true };\n\n const sarifContent = await readFile(sarifPath, \"utf-8\");\n const sarif: SemgrepSarifResult = JSON.parse(sarifContent);\n\n for (const run of sarif.runs ?? []) {\n for (const result of run.results ?? []) {\n const location = result.locations?.[0]?.physicalLocation;\n const filePath = location?.artifactLocation?.uri ?? \"unknown\";\n const line = location?.region?.startLine ?? 1;\n const snippet = location?.region?.snippet?.text ?? \"\";\n\n // Determine category from rule ID\n const ruleId = result.ruleId ?? \"semgrep\";\n const category = categorizeSemgrepRule(ruleId);\n\n findings.push({\n id: `SG-${filePath}:${line}:${ruleId}`,\n rule: ruleId,\n severity: semgrepSeverityToXploitscan(result.level ?? \"warning\"),\n title: truncate(result.message?.text ?? ruleId, 100),\n description: result.message?.text ?? \"\",\n file: filePath.replace(/^file:\\/\\//, \"\"),\n line,\n column: location?.region?.startColumn,\n snippet: formatSnippet(snippet, line),\n category,\n source: \"semgrep\",\n });\n }\n }\n } finally {\n // Clean up temp directory\n await rm(tmpDir, { recursive: true, force: true }).catch(() => {});\n }\n\n return { findings, available: true };\n}\n\nfunction categorizeSemgrepRule(ruleId: string): string {\n const id = ruleId.toLowerCase();\n if (id.includes(\"sql\") || id.includes(\"injection\") || id.includes(\"xss\") || id.includes(\"command\")) return \"Injection\";\n if (id.includes(\"auth\") || id.includes(\"session\")) return \"Authentication\";\n if (id.includes(\"crypto\") || id.includes(\"hash\") || id.includes(\"random\")) return \"Cryptography\";\n if (id.includes(\"cors\") || id.includes(\"header\") || id.includes(\"config\")) return \"Configuration\";\n if (id.includes(\"secret\") || id.includes(\"key\") || id.includes(\"password\") || id.includes(\"credential\")) return \"Secrets\";\n if (id.includes(\"path\") || id.includes(\"traversal\") || id.includes(\"file\")) return \"Path Traversal\";\n if (id.includes(\"deserial\")) return \"Deserialization\";\n return \"Security\";\n}\n\nfunction formatSnippet(text: string, line: number): string {\n if (!text) return \"\";\n const lines = text.split(\"\\n\");\n return lines\n .map((l, i) => {\n const num = line + i;\n return ` ${num.toString().padStart(4)} | ${l}`;\n })\n .join(\"\\n\");\n}\n\nfunction truncate(str: string, max: number): string {\n return str.length > max ? str.substring(0, max - 3) + \"...\" : str;\n}\n","import { execFile } from \"node:child_process\";\nimport { readFile, mkdtemp, rm } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { tmpdir } from \"node:os\";\nimport type { Finding, Severity } from \"../types.js\";\nimport { getSnippet, readFileContents } from \"../utils/files.js\";\n\ninterface GitleaksResult {\n Description: string;\n File: string;\n StartLine: number;\n EndLine: number;\n StartColumn: number;\n EndColumn: number;\n Match: string;\n Secret: string;\n RuleID: string;\n Entropy: number;\n Tags?: string[];\n}\n\nconst RULE_SEVERITY: Record<string, Severity> = {\n \"aws-access-token\": \"critical\",\n \"aws-secret-access-key\": \"critical\",\n \"stripe-access-token\": \"critical\",\n \"github-pat\": \"critical\",\n \"private-key\": \"critical\",\n \"generic-api-key\": \"high\",\n \"slack-webhook\": \"high\",\n \"twilio-api-key\": \"high\",\n \"sendgrid-api-key\": \"high\",\n \"shopify-access-token\": \"high\",\n \"gcp-api-key\": \"critical\",\n \"heroku-api-key\": \"high\",\n \"npm-access-token\": \"critical\",\n \"pypi-upload-token\": \"critical\",\n \"telegram-bot-api-token\": \"high\",\n \"discord-bot-token\": \"high\",\n \"firebase-api-key\": \"high\",\n};\n\nasync function isGitleaksInstalled(): Promise<boolean> {\n return new Promise((resolve) => {\n execFile(\"gitleaks\", [\"version\"], (error) => {\n resolve(!error);\n });\n });\n}\n\nexport async function runGitleaks(\n directory: string,\n): Promise<{ findings: Finding[]; available: boolean }> {\n const installed = await isGitleaksInstalled();\n if (!installed) {\n return { findings: [], available: false };\n }\n\n const findings: Finding[] = [];\n const tmpDir = await mkdtemp(join(tmpdir(), \"xploitscan-gitleaks-\"));\n const reportPath = join(tmpDir, \"results.json\");\n\n try {\n const args = [\n \"detect\",\n \"--source\", directory,\n \"--report-path\", reportPath,\n \"--report-format\", \"json\",\n \"--no-git\",\n \"--exit-code\", \"0\", // Don't fail on findings\n ];\n\n await new Promise<void>((resolve, reject) => {\n execFile(\n \"gitleaks\",\n args,\n { timeout: 60_000, maxBuffer: 10 * 1024 * 1024 },\n (error, _stdout, stderr) => {\n if (error) {\n const sanitizedError = (stderr || error.message).slice(0, 200);\n reject(new Error(`Gitleaks failed: ${sanitizedError}`));\n } else {\n resolve();\n }\n },\n );\n });\n\n // Parse results\n if (!existsSync(reportPath)) return { findings, available: true };\n\n const reportContent = await readFile(reportPath, \"utf-8\");\n if (!reportContent.trim()) return { findings, available: true };\n\n const results: GitleaksResult[] = JSON.parse(reportContent);\n\n for (const result of results) {\n const filePath = result.File;\n const line = result.StartLine + 1; // Gitleaks uses 0-based lines\n const severity = RULE_SEVERITY[result.RuleID] ?? \"high\";\n\n // Read file content for snippet\n const content = readFileContents(directory, filePath);\n const snippet = content ? getSnippet(content, line) : ` ${result.Match}`;\n\n // Redact the actual secret in the description\n const redactedSecret = result.Secret.length > 8\n ? result.Secret.substring(0, 4) + \"...\" + result.Secret.substring(result.Secret.length - 4)\n : \"****\";\n\n findings.push({\n id: `GL-${filePath}:${line}:${result.RuleID}`,\n rule: `GL:${result.RuleID}`,\n severity,\n title: `${result.Description} (detected by Gitleaks)`,\n description: `A secret matching \"${result.RuleID}\" pattern was found: ${redactedSecret}. If this is a real credential, it may already be compromised. Rotate it immediately and move it to environment variables.`,\n file: filePath,\n line,\n column: result.StartColumn,\n snippet,\n fix: `1. Rotate this credential immediately (it may be in git history)\\n2. Move it to a .env file: ${result.RuleID.toUpperCase().replace(/-/g, \"_\")}=<new-value>\\n3. Add .env to .gitignore\\n4. Remove from git history: git filter-branch or BFG Repo Cleaner`,\n category: \"Secrets\",\n source: \"gitleaks\",\n });\n }\n } finally {\n await rm(tmpDir, { recursive: true, force: true }).catch(() => {});\n }\n\n return { findings, available: true };\n}\n","import Anthropic from \"@anthropic-ai/sdk\";\nimport type { Finding, Severity } from \"../types.js\";\n\nconst SYSTEM_PROMPT = `You are a security auditor specializing in code generated by AI tools (Cursor, Lovable, Bolt, Replit, Claude). Your audience is non-expert developers who may not understand security jargon.\n\nAnalyze the provided code for security vulnerabilities. Focus on issues that AI code generators commonly introduce:\n- Missing authentication/authorization\n- Exposed secrets or credentials\n- SQL injection, XSS, and other injection flaws\n- Insecure direct object references (IDOR)\n- Missing input validation\n- Insecure defaults (permissive CORS, no rate limiting)\n- Client-side security checks without server-side enforcement\n- Supabase RLS misconfigurations\n- Unprotected payment/webhook endpoints\n\nFor each vulnerability found, respond with a JSON array of objects:\n{\n \"title\": \"Short, clear title\",\n \"severity\": \"critical|high|medium|low\",\n \"line\": <approximate line number>,\n \"description\": \"Plain-English explanation a non-developer can understand. What's the risk? What could an attacker do?\",\n \"fix\": \"Step-by-step fix instructions with code example if helpful\",\n \"category\": \"Secrets|Authentication|Authorization|Injection|Configuration|Payment Security|Data Exposure\"\n}\n\nIf no vulnerabilities are found, return an empty array: []\n\nRules:\n- Only report real, exploitable issues — no theoretical concerns\n- Be specific about what an attacker could do\n- Explain fixes in beginner-friendly language\n- Reference specific line numbers`;\n\nexport async function analyzeWithAI(\n files: { path: string; content: string }[],\n existingFindings: Finding[],\n): Promise<Finding[]> {\n const apiKey = process.env.ANTHROPIC_API_KEY;\n if (!apiKey) return [];\n\n const client = new Anthropic();\n\n // Build context: send files with existing findings for dedup\n const existingRules = new Set(\n existingFindings.map((f) => `${f.file}:${f.line}:${f.rule}`),\n );\n\n // Batch files into chunks to stay within token limits (~50KB per chunk)\n const chunks = chunkFiles(files, 50_000);\n const allFindings: Finding[] = [];\n\n for (const chunk of chunks) {\n const fileContext = chunk\n .map((f) => `--- ${f.path} ---\\n${f.content}`)\n .join(\"\\n\\n\");\n\n const existingNote = existingFindings.length > 0\n ? `\\n\\nThe following issues have already been found by static rules (do NOT duplicate these):\\n${existingFindings.map((f) => `- ${f.file}:${f.line} — ${f.title}`).join(\"\\n\")}`\n : \"\";\n\n try {\n const response = await client.messages.create({\n model: \"claude-sonnet-4-5-20250514\",\n max_tokens: 4096,\n messages: [\n {\n role: \"user\",\n content: `Analyze these source files for security vulnerabilities:${existingNote}\\n\\n${fileContext}`,\n },\n ],\n system: SYSTEM_PROMPT,\n });\n\n const text = response.content\n .filter((block): block is Anthropic.TextBlock => block.type === \"text\")\n .map((block) => block.text)\n .join(\"\");\n\n // Extract JSON from response (handles markdown code blocks)\n const jsonMatch = text.match(/\\[[\\s\\S]*\\]/);\n if (!jsonMatch) continue;\n\n const parsed = JSON.parse(jsonMatch[0]) as Array<{\n title: string;\n severity: Severity;\n line: number;\n description: string;\n fix: string;\n category: string;\n }>;\n\n for (const item of parsed) {\n // Find which file this finding belongs to\n const matchFile = chunk.find((f) => {\n const lines = f.content.split(\"\\n\");\n return item.line <= lines.length;\n });\n const file = matchFile?.path ?? chunk[0].path;\n\n const key = `${file}:${item.line}:AI`;\n if (existingRules.has(key)) continue;\n\n const content = chunk.find((f) => f.path === file)?.content ?? \"\";\n const lines = content.split(\"\\n\");\n const snippetStart = Math.max(0, item.line - 3);\n const snippetEnd = Math.min(lines.length, item.line + 2);\n const snippet = lines\n .slice(snippetStart, snippetEnd)\n .map((l, i) => {\n const num = snippetStart + i + 1;\n const marker = num === item.line ? \">\" : \" \";\n return `${marker} ${num.toString().padStart(4)} | ${l}`;\n })\n .join(\"\\n\");\n\n allFindings.push({\n id: `AI-${file}:${item.line}`,\n rule: \"AI\",\n severity: item.severity,\n title: item.title,\n description: item.description,\n file,\n line: item.line,\n snippet,\n fix: item.fix,\n category: item.category,\n source: \"ai\",\n });\n }\n } catch (error) {\n // AI analysis failed for this chunk — continue with others\n if (error instanceof Error && error.message.includes(\"API key\")) {\n throw new Error(\n \"Invalid ANTHROPIC_API_KEY. Get one at https://console.anthropic.com/\",\n );\n }\n }\n }\n\n return allFindings;\n}\n\nfunction chunkFiles(\n files: { path: string; content: string }[],\n maxChars: number,\n): { path: string; content: string }[][] {\n const chunks: { path: string; content: string }[][] = [];\n let current: { path: string; content: string }[] = [];\n let currentSize = 0;\n\n for (const file of files) {\n // Truncate individual files that exceed the chunk limit\n const truncatedContent = file.content.length > maxChars\n ? file.content.slice(0, maxChars) + \"\\n// ... truncated for analysis\"\n : file.content;\n const entry = { path: file.path, content: truncatedContent };\n\n if (currentSize + entry.content.length > maxChars && current.length > 0) {\n chunks.push(current);\n current = [];\n currentSize = 0;\n }\n current.push(entry);\n currentSize += entry.content.length;\n }\n\n if (current.length > 0) chunks.push(current);\n return chunks;\n}\n","import Anthropic from \"@anthropic-ai/sdk\";\nimport type { Finding } from \"../types.js\";\n\n/**\n * AI-powered false positive filter.\n *\n * Takes findings from the regex/entropy scanners and asks Claude Haiku\n * to classify each one as \"real\" or \"false positive\" based on the\n * surrounding code context. Findings marked FP are removed before the\n * user sees them, but preserved in filteredFindings so users can\n * review the AI's decisions.\n */\n\nconst REVIEW_SYSTEM_PROMPT = `You are reviewing security scan findings for false positives. Your job is to look at each finding and the surrounding code to determine if it's a REAL security vulnerability or a FALSE POSITIVE.\n\nCommon false positive patterns you should catch:\n- Auth check exists inside the function body (requireUser, requireUserForApi, getSession, etc.) but the scanner only checked the function signature\n- The flagged pattern is in example/documentation/tutorial code, not production code\n- The variable is developer-controlled (constants, config values, static strings), not user input\n- The flagged function is a database method (conn.exec, db.exec, prisma.$executeRaw) not a shell command (child_process.exec)\n- The comparison is a type check (typeof x === \"string\") not a secret comparison\n- The file is a test, mock, or fixture file\n- The \"secret\" is a publishable/public key (pk_test_, NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY) designed to be client-side\n- The innerHTML/dangerouslySetInnerHTML uses a static constant or JSON.stringify, not user input\n- The redirect URL has already been validated (isAllowedRedirect, validateRedirect)\n- The webhook endpoint is for a non-Stripe service but flagged as \"Stripe webhook\"\n- The \"sensitive data in URL\" is in a comment or documentation, not actual code\n- Package-lock.json URLs flagged as secrets (they're npm registry URLs, not secrets)\n\nFor each finding, respond ONLY with a JSON array. No other text.\nEach element: {\"index\": <number>, \"verdict\": \"real\" or \"fp\", \"reason\": \"<1 sentence>\"}`;\n\nconst MAX_FINDINGS_PER_BATCH = 15;\nconst MAX_CONTEXT_LINES = 10;\nconst MAX_TOTAL_FINDINGS = 50;\n\ninterface ReviewResult {\n index: number;\n verdict: \"real\" | \"fp\";\n reason: string;\n}\n\nexport interface FilteredFinding {\n finding: Finding;\n reason: string;\n}\n\nexport interface AIFilterResult {\n findings: Finding[];\n filteredFindings: FilteredFinding[];\n aiReviewed: boolean;\n removedCount: number;\n totalBefore: number;\n}\n\nfunction getExpandedContext(content: string, line: number, contextLines: number = MAX_CONTEXT_LINES): string {\n const lines = content.split(\"\\n\");\n const start = Math.max(0, line - 1 - contextLines);\n const end = Math.min(lines.length, line + contextLines);\n return lines.slice(start, end).map((l, i) => {\n const lineNum = start + i + 1;\n const marker = lineNum === line ? \">>>\" : \" \";\n return `${marker} ${lineNum} | ${l}`;\n }).join(\"\\n\");\n}\n\nfunction buildReviewPrompt(findings: Finding[], fileContent: string): string {\n const parts: string[] = [];\n for (let i = 0; i < findings.length; i++) {\n const f = findings[i];\n const context = getExpandedContext(fileContent, f.line);\n parts.push(`--- Finding ${i} ---\nRule: ${f.rule} (${f.title})\nSeverity: ${f.severity}\nFile: ${f.file}\nLine: ${f.line}\nDescription: ${f.description}\nSuggested fix: ${f.fix || \"N/A\"}\n\nCode context:\n${context}\n`);\n }\n return `Review these ${findings.length} security scan findings. For each one, determine if it's a real vulnerability or a false positive based on the code context.\\n\\n${parts.join(\"\\n\")}`;\n}\n\nfunction parseReviewResponse(text: string): ReviewResult[] {\n try {\n const cleaned = text.replace(/```json\\n?/g, \"\").replace(/```\\n?/g, \"\").trim();\n const parsed = JSON.parse(cleaned);\n if (!Array.isArray(parsed)) return [];\n return parsed.filter(\n (r: unknown): r is ReviewResult =>\n typeof r === \"object\" && r !== null &&\n \"index\" in r && \"verdict\" in r &&\n ((r as ReviewResult).verdict === \"real\" || (r as ReviewResult).verdict === \"fp\")\n );\n } catch {\n return [];\n }\n}\n\n/**\n * Filter false positives from findings using Claude Haiku.\n *\n * Returns filtered findings, the removed findings with AI reasons,\n * and metadata about what the AI did.\n */\nexport async function filterFalsePositives(\n findings: Finding[],\n fileContents: Map<string, string>,\n): Promise<AIFilterResult> {\n const empty: AIFilterResult = { findings, filteredFindings: [], aiReviewed: false, removedCount: 0, totalBefore: findings.length };\n if (!process.env.ANTHROPIC_API_KEY) return empty;\n if (findings.length === 0) return empty;\n\n const toReview = findings.slice(0, MAX_TOTAL_FINDINGS);\n const overflow = findings.slice(MAX_TOTAL_FINDINGS);\n const totalBefore = findings.length;\n\n const byFile = new Map<string, Finding[]>();\n for (const f of toReview) {\n const group = byFile.get(f.file) || [];\n group.push(f);\n byFile.set(f.file, group);\n }\n\n let client: Anthropic;\n try {\n client = new Anthropic();\n } catch {\n return empty;\n }\n\n // Track which findings are FP and why\n const fpMap = new Map<number, string>(); // globalIndex → reason\n\n for (const [file, fileFindings] of byFile) {\n const content = fileContents.get(file);\n if (!content) continue;\n\n for (let i = 0; i < fileFindings.length; i += MAX_FINDINGS_PER_BATCH) {\n const batch = fileFindings.slice(i, i + MAX_FINDINGS_PER_BATCH);\n const prompt = buildReviewPrompt(batch, content);\n\n try {\n const response = await client.messages.create({\n model: \"claude-haiku-4-5-20251001\",\n max_tokens: 1024,\n system: REVIEW_SYSTEM_PROMPT,\n messages: [{ role: \"user\", content: prompt }],\n });\n\n const text = response.content\n .filter((b): b is Anthropic.TextBlock => b.type === \"text\")\n .map((b) => b.text)\n .join(\"\");\n\n const results = parseReviewResponse(text);\n\n for (const r of results) {\n if (r.verdict === \"fp\" && r.index >= 0 && r.index < batch.length) {\n const globalIndex = toReview.indexOf(batch[r.index]);\n if (globalIndex !== -1) {\n fpMap.set(globalIndex, r.reason);\n }\n }\n }\n } catch {\n continue;\n }\n }\n }\n\n const filtered = toReview.filter((_, i) => !fpMap.has(i));\n const filteredFindings: FilteredFinding[] = [];\n for (const [idx, reason] of fpMap) {\n filteredFindings.push({ finding: toReview[idx], reason });\n }\n\n return {\n findings: [...filtered, ...overflow],\n filteredFindings,\n aiReviewed: true,\n removedCount: fpMap.size,\n totalBefore,\n };\n}\n","/**\n * AST-based JavaScript/TypeScript analyzer\n * Uses regex-based pseudo-AST parsing (no external deps) to understand code structure\n * and reduce false positives from comments, strings, and non-executable code.\n */\n\nimport type { Finding, Severity } from \"../types.js\";\n\n// ────────────────────────────────────────────\n// Code Structure Detection\n// ────────────────────────────────────────────\n\ninterface CodeContext {\n isComment: boolean;\n isString: boolean;\n isImport: boolean;\n isTestFile: boolean;\n isTypeDefinition: boolean;\n functionScope: string | null;\n nearestAssignment: string | null;\n}\n\n/** Strip all comments from JS/TS/Python code to prevent false positives */\nexport function stripComments(content: string): string {\n let result = \"\";\n let i = 0;\n let inSingleQuote = false;\n let inDoubleQuote = false;\n let inTemplate = false;\n let inSingleLineComment = false;\n let inMultiLineComment = false;\n\n while (i < content.length) {\n const ch = content[i];\n const next = content[i + 1];\n\n if (inSingleLineComment) {\n if (ch === \"\\n\") {\n inSingleLineComment = false;\n result += ch;\n }\n i++;\n continue;\n }\n\n if (inMultiLineComment) {\n if (ch === \"*\" && next === \"/\") {\n inMultiLineComment = false;\n i += 2;\n } else {\n if (ch === \"\\n\") result += ch; // preserve line numbers\n i++;\n }\n continue;\n }\n\n if (inSingleQuote) {\n result += ch;\n if (ch === \"'\" && content[i - 1] !== \"\\\\\") inSingleQuote = false;\n i++;\n continue;\n }\n\n if (inDoubleQuote) {\n result += ch;\n if (ch === '\"' && content[i - 1] !== \"\\\\\") inDoubleQuote = false;\n i++;\n continue;\n }\n\n if (inTemplate) {\n result += ch;\n if (ch === \"`\" && content[i - 1] !== \"\\\\\") inTemplate = false;\n i++;\n continue;\n }\n\n // Check for comment starts\n if (ch === \"/\" && next === \"/\") {\n inSingleLineComment = true;\n i += 2;\n continue;\n }\n if (ch === \"/\" && next === \"*\") {\n inMultiLineComment = true;\n i += 2;\n continue;\n }\n // Python/YAML comments\n if (ch === \"#\" && !inSingleQuote && !inDoubleQuote) {\n inSingleLineComment = true;\n i++;\n continue;\n }\n\n // Check for string starts\n if (ch === \"'\") inSingleQuote = true;\n if (ch === '\"') inDoubleQuote = true;\n if (ch === \"`\") inTemplate = true;\n\n result += ch;\n i++;\n }\n\n return result;\n}\n\n/** Detect if a line index falls within a function body */\nexport function getFunctionScope(content: string, lineIndex: number): string | null {\n const lines = content.split(\"\\n\");\n // Walk backwards from the match line to find enclosing function\n for (let i = lineIndex; i >= 0; i--) {\n const line = lines[i];\n const funcMatch = line.match(/(?:function\\s+(\\w+)|(?:const|let|var)\\s+(\\w+)\\s*=\\s*(?:async\\s+)?(?:function|\\([^)]*\\)\\s*=>)|(\\w+)\\s*\\([^)]*\\)\\s*\\{)/);\n if (funcMatch) {\n return funcMatch[1] || funcMatch[2] || funcMatch[3] || \"anonymous\";\n }\n }\n return null;\n}\n\n/** Check if content between two positions has a validation/sanitization pattern */\nexport function hasValidationBetween(content: string, startLine: number, endLine: number): boolean {\n const lines = content.split(\"\\n\");\n const segment = lines.slice(startLine, endLine + 1).join(\"\\n\");\n return /(?:validate|sanitize|escape|check|verify|assert|ensure|guard|protect|filter|whitelist|allowlist|isValid|isAllowed|isSafe)/i.test(segment);\n}\n\n/** Detect data flow: does user input reach a dangerous sink? */\nexport function tracesUserInput(content: string, sinkLine: number, linesBack: number = 15): boolean {\n const lines = content.split(\"\\n\");\n const start = Math.max(0, sinkLine - linesBack);\n const segment = lines.slice(start, sinkLine + 1).join(\"\\n\");\n\n const userInputSources = /(?:req\\.(?:body|query|params|headers)|body\\.|input\\.|params\\.|args\\.|request\\.|formData|searchParams|useSearchParams|URLSearchParams)/i;\n return userInputSources.test(segment);\n}\n\n/** Extract all import/require statements to understand dependencies */\nexport function extractImports(content: string): string[] {\n const imports: string[] = [];\n const patterns = [\n /import\\s+.*?from\\s+[\"'`]([^\"'`]+)[\"'`]/g,\n /require\\s*\\(\\s*[\"'`]([^\"'`]+)[\"'`]\\s*\\)/g,\n ];\n for (const p of patterns) {\n let m;\n while ((m = p.exec(content)) !== null) {\n imports.push(m[1]);\n }\n }\n return imports;\n}\n\n/** Check if a file imports/uses a specific security middleware */\nexport function hasSecurityMiddleware(content: string): { auth: boolean; rateLimit: boolean; helmet: boolean; cors: boolean; csrf: boolean } {\n return {\n auth: /(?:requireAuth|isAuthenticated|authenticate|passport|jwt\\.verify|clerk|auth0|nextauth|session\\.user|getServerSession|getAuth|withAuth|authMiddleware)/i.test(content),\n rateLimit: /(?:rateLimit|rate-limit|express-rate-limit|throttle|slowDown|limiter)/i.test(content),\n helmet: /(?:helmet|security-headers|securityHeaders)/i.test(content),\n cors: /(?:cors\\(|corsMiddleware|allowedOrigins)/i.test(content),\n csrf: /(?:csrf|csurf|csrfToken|_csrf)/i.test(content),\n };\n}\n\n// ────────────────────────────────────────────\n// AST-Enhanced Rule Runner\n// ────────────────────────────────────────────\n\nexport interface ASTContext {\n strippedContent: string; // Code with comments removed\n imports: string[]; // All imports/requires\n middleware: ReturnType<typeof hasSecurityMiddleware>;\n isTestFile: boolean;\n isConfigFile: boolean;\n isScannerFile: boolean; // Self-detection: is this a security scanner?\n}\n\nexport function buildASTContext(content: string, filePath: string): ASTContext {\n return {\n strippedContent: stripComments(content),\n imports: extractImports(content),\n middleware: hasSecurityMiddleware(content),\n isTestFile: /(?:\\.test\\.|\\.spec\\.|__tests__|__mocks__|\\.stories\\.|fixtures?\\/|mocks?\\/|\\.cy\\.|\\.e2e\\.)/i.test(filePath),\n isConfigFile: /(?:\\.config\\.|config\\/|\\.rc$|tsconfig|next\\.config|vite\\.config|webpack\\.config)/i.test(filePath),\n isScannerFile: /(?:custom-rules|scanner|security-check|eslint-plugin|lint)/i.test(filePath) &&\n /(?:findMatches|check\\s*\\(content|rule\\.check|severity|category)/i.test(content),\n };\n}\n","/**\n * OSV.dev client — queries the public Open Source Vulnerabilities database\n * for known CVEs affecting a list of dependencies. Augments the hand-curated\n * allowlist in dependency-scanner.ts with live data so we catch the long tail\n * of vulnerable packages we haven't manually added.\n *\n * API: https://google.github.io/osv.dev/post-v1-querybatch/\n * Batch endpoint accepts up to 1000 package queries per request so a typical\n * project's full dependency list resolves in one HTTP round trip.\n */\n\ninterface OsvQuery {\n package: {\n name: string;\n ecosystem: \"npm\" | \"PyPI\" | \"RubyGems\" | \"Go\" | \"crates.io\" | \"Maven\" | \"Packagist\" | \"NuGet\";\n };\n version: string;\n}\n\ninterface OsvVulnSummary {\n id: string; // e.g. GHSA-xxxx-xxxx-xxxx or CVE-2023-12345\n summary?: string;\n details?: string;\n aliases?: string[];\n severity?: Array<{ type: string; score: string }>;\n}\n\ninterface OsvBatchResponse {\n results: Array<{\n vulns?: Array<{ id: string }>;\n }>;\n}\n\nexport interface OsvMatch {\n ecosystem: string;\n name: string;\n version: string;\n vulns: OsvVulnSummary[];\n}\n\nconst OSV_BATCH_URL = \"https://api.osv.dev/v1/querybatch\";\nconst OSV_VULN_URL = \"https://api.osv.dev/v1/vulns\";\nconst NETWORK_TIMEOUT_MS = 5000;\n\nasync function fetchWithTimeout(url: string, init: RequestInit, timeoutMs: number): Promise<Response> {\n const controller = new AbortController();\n const id = setTimeout(() => controller.abort(), timeoutMs);\n try {\n return await fetch(url, { ...init, signal: controller.signal });\n } finally {\n clearTimeout(id);\n }\n}\n\n/**\n * Query OSV.dev for every given (ecosystem, name, version) tuple and return\n * any vulnerabilities found. Fails soft on network error / timeout so offline\n * scans still work — the caller just gets an empty array and continues.\n *\n * Rate limit aware: the batch endpoint lets us send the whole dependency\n * list in one request, then we only fetch details for packages that\n * actually matched (avoiding the N+1 lookup pattern).\n */\nexport async function queryOsvBatch(queries: OsvQuery[]): Promise<OsvMatch[]> {\n if (queries.length === 0) return [];\n try {\n // Step 1: batch query returns only vuln IDs per package\n const batchRes = await fetchWithTimeout(\n OSV_BATCH_URL,\n {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ queries }),\n },\n NETWORK_TIMEOUT_MS,\n );\n if (!batchRes.ok) return [];\n const batch = (await batchRes.json()) as OsvBatchResponse;\n if (!Array.isArray(batch.results)) return [];\n\n // Collect unique vuln IDs across all matched packages so we only fetch\n // each one once (many packages can share a CVE)\n const uniqueVulnIds = new Set<string>();\n for (const result of batch.results) {\n for (const v of result.vulns ?? []) uniqueVulnIds.add(v.id);\n }\n\n // Step 2: fetch full details for each unique vuln (sequential with\n // per-request timeout — parallelizing would hammer their rate limiter)\n const vulnDetails = new Map<string, OsvVulnSummary>();\n for (const vulnId of uniqueVulnIds) {\n try {\n const detailRes = await fetchWithTimeout(\n `${OSV_VULN_URL}/${encodeURIComponent(vulnId)}`,\n { method: \"GET\" },\n NETWORK_TIMEOUT_MS,\n );\n if (!detailRes.ok) continue;\n const detail = (await detailRes.json()) as OsvVulnSummary;\n vulnDetails.set(vulnId, detail);\n } catch {\n // Skip individual vuln detail failures\n }\n }\n\n // Step 3: zip results back into the caller's query order\n const matches: OsvMatch[] = [];\n batch.results.forEach((result, i) => {\n const ids = (result.vulns ?? []).map((v) => v.id);\n if (ids.length === 0) return;\n const q = queries[i];\n matches.push({\n ecosystem: q.package.ecosystem,\n name: q.package.name,\n version: q.version,\n vulns: ids.map((id) => vulnDetails.get(id) ?? { id }),\n });\n });\n return matches;\n } catch {\n // Network error, timeout, DNS failure — fail soft so scans still work\n // offline. The manual allowlist in dependency-scanner.ts is the fallback.\n return [];\n }\n}\n\n/**\n * Classify an OSV vuln summary into a Finding severity. OSV ships several\n * severity vectors (CVSS v3, CVSS v4, ecosystem-specific) and most entries\n * have at least one. Falls back to 'medium' when unclassified.\n */\nexport function osvSeverity(vuln: OsvVulnSummary): \"critical\" | \"high\" | \"medium\" | \"low\" {\n const vectors = vuln.severity ?? [];\n for (const v of vectors) {\n // CVSS scores are embedded in a vector string — easiest to yank the first\n // numeric \"base score\" value out of it\n const match = v.score.match(/\\b(\\d+(?:\\.\\d+)?)\\b/);\n if (!match) continue;\n const score = parseFloat(match[1]);\n if (Number.isFinite(score)) {\n if (score >= 9) return \"critical\";\n if (score >= 7) return \"high\";\n if (score >= 4) return \"medium\";\n return \"low\";\n }\n }\n return \"medium\";\n}\n","/**\n * Dependency Vulnerability Scanner\n * Parses package.json, requirements.txt, Gemfile, etc. and checks\n * for known vulnerable packages and risky dependency patterns.\n */\n\nimport type { Finding } from \"../types.js\";\nimport { queryOsvBatch, osvSeverity } from \"./osv.js\";\n\n// Known vulnerable packages and their details\n// Format: package -> { minSafeVersion, cve, severity, description }\ninterface VulnEntry {\n minSafe: string;\n cve: string;\n severity: \"critical\" | \"high\" | \"medium\" | \"low\";\n title: string;\n description: string;\n fix: string;\n}\n\n// Well-known vulnerable npm packages (manually curated, high-signal)\nconst KNOWN_VULN_NPM: Record<string, VulnEntry[]> = {\n \"lodash\": [{\n minSafe: \"4.17.21\", cve: \"CVE-2021-23337\", severity: \"critical\",\n title: \"Lodash Prototype Pollution\",\n description: \"lodash before 4.17.21 is vulnerable to prototype pollution via set, setWith, and zipObjectDeep.\",\n fix: \"Upgrade lodash to >= 4.17.21: npm install lodash@latest\"\n }],\n \"axios\": [{\n minSafe: \"1.6.0\", cve: \"CVE-2023-45857\", severity: \"high\",\n title: \"Axios CSRF Vulnerability\",\n description: \"axios before 1.6.0 inadvertently leaks XSRF-TOKEN cookie in cross-site requests.\",\n fix: \"Upgrade axios to >= 1.6.0: npm install axios@latest\"\n }],\n \"express\": [{\n minSafe: \"4.19.2\", cve: \"CVE-2024-29041\", severity: \"medium\",\n title: \"Express Open Redirect\",\n description: \"Express before 4.19.2 is vulnerable to open redirect via crafted URLs.\",\n fix: \"Upgrade express to >= 4.19.2: npm install express@latest\"\n }],\n \"jsonwebtoken\": [{\n minSafe: \"9.0.0\", cve: \"CVE-2022-23529\", severity: \"high\",\n title: \"jsonwebtoken Insecure Key Handling\",\n description: \"jsonwebtoken before 9.0.0 allows attackers to set secretOrPublicKey to a malicious object.\",\n fix: \"Upgrade jsonwebtoken to >= 9.0.0: npm install jsonwebtoken@latest\"\n }],\n \"node-fetch\": [{\n minSafe: \"2.6.7\", cve: \"CVE-2022-0235\", severity: \"high\",\n title: \"node-fetch Header Exposure\",\n description: \"node-fetch before 2.6.7 exposes authorization headers on redirect to different origin.\",\n fix: \"Upgrade node-fetch to >= 2.6.7 or switch to native fetch (Node 18+).\"\n }],\n \"minimatch\": [{\n minSafe: \"3.0.5\", cve: \"CVE-2022-3517\", severity: \"high\",\n title: \"Minimatch ReDoS\",\n description: \"minimatch before 3.0.5 is vulnerable to Regular Expression Denial of Service.\",\n fix: \"Upgrade minimatch to >= 3.0.5: npm install minimatch@latest\"\n }],\n \"moment\": [{\n minSafe: \"999.0.0\", cve: \"N/A\", severity: \"medium\",\n title: \"Moment.js is Deprecated\",\n description: \"moment.js is in maintenance mode with known path traversal issues. Consider alternatives.\",\n fix: \"Migrate to date-fns, dayjs, or Temporal API. See: https://momentjs.com/docs/#/-project-status/\"\n }],\n \"request\": [{\n minSafe: \"999.0.0\", cve: \"N/A\", severity: \"medium\",\n title: \"Request Package Deprecated\",\n description: \"The 'request' package is deprecated and no longer receives security updates.\",\n fix: \"Migrate to node-fetch, axios, got, or native fetch (Node 18+).\"\n }],\n \"underscore\": [{\n minSafe: \"1.13.6\", cve: \"CVE-2021-23358\", severity: \"high\",\n title: \"Underscore.js Arbitrary Code Execution\",\n description: \"underscore before 1.13.6 allows arbitrary code execution via the template function.\",\n fix: \"Upgrade underscore to >= 1.13.6 or migrate to lodash.\"\n }],\n \"tar\": [{\n minSafe: \"6.2.1\", cve: \"CVE-2024-28863\", severity: \"high\",\n title: \"Tar Path Traversal\",\n description: \"tar before 6.2.1 is vulnerable to denial of service via crafted archives.\",\n fix: \"Upgrade tar to >= 6.2.1: npm install tar@latest\"\n }],\n \"semver\": [{\n minSafe: \"7.5.2\", cve: \"CVE-2022-25883\", severity: \"medium\",\n title: \"Semver ReDoS\",\n description: \"semver before 7.5.2 is vulnerable to Regular Expression Denial of Service.\",\n fix: \"Upgrade semver to >= 7.5.2: npm install semver@latest\"\n }],\n \"xml2js\": [{\n minSafe: \"0.5.0\", cve: \"CVE-2023-0842\", severity: \"medium\",\n title: \"xml2js Prototype Pollution\",\n description: \"xml2js before 0.5.0 is vulnerable to prototype pollution when parsing XML.\",\n fix: \"Upgrade xml2js to >= 0.5.0: npm install xml2js@latest\"\n }],\n};\n\n// Known vulnerable Python packages\nconst KNOWN_VULN_PYTHON: Record<string, VulnEntry[]> = {\n \"django\": [{\n minSafe: \"4.2.11\", cve: \"CVE-2024-27351\", severity: \"high\",\n title: \"Django ReDoS in Truncator\",\n description: \"Django before 4.2.11 is vulnerable to Regular Expression Denial of Service.\",\n fix: \"Upgrade Django to >= 4.2.11: pip install Django --upgrade\"\n }],\n \"flask\": [{\n minSafe: \"2.3.2\", cve: \"CVE-2023-30861\", severity: \"high\",\n title: \"Flask Session Cookie Vulnerability\",\n description: \"Flask before 2.3.2 may set permanent session cookies on redirect responses.\",\n fix: \"Upgrade Flask to >= 2.3.2: pip install Flask --upgrade\"\n }],\n \"pillow\": [{\n minSafe: \"10.2.0\", cve: \"CVE-2023-50447\", severity: \"critical\",\n title: \"Pillow Arbitrary Code Execution\",\n description: \"Pillow before 10.2.0 is vulnerable to arbitrary code execution via crafted images.\",\n fix: \"Upgrade Pillow to >= 10.2.0: pip install Pillow --upgrade\"\n }],\n \"requests\": [{\n minSafe: \"2.31.0\", cve: \"CVE-2023-32681\", severity: \"medium\",\n title: \"Requests Proxy Header Leak\",\n description: \"requests before 2.31.0 leaks Proxy-Authorization headers to destination servers.\",\n fix: \"Upgrade requests to >= 2.31.0: pip install requests --upgrade\"\n }],\n \"pyyaml\": [{\n minSafe: \"6.0.1\", cve: \"CVE-2022-41316\", severity: \"high\",\n title: \"PyYAML Unsafe Loading\",\n description: \"PyYAML has known deserialization vulnerabilities. Always use yaml.safe_load().\",\n fix: \"Upgrade PyYAML to >= 6.0.1 and use yaml.safe_load() instead of yaml.load().\"\n }],\n \"jinja2\": [{\n minSafe: \"3.1.3\", cve: \"CVE-2024-22195\", severity: \"medium\",\n title: \"Jinja2 XSS Vulnerability\",\n description: \"Jinja2 before 3.1.3 is vulnerable to XSS via xmlattr filter.\",\n fix: \"Upgrade Jinja2 to >= 3.1.3: pip install Jinja2 --upgrade\"\n }],\n};\n\n// Risky patterns in dependency configs\ninterface DepPattern {\n id: string;\n title: string;\n severity: \"critical\" | \"high\" | \"medium\" | \"low\";\n description: string;\n fix: string;\n test: (content: string, filePath: string) => boolean;\n}\n\nconst DEP_PATTERNS: DepPattern[] = [\n {\n id: \"DEP001\", title: \"Wildcard Dependency Version\", severity: \"high\",\n description: \"Using '*' or 'latest' as a dependency version allows any version to be installed, including malicious ones.\",\n fix: \"Pin dependency versions: use exact (1.2.3) or caret (^1.2.3) versions, never '*' or 'latest'.\",\n test: (content, filePath) => filePath.endsWith(\"package.json\") && /[\"']\\*[\"']|[\"']latest[\"']/.test(content),\n },\n {\n id: \"DEP002\", title: \"Git Dependency Without Commit Hash\", severity: \"medium\",\n description: \"Git dependencies without a pinned commit hash can be replaced with malicious code.\",\n fix: \"Pin git dependencies to a specific commit: git+https://github.com/user/repo#commit-hash\",\n test: (content, filePath) => filePath.endsWith(\"package.json\") && /git\\+https?:\\/\\/[^#\"]+[\"']/.test(content),\n },\n {\n id: \"DEP003\", title: \"Postinstall Script in Package\", severity: \"medium\",\n description: \"postinstall scripts run automatically after npm install and can execute arbitrary code.\",\n fix: \"Audit postinstall scripts carefully. Use --ignore-scripts flag for untrusted packages.\",\n test: (content, filePath) => filePath.endsWith(\"package.json\") && /\"postinstall\"\\s*:/.test(content),\n },\n {\n id: \"DEP004\", title: \"No Package Lock File\", severity: \"medium\",\n description: \"Without a lockfile, npm install may resolve different versions on different machines.\",\n fix: \"Commit your lockfile (package-lock.json, pnpm-lock.yaml, or yarn.lock).\",\n test: (content, filePath) => filePath.endsWith(\".gitignore\") && /package-lock\\.json|pnpm-lock\\.yaml|yarn\\.lock/.test(content),\n },\n {\n id: \"DEP005\", title: \"HTTP Registry URL\", severity: \"high\",\n description: \"Using HTTP (not HTTPS) for npm registry allows man-in-the-middle attacks on package downloads.\",\n fix: \"Use HTTPS for registry: registry=https://registry.npmjs.org/\",\n test: (content, filePath) => filePath.endsWith(\".npmrc\") && /registry\\s*=\\s*http:\\/\\//.test(content),\n },\n];\n\nfunction compareVersions(v1: string, v2: string): number {\n const parts1 = v1.replace(/^[^0-9]*/, \"\").split(\".\").map(Number);\n const parts2 = v2.replace(/^[^0-9]*/, \"\").split(\".\").map(Number);\n for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {\n const a = parts1[i] || 0;\n const b = parts2[i] || 0;\n if (a < b) return -1;\n if (a > b) return 1;\n }\n return 0;\n}\n\nfunction getSnippet(content: string, line: number): string {\n const lines = content.split(\"\\n\");\n const start = Math.max(0, line - 2);\n const end = Math.min(lines.length, line + 2);\n return lines.slice(start, end).map((l, i) => {\n const lineNum = start + i + 1;\n const prefix = lineNum === line ? \">\" : \" \";\n return `${prefix} ${String(lineNum).padStart(5)} | ${l}`;\n }).join(\"\\n\");\n}\n\nexport function scanDependencies(files: { path: string; content: string }[]): Finding[] {\n const findings: Finding[] = [];\n\n for (const { path: filePath, content } of files) {\n // npm package.json\n if (filePath.endsWith(\"package.json\") && !filePath.includes(\"node_modules\")) {\n try {\n const pkg = JSON.parse(content);\n const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };\n\n for (const [name, version] of Object.entries(allDeps)) {\n const vulns = KNOWN_VULN_NPM[name];\n if (!vulns) continue;\n\n const versionStr = String(version).replace(/^[\\^~>=<]*/g, \"\");\n for (const vuln of vulns) {\n if (compareVersions(versionStr, vuln.minSafe) < 0) {\n const line = content.split(\"\\n\").findIndex(l => l.includes(`\"${name}\"`)) + 1;\n findings.push({\n id: `${vuln.cve}-${filePath}:${line}`,\n rule: vuln.cve,\n severity: vuln.severity,\n title: vuln.title,\n description: vuln.description,\n file: filePath,\n line,\n snippet: getSnippet(content, line),\n fix: vuln.fix,\n category: \"Dependencies\",\n source: \"custom\",\n owasp: \"A06:2021\",\n cwe: \"CWE-1395\",\n });\n }\n }\n }\n } catch {\n // Invalid JSON — skip\n }\n }\n\n // Python requirements.txt\n if (filePath.match(/requirements.*\\.txt$/i)) {\n const lines = content.split(\"\\n\");\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i].trim();\n const match = line.match(/^([a-zA-Z0-9_-]+)(?:[=<>!~]+(.+))?/);\n if (!match) continue;\n\n const name = match[1].toLowerCase();\n const version = match[2] || \"0.0.0\";\n const vulns = KNOWN_VULN_PYTHON[name];\n if (!vulns) continue;\n\n for (const vuln of vulns) {\n if (compareVersions(version, vuln.minSafe) < 0) {\n findings.push({\n id: `${vuln.cve}-${filePath}:${i + 1}`,\n rule: vuln.cve,\n severity: vuln.severity,\n title: vuln.title,\n description: vuln.description,\n file: filePath,\n line: i + 1,\n snippet: getSnippet(content, i + 1),\n fix: vuln.fix,\n category: \"Dependencies\",\n source: \"custom\",\n owasp: \"A06:2021\",\n cwe: \"CWE-1395\",\n });\n }\n }\n }\n }\n\n // Dependency pattern checks\n for (const pattern of DEP_PATTERNS) {\n if (pattern.test(content, filePath)) {\n findings.push({\n id: `${pattern.id}-${filePath}:1`,\n rule: pattern.id,\n severity: pattern.severity,\n title: pattern.title,\n description: pattern.description,\n file: filePath,\n line: 1,\n snippet: getSnippet(content, 1),\n fix: pattern.fix,\n category: \"Dependencies\",\n source: \"custom\",\n owasp: \"A06:2021\",\n cwe: \"CWE-1395\",\n });\n }\n }\n }\n\n return findings;\n}\n\n/**\n * Async companion to scanDependencies() that augments the hand-curated\n * allowlist with a live OSV.dev lookup of every package version. Fails\n * soft on network error — offline scans still run the static checks.\n *\n * Returns ONLY the extra findings from OSV so callers can merge with the\n * sync results without double-counting:\n * const findings = [...scanDependencies(files), ...await scanDependenciesOsv(files)];\n *\n * Deduplication is by (package name + CVE id) so a package already caught\n * by the static allowlist doesn't show up twice.\n */\nexport async function scanDependenciesOsv(\n files: { path: string; content: string }[],\n alreadyFoundByRule: Set<string>,\n): Promise<Finding[]> {\n interface PendingLookup {\n ecosystem: \"npm\" | \"PyPI\" | \"RubyGems\" | \"Go\";\n name: string;\n version: string;\n file: string;\n line: number;\n content: string;\n }\n\n const lookups: PendingLookup[] = [];\n\n for (const { path: filePath, content } of files) {\n // npm package.json\n if (filePath.endsWith(\"package.json\") && !filePath.includes(\"node_modules\")) {\n try {\n const pkg = JSON.parse(content);\n const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };\n for (const [name, rawVersion] of Object.entries(allDeps)) {\n const version = String(rawVersion).replace(/^[\\^~>=<]*/g, \"\").trim();\n if (!version || version === \"*\" || version === \"latest\") continue;\n const line = content.split(\"\\n\").findIndex((l) => l.includes(`\"${name}\"`)) + 1;\n lookups.push({ ecosystem: \"npm\", name, version, file: filePath, line: line || 1, content });\n }\n } catch {\n /* invalid JSON */\n }\n }\n\n // Python requirements.txt (exact version pins only — ranges are tricky\n // without a resolver and we'd rather report nothing than guess wrong)\n if (filePath.match(/requirements.*\\.txt$/i)) {\n const lines = content.split(\"\\n\");\n for (let i = 0; i < lines.length; i++) {\n const m = lines[i].trim().match(/^([a-zA-Z0-9_-]+)==([\\d.]+)/);\n if (!m) continue;\n lookups.push({\n ecosystem: \"PyPI\",\n name: m[1].toLowerCase(),\n version: m[2],\n file: filePath,\n line: i + 1,\n content,\n });\n }\n }\n\n // Ruby Gemfile.lock (exact pins)\n if (filePath.endsWith(\"Gemfile.lock\")) {\n const lines = content.split(\"\\n\");\n for (let i = 0; i < lines.length; i++) {\n const m = lines[i].match(/^\\s{4}([a-z0-9_-]+)\\s+\\(([\\d.]+)\\)/);\n if (!m) continue;\n lookups.push({\n ecosystem: \"RubyGems\",\n name: m[1],\n version: m[2],\n file: filePath,\n line: i + 1,\n content,\n });\n }\n }\n }\n\n if (lookups.length === 0) return [];\n\n // Chunk into batches of 500 to stay well under OSV's 1000-query limit\n const CHUNK = 500;\n const findings: Finding[] = [];\n for (let start = 0; start < lookups.length; start += CHUNK) {\n const chunk = lookups.slice(start, start + CHUNK);\n const matches = await queryOsvBatch(\n chunk.map((l) => ({ package: { name: l.name, ecosystem: l.ecosystem }, version: l.version })),\n );\n // Zip matches back to their lookup rows by index within the chunk\n const matchByKey = new Map<string, (typeof matches)[0]>();\n for (const m of matches) matchByKey.set(`${m.ecosystem}:${m.name}:${m.version}`, m);\n\n for (const lookup of chunk) {\n const key = `${lookup.ecosystem}:${lookup.name}:${lookup.version}`;\n const match = matchByKey.get(key);\n if (!match) continue;\n for (const vuln of match.vulns) {\n // Skip if the static allowlist already reported this CVE for this package\n const dedupeKey = `${lookup.name}:${vuln.id}`;\n if (alreadyFoundByRule.has(dedupeKey)) continue;\n alreadyFoundByRule.add(dedupeKey);\n\n const severity = osvSeverity(vuln);\n const title = vuln.summary || `${lookup.name} vulnerability ${vuln.id}`;\n const description =\n vuln.details ||\n vuln.summary ||\n `${lookup.name}@${lookup.version} is affected by ${vuln.id}. See https://osv.dev/vulnerability/${vuln.id} for details.`;\n findings.push({\n id: `${vuln.id}-${lookup.file}:${lookup.line}`,\n rule: vuln.id,\n severity,\n title,\n description: description.slice(0, 500),\n file: lookup.file,\n line: lookup.line,\n snippet: getSnippet(lookup.content, lookup.line),\n fix: `Upgrade ${lookup.name} to a version that resolves ${vuln.id}. See https://osv.dev/vulnerability/${vuln.id}`,\n category: \"Dependencies\",\n source: \"custom\",\n owasp: \"A06:2021\",\n cwe: \"CWE-1395\",\n });\n }\n }\n }\n\n return findings;\n}\n","/**\n * Secret Entropy Detection Scanner\n * Detects high-entropy strings that are likely secrets/keys/tokens\n * regardless of their naming pattern. Uses Shannon entropy calculation.\n */\n\nimport type { Finding } from \"../types.js\";\n\n// ────────────────────────────────────────────\n// Shannon Entropy Calculation\n// ────────────────────────────────────────────\n\nfunction shannonEntropy(str: string): number {\n const freq: Record<string, number> = {};\n for (const ch of str) {\n freq[ch] = (freq[ch] || 0) + 1;\n }\n const len = str.length;\n let entropy = 0;\n for (const count of Object.values(freq)) {\n const p = count / len;\n entropy -= p * Math.log2(p);\n }\n return entropy;\n}\n\n// ────────────────────────────────────────────\n// Known Safe Patterns (allowlist)\n// ────────────────────────────────────────────\n\nconst SAFE_PATTERNS = [\n // UUIDs\n /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,\n // Git commit hashes\n /^[0-9a-f]{40}$/i,\n // Short git hashes\n /^[0-9a-f]{7,8}$/i,\n // Hex colors\n /^#?[0-9a-fA-F]{3,8}$/,\n // Base64 encoded small data (< 20 chars)\n /^[A-Za-z0-9+/]{1,19}={0,2}$/,\n // Package versions\n /^\\d+\\.\\d+\\.\\d+/,\n // File hashes (integrity)\n /^sha\\d+-/i,\n // URLs without credentials\n /^https?:\\/\\/[^:@]*$/,\n // Date/time strings\n /^\\d{4}-\\d{2}-\\d{2}/,\n // Locale strings\n /^[a-z]{2}-[A-Z]{2}$/,\n // Common encodings\n /^utf-?8|ascii|latin|iso-8859/i,\n // MIME types\n /^(?:application|text|image|audio|video)\\//,\n // CSS/HTML values\n /^(?:inherit|none|auto|block|flex|grid|absolute|relative|fixed|px|em|rem|%)/,\n // Common placeholder/test values\n /^(?:test|example|sample|demo|placeholder|temp|tmp|foo|bar|baz|lorem|ipsum)/i,\n // DOCTYPE/DTD URLs\n /DTD|DOCTYPE|w3\\.org|apple\\.com\\/DTDs/i,\n // XML namespaces\n /xmlns|schema|xsd|xsi/i,\n // npm/package registry URLs\n /^https?:\\/\\/registry\\.npmjs\\.org\\//,\n /^https?:\\/\\/registry\\.yarnpkg\\.com\\//,\n // Package integrity hashes (sha512-..., sha256-...)\n /^sha\\d+-[A-Za-z0-9+/=]+$/,\n // Resolved package URLs (.tgz)\n /\\.tgz$/,\n // npm resolved URLs (any registry URL with package tarball)\n /registry.*\\/-\\/.*\\.tgz$/,\n];\n\n// File types that shouldn't be scanned for entropy\nconst SKIP_FILES = /\\.(css|scss|less|svg|md|txt|html?|xml|yml|yaml|toml|lock|map|woff2?|ttf|eot|ico|png|jpg|gif|webp)$/i;\n\n// File names that should be completely excluded from entropy scanning\nconst SKIP_FILENAMES = /(?:package-lock\\.json|pnpm-lock\\.yaml|yarn\\.lock|composer\\.lock|Gemfile\\.lock|Cargo\\.lock|poetry\\.lock|Pipfile\\.lock|shrinkwrap\\.json)$/i;\n\n// Variable names that commonly hold non-secret long strings\nconst SAFE_VAR_NAMES = /(?:description|message|text|label|title|content|template|html|svg|css|style|class|query|mutation|schema|regex|pattern|format|placeholder|comment|url|path|route|endpoint|href|src|alt|name|type|version|encoding|charset)/i;\n\n// ────────────────────────────────────────────\n// Scanner\n// ────────────────────────────────────────────\n\ninterface EntropyMatch {\n value: string;\n line: number;\n entropy: number;\n context: string;\n}\n\nfunction getSnippet(content: string, line: number): string {\n const lines = content.split(\"\\n\");\n const start = Math.max(0, line - 2);\n const end = Math.min(lines.length, line + 2);\n return lines.slice(start, end).map((l, i) => {\n const lineNum = start + i + 1;\n const prefix = lineNum === line ? \">\" : \" \";\n return `${prefix} ${String(lineNum).padStart(5)} | ${l}`;\n }).join(\"\\n\");\n}\n\nexport function scanEntropy(files: { path: string; content: string }[]): Finding[] {\n const findings: Finding[] = [];\n\n for (const { path: filePath, content } of files) {\n // Skip non-source files\n if (SKIP_FILES.test(filePath)) continue;\n if (SKIP_FILENAMES.test(filePath)) continue;\n // Also check the basename in case the path has a prefix (e.g., zip extraction)\n const basename = filePath.split(\"/\").pop() || \"\";\n if (SKIP_FILENAMES.test(basename)) continue;\n if (filePath.includes(\"node_modules\")) continue;\n if (filePath.includes(\".min.\")) continue;\n // Skip compiled bundles and generated files\n if (/pro-rules-bundle|\\.bundle\\.|\\.chunk\\./i.test(filePath)) continue;\n if (/(?:\\.test\\.|\\.spec\\.|__tests__|__mocks__|fixtures?\\/)/i.test(filePath)) continue;\n\n const lines = content.split(\"\\n\");\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n\n // Skip comment lines\n const trimmed = line.trimStart();\n if (trimmed.startsWith(\"//\") || trimmed.startsWith(\"#\") || trimmed.startsWith(\"*\") || trimmed.startsWith(\"/*\")) continue;\n\n // Find string assignments (quotes or backticks)\n const stringPattern = /(?:[:=]\\s*)([\"'`])([^\"'`\\n]{20,120})\\1/g;\n let match;\n\n while ((match = stringPattern.exec(line)) !== null) {\n const value = match[2];\n\n // Skip known safe patterns\n if (SAFE_PATTERNS.some(p => p.test(value))) continue;\n\n // Skip if the variable name suggests non-secret content\n const beforeAssign = line.substring(0, match.index);\n if (SAFE_VAR_NAMES.test(beforeAssign)) continue;\n\n // Skip if it looks like a URL without credentials\n if (/^https?:\\/\\/[^:@]*$/.test(value)) continue;\n\n // Skip if it contains spaces (likely a sentence/message)\n if ((value.match(/\\s/g) || []).length > 2) continue;\n\n // Calculate entropy\n const entropy = shannonEntropy(value);\n\n // High entropy thresholds:\n // - Hex strings (charset 16): entropy > 3.0\n // - Base64 strings (charset 64): entropy > 4.5\n // - General strings: entropy > 4.0\n const isHex = /^[0-9a-fA-F]+$/.test(value);\n const isBase64 = /^[A-Za-z0-9+/]+=*$/.test(value);\n\n let threshold = 4.0;\n if (isHex) threshold = 3.0;\n else if (isBase64) threshold = 4.5;\n\n // Minimum length for detection\n if (value.length < 20) continue;\n\n if (entropy >= threshold) {\n // Additional context checks to reduce false positives\n const varName = beforeAssign.match(/(\\w+)\\s*[:=]\\s*$/)?.[1] || \"\";\n\n // Boost confidence if variable name suggests secret\n const isLikelySecret = /(?:key|secret|token|password|passwd|pwd|api_?key|auth|credential|private|signing)/i.test(varName);\n\n // Only report if entropy is very high OR variable name suggests secret\n if (entropy >= 4.5 || isLikelySecret) {\n // Mask the value for display\n const masked = value.substring(0, 6) + \"...\" + value.substring(value.length - 4);\n\n findings.push({\n id: `ENTROPY-${filePath}:${i + 1}`,\n rule: \"ENTROPY\",\n severity: isLikelySecret ? \"critical\" : \"high\",\n title: \"High-Entropy String Detected (Possible Secret)\",\n description: `Found a high-entropy string (${entropy.toFixed(1)} bits) that may be a hardcoded secret or API key: \"${masked}\"`,\n file: filePath,\n line: i + 1,\n snippet: getSnippet(content, i + 1),\n fix: \"If this is a secret, move it to an environment variable. If it's not a secret (e.g., hash, encoded data), add it to .xploitscanignore.\",\n category: \"Secrets\",\n source: \"custom\",\n owasp: \"A02:2021\",\n cwe: \"CWE-798\",\n });\n }\n }\n }\n }\n }\n\n return findings;\n}\n","/**\n * Configuration File Deep Analyzer\n * Analyzes tsconfig, next.config, Dockerfile, GitHub Actions,\n * and other config files for security misconfigurations.\n */\n\nimport type { Finding } from \"../types.js\";\n\nfunction getSnippet(content: string, line: number): string {\n const lines = content.split(\"\\n\");\n const start = Math.max(0, line - 2);\n const end = Math.min(lines.length, line + 2);\n return lines.slice(start, end).map((l, i) => {\n const lineNum = start + i + 1;\n const prefix = lineNum === line ? \">\" : \" \";\n return `${prefix} ${String(lineNum).padStart(5)} | ${l}`;\n }).join(\"\\n\");\n}\n\ninterface ConfigCheck {\n id: string;\n title: string;\n severity: \"critical\" | \"high\" | \"medium\" | \"low\";\n category: string;\n description: string;\n fix: string;\n owasp: string;\n cwe: string;\n filePattern: RegExp;\n check: (content: string, filePath: string) => { line: number; snippet: string } | null;\n}\n\nconst CONFIG_CHECKS: ConfigCheck[] = [\n // ── TypeScript Config ──\n {\n id: \"CFG001\", title: \"TypeScript Strict Mode Disabled\", severity: \"medium\",\n category: \"Configuration\", description: \"TypeScript without strict mode misses type errors that can cause runtime bugs and security issues.\",\n fix: 'Enable strict mode in tsconfig.json: \"strict\": true',\n owasp: \"A05:2021\", cwe: \"CWE-710\",\n filePattern: /tsconfig\\.json$/,\n check(content) {\n try {\n if (/\"strict\"\\s*:\\s*false/.test(content)) {\n const line = content.split(\"\\n\").findIndex(l => /strict/.test(l)) + 1;\n return { line, snippet: getSnippet(content, line) };\n }\n if (!/\"strict\"\\s*:\\s*true/.test(content) && /\"compilerOptions\"/.test(content)) {\n return { line: 1, snippet: getSnippet(content, 1) };\n }\n } catch {}\n return null;\n },\n },\n {\n id: \"CFG002\", title: \"TypeScript allowJs Without checkJs\", severity: \"low\",\n category: \"Configuration\", description: \"allowJs without checkJs means JavaScript files bypass type checking entirely.\",\n fix: 'Add \"checkJs\": true alongside \"allowJs\": true in tsconfig.json.',\n owasp: \"A05:2021\", cwe: \"CWE-710\",\n filePattern: /tsconfig\\.json$/,\n check(content) {\n if (/\"allowJs\"\\s*:\\s*true/.test(content) && !/\"checkJs\"\\s*:\\s*true/.test(content)) {\n const line = content.split(\"\\n\").findIndex(l => /allowJs/.test(l)) + 1;\n return { line, snippet: getSnippet(content, line) };\n }\n return null;\n },\n },\n\n // ── Next.js Config ──\n {\n id: \"CFG003\", title: \"Next.js Missing Security Headers\", severity: \"medium\",\n category: \"Configuration\", description: \"Next.js app without security headers in next.config is missing important protections.\",\n fix: \"Add a headers() function to next.config.js with X-Frame-Options, X-Content-Type-Options, Referrer-Policy, and Content-Security-Policy.\",\n owasp: \"A05:2021\", cwe: \"CWE-693\",\n filePattern: /next\\.config\\.(js|mjs|ts)$/,\n check(content) {\n if (!/headers\\s*\\(/.test(content) && !/securityHeaders|security-headers/.test(content)) {\n return { line: 1, snippet: getSnippet(content, 1) };\n }\n return null;\n },\n },\n {\n id: \"CFG004\", title: \"Next.js Powered-By Header Not Disabled\", severity: \"low\",\n category: \"Configuration\", description: \"Next.js exposes X-Powered-By header by default, revealing your tech stack.\",\n fix: 'Add poweredByHeader: false to next.config.js.',\n owasp: \"A05:2021\", cwe: \"CWE-200\",\n filePattern: /next\\.config\\.(js|mjs|ts)$/,\n check(content) {\n if (!/poweredByHeader\\s*:\\s*false/.test(content)) {\n return { line: 1, snippet: getSnippet(content, 1) };\n }\n return null;\n },\n },\n\n // ── Dockerfile ──\n {\n id: \"CFG005\", title: \"Dockerfile Using Latest Tag\", severity: \"medium\",\n category: \"Configuration\", description: \"Using :latest tag in FROM makes builds non-reproducible and may pull vulnerable images.\",\n fix: \"Pin to a specific version: FROM node:20-alpine instead of FROM node:latest.\",\n owasp: \"A06:2021\", cwe: \"CWE-1395\",\n filePattern: /Dockerfile$/i,\n check(content) {\n const lines = content.split(\"\\n\");\n for (let i = 0; i < lines.length; i++) {\n if (/^FROM\\s+\\S+:latest/i.test(lines[i].trim()) || /^FROM\\s+\\w+\\s*$/i.test(lines[i].trim())) {\n return { line: i + 1, snippet: getSnippet(content, i + 1) };\n }\n }\n return null;\n },\n },\n {\n id: \"CFG006\", title: \"Dockerfile Not Using Multi-Stage Build\", severity: \"low\",\n category: \"Configuration\", description: \"Single-stage Docker builds include build tools in the final image, increasing attack surface.\",\n fix: \"Use multi-stage builds: first stage for building, second stage (FROM alpine) for running.\",\n owasp: \"A05:2021\", cwe: \"CWE-1059\",\n filePattern: /Dockerfile$/i,\n check(content) {\n const fromCount = (content.match(/^FROM\\s/gmi) || []).length;\n if (fromCount <= 1 && content.includes(\"npm\") && content.length > 200) {\n return { line: 1, snippet: getSnippet(content, 1) };\n }\n return null;\n },\n },\n {\n id: \"CFG007\", title: \"Dockerfile Copies .env File\", severity: \"critical\",\n category: \"Configuration\", description: \"COPY that includes .env files bakes secrets into the Docker image layer.\",\n fix: \"Add .env to .dockerignore. Use build args or runtime env vars instead.\",\n owasp: \"A02:2021\", cwe: \"CWE-312\",\n filePattern: /Dockerfile$/i,\n check(content) {\n const lines = content.split(\"\\n\");\n for (let i = 0; i < lines.length; i++) {\n if (/^COPY\\s.*\\.env\\b/i.test(lines[i].trim())) {\n return { line: i + 1, snippet: getSnippet(content, i + 1) };\n }\n }\n return null;\n },\n },\n\n // ── GitHub Actions ──\n {\n id: \"CFG008\", title: \"GitHub Actions with Broad Permissions\", severity: \"high\",\n category: \"Configuration\", description: \"GitHub Actions with write-all or broad permissions can be exploited if a dependency is compromised.\",\n fix: \"Use minimal permissions: permissions: { contents: read }. Only grant what's needed.\",\n owasp: \"A01:2021\", cwe: \"CWE-250\",\n filePattern: /\\.github\\/workflows\\/.*\\.(yml|yaml)$/,\n check(content) {\n const lines = content.split(\"\\n\");\n for (let i = 0; i < lines.length; i++) {\n if (/permissions\\s*:\\s*write-all/i.test(lines[i])) {\n return { line: i + 1, snippet: getSnippet(content, i + 1) };\n }\n }\n return null;\n },\n },\n {\n id: \"CFG009\", title: \"GitHub Actions Using Unpinned Action\", severity: \"medium\",\n category: \"Configuration\", description: \"Using actions with @main or @master instead of a pinned SHA allows supply chain attacks.\",\n fix: \"Pin actions to a specific SHA: uses: actions/checkout@abc123... instead of @main.\",\n owasp: \"A06:2021\", cwe: \"CWE-1395\",\n filePattern: /\\.github\\/workflows\\/.*\\.(yml|yaml)$/,\n check(content) {\n const lines = content.split(\"\\n\");\n for (let i = 0; i < lines.length; i++) {\n if (/uses:\\s*\\S+@(?:main|master|dev|latest)\\s*$/i.test(lines[i])) {\n return { line: i + 1, snippet: getSnippet(content, i + 1) };\n }\n }\n return null;\n },\n },\n\n // ── Vite Config ──\n {\n id: \"CFG010\", title: \"Vite Server Open to Network\", severity: \"medium\",\n category: \"Configuration\", description: \"Vite dev server with host: true exposes it to the entire network.\",\n fix: \"Remove host: true or use host: '127.0.0.1' for local-only access during development.\",\n owasp: \"A05:2021\", cwe: \"CWE-668\",\n filePattern: /vite\\.config\\.(js|ts|mjs)$/,\n check(content) {\n const lines = content.split(\"\\n\");\n for (let i = 0; i < lines.length; i++) {\n if (/host\\s*:\\s*true/.test(lines[i]) || /host\\s*:\\s*['\"]0\\.0\\.0\\.0['\"]/.test(lines[i])) {\n return { line: i + 1, snippet: getSnippet(content, i + 1) };\n }\n }\n return null;\n },\n },\n\n // ── .npmrc ──\n {\n id: \"CFG011\", title: \"NPM Auth Token in .npmrc\", severity: \"critical\",\n category: \"Secrets\", description: \"NPM auth tokens in committed .npmrc files expose registry credentials.\",\n fix: \"Remove the token and add .npmrc to .gitignore. Use NPM_TOKEN env var instead.\",\n owasp: \"A02:2021\", cwe: \"CWE-798\",\n filePattern: /\\.npmrc$/,\n check(content) {\n const lines = content.split(\"\\n\");\n for (let i = 0; i < lines.length; i++) {\n if (/_authToken|_auth=|\\/\\/registry.*:_password/.test(lines[i])) {\n return { line: i + 1, snippet: getSnippet(content, i + 1) };\n }\n }\n return null;\n },\n },\n\n // ── ESLint Config ──\n {\n id: \"CFG012\", title: \"Security ESLint Rules Disabled\", severity: \"medium\",\n category: \"Configuration\", description: \"Disabling security-related ESLint rules removes an important safety net.\",\n fix: \"Re-enable security rules or use eslint-plugin-security for automated checks.\",\n owasp: \"A05:2021\", cwe: \"CWE-710\",\n filePattern: /\\.eslint(rc)?(\\.(js|json|yml|yaml|cjs|mjs))?$/,\n check(content) {\n const lines = content.split(\"\\n\");\n for (let i = 0; i < lines.length; i++) {\n if (/no-eval|no-implied-eval|no-new-func|no-script-url|security\\/detect/i.test(lines[i]) && /[\"']off[\"']|:\\s*0/.test(lines[i])) {\n return { line: i + 1, snippet: getSnippet(content, i + 1) };\n }\n }\n return null;\n },\n },\n];\n\nexport function scanConfigs(files: { path: string; content: string }[]): Finding[] {\n const findings: Finding[] = [];\n\n for (const { path: filePath, content } of files) {\n for (const check of CONFIG_CHECKS) {\n if (!check.filePattern.test(filePath)) continue;\n\n const result = check.check(content, filePath);\n if (result) {\n findings.push({\n id: `${check.id}-${filePath}:${result.line}`,\n rule: check.id,\n severity: check.severity,\n title: check.title,\n description: check.description,\n file: filePath,\n line: result.line,\n snippet: result.snippet,\n fix: check.fix,\n category: check.category,\n source: \"custom\",\n owasp: check.owasp,\n cwe: check.cwe,\n });\n }\n }\n }\n\n return findings;\n}\n","/**\n * Multi-File Analysis Scanner\n * Analyzes relationships between files to detect cross-file vulnerabilities\n * that single-file scanners miss.\n */\n\nimport type { Finding } from \"../types.js\";\n\nfunction getSnippet(content: string, line: number): string {\n const lines = content.split(\"\\n\");\n const start = Math.max(0, line - 2);\n const end = Math.min(lines.length, line + 2);\n return lines.slice(start, end).map((l, i) => {\n const lineNum = start + i + 1;\n const prefix = lineNum === line ? \">\" : \" \";\n return `${prefix} ${String(lineNum).padStart(5)} | ${l}`;\n }).join(\"\\n\");\n}\n\ninterface FileInfo {\n path: string;\n content: string;\n}\n\nexport function scanMultiFile(files: FileInfo[]): Finding[] {\n const findings: Finding[] = [];\n\n // Build indexes\n const fileMap = new Map<string, string>();\n for (const f of files) {\n fileMap.set(f.path, f.content);\n }\n\n // ── Check 1: API routes without middleware protection ──\n // If there's a middleware file that handles auth, routes are protected.\n // But if routes exist WITHOUT middleware, they're exposed.\n const hasGlobalMiddleware = files.some(f =>\n /middleware\\.(ts|js)$/.test(f.path) &&\n /(?:auth|session|clerk|nextauth|getToken)/i.test(f.content)\n );\n const hasAuthModule = files.some(f =>\n /(?:auth|middleware)\\.(ts|js)$/i.test(f.path) &&\n /(?:requireAuth|isAuthenticated|authenticate|verify)/i.test(f.content)\n );\n\n // Check API route files for auth\n for (const f of files) {\n if (!/(?:\\/api\\/|routes?\\/|controllers?\\/)/i.test(f.path)) continue;\n if (/middleware/i.test(f.path)) continue;\n\n // Skip health/status endpoints\n if (/(?:health|status|ping|ready|live)\\.(?:ts|js)/i.test(f.path)) continue;\n\n const hasLocalAuth = /(?:requireAuth|isAuthenticated|authenticate|verify|getAuth|getSession|getServerSession|withAuth|jwt\\.verify|clerk)/i.test(f.content);\n const hasAuthImport = /import.*(?:auth|session|middleware|verify|clerk)/i.test(f.content);\n\n if (!hasLocalAuth && !hasAuthImport && !hasGlobalMiddleware) {\n // Only flag if the file has actual route handlers\n if (/\\.(get|post|put|delete|patch)\\s*\\(|export\\s+(?:async\\s+)?function\\s+(?:GET|POST|PUT|DELETE|PATCH)/i.test(f.content)) {\n const line = f.content.split(\"\\n\").findIndex(l =>\n /\\.(get|post|put|delete|patch)\\s*\\(|export\\s+(?:async\\s+)?function\\s+(?:GET|POST|PUT|DELETE|PATCH)/i.test(l)\n ) + 1;\n findings.push({\n id: `MFA001-${f.path}:${line}`,\n rule: \"MFA001\",\n severity: \"high\",\n title: \"API Route Without Auth (Cross-File Check)\",\n description: \"This API route file has no authentication checks and no auth middleware was detected in the project.\",\n file: f.path,\n line,\n snippet: getSnippet(f.content, line),\n fix: \"Add authentication middleware or import your auth module. If using Next.js, create middleware.ts in your app root.\",\n category: \"Authentication\",\n source: \"custom\",\n owasp: \"A01:2021\",\n cwe: \"CWE-862\",\n });\n }\n }\n }\n\n // ── Check 2: .env file exists but .gitignore doesn't exclude it ──\n const hasEnvFile = files.some(f => /^\\.env$|\\/\\.env$/.test(f.path));\n const gitignore = files.find(f => f.path.endsWith(\".gitignore\"));\n if (hasEnvFile && gitignore) {\n if (!/^\\.env$/m.test(gitignore.content) && !/^\\*\\.env$/m.test(gitignore.content)) {\n findings.push({\n id: \"MFA002-.gitignore:1\",\n rule: \"MFA002\",\n severity: \"critical\",\n title: \".env File Not in .gitignore\",\n description: \"An .env file exists in the project but .gitignore doesn't exclude it. Secrets may be committed.\",\n file: \".gitignore\",\n line: 1,\n snippet: getSnippet(gitignore.content, 1),\n fix: \"Add .env to .gitignore: echo '.env' >> .gitignore\",\n category: \"Secrets\",\n source: \"custom\",\n owasp: \"A02:2021\",\n cwe: \"CWE-312\",\n });\n }\n }\n\n // ── Check 3: Database client without connection pooling ──\n const dbFiles = files.filter(f =>\n /(?:db|database|prisma|drizzle|sequelize|mongoose|knex)\\.(ts|js)$/i.test(f.path) ||\n /(?:createClient|createPool|createConnection|PrismaClient)/i.test(f.content)\n );\n for (const f of dbFiles) {\n // Check if DB client is created at module level (not in a function)\n if (/new PrismaClient/i.test(f.content)) {\n // Good pattern: globalThis.prisma = globalThis.prisma || new PrismaClient()\n if (!/globalThis|global\\./i.test(f.content)) {\n const line = f.content.split(\"\\n\").findIndex(l => /new PrismaClient/.test(l)) + 1;\n findings.push({\n id: `MFA003-${f.path}:${line}`,\n rule: \"MFA003\",\n severity: \"medium\",\n title: \"Database Client Not Cached (Connection Leak)\",\n description: \"Creating a new PrismaClient on every request leaks database connections. Use a singleton pattern.\",\n file: f.path,\n line,\n snippet: getSnippet(f.content, line),\n fix: \"Use singleton: globalThis.prisma = globalThis.prisma || new PrismaClient(). This prevents connection exhaustion in serverless.\",\n category: \"Configuration\",\n source: \"custom\",\n owasp: \"A05:2021\",\n cwe: \"CWE-400\",\n });\n }\n }\n }\n\n // ── Check 4: CORS origin mismatch ──\n // Check if CORS is configured but the origin doesn't match APP_URL or frontend URL\n for (const f of files) {\n if (!/(?:server|app|index|main)\\.(ts|js)$/i.test(f.path)) continue;\n const corsMatch = f.content.match(/origin\\s*:\\s*[\"'`](https?:\\/\\/[^\"'`]+)[\"'`]/);\n if (corsMatch) {\n const corsOrigin = corsMatch[1];\n if (corsOrigin.includes(\"localhost\") || corsOrigin.includes(\"127.0.0.1\")) {\n const line = f.content.split(\"\\n\").findIndex(l => l.includes(corsOrigin)) + 1;\n findings.push({\n id: `MFA004-${f.path}:${line}`,\n rule: \"MFA004\",\n severity: \"medium\",\n title: \"CORS Origin Set to Localhost\",\n description: \"CORS origin is hardcoded to localhost. This will block requests from your production frontend.\",\n file: f.path,\n line,\n snippet: getSnippet(f.content, line),\n fix: \"Use environment variable: origin: process.env.FRONTEND_URL || 'http://localhost:3000'\",\n category: \"Configuration\",\n source: \"custom\",\n owasp: \"A05:2021\",\n cwe: \"CWE-942\",\n });\n }\n }\n }\n\n // ── Check 5: Secret referenced but not in .env.example ──\n const envExample = files.find(f => /\\.env\\.example$|\\.env\\.sample$|\\.env\\.template$/.test(f.path));\n if (envExample) {\n const envVarsUsed = new Set<string>();\n for (const f of files) {\n if (/\\.(ts|js|tsx|jsx)$/i.test(f.path)) {\n const matches = f.content.matchAll(/process\\.env\\.([A-Z_][A-Z0-9_]*)/g);\n for (const m of matches) {\n envVarsUsed.add(m[1]);\n }\n }\n }\n const envVarsDocumented = new Set<string>();\n const exampleMatches = envExample.content.matchAll(/^([A-Z_][A-Z0-9_]*)=/gm);\n for (const m of exampleMatches) {\n envVarsDocumented.add(m[1]);\n }\n\n const undocumented = [...envVarsUsed].filter(v => !envVarsDocumented.has(v) && !v.startsWith(\"NODE_\") && v !== \"npm_\");\n if (undocumented.length > 0) {\n findings.push({\n id: `MFA005-${envExample.path}:1`,\n rule: \"MFA005\",\n severity: \"low\",\n title: \"Env Vars Used But Not in .env.example\",\n description: `These environment variables are used in code but missing from .env.example: ${undocumented.slice(0, 5).join(\", \")}${undocumented.length > 5 ? ` (+${undocumented.length - 5} more)` : \"\"}`,\n file: envExample.path,\n line: 1,\n snippet: getSnippet(envExample.content, 1),\n fix: \"Add missing variables to .env.example so team members know which env vars are needed.\",\n category: \"Configuration\",\n source: \"custom\",\n owasp: \"A05:2021\",\n cwe: \"CWE-1188\",\n });\n }\n }\n\n // ── Check 6: Frontend fetching HTTP API in production code ──\n for (const f of files) {\n if (!/\\.(tsx|jsx|ts|js)$/i.test(f.path)) continue;\n if (/(?:test|spec|mock|fixture)/i.test(f.path)) continue;\n\n const httpFetches = f.content.matchAll(/fetch\\s*\\(\\s*[\"'`](http:\\/\\/(?!localhost|127\\.0\\.0\\.1)[^\"'`]+)[\"'`]/g);\n for (const m of httpFetches) {\n const line = f.content.substring(0, m.index).split(\"\\n\").length;\n findings.push({\n id: `MFA006-${f.path}:${line}`,\n rule: \"MFA006\",\n severity: \"high\",\n title: \"Frontend Fetching Over HTTP (Not HTTPS)\",\n description: `Fetching from ${m[1].substring(0, 40)}... over HTTP exposes data to interception.`,\n file: f.path,\n line,\n snippet: getSnippet(f.content, line),\n fix: \"Use HTTPS for all API calls in production.\",\n category: \"Configuration\",\n source: \"custom\",\n owasp: \"A02:2021\",\n cwe: \"CWE-319\",\n });\n }\n }\n\n return findings;\n}\n","import chalk from \"chalk\";\nimport Table from \"cli-table3\";\nimport type { Finding, ScanResult, Severity } from \"../types.js\";\nimport { calculateGrade, detectFramework, type SecurityGrade } from \"../scanners/custom-rules.js\";\n\nconst SEVERITY_COLORS: Record<Severity, (text: string) => string> = {\n critical: chalk.bgRed.white.bold,\n high: chalk.red.bold,\n medium: chalk.yellow.bold,\n low: chalk.blue,\n info: chalk.gray,\n};\n\nconst SEVERITY_ICONS: Record<Severity, string> = {\n critical: \"!!!\",\n high: \" !! \",\n medium: \" ! \",\n low: \" - \",\n info: \" i \",\n};\n\nconst SEVERITY_ORDER: Record<Severity, number> = {\n critical: 0,\n high: 1,\n medium: 2,\n low: 3,\n info: 4,\n};\n\nconst GRADE_COLORS: Record<SecurityGrade, (text: string) => string> = {\n \"A+\": chalk.green.bold,\n \"A\": chalk.green.bold,\n \"B\": chalk.cyan.bold,\n \"C\": chalk.yellow.bold,\n \"D\": chalk.red.bold,\n \"F\": chalk.bgRed.white.bold,\n};\n\nexport function renderTerminalReport(result: ScanResult, files?: { path: string; content: string }[]): void {\n const { findings, filesScanned, duration } = result;\n\n // Header\n console.log(\"\");\n console.log(chalk.bold.cyan(\" xploitscan\") + chalk.gray(\" — security scan results\"));\n console.log(chalk.gray(\" \" + \"─\".repeat(50)));\n console.log(\"\");\n\n // Framework detection\n if (files && files.length > 0) {\n const frameworks = detectFramework(files);\n if (frameworks.length > 0 && frameworks[0] !== \"unknown\") {\n console.log(chalk.gray(\" Frameworks: \") + chalk.white(frameworks.join(\", \")));\n }\n }\n\n // Grade + Benchmark\n const GRADE_PERCENTILES: Record<string, number> = { \"A+\": 98, \"A\": 90, \"B\": 70, \"C\": 45, \"D\": 20, \"F\": 5 };\n const { grade, score, summary } = calculateGrade(findings, filesScanned);\n const gradeColor = GRADE_COLORS[grade];\n const percentile = GRADE_PERCENTILES[grade] ?? 50;\n console.log(chalk.gray(\" Security Grade: \") + gradeColor(` ${grade} `) + chalk.gray(` (${score}/100) — ${summary}`));\n console.log(chalk.gray(` Benchmark: More secure than ${percentile}% of projects scanned`));\n console.log(\"\");\n\n if (findings.length === 0) {\n console.log(chalk.green.bold(\" No vulnerabilities found!\"));\n console.log(chalk.gray(` Scanned ${filesScanned} files in ${(duration / 1000).toFixed(1)}s`));\n console.log(\"\");\n return;\n }\n\n // Sort by severity\n const sorted = [...findings].sort(\n (a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity],\n );\n\n // Summary table\n const counts: Record<Severity, number> = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };\n for (const f of findings) counts[f.severity]++;\n\n const summaryParts: string[] = [];\n if (counts.critical > 0) summaryParts.push(chalk.bgRed.white.bold(` ${counts.critical} CRITICAL `));\n if (counts.high > 0) summaryParts.push(chalk.red.bold(`${counts.high} high`));\n if (counts.medium > 0) summaryParts.push(chalk.yellow.bold(`${counts.medium} medium`));\n if (counts.low > 0) summaryParts.push(chalk.blue(`${counts.low} low`));\n if (counts.info > 0) summaryParts.push(chalk.gray(`${counts.info} info`));\n\n console.log(` Found ${chalk.bold(findings.length.toString())} issues: ${summaryParts.join(chalk.gray(\" | \"))}`);\n console.log(chalk.gray(` Scanned ${filesScanned} files in ${(duration / 1000).toFixed(1)}s`));\n console.log(\"\");\n\n // Individual findings\n for (const finding of sorted) {\n const severityLabel = SEVERITY_COLORS[finding.severity](\n ` ${finding.severity.toUpperCase()} `,\n );\n const sourceLabel = finding.source === \"ai\"\n ? chalk.magenta(\" [AI] \")\n : finding.source === \"dependency\"\n ? chalk.cyan(\" [DEP] \")\n : finding.source === \"entropy\"\n ? chalk.yellow(\" [ENTROPY] \")\n : finding.source === \"config\"\n ? chalk.blue(\" [CFG] \")\n : finding.source === \"multi-file\"\n ? chalk.green(\" [CROSS-FILE] \")\n : chalk.gray(` [${finding.rule}] `);\n const confidenceLabel = finding.confidence\n ? chalk.gray(` (${finding.confidence} confidence)`)\n : \"\";\n\n const complianceTags = [\n finding.owasp ? chalk.yellow(`[${finding.owasp}]`) : \"\",\n finding.cwe ? chalk.blue(`[${finding.cwe}]`) : \"\",\n ].filter(Boolean).join(\" \");\n console.log(` ${severityLabel}${sourceLabel}${chalk.bold(finding.title)} ${complianceTags}${confidenceLabel}`);\n console.log(chalk.gray(` ${finding.file}:${finding.line}`));\n console.log(\"\");\n\n // Description\n console.log(chalk.white(` ${finding.description}`));\n console.log(\"\");\n\n // Code snippet\n if (finding.snippet) {\n const snippetLines = finding.snippet.split(\"\\n\");\n for (const line of snippetLines) {\n if (line.startsWith(\">\")) {\n console.log(chalk.red(` ${line}`));\n } else {\n console.log(chalk.gray(` ${line}`));\n }\n }\n console.log(\"\");\n }\n\n // Fix suggestion\n if (finding.fix) {\n console.log(chalk.green(` Fix: ${finding.fix}`));\n console.log(\"\");\n }\n\n console.log(chalk.gray(\" \" + \"─\".repeat(50)));\n console.log(\"\");\n }\n\n // OWASP Top 10 Summary\n const owaspFindings = findings.filter(f => f.owasp);\n if (owaspFindings.length > 0) {\n const owaspCats: Record<string, { name: string; count: number }> = {\n \"A01:2021\": { name: \"Broken Access Control\", count: 0 },\n \"A02:2021\": { name: \"Cryptographic Failures\", count: 0 },\n \"A03:2021\": { name: \"Injection\", count: 0 },\n \"A04:2021\": { name: \"Insecure Design\", count: 0 },\n \"A05:2021\": { name: \"Security Misconfiguration\", count: 0 },\n \"A06:2021\": { name: \"Vulnerable Components\", count: 0 },\n \"A07:2021\": { name: \"Auth Failures\", count: 0 },\n \"A08:2021\": { name: \"Data Integrity\", count: 0 },\n \"A09:2021\": { name: \"Logging Failures\", count: 0 },\n \"A10:2021\": { name: \"SSRF\", count: 0 },\n };\n for (const f of owaspFindings) {\n if (f.owasp && owaspCats[f.owasp]) owaspCats[f.owasp].count++;\n }\n console.log(chalk.bold(\" OWASP Top 10 Compliance\"));\n for (const [id, { name, count }] of Object.entries(owaspCats)) {\n const status = count > 0 ? chalk.red(`${count} issue${count > 1 ? \"s\" : \"\"}`) : chalk.green(\"PASS\");\n console.log(chalk.gray(` ${id} ${name}: `) + status);\n }\n console.log(\"\");\n }\n\n // Footer\n if (counts.critical > 0) {\n console.log(\n chalk.bgRed.white.bold(\" ACTION REQUIRED \") +\n chalk.red.bold(` ${counts.critical} critical issue${counts.critical > 1 ? \"s\" : \"\"} found. Fix these before deploying.`),\n );\n } else if (counts.high > 0) {\n console.log(\n chalk.yellow.bold(` Recommendation: Address the ${counts.high} high-severity issue${counts.high > 1 ? \"s\" : \"\"} before going to production.`),\n );\n }\n\n console.log(\"\");\n}\n","import type { ScanResult } from \"../types.js\";\n\nexport function renderJsonReport(result: ScanResult): void {\n console.log(JSON.stringify(result, null, 2));\n}\n","import type { Finding, ScanResult, Severity } from \"../types.js\";\n\nconst SEVERITY_TO_SARIF: Record<Severity, string> = {\n critical: \"error\",\n high: \"error\",\n medium: \"warning\",\n low: \"note\",\n info: \"none\",\n};\n\nconst SEVERITY_TO_LEVEL: Record<Severity, number> = {\n critical: 10.0,\n high: 8.0,\n medium: 5.0,\n low: 3.0,\n info: 1.0,\n};\n\ninterface SarifOutput {\n $schema: string;\n version: string;\n runs: Array<{\n tool: {\n driver: {\n name: string;\n version: string;\n informationUri: string;\n rules: Array<{\n id: string;\n shortDescription: { text: string };\n fullDescription: { text: string };\n defaultConfiguration: { level: string };\n properties: { security_severity: string; tags: string[] };\n }>;\n };\n };\n results: Array<{\n ruleId: string;\n ruleIndex: number;\n level: string;\n message: { text: string };\n locations: Array<{\n physicalLocation: {\n artifactLocation: { uri: string };\n region: {\n startLine: number;\n startColumn?: number;\n };\n };\n }>;\n }>;\n }>;\n}\n\nexport function renderSarifReport(result: ScanResult): void {\n // Collect unique rules\n const ruleMap = new Map<string, Finding>();\n for (const f of result.findings) {\n if (!ruleMap.has(f.rule)) {\n ruleMap.set(f.rule, f);\n }\n }\n\n const rules = Array.from(ruleMap.entries()).map(([id, f]) => ({\n id,\n shortDescription: { text: f.title },\n fullDescription: { text: f.description },\n defaultConfiguration: { level: SEVERITY_TO_SARIF[f.severity] },\n properties: {\n security_severity: SEVERITY_TO_LEVEL[f.severity].toFixed(1),\n tags: [\"security\", f.category.toLowerCase().replace(/\\s+/g, \"-\")],\n },\n }));\n\n const ruleIndex = new Map(rules.map((r, i) => [r.id, i]));\n\n const sarif: SarifOutput = {\n $schema: \"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json\",\n version: \"2.1.0\",\n runs: [\n {\n tool: {\n driver: {\n name: \"xploitscan\",\n version: \"0.1.0\",\n informationUri: \"https://github.com/bgage72590/xploitscan\",\n rules,\n },\n },\n results: result.findings.map((f) => {\n // SARIF schema requires that any `fixes[]` entry include an\n // `artifactChanges` array, which we don't have because findings\n // are pattern-based, not AST rewrites. Inline the fix text into\n // the message instead so GitHub still surfaces it.\n const messageText = f.fix\n ? `${f.title}: ${f.description}\\nSuggested fix: ${f.fix}`\n : `${f.title}: ${f.description}`;\n return {\n ruleId: f.rule,\n ruleIndex: ruleIndex.get(f.rule) ?? 0,\n level: SEVERITY_TO_SARIF[f.severity],\n message: { text: messageText },\n locations: [\n {\n physicalLocation: {\n artifactLocation: { uri: f.file },\n region: {\n startLine: f.line,\n ...(f.column ? { startColumn: f.column } : {}),\n },\n },\n },\n ],\n };\n }),\n },\n ],\n };\n\n console.log(JSON.stringify(sarif, null, 2));\n}\n","import { createServer } from \"node:http\";\nimport { URL } from \"node:url\";\nimport { randomUUID } from \"node:crypto\";\nimport { execFile } from \"node:child_process\";\nimport chalk from \"chalk\";\nimport ora from \"ora\";\nimport { storeToken, clearToken, getStoredToken, syncUser, clearProRulesCache } from \"../utils/api.js\";\n\nconst CLERK_PUBLISHABLE_KEY = process.env.CLERK_PUBLISHABLE_KEY ?? \"\";\n\n/**\n * Opens a browser-based login flow.\n * 1. Starts a local HTTP server on a random port\n * 2. Opens the Clerk sign-in page with redirect back to local server\n * 3. Receives the token via redirect callback\n * 4. Stores the token locally\n */\nexport async function loginCommand(): Promise<void> {\n const existing = getStoredToken();\n if (existing) {\n console.log(chalk.yellow(`Already logged in as ${existing.email}`));\n console.log(chalk.gray(\"Run `xploitscan auth logout` first to switch accounts.\"));\n return;\n }\n\n const spinner = ora(\"Waiting for browser login...\").start();\n\n // Start local callback server\n const { token, email, userId } = await waitForBrowserLogin();\n\n spinner.text = \"Syncing account...\";\n\n // Store token\n storeToken({\n token,\n userId,\n email,\n expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000, // 7 days\n });\n\n // Sync user with API\n const user = await syncUser();\n spinner.stop();\n\n console.log(chalk.green(`Logged in as ${email}`));\n if (user) {\n console.log(chalk.gray(`Plan: ${user.plan}`));\n }\n}\n\nexport async function logoutCommand(): Promise<void> {\n const existing = getStoredToken();\n if (!existing) {\n console.log(chalk.gray(\"Not logged in.\"));\n return;\n }\n\n clearToken();\n clearProRulesCache();\n console.log(chalk.green(\"Logged out successfully.\"));\n}\n\nexport async function whoamiCommand(): Promise<void> {\n const token = getStoredToken();\n if (!token) {\n console.log(chalk.gray(\"Not logged in. Run `xploitscan auth login` to authenticate.\"));\n return;\n }\n\n console.log(chalk.cyan(`Email: ${token.email}`));\n console.log(chalk.gray(`User ID: ${token.userId}`));\n\n const user = await syncUser();\n if (user) {\n const planBadge = user.plan === \"pro\"\n ? chalk.bgGreen.black(\" PRO \")\n : chalk.bgGray.white(\" FREE \");\n console.log(`Plan: ${planBadge}`);\n }\n}\n\nasync function waitForBrowserLogin(): Promise<{\n token: string;\n email: string;\n userId: string;\n}> {\n return new Promise((resolve, reject) => {\n const expectedState = randomUUID();\n\n const server = createServer((req, res) => {\n if (!req.url) {\n res.writeHead(400);\n res.end(\"Bad request\");\n return;\n }\n\n const url = new URL(req.url, `http://localhost`);\n\n if (url.pathname === \"/callback\") {\n const token = url.searchParams.get(\"token\");\n const email = url.searchParams.get(\"email\");\n const userId = url.searchParams.get(\"user_id\");\n const state = url.searchParams.get(\"state\");\n\n if (!state || state !== expectedState) {\n res.writeHead(403);\n res.end(\"Invalid state parameter — possible CSRF attack.\");\n return;\n }\n\n if (token && email && userId) {\n res.writeHead(200, { \"Content-Type\": \"text/html\" });\n res.end(`\n <html>\n <body style=\"font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #0a0a0f; color: #e0e0e0;\">\n <div style=\"text-align: center;\">\n <h1 style=\"color: #00d4ff;\">xploitscan</h1>\n <p style=\"color: #4ade80; font-size: 1.2rem;\">Login successful!</p>\n <p style=\"color: #888;\">You can close this tab and return to your terminal.</p>\n </div>\n </body>\n </html>\n `);\n\n server.close();\n resolve({ token, email, userId });\n } else {\n res.writeHead(400);\n res.end(\"Missing parameters\");\n }\n } else if (url.pathname === \"/login\") {\n // Serve a simple login page that redirects to Clerk\n const callbackUrl = `http://localhost:${(server.address() as { port: number }).port}/callback`;\n\n res.writeHead(200, { \"Content-Type\": \"text/html\" });\n res.end(`\n <html>\n <body style=\"font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #0a0a0f; color: #e0e0e0;\">\n <div style=\"text-align: center;\">\n <h1 style=\"color: #00d4ff;\">xploitscan</h1>\n <p>Redirecting to login...</p>\n <p style=\"color: #888; font-size: 0.85rem;\">If not redirected, <a href=\"#\" style=\"color: #00d4ff;\">click here</a>.</p>\n </div>\n </body>\n </html>\n `);\n } else {\n res.writeHead(302, { Location: \"/login\" });\n res.end();\n }\n });\n\n // Listen on random available port\n server.listen(0, \"127.0.0.1\", () => {\n const addr = server.address() as { port: number };\n const loginUrl = `http://localhost:${addr.port}/login`;\n\n console.log(chalk.cyan(`\\nOpen this URL in your browser to log in:`));\n console.log(chalk.bold.underline(loginUrl));\n console.log(\"\");\n\n // Try to open browser automatically\n const openCmd = process.platform === \"darwin\" ? \"open\" : process.platform === \"win32\" ? \"start\" : \"xdg-open\";\n execFile(openCmd, [loginUrl], () => {});\n });\n\n // Timeout after 5 minutes\n setTimeout(() => {\n server.close();\n reject(new Error(\"Login timed out. Please try again.\"));\n }, 5 * 60 * 1000);\n });\n}\n","import { execSync } from \"node:child_process\";\nimport {\n existsSync,\n readFileSync,\n writeFileSync,\n chmodSync,\n unlinkSync,\n} from \"node:fs\";\nimport { join } from \"node:path\";\nimport chalk from \"chalk\";\n\nconst HOOK_START_MARKER = \"# xploitscan-hook-start\";\nconst HOOK_END_MARKER = \"# xploitscan-hook-end\";\n\nconst HOOK_CONTENT = `${HOOK_START_MARKER}\n# Installed by XploitScan CLI — do not edit this block manually\necho \"\"\necho \"🛡 Running XploitScan security check...\"\nnpx --yes xploitscan scan . --no-ai --diff HEAD\nHOOK_EXIT=$?\nif [ $HOOK_EXIT -ne 0 ]; then\n echo \"\"\n echo \"❌ XploitScan found security issues. Commit blocked.\"\n echo \" Fix the issues above, or use 'git commit --no-verify' to skip.\"\n exit 1\nfi\n${HOOK_END_MARKER}`;\n\nfunction getGitRoot(): string | null {\n try {\n const root = execSync(\"git rev-parse --show-toplevel\", {\n encoding: \"utf-8\",\n stdio: [\"pipe\", \"pipe\", \"ignore\"],\n }).trim();\n return root || null;\n } catch {\n return null;\n }\n}\n\nfunction removeExistingBlock(content: string): string {\n const startIdx = content.indexOf(HOOK_START_MARKER);\n const endIdx = content.indexOf(HOOK_END_MARKER);\n if (startIdx === -1 || endIdx === -1 || endIdx < startIdx) {\n return content;\n }\n const before = content.slice(0, startIdx).replace(/\\n+$/, \"\");\n const after = content.slice(endIdx + HOOK_END_MARKER.length).replace(/^\\n+/, \"\");\n if (before && after) return `${before}\\n\\n${after}\\n`;\n if (before) return `${before}\\n`;\n if (after) return `${after}\\n`;\n return \"\";\n}\n\nexport async function installHookCommand(options: { force?: boolean }): Promise<void> {\n const gitRoot = getGitRoot();\n if (!gitRoot) {\n console.log(chalk.red(\"✗ Not a git repository.\"));\n console.log(chalk.gray(\" Run this command from inside a git repo.\"));\n process.exit(1);\n }\n\n const hooksDir = join(gitRoot, \".git\", \"hooks\");\n const hookPath = join(hooksDir, \"pre-commit\");\n\n let existingContent = \"\";\n let hadExisting = false;\n if (existsSync(hookPath)) {\n existingContent = readFileSync(hookPath, \"utf-8\");\n hadExisting = true;\n\n const alreadyInstalled =\n existingContent.includes(HOOK_START_MARKER) && existingContent.includes(HOOK_END_MARKER);\n\n if (alreadyInstalled && !options.force) {\n console.log(chalk.yellow(\"⚠ XploitScan hook is already installed.\"));\n console.log(chalk.gray(\" Use --force to reinstall, or `xploitscan hook uninstall` to remove it.\"));\n return;\n }\n\n if (alreadyInstalled && options.force) {\n existingContent = removeExistingBlock(existingContent);\n }\n }\n\n // Compose new hook file\n let newContent: string;\n if (hadExisting && existingContent.trim()) {\n // Preserve existing hook content, append our block\n const trimmed = existingContent.replace(/\\n+$/, \"\");\n newContent = `${trimmed}\\n\\n${HOOK_CONTENT}\\n`;\n } else {\n newContent = `#!/bin/sh\\n\\n${HOOK_CONTENT}\\n`;\n }\n\n try {\n writeFileSync(hookPath, newContent, \"utf-8\");\n chmodSync(hookPath, 0o755);\n } catch (err) {\n console.log(chalk.red(`✗ Failed to write pre-commit hook: ${(err as Error).message}`));\n process.exit(1);\n }\n\n console.log(chalk.green(\"✓ XploitScan pre-commit hook installed.\"));\n console.log(\"\");\n console.log(chalk.gray(\" XploitScan will now scan your code before every commit.\"));\n console.log(chalk.gray(\" To skip a scan, use: \") + chalk.cyan(\"git commit --no-verify\"));\n console.log(chalk.gray(\" To remove: \") + chalk.cyan(\"xploitscan hook uninstall\"));\n}\n\nexport async function uninstallHookCommand(): Promise<void> {\n const gitRoot = getGitRoot();\n if (!gitRoot) {\n console.log(chalk.red(\"✗ Not a git repository.\"));\n console.log(chalk.gray(\" Run this command from inside a git repo.\"));\n process.exit(1);\n }\n\n const hookPath = join(gitRoot, \".git\", \"hooks\", \"pre-commit\");\n\n if (!existsSync(hookPath)) {\n console.log(chalk.yellow(\"⚠ No pre-commit hook found. Nothing to uninstall.\"));\n return;\n }\n\n const content = readFileSync(hookPath, \"utf-8\");\n if (!content.includes(HOOK_START_MARKER)) {\n console.log(chalk.yellow(\"⚠ XploitScan hook not found in pre-commit file.\"));\n console.log(chalk.gray(\" Nothing to remove.\"));\n return;\n }\n\n const stripped = removeExistingBlock(content).trim();\n const shebangOnly = stripped === \"#!/bin/sh\" || stripped === \"#!/usr/bin/env sh\" || stripped === \"\";\n\n try {\n if (shebangOnly) {\n unlinkSync(hookPath);\n console.log(chalk.green(\"✓ XploitScan hook removed. Pre-commit file deleted (was empty after removal).\"));\n } else {\n writeFileSync(hookPath, stripped + \"\\n\", \"utf-8\");\n chmodSync(hookPath, 0o755);\n console.log(chalk.green(\"✓ XploitScan hook removed.\"));\n console.log(chalk.gray(\" Other hook content was preserved.\"));\n }\n } catch (err) {\n console.log(chalk.red(`✗ Failed to uninstall hook: ${(err as Error).message}`));\n process.exit(1);\n }\n}\n","import chalk from \"chalk\";\nimport { mkdirSync, existsSync, writeFileSync, readFileSync } from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\ninterface InstallOptions {\n force?: boolean;\n legacyOnly?: boolean;\n}\n\n/**\n * Install XploitScan security rules into a project so Cursor enforces them\n * at write-time. Drops:\n * - .cursor/rules/xploitscan-security.mdc (modern context-aware format)\n * - .cursorrules (legacy fallback for older Cursor versions)\n *\n * Idempotent: if files exist, won't overwrite without --force.\n */\nexport async function cursorInstallCommand(opts: InstallOptions = {}): Promise<void> {\n const cwd = process.cwd();\n\n // Resolve template paths relative to this file (works after tsup bundling\n // because we copy templates into dist via tsup config)\n const here = dirname(fileURLToPath(import.meta.url));\n const candidates = [\n join(here, \"templates\"),\n join(here, \"..\", \"templates\"),\n join(here, \"..\", \"src\", \"templates\"),\n ];\n const templatesDir = candidates.find((p) => existsSync(join(p, \"cursor-security.mdc\")));\n if (!templatesDir) {\n console.error(chalk.red(\"Could not locate XploitScan rule templates. Try reinstalling: npm i -g xploitscan@latest\"));\n process.exit(1);\n }\n\n const mdcSrc = readFileSync(join(templatesDir, \"cursor-security.mdc\"), \"utf-8\");\n const legacySrc = readFileSync(join(templatesDir, \"cursorrules-legacy.txt\"), \"utf-8\");\n\n // Modern format\n if (!opts.legacyOnly) {\n const mdcDir = join(cwd, \".cursor\", \"rules\");\n const mdcPath = join(mdcDir, \"xploitscan-security.mdc\");\n if (existsSync(mdcPath) && !opts.force) {\n console.log(chalk.yellow(`Skipping ${mdcPath} (already exists, use --force to overwrite)`));\n } else {\n mkdirSync(mdcDir, { recursive: true });\n writeFileSync(mdcPath, mdcSrc);\n console.log(chalk.green(`Installed ${mdcPath}`));\n }\n }\n\n // Legacy format\n const legacyPath = join(cwd, \".cursorrules\");\n if (existsSync(legacyPath) && !opts.force) {\n console.log(chalk.yellow(`Skipping ${legacyPath} (already exists, use --force to overwrite)`));\n } else {\n writeFileSync(legacyPath, legacySrc);\n console.log(chalk.green(`Installed ${legacyPath}`));\n }\n\n console.log(\"\");\n console.log(chalk.cyan(\"Cursor will pick up these rules automatically the next time you open the project.\"));\n console.log(chalk.gray(\"Run a scan any time to catch what slipped through: npx xploitscan scan .\"));\n}\n"],"mappings":"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","names":["resolve","join","chalk","readFileSync","join","rm","rm","rm","existsSync","join","resolve","execFile","readFile","mkdtemp","rm","existsSync","join","tmpdir","resolve","execFile","mkdtemp","join","tmpdir","existsSync","readFile","rm","lines","Anthropic","getSnippet","getSnippet","getSnippet","getSnippet","resolve","chalk","execSync","join","result","execFile","chalk","ora","chalk","ora","resolve","execFile","existsSync","readFileSync","join","chalk","chalk","existsSync","writeFileSync","readFileSync","join","getStoredToken","chalk","execFile"]}
|