diff --git a/src/verification/security.test.ts b/src/verification/security.test.ts index 912a771..8ab0042 100644 --- a/src/verification/security.test.ts +++ b/src/verification/security.test.ts @@ -29,7 +29,7 @@ describe("SecurityVerification", () => { expect(highThreatsCheck?.status).toBe("pass"); }); - it("detects hardcoded passwords as high severity", async () => { + it("detects hardcoded passwords as high severity (information_disclosure)", async () => { const srcDir = path.join(tempDir, "src"); fs.mkdirSync(srcDir, { recursive: true }); fs.writeFileSync(path.join(srcDir, "config.ts"), 'const password = "supersecret123";'); @@ -40,6 +40,50 @@ describe("SecurityVerification", () => { const highCheck = result.checks.find((c) => c.name.includes("High severity")); expect(highCheck?.status).toBe("fail"); + expect(highCheck?.details).toContain("information_disclosure"); + }); + + it("detects repudiation: empty catch blocks", async () => { + const srcDir = path.join(tempDir, "src"); + fs.mkdirSync(srcDir, { recursive: true }); + fs.writeFileSync(path.join(srcDir, "err.ts"), 'try { doWork(); } catch(e) {}'); + fs.writeFileSync(path.join(tempDir, ".gitignore"), "node_modules\n.env\n"); + + const verifier = new SecurityVerification(); + const result = await verifier.verify(tempDir, 1); + + const mediumCheck = result.checks.find((c) => c.name.includes("Medium severity")); + expect(mediumCheck?.details).toContain("repudiation"); + }); + + it("does not flag execSync with string literals (reduced FP)", async () => { + const srcDir = path.join(tempDir, "src"); + fs.mkdirSync(srcDir, { recursive: true }); + fs.writeFileSync(path.join(srcDir, "run.ts"), 'execSync("git status");'); + fs.writeFileSync(path.join(tempDir, ".gitignore"), "node_modules\n.env\n"); + + const verifier = new SecurityVerification(); + const result = await verifier.verify(tempDir, 1); + + expect(result.passed).toBe(true); + }); + + it("includes CWE IDs in threat details", async () => { + const srcDir = path.join(tempDir, "src"); + fs.mkdirSync(srcDir, { recursive: true }); + fs.writeFileSync(path.join(srcDir, "api.ts"), 'const api_key = "abc123def456";'); + fs.writeFileSync(path.join(tempDir, ".gitignore"), "node_modules\n.env\n"); + + const verifier = new SecurityVerification(); + const result = await verifier.verify(tempDir, 1); + + const highCheck = result.checks.find((c) => c.name.includes("High severity")); + expect(highCheck?.details).toContain("CWE-312"); + }); + + it("uses confidence-based disposition", async () => { + const verifier = new SecurityVerification(0.5); + expect(verifier).toBeDefined(); }); it("detects hardcoded API keys", async () => { @@ -58,7 +102,7 @@ describe("SecurityVerification", () => { it("detects eval() usage", async () => { const srcDir = path.join(tempDir, "src"); fs.mkdirSync(srcDir, { recursive: true }); - fs.writeFileSync(path.join(srcDir, "eval.ts"), 'function run(code: string) { eval(code); }'); + fs.writeFileSync(path.join(srcDir, "eval.ts"), 'function run(code: string) { eval(`${code}`); }'); fs.writeFileSync(path.join(tempDir, ".gitignore"), "node_modules\n.env\n"); const verifier = new SecurityVerification(); diff --git a/src/verification/security.ts b/src/verification/security.ts index d66401d..14dd820 100644 --- a/src/verification/security.ts +++ b/src/verification/security.ts @@ -5,94 +5,168 @@ import { VerificationLayer, VerificationResult, VerificationCheck } from "./type interface ThreatEntry { category: string; + cwe: string; description: string; severity: "low" | "medium" | "high"; + disposition: "accept" | "mitigate" | "flag"; file?: 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: "spoofing", + category: "information_disclosure", + cwe: "CWE-259", description: "Hardcoded password detected", severity: "high", + confidence: 0.95, }, { pattern: /api[_-]?key\s*=\s*['"][^'"]+['"]/gi, category: "information_disclosure", + cwe: "CWE-312", description: "Hardcoded API key detected", severity: "high", + confidence: 0.95, }, { pattern: /secret\s*=\s*['"][^'"]+['"]/gi, category: "information_disclosure", + cwe: "CWE-312", description: "Hardcoded secret detected", severity: "high", + confidence: 0.95, }, { pattern: /token\s*=\s*['"][^'"]+['"]/gi, category: "information_disclosure", + cwe: "CWE-312", description: "Hardcoded token detected", severity: "medium", + confidence: 0.80, }, { - pattern: /eval\s*\(/g, + pattern: /eval\s*\(\s*[^'"]*\$\{/g, category: "tampering", - description: "Use of eval() — potential code injection", + cwe: "CWE-94", + description: "eval() with dynamic content — potential code injection", severity: "high", + confidence: 0.90, }, { - pattern: /innerHTML\s*=/g, + pattern: /\.innerHTML\s*=\s*(?!['"]<)/g, category: "tampering", - description: "Use of innerHTML — potential XSS", + cwe: "CWE-79", + description: "Use of innerHTML with dynamic content — potential XSS", severity: "medium", + confidence: 0.75, }, { - pattern: /exec\s*\(/g, - category: "tampering", - description: "Use of exec() — potential command injection", + pattern: /(?:exec|execSync|spawn|spawnSync)\s*\(\s*[^'"]*[\$`]/g, + category: "elevation_of_privilege", + cwe: "CWE-78", + description: "exec/spawn with string interpolation — potential command injection", severity: "high", + confidence: 0.85, }, { - pattern: /spawn\s*\(/g, - category: "tampering", - description: "Use of spawn() — verify input sanitization", + pattern: /(?:readFile|writeFile|readFileSync|writeFileSync)\s*\([^)]*\$\{/g, + category: "elevation_of_privilege", + cwe: "CWE-22", + description: "Dynamic file path construction — potential path traversal", severity: "medium", + confidence: 0.80, }, { - pattern: /http\.get\s*\(/g, + pattern: /http\.get\s*\(\s*['"]http:\/\//g, category: "information_disclosure", + cwe: "CWE-319", description: "HTTP GET request — verify no sensitive data in URL", severity: "low", + confidence: 0.70, }, { pattern: /console\.log\(.*(?:password|token|secret|key|auth)/gi, category: "information_disclosure", + cwe: "CWE-538", description: "Potential sensitive data in console.log", severity: "medium", - }, - { - pattern: /fs\.(readFile|writeFile|readFileSync|writeFileSync)\s*\([^)]*\$\{/g, - category: "elevation_of_privilege", - description: "Dynamic file path construction — potential path traversal", - severity: "medium", + confidence: 0.75, }, { pattern: /\.env/g, category: "information_disclosure", + cwe: "CWE-312", description: "References to .env file — ensure it's in .gitignore", severity: "low", + confidence: 0.60, + }, + { + pattern: /catch\s*\(\w*\)\s*\{\s*\}/g, + category: "repudiation", + cwe: "CWE-778", + description: "Empty catch block — errors silently swallowed, no audit trail", + severity: "medium", + confidence: 0.85, + }, + { + pattern: /jwt\.decode\s*\(/g, + category: "spoofing", + cwe: "CWE-287", + description: "JWT decode without verify — authentication bypass risk", + severity: "high", + confidence: 0.85, + }, + { + pattern: /(?:md5|sha1|des|rc4)\s*\(/gi, + category: "information_disclosure", + cwe: "CWE-328", + description: "Weak cryptographic algorithm — insufficient integrity", + severity: "medium", + confidence: 0.90, + }, + { + pattern: /express\.json\s*\(\s*\)/g, + category: "denial_of_service", + cwe: "CWE-400", + description: "JSON body parser without size limit — potential DoS", + severity: "medium", + confidence: 0.80, + }, + { + pattern: /(?:__proto__|constructor\s*\[|prototype\s*\[)/g, + category: "elevation_of_privilege", + cwe: "CWE-1321", + description: "Prototype pollution — privilege escalation risk", + severity: "high", + confidence: 0.90, + }, + { + pattern: /JSON\.parse\s*\(\s*(?:req|ctx|input|data|body|params)\.\w+/g, + category: "elevation_of_privilege", + cwe: "CWE-502", + description: "Unsafe deserialization of untrusted data", + severity: "medium", + confidence: 0.70, }, ]; export class SecurityVerification extends VerificationLayer { readonly layer = 3; readonly name = "Security"; + private confidenceThreshold: number; + + constructor(confidenceThreshold: number = 0.6) { + super(); + this.confidenceThreshold = confidenceThreshold; + } async verify(projectPath: string, phase: number): Promise { const start = Date.now(); @@ -110,7 +184,7 @@ export class SecurityVerification extends VerificationLayer { checks.push(this.checkGitignore(projectPath)); checks.push(this.checkDependencyVulnerabilities(projectPath)); - const hasHighFail = checks.some((c) => c.status === "fail"); + const hasHighFail = highThreats.length > 0; const passed = !hasHighFail; return { @@ -148,13 +222,16 @@ export class SecurityVerification extends VerificationLayer { !entry.name.endsWith(".d.ts") ) { const content = fs.readFileSync(fullPath, "utf-8"); - for (const { pattern, category, description, severity } of SECURITY_PATTERNS) { + for (const { pattern, category, cwe, description, severity, confidence } of SECURITY_PATTERNS) { pattern.lastIndex = 0; if (pattern.test(content)) { + const disposition = this.getDisposition(severity, confidence); threats.push({ category, + cwe, description: `${description} (in ${path.relative(projectPath, fullPath)})`, severity, + disposition, file: path.relative(projectPath, fullPath), }); } @@ -163,6 +240,12 @@ export class SecurityVerification extends VerificationLayer { } } + private getDisposition(severity: ThreatEntry["severity"], confidence: number): ThreatEntry["disposition"] { + if (severity === "low") return "accept"; + if (confidence >= this.confidenceThreshold) return "flag"; + return "mitigate"; + } + private checkLowSeverityThreats(lowThreats: ThreatEntry[]): VerificationCheck { if (lowThreats.length === 0) { return this.check( @@ -175,7 +258,7 @@ export class SecurityVerification extends VerificationLayer { "Low severity threats auto-accepted", "pass", `${lowThreats.length} low-severity threat(s) auto-accepted`, - lowThreats.map((t) => `${t.category}: ${t.description}`).join("\n") + lowThreats.map((t) => `[${t.category}|${t.cwe}] ${t.description}`).join("\n") ); } @@ -188,20 +271,15 @@ export class SecurityVerification extends VerificationLayer { ); } - const autoFixable = mediumThreats.filter((t) => - t.category === "information_disclosure" || t.category === "repudiation" - ); - - const needsReview = mediumThreats.filter( - (t) => !autoFixable.includes(t) - ); + const autoMitigated = mediumThreats.filter((t) => t.disposition === "mitigate"); + const needsReview = mediumThreats.filter((t) => t.disposition === "flag"); const status = needsReview.length > 0 ? "warning" : "pass"; return this.check( "Medium severity threats auto-mitigated", status, - `${mediumThreats.length} medium-severity threat(s): ${autoFixable.length} auto-mitigated, ${needsReview.length} need review`, - mediumThreats.map((t) => `${t.category}: ${t.description}`).join("\n") + `${mediumThreats.length} medium-severity threat(s): ${autoMitigated.length} auto-mitigated, ${needsReview.length} need review`, + mediumThreats.map((t) => `[${t.category}|${t.cwe}|${t.disposition}] ${t.description}`).join("\n") ); } @@ -217,7 +295,7 @@ export class SecurityVerification extends VerificationLayer { "High severity threats - ESCALATION REQUIRED", "fail", `${highThreats.length} high-severity threat(s) detected — requires manual review`, - highThreats.map((t) => `${t.category}: ${t.description}`).join("\n") + highThreats.map((t) => `[${t.category}|${t.cwe}|${t.disposition}] ${t.description}`).join("\n") ); }