@blamejs/core 0.11.5 → 0.11.17
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 +1038 -733
- package/index.js +2 -0
- package/lib/audit.js +14 -13
- package/lib/auth/oid4vci.js +5 -4
- package/lib/compliance.js +3 -2
- package/lib/crypto.js +3 -1
- package/lib/safe-decompress.js +2 -2
- package/lib/safe-mount-info.js +306 -0
- package/lib/subject.js +3 -3
- package/lib/watcher.js +15 -50
- package/package.json +1 -1
- package/sbom.cdx.json +9 -9
package/index.js
CHANGED
|
@@ -119,6 +119,7 @@ var safeSql = require("./lib/safe-sql");
|
|
|
119
119
|
var chainWriter = require("./lib/chain-writer");
|
|
120
120
|
var safeBuffer = require("./lib/safe-buffer");
|
|
121
121
|
var safeDecompress = require("./lib/safe-decompress").safeDecompress;
|
|
122
|
+
var safeMountInfo = require("./lib/safe-mount-info");
|
|
122
123
|
var lazyRequire = require("./lib/lazy-require");
|
|
123
124
|
var frameworkError = require("./lib/framework-error");
|
|
124
125
|
var nistCrosswalk = require("./lib/nist-crosswalk");
|
|
@@ -456,6 +457,7 @@ module.exports = {
|
|
|
456
457
|
chainWriter: chainWriter,
|
|
457
458
|
safeBuffer: safeBuffer,
|
|
458
459
|
safeDecompress: safeDecompress,
|
|
460
|
+
safeMountInfo: safeMountInfo,
|
|
459
461
|
lazyRequire: lazyRequire,
|
|
460
462
|
frameworkError: frameworkError,
|
|
461
463
|
httpClient: httpClient,
|
package/lib/audit.js
CHANGED
|
@@ -74,8 +74,8 @@ var log = boot("audit");
|
|
|
74
74
|
// audit critical path — b.audit.record() must return, emit/safeEmit
|
|
75
75
|
// drains must not stall behind it. On timeout the shadow record is
|
|
76
76
|
// dropped (audit.shadow_timeout observability event) and the
|
|
77
|
-
// framework chain row remains committed
|
|
78
|
-
//
|
|
77
|
+
// framework chain row remains committed — audit emission MUST NOT
|
|
78
|
+
// crash or stall the request that triggered it.
|
|
79
79
|
var EXTERNAL_STORE_TIMEOUT_MS = C.TIME.seconds(30);
|
|
80
80
|
|
|
81
81
|
// External shadow store registered via `b.audit.useStore({ record })`.
|
|
@@ -88,11 +88,11 @@ var EXTERNAL_STORE_TIMEOUT_MS = C.TIME.seconds(30);
|
|
|
88
88
|
// the operator's record receives the fully-formed row (logical fields +
|
|
89
89
|
// `_id` + `recordedAt` + `monotonicCounter` + `prevHash` + `rowHash`).
|
|
90
90
|
//
|
|
91
|
-
// Shadow failures are drop-silent
|
|
92
|
-
//
|
|
93
|
-
// failure surfaces via `b.observability` as `audit.shadow_failed`;
|
|
94
|
-
// framework chain row still committed and downstream
|
|
95
|
-
// works against the framework store.
|
|
91
|
+
// Shadow failures are drop-silent — hot-path observability sinks
|
|
92
|
+
// must not crash the path that emitted them. An audit-shadow
|
|
93
|
+
// failure surfaces via `b.observability` as `audit.shadow_failed`;
|
|
94
|
+
// the framework chain row still committed and downstream
|
|
95
|
+
// verifyChain still works against the framework store.
|
|
96
96
|
var _externalStore = null;
|
|
97
97
|
|
|
98
98
|
// Per-operation timeout for framework-state SQL. A misbehaving
|
|
@@ -481,8 +481,9 @@ async function record(event) {
|
|
|
481
481
|
var appended = await _chainWriter.append(logical);
|
|
482
482
|
// Operator-registered shadow store: replicate the fully-formed
|
|
483
483
|
// row to an immutable external destination. Drop-silent on
|
|
484
|
-
// failure
|
|
485
|
-
//
|
|
484
|
+
// failure — the framework chain is authoritative and already
|
|
485
|
+
// committed; the shadow is a best-effort archival, and an
|
|
486
|
+
// unreachable destination must not crash the audit caller.
|
|
486
487
|
// The operator's record receives the SAME object the framework
|
|
487
488
|
// returns to its caller, so external consumers see identical
|
|
488
489
|
// hashes / counters / ids for cross-store reconciliation.
|
|
@@ -547,10 +548,10 @@ async function record(event) {
|
|
|
547
548
|
* or `audit.shadow_timeout` (cap exceeded) with `{ action,
|
|
548
549
|
* monotonicCounter, error, timeoutMs }` metadata, and the framework
|
|
549
550
|
* chain append still succeeds (the row is durable in the framework's
|
|
550
|
-
* own table; the shadow is a best-effort archival).
|
|
551
|
-
*
|
|
552
|
-
*
|
|
553
|
-
*
|
|
551
|
+
* own table; the shadow is a best-effort archival). Hot-path
|
|
552
|
+
* observability sinks emit drop-silent — an unreachable / hanging
|
|
553
|
+
* shadow MUST NOT crash or stall the request path that triggered
|
|
554
|
+
* the audit attempt.
|
|
554
555
|
*
|
|
555
556
|
* Call this once at boot, BEFORE the first `b.audit.record` /
|
|
556
557
|
* `b.audit.emit` / `b.audit.safeEmit`. Switching stores on a running
|
package/lib/auth/oid4vci.js
CHANGED
|
@@ -451,10 +451,11 @@ function create(opts) {
|
|
|
451
451
|
"exchangePreAuthorizedCode: tx_code required (offer mandates it)");
|
|
452
452
|
}
|
|
453
453
|
var txHash = sha3Hash("oid4vci-tx:" + eopts.txCode);
|
|
454
|
-
// Constant-time compare on the hashed tx_code
|
|
455
|
-
//
|
|
456
|
-
//
|
|
457
|
-
//
|
|
454
|
+
// Constant-time compare on the hashed tx_code — `===` on a
|
|
455
|
+
// hex digest leaks per-byte timing under attacker-controlled
|
|
456
|
+
// input. Every framework compare against attacker-influenced
|
|
457
|
+
// bytes routes through timingSafeEqual regardless of the
|
|
458
|
+
// operand length being fixed.
|
|
458
459
|
if (!timingSafeEqual(txHash, entry.txCodeHash)) {
|
|
459
460
|
// Don't consume on failure — wallet may be retrying. Operator
|
|
460
461
|
// attaches their own attempt counter / lockout via b.auth.lockout.
|
package/lib/compliance.js
CHANGED
|
@@ -1087,8 +1087,9 @@ var POSTURE_DEFAULTS = Object.freeze({
|
|
|
1087
1087
|
// being certified for the ML-KEM / ML-DSA primitives upstream.
|
|
1088
1088
|
//
|
|
1089
1089
|
// Conflict resolution: PQC-first remains the framework default
|
|
1090
|
-
//
|
|
1091
|
-
//
|
|
1090
|
+
// — the framework refuses to weaken security middleware to fit a
|
|
1091
|
+
// posture flag. Operators in a FedRAMP boundary opt into
|
|
1092
|
+
// `fipsMode: true` to
|
|
1092
1093
|
// switch `b.audit.sign` from SLH-DSA-SHAKE-256f to FIPS-validated
|
|
1093
1094
|
// AES-GCM + SHA-384 for the audit-chain signing path. The runtime
|
|
1094
1095
|
// emits a `compliance.posture.fips_conflict` audit warning when
|
package/lib/crypto.js
CHANGED
|
@@ -52,7 +52,9 @@ var C = require("./constants");
|
|
|
52
52
|
// Circular: audit imports b.crypto for sha3Hash + envelope sign. Lazy-
|
|
53
53
|
// load the audit module so the legacy-envelope decrypt path can emit
|
|
54
54
|
// `system.crypto.decrypt.allow_legacy` events without an inline
|
|
55
|
-
// require() inside setImmediate (
|
|
55
|
+
// require() inside setImmediate. (The framework's convention is
|
|
56
|
+
// top-of-file require() except where a documented circular-load
|
|
57
|
+
// reason forces lazy-load; this is one of those reasons.)
|
|
56
58
|
var lazyRequire = require("./lazy-require");
|
|
57
59
|
var audit = lazyRequire(function () { return require("./audit"); });
|
|
58
60
|
// safe-buffer hosts the canonical hasCrlf(s) helper used by every
|
package/lib/safe-decompress.js
CHANGED
|
@@ -266,7 +266,7 @@ function safeDecompress(input, opts) {
|
|
|
266
266
|
return out;
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
-
// Drop-silent audit emission
|
|
269
|
+
// Drop-silent audit emission — refuse-emit is best-effort,
|
|
270
270
|
// failures here don't crash the operator's path. Then throw the typed
|
|
271
271
|
// error so the caller's catch block decides downstream.
|
|
272
272
|
function _refuse(opts, code, message, originalError) {
|
|
@@ -283,7 +283,7 @@ function _refuse(opts, code, message, originalError) {
|
|
|
283
283
|
reason: message,
|
|
284
284
|
},
|
|
285
285
|
});
|
|
286
|
-
} catch (_e) { /* drop-silent
|
|
286
|
+
} catch (_e) { /* drop-silent — observability is itself hot-path */ }
|
|
287
287
|
}
|
|
288
288
|
var err = new SafeDecompressError(code, message);
|
|
289
289
|
if (originalError) err.cause = originalError;
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.safeMountInfo
|
|
4
|
+
* @nav Primitives
|
|
5
|
+
* @title Safe MountInfo
|
|
6
|
+
* @order 131
|
|
7
|
+
* @slug safe-mount-info
|
|
8
|
+
*
|
|
9
|
+
* @card
|
|
10
|
+
* Canonical /proc/self/mountinfo parser that always reads field 4
|
|
11
|
+
* (root-within-source-FS) — defeats the bind-mount detection bug
|
|
12
|
+
* class where ad-hoc parsers picked the wrong field.
|
|
13
|
+
*
|
|
14
|
+
* @intro
|
|
15
|
+
* Linux `/proc/self/mountinfo` is the per-process kernel-published
|
|
16
|
+
* mount table. The format is fixed per [kernel
|
|
17
|
+
* Documentation/filesystems/proc.rst §3.5](https://www.kernel.org/doc/Documentation/filesystems/proc.txt):
|
|
18
|
+
*
|
|
19
|
+
* <id> <parent> <major:minor> <root> <mountpoint> <options>
|
|
20
|
+
* [<optional-fields>...] - <fstype> <source> <super-options>
|
|
21
|
+
*
|
|
22
|
+
* The `<root>` field (positional index 3, 0-based) is "root within
|
|
23
|
+
* source FS" — `"/"` for a regular mount, a non-root path for a
|
|
24
|
+
* bind-mount (e.g. `/Users/me/data` mounted onto `/data` inside a
|
|
25
|
+
* container). Bind-mount detection MUST consult this field; ad-hoc
|
|
26
|
+
* parsers that scan the options string for the word "bind" miss
|
|
27
|
+
* the truth (kernel doesn't emit "bind" as an option — bind state
|
|
28
|
+
* is observable ONLY via field 4).
|
|
29
|
+
*
|
|
30
|
+
* Pre-v0.11.6 the only lib/ caller (lib/watcher.js) parsed mountinfo
|
|
31
|
+
* correctly inline. Future callers — container-escape detection,
|
|
32
|
+
* sealed-store path validation, sandbox auto-probe — would have to
|
|
33
|
+
* re-derive the discipline. This primitive centralizes it: a
|
|
34
|
+
* single canonical parser that ALWAYS reads field 4, ALWAYS
|
|
35
|
+
* handles the `" - "` optional-fields separator, ALWAYS skips
|
|
36
|
+
* malformed lines without throwing.
|
|
37
|
+
*
|
|
38
|
+
* Refusal posture:
|
|
39
|
+
* - `safe-mount-info/read-failed` — /proc/self/mountinfo
|
|
40
|
+
* unreadable (non-Linux, restricted sandbox, host filesystem
|
|
41
|
+
* hidden). Operators get the typed error AND opts.fallback
|
|
42
|
+
* value (default null) to take.
|
|
43
|
+
* - `safe-mount-info/parse-failed` — single malformed line
|
|
44
|
+
* within /proc/self/mountinfo. Silent-skip (per-line) by
|
|
45
|
+
* default; opts.strict: true upgrades to throw on first
|
|
46
|
+
* malformed line.
|
|
47
|
+
*
|
|
48
|
+
* Threat model:
|
|
49
|
+
* - **Container-escape detection** (CVE-2019-5736 Docker /
|
|
50
|
+
* CVE-2022-0185 fsconfig / CVE-2024-21626 leaky-vessels) —
|
|
51
|
+
* bind-mount + root-field analysis is the canonical signal.
|
|
52
|
+
* Wrong-field readers (operations on field 5 / 6 / options-
|
|
53
|
+
* indexOf-"bind") miss escape-attempt patterns.
|
|
54
|
+
* - **Sealed-store integrity** — sealed dbs / vault state
|
|
55
|
+
* atop a bind-mounted host directory cross trust boundaries
|
|
56
|
+
* on container restart. Detection requires reading field 4
|
|
57
|
+
* and matching the mount-point against operator-trusted paths.
|
|
58
|
+
*
|
|
59
|
+
* Composes:
|
|
60
|
+
* - lib/safe-decompress / lib/audit — operator-supplied audit
|
|
61
|
+
* handle receives `system.safe_mount_info.refused` events on
|
|
62
|
+
* read-failed and parse-failed (drop-silent — observability emission must not crash the hot path that emitted the event).
|
|
63
|
+
*
|
|
64
|
+
* RFC / kernel-doc citations:
|
|
65
|
+
* - [Linux Documentation/filesystems/proc.rst §3.5 — /proc/<pid>/mountinfo](https://www.kernel.org/doc/Documentation/filesystems/proc.txt)
|
|
66
|
+
* - [CVE-2024-21626](https://nvd.nist.gov/vuln/detail/CVE-2024-21626) — runc leaky-vessels (bind-mount detection)
|
|
67
|
+
* - [CVE-2022-0185](https://nvd.nist.gov/vuln/detail/CVE-2022-0185) — fsconfig integer underflow
|
|
68
|
+
*/
|
|
69
|
+
|
|
70
|
+
var nodeFs = require("node:fs");
|
|
71
|
+
var lazyRequire = require("./lazy-require");
|
|
72
|
+
var validateOpts = require("./validate-opts");
|
|
73
|
+
var numericBounds = require("./numeric-bounds");
|
|
74
|
+
var { defineClass } = require("./framework-error");
|
|
75
|
+
|
|
76
|
+
var audit = lazyRequire(function () { return require("./audit"); });
|
|
77
|
+
|
|
78
|
+
var SafeMountInfoError = defineClass("SafeMountInfoError", { alwaysPermanent: true });
|
|
79
|
+
|
|
80
|
+
var DEFAULT_PATH = "/proc/self/mountinfo";
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @primitive b.safeMountInfo.parse
|
|
84
|
+
* @signature b.safeMountInfo.parse(text, opts?)
|
|
85
|
+
* @since 0.11.6
|
|
86
|
+
* @status stable
|
|
87
|
+
* @related b.safeMountInfo.read, b.safeMountInfo.bestMatch
|
|
88
|
+
*
|
|
89
|
+
* Parse `/proc/self/mountinfo` text bytes into structured entries.
|
|
90
|
+
* Each entry carries `{ id, parent, devMajMin, root, mountPoint,
|
|
91
|
+
* options, fstype, source, superOptions }` — `root` is the
|
|
92
|
+
* positional field 4 ("root within source FS") that bind-mount
|
|
93
|
+
* detection requires.
|
|
94
|
+
*
|
|
95
|
+
* Malformed lines are skipped by default (operator's mountinfo MAY
|
|
96
|
+
* contain a stray line during a concurrent mount/unmount). Set
|
|
97
|
+
* `opts.strict: true` to throw on first malformed line.
|
|
98
|
+
*
|
|
99
|
+
* @opts
|
|
100
|
+
* strict: boolean, // default false; throw on malformed line
|
|
101
|
+
* maxLines: number, // default 4096; cap to bound parser work
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* var entries = b.safeMountInfo.parse(rawText);
|
|
105
|
+
* var bindMounts = entries.filter(function (e) { return e.root !== "/"; });
|
|
106
|
+
*/
|
|
107
|
+
function parse(text, opts) {
|
|
108
|
+
opts = opts || {};
|
|
109
|
+
validateOpts(opts, ["strict", "maxLines"], "safeMountInfo.parse");
|
|
110
|
+
if (typeof text !== "string") {
|
|
111
|
+
throw new SafeMountInfoError(
|
|
112
|
+
"safe-mount-info/bad-input",
|
|
113
|
+
"safeMountInfo.parse: text must be a string");
|
|
114
|
+
}
|
|
115
|
+
numericBounds.requirePositiveFiniteIntIfPresent(opts.maxLines,
|
|
116
|
+
"safeMountInfo.parse: opts.maxLines",
|
|
117
|
+
SafeMountInfoError, "safe-mount-info/bad-arg");
|
|
118
|
+
var maxLines = (typeof opts.maxLines === "number") ? opts.maxLines : 4096; // allow:raw-byte-literal — line cap matches max kernel-published mount count
|
|
119
|
+
var strict = opts.strict === true;
|
|
120
|
+
var lines = text.split("\n");
|
|
121
|
+
// `text.split("\n").length` counts the trailing empty segment that
|
|
122
|
+
// `/proc/self/mountinfo` produces with its final newline. Adjust
|
|
123
|
+
// the count so the cap reflects ACTUAL records, not segments —
|
|
124
|
+
// otherwise exactly-`maxLines` valid records gets rejected as
|
|
125
|
+
// `too-many-lines` because the segment count is `maxLines + 1`.
|
|
126
|
+
var trailingEmpty = (lines.length > 0 && lines[lines.length - 1] === "");
|
|
127
|
+
var recordCount = trailingEmpty ? lines.length - 1 : lines.length;
|
|
128
|
+
if (recordCount > maxLines) {
|
|
129
|
+
throw new SafeMountInfoError(
|
|
130
|
+
"safe-mount-info/too-many-lines",
|
|
131
|
+
"safeMountInfo.parse: mountinfo has " + recordCount +
|
|
132
|
+
" lines, exceeds maxLines " + maxLines);
|
|
133
|
+
}
|
|
134
|
+
var out = [];
|
|
135
|
+
for (var i = 0; i < lines.length; i += 1) {
|
|
136
|
+
var ln = lines[i];
|
|
137
|
+
if (!ln) continue;
|
|
138
|
+
// Format: <id> <parent> <major:minor> <root> <mountpoint> <options>
|
|
139
|
+
// [<optional-fields>...] - <fstype> <source> <super-options>
|
|
140
|
+
// The separator " - " divides the optional-fields half from the
|
|
141
|
+
// post-fields half.
|
|
142
|
+
var sepIdx = ln.indexOf(" - ");
|
|
143
|
+
if (sepIdx === -1) {
|
|
144
|
+
if (strict) {
|
|
145
|
+
throw new SafeMountInfoError(
|
|
146
|
+
"safe-mount-info/parse-failed",
|
|
147
|
+
"safeMountInfo.parse: line " + (i + 1) + " missing ' - ' separator");
|
|
148
|
+
}
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
var preFields = ln.slice(0, sepIdx).split(" ");
|
|
152
|
+
var postFields = ln.slice(sepIdx + 3).split(" ");
|
|
153
|
+
if (preFields.length < 6 || postFields.length < 1) { // allow:raw-byte-literal — kernel-mandated minimum field counts
|
|
154
|
+
if (strict) {
|
|
155
|
+
throw new SafeMountInfoError(
|
|
156
|
+
"safe-mount-info/parse-failed",
|
|
157
|
+
"safeMountInfo.parse: line " + (i + 1) + " has " + preFields.length +
|
|
158
|
+
" pre-fields, " + postFields.length + " post-fields (need >=6, >=1)");
|
|
159
|
+
}
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
out.push({
|
|
163
|
+
id: preFields[0],
|
|
164
|
+
parent: preFields[1],
|
|
165
|
+
devMajMin: preFields[2],
|
|
166
|
+
root: preFields[3], // *** field 4 (0-indexed 3) — bind-mount detection
|
|
167
|
+
mountPoint: preFields[4],
|
|
168
|
+
options: preFields[5],
|
|
169
|
+
// optional-fields (variable length, between [6] and the " - ")
|
|
170
|
+
// are exposed via `optionalFields` for advanced callers.
|
|
171
|
+
optionalFields: preFields.slice(6, preFields.length).filter(function (f) { return f.length > 0; }),
|
|
172
|
+
fstype: postFields[0],
|
|
173
|
+
source: postFields[1] || null,
|
|
174
|
+
superOptions: postFields[2] || null,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* @primitive b.safeMountInfo.read
|
|
182
|
+
* @signature b.safeMountInfo.read(opts?)
|
|
183
|
+
* @since 0.11.6
|
|
184
|
+
* @status stable
|
|
185
|
+
* @related b.safeMountInfo.parse, b.safeMountInfo.bestMatch
|
|
186
|
+
*
|
|
187
|
+
* Read + parse `/proc/self/mountinfo` in one call. Returns the same
|
|
188
|
+
* array shape as `parse(text)`. On non-Linux platforms (where /proc
|
|
189
|
+
* doesn't exist) returns `opts.fallback` (default `null`); audit
|
|
190
|
+
* emission per `safe-mount-info.refused` with code `read-failed`.
|
|
191
|
+
*
|
|
192
|
+
* @opts
|
|
193
|
+
* path: string, // override path (default /proc/self/mountinfo)
|
|
194
|
+
* fallback: any, // returned on read failure (default null)
|
|
195
|
+
* audit: object, // optional b.audit handle for refusal events
|
|
196
|
+
* strict: boolean, // forwarded to parse()
|
|
197
|
+
* maxLines: number, // forwarded to parse()
|
|
198
|
+
*
|
|
199
|
+
* @example
|
|
200
|
+
* var entries = b.safeMountInfo.read();
|
|
201
|
+
* if (entries === null) {
|
|
202
|
+
* // non-Linux / sandboxed / no /proc
|
|
203
|
+
* }
|
|
204
|
+
*/
|
|
205
|
+
function read(opts) {
|
|
206
|
+
opts = opts || {};
|
|
207
|
+
validateOpts(opts,
|
|
208
|
+
["path", "fallback", "audit", "strict", "maxLines"],
|
|
209
|
+
"safeMountInfo.read");
|
|
210
|
+
var path = typeof opts.path === "string" && opts.path.length > 0
|
|
211
|
+
? opts.path
|
|
212
|
+
: DEFAULT_PATH;
|
|
213
|
+
var text;
|
|
214
|
+
try { text = nodeFs.readFileSync(path, "utf8"); }
|
|
215
|
+
catch (e) {
|
|
216
|
+
_refuseEmit(opts, "safe-mount-info/read-failed",
|
|
217
|
+
"/proc/self/mountinfo unreadable: " + ((e && e.message) || String(e)));
|
|
218
|
+
return ("fallback" in opts) ? opts.fallback : null;
|
|
219
|
+
}
|
|
220
|
+
return parse(text, opts);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* @primitive b.safeMountInfo.bestMatch
|
|
225
|
+
* @signature b.safeMountInfo.bestMatch(entries, path)
|
|
226
|
+
* @since 0.11.6
|
|
227
|
+
* @status stable
|
|
228
|
+
* @related b.safeMountInfo.read, b.safeMountInfo.isBindMount
|
|
229
|
+
*
|
|
230
|
+
* Find the mountinfo entry whose `mountPoint` is the longest prefix
|
|
231
|
+
* of `path`. Returns `null` when no entry covers `path`. The "longest
|
|
232
|
+
* prefix" semantic is what bind-mount detection / sealed-store-path
|
|
233
|
+
* validation needs — a mounted subdir wins over the root mount.
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* var entries = b.safeMountInfo.read();
|
|
237
|
+
* var atPath = b.safeMountInfo.bestMatch(entries, "/var/lib/blamejs");
|
|
238
|
+
* if (atPath && atPath.root !== "/") {
|
|
239
|
+
* // path lives on a bind-mount (potentially crossing host/guest)
|
|
240
|
+
* }
|
|
241
|
+
*/
|
|
242
|
+
function bestMatch(entries, path) {
|
|
243
|
+
if (!Array.isArray(entries) || entries.length === 0) return null;
|
|
244
|
+
if (typeof path !== "string" || path.length === 0) return null;
|
|
245
|
+
var best = null;
|
|
246
|
+
var bestLen = -1;
|
|
247
|
+
for (var i = 0; i < entries.length; i += 1) {
|
|
248
|
+
var e = entries[i];
|
|
249
|
+
if (!e || typeof e.mountPoint !== "string" || e.mountPoint.length === 0) continue;
|
|
250
|
+
var mp = e.mountPoint;
|
|
251
|
+
if (path === mp ||
|
|
252
|
+
(path.length > mp.length &&
|
|
253
|
+
path.indexOf(mp) === 0 &&
|
|
254
|
+
(mp === "/" || path.charCodeAt(mp.length) === 47 /* "/" */))) { // allow:raw-byte-literal — ASCII forward-slash
|
|
255
|
+
if (mp.length > bestLen) {
|
|
256
|
+
bestLen = mp.length;
|
|
257
|
+
best = e;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return best;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* @primitive b.safeMountInfo.isBindMount
|
|
266
|
+
* @signature b.safeMountInfo.isBindMount(entry)
|
|
267
|
+
* @since 0.11.6
|
|
268
|
+
* @status stable
|
|
269
|
+
* @related b.safeMountInfo.bestMatch
|
|
270
|
+
*
|
|
271
|
+
* `true` when the mountinfo entry's `root` field is something other
|
|
272
|
+
* than `"/"` (i.e. the mount is a bind from a non-root path within
|
|
273
|
+
* the source filesystem). The canonical bind-mount test — does NOT
|
|
274
|
+
* consult the options string (the kernel doesn't emit "bind" there).
|
|
275
|
+
*
|
|
276
|
+
* @example
|
|
277
|
+
* var entries = b.safeMountInfo.read();
|
|
278
|
+
* var atData = b.safeMountInfo.bestMatch(entries, "/data");
|
|
279
|
+
* var isBind = b.safeMountInfo.isBindMount(atData);
|
|
280
|
+
*/
|
|
281
|
+
function isBindMount(entry) {
|
|
282
|
+
if (!entry || typeof entry !== "object") return false;
|
|
283
|
+
return typeof entry.root === "string" && entry.root.length > 0 && entry.root !== "/";
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function _refuseEmit(opts, code, message) {
|
|
287
|
+
var auditImpl = opts.audit || (audit() && audit().safeEmit ? audit() : null);
|
|
288
|
+
if (auditImpl && typeof auditImpl.safeEmit === "function") {
|
|
289
|
+
try {
|
|
290
|
+
auditImpl.safeEmit({
|
|
291
|
+
action: "system.safe_mount_info.refused",
|
|
292
|
+
outcome: "denied",
|
|
293
|
+
metadata: { code: code, reason: message },
|
|
294
|
+
});
|
|
295
|
+
} catch (_e) { /* drop-silent — observability emission must not crash the hot path that emitted the event */ }
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
module.exports = {
|
|
300
|
+
parse: parse,
|
|
301
|
+
read: read,
|
|
302
|
+
bestMatch: bestMatch,
|
|
303
|
+
isBindMount: isBindMount,
|
|
304
|
+
SafeMountInfoError: SafeMountInfoError,
|
|
305
|
+
DEFAULT_PATH: DEFAULT_PATH,
|
|
306
|
+
};
|
package/lib/subject.js
CHANGED
|
@@ -634,9 +634,9 @@ function _subjectHash(subjectId) {
|
|
|
634
634
|
}
|
|
635
635
|
|
|
636
636
|
function _writeAudit(action, subjectId, outcome, metadata) {
|
|
637
|
-
// recordSafe — audit failure must not roll back the subject
|
|
638
|
-
// that already touched the database.
|
|
639
|
-
//
|
|
637
|
+
// recordSafe — audit failure must not roll back the subject
|
|
638
|
+
// mutation that already touched the database. Hot-path audit
|
|
639
|
+
// sinks are drop-silent: swallow any throw from audit.emit so a
|
|
640
640
|
// misconfigured sink doesn't crash a partially-committed subject
|
|
641
641
|
// mutation. Errors surface via the audit sink's own logger.
|
|
642
642
|
try {
|
package/lib/watcher.js
CHANGED
|
@@ -49,6 +49,7 @@ var nodeFs = require("node:fs");
|
|
|
49
49
|
var nodePath = require("node:path");
|
|
50
50
|
var lazyRequire = require("./lazy-require");
|
|
51
51
|
var validateOpts = require("./validate-opts");
|
|
52
|
+
var safeMountInfo = require("./safe-mount-info");
|
|
52
53
|
var { WatcherError } = require("./framework-error");
|
|
53
54
|
|
|
54
55
|
var audit = lazyRequire(function () { return require("./audit"); });
|
|
@@ -240,50 +241,19 @@ function _detectAutoMode(rootPath) {
|
|
|
240
241
|
try { inContainer = nodeFs.existsSync("/.dockerenv"); }
|
|
241
242
|
catch (_e) { inContainer = false; }
|
|
242
243
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
244
|
+
// Route through b.safeMountInfo — single canonical parser that
|
|
245
|
+
// ALWAYS reads field 4 ("root within source FS") for the bind-
|
|
246
|
+
// mount check below. Pre-v0.11.6 this parsed inline; centralizing
|
|
247
|
+
// it means future container-escape / sealed-store / sandbox call
|
|
248
|
+
// sites inherit the discipline.
|
|
249
|
+
var entries = safeMountInfo.read();
|
|
250
|
+
if (entries === null || entries.length === 0) {
|
|
250
251
|
return { mode: "fs", reason: "no-mountinfo", fsType: null, inContainer: inContainer };
|
|
251
252
|
}
|
|
252
|
-
|
|
253
|
-
// Find the mount whose mount-point is the longest prefix of rootPath.
|
|
254
|
-
var lines = mountInfoRaw.split("\n");
|
|
255
|
-
var bestMatch = null;
|
|
256
|
-
var bestLen = -1;
|
|
257
|
-
for (var i = 0; i < lines.length; i += 1) {
|
|
258
|
-
var ln = lines[i];
|
|
259
|
-
if (!ln) continue;
|
|
260
|
-
// Format: <id> <parent> <major:minor> <root> <mountpoint> <options>
|
|
261
|
-
// [<optional-fields>...] - <fstype> <source> <super-options>
|
|
262
|
-
// The separator " - " divides the optional-fields half from the post-fields half.
|
|
263
|
-
var sepIdx = ln.indexOf(" - ");
|
|
264
|
-
if (sepIdx === -1) continue;
|
|
265
|
-
var preFields = ln.slice(0, sepIdx).split(" ");
|
|
266
|
-
var postFields = ln.slice(sepIdx + 3).split(" ");
|
|
267
|
-
if (preFields.length < 6 || postFields.length < 1) continue;
|
|
268
|
-
var rootField = preFields[3]; // "/" for regular mount; bound-source path for bind
|
|
269
|
-
var mountPoint = preFields[4];
|
|
270
|
-
var fstype = postFields[0];
|
|
271
|
-
if (typeof mountPoint !== "string" || mountPoint.length === 0) continue;
|
|
272
|
-
if (rootPath === mountPoint ||
|
|
273
|
-
(rootPath.length > mountPoint.length &&
|
|
274
|
-
rootPath.indexOf(mountPoint) === 0 &&
|
|
275
|
-
(mountPoint === "/" || rootPath.charCodeAt(mountPoint.length) === 47 /* / */))) {
|
|
276
|
-
if (mountPoint.length > bestLen) {
|
|
277
|
-
bestLen = mountPoint.length;
|
|
278
|
-
bestMatch = { mountPoint: mountPoint, rootField: rootField, fstype: fstype };
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
253
|
+
var bestMatch = safeMountInfo.bestMatch(entries, rootPath);
|
|
283
254
|
if (!bestMatch) {
|
|
284
255
|
return { mode: "fs", reason: "no-mount-match", fsType: null, inContainer: inContainer };
|
|
285
256
|
}
|
|
286
|
-
|
|
287
257
|
if (AUTO_PROBE_POLL_FSTYPES.has(bestMatch.fstype)) {
|
|
288
258
|
return {
|
|
289
259
|
mode: "poll",
|
|
@@ -292,16 +262,12 @@ function _detectAutoMode(rootPath) {
|
|
|
292
262
|
inContainer: inContainer,
|
|
293
263
|
};
|
|
294
264
|
}
|
|
295
|
-
|
|
296
|
-
//
|
|
297
|
-
//
|
|
298
|
-
//
|
|
299
|
-
//
|
|
300
|
-
|
|
301
|
-
// best-matching mount carries a non-"/" root, the mount is a bind
|
|
302
|
-
// and inotify chains across the host/guest boundary are unreliable.
|
|
303
|
-
// (Operator can still force fs via mode: "fs"; force poll via mode: "poll".)
|
|
304
|
-
if (inContainer && bestMatch.rootField && bestMatch.rootField !== "/") {
|
|
265
|
+
// Bind-mount detection via field 4 — `b.safeMountInfo.isBindMount`
|
|
266
|
+
// is the canonical predicate (does NOT consult options string;
|
|
267
|
+
// the kernel doesn't emit "bind" there). When we're inside a
|
|
268
|
+
// container AND the best-matching mount is a bind, inotify
|
|
269
|
+
// chains across the host/guest boundary are unreliable.
|
|
270
|
+
if (inContainer && safeMountInfo.isBindMount(bestMatch)) {
|
|
305
271
|
return {
|
|
306
272
|
mode: "poll",
|
|
307
273
|
reason: "container-bind-mount",
|
|
@@ -309,7 +275,6 @@ function _detectAutoMode(rootPath) {
|
|
|
309
275
|
inContainer: inContainer,
|
|
310
276
|
};
|
|
311
277
|
}
|
|
312
|
-
|
|
313
278
|
return {
|
|
314
279
|
mode: "fs",
|
|
315
280
|
reason: "native-fs",
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
|
-
"specVersion": "1.
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
4
|
+
"specVersion": "1.5",
|
|
5
|
+
"serialNumber": "urn:uuid:977499c0-aee7-4197-9b4d-c7af9fd7fda5",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-20T16:28:06.450Z",
|
|
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.11.
|
|
23
|
-
"type": "
|
|
22
|
+
"bom-ref": "@blamejs/core@0.11.17",
|
|
23
|
+
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.11.
|
|
25
|
+
"version": "0.11.17",
|
|
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.11.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.11.17",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/core@0.11.
|
|
57
|
+
"ref": "@blamejs/core@0.11.17",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|
|
61
|
-
}
|
|
61
|
+
}
|