@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/redact.js
CHANGED
|
@@ -1,29 +1,48 @@
|
|
|
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
|
-
*
|
|
3
|
+
* @module b.redact
|
|
4
|
+
* @nav Observability
|
|
5
|
+
* @title Redact
|
|
6
|
+
*
|
|
7
|
+
* @intro
|
|
8
|
+
* Operational-log redaction — regex-shape and field-name rules that
|
|
9
|
+
* strip PII / secrets out of every log payload before it reaches a
|
|
10
|
+
* file, debug sink, or external SIEM.
|
|
11
|
+
*
|
|
12
|
+
* Two complementary signals run on every walk: a sensitive-field
|
|
13
|
+
* name set (case-insensitive substring match against keys like
|
|
14
|
+
* `password`, `api_key`, `authorization`, `dpop`, `client_secret`,
|
|
15
|
+
* `refresh_token`) and a value-shape detector chain (Luhn-validated
|
|
16
|
+
* credit-card numbers, JWS triplets, PEM / OpenSSH private-key
|
|
17
|
+
* blocks, AWS access-key prefixes, vault-sealed ciphertexts,
|
|
18
|
+
* connection-string credential leaks). Field-name hits replace the
|
|
19
|
+
* whole value with the configured marker; value-shape hits replace
|
|
20
|
+
* with a per-detector marker (`[REDACTED-CC]`, `[REDACTED-JWT]`).
|
|
21
|
+
*
|
|
22
|
+
* The redactor never mutates the input — every call returns a fresh
|
|
23
|
+
* object. The same payload commonly lands in two paths simultaneously
|
|
24
|
+
* (audit-log seals via vault; operational log redacts here) so
|
|
25
|
+
* in-place mutation would corrupt the sealed-then-archived copy.
|
|
26
|
+
*
|
|
27
|
+
* `classifyDefaults` and `installOutboundDlp` extend the same primitive
|
|
28
|
+
* set into outbound-DLP duty: the classifier produces a verdict
|
|
29
|
+
* ("clean" / "redact" / "refuse") for a request body + headers, and
|
|
30
|
+
* the installer wraps `httpClient` / `mail` / `webhook` instances so
|
|
31
|
+
* refused requests fail with `DlpError` and redacted ones proceed
|
|
32
|
+
* with sanitized payloads. Posture presets (`pci-dss` / `hipaa` /
|
|
33
|
+
* `fapi2` / `soc2` / `gdpr`) pick a sensible default classifier.
|
|
34
|
+
*
|
|
35
|
+
* @card
|
|
36
|
+
* Operational-log redaction — regex-shape and field-name rules that strip PII / secrets out of every log payload before it reaches a file, debug sink, or external SIEM.
|
|
24
37
|
*/
|
|
25
38
|
|
|
26
39
|
var C = require("./constants");
|
|
40
|
+
var lazyRequire = require("./lazy-require");
|
|
41
|
+
var safeJson = require("./safe-json");
|
|
42
|
+
var validateOpts = require("./validate-opts");
|
|
43
|
+
var { DlpError } = require("./framework-error");
|
|
44
|
+
|
|
45
|
+
var audit = lazyRequire(function () { return require("./audit"); });
|
|
27
46
|
|
|
28
47
|
var DEFAULT_MARKER = "[REDACTED]";
|
|
29
48
|
|
|
@@ -142,6 +161,23 @@ var VALUE_DETECTORS = [
|
|
|
142
161
|
var sensitiveFieldsSet = new Set(SENSITIVE_FIELDS);
|
|
143
162
|
var customDetectors = [];
|
|
144
163
|
|
|
164
|
+
/**
|
|
165
|
+
* @primitive b.redact.registerFieldRule
|
|
166
|
+
* @signature b.redact.registerFieldRule(name, replacement?)
|
|
167
|
+
* @since 0.1.0
|
|
168
|
+
* @related b.redact.redact, b.redact.registerValueDetector
|
|
169
|
+
*
|
|
170
|
+
* Add a field name to the always-redact set. Match is
|
|
171
|
+
* case-insensitive substring, so registering `secret` also redacts
|
|
172
|
+
* `appSecret` / `customer_secret`. The `replacement` argument is
|
|
173
|
+
* accepted for symmetry with `registerValueDetector` but ignored —
|
|
174
|
+
* field-name hits always use the redactor's configured marker.
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* b.redact.registerFieldRule("internal_token");
|
|
178
|
+
* var out = b.redact.redact({ internal_token: "abc-123" });
|
|
179
|
+
* // → { internal_token: "[REDACTED]" }
|
|
180
|
+
*/
|
|
145
181
|
function registerFieldRule(name, replacement) {
|
|
146
182
|
void replacement; // marker not used here; redact replaces with marker
|
|
147
183
|
if (typeof name === "string") {
|
|
@@ -151,6 +187,28 @@ function registerFieldRule(name, replacement) {
|
|
|
151
187
|
throw new Error("registerFieldRule expects a string field name");
|
|
152
188
|
}
|
|
153
189
|
|
|
190
|
+
/**
|
|
191
|
+
* @primitive b.redact.registerValueDetector
|
|
192
|
+
* @signature b.redact.registerValueDetector(name, testFn, replacement)
|
|
193
|
+
* @since 0.1.0
|
|
194
|
+
* @related b.redact.redact, b.redact.registerFieldRule
|
|
195
|
+
*
|
|
196
|
+
* Register a custom value-shape detector. `testFn(value)` runs against
|
|
197
|
+
* every string value the redactor walks; truthy result substitutes the
|
|
198
|
+
* `replacement` (string or function — function receives the matched
|
|
199
|
+
* value and returns the substitution). Custom detectors run AFTER the
|
|
200
|
+
* built-in chain.
|
|
201
|
+
*
|
|
202
|
+
* @example
|
|
203
|
+
* // Redact internal employee IDs (shape: EMP-NNNNNN).
|
|
204
|
+
* b.redact.registerValueDetector("employee-id",
|
|
205
|
+
* function (v) { return /^EMP-\d{6}$/.test(v); },
|
|
206
|
+
* "[REDACTED-EMPID]");
|
|
207
|
+
* var out = b.redact.redact({ note: "owner EMP-123456" });
|
|
208
|
+
* // → { note: "owner EMP-123456" } — value-shape detectors only fire
|
|
209
|
+
* // on full-string match; in-string matches need a custom regex
|
|
210
|
+
* // replacement function.
|
|
211
|
+
*/
|
|
154
212
|
function registerValueDetector(name, testFn, replacement) {
|
|
155
213
|
if (typeof testFn !== "function") {
|
|
156
214
|
throw new Error("registerValueDetector requires a test function");
|
|
@@ -181,6 +239,34 @@ function _redactValue(value) {
|
|
|
181
239
|
return value;
|
|
182
240
|
}
|
|
183
241
|
|
|
242
|
+
/**
|
|
243
|
+
* @primitive b.redact.redact
|
|
244
|
+
* @signature b.redact.redact(value, opts?)
|
|
245
|
+
* @since 0.1.0
|
|
246
|
+
* @related b.redact.registerFieldRule, b.redact.registerValueDetector, b.redact.classifyDefaults
|
|
247
|
+
*
|
|
248
|
+
* Walk `value` and return a NEW value with sensitive fields and
|
|
249
|
+
* sensitive-shaped strings replaced by the marker. Handles plain
|
|
250
|
+
* objects, arrays, primitives, Buffers (always replaced — never log
|
|
251
|
+
* raw binary). The original input is never mutated.
|
|
252
|
+
*
|
|
253
|
+
* @opts
|
|
254
|
+
* marker: string, // replacement marker; default "[REDACTED]"
|
|
255
|
+
* maxDepth: number, // recursion cap; default 50
|
|
256
|
+
* parentKey: string | null, // seed parent-key for top-level scalars
|
|
257
|
+
*
|
|
258
|
+
* @example
|
|
259
|
+
* var safe = b.redact.redact({
|
|
260
|
+
* email: "alice@example.com",
|
|
261
|
+
* password: "hunter2",
|
|
262
|
+
* card: "4111 1111 1111 1111",
|
|
263
|
+
* note: "see eyJabcdefghijk.eyJxyz.signature for proof",
|
|
264
|
+
* });
|
|
265
|
+
* // → { email: "alice@example.com",
|
|
266
|
+
* // password: "[REDACTED]",
|
|
267
|
+
* // card: "[REDACTED-CC]",
|
|
268
|
+
* // note: "see eyJabcdefghijk.eyJxyz.signature for proof" }
|
|
269
|
+
*/
|
|
184
270
|
function redact(value, opts) {
|
|
185
271
|
opts = opts || {};
|
|
186
272
|
var marker = opts.marker || DEFAULT_MARKER;
|
|
@@ -225,11 +311,697 @@ function _resetForTest() {
|
|
|
225
311
|
customDetectors = [];
|
|
226
312
|
}
|
|
227
313
|
|
|
314
|
+
// ---- Classifier presets (for outbound DLP) ----
|
|
315
|
+
//
|
|
316
|
+
// Each pattern maps to a verdict-producing predicate. The default
|
|
317
|
+
// classifier walks the request body and headers, surfaces a verdict
|
|
318
|
+
// per-pattern, and returns either "clean", "redact" (if any pattern
|
|
319
|
+
// is sanitizable in-place by the redactor), or "refuse" (if any
|
|
320
|
+
// pattern is operator-flagged as refuse-only).
|
|
321
|
+
//
|
|
322
|
+
// Patterns:
|
|
323
|
+
// pan, credit-card — Luhn-validated card numbers
|
|
324
|
+
// ssn — US SSN shape
|
|
325
|
+
// ein — US EIN shape (NN-NNNNNNN)
|
|
326
|
+
// iban — IBAN shape + mod-97 checksum
|
|
327
|
+
// api-key-shape — generic high-entropy long token in known-key
|
|
328
|
+
// header / field names
|
|
329
|
+
// pem, ssh-private — private-key blocks
|
|
330
|
+
// jwt — JWS triplet
|
|
331
|
+
// aws-access-key — AWS access-key-id shape
|
|
332
|
+
// phi-shape — composite of US SSN + DOB-shape near a name field
|
|
333
|
+
// (used for HIPAA posture)
|
|
334
|
+
var CLASSIFIER_PATTERNS = Object.freeze({
|
|
335
|
+
"pan": {
|
|
336
|
+
detect: function (v) {
|
|
337
|
+
if (typeof v !== "string") return false;
|
|
338
|
+
// Two-stage match: full-field exact PAN OR embedded 13-19 digit
|
|
339
|
+
// run anywhere in a longer string. Both pass through Luhn before
|
|
340
|
+
// being flagged so high-digit-count IDs (timestamps, monotonic
|
|
341
|
+
// sequence numbers) don't false-positive.
|
|
342
|
+
var dExact = v.replace(/\s|-/g, "");
|
|
343
|
+
if (/^\d{13,19}$/.test(dExact) && _luhnCheck(dExact)) return true;
|
|
344
|
+
var m = v.match(/\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{1,7}\b/);
|
|
345
|
+
if (m) {
|
|
346
|
+
var inner = m[0].replace(/\s|-/g, "");
|
|
347
|
+
if (inner.length >= 13 && inner.length <= 19 && _luhnCheck(inner)) return true;
|
|
348
|
+
}
|
|
349
|
+
return false;
|
|
350
|
+
},
|
|
351
|
+
action: "refuse",
|
|
352
|
+
label: "pan",
|
|
353
|
+
},
|
|
354
|
+
"credit-card": {
|
|
355
|
+
detect: function (v) {
|
|
356
|
+
if (typeof v !== "string") return false;
|
|
357
|
+
var dExact = v.replace(/\s|-/g, "");
|
|
358
|
+
if (/^\d{13,19}$/.test(dExact) && _luhnCheck(dExact)) return true;
|
|
359
|
+
var m = v.match(/\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{1,7}\b/);
|
|
360
|
+
if (m) {
|
|
361
|
+
var inner = m[0].replace(/\s|-/g, "");
|
|
362
|
+
if (inner.length >= 13 && inner.length <= 19 && _luhnCheck(inner)) return true;
|
|
363
|
+
}
|
|
364
|
+
return false;
|
|
365
|
+
},
|
|
366
|
+
action: "refuse",
|
|
367
|
+
label: "credit-card",
|
|
368
|
+
},
|
|
369
|
+
"ssn": {
|
|
370
|
+
detect: function (v) { return typeof v === "string" && /\b\d{3}-\d{2}-\d{4}\b/.test(v); },
|
|
371
|
+
action: "redact",
|
|
372
|
+
label: "ssn",
|
|
373
|
+
},
|
|
374
|
+
"ein": {
|
|
375
|
+
detect: function (v) { return typeof v === "string" && /\b\d{2}-\d{7}\b/.test(v); },
|
|
376
|
+
action: "redact",
|
|
377
|
+
label: "ein",
|
|
378
|
+
},
|
|
379
|
+
"iban": {
|
|
380
|
+
detect: function (v) {
|
|
381
|
+
if (typeof v !== "string") return false;
|
|
382
|
+
var s = v.replace(/\s/g, "").toUpperCase();
|
|
383
|
+
if (!/^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$/.test(s)) return false;
|
|
384
|
+
// mod-97 checksum
|
|
385
|
+
var rearranged = s.slice(4) + s.slice(0, 4);
|
|
386
|
+
var num = "";
|
|
387
|
+
for (var i = 0; i < rearranged.length; i += 1) {
|
|
388
|
+
var c = rearranged.charCodeAt(i);
|
|
389
|
+
if (c >= 48 && c <= 57) num += rearranged.charAt(i); // allow:raw-byte-literal — ASCII '0'..'9' codepoint range
|
|
390
|
+
else if (c >= 65 && c <= 90) num += String(c - 55);
|
|
391
|
+
else return false;
|
|
392
|
+
}
|
|
393
|
+
// Long-integer mod 97 in chunks
|
|
394
|
+
var rem = 0;
|
|
395
|
+
for (var j = 0; j < num.length; j += 7) {
|
|
396
|
+
rem = parseInt(String(rem) + num.slice(j, j + 7), 10) % 97;
|
|
397
|
+
}
|
|
398
|
+
return rem === 1;
|
|
399
|
+
},
|
|
400
|
+
action: "refuse",
|
|
401
|
+
label: "iban",
|
|
402
|
+
},
|
|
403
|
+
"api-key-shape": {
|
|
404
|
+
detect: function (v) {
|
|
405
|
+
if (typeof v !== "string") return false;
|
|
406
|
+
// High-entropy string with at least one digit + one uppercase + length >= 24.
|
|
407
|
+
if (v.length < 24) return false; // allow:raw-byte-literal — minimum entropy-bearing string length in chars, not bytes
|
|
408
|
+
if (!/[A-Z]/.test(v)) return false;
|
|
409
|
+
if (!/[0-9]/.test(v)) return false;
|
|
410
|
+
if (!/^[A-Za-z0-9_-]+$/.test(v)) return false;
|
|
411
|
+
return true;
|
|
412
|
+
},
|
|
413
|
+
action: "redact",
|
|
414
|
+
label: "api-key-shape",
|
|
415
|
+
},
|
|
416
|
+
"pem": {
|
|
417
|
+
detect: function (v) { return typeof v === "string" && /-----BEGIN [A-Z ]+-----/.test(v); },
|
|
418
|
+
action: "refuse",
|
|
419
|
+
label: "pem",
|
|
420
|
+
},
|
|
421
|
+
"ssh-private": {
|
|
422
|
+
detect: function (v) { return typeof v === "string" && /-----BEGIN OPENSSH PRIVATE KEY-----/.test(v); },
|
|
423
|
+
action: "refuse",
|
|
424
|
+
label: "ssh-private",
|
|
425
|
+
},
|
|
426
|
+
"jwt": {
|
|
427
|
+
detect: function (v) {
|
|
428
|
+
return typeof v === "string" &&
|
|
429
|
+
/^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(v);
|
|
430
|
+
},
|
|
431
|
+
action: "redact",
|
|
432
|
+
label: "jwt",
|
|
433
|
+
},
|
|
434
|
+
"aws-access-key": {
|
|
435
|
+
detect: function (v) {
|
|
436
|
+
return typeof v === "string" &&
|
|
437
|
+
/\b(AKIA|ASIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASCA)[A-Z0-9]{16}\b/.test(v);
|
|
438
|
+
},
|
|
439
|
+
action: "refuse",
|
|
440
|
+
label: "aws-access-key",
|
|
441
|
+
},
|
|
442
|
+
"phi-shape": {
|
|
443
|
+
detect: function (v) {
|
|
444
|
+
if (typeof v !== "string") return false;
|
|
445
|
+
// SSN, DOB, MRN, ICD-10 shape — any one is enough to flag PHI
|
|
446
|
+
// adjacency in a body fragment. Operators using HIPAA posture
|
|
447
|
+
// get this composite by default.
|
|
448
|
+
if (/\b\d{3}-\d{2}-\d{4}\b/.test(v)) return true; // SSN
|
|
449
|
+
if (/\b(0[1-9]|1[0-2])\/(0[1-9]|[12]\d|3[01])\/(19|20)\d{2}\b/.test(v)) return true; // DOB
|
|
450
|
+
if (/\bMRN[:#]?\s*\d{4,12}\b/i.test(v)) return true; // MRN
|
|
451
|
+
if (/\b[A-TV-Z][0-9][0-9AB](\.[0-9A-TV-Z]{1,4})?\b/.test(v)) return true; // ICD-10
|
|
452
|
+
return false;
|
|
453
|
+
},
|
|
454
|
+
action: "refuse",
|
|
455
|
+
label: "phi-shape",
|
|
456
|
+
},
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
// classifyDefaults — build a classifier function from a list of pattern
|
|
460
|
+
// names. The returned classifier inspects body + headers and yields:
|
|
461
|
+
//
|
|
462
|
+
// { verdict: "clean" | "redact" | "refuse",
|
|
463
|
+
// hits: [ { label, action, where } ],
|
|
464
|
+
// redacted?: <body with matches replaced by marker> }
|
|
465
|
+
//
|
|
466
|
+
// "refuse" wins over "redact" wins over "clean". Operators choosing
|
|
467
|
+
// "redact" actions still get a redacted body so the request can proceed
|
|
468
|
+
// without leaking the matched value; "refuse" means the host primitive
|
|
469
|
+
// throws DlpError.
|
|
470
|
+
/**
|
|
471
|
+
* @primitive b.redact.classifyDefaults
|
|
472
|
+
* @signature b.redact.classifyDefaults(opts)
|
|
473
|
+
* @since 0.7.46
|
|
474
|
+
* @status stable
|
|
475
|
+
* @compliance hipaa, pci-dss, gdpr, soc2, fapi2
|
|
476
|
+
* @related b.redact.installOutboundDlp, b.redact.installForPosture
|
|
477
|
+
*
|
|
478
|
+
* Build a classifier function from a list of pattern names. The
|
|
479
|
+
* returned `classify({ body, headers, url })` walks the body (object,
|
|
480
|
+
* string, or Buffer), inspects every header value, and returns
|
|
481
|
+
* `{ verdict, hits, redactedBody }`. Verdict precedence is
|
|
482
|
+
* `refuse > redact > audit-only > clean`.
|
|
483
|
+
*
|
|
484
|
+
* @opts
|
|
485
|
+
* patterns: string[], // names from CLASSIFIER_PATTERNS
|
|
486
|
+
* extra: object, // additional { name: { detect, action, label } }
|
|
487
|
+
* overrideAction: "refuse" | "redact" | "audit-only",
|
|
488
|
+
* marker: string, // default "[REDACTED]"
|
|
489
|
+
*
|
|
490
|
+
* @example
|
|
491
|
+
* var classify = b.redact.classifyDefaults({
|
|
492
|
+
* patterns: ["pan", "ssn", "jwt", "aws-access-key"],
|
|
493
|
+
* });
|
|
494
|
+
* var v = classify({
|
|
495
|
+
* body: { card: "4111111111111111", note: "ok" },
|
|
496
|
+
* headers: { authorization: "Bearer eyJabc.eyJdef.sig" },
|
|
497
|
+
* });
|
|
498
|
+
* // → v.verdict === "refuse" (PAN match defaults to refuse)
|
|
499
|
+
*/
|
|
500
|
+
function classifyDefaults(opts) {
|
|
501
|
+
opts = opts || {};
|
|
502
|
+
validateOpts(opts, ["patterns", "extra", "overrideAction", "marker"], "redact.classifyDefaults");
|
|
503
|
+
var patterns = Array.isArray(opts.patterns) ? opts.patterns : Object.keys(CLASSIFIER_PATTERNS);
|
|
504
|
+
if (patterns.length === 0) {
|
|
505
|
+
throw new DlpError("redact-dlp/no-patterns",
|
|
506
|
+
"redact.classifyDefaults: opts.patterns must be a non-empty array");
|
|
507
|
+
}
|
|
508
|
+
for (var p = 0; p < patterns.length; p += 1) {
|
|
509
|
+
if (typeof patterns[p] !== "string") {
|
|
510
|
+
throw new DlpError("redact-dlp/bad-pattern",
|
|
511
|
+
"redact.classifyDefaults: patterns[" + p + "] must be a string, got " +
|
|
512
|
+
typeof patterns[p]);
|
|
513
|
+
}
|
|
514
|
+
if (!CLASSIFIER_PATTERNS[patterns[p]] &&
|
|
515
|
+
!(opts.extra && opts.extra[patterns[p]])) {
|
|
516
|
+
throw new DlpError("redact-dlp/unknown-pattern",
|
|
517
|
+
"redact.classifyDefaults: unknown pattern '" + patterns[p] +
|
|
518
|
+
"'. Known: " + Object.keys(CLASSIFIER_PATTERNS).join(", "));
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
var marker = typeof opts.marker === "string" && opts.marker.length > 0
|
|
522
|
+
? opts.marker : DEFAULT_MARKER;
|
|
523
|
+
var overrideAction = opts.overrideAction || null;
|
|
524
|
+
if (overrideAction && overrideAction !== "refuse" && overrideAction !== "redact" && overrideAction !== "audit-only") {
|
|
525
|
+
throw new DlpError("redact-dlp/bad-action",
|
|
526
|
+
"redact.classifyDefaults: overrideAction must be refuse|redact|audit-only");
|
|
527
|
+
}
|
|
528
|
+
var extra = opts.extra || {};
|
|
529
|
+
|
|
530
|
+
function _resolve(name) {
|
|
531
|
+
var spec = CLASSIFIER_PATTERNS[name] || extra[name];
|
|
532
|
+
return spec;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return function classify(input) {
|
|
536
|
+
var hits = [];
|
|
537
|
+
var bodyAccumulator = [];
|
|
538
|
+
|
|
539
|
+
function _scanString(str, where) {
|
|
540
|
+
if (typeof str !== "string" || str.length === 0) return str;
|
|
541
|
+
var out = str;
|
|
542
|
+
for (var i = 0; i < patterns.length; i += 1) {
|
|
543
|
+
var spec = _resolve(patterns[i]);
|
|
544
|
+
if (!spec) continue;
|
|
545
|
+
if (spec.detect(out)) {
|
|
546
|
+
var action = overrideAction || spec.action;
|
|
547
|
+
hits.push({ label: spec.label || patterns[i], action: action, where: where });
|
|
548
|
+
if (action === "redact") {
|
|
549
|
+
// Best-effort scrub of the matched fragment. Field-name
|
|
550
|
+
// redaction inside the body is handled by walking the
|
|
551
|
+
// structure separately.
|
|
552
|
+
out = out.replace(/\b\d{3}-\d{2}-\d{4}\b/g, marker)
|
|
553
|
+
.replace(/\b\d{2}-\d{7}\b/g, marker);
|
|
554
|
+
// For other shapes, replace the full string when matched.
|
|
555
|
+
if (spec.label !== "ssn" && spec.label !== "ein") out = marker;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return out;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function _walk(value, where) {
|
|
563
|
+
if (value === null || value === undefined) return value;
|
|
564
|
+
if (typeof value === "string") {
|
|
565
|
+
var scanned = _scanString(value, where);
|
|
566
|
+
bodyAccumulator.push(scanned);
|
|
567
|
+
return scanned;
|
|
568
|
+
}
|
|
569
|
+
if (typeof value === "number" || typeof value === "boolean") return value;
|
|
570
|
+
if (Buffer.isBuffer(value) || value instanceof Uint8Array) {
|
|
571
|
+
return value; // raw bytes — scanned separately when input.body
|
|
572
|
+
}
|
|
573
|
+
if (Array.isArray(value)) {
|
|
574
|
+
return value.map(function (item, idx) {
|
|
575
|
+
return _walk(item, where + "[" + idx + "]");
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
if (typeof value === "object") {
|
|
579
|
+
var copy = {};
|
|
580
|
+
for (var k in value) {
|
|
581
|
+
if (!Object.prototype.hasOwnProperty.call(value, k)) continue;
|
|
582
|
+
copy[k] = _walk(value[k], where + "." + k);
|
|
583
|
+
}
|
|
584
|
+
return copy;
|
|
585
|
+
}
|
|
586
|
+
return value;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
var redactedBody;
|
|
590
|
+
var input2 = input || {};
|
|
591
|
+
var bodyVal = input2.body;
|
|
592
|
+
if (Buffer.isBuffer(bodyVal) || bodyVal instanceof Uint8Array) {
|
|
593
|
+
var asText;
|
|
594
|
+
try { asText = Buffer.from(bodyVal).toString("utf8"); }
|
|
595
|
+
catch (_e) { asText = ""; }
|
|
596
|
+
var scannedText = _scanString(asText, "body");
|
|
597
|
+
redactedBody = scannedText === asText ? bodyVal : Buffer.from(scannedText, "utf8");
|
|
598
|
+
} else if (typeof bodyVal === "string") {
|
|
599
|
+
redactedBody = _scanString(bodyVal, "body");
|
|
600
|
+
} else if (bodyVal && typeof bodyVal === "object") {
|
|
601
|
+
redactedBody = _walk(bodyVal, "body");
|
|
602
|
+
} else {
|
|
603
|
+
redactedBody = bodyVal;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (input2.headers && typeof input2.headers === "object") {
|
|
607
|
+
for (var hk in input2.headers) {
|
|
608
|
+
if (!Object.prototype.hasOwnProperty.call(input2.headers, hk)) continue;
|
|
609
|
+
_scanString(String(input2.headers[hk]), "headers." + hk);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Verdict precedence: refuse > redact > audit-only > clean.
|
|
614
|
+
var verdict = "clean";
|
|
615
|
+
for (var hi = 0; hi < hits.length; hi += 1) {
|
|
616
|
+
if (hits[hi].action === "refuse") { verdict = "refuse"; break; }
|
|
617
|
+
if (hits[hi].action === "redact") verdict = "redact";
|
|
618
|
+
else if (hits[hi].action === "audit-only" && verdict === "clean") verdict = "audit-only";
|
|
619
|
+
}
|
|
620
|
+
return { verdict: verdict, hits: hits, redactedBody: redactedBody };
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// ---- Outbound DLP installer ----
|
|
625
|
+
|
|
626
|
+
var OUTBOUND_INSTALL_REGISTRY = new WeakMap();
|
|
627
|
+
|
|
628
|
+
function _emitDlp(action, outcome, metadata) {
|
|
629
|
+
try {
|
|
630
|
+
audit().safeEmit({
|
|
631
|
+
action: action,
|
|
632
|
+
outcome: outcome,
|
|
633
|
+
metadata: metadata || {},
|
|
634
|
+
});
|
|
635
|
+
} catch (_e) { /* drop-silent */ }
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function _wrapClassifier(fn, where) {
|
|
639
|
+
if (typeof fn !== "function") {
|
|
640
|
+
throw new DlpError("redact-dlp/bad-classifier",
|
|
641
|
+
where + ": classifier must be a function");
|
|
642
|
+
}
|
|
643
|
+
return function safeClassify(input) {
|
|
644
|
+
var v;
|
|
645
|
+
try { v = fn(input || {}); }
|
|
646
|
+
catch (e) {
|
|
647
|
+
// Classifier threw — treat as refuse (fail-closed, since the
|
|
648
|
+
// classifier is the gate; an unknown verdict cannot be
|
|
649
|
+
// sanitized as "clean").
|
|
650
|
+
return { verdict: "refuse", hits: [{ label: "classifier-error", action: "refuse", where: "classifier" }],
|
|
651
|
+
redactedBody: input && input.body, error: e && e.message };
|
|
652
|
+
}
|
|
653
|
+
if (!v || typeof v !== "object" || typeof v.verdict !== "string") {
|
|
654
|
+
return { verdict: "refuse", hits: [{ label: "classifier-bad-verdict", action: "refuse", where: "classifier" }],
|
|
655
|
+
redactedBody: input && input.body };
|
|
656
|
+
}
|
|
657
|
+
return v;
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// installOutboundDlp({ httpClient, mail, webhook, classifier?, posture?,
|
|
662
|
+
// onRefuse?, onRedact?, onScan? })
|
|
663
|
+
//
|
|
664
|
+
// Installs interceptors on each of the operator-supplied primitive
|
|
665
|
+
// instances. The installer is idempotent per primitive — a second
|
|
666
|
+
// install with the same instance no-ops on that instance. Each
|
|
667
|
+
// interceptor wraps the request-emit boundary; the original instance
|
|
668
|
+
// keeps its surface unchanged and any callers see DlpError on refuse
|
|
669
|
+
// or a sanitized payload on redact.
|
|
670
|
+
//
|
|
671
|
+
// Returns { uninstall(), installed: { httpClient, mail, webhook } }.
|
|
672
|
+
/**
|
|
673
|
+
* @primitive b.redact.installOutboundDlp
|
|
674
|
+
* @signature b.redact.installOutboundDlp(opts)
|
|
675
|
+
* @since 0.7.46
|
|
676
|
+
* @status stable
|
|
677
|
+
* @compliance hipaa, pci-dss, gdpr, soc2, fapi2
|
|
678
|
+
* @related b.redact.classifyDefaults, b.redact.installForPosture
|
|
679
|
+
*
|
|
680
|
+
* Install request-time interceptors on `httpClient` / `mail` /
|
|
681
|
+
* `webhook` instances so every outbound payload runs through a DLP
|
|
682
|
+
* classifier first. Refused requests reject with `DlpError`; redacted
|
|
683
|
+
* requests proceed with a sanitized body. Idempotent per primitive
|
|
684
|
+
* instance — installing twice on the same client no-ops.
|
|
685
|
+
*
|
|
686
|
+
* @opts
|
|
687
|
+
* httpClient: object, // instance with .request(opts)
|
|
688
|
+
* mail: object, // instance with .send(message)
|
|
689
|
+
* webhook: object, // signer instance with .send(input)
|
|
690
|
+
* classifier: function, // override the default classifier
|
|
691
|
+
* posture: string, // "pci-dss" | "hipaa" | "fapi2" | "soc2" | "gdpr"
|
|
692
|
+
* onRefuse: function, // hook fired on refuse verdict
|
|
693
|
+
* onRedact: function, // hook fired on redact verdict
|
|
694
|
+
* onScan: function, // hook fired on every classify call
|
|
695
|
+
*
|
|
696
|
+
* @example
|
|
697
|
+
* var http = b.httpClient.create({ baseUrl: "https://api.example.com" });
|
|
698
|
+
* var mail = b.mail.create({ host: "smtp.example.com", port: 587 });
|
|
699
|
+
* var dlp = b.redact.installOutboundDlp({
|
|
700
|
+
* httpClient: http,
|
|
701
|
+
* mail: mail,
|
|
702
|
+
* posture: "pci-dss",
|
|
703
|
+
* onRefuse: function (info) { console.warn("DLP refused", info.verdict.hits); },
|
|
704
|
+
* });
|
|
705
|
+
* // dlp.installed → { httpClient: true, mail: true, webhook: false }
|
|
706
|
+
* // dlp.uninstall() restores the original .request / .send methods.
|
|
707
|
+
*/
|
|
708
|
+
function installOutboundDlp(opts) {
|
|
709
|
+
opts = opts || {};
|
|
710
|
+
validateOpts(opts, [
|
|
711
|
+
"httpClient", "mail", "webhook", "classifier", "posture",
|
|
712
|
+
"onRefuse", "onRedact", "onScan",
|
|
713
|
+
], "redact.installOutboundDlp");
|
|
714
|
+
|
|
715
|
+
// Posture default-on. When posture is given but no classifier, we
|
|
716
|
+
// build a posture-derived default. PCI-DSS → pan + credit-card +
|
|
717
|
+
// pem + aws-access-key. HIPAA → phi-shape + ssn + ein + pem.
|
|
718
|
+
var classifier = opts.classifier;
|
|
719
|
+
var posturePatterns = null;
|
|
720
|
+
if (typeof opts.posture === "string" && opts.posture.length > 0) {
|
|
721
|
+
posturePatterns = _resolvePosturePatterns(opts.posture);
|
|
722
|
+
}
|
|
723
|
+
if (!classifier) {
|
|
724
|
+
classifier = classifyDefaults({
|
|
725
|
+
patterns: posturePatterns ||
|
|
726
|
+
["pan", "ssn", "ein", "iban", "credit-card", "api-key-shape", "pem", "ssh-private", "aws-access-key"],
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
classifier = _wrapClassifier(classifier, "redact.installOutboundDlp");
|
|
730
|
+
|
|
731
|
+
validateOpts.optionalFunction(opts.onRefuse, "redact.installOutboundDlp: onRefuse",
|
|
732
|
+
DlpError, "redact-dlp/bad-hook");
|
|
733
|
+
validateOpts.optionalFunction(opts.onRedact, "redact.installOutboundDlp: onRedact",
|
|
734
|
+
DlpError, "redact-dlp/bad-hook");
|
|
735
|
+
validateOpts.optionalFunction(opts.onScan, "redact.installOutboundDlp: onScan",
|
|
736
|
+
DlpError, "redact-dlp/bad-hook");
|
|
737
|
+
|
|
738
|
+
var uninstallers = [];
|
|
739
|
+
var installed = { httpClient: false, mail: false, webhook: false };
|
|
740
|
+
|
|
741
|
+
if (opts.httpClient) {
|
|
742
|
+
var u1 = _installHttpClient(opts.httpClient, classifier, opts);
|
|
743
|
+
if (u1) { uninstallers.push(u1); installed.httpClient = true; }
|
|
744
|
+
}
|
|
745
|
+
if (opts.mail) {
|
|
746
|
+
var u2 = _installMail(opts.mail, classifier, opts);
|
|
747
|
+
if (u2) { uninstallers.push(u2); installed.mail = true; }
|
|
748
|
+
}
|
|
749
|
+
if (opts.webhook) {
|
|
750
|
+
var u3 = _installWebhook(opts.webhook, classifier, opts);
|
|
751
|
+
if (u3) { uninstallers.push(u3); installed.webhook = true; }
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
_emitDlp("dlp.outbound.installed", "success", {
|
|
755
|
+
posture: opts.posture || null,
|
|
756
|
+
primitives: Object.keys(installed).filter(function (k) { return installed[k]; }),
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
return {
|
|
760
|
+
installed: installed,
|
|
761
|
+
uninstall: function () {
|
|
762
|
+
while (uninstallers.length > 0) {
|
|
763
|
+
var fn = uninstallers.pop();
|
|
764
|
+
try { fn(); } catch (_e) { /* best-effort */ }
|
|
765
|
+
}
|
|
766
|
+
},
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function _resolvePosturePatterns(name) {
|
|
771
|
+
var n = String(name).toLowerCase();
|
|
772
|
+
if (n === "pci-dss" || n === "pci") {
|
|
773
|
+
return ["pan", "credit-card", "pem", "aws-access-key", "api-key-shape"];
|
|
774
|
+
}
|
|
775
|
+
if (n === "hipaa") {
|
|
776
|
+
return ["phi-shape", "ssn", "ein", "pem", "aws-access-key", "api-key-shape"];
|
|
777
|
+
}
|
|
778
|
+
if (n === "fapi2") {
|
|
779
|
+
return ["pan", "credit-card", "iban", "pem", "aws-access-key", "jwt", "api-key-shape"];
|
|
780
|
+
}
|
|
781
|
+
if (n === "soc2" || n === "gdpr") {
|
|
782
|
+
return ["ssn", "ein", "pem", "ssh-private", "aws-access-key", "api-key-shape"];
|
|
783
|
+
}
|
|
784
|
+
throw new DlpError("redact-dlp/unknown-posture",
|
|
785
|
+
"redact.installOutboundDlp: unknown posture '" + name +
|
|
786
|
+
"'. Known: pci-dss | hipaa | fapi2 | soc2 | gdpr");
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function _runHook(hook, payload) {
|
|
790
|
+
if (typeof hook !== "function") return;
|
|
791
|
+
try { hook(payload); } catch (_e) { /* drop-silent */ }
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function _installHttpClient(client, classifier, opts) {
|
|
795
|
+
if (OUTBOUND_INSTALL_REGISTRY.has(client)) return null;
|
|
796
|
+
if (typeof client.request !== "function") {
|
|
797
|
+
throw new DlpError("redact-dlp/bad-target",
|
|
798
|
+
"redact.installOutboundDlp: httpClient must expose a request() function");
|
|
799
|
+
}
|
|
800
|
+
var original = client.request.bind(client);
|
|
801
|
+
client.request = function dlpScannedRequest(reqOpts) {
|
|
802
|
+
reqOpts = reqOpts || {};
|
|
803
|
+
var verdict = classifier({ body: reqOpts.body, headers: reqOpts.headers, url: reqOpts.url });
|
|
804
|
+
_runHook(opts.onScan, { primitive: "httpClient", verdict: verdict, opts: reqOpts });
|
|
805
|
+
if (verdict.verdict === "refuse") {
|
|
806
|
+
_emitDlp("dlp.outbound.refused", "denied", {
|
|
807
|
+
primitive: "httpClient",
|
|
808
|
+
url: reqOpts.url || null,
|
|
809
|
+
hits: verdict.hits.map(_summarizeHit),
|
|
810
|
+
});
|
|
811
|
+
_runHook(opts.onRefuse, { primitive: "httpClient", verdict: verdict, opts: reqOpts });
|
|
812
|
+
return Promise.reject(new DlpError("redact-dlp/refused",
|
|
813
|
+
"outbound httpClient.request refused by DLP classifier — hits: " +
|
|
814
|
+
verdict.hits.map(function (h) { return h.label; }).join(", ")));
|
|
815
|
+
}
|
|
816
|
+
if (verdict.verdict === "redact") {
|
|
817
|
+
// Mutate the body field on a defensive shallow clone built via
|
|
818
|
+
// explicit field copy, not Object.assign, so operator-shaped opts
|
|
819
|
+
// can't smuggle keys past the existing httpClient.request opts
|
|
820
|
+
// validator.
|
|
821
|
+
var newOpts = {};
|
|
822
|
+
for (var rk in reqOpts) {
|
|
823
|
+
if (Object.prototype.hasOwnProperty.call(reqOpts, rk)) newOpts[rk] = reqOpts[rk];
|
|
824
|
+
}
|
|
825
|
+
newOpts.body = verdict.redactedBody;
|
|
826
|
+
_emitDlp("dlp.outbound.redacted", "success", {
|
|
827
|
+
primitive: "httpClient",
|
|
828
|
+
url: reqOpts.url || null,
|
|
829
|
+
hits: verdict.hits.map(_summarizeHit),
|
|
830
|
+
});
|
|
831
|
+
_runHook(opts.onRedact, { primitive: "httpClient", verdict: verdict, opts: newOpts });
|
|
832
|
+
return original(newOpts);
|
|
833
|
+
}
|
|
834
|
+
return original(reqOpts);
|
|
835
|
+
};
|
|
836
|
+
OUTBOUND_INSTALL_REGISTRY.set(client, true);
|
|
837
|
+
return function uninstall() {
|
|
838
|
+
client.request = original;
|
|
839
|
+
OUTBOUND_INSTALL_REGISTRY.delete(client);
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function _installMail(mailInstance, classifier, opts) {
|
|
844
|
+
if (OUTBOUND_INSTALL_REGISTRY.has(mailInstance)) return null;
|
|
845
|
+
if (typeof mailInstance.send !== "function") {
|
|
846
|
+
throw new DlpError("redact-dlp/bad-target",
|
|
847
|
+
"redact.installOutboundDlp: mail must expose a send() function");
|
|
848
|
+
}
|
|
849
|
+
var original = mailInstance.send.bind(mailInstance);
|
|
850
|
+
mailInstance.send = function dlpScannedSend(message) {
|
|
851
|
+
message = message || {};
|
|
852
|
+
var bodyParts = {
|
|
853
|
+
text: message.text,
|
|
854
|
+
html: message.html,
|
|
855
|
+
subject: message.subject,
|
|
856
|
+
};
|
|
857
|
+
var verdict = classifier({ body: bodyParts, headers: message.headers || {} });
|
|
858
|
+
_runHook(opts.onScan, { primitive: "mail", verdict: verdict, message: message });
|
|
859
|
+
if (verdict.verdict === "refuse") {
|
|
860
|
+
_emitDlp("dlp.outbound.refused", "denied", {
|
|
861
|
+
primitive: "mail",
|
|
862
|
+
to: message.to || null,
|
|
863
|
+
hits: verdict.hits.map(_summarizeHit),
|
|
864
|
+
});
|
|
865
|
+
_runHook(opts.onRefuse, { primitive: "mail", verdict: verdict, message: message });
|
|
866
|
+
return Promise.reject(new DlpError("redact-dlp/refused",
|
|
867
|
+
"outbound mail.send refused by DLP classifier — hits: " +
|
|
868
|
+
verdict.hits.map(function (h) { return h.label; }).join(", ")));
|
|
869
|
+
}
|
|
870
|
+
if (verdict.verdict === "redact") {
|
|
871
|
+
var newMessage = {};
|
|
872
|
+
for (var mk in message) {
|
|
873
|
+
if (Object.prototype.hasOwnProperty.call(message, mk)) newMessage[mk] = message[mk];
|
|
874
|
+
}
|
|
875
|
+
newMessage.text = verdict.redactedBody && verdict.redactedBody.text;
|
|
876
|
+
newMessage.html = verdict.redactedBody && verdict.redactedBody.html;
|
|
877
|
+
newMessage.subject = verdict.redactedBody && verdict.redactedBody.subject;
|
|
878
|
+
_emitDlp("dlp.outbound.redacted", "success", {
|
|
879
|
+
primitive: "mail",
|
|
880
|
+
to: message.to || null,
|
|
881
|
+
hits: verdict.hits.map(_summarizeHit),
|
|
882
|
+
});
|
|
883
|
+
_runHook(opts.onRedact, { primitive: "mail", verdict: verdict, message: newMessage });
|
|
884
|
+
return original(newMessage);
|
|
885
|
+
}
|
|
886
|
+
return original(message);
|
|
887
|
+
};
|
|
888
|
+
OUTBOUND_INSTALL_REGISTRY.set(mailInstance, true);
|
|
889
|
+
return function uninstall() {
|
|
890
|
+
mailInstance.send = original;
|
|
891
|
+
OUTBOUND_INSTALL_REGISTRY.delete(mailInstance);
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
function _installWebhook(signerInstance, classifier, opts) {
|
|
896
|
+
if (OUTBOUND_INSTALL_REGISTRY.has(signerInstance)) return null;
|
|
897
|
+
if (typeof signerInstance.send !== "function") {
|
|
898
|
+
throw new DlpError("redact-dlp/bad-target",
|
|
899
|
+
"redact.installOutboundDlp: webhook must expose a send() function (signer instance)");
|
|
900
|
+
}
|
|
901
|
+
var original = signerInstance.send.bind(signerInstance);
|
|
902
|
+
signerInstance.send = function dlpScannedWebhookSend(input) {
|
|
903
|
+
input = input || {};
|
|
904
|
+
var bodyForScan = input.body;
|
|
905
|
+
// body may be a JSON string — try parsing for a richer scan.
|
|
906
|
+
var parsedBody = null;
|
|
907
|
+
if (typeof bodyForScan === "string") {
|
|
908
|
+
try { parsedBody = safeJson.parse(bodyForScan); }
|
|
909
|
+
catch (_e) { parsedBody = null; }
|
|
910
|
+
}
|
|
911
|
+
var verdict = classifier({
|
|
912
|
+
body: parsedBody !== null ? parsedBody : bodyForScan,
|
|
913
|
+
headers: input.headers || {},
|
|
914
|
+
url: input.url,
|
|
915
|
+
});
|
|
916
|
+
_runHook(opts.onScan, { primitive: "webhook", verdict: verdict, input: input });
|
|
917
|
+
if (verdict.verdict === "refuse") {
|
|
918
|
+
_emitDlp("dlp.outbound.refused", "denied", {
|
|
919
|
+
primitive: "webhook",
|
|
920
|
+
url: input.url || null,
|
|
921
|
+
hits: verdict.hits.map(_summarizeHit),
|
|
922
|
+
});
|
|
923
|
+
_runHook(opts.onRefuse, { primitive: "webhook", verdict: verdict, input: input });
|
|
924
|
+
return Promise.reject(new DlpError("redact-dlp/refused",
|
|
925
|
+
"outbound webhook.send refused by DLP classifier — hits: " +
|
|
926
|
+
verdict.hits.map(function (h) { return h.label; }).join(", ")));
|
|
927
|
+
}
|
|
928
|
+
if (verdict.verdict === "redact") {
|
|
929
|
+
var newBody = parsedBody !== null
|
|
930
|
+
? JSON.stringify(verdict.redactedBody)
|
|
931
|
+
: verdict.redactedBody;
|
|
932
|
+
// Build the rebuilt input from a fixed allowlist of fields rather
|
|
933
|
+
// than a spread, so an operator-shaped input object cannot smuggle
|
|
934
|
+
// unexpected keys into the downstream signer-send call.
|
|
935
|
+
var newInput = {
|
|
936
|
+
url: input.url,
|
|
937
|
+
body: newBody,
|
|
938
|
+
kid: input.kid,
|
|
939
|
+
headers: input.headers,
|
|
940
|
+
};
|
|
941
|
+
_emitDlp("dlp.outbound.redacted", "success", {
|
|
942
|
+
primitive: "webhook",
|
|
943
|
+
url: input.url || null,
|
|
944
|
+
hits: verdict.hits.map(_summarizeHit),
|
|
945
|
+
});
|
|
946
|
+
_runHook(opts.onRedact, { primitive: "webhook", verdict: verdict, input: newInput });
|
|
947
|
+
return original(newInput);
|
|
948
|
+
}
|
|
949
|
+
return original(input);
|
|
950
|
+
};
|
|
951
|
+
OUTBOUND_INSTALL_REGISTRY.set(signerInstance, true);
|
|
952
|
+
return function uninstall() {
|
|
953
|
+
signerInstance.send = original;
|
|
954
|
+
OUTBOUND_INSTALL_REGISTRY.delete(signerInstance);
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function _summarizeHit(h) {
|
|
959
|
+
return { label: h.label, action: h.action, where: h.where };
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// Posture-coordinated install — a thin wrapper used by b.compliance.set
|
|
963
|
+
// to wire DLP automatically when the posture is set. Operators using
|
|
964
|
+
// b.compliance can rely on this; direct callers use installOutboundDlp.
|
|
965
|
+
/**
|
|
966
|
+
* @primitive b.redact.installForPosture
|
|
967
|
+
* @signature b.redact.installForPosture(posture, primitives)
|
|
968
|
+
* @since 0.7.46
|
|
969
|
+
* @status stable
|
|
970
|
+
* @compliance hipaa, pci-dss, gdpr, soc2, fapi2
|
|
971
|
+
* @related b.redact.installOutboundDlp, b.redact.classifyDefaults
|
|
972
|
+
*
|
|
973
|
+
* Posture-coordinated install — a thin wrapper used by
|
|
974
|
+
* `b.compliance.set` so picking a posture also wires outbound DLP
|
|
975
|
+
* automatically. Direct callers usually want `installOutboundDlp`
|
|
976
|
+
* because it accepts the full hook surface.
|
|
977
|
+
*
|
|
978
|
+
* @example
|
|
979
|
+
* var dlp = b.redact.installForPosture("hipaa", {
|
|
980
|
+
* httpClient: myHttp,
|
|
981
|
+
* mail: myMail,
|
|
982
|
+
* webhook: myWebhook,
|
|
983
|
+
* });
|
|
984
|
+
* // → dlp.installed.httpClient === true
|
|
985
|
+
*/
|
|
986
|
+
function installForPosture(posture, primitives) {
|
|
987
|
+
return installOutboundDlp({
|
|
988
|
+
httpClient: primitives && primitives.httpClient,
|
|
989
|
+
mail: primitives && primitives.mail,
|
|
990
|
+
webhook: primitives && primitives.webhook,
|
|
991
|
+
posture: posture,
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
|
|
228
995
|
module.exports = {
|
|
229
996
|
redact: redact,
|
|
230
997
|
registerFieldRule: registerFieldRule,
|
|
231
998
|
registerValueDetector: registerValueDetector,
|
|
999
|
+
classifyDefaults: classifyDefaults,
|
|
1000
|
+
installOutboundDlp: installOutboundDlp,
|
|
1001
|
+
installForPosture: installForPosture,
|
|
1002
|
+
CLASSIFIER_PATTERNS: CLASSIFIER_PATTERNS,
|
|
232
1003
|
MARKER: DEFAULT_MARKER,
|
|
233
1004
|
SENSITIVE_FIELDS: SENSITIVE_FIELDS,
|
|
1005
|
+
DlpError: DlpError,
|
|
234
1006
|
_resetForTest: _resetForTest,
|
|
235
1007
|
};
|