import * as fs from "node:fs"; import * as path from "node:path"; import { BaseAgent, AgentContext, AgentResult } from "./base.js"; interface SecurityFinding { stride_category: string; cwe: string; severity: "low" | "medium" | "high"; disposition: "accept" | "mitigate" | "flag"; file: string; description: string; } const SECURITY_PATTERNS: Array<{ pattern: RegExp; category: string; cwe: string; description: string; severity: "low" | "medium" | "high"; confidence: number; }> = [ { pattern: /password\s*=\s*['"][^'"]+['"]/gi, category: "information_disclosure", cwe: "CWE-259", description: "Hardcoded password", severity: "high", confidence: 0.95 }, { pattern: /api[_-]?key\s*=\s*['"][^'"]+['"]/gi, category: "information_disclosure", cwe: "CWE-312", description: "Hardcoded API key", severity: "high", confidence: 0.95 }, { pattern: /secret\s*=\s*['"][^'"]+['"]/gi, category: "information_disclosure", cwe: "CWE-312", description: "Hardcoded secret", severity: "high", confidence: 0.95 }, { pattern: /token\s*=\s*['"][^'"]+['"]/gi, category: "information_disclosure", cwe: "CWE-312", description: "Hardcoded token", severity: "medium", confidence: 0.80 }, { pattern: /eval\s*\(\s*[^'"]*\$\{/g, category: "tampering", cwe: "CWE-94", description: "eval() with dynamic content", severity: "high", confidence: 0.90 }, { pattern: /(?:exec|execSync|spawn|spawnSync)\s*\(\s*[^'"]*[\$`]/g, category: "elevation_of_privilege", cwe: "CWE-78", description: "Command execution with interpolation", severity: "high", confidence: 0.85 }, { pattern: /catch\s*\(\w*\)\s*\{\s*\}/g, category: "repudiation", cwe: "CWE-778", description: "Empty catch block", severity: "medium", confidence: 0.85 }, { pattern: /jwt\.decode\s*\(/g, category: "spoofing", cwe: "CWE-287", description: "JWT decode without verify", severity: "high", confidence: 0.85 }, { pattern: /(?:__proto__|constructor\s*\[|prototype\s*\[)/g, category: "elevation_of_privilege", cwe: "CWE-1321", description: "Prototype pollution", severity: "high", confidence: 0.90 }, { pattern: /(?:md5|sha1|des|rc4)\s*\(/gi, category: "information_disclosure", cwe: "CWE-328", description: "Weak crypto", severity: "medium", confidence: 0.90 }, { pattern: /express\.json\s*\(\s*\)/g, category: "denial_of_service", cwe: "CWE-400", description: "JSON parser without size limit", severity: "medium", confidence: 0.80 }, ]; export class SecurityAuditorAgent extends BaseAgent { readonly name = "security-auditor"; readonly description = "Auto-dispositions threats: low=accept, medium=mitigate, high=escalate."; readonly workflow = "verify"; private confidenceThreshold: number; constructor(confidenceThreshold: number = 0.6) { super(); this.confidenceThreshold = confidenceThreshold; } async execute(context: AgentContext): Promise { const start = Date.now(); this.log("Running security audit..."); if (context.backend) { const result = await this.executeViaBackend( context, `Perform security audit for phase ${context.phase}. Specification: ${context.specification}` ); return { ...result, duration_ms: Date.now() - start }; } const findings = this.mechanicalAudit(context.project_path); const highCount = findings.filter((f) => f.severity === "high").length; const output = this.formatFindings(findings); return { success: highCount === 0, output, artifacts_created: [], decisions: 0, escalations: highCount, duration_ms: Date.now() - start, error: highCount > 0 ? `${highCount} high-severity finding(s) require escalation` : undefined, }; } mechanicalAudit(projectPath: string): SecurityFinding[] { const findings: SecurityFinding[] = []; const srcDir = path.join(projectPath, "src"); if (!fs.existsSync(srcDir)) return findings; this.scanDirectory(srcDir, projectPath, findings); return findings; } private getDisposition(severity: SecurityFinding["severity"], confidence: number): SecurityFinding["disposition"] { if (severity === "low") return "accept"; if (confidence >= this.confidenceThreshold) return "flag"; return "mitigate"; } private scanDirectory(dir: string, projectPath: string, findings: SecurityFinding[]): void { const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== ".git") { this.scanDirectory(fullPath, projectPath, findings); } else if ( entry.isFile() && (entry.name.endsWith(".ts") || entry.name.endsWith(".js")) && !entry.name.endsWith(".test.ts") && !entry.name.endsWith(".d.ts") ) { const content = fs.readFileSync(fullPath, "utf-8"); for (const { pattern, category, cwe, description, severity, confidence } of SECURITY_PATTERNS) { pattern.lastIndex = 0; if (pattern.test(content)) { findings.push({ stride_category: category, cwe, severity, disposition: this.getDisposition(severity, confidence), file: path.relative(projectPath, fullPath), description, }); } } } } } private formatFindings(findings: SecurityFinding[]): string { if (findings.length === 0) return "No security findings — audit passed."; const lines: string[] = ["Security Audit Findings:", ""]; for (const f of findings) { lines.push(`[${f.stride_category}|${f.cwe}|${f.disposition}] ${f.severity.toUpperCase()}: ${f.description} (${f.file})`); } return lines.join("\n"); } }