UNPKG

keyvault-cli

Version:

Secure API key management CLI tool

197 lines (196 loc) • 9.67 kB
import fs from 'fs'; import path from 'path'; export class CodeScanner { async scanDirectory(directoryPath) { const results = []; try { await this.scanDirectoryRecursive(directoryPath, results); } catch (error) { console.error(`Error scanning directory ${directoryPath}:`, error); } return results; } async scanDirectoryRecursive(directoryPath, results) { const entries = await fs.promises.readdir(directoryPath, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(directoryPath, entry.name); if (entry.isDirectory()) { if (!CodeScanner.IGNORED_DIRECTORIES.includes(entry.name)) { await this.scanDirectoryRecursive(fullPath, results); } } else if (entry.isFile()) { const ext = path.extname(entry.name).toLowerCase(); if (!CodeScanner.IGNORED_EXTENSIONS.includes(ext)) { const fileResults = await this.scanFile(fullPath); results.push(...fileResults); } } } } async scanFile(filePath) { const results = []; try { const content = await fs.promises.readFile(filePath, 'utf8'); const lines = content.split('\n'); for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { const line = lines[lineIndex]; // Skip comments and logs if (this.isCommentOrLog(line)) { continue; } for (const pattern of CodeScanner.API_KEY_PATTERNS) { const matches = line.match(pattern.pattern); if (matches) { for (const match of matches) { // Skip obvious false positives if (this.isFalsePositive(match, line)) { continue; } results.push({ file: filePath, line: lineIndex + 1, type: pattern.type, service: pattern.service, severity: pattern.severity, value: this.maskValue(match) }); } } } } } catch (error) { console.error(`Error scanning file ${filePath}:`, error); } return results; } isCommentOrLog(line) { const trimmed = line.trim(); return trimmed.startsWith('//') || trimmed.startsWith('#') || trimmed.startsWith('/*') || trimmed.includes('console.log') || trimmed.includes('console.error') || trimmed.includes('logger.'); } isFalsePositive(match, line) { const falsePositives = [ 'example', 'sample', 'test', 'demo', 'placeholder', 'your_key_here', 'your-key-here', 'replace-me', 'sk_test_123', 'pk_test_123', 'AKIA123456789', 'xxxxxxxx', 'aaaaaaaa', '00000000', 'undefined', 'null', 'false', 'true' ]; const lowerMatch = match.toLowerCase(); const lowerLine = line.toLowerCase(); return falsePositives.some(fp => lowerMatch.includes(fp) || lowerLine.includes(fp) || lowerLine.includes('todo') || lowerLine.includes('fixme')); } maskValue(value) { if (value.length <= 8) { return '*'.repeat(value.length); } return value.substring(0, 4) + '*'.repeat(value.length - 8) + value.substring(value.length - 4); } generateReport(results) { if (results.length === 0) { return 'No API keys or secrets found in the scanned files.'; } const grouped = results.reduce((acc, result) => { const key = `${result.service}_${result.severity}`; if (!acc[key]) { acc[key] = []; } acc[key].push(result); return acc; }, {}); let report = `Found ${results.length} potential API keys/secrets:\n\n`; // Sort by severity (critical first) const severityOrder = ['critical', 'high', 'medium', 'low']; const sortedGroups = Object.entries(grouped).sort(([a], [b]) => { const severityA = severityOrder.indexOf(a.split('_')[1]); const severityB = severityOrder.indexOf(b.split('_')[1]); return severityA - severityB; }); for (const [key, groupResults] of sortedGroups) { const [service, severity] = key.split('_'); const severityIcon = severity === 'critical' ? '🚨' : severity === 'high' ? 'šŸ”“' : severity === 'medium' ? '🟔' : '🟢'; report += `${severityIcon} ${service.toUpperCase()} (${severity}) - ${groupResults.length} found:\n`; for (const result of groupResults) { report += ` šŸ“„ ${result.file}:${result.line} - ${result.type}: ${result.value}\n`; } report += '\n'; } report += '\nāš ļø SECURITY RECOMMENDATIONS:\n'; report += '• Move all API keys to environment variables\n'; report += '• Use a secure vault like KeyVault for credential management\n'; report += '• Add sensitive files to .gitignore\n'; report += '• Rotate any exposed keys immediately\n'; report += '• Enable pre-commit hooks to prevent future leaks\n'; report += '• 🚨 CRITICAL: Never commit private keys or mnemonic seeds!\n'; report += '• šŸ” For blockchain: Use hardware wallets for production\n'; return report; } } CodeScanner.API_KEY_PATTERNS = [ // AWS { pattern: /AKIA[0-9A-Z]{16}/, service: 'aws', type: 'api-key', severity: 'high' }, { pattern: /aws_secret_access_key\s*=\s*[A-Za-z0-9/+=]{40}/, service: 'aws', type: 'secret', severity: 'high' }, // Stripe { pattern: /sk_live_[0-9a-zA-Z]{24}/, service: 'stripe', type: 'api-key', severity: 'high' }, { pattern: /sk_test_[0-9a-zA-Z]{24}/, service: 'stripe', type: 'api-key', severity: 'medium' }, { pattern: /pk_live_[0-9a-zA-Z]{24}/, service: 'stripe', type: 'api-key', severity: 'medium' }, // OpenAI { pattern: /sk-[a-zA-Z0-9]{48}/, service: 'openai', type: 'api-key', severity: 'high' }, // GitHub { pattern: /ghp_[A-Za-z0-9]{36}/, service: 'github', type: 'token', severity: 'high' }, { pattern: /gho_[A-Za-z0-9]{36}/, service: 'github', type: 'token', severity: 'high' }, { pattern: /ghu_[A-Za-z0-9]{36}/, service: 'github', type: 'token', severity: 'high' }, // Google Cloud { pattern: /AIza[0-9A-Za-z\\-_]{35}/, service: 'google', type: 'api-key', severity: 'high' }, // Slack { pattern: /xox[baprs]-[0-9a-zA-Z]{10,48}/, service: 'slack', type: 'token', severity: 'medium' }, // Twilio { pattern: /AC[a-fA-F0-9]{32}/, service: 'twilio', type: 'api-key', severity: 'medium' }, // Blockchain & Crypto // Ethereum private keys (64 hex chars) { pattern: /\b[a-fA-F0-9]{64}\b/, service: 'ethereum', type: 'private-key', severity: 'critical' }, // Ethereum addresses { pattern: /0x[a-fA-F0-9]{40}/, service: 'ethereum', type: 'wallet-address', severity: 'high' }, // Bitcoin private keys (WIF format) { pattern: /[5KL][1-9A-HJ-NP-Za-km-z]{50,51}/, service: 'bitcoin', type: 'private-key', severity: 'critical' }, // Bitcoin addresses (P2PKH) { pattern: /[13][a-km-zA-HJ-NP-Z1-9]{25,34}/, service: 'bitcoin', type: 'wallet-address', severity: 'high' }, // Bitcoin addresses (Bech32) { pattern: /bc1[a-z0-9]{39,59}/, service: 'bitcoin', type: 'wallet-address', severity: 'high' }, // Solana private keys (base58) { pattern: /[1-9A-HJ-NP-Za-km-z]{87,88}/, service: 'solana', type: 'private-key', severity: 'critical' }, // Solana addresses { pattern: /[1-9A-HJ-NP-Za-km-z]{32,44}/, service: 'solana', type: 'wallet-address', severity: 'medium' }, // BNB Smart Chain (BSC) addresses { pattern: /0x[a-fA-F0-9]{40}/, service: 'binance', type: 'wallet-address', severity: 'high' }, // Polygon addresses (same as Ethereum) { pattern: /0x[a-fA-F0-9]{40}/, service: 'polygon', type: 'wallet-address', severity: 'high' }, // Mnemonic seed phrases (12/24 words) { pattern: /\b(?:[a-z]{3,8}\s+){11,23}[a-z]{3,8}\b/, service: 'crypto', type: 'mnemonic-seed', severity: 'critical' }, // Generic patterns { pattern: /['"]\s*[A-Za-z0-9]{32,}\s*['"]/, service: 'generic', type: 'api-key', severity: 'low' }, { pattern: /api[_-]?key\s*[:=]\s*['"]\s*[A-Za-z0-9]{16,}\s*['"]/, service: 'generic', type: 'api-key', severity: 'medium' }, { pattern: /secret[_-]?key\s*[:=]\s*['"]\s*[A-Za-z0-9]{16,}\s*['"]/, service: 'generic', type: 'secret', severity: 'medium' }, { pattern: /token\s*[:=]\s*['"]\s*[A-Za-z0-9]{16,}\s*['"]/, service: 'generic', type: 'token', severity: 'medium' }, ]; CodeScanner.IGNORED_EXTENSIONS = [ '.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.pdf', '.doc', '.docx', '.zip', '.tar', '.gz', '.mp4', '.mp3', '.wav', '.avi', '.mov', '.ttf', '.woff', '.woff2', '.eot', '.min.js', '.min.css' ]; CodeScanner.IGNORED_DIRECTORIES = [ 'node_modules', '.git', '.next', 'dist', 'build', 'coverage', '.nyc_output', 'logs', 'tmp', 'temp' ];