90 lines
3.7 KiB
TypeScript
90 lines
3.7 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { readFileSync } from "node:fs";
|
|
import { resolve } from "node:path";
|
|
|
|
/**
|
|
* PII / secret regression test for SeedService.
|
|
*
|
|
* SeedService creates the bootstrap admin user with a hard-coded
|
|
* default password. The legacy console.log statements printed raw
|
|
* email addresses; the bigger risk is the password constant leaking
|
|
* by accident if a future change passes `password` into a logger arg.
|
|
*
|
|
* Policy:
|
|
* - never log raw `email` — always emailFingerprint(email)
|
|
* - never log `password`, `passwordHash`, `adminPassword`, or
|
|
* anything looking like a credential
|
|
* - no live console.* calls remain
|
|
*/
|
|
describe("SeedService PII discipline", () => {
|
|
const SOURCE = readFileSync(resolve(__dirname, "SeedService.ts"), "utf8");
|
|
|
|
it("contains no live console.* call sites", () => {
|
|
const live = SOURCE.split("\n").filter((line, idx, all) => {
|
|
if (!/console\.(log|error|warn|info|debug)\s*\(/.test(line)) return false;
|
|
const trimmed = line.trimStart();
|
|
if (trimmed.startsWith("//") || trimmed.startsWith("*")) return false;
|
|
const prev = idx > 0 ? all[idx - 1].trim() : "";
|
|
if (/eslint-disable-next-line[^\n]*no-console/.test(prev)) return false;
|
|
return true;
|
|
});
|
|
expect(live).toEqual([]);
|
|
});
|
|
|
|
it("never passes raw email / password fields as own seedLogger arg keys", () => {
|
|
const pattern = /seedLogger\.(info|warn|error|debug)\s*\(\s*\{([\s\S]*?)\}/g;
|
|
const forbiddenKeys = [
|
|
"email",
|
|
"password",
|
|
"passwordHash",
|
|
"adminEmail",
|
|
"adminPassword",
|
|
"rawPassword",
|
|
"plainPassword",
|
|
"secret",
|
|
"token",
|
|
];
|
|
const offenders: string[] = [];
|
|
let match: RegExpExecArray | null;
|
|
while ((match = pattern.exec(SOURCE)) !== null) {
|
|
const objContent = match[2];
|
|
for (const key of forbiddenKeys) {
|
|
const keyRe = new RegExp(`(^|[\\s,{])${key}\\s*:`, "m");
|
|
if (keyRe.test(objContent)) offenders.push(`key "${key}" in ${match[0].slice(0, 100)}...`);
|
|
}
|
|
}
|
|
expect(offenders, `seedLogger must not log PII/secret fields: ${offenders.join("; ")}`).toEqual([]);
|
|
});
|
|
|
|
it("defines and uses emailFingerprint() for any email-related diagnostics", () => {
|
|
expect(SOURCE).toMatch(/function emailFingerprint\(/);
|
|
expect(SOURCE).toMatch(/createHash\(["']sha256["']\)/);
|
|
// Anywhere we mention "email" inside a seedLogger call must be via emailFingerprint(...)
|
|
// (Aliased as emailFingerprint key — never raw email value)
|
|
expect(SOURCE).toMatch(/emailFingerprint\(email\)/);
|
|
});
|
|
|
|
it("uses createScopedLogger('seed')", () => {
|
|
expect(SOURCE).toMatch(/createScopedLogger\(\s*["']seed["']\s*\)/);
|
|
});
|
|
|
|
it("does not interpolate DEFAULT_ADMIN_PASSWORD into any log message", () => {
|
|
// Hard-coded admin password constant exists in source; ensure it never
|
|
// ends up in a logger arg object or template string.
|
|
const lines = SOURCE.split("\n");
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i];
|
|
if (/seedLogger\.|`/.test(line) && /DEFAULT_ADMIN_PASSWORD/.test(line)) {
|
|
// Only legal use of the constant: assignment line like `password: DEFAULT_ADMIN_PASSWORD` in the seed-user record.
|
|
// It must NOT appear inside a logger arg or template literal.
|
|
if (/seedLogger\./.test(line)) {
|
|
throw new Error(`SeedService PII leak: DEFAULT_ADMIN_PASSWORD used in seedLogger call at line ${i + 1}: ${line.trim()}`);
|
|
}
|
|
if (/`[^`]*\$\{[^}]*DEFAULT_ADMIN_PASSWORD/.test(line)) {
|
|
throw new Error(`SeedService PII leak: DEFAULT_ADMIN_PASSWORD interpolated in template at line ${i + 1}: ${line.trim()}`);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
});
|