@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.
- package/README.md +106 -4
- package/dist/cli.js +12 -18
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +5 -5
- 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 +157 -0
- package/dist/sandbox/credential-mask-files.d.ts.map +1 -0
- package/dist/sandbox/credential-mask-files.js +298 -0
- package/dist/sandbox/credential-mask-files.js.map +1 -0
- package/dist/sandbox/credential-sentinel.d.ts +72 -0
- package/dist/sandbox/credential-sentinel.d.ts.map +1 -0
- package/dist/sandbox/credential-sentinel.js +130 -0
- package/dist/sandbox/credential-sentinel.js.map +1 -0
- package/dist/sandbox/domain-pattern.d.ts +36 -0
- package/dist/sandbox/domain-pattern.d.ts.map +1 -0
- package/dist/sandbox/domain-pattern.js +60 -0
- package/dist/sandbox/domain-pattern.js.map +1 -0
- package/dist/sandbox/http-proxy.d.ts +33 -2
- package/dist/sandbox/http-proxy.d.ts.map +1 -1
- package/dist/sandbox/http-proxy.js +30 -2
- package/dist/sandbox/http-proxy.js.map +1 -1
- package/dist/sandbox/linux-sandbox-utils.d.ts +20 -0
- package/dist/sandbox/linux-sandbox-utils.d.ts.map +1 -1
- package/dist/sandbox/linux-sandbox-utils.js +134 -65
- 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/listen-in-range.d.ts +12 -0
- package/dist/sandbox/listen-in-range.d.ts.map +1 -0
- package/dist/sandbox/listen-in-range.js +43 -0
- package/dist/sandbox/listen-in-range.js.map +1 -0
- package/dist/sandbox/macos-sandbox-utils.d.ts +11 -0
- package/dist/sandbox/macos-sandbox-utils.d.ts.map +1 -1
- package/dist/sandbox/macos-sandbox-utils.js +72 -17
- package/dist/sandbox/macos-sandbox-utils.js.map +1 -1
- package/dist/sandbox/mitm-ca.d.ts +69 -2
- package/dist/sandbox/mitm-ca.d.ts.map +1 -1
- package/dist/sandbox/mitm-ca.js +188 -16
- 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 +26 -19
- package/dist/sandbox/mitm-leaf.js.map +1 -1
- package/dist/sandbox/mux-proxy.d.ts +59 -0
- package/dist/sandbox/mux-proxy.d.ts.map +1 -0
- package/dist/sandbox/mux-proxy.js +159 -0
- package/dist/sandbox/mux-proxy.js.map +1 -0
- package/dist/sandbox/request-filter.d.ts +11 -1
- package/dist/sandbox/request-filter.d.ts.map +1 -1
- package/dist/sandbox/request-filter.js.map +1 -1
- package/dist/sandbox/sandbox-config.d.ts +461 -41
- package/dist/sandbox/sandbox-config.d.ts.map +1 -1
- package/dist/sandbox/sandbox-config.js +342 -26
- package/dist/sandbox/sandbox-config.js.map +1 -1
- package/dist/sandbox/sandbox-manager.d.ts +5 -1
- package/dist/sandbox/sandbox-manager.d.ts.map +1 -1
- package/dist/sandbox/sandbox-manager.js +697 -211
- package/dist/sandbox/sandbox-manager.js.map +1 -1
- package/dist/sandbox/sandbox-schemas.d.ts +15 -0
- package/dist/sandbox/sandbox-schemas.d.ts.map +1 -1
- package/dist/sandbox/sandbox-utils.d.ts +55 -6
- package/dist/sandbox/sandbox-utils.d.ts.map +1 -1
- package/dist/sandbox/sandbox-utils.js +143 -30
- package/dist/sandbox/sandbox-utils.js.map +1 -1
- package/dist/sandbox/socks-proxy.d.ts +11 -5
- package/dist/sandbox/socks-proxy.d.ts.map +1 -1
- package/dist/sandbox/socks-proxy.js +13 -81
- package/dist/sandbox/socks-proxy.js.map +1 -1
- package/dist/sandbox/tls-terminate-proxy.d.ts +2 -2
- package/dist/sandbox/tls-terminate-proxy.d.ts.map +1 -1
- package/dist/sandbox/tls-terminate-proxy.js +31 -5
- package/dist/sandbox/tls-terminate-proxy.js.map +1 -1
- package/dist/sandbox/windows-sandbox-utils.d.ts +508 -116
- package/dist/sandbox/windows-sandbox-utils.d.ts.map +1 -1
- package/dist/sandbox/windows-sandbox-utils.js +721 -209
- 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 +7 -6
- package/vendor/seccomp/arm64/apply-seccomp +0 -0
- package/vendor/seccomp/build.ts +8 -30
- package/vendor/seccomp/x64/apply-seccomp +0 -0
- package/vendor/srt-win/build.ts +21 -0
- package/dist/vendor/seccomp/Dockerfile.build +0 -6
- package/dist/vendor/seccomp/arm64/apply-seccomp +0 -0
- package/dist/vendor/seccomp/build.ts +0 -91
- package/dist/vendor/seccomp/x64/apply-seccomp +0 -0
- package/dist/vendor/seccomp-src/apply-seccomp.c +0 -280
- package/dist/vendor/seccomp-src/seccomp-unix-block.c +0 -148
- package/dist/vendor/srt-win/Cargo.lock +0 -361
- package/dist/vendor/srt-win/Cargo.toml +0 -46
- package/dist/vendor/srt-win/ci/cleanup.ps1 +0 -49
- package/dist/vendor/srt-win/ci/smoke-exec.ps1 +0 -446
- package/dist/vendor/srt-win/ci/smoke.ps1 +0 -307
- package/dist/vendor/srt-win/src/job.rs +0 -102
- package/dist/vendor/srt-win/src/launch.rs +0 -732
- package/dist/vendor/srt-win/src/lib.rs +0 -20
- package/dist/vendor/srt-win/src/main.rs +0 -669
- package/dist/vendor/srt-win/src/self_protect.rs +0 -169
- package/dist/vendor/srt-win/src/sid.rs +0 -296
- package/dist/vendor/srt-win/src/token.rs +0 -341
- package/dist/vendor/srt-win/src/util.rs +0 -42
- package/dist/vendor/srt-win/src/wfp.rs +0 -992
- package/dist/vendor/srt-win/src/winsta.rs +0 -209
- package/dist/vendor/srt-win/tests/sd_access_check_matrix.rs +0 -259
- package/vendor/seccomp-src/apply-seccomp.c +0 -280
- package/vendor/seccomp-src/seccomp-unix-block.c +0 -148
- package/vendor/srt-win/Cargo.lock +0 -361
- package/vendor/srt-win/Cargo.toml +0 -46
- package/vendor/srt-win/ci/cleanup.ps1 +0 -49
- package/vendor/srt-win/ci/smoke-exec.ps1 +0 -446
- package/vendor/srt-win/ci/smoke.ps1 +0 -307
- package/vendor/srt-win/src/job.rs +0 -102
- package/vendor/srt-win/src/launch.rs +0 -732
- package/vendor/srt-win/src/lib.rs +0 -20
- package/vendor/srt-win/src/main.rs +0 -669
- package/vendor/srt-win/src/self_protect.rs +0 -169
- package/vendor/srt-win/src/sid.rs +0 -296
- package/vendor/srt-win/src/token.rs +0 -341
- package/vendor/srt-win/src/util.rs +0 -42
- package/vendor/srt-win/src/wfp.rs +0 -992
- package/vendor/srt-win/src/winsta.rs +0 -209
- package/vendor/srt-win/tests/sd_access_check_matrix.rs +0 -259
|
@@ -1,33 +1,90 @@
|
|
|
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 { generateProxyEnvVars } from './sandbox-utils.js';
|
|
8
|
+
import { generateProxyEnvVars, normalizePathForSandbox, containsGlobCharsWin, expandGlobPattern, } from './sandbox-utils.js';
|
|
9
|
+
// Re-export so existing tests (glob-expand.test.ts) and any
|
|
10
|
+
// out-of-tree caller keep their import path.
|
|
11
|
+
export { containsGlobCharsWin, stripExtendedPathPrefix, } from './sandbox-utils.js';
|
|
7
12
|
/**
|
|
8
13
|
* Windows sandbox backend.
|
|
9
14
|
*
|
|
10
15
|
* Network isolation is enforced by `srt-win.exe` — a Rust helper that
|
|
11
|
-
*
|
|
12
|
-
* keyed on that
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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.
|
|
16
30
|
*
|
|
17
31
|
* This module is a thin wrapper around the `srt-win` CLI; all status
|
|
18
|
-
* comes from live enumeration
|
|
19
|
-
* token-membership check; WFP via providerData-tag enumeration under
|
|
20
|
-
* the configured sublayer). There is no marker file.
|
|
32
|
+
* comes from live enumeration. There is no marker file.
|
|
21
33
|
*
|
|
22
|
-
* Filesystem
|
|
34
|
+
* Filesystem rules (`denyRead`/`denyWrite`/`allowRead`/`allowWrite`)
|
|
35
|
+
* are enforced via additive explicit ACEs for `<sb-SID>` — see
|
|
36
|
+
* {@link grantWindowsAcl} / {@link stampWindowsAcl}.
|
|
23
37
|
*/
|
|
24
38
|
// ────────────────────────────────────────────────────────────────────
|
|
25
39
|
// Types
|
|
26
40
|
// ────────────────────────────────────────────────────────────────────
|
|
27
|
-
export const DEFAULT_WINDOWS_GROUP_NAME = 'sandbox-runtime-net';
|
|
28
41
|
export const DEFAULT_WINDOWS_PROXY_PORT_RANGE = [
|
|
29
42
|
60080, 60089,
|
|
30
43
|
];
|
|
44
|
+
/**
|
|
45
|
+
* Adapter from the cross-platform `binShell?: string` surface
|
|
46
|
+
* ({@link SandboxManager.wrapWithSandboxArgv}) to the Windows
|
|
47
|
+
* discriminated union. Throws on any value outside the recognised
|
|
48
|
+
* set — there is no silent fallback to cmd.exe.
|
|
49
|
+
*
|
|
50
|
+
* Uses `path.win32` explicitly so the function (and its unit test)
|
|
51
|
+
* is platform-independent.
|
|
52
|
+
*/
|
|
53
|
+
export function parseWindowsBinShell(raw) {
|
|
54
|
+
if (raw === undefined)
|
|
55
|
+
return { kind: 'cmd' };
|
|
56
|
+
// bash/sh: path semantics — match on basename, keep the caller's
|
|
57
|
+
// absolute path verbatim.
|
|
58
|
+
const base = path.win32.basename(raw).toLowerCase();
|
|
59
|
+
if (base === 'bash' ||
|
|
60
|
+
base === 'bash.exe' ||
|
|
61
|
+
base === 'sh' ||
|
|
62
|
+
base === 'sh.exe') {
|
|
63
|
+
if (!path.win32.isAbsolute(raw)) {
|
|
64
|
+
throw new Error(`binShell bash path must be absolute (got ${JSON.stringify(raw)}); ` +
|
|
65
|
+
`pass the resolved Git Bash install path`);
|
|
66
|
+
}
|
|
67
|
+
return { kind: 'bash', path: raw };
|
|
68
|
+
}
|
|
69
|
+
// cmd/powershell/pwsh: token semantics — match on the FULL string,
|
|
70
|
+
// not basename, so an absolute path to pwsh.exe (whose path we'd
|
|
71
|
+
// otherwise discard) falls through to the explicit throw rather
|
|
72
|
+
// than silently degrading to a PATH lookup.
|
|
73
|
+
switch (raw.toLowerCase()) {
|
|
74
|
+
case 'pwsh':
|
|
75
|
+
case 'pwsh.exe':
|
|
76
|
+
return { kind: 'pwsh' };
|
|
77
|
+
case 'powershell':
|
|
78
|
+
case 'powershell.exe':
|
|
79
|
+
return { kind: 'powershell' };
|
|
80
|
+
case 'cmd':
|
|
81
|
+
case 'cmd.exe':
|
|
82
|
+
return { kind: 'cmd' };
|
|
83
|
+
default:
|
|
84
|
+
throw new Error(`unrecognised binShell ${JSON.stringify(raw)}: expected ` +
|
|
85
|
+
`'cmd' | 'powershell' | 'pwsh' or an absolute path to bash.exe/sh.exe`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
31
88
|
// ────────────────────────────────────────────────────────────────────
|
|
32
89
|
// Binary resolution
|
|
33
90
|
// ────────────────────────────────────────────────────────────────────
|
|
@@ -36,12 +93,22 @@ function repoRoot() {
|
|
|
36
93
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
37
94
|
return path.resolve(here, '..', '..');
|
|
38
95
|
}
|
|
96
|
+
const nodeArchToDir = { x64: 'x64', arm64: 'arm64' };
|
|
39
97
|
/**
|
|
40
|
-
* Locate `srt-win.exe`. Resolution order:
|
|
41
|
-
* 1. `
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
98
|
+
* Locate the packaged `srt-win.exe`. Resolution order:
|
|
99
|
+
* 1. `<root>/vendor/srt-win/{arch}/srt-win.exe` (prebuilt — published npm
|
|
100
|
+
* package, or after `npm run build:srt-win` locally).
|
|
101
|
+
* 2. `<root>/vendor/srt-win-src/target/release/srt-win.exe` (local
|
|
102
|
+
* `cargo build --release` fallback for development).
|
|
103
|
+
*
|
|
104
|
+
* `<root>` is {@link repoRoot} — `__dirname/../..`, which resolves to the
|
|
105
|
+
* repo root from `src/sandbox/` and `dist/sandbox/` alike, and to the
|
|
106
|
+
* package root when installed under `node_modules`.
|
|
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}.
|
|
45
112
|
*
|
|
46
113
|
* Resolution via the optional `@anthropic-ai/sandbox-runtime-win32-*`
|
|
47
114
|
* platform packages is added separately.
|
|
@@ -49,50 +116,75 @@ function repoRoot() {
|
|
|
49
116
|
* @throws if none exist.
|
|
50
117
|
*/
|
|
51
118
|
export function getSrtWinPath() {
|
|
52
|
-
const envPath = process.env.SRT_WIN_PATH;
|
|
53
|
-
if (envPath && fs.existsSync(envPath)) {
|
|
54
|
-
return envPath;
|
|
55
|
-
}
|
|
56
119
|
const root = repoRoot();
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
120
|
+
const arch = nodeArchToDir[process.arch];
|
|
121
|
+
const candidates = [];
|
|
122
|
+
if (arch) {
|
|
123
|
+
candidates.push(path.join(root, 'vendor', 'srt-win', arch, 'srt-win.exe'));
|
|
124
|
+
}
|
|
125
|
+
candidates.push(path.join(root, 'vendor', 'srt-win-src', 'target', 'release', 'srt-win.exe'));
|
|
61
126
|
for (const c of candidates) {
|
|
62
127
|
if (fs.existsSync(c))
|
|
63
128
|
return c;
|
|
64
129
|
}
|
|
65
|
-
throw new Error(`srt-win.exe not found. Set
|
|
66
|
-
`\`cargo build --release --manifest-path vendor/srt-win/Cargo.toml\`. ` +
|
|
67
|
-
`Looked in: ${
|
|
130
|
+
throw new Error(`srt-win.exe not found. Set windows.srtWin.path or build with ` +
|
|
131
|
+
`\`cargo build --release --manifest-path vendor/srt-win-src/Cargo.toml\`. ` +
|
|
132
|
+
`Looked in: ${candidates.join(', ')}`);
|
|
68
133
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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: [] };
|
|
76
161
|
}
|
|
77
|
-
function runSrtWin(args) {
|
|
78
|
-
|
|
79
|
-
|
|
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], {
|
|
169
|
+
encoding: 'utf8',
|
|
170
|
+
timeout: opts.timeoutMs ?? 15000,
|
|
171
|
+
...(opts.stdin !== undefined ? { input: opts.stdin } : {}),
|
|
172
|
+
});
|
|
80
173
|
if (r.error) {
|
|
81
174
|
throw new Error(`srt-win ${args[0]}: spawn failed: ${r.error.message}`);
|
|
82
175
|
}
|
|
83
176
|
return {
|
|
84
177
|
status: r.status,
|
|
178
|
+
signal: r.signal,
|
|
85
179
|
stdout: (r.stdout ?? '').trim(),
|
|
86
180
|
stderr: (r.stderr ?? '').trim(),
|
|
87
181
|
};
|
|
88
182
|
}
|
|
89
|
-
function runSrtWinJson(args) {
|
|
90
|
-
const r = runSrtWin(args);
|
|
183
|
+
function runSrtWinJson(args, opts) {
|
|
184
|
+
const r = runSrtWin(args, opts);
|
|
91
185
|
if (r.status !== 0) {
|
|
92
186
|
throw new Error(`srt-win ${args.join(' ')} exited ${r.status}: ${r.stderr || r.stdout}`);
|
|
93
187
|
}
|
|
94
|
-
// Status subcommands print exactly one line of JSON to stdout. stderr
|
|
95
|
-
// may carry `srt-win:` diagnostics — ignore it for parsing.
|
|
96
188
|
try {
|
|
97
189
|
return JSON.parse(r.stdout);
|
|
98
190
|
}
|
|
@@ -101,113 +193,285 @@ function runSrtWinJson(args) {
|
|
|
101
193
|
`${JSON.stringify(r.stdout)}: ${e.message}`);
|
|
102
194
|
}
|
|
103
195
|
}
|
|
104
|
-
// ────────────────────────────────────────────────────────────────────
|
|
105
|
-
// Status / install API
|
|
106
|
-
// ────────────────────────────────────────────────────────────────────
|
|
107
196
|
/**
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
* a fresh logon is needed before {@link initialize} can succeed.
|
|
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`).
|
|
113
201
|
*/
|
|
114
|
-
|
|
115
|
-
|
|
202
|
+
function runSrtWinJsonAllowFail(args, opts) {
|
|
203
|
+
const r = runSrtWin(args, opts);
|
|
204
|
+
let json;
|
|
205
|
+
try {
|
|
206
|
+
json = JSON.parse(r.stdout);
|
|
207
|
+
}
|
|
208
|
+
catch (e) {
|
|
209
|
+
throw new Error(`srt-win ${args.join(' ')}: unparseable JSON output ` +
|
|
210
|
+
`${JSON.stringify(r.stdout)}: ${e.message}`);
|
|
211
|
+
}
|
|
212
|
+
return { ok: r.status === 0, json, stderr: r.stderr };
|
|
116
213
|
}
|
|
214
|
+
// ────────────────────────────────────────────────────────────────────
|
|
215
|
+
// Status / install API
|
|
216
|
+
// ────────────────────────────────────────────────────────────────────
|
|
117
217
|
/**
|
|
118
|
-
* Query the WFP filter set under the given sublayer
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
* filters installed by other tooling without the
|
|
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}.
|
|
122
227
|
*/
|
|
123
228
|
export function getWindowsWfpStatus(opts = {}) {
|
|
124
229
|
const args = ['wfp', 'status'];
|
|
125
230
|
if (opts.sublayerGuid)
|
|
126
231
|
args.push('--sublayer-guid', opts.sublayerGuid);
|
|
127
|
-
const raw = runSrtWinJson(args);
|
|
232
|
+
const raw = runSrtWinJson(args, { srtWin: opts.srtWin });
|
|
128
233
|
return {
|
|
129
234
|
state: raw.state,
|
|
130
235
|
filters: raw.filters,
|
|
131
236
|
...(raw.port_range && { portRange: raw.port_range }),
|
|
237
|
+
...(raw.user_sid && { userSid: raw.user_sid }),
|
|
238
|
+
...(raw.hint && { hint: raw.hint }),
|
|
132
239
|
};
|
|
133
240
|
}
|
|
134
241
|
/**
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
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
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Query the sandbox user account's provisioning state. Each field
|
|
334
|
+
* is independently observed so a half-provisioned install (e.g.
|
|
335
|
+
* user exists but credential file missing) is distinguishable.
|
|
336
|
+
* Does not require elevation.
|
|
337
|
+
*/
|
|
338
|
+
export function getWindowsSandboxUserStatus(opts = {}) {
|
|
339
|
+
const raw = runSrtWinJson(['user', 'status'], { srtWin: opts.srtWin });
|
|
340
|
+
return {
|
|
341
|
+
provisioned: raw.user.exists,
|
|
342
|
+
...(raw.user.sid && { sid: raw.user.sid }),
|
|
343
|
+
groupExists: raw.user.group_exists,
|
|
344
|
+
...(raw.user.group_sid && { groupSid: raw.user.group_sid }),
|
|
345
|
+
inBuiltinUsers: raw.user.in_builtin_users,
|
|
346
|
+
inSandboxGroup: raw.user.in_sandbox_group,
|
|
347
|
+
hiddenFromLogon: raw.user.hidden_from_logon,
|
|
348
|
+
credPresent: raw.cred_present,
|
|
349
|
+
...(typeof raw.marker_version === 'number' && {
|
|
350
|
+
markerVersion: raw.marker_version,
|
|
351
|
+
}),
|
|
352
|
+
realUserSid: raw.real_user_sid,
|
|
353
|
+
...(raw.ca_cert_thumb && { caCertThumb: raw.ca_cert_thumb }),
|
|
354
|
+
...(raw.ca_cert_pem && { caCertPem: raw.ca_cert_pem }),
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Read back the persistent MITM CA the sandbox was installed with
|
|
359
|
+
* (via `srt-win user trust-ca` / {@link windowsTrustCa}).
|
|
360
|
+
* Returns `null` when no CA was installed. The PEM is what `srt-win
|
|
361
|
+
* user status` reconstructs from the DER stored in `state.db`.
|
|
362
|
+
*
|
|
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.
|
|
368
|
+
*
|
|
369
|
+
* @param status pass an already-fetched
|
|
370
|
+
* {@link getWindowsSandboxUserStatus} result to avoid a second
|
|
371
|
+
* `srt-win user status` spawn.
|
|
372
|
+
*/
|
|
373
|
+
export function getWindowsSandboxCaCert(status, opts = {}) {
|
|
374
|
+
const u = status ?? getWindowsSandboxUserStatus(opts);
|
|
375
|
+
if (!u.caCertThumb || !u.caCertPem)
|
|
376
|
+
return null;
|
|
377
|
+
return { pem: u.caCertPem, thumb: u.caCertThumb };
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Install (or replace) the MITM CA in the **sandbox user's**
|
|
381
|
+
* `CurrentUser\Root` and record it in `state.db` (so
|
|
382
|
+
* {@link getWindowsSandboxCaCert} surfaces its thumbprint + PEM).
|
|
383
|
+
* Thin wrapper around `srt-win user trust-ca <path>`. Does NOT
|
|
384
|
+
* require elevation. Persistent until {@link uninstallWindowsSandbox}
|
|
385
|
+
* deletes the sandbox user's profile.
|
|
386
|
+
*
|
|
387
|
+
* The CA has a separate lifecycle from {@link installWindowsSandbox}
|
|
388
|
+
* — install provisions the account/filters and never touches the CA;
|
|
389
|
+
* call this AFTER install when `tlsTerminate` will be used.
|
|
390
|
+
*
|
|
391
|
+
* @throws when the sandbox user is not provisioned, the file is not a
|
|
392
|
+
* parseable X.509 certificate, or the registry write into the
|
|
393
|
+
* sandbox user's hive fails.
|
|
394
|
+
*/
|
|
395
|
+
export function windowsTrustCa(caCertPath, opts = {}) {
|
|
396
|
+
// 60s: first call may create the sandbox user's profile
|
|
397
|
+
// (LOGON_WITH_PROFILE) via the one-shot CreateProcessWithLogonW.
|
|
398
|
+
const r = runSrtWin(['user', 'trust-ca', caCertPath], {
|
|
399
|
+
timeoutMs: 60000,
|
|
400
|
+
srtWin: opts.srtWin,
|
|
401
|
+
});
|
|
402
|
+
logForDebugging(`[Sandbox Windows] user trust-ca exit=${r.status}: ${r.stderr || r.stdout}`);
|
|
403
|
+
if (r.status !== 0) {
|
|
404
|
+
throw new Error(`srt-win user trust-ca '${caCertPath}' failed (exit ` +
|
|
405
|
+
`${r.status}): ${r.stderr || r.stdout}`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
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.
|
|
139
413
|
*
|
|
140
|
-
* Network for the calling user is **not disrupted
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
* filter-1 (PERMIT group-enabled) takes over for the broker; only
|
|
145
|
-
* `srt-win exec` children (group flipped deny-only) fall through to
|
|
146
|
-
* 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.
|
|
147
418
|
*
|
|
148
|
-
* Returns the post-call
|
|
149
|
-
* UAC prompt this returns `{cancelled: true, …}` rather
|
|
150
|
-
* 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.
|
|
151
422
|
*
|
|
152
|
-
* @throws on
|
|
153
|
-
*
|
|
154
|
-
*
|
|
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.
|
|
155
426
|
*/
|
|
156
427
|
export function installWindowsSandbox(opts = {}) {
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
args.push('--user-sid', opts.userSid);
|
|
428
|
+
const srtWin = opts.srtWin ?? resolveSrtWin();
|
|
429
|
+
const args = ['install'];
|
|
160
430
|
if (opts.sublayerGuid)
|
|
161
431
|
args.push('--sublayer-guid', opts.sublayerGuid);
|
|
162
432
|
if (opts.proxyPortRange) {
|
|
163
433
|
args.push('--proxy-port-range', `${opts.proxyPortRange[0]}-${opts.proxyPortRange[1]}`);
|
|
164
434
|
}
|
|
435
|
+
if (opts.sandboxUser)
|
|
436
|
+
args.push('--sandbox-user', opts.sandboxUser);
|
|
165
437
|
if (opts.force)
|
|
166
438
|
args.push('--force');
|
|
167
|
-
const r = runSrtWin(args);
|
|
439
|
+
const r = runSrtWin(args, { timeoutMs: 60000, srtWin });
|
|
168
440
|
logForDebugging(`[Sandbox Windows] install exit=${r.status}: ${r.stderr || r.stdout}`);
|
|
169
441
|
// srt-win install exit-code contract:
|
|
170
442
|
// 0 ok
|
|
171
443
|
// 10 user cancelled UAC elevation
|
|
172
|
-
// 11 group create failed
|
|
173
444
|
// 12 WFP install failed
|
|
174
445
|
// 13 already installed with different config (use --force)
|
|
446
|
+
// 14 sandbox-user provisioning failed
|
|
175
447
|
// 1 other error (stderr has detail)
|
|
176
448
|
const out = r.stderr || r.stdout;
|
|
449
|
+
const readBack = () => ({
|
|
450
|
+
wfp: getWindowsWfpStatus({ sublayerGuid: opts.sublayerGuid, srtWin }),
|
|
451
|
+
user: getWindowsSandboxUserStatus({ srtWin }),
|
|
452
|
+
});
|
|
177
453
|
switch (r.status) {
|
|
178
454
|
case 0:
|
|
179
|
-
|
|
455
|
+
return readBack();
|
|
180
456
|
case 10:
|
|
181
|
-
return {
|
|
182
|
-
group: getWindowsGroupStatus(opts),
|
|
183
|
-
wfp: getWindowsWfpStatus({ sublayerGuid: opts.sublayerGuid }),
|
|
184
|
-
cancelled: true,
|
|
185
|
-
};
|
|
186
|
-
case 11:
|
|
187
|
-
throw new Error(`srt-win install: group create failed: ${out}`);
|
|
457
|
+
return { ...readBack(), cancelled: true };
|
|
188
458
|
case 12:
|
|
189
459
|
throw new Error(`srt-win install: WFP filter install failed: ${out}`);
|
|
460
|
+
case 14:
|
|
461
|
+
throw new Error(`srt-win install: sandbox user provisioning failed: ${out}`);
|
|
190
462
|
case 13:
|
|
191
463
|
throw new Error(`srt-win install: filters already exist under this sublayer with ` +
|
|
192
|
-
`different
|
|
193
|
-
`
|
|
464
|
+
`a different port range or sandbox-user name. Pass ` +
|
|
465
|
+
`{force: true} to replace, or pick a different sublayerGuid. ` +
|
|
194
466
|
`Output: ${out}`);
|
|
195
467
|
default:
|
|
196
468
|
throw new Error(`srt-win install failed (exit ${r.status}): ${out}`);
|
|
197
469
|
}
|
|
198
|
-
return {
|
|
199
|
-
group: getWindowsGroupStatus(opts),
|
|
200
|
-
wfp: getWindowsWfpStatus({ sublayerGuid: opts.sublayerGuid }),
|
|
201
|
-
};
|
|
202
470
|
}
|
|
203
471
|
/**
|
|
204
|
-
* Remove the WFP filter set under `sublayerGuid`
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
* **Does NOT delete the discriminator group** — group membership is
|
|
208
|
-
* persistent user state and removing it would force every user to
|
|
209
|
-
* re-do the logout dance on the next install. Call
|
|
210
|
-
* {@link deleteWindowsGroup} explicitly if you want full teardown.
|
|
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.
|
|
211
475
|
*
|
|
212
476
|
* @returns `{cancelled: true}` if the user dismissed UAC.
|
|
213
477
|
*/
|
|
@@ -215,7 +479,9 @@ export function uninstallWindowsSandbox(opts = {}) {
|
|
|
215
479
|
const args = ['uninstall'];
|
|
216
480
|
if (opts.sublayerGuid)
|
|
217
481
|
args.push('--sublayer-guid', opts.sublayerGuid);
|
|
218
|
-
|
|
482
|
+
if (opts.keepUser)
|
|
483
|
+
args.push('--keep-user');
|
|
484
|
+
const r = runSrtWin(args, { srtWin: opts.srtWin });
|
|
219
485
|
logForDebugging(`[Sandbox Windows] uninstall exit=${r.status}: ${r.stderr || r.stdout}`);
|
|
220
486
|
if (r.status === 10)
|
|
221
487
|
return { cancelled: true };
|
|
@@ -225,64 +491,249 @@ export function uninstallWindowsSandbox(opts = {}) {
|
|
|
225
491
|
return {};
|
|
226
492
|
}
|
|
227
493
|
/**
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
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.
|
|
232
503
|
*/
|
|
233
|
-
export function
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
504
|
+
export function expandWindowsFsPaths(patterns) {
|
|
505
|
+
const out = new Set();
|
|
506
|
+
for (const raw of patterns) {
|
|
507
|
+
const norm = normalizePathForSandbox(raw);
|
|
508
|
+
const candidates = containsGlobCharsWin(norm)
|
|
509
|
+
? expandGlobPattern(norm, { caseInsensitive: true })
|
|
510
|
+
: [norm];
|
|
511
|
+
for (const c of candidates) {
|
|
512
|
+
const st = fs.statSync(c, { throwIfNoEntry: false });
|
|
513
|
+
if (!st)
|
|
514
|
+
continue;
|
|
515
|
+
out.add(c);
|
|
516
|
+
}
|
|
238
517
|
}
|
|
239
|
-
|
|
518
|
+
return [...out];
|
|
240
519
|
}
|
|
241
520
|
/**
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
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.
|
|
526
|
+
*
|
|
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}.
|
|
530
|
+
*
|
|
531
|
+
* @throws on exit ≠ 0 — including exit 2 (one or more inputs
|
|
532
|
+
* skipped). srt-win stamps the resolvable inputs before exiting
|
|
533
|
+
* 2, so on throw the caller should call {@link restoreWindowsAcl}
|
|
534
|
+
* to release whatever WAS stamped.
|
|
247
535
|
*/
|
|
248
|
-
export function
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
536
|
+
export function stampWindowsAcl(opts) {
|
|
537
|
+
const holder = opts.holderPid ?? process.pid;
|
|
538
|
+
const stdin = JSON.stringify({
|
|
539
|
+
denyRead: opts.denyRead,
|
|
540
|
+
denyWrite: opts.denyWrite,
|
|
541
|
+
});
|
|
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 });
|
|
550
|
+
logForDebugging(`[Sandbox Windows] acl stamp exit=${r.status}: ${r.stderr || r.stdout}`);
|
|
253
551
|
if (r.status !== 0) {
|
|
254
|
-
throw new Error(`srt-win
|
|
255
|
-
|
|
256
|
-
|
|
552
|
+
throw new Error(`srt-win acl stamp exited ${r.status} ` +
|
|
553
|
+
(r.status === 2 ? '(partial — some inputs skipped)' : '(failed)') +
|
|
554
|
+
`: ${r.stderr || r.stdout}`);
|
|
257
555
|
}
|
|
258
|
-
logForDebugging(`[Sandbox Windows] group create: ${r.stderr || r.stdout}`);
|
|
259
556
|
}
|
|
260
557
|
/**
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
* existing srt-win-tagged filters under that sublayer.
|
|
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).
|
|
267
563
|
*/
|
|
268
|
-
export function
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
564
|
+
export function restoreWindowsAcl(opts) {
|
|
565
|
+
const holder = opts.holderPid ?? process.pid;
|
|
566
|
+
// Don't let a teardown helper throw — the caller's reset() must
|
|
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.
|
|
571
|
+
try {
|
|
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 });
|
|
581
|
+
if (!r.ok) {
|
|
582
|
+
logForDebugging(`[Sandbox Windows] acl restore exited non-zero (per-path ` +
|
|
583
|
+
`outcomes preserved): ${r.stderr}`, { level: 'error' });
|
|
584
|
+
}
|
|
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 ?? [])];
|
|
591
|
+
}
|
|
592
|
+
catch (e) {
|
|
593
|
+
logForDebugging(`[Sandbox Windows] acl restore: ${e.message}`, {
|
|
594
|
+
level: 'error',
|
|
595
|
+
});
|
|
596
|
+
return undefined;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
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.
|
|
609
|
+
*/
|
|
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}`);
|
|
276
622
|
if (r.status !== 0) {
|
|
277
|
-
throw new Error(`srt-win
|
|
278
|
-
|
|
279
|
-
|
|
623
|
+
throw new Error(`srt-win acl grant exited ${r.status}: ${r.stderr || r.stdout}`);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
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.
|
|
630
|
+
*/
|
|
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;
|
|
280
653
|
}
|
|
281
|
-
logForDebugging(`[Sandbox Windows] wfp install: ${r.stderr || r.stdout}`);
|
|
282
654
|
}
|
|
283
655
|
// ────────────────────────────────────────────────────────────────────
|
|
284
656
|
// Wrap
|
|
285
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
|
+
}
|
|
286
737
|
/**
|
|
287
738
|
* Build the spawn descriptor for running `command` inside the Windows
|
|
288
739
|
* sandbox: an `argv` array plus the `env` to spawn it with.
|
|
@@ -290,41 +741,111 @@ export function createWindowsWfp(ref) {
|
|
|
290
741
|
* Caller MUST spawn the result with `{shell: false}` — that is the
|
|
291
742
|
* security boundary that keeps untrusted bytes off the host's shell
|
|
292
743
|
* (the inner `cmd.exe /c` runs INSIDE the sandbox; see
|
|
293
|
-
* `vendor/srt-win/src/launch.rs` `build_cmdline` for the passthrough
|
|
744
|
+
* `vendor/srt-win-src/src/launch.rs` `build_cmdline` for the passthrough
|
|
294
745
|
* rationale) — AND with the returned `env`.
|
|
295
746
|
*
|
|
296
747
|
* Proxy configuration is single-sourced by {@link generateProxyEnvVars}
|
|
297
748
|
* (the same canonical builder used on macOS/Linux). `srt-win exec`
|
|
298
749
|
* takes no `--http-proxy` / `--socks-proxy` flags and synthesizes no
|
|
299
|
-
* proxy env
|
|
300
|
-
*
|
|
301
|
-
*
|
|
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.
|
|
302
754
|
*/
|
|
303
755
|
export function wrapCommandWithSandboxWindows(p) {
|
|
304
|
-
const exe =
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
// single argv element; srt-win's build_cmdline wraps it in one
|
|
319
|
-
// outer "…" pair for /s to consume. See launch.rs.
|
|
320
|
-
argv.push(path.join(systemRoot, 'System32', 'cmd.exe'), '/d', '/s', '/c', p.command);
|
|
321
|
-
}
|
|
322
|
-
// Generated proxy vars override any inherited ones so the child
|
|
323
|
-
// always routes through this sandbox's proxies.
|
|
324
|
-
const generated = envListToObject(generateProxyEnvVars(p.httpProxyPort, p.socksProxyPort, undefined, p.proxyAuthToken));
|
|
756
|
+
const { exe, prependArgs } = p.srtWin ?? resolveSrtWin();
|
|
757
|
+
// Generated proxy + CA-trust env. Single-sourced here so the
|
|
758
|
+
// same object feeds (a) the spawn env merge below and (b) the
|
|
759
|
+
// explicit `--env` overlay for the runner.
|
|
760
|
+
//
|
|
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));
|
|
325
770
|
// TMPDIR is a POSIX path meant for the macOS/Linux FS sandbox — it
|
|
326
771
|
// serves no purpose on Windows and breaks msys2 tools (mktemp etc.).
|
|
327
772
|
delete generated.TMPDIR;
|
|
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}`);
|
|
806
|
+
}
|
|
807
|
+
argv.push('--');
|
|
808
|
+
const systemRoot = process.env.SystemRoot ?? 'C:\\Windows';
|
|
809
|
+
const sh = p.binShell ?? { kind: 'cmd' };
|
|
810
|
+
switch (sh.kind) {
|
|
811
|
+
case 'bash':
|
|
812
|
+
// Git Bash: invoke the caller-supplied path directly with
|
|
813
|
+
// `-c <command>`. `command` is a fully-assembled bash command
|
|
814
|
+
// string with its own internal quoting; srt-win's `build_cmdline`
|
|
815
|
+
// takes the generic non-cmd branch and MSVCRT-quotes it as a
|
|
816
|
+
// SINGLE argv element, so bash receives it intact as argv[2].
|
|
817
|
+
argv.push(sh.path, '-c', p.command);
|
|
818
|
+
break;
|
|
819
|
+
case 'pwsh':
|
|
820
|
+
argv.push('pwsh.exe', '-NoProfile', '-Command', p.command);
|
|
821
|
+
break;
|
|
822
|
+
case 'powershell':
|
|
823
|
+
argv.push(path.join(systemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe'), '-NoProfile', '-Command', p.command);
|
|
824
|
+
break;
|
|
825
|
+
case 'cmd':
|
|
826
|
+
// cmd /d (no AutoRun) /s (strip first+last quote of post-/c by
|
|
827
|
+
// position) /c (run-then-exit). The `command` string lands as a
|
|
828
|
+
// single argv element; srt-win's build_cmdline wraps it in one
|
|
829
|
+
// outer "…" pair for /s to consume. See launch.rs.
|
|
830
|
+
argv.push(path.join(systemRoot, 'System32', 'cmd.exe'), '/d', '/s', '/c', p.command);
|
|
831
|
+
break;
|
|
832
|
+
}
|
|
833
|
+
// CreateProcessW's lpCommandLine is capped at 32 767 WCHARs.
|
|
834
|
+
// Node's `shell:false` spawn builds it by MSVCRT-quoting each
|
|
835
|
+
// argv element and joining with spaces; ~30 000 leaves headroom
|
|
836
|
+
// for the quote overhead the estimate doesn't model.
|
|
837
|
+
const cmdlineEstimate = argv.reduce((n, a) => n + a.length + 3, 0);
|
|
838
|
+
if (cmdlineEstimate > 30000) {
|
|
839
|
+
throw new Error(`Windows sandbox argv is ~${cmdlineEstimate} chars ` +
|
|
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).
|
|
328
849
|
const env = { ...process.env, ...generated };
|
|
329
850
|
return { argv, env };
|
|
330
851
|
}
|
|
@@ -348,80 +869,71 @@ function envListToObject(list) {
|
|
|
348
869
|
// ────────────────────────────────────────────────────────────────────
|
|
349
870
|
/**
|
|
350
871
|
* Install instructions, surfaced verbatim in error messages.
|
|
351
|
-
* Tailored to the observed group state: if the install already
|
|
352
|
-
* ran (`created-not-on-token`), only the logout is missing.
|
|
353
872
|
*/
|
|
354
|
-
export function windowsInstallInstructions(
|
|
355
|
-
if (groupState === 'created-not-on-token') {
|
|
356
|
-
return (`The discriminator group exists but is not yet in this session's ` +
|
|
357
|
-
`token. LOG OUT and back in to pick up the new group membership ` +
|
|
358
|
-
`(it enters TokenGroups at logon). Network is not disrupted ` +
|
|
359
|
-
`meanwhile — WFP filter-0 PERMITs traffic while the group is absent ` +
|
|
360
|
-
`from your token.`);
|
|
361
|
-
}
|
|
362
|
-
const g = ref.groupSid
|
|
363
|
-
? `--group-sid ${ref.groupSid}`
|
|
364
|
-
: `--name ${ref.groupName ?? DEFAULT_WINDOWS_GROUP_NAME}`;
|
|
873
|
+
export function windowsInstallInstructions(sublayerGuid) {
|
|
365
874
|
const sl = sublayerGuid ? ` --sublayer-guid ${sublayerGuid}` : '';
|
|
366
875
|
return (`Windows sandbox needs a one-time install (one UAC prompt):\n` +
|
|
367
876
|
` npx sandbox-runtime windows-install\n` +
|
|
368
877
|
` — or call installWindowsSandbox(), or run ` +
|
|
369
|
-
`\`srt-win.exe install
|
|
370
|
-
`
|
|
371
|
-
|
|
372
|
-
`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.`);
|
|
373
881
|
}
|
|
374
882
|
/**
|
|
375
883
|
* Check the Windows backend is ready to sandbox. Errors block
|
|
376
884
|
* `initialize()`; warnings are informational.
|
|
377
885
|
*/
|
|
378
|
-
export function checkWindowsDependencies(
|
|
886
|
+
export function checkWindowsDependencies(opts = {}) {
|
|
887
|
+
const { sublayerGuid } = opts;
|
|
379
888
|
const errors = [];
|
|
380
889
|
const warnings = [];
|
|
381
|
-
// 1. Binary present
|
|
382
|
-
|
|
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;
|
|
383
894
|
try {
|
|
384
|
-
|
|
895
|
+
srtWin = opts.srtWin ?? resolveSrtWin();
|
|
385
896
|
}
|
|
386
897
|
catch (e) {
|
|
387
898
|
return { errors: [e.message], warnings };
|
|
388
899
|
}
|
|
389
|
-
logForDebugging(`[Sandbox Windows] using srt-win at ${exe}`);
|
|
390
|
-
// 2.
|
|
391
|
-
let
|
|
900
|
+
logForDebugging(`[Sandbox Windows] using srt-win at ${srtWin.exe}`);
|
|
901
|
+
// 2. Sandbox user provisioned + credential readable.
|
|
902
|
+
let us;
|
|
392
903
|
try {
|
|
393
|
-
|
|
904
|
+
us = getWindowsSandboxUserStatus({ srtWin });
|
|
394
905
|
}
|
|
395
906
|
catch (e) {
|
|
396
|
-
errors.push(`srt-win
|
|
907
|
+
errors.push(`srt-win user status failed: ${e.message}`);
|
|
397
908
|
return { errors, warnings };
|
|
398
909
|
}
|
|
399
|
-
if (
|
|
400
|
-
errors.push(`
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
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));
|
|
404
914
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
//
|
|
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.
|
|
408
919
|
let ws;
|
|
409
920
|
try {
|
|
410
|
-
ws = getWindowsWfpStatus({ sublayerGuid });
|
|
921
|
+
ws = getWindowsWfpStatus({ sublayerGuid, srtWin });
|
|
411
922
|
}
|
|
412
923
|
catch (e) {
|
|
413
924
|
errors.push(`srt-win wfp status failed: ${e.message}`);
|
|
414
925
|
return { errors, warnings };
|
|
415
926
|
}
|
|
416
|
-
if (ws.state
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
//
|
|
421
|
-
|
|
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) {
|
|
422
934
|
errors.push(`WFP filters not installed under sublayer ` +
|
|
423
935
|
`${sublayerGuid ?? '(default)'}. ` +
|
|
424
|
-
windowsInstallInstructions(
|
|
936
|
+
windowsInstallInstructions(sublayerGuid));
|
|
425
937
|
}
|
|
426
938
|
}
|
|
427
939
|
else if (ws.portRange) {
|