2026-04-22 18:10:04 +08:00
|
|
|
import { randomUUID } from "node:crypto";
|
2026-03-28 11:19:09 +08:00
|
|
|
import type { FastifyBaseLogger } from "fastify";
|
2026-04-22 18:10:04 +08:00
|
|
|
import { db, schema } from "../db/index.js";
|
2026-03-28 11:19:09 +08:00
|
|
|
|
2026-06-07 21:54:27 +08:00
|
|
|
const MAX_AUDIT_INPUT_LENGTH = 200;
|
|
|
|
|
|
|
|
|
|
export function sanitizeAuditInput(raw: string): string {
|
|
|
|
|
return raw.replace(/[<>&"']/g, "").slice(0, MAX_AUDIT_INPUT_LENGTH) || "(empty)";
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-28 11:19:09 +08:00
|
|
|
/**
|
|
|
|
|
* Emit a structured audit log entry for security-relevant events.
|
|
|
|
|
*
|
2026-06-13 16:27:53 +08:00
|
|
|
* Dual-writes: structured stdout log (for aggregators) + DB row.
|
2026-03-28 11:19:09 +08:00
|
|
|
*/
|
2026-06-13 10:15:23 +08:00
|
|
|
export async function auditLog(
|
2026-03-28 11:19:09 +08:00
|
|
|
logger: FastifyBaseLogger,
|
2026-06-13 16:27:53 +08:00
|
|
|
event: string,
|
2026-03-28 11:19:09 +08:00
|
|
|
details: Record<string, unknown> = {},
|
2026-06-13 16:25:58 +08:00
|
|
|
ip: string | null = null,
|
2026-06-13 10:15:23 +08:00
|
|
|
): Promise<void> {
|
2026-06-13 16:25:58 +08:00
|
|
|
logger.info({ audit: true, event, ip, ...details }, `[AUDIT] ${event}`);
|
2026-04-22 18:10:04 +08:00
|
|
|
|
|
|
|
|
const actorId = (details.userId as string) ?? (details.adminId as string) ?? null;
|
|
|
|
|
const actorUsername = (details.username as string) ?? (details.newUsername as string) ?? "system";
|
|
|
|
|
const targetId = (details.targetUserId as string) ?? (details.keyId as string) ?? null;
|
|
|
|
|
const targetType = deriveTargetType(event);
|
|
|
|
|
|
|
|
|
|
try {
|
2026-06-13 10:15:23 +08:00
|
|
|
await db.insert(schema.auditLog).values({
|
|
|
|
|
id: randomUUID(),
|
|
|
|
|
actorId,
|
|
|
|
|
actorUsername,
|
|
|
|
|
action: event,
|
|
|
|
|
targetType,
|
|
|
|
|
targetId,
|
|
|
|
|
details,
|
2026-06-13 16:25:58 +08:00
|
|
|
ipAddress: ip,
|
2026-06-13 10:15:23 +08:00
|
|
|
});
|
2026-04-22 18:10:04 +08:00
|
|
|
} catch {
|
|
|
|
|
logger.warn({ event }, "Failed to write audit log to DB");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-13 16:27:53 +08:00
|
|
|
function deriveTargetType(event: string): string | null {
|
2026-04-22 18:10:04 +08:00
|
|
|
if (
|
|
|
|
|
event.startsWith("USER_") ||
|
|
|
|
|
event.startsWith("LOGIN") ||
|
|
|
|
|
event.startsWith("PASSWORD") ||
|
2026-05-13 18:52:39 +08:00
|
|
|
event.startsWith("OIDC_") ||
|
2026-06-13 16:27:53 +08:00
|
|
|
event.startsWith("SAML_") ||
|
|
|
|
|
event.startsWith("SCIM_") ||
|
|
|
|
|
event.startsWith("MFA_") ||
|
2026-04-22 18:10:04 +08:00
|
|
|
event === "LOGOUT"
|
|
|
|
|
)
|
|
|
|
|
return "user";
|
|
|
|
|
if (event.startsWith("API_KEY")) return "api_key";
|
|
|
|
|
if (event.startsWith("FILE")) return "file";
|
|
|
|
|
if (event.startsWith("ROLE")) return "role";
|
2026-06-13 16:27:53 +08:00
|
|
|
if (event === "SETTINGS_UPDATED" || event === "IP_ALLOWLIST_UPDATED") return "setting";
|
|
|
|
|
if (event.startsWith("TOOL_") || event.startsWith("BATCH_") || event.startsWith("PIPELINE_"))
|
|
|
|
|
return "tool";
|
|
|
|
|
if (event.startsWith("LEGAL_HOLD")) return "compliance";
|
|
|
|
|
if (event.startsWith("SIEM_") || event.startsWith("WEBHOOK_")) return "integration";
|
2026-04-22 18:10:04 +08:00
|
|
|
return null;
|
2026-03-28 11:19:09 +08:00
|
|
|
}
|