@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,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)'
|