273 lines
8.4 KiB
TypeScript
273 lines
8.4 KiB
TypeScript
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<VerificationResult> {
|
|
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)"
|
|
);
|
|
}
|
|
} |