mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat(enterprise): add tamper-resistant audit mode with HMAC integrity
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
* calling runSystemJob); anything else is a bug.
|
||||
*/
|
||||
import type { Job } from "bullmq";
|
||||
import { inArray, sql } from "drizzle-orm";
|
||||
import { eq, inArray, sql } from "drizzle-orm";
|
||||
import { env } from "../config.js";
|
||||
import { db, schema } from "../db/index.js";
|
||||
import { getMaxAgeMs } from "../lib/cleanup.js";
|
||||
@@ -158,8 +158,19 @@ async function retentionSweep(): Promise<void> {
|
||||
);
|
||||
}
|
||||
if (env.AUDIT_RETENTION_DAYS > 0) {
|
||||
await db.execute(
|
||||
sql`DELETE FROM audit_log WHERE created_at < now() - ${env.AUDIT_RETENTION_DAYS} * interval '1 day'`,
|
||||
);
|
||||
const tamperResult = await db
|
||||
.select({ value: schema.settings.value })
|
||||
.from(schema.settings)
|
||||
.where(eq(schema.settings.key, "tamperResistantAudit"))
|
||||
.limit(1);
|
||||
|
||||
const isTamperResistant = tamperResult.length > 0 && tamperResult[0].value === "true";
|
||||
|
||||
// Only delete audit logs if tamper-resistant mode is OFF
|
||||
if (!isTamperResistant) {
|
||||
await db.execute(
|
||||
sql`DELETE FROM audit_log WHERE created_at < now() - ${env.AUDIT_RETENTION_DAYS} * interval '1 day'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { createHmac } from "node:crypto";
|
||||
|
||||
export function canonicalize(obj: unknown): string {
|
||||
if (obj === null || obj === undefined) return "null";
|
||||
if (typeof obj !== "object") return JSON.stringify(obj);
|
||||
if (Array.isArray(obj)) return `[${obj.map(canonicalize).join(",")}]`;
|
||||
|
||||
const sorted = Object.keys(obj as Record<string, unknown>)
|
||||
.sort()
|
||||
.map((k) => `${JSON.stringify(k)}:${canonicalize((obj as Record<string, unknown>)[k])}`)
|
||||
.join(",");
|
||||
return `{${sorted}}`;
|
||||
}
|
||||
|
||||
export function computeHmac(data: Record<string, unknown>, key: Buffer): string {
|
||||
return createHmac("sha256", key).update(canonicalize(data)).digest("hex");
|
||||
}
|
||||
|
||||
export function verifyHmac(
|
||||
data: Record<string, unknown>,
|
||||
hmac: string,
|
||||
key: Buffer,
|
||||
): boolean {
|
||||
const computed = computeHmac(data, key);
|
||||
return computed === hmac;
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { FastifyBaseLogger } from "fastify";
|
||||
import { env } from "../config.js";
|
||||
import { db, schema } from "../db/index.js";
|
||||
import { computeHmac } from "./audit-integrity.js";
|
||||
import { deriveAuditHmacKey } from "./encryption.js";
|
||||
|
||||
const MAX_AUDIT_INPUT_LENGTH = 200;
|
||||
|
||||
@@ -56,9 +59,10 @@ export async function auditLog(
|
||||
const targetId = (details.targetUserId as string) ?? (details.keyId as string) ?? null;
|
||||
const targetType = deriveTargetType(event);
|
||||
|
||||
const id = randomUUID();
|
||||
try {
|
||||
await db.insert(schema.auditLog).values({
|
||||
id: randomUUID(),
|
||||
id,
|
||||
actorId,
|
||||
actorUsername,
|
||||
action: event,
|
||||
@@ -69,6 +73,38 @@ export async function auditLog(
|
||||
});
|
||||
} catch {
|
||||
logger.warn({ event }, "Failed to write audit log to DB");
|
||||
return;
|
||||
}
|
||||
|
||||
// Compute HMAC for tamper-resistant mode
|
||||
if (env.DATA_ENCRYPTION_KEY) {
|
||||
try {
|
||||
const tamperResult = await db
|
||||
.select({ value: schema.settings.value })
|
||||
.from(schema.settings)
|
||||
.where(eq(schema.settings.key, "tamperResistantAudit"))
|
||||
.limit(1);
|
||||
|
||||
if (tamperResult.length > 0 && tamperResult[0].value === "true") {
|
||||
const hmacKey = await deriveAuditHmacKey(env.DATA_ENCRYPTION_KEY);
|
||||
const rowData = {
|
||||
actorId,
|
||||
actorUsername,
|
||||
action: event,
|
||||
targetType,
|
||||
targetId,
|
||||
details,
|
||||
ipAddress: ip,
|
||||
};
|
||||
const integrity = computeHmac(rowData, hmacKey);
|
||||
await db
|
||||
.update(schema.auditLog)
|
||||
.set({ integrity })
|
||||
.where(eq(schema.auditLog.id, id));
|
||||
}
|
||||
} catch {
|
||||
logger.warn({ event }, "Failed to compute audit HMAC");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user