Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions core/audit/__tests__/compliance-snapshot.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { describe, expect, it } from "vitest";
import {
COMPLIANCE_SNAPSHOT_SCHEMA_VERSION,
ComplianceSnapshotStore,
canonicalJson,
createComplianceSnapshot,
verifyComplianceSnapshot,
type ComplianceSnapshot,
type ComplianceSnapshotInput,
type SnapshotStorage
} from "../compliance-snapshot";

class MemoryStorage implements SnapshotStorage {
readonly values = new Map<string, string>();

getItem(key: string) {
return this.values.get(key) ?? null;
}

setItem(key: string, value: string) {
this.values.set(key, value);
}
}

const input: ComplianceSnapshotInput = {
eventId: "evt-1",
event: "authorization.revoked",
capturedAt: "2026-10-18T10:00:00.000Z",
activeRules: { maxFeeStroops: 500, requireSignature: true },
state: { holders: 3 }
};

function created(): ComplianceSnapshot {
const result = createComplianceSnapshot(input);
if (!result.ok) throw new Error(`expected snapshot, got ${result.code}`);
return result.value;
}

describe("canonicalJson", () => {
it("sorts keys at every depth so equal data serialises identically", () => {
expect(canonicalJson({ b: 1, a: { d: 2, c: 3 } })).toBe('{"a":{"c":3,"d":2},"b":1}');
});
});

describe("createComplianceSnapshot", () => {
it("stores a canonical payload with schema version and integrity hash", () => {
const snapshot = created();

expect(snapshot.schemaVersion).toBe(COMPLIANCE_SNAPSHOT_SCHEMA_VERSION);
expect(JSON.parse(snapshot.payload)).toMatchObject({ eventId: "evt-1", state: { holders: 3 } });
expect(snapshot.integrityHash).toHaveLength(64);
});

it("produces the same hash for the same input", () => {
expect(created().integrityHash).toBe(created().integrityHash);
});

it("rejects an empty event id", () => {
const result = createComplianceSnapshot({ ...input, eventId: " " });

expect(result).toEqual({ ok: false, code: "invalid_snapshot_input" });
});
});

describe("verifyComplianceSnapshot", () => {
it("accepts an untouched snapshot", () => {
expect(verifyComplianceSnapshot(created())).toEqual({ ok: true, value: true });
});

it("detects a payload that was altered after capture", () => {
const snapshot = created();
const tampered = { ...snapshot, payload: snapshot.payload.replace("\"holders\":3", "\"holders\":4") };

expect(verifyComplianceSnapshot(tampered)).toEqual({ ok: false, code: "snapshot_tampered" });
});

it("refuses an unknown schema version", () => {
const snapshot = { ...created(), schemaVersion: 99 };

expect(verifyComplianceSnapshot(snapshot)).toEqual({ ok: false, code: "unsupported_schema_version" });
});
});

describe("ComplianceSnapshotStore", () => {
it("captures once per event and returns the original on repeat capture", () => {
const storage = new MemoryStorage();
const store = new ComplianceSnapshotStore(storage);

const first = store.capture(input);
const second = store.capture({ ...input, state: { holders: 99 } });

expect(first.ok && second.ok).toBe(true);
if (!first.ok || !second.ok) return;
expect(second.value.payload).toBe(first.value.payload);
expect(storage.values.size).toBe(1);
});

it("returns the stored snapshot to a maintainer after verifying it", () => {
const store = new ComplianceSnapshotStore(new MemoryStorage());
store.capture(input);

const read = store.read("evt-1", "maintainer");

expect(read.ok).toBe(true);
});

it("denies snapshot reads to the own scope", () => {
const store = new ComplianceSnapshotStore(new MemoryStorage());
store.capture(input);

expect(store.read("evt-1", "own")).toEqual({ ok: false, code: "audit_denied" });
});

it("reports tampering when the stored envelope was modified", () => {
const storage = new MemoryStorage();
const store = new ComplianceSnapshotStore(storage);
store.capture(input);
const key = "utix:compliance-snapshot:v1:evt-1";
const stored = JSON.parse(storage.getItem(key) ?? "{}") as ComplianceSnapshot;
storage.setItem(key, JSON.stringify({ ...stored, payload: stored.payload.replace("true", "false") }));

expect(store.read("evt-1", "maintainer")).toEqual({ ok: false, code: "snapshot_tampered" });
});

it("reports not found for an unknown event", () => {
const store = new ComplianceSnapshotStore(new MemoryStorage());

expect(store.read("missing", "maintainer")).toEqual({ ok: false, code: "snapshot_not_found" });
});
});
14 changes: 13 additions & 1 deletion core/audit/audit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,19 @@ export const SENSITIVE_ACTIONS = [
/** A delegated permission was revoked (or a revocation was refused). */
"authorization.revoked",
/** A stale delegated permission was removed by cleanup (or cleanup was refused). */
"authorization.cleaned_up"
"authorization.cleaned_up",
/** A recipient's notification preferences were changed (or the change was refused). */
"notification_preferences.updated",
/** A recipient's notification preferences were reset to defaults. */
"notification_preferences.reset",
/** An optional notification was suppressed by the recipient's preferences. */
"notification.suppressed_by_preference",
/** A mandatory alert was delivered despite the recipient's preferences. */
"notification.mandatory_override",
/** A freeze window was bypassed for an emergency action. */
"freeze.bypassed",
/** An emergency bypass of a freeze window was refused. */
"freeze.bypass_denied"
] as const;

export type AuditAction = (typeof SENSITIVE_ACTIONS)[number];
Expand Down
188 changes: 188 additions & 0 deletions core/audit/compliance-snapshot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/**
* Immutable compliance snapshots for critical lifecycle events.
*
* A snapshot freezes the rules and data that were active when a critical event
* happened. The payload is canonical JSON (sorted keys) and carries a SHA-256
* integrity hash, so a later audit can prove the record was not altered.
*/
import { err, ok, type Result } from "@/core/result/result";
import { sha256Hex } from "@/core/ledger/sha256";
import type { AuditAction } from "./audit";

export const COMPLIANCE_SNAPSHOT_SCHEMA_VERSION = 1;

/** Lifecycle events that must be captured. Each one is also a sensitive audit action. */
export const CRITICAL_SNAPSHOT_EVENTS: readonly AuditAction[] = [
"admin.config_changed",
"authorization.granted",
"authorization.revoked",
"projection.activated",
"reconciliation.repaired",
"quota.override_granted",
"freeze.bypassed"
];

export type CriticalSnapshotEvent = AuditAction;

export type ComplianceSnapshotErrorCode =
| "invalid_snapshot_input"
| "snapshot_not_found"
| "snapshot_tampered"
| "unsupported_schema_version"
| "audit_denied";

export interface ComplianceSnapshotInput {
readonly eventId: string;
readonly event: CriticalSnapshotEvent;
readonly capturedAt: string;
readonly activeRules: Record<string, unknown>;
readonly state: Record<string, unknown>;
}

export interface ComplianceSnapshot {
readonly eventId: string;
readonly schemaVersion: number;
readonly event: CriticalSnapshotEvent;
readonly capturedAt: string;
/** Canonical JSON of the full payload. This is the exact text that was hashed. */
readonly payload: string;
readonly integrityHash: string;
}

export interface SnapshotStorage {
getItem(key: string): string | null;
setItem(key: string, value: string): void;
}

export const COMPLIANCE_SNAPSHOT_PREFIX = "utix:compliance-snapshot:v1:";

/** Stable JSON: object keys sorted at every depth so equal data always hashes equally. */
export function canonicalJson(value: unknown): string {
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
if (value !== null && typeof value === "object") {
const record = value as Record<string, unknown>;
const members = Object.keys(record)
.sort()
.map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`);
return `{${members.join(",")}}`;
}
return JSON.stringify(value) ?? "null";
}

function isNonEmpty(value: string): boolean {
return value.trim().length > 0 && value.length <= 128;
}

export function createComplianceSnapshot(
input: ComplianceSnapshotInput
): Result<ComplianceSnapshot, ComplianceSnapshotErrorCode> {
if (!isNonEmpty(input.eventId) || !isNonEmpty(input.capturedAt)) {
return err("invalid_snapshot_input");
}

const payload = canonicalJson({
schemaVersion: COMPLIANCE_SNAPSHOT_SCHEMA_VERSION,
eventId: input.eventId,
event: input.event,
capturedAt: input.capturedAt,
activeRules: input.activeRules,
state: input.state
});

return ok({
eventId: input.eventId,
schemaVersion: COMPLIANCE_SNAPSHOT_SCHEMA_VERSION,
event: input.event,
capturedAt: input.capturedAt,
payload,
integrityHash: sha256Hex(payload)
});
}

/** Recomputes the hash and checks the payload matches the stored envelope. */
export function verifyComplianceSnapshot(
snapshot: ComplianceSnapshot
): Result<true, ComplianceSnapshotErrorCode> {
if (snapshot.schemaVersion !== COMPLIANCE_SNAPSHOT_SCHEMA_VERSION) {
return err("unsupported_schema_version");
}
if (typeof snapshot.payload !== "string" || sha256Hex(snapshot.payload) !== snapshot.integrityHash) {
return err("snapshot_tampered");
}

let parsed: unknown;
try {
parsed = JSON.parse(snapshot.payload);
} catch {
return err("snapshot_tampered");
}
const envelope = parsed as Partial<ComplianceSnapshotInput & { schemaVersion: number }>;
const matches =
envelope.eventId === snapshot.eventId &&
envelope.event === snapshot.event &&
envelope.capturedAt === snapshot.capturedAt &&
envelope.schemaVersion === snapshot.schemaVersion;

return matches ? ok(true) : err("snapshot_tampered");
}

/**
* Keeps one snapshot per event id. Capturing the same event again returns the
* original, so a retried emitter cannot overwrite the record.
*/
export class ComplianceSnapshotStore {
private readonly storage: SnapshotStorage;

constructor(storage: SnapshotStorage) {
this.storage = storage;
}

private key(eventId: string): string {
return `${COMPLIANCE_SNAPSHOT_PREFIX}${eventId}`;
}

capture(input: ComplianceSnapshotInput): Result<ComplianceSnapshot, ComplianceSnapshotErrorCode> {
const existing = this.storage.getItem(this.key(input.eventId));
if (existing) {
return this.parse(existing);
}

const created = createComplianceSnapshot(input);
if (created.ok) {
this.storage.setItem(this.key(input.eventId), JSON.stringify(created.value));
}
return created;
}

/**
* Reads a snapshot for audit review. Only the maintainer scope may read them, and
* the integrity hash is verified before the snapshot is returned.
*/
read(
eventId: string,
scope: "own" | "maintainer"
): Result<ComplianceSnapshot, ComplianceSnapshotErrorCode> {
if (scope !== "maintainer") return err("audit_denied");
if (!isNonEmpty(eventId)) return err("invalid_snapshot_input");

const raw = this.storage.getItem(this.key(eventId));
if (!raw) return err("snapshot_not_found");

return this.parse(raw);
}

private parse(raw: string): Result<ComplianceSnapshot, ComplianceSnapshotErrorCode> {
let parsed: unknown;
try {
parsed = JSON.parse(raw);
} catch {
return err("snapshot_tampered");
}
if (typeof parsed !== "object" || parsed === null) {
return err("snapshot_tampered");
}
const snapshot = parsed as ComplianceSnapshot;
const verified = verifyComplianceSnapshot(snapshot);
return verified.ok ? ok(snapshot) : err(verified.code);
}
}
Loading