Audit Tools
Operator-side audit-chain inspection / export — verify chain integrity end-to-end, export RFC 8785 canonical-JSON slices, format rows for downstream SIEM (CADF / DMTF DSP0262), and generate tamper-evident compliance-evidence bundles auditors can verify off-line.
Four core operations on top of the live audit_log chain:
archive(opts) Bundle rows older than before into a PQC-encrypted archive with chain proof + a covering signed checkpoint. Live rows are untouched until a separate purge() call. exportSlice(opts) Auditor-shaped slice (date range / action filter) with chain proof — deliver evidence to an external auditor without surrendering the whole log. verifyBundle(opts) Round-trip integrity: decrypt the bundle, walk chain math across the contained rows, verify the covering checkpoint's ML-DSA signature (archive bundles only). purge(opts) Confirmation-gated deletion of live rows already captured in a verified archive bundle. Inserts a purge-anchor so b.audit.verify() keeps working post-purge.
Bundle layout (POSIX-flat directory; matches the backup-bundle shape so operators see one mental model for "encrypted blamejs bundle"):
kind="archive" bundles always include a covering checkpoint (atMonotonicCounter >= lastCounter) so the off-chain signature tamper-evidences the whole archive. kind="export" bundles are auditor evidence; the chain math is self-contained, with the upstream signature anchor optional.
b.auditTools.withRecordedAtIso(row) #
Surface recordedAt as ISO-8601 / RFC 3339 (with explicit Z) alongside the framework's primary Unix-ms integer. Auditors comparing rows against external SIEM events expect ISO; the chain hash is unaffected because the canonical wire form used for hashing doesn't include the derived recordedAtIso field.
Returns a shallow copy with recordedAtIso added when recordedAt is a finite number / bigint; otherwise returns the input unchanged.
var row = { _id: "evt-1", recordedAt: 1762560000000, action: "auth.login" };
var formatted = b.auditTools.withRecordedAtIso(row);
// → { _id: "evt-1", recordedAt: 1762560000000,
// recordedAtIso: "2025-11-08T00:00:00.000Z", action: "auth.login" }
b.auditTools.archive(opts) #
{
out: string, // fresh directory path (omit when returnBytes)
returnBytes:boolean, // true → return { manifest, files } in memory, no disk
before: number|Date|string, // archive rows recordedAt < this
passphrase: Buffer|string, // bundle-encryption passphrase
}
Bundle every audit row older than opts.before into a PQC-encrypted archive (XChaCha20-Poly1305 + Argon2id-derived key) containing a chain proof and the covering ML-DSA-87 checkpoint. Live rows are untouched — call b.auditTools.purge separately once the archive is verified.
Refuses if opts.out exists, no rows match, or no signed checkpoint covers the slice (run b.audit.checkpoint() first).
Pass returnBytes: true instead of out for the bundle as an in-memory { filename: Buffer } map (rows.enc + checkpoint.enc + manifest.json) — the read-only / serverless path. out and returnBytes are mutually exclusive.
var ninetyDaysAgo = Date.now() - 90 * 24 * 60 * 60 * 1000;
var result = await b.auditTools.archive({
out: "/var/audit/2026-Q1.bundle",
before: ninetyDaysAgo,
passphrase: process.env.AUDIT_BUNDLE_PASSPHRASE,
});
// → { rowCount: 14823, range: { firstCounter: 1, lastCounter: 14823, ... },
// manifestPath: "/var/audit/2026-Q1.bundle/manifest.json", ... }
b.auditTools.exportSlice(opts) #
{
out: string, // fresh directory path (omit when returnBytes)
returnBytes:boolean, // true → return { manifest, files } in memory, no disk
from: number|Date|string, // recordedAt >= this (inclusive)
to: number|Date|string, // recordedAt <= this (inclusive)
action: string, // exact action match (optional)
passphrase: Buffer|string, // bundle-encryption passphrase
}
Auditor-shaped slice — bundle the audit rows in [from, to] (optionally filtered by exact action) into a PQC-encrypted directory carrying chain-proof material. Refuses non-contiguous slices because chain verification cannot ground a sequence with gaps in monotonicCounter.
Use date-range filters that cover every row in the range; an action filter that drops intermediate counters is rejected with audit-tools/non-contiguous.
Pass returnBytes: true instead of out to get the bundle as an in-memory { filename: Buffer } map (rows.enc + manifest.json) with no filesystem touch — the read-only / serverless path; ship it to object storage or over the wire. out and returnBytes are mutually exclusive, and passing both refuses with audit-tools/out-and-return-bytes. A range that matches no audit row refuses with audit-tools/empty.
The range and the destination are checked before anything is read: audit-tools/bad-date for a bound that is not a readable date, audit-tools/no-outdir when neither out nor returnBytes is given, audit-tools/outdir-exists when the output directory is already there, since the export never merges into one, and audit-tools/no-passphrase when the slice is to be encrypted and no passphrase was supplied.
var bundle = await b.auditTools.exportSlice({
out: "/tmp/audit-2026-q1.bundle",
from: "2026-01-01T00:00:00Z",
to: "2026-03-31T23:59:59Z",
passphrase: process.env.AUDIT_BUNDLE_PASSPHRASE,
});
// → { rowCount: 4218, manifest: { kind: "export", ... }, ... }
b.auditTools.verifyBundle(opts) #
{
in: string, // bundle directory
passphrase: Buffer|string, // decryption passphrase
verifyCheckpointSignature: boolean, // default true
verifySignature: function(checkpoint), // override the default verifier
includeRows: boolean, // attach decrypted rows to result
}
Round-trip integrity check on a bundle directory: decrypt rows.enc, walk the prevHash → rowHash chain across the contained rows starting from the manifest's predecessorRowHash witness, confirm firstRowHash / lastRowHash match, and (archive only) verify the covering checkpoint's ML-DSA-87 signature against the locally-loaded audit-sign public key (or opts.verifySignature for cross-machine auditors).
Returns { ok: true, kind, rowsVerified, range, manifest } on success or { ok: false, reason, breakAt? } at the first break.
var result = await b.auditTools.verifyBundle({
in: "/var/audit/2026-Q1.bundle",
passphrase: process.env.AUDIT_BUNDLE_PASSPHRASE,
});
if (!result.ok) {
console.error("bundle integrity break:", result.reason);
process.exit(1);
}
// → { ok: true, kind: "archive", rowsVerified: 14823, range: { ... } }
b.auditTools.purge(opts) #
{
confirm: true, // exact `true` required
archive: string, // path to a verified archive bundle
passphrase: Buffer|string, // bundle decryption passphrase
verifySignature: function(checkpoint),// auditor pubkey override
dualControlGrant: object, // required when audit_log is declared under b.db.declareRequireDualControl — from b.dualControl.consume({ action: "auditTools.purge" })
}
Confirmation-gated deletion of live audit rows already captured in a verified archive bundle. Refuses unless opts.confirm === true, the bundle verifies clean as kind="archive", and the bundle's firstCounter / predecessorRowHash match the next contiguous purge point on disk. Inserts a _blamejs_audit_purge_anchor row so b.audit.verify() keeps chaining post-purge — the anchor's lastPurgedRowHash becomes the new chain origin.
var result = await b.auditTools.purge({
confirm: true,
archive: "/var/audit/2026-Q1.bundle",
passphrase: process.env.AUDIT_BUNDLE_PASSPHRASE,
});
// → { purged: true, rowsDeleted: 14823, lastPurgedCounter: 14823, ... }
b.auditTools.signExistingPurgeAnchor(opts?) #
{
allow: string[], // default: ["unsigned", "rotated-key"] — which situations this call may repair. `b.db.init` passes only what its own flags asserted, so one flag cannot buy the other's repair; they are different claims
}
Sign the purge anchor a previous version left unsigned, pinning the boundary it already claims so it can never be moved again. Returns { signed: false, reason } when there is nothing to do — no anchor, an anchor that is already signed, or a deployment running without audit signing — and { signed: true, lastPurgedCounter, publicKeyFingerprint } when it wrote one.
This is the upgrade path, and it exists because refusing an unsigned anchor would otherwise strand the installation that has one: re-running the purge is turned away by the contiguity guard, which requires an archive starting one counter past the recorded boundary, and the boundary is exactly what is in question. Reached through b.db.init({ acceptUnsignedPurgeAnchor: true }) once, after which the flag does nothing and should be removed.
It pins what is already there rather than proving it. An operator who wants the boundary PROVEN verifies the retained archive against the chain with b.auditTools.verifyBundle before running this; nothing here can tell a boundary a previous version wrote from one somebody else did, which is the whole reason the signature exists.
It also repairs the other way an anchor stops verifying without being forged: naming a key that has since been rotated out. There the existing signature is checked under that rotated key first, so a tampered anchor is refused rather than re-signed into a valid one.
var r = await b.auditTools.signExistingPurgeAnchor();
// → { signed: true, lastPurgedCounter: , publicKeyFingerprint: }
// where audit signing is configured, and { signed: false, reason } where
// it is not
b.auditTools.forensicSnapshot(opts) #
{
out: string, // fresh directory path (omit when returnBytes)
returnBytes:boolean, // true → return { ...manifest, files } in memory, no disk
since: number|Date|string, // include rows recordedAt >= this (windowed since → now)
passphrase: Buffer|string, // bundle-encryption passphrase
reason: string, // required incident-context reason
incidentId: string, // optional ticket / incident id
actor: { id, role }, // optional incident-commander identity
}
Post-compromise composer that bundles an audit slice (from since → now) plus operator-supplied incident metadata (incidentId, reason, actor) and runtime fingerprint (Node version / platform / pid / uptime) into a single tamper-evident artifact for legal / regulators / the IR team. Emits an audit.forensic_snapshot.composed audit event so the act of composing the snapshot is itself on-chain.
Pass returnBytes: true instead of out for the snapshot as an in-memory { filename: Buffer } map (the slice's rows.enc + manifest.json plus forensic-snapshot.json) — the read-only / serverless path. out and returnBytes are mutually exclusive.
var snap = await b.auditTools.forensicSnapshot({
out: "/forensics/2026-05-08-inc-42",
since: Date.now() - 7 * 24 * 60 * 60 * 1000,
passphrase: process.env.AUDIT_BUNDLE_PASSPHRASE,
incidentId: "inc-2026-05-08-42",
reason: "ATO investigation: 14 failed MFA from new geo, user u-42",
actor: { id: "alice@ops.example.com", role: "incident-commander" },
});
// → { snapshotKind: "forensic", incidentId: "inc-2026-05-08-42", ... }
b.auditTools.exportCadf(opts) #
{
format: "cadf", // optional — defaults to "cadf"
from: number|Date|string, // recordedAt >= this
to: number|Date|string, // recordedAt <= this
action: string, // exact action filter
}
Format an audit slice as a CADF event-batch (Cloud Auditing Data Federation, DMTF DSP0262) — the FedRAMP / OpenStack envelope cross-tenant SIEMs and CSP reporting tools expect for federated tooling. Maps blamejs fields onto CADF attributes (initiator / target / observer / outcome / reason) and embeds a blamejs:chain extension carrying monotonicCounter / prevHash / rowHash so auditors can correlate the envelope back to the chain.
Returns an object with events: [...] ready to ship as JSON.
var batch = await b.auditTools.exportCadf({
from: "2026-05-01T00:00:00Z",
to: "2026-05-08T00:00:00Z",
action: "auth.login",
});
// → { typeURI: ".../event-batch", framework: "blamejs", events: [...] }
b.auditTools.exportAudit(opts) #
{
format: "cadf", // selector — defaults to "cadf"
from: number|Date|string, // recordedAt >= this
to: number|Date|string, // recordedAt <= this
action: string, // exact action filter
}
Format dispatcher for downstream-SIEM exports. Reads opts.format (default "cadf") and delegates to the matching formatter. Future envelope formats (CEF / OCSF / etc.) register here so callers stay on a stable signature even when the framework adds formats.
var batch = await b.auditTools.exportAudit({
format: "cadf",
from: "2026-05-01T00:00:00Z",
to: "2026-05-08T00:00:00Z",
});
// → { typeURI: ".../event-batch", framework: "blamejs", events: [...] }