@blamejs/core 0.18.51 → 0.18.54
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 +185 -0
- package/NOTICE +1 -1
- package/README.md +3 -3
- package/lib/ai-adverse-decision.js +18 -2
- package/lib/codepoint-class.js +72 -0
- package/lib/cookies.js +7 -10
- package/lib/credential-hash.js +8 -1
- package/lib/crypto.js +7 -5
- package/lib/guard-auth.js +34 -11
- package/lib/guard-filename.js +39 -16
- package/lib/guard-managesieve-command.js +49 -9
- package/lib/guard-regex.js +3 -5
- package/lib/guard-yaml.js +212 -31
- package/lib/mail-agent.js +23 -9
- package/lib/mail-arc-sign.js +40 -7
- package/lib/mail-auth.js +75 -20
- package/lib/mail-crypto-pgp.js +1 -1
- package/lib/mail-dkim.js +80 -11
- package/lib/mail-helo.js +10 -0
- package/lib/mail-rbl.js +10 -3
- package/lib/mail-send-deliver.js +151 -32
- package/lib/mail-server-imap.js +121 -55
- package/lib/mail-server-jmap.js +31 -4
- package/lib/mail-server-managesieve.js +168 -25
- package/lib/mail-server-mx.js +76 -4
- package/lib/mail-server-net.js +126 -0
- package/lib/mail-server-pop3.js +73 -22
- package/lib/mail-server-submission.js +21 -2
- package/lib/mail-store.js +33 -11
- package/lib/mail.js +355 -17
- package/lib/middleware/bearer-auth.js +6 -1
- package/lib/middleware/fetch-metadata.js +5 -1
- package/lib/middleware/headers.js +7 -10
- package/lib/network-dns-resolver.js +71 -8
- package/lib/network-dns.js +26 -0
- package/lib/network-smtp-policy.js +42 -10
- package/lib/parsers/safe-yaml.js +24 -3
- package/lib/redact.js +13 -3
- package/lib/retention.js +22 -2
- package/lib/vendor/MANIFEST.json +12 -12
- package/lib/vendor/blamejs-pki.cjs +278 -40
- package/lib/yaml-lex.js +587 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
* });
|
|
36
36
|
*/
|
|
37
37
|
|
|
38
|
+
var codepointClass = require("../codepoint-class");
|
|
38
39
|
var lazyRequire = require("../lazy-require");
|
|
39
40
|
var safeBuffer = require("../safe-buffer");
|
|
40
41
|
|
|
@@ -104,16 +105,12 @@ function _detectIssues(headers, opts) {
|
|
|
104
105
|
});
|
|
105
106
|
continue;
|
|
106
107
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
"— header-injection defense in depth",
|
|
114
|
-
});
|
|
115
|
-
break;
|
|
116
|
-
}
|
|
108
|
+
if (codepointClass.firstLineInjectionCharOffset(v) !== -1) {
|
|
109
|
+
issues.push({
|
|
110
|
+
kind: "header-value-control-byte", severity: "high", header: name,
|
|
111
|
+
snippet: "header `" + name + "` value contains CR / LF / NUL " +
|
|
112
|
+
"— header-injection defense in depth",
|
|
113
|
+
});
|
|
117
114
|
}
|
|
118
115
|
}
|
|
119
116
|
}
|
|
@@ -402,7 +402,21 @@ function create(opts) {
|
|
|
402
402
|
|
|
403
403
|
if (parsed.rcode !== 0) {
|
|
404
404
|
// RFC 1035 §4.1.1 — non-zero RCODE. Surface and refuse caching.
|
|
405
|
-
|
|
405
|
+
//
|
|
406
|
+
// NXDOMAIN and everything else are DIFFERENT ANSWERS and no longer share
|
|
407
|
+
// a code. NXDOMAIN (RCODE 3) means the name does not exist, which for a
|
|
408
|
+
// policy fetch or a blocklist is the ordinary case: no MTA-STS record, not
|
|
409
|
+
// on the list. SERVFAIL and REFUSED mean the question was not answered,
|
|
410
|
+
// and reading those as absence is a downgrade an attacker can arrange —
|
|
411
|
+
// break the lookup and the policy that would have refused you disappears.
|
|
412
|
+
//
|
|
413
|
+
// One code called `nxdomain-or-error` forced both readings on every
|
|
414
|
+
// caller, and both callers picked absence: `safeResolveTxt` could not
|
|
415
|
+
// match it at all so the commonest form of absence threw, and `b.mail.rbl`
|
|
416
|
+
// matched it and returned "not listed" for a lookup that failed.
|
|
417
|
+
var absent = parsed.rcode === 3; // RFC 1035 §4.1.1 NXDOMAIN
|
|
418
|
+
throw new ResolverError(
|
|
419
|
+
absent ? "resolver/nxdomain" : "resolver/query-failed",
|
|
406
420
|
"query: upstream RCODE=" + parsed.rcode + " for " + name + "/" + qtype);
|
|
407
421
|
}
|
|
408
422
|
|
|
@@ -501,6 +515,12 @@ function create(opts) {
|
|
|
501
515
|
queryCname: _typed("CNAME"),
|
|
502
516
|
queryMx: _typed("MX"),
|
|
503
517
|
queryNs: _typed("NS"),
|
|
518
|
+
// PTR was the one common type with no helper, though it has been in
|
|
519
|
+
// QTYPE_BY_NAME all along. `b.mail.helo` called `resolver.queryPtr` for its
|
|
520
|
+
// forward-confirmed reverse DNS check, got `undefined`, and the resulting
|
|
521
|
+
// TypeError landed in a catch written for NXDOMAIN — so the check reported
|
|
522
|
+
// a clean "no reverse name" for every address instead of a broken call.
|
|
523
|
+
queryPtr: _typed("PTR"),
|
|
504
524
|
queryTxt: _typed("TXT"),
|
|
505
525
|
querySrv: _typed("SRV"),
|
|
506
526
|
queryTlsa: _typed("TLSA"),
|
|
@@ -533,12 +553,31 @@ function _defaultTransport(timeoutMs) {
|
|
|
533
553
|
async function _wireLookup(name, qtype, timeoutMs) {
|
|
534
554
|
var ms = typeof timeoutMs === "number" && isFinite(timeoutMs) && timeoutMs > 0
|
|
535
555
|
? timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
536
|
-
|
|
556
|
+
// Where the deployment's DoH actually points. This used to be a hardcoded
|
|
557
|
+
// public provider, so an operator who had configured DoH to their own
|
|
558
|
+
// resolver still had every resolver.query() leave for the public one — and
|
|
559
|
+
// any statement they made about that resolver (that it validates DNSSEC, that
|
|
560
|
+
// it serves a split-horizon zone) described a resolver the query never
|
|
561
|
+
// reached. b.mail.send.deliver's `policy.dnssecValidated` is exactly such a
|
|
562
|
+
// statement. The endpoint is read per lookup rather than captured at create,
|
|
563
|
+
// so a later useDnsOverHttps reaches the next query instead of a handle's
|
|
564
|
+
// whole lifetime being pinned to boot-time configuration.
|
|
565
|
+
var endpoint = networkDns.activeDohEndpoint();
|
|
566
|
+
var url = (endpoint && endpoint.url) || "https://cloudflare-dns.com/dns-query";
|
|
537
567
|
// Encode a wire-format query for the target qtype.
|
|
538
568
|
var qbuf = _encodeWireQuery(name, qtype);
|
|
539
569
|
var b64 = bCrypto.toBase64Url(qbuf);
|
|
540
570
|
var getUrl = url + (url.indexOf("?") === -1 ? "?" : "&") + "dns=" + b64;
|
|
541
|
-
|
|
571
|
+
// RFC 8484 §4.1 — GET carries the query in the URL, POST in the body. An
|
|
572
|
+
// endpoint may accept only POST, and `useDnsOverHttps({ method })` says so;
|
|
573
|
+
// honouring the operator's URL while ignoring their method leaves that
|
|
574
|
+
// endpoint unusable. With no method configured the choice falls to URL
|
|
575
|
+
// length, using the threshold b.network.dns owns rather than a second copy
|
|
576
|
+
// of the number.
|
|
577
|
+
var maxGet = (endpoint && endpoint.getUrlMaxBytes) || 2048; // RFC 9112 §3-conservative GET ceiling
|
|
578
|
+
var usePost = (endpoint && endpoint.method === "POST") ||
|
|
579
|
+
(!(endpoint && endpoint.method) && getUrl.length > maxGet);
|
|
580
|
+
var u = safeUrl.parse(usePost ? url : getUrl, { allowedProtocols: safeUrl.ALLOW_HTTP_TLS });
|
|
542
581
|
return new Promise(function (resolve, reject) {
|
|
543
582
|
var settled = false;
|
|
544
583
|
function _fail(err) {
|
|
@@ -555,13 +594,23 @@ async function _wireLookup(name, qtype, timeoutMs) {
|
|
|
555
594
|
// Raw DoH wire-format request — bypasses b.httpClient envelope
|
|
556
595
|
// because we need the raw binary response bytes for safeDns to
|
|
557
596
|
// parse (httpClient assumes JSON/text shapes).
|
|
558
|
-
|
|
597
|
+
// The operator's CA travels with the endpoint. A private DoH resolver is
|
|
598
|
+
// usually issued by a private CA, so honouring the URL without the trust
|
|
599
|
+
// anchor that validates it would swap one unreachable endpoint for another.
|
|
600
|
+
var headers = { "accept": "application/dns-message" };
|
|
601
|
+
if (usePost) {
|
|
602
|
+
headers["content-type"] = "application/dns-message";
|
|
603
|
+
headers["content-length"] = String(qbuf.length);
|
|
604
|
+
}
|
|
605
|
+
var reqOpts = Object.assign({
|
|
559
606
|
hostname: u.hostname,
|
|
560
607
|
port: u.port || 443, // HTTPS port
|
|
561
608
|
path: u.pathname + u.search,
|
|
562
|
-
method: "GET",
|
|
563
|
-
headers:
|
|
564
|
-
}, networkTls().outboundPosture())
|
|
609
|
+
method: usePost ? "POST" : "GET",
|
|
610
|
+
headers: headers,
|
|
611
|
+
}, networkTls().outboundPosture());
|
|
612
|
+
if (endpoint && endpoint.ca) reqOpts.ca = endpoint.ca;
|
|
613
|
+
var req = https.request(reqOpts, function (res) { // allow:raw-outbound-http-framework-internal — DoH wire-format response bytes; b.httpClient envelopes assume text/JSON, and httpClient → ssrfGuard → DNS → DoH would form a cycle
|
|
565
614
|
var collector = safeBuffer.boundedChunkCollector({
|
|
566
615
|
maxBytes: C.BYTES.kib(64),
|
|
567
616
|
errorClass: ResolverError,
|
|
@@ -599,6 +648,8 @@ async function _wireLookup(name, qtype, timeoutMs) {
|
|
|
599
648
|
_fail(new ResolverError("resolver/upstream-failed",
|
|
600
649
|
"DoH request failed: " + e.message));
|
|
601
650
|
});
|
|
651
|
+
// POST carries the wire-format query as the body; GET carried it in the URL.
|
|
652
|
+
if (usePost) req.write(qbuf);
|
|
602
653
|
req.end();
|
|
603
654
|
});
|
|
604
655
|
}
|
|
@@ -709,7 +760,19 @@ async function safeResolveTxt(qname, opts) {
|
|
|
709
760
|
try {
|
|
710
761
|
return await resolveTxt(qname, opts.dnsLookup);
|
|
711
762
|
} catch (e) {
|
|
712
|
-
|
|
763
|
+
// `resolver/nxdomain` belongs here and was the omission: it is how THIS
|
|
764
|
+
// framework's own resolver reports a name that does not exist, which is the
|
|
765
|
+
// commonest way a policy record is absent. Only ENOTFOUND / ENODATA were
|
|
766
|
+
// listed — the codes node's stub resolver uses — so the documented
|
|
767
|
+
// "absence is not an error" convention held for an operator-supplied
|
|
768
|
+
// `dnsLookup` and not for the resolver the module ships with.
|
|
769
|
+
//
|
|
770
|
+
// `resolver/query-failed` deliberately does NOT belong here. A lookup that
|
|
771
|
+
// was not answered is not a record that is not published, and treating it
|
|
772
|
+
// as one lets an attacker who can break the query delete the policy that
|
|
773
|
+
// would have refused them.
|
|
774
|
+
if (e && (e.code === "ENOTFOUND" || e.code === "ENODATA" ||
|
|
775
|
+
e.code === "resolver/nxdomain")) return null;
|
|
713
776
|
var msg = "TXT lookup for " + qname + " failed: " + ((e && e.message) || String(e));
|
|
714
777
|
if (typeof opts.errorFactory === "function") throw opts.errorFactory(opts.code, msg);
|
|
715
778
|
throw e;
|
package/lib/network-dns.js
CHANGED
|
@@ -437,6 +437,31 @@ function useDnsOverHttps(opts) {
|
|
|
437
437
|
observability().safeEvent("network.dns.doh.set", 1, { url: url, method: method || "auto" });
|
|
438
438
|
}
|
|
439
439
|
|
|
440
|
+
// activeDohEndpoint() — where DoH queries go right now, or null when the
|
|
441
|
+
// deployment is not on DoH (the operator chose DoT or the system resolver).
|
|
442
|
+
//
|
|
443
|
+
// b.network.dns.resolver runs its own wire-format request, because it needs the
|
|
444
|
+
// raw response bytes for arbitrary record types and this module's DoH path
|
|
445
|
+
// returns decoded addresses. Reading the endpoint from here is what keeps the
|
|
446
|
+
// two agreeing: the resolver previously hardcoded a public provider, so a
|
|
447
|
+
// deployment that had configured DoH to its own resolver — for split horizon,
|
|
448
|
+
// for privacy, or because that resolver validates DNSSEC — had every
|
|
449
|
+
// resolver.query() go to the public one instead, silently.
|
|
450
|
+
// `getUrlMaxBytes` travels with it because the method is not always configured:
|
|
451
|
+
// RFC 8484 §4.1 lets a client switch to POST when the GET URL would be too
|
|
452
|
+
// long, and that threshold is this module's to choose. Returning it keeps the
|
|
453
|
+
// resolver's decision identical to the one made here rather than a second
|
|
454
|
+
// number that can drift.
|
|
455
|
+
function activeDohEndpoint() {
|
|
456
|
+
if (!STATE.doh) return null;
|
|
457
|
+
return {
|
|
458
|
+
url: STATE.doh.url,
|
|
459
|
+
method: STATE.doh.method || null,
|
|
460
|
+
ca: STATE.doh.ca || null,
|
|
461
|
+
getUrlMaxBytes: DOH_GET_URL_MAX_BYTES,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
440
465
|
function useDnsOverTls(opts) {
|
|
441
466
|
opts = opts || {};
|
|
442
467
|
validateOpts(opts, ["host", "port", "servername", "ca"], "dns.useDnsOverTls");
|
|
@@ -2259,6 +2284,7 @@ module.exports = {
|
|
|
2259
2284
|
setLookupTimeoutMs: setLookupTimeoutMs,
|
|
2260
2285
|
setCacheTtlMs: setCacheTtlMs,
|
|
2261
2286
|
useDnsOverHttps: useDnsOverHttps,
|
|
2287
|
+
activeDohEndpoint: activeDohEndpoint,
|
|
2262
2288
|
useDnsOverTls: useDnsOverTls,
|
|
2263
2289
|
useSystemResolver: useSystemResolver,
|
|
2264
2290
|
useDesignatedResolvers: useDesignatedResolvers,
|
|
@@ -243,17 +243,49 @@ async function daneTlsa(domain, port, opts) {
|
|
|
243
243
|
opts = opts || {};
|
|
244
244
|
var p = typeof port === "number" ? port : 25; // IANA SMTP port
|
|
245
245
|
var qname = "_" + p + "._tcp." + domain.toLowerCase();
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
246
|
+
|
|
247
|
+
// `opts.resolver` is the caller's own resolver — the one whose DNSSEC posture
|
|
248
|
+
// `opts.dnssecValidated` is an assertion ABOUT. Falling back to node:dns here
|
|
249
|
+
// when one was supplied would mean the assertion described resolver A while
|
|
250
|
+
// the records arrived from resolver B, so a non-validating system resolver
|
|
251
|
+
// could hand over spoofed TLSA data that DANE then treats as authenticated.
|
|
252
|
+
// A supplied resolver that cannot answer TLSA is refused rather than bypassed.
|
|
251
253
|
var records;
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
254
|
+
if (opts.resolver) {
|
|
255
|
+
if (typeof opts.resolver.queryTlsa !== "function") {
|
|
256
|
+
throw new SmtpPolicyError("smtp/dane-resolver-no-tlsa",
|
|
257
|
+
"dane.tlsa: opts.resolver was supplied but has no queryTlsa(name); the " +
|
|
258
|
+
"records must come from the resolver whose DNSSEC posture was asserted, " +
|
|
259
|
+
"so falling back to the system resolver is refused");
|
|
260
|
+
}
|
|
261
|
+
var rv;
|
|
262
|
+
try { rv = await opts.resolver.queryTlsa(qname); }
|
|
263
|
+
catch (e0) {
|
|
264
|
+
if (e0 && e0.code === "resolver/nxdomain") return []; // no TLSA published
|
|
265
|
+
throw new SmtpPolicyError("smtp/dane-lookup-failed",
|
|
266
|
+
"TLSA lookup for " + qname + " failed: " + ((e0 && e0.message) || String(e0)));
|
|
267
|
+
}
|
|
268
|
+
// b.network.dns decodes TLSA into { usage, selector, matchingType, certData };
|
|
269
|
+
// node:dns names the same fields certUsage / selector / match / data. Mapped
|
|
270
|
+
// here so the normalisation below has one shape to read.
|
|
271
|
+
records = ((rv && rv.rrs) || []).filter(function (r) {
|
|
272
|
+
return r && r.decoded && typeof r.decoded.usage === "number";
|
|
273
|
+
}).map(function (r) {
|
|
274
|
+
return { certUsage: r.decoded.usage, selector: r.decoded.selector,
|
|
275
|
+
match: r.decoded.matchingType, data: r.decoded.certData };
|
|
276
|
+
});
|
|
277
|
+
} else {
|
|
278
|
+
// node:dns has resolveTlsa() since Node 18.16.0.
|
|
279
|
+
if (typeof dnsPromises.resolveTlsa !== "function") {
|
|
280
|
+
throw new SmtpPolicyError("smtp/dane-unavailable",
|
|
281
|
+
"node:dns.resolveTlsa is not available on this runtime");
|
|
282
|
+
}
|
|
283
|
+
try { records = await dnsPromises.resolveTlsa(qname); }
|
|
284
|
+
catch (e) {
|
|
285
|
+
if (e && (e.code === "ENOTFOUND" || e.code === "ENODATA")) return [];
|
|
286
|
+
throw new SmtpPolicyError("smtp/dane-lookup-failed",
|
|
287
|
+
"TLSA lookup for " + qname + " failed: " + ((e && e.message) || String(e)));
|
|
288
|
+
}
|
|
257
289
|
}
|
|
258
290
|
// RFC 7672 §1.3 — TLSA records that are NOT DNSSEC-validated MUST
|
|
259
291
|
// NOT be used. node:dns.resolveTlsa does not surface the AD bit
|
package/lib/parsers/safe-yaml.js
CHANGED
|
@@ -60,6 +60,7 @@ var boundedMap = require("../bounded-map");
|
|
|
60
60
|
var numericBounds = require("../numeric-bounds");
|
|
61
61
|
var safeBuffer = require("../safe-buffer");
|
|
62
62
|
var codepointClass = require("../codepoint-class");
|
|
63
|
+
var yamlLex = require("../yaml-lex");
|
|
63
64
|
var { FrameworkError } = require("../framework-error");
|
|
64
65
|
|
|
65
66
|
class SafeYamlError extends FrameworkError {
|
|
@@ -227,7 +228,10 @@ function _findAnchorOrAlias(text) {
|
|
|
227
228
|
var ch = text.charAt(i);
|
|
228
229
|
if (ch !== "&" && ch !== "*") continue;
|
|
229
230
|
var atStart = i === 0;
|
|
230
|
-
|
|
231
|
+
// No test on the preceding character: `text` is the shared mask, in which a
|
|
232
|
+
// sigil survives only where it opens a node. The whitespace test was the
|
|
233
|
+
// original defect and it also had no way to admit `{"a":&anchor v}`, where
|
|
234
|
+
// a quoted key takes its colon with no space after it.
|
|
231
235
|
if (_NAME_HEAD.indexOf(text.charAt(i + 1)) === -1 || text.charAt(i + 1) === "") continue;
|
|
232
236
|
return { index: atStart ? i : i - 1, sigil: ch };
|
|
233
237
|
}
|
|
@@ -239,8 +243,10 @@ function _findTag(text) {
|
|
|
239
243
|
for (var i = 0; i < text.length; i += 1) {
|
|
240
244
|
if (text.charAt(i) !== "!") continue;
|
|
241
245
|
var atStart = i === 0;
|
|
242
|
-
|
|
243
|
-
|
|
246
|
+
// Same as above: position is the mask's judgement. Requiring whitespace or
|
|
247
|
+
// a dash before the tag let `{"a":!!python/object x}` through, because the
|
|
248
|
+
// character in front of it is the colon of a JSON-style key — and a
|
|
249
|
+
// deserialization tag is the one thing this ban exists for.
|
|
244
250
|
// The pattern is greedy, so it takes two `!` when both are present.
|
|
245
251
|
var after = text.charAt(i + 1) === "!" ? text.charAt(i + 2) : text.charAt(i + 1);
|
|
246
252
|
if (after.length !== 1) continue;
|
|
@@ -1194,6 +1200,21 @@ function _preValidate(input) {
|
|
|
1194
1200
|
advance();
|
|
1195
1201
|
}
|
|
1196
1202
|
|
|
1203
|
+
// The masking above is kept for ONE thing: it is the scan that notices an
|
|
1204
|
+
// unterminated quote, and that diagnosis is this parser's to give. What it
|
|
1205
|
+
// produced was not a usable screen. Its own header promised that block-scalar
|
|
1206
|
+
// bodies were masked and there was no block-scalar handling in it at all, and
|
|
1207
|
+
// comment text was copied through verbatim under a comment saying so — which
|
|
1208
|
+
// is why `x: 1 # note !bang` and `x: |` / ` echo !boom` were both refused as
|
|
1209
|
+
// tags. A plain scalar was not understood either, so `x: hello !world` was
|
|
1210
|
+
// refused for a bang sitting in the middle of a value.
|
|
1211
|
+
//
|
|
1212
|
+
// The banned-construct scan therefore runs against the shared lexer's mask.
|
|
1213
|
+
// `guard-yaml` had the same three gaps in a separate implementation, and one
|
|
1214
|
+
// scan answering "which region is this character in" is what stops the two
|
|
1215
|
+
// drifting apart again.
|
|
1216
|
+
safe = yamlLex.maskNonStructural(input);
|
|
1217
|
+
|
|
1197
1218
|
// Now scan `safe` for banned constructs.
|
|
1198
1219
|
// Banned tokens (must be at line-start or after whitespace, not in keys):
|
|
1199
1220
|
// &name anchor
|
package/lib/redact.js
CHANGED
|
@@ -527,8 +527,14 @@ function classifyDefaults(opts) {
|
|
|
527
527
|
"redact.classifyDefaults: patterns[" + p + "] must be a string, got " +
|
|
528
528
|
typeof patterns[p]);
|
|
529
529
|
}
|
|
530
|
+
// Own-key on BOTH tables. `extra` is operator-supplied, so reading it
|
|
531
|
+
// plainly resolved "constructor" / "toString" / "valueOf" to inherited
|
|
532
|
+
// functions: the name passed as known, and the scanner then met a function
|
|
533
|
+
// where a pattern spec belongs and threw an untyped error at scan time.
|
|
534
|
+
// A DLP classifier must fail at boot on a name it cannot honour, never at
|
|
535
|
+
// the moment it is asked to look.
|
|
530
536
|
if (!Object.prototype.hasOwnProperty.call(CLASSIFIER_PATTERNS, patterns[p]) &&
|
|
531
|
-
!(opts.extra && opts.extra
|
|
537
|
+
!(opts.extra && Object.prototype.hasOwnProperty.call(opts.extra, patterns[p]))) {
|
|
532
538
|
throw new DlpError("redact-dlp/unknown-pattern",
|
|
533
539
|
"redact.classifyDefaults: unknown pattern '" + patterns[p] +
|
|
534
540
|
"'. Known: " + Object.keys(CLASSIFIER_PATTERNS).join(", "));
|
|
@@ -544,8 +550,12 @@ function classifyDefaults(opts) {
|
|
|
544
550
|
var extra = opts.extra || {};
|
|
545
551
|
|
|
546
552
|
function _resolve(name) {
|
|
547
|
-
|
|
548
|
-
|
|
553
|
+
// Own-key, matching the validation above: the names reaching here are
|
|
554
|
+
// already vetted, and reading through the prototype anyway would let a
|
|
555
|
+
// future caller that skips the vetting resolve a function as a spec.
|
|
556
|
+
if (Object.prototype.hasOwnProperty.call(CLASSIFIER_PATTERNS, name)) return CLASSIFIER_PATTERNS[name];
|
|
557
|
+
if (Object.prototype.hasOwnProperty.call(extra, name)) return extra[name];
|
|
558
|
+
return undefined;
|
|
549
559
|
}
|
|
550
560
|
|
|
551
561
|
return function classify(input) {
|
package/lib/retention.js
CHANGED
|
@@ -55,6 +55,9 @@ var { defineClass } = require("./framework-error");
|
|
|
55
55
|
var audit = lazyRequire(function () { return require("./audit"); });
|
|
56
56
|
var cryptoField = require("./crypto-field");
|
|
57
57
|
var legalHold = lazyRequire(function () { return require("./legal-hold"); });
|
|
58
|
+
// Lazy — compliance.js requires this module for its posture cascade, so the
|
|
59
|
+
// dependency is mutual and neither may load the other at module scope.
|
|
60
|
+
var compliance = lazyRequire(function () { return require("./compliance"); });
|
|
58
61
|
|
|
59
62
|
var RetentionError = defineClass("RetentionError", { alwaysPermanent: true });
|
|
60
63
|
var _err = RetentionError.factory;
|
|
@@ -648,10 +651,27 @@ function complianceFloor(posture, candidateTtlMs) {
|
|
|
648
651
|
var floor = Object.prototype.hasOwnProperty.call(COMPLIANCE_RETENTION_FLOOR_MS, posture)
|
|
649
652
|
? COMPLIANCE_RETENTION_FLOOR_MS[posture] : undefined;
|
|
650
653
|
if (floor === undefined) {
|
|
654
|
+
// "This regime sets no retention minimum" and "this is not a regime" are
|
|
655
|
+
// different facts, and the floor table only ever answered the first. Most
|
|
656
|
+
// postures the framework knows impose no minimum — GDPR Art. 5(1)(e) is
|
|
657
|
+
// storage LIMITATION, the opposite of a floor — so treating absence from
|
|
658
|
+
// this table as a typo reported 158 of the 169 postures b.compliance.set
|
|
659
|
+
// accepts as misspellings, and an operator who set one could not compute a
|
|
660
|
+
// TTL at all. The posture vocabulary lives in b.compliance; this table
|
|
661
|
+
// holds only the subset carrying a regulator-mandated minimum.
|
|
662
|
+
if (compliance().KNOWN_POSTURES.indexOf(posture) !== -1) return _atLeast(0, candidateTtlMs);
|
|
651
663
|
throw new RetentionError("retention/unknown-posture",
|
|
652
|
-
"complianceFloor: unknown posture '" + posture + "';
|
|
653
|
-
|
|
664
|
+
"complianceFloor: unknown posture '" + posture + "'; it is not in " +
|
|
665
|
+
"b.compliance.KNOWN_POSTURES. Postures carrying a retention minimum are " +
|
|
666
|
+
Object.keys(COMPLIANCE_RETENTION_FLOOR_MS).join(", ") + "; the rest resolve to 0");
|
|
654
667
|
}
|
|
668
|
+
return _atLeast(floor, candidateTtlMs);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// The floor wins unless the operator's candidate is longer. A candidate that
|
|
672
|
+
// is absent or not a positive finite number is no candidate at all, and the
|
|
673
|
+
// floor stands alone.
|
|
674
|
+
function _atLeast(floor, candidateTtlMs) {
|
|
655
675
|
if (typeof candidateTtlMs !== "number" || !isFinite(candidateTtlMs) || candidateTtlMs <= 0) {
|
|
656
676
|
return floor;
|
|
657
677
|
}
|
package/lib/vendor/MANIFEST.json
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"server": "sha256:f3325f480cb8eb814fcb0baaa19336cbbf2b993f48624c6aa9600ffd69d0be5e",
|
|
22
22
|
"browser": "sha256:0ffd91540bcb586a29b56e52ee1c29df69097b50776beb4036a07558f7a4e12e"
|
|
23
23
|
},
|
|
24
|
-
"refreshedAt": "2026-08-
|
|
24
|
+
"refreshedAt": "2026-08-24T23:54:10.282Z"
|
|
25
25
|
},
|
|
26
26
|
"@noble/hashes": {
|
|
27
27
|
"version": "2.3.0",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"hashes": {
|
|
49
49
|
"browser": "sha256:dfe4b7ae3c9880e388c8da4b68f44742b229b53afacd1e674179527e33da62b0"
|
|
50
50
|
},
|
|
51
|
-
"refreshedAt": "2026-08-
|
|
51
|
+
"refreshedAt": "2026-08-24T23:54:10.282Z"
|
|
52
52
|
},
|
|
53
53
|
"@noble/curves": {
|
|
54
54
|
"version": "2.3.0",
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"hashes": {
|
|
71
71
|
"server": "sha256:b5fe88d1ea780d0581dee6145d666f89d46fc9531b5db35db2e5b16627840890"
|
|
72
72
|
},
|
|
73
|
-
"refreshedAt": "2026-08-
|
|
73
|
+
"refreshedAt": "2026-08-24T23:54:10.282Z",
|
|
74
74
|
"components": {
|
|
75
75
|
"@noble/hashes": {
|
|
76
76
|
"url": "https://github.com/paulmillr/noble-hashes",
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
"server": "sha256:fab7ebe5737793862c473444f4ee5912f79dd1edec86683acbb4eecbca0f5892",
|
|
115
115
|
"browser": "sha256:cae1d5bbdc7184b202b6ca68df6e1db7b0d0f668c77809ded189ca7f271accc9"
|
|
116
116
|
},
|
|
117
|
-
"refreshedAt": "2026-08-
|
|
117
|
+
"refreshedAt": "2026-08-24T23:54:10.282Z",
|
|
118
118
|
"components": {
|
|
119
119
|
"@noble/hashes": {
|
|
120
120
|
"url": "https://github.com/paulmillr/noble-hashes",
|
|
@@ -148,7 +148,7 @@
|
|
|
148
148
|
},
|
|
149
149
|
"runtime_artifact": "lib/vendor/common-passwords-top-10000.data.js",
|
|
150
150
|
"integrity_layers": "sha256 + sha3-512 + SLH-DSA-SHAKE-256f signature + in-payload canary (where applicable)",
|
|
151
|
-
"refreshedAt": "2026-08-
|
|
151
|
+
"refreshedAt": "2026-08-24T23:54:10.282Z"
|
|
152
152
|
},
|
|
153
153
|
"bimi-trust-anchors": {
|
|
154
154
|
"version": "operator-managed",
|
|
@@ -173,7 +173,7 @@
|
|
|
173
173
|
},
|
|
174
174
|
"runtime_artifact": "lib/vendor/bimi-trust-anchors.data.js",
|
|
175
175
|
"integrity_layers": "sha256 + sha3-512 + SLH-DSA-SHAKE-256f signature + in-payload canary (where applicable)",
|
|
176
|
-
"refreshedAt": "2026-08-
|
|
176
|
+
"refreshedAt": "2026-08-24T23:54:10.282Z"
|
|
177
177
|
},
|
|
178
178
|
"publicsuffix-list": {
|
|
179
179
|
"version": "master",
|
|
@@ -193,10 +193,10 @@
|
|
|
193
193
|
},
|
|
194
194
|
"runtime_artifact": "lib/vendor/public-suffix-list.data.js",
|
|
195
195
|
"integrity_layers": "sha256 + sha3-512 + SLH-DSA-SHAKE-256f signature + in-payload canary (where applicable)",
|
|
196
|
-
"refreshedAt": "2026-08-
|
|
196
|
+
"refreshedAt": "2026-08-24T23:54:10.282Z"
|
|
197
197
|
},
|
|
198
198
|
"@blamejs/pki": {
|
|
199
|
-
"version": "0.5.
|
|
199
|
+
"version": "0.5.30",
|
|
200
200
|
"license": "Apache-2.0",
|
|
201
201
|
"author": "blamejs",
|
|
202
202
|
"source": "https://github.com/blamejs/pki",
|
|
@@ -216,12 +216,12 @@
|
|
|
216
216
|
"server": "lib/vendor/blamejs-pki.cjs"
|
|
217
217
|
},
|
|
218
218
|
"bundler": "esbuild --format=cjs --platform=node --external:crypto --external:node:crypto",
|
|
219
|
-
"bundledAt": "2026-08-
|
|
220
|
-
"cpe": "cpe:2.3:a:blamejs:pki:0.5.
|
|
219
|
+
"bundledAt": "2026-08-24T00:00:00Z",
|
|
220
|
+
"cpe": "cpe:2.3:a:blamejs:pki:0.5.30:*:*:*:*:node.js:*:*",
|
|
221
221
|
"hashes": {
|
|
222
|
-
"server": "sha256:
|
|
222
|
+
"server": "sha256:fc8b1783ae40c377c1bc7afb7bd902b5282c165ed11354b803867f7d9f75f021"
|
|
223
223
|
},
|
|
224
|
-
"refreshedAt": "2026-08-
|
|
224
|
+
"refreshedAt": "2026-08-24T23:54:10.282Z"
|
|
225
225
|
}
|
|
226
226
|
}
|
|
227
227
|
}
|