import * as fs from "node:fs"; import * as path from "node:path"; import { VerificationLayer, VerificationResult, VerificationCheck } from "./types.js"; interface ThreatEntry { category: string; description: string; severity: "low" | "medium" | "high"; file?: string; } const SECURITY_PATTERNS: Array<{ pattern: RegExp; category: string; description: string; severity: "low" | "medium" | "high"; }> = [ { pattern: /password\s*=\s*['"][^'"]+['"]/gi, category: "spoofing", description: "Hardcoded password detected", severity: "high", }, { pattern: /api[_-]?key\s*=\s*['"][^'"]+['"]/gi, category: "information_disclosure", description: "Hardcoded API key detected", severity: "high", }, { pattern: /secret\s*=\s*['"][^'"]+['"]/gi, category: "information_disclosure", description: "Hardcoded secret detected", severity: "high", }, { pattern: /token\s*=\s*['"][^'"]+['"]/gi, category: "information_disclosure", description: "Hardcoded token detected", severity: "medium", }, { pattern: /eval\s*\(/g, category: "tampering", description: "Use of eval() — potential code injection", severity: "high", }, { pattern: /innerHTML\s*=/g, category: "tampering", description: "Use of innerHTML — potential XSS", severity: "medium", }, { pattern: /exec\s*\(/g, category: "tampering", description: "Use of exec() — potential command injection", severity: "high", }, { pattern: /spawn\s*\(/g, category: "tampering", description: "Use of spawn() — verify input sanitization", severity: "medium", }, { pattern: /http\.get\s*\(/g, category: "information_disclosure", description: "HTTP GET request — verify no sensitive data in URL", severity: "low", }, { pattern: /console\.log\(.*(?:password|token|secret|key|auth)/gi, category: "information_disclosure", 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", }, { pattern: /\.env/g, category: "information_disclosure", description: "References to .env file — ensure it's in .gitignore", severity: "low", }, ]; export class SecurityVerification extends VerificationLayer { readonly layer = 3; readonly name = "Security"; async verify(projectPath: string, phase: number): Promise { const start = Date.now(); const checks: VerificationCheck[] = []; const threats = this.scanForThreats(projectPath); const lowThreats = threats.filter((t) => t.severity === "low"); const mediumThreats = threats.filter((t) => t.severity === "medium"); const highThreats = threats.filter((t) => t.severity === "high"); checks.push(this.checkLowSeverityThreats(lowThreats)); checks.push(this.checkMediumSeverityThreats(mediumThreats)); checks.push(this.checkHighSeverityThreats(highThreats)); checks.push(this.checkGitignore(projectPath)); checks.push(this.checkDependencyVulnerabilities(projectPath)); const hasHighFail = checks.some((c) => c.status === "fail"); const passed = !hasHighFail; return { layer: this.layer, name: this.name, passed, checks, summary: `${threats.length} threats found (low: ${lowThreats.length}, medium: ${mediumThreats.length}, high: ${highThreats.length})`, duration_ms: Date.now() - start, }; } private scanForThreats(projectPath: string): ThreatEntry[] { const threats: ThreatEntry[] = []; const srcDir = path.join(projectPath, "src"); if (!fs.existsSync(srcDir)) { return threats; } this.scanDirectory(srcDir, projectPath, threats); return threats; } private scanDirectory(dir: string, projectPath: string, threats: ThreatEntry[]): 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, threats); } 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, description, severity } of SECURITY_PATTERNS) { pattern.lastIndex = 0; if (pattern.test(content)) { threats.push({ category, description: `${description} (in ${path.relative(projectPath, fullPath)})`, severity, file: path.relative(projectPath, fullPath), }); } } } } } private checkLowSeverityThreats(lowThreats: ThreatEntry[]): VerificationCheck { if (lowThreats.length === 0) { return this.check( "Low severity threats auto-accepted", "pass", "No low-severity threats detected" ); } return this.check( "Low severity threats auto-accepted", "pass", `${lowThreats.length} low-severity threat(s) auto-accepted`, lowThreats.map((t) => `${t.category}: ${t.description}`).join("\n") ); } private checkMediumSeverityThreats(mediumThreats: ThreatEntry[]): VerificationCheck { if (mediumThreats.length === 0) { return this.check( "Medium severity threats auto-mitigated", "pass", "No medium-severity threats detected" ); } const autoFixable = mediumThreats.filter((t) => t.category === "information_disclosure" || t.category === "repudiation" ); const needsReview = mediumThreats.filter( (t) => !autoFixable.includes(t) ); 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") ); } private checkHighSeverityThreats(highThreats: ThreatEntry[]): VerificationCheck { if (highThreats.length === 0) { return this.check( "High severity threats", "pass", "No high-severity threats detected" ); } return this.check( "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") ); } private checkGitignore(projectPath: string): VerificationCheck { const gitignorePath = path.join(projectPath, ".gitignore"); if (!fs.existsSync(gitignorePath)) { return this.check( ".gitignore security", "warning", "No .gitignore found — potential risk of committing secrets" ); } const content = fs.readFileSync(gitignorePath, "utf-8"); const hasEnvIgnore = content.includes(".env"); const hasNodeModules = content.includes("node_modules"); const issues: string[] = []; if (!hasEnvIgnore) issues.push(".env not in .gitignore"); if (!hasNodeModules) issues.push("node_modules not in .gitignore"); if (issues.length === 0) { return this.check(".gitignore security", "pass", "Essential patterns present in .gitignore"); } return this.check( ".gitignore security", "warning", `Missing patterns: ${issues.join(", ")}` ); } private checkDependencyVulnerabilities(projectPath: string): VerificationCheck { const packageLockPath = path.join(projectPath, "package-lock.json"); if (!fs.existsSync(packageLockPath)) { return this.check( "Dependency audit", "skipped", "No package-lock.json found — cannot audit dependencies" ); } const packageJsonPath = path.join(projectPath, "package.json"); if (!fs.existsSync(packageJsonPath)) { return this.check("Dependency audit", "skipped", "No package.json found"); } return this.check( "Dependency audit", "pass", "Dependency structure available for audit (run `npm audit` for full check)" ); } }