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 / ISO 19395), 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.
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.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, ISO/IEC 19395:2017 + DMTF) — 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: [...] }
Last updated 2026-08-08T16:39:49.652Z by seeder.