@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.
Files changed (222) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/README.md +10 -10
  3. package/index.js +52 -0
  4. package/lib/a2a.js +159 -34
  5. package/lib/acme.js +762 -0
  6. package/lib/ai-pref.js +166 -43
  7. package/lib/api-key.js +108 -47
  8. package/lib/api-snapshot.js +157 -40
  9. package/lib/app-shutdown.js +113 -77
  10. package/lib/archive.js +337 -40
  11. package/lib/arg-parser.js +697 -0
  12. package/lib/asyncapi.js +99 -55
  13. package/lib/atomic-file.js +465 -104
  14. package/lib/audit-chain.js +123 -34
  15. package/lib/audit-daily-review.js +389 -0
  16. package/lib/audit-sign.js +302 -56
  17. package/lib/audit-tools.js +412 -63
  18. package/lib/audit.js +656 -35
  19. package/lib/auth/jwt-external.js +17 -0
  20. package/lib/auth/oauth.js +7 -0
  21. package/lib/auth-bot-challenge.js +505 -0
  22. package/lib/auth-header.js +92 -25
  23. package/lib/backup/bundle.js +26 -0
  24. package/lib/backup/index.js +512 -89
  25. package/lib/backup/manifest.js +168 -7
  26. package/lib/break-glass.js +415 -39
  27. package/lib/budr.js +103 -30
  28. package/lib/bundler.js +86 -66
  29. package/lib/cache.js +192 -72
  30. package/lib/chain-writer.js +65 -40
  31. package/lib/circuit-breaker.js +56 -33
  32. package/lib/cli-helpers.js +106 -75
  33. package/lib/cli.js +6 -30
  34. package/lib/cloud-events.js +99 -32
  35. package/lib/cluster-storage.js +162 -37
  36. package/lib/cluster.js +340 -49
  37. package/lib/codepoint-class.js +66 -0
  38. package/lib/compliance.js +424 -24
  39. package/lib/config-drift.js +111 -46
  40. package/lib/config.js +94 -40
  41. package/lib/consent.js +165 -18
  42. package/lib/constants.js +1 -0
  43. package/lib/content-credentials.js +153 -48
  44. package/lib/cookies.js +154 -62
  45. package/lib/credential-hash.js +133 -61
  46. package/lib/crypto-field.js +702 -18
  47. package/lib/crypto-hpke.js +256 -0
  48. package/lib/crypto.js +744 -22
  49. package/lib/csv.js +178 -35
  50. package/lib/daemon.js +456 -0
  51. package/lib/dark-patterns.js +186 -55
  52. package/lib/db-query.js +79 -2
  53. package/lib/db.js +1431 -60
  54. package/lib/ddl-change-control.js +523 -0
  55. package/lib/deprecate.js +195 -40
  56. package/lib/dev.js +82 -39
  57. package/lib/dora.js +67 -48
  58. package/lib/dr-runbook.js +368 -0
  59. package/lib/dsr.js +142 -11
  60. package/lib/dual-control.js +91 -56
  61. package/lib/events.js +120 -41
  62. package/lib/external-db-migrate.js +192 -2
  63. package/lib/external-db.js +795 -50
  64. package/lib/fapi2.js +122 -1
  65. package/lib/fda-21cfr11.js +395 -0
  66. package/lib/fdx.js +132 -2
  67. package/lib/file-type.js +87 -0
  68. package/lib/file-upload.js +93 -0
  69. package/lib/flag.js +82 -20
  70. package/lib/forms.js +132 -29
  71. package/lib/framework-error.js +169 -0
  72. package/lib/framework-schema.js +163 -35
  73. package/lib/gate-contract.js +849 -175
  74. package/lib/graphql-federation.js +68 -7
  75. package/lib/guard-all.js +172 -55
  76. package/lib/guard-archive.js +286 -124
  77. package/lib/guard-auth.js +194 -21
  78. package/lib/guard-cidr.js +190 -28
  79. package/lib/guard-csv.js +397 -51
  80. package/lib/guard-domain.js +213 -91
  81. package/lib/guard-email.js +236 -29
  82. package/lib/guard-filename.js +307 -75
  83. package/lib/guard-graphql.js +263 -30
  84. package/lib/guard-html.js +310 -116
  85. package/lib/guard-image.js +243 -30
  86. package/lib/guard-json.js +260 -54
  87. package/lib/guard-jsonpath.js +235 -23
  88. package/lib/guard-jwt.js +284 -30
  89. package/lib/guard-markdown.js +204 -22
  90. package/lib/guard-mime.js +190 -26
  91. package/lib/guard-oauth.js +277 -28
  92. package/lib/guard-pdf.js +251 -27
  93. package/lib/guard-regex.js +226 -18
  94. package/lib/guard-shell.js +229 -26
  95. package/lib/guard-svg.js +177 -10
  96. package/lib/guard-template.js +232 -21
  97. package/lib/guard-time.js +195 -29
  98. package/lib/guard-uuid.js +189 -30
  99. package/lib/guard-xml.js +259 -36
  100. package/lib/guard-yaml.js +241 -44
  101. package/lib/honeytoken.js +63 -27
  102. package/lib/html-balance.js +83 -0
  103. package/lib/http-client.js +486 -59
  104. package/lib/http-message-signature.js +582 -0
  105. package/lib/i18n.js +102 -49
  106. package/lib/iab-mspa.js +112 -32
  107. package/lib/iab-tcf.js +107 -2
  108. package/lib/inbox.js +90 -52
  109. package/lib/keychain.js +865 -0
  110. package/lib/legal-hold.js +374 -0
  111. package/lib/local-db-thin.js +320 -0
  112. package/lib/log-stream.js +281 -51
  113. package/lib/log.js +184 -86
  114. package/lib/mail-bounce.js +107 -62
  115. package/lib/mail.js +295 -58
  116. package/lib/mcp.js +108 -27
  117. package/lib/metrics.js +98 -89
  118. package/lib/middleware/age-gate.js +36 -0
  119. package/lib/middleware/ai-act-disclosure.js +37 -0
  120. package/lib/middleware/api-encrypt.js +45 -0
  121. package/lib/middleware/assetlinks.js +40 -0
  122. package/lib/middleware/asyncapi-serve.js +35 -0
  123. package/lib/middleware/attach-user.js +40 -0
  124. package/lib/middleware/bearer-auth.js +40 -0
  125. package/lib/middleware/body-parser.js +230 -0
  126. package/lib/middleware/bot-disclose.js +34 -0
  127. package/lib/middleware/bot-guard.js +39 -0
  128. package/lib/middleware/compression.js +37 -0
  129. package/lib/middleware/cookies.js +32 -0
  130. package/lib/middleware/cors.js +40 -0
  131. package/lib/middleware/csp-nonce.js +40 -0
  132. package/lib/middleware/csp-report.js +34 -0
  133. package/lib/middleware/csrf-protect.js +43 -0
  134. package/lib/middleware/daily-byte-quota.js +53 -85
  135. package/lib/middleware/db-role-for.js +40 -0
  136. package/lib/middleware/dpop.js +40 -0
  137. package/lib/middleware/error-handler.js +37 -14
  138. package/lib/middleware/fetch-metadata.js +39 -0
  139. package/lib/middleware/flag-context.js +34 -0
  140. package/lib/middleware/gpc.js +33 -0
  141. package/lib/middleware/headers.js +35 -0
  142. package/lib/middleware/health.js +46 -0
  143. package/lib/middleware/host-allowlist.js +30 -0
  144. package/lib/middleware/network-allowlist.js +38 -0
  145. package/lib/middleware/openapi-serve.js +34 -0
  146. package/lib/middleware/rate-limit.js +160 -18
  147. package/lib/middleware/request-id.js +36 -18
  148. package/lib/middleware/request-log.js +37 -0
  149. package/lib/middleware/require-aal.js +29 -0
  150. package/lib/middleware/require-auth.js +32 -0
  151. package/lib/middleware/require-bound-key.js +41 -0
  152. package/lib/middleware/require-content-type.js +32 -0
  153. package/lib/middleware/require-methods.js +27 -0
  154. package/lib/middleware/require-mtls.js +33 -0
  155. package/lib/middleware/require-step-up.js +37 -0
  156. package/lib/middleware/security-headers.js +44 -0
  157. package/lib/middleware/security-txt.js +38 -0
  158. package/lib/middleware/span-http-server.js +37 -0
  159. package/lib/middleware/sse.js +36 -0
  160. package/lib/middleware/trace-log-correlation.js +33 -0
  161. package/lib/middleware/trace-propagate.js +32 -0
  162. package/lib/middleware/tus-upload.js +90 -0
  163. package/lib/middleware/web-app-manifest.js +53 -0
  164. package/lib/mtls-ca.js +100 -70
  165. package/lib/network-byte-quota.js +308 -0
  166. package/lib/network-heartbeat.js +135 -0
  167. package/lib/network-tls.js +534 -4
  168. package/lib/network.js +103 -0
  169. package/lib/notify.js +114 -43
  170. package/lib/ntp-check.js +192 -51
  171. package/lib/observability.js +145 -47
  172. package/lib/openapi.js +90 -44
  173. package/lib/outbox.js +99 -1
  174. package/lib/pagination.js +168 -86
  175. package/lib/parsers/index.js +16 -5
  176. package/lib/permissions.js +93 -40
  177. package/lib/pqc-agent.js +84 -8
  178. package/lib/pqc-software.js +94 -60
  179. package/lib/process-spawn.js +95 -21
  180. package/lib/pubsub.js +96 -66
  181. package/lib/queue.js +375 -54
  182. package/lib/redact.js +793 -21
  183. package/lib/render.js +139 -47
  184. package/lib/request-helpers.js +485 -121
  185. package/lib/restore-bundle.js +142 -39
  186. package/lib/restore-rollback.js +136 -45
  187. package/lib/retention.js +178 -50
  188. package/lib/retry.js +116 -33
  189. package/lib/router.js +475 -23
  190. package/lib/safe-async.js +543 -94
  191. package/lib/safe-buffer.js +337 -41
  192. package/lib/safe-json.js +467 -62
  193. package/lib/safe-jsonpath.js +285 -0
  194. package/lib/safe-schema.js +631 -87
  195. package/lib/safe-sql.js +221 -59
  196. package/lib/safe-url.js +278 -46
  197. package/lib/sandbox-worker.js +135 -0
  198. package/lib/sandbox.js +358 -0
  199. package/lib/scheduler.js +135 -70
  200. package/lib/self-update.js +647 -0
  201. package/lib/session-device-binding.js +431 -0
  202. package/lib/session.js +259 -49
  203. package/lib/slug.js +138 -26
  204. package/lib/ssrf-guard.js +316 -56
  205. package/lib/storage.js +433 -70
  206. package/lib/subject.js +405 -23
  207. package/lib/template.js +148 -8
  208. package/lib/tenant-quota.js +545 -0
  209. package/lib/testing.js +440 -53
  210. package/lib/time.js +291 -23
  211. package/lib/tls-exporter.js +239 -0
  212. package/lib/tracing.js +90 -74
  213. package/lib/uuid.js +97 -22
  214. package/lib/vault/index.js +284 -22
  215. package/lib/vault/seal-pem-file.js +66 -0
  216. package/lib/watcher.js +368 -0
  217. package/lib/webhook.js +196 -63
  218. package/lib/websocket.js +393 -68
  219. package/lib/wiki-concepts.js +338 -0
  220. package/lib/worker-pool.js +464 -0
  221. package/package.json +3 -3
  222. package/sbom.cyclonedx.json +7 -7
@@ -1,22 +1,37 @@
1
1
  "use strict";
2
2
  /**
3
- * Audit hash chain — tamper-evidence math.
3
+ * @module b.auditChain
4
+ * @nav Observability
5
+ * @title Audit Chain Primitives
4
6
  *
5
- * Per the compliance spec ("Tamper evidence (the chain + checkpoint signing)"
6
- * in the roadmap):
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
- * rowHash = SHA3-512(
9
- * prevHash || canonicalize(row-fields-except-hash) || nonce
10
- * )
12
+ * rowHash = SHA3-512(
13
+ * prevHash || canonicalize(row-fields-except-hash) || nonce
14
+ * )
11
15
  *
12
- * Each row's prevHash equals the previous row's rowHash (in monotonic-counter
13
- * order). The first row uses ZERO_HASH as prevHash. Verification walks the
14
- * chain forward; any row whose prevHash doesn't match the running hash, or
15
- * whose rowHash recomputes differently, breaks the chain.
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
- * Checkpoint signing (ML-DSA-87 over (atRow || atRowHash)) lives in
18
- * lib/audit-sign.js. This module owns the chain hash math only;
19
- * verification is O(n) and walks every row at boot.
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
- // Canonicalize a row for hashing. Excludes the hash/nonce columns themselves
35
- // and any caller-specified columns. Sorted keys, JSON-encoded values; Buffer
36
- // values converted to hex for stable byte serialization. Routes through
37
- // the shared `lib/canonical-json` walker so the four canonicalize sites
38
- // (this one, audit-tools, config-drift, pagination) share one
39
- // implementation of the bug-class fix that started in v0.6.60 and
40
- // completed in v0.6.67.
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
- // Compute a row's hash given its predecessor's hash, the row's logical fields
52
- // (already excluding prevHash, rowHash, nonce), and the row's nonce buffer.
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
- // Read the current chain tip (last row's rowHash + monotonicCounter) for a
72
- // given audit table. Async to accommodate operator-supplied external-db
73
- // drivers; queryOneAsync is `async (sql, params?) → row | null`.
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
- // Walk the entire chain forward, recomputing each row's hash. Returns an
84
- // object describing the result; callers decide how to react (refuse-to-boot,
85
- // log warning, etc.). queryAllAsync is `async (sql, params?) → rows`.
86
- //
87
- // audit_log only: if a `_blamejs_audit_purge_anchor` row exists, the walk
88
- // starts at lastPurgedCounter+1 with prevHash = lastPurgedRowHash. The
89
- // anchor is written by audit-tools.purge() after a successful archive,
90
- // and lets the chain math survive deletion of historical rows without
91
- // the bundle as the source of truth.
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
+ };