@sysid/sandbox-runtime-improved 0.0.55-sysid.1 → 0.0.61-sysid.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/README.md +43 -2
  2. package/dist/cli.js +8 -0
  3. package/dist/cli.js.map +1 -1
  4. package/dist/index.d.ts +6 -6
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +2 -2
  7. package/dist/index.js.map +1 -1
  8. package/dist/sandbox/credential-mask-files.d.ts +72 -0
  9. package/dist/sandbox/credential-mask-files.d.ts.map +1 -0
  10. package/dist/sandbox/credential-mask-files.js +146 -0
  11. package/dist/sandbox/credential-mask-files.js.map +1 -0
  12. package/dist/sandbox/credential-sentinel.d.ts +72 -0
  13. package/dist/sandbox/credential-sentinel.d.ts.map +1 -0
  14. package/dist/sandbox/credential-sentinel.js +130 -0
  15. package/dist/sandbox/credential-sentinel.js.map +1 -0
  16. package/dist/sandbox/domain-pattern.d.ts +36 -0
  17. package/dist/sandbox/domain-pattern.d.ts.map +1 -0
  18. package/dist/sandbox/domain-pattern.js +60 -0
  19. package/dist/sandbox/domain-pattern.js.map +1 -0
  20. package/dist/sandbox/http-proxy.d.ts +32 -1
  21. package/dist/sandbox/http-proxy.d.ts.map +1 -1
  22. package/dist/sandbox/http-proxy.js +10 -2
  23. package/dist/sandbox/http-proxy.js.map +1 -1
  24. package/dist/sandbox/linux-sandbox-utils.d.ts +17 -0
  25. package/dist/sandbox/linux-sandbox-utils.d.ts.map +1 -1
  26. package/dist/sandbox/linux-sandbox-utils.js +155 -47
  27. package/dist/sandbox/linux-sandbox-utils.js.map +1 -1
  28. package/dist/sandbox/listen-in-range.d.ts +12 -0
  29. package/dist/sandbox/listen-in-range.d.ts.map +1 -0
  30. package/dist/sandbox/listen-in-range.js +43 -0
  31. package/dist/sandbox/listen-in-range.js.map +1 -0
  32. package/dist/sandbox/macos-sandbox-utils.d.ts +13 -0
  33. package/dist/sandbox/macos-sandbox-utils.d.ts.map +1 -1
  34. package/dist/sandbox/macos-sandbox-utils.js +76 -14
  35. package/dist/sandbox/macos-sandbox-utils.js.map +1 -1
  36. package/dist/sandbox/mitm-ca.d.ts +18 -2
  37. package/dist/sandbox/mitm-ca.d.ts.map +1 -1
  38. package/dist/sandbox/mitm-ca.js +49 -9
  39. package/dist/sandbox/mitm-ca.js.map +1 -1
  40. package/dist/sandbox/mitm-leaf.d.ts.map +1 -1
  41. package/dist/sandbox/mitm-leaf.js +24 -6
  42. package/dist/sandbox/mitm-leaf.js.map +1 -1
  43. package/dist/sandbox/mux-proxy.d.ts +59 -0
  44. package/dist/sandbox/mux-proxy.d.ts.map +1 -0
  45. package/dist/sandbox/mux-proxy.js +159 -0
  46. package/dist/sandbox/mux-proxy.js.map +1 -0
  47. package/dist/sandbox/request-filter.d.ts +11 -1
  48. package/dist/sandbox/request-filter.d.ts.map +1 -1
  49. package/dist/sandbox/request-filter.js.map +1 -1
  50. package/dist/sandbox/sandbox-config.d.ts +383 -1
  51. package/dist/sandbox/sandbox-config.d.ts.map +1 -1
  52. package/dist/sandbox/sandbox-config.js +252 -1
  53. package/dist/sandbox/sandbox-config.js.map +1 -1
  54. package/dist/sandbox/sandbox-manager.d.ts +4 -0
  55. package/dist/sandbox/sandbox-manager.d.ts.map +1 -1
  56. package/dist/sandbox/sandbox-manager.js +618 -168
  57. package/dist/sandbox/sandbox-manager.js.map +1 -1
  58. package/dist/sandbox/sandbox-schemas.d.ts +27 -0
  59. package/dist/sandbox/sandbox-schemas.d.ts.map +1 -1
  60. package/dist/sandbox/sandbox-utils.d.ts +42 -6
  61. package/dist/sandbox/sandbox-utils.d.ts.map +1 -1
  62. package/dist/sandbox/sandbox-utils.js +115 -27
  63. package/dist/sandbox/sandbox-utils.js.map +1 -1
  64. package/dist/sandbox/socks-proxy.d.ts +11 -5
  65. package/dist/sandbox/socks-proxy.d.ts.map +1 -1
  66. package/dist/sandbox/socks-proxy.js +13 -81
  67. package/dist/sandbox/socks-proxy.js.map +1 -1
  68. package/dist/sandbox/tls-terminate-proxy.d.ts +2 -2
  69. package/dist/sandbox/tls-terminate-proxy.d.ts.map +1 -1
  70. package/dist/sandbox/tls-terminate-proxy.js +31 -5
  71. package/dist/sandbox/tls-terminate-proxy.js.map +1 -1
  72. package/dist/sandbox/windows-sandbox-utils.d.ts +347 -11
  73. package/dist/sandbox/windows-sandbox-utils.d.ts.map +1 -1
  74. package/dist/sandbox/windows-sandbox-utils.js +437 -45
  75. package/dist/sandbox/windows-sandbox-utils.js.map +1 -1
  76. package/package.json +7 -4
  77. package/vendor/seccomp/build.ts +8 -30
  78. package/vendor/srt-win/build.ts +21 -0
  79. package/dist/vendor/seccomp/Dockerfile.build +0 -6
  80. package/dist/vendor/seccomp/arm64/apply-seccomp +0 -0
  81. package/dist/vendor/seccomp/build.ts +0 -91
  82. package/dist/vendor/seccomp/x64/apply-seccomp +0 -0
  83. package/dist/vendor/seccomp-src/apply-seccomp.c +0 -280
  84. package/dist/vendor/seccomp-src/seccomp-unix-block.c +0 -148
  85. package/dist/vendor/srt-win/Cargo.lock +0 -361
  86. package/dist/vendor/srt-win/Cargo.toml +0 -46
  87. package/dist/vendor/srt-win/ci/cleanup.ps1 +0 -49
  88. package/dist/vendor/srt-win/ci/smoke-exec.ps1 +0 -446
  89. package/dist/vendor/srt-win/ci/smoke.ps1 +0 -307
  90. package/dist/vendor/srt-win/src/job.rs +0 -102
  91. package/dist/vendor/srt-win/src/launch.rs +0 -732
  92. package/dist/vendor/srt-win/src/lib.rs +0 -20
  93. package/dist/vendor/srt-win/src/main.rs +0 -669
  94. package/dist/vendor/srt-win/src/self_protect.rs +0 -169
  95. package/dist/vendor/srt-win/src/sid.rs +0 -296
  96. package/dist/vendor/srt-win/src/token.rs +0 -341
  97. package/dist/vendor/srt-win/src/util.rs +0 -42
  98. package/dist/vendor/srt-win/src/wfp.rs +0 -992
  99. package/dist/vendor/srt-win/src/winsta.rs +0 -209
  100. package/dist/vendor/srt-win/tests/sd_access_check_matrix.rs +0 -259
  101. package/vendor/seccomp-src/apply-seccomp.c +0 -280
  102. package/vendor/seccomp-src/seccomp-unix-block.c +0 -148
  103. package/vendor/srt-win/Cargo.lock +0 -361
  104. package/vendor/srt-win/Cargo.toml +0 -46
  105. package/vendor/srt-win/ci/cleanup.ps1 +0 -49
  106. package/vendor/srt-win/ci/smoke-exec.ps1 +0 -446
  107. package/vendor/srt-win/ci/smoke.ps1 +0 -307
  108. package/vendor/srt-win/src/job.rs +0 -102
  109. package/vendor/srt-win/src/launch.rs +0 -732
  110. package/vendor/srt-win/src/lib.rs +0 -20
  111. package/vendor/srt-win/src/main.rs +0 -669
  112. package/vendor/srt-win/src/self_protect.rs +0 -169
  113. package/vendor/srt-win/src/sid.rs +0 -296
  114. package/vendor/srt-win/src/token.rs +0 -341
  115. package/vendor/srt-win/src/util.rs +0 -42
  116. package/vendor/srt-win/src/wfp.rs +0 -992
  117. package/vendor/srt-win/src/winsta.rs +0 -209
  118. package/vendor/srt-win/tests/sd_access_check_matrix.rs +0 -259
@@ -1,446 +0,0 @@
1
- <#
2
- Smoke test for `srt-win exec`.
3
-
4
- Self-contained: provisions WFP filters under a fixed test-only
5
- sublayer GUID and uses `BUILTIN\Administrators` (S-1-5-32-544) as
6
- the discriminator group SID. We do NOT create a custom group.
7
-
8
- Why Administrators as the group:
9
- - The custom-group approach can't exercise the WFP fence on
10
- hosted CI: the runner can't logout/login mid-job, so a freshly
11
- created group is *absent* from the token (not deny-only). With
12
- the machine-wide filter shape, an absent-group child is
13
- PERMITted by the non-member filter — the fence test would lie.
14
- - `BUILTIN\Administrators` IS in the runner token (the GHA
15
- Windows runner runs as admin). `srt-win exec` flips it
16
- deny-only along with the discriminator. The child therefore
17
- genuinely matches the BLOCK filter and is fenced.
18
- - It also means the broker pre-flight passes without
19
- `--skip-group-check` (Admins is enabled in the broker token),
20
- so we exercise the normal pre-flight path.
21
-
22
- This is a CI-only configuration. Production callers use a
23
- dedicated group. The `srt-win exec` code path is identical.
24
-
25
- The fixed sublayer GUID lets the workflow's `if: always()`
26
- cleanup step uninstall any leaked filters even if this script
27
- throws mid-run.
28
- #>
29
- param(
30
- [Parameter(Mandatory = $true, Position = 0)]
31
- [string] $Exe
32
- )
33
-
34
- $ErrorActionPreference = 'Stop'
35
-
36
- # Fixed test-only sublayer; distinct from srt-win's compile-time
37
- # default and from anything smoke.ps1 uses. Referenced verbatim by
38
- # the workflow's always()-cleanup step.
39
- $Sublayer = '5b0e64f4-09f1-4c2e-8c97-4d2c0f4e9b7d'
40
- $GroupSid = 'S-1-5-32-544' # BUILTIN\Administrators
41
- # Loopback PERMIT is scoped to this port range (filter 2). Anything
42
- # on 127.0.0.1 outside it is BLOCKed (filter 3). Match srt-win's
43
- # default but pass it explicitly so this script doesn't drift if
44
- # the default ever changes.
45
- $PortRange = '60080-60089'
46
- $PortLo = 60080
47
- $PortHi = 60089
48
-
49
- # Bind a TcpListener on the first free port from $candidates.
50
- # Throws if none bind.
51
- function Bind-Listener {
52
- param([int[]] $Candidates)
53
- foreach ($p in $Candidates) {
54
- try {
55
- $l = [System.Net.Sockets.TcpListener]::new(
56
- [System.Net.IPAddress]::Loopback, $p)
57
- $l.Start()
58
- return $l
59
- } catch {
60
- # port in use — try next
61
- }
62
- }
63
- throw "no free port among: $($Candidates -join ',')"
64
- }
65
-
66
- function Run {
67
- param([string[]] $argv)
68
- & $Exe @argv
69
- if ($LASTEXITCODE -ne 0) {
70
- throw "srt-win $($argv -join ' ') exited $LASTEXITCODE"
71
- }
72
- }
73
- function J { param([string[]] $argv) Run $argv | ConvertFrom-Json }
74
-
75
- # Capture exit code + output without throwing on non-zero.
76
- # `srt-win exec` writes its own diagnostics (self-protect SDDL,
77
- # pre-flight warnings, errors) to stderr with a `srt-win:`
78
- # prefix; the CHILD's output is everything else. We merge
79
- # 2>&1 so nothing is lost, then split:
80
- # .exit — exit code
81
- # .raw — full merged output (use for E-rows that assert on
82
- # srt-win's own messages: E6 diag, E9, E10, E10b)
83
- # .out — child output only (lines NOT starting `srt-win:`),
84
- # rejoined; use for E-rows that parse what the
85
- # sandboxed child wrote: E2/E4/E5/E7
86
- function Exec {
87
- param([string[]] $tail)
88
- $argv = @('exec', '--group-sid', $GroupSid) + $tail
89
- $raw = & $Exe @argv 2>&1 | Out-String
90
- $exit = $LASTEXITCODE
91
- $lines = $raw -split "`r?`n"
92
- $child = ($lines | Where-Object { $_ -notmatch '^srt-win:' }) -join "`n"
93
- return [pscustomobject]@{
94
- exit = $exit; raw = $raw; out = $child
95
- }
96
- }
97
-
98
- $cmd = Join-Path $env:SystemRoot 'System32\cmd.exe'
99
- $pwsh = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
100
- # Enable srt-win's per-exec stderr diagnostics (notably the
101
- # self-protect SDDL dump that E6 records). Production callers
102
- # leave this unset; the Exec helper's .out filter tolerates it
103
- # either way.
104
- $env:SANDBOX_RUNTIME_WIN_DEBUG = '1'
105
- Write-Host "smoke-exec: group_sid=$GroupSid sublayer=$Sublayer exe=$Exe"
106
-
107
- # ── precondition: Administrators is enabled in this token ───────
108
- # If the runner ever stops running as admin, the BLOCK filter
109
- # wouldn't apply and E3 would false-pass; fail loudly here instead.
110
- $gs = J @('group','status','--group-sid',$GroupSid)
111
- if ($gs.state -ne 'ready') {
112
- throw "smoke-exec requires BUILTIN\Administrators enabled in the " +
113
- "broker token (got state=$($gs.state)). This script depends " +
114
- "on the GHA Windows runner running elevated."
115
- }
116
-
117
- # ── setup: WFP filters under the test sublayer ──────────────────
118
- Run @('wfp','install',
119
- '--group-sid',$GroupSid,
120
- '--sublayer-guid',$Sublayer,
121
- '--proxy-port-range',$PortRange)
122
- $ws = J @('wfp','status','--sublayer-guid',$Sublayer)
123
- if ($ws.state -ne 'installed') {
124
- throw "wfp not installed under test sublayer: $($ws.state)"
125
- }
126
-
127
- # ── E1: exit code propagates verbatim ────────────────────────────
128
- $r = Exec @('--', $cmd, '/c', 'exit 42')
129
- if ($r.exit -ne 42) {
130
- throw "E1: expected exit 42, got $($r.exit). out: $($r.out)"
131
- }
132
- Write-Host 'E1 ok: exit code propagates'
133
-
134
- # ── E2: group SID is deny-only in the child's token ──────────────
135
- # `/FO CSV /NH` — machine-parseable, no header. The default table
136
- # format pads columns to the widest value, which the SID column
137
- # won't survive when the runner has long group names.
138
- $r = Exec @('--', $cmd, '/c', 'whoami /groups /FO CSV /NH')
139
- if ($r.exit -ne 0) { throw "E2: whoami exited $($r.exit): $($r.out)" }
140
- $rows = $r.out | ConvertFrom-Csv -Header Name,Type,SID,Attributes
141
- $g = $rows | Where-Object { $_.SID -eq $GroupSid }
142
- if (-not $g) {
143
- throw "E2: SID $GroupSid not in whoami /groups:`n$($r.out)"
144
- }
145
- if ($g.Attributes -notmatch '(?i)deny') {
146
- throw "E2: $GroupSid attrs '$($g.Attributes)' — expected " +
147
- "'Group used for deny only'"
148
- }
149
- Write-Host 'E2 ok: discriminator SID is deny-only in child token'
150
-
151
- # ── E3: outbound network blocked when no proxy is configured ─────
152
- $r = Exec @('--', $cmd, '/c', 'curl -sS -m 5 https://example.com')
153
- if ($r.exit -eq 0) {
154
- throw "E3: outbound curl succeeded under sandbox " +
155
- "(fence not in effect?). out: $($r.out)"
156
- }
157
- Write-Host "E3 ok: outbound blocked (curl exit=$($r.exit))"
158
-
159
- # ── E4: loopback in proxy-port-range permitted ───────────────────
160
- # Bind a real listener on the broker side at an in-range port and
161
- # connect from inside the sandbox — proves filter 2 (PERMIT 127/8
162
- # ∩ port-range) fires. A closed port wouldn't distinguish
163
- # WFP-block from RST, hence the live listener. Try the high half
164
- # of the range to avoid clashing with anything smoke.ps1 binds.
165
- $inRange = Bind-Listener ($PortHi..($PortLo+5))
166
- $portIn = $inRange.LocalEndpoint.Port
167
- try {
168
- $r = Exec @('--', $pwsh, '-NoProfile', '-Command',
169
- "(Test-NetConnection 127.0.0.1 -Port $portIn " +
170
- "-WarningAction SilentlyContinue).TcpTestSucceeded")
171
- if ($r.exit -ne 0) {
172
- throw "E4: Test-NetConnection exited $($r.exit): $($r.out)"
173
- }
174
- if ($r.out -notmatch '(?i)\bTrue\b') {
175
- throw "E4: loopback connect to in-range port $portIn did " +
176
- "not succeed. out: $($r.out)"
177
- }
178
- Write-Host "E4 ok: loopback to in-range port $portIn permitted"
179
- } finally {
180
- $inRange.Stop()
181
- }
182
-
183
- # ── E4b: loopback outside proxy-port-range blocked ───────────────
184
- # Same setup but on a port well outside the range. The listener
185
- # is reachable from the broker (we bind it), but the sandboxed
186
- # child must NOT reach it — filter 3 BLOCKs.
187
- $outRange = Bind-Listener (50000, 50001, 50002, 49999)
188
- $portOut = $outRange.LocalEndpoint.Port
189
- try {
190
- $r = Exec @('--', $pwsh, '-NoProfile', '-Command',
191
- "(Test-NetConnection 127.0.0.1 -Port $portOut " +
192
- "-WarningAction SilentlyContinue).TcpTestSucceeded")
193
- if ($r.out -match '(?i)\bTrue\b') {
194
- throw "E4b: loopback to out-of-range port $portOut " +
195
- "succeeded (range tightening not in effect?). out: $($r.out)"
196
- }
197
- # Sanity: prove the listener was actually live (reachable from
198
- # the unsandboxed broker), so a False isn't just "port closed".
199
- $bs = (Test-NetConnection 127.0.0.1 -Port $portOut `
200
- -WarningAction SilentlyContinue).TcpTestSucceeded
201
- if (-not $bs) {
202
- throw "E4b: broker-side connect to its own listener on " +
203
- "$portOut failed — test invalid"
204
- }
205
- Write-Host "E4b ok: loopback to out-of-range port $portOut blocked"
206
- } finally {
207
- $outRange.Stop()
208
- }
209
-
210
- # ── E5: exec forwards the broker's env to the child verbatim ─────
211
- # Proxy config is single-sourced by the TS caller now: `srt-win exec`
212
- # has no --http-proxy/--socks-proxy flags and synthesizes nothing — it
213
- # forwards its OWN environment to the child. Prove that by setting the
214
- # proxy vars in THIS (broker) process and asserting the sandboxed child
215
- # sees the same values. `cmd /c set VAR` prints `VAR=value` if set,
216
- # exits 1 if unset. One Exec per var — no `&` chaining, so this row is
217
- # independent of the cmd-quoting behaviour exercised by E7.
218
-
219
- # Set $Var to $Value for the duration of $Body, then restore exactly
220
- # what was there before (including absence).
221
- function Invoke-WithEnv {
222
- param([string]$Var, [string]$Value, [scriptblock]$Body)
223
- $had = Test-Path "Env:$Var"
224
- $old = if ($had) { (Get-Item "Env:$Var").Value } else { $null }
225
- Set-Item -Path "Env:$Var" -Value $Value
226
- try { & $Body }
227
- finally {
228
- if ($had) { Set-Item -Path "Env:$Var" -Value $old }
229
- else { Remove-Item -Path "Env:$Var" -ErrorAction SilentlyContinue }
230
- }
231
- }
232
-
233
- function Assert-EnvPassthrough {
234
- param([string]$Var, [string]$Want)
235
- Invoke-WithEnv $Var $Want {
236
- $r = Exec @('--', $cmd, '/c', "set $Var")
237
- if ($r.exit -ne 0) {
238
- throw "E5: 'set $Var' exited $($r.exit) (var unset in child?). out: $($r.out)"
239
- }
240
- $line = ($r.out -split "`r?`n" |
241
- Where-Object { $_ -like "$Var=*" } |
242
- Select-Object -First 1)
243
- if ($line -ne "$Var=$Want") {
244
- throw "E5: $Var expected '$Want', got '$line'. full: $($r.out)"
245
- }
246
- }
247
- }
248
- # Values are arbitrary — this proves verbatim passthrough; the real
249
- # values come from the TS generateProxyEnvVars. NO_PROXY doubles as the
250
- # regression guard for the old exec blanking it.
251
- Assert-EnvPassthrough 'HTTPS_PROXY' "http://127.0.0.1:$PortLo"
252
- Assert-EnvPassthrough 'NO_PROXY' 'localhost,127.0.0.1'
253
- Write-Host 'E5 ok: exec forwards broker env (incl. proxy set) to child verbatim'
254
-
255
- # ── E5b: broker restores the twin casing of *_PROXY vars ────────
256
- # The host spawn layer (Node/bun child_process on win32) keeps only ONE
257
- # casing of an env key, but Cygwin/MSYS2 children have case-sensitive
258
- # environments whose tools read the lowercase names — so exec appends
259
- # the missing twin (identical value, never invented). The broker process
260
- # itself can only hold one casing (Win32 env is case-insensitive), so
261
- # setting HTTP_PROXY here and finding BOTH casings in the child proves
262
- # the repair. `set http` lists matching vars with their STORED casing;
263
- # -clike is the case-SENSITIVE match.
264
- Invoke-WithEnv 'HTTP_PROXY' "http://127.0.0.1:$PortLo" {
265
- $r = Exec @('--', $cmd, '/c', 'set http')
266
- if ($r.exit -ne 0) {
267
- throw "E5b: 'set http' exited $($r.exit). out: $($r.out)"
268
- }
269
- $lines = $r.out -split "`r?`n"
270
- if (-not ($lines | Where-Object { $_ -clike 'http_proxy=*' })) {
271
- throw "E5b: lowercase http_proxy twin missing in child. out: $($r.out)"
272
- }
273
- if (-not ($lines | Where-Object { $_ -clike 'HTTP_PROXY=*' })) {
274
- throw "E5b: uppercase HTTP_PROXY missing in child. out: $($r.out)"
275
- }
276
- }
277
- Write-Host 'E5b ok: broker restores the lowercase twin of proxy vars for the child'
278
-
279
- # ── E6: self-protect — child cannot OpenProcess the broker ──────
280
- # The child discovers the broker by walking its parent-process chain
281
- # by NAME (the depth differs between cmd / powershell / git-bash
282
- # children, so no fixed hop count). Win32_Process.ParentProcessId is
283
- # readable without any handle on the broker itself, so discovery does
284
- # not depend on the access self-protect denies. The probe then
285
- # P/Invokes OpenProcess directly with PROCESS_VM_READ — an unambiguous
286
- # mask that the broker-only DACL must deny. (`.NET Process.Handle`
287
- # is NOT sufficient: it lazily falls back to
288
- # PROCESS_QUERY_LIMITED_INFORMATION, which can succeed via paths
289
- # the DACL doesn't fully gate; that produced a false "OPENED"
290
- # on the previous CI run.)
291
- $probe = @'
292
- $bp = 0
293
- $why = ''
294
- $cur = $PID
295
- for ($i = 0; $i -lt 6; $i++) {
296
- $p = Get-CimInstance Win32_Process -Filter "ProcessId=$cur" -ErrorAction SilentlyContinue
297
- if (-not $p) { $why = "WMI query failed at pid $cur (hop $i)"; break }
298
- if ($p.Name -eq 'srt-win.exe') { $bp = [int]$p.ProcessId; break }
299
- if (-not $p.ParentProcessId) { $why = "no parent beyond pid $cur (hop $i)"; break }
300
- $cur = $p.ParentProcessId
301
- }
302
- if ($bp -eq 0) {
303
- if (-not $why) { $why = 'hop cap reached without finding srt-win.exe' }
304
- Write-Output "NOBROKER: $why"
305
- exit 0
306
- }
307
- $sig = '[DllImport("kernel32.dll",SetLastError=true)]public static extern System.IntPtr OpenProcess(uint a,bool b,uint p);'
308
- $k32 = Add-Type -MemberDefinition $sig -Name K32 -Namespace W -PassThru
309
- # 0x0010 = PROCESS_VM_READ
310
- $h = $k32::OpenProcess(0x0010, $false, $bp)
311
- $le = [Runtime.InteropServices.Marshal]::GetLastWin32Error()
312
- if ($h -ne [System.IntPtr]::Zero) {
313
- Write-Output "OPENED:vm_read handle=$h"
314
- } elseif ($le -eq 5) {
315
- Write-Output "DENIED:vm_read le=5"
316
- } else {
317
- Write-Output "OTHER:vm_read le=$le"
318
- }
319
- # Also try PROCESS_QUERY_LIMITED_INFORMATION (0x1000) so the CI
320
- # log records whether THAT is granted — not asserted on, but
321
- # useful diagnostic for the threat model.
322
- $h2 = $k32::OpenProcess(0x1000, $false, $bp)
323
- $le2 = [Runtime.InteropServices.Marshal]::GetLastWin32Error()
324
- Write-Output "INFO:limited_query handle=$h2 le=$le2"
325
- '@
326
- $r = Exec @('--', $pwsh, '-NoProfile', '-Command', $probe)
327
- # self_protect.rs eprintln!s the applied SDDL to stderr; that
328
- # line is in .raw (filtered out of .out by the `srt-win:`
329
- # prefix). Surface it here so the DACL is visible next to the
330
- # probe result.
331
- $sddl = ($r.raw -split "`r?`n" |
332
- Where-Object { $_ -match 'self-protect applied' }) -join ' '
333
- Write-Host "E6 broker DACL: $sddl"
334
- Write-Host "E6 probe output: $($r.out.Trim())"
335
- if ($r.out -match 'NOBROKER') {
336
- $reason = ($r.out -split "`r?`n" |
337
- Where-Object { $_ -match '^NOBROKER' }) -join ' '
338
- throw "E6: broker discovery failed — $reason. raw: $($r.raw)"
339
- }
340
- if ($r.out -match 'OPENED:vm_read') {
341
- throw "E6: child got PROCESS_VM_READ on broker " +
342
- "(self-protect ineffective). raw: $($r.raw)"
343
- }
344
- if ($r.out -notmatch 'DENIED:vm_read') {
345
- throw "E6: expected ACCESS_DENIED (5) for PROCESS_VM_READ. " +
346
- "raw: $($r.raw)"
347
- }
348
- Write-Host 'E6 ok: child denied PROCESS_VM_READ on broker'
349
-
350
- # ── E7: cmd.exe /c passthrough — user-quoted payload survives ───
351
- # build_cmdline wraps the post-/c content in ONE outer "…" pair
352
- # for /s to strip; inner content is verbatim. The `&` is inside
353
- # the user's own "…" so cmd treats it literally.
354
- $r = Exec @('--', $cmd, '/d', '/s', '/c', 'echo "x & y"')
355
- if ($r.exit -ne 0) { throw "E7 exited $($r.exit): $($r.out)" }
356
- $got = $r.out.Trim()
357
- if ($got -ne '"x & y"') {
358
- throw "E7: expected literal '`"x & y`"', got '$got'"
359
- }
360
- Write-Host 'E7 ok: user-quoted payload passes through verbatim'
361
-
362
- # ── E7b: cmd metachar passthrough — `&` works as separator ──────
363
- # By design: the post-/c string is the user's cmd command, NOT
364
- # escaped. `&` chains commands inside the *sandboxed* cmd.exe.
365
- # This is not the Phase-6 N1 host-shell injection — that
366
- # concerned the OUTER spawn (solved by argv-mode in batch 03);
367
- # the child here IS the sandbox.
368
- $r = Exec @('--', $cmd, '/d', '/s', '/c', 'echo MARKER & exit 5')
369
- if ($r.exit -ne 5) {
370
- throw "E7b: expected exit 5 from chained command, got $($r.exit). " +
371
- "out: $($r.out)"
372
- }
373
- if ($r.out.Trim() -notlike 'MARKER*') {
374
- throw "E7b: expected MARKER in output. out: $($r.out)"
375
- }
376
- Write-Host 'E7b ok: & chains commands inside sandboxed cmd (passthrough)'
377
-
378
- # ── E8: --name resolution path through exec ─────────────────────
379
- # Every row above used --group-sid. Run one row via --name to cover
380
- # `resolve_group_sid`'s LookupAccountNameW branch in the exec path.
381
- # `BUILTIN\Administrators` resolves on every Windows install.
382
- $r = & $Exe exec --name 'BUILTIN\Administrators' -- $cmd /c 'exit 7' 2>&1
383
- if ($LASTEXITCODE -ne 7) {
384
- throw "E8: --name exec expected exit 7, got $LASTEXITCODE. out: $r"
385
- }
386
- Write-Host 'E8 ok: --name resolution path through exec works'
387
-
388
- # ── E9: refuse to nest — exec from inside exec fails fast ───────
389
- # Inside the sandbox child, the discriminator SID is deny-only;
390
- # the inner `srt-win exec` pre-flight (no --skip-group-check) must
391
- # refuse with the deny-only message.
392
- $inner = "`"$Exe`" exec --group-sid $GroupSid -- $cmd /c exit 0"
393
- $r = Exec @('--', $cmd, '/c', $inner)
394
- if ($r.exit -eq 0) {
395
- throw "E9: nested exec succeeded; expected refusal. raw: $($r.raw)"
396
- }
397
- # The deny-only refusal comes from the INNER srt-win's stderr,
398
- # which is `srt-win:`-prefixed and therefore in .raw (filtered
399
- # out of .out — the filter can't distinguish inner-vs-outer
400
- # srt-win lines).
401
- if ($r.raw -notmatch '(?i)deny-only') {
402
- throw "E9: nested exec failed but not with the deny-only " +
403
- "message. raw: $($r.raw)"
404
- }
405
- Write-Host 'E9 ok: nested exec refused (deny-only guard)'
406
-
407
- # ── E10: --skip-group-check is silent when group is ready ───────
408
- # The flag must not break the run and must NOT warn when the
409
- # group is in fact enabled (warning fires only on Absent).
410
- $r = & $Exe exec --group-sid $GroupSid --skip-group-check `
411
- -- $cmd /c 'exit 0' 2>&1 | Out-String
412
- if ($LASTEXITCODE -ne 0) {
413
- throw "E10: --skip-group-check run exited ${LASTEXITCODE}: $r"
414
- }
415
- if ($r -match '(?i)WARNING:.*skip-group-check') {
416
- throw "E10: warning fired despite group being ready. out: $r"
417
- }
418
- Write-Host 'E10 ok: --skip-group-check silent when group is ready'
419
-
420
- # ── E10b: --skip-group-check warns when group is ABSENT ─────────
421
- # A well-formed but unmapped SID (alias RID 9999 doesn't exist)
422
- # is Absent in the broker token. With the flag, exec must warn
423
- # and proceed (exit = child's). Without the flag it would refuse
424
- # — that path is covered by E9's deny-only refusal; the Absent
425
- # refusal differs only in the message.
426
- $r = & $Exe exec --group-sid S-1-5-32-9999 --skip-group-check `
427
- -- $cmd /c 'exit 0' 2>&1 | Out-String
428
- if ($LASTEXITCODE -ne 0) {
429
- throw "E10b: --skip-group-check + absent group exited ${LASTEXITCODE}: $r"
430
- }
431
- if ($r -notmatch '(?i)WARNING:.*skip-group-check') {
432
- throw "E10b: expected absent-group warning. out: $r"
433
- }
434
- Write-Host 'E10b ok: --skip-group-check warns when group is absent'
435
-
436
- # TODO E11: verify mitigation policies actually applied (child-side
437
- # GetProcessMitigationPolicy probe). Deferred — would need a
438
- # helper binary or P/Invoke from inside the sandboxed PowerShell.
439
-
440
- # ── teardown ─────────────────────────────────────────────────────
441
- Run @('wfp','uninstall','--sublayer-guid',$Sublayer)
442
- $post = J @('wfp','status','--sublayer-guid',$Sublayer)
443
- if ($post.state -ne 'absent') {
444
- throw "post-uninstall expected absent, got $($post.state)"
445
- }
446
- Write-Host 'smoke-exec: PASS (E1-E10b incl. E4b/E7b)'