pury
Version:
🛡️ AI-powered security scanner with advanced threat detection, dual reporting system (detailed & summary), and comprehensive code analysis
319 lines • 12.3 kB
JavaScript
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