186 lines
6.4 KiB
TypeScript
186 lines
6.4 KiB
TypeScript
import { beforeEach, describe, expect, it } from "@suaveplan/testing/runner";
|
|
import { MemoryAuditAdapter } from "../src/adapters/memory/memory.js";
|
|
import { AuditEventType, AuditSeverity } from "../src/constants/index.js";
|
|
import { AuditLogger } from "../src/core/audit-logger/audit-logger.js";
|
|
|
|
describe("AuditLogger integration", () => {
|
|
let adapter: MemoryAuditAdapter;
|
|
let logger: AuditLogger;
|
|
|
|
beforeEach(() => {
|
|
adapter = new MemoryAuditAdapter();
|
|
logger = new AuditLogger({
|
|
storage: adapter,
|
|
secretKey: "integration-secret-key",
|
|
enableChainIntegrity: true,
|
|
});
|
|
});
|
|
|
|
it("full lifecycle: log, query, verify, export", {
|
|
timeout: 30000,
|
|
}, async () => {
|
|
// Log several events
|
|
await logger.log({
|
|
type: AuditEventType.AUTH_LOGIN,
|
|
severity: AuditSeverity.INFO,
|
|
actor: { userId: "alice" },
|
|
});
|
|
await logger.log({
|
|
type: AuditEventType.DATA_READ,
|
|
severity: AuditSeverity.INFO,
|
|
actor: { userId: "alice" },
|
|
target: { type: "report", id: "r1" },
|
|
});
|
|
await logger.log({
|
|
type: AuditEventType.AUTH_LOGOUT,
|
|
severity: AuditSeverity.INFO,
|
|
actor: { userId: "alice" },
|
|
});
|
|
await logger.log({
|
|
type: AuditEventType.AUTH_LOGIN,
|
|
severity: AuditSeverity.INFO,
|
|
actor: { userId: "bob" },
|
|
});
|
|
|
|
// Query all events
|
|
const all = await logger.query({});
|
|
expect(all.length).toBe(4);
|
|
|
|
// Query by user
|
|
const aliceEvents = await logger.query({ userId: "alice" });
|
|
expect(aliceEvents.length).toBe(3);
|
|
|
|
// Verify chain integrity
|
|
const ordered = await logger.query({ order: "asc" });
|
|
const verifyResult = await logger.verifyChainIntegrity(ordered);
|
|
expect(verifyResult.valid).toBe(true);
|
|
|
|
// GDPR export
|
|
const gdprExport = await logger.exportGDPRData("alice");
|
|
expect(gdprExport.userId).toBe("alice");
|
|
expect(gdprExport.summary.totalEvents).toBe(3);
|
|
expect(gdprExport.summary.eventTypes[AuditEventType.AUTH_LOGIN]).toBe(
|
|
1
|
|
);
|
|
});
|
|
|
|
it("chain links correctly across multiple events", {
|
|
timeout: 30000,
|
|
}, async () => {
|
|
let tick = 0;
|
|
const chainLogger = new AuditLogger({
|
|
storage: adapter,
|
|
secretKey: "integration-secret-key",
|
|
enableChainIntegrity: true,
|
|
clock: () => new Date("2024-01-01").getTime() + tick++ * 1000,
|
|
});
|
|
|
|
const events = [];
|
|
for (let i = 0; i < 5; i++) {
|
|
events.push(
|
|
await chainLogger.log({
|
|
type: AuditEventType.DATA_READ,
|
|
severity: AuditSeverity.INFO,
|
|
actor: { userId: `user-${i}` },
|
|
})
|
|
);
|
|
}
|
|
|
|
// Verify each event links to previous
|
|
for (let i = 1; i < events.length; i++) {
|
|
expect(events[i]?.previousHash).toBe(events[i - 1]?.hash);
|
|
}
|
|
|
|
const stored = await chainLogger.query({ order: "asc" });
|
|
const chainEvents = stored.slice(-5); // last 5 we logged
|
|
const result = await chainLogger.verifyChainIntegrity(chainEvents);
|
|
expect(result.valid).toBe(true);
|
|
});
|
|
|
|
it("retention policy lifecycle", { timeout: 30000 }, async () => {
|
|
const now = new Date("2024-12-31").getTime();
|
|
const retentionLogger = new AuditLogger({
|
|
storage: adapter,
|
|
secretKey: "secret",
|
|
enableChainIntegrity: false,
|
|
retentionPolicy: {
|
|
retentionDays: 1,
|
|
archiveBeforeDelete: true,
|
|
archiveLocation: "s3://archive",
|
|
},
|
|
clock: () => now,
|
|
});
|
|
|
|
// Add old event
|
|
await adapter.save({
|
|
id: "old-event",
|
|
type: AuditEventType.AUTH_LOGIN,
|
|
severity: AuditSeverity.INFO,
|
|
actor: {},
|
|
timestamp: new Date("2024-01-01"),
|
|
hash: "h",
|
|
});
|
|
|
|
const result = await retentionLogger.applyRetentionPolicy();
|
|
expect(result.eventsArchived).toBe(1);
|
|
expect(result.eventsDeleted).toBe(0); // already moved to archive
|
|
expect(adapter.snapshot().length).toBe(0);
|
|
expect(adapter.archivedSnapshot().length).toBe(1);
|
|
});
|
|
|
|
it("multi-tenant chain isolation", { timeout: 30000 }, async () => {
|
|
// Each tenant gets its own chain: getLatestEvent(tenantId) scopes the lookup
|
|
await logger.log({
|
|
type: AuditEventType.AUTH_LOGIN,
|
|
severity: AuditSeverity.INFO,
|
|
actor: { userId: "u1" },
|
|
tenantId: "t1",
|
|
});
|
|
await logger.log({
|
|
type: AuditEventType.AUTH_LOGIN,
|
|
severity: AuditSeverity.INFO,
|
|
actor: { userId: "u2" },
|
|
tenantId: "t2",
|
|
});
|
|
await logger.log({
|
|
type: AuditEventType.DATA_READ,
|
|
severity: AuditSeverity.INFO,
|
|
actor: { userId: "u1" },
|
|
tenantId: "t1",
|
|
});
|
|
|
|
const t1Events = await logger.query({ tenantId: "t1", order: "asc" });
|
|
const t2Events = await logger.query({ tenantId: "t2", order: "asc" });
|
|
|
|
// First t1 event has no predecessor → no previousHash
|
|
expect(t1Events[0]?.previousHash).toBeUndefined();
|
|
// Second t1 event links to first t1 event
|
|
expect(t1Events[1]?.previousHash).toBe(t1Events[0]?.hash);
|
|
// t2 only has one event, so no previousHash
|
|
expect(t2Events[0]?.previousHash).toBeUndefined();
|
|
});
|
|
|
|
it("tamper detection in integration", { timeout: 30000 }, async () => {
|
|
await logger.log({
|
|
type: AuditEventType.AUTH_LOGIN,
|
|
severity: AuditSeverity.INFO,
|
|
actor: { userId: "u1" },
|
|
});
|
|
await logger.log({
|
|
type: AuditEventType.DATA_READ,
|
|
severity: AuditSeverity.INFO,
|
|
actor: { userId: "u1" },
|
|
});
|
|
|
|
const events = await logger.query({ order: "asc" });
|
|
|
|
// Tamper with first event
|
|
const tampered = [...events];
|
|
tampered[0] = { ...events[0]!, actor: { userId: "attacker" } };
|
|
|
|
const result = await logger.verifyChainIntegrity(tampered);
|
|
expect(result.valid).toBe(false);
|
|
expect(result.brokenAt).toBe(0);
|
|
});
|
|
});
|