UNPKG

pury

Version:

🛡️ AI-powered security scanner with advanced threat detection, dual reporting system (detailed & summary), and comprehensive code analysis

319 lines 12.3 kB
import { FindingType, Severity } from '../types/index.js'; import { logger } from '../utils/logger.js'; import { extractLineContext } from '../utils/file-utils.js'; export class SecretsAnalyzer { patterns; sensitivity; constructor(sensitivity = 'medium') { this.sensitivity = sensitivity; this.patterns = this.loadPatterns(); } async analyze(files, onProgress) { const startTime = Date.now(); const findings = []; for (let i = 0; i < files.length; i++) { const file = files[i]; onProgress?.(i + 1, files.length, file.path); try { const fileFindings = await this.analyzeFile(file); findings.push(...fileFindings); } catch (error) { logger.warn(`Failed to analyze file ${file.path}: ${error.message}`); } } const processingTime = Date.now() - startTime; logger.debug(`Secrets analysis completed in ${processingTime}ms`); return { findings, processingTime, filesAnalyzed: files.length }; } async analyzeFile(file) { const findings = []; const lines = file.content.split('\n'); // Skip certain file types that commonly contain base64 or similar patterns if (this.shouldSkipFile(file)) { return findings; } for (const pattern of this.patterns) { if (!this.shouldApplyPattern(pattern)) { continue; } const matches = this.findSecretMatches(file.content, lines, pattern); for (const match of matches) { // Additional validation to reduce false positives if (this.validateMatch(match.match, pattern)) { findings.push(this.createFinding(match, file, pattern)); } } } return findings; } shouldSkipFile(file) { const skipExtensions = [ '.png', '.jpg', '.jpeg', '.gif', '.ico', '.svg', '.woff', '.ttf', '.pdf' ]; const skipPatterns = [ 'package-lock.json', 'yarn.lock', '.min.js', '.min.css', 'node_modules', 'dist/', 'build/' ]; return (skipExtensions.includes(file.extension.toLowerCase()) || skipPatterns.some(pattern => file.path.includes(pattern))); } shouldApplyPattern(pattern) { switch (this.sensitivity) { case 'low': return pattern.severity === Severity.CRITICAL || pattern.severity === Severity.HIGH; case 'medium': return pattern.severity !== Severity.LOW; case 'high': return true; default: return true; } } findSecretMatches(content, lines, pattern) { const matches = []; lines.forEach((line, index) => { const match = line.match(pattern.pattern); if (match) { // Check for keyword context if specified if (pattern.keywords && pattern.keywords.length > 0) { const hasKeyword = pattern.keywords.some(keyword => line.toLowerCase().includes(keyword.toLowerCase())); if (!hasKeyword) { return; // Skip if required keywords are not found } } matches.push({ line: index + 1, match: match[0], context: extractLineContext(content, index + 1, 1) }); } }); return matches; } validateMatch(match, pattern) { // Entropy check for patterns that specify it if (pattern.entropy && this.calculateEntropy(match) < pattern.entropy) { return false; } // Filter out obvious false positives const falsePositives = [ /^[0-9]+$/, // Pure numbers /^[a-f0-9]+$/i, // Hex that might be CSS colors or similar /test|example|sample|demo|placeholder|dummy|fake/i, // Test/example values /^(true|false|null|undefined)$/i, // Boolean/null values /^[a-z_]+$/i, // Variable names without mixed case /(password|secret|key|token)\s*[:=]\s*['"`]?\s*$/ // Empty assignments ]; if (falsePositives.some(regex => regex.test(match))) { return false; } // Additional validation for specific pattern types if (pattern.id.startsWith('aws-') && match.length < 16) { return false; // AWS keys are typically longer } if (pattern.id.startsWith('jwt-') && !match.includes('.')) { return false; // JWT tokens contain dots } return true; } calculateEntropy(str) { const charCounts = new Map(); for (const char of str) { charCounts.set(char, (charCounts.get(char) || 0) + 1); } let entropy = 0; const { length } = str; for (const count of charCounts.values()) { const probability = count / length; entropy -= probability * Math.log2(probability); } return entropy; } createFinding(match, file, pattern) { // Mask the secret in the evidence const maskedEvidence = this.maskSecret(match.match); return { id: `secret-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, type: FindingType.SECRET, severity: pattern.severity, title: pattern.name, description: `Potential ${pattern.name.toLowerCase()} detected in source code`, file: file.path, line: match.line, evidence: maskedEvidence, suggestion: this.getSuggestion(pattern), references: [`Pattern ID: ${pattern.id}`] }; } maskSecret(secret) { if (secret.length <= 8) { return '*'.repeat(secret.length); } const visibleChars = 3; const start = secret.substring(0, visibleChars); const end = secret.substring(secret.length - visibleChars); const middle = '*'.repeat(secret.length - visibleChars * 2); return `${start}${middle}${end}`; } getSuggestion(pattern) { const suggestions = { 'api-key': 'Move API keys to environment variables or secure configuration management systems. Never commit keys to version control.', database: 'Use environment variables for database credentials. Consider using connection string encryption or secret management services.', aws: 'Use AWS IAM roles or store credentials securely using AWS Secrets Manager or environment variables.', jwt: 'Store JWT secrets in environment variables. Ensure they are sufficiently long and random.', 'private-key': 'Never commit private keys to source control. Use secure key management systems and rotate keys regularly.', password: 'Remove hardcoded passwords. Use secure authentication mechanisms and environment variables.', webhook: 'Store webhook URLs and secrets in environment variables. Validate webhook signatures.', oauth: 'Use secure OAuth flows and store tokens securely. Never commit OAuth secrets to source control.' }; // Find the most appropriate suggestion based on pattern ID for (const [key, suggestion] of Object.entries(suggestions)) { if (pattern.id.includes(key)) { return suggestion; } } return 'Remove this secret from source code and use secure configuration management or environment variables instead.'; } loadPatterns() { return [ // API Keys { id: 'api-key-001', name: 'Generic API Key', pattern: /['\"]?[a-zA-Z0-9_-]{20,}['\"]?/g, entropy: 4.5, keywords: ['api_key', 'apikey', 'api-key', 'key'], severity: Severity.HIGH }, { id: 'api-key-002', name: 'API Key Assignment', pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*['\"]([a-zA-Z0-9_-]{16,})['\"]?/gi, severity: Severity.HIGH }, // AWS Credentials { id: 'aws-001', name: 'AWS Access Key ID', pattern: /AKIA[0-9A-Z]{16}/g, severity: Severity.CRITICAL }, { id: 'aws-002', name: 'AWS Secret Access Key', pattern: /['\"]?[A-Za-z0-9/+=]{40}['\"]?/g, keywords: ['aws_secret_access_key', 'secret_access_key'], entropy: 5.0, severity: Severity.CRITICAL }, // Google API Keys { id: 'google-001', name: 'Google API Key', pattern: /AIza[0-9A-Za-z_-]{35}/g, severity: Severity.HIGH }, // GitHub Tokens { id: 'github-001', name: 'GitHub Personal Access Token', pattern: /ghp_[a-zA-Z0-9]{36}/g, severity: Severity.HIGH }, { id: 'github-002', name: 'GitHub OAuth Token', pattern: /gho_[a-zA-Z0-9]{36}/g, severity: Severity.HIGH }, // Database Connections { id: 'db-001', name: 'Database Password', pattern: /(?:password|pwd)\s*[:=]\s*['\"]([^'\"]+)['\"]?/gi, severity: Severity.HIGH }, { id: 'db-002', name: 'Connection String', pattern: /(mongodb|mysql|postgresql|postgres):\/\/[^:]+:[^@]+@[^\/]+/gi, severity: Severity.CRITICAL }, // JWT Tokens { id: 'jwt-001', name: 'JWT Token', pattern: /eyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+/g, severity: Severity.MEDIUM }, // Private Keys { id: 'private-key-001', name: 'RSA Private Key', pattern: /-----BEGIN RSA PRIVATE KEY-----[\s\S]*?-----END RSA PRIVATE KEY-----/g, severity: Severity.CRITICAL }, { id: 'private-key-002', name: 'Private Key', pattern: /-----BEGIN PRIVATE KEY-----[\s\S]*?-----END PRIVATE KEY-----/g, severity: Severity.CRITICAL }, // OAuth and Social Media { id: 'oauth-001', name: 'OAuth Client Secret', pattern: /(?:client[_-]?secret|oauth[_-]?secret)\s*[:=]\s*['\"]([a-zA-Z0-9_-]{16,})['\"]?/gi, severity: Severity.HIGH }, { id: 'slack-001', name: 'Slack Token', pattern: /xox[baprs]-[0-9a-zA-Z-]{10,}/g, severity: Severity.HIGH }, // Generic Secrets { id: 'secret-001', name: 'Generic Secret', pattern: /(?:secret|token|password|pwd|pass)\s*[:=]\s*['\"]([a-zA-Z0-9!@#$%^&*()_+\-=\[\]{};':\"\\|,.<>\/?]{8,})['\"]?/gi, severity: Severity.MEDIUM }, // Webhook URLs { id: 'webhook-001', name: 'Webhook URL', pattern: /https?:\/\/[^\s\/]+\/webhooks?\/[a-zA-Z0-9_-]+/gi, severity: Severity.MEDIUM } ]; } addCustomPattern(pattern) { this.patterns.push(pattern); } getPatternCount() { return this.patterns.length; } setSensitivity(level) { this.sensitivity = level; } } //# sourceMappingURL=secrets-analyzer.js.map