UNPKG

mcp-adr-analysis-server

Version:

MCP server for analyzing Architectural Decision Records and project architecture

334 lines 12 kB
/** * Gitleaks integration for sensitive content detection * * Replaces custom secret detection with industry-standard gitleaks tool * while maintaining compatible interface with existing code */ import { execSync } from 'node:child_process'; import { writeFileSync, unlinkSync, existsSync, readFileSync } from 'fs'; import { join, basename } from 'path'; import { tmpdir } from 'os'; /** * Analyze content for sensitive information using gitleaks */ export async function analyzeSensitiveContent(filePath, content) { const tempDir = tmpdir(); const tempFile = join(tempDir, `gitleaks-scan-${Date.now()}-${basename(filePath)}`); const outputFile = join(tempDir, `gitleaks-output-${Date.now()}.json`); try { // Write content to temporary file writeFileSync(tempFile, content); // Run gitleaks on the temporary file const gitleaksConfig = findGitleaksConfig(); const configArg = gitleaksConfig ? `--config=${gitleaksConfig}` : ''; try { execSync(`gitleaks detect --source=${tempFile} --report-format=json --report-path=${outputFile} ${configArg} --no-git`, { stdio: 'pipe' }); // No issues found if gitleaks exits with 0 return createEmptyResult(filePath); } catch (error) { // Gitleaks exits with 1 when secrets are found if (error.status === 1 && existsSync(outputFile)) { return parseGitleaksOutput(filePath, outputFile, content); } // Real error occurred console.warn(`Gitleaks analysis failed for ${filePath}:`, error.message); return createEmptyResult(filePath); } } finally { // Clean up temporary files try { if (existsSync(tempFile)) unlinkSync(tempFile); if (existsSync(outputFile)) unlinkSync(outputFile); } catch { // Ignore cleanup errors } } } /** * Find gitleaks configuration file */ function findGitleaksConfig() { const configPaths = ['.gitleaks.toml', '.gitleaks.yml', '.gitleaks.yaml', 'gitleaks.toml']; for (const configPath of configPaths) { if (existsSync(configPath)) { return configPath; } } return null; } /** * Parse gitleaks JSON output */ function parseGitleaksOutput(filePath, outputFile, content) { try { const outputContent = readFileSync(outputFile, 'utf8'); const gitleaksResults = JSON.parse(outputContent); const matches = gitleaksResults.map(result => ({ pattern: { name: result.RuleID, description: result.Description, category: categorizeRule(result.RuleID, result.Tags), severity: getSeverity(result.RuleID, result.Tags), }, match: result.Secret || result.Match, line: result.StartLine, column: result.StartColumn, context: getContext(content, result.StartLine), confidence: calculateConfidence(result), suggestions: generateSuggestions(result.RuleID, result.Tags), })); const summary = { criticalCount: matches.filter(m => m.pattern.severity === 'critical').length, highCount: matches.filter(m => m.pattern.severity === 'high').length, mediumCount: matches.filter(m => m.pattern.severity === 'medium').length, lowCount: matches.filter(m => m.pattern.severity === 'low').length, totalCount: matches.length, }; return { filePath, hasIssues: matches.length > 0, matches, summary, recommendations: generateRecommendations(matches), }; } catch (error) { console.warn(`Failed to parse gitleaks output:`, error); return createEmptyResult(filePath); } } /** * Get context around a line */ function getContext(content, lineNumber) { const lines = content.split('\n'); const contextStart = Math.max(0, lineNumber - 3); const contextEnd = Math.min(lines.length, lineNumber + 2); return lines.slice(contextStart, contextEnd).join('\n'); } /** * Categorize gitleaks rule by ID and tags */ function categorizeRule(ruleId, _tags) { const lowerRuleId = ruleId.toLowerCase(); if (lowerRuleId.includes('key') || lowerRuleId.includes('token') || lowerRuleId.includes('credential')) { return 'credentials'; } if (lowerRuleId.includes('secret') || lowerRuleId.includes('password')) { return 'secrets'; } if (lowerRuleId.includes('email') || lowerRuleId.includes('phone')) { return 'personal'; } if (lowerRuleId.includes('ip') || lowerRuleId.includes('url') || lowerRuleId.includes('domain')) { return 'infrastructure'; } return 'development'; } /** * Determine severity based on rule ID and tags */ function getSeverity(ruleId, _tags) { const lowerRuleId = ruleId.toLowerCase(); // Critical: Production API keys, private keys, database URLs if (lowerRuleId.includes('private-key') || lowerRuleId.includes('aws-') || lowerRuleId.includes('github-') || lowerRuleId.includes('stripe-') || lowerRuleId.includes('database')) { return 'critical'; } // High: Generic secrets, tokens if (lowerRuleId.includes('secret') || lowerRuleId.includes('token') || lowerRuleId.includes('password')) { return 'high'; } // Medium: Personal info, infrastructure details if (lowerRuleId.includes('email') || lowerRuleId.includes('phone') || lowerRuleId.includes('ip')) { return 'medium'; } // Low: Everything else return 'low'; } /** * Calculate confidence based on gitleaks result */ function calculateConfidence(result) { let confidence = 0.7; // Base confidence for gitleaks detection // Entropy-based confidence adjustment if (result.Entropy > 4.0) { confidence += 0.2; } // Rule-specific adjustments if (result.RuleID.includes('generic')) { confidence -= 0.2; } if (result.RuleID.includes('aws') || result.RuleID.includes('github')) { confidence += 0.1; } return Math.max(0.1, Math.min(1.0, confidence)); } /** * Generate suggestions based on rule */ function generateSuggestions(ruleId, _tags) { const suggestions = []; const lowerRuleId = ruleId.toLowerCase(); if (lowerRuleId.includes('key') || lowerRuleId.includes('token')) { suggestions.push('Move to environment variables'); suggestions.push('Use a secrets management service'); suggestions.push('🚨 ROTATE THIS CREDENTIAL IMMEDIATELY'); } if (lowerRuleId.includes('private-key')) { suggestions.push('🚨 CRITICAL: Remove private key from code'); suggestions.push('Generate new key pair'); suggestions.push('Store private keys securely outside repository'); } if (lowerRuleId.includes('password')) { suggestions.push('Use environment variables for passwords'); suggestions.push('Consider using encrypted configuration'); } if (lowerRuleId.includes('email') || lowerRuleId.includes('phone')) { suggestions.push('Replace with placeholder values'); suggestions.push('Use fake data for examples'); } // Generic suggestions suggestions.push('Add sensitive files to .gitignore'); suggestions.push('Use the content masking tool to sanitize content'); return suggestions; } /** * Generate recommendations based on matches */ function generateRecommendations(matches) { const recommendations = []; if (matches.length === 0) { return ['No sensitive content detected']; } const criticalCount = matches.filter(m => m.pattern.severity === 'critical').length; const highCount = matches.filter(m => m.pattern.severity === 'high').length; if (criticalCount > 0) { recommendations.push(`🚨 ${criticalCount} CRITICAL security issue(s) found - DO NOT COMMIT`); recommendations.push('Rotate any exposed credentials immediately'); } if (highCount > 0) { recommendations.push(`⚠️ ${highCount} HIGH severity issue(s) found`); recommendations.push('Review and secure sensitive information'); } recommendations.push('Use environment variables for sensitive configuration'); recommendations.push('Consider using a secrets management service'); recommendations.push('Gitleaks detected these issues - industry-standard tool'); return recommendations; } /** * Create empty result when no issues found */ function createEmptyResult(filePath) { return { filePath, hasIssues: false, matches: [], summary: { criticalCount: 0, highCount: 0, mediumCount: 0, lowCount: 0, totalCount: 0, }, recommendations: ['No sensitive content detected by gitleaks'], }; } /** * Quick check for obviously sensitive files (compatible with existing code) */ export function isObviouslySensitive(filePath) { const fileName = basename(filePath).toLowerCase(); const sensitiveFilePatterns = [ /^\.env$/, /^\.env\./, /secrets?\./, /credentials?\./, /private.*key/, /id_rsa$/, /id_ed25519$/, /\.pem$/, /\.p12$/, /\.pfx$/, /keystore/, /truststore/, ]; return sensitiveFilePatterns.some(pattern => pattern.test(fileName)); } /** * Integration with existing content masking tool (compatible interface) */ export async function integrateWithContentMasking(filePath, content) { // Get gitleaks analysis const sensitiveAnalysis = await analyzeSensitiveContent(filePath, content); // Try to use existing content masking tool let maskingPrompt; let existingRecommendations = []; try { const { analyzeContentSecurity } = await import('../tools/content-masking-tool.js'); const maskingResult = await analyzeContentSecurity({ content, contentType: getContentType(filePath), enhancedMode: false, knowledgeEnhancement: false, }); if (maskingResult.content && maskingResult.content[0]) { const resultText = maskingResult.content[0].text; const promptMatch = resultText.match(/## AI.*Prompt\n\n(.*?)(?=\n##|$)/s); if (promptMatch) { maskingPrompt = promptMatch[1]; } const recMatch = resultText.match(/## Next Steps\n\n(.*?)(?=\n##|$)/s); if (recMatch) { existingRecommendations = recMatch[1].split('\n').filter((line) => line.trim()); } } } catch { // Silently handle integration errors } // Combine recommendations const combinedRecommendations = [ ...sensitiveAnalysis.recommendations, ...existingRecommendations, ].filter((rec, index, arr) => arr.indexOf(rec) === index); return { sensitiveAnalysis, ...(maskingPrompt && { maskingPrompt }), combinedRecommendations, }; } /** * Get content type for file (compatible helper) */ function getContentType(filePath) { const ext = filePath.split('.').pop()?.toLowerCase() || ''; if (['js', 'ts', 'py', 'java', 'cpp', 'c', 'go', 'rs', 'rb', 'php'].includes(ext)) { return 'code'; } if (['md', 'txt', 'rst', 'doc', 'docx'].includes(ext)) { return 'documentation'; } if (['json', 'yaml', 'yml', 'ini', 'conf', 'config', 'env'].includes(ext)) { return 'configuration'; } if (['log', 'out', 'err'].includes(ext)) { return 'logs'; } return 'general'; } //# sourceMappingURL=gitleaks-detector.js.map