UNPKG

mira-consciousness

Version:

Memory & Intelligence Retention Archive - Preserving The Spark

336 lines 14.6 kB
/** * Secret Scanner * Detects hardcoded secrets, API keys, and sensitive information */ import fs from 'fs-extra'; import * as path from 'path'; import { glob } from 'glob'; export class SecretScanner { projectRoot; constructor(projectRoot) { this.projectRoot = projectRoot; } isTestFile(filePath) { const testPatterns = [ /\.test\.[jt]sx?$/, /\.spec\.[jt]sx?$/, /\/__tests__\//, /\/tests?\//, /\/e2e\//, /test-utils/, /\.mock\.[jt]sx?$/ ]; return testPatterns.some(pattern => pattern.test(filePath)); } async scan(vulnerabilities) { await Promise.all([ this.checkEnvironmentFiles(vulnerabilities), this.checkHardcodedSecrets(vulnerabilities), this.checkFilePermissions(vulnerabilities) ]); } async performEntropyAnalysis() { const suspiciousStrings = []; const files = await glob('**/*.{js,ts,jsx,tsx,json,env,yml,yaml,xml}', { cwd: this.projectRoot, ignore: ['node_modules/**', '.git/**', 'dist/**', 'build/**', '**/*.min.js'] }); for (const file of files) { const filePath = path.join(this.projectRoot, file); const isTest = this.isTestFile(file); try { const content = await fs.readFile(filePath, 'utf-8'); const lines = content.split('\n'); for (let i = 0; i < lines.length; i++) { const line = lines[i]; // Skip test files with intentional examples if (isTest && (content.includes('intentional security issues') || content.includes('testing'))) { continue; } const highEntropyStrings = this.findHighEntropyStrings(line); for (const str of highEntropyStrings) { const entropy = this.calculateEntropy(str); const type = this.classifySecretType(str); if (entropy > 4.5) { // High entropy threshold suspiciousStrings.push({ string: this.maskSecret(str), file, line: i + 1, entropy, type, confidence: this.calculateConfidence(entropy, type, str) }); } } } } catch (error) { // Skip files that can't be read } } // Sort by confidence suspiciousStrings.sort((a, b) => b.confidence - a.confidence); const totalHighEntropyStrings = suspiciousStrings.length; const criticalCount = suspiciousStrings.filter(s => s.confidence > 0.8).length; let overallRisk = 'low'; if (criticalCount > 5) overallRisk = 'critical'; else if (criticalCount > 2) overallRisk = 'high'; else if (totalHighEntropyStrings > 10) overallRisk = 'medium'; return { suspiciousStrings: suspiciousStrings.slice(0, 50), // Limit to top 50 overallRisk, totalHighEntropyStrings }; } async checkEnvironmentFiles(vulnerabilities) { const envFiles = ['.env', '.env.local', '.env.production', '.env.development']; for (const envFile of envFiles) { const envPath = path.join(this.projectRoot, envFile); if (await fs.pathExists(envPath)) { // Check if .env files are in .gitignore const gitignorePath = path.join(this.projectRoot, '.gitignore'); if (await fs.pathExists(gitignorePath)) { const gitignoreContent = await fs.readFile(gitignorePath, 'utf-8'); if (!gitignoreContent.includes('.env')) { vulnerabilities.push({ file: envFile, severity: 'critical', type: 'exposed_secrets', message: `Environment file ${envFile} exists but .env is not in .gitignore`, recommendation: 'Add .env to .gitignore to prevent committing secrets' }); } } else { vulnerabilities.push({ file: envFile, severity: 'high', type: 'missing_gitignore', message: `Environment file ${envFile} exists but no .gitignore found`, recommendation: 'Create .gitignore and add .env files to it' }); } // Check for exposed sensitive values const envContent = await fs.readFile(envPath, 'utf-8'); const sensitivePatterns = [ { pattern: /DATABASE_URL=.+/i, name: 'Database URL' }, { pattern: /API_KEY=.+/i, name: 'API Key' }, { pattern: /SECRET=.+/i, name: 'Secret' }, { pattern: /PASSWORD=.+/i, name: 'Password' }, { pattern: /PRIVATE_KEY=.+/i, name: 'Private Key' } ]; for (const { pattern, name } of sensitivePatterns) { if (pattern.test(envContent)) { vulnerabilities.push({ file: envFile, severity: 'high', type: 'sensitive_env_var', message: `${name} found in environment file`, recommendation: 'Ensure this file is never committed to version control' }); } } } } } async checkHardcodedSecrets(vulnerabilities) { // Common secret patterns const secretPatterns = [ { pattern: /api[_-]?key\s*[:=]\s*['"][^'"]{10,}['"]/i, type: 'api_key', severity: 'high' }, { pattern: /password\s*[:=]\s*['"][^'"]{5,}['"]/i, type: 'password', severity: 'critical' }, { pattern: /secret\s*[:=]\s*['"][^'"]{10,}['"]/i, type: 'secret', severity: 'high' }, { pattern: /token\s*[:=]\s*['"][^'"]{10,}['"]/i, type: 'token', severity: 'high' }, { pattern: /private[_-]?key\s*[:=]\s*['"][^'"]{20,}['"]/i, type: 'private_key', severity: 'critical' }, { pattern: /aws[_-]?access[_-]?key[_-]?id\s*[:=]\s*['"][A-Z0-9]{20}['"]/i, type: 'aws_access_key', severity: 'critical' }, { pattern: /aws[_-]?secret[_-]?access[_-]?key\s*[:=]\s*['"][^'"]{40}['"]/i, type: 'aws_secret_key', severity: 'critical' } ]; // Check common file types const filePatterns = ['**/*.ts', '**/*.js', '**/*.jsx', '**/*.tsx', '**/*.json', '**/*.yml', '**/*.yaml', '**/*.xml']; for (const pattern of filePatterns) { try { const files = await glob(pattern, { cwd: this.projectRoot, ignore: ['node_modules/**', '.git/**', 'dist/**', 'build/**'] }); for (const file of files) { const filePath = path.join(this.projectRoot, file); try { const content = await fs.readFile(filePath, 'utf-8'); const lines = content.split('\n'); lines.forEach((line, index) => { for (const secretPattern of secretPatterns) { if (secretPattern.pattern.test(line)) { // Check for false positives if (!this.isFalsePositive(line, secretPattern.type)) { vulnerabilities.push({ file, line: index + 1, severity: secretPattern.severity, type: 'hardcoded_secret', message: `Potential ${secretPattern.type.replace(/_/g, ' ')} found in source code`, recommendation: 'Move sensitive data to environment variables or secure vault' }); } } } }); } catch (error) { // Skip files that can't be read } } } catch (error) { // Skip glob patterns that fail } } } async checkFilePermissions(vulnerabilities) { // Check for overly permissive files const sensitiveFiles = [ '.env', '.env.*', 'id_rsa', 'id_dsa', 'id_ecdsa', 'id_ed25519', '*.pem', '*.key', '*.pfx', '*.p12', '.ssh/*', 'private.key' ]; for (const pattern of sensitiveFiles) { const files = await glob(pattern, { cwd: this.projectRoot, dot: true, // Include hidden files ignore: ['node_modules/**', '.git/**'] }); for (const file of files) { const filePath = path.join(this.projectRoot, file); try { const stats = await fs.stat(filePath); const mode = stats.mode; // Check if file is readable by others (permission 044) if (mode & 0o044) { vulnerabilities.push({ file, severity: 'high', type: 'file_permissions', message: `Sensitive file ${file} has overly permissive permissions (${(mode & parseInt('777', 8)).toString(8)})`, recommendation: 'Set file permissions to 600 (owner read/write only)' }); } } catch (error) { // File doesn't exist or can't be accessed } } } } findHighEntropyStrings(line) { const strings = []; // Extract quoted strings const quotedStrings = line.match(/['"]([^'"]+)['"]/g) || []; quotedStrings.forEach(str => { const content = str.slice(1, -1); if (content.length >= 16) { strings.push(content); } }); // Extract base64-like strings const base64Pattern = /[A-Za-z0-9+/]{20,}={0,2}/g; const base64Matches = line.match(base64Pattern) || []; strings.push(...base64Matches); // Extract hex strings const hexPattern = /[0-9a-fA-F]{32,}/g; const hexMatches = line.match(hexPattern) || []; strings.push(...hexMatches); return strings.filter(str => str.length >= 16); } calculateEntropy(str) { const freq = {}; for (const char of str) { freq[char] = (freq[char] || 0) + 1; } let entropy = 0; const len = str.length; for (const count of Object.values(freq)) { const p = count / len; entropy -= p * Math.log2(p); } return entropy; } classifySecretType(str) { // API key patterns if (/^[A-Za-z0-9]{32,}$/.test(str) && str.includes('_')) { return 'api_key'; } // JWT token pattern if (/^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+$/.test(str)) { return 'token'; } // Crypto key pattern (base64 encoded) if (/^[A-Za-z0-9+/]{43,}={0,2}$/.test(str)) { return 'crypto_key'; } // Password pattern (mixed case, numbers, special chars) if (/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/.test(str)) { return 'password'; } return 'unknown'; } calculateConfidence(entropy, type, str) { let confidence = 0; // Base confidence from entropy if (entropy > 5.5) confidence = 0.9; else if (entropy > 5.0) confidence = 0.7; else if (entropy > 4.5) confidence = 0.5; else confidence = 0.3; // Adjust based on type if (type !== 'unknown') confidence += 0.1; // Adjust based on string characteristics if (str.length > 40) confidence += 0.1; if (/[A-Z]/.test(str) && /[a-z]/.test(str) && /\d/.test(str)) confidence += 0.1; // Common false positive patterns reduce confidence if (/^[A-F0-9]+$/i.test(str)) confidence -= 0.2; // Hex only (could be hash) if (/^[0-9]+$/.test(str)) confidence -= 0.3; // Numbers only return Math.max(0, Math.min(1, confidence)); } maskSecret(secret) { if (secret.length <= 8) { return '*'.repeat(secret.length); } const visibleChars = 4; const prefix = secret.substring(0, visibleChars); const suffix = secret.substring(secret.length - visibleChars); const maskedLength = secret.length - (visibleChars * 2); return `${prefix}${'*'.repeat(Math.min(maskedLength, 20))}${suffix}`; } isFalsePositive(line, type) { const lowerLine = line.toLowerCase(); // Common false positives const falsePositives = [ 'example', 'sample', 'demo', 'test', 'fake', 'dummy', 'placeholder', 'your-api-key', 'your-password', 'xxx', '123456', 'abcdef', 'password123', 'changeme' ]; for (const fp of falsePositives) { if (lowerLine.includes(fp)) { return true; } } // Check for common test/example patterns if (/\b(test|spec|mock|stub)\b/i.test(line)) { return true; } return false; } } //# sourceMappingURL=SecretScanner.js.map