recoder-security
Version:
Enterprise-grade security and compliance layer for CodeCraft CLI
673 lines • 26.6 kB
JavaScript
/**
* Secret Detection and Prevention System
* Prevents API keys, passwords, and sensitive data from being leaked in generated code
* Provides real-time scanning, pattern matching, and automatic remediation
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SecretDetector = void 0;
const fs_1 = require("fs");
const path_1 = __importDefault(require("path"));
const crypto_1 = __importDefault(require("crypto"));
const shared_1 = require("@recoder/shared");
class SecretDetector {
constructor(config) {
this.logger = new shared_1.Logger('SecretDetector');
this.patterns = new Map();
this.detectionHistory = new Map();
this.config = {
enabled: true,
scanGenerated: true,
scanUploaded: true,
scanRealtime: false,
autoRemediate: true,
alertOnDetection: true,
maxFileSize: 5 * 1024 * 1024, // 5MB
excludePatterns: ['node_modules/**', '.git/**', '*.min.js', '*.map'],
customPatterns: [],
verificationEnabled: false, // Disabled by default to avoid external calls
verificationTimeout: 5000,
...config,
};
this.loadSecretPatterns();
this.loadCustomPatterns();
}
/**
* Scan text for secrets
*/
async scanText(text, context) {
if (!this.config.enabled) {
return [];
}
const detections = [];
const lines = text.split('\n');
for (const [patternId, pattern] of this.patterns) {
const matches = text.matchAll(pattern.pattern);
for (const match of matches) {
const matchText = match[0];
const matchIndex = match.index || 0;
// Skip if this looks like a false positive
if (this.isFalsePositive(matchText, pattern)) {
continue;
}
// Calculate entropy to reduce false positives
const entropy = this.calculateEntropy(matchText);
if (pattern.entropy && entropy < pattern.entropy) {
continue;
}
// Get line and column information
const lineInfo = this.getLineInfo(text, matchIndex);
const contextLine = lines[lineInfo.line - 1] || '';
// Calculate confidence score
const confidence = this.calculateConfidence(matchText, pattern, contextLine);
// Skip low confidence matches unless they're critical patterns
if (confidence < 0.3 && pattern.severity !== 'critical') {
continue;
}
const detection = {
id: this.generateDetectionId(),
pattern,
match: matchText,
redactedMatch: this.redactSecret(matchText),
file: context.file || 'generated_code',
line: lineInfo.line,
column: lineInfo.column,
context: contextLine,
confidence,
entropy,
verified: false,
remediation: this.generateRemediation(matchText, pattern, contextLine),
metadata: {
timestamp: Date.now(),
userId: context.userId,
sessionId: context.sessionId,
scanType: context.scanType,
},
};
detections.push(detection);
}
}
// Verify secrets if enabled
if (this.config.verificationEnabled) {
await this.verifySecrets(detections);
}
// Store detection history
const sessionKey = context.sessionId || 'default';
const existingDetections = this.detectionHistory.get(sessionKey) || [];
this.detectionHistory.set(sessionKey, [...existingDetections, ...detections]);
// Alert on critical detections
if (this.config.alertOnDetection && detections.length > 0) {
await this.alertOnDetections(detections);
}
return detections.sort((a, b) => {
// Sort by severity then confidence
const severityOrder = { critical: 4, high: 3, medium: 2, low: 1 };
if (severityOrder[a.pattern.severity] !== severityOrder[b.pattern.severity]) {
return severityOrder[b.pattern.severity] - severityOrder[a.pattern.severity];
}
return b.confidence - a.confidence;
});
}
/**
* Scan file for secrets
*/
async scanFile(filePath, context) {
try {
// Check file size
const stats = await fs_1.promises.stat(filePath);
if (stats.size > this.config.maxFileSize) {
this.logger.warn(`File too large to scan: ${filePath} (${stats.size} bytes)`);
return [];
}
// Check if file should be excluded
const relativePath = path_1.default.relative(process.cwd(), filePath);
if (this.shouldExcludeFile(relativePath)) {
return [];
}
// Read and scan file
const content = await fs_1.promises.readFile(filePath, 'utf8');
return await this.scanText(content, {
file: filePath,
userId: context?.userId,
sessionId: context?.sessionId,
scanType: 'file_scan',
});
}
catch (error) {
this.logger.error(`Failed to scan file ${filePath}:`, error);
return [];
}
}
/**
* Scan directory recursively
*/
async scanDirectory(dirPath, context) {
const scanId = this.generateScanId();
const startTime = Date.now();
this.logger.info(`Starting secret scan: ${scanId} on ${dirPath}`);
try {
const files = await this.getFilesToScan(dirPath);
const allDetections = [];
let scannedFiles = 0;
for (const file of files) {
try {
const detections = await this.scanFile(file, context);
allDetections.push(...detections);
scannedFiles++;
}
catch (error) {
this.logger.warn(`Failed to scan ${file}:`, error);
}
}
const result = {
scanId,
timestamp: startTime,
target: dirPath,
totalFiles: files.length,
scannedFiles,
totalDetections: allDetections.length,
detections: allDetections,
summary: {
critical: allDetections.filter(d => d.pattern.severity === 'critical').length,
high: allDetections.filter(d => d.pattern.severity === 'high').length,
medium: allDetections.filter(d => d.pattern.severity === 'medium').length,
low: allDetections.filter(d => d.pattern.severity === 'low').length,
verified: allDetections.filter(d => d.verified).length,
falsePositives: allDetections.filter(d => d.confidence < 0.5).length,
},
remediated: false,
duration: Date.now() - startTime,
};
// Auto-remediate if enabled
if (this.config.autoRemediate && allDetections.length > 0) {
await this.remediateDetections(allDetections);
result.remediated = true;
}
this.logger.info(`Scan completed: ${allDetections.length} secrets detected in ${scannedFiles} files`);
return result;
}
catch (error) {
this.logger.error('Directory scan failed:', error);
throw error;
}
}
/**
* Remediate detected secrets by removing or replacing them
*/
async remediateText(text, detections) {
if (!this.config.autoRemediate || detections.length === 0) {
return text;
}
let remediatedText = text;
// Sort detections by position (reverse order to maintain indices)
const sortedDetections = [...detections].sort((a, b) => {
const aIndex = text.indexOf(a.match);
const bIndex = text.indexOf(b.match);
return bIndex - aIndex;
});
for (const detection of sortedDetections) {
const { action, replacement } = detection.remediation;
switch (action) {
case 'remove':
remediatedText = remediatedText.replace(detection.match, '');
break;
case 'redact':
remediatedText = remediatedText.replace(detection.match, detection.redactedMatch);
break;
case 'replace':
remediatedText = remediatedText.replace(detection.match, replacement);
break;
case 'warn':
// Add warning comment
const warningComment = `// WARNING: Potential secret detected and removed\n`;
remediatedText = remediatedText.replace(detection.match, warningComment + replacement);
break;
}
}
return remediatedText;
}
/**
* Add custom secret pattern
*/
addCustomPattern(pattern) {
this.patterns.set(pattern.id, pattern);
this.logger.info(`Added custom secret pattern: ${pattern.name}`);
}
/**
* Remove custom pattern
*/
removeCustomPattern(patternId) {
if (this.patterns.delete(patternId)) {
this.logger.info(`Removed custom pattern: ${patternId}`);
}
}
/**
* Get detection statistics
*/
getStatistics() {
const allDetections = Array.from(this.detectionHistory.values()).flat();
return {
patternsLoaded: this.patterns.size,
totalDetections: allDetections.length,
criticalDetections: allDetections.filter(d => d.pattern.severity === 'critical').length,
verifiedDetections: allDetections.filter(d => d.verified).length,
sessionsScanned: this.detectionHistory.size,
};
}
/**
* Load built-in secret patterns
*/
loadSecretPatterns() {
const patterns = [
// AWS Access Keys
{
id: 'aws-access-key',
name: 'AWS Access Key ID',
description: 'Amazon Web Services access key identifier',
pattern: /AKIA[0-9A-Z]{16}/g,
severity: 'critical',
confidence: 0.9,
category: 'cloud',
provider: 'AWS',
entropy: 4.5,
examples: ['AKIAIOSFODNN7EXAMPLE'],
validationUrl: 'https://sts.amazonaws.com/',
},
// AWS Secret Keys
{
id: 'aws-secret-key',
name: 'AWS Secret Access Key',
description: 'Amazon Web Services secret access key',
pattern: /[A-Za-z0-9/+=]{40}/g,
severity: 'critical',
confidence: 0.7,
category: 'cloud',
provider: 'AWS',
entropy: 5.0,
examples: ['wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'],
},
// GitHub Personal Access Tokens
{
id: 'github-token',
name: 'GitHub Personal Access Token',
description: 'GitHub personal access token',
pattern: /ghp_[a-zA-Z0-9]{36}/g,
severity: 'high',
confidence: 0.95,
category: 'token',
provider: 'GitHub',
entropy: 5.2,
examples: ['ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'],
},
// GitHub OAuth Tokens
{
id: 'github-oauth',
name: 'GitHub OAuth Token',
description: 'GitHub OAuth access token',
pattern: /gho_[a-zA-Z0-9]{36}/g,
severity: 'high',
confidence: 0.95,
category: 'token',
provider: 'GitHub',
examples: ['gho_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'],
},
// Slack Tokens
{
id: 'slack-token',
name: 'Slack Token',
description: 'Slack API token',
pattern: /xox[baprs]-([0-9a-zA-Z]{10,48})?/g,
severity: 'high',
confidence: 0.9,
category: 'api_key',
provider: 'Slack',
examples: ['xoxb-1234567890123-1234567890123-xxxxxxxxxxxxxxxxxxxxxxxx'],
},
// Google API Keys
{
id: 'google-api-key',
name: 'Google API Key',
description: 'Google Cloud Platform API key',
pattern: /AIza[0-9A-Za-z\-_]{35}/g,
severity: 'high',
confidence: 0.9,
category: 'api_key',
provider: 'Google',
examples: ['AIzaSyDaGmWKa4JsXZ-HjGw7ISLn_3namBGewQe'],
},
// JWT Tokens
{
id: 'jwt-token',
name: 'JWT Token',
description: 'JSON Web Token',
pattern: /eyJ[a-zA-Z0-9=]+\.[a-zA-Z0-9=]+\.[a-zA-Z0-9=\-_+/]*/g,
severity: 'medium',
confidence: 0.8,
category: 'token',
entropy: 4.8,
examples: ['eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'],
},
// Private Keys
{
id: 'private-key-rsa',
name: 'RSA Private Key',
description: 'RSA private key',
pattern: /-----BEGIN RSA PRIVATE KEY-----[\s\S]*?-----END RSA PRIVATE KEY-----/g,
severity: 'critical',
confidence: 0.95,
category: 'certificate',
examples: ['-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----'],
},
// Database URLs
{
id: 'database-url',
name: 'Database Connection URL',
description: 'Database connection string with credentials',
pattern: /(mysql|postgres|mongodb|redis):\/\/[a-zA-Z0-9_\-]+:[a-zA-Z0-9_\-!@#$%^&*()]+@[a-zA-Z0-9\-._]+:[0-9]+\/[a-zA-Z0-9_\-]+/g,
severity: 'critical',
confidence: 0.85,
category: 'database',
examples: ['postgres://user:password@localhost:5432/database'],
},
// Generic API Keys
{
id: 'generic-api-key',
name: 'Generic API Key',
description: 'Generic API key pattern',
pattern: /['"](api_key|apikey|api-key|secret|password|token)['"]\s*[:=]\s*['"][a-zA-Z0-9\-_!@#$%^&*()+=]{16,}['"]/gi,
severity: 'medium',
confidence: 0.6,
category: 'api_key',
entropy: 4.0,
examples: ['"api_key": "sk_live_1234567890abcdef"'],
falsePositivePatterns: [
/placeholder/i,
/example/i,
/test/i,
/demo/i,
/\*\*\*\*/,
/xxx/i,
],
},
// Stripe Keys
{
id: 'stripe-secret-key',
name: 'Stripe Secret Key',
description: 'Stripe secret API key',
pattern: /sk_(test|live)_[a-zA-Z0-9]{24,34}/g,
severity: 'critical',
confidence: 0.95,
category: 'api_key',
provider: 'Stripe',
examples: ['sk_test_1234567890abcdef1234567890ab'],
},
// SendGrid API Keys
{
id: 'sendgrid-api-key',
name: 'SendGrid API Key',
description: 'SendGrid email service API key',
pattern: /SG\.[a-zA-Z0-9\-_]{22}\.[a-zA-Z0-9\-_]{43}/g,
severity: 'high',
confidence: 0.9,
category: 'api_key',
provider: 'SendGrid',
examples: ['SG.xxxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'],
},
// Password patterns
{
id: 'hardcoded-password',
name: 'Hardcoded Password',
description: 'Hardcoded password in source code',
pattern: /(password|pwd|pass)\s*[:=]\s*['"][^'"]{8,}['"]/gi,
severity: 'high',
confidence: 0.7,
category: 'password',
examples: ['password: "mySecretPassword123"'],
falsePositivePatterns: [
/password.*placeholder/i,
/password.*example/i,
/\*\*\*\*/,
],
},
];
// Load patterns into map
for (const pattern of patterns) {
this.patterns.set(pattern.id, pattern);
}
this.logger.info(`Loaded ${patterns.length} built-in secret patterns`);
}
/**
* Load custom patterns from configuration
*/
loadCustomPatterns() {
for (const pattern of this.config.customPatterns) {
this.patterns.set(pattern.id, pattern);
}
if (this.config.customPatterns.length > 0) {
this.logger.info(`Loaded ${this.config.customPatterns.length} custom secret patterns`);
}
}
/**
* Check if match is a false positive
*/
isFalsePositive(match, pattern) {
if (!pattern.falsePositivePatterns) {
return false;
}
return pattern.falsePositivePatterns.some(fpPattern => fpPattern.test(match));
}
/**
* Calculate entropy of a string
*/
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.length;
for (const count of charCounts.values()) {
const probability = count / length;
entropy -= probability * Math.log2(probability);
}
return entropy;
}
/**
* Calculate confidence score for a detection
*/
calculateConfidence(match, pattern, context) {
let confidence = pattern.confidence;
// Adjust based on entropy
const entropy = this.calculateEntropy(match);
if (pattern.entropy && entropy < pattern.entropy) {
confidence *= 0.7; // Reduce confidence for low entropy
}
// Adjust based on context
const suspiciousContext = /config|env|secret|key|token|password|credential/i.test(context);
if (suspiciousContext) {
confidence *= 1.2; // Increase confidence
}
// Check for test/example indicators
const testIndicators = /test|example|demo|placeholder|mock|fake/i.test(context);
if (testIndicators) {
confidence *= 0.5; // Reduce confidence significantly
}
return Math.min(confidence, 1.0);
}
/**
* Generate remediation strategy for a detection
*/
generateRemediation(match, pattern, context) {
const redacted = this.redactSecret(match);
// Critical secrets should be removed entirely
if (pattern.severity === 'critical') {
return {
action: 'remove',
replacement: '',
explanation: `Critical secret (${pattern.name}) detected and removed for security`,
};
}
// API keys and tokens should be replaced with environment variables
if (pattern.category === 'api_key' || pattern.category === 'token') {
const envVarName = this.generateEnvVarName(pattern.name);
return {
action: 'replace',
replacement: `process.env.${envVarName}`,
explanation: `Replaced ${pattern.name} with environment variable reference`,
};
}
// Passwords should be replaced with secure references
if (pattern.category === 'password') {
return {
action: 'replace',
replacement: 'process.env.PASSWORD',
explanation: 'Replaced hardcoded password with environment variable',
};
}
// Default to redaction
return {
action: 'redact',
replacement: redacted,
explanation: `Redacted ${pattern.name} for security`,
};
}
/**
* Redact secret by showing only first and last few characters
*/
redactSecret(secret) {
if (secret.length <= 8) {
return '*'.repeat(secret.length);
}
const start = secret.substring(0, 3);
const end = secret.substring(secret.length - 3);
const middle = '*'.repeat(secret.length - 6);
return start + middle + end;
}
/**
* Generate environment variable name from pattern name
*/
generateEnvVarName(patternName) {
return patternName
.toUpperCase()
.replace(/[^A-Z0-9]/g, '_')
.replace(/_+/g, '_')
.replace(/^_|_$/g, '');
}
/**
* Get line and column information for character index
*/
getLineInfo(text, index) {
const lines = text.substring(0, index).split('\n');
return {
line: lines.length,
column: lines[lines.length - 1].length + 1,
};
}
/**
* Check if file should be excluded from scanning
*/
shouldExcludeFile(filePath) {
return this.config.excludePatterns.some(pattern => {
// Convert glob pattern to regex
const regexPattern = pattern
.replace(/\*\*/g, '.*')
.replace(/\*/g, '[^/]*')
.replace(/\?/g, '[^/]');
return new RegExp(regexPattern).test(filePath);
});
}
/**
* Get files to scan in directory
*/
async getFilesToScan(dirPath) {
const files = [];
const scanDir = async (dir) => {
const entries = await fs_1.promises.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path_1.default.join(dir, entry.name);
if (entry.isDirectory()) {
if (!this.shouldExcludeFile(path_1.default.relative(dirPath, fullPath))) {
await scanDir(fullPath);
}
}
else if (entry.isFile()) {
if (!this.shouldExcludeFile(path_1.default.relative(dirPath, fullPath))) {
files.push(fullPath);
}
}
}
};
await scanDir(dirPath);
return files;
}
/**
* Verify if detected secrets are real (optional)
*/
async verifySecrets(detections) {
// This would make external API calls to verify if secrets are valid
// Disabled by default for security and privacy reasons
for (const detection of detections) {
// Placeholder - in real implementation, this would:
// 1. Make safe API calls to verify if the secret is valid
// 2. Update the 'verified' field accordingly
// 3. Handle rate limiting and errors gracefully
// For now, just mark high-confidence detections as potentially verified
detection.verified = detection.confidence > 0.8;
}
}
/**
* Alert on critical secret detections
*/
async alertOnDetections(detections) {
const criticalDetections = detections.filter(d => d.pattern.severity === 'critical');
if (criticalDetections.length > 0) {
this.logger.error(`SECURITY ALERT: ${criticalDetections.length} critical secrets detected!`);
for (const detection of criticalDetections) {
this.logger.error(`Critical secret: ${detection.pattern.name} in ${detection.file}:${detection.line}`);
}
}
}
/**
* Remediate detections by modifying files
*/
async remediateDetections(detections) {
// Group detections by file
const fileDetections = new Map();
for (const detection of detections) {
const existing = fileDetections.get(detection.file) || [];
existing.push(detection);
fileDetections.set(detection.file, existing);
}
// Remediate each file
for (const [filePath, fileDetectionList] of fileDetections) {
try {
if (filePath !== 'generated_code') {
const content = await fs_1.promises.readFile(filePath, 'utf8');
const remediatedContent = await this.remediateText(content, fileDetectionList);
await fs_1.promises.writeFile(filePath, remediatedContent);
this.logger.info(`Remediated ${fileDetectionList.length} secrets in ${filePath}`);
}
}
catch (error) {
this.logger.error(`Failed to remediate ${filePath}:`, error);
}
}
}
/**
* Generate unique detection ID
*/
generateDetectionId() {
return crypto_1.default.randomBytes(8).toString('hex');
}
/**
* Generate unique scan ID
*/
generateScanId() {
return `scan_${Date.now()}_${crypto_1.default.randomBytes(4).toString('hex')}`;
}
}
exports.SecretDetector = SecretDetector;
//# sourceMappingURL=secret-detector.js.map
;