@blamejs/core 0.8.43 → 0.8.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +93 -0
- package/README.md +10 -10
- package/index.js +52 -0
- package/lib/a2a.js +159 -34
- package/lib/acme.js +762 -0
- package/lib/ai-pref.js +166 -43
- package/lib/api-key.js +108 -47
- package/lib/api-snapshot.js +157 -40
- package/lib/app-shutdown.js +113 -77
- package/lib/archive.js +337 -40
- package/lib/arg-parser.js +697 -0
- package/lib/asyncapi.js +99 -55
- package/lib/atomic-file.js +465 -104
- package/lib/audit-chain.js +123 -34
- package/lib/audit-daily-review.js +389 -0
- package/lib/audit-sign.js +302 -56
- package/lib/audit-tools.js +412 -63
- package/lib/audit.js +656 -35
- package/lib/auth/jwt-external.js +17 -0
- package/lib/auth/oauth.js +7 -0
- package/lib/auth-bot-challenge.js +505 -0
- package/lib/auth-header.js +92 -25
- package/lib/backup/bundle.js +26 -0
- package/lib/backup/index.js +512 -89
- package/lib/backup/manifest.js +168 -7
- package/lib/break-glass.js +415 -39
- package/lib/budr.js +103 -30
- package/lib/bundler.js +86 -66
- package/lib/cache.js +192 -72
- package/lib/chain-writer.js +65 -40
- package/lib/circuit-breaker.js +56 -33
- package/lib/cli-helpers.js +106 -75
- package/lib/cli.js +6 -30
- package/lib/cloud-events.js +99 -32
- package/lib/cluster-storage.js +162 -37
- package/lib/cluster.js +340 -49
- package/lib/codepoint-class.js +66 -0
- package/lib/compliance.js +424 -24
- package/lib/config-drift.js +111 -46
- package/lib/config.js +94 -40
- package/lib/consent.js +165 -18
- package/lib/constants.js +1 -0
- package/lib/content-credentials.js +153 -48
- package/lib/cookies.js +154 -62
- package/lib/credential-hash.js +133 -61
- package/lib/crypto-field.js +702 -18
- package/lib/crypto-hpke.js +256 -0
- package/lib/crypto.js +744 -22
- package/lib/csv.js +178 -35
- package/lib/daemon.js +456 -0
- package/lib/dark-patterns.js +186 -55
- package/lib/db-query.js +79 -2
- package/lib/db.js +1431 -60
- package/lib/ddl-change-control.js +523 -0
- package/lib/deprecate.js +195 -40
- package/lib/dev.js +82 -39
- package/lib/dora.js +67 -48
- package/lib/dr-runbook.js +368 -0
- package/lib/dsr.js +142 -11
- package/lib/dual-control.js +91 -56
- package/lib/events.js +120 -41
- package/lib/external-db-migrate.js +192 -2
- package/lib/external-db.js +795 -50
- package/lib/fapi2.js +122 -1
- package/lib/fda-21cfr11.js +395 -0
- package/lib/fdx.js +132 -2
- package/lib/file-type.js +87 -0
- package/lib/file-upload.js +93 -0
- package/lib/flag.js +82 -20
- package/lib/forms.js +132 -29
- package/lib/framework-error.js +169 -0
- package/lib/framework-schema.js +163 -35
- package/lib/gate-contract.js +849 -175
- package/lib/graphql-federation.js +68 -7
- package/lib/guard-all.js +172 -55
- package/lib/guard-archive.js +286 -124
- package/lib/guard-auth.js +194 -21
- package/lib/guard-cidr.js +190 -28
- package/lib/guard-csv.js +397 -51
- package/lib/guard-domain.js +213 -91
- package/lib/guard-email.js +236 -29
- package/lib/guard-filename.js +307 -75
- package/lib/guard-graphql.js +263 -30
- package/lib/guard-html.js +310 -116
- package/lib/guard-image.js +243 -30
- package/lib/guard-json.js +260 -54
- package/lib/guard-jsonpath.js +235 -23
- package/lib/guard-jwt.js +284 -30
- package/lib/guard-markdown.js +204 -22
- package/lib/guard-mime.js +190 -26
- package/lib/guard-oauth.js +277 -28
- package/lib/guard-pdf.js +251 -27
- package/lib/guard-regex.js +226 -18
- package/lib/guard-shell.js +229 -26
- package/lib/guard-svg.js +177 -10
- package/lib/guard-template.js +232 -21
- package/lib/guard-time.js +195 -29
- package/lib/guard-uuid.js +189 -30
- package/lib/guard-xml.js +259 -36
- package/lib/guard-yaml.js +241 -44
- package/lib/honeytoken.js +63 -27
- package/lib/html-balance.js +83 -0
- package/lib/http-client.js +486 -59
- package/lib/http-message-signature.js +582 -0
- package/lib/i18n.js +102 -49
- package/lib/iab-mspa.js +112 -32
- package/lib/iab-tcf.js +107 -2
- package/lib/inbox.js +90 -52
- package/lib/keychain.js +865 -0
- package/lib/legal-hold.js +374 -0
- package/lib/local-db-thin.js +320 -0
- package/lib/log-stream.js +281 -51
- package/lib/log.js +184 -86
- package/lib/mail-bounce.js +107 -62
- package/lib/mail.js +295 -58
- package/lib/mcp.js +108 -27
- package/lib/metrics.js +98 -89
- package/lib/middleware/age-gate.js +36 -0
- package/lib/middleware/ai-act-disclosure.js +37 -0
- package/lib/middleware/api-encrypt.js +45 -0
- package/lib/middleware/assetlinks.js +40 -0
- package/lib/middleware/asyncapi-serve.js +35 -0
- package/lib/middleware/attach-user.js +40 -0
- package/lib/middleware/bearer-auth.js +40 -0
- package/lib/middleware/body-parser.js +230 -0
- package/lib/middleware/bot-disclose.js +34 -0
- package/lib/middleware/bot-guard.js +39 -0
- package/lib/middleware/compression.js +37 -0
- package/lib/middleware/cookies.js +32 -0
- package/lib/middleware/cors.js +40 -0
- package/lib/middleware/csp-nonce.js +40 -0
- package/lib/middleware/csp-report.js +34 -0
- package/lib/middleware/csrf-protect.js +43 -0
- package/lib/middleware/daily-byte-quota.js +53 -85
- package/lib/middleware/db-role-for.js +40 -0
- package/lib/middleware/dpop.js +40 -0
- package/lib/middleware/error-handler.js +37 -14
- package/lib/middleware/fetch-metadata.js +39 -0
- package/lib/middleware/flag-context.js +34 -0
- package/lib/middleware/gpc.js +33 -0
- package/lib/middleware/headers.js +35 -0
- package/lib/middleware/health.js +46 -0
- package/lib/middleware/host-allowlist.js +30 -0
- package/lib/middleware/network-allowlist.js +38 -0
- package/lib/middleware/openapi-serve.js +34 -0
- package/lib/middleware/rate-limit.js +160 -18
- package/lib/middleware/request-id.js +36 -18
- package/lib/middleware/request-log.js +37 -0
- package/lib/middleware/require-aal.js +29 -0
- package/lib/middleware/require-auth.js +32 -0
- package/lib/middleware/require-bound-key.js +41 -0
- package/lib/middleware/require-content-type.js +32 -0
- package/lib/middleware/require-methods.js +27 -0
- package/lib/middleware/require-mtls.js +33 -0
- package/lib/middleware/require-step-up.js +37 -0
- package/lib/middleware/security-headers.js +44 -0
- package/lib/middleware/security-txt.js +38 -0
- package/lib/middleware/span-http-server.js +37 -0
- package/lib/middleware/sse.js +36 -0
- package/lib/middleware/trace-log-correlation.js +33 -0
- package/lib/middleware/trace-propagate.js +32 -0
- package/lib/middleware/tus-upload.js +90 -0
- package/lib/middleware/web-app-manifest.js +53 -0
- package/lib/mtls-ca.js +100 -70
- package/lib/network-byte-quota.js +308 -0
- package/lib/network-heartbeat.js +135 -0
- package/lib/network-tls.js +534 -4
- package/lib/network.js +103 -0
- package/lib/notify.js +114 -43
- package/lib/ntp-check.js +192 -51
- package/lib/observability.js +145 -47
- package/lib/openapi.js +90 -44
- package/lib/outbox.js +99 -1
- package/lib/pagination.js +168 -86
- package/lib/parsers/index.js +16 -5
- package/lib/permissions.js +93 -40
- package/lib/pqc-agent.js +84 -8
- package/lib/pqc-software.js +94 -60
- package/lib/process-spawn.js +95 -21
- package/lib/pubsub.js +96 -66
- package/lib/queue.js +375 -54
- package/lib/redact.js +793 -21
- package/lib/render.js +139 -47
- package/lib/request-helpers.js +485 -121
- package/lib/restore-bundle.js +142 -39
- package/lib/restore-rollback.js +136 -45
- package/lib/retention.js +178 -50
- package/lib/retry.js +116 -33
- package/lib/router.js +475 -23
- package/lib/safe-async.js +543 -94
- package/lib/safe-buffer.js +337 -41
- package/lib/safe-json.js +467 -62
- package/lib/safe-jsonpath.js +285 -0
- package/lib/safe-schema.js +631 -87
- package/lib/safe-sql.js +221 -59
- package/lib/safe-url.js +278 -46
- package/lib/sandbox-worker.js +135 -0
- package/lib/sandbox.js +358 -0
- package/lib/scheduler.js +135 -70
- package/lib/self-update.js +647 -0
- package/lib/session-device-binding.js +431 -0
- package/lib/session.js +259 -49
- package/lib/slug.js +138 -26
- package/lib/ssrf-guard.js +316 -56
- package/lib/storage.js +433 -70
- package/lib/subject.js +405 -23
- package/lib/template.js +148 -8
- package/lib/tenant-quota.js +545 -0
- package/lib/testing.js +440 -53
- package/lib/time.js +291 -23
- package/lib/tls-exporter.js +239 -0
- package/lib/tracing.js +90 -74
- package/lib/uuid.js +97 -22
- package/lib/vault/index.js +284 -22
- package/lib/vault/seal-pem-file.js +66 -0
- package/lib/watcher.js +368 -0
- package/lib/webhook.js +196 -63
- package/lib/websocket.js +393 -68
- package/lib/wiki-concepts.js +338 -0
- package/lib/worker-pool.js +464 -0
- package/package.json +3 -3
- package/sbom.cyclonedx.json +7 -7
package/lib/audit-tools.js
CHANGED
|
@@ -1,54 +1,57 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
3
|
+
* @module b.auditTools
|
|
4
|
+
* @nav Observability
|
|
5
|
+
* @title Audit Tools
|
|
6
|
+
*
|
|
7
|
+
* @intro
|
|
8
|
+
* Operator-side audit-chain inspection / export — verify chain
|
|
9
|
+
* integrity end-to-end, export RFC 8785 canonical-JSON slices,
|
|
10
|
+
* format rows for downstream SIEM (CADF / ISO 19395), and generate
|
|
11
|
+
* tamper-evident compliance-evidence bundles auditors can verify
|
|
12
|
+
* off-line.
|
|
13
|
+
*
|
|
14
|
+
* Four core operations on top of the live `audit_log` chain:
|
|
15
|
+
*
|
|
16
|
+
* archive(opts) Bundle rows older than `before` into a
|
|
17
|
+
* PQC-encrypted archive with chain proof + a
|
|
18
|
+
* covering signed checkpoint. Live rows are
|
|
19
|
+
* untouched until a separate `purge()` call.
|
|
20
|
+
* exportSlice(opts) Auditor-shaped slice (date range / action
|
|
21
|
+
* filter) with chain proof — deliver evidence
|
|
22
|
+
* to an external auditor without surrendering
|
|
23
|
+
* the whole log.
|
|
24
|
+
* verifyBundle(opts) Round-trip integrity: decrypt the bundle,
|
|
25
|
+
* walk chain math across the contained rows,
|
|
26
|
+
* verify the covering checkpoint's ML-DSA
|
|
27
|
+
* signature (archive bundles only).
|
|
28
|
+
* purge(opts) Confirmation-gated deletion of live rows
|
|
29
|
+
* already captured in a verified archive
|
|
30
|
+
* bundle. Inserts a purge-anchor so
|
|
31
|
+
* `b.audit.verify()` keeps working post-purge.
|
|
32
|
+
*
|
|
33
|
+
* Bundle layout (POSIX-flat directory; matches the backup-bundle
|
|
34
|
+
* shape so operators see one mental model for "encrypted blamejs
|
|
35
|
+
* bundle"):
|
|
36
|
+
*
|
|
37
|
+
* <out>/manifest.json Canonical-JSON manifest (format / kind /
|
|
38
|
+
* range / rowCount / per-blob salts /
|
|
39
|
+
* framework version; archive bundles also
|
|
40
|
+
* carry the covering checkpoint summary).
|
|
41
|
+
* <out>/rows.enc PQC-encrypted JSONL of audit rows in
|
|
42
|
+
* sealed form so rowHash stays computable
|
|
43
|
+
* from disk bytes byte-for-byte.
|
|
44
|
+
* <out>/checkpoint.enc Archive-only. PQC-encrypted JSON of the
|
|
45
|
+
* covering audit_checkpoints row.
|
|
46
|
+
*
|
|
47
|
+
* `kind="archive"` bundles always include a covering checkpoint
|
|
48
|
+
* (atMonotonicCounter >= lastCounter) so the off-chain signature
|
|
49
|
+
* tamper-evidences the whole archive. `kind="export"` bundles are
|
|
50
|
+
* auditor evidence; the chain math is self-contained, with the
|
|
51
|
+
* upstream signature anchor optional.
|
|
52
|
+
*
|
|
53
|
+
* @card
|
|
54
|
+
* 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.
|
|
52
55
|
*/
|
|
53
56
|
|
|
54
57
|
var fs = require("fs");
|
|
@@ -153,6 +156,28 @@ function _rowToWireForm(row) {
|
|
|
153
156
|
// unchanged AND _rowToWireForm (which the chain-hash canonicalizes
|
|
154
157
|
// over) doesn't change its bytes — so chain verify continues to
|
|
155
158
|
// match. Operators call this on retrieved rows for export.
|
|
159
|
+
/**
|
|
160
|
+
* @primitive b.auditTools.withRecordedAtIso
|
|
161
|
+
* @signature b.auditTools.withRecordedAtIso(row)
|
|
162
|
+
* @since 0.7.30
|
|
163
|
+
* @related b.auditTools.exportSlice, b.auditTools.exportCadf
|
|
164
|
+
*
|
|
165
|
+
* Surface `recordedAt` as ISO-8601 / RFC 3339 (with explicit `Z`)
|
|
166
|
+
* alongside the framework's primary Unix-ms integer. Auditors
|
|
167
|
+
* comparing rows against external SIEM events expect ISO; the chain
|
|
168
|
+
* hash is unaffected because the canonical wire form used for
|
|
169
|
+
* hashing doesn't include the derived `recordedAtIso` field.
|
|
170
|
+
*
|
|
171
|
+
* Returns a shallow copy with `recordedAtIso` added when
|
|
172
|
+
* `recordedAt` is a finite number / bigint; otherwise returns the
|
|
173
|
+
* input unchanged.
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* var row = { _id: "evt-1", recordedAt: 1762560000000, action: "auth.login" };
|
|
177
|
+
* var formatted = b.auditTools.withRecordedAtIso(row);
|
|
178
|
+
* // → { _id: "evt-1", recordedAt: 1762560000000,
|
|
179
|
+
* // recordedAtIso: "2025-11-08T00:00:00.000Z", action: "auth.login" }
|
|
180
|
+
*/
|
|
156
181
|
function withRecordedAtIso(row) {
|
|
157
182
|
if (!row) return row;
|
|
158
183
|
var out = Object.assign({}, row);
|
|
@@ -396,6 +421,37 @@ async function _readBundle(inDir, passphrase) {
|
|
|
396
421
|
|
|
397
422
|
// ---- Public ops ----
|
|
398
423
|
|
|
424
|
+
/**
|
|
425
|
+
* @primitive b.auditTools.archive
|
|
426
|
+
* @signature b.auditTools.archive(opts)
|
|
427
|
+
* @since 0.7.30
|
|
428
|
+
* @compliance hipaa, pci-dss, gdpr, soc2, sox-404
|
|
429
|
+
* @related b.auditTools.verifyBundle, b.auditTools.purge, b.audit.checkpoint
|
|
430
|
+
*
|
|
431
|
+
* Bundle every audit row older than `opts.before` into a
|
|
432
|
+
* PQC-encrypted archive (XChaCha20-Poly1305 + Argon2id-derived key)
|
|
433
|
+
* containing a chain proof and the covering ML-DSA-87 checkpoint.
|
|
434
|
+
* Live rows are untouched — call `b.auditTools.purge` separately
|
|
435
|
+
* once the archive is verified.
|
|
436
|
+
*
|
|
437
|
+
* Refuses if `opts.out` exists, no rows match, or no signed
|
|
438
|
+
* checkpoint covers the slice (run `b.audit.checkpoint()` first).
|
|
439
|
+
*
|
|
440
|
+
* @opts
|
|
441
|
+
* out: string, // fresh directory path for the bundle
|
|
442
|
+
* before: number|Date|string, // archive rows recordedAt < this
|
|
443
|
+
* passphrase: Buffer|string, // bundle-encryption passphrase
|
|
444
|
+
*
|
|
445
|
+
* @example
|
|
446
|
+
* var ninetyDaysAgo = Date.now() - 90 * 24 * 60 * 60 * 1000;
|
|
447
|
+
* var result = await b.auditTools.archive({
|
|
448
|
+
* out: "/var/audit/2026-Q1.bundle",
|
|
449
|
+
* before: ninetyDaysAgo,
|
|
450
|
+
* passphrase: process.env.AUDIT_BUNDLE_PASSPHRASE,
|
|
451
|
+
* });
|
|
452
|
+
* // → { rowCount: 14823, range: { firstCounter: 1, lastCounter: 14823, ... },
|
|
453
|
+
* // manifestPath: "/var/audit/2026-Q1.bundle/manifest.json", ... }
|
|
454
|
+
*/
|
|
399
455
|
async function archive(opts) {
|
|
400
456
|
opts = opts || {};
|
|
401
457
|
_requirePassphrase(opts.passphrase);
|
|
@@ -444,6 +500,39 @@ async function archive(opts) {
|
|
|
444
500
|
};
|
|
445
501
|
}
|
|
446
502
|
|
|
503
|
+
/**
|
|
504
|
+
* @primitive b.auditTools.exportSlice
|
|
505
|
+
* @signature b.auditTools.exportSlice(opts)
|
|
506
|
+
* @since 0.7.30
|
|
507
|
+
* @compliance hipaa, pci-dss, gdpr, soc2
|
|
508
|
+
* @related b.auditTools.archive, b.auditTools.verifyBundle, b.auditTools.exportCadf
|
|
509
|
+
*
|
|
510
|
+
* Auditor-shaped slice — bundle the audit rows in `[from, to]`
|
|
511
|
+
* (optionally filtered by exact `action`) into a PQC-encrypted
|
|
512
|
+
* directory carrying chain-proof material. Refuses non-contiguous
|
|
513
|
+
* slices because chain verification cannot ground a sequence with
|
|
514
|
+
* gaps in `monotonicCounter`.
|
|
515
|
+
*
|
|
516
|
+
* Use date-range filters that cover every row in the range; an
|
|
517
|
+
* action filter that drops intermediate counters is rejected with
|
|
518
|
+
* `audit-tools/non-contiguous`.
|
|
519
|
+
*
|
|
520
|
+
* @opts
|
|
521
|
+
* out: string, // fresh directory path
|
|
522
|
+
* from: number|Date|string, // recordedAt >= this (inclusive)
|
|
523
|
+
* to: number|Date|string, // recordedAt <= this (inclusive)
|
|
524
|
+
* action: string, // exact action match (optional)
|
|
525
|
+
* passphrase: Buffer|string, // bundle-encryption passphrase
|
|
526
|
+
*
|
|
527
|
+
* @example
|
|
528
|
+
* var bundle = await b.auditTools.exportSlice({
|
|
529
|
+
* out: "/tmp/audit-2026-q1.bundle",
|
|
530
|
+
* from: "2026-01-01T00:00:00Z",
|
|
531
|
+
* to: "2026-03-31T23:59:59Z",
|
|
532
|
+
* passphrase: process.env.AUDIT_BUNDLE_PASSPHRASE,
|
|
533
|
+
* });
|
|
534
|
+
* // → { rowCount: 4218, manifest: { kind: "export", ... }, ... }
|
|
535
|
+
*/
|
|
447
536
|
async function exportSlice(opts) {
|
|
448
537
|
opts = opts || {};
|
|
449
538
|
_requirePassphrase(opts.passphrase);
|
|
@@ -496,6 +585,42 @@ async function exportSlice(opts) {
|
|
|
496
585
|
};
|
|
497
586
|
}
|
|
498
587
|
|
|
588
|
+
/**
|
|
589
|
+
* @primitive b.auditTools.verifyBundle
|
|
590
|
+
* @signature b.auditTools.verifyBundle(opts)
|
|
591
|
+
* @since 0.7.30
|
|
592
|
+
* @compliance hipaa, pci-dss, gdpr, soc2, sox-404
|
|
593
|
+
* @related b.auditTools.archive, b.auditTools.exportSlice, b.auditTools.purge
|
|
594
|
+
*
|
|
595
|
+
* Round-trip integrity check on a bundle directory: decrypt
|
|
596
|
+
* `rows.enc`, walk the prevHash → rowHash chain across the contained
|
|
597
|
+
* rows starting from the manifest's `predecessorRowHash` witness,
|
|
598
|
+
* confirm `firstRowHash` / `lastRowHash` match, and (archive only)
|
|
599
|
+
* verify the covering checkpoint's ML-DSA-87 signature against the
|
|
600
|
+
* locally-loaded audit-sign public key (or `opts.verifySignature`
|
|
601
|
+
* for cross-machine auditors).
|
|
602
|
+
*
|
|
603
|
+
* Returns `{ ok: true, kind, rowsVerified, range, manifest }` on
|
|
604
|
+
* success or `{ ok: false, reason, breakAt? }` at the first break.
|
|
605
|
+
*
|
|
606
|
+
* @opts
|
|
607
|
+
* in: string, // bundle directory
|
|
608
|
+
* passphrase: Buffer|string, // decryption passphrase
|
|
609
|
+
* verifyCheckpointSignature: boolean, // default true
|
|
610
|
+
* verifySignature: function(checkpoint), // override the default verifier
|
|
611
|
+
* includeRows: boolean, // attach decrypted rows to result
|
|
612
|
+
*
|
|
613
|
+
* @example
|
|
614
|
+
* var result = await b.auditTools.verifyBundle({
|
|
615
|
+
* in: "/var/audit/2026-Q1.bundle",
|
|
616
|
+
* passphrase: process.env.AUDIT_BUNDLE_PASSPHRASE,
|
|
617
|
+
* });
|
|
618
|
+
* if (!result.ok) {
|
|
619
|
+
* console.error("bundle integrity break:", result.reason);
|
|
620
|
+
* process.exit(1);
|
|
621
|
+
* }
|
|
622
|
+
* // → { ok: true, kind: "archive", rowsVerified: 14823, range: { ... } }
|
|
623
|
+
*/
|
|
499
624
|
async function verifyBundle(opts) {
|
|
500
625
|
opts = opts || {};
|
|
501
626
|
_requirePassphrase(opts.passphrase);
|
|
@@ -590,6 +715,35 @@ function _defaultVerifyCheckpointSignature(checkpoint) {
|
|
|
590
715
|
} catch (_e) { return false; }
|
|
591
716
|
}
|
|
592
717
|
|
|
718
|
+
/**
|
|
719
|
+
* @primitive b.auditTools.purge
|
|
720
|
+
* @signature b.auditTools.purge(opts)
|
|
721
|
+
* @since 0.7.30
|
|
722
|
+
* @compliance hipaa, pci-dss, gdpr, soc2, sox-404
|
|
723
|
+
* @related b.auditTools.archive, b.auditTools.verifyBundle, b.audit.verify
|
|
724
|
+
*
|
|
725
|
+
* Confirmation-gated deletion of live audit rows already captured in
|
|
726
|
+
* a verified archive bundle. Refuses unless `opts.confirm === true`,
|
|
727
|
+
* the bundle verifies clean as `kind="archive"`, and the bundle's
|
|
728
|
+
* `firstCounter` / `predecessorRowHash` match the next contiguous
|
|
729
|
+
* purge point on disk. Inserts a `_blamejs_audit_purge_anchor` row
|
|
730
|
+
* so `b.audit.verify()` keeps chaining post-purge — the anchor's
|
|
731
|
+
* `lastPurgedRowHash` becomes the new chain origin.
|
|
732
|
+
*
|
|
733
|
+
* @opts
|
|
734
|
+
* confirm: true, // exact `true` required
|
|
735
|
+
* archive: string, // path to a verified archive bundle
|
|
736
|
+
* passphrase: Buffer|string, // bundle decryption passphrase
|
|
737
|
+
* verifySignature: function(checkpoint),// auditor pubkey override
|
|
738
|
+
*
|
|
739
|
+
* @example
|
|
740
|
+
* var result = await b.auditTools.purge({
|
|
741
|
+
* confirm: true,
|
|
742
|
+
* archive: "/var/audit/2026-Q1.bundle",
|
|
743
|
+
* passphrase: process.env.AUDIT_BUNDLE_PASSPHRASE,
|
|
744
|
+
* });
|
|
745
|
+
* // → { purged: true, rowsDeleted: 14823, lastPurgedCounter: 14823, ... }
|
|
746
|
+
*/
|
|
593
747
|
async function purge(opts) {
|
|
594
748
|
opts = opts || {};
|
|
595
749
|
if (opts.confirm !== true) {
|
|
@@ -683,20 +837,40 @@ async function _defaultApplyPurge(args) {
|
|
|
683
837
|
};
|
|
684
838
|
}
|
|
685
839
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
840
|
+
/**
|
|
841
|
+
* @primitive b.auditTools.forensicSnapshot
|
|
842
|
+
* @signature b.auditTools.forensicSnapshot(opts)
|
|
843
|
+
* @since 0.8.40
|
|
844
|
+
* @compliance hipaa, pci-dss, gdpr, soc2, sox-404, dora, nis2
|
|
845
|
+
* @related b.auditTools.exportSlice, b.auditTools.archive
|
|
846
|
+
*
|
|
847
|
+
* Post-compromise composer that bundles an audit slice (from
|
|
848
|
+
* `since` → now) plus operator-supplied incident metadata
|
|
849
|
+
* (incidentId, reason, actor) and runtime fingerprint (Node version
|
|
850
|
+
* / platform / pid / uptime) into a single tamper-evident artifact
|
|
851
|
+
* for legal / regulators / the IR team. Emits an
|
|
852
|
+
* `audit.forensic_snapshot.composed` audit event so the act of
|
|
853
|
+
* composing the snapshot is itself on-chain.
|
|
854
|
+
*
|
|
855
|
+
* @opts
|
|
856
|
+
* out: string, // fresh directory path
|
|
857
|
+
* since: number|Date|string, // include rows recordedAt >= this
|
|
858
|
+
* passphrase: Buffer|string, // bundle-encryption passphrase
|
|
859
|
+
* reason: string, // required incident-context reason
|
|
860
|
+
* incidentId: string, // optional ticket / incident id
|
|
861
|
+
* actor: { id, role }, // optional incident-commander identity
|
|
862
|
+
*
|
|
863
|
+
* @example
|
|
864
|
+
* var snap = await b.auditTools.forensicSnapshot({
|
|
865
|
+
* out: "/forensics/2026-05-08-inc-42",
|
|
866
|
+
* since: Date.now() - 7 * 24 * 60 * 60 * 1000,
|
|
867
|
+
* passphrase: process.env.AUDIT_BUNDLE_PASSPHRASE,
|
|
868
|
+
* incidentId: "inc-2026-05-08-42",
|
|
869
|
+
* reason: "ATO investigation: 14 failed MFA from new geo, user u-42",
|
|
870
|
+
* actor: { id: "alice@ops.example.com", role: "incident-commander" },
|
|
871
|
+
* });
|
|
872
|
+
* // → { snapshotKind: "forensic", incidentId: "inc-2026-05-08-42", ... }
|
|
873
|
+
*/
|
|
700
874
|
async function forensicSnapshot(opts) {
|
|
701
875
|
opts = opts || {};
|
|
702
876
|
_requirePassphrase(opts.passphrase);
|
|
@@ -750,9 +924,184 @@ async function forensicSnapshot(opts) {
|
|
|
750
924
|
return Object.assign({}, manifest, { manifestPath: manifestPath });
|
|
751
925
|
}
|
|
752
926
|
|
|
927
|
+
// CADF (Cloud Auditing Data Federation, ISO/IEC 19395:2017) is the
|
|
928
|
+
// OpenStack/FedRAMP-tier cloud-audit envelope auditors increasingly
|
|
929
|
+
// expect for federated tooling (cross-tenant SIEM, CSP reporting).
|
|
930
|
+
//
|
|
931
|
+
// We map blamejs audit fields onto CADF attributes:
|
|
932
|
+
//
|
|
933
|
+
// blamejs CADF
|
|
934
|
+
// ---------------------- ----------------------------------
|
|
935
|
+
// _id eventid (UUID-ish)
|
|
936
|
+
// action action (typed verb namespace)
|
|
937
|
+
// outcome outcome (success | failure | unknown | pending)
|
|
938
|
+
// actorUserId initiator.id (typed via initiator.typeURI)
|
|
939
|
+
// resourceKind+resourceId target.id + target.typeURI
|
|
940
|
+
// recordedAt eventTime (ISO-8601)
|
|
941
|
+
// reason reason.reasonCode + reason.policyType
|
|
942
|
+
// metadata attachments[] (operator-supplied free-form)
|
|
943
|
+
// prevHash/rowHash observer.id link to chain anchor
|
|
944
|
+
//
|
|
945
|
+
// CADF requires every event to declare its observer (the auditing
|
|
946
|
+
// system). We declare blamejs as the observer with a typeURI of
|
|
947
|
+
// service/audit. The framework version pins observer.id so an auditor
|
|
948
|
+
// can correlate envelope-level events back to a deployment.
|
|
949
|
+
function _toCadfOutcome(outcome) {
|
|
950
|
+
if (outcome === "success") return "success";
|
|
951
|
+
if (outcome === "failure" || outcome === "denied") return "failure";
|
|
952
|
+
if (outcome === "warning") return "unknown";
|
|
953
|
+
return outcome || "unknown";
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
function _toCadfEvent(row) {
|
|
957
|
+
var meta = null;
|
|
958
|
+
if (row.metadata) {
|
|
959
|
+
try { meta = typeof row.metadata === "string" ? jsonSafe.parse(row.metadata) : row.metadata; }
|
|
960
|
+
catch (_e) { meta = { raw: String(row.metadata) }; }
|
|
961
|
+
}
|
|
962
|
+
var ev = {
|
|
963
|
+
typeURI: "http://schemas.dmtf.org/cloud/audit/1.0/event",
|
|
964
|
+
eventType: "activity",
|
|
965
|
+
id: row._id,
|
|
966
|
+
eventTime: new Date(Number(row.recordedAt)).toISOString(),
|
|
967
|
+
action: row.action,
|
|
968
|
+
outcome: _toCadfOutcome(row.outcome),
|
|
969
|
+
initiator: {
|
|
970
|
+
id: row.actorUserIdHash || row.actorUserId || "unknown",
|
|
971
|
+
typeURI: "service/security/account/user",
|
|
972
|
+
addresses: row.actorIp ? [{ url: row.actorIp, name: "actorIp" }] : undefined,
|
|
973
|
+
name: row.actorSessionId || undefined,
|
|
974
|
+
},
|
|
975
|
+
target: {
|
|
976
|
+
id: row.resourceIdHash || row.resourceId || row.resourceKind || "n/a",
|
|
977
|
+
typeURI: row.resourceKind ? ("service/storage/" + row.resourceKind) : "service/security",
|
|
978
|
+
},
|
|
979
|
+
observer: {
|
|
980
|
+
id: "blamejs:" + (pkg.version || "unknown"),
|
|
981
|
+
typeURI: "service/security/audit",
|
|
982
|
+
name: "blamejs.audit",
|
|
983
|
+
},
|
|
984
|
+
reason: row.reason ? {
|
|
985
|
+
reasonCode: String(row.reason).slice(0, 256), // allow:raw-byte-literal — reason cap
|
|
986
|
+
policyType: "blamejs.audit-chain",
|
|
987
|
+
} : undefined,
|
|
988
|
+
attachments: meta ? [{
|
|
989
|
+
contentType: "application/json",
|
|
990
|
+
content: JSON.stringify(meta),
|
|
991
|
+
name: "blamejs.metadata",
|
|
992
|
+
}] : undefined,
|
|
993
|
+
// Custom CADF extension — anchors back into the audit chain.
|
|
994
|
+
"blamejs:chain": {
|
|
995
|
+
monotonicCounter: Number(row.monotonicCounter),
|
|
996
|
+
prevHash: row.prevHash,
|
|
997
|
+
rowHash: row.rowHash,
|
|
998
|
+
},
|
|
999
|
+
};
|
|
1000
|
+
return ev;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/**
|
|
1004
|
+
* @primitive b.auditTools.exportCadf
|
|
1005
|
+
* @signature b.auditTools.exportCadf(opts)
|
|
1006
|
+
* @since 0.7.30
|
|
1007
|
+
* @compliance soc2, pci-dss, gdpr
|
|
1008
|
+
* @related b.auditTools.exportAudit, b.auditTools.exportSlice
|
|
1009
|
+
*
|
|
1010
|
+
* Format an audit slice as a CADF event-batch (Cloud Auditing Data
|
|
1011
|
+
* Federation, ISO/IEC 19395:2017 + DMTF) — the FedRAMP / OpenStack
|
|
1012
|
+
* envelope cross-tenant SIEMs and CSP reporting tools expect for
|
|
1013
|
+
* federated tooling. Maps blamejs fields onto CADF attributes
|
|
1014
|
+
* (initiator / target / observer / outcome / reason) and embeds a
|
|
1015
|
+
* `blamejs:chain` extension carrying `monotonicCounter` / prevHash /
|
|
1016
|
+
* rowHash so auditors can correlate the envelope back to the chain.
|
|
1017
|
+
*
|
|
1018
|
+
* Returns an object with `events: [...]` ready to ship as JSON.
|
|
1019
|
+
*
|
|
1020
|
+
* @opts
|
|
1021
|
+
* format: "cadf", // optional — defaults to "cadf"
|
|
1022
|
+
* from: number|Date|string, // recordedAt >= this
|
|
1023
|
+
* to: number|Date|string, // recordedAt <= this
|
|
1024
|
+
* action: string, // exact action filter
|
|
1025
|
+
*
|
|
1026
|
+
* @example
|
|
1027
|
+
* var batch = await b.auditTools.exportCadf({
|
|
1028
|
+
* from: "2026-05-01T00:00:00Z",
|
|
1029
|
+
* to: "2026-05-08T00:00:00Z",
|
|
1030
|
+
* action: "auth.login",
|
|
1031
|
+
* });
|
|
1032
|
+
* // → { typeURI: ".../event-batch", framework: "blamejs", events: [...] }
|
|
1033
|
+
*/
|
|
1034
|
+
async function exportCadf(opts) {
|
|
1035
|
+
opts = opts || {};
|
|
1036
|
+
if (opts.format !== undefined && opts.format !== "cadf") {
|
|
1037
|
+
throw new AuditToolsError("audit-tools/bad-format",
|
|
1038
|
+
"audit.export: format must be 'cadf' for exportCadf");
|
|
1039
|
+
}
|
|
1040
|
+
var fromMs = _toMs(opts.from);
|
|
1041
|
+
var toMs = _toMs(opts.to);
|
|
1042
|
+
var readRows = opts.readRows || _defaultReadRows;
|
|
1043
|
+
var criteria = {};
|
|
1044
|
+
if (fromMs != null) criteria.fromMs = fromMs;
|
|
1045
|
+
if (toMs != null) criteria.toMs = toMs;
|
|
1046
|
+
if (opts.action) criteria.action = opts.action;
|
|
1047
|
+
var rows = await readRows(criteria);
|
|
1048
|
+
var events = new Array(rows.length);
|
|
1049
|
+
for (var i = 0; i < rows.length; i++) {
|
|
1050
|
+
events[i] = _toCadfEvent(rows[i]);
|
|
1051
|
+
}
|
|
1052
|
+
return {
|
|
1053
|
+
typeURI: "http://schemas.dmtf.org/cloud/audit/1.0/event-batch",
|
|
1054
|
+
framework: "blamejs",
|
|
1055
|
+
frameworkVersion: pkg.version,
|
|
1056
|
+
range: {
|
|
1057
|
+
from: fromMs != null ? new Date(fromMs).toISOString() : null,
|
|
1058
|
+
to: toMs != null ? new Date(toMs).toISOString() : null,
|
|
1059
|
+
},
|
|
1060
|
+
events: events,
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// Operator-facing dispatcher — `b.audit.export({ format })`. Future
|
|
1065
|
+
// formats register here.
|
|
1066
|
+
/**
|
|
1067
|
+
* @primitive b.auditTools.exportAudit
|
|
1068
|
+
* @signature b.auditTools.exportAudit(opts)
|
|
1069
|
+
* @since 0.7.30
|
|
1070
|
+
* @compliance soc2, pci-dss, gdpr
|
|
1071
|
+
* @related b.auditTools.exportCadf, b.auditTools.exportSlice
|
|
1072
|
+
*
|
|
1073
|
+
* Format dispatcher for downstream-SIEM exports. Reads `opts.format`
|
|
1074
|
+
* (default `"cadf"`) and delegates to the matching formatter. Future
|
|
1075
|
+
* envelope formats (CEF / OCSF / etc.) register here so callers stay
|
|
1076
|
+
* on a stable signature even when the framework adds formats.
|
|
1077
|
+
*
|
|
1078
|
+
* @opts
|
|
1079
|
+
* format: "cadf", // selector — defaults to "cadf"
|
|
1080
|
+
* from: number|Date|string, // recordedAt >= this
|
|
1081
|
+
* to: number|Date|string, // recordedAt <= this
|
|
1082
|
+
* action: string, // exact action filter
|
|
1083
|
+
*
|
|
1084
|
+
* @example
|
|
1085
|
+
* var batch = await b.auditTools.exportAudit({
|
|
1086
|
+
* format: "cadf",
|
|
1087
|
+
* from: "2026-05-01T00:00:00Z",
|
|
1088
|
+
* to: "2026-05-08T00:00:00Z",
|
|
1089
|
+
* });
|
|
1090
|
+
* // → { typeURI: ".../event-batch", framework: "blamejs", events: [...] }
|
|
1091
|
+
*/
|
|
1092
|
+
async function exportAudit(opts) {
|
|
1093
|
+
opts = opts || {};
|
|
1094
|
+
var format = opts.format || "cadf";
|
|
1095
|
+
if (format === "cadf") return await exportCadf(opts);
|
|
1096
|
+
throw new AuditToolsError("audit-tools/bad-format",
|
|
1097
|
+
"audit.export: format must be one of: cadf (got '" + format + "')");
|
|
1098
|
+
}
|
|
1099
|
+
|
|
753
1100
|
module.exports = {
|
|
754
1101
|
archive: archive,
|
|
755
1102
|
exportSlice: exportSlice,
|
|
1103
|
+
exportAudit: exportAudit,
|
|
1104
|
+
exportCadf: exportCadf,
|
|
756
1105
|
forensicSnapshot: forensicSnapshot,
|
|
757
1106
|
verifyBundle: verifyBundle,
|
|
758
1107
|
purge: purge,
|