verify_audit_chain
Recomputes the SHA-256 hash chain over your audit log to detect mutated, deleted, or reordered events. Use as a tamper-evidence check to verify integrity.
Instructions
[audit] Recompute the SHA-256 hash chain over the audit log and confirm no event has been mutated, deleted, or reordered. Use periodically as a tamper-evidence check, or whenever you suspect the audit log has been touched outside q-ring; the result is informational — this tool does not repair the chain if it is broken. Read-only. Returns JSON { ok, valid, brokenAt? } where valid is true for an intact chain and brokenAt (when present) names the first event whose hash did not match.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
No arguments | |||
Implementation Reference
- src/core/observer.ts:196-252 (handler)Core handler: Recomputes SHA-256 hash chain over the audit log file (audit.jsonl) and verifies each event's prevHash matches the hash of the previous event. Returns VerifyResult with totalEvents, validEvents, brokenAt index, brokenEvent, and intact flag.
/** * Verify the hash-chain integrity of the entire audit log. * Returns the first break point if the chain has been tampered with. */ export function verifyAuditChain(): VerifyResult { const path = getAuditPath(); if (!existsSync(path)) { return { totalEvents: 0, validEvents: 0, intact: true }; } const lines = readFileSync(path, "utf8") .split("\n") .filter((l) => l.trim()); if (lines.length === 0) { return { totalEvents: 0, validEvents: 0, intact: true }; } let validEvents = 0; for (let i = 0; i < lines.length; i++) { let event: AuditEvent; try { event = JSON.parse(lines[i]); } catch { return { totalEvents: lines.length, validEvents, brokenAt: i, intact: false, }; } if (i === 0) { validEvents++; continue; } const expectedHash = createHash("sha256") .update(lines[i - 1]) .digest("hex"); if (event.prevHash !== expectedHash) { return { totalEvents: lines.length, validEvents, brokenAt: i, brokenEvent: event, intact: false, }; } validEvents++; } return { totalEvents: lines.length, validEvents, intact: true }; } - src/core/observer.ts:188-194 (schema)VerifyResult interface: Return type for verifyAuditChain() containing totalEvents, validEvents, optional brokenAt index, optional brokenEvent, and intact boolean.
export interface VerifyResult { totalEvents: number; validEvents: number; brokenAt?: number; brokenEvent?: AuditEvent; intact: boolean; } - src/mcp/tools/audit.ts:171-186 (registration)MCP tool registration: Registers 'verify_audit_chain' as a read-only MCP tool with description, empty schema params, and async handler that calls verifyAuditChain() and returns JSON result.
server.tool( "verify_audit_chain", [ "[audit] Recompute the SHA-256 hash chain over the audit log and confirm no event has been mutated, deleted, or reordered.", "Use periodically as a tamper-evidence check, or whenever you suspect the audit log has been touched outside q-ring; the result is informational — this tool does not repair the chain if it is broken.", "Read-only. Returns JSON `{ ok, valid, brokenAt? }` where `valid` is `true` for an intact chain and `brokenAt` (when present) names the first event whose hash did not match.", ].join(" "), {}, async () => { const toolBlock = enforceToolPolicy("verify_audit_chain"); if (toolBlock) return toolBlock; const result = verifyAuditChain(); return text(JSON.stringify(result, null, 2)); }, ); - src/cli/commands/audit.ts:81-113 (registration)CLI command registration: Registers 'audit:verify' commander command that calls verifyAuditChain() and displays result with color-coded success/failure output.
program .command("audit:verify") .description("Verify the integrity of the audit hash chain") .action(() => { const result = verifyAuditChain(); if (result.totalEvents === 0) { console.log(c.dim(" No audit events to verify")); return; } if (result.intact) { console.log( `${SYMBOLS.shield} ${c.green("Audit chain intact")} — ${result.totalEvents} events verified`, ); } else { console.log( `${SYMBOLS.cross} ${c.red("Audit chain BROKEN")} at event #${result.brokenAt}`, ); console.log( c.dim( ` ${result.validEvents}/${result.totalEvents} events valid before break`, ), ); if (result.brokenEvent) { console.log( c.dim( ` Broken event: ${result.brokenEvent.timestamp} ${result.brokenEvent.action} ${result.brokenEvent.key ?? ""}`, ), ); } process.exitCode = 1; } }); - src/core/observer.ts:59-75 (helper)Helper functions: getAuditDir() resolves audit directory (from QRING_AUDIT_DIR env var or ~/.config/q-ring), getAuditPath() returns full path to audit.jsonl.
function getAuditDir(): string { if (process.env.QRING_AUDIT_DIR) { if (!existsSync(process.env.QRING_AUDIT_DIR)) { mkdirSync(process.env.QRING_AUDIT_DIR, { recursive: true }); } return process.env.QRING_AUDIT_DIR; } const dir = join(homedir(), ".config", "q-ring"); if (!existsSync(dir)) { mkdirSync(dir, { recursive: true }); } return dir; } function getAuditPath(): string { return join(getAuditDir(), "audit.jsonl"); }