94 lines
3.2 KiB
TypeScript
94 lines
3.2 KiB
TypeScript
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
import request from "supertest";
|
|
import type { FastifyInstance } from "fastify";
|
|
import { buildApp } from "../src/server";
|
|
|
|
let authApp: FastifyInstance;
|
|
let csrfApp: FastifyInstance;
|
|
const capturedEvents: Array<Record<string, unknown>> = [];
|
|
|
|
function attachSecurityEventCapture(app: FastifyInstance) {
|
|
const logger = app.log as any;
|
|
const originalChild = logger.child.bind(logger);
|
|
logger.child = (...args: any[]) => {
|
|
const child = originalChild(...args);
|
|
const originalWarn = child.warn.bind(child);
|
|
child.warn = (obj: unknown, ...rest: unknown[]) => {
|
|
if (obj && typeof obj === "object") {
|
|
const payload = obj as Record<string, unknown>;
|
|
if (typeof payload.securityEvent === "string") capturedEvents.push(payload);
|
|
}
|
|
return originalWarn(obj, ...rest);
|
|
};
|
|
return child;
|
|
};
|
|
}
|
|
|
|
beforeAll(async () => {
|
|
authApp = await buildApp({ AUTH_DISABLED: false, SEED_DEFAULT_BUDGET: false });
|
|
await authApp.ready();
|
|
(authApp as any).ensureUser = async () => undefined;
|
|
attachSecurityEventCapture(authApp);
|
|
|
|
csrfApp = await buildApp({ AUTH_DISABLED: true, SEED_DEFAULT_BUDGET: false });
|
|
await csrfApp.ready();
|
|
(csrfApp as any).ensureUser = async () => undefined;
|
|
attachSecurityEventCapture(csrfApp);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
if (authApp) await authApp.close();
|
|
if (csrfApp) await csrfApp.close();
|
|
});
|
|
|
|
describe("A09 Security Logging and Monitoring Failures", () => {
|
|
it("emits structured security log for unauthenticated protected-route access", async () => {
|
|
capturedEvents.length = 0;
|
|
|
|
const res = await request(authApp.server).get("/dashboard");
|
|
expect(res.status).toBe(401);
|
|
|
|
const event = capturedEvents.find((payload) => {
|
|
return payload.securityEvent === "auth.unauthenticated_request";
|
|
});
|
|
expect(event).toBeTruthy();
|
|
expect(event?.outcome).toBe("failure");
|
|
expect(typeof event?.requestId).toBe("string");
|
|
expect(typeof event?.ip).toBe("string");
|
|
});
|
|
|
|
it("emits structured security log for csrf validation failures", async () => {
|
|
capturedEvents.length = 0;
|
|
|
|
const res = await request(csrfApp.server)
|
|
.post("/me")
|
|
.set("x-user-id", `csrf-user-${Date.now()}`)
|
|
.send({ displayName: "NoCsrf" });
|
|
|
|
expect(res.status).toBe(403);
|
|
expect(res.body.code).toBe("CSRF");
|
|
|
|
const event = capturedEvents.find((payload) => payload.securityEvent === "csrf.validation");
|
|
expect(event).toBeTruthy();
|
|
expect(event?.outcome).toBe("failure");
|
|
expect(typeof event?.requestId).toBe("string");
|
|
expect(typeof event?.ip).toBe("string");
|
|
});
|
|
|
|
it("emits structured security log for forgot-password requests without raw token data", async () => {
|
|
capturedEvents.length = 0;
|
|
|
|
const res = await request(authApp.server)
|
|
.post("/auth/forgot-password/request")
|
|
.send({ email: `missing-${Date.now()}@test.dev` });
|
|
|
|
expect(res.status).toBe(200);
|
|
const event = capturedEvents.find(
|
|
(payload) => payload.securityEvent === "auth.password_reset.request"
|
|
);
|
|
expect(event).toBeTruthy();
|
|
expect(event?.outcome).toBe("success");
|
|
expect(event && "token" in event).toBe(false);
|
|
});
|
|
});
|