release: WRNexusJS 0.8.0
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
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<Record<string, string>> {
|
||||
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<SecurityAuditReport> {
|
||||
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<boolean> {
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user