docfast/src/__tests__/no-vulnerable-deps.test.ts
openclawd 2186747940
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 18m58s
security(deps): fix npm audit vulnerabilities (nodemailer CRLF, path-to-regexp ReDoS)
Resolves 7 npm audit findings (3 moderate, 4 high) via `npm audit fix`
— no --force needed, all bumps satisfied by existing semver ranges:

  basic-ftp        5.2.0   -> 5.2.2    (high: FTP command injection via CRLF)
  brace-expansion  1.1.12  -> 1.1.13   (moderate: ReDoS / mem exhaustion)
  nodemailer       8.0.3   -> 8.0.5    (high: SMTP command injection via
                                        CRLF in EHLO/HELO transport name,
                                        GHSA-vvjj-xcjg-gr5g, and envelope.size
                                        injection GHSA-c7w3-x93f-qmm8)
  path-to-regexp   8.3.0   -> 8.4.2    (high: ReDoS, GHSA-j3q9-mxjg-w52f and
                                        GHSA-27v5-c462-wpq7)
  picomatch        4.0.3   -> 4.0.4    (high: method injection + ReDoS)
  vite             0.115.0 -> 0.124.0  (high: path traversal / FS bypass,
                                        dev-only, transitive via vitest)
  yaml             2.x     -> patched  (moderate: stack overflow, dev-only)

Only package-lock.json changed — no source changes required, no API
breaks. nodemailer 8.0.5 is fully backwards-compatible with our usage
in src/services/email.ts.

Adds src/__tests__/no-vulnerable-deps.test.ts as a TDD regression guard:
runs `npm audit --omit=dev --json` and asserts
metadata.vulnerabilities.high === 0 && critical === 0. Network failures
are skipped rather than failing CI. Red→Green verified locally (stashed
lockfile -> 2 high failures; restored -> 0).

Test count: 901 -> 902 (new regression guard). npm audit: 4 high -> 0.
2026-04-10 20:09:44 +02:00

91 lines
3.3 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { execSync } from "node:child_process";
/**
* Regression guard for npm audit vulnerabilities.
*
* Runs `npm audit --omit=dev --json` and asserts that no high or critical
* severity vulnerabilities exist in the production dependency tree.
*
* Rationale: after fixing CVEs (nodemailer CRLF GHSA-vvjj-xcjg-gr5g,
* path-to-regexp ReDoS GHSA-j3q9-mxjg-w52f / GHSA-27v5-c462-wpq7, and
* related transitive issues in basic-ftp, brace-expansion, picomatch),
* we want CI to fail fast if a new high/critical vuln is introduced into
* the production dependency graph rather than discovering it later.
*
* Network access is required (audit hits the npm registry). If the
* registry is unreachable in a given environment, the test is skipped
* rather than reported as a failure — we don't want flaky network to
* break builds. All other errors (including the audit finding vulns)
* must fail loudly.
*/
describe("npm audit regression guard", () => {
it(
"has zero high or critical vulnerabilities in production dependencies",
() => {
let stdout: string;
try {
// `npm audit` exits non-zero when vulnerabilities are found, which
// makes execSync throw. We still want to parse stdout in that case,
// so we catch and inspect the error object.
stdout = execSync("npm audit --omit=dev --json", {
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
timeout: 60_000,
});
} catch (err: unknown) {
const e = err as { stdout?: Buffer | string; stderr?: Buffer | string; message?: string };
const out = typeof e.stdout === "string" ? e.stdout : e.stdout?.toString() ?? "";
const errOut = typeof e.stderr === "string" ? e.stderr : e.stderr?.toString() ?? "";
// No JSON at all? Likely a network/registry failure — skip, don't fail CI.
if (!out || !out.trim().startsWith("{")) {
const msg = (errOut || e.message || "").toLowerCase();
if (
msg.includes("enotfound") ||
msg.includes("etimedout") ||
msg.includes("econnrefused") ||
msg.includes("network") ||
msg.includes("registry")
) {
console.warn("npm audit: registry unreachable, skipping regression guard");
return;
}
throw new Error(
`npm audit did not return parseable JSON. stderr=${errOut} message=${e.message}`,
);
}
stdout = out;
}
const report = JSON.parse(stdout) as {
metadata?: {
vulnerabilities?: {
info?: number;
low?: number;
moderate?: number;
high?: number;
critical?: number;
total?: number;
};
};
};
const vulns = report.metadata?.vulnerabilities;
expect(vulns, "npm audit JSON missing metadata.vulnerabilities").toBeDefined();
const high = vulns?.high ?? 0;
const critical = vulns?.critical ?? 0;
if (high > 0 || critical > 0) {
// Surface the full report so failure output is actionable.
console.error("npm audit found high/critical vulnerabilities:");
console.error(JSON.stringify(vulns, null, 2));
}
expect(critical).toBe(0);
expect(high).toBe(0);
},
90_000,
);
});