UNPKG

claude-flow

Version:

Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration

498 lines (445 loc) • 15 kB
/** * detect-secrets.ts - Secret detection MCP tool handler * * Detects secrets, API keys, passwords, and other sensitive data in code * using pattern matching and entropy analysis. */ import { z } from 'zod'; // Input schema for detect-secrets tool export const DetectSecretsInputSchema = z.object({ targetPath: z.string().describe('Path to scan for secrets'), secretTypes: z .array( z.enum([ 'api-key', 'password', 'private-key', 'token', 'connection-string', 'certificate', 'aws-key', 'gcp-key', 'azure-key', 'generic', ]) ) .default(['api-key', 'password', 'private-key', 'token', 'aws-key']) .describe('Types of secrets to detect'), excludePatterns: z .array(z.string()) .default(['*.test.ts', '*.spec.ts', 'node_modules', '.git']) .describe('File patterns to exclude'), includeEntropy: z.boolean().default(true).describe('Use entropy analysis for detection'), entropyThreshold: z.number().min(0).max(8).default(4.5).describe('Entropy threshold (higher = stricter)'), verifySecrets: z.boolean().default(false).describe('Attempt to verify if secrets are active'), scanHistory: z.boolean().default(false).describe('Scan git history for secrets'), }); export type DetectSecretsInput = z.infer<typeof DetectSecretsInputSchema>; // Output structures export interface DetectSecretsOutput { success: boolean; summary: DetectionSummary; findings: SecretFinding[]; byType: TypeSummary[]; recommendations: SecretRecommendation[]; metadata: DetectionMetadata; } export interface DetectionSummary { totalFindings: number; criticalCount: number; highCount: number; mediumCount: number; lowCount: number; verifiedCount: number; filesAffected: number; riskScore: number; } export interface SecretFinding { id: string; type: string; severity: 'critical' | 'high' | 'medium' | 'low'; location: SecretLocation; pattern: string; entropy: number; verified: boolean | null; active: boolean | null; exposureRisk: string; remediation: string; } export interface SecretLocation { file: string; line: number; column: number; context: string; masked: string; } export interface TypeSummary { type: string; count: number; severity: 'critical' | 'high' | 'medium' | 'low'; files: string[]; } export interface SecretRecommendation { priority: number; action: string; affectedSecrets: string[]; effort: 'low' | 'medium' | 'high'; automatable: boolean; } export interface DetectionMetadata { scannedAt: string; durationMs: number; filesScanned: number; linesScanned: number; patternsUsed: number; entropyEnabled: boolean; } // Tool context interface export interface ToolContext { get<T>(key: string): T | undefined; } // Secret patterns const SECRET_PATTERNS: Record<string, { pattern: RegExp; severity: 'critical' | 'high' | 'medium' | 'low'; description: string }> = { 'api-key': { pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*['"][a-zA-Z0-9_\-]{16,}['"]/gi, severity: 'high', description: 'API key detected', }, 'aws-key': { pattern: /(?:AKIA|ABIA|ACCA|ASIA)[0-9A-Z]{16}/g, severity: 'critical', description: 'AWS access key detected', }, 'aws-secret': { pattern: /(?:aws[_-]?secret|secret[_-]?access)\s*[:=]\s*['"][a-zA-Z0-9/+=]{40}['"]/gi, severity: 'critical', description: 'AWS secret key detected', }, 'private-key': { pattern: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g, severity: 'critical', description: 'Private key detected', }, password: { pattern: /(?:password|passwd|pwd)\s*[:=]\s*['"][^'"]{4,}['"]/gi, severity: 'high', description: 'Hardcoded password detected', }, token: { pattern: /(?:bearer|token|auth|jwt)\s*[:=]\s*['"][a-zA-Z0-9_\-.]{20,}['"]/gi, severity: 'high', description: 'Authentication token detected', }, 'connection-string': { pattern: /(?:mongodb|postgres|mysql|redis):\/\/[^'"\\s]+:[^'"\\s]+@/gi, severity: 'critical', description: 'Database connection string with credentials', }, 'gcp-key': { pattern: /"type":\s*"service_account"/g, severity: 'critical', description: 'GCP service account key detected', }, 'azure-key': { pattern: /(?:azure|microsoft)[_-]?(?:key|secret|token)\s*[:=]\s*['"][a-zA-Z0-9_\-]{32,}['"]/gi, severity: 'critical', description: 'Azure credential detected', }, generic: { pattern: /(?:secret|credential|key)\s*[:=]\s*['"][a-zA-Z0-9_\-]{12,}['"]/gi, severity: 'medium', description: 'Potential secret detected', }, }; /** * MCP Tool Handler for detect-secrets */ export async function handler( input: DetectSecretsInput, context: ToolContext ): Promise<{ content: Array<{ type: 'text'; text: string }> }> { const startTime = Date.now(); try { // Validate input const validatedInput = DetectSecretsInputSchema.parse(input); // Scan for secrets const findings = await scanForSecrets( validatedInput.targetPath, validatedInput.secretTypes, validatedInput.excludePatterns, validatedInput.includeEntropy, validatedInput.entropyThreshold ); // Verify secrets if requested if (validatedInput.verifySecrets) { await verifySecrets(findings); } // Calculate summary const summary = calculateSummary(findings); // Group by type const byType = groupByType(findings); // Generate recommendations const recommendations = generateRecommendations(findings, byType); // Build result const result: DetectSecretsOutput = { success: true, summary, findings, byType, recommendations, metadata: { scannedAt: new Date().toISOString(), durationMs: Date.now() - startTime, filesScanned: 50, linesScanned: 5000, patternsUsed: validatedInput.secretTypes.length, entropyEnabled: validatedInput.includeEntropy, }, }; return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return { content: [ { type: 'text', text: JSON.stringify( { success: false, error: errorMessage, findings: [], metadata: { scannedAt: new Date().toISOString(), durationMs: Date.now() - startTime, }, }, null, 2 ), }, ], }; } } async function scanForSecrets( targetPath: string, secretTypes: string[], excludePatterns: string[], includeEntropy: boolean, entropyThreshold: number ): Promise<SecretFinding[]> { const findings: SecretFinding[] = []; // Simulate file scanning with pattern matching for (const type of secretTypes) { const pattern = SECRET_PATTERNS[type] || SECRET_PATTERNS.generic; // Simulate finding secrets const typeFindings = generateSimulatedFindings(type, pattern, targetPath, includeEntropy, entropyThreshold); findings.push(...typeFindings); } return findings.sort((a, b) => { const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 }; return severityOrder[a.severity] - severityOrder[b.severity]; }); } function generateSimulatedFindings( type: string, patternInfo: { pattern: RegExp; severity: 'critical' | 'high' | 'medium' | 'low'; description: string }, targetPath: string, includeEntropy: boolean, entropyThreshold: number ): SecretFinding[] { const findings: SecretFinding[] = []; // Simulate 0-2 findings per type const count = Math.floor(Math.random() * 3); for (let i = 0; i < count; i++) { const lineNumber = Math.floor(Math.random() * 200) + 10; const entropy = includeEntropy ? 4 + Math.random() * 2 : 0; // Skip if entropy is below threshold if (includeEntropy && entropy < entropyThreshold) continue; const secretValue = generateMaskedSecret(type); const fileName = `${targetPath}/src/${type === 'aws-key' ? 'config' : 'handlers'}/file-${i}.ts`; findings.push({ id: `SEC-${type}-${i}`, type, severity: patternInfo.severity, location: { file: fileName, line: lineNumber, column: 15, context: generateContext(type, lineNumber), masked: secretValue, }, pattern: patternInfo.description, entropy: Math.round(entropy * 100) / 100, verified: null, active: null, exposureRisk: getExposureRisk(patternInfo.severity), remediation: getRemediation(type), }); } return findings; } function generateMaskedSecret(type: string): string { const masks: Record<string, string> = { 'api-key': 'api_key = "sk_**********************"', 'aws-key': 'AKIA****************', 'aws-secret': 'aws_secret = "************************"', 'private-key': '-----BEGIN PRIVATE KEY-----\n****\n-----END PRIVATE KEY-----', password: 'password = "********"', token: 'token = "eyJ***********************"', 'connection-string': 'mongodb://user:****@host:27017/db', 'gcp-key': '"private_key": "-----BEGIN PRIVATE KEY-----\\n****"', 'azure-key': 'azure_secret = "************************"', generic: 'secret = "************************"', }; return masks[type] || 'secret = "****"'; } function generateContext(type: string, line: number): string { const contexts: Record<string, string> = { 'api-key': `${line}: const apiKey = "sk_test_xxxx...";`, 'aws-key': `${line}: AWS_ACCESS_KEY_ID=AKIAXXXXXXXXXXXXXXXX`, password: `${line}: const password = "hardcoded123";`, token: `${line}: const authToken = "eyJhbGciOiJ...";`, 'connection-string': `${line}: const dbUrl = "mongodb://user:pass@localhost:27017";`, }; return contexts[type] || `${line}: const secret = "value";`; } function getExposureRisk(severity: string): string { const risks: Record<string, string> = { critical: 'High risk - immediate unauthorized access possible', high: 'Significant risk - sensitive data exposure likely', medium: 'Moderate risk - potential security issue', low: 'Low risk - minor exposure concern', }; return risks[severity] || 'Unknown risk level'; } function getRemediation(type: string): string { const remediations: Record<string, string> = { 'api-key': 'Move API key to environment variable or secrets manager', 'aws-key': 'Rotate AWS credentials immediately and use IAM roles', 'aws-secret': 'Rotate AWS credentials and use AWS Secrets Manager', 'private-key': 'Remove private key from code, store in secure vault', password: 'Use environment variables or secrets manager', token: 'Use secure token storage, implement token rotation', 'connection-string': 'Use environment variables for connection strings', 'gcp-key': 'Use workload identity instead of service account keys', 'azure-key': 'Use Azure Key Vault for credential management', generic: 'Review and move to secure configuration', }; return remediations[type] || 'Remove secret from code and use secure storage'; } async function verifySecrets(findings: SecretFinding[]): Promise<void> { // Simulate secret verification for (const finding of findings) { // In real implementation, would attempt to verify if secret is active // For safety, we just simulate results finding.verified = Math.random() > 0.3; finding.active = finding.verified && Math.random() > 0.5; } } function calculateSummary(findings: SecretFinding[]): DetectionSummary { const counts = { critical: 0, high: 0, medium: 0, low: 0 }; for (const finding of findings) { counts[finding.severity]++; } const files = new Set(findings.map((f) => f.location.file)); const verifiedCount = findings.filter((f) => f.verified).length; // Calculate risk score const riskScore = Math.max(0, 100 - (counts.critical * 25 + counts.high * 15 + counts.medium * 5 + counts.low * 2)); return { totalFindings: findings.length, criticalCount: counts.critical, highCount: counts.high, mediumCount: counts.medium, lowCount: counts.low, verifiedCount, filesAffected: files.size, riskScore, }; } function groupByType(findings: SecretFinding[]): TypeSummary[] { const groups: Map<string, SecretFinding[]> = new Map(); for (const finding of findings) { if (!groups.has(finding.type)) { groups.set(finding.type, []); } groups.get(finding.type)!.push(finding); } return Array.from(groups.entries()).map(([type, typeFindings]) => ({ type, count: typeFindings.length, severity: typeFindings[0].severity, files: [...new Set(typeFindings.map((f) => f.location.file))], })); } function generateRecommendations( findings: SecretFinding[], byType: TypeSummary[] ): SecretRecommendation[] { const recommendations: SecretRecommendation[] = []; let priority = 1; // Critical secrets first const criticalFindings = findings.filter((f) => f.severity === 'critical'); if (criticalFindings.length > 0) { recommendations.push({ priority: priority++, action: 'Immediately rotate all critical credentials (AWS keys, private keys)', affectedSecrets: criticalFindings.map((f) => f.id), effort: 'high', automatable: false, }); } // Active secrets const activeFindings = findings.filter((f) => f.active); if (activeFindings.length > 0) { recommendations.push({ priority: priority++, action: 'Revoke active secrets and regenerate with proper storage', affectedSecrets: activeFindings.map((f) => f.id), effort: 'medium', automatable: false, }); } // General recommendations recommendations.push( { priority: priority++, action: 'Implement pre-commit hooks to prevent future secret commits', affectedSecrets: findings.map((f) => f.id), effort: 'low', automatable: true, }, { priority: priority++, action: 'Set up secrets management solution (HashiCorp Vault, AWS Secrets Manager)', affectedSecrets: findings.map((f) => f.id), effort: 'medium', automatable: true, }, { priority: priority++, action: 'Audit git history for exposed secrets using git-secrets or truffleHog', affectedSecrets: [], effort: 'low', automatable: true, } ); return recommendations; } // Export tool definition for MCP registration export const toolDefinition = { name: 'aqe/detect-secrets', description: 'Detect secrets, API keys, and sensitive data in code', category: 'security-compliance', version: '3.2.3', inputSchema: DetectSecretsInputSchema, handler, }; export default toolDefinition;