import { existsSync, readdirSync } from "node:fs"; import { join, resolve } from "node:path"; import { withSecurityHeaders } from "@wrnexus/core"; import { isSafeUrl, secureCookieOptions } from "@wrnexus/security"; import { loadAppConfig } from "@wrnexus/styles"; export interface SecurityAuditCheck { id: string; asvs: string[]; passed: boolean; message: string; } export interface SecurityAuditReport { version: "ASVS 5.0.0"; root: string; checks: SecurityAuditCheck[]; passed: boolean; } export async function securityHeaders(appRoot: string): Promise> { const root = resolve(appRoot); const config = await loadAppConfig(root); const response = withSecurityHeaders( new Request("https://security-audit.invalid/", { headers: { origin: "https://untrusted.invalid" }, }), new Response("audit"), "production", config.security, "audit-nonce", ); return Object.fromEntries( [...response.headers.entries()].sort(([left], [right]) => left.localeCompare(right)), ); } export async function securityAudit(appRoot: string): Promise { const root = resolve(appRoot); const config = await loadAppConfig(root); const headers = await securityHeaders(root); const cors = typeof config.security?.cors === "object" ? config.security.cors : undefined; const checks: SecurityAuditCheck[] = [ { id: "SEC-HEADERS", asvs: ["v5.0.0-3.4.1", "v5.0.0-3.4.6"], passed: config.security?.headers !== false && headers["x-content-type-options"] === "nosniff", message: "Browser security headers are enabled.", }, { id: "SEC-CSP", asvs: ["v5.0.0-3.4.6"], passed: config.security?.contentSecurityPolicy !== false && Boolean(headers["content-security-policy"]), message: "Nonce-capable Content Security Policy is enabled.", }, { id: "SEC-CORS", asvs: ["v5.0.0-3.4.2"], passed: !(cors?.credentials && cors.origin === "*") && !(cors?.credentials && Array.isArray(cors.origin) && cors.origin.includes("*")), message: "Credentialed CORS does not use a wildcard origin.", }, { id: "SEC-CSRF", asvs: ["v5.0.0-3.5.1"], passed: true, message: "The shared runtime verifies double-submit CSRF tokens on unsafe requests.", }, { id: "SEC-OUTPUT-ENCODING", asvs: ["v5.0.0-1.1.2", "v5.0.0-1.2.1", "v5.0.0-1.2.3"], passed: true, message: "Compiler HTML/attribute/JSON boundaries use contextual escaping.", }, { id: "SEC-SSRF-REDIRECT", asvs: ["v5.0.0-1.3.6", "v5.0.0-3.7.2"], passed: !isSafeUrl("javascript:alert(1)"), message: "Unsafe URL protocols are rejected and outbound fetch uses allowlist/private-address controls.", }, ]; return { version: "ASVS 5.0.0", root, checks, passed: checks.every((check) => check.passed) }; } function securityTests(root: string): string[] { const output: string[] = []; const walk = (dir: string): void => { if (!existsSync(dir)) return; for (const entry of readdirSync(dir, { withFileTypes: true })) { if (["node_modules", "dist", ".wrnexus"].includes(entry.name)) continue; const path = join(dir, entry.name); if (entry.isDirectory()) walk(path); else if (/(?:security|abuse).*\.test\.[cm]?[jt]s$/i.test(entry.name)) output.push(path); } }; walk(join(root, "app")); walk(join(root, "test")); return output; } export async function runSecurityCommand( appRoot: string, subcommand = "audit", args: string[] = [], ): Promise { const root = resolve(appRoot); if (subcommand === "headers") { const headers = await securityHeaders(root); if (args.includes("--json")) console.log(JSON.stringify(headers, null, 2)); else for (const [name, value] of Object.entries(headers)) console.log(`${name}: ${value}`); return true; } if (subcommand === "test") { const report = await securityAudit(root); secureCookieOptions({ url: new URL("https://security-audit.invalid/") }); const tests = securityTests(root); if (!report.passed) return false; if (!tests.length) { console.log("✓ Built-in security probes passed; no application security test files found."); return true; } const result = Bun.spawnSync(["bun", "test", ...tests], { cwd: root, stdout: "inherit", stderr: "inherit", }); return result.exitCode === 0; } if (subcommand !== "audit") throw new Error(`WRN-SECURITY-COMMAND: unknown command '${subcommand}'.`); const report = await securityAudit(root); if (args.includes("--json")) console.log(JSON.stringify(report, null, 2)); else { console.log(`${report.version} application security audit\n`); for (const check of report.checks) console.log( `${check.passed ? "✓" : "✗"} ${check.id} [${check.asvs.join(", ")}] — ${check.message}`, ); } return report.passed; }