UNPKG

vibe-guard

Version:

██ Vibe-Guard Security Scanner - 28 essential security rules to catch vulnerabilities before they catch you! Zero dependencies, instant setup, works everywhere, optimized performance. Detects SQL injection, XSS, exposed secrets, CSRF, CORS issues, contain

403 lines 17.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ExposedSecretsRule = void 0; const types_1 = require("../types"); class ExposedSecretsRule extends types_1.BaseRule { constructor() { super(...arguments); this.name = 'exposed-secrets'; this.description = 'Detects exposed API keys, tokens, and credentials with context-aware analysis'; this.severity = 'critical'; this.secretPatterns = [ { pattern: /AKIA[0-9A-Z]{16}/g, type: 'AWS Access Key', confidence: 0.95, validation: (secret) => this.validateAwsKey(secret) }, { pattern: /(?:aws[_-]?secret|AWS_SECRET)\s*[:=]\s*['"`]([a-zA-Z0-9/+=]{40})/gi, type: 'AWS Secret', confidence: 0.9, validation: (secret) => this.validateAwsSecret(secret) }, { pattern: /ghp_[a-zA-Z0-9]{36}/g, type: 'GitHub Personal Access Token', confidence: 0.95, validation: (secret) => this.validateGitHubToken(secret) }, { pattern: /ghs_[a-zA-Z0-9]{36}/g, type: 'GitHub App Token', confidence: 0.95, validation: (secret) => this.validateGitHubToken(secret) }, { pattern: /ghr_[a-zA-Z0-9]{36}/g, type: 'GitHub Refresh Token', confidence: 0.95, validation: (secret) => this.validateGitHubToken(secret) }, { pattern: /AIza[0-9A-Za-z_\-]{35}/g, type: 'Google API Key', confidence: 0.9, validation: (secret) => this.validateGoogleKey(secret) }, { pattern: /xox[baprs]-[0-9a-zA-Z\-]{10,}/g, type: 'Slack Token', confidence: 0.85, validation: (secret) => this.validateSlackToken(secret) }, { pattern: /sk_live_[a-zA-Z0-9]{24}/g, type: 'Stripe Live Secret Key', confidence: 0.95, validation: (secret) => this.validateStripeKey(secret) }, { pattern: /sk_test_[a-zA-Z0-9]{24}/g, type: 'Stripe Test Secret Key', confidence: 0.8, validation: (secret) => this.validateStripeKey(secret) }, { pattern: /pk_live_[a-zA-Z0-9]{24}/g, type: 'Stripe Live Publishable Key', confidence: 0.7, validation: (secret) => this.validateStripeKey(secret) }, { pattern: /SK[a-f0-9]{32}/g, type: 'Twilio Secret Key', confidence: 0.9, validation: (secret) => this.validateTwilioKey(secret) }, { pattern: /AC[a-f0-9]{32}/g, type: 'Twilio Account SID', confidence: 0.85, validation: (secret) => this.validateTwilioSid(secret) }, { pattern: /SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}/g, type: 'SendGrid API Key', confidence: 0.9, validation: (secret) => this.validateSendGridKey(secret) }, { pattern: /[a-zA-Z0-9]{8}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{12}/g, type: 'Azure Service Principal', confidence: 0.85, validation: (secret) => this.validateAzurePrincipal(secret) }, { pattern: /"type":\s*"service_account".*"private_key_id":\s*"[a-zA-Z0-9]+"/gs, type: 'GCP Service Account Key', confidence: 0.95, validation: (secret) => this.validateGcpServiceAccount(secret), multiLine: true }, { pattern: /[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/g, type: 'Heroku API Key', confidence: 0.8, validation: (secret) => this.validateHerokuKey(secret) }, { pattern: /eyJ[a-zA-Z0-9_\-]*\.eyJ[a-zA-Z0-9_\-]*\.[a-zA-Z0-9_\-]*/g, type: 'JWT Token', confidence: 0.8, validation: (secret) => this.validateJWT(secret) }, { pattern: /-----BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY-----\s*[A-Za-z0-9+/=\s]+\s*-----END\s+(?:RSA\s+)?PRIVATE\s+KEY-----/g, type: 'PEM Private Key', confidence: 0.95, validation: (secret) => this.validatePemKey(secret), multiLine: true }, { pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*['"`]([a-zA-Z0-9_\-]{20,})/gi, type: 'API Key', confidence: 0.7, validation: (secret) => this.validateGenericSecret(secret) }, { pattern: /(?:secret[_-]?key|secretkey)\s*[:=]\s*['"`]([a-zA-Z0-9_\-]{20,})/gi, type: 'Secret Key', confidence: 0.7, validation: (secret) => this.validateGenericSecret(secret) }, { pattern: /(?:access[_-]?token|accesstoken)\s*[:=]\s*['"`]([a-zA-Z0-9_\-]{20,})/gi, type: 'Access Token', confidence: 0.7, validation: (secret) => this.validateGenericSecret(secret) } ]; } check(fileContent) { const issues = []; const language = this.detectLanguage(fileContent.path); const framework = this.detectFramework(fileContent.content, language); const context = this.analyzeFileContext(fileContent.path); for (const { pattern, type, confidence, validation, multiLine } of this.secretPatterns) { const matches = multiLine ? this.findMultiLineMatches(fileContent.content, pattern) : this.findMatches(fileContent.content, pattern); for (const { match, line, column, lineContent } of matches) { const matchedText = match[0]; if (this.isSafeContext(context, matchedText)) { continue; } if (!validation(matchedText)) { continue; } const finalConfidence = this.calculateConfidence(confidence, context, type); const severity = this.determineSeverity(finalConfidence, context, type); if (finalConfidence >= 0.6) { issues.push(this.createIssue(fileContent.path, line, column, lineContent, `Exposed ${type} detected (confidence: ${Math.round(finalConfidence * 100)}%): ${this.maskSecret(matchedText)}`, this.generateSuggestion(type, context, framework), severity)); } } } return issues; } analyzeFileContext(filePath) { const lowerPath = filePath.toLowerCase(); return { isInEnvFile: lowerPath.includes('.env') || lowerPath.includes('environment'), isInConfigFile: lowerPath.includes('config') || lowerPath.includes('.yml') || lowerPath.includes('.yaml') || lowerPath.includes('.json'), isInDocumentation: lowerPath.includes('readme') || lowerPath.includes('.md') || lowerPath.includes('docs'), isInTestFile: lowerPath.includes('test') || lowerPath.includes('spec') || lowerPath.includes('__tests__'), isInExampleFile: lowerPath.includes('example') || lowerPath.includes('sample') || lowerPath.includes('demo'), language: this.detectLanguage(filePath) }; } isSafeContext(context, secret) { if (context.isInTestFile || context.isInExampleFile) { return false; } if (context.isInDocumentation) { return true; } if (this.hasTestPrefix(secret)) { return true; } return false; } hasTestPrefix(secret) { const testPrefixes = ['test-', 'demo-', 'example-', 'sample-', 'dev-', 'staging-']; return testPrefixes.some(prefix => secret.toLowerCase().startsWith(prefix)); } calculateConfidence(baseConfidence, context, type) { let confidence = baseConfidence; if (type === 'JWT Token' && (context.isInTestFile || context.isInExampleFile)) { confidence *= 0.5; } if (context.isInEnvFile || context.isInConfigFile) { confidence *= 1.2; } if (context.isInDocumentation) { confidence *= 0.3; } return Math.min(confidence, 1.0); } determineSeverity(confidence, context, type) { if ((context.isInEnvFile || context.isInConfigFile) && confidence >= 0.8) { return 'critical'; } if (context.isInDocumentation) { return 'medium'; } if (type === 'JWT Token' && (context.isInTestFile || context.isInExampleFile)) { return 'medium'; } if (confidence >= 0.9) return 'critical'; if (confidence >= 0.7) return 'high'; return 'medium'; } findMultiLineMatches(content, pattern) { const matches = []; const lines = content.split('\n'); const fullMatch = content.match(pattern); if (fullMatch) { const matchIndex = content.indexOf(fullMatch[0]); const linesBeforeMatch = content.substring(0, matchIndex).split('\n').length - 1; const lineNumber = linesBeforeMatch + 1; const lineContent = lines[lineNumber - 1] || ''; matches.push({ match: fullMatch, line: lineNumber, column: 1, lineContent }); } return matches; } validateAwsKey(key) { return /^AKIA[0-9A-Z]{16}$/.test(key); } validateAwsSecret(secret) { return /^[A-Za-z0-9/+=]{40}$/.test(secret); } validateGitHubToken(token) { return /^gh[psr]_[a-zA-Z0-9]{36}$/.test(token); } validateGoogleKey(key) { return /^AIza[0-9A-Za-z_\-]{35}$/.test(key); } validateSlackToken(token) { return /^xox[baprs]-[0-9a-zA-Z\-]{10,}$/.test(token); } validateStripeKey(key) { return /^sk_(live|test)_[a-zA-Z0-9]{24}$/.test(key) || /^pk_(live|test)_[a-zA-Z0-9]{24}$/.test(key); } validateTwilioKey(key) { return /^SK[a-f0-9]{32}$/.test(key); } validateTwilioSid(sid) { return /^AC[a-f0-9]{32}$/.test(sid); } validateSendGridKey(key) { return /^SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}$/.test(key); } validateAzurePrincipal(principal) { return /^[a-zA-Z0-9]{8}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{12}$/.test(principal); } validateGcpServiceAccount(content) { return content.includes('"type": "service_account"') && content.includes('"private_key_id"'); } validateHerokuKey(key) { return /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(key); } validateJWT(token) { try { const parts = token.split('.'); if (parts.length !== 3) return false; return parts.every(part => /^[A-Za-z0-9_\-]+$/.test(part)); } catch { return false; } } validatePemKey(key) { return key.includes('-----BEGIN') && key.includes('-----END') && key.includes('PRIVATE KEY'); } validateGenericSecret(secret) { if (secret.length < 20) return false; const uniqueChars = new Set(secret).size; if (uniqueChars < 15) return false; if (/^(.)\1+$/.test(secret)) return false; if (/^(012|123|234|345|456|567|678|789|890)+$/.test(secret)) return false; if (this.hasTestPrefix(secret)) return false; const charTypes = { lowercase: /[a-z]/.test(secret), uppercase: /[A-Z]/.test(secret), numbers: /[0-9]/.test(secret), special: /[^a-zA-Z0-9]/.test(secret) }; const typeCount = Object.values(charTypes).filter(Boolean).length; if (typeCount < 2) return false; return true; } generateSuggestion(type, context, framework) { const baseSuggestions = { 'AWS Access Key': 'Use AWS IAM roles or environment variables. Never commit AWS credentials to version control.', 'AWS Secret': 'Use AWS Secrets Manager or environment variables for AWS secrets.', 'GitHub Personal Access Token': 'Use GitHub Actions secrets or environment variables. Rotate tokens regularly.', 'GitHub App Token': 'Use GitHub App installation tokens or environment variables.', 'GitHub Refresh Token': 'Use GitHub OAuth refresh tokens securely.', 'Google API Key': 'Restrict API key usage and use environment variables.', 'Slack Token': 'Use Slack app configuration or environment variables.', 'Stripe Live Secret Key': 'Use Stripe webhook endpoints and environment variables for live keys.', 'Stripe Test Secret Key': 'Use environment variables for test keys.', 'Stripe Live Publishable Key': 'Publishable keys are safe to expose but should be restricted.', 'Twilio Secret Key': 'Use Twilio environment variables or secure storage.', 'Twilio Account SID': 'Use environment variables for Twilio credentials.', 'SendGrid API Key': 'Use SendGrid environment variables or secure storage.', 'Azure Service Principal': 'Use Azure Key Vault or environment variables.', 'GCP Service Account Key': 'Use Google Cloud IAM roles or secure storage.', 'Heroku API Key': 'Use Heroku environment variables or secure storage.', 'JWT Token': 'Use secure token storage and environment variables for JWT secrets.', 'PEM Private Key': 'Use secure key storage and never commit private keys.', 'API Key': 'Use environment variables or secure secret management systems.', 'Secret Key': 'Use environment variables or secure secret management systems.', 'Access Token': 'Use environment variables or secure token management systems.' }; let suggestion = baseSuggestions[type] || 'Use environment variables or secure secret management.'; suggestion += ' **Rotate the secret immediately and purge from git history.**'; if (context.isInEnvFile) { suggestion += ' Move secrets to a secure environment variable management system.'; } else if (context.isInConfigFile) { suggestion += ' Use configuration management tools that support encrypted secrets.'; } else if (context.isInDocumentation) { suggestion += ' Remove secrets from documentation and use placeholder examples.'; } if (framework) { suggestion += ` For ${framework}, consider using framework-specific secret management.`; } return suggestion; } maskSecret(secret) { if (secret.length <= 8) { return '*'.repeat(secret.length); } const start = secret.substring(0, 4); const end = secret.substring(secret.length - 4); const middle = '*'.repeat(Math.min(secret.length - 8, 10)); return `${start}${middle}${end}`; } detectLanguage(filePath) { const ext = filePath.split('.').pop()?.toLowerCase(); const languageMap = { 'js': 'javascript', 'jsx': 'javascript', 'ts': 'typescript', 'tsx': 'typescript', 'py': 'python', 'php': 'php', 'rb': 'ruby', 'go': 'go', 'java': 'java', 'cs': 'csharp' }; return languageMap[ext || ''] || 'unknown'; } detectFramework(content, language) { if (language === 'javascript' || language === 'typescript') { if (content.includes('react') || content.includes('React')) return 'react'; if (content.includes('vue') || content.includes('Vue')) return 'vue'; if (content.includes('angular') || content.includes('Angular')) return 'angular'; if (content.includes('express') || content.includes('Express')) return 'express'; } if (language === 'python') { if (content.includes('django') || content.includes('Django')) return 'django'; if (content.includes('flask') || content.includes('Flask')) return 'flask'; if (content.includes('fastapi') || content.includes('FastAPI')) return 'fastapi'; } return undefined; } } exports.ExposedSecretsRule = ExposedSecretsRule; //# sourceMappingURL=exposed-secrets.js.map