#!/usr/bin/env node /** * Credential Exposure Scanner - Enforces inst_069, inst_070 * Detects real credentials in documentation and code */ const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); // Patterns for real credentials (not example/redacted) const CREDENTIAL_PATTERNS = [ // API keys that look real (not EXAMPLE/REDACTED) { pattern: /sk-ant-api03-[A-Za-z0-9_-]{95,}(?!EXAMPLE|REDACTED)/g, type: 'Anthropic API Key' }, { pattern: /sk-[a-z0-9]{32,}(?!EXAMPLE|REDACTED|your-key)/gi, type: 'Stripe Secret Key' }, { pattern: /pk-[a-z0-9]{32,}(?!EXAMPLE|REDACTED|your-key)/gi, type: 'Stripe Public Key' }, // Generic patterns that look suspicious { pattern: /api[_-]?key[\s:=]+["']?([a-z0-9]{32,})["']?(?!EXAMPLE|REDACTED|your-|xxx)/gi, type: 'Generic API Key' }, { pattern: /secret[\s:=]+["']?([a-z0-9]{32,})["']?(?!EXAMPLE|REDACTED|your-|xxx)/gi, type: 'Generic Secret' }, { pattern: /password[\s:=]+["']?([^"'\s]{8,})["']?(?!REDACTED|your-|xxx|example|password123)/gi, type: 'Possible Password' }, // AWS { pattern: /AKIA[0-9A-Z]{16}(?!EXAMPLE)/g, type: 'AWS Access Key' }, // JWT tokens (look for proper structure, not examples) { pattern: /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}(?!EXAMPLE)/g, type: 'JWT Token' }, // Database connection strings with real passwords { pattern: /mongodb:\/\/[^:]+:([^@\s]{8,})@(?!REDACTED|your-password|example)/gi, type: 'MongoDB Credential' }, { pattern: /postgres:\/\/[^:]+:([^@\s]{8,})@(?!REDACTED|your-password|example)/gi, type: 'PostgreSQL Credential' } ]; function checkFile(filePath) { const content = fs.readFileSync(filePath, 'utf8'); const lines = content.split('\n'); const findings = []; lines.forEach((line, idx) => { CREDENTIAL_PATTERNS.forEach(({ pattern, type }) => { const matches = line.matchAll(pattern); for (const match of matches) { findings.push({ file: filePath, line: idx + 1, type, text: line.trim(), match: match[0] }); } }); }); return findings; } function scanFiles(files) { const allFindings = []; files.forEach(file => { if (!fs.existsSync(file)) return; // Skip binary files, node_modules, etc. if (file.includes('node_modules') || file.includes('.git/') || file.includes('dist/')) return; const ext = path.extname(file).toLowerCase(); if (['.png', '.jpg', '.jpeg', '.gif', '.pdf', '.zip'].includes(ext)) return; try { const findings = checkFile(file); allFindings.push(...findings); } catch (err) { // Binary file or encoding issue - skip } }); return allFindings; } function main() { const args = process.argv.slice(2); let files = []; if (args.length === 0) { // Scan staged git files try { const staged = execSync('git diff --cached --name-only --diff-filter=ACM', { encoding: 'utf8' }); files = staged.trim().split('\n').filter(f => f.length > 0); } catch (err) { console.log('āš ļø Not in git repository - skipping credential scan'); process.exit(0); } } else { files = args; } if (files.length === 0) { console.log('āœ… No files to scan for credentials'); process.exit(0); } console.log(`\nšŸ” Scanning ${files.length} file(s) for credential exposure...\n`); const findings = scanFiles(files); if (findings.length === 0) { console.log('āœ… No credentials detected\n'); process.exit(0); } // Report findings console.log(`āŒ Found ${findings.length} potential credential(s):\n`); console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); findings.forEach(f => { console.log(`šŸ”“ ${f.file}:${f.line}`); console.log(` Type: ${f.type}`); console.log(` Match: ${f.match.substring(0, 50)}${f.match.length > 50 ? '...' : ''}`); console.log(` Line: ${f.text.substring(0, 80)}${f.text.length > 80 ? '...' : ''}`); console.log(''); }); console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); console.log('āš ļø CRITICAL SECURITY ISSUE (inst_069/inst_070)\n'); console.log('Replace real credentials with:'); console.log(' • API keys: "sk-ant-api03-EXAMPLE-REDACTED-NEVER-USE"'); console.log(' • Secrets: "REDACTED" or "your-secret-here"'); console.log(' • Passwords: "your-password-here"\n'); console.log('Use environment variables or secret management for real credentials.\n'); process.exit(1); } main();