@blamejs/core 0.14.25 → 0.14.27
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 +4 -0
- package/lib/agent-envelope-mac.js +104 -0
- package/lib/agent-event-bus.js +105 -4
- package/lib/agent-posture-chain.js +8 -42
- package/lib/atomic-file.js +33 -3
- package/lib/audit.js +31 -23
- package/lib/auth/oauth.js +25 -5
- package/lib/auth/openid-federation.js +108 -47
- package/lib/auth/sd-jwt-vc.js +16 -3
- package/lib/break-glass.js +153 -3
- package/lib/compliance.js +147 -4
- package/lib/crypto-field.js +87 -1
- package/lib/dsr.js +378 -52
- package/lib/error-page.js +14 -1
- package/lib/file-upload.js +52 -7
- package/lib/framework-error.js +3 -1
- package/lib/gate-contract.js +53 -0
- package/lib/http-client.js +23 -9
- package/lib/mail-server-jmap.js +117 -12
- package/lib/middleware/body-parser.js +71 -25
- package/lib/middleware/csrf-protect.js +19 -8
- package/lib/object-store/azure-blob.js +28 -2
- package/lib/observability.js +87 -0
- package/lib/otel-export.js +25 -1
- package/lib/parsers/safe-xml.js +47 -7
- package/lib/queue-local.js +23 -1
- package/lib/queue.js +7 -0
- package/lib/redact.js +68 -11
- package/lib/redis-client.js +160 -31
- package/lib/request-helpers.js +7 -0
- package/lib/router.js +212 -5
- package/lib/ssrf-guard.js +51 -4
- package/lib/static.js +132 -27
- package/lib/vault/rotate.js +64 -44
- package/lib/websocket.js +19 -5
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/observability.js
CHANGED
|
@@ -55,6 +55,14 @@ var safeBuffer = lazyRequire(function () { return require("./safe-buffer"); });
|
|
|
55
55
|
var tracing = lazyRequire(function () { return require("./tracing"); });
|
|
56
56
|
var metrics = lazyRequire(function () { return require("./metrics"); });
|
|
57
57
|
|
|
58
|
+
// redact is the framework's central PII/secret scrubber. Lazy-loaded so
|
|
59
|
+
// the require graph stays acyclic at boot (redact lazy-pulls audit, which
|
|
60
|
+
// pulls observability back). Composed by the default telemetry redactor
|
|
61
|
+
// below so span/metric attribute VALUES are scrubbed before the OTLP
|
|
62
|
+
// exporter serializes them — CWE-532 (insertion of sensitive information
|
|
63
|
+
// into a telemetry/log egress sink).
|
|
64
|
+
var redact = lazyRequire(function () { return require("./redact"); });
|
|
65
|
+
|
|
58
66
|
// Operator-installed tap handler — wired via setTap(). When non-null,
|
|
59
67
|
// every observability event/tap dispatch routes here in addition to
|
|
60
68
|
// the framework's metrics module. Used by b.otelExport.create() so an
|
|
@@ -62,6 +70,26 @@ var metrics = lazyRequire(function () { return require("./metrics"); });
|
|
|
62
70
|
// emits internally.
|
|
63
71
|
var _externalTap = null;
|
|
64
72
|
|
|
73
|
+
// Telemetry-attribute redactor seam. Span / metric attribute VALUES are
|
|
74
|
+
// a first-class egress surface: a span attribute holding a user email,
|
|
75
|
+
// bearer token, or vault-sealed ciphertext is shipped verbatim to the
|
|
76
|
+
// OTLP collector unless it is scrubbed at the assembly boundary, the same
|
|
77
|
+
// way log-stream redacts every record before any sink sees it. Defaults
|
|
78
|
+
// ON — the default redactor composes b.redact.redact, passing the
|
|
79
|
+
// attribute key as the parent-key context so both field-name rules
|
|
80
|
+
// (authorization / token / session / password) and value-shape detectors
|
|
81
|
+
// (JWT / PEM / credit-card / SSN / connection-string) fire. CWE-532.
|
|
82
|
+
//
|
|
83
|
+
// The redactor is (value, key) → redactedValue. The exporter calls it for
|
|
84
|
+
// every attribute value; a thrown redactor drops the attribute rather
|
|
85
|
+
// than leaking it (the exporter enforces fail-toward-dropping), so a
|
|
86
|
+
// misbehaving custom redactor can never widen the egress surface.
|
|
87
|
+
function _defaultTelemetryRedactor(value, key) {
|
|
88
|
+
return redact().redact(value, { parentKey: typeof key === "string" ? key : null });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
var _telemetryRedactor = _defaultTelemetryRedactor;
|
|
92
|
+
|
|
65
93
|
function _safeMetricsTap(name, value, labels) {
|
|
66
94
|
try { metrics().tap(name, value, labels); }
|
|
67
95
|
catch (_e) { /* boot-order tolerance — metrics may not be loaded */ }
|
|
@@ -106,6 +134,63 @@ function setTap(handler) {
|
|
|
106
134
|
_externalTap = handler;
|
|
107
135
|
}
|
|
108
136
|
|
|
137
|
+
/**
|
|
138
|
+
* @primitive b.observability.setRedactor
|
|
139
|
+
* @signature b.observability.setRedactor(redactor)
|
|
140
|
+
* @since 0.14.27
|
|
141
|
+
* @related b.observability.getRedactor, b.redact.redact
|
|
142
|
+
*
|
|
143
|
+
* Override the redactor applied to every span / metric attribute VALUE
|
|
144
|
+
* before the OTLP exporter serializes it onto the wire. Telemetry is a
|
|
145
|
+
* first-class egress sink: an attribute holding a user email, bearer
|
|
146
|
+
* token, or secret would otherwise reach the collector in plaintext
|
|
147
|
+
* (CWE-532). Redaction is ON by default — the default redactor composes
|
|
148
|
+
* `b.redact.redact` and fires both field-name and value-shape rules; this
|
|
149
|
+
* setter only lets an operator swap in a stricter or domain-specific
|
|
150
|
+
* scrubber.
|
|
151
|
+
*
|
|
152
|
+
* The redactor is `redactor(value, key)` and returns the value to export.
|
|
153
|
+
* It runs on the export hot path, so a throw is caught and the attribute
|
|
154
|
+
* is dropped (never exported raw) — a redactor that throws can only
|
|
155
|
+
* shrink the egress surface, never widen it. Pass `null` to restore the
|
|
156
|
+
* default `b.redact.redact`-backed redactor.
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* b.observability.setRedactor(function (value, key) {
|
|
160
|
+
* if (key === "enduser.id") return "[REDACTED]";
|
|
161
|
+
* return b.redact.redact(value, { parentKey: key });
|
|
162
|
+
* });
|
|
163
|
+
* b.observability.setRedactor(null); // restore the default
|
|
164
|
+
*/
|
|
165
|
+
function setRedactor(redactor) {
|
|
166
|
+
if (redactor !== null && typeof redactor !== "function") {
|
|
167
|
+
throw new TypeError("observability.setRedactor: redactor must be a function or null, got " +
|
|
168
|
+
typeof redactor);
|
|
169
|
+
}
|
|
170
|
+
_telemetryRedactor = redactor === null ? _defaultTelemetryRedactor : redactor;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* @primitive b.observability.getRedactor
|
|
175
|
+
* @signature b.observability.getRedactor()
|
|
176
|
+
* @since 0.14.27
|
|
177
|
+
* @related b.observability.setRedactor, b.redact.redact
|
|
178
|
+
*
|
|
179
|
+
* Return the redactor currently applied to span / metric attribute
|
|
180
|
+
* values on the OTLP egress path. The OTLP exporter calls this to scrub
|
|
181
|
+
* every attribute value before serialization; operators rarely need it
|
|
182
|
+
* directly. When no override has been installed it returns the default
|
|
183
|
+
* `b.redact.redact`-backed redactor.
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* var redactor = b.observability.getRedactor();
|
|
187
|
+
* redactor("Bearer eyJabc.eyJdef.sig", "authorization");
|
|
188
|
+
* // → "[REDACTED]" (field-name rule on the "authorization" key)
|
|
189
|
+
*/
|
|
190
|
+
function getRedactor() {
|
|
191
|
+
return _telemetryRedactor;
|
|
192
|
+
}
|
|
193
|
+
|
|
109
194
|
/**
|
|
110
195
|
* @primitive b.observability.tap
|
|
111
196
|
* @signature b.observability.tap(name, attrs, fn)
|
|
@@ -750,6 +835,8 @@ module.exports = {
|
|
|
750
835
|
safeEvent: safeEvent,
|
|
751
836
|
timed: timed,
|
|
752
837
|
setTap: setTap,
|
|
838
|
+
setRedactor: setRedactor,
|
|
839
|
+
getRedactor: getRedactor,
|
|
753
840
|
SEMCONV: SEMCONV,
|
|
754
841
|
traceContext: traceContext,
|
|
755
842
|
baggage: baggage,
|
package/lib/otel-export.js
CHANGED
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
var C = require("./constants");
|
|
43
43
|
var canonicalJson = require("./canonical-json");
|
|
44
44
|
var httpClient = require("./http-client");
|
|
45
|
+
var observability = require("./observability");
|
|
45
46
|
var safeAsync = require("./safe-async");
|
|
46
47
|
var validateOpts = require("./validate-opts");
|
|
47
48
|
var { defineClass } = require("./framework-error");
|
|
@@ -64,6 +65,28 @@ var MAX_RESPONSE_BYTES = C.BYTES.mib(1);
|
|
|
64
65
|
// receiving backend handles the running sum.
|
|
65
66
|
var TEMPORALITY_DELTA = 1;
|
|
66
67
|
|
|
68
|
+
// Run a single attribute value through the active telemetry redactor.
|
|
69
|
+
// Telemetry is a first-class EGRESS sink — an attribute value holding a
|
|
70
|
+
// user email, bearer token, or vault-sealed ciphertext would otherwise
|
|
71
|
+
// be serialized verbatim onto the OTLP wire (CWE-532: insertion of
|
|
72
|
+
// sensitive information into an externally-shipped sink). The redactor is
|
|
73
|
+
// resolved per-call from b.observability so an operator-installed
|
|
74
|
+
// override (setRedactor) takes effect without re-creating the exporter.
|
|
75
|
+
//
|
|
76
|
+
// Drop-silent by design: this runs on the export hot path, where a throw
|
|
77
|
+
// from the redactor must never crash the request that produced the span.
|
|
78
|
+
// On a throw we DROP the attribute (signalled by the `_DROP` sentinel)
|
|
79
|
+
// rather than fall through to the raw value — failing toward dropping,
|
|
80
|
+
// not leaking.
|
|
81
|
+
var _DROP = {};
|
|
82
|
+
function _redactAttrValue(key, value) {
|
|
83
|
+
try {
|
|
84
|
+
return observability.getRedactor()(value, key);
|
|
85
|
+
} catch (_e) {
|
|
86
|
+
return _DROP; // redactor threw — drop the attribute, never export raw
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
67
90
|
// ---- attribute encoding ----
|
|
68
91
|
// OTLP attributes are KeyValue with typed `value` fields:
|
|
69
92
|
// { key, value: { stringValue | intValue | doubleValue | boolValue } }
|
|
@@ -72,7 +95,8 @@ function _attrsToOtlp(attrs) {
|
|
|
72
95
|
var out = [];
|
|
73
96
|
for (var k in attrs) {
|
|
74
97
|
if (!Object.prototype.hasOwnProperty.call(attrs, k)) continue;
|
|
75
|
-
var v = attrs[k];
|
|
98
|
+
var v = _redactAttrValue(k, attrs[k]);
|
|
99
|
+
if (v === _DROP) continue; // redactor threw — drop, don't leak
|
|
76
100
|
var kv;
|
|
77
101
|
if (typeof v === "string") kv = { stringValue: v };
|
|
78
102
|
else if (typeof v === "number") {
|
package/lib/parsers/safe-xml.js
CHANGED
|
@@ -13,10 +13,18 @@
|
|
|
13
13
|
* - Processing instructions referencing external resources
|
|
14
14
|
* - Unbounded recursion / element count / attribute count
|
|
15
15
|
* - CDATA sections of arbitrary length
|
|
16
|
+
* - Prototype pollution: an element or attribute named __proto__,
|
|
17
|
+
* constructor, or prototype landing as a key in the result tree
|
|
18
|
+
* (CWE-1321 / OWASP prototype-pollution)
|
|
16
19
|
*
|
|
17
20
|
* This parser closes all of them by default. DOCTYPE, external entities,
|
|
18
21
|
* and processing instructions other than '<?xml ?>' are REJECTED — apps
|
|
19
|
-
* that need them are using the wrong parser.
|
|
22
|
+
* that need them are using the wrong parser. Element and attribute names
|
|
23
|
+
* equal to __proto__ / constructor / prototype are REJECTED with
|
|
24
|
+
* xml/forbidden-name so they can never collide with an inherited member
|
|
25
|
+
* or reassign an accumulator's prototype; the result tree and every
|
|
26
|
+
* nested object it contains have a null prototype, so a consumer reading
|
|
27
|
+
* an absent key sees undefined rather than an inherited Object member.
|
|
20
28
|
*
|
|
21
29
|
* Output: a plain JS object. Element with attributes + children:
|
|
22
30
|
* <root id="x"><child>text</child></root>
|
|
@@ -75,6 +83,18 @@ var ABSOLUTE_MAX_ATTRIBUTES = 1_000;
|
|
|
75
83
|
// XML built-in entities (the ONLY entities allowed)
|
|
76
84
|
var BUILT_IN_ENTITIES = { lt: "<", gt: ">", amp: "&", quot: "\"", apos: "'" };
|
|
77
85
|
|
|
86
|
+
// Names that must never become a key in the result tree. A plain object
|
|
87
|
+
// inherits these from Object.prototype; an element/attribute named after
|
|
88
|
+
// one of them would otherwise collide with the inherited member (a
|
|
89
|
+
// consumer sees a function/object instead of undefined) or — for a
|
|
90
|
+
// computed-member write of an object value — reassign the accumulator's
|
|
91
|
+
// prototype (CWE-1321 / OWASP prototype-pollution). The accumulators are
|
|
92
|
+
// built with a null prototype, and these names are rejected outright so
|
|
93
|
+
// the result is always a clean key→value map. Mirrors the
|
|
94
|
+
// __proto__/constructor/prototype rejection the toml / yaml / ini
|
|
95
|
+
// parsers in this family already apply.
|
|
96
|
+
var FORBIDDEN_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
97
|
+
|
|
78
98
|
function _validateAndCap(name, value, defaultValue, ceiling) {
|
|
79
99
|
if (value === undefined) return defaultValue;
|
|
80
100
|
if (!numericBounds.isPositiveFiniteInt(value)) {
|
|
@@ -178,7 +198,12 @@ function parse(input, opts) {
|
|
|
178
198
|
} else break;
|
|
179
199
|
}
|
|
180
200
|
if (pos === start) throw _err("expected name", "xml/bad-name");
|
|
181
|
-
|
|
201
|
+
var parsed = input.substring(start, pos);
|
|
202
|
+
if (FORBIDDEN_KEYS.has(parsed)) {
|
|
203
|
+
throw _err("element/attribute name '" + parsed +
|
|
204
|
+
"' is reserved (prototype-pollution defense)", "xml/forbidden-name");
|
|
205
|
+
}
|
|
206
|
+
return parsed;
|
|
182
207
|
}
|
|
183
208
|
|
|
184
209
|
// Parse an attribute value (single- or double-quoted)
|
|
@@ -267,7 +292,12 @@ function parse(input, opts) {
|
|
|
267
292
|
|
|
268
293
|
expectChar("<");
|
|
269
294
|
var name = parseName();
|
|
270
|
-
|
|
295
|
+
// Null-prototype accumulator keyed by attacker-influenced attribute
|
|
296
|
+
// names — no inherited Object member can shadow a missing key, and the
|
|
297
|
+
// duplicate-attribute check below can't be fooled by an inherited
|
|
298
|
+
// function (CWE-1321). Forbidden names are already rejected in
|
|
299
|
+
// parseName.
|
|
300
|
+
var attrs = Object.create(null);
|
|
271
301
|
var attrCount = 0;
|
|
272
302
|
|
|
273
303
|
while (pos < len) {
|
|
@@ -351,10 +381,15 @@ function parse(input, opts) {
|
|
|
351
381
|
// Pure-text element → string
|
|
352
382
|
return _make(name, textParts.join("").trim() === "" ? textParts.join("") : textParts.join(""));
|
|
353
383
|
}
|
|
354
|
-
// Mixed / attributed element → object
|
|
355
|
-
|
|
384
|
+
// Mixed / attributed element → object. Both accumulators carry a null
|
|
385
|
+
// prototype: `grouped` is keyed by attacker-influenced child element
|
|
386
|
+
// names and `obj` receives them via Object.assign, so neither may
|
|
387
|
+
// expose an inherited Object member or be prototype-poisoned by a
|
|
388
|
+
// computed-member write (CWE-1321). Forbidden child names were already
|
|
389
|
+
// rejected in parseName.
|
|
390
|
+
var obj = Object.create(null);
|
|
356
391
|
if (hasAttrs) obj["@attrs"] = attrs;
|
|
357
|
-
var grouped =
|
|
392
|
+
var grouped = Object.create(null);
|
|
358
393
|
for (var i = 0; i < elementChildren.length; i++) {
|
|
359
394
|
var childWrap = elementChildren[i].value;
|
|
360
395
|
var childName = Object.keys(childWrap)[0];
|
|
@@ -374,7 +409,12 @@ function parse(input, opts) {
|
|
|
374
409
|
}
|
|
375
410
|
|
|
376
411
|
function _make(name, value) {
|
|
377
|
-
|
|
412
|
+
// Null-prototype wrapper keyed by the element name (parser-controlled,
|
|
413
|
+
// attacker-influenced). `out[name] = value` with a forbidden name
|
|
414
|
+
// would otherwise reassign the wrapper's prototype when value is an
|
|
415
|
+
// object; the name is already rejected in parseName and the null
|
|
416
|
+
// prototype removes the inherited-member surface entirely (CWE-1321).
|
|
417
|
+
var out = Object.create(null);
|
|
378
418
|
out[name] = value;
|
|
379
419
|
return out;
|
|
380
420
|
}
|
package/lib/queue-local.js
CHANGED
|
@@ -70,6 +70,22 @@ var DEFAULT_TABLE = "_blamejs_jobs";
|
|
|
70
70
|
// (queue-local → vault → db → audit → cluster) tolerates the late bind.
|
|
71
71
|
var vault = lazyRequire(function () { return require("./vault"); });
|
|
72
72
|
|
|
73
|
+
// Self-register the _blamejs_jobs sealed-column declaration with
|
|
74
|
+
// cryptoField so payload + lastError seal at rest even when db.init never
|
|
75
|
+
// ran in this process. cryptoField.sealRow is a SILENT pass-through for an
|
|
76
|
+
// unregistered table — a standalone redis/sqs queue node (no db.init) would
|
|
77
|
+
// otherwise write job payloads (webhook bodies, credentials, PII) in
|
|
78
|
+
// cleartext. db.init registers the same shape from its FRAMEWORK_SCHEMA;
|
|
79
|
+
// registerTable is idempotent, and probing getSchema (rather than a module
|
|
80
|
+
// boolean) keeps this reset-safe — db._resetForTest() clears the cryptoField
|
|
81
|
+
// registry between tests, and a boolean cache would then leave seal a no-op.
|
|
82
|
+
function _ensureSealTable() {
|
|
83
|
+
if (cryptoField.getSchema(SEAL_TABLE)) return;
|
|
84
|
+
cryptoField.registerTable(SEAL_TABLE, {
|
|
85
|
+
sealedFields: ["payload", "lastError"],
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
73
89
|
// Column order kept as a constant so the placeholders + values lists
|
|
74
90
|
// stay in sync. Mirrors db.js's FRAMEWORK_SCHEMA for _blamejs_jobs.
|
|
75
91
|
var JOB_COLS = [
|
|
@@ -583,4 +599,10 @@ function create(config) {
|
|
|
583
599
|
};
|
|
584
600
|
}
|
|
585
601
|
|
|
586
|
-
module.exports = {
|
|
602
|
+
module.exports = {
|
|
603
|
+
create: create,
|
|
604
|
+
// Idempotent, reset-safe self-registration of the _blamejs_jobs sealed-
|
|
605
|
+
// column declaration. queue.init calls this so seal-at-rest engages on a
|
|
606
|
+
// standalone queue node that never ran db.init.
|
|
607
|
+
_ensureSealTable: _ensureSealTable,
|
|
608
|
+
};
|
package/lib/queue.js
CHANGED
|
@@ -152,6 +152,13 @@ function init(opts) {
|
|
|
152
152
|
throw _err("INVALID_CONFIG", "queue.init({ backends }) is required", true);
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
// Self-register the _blamejs_jobs sealed-column declaration so payload +
|
|
156
|
+
// lastError seal at rest even when this process never ran db.init (a
|
|
157
|
+
// standalone redis/sqs queue node). cryptoField.sealRow silently passes
|
|
158
|
+
// through for an unregistered table, so without this a queue node would
|
|
159
|
+
// write job payloads to Redis/SQS in cleartext. Idempotent + reset-safe.
|
|
160
|
+
localProto._ensureSealTable();
|
|
161
|
+
|
|
155
162
|
backends = {};
|
|
156
163
|
// IIFE per-iteration so each backend's wrappers close over its own
|
|
157
164
|
// raw / breaker / cfg. With `var` (function-scoped) those bindings
|
package/lib/redact.js
CHANGED
|
@@ -309,6 +309,7 @@ function _redact(value, depth, maxDepth, marker, parentKey) {
|
|
|
309
309
|
function _resetForTest() {
|
|
310
310
|
sensitiveFieldsSet = new Set(SENSITIVE_FIELDS);
|
|
311
311
|
customDetectors = [];
|
|
312
|
+
outboundInstallCount = 0;
|
|
312
313
|
}
|
|
313
314
|
|
|
314
315
|
// ---- Classifier presets (for outbound DLP) ----
|
|
@@ -625,6 +626,14 @@ function classifyDefaults(opts) {
|
|
|
625
626
|
|
|
626
627
|
var OUTBOUND_INSTALL_REGISTRY = new WeakMap();
|
|
627
628
|
|
|
629
|
+
// Count of primitive instances currently wrapped by an outbound-DLP
|
|
630
|
+
// interceptor. A WeakMap can't be enumerated, so this counter is the
|
|
631
|
+
// cheap "is anything installed?" signal b.compliance.set reads to decide
|
|
632
|
+
// whether to warn that a DLP-floor posture was pinned without wiring.
|
|
633
|
+
// Incremented per primitive on install, decremented on uninstall; never
|
|
634
|
+
// negative.
|
|
635
|
+
var outboundInstallCount = 0;
|
|
636
|
+
|
|
628
637
|
function _emitDlp(action, outcome, metadata) {
|
|
629
638
|
try {
|
|
630
639
|
audit().safeEmit({
|
|
@@ -740,15 +749,15 @@ function installOutboundDlp(opts) {
|
|
|
740
749
|
|
|
741
750
|
if (opts.httpClient) {
|
|
742
751
|
var u1 = _installHttpClient(opts.httpClient, classifier, opts);
|
|
743
|
-
if (u1) { uninstallers.push(u1); installed.httpClient = true; }
|
|
752
|
+
if (u1) { uninstallers.push(u1); installed.httpClient = true; outboundInstallCount += 1; }
|
|
744
753
|
}
|
|
745
754
|
if (opts.mail) {
|
|
746
755
|
var u2 = _installMail(opts.mail, classifier, opts);
|
|
747
|
-
if (u2) { uninstallers.push(u2); installed.mail = true; }
|
|
756
|
+
if (u2) { uninstallers.push(u2); installed.mail = true; outboundInstallCount += 1; }
|
|
748
757
|
}
|
|
749
758
|
if (opts.webhook) {
|
|
750
759
|
var u3 = _installWebhook(opts.webhook, classifier, opts);
|
|
751
|
-
if (u3) { uninstallers.push(u3); installed.webhook = true; }
|
|
760
|
+
if (u3) { uninstallers.push(u3); installed.webhook = true; outboundInstallCount += 1; }
|
|
752
761
|
}
|
|
753
762
|
|
|
754
763
|
_emitDlp("dlp.outbound.installed", "success", {
|
|
@@ -761,12 +770,37 @@ function installOutboundDlp(opts) {
|
|
|
761
770
|
uninstall: function () {
|
|
762
771
|
while (uninstallers.length > 0) {
|
|
763
772
|
var fn = uninstallers.pop();
|
|
764
|
-
try { fn();
|
|
773
|
+
try { fn(); if (outboundInstallCount > 0) outboundInstallCount -= 1; }
|
|
774
|
+
catch (_e) { /* best-effort */ }
|
|
765
775
|
}
|
|
766
776
|
},
|
|
767
777
|
};
|
|
768
778
|
}
|
|
769
779
|
|
|
780
|
+
/**
|
|
781
|
+
* @primitive b.redact.isOutboundDlpInstalled
|
|
782
|
+
* @signature b.redact.isOutboundDlpInstalled()
|
|
783
|
+
* @since 0.14.27
|
|
784
|
+
* @compliance hipaa, pci-dss, gdpr, soc2, fapi2
|
|
785
|
+
* @related b.redact.installForPosture, b.redact.installOutboundDlp
|
|
786
|
+
*
|
|
787
|
+
* Returns `true` when at least one primitive instance (httpClient /
|
|
788
|
+
* mail / webhook) currently carries an outbound-DLP interceptor.
|
|
789
|
+
* `b.compliance.set` reads this to decide whether to emit the one-time
|
|
790
|
+
* `compliance.posture.outbound_dlp_unwired` warning when a posture whose
|
|
791
|
+
* floor implies outbound DLP is pinned without any wiring. Read-only.
|
|
792
|
+
*
|
|
793
|
+
* @example
|
|
794
|
+
* b.redact.isOutboundDlpInstalled(); // → false
|
|
795
|
+
* var dlp = b.redact.installForPosture("hipaa", { httpClient: myHttp });
|
|
796
|
+
* b.redact.isOutboundDlpInstalled(); // → true
|
|
797
|
+
* dlp.uninstall();
|
|
798
|
+
* b.redact.isOutboundDlpInstalled(); // → false
|
|
799
|
+
*/
|
|
800
|
+
function isOutboundDlpInstalled() {
|
|
801
|
+
return outboundInstallCount > 0;
|
|
802
|
+
}
|
|
803
|
+
|
|
770
804
|
function _resolvePosturePatterns(name) {
|
|
771
805
|
var n = String(name).toLowerCase();
|
|
772
806
|
if (n === "pci-dss" || n === "pci") {
|
|
@@ -779,6 +813,15 @@ function _resolvePosturePatterns(name) {
|
|
|
779
813
|
return ["pan", "credit-card", "iban", "pem", "aws-access-key", "jwt", "api-key-shape"];
|
|
780
814
|
}
|
|
781
815
|
if (n === "soc2" || n === "gdpr") {
|
|
816
|
+
// Known fidelity collapse: soc2 and gdpr share one outbound-DLP
|
|
817
|
+
// pattern set here. They are distinct regimes — GDPR's special-
|
|
818
|
+
// category personal data (Art. 9) is broader than the SOC 2 Trust
|
|
819
|
+
// Services criteria this preset targets — but the built-in
|
|
820
|
+
// value-shape detectors don't yet distinguish them, so the same
|
|
821
|
+
// pattern list backs both. Operators needing GDPR-specific shapes
|
|
822
|
+
// pass an explicit classifier (classifyDefaults) rather than the
|
|
823
|
+
// posture preset. This is intentional, not an accidental alias —
|
|
824
|
+
// documented so it isn't mistaken for per-regime handling.
|
|
782
825
|
return ["ssn", "ein", "pem", "ssh-private", "aws-access-key", "api-key-shape"];
|
|
783
826
|
}
|
|
784
827
|
throw new DlpError("redact-dlp/unknown-posture",
|
|
@@ -959,9 +1002,12 @@ function _summarizeHit(h) {
|
|
|
959
1002
|
return { label: h.label, action: h.action, where: h.where };
|
|
960
1003
|
}
|
|
961
1004
|
|
|
962
|
-
// Posture-coordinated install —
|
|
963
|
-
// to wire
|
|
964
|
-
// b.compliance
|
|
1005
|
+
// Posture-coordinated install — the operator passes the primitive
|
|
1006
|
+
// instances to wire (httpClient / mail / webhook); the posture name
|
|
1007
|
+
// selects the default classifier. b.compliance.set does NOT call this:
|
|
1008
|
+
// it holds no httpClient / mail / webhook handles, so it cannot install
|
|
1009
|
+
// outbound interceptors. Operators wire DLP explicitly by calling this
|
|
1010
|
+
// (or installOutboundDlp) with their own primitive instances.
|
|
965
1011
|
/**
|
|
966
1012
|
* @primitive b.redact.installForPosture
|
|
967
1013
|
* @signature b.redact.installForPosture(posture, primitives)
|
|
@@ -970,10 +1016,20 @@ function _summarizeHit(h) {
|
|
|
970
1016
|
* @compliance hipaa, pci-dss, gdpr, soc2, fapi2
|
|
971
1017
|
* @related b.redact.installOutboundDlp, b.redact.classifyDefaults
|
|
972
1018
|
*
|
|
973
|
-
* Posture-coordinated install —
|
|
974
|
-
* `
|
|
975
|
-
*
|
|
976
|
-
*
|
|
1019
|
+
* Posture-coordinated install — picks the default classifier for
|
|
1020
|
+
* `posture` and wraps the operator-supplied `primitives.httpClient` /
|
|
1021
|
+
* `.mail` / `.webhook` so every outbound payload runs through it. A
|
|
1022
|
+
* thin convenience over `installOutboundDlp`; direct callers usually
|
|
1023
|
+
* want `installOutboundDlp` because it accepts the full hook surface.
|
|
1024
|
+
*
|
|
1025
|
+
* The operator MUST call this with the primitive instances — pinning a
|
|
1026
|
+
* posture via `b.compliance.set` does NOT auto-install outbound DLP,
|
|
1027
|
+
* because the compliance coordinator holds no httpClient / mail /
|
|
1028
|
+
* webhook handles. When a posture whose floor implies outbound DLP
|
|
1029
|
+
* (hipaa / pci-dss / gdpr / soc2 / fapi-2.0) is pinned without this
|
|
1030
|
+
* call, `b.compliance.set` emits a one-time
|
|
1031
|
+
* `compliance.posture.outbound_dlp_unwired` audit warning so the gap is
|
|
1032
|
+
* grep-able in the audit chain.
|
|
977
1033
|
*
|
|
978
1034
|
* @example
|
|
979
1035
|
* var dlp = b.redact.installForPosture("hipaa", {
|
|
@@ -999,6 +1055,7 @@ module.exports = {
|
|
|
999
1055
|
classifyDefaults: classifyDefaults,
|
|
1000
1056
|
installOutboundDlp: installOutboundDlp,
|
|
1001
1057
|
installForPosture: installForPosture,
|
|
1058
|
+
isOutboundDlpInstalled: isOutboundDlpInstalled,
|
|
1002
1059
|
CLASSIFIER_PATTERNS: CLASSIFIER_PATTERNS,
|
|
1003
1060
|
MARKER: DEFAULT_MARKER,
|
|
1004
1061
|
SENSITIVE_FIELDS: SENSITIVE_FIELDS,
|