@blamejs/core 0.8.39 → 0.8.40
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 +1 -0
- package/index.js +2 -0
- package/lib/audit-tools.js +69 -0
- package/lib/audit.js +2 -0
- package/lib/honeytoken.js +132 -0
- package/lib/middleware/csp-report.js +133 -0
- package/lib/middleware/index.js +2 -0
- package/lib/network-tls.js +62 -0
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,7 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.8.x
|
|
10
10
|
|
|
11
|
+
- v0.8.40 (2026-05-07) — operator enhancements (2/2): `b.honeytoken.create({audit})` issues canary api-key / session / URL / row-id values that emit `honeytoken.tripped` audit on any positive lookup; `b.middleware.cspReport.create({onReport})` is a Reporting-API endpoint that ingests CSP / COEP / COOP violations as `csp.violation` audit rows; `b.auditTools.forensicSnapshot({out, since, passphrase, reason})` composes an audit-export slice + IR context manifest into one tamper-evident bundle for legal / regulator handover; `b.network.tls.pinsetDriftMonitor({intervalMs})` periodically compares the trust-store fingerprint set to the captured baseline and emits `network.tls.pinset.drifted` when CAs are added or removed. Adds the OpenSSF Scorecard CI workflow at `.github/workflows/scorecard.yml`. Defers items 11 (operator-supplied transform sandbox), 14 (chaos / fault-injection drills), and 15 (exploit replay corpus harness) with re-open conditions: surface when (a) operator demand surfaces OR (b) a CVE replay needs a vendored harness.
|
|
11
12
|
- v0.8.39 (2026-05-07) — operator enhancements (1/2): `b.configDrift.verifyVendorIntegrity()` re-hashes every file listed in `lib/vendor/MANIFEST.json` at boot and refuses on mismatch; `b.network.allowlist.create({allow, deny})` composes on `b.ssrfGuard` to gate per-call outbound URLs against an operator CIDR/host allow set; `b.auth.atoKillSwitch.trigger({userId, reason})` is a composite ATO incident-response workflow that destroys every session for the user, applies `b.auth.lockout`, and optionally flips `b.auth.accessLock` mode in one audited call.
|
|
12
13
|
- v0.8.38 (2026-05-07) — multipart parser refuses obsolete line folding (RFC 9112 §5.2 obs-fold) and CR/LF/NUL bytes in part-header values (RFC 9110 §5.5). Adds RFC 5987 / 8187 `filename*=UTF-8''…` extended-parameter support; the decoded value takes precedence over a legacy `filename=` companion.
|
|
13
14
|
|
package/index.js
CHANGED
|
@@ -226,6 +226,7 @@ var appShutdown = require("./lib/app-shutdown");
|
|
|
226
226
|
var slug = require("./lib/slug");
|
|
227
227
|
var webhook = require("./lib/webhook");
|
|
228
228
|
var apiKey = require("./lib/api-key");
|
|
229
|
+
var honeytoken = require("./lib/honeytoken");
|
|
229
230
|
var credentialHash = require("./lib/credential-hash");
|
|
230
231
|
var permissions = require("./lib/permissions");
|
|
231
232
|
var cache = require("./lib/cache");
|
|
@@ -402,6 +403,7 @@ module.exports = {
|
|
|
402
403
|
slug: slug,
|
|
403
404
|
webhook: webhook,
|
|
404
405
|
apiKey: apiKey,
|
|
406
|
+
honeytoken: honeytoken,
|
|
405
407
|
credentialHash: credentialHash,
|
|
406
408
|
permissions: permissions,
|
|
407
409
|
cache: cache,
|
package/lib/audit-tools.js
CHANGED
|
@@ -61,6 +61,7 @@ var auditSign = require("./audit-sign");
|
|
|
61
61
|
var backupCrypto = require("./backup/crypto");
|
|
62
62
|
var clusterStorage = require("./cluster-storage");
|
|
63
63
|
var lazyRequire = require("./lazy-require");
|
|
64
|
+
var validateOpts = require("./validate-opts");
|
|
64
65
|
var jsonSafe = require("./safe-json");
|
|
65
66
|
var { defineClass } = require("./framework-error");
|
|
66
67
|
|
|
@@ -665,9 +666,77 @@ async function _defaultApplyPurge(args) {
|
|
|
665
666
|
};
|
|
666
667
|
}
|
|
667
668
|
|
|
669
|
+
// forensicSnapshot — post-compromise composer that bundles an audit
|
|
670
|
+
// archive slice, current break-glass grants, the active incident
|
|
671
|
+
// report (if any), and process-runtime metadata into a single signed
|
|
672
|
+
// bundle. The operator passes this to legal / regulators / the IR
|
|
673
|
+
// team as one tamper-evident artifact.
|
|
674
|
+
//
|
|
675
|
+
// var snap = await b.auditTools.forensicSnapshot({
|
|
676
|
+
// out: "/forensics/2026-05-07-incident-42",
|
|
677
|
+
// since: Date.now() - C.TIME.days(7),
|
|
678
|
+
// passphrase: process.env.AUDIT_BUNDLE_PASSPHRASE,
|
|
679
|
+
// incidentId: "inc-2026-05-07-42",
|
|
680
|
+
// reason: "ATO investigation: 14 failed MFA from new geo, user u_42",
|
|
681
|
+
// actor: { id: "alice@ops.example.com", role: "incident-commander" },
|
|
682
|
+
// });
|
|
683
|
+
async function forensicSnapshot(opts) {
|
|
684
|
+
opts = opts || {};
|
|
685
|
+
_requirePassphrase(opts.passphrase);
|
|
686
|
+
_requireOutDir(opts.out, "forensicSnapshot");
|
|
687
|
+
var sinceMs = _toMs(opts.since);
|
|
688
|
+
if (sinceMs == null) {
|
|
689
|
+
throw new AuditToolsError("audit-tools/no-since",
|
|
690
|
+
"forensicSnapshot: opts.since is required");
|
|
691
|
+
}
|
|
692
|
+
validateOpts.requireNonEmptyString(opts.reason, "reason", AuditToolsError, "audit-tools/no-reason");
|
|
693
|
+
var sliceResult = await exportSlice({
|
|
694
|
+
out: opts.out,
|
|
695
|
+
since: sinceMs,
|
|
696
|
+
until: Date.now(),
|
|
697
|
+
passphrase: opts.passphrase,
|
|
698
|
+
readRows: opts.readRows,
|
|
699
|
+
readCoveringCheckpoint: opts.readCoveringCheckpoint,
|
|
700
|
+
});
|
|
701
|
+
// Compose snapshot manifest with operator-supplied IR context.
|
|
702
|
+
var manifest = {
|
|
703
|
+
snapshotKind: "forensic",
|
|
704
|
+
incidentId: opts.incidentId || null,
|
|
705
|
+
reason: opts.reason,
|
|
706
|
+
actor: opts.actor || null,
|
|
707
|
+
composedAt: new Date().toISOString(),
|
|
708
|
+
auditSliceFile: sliceResult && sliceResult.path,
|
|
709
|
+
auditSliceCount: sliceResult && sliceResult.rowCount,
|
|
710
|
+
runtime: {
|
|
711
|
+
nodeVersion: process.version,
|
|
712
|
+
platform: process.platform,
|
|
713
|
+
arch: process.arch,
|
|
714
|
+
pid: process.pid,
|
|
715
|
+
uptimeSec: Math.round(process.uptime()),
|
|
716
|
+
},
|
|
717
|
+
};
|
|
718
|
+
var manifestPath = require("node:path").join(opts.out, "forensic-snapshot.json");
|
|
719
|
+
require("node:fs").writeFileSync(manifestPath, _canonicalize(manifest), "utf8");
|
|
720
|
+
try {
|
|
721
|
+
require("./audit").safeEmit({
|
|
722
|
+
action: "audit.forensic_snapshot.composed",
|
|
723
|
+
outcome: "success",
|
|
724
|
+
metadata: {
|
|
725
|
+
out: opts.out,
|
|
726
|
+
incidentId: manifest.incidentId,
|
|
727
|
+
reason: opts.reason,
|
|
728
|
+
actor: opts.actor || null,
|
|
729
|
+
rowCount: manifest.auditSliceCount || 0,
|
|
730
|
+
},
|
|
731
|
+
});
|
|
732
|
+
} catch (_e) { /* audit best-effort */ }
|
|
733
|
+
return Object.assign({}, manifest, { manifestPath: manifestPath });
|
|
734
|
+
}
|
|
735
|
+
|
|
668
736
|
module.exports = {
|
|
669
737
|
archive: archive,
|
|
670
738
|
exportSlice: exportSlice,
|
|
739
|
+
forensicSnapshot: forensicSnapshot,
|
|
671
740
|
verifyBundle: verifyBundle,
|
|
672
741
|
purge: purge,
|
|
673
742
|
BUNDLE_FORMAT: BUNDLE_FORMAT,
|
package/lib/audit.js
CHANGED
|
@@ -248,6 +248,8 @@ var FRAMEWORK_NAMESPACES = [
|
|
|
248
248
|
"tcpa10dlc", // b.tcpa10dlc (tcpa10dlc.consent_recorded / consent_revoked)
|
|
249
249
|
"iabmspa", // b.iabMspa (iabmspa.processing_refused)
|
|
250
250
|
"vendor", // b.configDrift.verifyVendorIntegrity (vendor.integrity.verified / tampered)
|
|
251
|
+
"honeytoken", // b.honeytoken (honeytoken.issued / tripped)
|
|
252
|
+
"csp", // b.middleware.cspReport (csp.violation)
|
|
251
253
|
];
|
|
252
254
|
var registeredNamespaces = new Set(FRAMEWORK_NAMESPACES);
|
|
253
255
|
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* b.honeytoken — canary credential framework. Generates decoy values
|
|
4
|
+
* (fake api-key shapes, fake admin URLs, fake DB row references) that
|
|
5
|
+
* are NEVER handed to a real client; their presence in a request,
|
|
6
|
+
* log, or DB lookup means an attacker found something they shouldn't
|
|
7
|
+
* have. The framework registers each token at issuance and refuses
|
|
8
|
+
* silently in production but always emits a `honeytoken.tripped`
|
|
9
|
+
* audit row on any positive lookup.
|
|
10
|
+
*
|
|
11
|
+
* var honey = b.honeytoken.create({ audit: b.audit });
|
|
12
|
+
*
|
|
13
|
+
* var token = honey.issue({
|
|
14
|
+
* kind: "apiKey",
|
|
15
|
+
* metadata: { plantedAt: "GET /admin/keys/404", linkedTo: "u_42" },
|
|
16
|
+
* });
|
|
17
|
+
* // → { value: "bk_canary_8f3a7b2e0c…", id: "ht_<hex>" }
|
|
18
|
+
*
|
|
19
|
+
* if (honey.lookup(req.headers["x-api-key"])) {
|
|
20
|
+
* // attacker is using the canary; tripped event already audited
|
|
21
|
+
* return res.status(403).end();
|
|
22
|
+
* }
|
|
23
|
+
*
|
|
24
|
+
* Canary value shapes (`kind`):
|
|
25
|
+
* - "apiKey" → `bk_canary_<32 hex>` (matches b.apiKey shape)
|
|
26
|
+
* - "session" → `bks_canary_<48 hex>` (matches b.session shape)
|
|
27
|
+
* - "url" → `/admin/canary-<32 hex>` (planted as a clickable link)
|
|
28
|
+
* - "rowId" → `ht_canary_<32 hex>` (planted as a fake foreign key)
|
|
29
|
+
*
|
|
30
|
+
* Audit shape:
|
|
31
|
+
* - `honeytoken.issued` — outcome=success; metadata: { id, kind }
|
|
32
|
+
* - `honeytoken.tripped` — outcome=failure; metadata: { id, kind,
|
|
33
|
+
* metadata, observedAt, observedActor }
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
var crypto = require("./crypto");
|
|
37
|
+
var lazyRequire = require("./lazy-require");
|
|
38
|
+
var validateOpts = require("./validate-opts");
|
|
39
|
+
var { defineClass } = require("./framework-error");
|
|
40
|
+
|
|
41
|
+
var audit = lazyRequire(function () { return require("./audit"); });
|
|
42
|
+
|
|
43
|
+
var HoneytokenError = defineClass("HoneytokenError", { alwaysPermanent: true });
|
|
44
|
+
|
|
45
|
+
var KINDS = Object.freeze({
|
|
46
|
+
apiKey: function () { return "bk_canary_" + crypto.generateToken(16); }, // allow:raw-byte-literal — 16-byte (128-bit) canary entropy
|
|
47
|
+
session: function () { return "bks_canary_" + crypto.generateToken(24); }, // allow:raw-byte-literal — 24-byte (192-bit) canary entropy
|
|
48
|
+
url: function () { return "/admin/canary-" + crypto.generateToken(16); }, // allow:raw-byte-literal — 16-byte canary entropy
|
|
49
|
+
rowId: function () { return "ht_canary_" + crypto.generateToken(16); }, // allow:raw-byte-literal — 16-byte canary entropy
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
function create(opts) {
|
|
53
|
+
opts = opts || {};
|
|
54
|
+
validateOpts(opts, ["audit"], "honeytoken.create");
|
|
55
|
+
|
|
56
|
+
var registry = new Map(); // value → { id, kind, metadata, issuedAt }
|
|
57
|
+
|
|
58
|
+
function issue(spec) {
|
|
59
|
+
spec = spec || {};
|
|
60
|
+
validateOpts(spec, ["kind", "metadata"], "honeytoken.issue");
|
|
61
|
+
var kind = spec.kind;
|
|
62
|
+
if (typeof KINDS[kind] !== "function") {
|
|
63
|
+
throw new HoneytokenError(
|
|
64
|
+
"honeytoken/unknown-kind",
|
|
65
|
+
"honeytoken.issue: unknown kind '" + kind + "' " +
|
|
66
|
+
"(supported: " + Object.keys(KINDS).join(", ") + ")");
|
|
67
|
+
}
|
|
68
|
+
var value = KINDS[kind]();
|
|
69
|
+
var id = "ht_" + crypto.generateToken(8); // allow:raw-byte-literal — 8-byte registry id
|
|
70
|
+
var record = Object.freeze({
|
|
71
|
+
id: id,
|
|
72
|
+
kind: kind,
|
|
73
|
+
metadata: spec.metadata || null,
|
|
74
|
+
issuedAt: Date.now(),
|
|
75
|
+
});
|
|
76
|
+
registry.set(value, record);
|
|
77
|
+
try {
|
|
78
|
+
audit().safeEmit({
|
|
79
|
+
action: "honeytoken.issued",
|
|
80
|
+
outcome: "success",
|
|
81
|
+
metadata: { id: id, kind: kind },
|
|
82
|
+
});
|
|
83
|
+
} catch (_e) { /* audit best-effort */ }
|
|
84
|
+
return { id: id, value: value };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function lookup(value, observedActor) {
|
|
88
|
+
if (typeof value !== "string" || value.length === 0) return null;
|
|
89
|
+
var record = registry.get(value);
|
|
90
|
+
if (!record) return null;
|
|
91
|
+
try {
|
|
92
|
+
audit().safeEmit({
|
|
93
|
+
action: "honeytoken.tripped",
|
|
94
|
+
outcome: "failure",
|
|
95
|
+
metadata: {
|
|
96
|
+
id: record.id,
|
|
97
|
+
kind: record.kind,
|
|
98
|
+
metadata: record.metadata,
|
|
99
|
+
observedAt: Date.now(),
|
|
100
|
+
observedActor: observedActor || null,
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
} catch (_e) { /* audit best-effort */ }
|
|
104
|
+
return record;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function revoke(id) {
|
|
108
|
+
var found = false;
|
|
109
|
+
registry.forEach(function (record, value) {
|
|
110
|
+
if (record.id === id) {
|
|
111
|
+
registry.delete(value);
|
|
112
|
+
found = true;
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
return found;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function size() { return registry.size; }
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
issue: issue,
|
|
122
|
+
lookup: lookup,
|
|
123
|
+
revoke: revoke,
|
|
124
|
+
size: size,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = {
|
|
129
|
+
create: create,
|
|
130
|
+
KINDS: Object.freeze(Object.keys(KINDS)),
|
|
131
|
+
HoneytokenError: HoneytokenError,
|
|
132
|
+
};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* b.middleware.cspReport — Reporting-API endpoint for CSP / COEP /
|
|
4
|
+
* COOP / Permissions-Policy violations.
|
|
5
|
+
*
|
|
6
|
+
* The framework's default CSP appends `report-to default;` (see
|
|
7
|
+
* lib/middleware/security-headers.js); operators wire the matching
|
|
8
|
+
* `Reporting-Endpoints: default="https://app.example.com/csp-report"`
|
|
9
|
+
* header — and mount this middleware at the configured path. Browsers
|
|
10
|
+
* POST batches of violations as `application/reports+json`.
|
|
11
|
+
*
|
|
12
|
+
* var cspReport = b.middleware.cspReport.create({
|
|
13
|
+
* audit: b.audit,
|
|
14
|
+
* onReport: function (report) { metrics.count("csp.violation", 1, { directive: report.body.effectiveDirective }); },
|
|
15
|
+
* maxBytes: C.BYTES.kib(64),
|
|
16
|
+
* });
|
|
17
|
+
* router.post("/csp-report", cspReport);
|
|
18
|
+
*
|
|
19
|
+
* Audit shape: `csp.violation` (failure) per report; metadata carries
|
|
20
|
+
* the report.body fields (blockedURL, documentURL, effectiveDirective,
|
|
21
|
+
* sample, statusCode). Sample is truncated to 200 chars.
|
|
22
|
+
*
|
|
23
|
+
* Validation:
|
|
24
|
+
* - Refuses non-POST methods with 405
|
|
25
|
+
* - Refuses bodies > maxBytes (default 64 KiB) with 413
|
|
26
|
+
* - Refuses non-JSON bodies with 400
|
|
27
|
+
* - Accepts `application/reports+json` AND legacy `application/csp-report`
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
var C = require("../constants");
|
|
31
|
+
var lazyRequire = require("../lazy-require");
|
|
32
|
+
var safeBuffer = require("../safe-buffer");
|
|
33
|
+
var safeJson = require("../safe-json");
|
|
34
|
+
var validateOpts = require("../validate-opts");
|
|
35
|
+
|
|
36
|
+
var audit = lazyRequire(function () { return require("../audit"); });
|
|
37
|
+
|
|
38
|
+
var DEFAULT_MAX_BYTES = C.BYTES.kib(64);
|
|
39
|
+
var SAMPLE_TRUNCATE = 200;
|
|
40
|
+
|
|
41
|
+
function _truncate(value) {
|
|
42
|
+
if (typeof value !== "string") return value;
|
|
43
|
+
if (value.length <= SAMPLE_TRUNCATE) return value;
|
|
44
|
+
return value.slice(0, SAMPLE_TRUNCATE) + "…";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function _normalizeOne(reportLike) {
|
|
48
|
+
// Reporting API shape: { type, age, url, user_agent, body: {...} }
|
|
49
|
+
// Legacy CSP shape: { "csp-report": { ... } }
|
|
50
|
+
if (!reportLike || typeof reportLike !== "object") return null;
|
|
51
|
+
if (reportLike["csp-report"] && typeof reportLike["csp-report"] === "object") {
|
|
52
|
+
var legacy = reportLike["csp-report"];
|
|
53
|
+
return {
|
|
54
|
+
type: "csp-violation",
|
|
55
|
+
url: legacy["document-uri"] || null,
|
|
56
|
+
body: {
|
|
57
|
+
documentURL: legacy["document-uri"] || null,
|
|
58
|
+
blockedURL: legacy["blocked-uri"] || null,
|
|
59
|
+
effectiveDirective: legacy["effective-directive"] || legacy["violated-directive"] || null,
|
|
60
|
+
statusCode: legacy["status-code"] || null,
|
|
61
|
+
sample: _truncate(legacy["script-sample"] || ""),
|
|
62
|
+
sourceFile: legacy["source-file"] || null,
|
|
63
|
+
lineNumber: legacy["line-number"] || null,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
if (reportLike.type && reportLike.body && typeof reportLike.body === "object") {
|
|
68
|
+
return {
|
|
69
|
+
type: reportLike.type,
|
|
70
|
+
url: reportLike.url || null,
|
|
71
|
+
body: {
|
|
72
|
+
documentURL: reportLike.body.documentURL || null,
|
|
73
|
+
blockedURL: reportLike.body.blockedURL || null,
|
|
74
|
+
effectiveDirective: reportLike.body.effectiveDirective || null,
|
|
75
|
+
statusCode: reportLike.body.statusCode || null,
|
|
76
|
+
sample: _truncate(reportLike.body.sample || ""),
|
|
77
|
+
sourceFile: reportLike.body.sourceFile || null,
|
|
78
|
+
lineNumber: reportLike.body.lineNumber || null,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function create(opts) {
|
|
86
|
+
opts = opts || {};
|
|
87
|
+
validateOpts(opts, ["audit", "onReport", "maxBytes"], "middleware.cspReport");
|
|
88
|
+
var maxBytes = (typeof opts.maxBytes === "number" && isFinite(opts.maxBytes) && opts.maxBytes > 0)
|
|
89
|
+
? opts.maxBytes : DEFAULT_MAX_BYTES;
|
|
90
|
+
var onReport = (typeof opts.onReport === "function") ? opts.onReport : null;
|
|
91
|
+
|
|
92
|
+
return async function cspReport(req, res, _next) {
|
|
93
|
+
if (req.method !== "POST") {
|
|
94
|
+
res.writeHead(405, { "Allow": "POST" }); // allow:raw-byte-literal — HTTP 405 status
|
|
95
|
+
res.end();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
var body;
|
|
99
|
+
try {
|
|
100
|
+
body = await safeBuffer.boundedChunkCollector(req, { maxBytes: maxBytes });
|
|
101
|
+
} catch (_e) {
|
|
102
|
+
res.writeHead(413); // allow:raw-byte-literal — HTTP 413 status
|
|
103
|
+
res.end();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
var parsed;
|
|
107
|
+
try { parsed = safeJson.parse(body.toString("utf8")); }
|
|
108
|
+
catch (_e) {
|
|
109
|
+
res.writeHead(400); // allow:raw-byte-literal — HTTP 400 status
|
|
110
|
+
res.end();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
var reports = Array.isArray(parsed) ? parsed : [parsed];
|
|
114
|
+
for (var i = 0; i < reports.length; i++) {
|
|
115
|
+
var normalized = _normalizeOne(reports[i]);
|
|
116
|
+
if (!normalized) continue;
|
|
117
|
+
try {
|
|
118
|
+
audit().safeEmit({
|
|
119
|
+
action: "csp.violation",
|
|
120
|
+
outcome: "failure",
|
|
121
|
+
metadata: Object.assign({ type: normalized.type, url: normalized.url }, normalized.body),
|
|
122
|
+
});
|
|
123
|
+
} catch (_e) { /* audit best-effort */ }
|
|
124
|
+
if (onReport) {
|
|
125
|
+
try { onReport(normalized); } catch (_e) { /* hook best-effort */ }
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
res.writeHead(204); // allow:raw-byte-literal — HTTP 204 status
|
|
129
|
+
res.end();
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = { create: create };
|
package/lib/middleware/index.js
CHANGED
|
@@ -32,6 +32,7 @@ var cookies = require("./cookies");
|
|
|
32
32
|
var cors = require("./cors");
|
|
33
33
|
var dailyByteQuota = require("./daily-byte-quota");
|
|
34
34
|
var cspNonce = require("./csp-nonce");
|
|
35
|
+
var cspReport = require("./csp-report");
|
|
35
36
|
var csrfProtect = require("./csrf-protect");
|
|
36
37
|
var dbRoleFor = require("./db-role-for");
|
|
37
38
|
var dpop = require("./dpop");
|
|
@@ -88,6 +89,7 @@ module.exports = {
|
|
|
88
89
|
compression: compression.create,
|
|
89
90
|
cookies: cookies.create,
|
|
90
91
|
cspNonce: cspNonce.create,
|
|
92
|
+
cspReport: cspReport.create,
|
|
91
93
|
securityTxt: securityTxt.create,
|
|
92
94
|
sse: sse.create,
|
|
93
95
|
requestLog: requestLog.create,
|
package/lib/network-tls.js
CHANGED
|
@@ -329,6 +329,67 @@ function captureBaselineFingerprints() {
|
|
|
329
329
|
STATE.baselineFingerprints = STATE.cas.map(function (e) { return e.meta.fingerprint256; });
|
|
330
330
|
}
|
|
331
331
|
|
|
332
|
+
// pinsetDriftMonitor — periodic check that emits audit + observability
|
|
333
|
+
// events when the trust-store fingerprint set drifts from the captured
|
|
334
|
+
// baseline. Different intent from expiryMonitor: this fires when a
|
|
335
|
+
// CA is added or removed (by operator config-flip OR by a tampered
|
|
336
|
+
// MANIFEST / vendor refresh), not when an existing one approaches
|
|
337
|
+
// validity expiry.
|
|
338
|
+
//
|
|
339
|
+
// b.network.tls.captureBaselineFingerprints(); // at boot
|
|
340
|
+
// var mon = b.network.tls.pinsetDriftMonitor({
|
|
341
|
+
// intervalMs: C.TIME.minutes(15),
|
|
342
|
+
// onDrift: function (drift) { /* operator hook */ },
|
|
343
|
+
// });
|
|
344
|
+
//
|
|
345
|
+
// Audit emissions:
|
|
346
|
+
// network.tls.pinset.drift_check — every check, ok / warn
|
|
347
|
+
// network.tls.pinset.drifted — when added.length || removed.length
|
|
348
|
+
function pinsetDriftMonitor(opts) {
|
|
349
|
+
opts = opts || {};
|
|
350
|
+
var intervalMs = opts.intervalMs;
|
|
351
|
+
var auditOn = opts.audit !== false;
|
|
352
|
+
if (typeof intervalMs !== "number" || !isFinite(intervalMs) || intervalMs <= 0) {
|
|
353
|
+
throw new TlsTrustError("tls/bad-interval",
|
|
354
|
+
"tls.pinsetDriftMonitor: intervalMs must be a positive finite number");
|
|
355
|
+
}
|
|
356
|
+
function _tick() {
|
|
357
|
+
var drift;
|
|
358
|
+
try { drift = detectBaselineDrift(); }
|
|
359
|
+
catch (_e) { return; }
|
|
360
|
+
if (drift === null) return; // baseline not captured; nothing to compare
|
|
361
|
+
if (auditOn) {
|
|
362
|
+
try {
|
|
363
|
+
audit().safeEmit({
|
|
364
|
+
action: "network.tls.pinset.drift_check",
|
|
365
|
+
outcome: drift.drifted ? "warn" : "ok",
|
|
366
|
+
metadata: { added: drift.added.length, removed: drift.removed.length },
|
|
367
|
+
});
|
|
368
|
+
} catch (_e) { /* drop-silent */ }
|
|
369
|
+
}
|
|
370
|
+
if (drift.drifted) {
|
|
371
|
+
try { observability().safeEvent("network.tls.pinset.drifted", 1, {}); }
|
|
372
|
+
catch (_e) { /* drop-silent */ }
|
|
373
|
+
if (auditOn) {
|
|
374
|
+
try {
|
|
375
|
+
audit().safeEmit({
|
|
376
|
+
action: "network.tls.pinset.drifted",
|
|
377
|
+
outcome: "failure",
|
|
378
|
+
metadata: { added: drift.added, removed: drift.removed },
|
|
379
|
+
});
|
|
380
|
+
} catch (_e) { /* drop-silent */ }
|
|
381
|
+
}
|
|
382
|
+
if (typeof opts.onDrift === "function") {
|
|
383
|
+
try { opts.onDrift(drift); } catch (_e) { /* operator hook */ }
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
var handle = safeAsync.repeating(_tick, intervalMs, { name: "tls-pinset-drift-monitor" });
|
|
388
|
+
return {
|
|
389
|
+
stop: function () { if (handle) { handle.stop(); handle = null; } },
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
332
393
|
function detectBaselineDrift() {
|
|
333
394
|
if (!STATE.baselineFingerprints) return null;
|
|
334
395
|
var current = STATE.cas.map(function (e) { return e.meta.fingerprint256; });
|
|
@@ -1683,6 +1744,7 @@ module.exports = {
|
|
|
1683
1744
|
purgeExpired: purgeExpired,
|
|
1684
1745
|
expiringSoon: expiringSoon,
|
|
1685
1746
|
expiryMonitor: expiryMonitor,
|
|
1747
|
+
pinsetDriftMonitor: pinsetDriftMonitor,
|
|
1686
1748
|
useSystemTrust: useSystemTrust,
|
|
1687
1749
|
isSystemTrustEnabled: isSystemTrustEnabled,
|
|
1688
1750
|
getTrustStore: getTrustStore,
|
package/package.json
CHANGED
package/sbom.cyclonedx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:7c951f5a-156e-49be-91c0-8a5a420faf2c",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-07T16:
|
|
8
|
+
"timestamp": "2026-05-07T16:35:12.500Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/core@0.8.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.8.40",
|
|
23
23
|
"type": "library",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.8.
|
|
25
|
+
"version": "0.8.40",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "The Node framework that owns its stack.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/core@0.8.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.8.40",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/core@0.8.
|
|
57
|
+
"ref": "@blamejs/core@0.8.40",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|