mira-consciousness
Version:
Memory & Intelligence Retention Archive - Preserving The Spark
336 lines • 14.6 kB
JavaScript
/**
* 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