@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,20 +0,0 @@
1
- //! `srt-win` — Windows network sandbox helper for sandbox-runtime.
2
- //!
3
- //! This crate is the Rust half of the Windows backend. The library
4
- //! exposes the SID, group, and WFP primitives so they can be unit-
5
- //! tested; the binary (`main.rs`) is a thin CLI over them.
6
- //!
7
- //! Windows-only. Building on other platforms yields an empty crate so
8
- //! `cargo check` from a non-Windows host doesn't error.
9
-
10
- #![cfg(windows)]
11
-
12
- pub mod sid;
13
- pub mod util;
14
- pub mod wfp;
15
-
16
- pub mod token;
17
- pub mod job;
18
- pub mod winsta;
19
- pub mod self_protect;
20
- pub mod launch;
@@ -1,669 +0,0 @@
1
- //! `srt-win` — CLI for the sandbox-runtime Windows network fence.
2
- //!
3
- //! Subcommands:
4
- //! install | uninstall — convenience: group + WFP in one
5
- //! elevated call (one UAC prompt)
6
- //! group create | status | delete — manage the discriminator local group
7
- //! wfp install | status | uninstall — manage the persistent WFP filters
8
- //! exec -- <target> [args...] — spawn under the deny-only-group
9
- //! token + job + hardening stack
10
- //!
11
- //! `status` subcommands write one line of JSON to stdout and exit 0.
12
- //! Mutating subcommands require elevation and write human-readable
13
- //! progress to stderr. `exec` propagates the child's exit code.
14
-
15
- use clap::{Args, Parser, Subcommand};
16
-
17
- /// Default group name. Lives here (not in the `#[cfg(windows)]`
18
- /// library crate) so the clap-derive CLI structs compile on
19
- /// non-Windows hosts where the library is empty.
20
- const DEFAULT_GROUP_NAME: &str = "sandbox-runtime-net";
21
-
22
- #[derive(Parser)]
23
- #[command(name = "srt-win", version, about)]
24
- struct Cli {
25
- #[command(subcommand)]
26
- cmd: Cmd,
27
- }
28
-
29
- #[derive(Subcommand)]
30
- enum Cmd {
31
- /// Group create + WFP install in one elevated step.
32
- ///
33
- /// Self-elevates via UAC if not already running as admin
34
- /// (one prompt; the elevated child does the work and the
35
- /// parent relays its exit code). With the machine-wide
36
- /// filter design, a token where the group is **absent**
37
- /// (i.e. this session, before logout) matches filter-0
38
- /// (PERMIT non-members) — so installing the WFP filters
39
- /// here does NOT break the user's network. Logout is still
40
- /// required before `srt-win exec` works (the broker
41
- /// pre-flight needs the group **enabled** to build a
42
- /// deny-only child token), but the install itself is one
43
- /// safe step → one UAC prompt.
44
- ///
45
- /// Equivalent to `group create --name <N> --user-sid <U>`
46
- /// followed by `wfp install --name <N> …`. With `--group-sid`,
47
- /// the group is assumed to already exist (e.g. provisioned by
48
- /// domain GPO) and only the filters are installed.
49
- ///
50
- /// Exit codes:
51
- /// 0 — installed (or already installed with the same
52
- /// port-range; no changes)
53
- /// 10 — UAC prompt cancelled by the user
54
- /// 11 — group create / lookup failed
55
- /// 12 — WFP filter install failed
56
- /// 13 — already installed under this sublayer with a
57
- /// DIFFERENT port-range; pass `--force` to replace
58
- /// 1 — other error (parse, elevation check, etc.)
59
- Install {
60
- #[command(flatten)]
61
- group: GroupRef,
62
- /// User SID to add to the group (default: current user).
63
- /// Ignored with `--group-sid`.
64
- #[arg(long)]
65
- user_sid: Option<String>,
66
- /// Sublayer GUID (default: compile-time constant).
67
- #[arg(long)]
68
- sublayer_guid: Option<String>,
69
- /// Loopback port range (`LOW-HIGH`, default 60080-60089).
70
- #[arg(long, value_name = "LOW-HIGH")]
71
- proxy_port_range: Option<String>,
72
- /// Replace an existing install whose port-range differs
73
- /// (otherwise exits 13).
74
- #[arg(long)]
75
- force: bool,
76
- },
77
- /// Remove the srt-win WFP filters under the sublayer.
78
- ///
79
- /// Self-elevates via UAC if not already admin. Does NOT
80
- /// delete the discriminator group — use `srt-win group
81
- /// delete --name <N>` for that explicitly.
82
- Uninstall {
83
- #[arg(long)]
84
- sublayer_guid: Option<String>,
85
- },
86
- /// Manage the local discriminator group.
87
- Group {
88
- #[command(subcommand)]
89
- sub: GroupCmd,
90
- },
91
- /// Manage the persistent WFP filters.
92
- Wfp {
93
- #[command(subcommand)]
94
- sub: WfpCmd,
95
- },
96
- /// Spawn a process under the deny-only-group sandbox.
97
- ///
98
- /// Builds a restricted token (group + Admins flipped deny-only,
99
- /// LUA, Medium IL, all privs stripped except SeChangeNotify),
100
- /// self-protects the broker, assigns the child to a
101
- /// kill-on-close job with full UI lockdown, places it on a
102
- /// non-interactive desktop, applies process-mitigation
103
- /// policies + an explicit handle whitelist, and waits for it
104
- /// to exit. Propagates the child's exit code.
105
- ///
106
- /// The child inherits this process's environment verbatim — proxy
107
- /// configuration is single-sourced by the caller, which sets the
108
- /// proxy vars (TS `generateProxyEnvVars`) in the environment it
109
- /// spawns `srt-win exec` with. There are intentionally no
110
- /// `--http-proxy` / `--socks-proxy` flags and no proxy fallback.
111
- Exec {
112
- #[command(flatten)]
113
- group: GroupRef,
114
- /// Skip the "is the group enabled in the broker's token"
115
- /// pre-flight. **Fail-open** — the WFP fence depends on
116
- /// that membership; with this set the child may run with
117
- /// weaker isolation if the install was incomplete.
118
- /// Surfaced as a flag (not an env var) so the bypass is
119
- /// intentional and not accidentally inherited. Use ONLY
120
- /// in ephemeral CI runners that create the group in-job
121
- /// and cannot logout/login mid-run.
122
- #[arg(long)]
123
- skip_group_check: bool,
124
- /// Target executable followed by its arguments. Use `--`
125
- /// to terminate srt-win's own option parsing.
126
- #[arg(
127
- trailing_var_arg = true,
128
- allow_hyphen_values = true,
129
- required = true,
130
- num_args = 1..,
131
- )]
132
- target: Vec<String>,
133
- },
134
- }
135
-
136
- /// Group resolution: either by name (looked up via
137
- /// `LookupAccountNameW`) or directly by SID. If both are given the
138
- /// SID wins; `group create`/`delete` always need a name.
139
- #[derive(Args, Clone)]
140
- struct GroupRef {
141
- /// Group name (local or `DOMAIN\name`). Default
142
- /// `sandbox-runtime-net`.
143
- #[arg(long, default_value = DEFAULT_GROUP_NAME)]
144
- name: String,
145
- /// Group SID (`S-1-…`). Overrides `--name` for SID resolution.
146
- /// Use when the group is provisioned by external tooling and name
147
- /// lookup may be unreliable.
148
- #[arg(long)]
149
- group_sid: Option<String>,
150
- }
151
-
152
- #[derive(Subcommand)]
153
- enum GroupCmd {
154
- /// Create the local group and add the current (or `--user-sid`)
155
- /// user to it. Idempotent. Self-elevates via UAC if not already
156
- /// admin.
157
- Create {
158
- #[command(flatten)]
159
- group: GroupRef,
160
- /// User SID to add (default: current user).
161
- #[arg(long)]
162
- user_sid: Option<String>,
163
- },
164
- /// Print group state as JSON: `{state, sid?, warning?}`.
165
- Status {
166
- #[command(flatten)]
167
- group: GroupRef,
168
- },
169
- /// Delete the local group. Idempotent. Self-elevates via UAC if
170
- /// not already admin.
171
- Delete {
172
- #[command(flatten)]
173
- group: GroupRef,
174
- },
175
- }
176
-
177
- #[derive(Subcommand)]
178
- enum WfpCmd {
179
- /// Install (or refresh) the machine-wide persistent WFP filters
180
- /// keyed on the group SID. Idempotent. Self-elevates via UAC if
181
- /// not already admin.
182
- Install {
183
- #[command(flatten)]
184
- group: GroupRef,
185
- /// Sublayer GUID. Default is the compile-time constant; pass
186
- /// when integrating with externally-managed WFP state.
187
- #[arg(long)]
188
- sublayer_guid: Option<String>,
189
- /// Loopback port range the sandboxed child may reach
190
- /// (`LOW-HIGH`, inclusive; default 60080-60089). The host
191
- /// http/socks proxies bind inside this range on Windows.
192
- #[arg(long, value_name = "LOW-HIGH")]
193
- proxy_port_range: Option<String>,
194
- },
195
- /// Print WFP fence state as JSON: `{state, filters,
196
- /// port_range?}`. Filters are identified by their
197
- /// `providerData` tag, so only `--sublayer-guid` is relevant.
198
- Status {
199
- #[arg(long)]
200
- sublayer_guid: Option<String>,
201
- },
202
- /// Remove every srt-win-tagged WFP filter under the sublayer.
203
- /// Self-elevates via UAC if not already admin.
204
- Uninstall {
205
- #[arg(long)]
206
- sublayer_guid: Option<String>,
207
- },
208
- }
209
-
210
- #[cfg(windows)]
211
- fn main() {
212
- if let Err(e) = run() {
213
- eprintln!("srt-win: error: {e:#}");
214
- std::process::exit(1);
215
- }
216
- }
217
-
218
- #[cfg(windows)]
219
- fn run() -> anyhow::Result<()> {
220
- use anyhow::{anyhow, Context};
221
- use serde_json::json;
222
- use srt_win::{sid, wfp};
223
-
224
- let cli = Cli::parse();
225
-
226
- // Validate a caller-supplied SID string up front so a typo
227
- // surfaces as "invalid --<flag>" rather than an SDDL parse
228
- // error three calls deep. Returns the CANONICAL `S-1-…` form
229
- // (round-tripped through ConvertSidToStringSidW) so SDDL
230
- // shorthands like `BA` or lower-case `s-1-…` collapse to a
231
- // single comparable representation; downstream
232
- // `eq_ignore_ascii_case("S-1-5-32-544")` dedup checks rely on
233
- // that.
234
- let canonicalize_sid =
235
- |flag: &str, s: &str| -> anyhow::Result<String> {
236
- let p = sid::LocalPsid::from_string(s)
237
- .with_context(|| format!("invalid --{flag} '{s}'"))?;
238
- sid::psid_to_string(p.as_psid())
239
- .with_context(|| format!("canonicalize --{flag} '{s}'"))
240
- };
241
- let resolve_group_sid = |g: &GroupRef| -> anyhow::Result<String> {
242
- if let Some(s) = &g.group_sid {
243
- return canonicalize_sid("group-sid", s);
244
- }
245
- sid::lookup_account_sid(&g.name)
246
- .with_context(|| format!("resolve group '{}'", g.name))
247
- };
248
- let resolve_sublayer = |s: &Option<String>| -> anyhow::Result<windows::core::GUID> {
249
- match s {
250
- Some(g) => wfp::parse_guid(g),
251
- None => Ok(wfp::DEFAULT_SUBLAYER_GUID),
252
- }
253
- };
254
-
255
- match cli.cmd {
256
- // ─── install / uninstall (convenience) ─────────────────────
257
- Cmd::Install {
258
- group,
259
- user_sid,
260
- sublayer_guid,
261
- proxy_port_range,
262
- force,
263
- } => {
264
- if let Some(code) = maybe_self_elevate()? {
265
- std::process::exit(code);
266
- }
267
- let sl = resolve_sublayer(&sublayer_guid)?;
268
- let range = match &proxy_port_range {
269
- Some(s) => wfp::parse_port_range(s)
270
- .with_context(|| format!("invalid --proxy-port-range '{s}'"))?,
271
- None => wfp::DEFAULT_PROXY_PORT_RANGE,
272
- };
273
- // Idempotency / conflict pre-check. If filters are
274
- // already installed under this sublayer with the SAME
275
- // port-range, this is a no-op (exit 0). With a
276
- // DIFFERENT range and no --force, refuse (exit 13) so
277
- // an unintended config drift surfaces instead of
278
- // silently overwriting. A pre-existing install whose
279
- // tags lack a port_range (legacy) is treated as
280
- // "different" and requires --force.
281
- if !force
282
- && let Ok(st) = wfp::filter_status(&sl)
283
- && st.state == "installed"
284
- {
285
- let want = [range.0, range.1];
286
- if st.port_range == Some(want) {
287
- eprintln!(
288
- "srt-win: already installed (sublayer={sl:?}, \
289
- port_range={}-{}, filters={}); no changes",
290
- range.0, range.1, st.filters,
291
- );
292
- return Ok(());
293
- }
294
- let have = st
295
- .port_range
296
- .map(|[l, h]| format!("{l}-{h}"))
297
- .unwrap_or_else(|| "<unknown>".into());
298
- eprintln!(
299
- "srt-win: error: already installed under sublayer \
300
- {sl:?} with port_range={have}; pass --force to \
301
- replace, or run `srt-win uninstall` first."
302
- );
303
- std::process::exit(13);
304
- }
305
- // With --group-sid the group is externally managed;
306
- // just canonicalize. With --name (or the default),
307
- // create the local group, add the user, then resolve
308
- // the SID. Failures here exit 11.
309
- let group_step = || -> anyhow::Result<(String, String)> {
310
- if let Some(s) = &group.group_sid {
311
- let g = canonicalize_sid("group-sid", s)?;
312
- Ok((g.clone(), g))
313
- } else {
314
- let user = match &user_sid {
315
- Some(s) => canonicalize_sid("user-sid", s)?,
316
- None => sid::current_user_sid()
317
- .context("resolve current user")?,
318
- };
319
- wfp::ensure_group(&group.name, &user)?;
320
- let g = sid::lookup_account_sid(&group.name)?;
321
- Ok((group.name.clone(), g))
322
- }
323
- };
324
- let (label, gsid) = match group_step() {
325
- Ok(v) => v,
326
- Err(e) => {
327
- eprintln!("srt-win: error: group step: {e:#}");
328
- std::process::exit(11);
329
- }
330
- };
331
- if let Err(e) = wfp::install_filters(&sl, &gsid, range) {
332
- eprintln!("srt-win: error: WFP install: {e:#}");
333
- std::process::exit(12);
334
- }
335
- eprintln!(
336
- "srt-win: installed (group={label} sid={gsid}, sublayer={sl:?}, \
337
- proxy_port_range={}-{}, filters=8)",
338
- range.0, range.1,
339
- );
340
- eprintln!(
341
- "srt-win: NOTE — log out and back in before running \
342
- `srt-win exec` (the group SID enters TokenGroups at \
343
- logon; your network is unaffected meanwhile)."
344
- );
345
- }
346
- Cmd::Uninstall { sublayer_guid } => {
347
- if let Some(code) = maybe_self_elevate()? {
348
- std::process::exit(code);
349
- }
350
- let sl = resolve_sublayer(&sublayer_guid)?;
351
- let n = wfp::uninstall_filters(&sl)?;
352
- eprintln!(
353
- "srt-win: uninstalled ({n} filter(s) removed). \
354
- Group is left intact — run `srt-win group delete` \
355
- to remove it."
356
- );
357
- }
358
-
359
- // ─── group ─────────────────────────────────────────────────
360
- Cmd::Group { sub: GroupCmd::Create { group, user_sid } } => {
361
- if let Some(code) = maybe_self_elevate()? {
362
- std::process::exit(code);
363
- }
364
- if group.group_sid.is_some() {
365
- return Err(anyhow!(
366
- "`group create` needs --name; --group-sid is for \
367
- referencing an existing group"
368
- ));
369
- }
370
- let user = match &user_sid {
371
- Some(s) => canonicalize_sid("user-sid", s)?,
372
- None => sid::current_user_sid()
373
- .context("resolve current user")?,
374
- };
375
- wfp::ensure_group(&group.name, &user)?;
376
- let gsid = sid::lookup_account_sid(&group.name)?;
377
- eprintln!(
378
- "srt-win: group '{}' present (sid={gsid}); user {user} added",
379
- group.name
380
- );
381
- eprintln!(
382
- "srt-win: NOTE — the group SID enters TokenGroups at logon. \
383
- Log out and back in before running `srt-win exec`."
384
- );
385
- }
386
- Cmd::Group { sub: GroupCmd::Status { group } } => {
387
- // Resolve SID first; if that fails the group is absent.
388
- let gsid = match &group.group_sid {
389
- Some(s) => {
390
- // --group-sid bypasses the name lookup, so do a
391
- // reverse lookup to distinguish "exists but not on
392
- // this token yet" from "no such account at all".
393
- // Tolerate transient lookup failure (domain
394
- // unreachable) by falling through to the token
395
- // check.
396
- match sid::sid_account_exists(s) {
397
- Ok(sid::SidExistence::Unmapped) => {
398
- println!("{}", json!({"state": "absent"}));
399
- return Ok(());
400
- }
401
- Ok(_) => {}
402
- Err(e) => {
403
- // Malformed SID string.
404
- println!(
405
- "{}",
406
- json!({"state": "absent", "error": e.to_string()})
407
- );
408
- return Ok(());
409
- }
410
- }
411
- s.clone()
412
- }
413
- None => match sid::lookup_account_sid(&group.name) {
414
- Ok(s) => s,
415
- Err(_) => {
416
- println!("{}", json!({"state": "absent"}));
417
- return Ok(());
418
- }
419
- },
420
- };
421
- let out = match sid::group_state_for_self(&gsid)? {
422
- sid::GroupState::Enabled => {
423
- json!({"state": "ready", "sid": gsid})
424
- }
425
- sid::GroupState::Absent => {
426
- json!({"state": "created-not-on-token", "sid": gsid})
427
- }
428
- sid::GroupState::DenyOnly => json!({
429
- "state": "created-not-on-token",
430
- "sid": gsid,
431
- "warning": "group is deny-only in this token — running \
432
- inside a sandbox child?"
433
- }),
434
- sid::GroupState::Present => json!({
435
- "state": "created-not-on-token",
436
- "sid": gsid,
437
- "warning": "group present but neither enabled nor \
438
- deny-only (unexpected)"
439
- }),
440
- };
441
- println!("{out}");
442
- }
443
- Cmd::Group { sub: GroupCmd::Delete { group } } => {
444
- if let Some(code) = maybe_self_elevate()? {
445
- std::process::exit(code);
446
- }
447
- if group.group_sid.is_some() {
448
- return Err(anyhow!(
449
- "`group delete` needs --name; cannot delete by SID"
450
- ));
451
- }
452
- wfp::delete_group(&group.name)?;
453
- eprintln!("srt-win: group '{}' deleted (if it existed)", group.name);
454
- }
455
-
456
- // ─── wfp ───────────────────────────────────────────────────
457
- Cmd::Wfp {
458
- sub:
459
- WfpCmd::Install {
460
- group,
461
- sublayer_guid,
462
- proxy_port_range,
463
- },
464
- } => {
465
- if let Some(code) = maybe_self_elevate()? {
466
- std::process::exit(code);
467
- }
468
- let gsid = resolve_group_sid(&group)?;
469
- let sl = resolve_sublayer(&sublayer_guid)?;
470
- let range = match &proxy_port_range {
471
- Some(s) => wfp::parse_port_range(s)
472
- .with_context(|| format!("invalid --proxy-port-range '{s}'"))?,
473
- None => wfp::DEFAULT_PROXY_PORT_RANGE,
474
- };
475
- wfp::install_filters(&sl, &gsid, range)?;
476
- eprintln!(
477
- "srt-win: WFP filters installed (group_sid={gsid}, \
478
- sublayer={sl:?}, proxy_port_range={}-{})",
479
- range.0, range.1,
480
- );
481
- }
482
- Cmd::Wfp { sub: WfpCmd::Status { sublayer_guid } } => {
483
- let sl = resolve_sublayer(&sublayer_guid)?;
484
- let st = wfp::filter_status(&sl)?;
485
- println!("{}", serde_json::to_string(&st)?);
486
- }
487
- Cmd::Wfp { sub: WfpCmd::Uninstall { sublayer_guid } } => {
488
- if let Some(code) = maybe_self_elevate()? {
489
- std::process::exit(code);
490
- }
491
- let sl = resolve_sublayer(&sublayer_guid)?;
492
- let n = wfp::uninstall_filters(&sl)?;
493
- eprintln!("srt-win: removed {n} WFP filter(s)");
494
- }
495
-
496
- // ─── exec ──────────────────────────────────────────────────
497
- Cmd::Exec {
498
- group,
499
- skip_group_check,
500
- target,
501
- } => {
502
- use srt_win::launch;
503
- let gsid = resolve_group_sid(&group)?;
504
- // `target` is `required, num_args=1..` so non-empty.
505
- let exe = std::path::PathBuf::from(&target[0]);
506
- let args = &target[1..];
507
- let spec = launch::ExecSpec {
508
- group_sid: &gsid,
509
- skip_group_check,
510
- target_exe: &exe,
511
- target_args: args,
512
- };
513
- let code = launch::run(&spec)?;
514
- // Propagate the child's exit code verbatim.
515
- std::process::exit(code as i32);
516
- }
517
- }
518
- Ok(())
519
- }
520
-
521
- #[cfg(windows)]
522
- fn is_elevated() -> anyhow::Result<bool> {
523
- use anyhow::Context;
524
- use std::ffi::c_void;
525
- use std::mem::size_of;
526
- use windows::Win32::Foundation::{CloseHandle, HANDLE};
527
- use windows::Win32::Security::{
528
- GetTokenInformation, TokenElevation, TOKEN_ELEVATION, TOKEN_QUERY,
529
- };
530
- use windows::Win32::System::Threading::{
531
- GetCurrentProcess, OpenProcessToken,
532
- };
533
- unsafe {
534
- let mut tok = HANDLE::default();
535
- OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut tok)
536
- .context("OpenProcessToken")?;
537
- let mut elev = TOKEN_ELEVATION::default();
538
- let mut ret = 0u32;
539
- let r = GetTokenInformation(
540
- tok,
541
- TokenElevation,
542
- Some(&mut elev as *mut _ as *mut c_void),
543
- size_of::<TOKEN_ELEVATION>() as u32,
544
- &mut ret,
545
- );
546
- let _ = CloseHandle(tok);
547
- r.context("GetTokenInformation(TokenElevation)")?;
548
- Ok(elev.TokenIsElevated != 0)
549
- }
550
- }
551
-
552
- /// Hard elevation gate: returns an error (no UAC relaunch) when not
553
- /// admin. The granular admin mutators now self-elevate via
554
- /// [`maybe_self_elevate`], so this currently has no caller — it's
555
- /// retained as the non-interactive counterpart for `acl recover`
556
- /// (a later batch), hence `allow(dead_code)`.
557
- #[cfg(windows)]
558
- #[allow(dead_code)]
559
- fn require_elevated() -> anyhow::Result<()> {
560
- if is_elevated()? {
561
- Ok(())
562
- } else {
563
- Err(anyhow::anyhow!(
564
- "this command requires elevation — run from an \
565
- administrator prompt"
566
- ))
567
- }
568
- }
569
-
570
- /// If not already elevated, re-launch ourselves with the same
571
- /// argv via `ShellExecuteExW(verb="runas")` — one UAC prompt —
572
- /// wait for the elevated child, and return its exit code. If
573
- /// already elevated, returns `Ok(None)` and the caller proceeds
574
- /// in-process. If the user cancels the UAC dialog
575
- /// (`ERROR_CANCELLED`), exits with code **10** so the caller's
576
- /// exit-code contract holds without the caller needing a
577
- /// separate match.
578
- ///
579
- /// The elevated child runs in its own (hidden) console, so its
580
- /// stdout/stderr are NOT relayed to the parent. For
581
- /// `install`/`uninstall` that's acceptable: the exit code is the
582
- /// contract; the convenience commands' stderr is informational
583
- /// only. The granular `group create|delete` and `wfp
584
- /// install|uninstall` admin mutators call this too; their stderr is
585
- /// likewise informational. Read-only subcommands (`group status`,
586
- /// `wfp status`, `exec`) run as the broker and never self-elevate.
587
- #[cfg(windows)]
588
- fn maybe_self_elevate() -> anyhow::Result<Option<i32>> {
589
- use anyhow::Context;
590
- use srt_win::launch::quote_arg;
591
- use srt_win::util::wstr;
592
- use windows::core::PCWSTR;
593
- use windows::Win32::Foundation::{
594
- CloseHandle, ERROR_CANCELLED, GetLastError,
595
- };
596
- use windows::Win32::System::Threading::{
597
- GetExitCodeProcess, WaitForSingleObject, INFINITE,
598
- };
599
- use windows::Win32::UI::Shell::{
600
- ShellExecuteExW, SEE_MASK_NOCLOSEPROCESS, SEE_MASK_NO_CONSOLE,
601
- SHELLEXECUTEINFOW,
602
- };
603
- use windows::Win32::UI::WindowsAndMessaging::SW_HIDE;
604
-
605
- if is_elevated()? {
606
- return Ok(None);
607
- }
608
-
609
- let exe = std::env::current_exe().context("current_exe")?;
610
- let exe_w = wstr(&exe.to_string_lossy());
611
- // Rebuild the original argv (minus argv[0]) using
612
- // CommandLineToArgvW-compatible quoting so the elevated
613
- // child parses identically.
614
- let params: String = std::env::args()
615
- .skip(1)
616
- .map(|a| quote_arg(&a))
617
- .collect::<Vec<_>>()
618
- .join(" ");
619
- let params_w = wstr(&params);
620
- let verb_w = wstr("runas");
621
-
622
- let mut sei = SHELLEXECUTEINFOW {
623
- cbSize: std::mem::size_of::<SHELLEXECUTEINFOW>() as u32,
624
- fMask: SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NO_CONSOLE,
625
- lpVerb: PCWSTR(verb_w.as_ptr()),
626
- lpFile: PCWSTR(exe_w.as_ptr()),
627
- lpParameters: PCWSTR(params_w.as_ptr()),
628
- nShow: SW_HIDE.0,
629
- ..Default::default()
630
- };
631
- // SAFETY: sei is fully initialized; the wide-string buffers
632
- // outlive the call.
633
- let ok = unsafe { ShellExecuteExW(&mut sei) };
634
- if ok.is_err() {
635
- let err = unsafe { GetLastError() };
636
- if err == ERROR_CANCELLED {
637
- eprintln!("srt-win: UAC prompt cancelled by user");
638
- std::process::exit(10);
639
- }
640
- return Err(anyhow::anyhow!(
641
- "ShellExecuteExW(runas): {} ({}",
642
- std::io::Error::from_raw_os_error(err.0 as i32),
643
- err.0,
644
- ));
645
- }
646
- let h = sei.hProcess;
647
- if h.is_invalid() {
648
- return Err(anyhow::anyhow!(
649
- "ShellExecuteExW returned no process handle"
650
- ));
651
- }
652
- unsafe { WaitForSingleObject(h, INFINITE) };
653
- let mut code: u32 = 1;
654
- unsafe {
655
- GetExitCodeProcess(h, &mut code)
656
- .context("GetExitCodeProcess(elevated child)")?;
657
- let _ = CloseHandle(h);
658
- }
659
- Ok(Some(code as i32))
660
- }
661
-
662
- #[cfg(not(windows))]
663
- fn main() {
664
- // The clap-derived structs above keep `clap` referenced; just
665
- // print the platform error.
666
- let _ = <Cli as clap::CommandFactory>::command();
667
- eprintln!("srt-win: Windows only");
668
- std::process::exit(2);
669
- }