@blamejs/core 0.15.10 → 0.15.12
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/acme.js +7 -11
- package/lib/client-hints.js +3 -1
- package/lib/cluster.js +4 -2
- package/lib/crypto-field.js +30 -0
- package/lib/db-declare-row-policy.js +20 -1
- package/lib/db.js +7 -0
- package/lib/guard-csv.js +13 -4
- package/lib/guard-filename.js +6 -2
- package/lib/http-client-cache.js +3 -1
- package/lib/http-message-signature.js +25 -8
- package/lib/log-stream-otlp-grpc.js +12 -1
- package/lib/log-stream-syslog.js +6 -0
- package/lib/log.js +24 -2
- package/lib/mail-bimi.js +16 -3
- package/lib/mail.js +21 -9
- package/lib/mcp.js +28 -6
- package/lib/middleware/body-parser.js +48 -6
- package/lib/middleware/bot-disclose.js +7 -5
- package/lib/middleware/speculation-rules.js +6 -4
- package/lib/network-dns.js +22 -26
- package/lib/network-heartbeat.js +3 -3
- package/lib/network-proxy.js +3 -7
- package/lib/network-tls.js +34 -13
- package/lib/network.js +2 -6
- package/lib/notify.js +7 -12
- package/lib/parsers/safe-env.js +6 -3
- package/lib/parsers/safe-yaml.js +6 -6
- package/lib/safe-buffer.js +69 -1
- package/lib/seeders.js +5 -10
- package/lib/structured-fields.js +38 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/mcp.js
CHANGED
|
@@ -435,7 +435,28 @@ var PROMPT_INJECTION_MARKERS = [
|
|
|
435
435
|
"</?(?:assistant|system|user|tool)>",
|
|
436
436
|
];
|
|
437
437
|
var INJECTION_RE = new RegExp(PROMPT_INJECTION_MARKERS.join("|"), "i"); // allow:dynamic-regex — composed from the const PROMPT_INJECTION_MARKERS list above; not operator-supplied input
|
|
438
|
-
|
|
438
|
+
// vbscript: and data:text/html are dangerous URL schemes the guard claims to
|
|
439
|
+
// neutralize but the original alternation omitted (CodeQL js/incomplete-url-
|
|
440
|
+
// scheme-check). data: is scoped to text/html so a benign data:image/png isn't
|
|
441
|
+
// over-redacted. The alternation stays linear (no nested quantifier).
|
|
442
|
+
var DANGEROUS_HTML_RE = /<script\b|<iframe\b|<object\b|<embed\b|javascript:|vbscript:|data:\s*text\/html/i;
|
|
443
|
+
|
|
444
|
+
// Global variants for sanitize-mode redaction. A non-global .replace removes
|
|
445
|
+
// only the LEFTMOST match, so on `data:text/html,<script>alert(1)</script>`
|
|
446
|
+
// the leftmost `data:text/html` would be stripped and the executable
|
|
447
|
+
// `<script>` left behind — sanitize mode returning runnable HTML. _redactAll
|
|
448
|
+
// replaces EVERY dangerous token, repeating to a fixpoint in case removing one
|
|
449
|
+
// abuts two halves into a new one. Input byte-length is bounded by the caller,
|
|
450
|
+
// and [REDACTED] introduces no dangerous token, so the loop terminates quickly.
|
|
451
|
+
var INJECTION_RE_G = new RegExp(PROMPT_INJECTION_MARKERS.join("|"), "gi"); // allow:dynamic-regex — same const-composed source as INJECTION_RE
|
|
452
|
+
var DANGEROUS_HTML_RE_G = /<script\b|<iframe\b|<object\b|<embed\b|javascript:|vbscript:|data:\s*text\/html/gi;
|
|
453
|
+
|
|
454
|
+
function _redactAll(text, globalRe) {
|
|
455
|
+
var out = text;
|
|
456
|
+
var prev;
|
|
457
|
+
do { prev = out; out = out.replace(globalRe, "[REDACTED]"); } while (out !== prev);
|
|
458
|
+
return out;
|
|
459
|
+
}
|
|
439
460
|
|
|
440
461
|
function _toolResultSanitize(result, opts) {
|
|
441
462
|
opts = opts || {};
|
|
@@ -475,14 +496,15 @@ function _toolResultSanitize(result, opts) {
|
|
|
475
496
|
if (INJECTION_RE.test(regexInput)) { // allow:regex-no-length-cap regexInput byteLength bounded above
|
|
476
497
|
issues.push({ kind: "prompt-injection", index: i });
|
|
477
498
|
if (posture === "sanitize") {
|
|
478
|
-
// Strip
|
|
479
|
-
//
|
|
480
|
-
t
|
|
499
|
+
// Strip EVERY injection marker — operators wanting structural
|
|
500
|
+
// redaction wire their own classifier. Global fixpoint redact so a
|
|
501
|
+
// second marker after the first isn't left behind.
|
|
502
|
+
t = _redactAll(t, INJECTION_RE_G);
|
|
481
503
|
}
|
|
482
504
|
}
|
|
483
505
|
if (DANGEROUS_HTML_RE.test(regexInput)) { // allow:regex-no-length-cap regexInput byteLength bounded above
|
|
484
506
|
issues.push({ kind: "dangerous-html", index: i });
|
|
485
|
-
if (posture === "sanitize") t = t
|
|
507
|
+
if (posture === "sanitize") t = _redactAll(t, DANGEROUS_HTML_RE_G);
|
|
486
508
|
}
|
|
487
509
|
cleaned.push({ type: "text", text: t });
|
|
488
510
|
} else if (block.type === "image" || block.type === "resource_link" || block.type === "audio") {
|
|
@@ -922,7 +944,7 @@ function _elicitationGuard(opts) {
|
|
|
922
944
|
}
|
|
923
945
|
if (posture === "sanitize") {
|
|
924
946
|
return Object.assign({}, elicitRequest, {
|
|
925
|
-
message: message
|
|
947
|
+
message: _redactAll(message, INJECTION_RE_G),
|
|
926
948
|
});
|
|
927
949
|
}
|
|
928
950
|
}
|
|
@@ -375,6 +375,19 @@ function _detectSmuggling(req) {
|
|
|
375
375
|
return null;
|
|
376
376
|
}
|
|
377
377
|
|
|
378
|
+
// Generic, operator-safe status phrase for a client error body — used when the
|
|
379
|
+
// thrown error is NOT a framework-classified BodyParserError (or is a 5xx), so
|
|
380
|
+
// an internal exception's message (fs errno + tmp path, a parse hook's thrown
|
|
381
|
+
// detail) is never echoed to the client (CWE-209 / CodeQL js/stack-trace-exposure).
|
|
382
|
+
var _GENERIC_REASON = {};
|
|
383
|
+
_GENERIC_REASON[HTTP_STATUS.BAD_REQUEST] = "Bad Request";
|
|
384
|
+
_GENERIC_REASON[HTTP_STATUS.PAYLOAD_TOO_LARGE] = "Payload Too Large";
|
|
385
|
+
_GENERIC_REASON[HTTP_STATUS.UNSUPPORTED_MEDIA_TYPE] = "Unsupported Media Type";
|
|
386
|
+
_GENERIC_REASON[HTTP_STATUS.INTERNAL_SERVER_ERROR] = "Internal Server Error";
|
|
387
|
+
function _genericReason(status) {
|
|
388
|
+
return _GENERIC_REASON[status] || (status >= 500 ? "Internal Server Error" : "Bad Request");
|
|
389
|
+
}
|
|
390
|
+
|
|
378
391
|
function _writeError(res, status, message, code) {
|
|
379
392
|
if (res.headersSent) return;
|
|
380
393
|
var body = JSON.stringify({ error: message, code: code });
|
|
@@ -469,10 +482,13 @@ async function _parseJson(req, opts) {
|
|
|
469
482
|
}
|
|
470
483
|
if (typeof opts.parseHook === "function") {
|
|
471
484
|
try { parsed = opts.parseHook(parsed); }
|
|
472
|
-
catch (
|
|
485
|
+
catch (_e) {
|
|
473
486
|
throw new BodyParserError(
|
|
474
487
|
"body-parser/json-hook",
|
|
475
|
-
|
|
488
|
+
// Operator parseHook threw — surface a fixed, safe message; the hook's
|
|
489
|
+
// own thrown detail (which may carry secrets) is the operator's to log,
|
|
490
|
+
// never echoed to the client (CWE-209).
|
|
491
|
+
"request body rejected by parse hook",
|
|
476
492
|
true, HTTP_STATUS.BAD_REQUEST
|
|
477
493
|
);
|
|
478
494
|
}
|
|
@@ -1476,8 +1492,32 @@ function create(opts) {
|
|
|
1476
1492
|
}
|
|
1477
1493
|
var status = (e && typeof e.statusCode === "number") ? e.statusCode : HTTP_STATUS.BAD_REQUEST;
|
|
1478
1494
|
var code = (e && typeof e.code === "string") ? e.code : "body-parser/error";
|
|
1479
|
-
|
|
1480
|
-
|
|
1495
|
+
// Only a framework-classified 4xx BodyParserError carries a curated,
|
|
1496
|
+
// operator-safe message (malformed JSON, poisoned key, smuggling). Any
|
|
1497
|
+
// other thrown error — and every 5xx — gets a generic status phrase, so
|
|
1498
|
+
// an internal exception (fs errno + tmp path, a parse hook's thrown
|
|
1499
|
+
// secret) is never echoed to the client (CWE-209). The full diagnostic
|
|
1500
|
+
// stays on the audit chain, redacted by safeEmit.
|
|
1501
|
+
// Object(e) tolerates a non-object throw (throw null, or a string from
|
|
1502
|
+
// an operator parse hook) without a truthiness guard on the caught
|
|
1503
|
+
// binding — the caught value is always defined to CodeQL, so `e && …`
|
|
1504
|
+
// reads as a dead sub-condition; this keeps the null-safety explicit.
|
|
1505
|
+
var eo = Object(e);
|
|
1506
|
+
var clientMessage = (eo.isBodyParserError === true && status < 500 && typeof eo.message === "string")
|
|
1507
|
+
? eo.message
|
|
1508
|
+
: _genericReason(status);
|
|
1509
|
+
try {
|
|
1510
|
+
audit().safeEmit({
|
|
1511
|
+
action: "body-parser.error",
|
|
1512
|
+
outcome: status >= 500 ? "failure" : "denied",
|
|
1513
|
+
metadata: {
|
|
1514
|
+
status: status,
|
|
1515
|
+
code: code,
|
|
1516
|
+
message: (e && e.message) ? String(e.message).slice(0, 256) : "",
|
|
1517
|
+
},
|
|
1518
|
+
});
|
|
1519
|
+
} catch (_e) { /* audit best-effort — never mask the response */ }
|
|
1520
|
+
_writeError(res, status, clientMessage, code);
|
|
1481
1521
|
}
|
|
1482
1522
|
};
|
|
1483
1523
|
}
|
|
@@ -1501,9 +1541,11 @@ async function _parseJsonFromBuf(buf, opts) {
|
|
|
1501
1541
|
}
|
|
1502
1542
|
if (typeof opts.parseHook === "function") {
|
|
1503
1543
|
try { parsed = opts.parseHook(parsed); }
|
|
1504
|
-
catch (
|
|
1544
|
+
catch (_e) {
|
|
1545
|
+
// Operator parseHook threw — fixed safe message; the hook's thrown
|
|
1546
|
+
// detail (possible secrets) is never echoed to the client (CWE-209).
|
|
1505
1547
|
throw new BodyParserError("body-parser/json-hook",
|
|
1506
|
-
"
|
|
1548
|
+
"request body rejected by parse hook", true, HTTP_STATUS.BAD_REQUEST);
|
|
1507
1549
|
}
|
|
1508
1550
|
}
|
|
1509
1551
|
return parsed;
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
var defineClass = require("../framework-error").defineClass;
|
|
30
30
|
var lazyRequire = require("../lazy-require");
|
|
31
31
|
var validateOpts = require("../validate-opts");
|
|
32
|
+
var safeBuffer = require("../safe-buffer");
|
|
32
33
|
|
|
33
34
|
var audit = lazyRequire(function () { return require("../audit"); });
|
|
34
35
|
|
|
@@ -140,11 +141,12 @@ function create(opts) {
|
|
|
140
141
|
if (typeof ct !== "string" || ct.indexOf("text/html") === -1) return chunk;
|
|
141
142
|
var body = Buffer.isBuffer(chunk) ? chunk.toString("utf8") :
|
|
142
143
|
(typeof chunk === "string" ? chunk : "");
|
|
143
|
-
// Inject after <body> opening tag if present, else
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
144
|
+
// Inject after the <body> opening tag if present, else prepend.
|
|
145
|
+
// Linear tag-find — NOT body.match(/<body[^>]*>/i), which is O(n^2)
|
|
146
|
+
// in V8 on a body carrying many `<body` starts with no closing `>`
|
|
147
|
+
// (rendered user content can produce exactly that).
|
|
148
|
+
var idx = safeBuffer.indexAfterOpenTag(body, "body");
|
|
149
|
+
if (idx !== -1) {
|
|
148
150
|
body = body.slice(0, idx) + "\n" + bannerHtml + "\n" + body.slice(idx);
|
|
149
151
|
} else {
|
|
150
152
|
body = bannerHtml + "\n" + body;
|
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
*/
|
|
66
66
|
|
|
67
67
|
var validateOpts = require("../validate-opts");
|
|
68
|
+
var safeBuffer = require("../safe-buffer");
|
|
68
69
|
|
|
69
70
|
// Per W3C draft + Chromium implementation. `immediate` triggers the
|
|
70
71
|
// speculation as soon as the rules are seen; `conservative` waits
|
|
@@ -287,10 +288,11 @@ function create(opts) {
|
|
|
287
288
|
if (headClose !== -1) {
|
|
288
289
|
body = body.slice(0, headClose) + tag + body.slice(headClose);
|
|
289
290
|
} else {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
291
|
+
// Linear tag-find — NOT body.match(/<body[^>]*>/i) (O(n^2) in V8
|
|
292
|
+
// on a body with many `<body` starts and no closing `>`).
|
|
293
|
+
var bodyIdx = safeBuffer.indexAfterOpenTag(body, "body");
|
|
294
|
+
if (bodyIdx !== -1) {
|
|
295
|
+
body = body.slice(0, bodyIdx) + tag + body.slice(bodyIdx);
|
|
294
296
|
} else {
|
|
295
297
|
// No <head> or <body> — prepend so the rules at least
|
|
296
298
|
// reach the parser. This matches the bot-disclose fallback.
|
package/lib/network-dns.js
CHANGED
|
@@ -149,7 +149,7 @@ function setServers(serverList) {
|
|
|
149
149
|
throw new DnsError("dns/setservers-failed", "dns.setServers failed: " + e.message);
|
|
150
150
|
}
|
|
151
151
|
_clearCache();
|
|
152
|
-
|
|
152
|
+
observability().safeEvent("network.dns.servers.set", 1, { count: serverList.length });
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
function getServers() {
|
|
@@ -169,7 +169,7 @@ function setResultOrder(order) {
|
|
|
169
169
|
try { dns.setDefaultResultOrder(order); } catch (_e) { /* node may not support setter on this version — best-effort */ }
|
|
170
170
|
}
|
|
171
171
|
_clearCache();
|
|
172
|
-
|
|
172
|
+
observability().safeEvent("network.dns.result_order.set", 1, { order: order });
|
|
173
173
|
}
|
|
174
174
|
|
|
175
175
|
function setFamily(fam) {
|
|
@@ -215,7 +215,7 @@ function useSystemResolver() {
|
|
|
215
215
|
STATE.systemResolver = true;
|
|
216
216
|
_resetDotPool();
|
|
217
217
|
_clearCache();
|
|
218
|
-
|
|
218
|
+
observability().safeEvent("network.dns.system_resolver.set", 1, {});
|
|
219
219
|
}
|
|
220
220
|
|
|
221
221
|
function useDnsOverHttps(opts) {
|
|
@@ -246,7 +246,7 @@ function useDnsOverHttps(opts) {
|
|
|
246
246
|
}
|
|
247
247
|
STATE.doh = { url: url, method: method, ca: opts.ca || null };
|
|
248
248
|
_clearCache();
|
|
249
|
-
|
|
249
|
+
observability().safeEvent("network.dns.doh.set", 1, { url: url, method: method || "auto" });
|
|
250
250
|
}
|
|
251
251
|
|
|
252
252
|
function useDnsOverTls(opts) {
|
|
@@ -267,7 +267,7 @@ function useDnsOverTls(opts) {
|
|
|
267
267
|
};
|
|
268
268
|
_resetDotPool();
|
|
269
269
|
_clearCache();
|
|
270
|
-
|
|
270
|
+
observability().safeEvent("network.dns.dot.set", 1, { host: STATE.dot.host, port: STATE.dot.port });
|
|
271
271
|
}
|
|
272
272
|
|
|
273
273
|
function _withTimeout(promise, ms, host) {
|
|
@@ -1178,13 +1178,13 @@ async function _querySvcbLike(host, qtype, opts) {
|
|
|
1178
1178
|
throw new DnsError("dns/bad-transport",
|
|
1179
1179
|
"dns.querySvcb: transport must be 'doh' | 'dot' | 'system' | undefined");
|
|
1180
1180
|
}
|
|
1181
|
-
|
|
1181
|
+
observability().safeEvent("network.dns.svcb.requested", 1, { qtype: qtype, transport: opts.transport || "auto" });
|
|
1182
1182
|
var startMs = _now();
|
|
1183
1183
|
var reply;
|
|
1184
1184
|
try {
|
|
1185
1185
|
reply = await _rawQuery(host, qtype, opts.transport);
|
|
1186
1186
|
} catch (e) {
|
|
1187
|
-
|
|
1187
|
+
observability().safeEvent("network.dns.svcb.failure", 1, {
|
|
1188
1188
|
latencyMs: _now() - startMs,
|
|
1189
1189
|
code: e.code || "unknown",
|
|
1190
1190
|
});
|
|
@@ -1198,7 +1198,7 @@ async function _querySvcbLike(host, qtype, opts) {
|
|
|
1198
1198
|
records.push(_parseSvcbRdata(decoded.msg, ans.rdataOff, ans.rdlen));
|
|
1199
1199
|
}
|
|
1200
1200
|
records.sort(function (a, b) { return a.priority - b.priority; });
|
|
1201
|
-
|
|
1201
|
+
observability().safeEvent("network.dns.svcb.success", 1, {
|
|
1202
1202
|
latencyMs: _now() - startMs,
|
|
1203
1203
|
count: records.length,
|
|
1204
1204
|
qtype: qtype,
|
|
@@ -1322,7 +1322,7 @@ async function discoverEncrypted(opts) {
|
|
|
1322
1322
|
try {
|
|
1323
1323
|
records = await _querySvcbLike(name, DNS_QTYPE_SVCB, { transport: transport });
|
|
1324
1324
|
} catch (e) {
|
|
1325
|
-
|
|
1325
|
+
observability().safeEvent("network.dns.ddr.failure", 1, {
|
|
1326
1326
|
latencyMs: _now() - startMs,
|
|
1327
1327
|
code: e.code || "unknown",
|
|
1328
1328
|
});
|
|
@@ -1333,7 +1333,7 @@ async function discoverEncrypted(opts) {
|
|
|
1333
1333
|
throw e;
|
|
1334
1334
|
}
|
|
1335
1335
|
if (records.length === 0) {
|
|
1336
|
-
|
|
1336
|
+
observability().safeEvent("network.dns.ddr.empty", 1, { latencyMs: _now() - startMs });
|
|
1337
1337
|
throw new DnsError("dns/ddr-not-discovered",
|
|
1338
1338
|
"dns.discoverEncrypted: resolver returned empty DDR record set at " + name);
|
|
1339
1339
|
}
|
|
@@ -1364,7 +1364,7 @@ async function discoverEncrypted(opts) {
|
|
|
1364
1364
|
throw new DnsError("dns/ddr-not-discovered",
|
|
1365
1365
|
"dns.discoverEncrypted: DDR records present but none advertised a recognized transport (alpn=dot/h2/h3)");
|
|
1366
1366
|
}
|
|
1367
|
-
|
|
1367
|
+
observability().safeEvent("network.dns.ddr.success", 1, {
|
|
1368
1368
|
latencyMs: _now() - startMs,
|
|
1369
1369
|
count: resolvers.length,
|
|
1370
1370
|
});
|
|
@@ -1454,7 +1454,7 @@ function useDesignatedResolvers(list) {
|
|
|
1454
1454
|
});
|
|
1455
1455
|
}
|
|
1456
1456
|
_designatedResolvers = validated.slice();
|
|
1457
|
-
|
|
1457
|
+
observability().safeEvent("network.dns.dnr.set", 1, {
|
|
1458
1458
|
count: validated.length,
|
|
1459
1459
|
active: j,
|
|
1460
1460
|
transport: v.transport,
|
|
@@ -1462,7 +1462,7 @@ function useDesignatedResolvers(list) {
|
|
|
1462
1462
|
return { active: j, count: validated.length };
|
|
1463
1463
|
} catch (e) {
|
|
1464
1464
|
lastErr = e;
|
|
1465
|
-
|
|
1465
|
+
observability().safeEvent("network.dns.dnr.entry_failed", 1, {
|
|
1466
1466
|
index: j,
|
|
1467
1467
|
transport: v.transport,
|
|
1468
1468
|
code: e.code || "unknown",
|
|
@@ -1511,7 +1511,7 @@ async function lookup(host, opts) {
|
|
|
1511
1511
|
if (cached.error) throw cached.error;
|
|
1512
1512
|
return opts.all ? cached.value : cached.value[0];
|
|
1513
1513
|
}
|
|
1514
|
-
|
|
1514
|
+
observability().safeEvent("network.dns.lookup.requested", 1, { family: cacheKey });
|
|
1515
1515
|
var startMs = _now();
|
|
1516
1516
|
// Resolve secure-DNS default on first use. Idempotent.
|
|
1517
1517
|
_ensureSecureDefault();
|
|
@@ -1546,11 +1546,11 @@ async function lookup(host, opts) {
|
|
|
1546
1546
|
throw new DnsError("dns/no-result", "dns lookup of '" + host + "' returned no addresses");
|
|
1547
1547
|
}
|
|
1548
1548
|
_cachePutPositive(host, cacheKey, normalized);
|
|
1549
|
-
|
|
1549
|
+
observability().safeEvent("network.dns.lookup.success", 1, { latencyMs: _now() - startMs, count: normalized.length });
|
|
1550
1550
|
return opts.all ? normalized : normalized[0];
|
|
1551
1551
|
} catch (e) {
|
|
1552
1552
|
_cachePutNegative(host, cacheKey, e);
|
|
1553
|
-
|
|
1553
|
+
observability().safeEvent("network.dns.lookup.failure", 1, { latencyMs: _now() - startMs, code: e.code || "unknown" });
|
|
1554
1554
|
throw e;
|
|
1555
1555
|
}
|
|
1556
1556
|
}
|
|
@@ -1571,7 +1571,7 @@ async function _resolveProtocol(host, family, opts) {
|
|
|
1571
1571
|
throw new DnsError("dns/bad-transport",
|
|
1572
1572
|
"dns.resolve" + family + ": transport must be 'doh' | 'dot' | 'system' | undefined");
|
|
1573
1573
|
}
|
|
1574
|
-
|
|
1574
|
+
observability().safeEvent("network.dns.resolve.requested", 1, { family: family, transport: opts.transport || "auto" });
|
|
1575
1575
|
var startMs = _now();
|
|
1576
1576
|
try {
|
|
1577
1577
|
var addrs;
|
|
@@ -1597,10 +1597,10 @@ async function _resolveProtocol(host, family, opts) {
|
|
|
1597
1597
|
if (normalized.length === 0) {
|
|
1598
1598
|
throw new DnsError("dns/no-result", "dns.resolve" + family + " of '" + host + "' returned no addresses");
|
|
1599
1599
|
}
|
|
1600
|
-
|
|
1600
|
+
observability().safeEvent("network.dns.resolve.success", 1, { family: family, latencyMs: _now() - startMs, count: normalized.length });
|
|
1601
1601
|
return normalized;
|
|
1602
1602
|
} catch (e) {
|
|
1603
|
-
|
|
1603
|
+
observability().safeEvent("network.dns.resolve.failure", 1, { family: family, latencyMs: _now() - startMs, code: e.code || "unknown" });
|
|
1604
1604
|
if (e instanceof DnsError) throw e;
|
|
1605
1605
|
throw new DnsError("dns/resolve-failed",
|
|
1606
1606
|
"dns.resolve" + family + " of '" + host + "' failed: " + (e.message || String(e)));
|
|
@@ -1643,16 +1643,16 @@ async function reverse(ip) {
|
|
|
1643
1643
|
throw new DnsError("dns/bad-ip",
|
|
1644
1644
|
"dns.reverse: '" + ip + "' is not a valid IPv4 or IPv6 address");
|
|
1645
1645
|
}
|
|
1646
|
-
|
|
1646
|
+
observability().safeEvent("network.dns.reverse.requested", 1, { family: net.isIPv6(ip) ? 6 : 4 });
|
|
1647
1647
|
var startMs = _now();
|
|
1648
1648
|
try {
|
|
1649
1649
|
var ptrs = await _withTimeout(dnsPromises.reverse(ip), STATE.lookupTimeoutMs, ip);
|
|
1650
|
-
|
|
1650
|
+
observability().safeEvent("network.dns.reverse.success", 1, {
|
|
1651
1651
|
latencyMs: _now() - startMs, count: Array.isArray(ptrs) ? ptrs.length : 0,
|
|
1652
1652
|
});
|
|
1653
1653
|
return Array.isArray(ptrs) ? ptrs : [];
|
|
1654
1654
|
} catch (e) {
|
|
1655
|
-
|
|
1655
|
+
observability().safeEvent("network.dns.reverse.failure", 1, {
|
|
1656
1656
|
latencyMs: _now() - startMs, code: e.code || "unknown",
|
|
1657
1657
|
});
|
|
1658
1658
|
if (e instanceof DnsError) throw e;
|
|
@@ -1674,10 +1674,6 @@ function nodeLookup(host, options, callback) {
|
|
|
1674
1674
|
);
|
|
1675
1675
|
}
|
|
1676
1676
|
|
|
1677
|
-
function _emitObs(name, fields) {
|
|
1678
|
-
try { observability().emit(name, fields || {}); } catch (_e) { /* obs best-effort */ }
|
|
1679
|
-
}
|
|
1680
|
-
|
|
1681
1677
|
function _stateForTest() { return STATE; }
|
|
1682
1678
|
function _resetForTest() {
|
|
1683
1679
|
STATE.servers = null; STATE.resultOrder = null; STATE.family = 0;
|
package/lib/network-heartbeat.js
CHANGED
|
@@ -259,7 +259,7 @@ function statuses() {
|
|
|
259
259
|
|
|
260
260
|
function _emitObsProbe(entry, result) {
|
|
261
261
|
try {
|
|
262
|
-
observability().
|
|
262
|
+
observability().safeEvent("network.heartbeat.probe", 1, {
|
|
263
263
|
name: entry.target.name,
|
|
264
264
|
type: entry.target.type,
|
|
265
265
|
ok: result.ok,
|
|
@@ -381,7 +381,7 @@ function passive(opts) {
|
|
|
381
381
|
|
|
382
382
|
function _emitObsPong(state) {
|
|
383
383
|
try {
|
|
384
|
-
observability().
|
|
384
|
+
observability().safeEvent("network.heartbeat.passive.pong", 1, {
|
|
385
385
|
pongCount: state.pongCount,
|
|
386
386
|
timeoutMs: state.timeoutMs,
|
|
387
387
|
});
|
|
@@ -390,7 +390,7 @@ function _emitObsPong(state) {
|
|
|
390
390
|
|
|
391
391
|
function _emitObsTimeout(state) {
|
|
392
392
|
try {
|
|
393
|
-
observability().
|
|
393
|
+
observability().safeEvent("network.heartbeat.passive.timeout", 1, {
|
|
394
394
|
pongCount: state.pongCount,
|
|
395
395
|
lastPongMs: state.lastPongMs,
|
|
396
396
|
timeoutMs: state.timeoutMs,
|
package/lib/network-proxy.js
CHANGED
|
@@ -70,7 +70,7 @@ function set(opts) {
|
|
|
70
70
|
if (opts.no !== undefined) STATE.noProxy = _parseNoProxy(opts.no);
|
|
71
71
|
if (opts.auth !== undefined) STATE.auth = opts.auth ? _parseAuth(opts.auth) : null;
|
|
72
72
|
STATE.agentCache.clear();
|
|
73
|
-
|
|
73
|
+
observability().safeEvent("network.proxy.set", 1, {
|
|
74
74
|
httpSet: !!STATE.http,
|
|
75
75
|
httpsSet: !!STATE.https,
|
|
76
76
|
noProxyCount: STATE.noProxy.length,
|
|
@@ -93,7 +93,7 @@ function fromEnv(envObj) {
|
|
|
93
93
|
if (authEnv) { STATE.auth = _parseAuth(authEnv); changed = true; }
|
|
94
94
|
if (changed) {
|
|
95
95
|
STATE.agentCache.clear();
|
|
96
|
-
|
|
96
|
+
observability().safeEvent("network.proxy.from_env", 1, {
|
|
97
97
|
httpSet: !!STATE.http,
|
|
98
98
|
httpsSet: !!STATE.https,
|
|
99
99
|
noProxyCount: STATE.noProxy.length,
|
|
@@ -255,7 +255,7 @@ function agentFor(targetUrl) {
|
|
|
255
255
|
};
|
|
256
256
|
}
|
|
257
257
|
STATE.agentCache.set(key, agent);
|
|
258
|
-
|
|
258
|
+
observability().safeEvent("network.proxy.agent.created", 1, { protocol: u.protocol });
|
|
259
259
|
return agent;
|
|
260
260
|
}
|
|
261
261
|
|
|
@@ -268,10 +268,6 @@ function snapshot() {
|
|
|
268
268
|
};
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
-
function _emitObs(name, fields) {
|
|
272
|
-
try { observability().emit(name, fields || {}); } catch (_e) { /* obs best-effort */ }
|
|
273
|
-
}
|
|
274
|
-
|
|
275
271
|
function _resetForTest() {
|
|
276
272
|
STATE.http = null; STATE.https = null; STATE.noProxy = []; STATE.auth = null;
|
|
277
273
|
STATE.agentCache.clear();
|
package/lib/network-tls.js
CHANGED
|
@@ -23,6 +23,29 @@ var networkDns = lazyRequire(function () { return require("./network-dns"); });
|
|
|
23
23
|
var httpClient = lazyRequire(function () { return require("./http-client"); });
|
|
24
24
|
var asn1 = require("./asn1-der");
|
|
25
25
|
|
|
26
|
+
// Audit + observability emit for an outbound TLS connection that runs with
|
|
27
|
+
// peer-certificate validation DISABLED (an explicit operator opt-in —
|
|
28
|
+
// rejectUnauthorized:false / allowInsecure — never a framework default).
|
|
29
|
+
// Emitted at the point the disable is HONORED so the degraded posture is
|
|
30
|
+
// observable (compliance evidence + incident response), parallel to the
|
|
31
|
+
// tls.classical_downgrade audit. Drop-silent best-effort (§8 hot-path sink) —
|
|
32
|
+
// an audit-sink failure must never break the TLS connect itself.
|
|
33
|
+
function auditInsecureTls(meta) {
|
|
34
|
+
meta = meta || {};
|
|
35
|
+
try {
|
|
36
|
+
observability().safeEvent("tls.insecure_skip_verify", 1, {
|
|
37
|
+
host: meta.host || null, port: meta.port || null, source: meta.source || null,
|
|
38
|
+
});
|
|
39
|
+
} catch (_e) { /* drop-silent */ }
|
|
40
|
+
try {
|
|
41
|
+
audit().safeEmit({
|
|
42
|
+
action: "tls.insecure_skip_verify",
|
|
43
|
+
outcome: "success",
|
|
44
|
+
metadata: { host: meta.host || null, port: meta.port || null, source: meta.source || null },
|
|
45
|
+
});
|
|
46
|
+
} catch (_e) { /* drop-silent — audit best-effort, never break TLS */ }
|
|
47
|
+
}
|
|
48
|
+
|
|
26
49
|
// STATE.tlsKeyShares is initialized to the default PQC group list at
|
|
27
50
|
// module load — operator setKeyShares() overrides; resetKeyShares()
|
|
28
51
|
// restores the default. Empty array means "fall back to Node's TLS
|
|
@@ -144,7 +167,7 @@ function addCa(pemOrPath, opts) {
|
|
|
144
167
|
added.push(meta);
|
|
145
168
|
}
|
|
146
169
|
_emitAuditAdd(added, opts);
|
|
147
|
-
|
|
170
|
+
observability().safeEvent("network.tls.ca.added", 1, { count: added.length });
|
|
148
171
|
return added;
|
|
149
172
|
}
|
|
150
173
|
|
|
@@ -154,7 +177,7 @@ function addCaBundle(p, opts) {
|
|
|
154
177
|
|
|
155
178
|
function useSystemTrust(enable) {
|
|
156
179
|
STATE.systemTrust = enable !== false;
|
|
157
|
-
|
|
180
|
+
observability().safeEvent("network.tls.system_trust.set", 1, { enabled: STATE.systemTrust });
|
|
158
181
|
}
|
|
159
182
|
|
|
160
183
|
function isSystemTrustEnabled() { return !!STATE.systemTrust; }
|
|
@@ -216,7 +239,7 @@ function removeCa(fingerprint256, opts) {
|
|
|
216
239
|
});
|
|
217
240
|
if (removed.length === 0) return 0;
|
|
218
241
|
if (!opts || opts.audit !== false) _emitAuditRemove(removed, "operator-remove");
|
|
219
|
-
|
|
242
|
+
observability().safeEvent("network.tls.ca.removed", 1, { count: removed.length, reason: "operator" });
|
|
220
243
|
return removed.length;
|
|
221
244
|
}
|
|
222
245
|
|
|
@@ -234,7 +257,7 @@ function removeCaByLabel(label, opts) {
|
|
|
234
257
|
});
|
|
235
258
|
if (removed.length === 0) return 0;
|
|
236
259
|
if (!opts || opts.audit !== false) _emitAuditRemove(removed, "operator-remove-by-label");
|
|
237
|
-
|
|
260
|
+
observability().safeEvent("network.tls.ca.removed", 1, { count: removed.length, reason: "label" });
|
|
238
261
|
return removed.length;
|
|
239
262
|
}
|
|
240
263
|
|
|
@@ -243,7 +266,7 @@ function clearAll(opts) {
|
|
|
243
266
|
var removed = STATE.cas.map(function (e) { return Object.assign({ label: e.label }, e.meta); });
|
|
244
267
|
STATE.cas = [];
|
|
245
268
|
if (!opts || opts.audit !== false) _emitAuditRemove(removed, "operator-clear-all");
|
|
246
|
-
|
|
269
|
+
observability().safeEvent("network.tls.ca.cleared", 1, { count: removed.length });
|
|
247
270
|
return removed.length;
|
|
248
271
|
}
|
|
249
272
|
|
|
@@ -260,7 +283,7 @@ function purgeExpired(opts) {
|
|
|
260
283
|
});
|
|
261
284
|
if (removed.length === 0) return 0;
|
|
262
285
|
if (!opts || opts.audit !== false) _emitAuditRemove(removed, "expired");
|
|
263
|
-
|
|
286
|
+
observability().safeEvent("network.tls.ca.purged_expired", 1, { count: removed.length });
|
|
264
287
|
return removed.length;
|
|
265
288
|
}
|
|
266
289
|
|
|
@@ -705,10 +728,6 @@ function _emitAuditAdd(metaList, opts) {
|
|
|
705
728
|
}
|
|
706
729
|
}
|
|
707
730
|
|
|
708
|
-
function _emitObs(name, fields) {
|
|
709
|
-
try { observability().emit(name, fields || {}); } catch (_e) { /* obs best-effort */ }
|
|
710
|
-
}
|
|
711
|
-
|
|
712
731
|
function _resetForTest() {
|
|
713
732
|
STATE.cas = [];
|
|
714
733
|
STATE.systemTrust = false;
|
|
@@ -2725,6 +2744,7 @@ function connectWithEch(opts) {
|
|
|
2725
2744
|
}
|
|
2726
2745
|
if (opts.rejectUnauthorized === false) {
|
|
2727
2746
|
connectOpts.rejectUnauthorized = false;
|
|
2747
|
+
auditInsecureTls({ host: opts.host, port: port, source: "network.tls.connectWithEch" });
|
|
2728
2748
|
}
|
|
2729
2749
|
var echAttached = false;
|
|
2730
2750
|
if (echConfigBuf && nodeSupportsEch) {
|
|
@@ -2735,7 +2755,7 @@ function connectWithEch(opts) {
|
|
|
2735
2755
|
// gracefully with a one-shot warn so operators know they're
|
|
2736
2756
|
// sending an outer-only ClientHello.
|
|
2737
2757
|
try {
|
|
2738
|
-
observability().
|
|
2758
|
+
observability().safeEvent("network.tls.ech.unsupported", 1, {
|
|
2739
2759
|
host: opts.host, source: sourceLabel,
|
|
2740
2760
|
});
|
|
2741
2761
|
} catch (_e) { /* drop-silent */ }
|
|
@@ -2772,7 +2792,7 @@ function connectWithEch(opts) {
|
|
|
2772
2792
|
settled = true;
|
|
2773
2793
|
if (to) clearTimeout(to);
|
|
2774
2794
|
try {
|
|
2775
|
-
observability().
|
|
2795
|
+
observability().safeEvent("network.tls.ech.connected", 1, {
|
|
2776
2796
|
host: opts.host, echAttached: echAttached, source: sourceLabel,
|
|
2777
2797
|
});
|
|
2778
2798
|
} catch (_e) { /* drop-silent */ }
|
|
@@ -2832,7 +2852,7 @@ function connectWithEch(opts) {
|
|
|
2832
2852
|
// operator still gets a working TLS session. Emit obs so the
|
|
2833
2853
|
// operator sees the degradation.
|
|
2834
2854
|
try {
|
|
2835
|
-
observability().
|
|
2855
|
+
observability().safeEvent("network.tls.ech.dns_failed", 1, {
|
|
2836
2856
|
host: opts.host, error: (e && e.message) || String(e),
|
|
2837
2857
|
});
|
|
2838
2858
|
} catch (_e) { /* drop-silent */ }
|
|
@@ -3174,6 +3194,7 @@ function wrapSNICallback(operatorCb) {
|
|
|
3174
3194
|
}
|
|
3175
3195
|
|
|
3176
3196
|
module.exports = {
|
|
3197
|
+
auditInsecureTls: auditInsecureTls,
|
|
3177
3198
|
addCa: addCa,
|
|
3178
3199
|
addCaBundle: addCaBundle,
|
|
3179
3200
|
removeCa: removeCa,
|
package/lib/network.js
CHANGED
|
@@ -151,7 +151,7 @@ var ntpFacade = {
|
|
|
151
151
|
throw new NetworkError("ntp/bad-servers", "ntp.setServers: expected non-empty array");
|
|
152
152
|
}
|
|
153
153
|
ntpFacade._defaultServers = list.slice();
|
|
154
|
-
|
|
154
|
+
observability().safeEvent("network.ntp.servers.set", 1, { count: list.length });
|
|
155
155
|
},
|
|
156
156
|
getServers: function () {
|
|
157
157
|
return (ntpFacade._defaultServers || ntpCheck.DEFAULT_SERVERS).slice();
|
|
@@ -262,7 +262,7 @@ function bootFromEnv(opts) {
|
|
|
262
262
|
} catch (_e) { /* audit best-effort — never break boot */ }
|
|
263
263
|
}
|
|
264
264
|
}
|
|
265
|
-
|
|
265
|
+
observability().safeEvent("network.boot.from_env", 1, { source: "env" });
|
|
266
266
|
return applied;
|
|
267
267
|
}
|
|
268
268
|
|
|
@@ -301,10 +301,6 @@ function snapshot() {
|
|
|
301
301
|
};
|
|
302
302
|
}
|
|
303
303
|
|
|
304
|
-
function _emitObs(name, fields) {
|
|
305
|
-
try { observability().emit(name, fields || {}); } catch (_e) { /* obs best-effort */ }
|
|
306
|
-
}
|
|
307
|
-
|
|
308
304
|
function _resetForTest() {
|
|
309
305
|
ntpFacade._defaultServers = null;
|
|
310
306
|
ntpFacade._defaultTimeoutMs = null;
|