feat(P05): flesh 4 agents with intrinsic mechanical logic
---ci---
project: ci
phase: 5
milestone: v0.8
status: complete
decisions:
- id: D-033
decision: Flesh SecurityAuditorAgent with STRIDE-aware mechanical scanning
rationale: Runs L3 security patterns intrinsically; no backend required
confidence: 0.90
- id: D-034
decision: Flesh DocWriterAgent with template-based doc update
rationale: Updates ROADMAP.md phase status, REQUIREMENTS.md req status, reads git log for new decisions
confidence: 0.85
- id: D-035
decision: Flesh DebuggerAgent with stack trace parsing + git bisect
rationale: Parses stack traces to find file:line, bisects to find introducing commit
confidence: 0.80
- id: D-036
decision: Flesh ChallengerAgent with plan DAG/wave/must-have/REQ validation
rationale: Validates plan structure mechanically; catches circular deps and gaps
confidence: 0.82
requirements:
covered: [AGENT-01, AGENT-02, AGENT-03, AGENT-04]
---/ci---
AGENT-01: SecurityAuditorAgent.mechanicalAudit() runs STRIDE+ CWE pattern
scan intrinsically. Each finding has stride_category, cwe, severity, and
disposition (accept/mitigate/flag based on confidence threshold).
AGENT-02: DocWriterAgent.mechanicalDocUpdate() reads plan data, updates
.ciagent/ROADMAP.md phase status to complete, .ciagent/REQUIREMENTS.md
pending→covered, and reads git log for new decision entries.
AGENT-03: DebuggerAgent.mechanicalDebug() parses stack traces (4 regex
patterns for different formats), identifies root file:line, runs
git bisect to find introducing commit, suggests git revert.
AGENT-04: ChallengerAgent.mechanicalChallenge() validates plan structure:
circular dependency detection via DFS, wave ordering validation,
must-haves presence check, and requirement coverage check.
This commit is contained in:
@@ -1,13 +1,52 @@
|
||||
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<AgentResult> {
|
||||
const start = Date.now();
|
||||
this.log("Running security audit...");
|
||||
|
||||
if (context.backend) {
|
||||
const result = await this.executeViaBackend(
|
||||
context,
|
||||
@@ -15,14 +54,74 @@ export class SecurityAuditorAgent extends BaseAgent {
|
||||
);
|
||||
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: false,
|
||||
output: "Security auditing requires an intelligence backend. Configure one with: ci init --backend",
|
||||
success: highCount === 0,
|
||||
output,
|
||||
artifacts_created: [],
|
||||
decisions: 0,
|
||||
escalations: 0,
|
||||
escalations: highCount,
|
||||
duration_ms: Date.now() - start,
|
||||
error: "No intelligence backend available",
|
||||
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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user