@blamejs/core 0.14.26 → 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.
@@ -35,6 +35,7 @@ var { URL } = require("node:url");
35
35
  var { Readable } = require("node:stream");
36
36
  var safeXml = require("../parsers/safe-xml");
37
37
  var sharedRequest = require("./http-request");
38
+ var sigv4 = require("./sigv4");
38
39
  var C = require("../constants");
39
40
  var requestHelpers = require("../request-helpers");
40
41
  var { ObjectStoreError } = require("../framework-error");
@@ -65,6 +66,26 @@ function _arrayify(value) {
65
66
  return Array.isArray(value) ? value : [value];
66
67
  }
67
68
 
69
+ // Percent-encode a hierarchical blob name for use in a URL path. Azure
70
+ // blob names are `/`-delimited virtual directories, so each segment is
71
+ // RFC 3986 percent-encoded (via the family-shared encoder used by the
72
+ // S3 / GCS backends) while the `/` separators are preserved. Without
73
+ // this, a key containing `?`, `#`, a space, or other reserved chars is
74
+ // interpolated raw into the request URL — `?`/`#` start the query /
75
+ // fragment (so the blob path is truncated, hitting the wrong object or
76
+ // the container root), and spaces / control bytes corrupt the request
77
+ // line (CWE-20 improper input → request-smuggling-adjacent). A null
78
+ // byte is refused outright (it can't appear in a valid blob name and
79
+ // indicates a malformed / hostile key), matching the S3 / GCS guards.
80
+ function _encodeBlobKey(key) {
81
+ if (key.indexOf("\0") !== -1) {
82
+ throw _err("INVALID_KEY", "null byte in blob key", true);
83
+ }
84
+ return key.split("/").map(function (s) {
85
+ return sigv4.awsUriEncode(s, true);
86
+ }).join("/");
87
+ }
88
+
68
89
  var DEFAULT_API_VERSION = "2024-08-04";
69
90
 
70
91
  // Service SAS expiry bounds. Azure doesn't enforce a hard max, but
@@ -208,7 +229,8 @@ function create(config) {
208
229
  if (allowInternal !== null) reqOpts.allowInternal = allowInternal;
209
230
 
210
231
  function _blobUrl(key, params) {
211
- var u = _internalUrl(endpoint + "/" + config.container + "/" + key, allowedProtocols);
232
+ var u = _internalUrl(endpoint + "/" + config.container + "/" + _encodeBlobKey(key),
233
+ allowedProtocols);
212
234
  if (params) {
213
235
  Object.keys(params).forEach(function (k) { u.searchParams.set(k, params[k]); });
214
236
  }
@@ -425,8 +447,12 @@ function create(config) {
425
447
  throw _err("INVALID_KEY", "null byte in key", true);
426
448
  }
427
449
 
450
+ // _buildSasToken signs the canonicalized resource with the RAW
451
+ // (decoded) blob name per the Azure SAS spec; the URL PATH carries the
452
+ // percent-encoded key so a key with reserved chars (`?` / `#` / space)
453
+ // doesn't truncate the path or corrupt the request line.
428
454
  var token = _buildSasToken(permissions, opts);
429
- var url = _internalUrl(endpoint + "/" + config.container + "/" + opts.key + "?" + token.sas, allowedProtocols);
455
+ var url = _internalUrl(endpoint + "/" + config.container + "/" + _encodeBlobKey(opts.key) + "?" + token.sas, allowedProtocols);
430
456
 
431
457
  var clientHeaders = {};
432
458
  if (opts.contentType) clientHeaders["Content-Type"] = opts.contentType;
@@ -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,
@@ -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") {
@@ -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
- return input.substring(start, pos);
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
- var attrs = {};
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
- var obj = {};
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
- var out = {};
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/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(); } catch (_e) { /* best-effort */ }
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 — 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.
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 — 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.
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,