@blamejs/core 0.8.43 → 0.8.49
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 +92 -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-chain.js
CHANGED
|
@@ -1,22 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* @module b.auditChain
|
|
4
|
+
* @nav Observability
|
|
5
|
+
* @title Audit Chain Primitives
|
|
4
6
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
+
* @intro
|
|
8
|
+
* Low-level audit-chain hash + verify primitives — `b.audit` composes
|
|
9
|
+
* on top of these so operators rarely call them directly. Every audit
|
|
10
|
+
* row carries `prevHash` + `rowHash` + `nonce` and the chain math is:
|
|
7
11
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
12
|
+
* rowHash = SHA3-512(
|
|
13
|
+
* prevHash || canonicalize(row-fields-except-hash) || nonce
|
|
14
|
+
* )
|
|
11
15
|
*
|
|
12
|
-
*
|
|
13
|
-
* order
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
+
* Each row's `prevHash` equals the previous row's `rowHash` in
|
|
17
|
+
* monotonic-counter order. The first row uses `ZERO_HASH` as the
|
|
18
|
+
* anchor. `verifyChain` walks every row forward, recomputing each
|
|
19
|
+
* hash; any mismatch returns `{ ok: false, reason, breakAt, ... }`
|
|
20
|
+
* and the caller (audit boot, `b.cli verify-chain`, restore-rollback,
|
|
21
|
+
* forensic snapshot) decides whether to refuse-to-boot or just log.
|
|
16
22
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
23
|
+
* Checkpoint signing (SLH-DSA-SHAKE-256f over `(atRow || atRowHash)`)
|
|
24
|
+
* lives in `b.auditSign`. This module owns the chain hash math only;
|
|
25
|
+
* verification is O(n) over `audit_log` rows.
|
|
26
|
+
*
|
|
27
|
+
* Operators reach for `b.auditChain.verifyChain` directly when
|
|
28
|
+
* restoring from backup (verify the restored DB before promoting it),
|
|
29
|
+
* when running a forensic offline check, or when extending the chain
|
|
30
|
+
* primitive into a custom append-only table. Day-to-day appends go
|
|
31
|
+
* through `b.audit.record` / `b.audit.safeEmit`.
|
|
32
|
+
*
|
|
33
|
+
* @card
|
|
34
|
+
* Low-level audit-chain hash + verify primitives — `b.audit` composes on top of these so operators rarely call them directly.
|
|
20
35
|
*/
|
|
21
36
|
var canonicalJson = require("./canonical-json");
|
|
22
37
|
var C = require("./constants");
|
|
@@ -31,13 +46,26 @@ var SHA3_512_HEX_LEN = SHA3_512_BYTES * 2;
|
|
|
31
46
|
// All-zero SHA3-512 sentinel prevHash for the first row.
|
|
32
47
|
var ZERO_HASH = "0".repeat(SHA3_512_HEX_LEN);
|
|
33
48
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
49
|
+
/**
|
|
50
|
+
* @primitive b.auditChain.canonicalize
|
|
51
|
+
* @signature b.auditChain.canonicalize(row, excludeKeys)
|
|
52
|
+
* @since 0.6.67
|
|
53
|
+
* @related b.auditChain.computeRowHash
|
|
54
|
+
*
|
|
55
|
+
* RFC 8785 (JSON Canonicalization Scheme) serialization of an audit
|
|
56
|
+
* row's logical fields, used as the middle slice of the row-hash
|
|
57
|
+
* preimage. Sorted keys, Buffer values rendered as hex, every other
|
|
58
|
+
* value passed through the shared `lib/canonical-json` walker so the
|
|
59
|
+
* four canonicalize sites in the framework (chain, audit-tools,
|
|
60
|
+
* config-drift, pagination) emit byte-identical output.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* var bytes = b.auditChain.canonicalize(
|
|
64
|
+
* { actor: "u-42", action: "auth.login.success", recordedAt: 1700000000000 },
|
|
65
|
+
* ["prevHash", "rowHash", "nonce"]
|
|
66
|
+
* );
|
|
67
|
+
* // → '{"action":"auth.login.success","actor":"u-42","recordedAt":1700000000000}'
|
|
68
|
+
*/
|
|
41
69
|
function canonicalize(row, excludeKeys) {
|
|
42
70
|
var ex = new Set(excludeKeys || []);
|
|
43
71
|
var keys = Object.keys(row).filter(function (k) { return !ex.has(k); }).sort();
|
|
@@ -48,8 +76,30 @@ function canonicalize(row, excludeKeys) {
|
|
|
48
76
|
return canonicalJson.stringify(pairs);
|
|
49
77
|
}
|
|
50
78
|
|
|
51
|
-
|
|
52
|
-
|
|
79
|
+
/**
|
|
80
|
+
* @primitive b.auditChain.computeRowHash
|
|
81
|
+
* @signature b.auditChain.computeRowHash(prevHash, rowFields, nonce)
|
|
82
|
+
* @since 0.4.0
|
|
83
|
+
* @related b.auditChain.verifyChain, b.auditChain.canonicalize
|
|
84
|
+
*
|
|
85
|
+
* Compute a row's `rowHash` given its predecessor's hash, the row's
|
|
86
|
+
* logical fields (already excluding `prevHash` / `rowHash` / `nonce`),
|
|
87
|
+
* and the row's nonce buffer. The hash is `SHA3-512(prevHashBytes ||
|
|
88
|
+
* canonicalize(rowFields) || nonce)`, returned as a 128-char lowercase
|
|
89
|
+
* hex string.
|
|
90
|
+
*
|
|
91
|
+
* `prevHash` must be the 128-char hex form (use `b.auditChain.ZERO_HASH`
|
|
92
|
+
* for the chain anchor). `nonce` must be a non-empty Buffer; the
|
|
93
|
+
* framework writes 16 random bytes per row.
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* var rowHash = b.auditChain.computeRowHash(
|
|
97
|
+
* b.auditChain.ZERO_HASH,
|
|
98
|
+
* { action: "system.boot", recordedAt: 1700000000000, outcome: "success" },
|
|
99
|
+
* Buffer.from("0123456789abcdef0123456789abcdef", "hex")
|
|
100
|
+
* );
|
|
101
|
+
* // → "<128-char SHA3-512 hex>"
|
|
102
|
+
*/
|
|
53
103
|
function computeRowHash(prevHash, rowFields, nonce) {
|
|
54
104
|
if (typeof prevHash !== "string" || prevHash.length !== SHA3_512_HEX_LEN) {
|
|
55
105
|
throw new Error("prevHash must be a " + SHA3_512_HEX_LEN +
|
|
@@ -68,9 +118,27 @@ function computeRowHash(prevHash, rowFields, nonce) {
|
|
|
68
118
|
return sha3Hash(input);
|
|
69
119
|
}
|
|
70
120
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
121
|
+
/**
|
|
122
|
+
* @primitive b.auditChain.getChainTip
|
|
123
|
+
* @signature b.auditChain.getChainTip(queryOneAsync, tableName)
|
|
124
|
+
* @since 0.4.0
|
|
125
|
+
* @related b.auditChain.verifyChain, b.auditChain.computeRowHash
|
|
126
|
+
*
|
|
127
|
+
* Read the current chain tip (last row's `rowHash` + `monotonicCounter`)
|
|
128
|
+
* for a given audit table. Empty tables return
|
|
129
|
+
* `{ prevHash: ZERO_HASH, counter: 0 }` so callers can treat first-row
|
|
130
|
+
* insert and append uniformly. Async so operator-supplied external-db
|
|
131
|
+
* drivers can use any await-able query function of the shape
|
|
132
|
+
* `async (sql, params?) -> row | null`.
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* async function queryOne(sql) {
|
|
136
|
+
* var rows = await myDriver.query(sql);
|
|
137
|
+
* return rows[0] || null;
|
|
138
|
+
* }
|
|
139
|
+
* var tip = await b.auditChain.getChainTip(queryOne, "audit_log");
|
|
140
|
+
* // → { prevHash: "<128-char hex>", counter: 4217 }
|
|
141
|
+
*/
|
|
74
142
|
async function getChainTip(queryOneAsync, tableName) {
|
|
75
143
|
var row = await queryOneAsync(
|
|
76
144
|
'SELECT rowHash, monotonicCounter FROM "' + tableName + '" ' +
|
|
@@ -80,15 +148,36 @@ async function getChainTip(queryOneAsync, tableName) {
|
|
|
80
148
|
return { prevHash: row.rowHash, counter: row.monotonicCounter };
|
|
81
149
|
}
|
|
82
150
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
151
|
+
/**
|
|
152
|
+
* @primitive b.auditChain.verifyChain
|
|
153
|
+
* @signature b.auditChain.verifyChain(queryAllAsync, tableName, opts)
|
|
154
|
+
* @since 0.4.0
|
|
155
|
+
* @related b.auditChain.getChainTip, b.audit.verify, b.auditTools.archive
|
|
156
|
+
*
|
|
157
|
+
* Walk the entire chain forward, recomputing each row's hash and
|
|
158
|
+
* comparing against the stored `prevHash` / `rowHash`. Returns
|
|
159
|
+
* `{ ok: true, table, rowsVerified, lastHash }` on a clean walk, or
|
|
160
|
+
* `{ ok: false, table, rowsVerified, breakAt, breakRowId, reason,
|
|
161
|
+
* expected, actual }` on the first mismatch. Callers decide how to
|
|
162
|
+
* react — `b.audit.verify` refuses-to-boot, `b.cli verify-chain`
|
|
163
|
+
* exits non-zero, `b.restoreRollback` blocks promotion.
|
|
164
|
+
*
|
|
165
|
+
* For `audit_log`: if a `_blamejs_audit_purge_anchor` row exists, the
|
|
166
|
+
* walk starts at `lastPurgedCounter+1` with `prevHash =
|
|
167
|
+
* lastPurgedRowHash`. The anchor is written by `b.auditTools.purge`
|
|
168
|
+
* after a successful archive and lets the chain math survive deletion
|
|
169
|
+
* of historical rows without the archive bundle as source of truth.
|
|
170
|
+
*
|
|
171
|
+
* @opts
|
|
172
|
+
* {
|
|
173
|
+
* maxRows?: number, // stop after N rows (default: walk every row)
|
|
174
|
+
* }
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* async function queryAll(sql) { return await myDriver.query(sql); }
|
|
178
|
+
* var result = await b.auditChain.verifyChain(queryAll, "audit_log", {});
|
|
179
|
+
* // → { ok: true, table: "audit_log", rowsVerified: 4217, lastHash: "<hex>" }
|
|
180
|
+
*/
|
|
92
181
|
async function verifyChain(queryAllAsync, tableName, opts) {
|
|
93
182
|
opts = opts || {};
|
|
94
183
|
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.auditDailyReview
|
|
4
|
+
* @nav Compliance
|
|
5
|
+
* @title Audit Daily Review
|
|
6
|
+
*
|
|
7
|
+
* @intro
|
|
8
|
+
* PCI DSS 4.0 Req 10.4.1.1 daily-review primitive (mandatory
|
|
9
|
+
* effective 2025-03-31). Automated review of all security event
|
|
10
|
+
* logs, CHD/SAD components, critical system components, and security-
|
|
11
|
+
* function components — surfacing anomalies and exceptions for
|
|
12
|
+
* follow-up. The framework provides scheduling, query, severity
|
|
13
|
+
* classification, and notify wiring; the operator supplies the
|
|
14
|
+
* notify channel and any post-review workflow.
|
|
15
|
+
*
|
|
16
|
+
* Adjacent regimes covered: HIPAA §164.308(a)(1)(ii)(D) (regular
|
|
17
|
+
* review of activity records), SOX §302/§404 (quarterly self-
|
|
18
|
+
* attestation), SOC 2 CC7.2 (anomaly identification and response),
|
|
19
|
+
* GDPR Art. 32 (ongoing security testing/evaluation). When `posture`
|
|
20
|
+
* is one of `pci-dss` / `hipaa` / `sox` / `soc2`, a `notify`
|
|
21
|
+
* callback is mandatory at create-time — the regulators all demand
|
|
22
|
+
* a follow-up channel.
|
|
23
|
+
*
|
|
24
|
+
* Severity classification: `denied` / `failure` outcomes default to
|
|
25
|
+
* `warning`; `auth.fail*` / `audit.read` / `csrf.bad_*` / `ato.*` /
|
|
26
|
+
* `honeytoken.tripped` / `breakglass.*` / `ddl.change.applied`
|
|
27
|
+
* raise to `alert`; `audit.tamper*` / `vault.aad.unseal_failed` /
|
|
28
|
+
* `config.drift.detected` / `vendor.integrity.tampered` /
|
|
29
|
+
* `ato.killSwitch.tripped` raise to `critical`. Operators with
|
|
30
|
+
* richer rules pass `opts.classify(event) → severity`.
|
|
31
|
+
*
|
|
32
|
+
* Audit events: `audit.daily_review.completed` (every run),
|
|
33
|
+
* `.notified` (notify fired), `.notify_failed` (notify threw or
|
|
34
|
+
* rejected; the review itself still completed), `.scheduled`,
|
|
35
|
+
* `.stopped`.
|
|
36
|
+
*
|
|
37
|
+
* @card
|
|
38
|
+
* PCI DSS 4.0 Req 10.4.1.1 daily-review primitive (mandatory effective 2025-03-31).
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
var validateOpts = require("./validate-opts");
|
|
42
|
+
var C = require("./constants");
|
|
43
|
+
var { AuditDailyReviewError } = require("./framework-error");
|
|
44
|
+
|
|
45
|
+
var SEVERITY_ORDER = ["info", "notice", "warning", "alert", "critical"];
|
|
46
|
+
|
|
47
|
+
var ALERT_PATTERNS = [
|
|
48
|
+
/^auth\.(fail|failed|locked|denied|invalid)/,
|
|
49
|
+
/^audit\.read$/,
|
|
50
|
+
/^audit\.tamper/,
|
|
51
|
+
/^csrf\.bad_/,
|
|
52
|
+
/^ato\./,
|
|
53
|
+
/^honeytoken\.tripped/,
|
|
54
|
+
/^compliance\.posture\.set_rejected/,
|
|
55
|
+
/^audit\.actor_binding\.violation/,
|
|
56
|
+
/^ddl\.change\.applied/,
|
|
57
|
+
/^breakglass\./,
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
var CRITICAL_PATTERNS = [
|
|
61
|
+
/^audit\.tamper/,
|
|
62
|
+
/^vault\.aad\.unseal_failed/,
|
|
63
|
+
/^config\.drift\.detected/,
|
|
64
|
+
/^vendor\.integrity\.tampered/,
|
|
65
|
+
/^ato\.killSwitch\.tripped/,
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
var POSTURES_REQUIRING_NOTIFY = ["pci-dss", "hipaa", "sox", "soc2"];
|
|
69
|
+
|
|
70
|
+
function _defaultClassify(event) {
|
|
71
|
+
if (!event || typeof event !== "object" || typeof event.action !== "string") {
|
|
72
|
+
return "info";
|
|
73
|
+
}
|
|
74
|
+
var action = event.action;
|
|
75
|
+
for (var i = 0; i < CRITICAL_PATTERNS.length; i++) {
|
|
76
|
+
if (CRITICAL_PATTERNS[i].test(action)) return "critical";
|
|
77
|
+
}
|
|
78
|
+
for (var j = 0; j < ALERT_PATTERNS.length; j++) {
|
|
79
|
+
if (ALERT_PATTERNS[j].test(action)) return "alert";
|
|
80
|
+
}
|
|
81
|
+
if (event.outcome === "denied" || event.outcome === "failure") return "warning";
|
|
82
|
+
return "info";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function _severityAtLeast(severity, threshold) {
|
|
86
|
+
var sIdx = SEVERITY_ORDER.indexOf(severity);
|
|
87
|
+
var tIdx = SEVERITY_ORDER.indexOf(threshold);
|
|
88
|
+
if (sIdx === -1 || tIdx === -1) return false;
|
|
89
|
+
return sIdx >= tIdx;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function _err(code, msg) {
|
|
93
|
+
return new AuditDailyReviewError(code, msg);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @primitive b.auditDailyReview.create
|
|
98
|
+
* @signature b.auditDailyReview.create(opts)
|
|
99
|
+
* @since 0.8.48
|
|
100
|
+
* @status stable
|
|
101
|
+
* @compliance pci-dss, hipaa, sox-404, soc2, gdpr
|
|
102
|
+
* @related b.audit, b.scheduler, b.compliance
|
|
103
|
+
*
|
|
104
|
+
* Build a daily-review scheduler. Returns
|
|
105
|
+
* `{ run, list, lastRun, schedule, start, stop, classify, posture,
|
|
106
|
+
* cron, severityThreshold, lookbackHours }`. `run()` executes a single
|
|
107
|
+
* review window on demand; `start()` arms the scheduler so the review
|
|
108
|
+
* fires on the configured cron; `list()` returns the bounded history
|
|
109
|
+
* buffer of past summaries.
|
|
110
|
+
*
|
|
111
|
+
* @opts
|
|
112
|
+
* audit: Object, // b.audit instance (query / safeEmit)
|
|
113
|
+
* scheduler: Object, // b.scheduler instance; required for start()
|
|
114
|
+
* lookbackHours: number, // window size in hours (default 24)
|
|
115
|
+
* severityThreshold: string, // info|notice|warning|alert|critical (default "warning")
|
|
116
|
+
* posture: string, // pci-dss | hipaa | sox-404 | soc2 | …
|
|
117
|
+
* cron: string, // POSIX 5-field expr (default "0 6 * * *")
|
|
118
|
+
* notify: Function, // async (summary) → void; required under listed postures
|
|
119
|
+
* classify: Function, // (event) → severity; default action-prefix table
|
|
120
|
+
* queryLimit: number, // max rows pulled from audit.query (default 10000)
|
|
121
|
+
* historyLimit: number, // bounded summary buffer (default 30)
|
|
122
|
+
* now: Function, // () → number; testing override
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* var review = b.auditDailyReview.create({
|
|
126
|
+
* audit: auditInstance,
|
|
127
|
+
* scheduler: schedulerInstance,
|
|
128
|
+
* lookbackHours: 24,
|
|
129
|
+
* severityThreshold: "warning",
|
|
130
|
+
* posture: "pci-dss",
|
|
131
|
+
* cron: "0 6 * * *",
|
|
132
|
+
* notify: async function (summary) {
|
|
133
|
+
* if (summary.hitCount > 0) {
|
|
134
|
+
* // page on-call with summary.thresholdHits
|
|
135
|
+
* }
|
|
136
|
+
* },
|
|
137
|
+
* });
|
|
138
|
+
*
|
|
139
|
+
* // On-demand:
|
|
140
|
+
* var summary = await review.run();
|
|
141
|
+
* summary.totalEvents; // → 1842
|
|
142
|
+
* summary.bySeverity; // → { info: 1700, warning: 120, alert: 22, critical: 0, notice: 0 }
|
|
143
|
+
* summary.hitCount; // → 142 (events at-or-above warning)
|
|
144
|
+
*
|
|
145
|
+
* // Or arm the scheduler so the review fires nightly at 06:00 UTC:
|
|
146
|
+
* await review.start();
|
|
147
|
+
* review.lastRun(); // → most recent summary or null
|
|
148
|
+
*/
|
|
149
|
+
function create(opts) {
|
|
150
|
+
opts = opts || {};
|
|
151
|
+
validateOpts(opts, [
|
|
152
|
+
"audit", "scheduler", "lookbackHours", "severityThreshold",
|
|
153
|
+
"posture", "cron", "notify", "classify", "queryLimit", "historyLimit",
|
|
154
|
+
"now",
|
|
155
|
+
], "auditDailyReview.create");
|
|
156
|
+
|
|
157
|
+
validateOpts.auditShape(opts.audit, "auditDailyReview",
|
|
158
|
+
AuditDailyReviewError, "auditDailyReview/bad-audit");
|
|
159
|
+
if (!opts.audit) {
|
|
160
|
+
throw _err("auditDailyReview/audit-required",
|
|
161
|
+
"auditDailyReview.create: opts.audit is required (must expose query() / safeEmit())");
|
|
162
|
+
}
|
|
163
|
+
if (typeof opts.audit.query !== "function") {
|
|
164
|
+
throw _err("auditDailyReview/audit-query-missing",
|
|
165
|
+
"auditDailyReview.create: opts.audit.query must be a function");
|
|
166
|
+
}
|
|
167
|
+
validateOpts.optionalFunction(opts.notify,
|
|
168
|
+
"auditDailyReview: notify", AuditDailyReviewError, "auditDailyReview/bad-notify");
|
|
169
|
+
validateOpts.optionalFunction(opts.classify,
|
|
170
|
+
"auditDailyReview: classify", AuditDailyReviewError, "auditDailyReview/bad-classify");
|
|
171
|
+
validateOpts.optionalFunction(opts.now,
|
|
172
|
+
"auditDailyReview: now", AuditDailyReviewError, "auditDailyReview/bad-now");
|
|
173
|
+
validateOpts.optionalNonEmptyString(opts.posture,
|
|
174
|
+
"auditDailyReview: posture", AuditDailyReviewError, "auditDailyReview/bad-posture");
|
|
175
|
+
validateOpts.optionalNonEmptyString(opts.cron,
|
|
176
|
+
"auditDailyReview: cron", AuditDailyReviewError, "auditDailyReview/bad-cron");
|
|
177
|
+
validateOpts.optionalPositiveInt(opts.queryLimit,
|
|
178
|
+
"auditDailyReview: queryLimit", AuditDailyReviewError, "auditDailyReview/bad-querylimit");
|
|
179
|
+
validateOpts.optionalPositiveInt(opts.historyLimit,
|
|
180
|
+
"auditDailyReview: historyLimit", AuditDailyReviewError, "auditDailyReview/bad-historylimit");
|
|
181
|
+
|
|
182
|
+
// lookbackHours — default 24 per PCI DSS 4.0 daily cadence. Caller can
|
|
183
|
+
// pass weekly / monthly via larger numbers.
|
|
184
|
+
var lookbackHours = 24; // allow:raw-byte-literal — lookback in HOURS, not bytes
|
|
185
|
+
if (opts.lookbackHours !== undefined) {
|
|
186
|
+
if (typeof opts.lookbackHours !== "number" || !isFinite(opts.lookbackHours) ||
|
|
187
|
+
opts.lookbackHours <= 0) {
|
|
188
|
+
throw _err("auditDailyReview/bad-lookback",
|
|
189
|
+
"auditDailyReview.create: lookbackHours must be a positive finite number");
|
|
190
|
+
}
|
|
191
|
+
lookbackHours = opts.lookbackHours;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
var severityThreshold = opts.severityThreshold || "warning";
|
|
195
|
+
if (SEVERITY_ORDER.indexOf(severityThreshold) === -1) {
|
|
196
|
+
throw _err("auditDailyReview/bad-severity",
|
|
197
|
+
"auditDailyReview.create: severityThreshold must be one of " +
|
|
198
|
+
SEVERITY_ORDER.join(", "));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
var posture = opts.posture || null;
|
|
202
|
+
if (posture && POSTURES_REQUIRING_NOTIFY.indexOf(posture) !== -1 && !opts.notify) {
|
|
203
|
+
throw _err("auditDailyReview/notify-required-under-posture",
|
|
204
|
+
"auditDailyReview.create: posture '" + posture + "' requires notify callback " +
|
|
205
|
+
"(PCI DSS 10.4.1.1 / HIPAA §164.308(a)(1)(ii)(D) demand a follow-up channel)");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
var cron = opts.cron || "0 6 * * *"; // 06:00 UTC daily
|
|
209
|
+
var queryLimit = opts.queryLimit || 10000; // allow:raw-byte-literal — operator-tunable result cap, count not bytes
|
|
210
|
+
var historyLimit = opts.historyLimit || 30; // allow:raw-byte-literal — bounded history buffer (count, not bytes)
|
|
211
|
+
var classify = typeof opts.classify === "function" ? opts.classify : _defaultClassify;
|
|
212
|
+
var now = typeof opts.now === "function" ? opts.now : Date.now;
|
|
213
|
+
var auditMod = opts.audit;
|
|
214
|
+
var notify = typeof opts.notify === "function" ? opts.notify : null;
|
|
215
|
+
var schedulerMod = opts.scheduler || null;
|
|
216
|
+
|
|
217
|
+
var history = [];
|
|
218
|
+
var taskName = "blamejs.auditDailyReview." + (posture || "default");
|
|
219
|
+
var armedScheduler = null;
|
|
220
|
+
|
|
221
|
+
function _emit(action, metadata, outcome) {
|
|
222
|
+
try {
|
|
223
|
+
auditMod.safeEmit({
|
|
224
|
+
action: action,
|
|
225
|
+
outcome: outcome || "success",
|
|
226
|
+
metadata: metadata || {},
|
|
227
|
+
});
|
|
228
|
+
} catch (_e) { /* audit best-effort */ }
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function run() {
|
|
232
|
+
var startedAt = now();
|
|
233
|
+
var fromMs = startedAt - C.TIME.hours(lookbackHours);
|
|
234
|
+
var rows;
|
|
235
|
+
try {
|
|
236
|
+
rows = await auditMod.query({
|
|
237
|
+
from: fromMs,
|
|
238
|
+
to: startedAt,
|
|
239
|
+
limit: queryLimit,
|
|
240
|
+
});
|
|
241
|
+
} catch (e) {
|
|
242
|
+
_emit("audit.daily_review.failed", {
|
|
243
|
+
reason: (e && e.message) || String(e),
|
|
244
|
+
lookbackHours: lookbackHours,
|
|
245
|
+
}, "failure");
|
|
246
|
+
throw _err("auditDailyReview/query-failed",
|
|
247
|
+
"auditDailyReview.run: audit.query failed: " + ((e && e.message) || String(e)));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
var bySeverity = { info: 0, notice: 0, warning: 0, alert: 0, critical: 0 };
|
|
251
|
+
var byOutcome = { success: 0, failure: 0, denied: 0, other: 0 };
|
|
252
|
+
var byNamespace = Object.create(null);
|
|
253
|
+
var thresholdHits = [];
|
|
254
|
+
for (var i = 0; i < rows.length; i++) {
|
|
255
|
+
var r = rows[i];
|
|
256
|
+
var sev = classify(r);
|
|
257
|
+
if (bySeverity[sev] === undefined) bySeverity[sev] = 0;
|
|
258
|
+
bySeverity[sev]++;
|
|
259
|
+
|
|
260
|
+
var oc = r && r.outcome;
|
|
261
|
+
if (oc === "success" || oc === "failure" || oc === "denied") byOutcome[oc]++;
|
|
262
|
+
else byOutcome.other++;
|
|
263
|
+
|
|
264
|
+
var ns = (r && typeof r.action === "string") ? r.action.split(".")[0] : "unknown";
|
|
265
|
+
byNamespace[ns] = (byNamespace[ns] || 0) + 1;
|
|
266
|
+
|
|
267
|
+
if (_severityAtLeast(sev, severityThreshold)) {
|
|
268
|
+
thresholdHits.push({
|
|
269
|
+
action: r.action,
|
|
270
|
+
outcome: r.outcome,
|
|
271
|
+
severity: sev,
|
|
272
|
+
recordedAt: r.recordedAt,
|
|
273
|
+
actorUserId: r.actorUserId || null,
|
|
274
|
+
requestId: r.requestId || null,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
var summary = {
|
|
280
|
+
runAt: new Date(startedAt).toISOString(),
|
|
281
|
+
lookbackHours: lookbackHours,
|
|
282
|
+
windowFromMs: fromMs,
|
|
283
|
+
windowToMs: startedAt,
|
|
284
|
+
totalEvents: rows.length,
|
|
285
|
+
bySeverity: bySeverity,
|
|
286
|
+
byOutcome: byOutcome,
|
|
287
|
+
byNamespace: byNamespace,
|
|
288
|
+
severityThreshold: severityThreshold,
|
|
289
|
+
thresholdHits: thresholdHits,
|
|
290
|
+
hitCount: thresholdHits.length,
|
|
291
|
+
durationMs: now() - startedAt,
|
|
292
|
+
posture: posture,
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
history.push(summary);
|
|
296
|
+
if (history.length > historyLimit) history.splice(0, history.length - historyLimit);
|
|
297
|
+
|
|
298
|
+
_emit("audit.daily_review.completed", {
|
|
299
|
+
lookbackHours: lookbackHours,
|
|
300
|
+
totalEvents: summary.totalEvents,
|
|
301
|
+
hitCount: summary.hitCount,
|
|
302
|
+
durationMs: summary.durationMs,
|
|
303
|
+
posture: posture,
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
if (notify && thresholdHits.length > 0) {
|
|
307
|
+
try {
|
|
308
|
+
await notify(summary);
|
|
309
|
+
_emit("audit.daily_review.notified", {
|
|
310
|
+
hitCount: thresholdHits.length, posture: posture,
|
|
311
|
+
});
|
|
312
|
+
} catch (e) {
|
|
313
|
+
_emit("audit.daily_review.notify_failed", {
|
|
314
|
+
reason: (e && e.message) || String(e),
|
|
315
|
+
hitCount: thresholdHits.length, posture: posture,
|
|
316
|
+
}, "failure");
|
|
317
|
+
// Don't throw — the daily review completed, only notify failed.
|
|
318
|
+
// Operators read audit.daily_review.notify_failed to chase down
|
|
319
|
+
// their notify-channel outage.
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return summary;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function lastRun() {
|
|
327
|
+
return history.length > 0 ? history[history.length - 1] : null;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function list() {
|
|
331
|
+
return history.slice();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function schedule() {
|
|
335
|
+
return cron;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function start() {
|
|
339
|
+
if (!schedulerMod) {
|
|
340
|
+
throw _err("auditDailyReview/no-scheduler",
|
|
341
|
+
"auditDailyReview.start: opts.scheduler is required to arm the cron firing — " +
|
|
342
|
+
"operators without a scheduler call run() on their own cadence");
|
|
343
|
+
}
|
|
344
|
+
if (armedScheduler) return;
|
|
345
|
+
armedScheduler = schedulerMod;
|
|
346
|
+
armedScheduler.schedule({
|
|
347
|
+
name: taskName,
|
|
348
|
+
cron: cron,
|
|
349
|
+
run: run,
|
|
350
|
+
});
|
|
351
|
+
if (typeof armedScheduler.start === "function") {
|
|
352
|
+
// Scheduler.start() is idempotent — safe to call when the scheduler
|
|
353
|
+
// was already armed by other tasks.
|
|
354
|
+
try { await armedScheduler.start(); } catch (_e) { /* operator-controlled */ }
|
|
355
|
+
}
|
|
356
|
+
_emit("audit.daily_review.scheduled", {
|
|
357
|
+
cron: cron, taskName: taskName, posture: posture,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function stop() {
|
|
362
|
+
if (!armedScheduler) return;
|
|
363
|
+
armedScheduler = null;
|
|
364
|
+
_emit("audit.daily_review.stopped", { taskName: taskName, posture: posture });
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
run: run,
|
|
369
|
+
list: list,
|
|
370
|
+
lastRun: lastRun,
|
|
371
|
+
schedule: schedule,
|
|
372
|
+
start: start,
|
|
373
|
+
stop: stop,
|
|
374
|
+
classify: classify,
|
|
375
|
+
posture: posture,
|
|
376
|
+
cron: cron,
|
|
377
|
+
severityThreshold: severityThreshold,
|
|
378
|
+
lookbackHours: lookbackHours,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
module.exports = {
|
|
383
|
+
create: create,
|
|
384
|
+
SEVERITY_ORDER: SEVERITY_ORDER,
|
|
385
|
+
ALERT_PATTERNS: ALERT_PATTERNS,
|
|
386
|
+
CRITICAL_PATTERNS: CRITICAL_PATTERNS,
|
|
387
|
+
POSTURES_REQUIRING_NOTIFY: POSTURES_REQUIRING_NOTIFY,
|
|
388
|
+
AuditDailyReviewError: AuditDailyReviewError,
|
|
389
|
+
};
|