@sysid/sandbox-runtime-improved 0.0.56-sysid.1 → 0.0.64-sysid.1

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.
Files changed (129) hide show
  1. package/README.md +106 -4
  2. package/dist/cli.js +12 -18
  3. package/dist/cli.js.map +1 -1
  4. package/dist/index.d.ts +5 -5
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +2 -2
  7. package/dist/index.js.map +1 -1
  8. package/dist/sandbox/credential-mask-files.d.ts +157 -0
  9. package/dist/sandbox/credential-mask-files.d.ts.map +1 -0
  10. package/dist/sandbox/credential-mask-files.js +298 -0
  11. package/dist/sandbox/credential-mask-files.js.map +1 -0
  12. package/dist/sandbox/credential-sentinel.d.ts +72 -0
  13. package/dist/sandbox/credential-sentinel.d.ts.map +1 -0
  14. package/dist/sandbox/credential-sentinel.js +130 -0
  15. package/dist/sandbox/credential-sentinel.js.map +1 -0
  16. package/dist/sandbox/domain-pattern.d.ts +36 -0
  17. package/dist/sandbox/domain-pattern.d.ts.map +1 -0
  18. package/dist/sandbox/domain-pattern.js +60 -0
  19. package/dist/sandbox/domain-pattern.js.map +1 -0
  20. package/dist/sandbox/http-proxy.d.ts +33 -2
  21. package/dist/sandbox/http-proxy.d.ts.map +1 -1
  22. package/dist/sandbox/http-proxy.js +30 -2
  23. package/dist/sandbox/http-proxy.js.map +1 -1
  24. package/dist/sandbox/linux-sandbox-utils.d.ts +20 -0
  25. package/dist/sandbox/linux-sandbox-utils.d.ts.map +1 -1
  26. package/dist/sandbox/linux-sandbox-utils.js +134 -65
  27. package/dist/sandbox/linux-sandbox-utils.js.map +1 -1
  28. package/dist/sandbox/linux-violation-monitor.d.ts +48 -0
  29. package/dist/sandbox/linux-violation-monitor.d.ts.map +1 -0
  30. package/dist/sandbox/linux-violation-monitor.js +156 -0
  31. package/dist/sandbox/linux-violation-monitor.js.map +1 -0
  32. package/dist/sandbox/listen-in-range.d.ts +12 -0
  33. package/dist/sandbox/listen-in-range.d.ts.map +1 -0
  34. package/dist/sandbox/listen-in-range.js +43 -0
  35. package/dist/sandbox/listen-in-range.js.map +1 -0
  36. package/dist/sandbox/macos-sandbox-utils.d.ts +11 -0
  37. package/dist/sandbox/macos-sandbox-utils.d.ts.map +1 -1
  38. package/dist/sandbox/macos-sandbox-utils.js +72 -17
  39. package/dist/sandbox/macos-sandbox-utils.js.map +1 -1
  40. package/dist/sandbox/mitm-ca.d.ts +69 -2
  41. package/dist/sandbox/mitm-ca.d.ts.map +1 -1
  42. package/dist/sandbox/mitm-ca.js +188 -16
  43. package/dist/sandbox/mitm-ca.js.map +1 -1
  44. package/dist/sandbox/mitm-leaf.d.ts +1 -1
  45. package/dist/sandbox/mitm-leaf.d.ts.map +1 -1
  46. package/dist/sandbox/mitm-leaf.js +26 -19
  47. package/dist/sandbox/mitm-leaf.js.map +1 -1
  48. package/dist/sandbox/mux-proxy.d.ts +59 -0
  49. package/dist/sandbox/mux-proxy.d.ts.map +1 -0
  50. package/dist/sandbox/mux-proxy.js +159 -0
  51. package/dist/sandbox/mux-proxy.js.map +1 -0
  52. package/dist/sandbox/request-filter.d.ts +11 -1
  53. package/dist/sandbox/request-filter.d.ts.map +1 -1
  54. package/dist/sandbox/request-filter.js.map +1 -1
  55. package/dist/sandbox/sandbox-config.d.ts +461 -41
  56. package/dist/sandbox/sandbox-config.d.ts.map +1 -1
  57. package/dist/sandbox/sandbox-config.js +342 -26
  58. package/dist/sandbox/sandbox-config.js.map +1 -1
  59. package/dist/sandbox/sandbox-manager.d.ts +5 -1
  60. package/dist/sandbox/sandbox-manager.d.ts.map +1 -1
  61. package/dist/sandbox/sandbox-manager.js +697 -211
  62. package/dist/sandbox/sandbox-manager.js.map +1 -1
  63. package/dist/sandbox/sandbox-schemas.d.ts +15 -0
  64. package/dist/sandbox/sandbox-schemas.d.ts.map +1 -1
  65. package/dist/sandbox/sandbox-utils.d.ts +55 -6
  66. package/dist/sandbox/sandbox-utils.d.ts.map +1 -1
  67. package/dist/sandbox/sandbox-utils.js +143 -30
  68. package/dist/sandbox/sandbox-utils.js.map +1 -1
  69. package/dist/sandbox/socks-proxy.d.ts +11 -5
  70. package/dist/sandbox/socks-proxy.d.ts.map +1 -1
  71. package/dist/sandbox/socks-proxy.js +13 -81
  72. package/dist/sandbox/socks-proxy.js.map +1 -1
  73. package/dist/sandbox/tls-terminate-proxy.d.ts +2 -2
  74. package/dist/sandbox/tls-terminate-proxy.d.ts.map +1 -1
  75. package/dist/sandbox/tls-terminate-proxy.js +31 -5
  76. package/dist/sandbox/tls-terminate-proxy.js.map +1 -1
  77. package/dist/sandbox/windows-sandbox-utils.d.ts +508 -116
  78. package/dist/sandbox/windows-sandbox-utils.d.ts.map +1 -1
  79. package/dist/sandbox/windows-sandbox-utils.js +721 -209
  80. package/dist/sandbox/windows-sandbox-utils.js.map +1 -1
  81. package/dist/utils/shell-quote.d.ts +27 -0
  82. package/dist/utils/shell-quote.d.ts.map +1 -0
  83. package/dist/utils/shell-quote.js +47 -0
  84. package/dist/utils/shell-quote.js.map +1 -0
  85. package/package.json +7 -6
  86. package/vendor/seccomp/arm64/apply-seccomp +0 -0
  87. package/vendor/seccomp/build.ts +8 -30
  88. package/vendor/seccomp/x64/apply-seccomp +0 -0
  89. package/vendor/srt-win/build.ts +21 -0
  90. package/dist/vendor/seccomp/Dockerfile.build +0 -6
  91. package/dist/vendor/seccomp/arm64/apply-seccomp +0 -0
  92. package/dist/vendor/seccomp/build.ts +0 -91
  93. package/dist/vendor/seccomp/x64/apply-seccomp +0 -0
  94. package/dist/vendor/seccomp-src/apply-seccomp.c +0 -280
  95. package/dist/vendor/seccomp-src/seccomp-unix-block.c +0 -148
  96. package/dist/vendor/srt-win/Cargo.lock +0 -361
  97. package/dist/vendor/srt-win/Cargo.toml +0 -46
  98. package/dist/vendor/srt-win/ci/cleanup.ps1 +0 -49
  99. package/dist/vendor/srt-win/ci/smoke-exec.ps1 +0 -446
  100. package/dist/vendor/srt-win/ci/smoke.ps1 +0 -307
  101. package/dist/vendor/srt-win/src/job.rs +0 -102
  102. package/dist/vendor/srt-win/src/launch.rs +0 -732
  103. package/dist/vendor/srt-win/src/lib.rs +0 -20
  104. package/dist/vendor/srt-win/src/main.rs +0 -669
  105. package/dist/vendor/srt-win/src/self_protect.rs +0 -169
  106. package/dist/vendor/srt-win/src/sid.rs +0 -296
  107. package/dist/vendor/srt-win/src/token.rs +0 -341
  108. package/dist/vendor/srt-win/src/util.rs +0 -42
  109. package/dist/vendor/srt-win/src/wfp.rs +0 -992
  110. package/dist/vendor/srt-win/src/winsta.rs +0 -209
  111. package/dist/vendor/srt-win/tests/sd_access_check_matrix.rs +0 -259
  112. package/vendor/seccomp-src/apply-seccomp.c +0 -280
  113. package/vendor/seccomp-src/seccomp-unix-block.c +0 -148
  114. package/vendor/srt-win/Cargo.lock +0 -361
  115. package/vendor/srt-win/Cargo.toml +0 -46
  116. package/vendor/srt-win/ci/cleanup.ps1 +0 -49
  117. package/vendor/srt-win/ci/smoke-exec.ps1 +0 -446
  118. package/vendor/srt-win/ci/smoke.ps1 +0 -307
  119. package/vendor/srt-win/src/job.rs +0 -102
  120. package/vendor/srt-win/src/launch.rs +0 -732
  121. package/vendor/srt-win/src/lib.rs +0 -20
  122. package/vendor/srt-win/src/main.rs +0 -669
  123. package/vendor/srt-win/src/self_protect.rs +0 -169
  124. package/vendor/srt-win/src/sid.rs +0 -296
  125. package/vendor/srt-win/src/token.rs +0 -341
  126. package/vendor/srt-win/src/util.rs +0 -42
  127. package/vendor/srt-win/src/wfp.rs +0 -992
  128. package/vendor/srt-win/src/winsta.rs +0 -209
  129. package/vendor/srt-win/tests/sd_access_check_matrix.rs +0 -259
@@ -0,0 +1,298 @@
1
+ /**
2
+ * Credential file masking (Linux).
3
+ *
4
+ * For a `credentials.files` entry with `mode: "mask"`, srt reads the real
5
+ * file content on the host, registers one or more sentinels in the
6
+ * {@link SentinelRegistry}, and writes a fake file (sentinel-substituted)
7
+ * to a manager-owned temp directory. The Linux sandbox then `--ro-bind`s
8
+ * the fake over the real path, so the sandboxed process reads the
9
+ * sentinel(s). The proxy substitution from env-var masking already scans
10
+ * every header for any registered sentinel, so a tool that does
11
+ * `Authorization: Bearer $(cat <maskedFile>)` reaches the upstream with
12
+ * the real bytes — no proxy changes required.
13
+ *
14
+ * Without `extract`, masking is **whole-file**: one sentinel replaces the
15
+ * entire content. With `extract`, masking is **structured**: a regex picks
16
+ * out the credential value(s) and only those spans are replaced, so a tool
17
+ * that parses the file (JSON/YAML/.netrc) still sees valid syntax. See
18
+ * {@link extractAndSubstitute} and {@link CredentialFileConfigSchema}.
19
+ *
20
+ * On macOS, SBPL cannot redirect reads, so masked files degrade to
21
+ * `mode: "deny"` (see macos-sandbox-utils.ts).
22
+ */
23
+ import * as fs from 'node:fs';
24
+ import { tmpdir } from 'node:os';
25
+ import { join } from 'node:path';
26
+ import { logForDebugging } from '../utils/debug.js';
27
+ import { normalizePathForSandbox } from './sandbox-utils.js';
28
+ /**
29
+ * Sentinel-registry key prefix for masked files. Keeps file keys disjoint
30
+ * from env-var names so a credential file at path `GH_TOKEN` cannot collide
31
+ * with the env var `GH_TOKEN`.
32
+ */
33
+ const FILE_KEY_PREFIX = 'file:';
34
+ /**
35
+ * Apply `pattern` globally to `content` and return `content` with each
36
+ * matched capture-group-1 span replaced by `sentinelFor(capture, i)`,
37
+ * where `i` is the zero-based index of the distinct captured value in
38
+ * first-seen order.
39
+ *
40
+ * Offset-based: the regex `d` flag exposes capture-group offsets, so the
41
+ * output is built by slicing between spans and splicing the sentinel in
42
+ * at the exact `[start, end)` of group 1. By default only the
43
+ * regex-matched span is replaced — a captured value that coincidentally
44
+ * appears elsewhere in the file (outside any match) is left intact. No
45
+ * placeholder pass, no substring-ordering concern.
46
+ *
47
+ * With `maskDuplicates` (see {@link ExtractOptions}), an indexOf scan
48
+ * additionally collects every verbatim occurrence of each captured value
49
+ * elsewhere in the file. All spans are computed against the ORIGINAL
50
+ * content — never the partially substituted output — so an inserted
51
+ * sentinel can never be re-matched and corrupted, even when a captured
52
+ * value is a substring of the sentinel literal. Regex-match spans win
53
+ * over verbatim ones, and verbatim scans run longest-capture-first so a
54
+ * shorter capture that is a substring of a longer one cannot claim part
55
+ * of the longer secret's occurrence.
56
+ *
57
+ * Returns `null` when the pattern matches nothing — the caller routes
58
+ * that per the entry's `onExtractNoMatch` option (warn / deny / error;
59
+ * see {@link buildMaskedFileBinds}).
60
+ *
61
+ * Throws when a match has no group-1 capture. The schema already rejects
62
+ * patterns with zero groups, so this only fires when group 1 is optional
63
+ * and absent for some match (e.g. `"token: (\\S+)?"`); accepting that
64
+ * would silently mask nothing for that occurrence.
65
+ *
66
+ * Pure on `content`/`pattern`; the callback may close over a registry.
67
+ */
68
+ export function extractAndSubstitute(content, pattern, sentinelFor, options = {}) {
69
+ // The schema validates `pattern` compiles; `g` makes matchAll iterate
70
+ // every occurrence and `d` populates `m.indices` with group offsets.
71
+ const re = new RegExp(pattern, 'gd');
72
+ const indexByCapture = new Map();
73
+ // First sentinel returned per capture — reused for verbatim spans so
74
+ // the duplicate pass never re-invokes the callback.
75
+ const sentinelByCapture = new Map();
76
+ const spans = [];
77
+ for (const m of content.matchAll(re)) {
78
+ const cap = m[1];
79
+ if (cap === undefined) {
80
+ throw new Error(`extract pattern /${pattern}/ matched at offset ${m.index} but ` +
81
+ `capture group 1 is undefined — group 1 must capture the ` +
82
+ `credential value on every match.`);
83
+ }
84
+ // Empty captures are skipped: a zero-width span has nothing to mask.
85
+ if (cap.length === 0)
86
+ continue;
87
+ let i = indexByCapture.get(cap);
88
+ if (i === undefined)
89
+ indexByCapture.set(cap, (i = indexByCapture.size));
90
+ const [start, end] = m.indices[1];
91
+ const sentinel = sentinelFor(cap, i);
92
+ if (!sentinelByCapture.has(cap))
93
+ sentinelByCapture.set(cap, sentinel);
94
+ spans.push({ start, end, sentinel });
95
+ }
96
+ if (indexByCapture.size === 0)
97
+ return null;
98
+ if (options.maskDuplicates) {
99
+ // Longest capture first: a shorter capture that is a substring of a
100
+ // longer one would otherwise claim part of the longer secret's
101
+ // occurrence and leave the remainder exposed.
102
+ const byLength = [...indexByCapture.keys()].sort((a, b) => b.length - a.length);
103
+ for (const cap of byLength) {
104
+ const sentinel = sentinelByCapture.get(cap);
105
+ for (let start = content.indexOf(cap); start !== -1; start = content.indexOf(cap, start + 1)) {
106
+ const end = start + cap.length;
107
+ // Spans already collected (regex matches, then earlier — longer —
108
+ // captures' verbatim occurrences) win over this occurrence.
109
+ if (spans.some(s => start < s.end && s.start < end))
110
+ continue;
111
+ spans.push({ start, end, sentinel });
112
+ }
113
+ }
114
+ spans.sort((a, b) => a.start - b.start);
115
+ }
116
+ // Spans are non-overlapping and sorted by offset (matchAll yields
117
+ // matches in order; the verbatim pass re-sorts), so a single
118
+ // slice-and-concat pass over the original content is sound.
119
+ let out = '';
120
+ let pos = 0;
121
+ for (const s of spans) {
122
+ out += content.slice(pos, s.start) + s.sentinel;
123
+ pos = s.end;
124
+ }
125
+ return {
126
+ fakeContent: out + content.slice(pos),
127
+ captures: [...indexByCapture.keys()],
128
+ };
129
+ }
130
+ /**
131
+ * Manager-owned temp dir holding the fake files.
132
+ *
133
+ * INVARIANT: this directory must never be writable from inside the sandbox.
134
+ * The Linux layer enforces this by emitting `--ro-bind <dirPath> <dirPath>`
135
+ * after every other filesystem mount (see generateFilesystemArgs), so the
136
+ * store stays read-only even if a caller's allowWrite covers os.tmpdir() or
137
+ * the host's $TMPDIR points under a default-writable path. If the sandbox
138
+ * could write here it could replace a fake's content (the bind exposes the
139
+ * source file) or plant a symlink for a later host-side write() to follow.
140
+ */
141
+ export class MaskedFileStore {
142
+ constructor() {
143
+ this.byKey = new Map();
144
+ }
145
+ /**
146
+ * Write `sentinel` to a fake file for `key` and return its path.
147
+ * Idempotent on `key`: a repeat call rewrites the same fake (so a
148
+ * changed sentinel after re-register propagates) instead of leaking a
149
+ * new file per wrapWithSandbox() call.
150
+ */
151
+ write(key, sentinel) {
152
+ if (this.dir === undefined) {
153
+ this.dir = fs.mkdtempSync(join(tmpdir(), 'srt-credmask-'));
154
+ }
155
+ let fakePath = this.byKey.get(key);
156
+ if (fakePath === undefined) {
157
+ fakePath = join(this.dir, `${this.byKey.size}.fake`);
158
+ this.byKey.set(key, fakePath);
159
+ }
160
+ // Never follow a symlink at fakePath: a prior sandbox invocation may
161
+ // have planted one (the store dir is ro-bound now, but defence in
162
+ // depth). Unlink first so writeFileSync creates a fresh regular file.
163
+ fs.rmSync(fakePath, { force: true });
164
+ // 0600: owner rw so the idempotent rewrite above succeeds; the bind
165
+ // into the sandbox is --ro-bind so the sandboxed process sees it
166
+ // read-only regardless of the host mode. No group/other.
167
+ fs.writeFileSync(fakePath, sentinel, { mode: 0o600 });
168
+ return fakePath;
169
+ }
170
+ /** Remove the temp dir and every fake file in it. Idempotent. */
171
+ dispose() {
172
+ if (this.dir !== undefined) {
173
+ try {
174
+ fs.rmSync(this.dir, { recursive: true, force: true });
175
+ }
176
+ catch (err) {
177
+ logForDebugging(`MaskedFileStore cleanup failed: ${err}`, {
178
+ level: 'error',
179
+ });
180
+ }
181
+ }
182
+ this.dir = undefined;
183
+ this.byKey.clear();
184
+ }
185
+ /** Temp dir path, or undefined if no fake has been written yet. */
186
+ get dirPath() {
187
+ return this.dir;
188
+ }
189
+ }
190
+ /**
191
+ * For each `mode: "mask"` file entry: resolve the path, read the real
192
+ * content, build the fake content (whole-file or structured per `extract`),
193
+ * register sentinels in `registry`, write the fake via `store`, and return
194
+ * the bind list plus any entries that degraded to deny.
195
+ *
196
+ * Whole-file mode (no `extract`): one sentinel keyed `file:<path>` whose
197
+ * real value is the entire file content; the fake file *is* the sentinel.
198
+ *
199
+ * Structured mode (`extract` set): one sentinel per distinct captured
200
+ * value, keyed `file:<path>#<i>`; the fake file is the real content with
201
+ * each captured span replaced by its sentinel. If the regex matches
202
+ * nothing, the entry's `onExtractNoMatch` decides:
203
+ * - `"warn"` (default): skip the entry with a loud stderr warning —
204
+ * fail-open, the file stays readable via the root mount;
205
+ * - `"deny"`: push the path to `degradeToDenyPaths` — fail-closed, the
206
+ * file becomes unreadable inside the sandbox;
207
+ * - `"error"`: throw, so nothing runs until the regex is fixed.
208
+ * With `maskDuplicates`, verbatim occurrences of each captured value
209
+ * outside the matched spans are also replaced (see {@link ExtractOptions}).
210
+ *
211
+ * Entries whose path does not exist, is unreadable, or resolves to a
212
+ * directory are skipped with a debug log — same posture as a masked env
213
+ * var that's unset on the host: nothing to protect, and surfacing a hard
214
+ * error would make a portable config brittle across machines.
215
+ *
216
+ * The directory check is the authoritative one (the schema only catches a
217
+ * trailing slash); whole-file masking has no meaning for a directory.
218
+ */
219
+ export function buildMaskedFileBinds(files, allowedDomains, registry, store) {
220
+ const binds = [];
221
+ const degradeToDenyPaths = [];
222
+ for (const f of files) {
223
+ if (f.mode !== 'mask')
224
+ continue;
225
+ const realPath = normalizePathForSandbox(f.path);
226
+ let content;
227
+ try {
228
+ const stat = fs.statSync(realPath);
229
+ if (stat.isDirectory()) {
230
+ logForDebugging(`[credential-mask] Skipping masked file entry that resolves to ` +
231
+ `a directory: ${f.path} — use mode "deny" for directories.`, { level: 'warn' });
232
+ continue;
233
+ }
234
+ // Read as bytes first: a utf8 read silently maps invalid sequences
235
+ // to U+FFFD, so the sentinel would round-trip to corrupted bytes at
236
+ // the proxy. Masking (whole-file or extract) is for text credential
237
+ // files; reject binary.
238
+ const raw = fs.readFileSync(realPath);
239
+ content = raw.toString('utf8');
240
+ if (Buffer.byteLength(content, 'utf8') !== raw.length) {
241
+ logForDebugging(`[credential-mask] Skipping masked file with non-UTF-8 content ` +
242
+ `(binary credential files are not supported in mask mode): ` +
243
+ `${f.path}`, { level: 'warn' });
244
+ continue;
245
+ }
246
+ }
247
+ catch (err) {
248
+ logForDebugging(`[credential-mask] Skipping masked file (unreadable on host): ` +
249
+ `${f.path} — ${err.message}`);
250
+ continue;
251
+ }
252
+ const injectHosts = f.injectHosts ?? allowedDomains;
253
+ const key = FILE_KEY_PREFIX + realPath;
254
+ let fakeContent;
255
+ if (f.extract === undefined) {
256
+ // Whole-file: one sentinel for the entire content.
257
+ fakeContent = registry.register(key, content, injectHosts);
258
+ }
259
+ else {
260
+ const extracted = extractAndSubstitute(content, f.extract, (cap, i) => registry.register(`${key}#${i}`, cap, injectHosts), { maskDuplicates: f.maskDuplicates ?? false });
261
+ if (extracted === null) {
262
+ const onNoMatch = f.onExtractNoMatch ?? 'warn';
263
+ if (onNoMatch === 'error') {
264
+ throw new Error(`credentials.files entry "${f.path}": extract pattern ` +
265
+ `"${f.extract}" matched nothing (onExtractNoMatch: "error"). ` +
266
+ `Fix the regex, change to "warn"/"deny", or remove the entry.`);
267
+ }
268
+ if (onNoMatch === 'deny') {
269
+ // Fail-closed: the operator declared this file as containing a
270
+ // credential. Masking can't apply — degrade to deny so the
271
+ // sandboxed process cannot read the credential at all.
272
+ logForDebugging(`[credential-mask] extract pattern /${f.extract}/ matched ` +
273
+ `nothing in ${f.path} — degrading to mode "deny".`, { level: 'warn' });
274
+ degradeToDenyPaths.push(realPath);
275
+ continue;
276
+ }
277
+ // 'warn' (default): fail-open. A non-matching pattern is a config
278
+ // error to surface, not a reason to block file access. Skip the
279
+ // entry (no bind, no deny) — the file stays readable via the root
280
+ // mount — and warn loudly on stderr so the operator fixes the regex.
281
+ const msg = `[sandbox-runtime] WARNING: credentials.files entry ` +
282
+ `"${f.path}" has extract pattern "${f.extract}" that matched ` +
283
+ `nothing in the file. The file is left UNPROTECTED (readable ` +
284
+ `as-is inside the sandbox). Fix the regex, set onExtractNoMatch ` +
285
+ `to "deny" or "error", or remove the entry.`;
286
+ console.warn(msg);
287
+ logForDebugging(msg, { level: 'warn' });
288
+ continue;
289
+ }
290
+ fakeContent = extracted.fakeContent;
291
+ }
292
+ const fakePath = store.write(key, fakeContent);
293
+ binds.push({ realPath, fakePath });
294
+ }
295
+ return { binds, degradeToDenyPaths };
296
+ }
297
+ export const MASKED_FILE_STORE_PREFIX = 'srt-credmask-';
298
+ //# sourceMappingURL=credential-mask-files.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credential-mask-files.js","sourceRoot":"","sources":["../../src/sandbox/credential-mask-files.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Per-session sentinel registry for credential masking.
3
+ *
4
+ * A masked credential's real value is replaced inside the sandbox with a
5
+ * sentinel of the form `fake_value_<uuid4>`. The sandboxed process sees only
6
+ * the sentinel; the host-side proxy substitutes sentinel→real on egress to
7
+ * allowlisted destinations. The map lives only in process memory — it is
8
+ * never written to disk and never logged.
9
+ */
10
+ import type { IncomingHttpHeaders } from 'node:http';
11
+ export declare const SENTINEL_PREFIX = "fake_value_";
12
+ /** Predicate matching a destination host against one injectHosts pattern. */
13
+ export type HostMatcher = (host: string, pattern: string) => boolean;
14
+ /**
15
+ * Sentinel↔real-value map for one sandbox session, keyed by credential name.
16
+ *
17
+ * Each credential carries its own `injectHosts` list, and substitution is
18
+ * gated per sentinel: a sentinel is swapped to its real value only when the
19
+ * destination matches THAT credential's hosts. This prevents laundering
20
+ * credential A through credential B's allowlisted host by sending A's
21
+ * sentinel there — the proxy leaves A's sentinel intact on B's host.
22
+ *
23
+ * Keying on name (not value) means two env vars holding the same secret get
24
+ * distinct sentinels, so each can have an independent host list.
25
+ */
26
+ export declare class SentinelRegistry {
27
+ private readonly byName;
28
+ private readonly bySentinel;
29
+ /**
30
+ * Return the sentinel for the credential named `name`, minting a fresh one
31
+ * on first use. The sentinel is `fake_value_<uuid4>`: long enough that an
32
+ * accidental collision with legitimate header content is negligible, and
33
+ * free of shell/URL metacharacters so it survives `--setenv` and
34
+ * `env NAME=value` unquoted.
35
+ *
36
+ * Idempotent on `name`: a repeat call returns the same sentinel and updates
37
+ * `realValue`/`injectHosts` in place so `updateConfig()` can change either
38
+ * without invalidating sentinels the sandboxed process has already read.
39
+ */
40
+ register(name: string, realValue: string, injectHosts: readonly string[]): string;
41
+ /** Real value for `sentinel`, or undefined if not registered. */
42
+ lookupReal(sentinel: string): string | undefined;
43
+ /**
44
+ * Names of the registered credentials whose `injectHosts` cover
45
+ * `destHost`. Diagnostic helper: the proxy uses it to warn when a host is
46
+ * exempted from TLS termination (so substitution can never run there) but
47
+ * a masked credential is configured for injection at it.
48
+ */
49
+ namesInjectableAt(destHost: string, matches: HostMatcher): string[];
50
+ /** Iterate registered `[sentinel, realValue]` pairs. */
51
+ entries(): IterableIterator<[string, string]>;
52
+ /** Number of registered sentinels. */
53
+ get size(): number;
54
+ /** Drop every mapping. Called on session teardown. */
55
+ clear(): void;
56
+ /**
57
+ * Replace registered sentinels found in `headers` with their real values,
58
+ * in place. Each sentinel substitutes only when `destHost` matches one of
59
+ * THAT credential's `injectHosts` patterns (via `matches`); a sentinel
60
+ * whose host list does not cover `destHost` is left as the useless fake.
61
+ *
62
+ * Scans all header values rather than a fixed set — a sentinel showing up
63
+ * anywhere is the substitution trigger, regardless of header name
64
+ * (Authorization, X-Api-Key, Private-Token, ...).
65
+ *
66
+ * The caller remains responsible for transport gating (TLS-terminated path
67
+ * unless `allowPlaintextInject`).
68
+ */
69
+ substituteInHeaders(headers: IncomingHttpHeaders, destHost: string, matches: HostMatcher): void;
70
+ private substituteInString;
71
+ }
72
+ //# sourceMappingURL=credential-sentinel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credential-sentinel.d.ts","sourceRoot":"","sources":["../../src/sandbox/credential-sentinel.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAA;AAEpD,eAAO,MAAM,eAAe,gBAAgB,CAAA;AAE5C,6EAA6E;AAC7E,MAAM,MAAM,WAAW,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAA;AASpE;;;;;;;;;;;GAWG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAmC;IAC1D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAmC;IAE9D;;;;;;;;;;OAUG;IACH,QAAQ,CACN,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,SAAS,MAAM,EAAE,GAC7B,MAAM;IAcT,iEAAiE;IACjE,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAIhD;;;;;OAKG;IACH,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,MAAM,EAAE;IAQnE,wDAAwD;IACvD,OAAO,IAAI,gBAAgB,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAI9C,sCAAsC;IACtC,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,sDAAsD;IACtD,KAAK,IAAI,IAAI;IAKb;;;;;;;;;;;;OAYG;IACH,mBAAmB,CACjB,OAAO,EAAE,mBAAmB,EAC5B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,WAAW,GACnB,IAAI;IAcP,OAAO,CAAC,kBAAkB;CAgB3B"}
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Per-session sentinel registry for credential masking.
3
+ *
4
+ * A masked credential's real value is replaced inside the sandbox with a
5
+ * sentinel of the form `fake_value_<uuid4>`. The sandboxed process sees only
6
+ * the sentinel; the host-side proxy substitutes sentinel→real on egress to
7
+ * allowlisted destinations. The map lives only in process memory — it is
8
+ * never written to disk and never logged.
9
+ */
10
+ import { randomUUID } from 'node:crypto';
11
+ export const SENTINEL_PREFIX = 'fake_value_';
12
+ /**
13
+ * Sentinel↔real-value map for one sandbox session, keyed by credential name.
14
+ *
15
+ * Each credential carries its own `injectHosts` list, and substitution is
16
+ * gated per sentinel: a sentinel is swapped to its real value only when the
17
+ * destination matches THAT credential's hosts. This prevents laundering
18
+ * credential A through credential B's allowlisted host by sending A's
19
+ * sentinel there — the proxy leaves A's sentinel intact on B's host.
20
+ *
21
+ * Keying on name (not value) means two env vars holding the same secret get
22
+ * distinct sentinels, so each can have an independent host list.
23
+ */
24
+ export class SentinelRegistry {
25
+ constructor() {
26
+ this.byName = new Map();
27
+ this.bySentinel = new Map();
28
+ }
29
+ /**
30
+ * Return the sentinel for the credential named `name`, minting a fresh one
31
+ * on first use. The sentinel is `fake_value_<uuid4>`: long enough that an
32
+ * accidental collision with legitimate header content is negligible, and
33
+ * free of shell/URL metacharacters so it survives `--setenv` and
34
+ * `env NAME=value` unquoted.
35
+ *
36
+ * Idempotent on `name`: a repeat call returns the same sentinel and updates
37
+ * `realValue`/`injectHosts` in place so `updateConfig()` can change either
38
+ * without invalidating sentinels the sandboxed process has already read.
39
+ */
40
+ register(name, realValue, injectHosts) {
41
+ const existing = this.byName.get(name);
42
+ if (existing !== undefined) {
43
+ existing.realValue = realValue;
44
+ existing.injectHosts = injectHosts;
45
+ return existing.sentinel;
46
+ }
47
+ const sentinel = SENTINEL_PREFIX + randomUUID();
48
+ const entry = { name, sentinel, realValue, injectHosts };
49
+ this.byName.set(name, entry);
50
+ this.bySentinel.set(sentinel, entry);
51
+ return sentinel;
52
+ }
53
+ /** Real value for `sentinel`, or undefined if not registered. */
54
+ lookupReal(sentinel) {
55
+ return this.bySentinel.get(sentinel)?.realValue;
56
+ }
57
+ /**
58
+ * Names of the registered credentials whose `injectHosts` cover
59
+ * `destHost`. Diagnostic helper: the proxy uses it to warn when a host is
60
+ * exempted from TLS termination (so substitution can never run there) but
61
+ * a masked credential is configured for injection at it.
62
+ */
63
+ namesInjectableAt(destHost, matches) {
64
+ const names = [];
65
+ for (const e of this.byName.values()) {
66
+ if (e.injectHosts.some(p => matches(destHost, p)))
67
+ names.push(e.name);
68
+ }
69
+ return names;
70
+ }
71
+ /** Iterate registered `[sentinel, realValue]` pairs. */
72
+ *entries() {
73
+ for (const e of this.bySentinel.values())
74
+ yield [e.sentinel, e.realValue];
75
+ }
76
+ /** Number of registered sentinels. */
77
+ get size() {
78
+ return this.bySentinel.size;
79
+ }
80
+ /** Drop every mapping. Called on session teardown. */
81
+ clear() {
82
+ this.byName.clear();
83
+ this.bySentinel.clear();
84
+ }
85
+ /**
86
+ * Replace registered sentinels found in `headers` with their real values,
87
+ * in place. Each sentinel substitutes only when `destHost` matches one of
88
+ * THAT credential's `injectHosts` patterns (via `matches`); a sentinel
89
+ * whose host list does not cover `destHost` is left as the useless fake.
90
+ *
91
+ * Scans all header values rather than a fixed set — a sentinel showing up
92
+ * anywhere is the substitution trigger, regardless of header name
93
+ * (Authorization, X-Api-Key, Private-Token, ...).
94
+ *
95
+ * The caller remains responsible for transport gating (TLS-terminated path
96
+ * unless `allowPlaintextInject`).
97
+ */
98
+ substituteInHeaders(headers, destHost, matches) {
99
+ if (this.bySentinel.size === 0)
100
+ return;
101
+ for (const [name, value] of Object.entries(headers)) {
102
+ if (value === undefined)
103
+ continue;
104
+ if (Array.isArray(value)) {
105
+ for (let i = 0; i < value.length; i++) {
106
+ value[i] = this.substituteInString(value[i], destHost, matches);
107
+ }
108
+ }
109
+ else {
110
+ headers[name] = this.substituteInString(value, destHost, matches);
111
+ }
112
+ }
113
+ }
114
+ substituteInString(s, destHost, matches) {
115
+ // Fast path: the sentinel prefix is fixed, so a header value that
116
+ // doesn't contain it cannot contain any sentinel.
117
+ if (!s.includes(SENTINEL_PREFIX))
118
+ return s;
119
+ let out = s;
120
+ for (const e of this.bySentinel.values()) {
121
+ if (!out.includes(e.sentinel))
122
+ continue;
123
+ if (!e.injectHosts.some(p => matches(destHost, p)))
124
+ continue;
125
+ out = out.split(e.sentinel).join(e.realValue);
126
+ }
127
+ return out;
128
+ }
129
+ }
130
+ //# sourceMappingURL=credential-sentinel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credential-sentinel.js","sourceRoot":"","sources":["../../src/sandbox/credential-sentinel.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Domain-pattern matching shared between runtime host filtering
3
+ * (sandbox-manager) and config-time validation (sandbox-config).
4
+ * Lives in its own module so the schema can import it without pulling
5
+ * in sandbox-manager (which imports the schema — circular).
6
+ */
7
+ /**
8
+ * Match a hostname against a domain pattern.
9
+ *
10
+ * Patterns:
11
+ * - `*` matches everything (deny-all; the schema only accepts this in
12
+ * deniedDomains).
13
+ * - `*.example.com` matches any strict subdomain of example.com.
14
+ * - anything else matches exactly (case-insensitive).
15
+ *
16
+ * Wildcard suffix matching is refused for IP literals so an IPv6 zone-ID
17
+ * payload like `::ffff:1.2.3.4%x.allowed.com` cannot pass `.endsWith()`
18
+ * while the OS connects to the bare IP. isValidHost already rejects `%`,
19
+ * but we refuse here too for defence in depth.
20
+ */
21
+ export declare function matchesDomainPattern(hostname: string, pattern: string): boolean;
22
+ /**
23
+ * Decide whether a per-credential `injectHosts` entry is reachable via
24
+ * `network.allowedDomains` — i.e. every concrete host that could match
25
+ * `injectHost` is allowed by at least one entry in `allowedDomains`.
26
+ *
27
+ * For an exact `injectHost` (`api.github.com`) this is just
28
+ * `matchesDomainPattern` against each allowed pattern.
29
+ *
30
+ * For a wildcard `injectHost` (`*.X`), an exact allowedDomain can never
31
+ * cover it (it admits only one host), so coverage requires an allowed
32
+ * wildcard `*.Y` whose base is `X` or an ancestor of `X` — e.g.
33
+ * `*.api.github.com` is covered by `*.github.com`.
34
+ */
35
+ export declare function isInjectHostCoveredByAllowedDomains(injectHost: string, allowedDomains: readonly string[]): boolean;
36
+ //# sourceMappingURL=domain-pattern.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"domain-pattern.d.ts","sourceRoot":"","sources":["../../src/sandbox/domain-pattern.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH;;;;;;;;;;;;;GAaG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,GACd,OAAO,CAST;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,mCAAmC,CACjD,UAAU,EAAE,MAAM,EAClB,cAAc,EAAE,SAAS,MAAM,EAAE,GAChC,OAAO,CAUT"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Domain-pattern matching shared between runtime host filtering
3
+ * (sandbox-manager) and config-time validation (sandbox-config).
4
+ * Lives in its own module so the schema can import it without pulling
5
+ * in sandbox-manager (which imports the schema — circular).
6
+ */
7
+ import { isIP } from 'node:net';
8
+ import { stripBrackets } from './parent-proxy.js';
9
+ /**
10
+ * Match a hostname against a domain pattern.
11
+ *
12
+ * Patterns:
13
+ * - `*` matches everything (deny-all; the schema only accepts this in
14
+ * deniedDomains).
15
+ * - `*.example.com` matches any strict subdomain of example.com.
16
+ * - anything else matches exactly (case-insensitive).
17
+ *
18
+ * Wildcard suffix matching is refused for IP literals so an IPv6 zone-ID
19
+ * payload like `::ffff:1.2.3.4%x.allowed.com` cannot pass `.endsWith()`
20
+ * while the OS connects to the bare IP. isValidHost already rejects `%`,
21
+ * but we refuse here too for defence in depth.
22
+ */
23
+ export function matchesDomainPattern(hostname, pattern) {
24
+ const h = hostname.toLowerCase();
25
+ if (pattern === '*')
26
+ return true;
27
+ if (pattern.startsWith('*.')) {
28
+ if (isIP(stripBrackets(h)))
29
+ return false;
30
+ const baseDomain = pattern.substring(2).toLowerCase();
31
+ return h.endsWith('.' + baseDomain);
32
+ }
33
+ return h === pattern.toLowerCase();
34
+ }
35
+ /**
36
+ * Decide whether a per-credential `injectHosts` entry is reachable via
37
+ * `network.allowedDomains` — i.e. every concrete host that could match
38
+ * `injectHost` is allowed by at least one entry in `allowedDomains`.
39
+ *
40
+ * For an exact `injectHost` (`api.github.com`) this is just
41
+ * `matchesDomainPattern` against each allowed pattern.
42
+ *
43
+ * For a wildcard `injectHost` (`*.X`), an exact allowedDomain can never
44
+ * cover it (it admits only one host), so coverage requires an allowed
45
+ * wildcard `*.Y` whose base is `X` or an ancestor of `X` — e.g.
46
+ * `*.api.github.com` is covered by `*.github.com`.
47
+ */
48
+ export function isInjectHostCoveredByAllowedDomains(injectHost, allowedDomains) {
49
+ if (!injectHost.startsWith('*.')) {
50
+ return allowedDomains.some(p => matchesDomainPattern(injectHost, p));
51
+ }
52
+ const injectBase = injectHost.slice(2).toLowerCase();
53
+ return allowedDomains.some(p => {
54
+ if (!p.startsWith('*.'))
55
+ return false;
56
+ const allowedBase = p.slice(2).toLowerCase();
57
+ return injectBase === allowedBase || injectBase.endsWith('.' + allowedBase);
58
+ });
59
+ }
60
+ //# sourceMappingURL=domain-pattern.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"domain-pattern.js","sourceRoot":"","sources":["../../src/sandbox/domain-pattern.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAA;AAC/B,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAEjD;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,oBAAoB,CAClC,QAAgB,EAChB,OAAe;IAEf,MAAM,CAAC,GAAG,QAAQ,CAAC,WAAW,EAAE,CAAA;IAChC,IAAI,OAAO,KAAK,GAAG;QAAE,OAAO,IAAI,CAAA;IAChC,IAAI,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QAC7B,IAAI,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;YAAE,OAAO,KAAK,CAAA;QACxC,MAAM,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAA;QACrD,OAAO,CAAC,CAAC,QAAQ,CAAC,GAAG,GAAG,UAAU,CAAC,CAAA;IACrC,CAAC;IACD,OAAO,CAAC,KAAK,OAAO,CAAC,WAAW,EAAE,CAAA;AACpC,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,mCAAmC,CACjD,UAAkB,EAClB,cAAiC;IAEjC,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACjC,OAAO,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,oBAAoB,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,CAAA;IACtE,CAAC;IACD,MAAM,UAAU,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAA;IACpD,OAAO,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE;QAC7B,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,OAAO,KAAK,CAAA;QACrC,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAA;QAC5C,OAAO,UAAU,KAAK,WAAW,IAAI,UAAU,CAAC,QAAQ,CAAC,GAAG,GAAG,WAAW,CAAC,CAAA;IAC7E,CAAC,CAAC,CAAA;AACJ,CAAC"}