@sysid/sandbox-runtime-improved 0.0.61-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.
- package/README.md +76 -15
- package/dist/cli.js +12 -18
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/sandbox/credential-mask-files.d.ts +100 -15
- package/dist/sandbox/credential-mask-files.d.ts.map +1 -1
- package/dist/sandbox/credential-mask-files.js +172 -20
- package/dist/sandbox/credential-mask-files.js.map +1 -1
- package/dist/sandbox/http-proxy.d.ts +1 -1
- package/dist/sandbox/http-proxy.d.ts.map +1 -1
- package/dist/sandbox/http-proxy.js +20 -0
- package/dist/sandbox/http-proxy.js.map +1 -1
- package/dist/sandbox/linux-sandbox-utils.d.ts +5 -0
- package/dist/sandbox/linux-sandbox-utils.d.ts.map +1 -1
- package/dist/sandbox/linux-sandbox-utils.js +29 -19
- package/dist/sandbox/linux-sandbox-utils.js.map +1 -1
- package/dist/sandbox/linux-violation-monitor.d.ts +48 -0
- package/dist/sandbox/linux-violation-monitor.d.ts.map +1 -0
- package/dist/sandbox/linux-violation-monitor.js +156 -0
- package/dist/sandbox/linux-violation-monitor.js.map +1 -0
- package/dist/sandbox/macos-sandbox-utils.js +3 -3
- package/dist/sandbox/macos-sandbox-utils.js.map +1 -1
- package/dist/sandbox/mitm-ca.d.ts +52 -1
- package/dist/sandbox/mitm-ca.d.ts.map +1 -1
- package/dist/sandbox/mitm-ca.js +143 -11
- package/dist/sandbox/mitm-ca.js.map +1 -1
- package/dist/sandbox/mitm-leaf.d.ts +1 -1
- package/dist/sandbox/mitm-leaf.d.ts.map +1 -1
- package/dist/sandbox/mitm-leaf.js +21 -32
- package/dist/sandbox/mitm-leaf.js.map +1 -1
- package/dist/sandbox/sandbox-config.d.ts +231 -40
- package/dist/sandbox/sandbox-config.d.ts.map +1 -1
- package/dist/sandbox/sandbox-config.js +161 -32
- package/dist/sandbox/sandbox-config.js.map +1 -1
- package/dist/sandbox/sandbox-manager.d.ts +1 -1
- package/dist/sandbox/sandbox-manager.d.ts.map +1 -1
- package/dist/sandbox/sandbox-manager.js +331 -252
- package/dist/sandbox/sandbox-manager.js.map +1 -1
- package/dist/sandbox/sandbox-utils.d.ts +13 -0
- package/dist/sandbox/sandbox-utils.d.ts.map +1 -1
- package/dist/sandbox/sandbox-utils.js +32 -7
- package/dist/sandbox/sandbox-utils.js.map +1 -1
- package/dist/sandbox/windows-sandbox-utils.d.ts +353 -297
- package/dist/sandbox/windows-sandbox-utils.d.ts.map +1 -1
- package/dist/sandbox/windows-sandbox-utils.js +516 -396
- package/dist/sandbox/windows-sandbox-utils.js.map +1 -1
- package/dist/utils/shell-quote.d.ts +27 -0
- package/dist/utils/shell-quote.d.ts.map +1 -0
- package/dist/utils/shell-quote.js +47 -0
- package/dist/utils/shell-quote.js.map +1 -0
- package/package.json +1 -3
- package/vendor/seccomp/arm64/apply-seccomp +0 -0
- package/vendor/seccomp/x64/apply-seccomp +0 -0
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
|
+
import * as net from 'node:net';
|
|
2
3
|
import * as path from 'node:path';
|
|
3
4
|
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { once } from 'node:events';
|
|
4
6
|
import { fileURLToPath } from 'node:url';
|
|
5
7
|
import { logForDebugging } from '../utils/debug.js';
|
|
6
|
-
import {
|
|
8
|
+
import { generateProxyEnvVars, normalizePathForSandbox, containsGlobCharsWin, expandGlobPattern, } from './sandbox-utils.js';
|
|
7
9
|
// Re-export so existing tests (glob-expand.test.ts) and any
|
|
8
10
|
// out-of-tree caller keep their import path.
|
|
9
11
|
export { containsGlobCharsWin, stripExtendedPathPrefix, } from './sandbox-utils.js';
|
|
@@ -11,28 +13,31 @@ export { containsGlobCharsWin, stripExtendedPathPrefix, } from './sandbox-utils.
|
|
|
11
13
|
* Windows sandbox backend.
|
|
12
14
|
*
|
|
13
15
|
* Network isolation is enforced by `srt-win.exe` — a Rust helper that
|
|
14
|
-
*
|
|
15
|
-
* keyed on that
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* provisions a dedicated `srt-sandbox` local user account, installs a
|
|
17
|
+
* machine-wide WFP filter set keyed on that account's SID, and
|
|
18
|
+
* provides an `exec` subcommand that spawns the target via a two-hop
|
|
19
|
+
* launch (broker → `CreateProcessWithLogonW(runner)` → runner →
|
|
20
|
+
* restricted-token child) under `srt-sandbox`. The sandboxed child
|
|
21
|
+
* reaches the host only via the JS mux proxy, which the caller
|
|
22
|
+
* passes in via `--env`.
|
|
23
|
+
*
|
|
24
|
+
* The separate-user account structurally closes the surrogate-spawn
|
|
25
|
+
* class (schtasks, `PROC_THREAD_ATTRIBUTE_PARENT_PROCESS`, BITS,
|
|
26
|
+
* RunAs="Interactive User" COM): the child's token carries a
|
|
27
|
+
* different user SID, so it cannot reach real-user processes, tasks
|
|
28
|
+
* register under `srt-sandbox`, and the user-SID WFP filter fences
|
|
29
|
+
* `srt-sandbox` egress regardless of how the child was spawned.
|
|
19
30
|
*
|
|
20
31
|
* This module is a thin wrapper around the `srt-win` CLI; all status
|
|
21
|
-
* comes from live enumeration
|
|
22
|
-
* token-membership check; WFP via providerData-tag enumeration under
|
|
23
|
-
* the configured sublayer). There is no marker file.
|
|
32
|
+
* comes from live enumeration. There is no marker file.
|
|
24
33
|
*
|
|
25
|
-
* Filesystem
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* allow-list on its immediate parent directory, with restore state
|
|
29
|
-
* sealed by an inert hash-ACE marker so the on-disk SD is
|
|
30
|
-
* self-authenticating. See {@link stampWindowsAcl}.
|
|
34
|
+
* Filesystem rules (`denyRead`/`denyWrite`/`allowRead`/`allowWrite`)
|
|
35
|
+
* are enforced via additive explicit ACEs for `<sb-SID>` — see
|
|
36
|
+
* {@link grantWindowsAcl} / {@link stampWindowsAcl}.
|
|
31
37
|
*/
|
|
32
38
|
// ────────────────────────────────────────────────────────────────────
|
|
33
39
|
// Types
|
|
34
40
|
// ────────────────────────────────────────────────────────────────────
|
|
35
|
-
export const DEFAULT_WINDOWS_GROUP_NAME = 'sandbox-runtime-net';
|
|
36
41
|
export const DEFAULT_WINDOWS_PROXY_PORT_RANGE = [
|
|
37
42
|
60080, 60089,
|
|
38
43
|
];
|
|
@@ -90,134 +95,248 @@ function repoRoot() {
|
|
|
90
95
|
}
|
|
91
96
|
const nodeArchToDir = { x64: 'x64', arm64: 'arm64' };
|
|
92
97
|
/**
|
|
93
|
-
* Locate `srt-win.exe`. Resolution order:
|
|
94
|
-
* 1. `
|
|
95
|
-
* 2. `<root>/vendor/srt-win/{arch}/srt-win.exe` (prebuilt — published npm
|
|
98
|
+
* Locate the packaged `srt-win.exe`. Resolution order:
|
|
99
|
+
* 1. `<root>/vendor/srt-win/{arch}/srt-win.exe` (prebuilt — published npm
|
|
96
100
|
* package, or after `npm run build:srt-win` locally).
|
|
97
|
-
*
|
|
101
|
+
* 2. `<root>/vendor/srt-win-src/target/release/srt-win.exe` (local
|
|
98
102
|
* `cargo build --release` fallback for development).
|
|
99
|
-
* 4. `<root>/vendor/srt-win/target/release/srt-win.exe` (transitional:
|
|
100
|
-
* stale local build from before the srt-win-src rename).
|
|
101
103
|
*
|
|
102
104
|
* `<root>` is {@link repoRoot} — `__dirname/../..`, which resolves to the
|
|
103
105
|
* repo root from `src/sandbox/` and `dist/sandbox/` alike, and to the
|
|
104
106
|
* package root when installed under `node_modules`.
|
|
105
107
|
*
|
|
108
|
+
* Callers that ship their own binary (or a multicall binary that
|
|
109
|
+
* routes on `argv[1] == `{@link SRT_WIN_DISPATCH_ARG1}) pass
|
|
110
|
+
* `windows.srtWin` instead of relying on this lookup — see
|
|
111
|
+
* {@link resolveSrtWin}.
|
|
112
|
+
*
|
|
106
113
|
* Resolution via the optional `@anthropic-ai/sandbox-runtime-win32-*`
|
|
107
114
|
* platform packages is added separately.
|
|
108
115
|
*
|
|
109
116
|
* @throws if none exist.
|
|
110
117
|
*/
|
|
111
118
|
export function getSrtWinPath() {
|
|
112
|
-
const envPath = process.env.SRT_WIN_PATH;
|
|
113
|
-
if (envPath && fs.existsSync(envPath)) {
|
|
114
|
-
return envPath;
|
|
115
|
-
}
|
|
116
119
|
const root = repoRoot();
|
|
117
120
|
const arch = nodeArchToDir[process.arch];
|
|
118
121
|
const candidates = [];
|
|
119
122
|
if (arch) {
|
|
120
123
|
candidates.push(path.join(root, 'vendor', 'srt-win', arch, 'srt-win.exe'));
|
|
121
124
|
}
|
|
122
|
-
candidates.push(path.join(root, 'vendor', 'srt-win-src', 'target', 'release', 'srt-win.exe')
|
|
123
|
-
// transitional: stale local build from before the srt-win-src rename
|
|
124
|
-
path.join(root, 'vendor', 'srt-win', 'target', 'release', 'srt-win.exe'));
|
|
125
|
+
candidates.push(path.join(root, 'vendor', 'srt-win-src', 'target', 'release', 'srt-win.exe'));
|
|
125
126
|
for (const c of candidates) {
|
|
126
127
|
if (fs.existsSync(c))
|
|
127
128
|
return c;
|
|
128
129
|
}
|
|
129
|
-
throw new Error(`srt-win.exe not found. Set
|
|
130
|
+
throw new Error(`srt-win.exe not found. Set windows.srtWin.path or build with ` +
|
|
130
131
|
`\`cargo build --release --manifest-path vendor/srt-win-src/Cargo.toml\`. ` +
|
|
131
|
-
`Looked in: ${
|
|
132
|
+
`Looked in: ${candidates.join(', ')}`);
|
|
132
133
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
134
|
+
/**
|
|
135
|
+
* `argv[1]` sentinel a multicall embedder's dispatcher matches
|
|
136
|
+
* against to route into `srt_win::run_from_args`. Mirrors the Rust
|
|
137
|
+
* `srt_win::SRT_WIN_DISPATCH_ARG1`; the two MUST stay in sync.
|
|
138
|
+
* `run_from_args` strips it before clap, so the standalone binary
|
|
139
|
+
* accepts it harmlessly.
|
|
140
|
+
*/
|
|
141
|
+
export const SRT_WIN_DISPATCH_ARG1 = '--srt-win';
|
|
142
|
+
/**
|
|
143
|
+
* Resolve the `srt-win` spawn target from config. When `cfg.path` is
|
|
144
|
+
* set it is used verbatim (no fallback to the packaged binary — an
|
|
145
|
+
* explicit override is a directive, not a hint) and
|
|
146
|
+
* {@link SRT_WIN_DISPATCH_ARG1} is prepended so a multicall
|
|
147
|
+
* dispatcher routes on `argv[1]`. When unset, falls back to
|
|
148
|
+
* {@link getSrtWinPath} with no sentinel (the packaged binary
|
|
149
|
+
* doesn't need it; `run_from_args` would strip it anyway).
|
|
150
|
+
*/
|
|
151
|
+
export function resolveSrtWin(cfg) {
|
|
152
|
+
if (cfg?.path !== undefined) {
|
|
153
|
+
if (!fs.existsSync(cfg.path)) {
|
|
154
|
+
throw new Error(`windows.srtWin.path is set to '${cfg.path}' but the file does ` +
|
|
155
|
+
`not exist; remove srtWin.path to fall back to the packaged ` +
|
|
156
|
+
`binary`);
|
|
157
|
+
}
|
|
158
|
+
return { exe: cfg.path, prependArgs: [SRT_WIN_DISPATCH_ARG1] };
|
|
159
|
+
}
|
|
160
|
+
return { exe: getSrtWinPath(), prependArgs: [] };
|
|
140
161
|
}
|
|
141
|
-
function runSrtWin(args,
|
|
142
|
-
|
|
143
|
-
|
|
162
|
+
function runSrtWin(args, opts = {}) {
|
|
163
|
+
// Direct callers of the exported helpers may omit `srtWin`
|
|
164
|
+
// (backward-compat) — fall back to the packaged-binary lookup.
|
|
165
|
+
// `SandboxManager` resolves once at `initialize()` and threads the
|
|
166
|
+
// handle, so this per-call resolve is only hit outside a session.
|
|
167
|
+
const { exe, prependArgs } = opts.srtWin ?? resolveSrtWin();
|
|
168
|
+
const r = spawnSync(exe, [...prependArgs, ...args], {
|
|
144
169
|
encoding: 'utf8',
|
|
145
|
-
timeout: timeoutMs,
|
|
146
|
-
...(stdin !== undefined
|
|
170
|
+
timeout: opts.timeoutMs ?? 15000,
|
|
171
|
+
...(opts.stdin !== undefined ? { input: opts.stdin } : {}),
|
|
147
172
|
});
|
|
148
173
|
if (r.error) {
|
|
149
174
|
throw new Error(`srt-win ${args[0]}: spawn failed: ${r.error.message}`);
|
|
150
175
|
}
|
|
151
176
|
return {
|
|
152
177
|
status: r.status,
|
|
178
|
+
signal: r.signal,
|
|
153
179
|
stdout: (r.stdout ?? '').trim(),
|
|
154
180
|
stderr: (r.stderr ?? '').trim(),
|
|
155
181
|
};
|
|
156
182
|
}
|
|
157
183
|
function runSrtWinJson(args, opts) {
|
|
158
|
-
const r = runSrtWin(args,
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
let json;
|
|
163
|
-
let parseErr;
|
|
184
|
+
const r = runSrtWin(args, opts);
|
|
185
|
+
if (r.status !== 0) {
|
|
186
|
+
throw new Error(`srt-win ${args.join(' ')} exited ${r.status}: ${r.stderr || r.stdout}`);
|
|
187
|
+
}
|
|
164
188
|
try {
|
|
165
|
-
|
|
189
|
+
return JSON.parse(r.stdout);
|
|
166
190
|
}
|
|
167
191
|
catch (e) {
|
|
168
|
-
|
|
192
|
+
throw new Error(`srt-win ${args.join(' ')}: unparseable JSON output ` +
|
|
193
|
+
`${JSON.stringify(r.stdout)}: ${e.message}`);
|
|
169
194
|
}
|
|
170
|
-
|
|
171
|
-
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* As {@link runSrtWinJson} but parses stdout BEFORE checking the
|
|
198
|
+
* exit code, so a non-zero exit with the per-path JSON intact
|
|
199
|
+
* still surfaces every entry. For best-effort teardown helpers
|
|
200
|
+
* (`acl restore`/`acl revoke`).
|
|
201
|
+
*/
|
|
202
|
+
function runSrtWinJsonAllowFail(args, opts) {
|
|
203
|
+
const r = runSrtWin(args, opts);
|
|
204
|
+
let json;
|
|
205
|
+
try {
|
|
206
|
+
json = JSON.parse(r.stdout);
|
|
172
207
|
}
|
|
173
|
-
|
|
174
|
-
throw new Error(`srt-win ${args.join(' ')}
|
|
208
|
+
catch (e) {
|
|
209
|
+
throw new Error(`srt-win ${args.join(' ')}: unparseable JSON output ` +
|
|
210
|
+
`${JSON.stringify(r.stdout)}: ${e.message}`);
|
|
175
211
|
}
|
|
176
|
-
|
|
177
|
-
return json;
|
|
178
|
-
throw new Error(`srt-win ${args.join(' ')}: unparseable JSON output ` +
|
|
179
|
-
`${JSON.stringify(r.stdout)}: ${parseErr}`);
|
|
212
|
+
return { ok: r.status === 0, json, stderr: r.stderr };
|
|
180
213
|
}
|
|
181
214
|
// ────────────────────────────────────────────────────────────────────
|
|
182
215
|
// Status / install API
|
|
183
216
|
// ────────────────────────────────────────────────────────────────────
|
|
184
217
|
/**
|
|
185
|
-
* Query the
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
/**
|
|
195
|
-
* Query the WFP filter set under the given sublayer. `installed` means
|
|
196
|
-
* srt-win-tagged `permit-group` AND `block` filters are both present
|
|
197
|
-
* under that sublayer. Detection is **tag-based** (providerData JSON);
|
|
198
|
-
* filters installed by other tooling without the tag are not counted.
|
|
218
|
+
* Query the WFP filter set under the given sublayer via live BFE
|
|
219
|
+
* enumeration. `installed` means at least one srt-win-tagged
|
|
220
|
+
* `block-user` filter is present. Detection is **tag-based**
|
|
221
|
+
* (providerData JSON); filters installed by other tooling without the
|
|
222
|
+
* tag are not counted.
|
|
223
|
+
*
|
|
224
|
+
* BFE enumeration is admin-gated — a non-elevated caller gets
|
|
225
|
+
* `state:"cannot-read"` with a `hint` (not an error). The
|
|
226
|
+
* non-elevated readiness check is {@link verifyWindowsWfpEgress}.
|
|
199
227
|
*/
|
|
200
228
|
export function getWindowsWfpStatus(opts = {}) {
|
|
201
229
|
const args = ['wfp', 'status'];
|
|
202
230
|
if (opts.sublayerGuid)
|
|
203
231
|
args.push('--sublayer-guid', opts.sublayerGuid);
|
|
204
|
-
const raw = runSrtWinJson(args);
|
|
232
|
+
const raw = runSrtWinJson(args, { srtWin: opts.srtWin });
|
|
205
233
|
return {
|
|
206
234
|
state: raw.state,
|
|
207
235
|
filters: raw.filters,
|
|
208
236
|
...(raw.port_range && { portRange: raw.port_range }),
|
|
209
|
-
userFilters: raw.user_filters ?? 0,
|
|
210
237
|
...(raw.user_sid && { userSid: raw.user_sid }),
|
|
238
|
+
...(raw.hint && { hint: raw.hint }),
|
|
211
239
|
};
|
|
212
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* Behavioral proof that the WFP egress fence is active for the
|
|
243
|
+
* sandbox user. Binds a local listener on an ephemeral loopback port
|
|
244
|
+
* outside the WFP loopback-permit range, then spawns `srt-win
|
|
245
|
+
* runner` as the sandbox user (via `CreateProcessWithLogonW`) to
|
|
246
|
+
* attempt a direct TCP connect to it. The WFP block-user filter
|
|
247
|
+
* fires at `ALE_AUTH_CONNECT` — before any packet leaves — so an
|
|
248
|
+
* active fence yields WSAEACCES immediately and a missing fence lets
|
|
249
|
+
* the connect through (the kernel completes the handshake against
|
|
250
|
+
* the listening socket's backlog; no event-loop tick required, so
|
|
251
|
+
* the synchronous `runSrtWin` is safe). Does not require elevation
|
|
252
|
+
* and does not depend on any external host.
|
|
253
|
+
*
|
|
254
|
+
* `initialize()` calls this once per session, so a stale install
|
|
255
|
+
* (sandbox user provisioned but filters since removed) fails closed
|
|
256
|
+
* at session start instead of running every exec with full egress.
|
|
257
|
+
*
|
|
258
|
+
* @param opts.target overrides the probe target (skips the local
|
|
259
|
+
* listener bind).
|
|
260
|
+
* @param opts.proxyPortRange the WFP loopback-permit range the
|
|
261
|
+
* listener must avoid. Default
|
|
262
|
+
* {@link DEFAULT_WINDOWS_PROXY_PORT_RANGE}.
|
|
263
|
+
* @throws on any outcome other than `blocked` (exit 0).
|
|
264
|
+
*/
|
|
265
|
+
export async function verifyWindowsWfpEgress(opts = {}) {
|
|
266
|
+
let target = opts.target;
|
|
267
|
+
let server;
|
|
268
|
+
if (!target) {
|
|
269
|
+
// Bind ephemeral; retry if it lands inside the WFP
|
|
270
|
+
// loopback-permit range (a port in-range would be PERMITted
|
|
271
|
+
// even with the fence active → false `connected`).
|
|
272
|
+
const [lo, hi] = opts.proxyPortRange ?? DEFAULT_WINDOWS_PROXY_PORT_RANGE;
|
|
273
|
+
for (let i = 0; i < 5; i++) {
|
|
274
|
+
const s = net.createServer();
|
|
275
|
+
s.listen(0, '127.0.0.1');
|
|
276
|
+
await once(s, 'listening');
|
|
277
|
+
const p = s.address().port;
|
|
278
|
+
if (p < lo || p > hi) {
|
|
279
|
+
server = s;
|
|
280
|
+
target = `127.0.0.1:${p}`;
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
s.close();
|
|
284
|
+
}
|
|
285
|
+
if (!target) {
|
|
286
|
+
throw new Error(`verifyWindowsWfpEgress: could not bind a loopback ` +
|
|
287
|
+
`listener outside the WFP permit range [${lo},${hi}] in ` +
|
|
288
|
+
`5 attempts`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
try {
|
|
292
|
+
// 30s: first call after install may create the sandbox user's
|
|
293
|
+
// profile (LOGON_WITH_PROFILE) via CreateProcessWithLogonW —
|
|
294
|
+
// same budget as windowsTrustCa, plus the runner's own 2s
|
|
295
|
+
// connect timeout.
|
|
296
|
+
const r = runSrtWin(['wfp', 'verify', '--target', target], {
|
|
297
|
+
timeoutMs: 30000,
|
|
298
|
+
srtWin: opts.srtWin,
|
|
299
|
+
});
|
|
300
|
+
logForDebugging(`[Sandbox Windows] wfp verify exit=${r.status}: ${r.stderr || r.stdout}`);
|
|
301
|
+
let raw;
|
|
302
|
+
try {
|
|
303
|
+
raw = JSON.parse(r.stdout);
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
// status=null → spawnSync killed the child (timeout or external
|
|
307
|
+
// signal). Include signal + stderr so the CI log self-explains
|
|
308
|
+
// instead of just `exited null with unparseable output ""`.
|
|
309
|
+
throw new Error(`WFP egress fence could not be verified — \`srt-win wfp ` +
|
|
310
|
+
`verify\` exited ${r.status}` +
|
|
311
|
+
(r.signal ? ` (signal ${r.signal})` : '') +
|
|
312
|
+
` with unparseable output ${JSON.stringify(r.stdout)} ` +
|
|
313
|
+
`(stderr: ${JSON.stringify(r.stderr)})`);
|
|
314
|
+
}
|
|
315
|
+
if (r.status === 3) {
|
|
316
|
+
throw new Error(`WFP egress fence is not active — direct outbound from the ` +
|
|
317
|
+
`sandbox user to ${raw.target} succeeded. Re-run ` +
|
|
318
|
+
`\`srt-win install\` (one UAC prompt). (${r.stderr})`);
|
|
319
|
+
}
|
|
320
|
+
if (r.status !== 0) {
|
|
321
|
+
throw new Error(`WFP egress fence could not be verified — probe to ` +
|
|
322
|
+
`${raw.target} was '${raw.egress_probe}' (exit ` +
|
|
323
|
+
`${r.status}). The fence may be absent. Re-run \`srt-win ` +
|
|
324
|
+
`install\`. (${r.stderr})`);
|
|
325
|
+
}
|
|
326
|
+
return { target: raw.target, stderr: r.stderr };
|
|
327
|
+
}
|
|
328
|
+
finally {
|
|
329
|
+
server?.close();
|
|
330
|
+
}
|
|
331
|
+
}
|
|
213
332
|
/**
|
|
214
333
|
* Query the sandbox user account's provisioning state. Each field
|
|
215
334
|
* is independently observed so a half-provisioned install (e.g.
|
|
216
335
|
* user exists but credential file missing) is distinguishable.
|
|
217
336
|
* Does not require elevation.
|
|
218
337
|
*/
|
|
219
|
-
export function getWindowsSandboxUserStatus() {
|
|
220
|
-
const raw = runSrtWinJson(['user', 'status']);
|
|
338
|
+
export function getWindowsSandboxUserStatus(opts = {}) {
|
|
339
|
+
const raw = runSrtWinJson(['user', 'status'], { srtWin: opts.srtWin });
|
|
221
340
|
return {
|
|
222
341
|
provisioned: raw.user.exists,
|
|
223
342
|
...(raw.user.sid && { sid: raw.user.sid }),
|
|
@@ -230,6 +349,7 @@ export function getWindowsSandboxUserStatus() {
|
|
|
230
349
|
...(typeof raw.marker_version === 'number' && {
|
|
231
350
|
markerVersion: raw.marker_version,
|
|
232
351
|
}),
|
|
352
|
+
realUserSid: raw.real_user_sid,
|
|
233
353
|
...(raw.ca_cert_thumb && { caCertThumb: raw.ca_cert_thumb }),
|
|
234
354
|
...(raw.ca_cert_pem && { caCertPem: raw.ca_cert_pem }),
|
|
235
355
|
};
|
|
@@ -240,18 +360,18 @@ export function getWindowsSandboxUserStatus() {
|
|
|
240
360
|
* Returns `null` when no CA was installed. The PEM is what `srt-win
|
|
241
361
|
* user status` reconstructs from the DER stored in `state.db`.
|
|
242
362
|
*
|
|
243
|
-
* On Windows
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
363
|
+
* On Windows, `tlsTerminate` requires this CA to be present in the
|
|
364
|
+
* sandbox user's `CurrentUser\Root` (schannel-level trust is an
|
|
365
|
+
* install-time concern, not per-session); the host calls this from
|
|
366
|
+
* `initialize()` to fail early with an actionable message when it
|
|
367
|
+
* isn't.
|
|
248
368
|
*
|
|
249
369
|
* @param status pass an already-fetched
|
|
250
370
|
* {@link getWindowsSandboxUserStatus} result to avoid a second
|
|
251
371
|
* `srt-win user status` spawn.
|
|
252
372
|
*/
|
|
253
|
-
export function getWindowsSandboxCaCert(status) {
|
|
254
|
-
const u = status ?? getWindowsSandboxUserStatus();
|
|
373
|
+
export function getWindowsSandboxCaCert(status, opts = {}) {
|
|
374
|
+
const u = status ?? getWindowsSandboxUserStatus(opts);
|
|
255
375
|
if (!u.caCertThumb || !u.caCertPem)
|
|
256
376
|
return null;
|
|
257
377
|
return { pem: u.caCertPem, thumb: u.caCertThumb };
|
|
@@ -266,17 +386,19 @@ export function getWindowsSandboxCaCert(status) {
|
|
|
266
386
|
*
|
|
267
387
|
* The CA has a separate lifecycle from {@link installWindowsSandbox}
|
|
268
388
|
* — install provisions the account/filters and never touches the CA;
|
|
269
|
-
* call this AFTER install when `tlsTerminate` will be used
|
|
270
|
-
* `windows.asSandboxUser`.
|
|
389
|
+
* call this AFTER install when `tlsTerminate` will be used.
|
|
271
390
|
*
|
|
272
391
|
* @throws when the sandbox user is not provisioned, the file is not a
|
|
273
392
|
* parseable X.509 certificate, or the registry write into the
|
|
274
393
|
* sandbox user's hive fails.
|
|
275
394
|
*/
|
|
276
|
-
export function windowsTrustCa(caCertPath) {
|
|
395
|
+
export function windowsTrustCa(caCertPath, opts = {}) {
|
|
277
396
|
// 60s: first call may create the sandbox user's profile
|
|
278
397
|
// (LOGON_WITH_PROFILE) via the one-shot CreateProcessWithLogonW.
|
|
279
|
-
const r = runSrtWin(['user', 'trust-ca', caCertPath],
|
|
398
|
+
const r = runSrtWin(['user', 'trust-ca', caCertPath], {
|
|
399
|
+
timeoutMs: 60000,
|
|
400
|
+
srtWin: opts.srtWin,
|
|
401
|
+
});
|
|
280
402
|
logForDebugging(`[Sandbox Windows] user trust-ca exit=${r.status}: ${r.stderr || r.stdout}`);
|
|
281
403
|
if (r.status !== 0) {
|
|
282
404
|
throw new Error(`srt-win user trust-ca '${caCertPath}' failed (exit ` +
|
|
@@ -284,92 +406,72 @@ export function windowsTrustCa(caCertPath) {
|
|
|
284
406
|
}
|
|
285
407
|
}
|
|
286
408
|
/**
|
|
287
|
-
* One-shot install:
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
*
|
|
409
|
+
* One-shot install: provisions the `srt-sandbox` user account and
|
|
410
|
+
* installs the user-SID-keyed WFP filter set — all in a single
|
|
411
|
+
* self-elevating process (one UAC prompt). Idempotent; re-running
|
|
412
|
+
* rotates the sandbox user's password.
|
|
291
413
|
*
|
|
292
|
-
* Network for the calling user is **not disrupted
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
* filter-1 (PERMIT group-enabled) takes over for the broker; only
|
|
297
|
-
* `srt-win exec` children (group flipped deny-only) fall through to
|
|
298
|
-
* the loopback/BLOCK filters.
|
|
414
|
+
* Network for the calling user is **not disrupted**: the filters key
|
|
415
|
+
* on the `srt-sandbox` user's SID, so the broker, services, and
|
|
416
|
+
* every other principal fall through to default-permit. No logout
|
|
417
|
+
* is required.
|
|
299
418
|
*
|
|
300
|
-
* Returns the post-call
|
|
301
|
-
* UAC prompt this returns `{cancelled: true, …}` rather
|
|
302
|
-
* throwing — cancellation is a user choice, not an error.
|
|
419
|
+
* Returns the post-call WFP + sandbox-user state. If the user
|
|
420
|
+
* cancels the UAC prompt this returns `{cancelled: true, …}` rather
|
|
421
|
+
* than throwing — cancellation is a user choice, not an error.
|
|
303
422
|
*
|
|
304
|
-
* @throws on
|
|
305
|
-
*
|
|
306
|
-
*
|
|
423
|
+
* @throws on user/WFP creation failure, or if filters already exist
|
|
424
|
+
* under `sublayerGuid` with a different port range and `force` is
|
|
425
|
+
* not set.
|
|
307
426
|
*/
|
|
308
427
|
export function installWindowsSandbox(opts = {}) {
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
args.push('--user-sid', opts.userSid);
|
|
428
|
+
const srtWin = opts.srtWin ?? resolveSrtWin();
|
|
429
|
+
const args = ['install'];
|
|
312
430
|
if (opts.sublayerGuid)
|
|
313
431
|
args.push('--sublayer-guid', opts.sublayerGuid);
|
|
314
432
|
if (opts.proxyPortRange) {
|
|
315
433
|
args.push('--proxy-port-range', `${opts.proxyPortRange[0]}-${opts.proxyPortRange[1]}`);
|
|
316
434
|
}
|
|
435
|
+
if (opts.sandboxUser)
|
|
436
|
+
args.push('--sandbox-user', opts.sandboxUser);
|
|
317
437
|
if (opts.force)
|
|
318
438
|
args.push('--force');
|
|
319
|
-
const r = runSrtWin(args,
|
|
439
|
+
const r = runSrtWin(args, { timeoutMs: 60000, srtWin });
|
|
320
440
|
logForDebugging(`[Sandbox Windows] install exit=${r.status}: ${r.stderr || r.stdout}`);
|
|
321
441
|
// srt-win install exit-code contract:
|
|
322
442
|
// 0 ok
|
|
323
443
|
// 10 user cancelled UAC elevation
|
|
324
|
-
// 11 group create failed
|
|
325
444
|
// 12 WFP install failed
|
|
326
445
|
// 13 already installed with different config (use --force)
|
|
327
446
|
// 14 sandbox-user provisioning failed
|
|
328
447
|
// 1 other error (stderr has detail)
|
|
329
448
|
const out = r.stderr || r.stdout;
|
|
449
|
+
const readBack = () => ({
|
|
450
|
+
wfp: getWindowsWfpStatus({ sublayerGuid: opts.sublayerGuid, srtWin }),
|
|
451
|
+
user: getWindowsSandboxUserStatus({ srtWin }),
|
|
452
|
+
});
|
|
330
453
|
switch (r.status) {
|
|
331
454
|
case 0:
|
|
332
|
-
|
|
455
|
+
return readBack();
|
|
333
456
|
case 10:
|
|
334
|
-
return {
|
|
335
|
-
group: getWindowsGroupStatus(opts),
|
|
336
|
-
wfp: getWindowsWfpStatus({ sublayerGuid: opts.sublayerGuid }),
|
|
337
|
-
user: getWindowsSandboxUserStatus(),
|
|
338
|
-
cancelled: true,
|
|
339
|
-
};
|
|
340
|
-
case 11:
|
|
341
|
-
throw new Error(`srt-win install: group create failed: ${out}`);
|
|
457
|
+
return { ...readBack(), cancelled: true };
|
|
342
458
|
case 12:
|
|
343
459
|
throw new Error(`srt-win install: WFP filter install failed: ${out}`);
|
|
344
460
|
case 14:
|
|
345
461
|
throw new Error(`srt-win install: sandbox user provisioning failed: ${out}`);
|
|
346
462
|
case 13:
|
|
347
463
|
throw new Error(`srt-win install: filters already exist under this sublayer with ` +
|
|
348
|
-
`different
|
|
349
|
-
`
|
|
464
|
+
`a different port range or sandbox-user name. Pass ` +
|
|
465
|
+
`{force: true} to replace, or pick a different sublayerGuid. ` +
|
|
350
466
|
`Output: ${out}`);
|
|
351
467
|
default:
|
|
352
468
|
throw new Error(`srt-win install failed (exit ${r.status}): ${out}`);
|
|
353
469
|
}
|
|
354
|
-
return {
|
|
355
|
-
group: getWindowsGroupStatus(opts),
|
|
356
|
-
wfp: getWindowsWfpStatus({ sublayerGuid: opts.sublayerGuid }),
|
|
357
|
-
user: getWindowsSandboxUserStatus(),
|
|
358
|
-
};
|
|
359
470
|
}
|
|
360
471
|
/**
|
|
361
|
-
* Remove the WFP filter set under `sublayerGuid`
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
* **Does NOT delete the discriminator group** — group membership is
|
|
365
|
-
* persistent user state and removing it would force every user to
|
|
366
|
-
* re-do the logout dance on the next install. Call
|
|
367
|
-
* {@link deleteWindowsGroup} explicitly if you want full teardown.
|
|
368
|
-
*
|
|
369
|
-
* **Does** remove the `srt-sandbox` account, its credential file,
|
|
370
|
-
* and the setup marker, unless `keepUser` is set — the credential
|
|
371
|
-
* is useless without the account and vice versa, so they're
|
|
372
|
-
* treated as one unit.
|
|
472
|
+
* Remove the WFP filter set under `sublayerGuid` and the
|
|
473
|
+
* `srt-sandbox` account, its credential file, and the setup marker
|
|
474
|
+
* (one UAC prompt). Idempotent.
|
|
373
475
|
*
|
|
374
476
|
* @returns `{cancelled: true}` if the user dismissed UAC.
|
|
375
477
|
*/
|
|
@@ -379,7 +481,7 @@ export function uninstallWindowsSandbox(opts = {}) {
|
|
|
379
481
|
args.push('--sublayer-guid', opts.sublayerGuid);
|
|
380
482
|
if (opts.keepUser)
|
|
381
483
|
args.push('--keep-user');
|
|
382
|
-
const r = runSrtWin(args);
|
|
484
|
+
const r = runSrtWin(args, { srtWin: opts.srtWin });
|
|
383
485
|
logForDebugging(`[Sandbox Windows] uninstall exit=${r.status}: ${r.stderr || r.stdout}`);
|
|
384
486
|
if (r.status === 10)
|
|
385
487
|
return { cancelled: true };
|
|
@@ -389,78 +491,17 @@ export function uninstallWindowsSandbox(opts = {}) {
|
|
|
389
491
|
return {};
|
|
390
492
|
}
|
|
391
493
|
/**
|
|
392
|
-
*
|
|
393
|
-
*
|
|
394
|
-
*
|
|
395
|
-
*
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
`Requires elevation. Output: ${r.stderr || r.stdout}`);
|
|
402
|
-
}
|
|
403
|
-
logForDebugging(`[Sandbox Windows] group delete: ${r.stderr || r.stdout}`);
|
|
404
|
-
}
|
|
405
|
-
/**
|
|
406
|
-
* Granular primitive: create the discriminator group and add the
|
|
407
|
-
* current user (or `userSid`). Most callers should use
|
|
408
|
-
* {@link installWindowsSandbox} instead; this exists for
|
|
409
|
-
* enterprise/CI flows that manage group and WFP separately.
|
|
410
|
-
* **Requires elevation.** Idempotent.
|
|
411
|
-
*/
|
|
412
|
-
export function createWindowsGroup(ref) {
|
|
413
|
-
const args = ['group', 'create', ...groupRefArgs(ref)];
|
|
414
|
-
if (ref.userSid)
|
|
415
|
-
args.push('--user-sid', ref.userSid);
|
|
416
|
-
const r = runSrtWin(args);
|
|
417
|
-
if (r.status !== 0) {
|
|
418
|
-
throw new Error(`srt-win group create failed (exit ${r.status}). ` +
|
|
419
|
-
`This requires elevation — run as administrator. ` +
|
|
420
|
-
`Output: ${r.stderr || r.stdout}`);
|
|
421
|
-
}
|
|
422
|
-
logForDebugging(`[Sandbox Windows] group create: ${r.stderr || r.stdout}`);
|
|
423
|
-
}
|
|
424
|
-
/**
|
|
425
|
-
* Granular primitive: install the machine-wide WFP filter set
|
|
426
|
-
* under `sublayerGuid` keyed on the group SID. Most callers should
|
|
427
|
-
* use {@link installWindowsSandbox} instead; this exists for
|
|
428
|
-
* enterprise/CI flows that manage group and WFP separately.
|
|
429
|
-
* **Requires elevation.** Idempotent — re-running replaces any
|
|
430
|
-
* existing srt-win-tagged filters under that sublayer.
|
|
494
|
+
* Resolve any Windows filesystem-config path list — `allowRead`/
|
|
495
|
+
* `allowWrite` grants and `denyRead`/`denyWrite` stamps — to
|
|
496
|
+
* concrete existing paths via the single platform-aware
|
|
497
|
+
* {@link normalizePathForSandbox} chokepoint (Linux/macOS parity:
|
|
498
|
+
* point-in-time expansion at session initialize, not per-exec).
|
|
499
|
+
* Glob patterns are expanded; non-glob paths are normalized and
|
|
500
|
+
* returned 1:1. Missing paths are dropped (statSync probe).
|
|
501
|
+
* Directory targets are accepted — the additive sandbox-user ACE
|
|
502
|
+
* carries `(OI)(CI)` so it covers the subtree.
|
|
431
503
|
*/
|
|
432
|
-
export function
|
|
433
|
-
const args = ['wfp', 'install', ...groupRefArgs(ref)];
|
|
434
|
-
if (ref.sublayerGuid)
|
|
435
|
-
args.push('--sublayer-guid', ref.sublayerGuid);
|
|
436
|
-
if (ref.proxyPortRange) {
|
|
437
|
-
args.push('--proxy-port-range', `${ref.proxyPortRange[0]}-${ref.proxyPortRange[1]}`);
|
|
438
|
-
}
|
|
439
|
-
const r = runSrtWin(args);
|
|
440
|
-
if (r.status !== 0) {
|
|
441
|
-
throw new Error(`srt-win wfp install failed (exit ${r.status}). ` +
|
|
442
|
-
`This requires elevation — run as administrator. ` +
|
|
443
|
-
`Output: ${r.stderr || r.stdout}`);
|
|
444
|
-
}
|
|
445
|
-
logForDebugging(`[Sandbox Windows] wfp install: ${r.stderr || r.stdout}`);
|
|
446
|
-
}
|
|
447
|
-
/**
|
|
448
|
-
* Expand the `denyRead`/`denyWrite` input set to a flat list of
|
|
449
|
-
* existing FILE paths for `srt-win acl stamp`.
|
|
450
|
-
*
|
|
451
|
-
* Every input goes through {@link normalizePathForSandbox} (the
|
|
452
|
-
* single Windows-aware chokepoint: `\\?\`/UNC-strip, drive-letter
|
|
453
|
-
* case-fold, ~-expand, realpath). Globs (`*`/`?` only — `[`/`]`
|
|
454
|
-
* are legal Win32 filename chars) expand via the shared walker
|
|
455
|
-
* with case-insensitive matching (point-in-time: a file appearing
|
|
456
|
-
* after this returns is NOT covered). Each candidate is checked
|
|
457
|
-
* with one `statSync({throwIfNoEntry:false})`: missing → drop
|
|
458
|
-
* (the protection model covers files present at session start);
|
|
459
|
-
* directory → reject (the file stamp applies a per-file DACL plus
|
|
460
|
-
* a per-parent-directory allow-list; stamping a directory itself
|
|
461
|
-
* would touch every child); file → keep.
|
|
462
|
-
*/
|
|
463
|
-
export function expandWindowsFsDenyPaths(patterns) {
|
|
504
|
+
export function expandWindowsFsPaths(patterns) {
|
|
464
505
|
const out = new Set();
|
|
465
506
|
for (const raw of patterns) {
|
|
466
507
|
const norm = normalizePathForSandbox(raw);
|
|
@@ -471,32 +512,26 @@ export function expandWindowsFsDenyPaths(patterns) {
|
|
|
471
512
|
const st = fs.statSync(c, { throwIfNoEntry: false });
|
|
472
513
|
if (!st)
|
|
473
514
|
continue;
|
|
474
|
-
if (st.isDirectory()) {
|
|
475
|
-
throw new Error(`Windows fs deny requires explicit file paths; ` +
|
|
476
|
-
`${JSON.stringify(raw)} resolved to directory ` +
|
|
477
|
-
`${JSON.stringify(c)}. Directory targets are not supported.`);
|
|
478
|
-
}
|
|
479
515
|
out.add(c);
|
|
480
516
|
}
|
|
481
517
|
}
|
|
482
518
|
return [...out];
|
|
483
519
|
}
|
|
484
520
|
/**
|
|
485
|
-
* Apply the file-deny
|
|
486
|
-
*
|
|
487
|
-
*
|
|
488
|
-
*
|
|
521
|
+
* Apply the file-deny ACE set for one host session: an additive
|
|
522
|
+
* `(D;OICI;mask;;;<sb-SID>)` on the target plus a
|
|
523
|
+
* `(D;OICI;FILE_DELETE_CHILD;;;<sb-SID>)` on the parent — no
|
|
524
|
+
* PROTECTED rewrite, no SD snapshot. Idempotent and refcounted via
|
|
525
|
+
* srt-win's `working_aces` table.
|
|
489
526
|
*
|
|
490
|
-
* Inputs are passed verbatim to `srt-win` (which canonicalizes
|
|
491
|
-
* rejects
|
|
492
|
-
*
|
|
493
|
-
* pre-expand via {@link expandWindowsFsDenyPaths}.
|
|
527
|
+
* Inputs are passed verbatim to `srt-win` (which canonicalizes and
|
|
528
|
+
* rejects globs). Callers that accept globs should pre-expand via
|
|
529
|
+
* {@link expandWindowsFsPaths}.
|
|
494
530
|
*
|
|
495
531
|
* @throws on exit ≠ 0 — including exit 2 (one or more inputs
|
|
496
532
|
* skipped). srt-win stamps the resolvable inputs before exiting
|
|
497
533
|
* 2, so on throw the caller should call {@link restoreWindowsAcl}
|
|
498
|
-
* to release whatever WAS stamped
|
|
499
|
-
* means tearing down a partial setup).
|
|
534
|
+
* to release whatever WAS stamped.
|
|
500
535
|
*/
|
|
501
536
|
export function stampWindowsAcl(opts) {
|
|
502
537
|
const holder = opts.holderPid ?? process.pid;
|
|
@@ -504,54 +539,55 @@ export function stampWindowsAcl(opts) {
|
|
|
504
539
|
denyRead: opts.denyRead,
|
|
505
540
|
denyWrite: opts.denyWrite,
|
|
506
541
|
});
|
|
507
|
-
const r = runSrtWin([
|
|
542
|
+
const r = runSrtWin([
|
|
543
|
+
'acl',
|
|
544
|
+
'stamp',
|
|
545
|
+
'--holder-pid',
|
|
546
|
+
`${holder}`,
|
|
547
|
+
'--sandbox-user-sid',
|
|
548
|
+
opts.sandboxUserSid,
|
|
549
|
+
], { timeoutMs: 60000, stdin, srtWin: opts.srtWin });
|
|
508
550
|
logForDebugging(`[Sandbox Windows] acl stamp exit=${r.status}: ${r.stderr || r.stdout}`);
|
|
509
551
|
if (r.status !== 0) {
|
|
510
|
-
// exit 2 = partial (some inputs skipped); exit 1 = at least
|
|
511
|
-
// one path could not be stamped. Either is a setup failure.
|
|
512
552
|
throw new Error(`srt-win acl stamp exited ${r.status} ` +
|
|
513
553
|
(r.status === 2 ? '(partial — some inputs skipped)' : '(failed)') +
|
|
514
554
|
`: ${r.stderr || r.stdout}`);
|
|
515
555
|
}
|
|
516
556
|
}
|
|
517
557
|
/**
|
|
518
|
-
* Release this holder's
|
|
519
|
-
*
|
|
520
|
-
*
|
|
521
|
-
*
|
|
522
|
-
*
|
|
523
|
-
*
|
|
524
|
-
* Returns `undefined` when `srt-win acl restore` itself failed
|
|
525
|
-
* (no JSON to parse) — the caller should log and move on rather
|
|
526
|
-
* than block teardown.
|
|
558
|
+
* Release this holder's deny ACEs and remove the sandbox-user ACE
|
|
559
|
+
* on any path whose refcount falls to zero. Best-effort (does not
|
|
560
|
+
* throw on per-path anomalies); returns per-path outcomes for the
|
|
561
|
+
* caller to surface. Returns `undefined` only when `srt-win`
|
|
562
|
+
* itself failed (no JSON to parse).
|
|
527
563
|
*/
|
|
528
564
|
export function restoreWindowsAcl(opts) {
|
|
529
565
|
const holder = opts.holderPid ?? process.pid;
|
|
530
|
-
const args = [
|
|
531
|
-
'acl',
|
|
532
|
-
'restore',
|
|
533
|
-
...groupRefArgs(opts.group),
|
|
534
|
-
'--holder-pid',
|
|
535
|
-
`${holder}`,
|
|
536
|
-
'--json',
|
|
537
|
-
];
|
|
538
566
|
// Don't let a teardown helper throw — the caller's reset() must
|
|
539
|
-
// complete.
|
|
540
|
-
// exit code, so a non-zero exit with the per-path JSON intact
|
|
541
|
-
//
|
|
542
|
-
//
|
|
543
|
-
// instead of collapsing to `undefined`. Only spawn-fail /
|
|
544
|
-
// unparseable output throws → log and return undefined.
|
|
567
|
+
// complete. runSrtWinJsonAllowFail parses stdout before checking
|
|
568
|
+
// the exit code, so a non-zero exit with the per-path JSON intact
|
|
569
|
+
// still surfaces every entry to reset()'s loop. Only spawn-fail
|
|
570
|
+
// / unparseable output throws → log and return undefined.
|
|
545
571
|
try {
|
|
546
|
-
const r =
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
572
|
+
const r = runSrtWinJsonAllowFail([
|
|
573
|
+
'acl',
|
|
574
|
+
'restore',
|
|
575
|
+
'--holder-pid',
|
|
576
|
+
`${holder}`,
|
|
577
|
+
'--sandbox-user-sid',
|
|
578
|
+
opts.sandboxUserSid,
|
|
579
|
+
'--json',
|
|
580
|
+
], { timeoutMs: 60000, srtWin: opts.srtWin });
|
|
550
581
|
if (!r.ok) {
|
|
551
582
|
logForDebugging(`[Sandbox Windows] acl restore exited non-zero (per-path ` +
|
|
552
583
|
`outcomes preserved): ${r.stderr}`, { level: 'error' });
|
|
553
584
|
}
|
|
554
|
-
|
|
585
|
+
// Pre- same-user-removal builds emit `{paths, parents}`; post-
|
|
586
|
+
// emit a flat array. Flatten either so reset()'s logging loop
|
|
587
|
+
// is shape-agnostic across the transition.
|
|
588
|
+
return Array.isArray(r.json)
|
|
589
|
+
? r.json
|
|
590
|
+
: [...(r.json.paths ?? []), ...(r.json.parents ?? [])];
|
|
555
591
|
}
|
|
556
592
|
catch (e) {
|
|
557
593
|
logForDebugging(`[Sandbox Windows] acl restore: ${e.message}`, {
|
|
@@ -561,22 +597,143 @@ export function restoreWindowsAcl(opts) {
|
|
|
561
597
|
}
|
|
562
598
|
}
|
|
563
599
|
/**
|
|
564
|
-
*
|
|
565
|
-
*
|
|
566
|
-
*
|
|
600
|
+
* Apply per-session additive `(OI)(CI)` ALLOW ACEs for the sandbox
|
|
601
|
+
* user on each path. The sandbox user has no inherent rights on
|
|
602
|
+
* real-user-owned files; this is what makes the working tree (and
|
|
603
|
+
* explicit `allowRead`/`allowWrite` paths) reachable from the
|
|
604
|
+
* child. Idempotent and refcounted via srt-win's `working_aces`
|
|
605
|
+
* table.
|
|
606
|
+
*
|
|
607
|
+
* @throws on exit ≠ 0. On throw the caller should call
|
|
608
|
+
* {@link revokeWindowsAcl} to release whatever WAS granted.
|
|
567
609
|
*/
|
|
568
|
-
export
|
|
569
|
-
|
|
570
|
-
|
|
610
|
+
export function grantWindowsAcl(opts) {
|
|
611
|
+
const holder = opts.holderPid ?? process.pid;
|
|
612
|
+
const stdin = JSON.stringify({ read: opts.read, write: opts.write });
|
|
613
|
+
const r = runSrtWin([
|
|
614
|
+
'acl',
|
|
615
|
+
'grant',
|
|
616
|
+
'--holder-pid',
|
|
617
|
+
`${holder}`,
|
|
618
|
+
'--sandbox-user-sid',
|
|
619
|
+
opts.sandboxUserSid,
|
|
620
|
+
], { timeoutMs: 60000, stdin, srtWin: opts.srtWin });
|
|
621
|
+
logForDebugging(`[Sandbox Windows] acl grant exit=${r.status}: ${r.stderr || r.stdout}`);
|
|
622
|
+
if (r.status !== 0) {
|
|
623
|
+
throw new Error(`srt-win acl grant exited ${r.status}: ${r.stderr || r.stdout}`);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
571
626
|
/**
|
|
572
|
-
*
|
|
573
|
-
*
|
|
574
|
-
*
|
|
627
|
+
* Release this holder's grants and remove the sandbox-user ACE on
|
|
628
|
+
* any path whose refcount falls to zero. Best-effort (does not
|
|
629
|
+
* throw); logs anomalies.
|
|
575
630
|
*/
|
|
576
|
-
export
|
|
631
|
+
export function revokeWindowsAcl(opts) {
|
|
632
|
+
const holder = opts.holderPid ?? process.pid;
|
|
633
|
+
try {
|
|
634
|
+
const r = runSrtWinJsonAllowFail([
|
|
635
|
+
'acl',
|
|
636
|
+
'revoke',
|
|
637
|
+
'--holder-pid',
|
|
638
|
+
`${holder}`,
|
|
639
|
+
'--sandbox-user-sid',
|
|
640
|
+
opts.sandboxUserSid,
|
|
641
|
+
'--json',
|
|
642
|
+
], { timeoutMs: 60000, srtWin: opts.srtWin });
|
|
643
|
+
if (!r.ok) {
|
|
644
|
+
logForDebugging(`[Sandbox Windows] acl revoke exited non-zero: ${r.stderr}`, { level: 'error' });
|
|
645
|
+
}
|
|
646
|
+
return r.json;
|
|
647
|
+
}
|
|
648
|
+
catch (e) {
|
|
649
|
+
logForDebugging(`[Sandbox Windows] acl revoke: ${e.message}`, {
|
|
650
|
+
level: 'error',
|
|
651
|
+
});
|
|
652
|
+
return undefined;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
577
655
|
// ────────────────────────────────────────────────────────────────────
|
|
578
656
|
// Wrap
|
|
579
657
|
// ────────────────────────────────────────────────────────────────────
|
|
658
|
+
/**
|
|
659
|
+
* `safe.directory` entries above this count collapse to a single
|
|
660
|
+
* `safe.directory=*`. Keeps `GIT_CONFIG_COUNT` (and the `--env`
|
|
661
|
+
* argv it rides on) bounded when `allowWrite` is wide.
|
|
662
|
+
*/
|
|
663
|
+
const SAFE_DIRECTORY_WILDCARD_THRESHOLD = 8;
|
|
664
|
+
/**
|
|
665
|
+
* Build the `GIT_CONFIG_COUNT` / `GIT_CONFIG_KEY_<n>` /
|
|
666
|
+
* `GIT_CONFIG_VALUE_<n>` env-var set for the sandboxed child.
|
|
667
|
+
*
|
|
668
|
+
* Emits:
|
|
669
|
+
* - `safe.directory=<dir>` for each entry in `safeDirs` (or one
|
|
670
|
+
* `safe.directory=*` when the list is long) — the working tree
|
|
671
|
+
* is owned by the real user, so git running as `srt-sandbox`
|
|
672
|
+
* refuses with "detected dubious ownership" without it.
|
|
673
|
+
* - `http.schannelUseSSLCAInfo=true` and
|
|
674
|
+
* `http.schannelCheckRevoke=false` when `schannelCa` — makes
|
|
675
|
+
* git's default (schannel) backend honor `GIT_SSL_CAINFO`
|
|
676
|
+
* without `-c http.sslBackend=openssl`. Revocation is disabled
|
|
677
|
+
* because CryptoAPI CRL/OCSP fetches ignore proxy env and would
|
|
678
|
+
* be WFP-fenced.
|
|
679
|
+
*
|
|
680
|
+
* Composes with an existing `GIT_CONFIG_COUNT` in `baseEnv` by
|
|
681
|
+
* continuing its numbering; the returned `GIT_CONFIG_COUNT` is the
|
|
682
|
+
* new total. Under the two-hop launch the broker's own environment
|
|
683
|
+
* never reaches the child, so `baseEnv` is the caller-supplied
|
|
684
|
+
* overlay ({@link WindowsSandboxParams.setEnvVars}), not
|
|
685
|
+
* `process.env`.
|
|
686
|
+
*
|
|
687
|
+
* Paths are emitted with forward slashes so the value survives
|
|
688
|
+
* msys2's env conversion untouched and native git accepts it.
|
|
689
|
+
*/
|
|
690
|
+
export function buildGitConfigEnv(opts) {
|
|
691
|
+
// An explicit `GIT_CONFIG_COUNT=0` in baseEnv is an opt-out ("no
|
|
692
|
+
// env-level git config") — respect it rather than overwriting.
|
|
693
|
+
if (opts.baseEnv?.GIT_CONFIG_COUNT === '0')
|
|
694
|
+
return {};
|
|
695
|
+
const parsed = Number.parseInt(opts.baseEnv?.GIT_CONFIG_COUNT ?? '', 10);
|
|
696
|
+
const start = Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
|
|
697
|
+
let n = start;
|
|
698
|
+
const out = {};
|
|
699
|
+
const emit = (key, value) => {
|
|
700
|
+
out[`GIT_CONFIG_KEY_${n}`] = key;
|
|
701
|
+
out[`GIT_CONFIG_VALUE_${n}`] = value;
|
|
702
|
+
n++;
|
|
703
|
+
};
|
|
704
|
+
const dirs = [
|
|
705
|
+
...new Set(opts.safeDirs
|
|
706
|
+
.filter((d) => !!d)
|
|
707
|
+
.map(d => {
|
|
708
|
+
const fwd = d.replace(/\\/g, '/');
|
|
709
|
+
const stripped = fwd.replace(/\/+$/, '');
|
|
710
|
+
// Don't strip the trailing slash off a drive root — `C:`
|
|
711
|
+
// is drive-relative-cwd, not the root; git wants `C:/`.
|
|
712
|
+
return /^[A-Za-z]:$/.test(stripped) ? `${stripped}/` : stripped;
|
|
713
|
+
})),
|
|
714
|
+
];
|
|
715
|
+
if (dirs.length > SAFE_DIRECTORY_WILDCARD_THRESHOLD) {
|
|
716
|
+
emit('safe.directory', '*');
|
|
717
|
+
}
|
|
718
|
+
else {
|
|
719
|
+
// git matches safe.directory against the REPO TOP-LEVEL exactly,
|
|
720
|
+
// so a workspace root doesn't cover a nested repo. Emit both the
|
|
721
|
+
// exact path and the `<dir>/*` glob (git ≥2.46) so any repo
|
|
722
|
+
// at-or-under a granted dir is trusted.
|
|
723
|
+
for (const d of dirs) {
|
|
724
|
+
emit('safe.directory', d);
|
|
725
|
+
emit('safe.directory', `${d}/*`);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
if (opts.schannelCa) {
|
|
729
|
+
emit('http.schannelUseSSLCAInfo', 'true');
|
|
730
|
+
emit('http.schannelCheckRevoke', 'false');
|
|
731
|
+
}
|
|
732
|
+
if (n === start)
|
|
733
|
+
return {};
|
|
734
|
+
out.GIT_CONFIG_COUNT = String(n);
|
|
735
|
+
return out;
|
|
736
|
+
}
|
|
580
737
|
/**
|
|
581
738
|
* Build the spawn descriptor for running `command` inside the Windows
|
|
582
739
|
* sandbox: an `argv` array plus the `env` to spawn it with.
|
|
@@ -590,65 +747,62 @@ export const WINDOWS_ACL_PARENT_OK = new Set(['restored', 'alreadyOriginal', 'st
|
|
|
590
747
|
* Proxy configuration is single-sourced by {@link generateProxyEnvVars}
|
|
591
748
|
* (the same canonical builder used on macOS/Linux). `srt-win exec`
|
|
592
749
|
* takes no `--http-proxy` / `--socks-proxy` flags and synthesizes no
|
|
593
|
-
* proxy env
|
|
594
|
-
*
|
|
595
|
-
*
|
|
750
|
+
* proxy env. The two-hop runner starts with the SANDBOX user's
|
|
751
|
+
* profile env (`USERPROFILE`/`TEMP` isolated) and overlays exactly
|
|
752
|
+
* what we pass as `--env` — built here from the broker's `PATH` plus
|
|
753
|
+
* the generated proxy set.
|
|
596
754
|
*/
|
|
597
755
|
export function wrapCommandWithSandboxWindows(p) {
|
|
598
|
-
const exe =
|
|
756
|
+
const { exe, prependArgs } = p.srtWin ?? resolveSrtWin();
|
|
599
757
|
// Generated proxy + CA-trust env. Single-sourced here so the
|
|
600
758
|
// same object feeds (a) the spawn env merge below and (b) the
|
|
601
|
-
// explicit `--env` overlay for the
|
|
759
|
+
// explicit `--env` overlay for the runner.
|
|
602
760
|
//
|
|
603
|
-
//
|
|
604
|
-
//
|
|
605
|
-
//
|
|
606
|
-
//
|
|
607
|
-
//
|
|
608
|
-
// from the registry write `srt-win user trust-ca`
|
|
609
|
-
// time; the env-var
|
|
610
|
-
//
|
|
611
|
-
|
|
612
|
-
logForDebugging(`[Sandbox Windows] caCertPath '${p.caCertPath}' not forwarded ` +
|
|
613
|
-
`under asSandboxUser (broker %TEMP% is unreadable by ` +
|
|
614
|
-
`srt-sandbox); schannel trust via 'srt-win user trust-ca' ` +
|
|
615
|
-
`is the only CA-trust path for the two-hop launch`);
|
|
616
|
-
}
|
|
617
|
-
const generated = envListToObject(generateProxyEnvVars(p.httpProxyPort, p.socksProxyPort, p.asSandboxUser ? undefined : p.caCertPath, p.proxyAuthToken));
|
|
761
|
+
// The CA trust-bundle path is emitted with forward slashes:
|
|
762
|
+
// msys2's POSIX-path conversion leaves `C:/…` alone and every
|
|
763
|
+
// tool we set the var for (curl, git, node, python, …) accepts
|
|
764
|
+
// forward slashes on Windows; backslashes would be mangled if
|
|
765
|
+
// the value passes through a bash command line. Schannel-level
|
|
766
|
+
// trust comes from the registry write `srt-win user trust-ca`
|
|
767
|
+
// did at install time; the env-var layer here covers the
|
|
768
|
+
// OpenSSL-backed tools.
|
|
769
|
+
const generated = envListToObject(generateProxyEnvVars(p.httpProxyPort, p.socksProxyPort, p.caCertPath?.replace(/\\/g, '/'), p.proxyAuthToken));
|
|
618
770
|
// TMPDIR is a POSIX path meant for the macOS/Linux FS sandbox — it
|
|
619
771
|
// serves no purpose on Windows and breaks msys2 tools (mktemp etc.).
|
|
620
772
|
delete generated.TMPDIR;
|
|
621
|
-
|
|
622
|
-
//
|
|
623
|
-
//
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
for (const
|
|
633
|
-
argv.push('--deny-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
773
|
+
// GIT_CONFIG_* set — safe.directory (dubious-ownership) + the
|
|
774
|
+
// schannel CA knobs. Composed against setEnvVars so a caller
|
|
775
|
+
// that already emits GIT_CONFIG_COUNT keeps its entries.
|
|
776
|
+
const gitCfg = buildGitConfigEnv({
|
|
777
|
+
safeDirs: [p.cwd ?? process.cwd(), ...(p.allowWrite ?? [])],
|
|
778
|
+
schannelCa: p.caCertPath !== undefined,
|
|
779
|
+
baseEnv: p.setEnvVars,
|
|
780
|
+
});
|
|
781
|
+
const argv = [exe, ...prependArgs, 'exec'];
|
|
782
|
+
for (const d of p.denyRead ?? [])
|
|
783
|
+
argv.push('--deny-read', d);
|
|
784
|
+
for (const d of p.denyWrite ?? [])
|
|
785
|
+
argv.push('--deny-write', d);
|
|
786
|
+
// The two-hop runner starts with the SANDBOX user's profile env
|
|
787
|
+
// (USERPROFILE/TEMP isolated) and overlays exactly what we pass as
|
|
788
|
+
// `--env`. The broker does NOT enumerate its own env — the overlay
|
|
789
|
+
// is built here from the broker's PATH, the mode:'mask' sentinel
|
|
790
|
+
// set, the generated proxy set, and the GIT_CONFIG_* set.
|
|
791
|
+
// Sentinels precede `generated` so a caller masking e.g.
|
|
792
|
+
// `HTTPS_PROXY` cannot break the sandbox's own proxy plumbing —
|
|
793
|
+
// same precedence as the macOS/Linux `env -u … VAR=…
|
|
794
|
+
// sandbox-exec` order. `gitCfg` is last so its GIT_CONFIG_COUNT
|
|
795
|
+
// (which composes against setEnvVars) wins.
|
|
796
|
+
const overlay = {
|
|
797
|
+
PATH: process.env.PATH,
|
|
798
|
+
PATHEXT: process.env.PATHEXT,
|
|
799
|
+
...(p.setEnvVars ?? {}),
|
|
800
|
+
...generated,
|
|
801
|
+
...gitCfg,
|
|
802
|
+
};
|
|
803
|
+
for (const [k, v] of Object.entries(overlay)) {
|
|
804
|
+
if (v !== undefined)
|
|
805
|
+
argv.push('--env', `${k}=${v}`);
|
|
652
806
|
}
|
|
653
807
|
argv.push('--');
|
|
654
808
|
const systemRoot = process.env.SystemRoot ?? 'C:\\Windows';
|
|
@@ -660,9 +814,6 @@ export function wrapCommandWithSandboxWindows(p) {
|
|
|
660
814
|
// string with its own internal quoting; srt-win's `build_cmdline`
|
|
661
815
|
// takes the generic non-cmd branch and MSVCRT-quotes it as a
|
|
662
816
|
// SINGLE argv element, so bash receives it intact as argv[2].
|
|
663
|
-
// TODO: MSYS2 derives POSIX /tmp from Windows TEMP/TMP itself;
|
|
664
|
-
// revisit whether any extra TEMP/TMP normalisation is needed for
|
|
665
|
-
// the bash inner shell under the restricted token.
|
|
666
817
|
argv.push(sh.path, '-c', p.command);
|
|
667
818
|
break;
|
|
668
819
|
case 'pwsh':
|
|
@@ -681,43 +832,21 @@ export function wrapCommandWithSandboxWindows(p) {
|
|
|
681
832
|
}
|
|
682
833
|
// CreateProcessW's lpCommandLine is capped at 32 767 WCHARs.
|
|
683
834
|
// Node's `shell:false` spawn builds it by MSVCRT-quoting each
|
|
684
|
-
// argv element and joining with spaces;
|
|
685
|
-
//
|
|
686
|
-
// doubling before embedded quotes (rare in file paths). A
|
|
687
|
-
// path-count proxy can't see this — 60 long paths overflow
|
|
688
|
-
// while 200 short ones fit — so estimate the actual command
|
|
689
|
-
// line and refuse with the same guidance the old count check
|
|
690
|
-
// gave. ~30 000 leaves headroom for the quote overhead the
|
|
691
|
-
// estimate doesn't model.
|
|
835
|
+
// argv element and joining with spaces; ~30 000 leaves headroom
|
|
836
|
+
// for the quote overhead the estimate doesn't model.
|
|
692
837
|
const cmdlineEstimate = argv.reduce((n, a) => n + a.length + 3, 0);
|
|
693
838
|
if (cmdlineEstimate > 30000) {
|
|
694
|
-
const nDeny = (p.denyRead?.length ?? 0) + (p.denyWrite?.length ?? 0);
|
|
695
839
|
throw new Error(`Windows sandbox argv is ~${cmdlineEstimate} chars ` +
|
|
696
|
-
`(CreateProcessW limit is 32 767).
|
|
697
|
-
`
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
//
|
|
703
|
-
//
|
|
704
|
-
//
|
|
705
|
-
|
|
706
|
-
// macOS/Linux `env -u … VAR=… sandbox-exec` order.
|
|
707
|
-
//
|
|
708
|
-
// Windows env is case-insensitive but Node preserves the OS
|
|
709
|
-
// casing on enumeration, so a `delete baseEnv['SECRET']` would
|
|
710
|
-
// miss a `Secret` key. Match by uppercased name instead.
|
|
711
|
-
const baseEnv = { ...process.env };
|
|
712
|
-
const unsetUpper = new Set((p.unsetEnvVars ?? []).map(k => k.toUpperCase()));
|
|
713
|
-
for (const k of Object.keys(baseEnv)) {
|
|
714
|
-
if (unsetUpper.has(k.toUpperCase()))
|
|
715
|
-
delete baseEnv[k];
|
|
716
|
-
}
|
|
717
|
-
Object.assign(baseEnv, p.setEnvVars ?? {});
|
|
718
|
-
// Generated proxy vars override any inherited (or just-masked)
|
|
719
|
-
// ones so the child always routes through this sandbox's proxies.
|
|
720
|
-
const env = { ...baseEnv, ...generated };
|
|
840
|
+
`(CreateProcessW limit is 32 767). Shorten the command, ` +
|
|
841
|
+
`or move broad globs to session-level filesystem.denyRead.`);
|
|
842
|
+
}
|
|
843
|
+
// The two-hop runner starts with a FRESH `srt-sandbox` profile
|
|
844
|
+
// env (`lpEnvironment = NULL` + `LOGON_WITH_PROFILE`), so the
|
|
845
|
+
// broker process's environment never reaches the child. The
|
|
846
|
+
// returned `env` is the spawn env for the broker (srt-win)
|
|
847
|
+
// process only; the child sees the `--env` overlay built into
|
|
848
|
+
// `argv` above (PATH/PATHEXT + mode:'mask' sentinels + proxy).
|
|
849
|
+
const env = { ...process.env, ...generated };
|
|
721
850
|
return { argv, env };
|
|
722
851
|
}
|
|
723
852
|
/**
|
|
@@ -740,80 +869,71 @@ function envListToObject(list) {
|
|
|
740
869
|
// ────────────────────────────────────────────────────────────────────
|
|
741
870
|
/**
|
|
742
871
|
* Install instructions, surfaced verbatim in error messages.
|
|
743
|
-
* Tailored to the observed group state: if the install already
|
|
744
|
-
* ran (`created-not-on-token`), only the logout is missing.
|
|
745
872
|
*/
|
|
746
|
-
export function windowsInstallInstructions(
|
|
747
|
-
if (groupState === 'created-not-on-token') {
|
|
748
|
-
return (`The discriminator group exists but is not yet in this session's ` +
|
|
749
|
-
`token. LOG OUT and back in to pick up the new group membership ` +
|
|
750
|
-
`(it enters TokenGroups at logon). Network is not disrupted ` +
|
|
751
|
-
`meanwhile — WFP filter-0 PERMITs traffic while the group is absent ` +
|
|
752
|
-
`from your token.`);
|
|
753
|
-
}
|
|
754
|
-
const g = ref.groupSid
|
|
755
|
-
? `--group-sid ${ref.groupSid}`
|
|
756
|
-
: `--name ${ref.groupName ?? DEFAULT_WINDOWS_GROUP_NAME}`;
|
|
873
|
+
export function windowsInstallInstructions(sublayerGuid) {
|
|
757
874
|
const sl = sublayerGuid ? ` --sublayer-guid ${sublayerGuid}` : '';
|
|
758
875
|
return (`Windows sandbox needs a one-time install (one UAC prompt):\n` +
|
|
759
876
|
` npx sandbox-runtime windows-install\n` +
|
|
760
877
|
` — or call installWindowsSandbox(), or run ` +
|
|
761
|
-
`\`srt-win.exe install
|
|
762
|
-
`
|
|
763
|
-
|
|
764
|
-
`from your token, WFP filter-0 PERMITs all traffic.`);
|
|
878
|
+
`\`srt-win.exe install${sl}\` directly.\n` +
|
|
879
|
+
`No logout is needed: the WFP filter keys on the dedicated ` +
|
|
880
|
+
`\`srt-sandbox\` user's SID, so your network is unaffected.`);
|
|
765
881
|
}
|
|
766
882
|
/**
|
|
767
883
|
* Check the Windows backend is ready to sandbox. Errors block
|
|
768
884
|
* `initialize()`; warnings are informational.
|
|
769
885
|
*/
|
|
770
|
-
export function checkWindowsDependencies(
|
|
886
|
+
export function checkWindowsDependencies(opts = {}) {
|
|
887
|
+
const { sublayerGuid } = opts;
|
|
771
888
|
const errors = [];
|
|
772
889
|
const warnings = [];
|
|
773
|
-
// 1. Binary present
|
|
774
|
-
|
|
890
|
+
// 1. Binary present (`resolveSrtWin` throws on a missing
|
|
891
|
+
// override, `getSrtWinPath` on a missing packaged binary). Resolve
|
|
892
|
+
// once and reuse for the status calls below.
|
|
893
|
+
let srtWin;
|
|
775
894
|
try {
|
|
776
|
-
|
|
895
|
+
srtWin = opts.srtWin ?? resolveSrtWin();
|
|
777
896
|
}
|
|
778
897
|
catch (e) {
|
|
779
898
|
return { errors: [e.message], warnings };
|
|
780
899
|
}
|
|
781
|
-
logForDebugging(`[Sandbox Windows] using srt-win at ${exe}`);
|
|
782
|
-
// 2.
|
|
783
|
-
let
|
|
900
|
+
logForDebugging(`[Sandbox Windows] using srt-win at ${srtWin.exe}`);
|
|
901
|
+
// 2. Sandbox user provisioned + credential readable.
|
|
902
|
+
let us;
|
|
784
903
|
try {
|
|
785
|
-
|
|
904
|
+
us = getWindowsSandboxUserStatus({ srtWin });
|
|
786
905
|
}
|
|
787
906
|
catch (e) {
|
|
788
|
-
errors.push(`srt-win
|
|
907
|
+
errors.push(`srt-win user status failed: ${e.message}`);
|
|
789
908
|
return { errors, warnings };
|
|
790
909
|
}
|
|
791
|
-
if (
|
|
792
|
-
errors.push(`
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
windowsInstallInstructions(ref, sublayerGuid, gs.state));
|
|
910
|
+
if (!us.provisioned || !us.credPresent) {
|
|
911
|
+
errors.push(`Sandbox user is not provisioned (user=${us.provisioned}, ` +
|
|
912
|
+
`cred=${us.credPresent}). ` +
|
|
913
|
+
windowsInstallInstructions(sublayerGuid));
|
|
796
914
|
}
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
//
|
|
915
|
+
// 3. WFP filters installed under the sublayer. BFE enumeration is
|
|
916
|
+
// admin-gated; `cannot-read` is informational only — the
|
|
917
|
+
// BEHAVIORAL check (`verifyWindowsWfpEgress`) runs at
|
|
918
|
+
// `initialize()` and is what actually fails closed.
|
|
800
919
|
let ws;
|
|
801
920
|
try {
|
|
802
|
-
ws = getWindowsWfpStatus({ sublayerGuid });
|
|
921
|
+
ws = getWindowsWfpStatus({ sublayerGuid, srtWin });
|
|
803
922
|
}
|
|
804
923
|
catch (e) {
|
|
805
924
|
errors.push(`srt-win wfp status failed: ${e.message}`);
|
|
806
925
|
return { errors, warnings };
|
|
807
926
|
}
|
|
808
|
-
if (ws.state
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
//
|
|
813
|
-
|
|
927
|
+
if (ws.state === 'cannot-read') {
|
|
928
|
+
logForDebugging(`[Sandbox Windows] wfp status cannot-read (non-elevated): ${ws.hint}`);
|
|
929
|
+
}
|
|
930
|
+
else if (ws.state !== 'installed') {
|
|
931
|
+
// 'absent'. If the user is also not-provisioned, the user-state
|
|
932
|
+
// error above already gave the right instruction; don't repeat.
|
|
933
|
+
if (us.provisioned && us.credPresent) {
|
|
814
934
|
errors.push(`WFP filters not installed under sublayer ` +
|
|
815
935
|
`${sublayerGuid ?? '(default)'}. ` +
|
|
816
|
-
windowsInstallInstructions(
|
|
936
|
+
windowsInstallInstructions(sublayerGuid));
|
|
817
937
|
}
|
|
818
938
|
}
|
|
819
939
|
else if (ws.portRange) {
|