diff --git a/crates/socket-patch-cli/tests/common/hermetic.rs b/crates/socket-patch-cli/tests/common/hermetic.rs index c54cf49b6..fa5aec691 100644 --- a/crates/socket-patch-cli/tests/common/hermetic.rs +++ b/crates/socket-patch-cli/tests/common/hermetic.rs @@ -34,11 +34,12 @@ use std::process::Command; /// never sees it, but if a scrub line is ever dropped the seed (rather than a /// developer's ambient shell, which the suites can't rely on) turns the tests /// red immediately. -const HOSTILE_SEEDS: &[(&str, &str)] = &[ +pub const HOSTILE_SEEDS: &[(&str, &str)] = &[ ("SOCKET_CWD", "/nonexistent"), ("SOCKET_GLOBAL", "true"), ("SOCKET_GLOBAL_PREFIX", "/nonexistent"), ("SOCKET_DRY_RUN", "true"), + ("SOCKET_SAVE_ONLY", "true"), ("SOCKET_ECOSYSTEMS", "cargo"), ("SOCKET_MANIFEST_PATH", "/nonexistent/manifest.json"), ("SOCKET_JSON", "true"), diff --git a/crates/socket-patch-cli/tests/e2e_nuget.rs b/crates/socket-patch-cli/tests/e2e_nuget.rs index 2dfce5ad6..9e7584efc 100644 --- a/crates/socket-patch-cli/tests/e2e_nuget.rs +++ b/crates/socket-patch-cli/tests/e2e_nuget.rs @@ -15,10 +15,10 @@ #[path = "common/mod.rs"] mod common; -use common::binary; +use common::{binary, hermetic}; use std::path::Path; -use std::process::{Command, Output}; +use std::process::Output; use wiremock::matchers::{method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; @@ -46,11 +46,12 @@ async fn start_proxy() -> MockServer { /// Run the binary as a blocking subprocess (off the async runtime so the /// in-test proxy can service its requests concurrently), pinned to `proxy_url`. /// -/// `SOCKET_API_TOKEN` is stripped so the binary deterministically takes the -/// public-proxy path (an ambient token would flip it onto the authenticated -/// API, bypassing `--proxy-url`), and every other variable that could -/// redirect the API elsewhere or disable it is scrubbed so an ambient value -/// can't quietly change what the scan reports. +/// The child gets the hermetic `SOCKET_*` environment (no token, so the +/// binary deterministically takes the public-proxy path `--proxy-url` +/// pins). The scans are unscoped, so an activated venv is scrubbed and +/// `HOME` points at a stand-in (`jvm_env::isolate_cli`): the developer's +/// venv, `~/.m2` or user site-packages would add packages to the counts +/// these tests assert. async fn run(args: &[&str], cwd: &Path, nuget_packages: &Path, proxy_url: &str) -> Output { let mut args: Vec = args.iter().map(|s| s.to_string()).collect(); args.extend(["--proxy-url".to_string(), proxy_url.to_string()]); @@ -58,16 +59,12 @@ async fn run(args: &[&str], cwd: &Path, nuget_packages: &Path, proxy_url: &str) let nuget_packages = nuget_packages.to_path_buf(); tokio::task::spawn_blocking(move || { let arg_refs: Vec<&str> = args.iter().map(String::as_str).collect(); - Command::new(binary()) - .args(&arg_refs) + let mut cmd = hermetic::command(&binary()); + hermetic::scrub_extra(&mut cmd, &[hermetic::Extra::Venv]); + common::jvm_env::isolate_cli(&mut cmd); + cmd.args(&arg_refs) .current_dir(&cwd) .env("NUGET_PACKAGES", &nuget_packages) - .env_remove("SOCKET_API_TOKEN") - .env_remove("SOCKET_CLI_API_TOKEN") - .env_remove("SOCKET_API_URL") - .env_remove("SOCKET_OFFLINE") - .env_remove("SOCKET_PROXY_URL") - .env_remove("SOCKET_BATCH_SIZE") .output() .expect("Failed to run socket-patch binary") }) diff --git a/crates/socket-patch-cli/tests/e2e_nuget_dotnet_build.rs b/crates/socket-patch-cli/tests/e2e_nuget_dotnet_build.rs index 766194472..f9c582852 100644 --- a/crates/socket-patch-cli/tests/e2e_nuget_dotnet_build.rs +++ b/crates/socket-patch-cli/tests/e2e_nuget_dotnet_build.rs @@ -376,19 +376,12 @@ fn b64(bytes: &[u8]) -> String { /// Run socket-patch (`args`) with a scrubbed environment and the crawler's /// global packages folder pinned to `store` → (exit, envelope, stderr). fn socket_patch(cwd: &Path, store: &Path, args: &[&str]) -> (Option, Value, String) { - let mut cmd = Command::new(binary()); - for (key, _) in std::env::vars_os() { - if key.to_string_lossy().starts_with("SOCKET_") { - cmd.env_remove(key); - } - } + let mut cmd = hermetic::command(&binary()); + hermetic::scrub_extra(&mut cmd, &[hermetic::Extra::Venv]); let out = cmd .args(args) .current_dir(cwd) - .env("SOCKET_TELEMETRY_DISABLED", "1") - .env("SOCKET_NO_CONFIG", "1") .env("NUGET_PACKAGES", store) - .env_remove("VIRTUAL_ENV") .output() .expect("spawn socket-patch"); let stderr = String::from_utf8_lossy(&out.stderr).into_owned(); diff --git a/crates/socket-patch-cli/tests/e2e_pypi.rs b/crates/socket-patch-cli/tests/e2e_pypi.rs index e60325246..c4646b4c2 100644 --- a/crates/socket-patch-cli/tests/e2e_pypi.rs +++ b/crates/socket-patch-cli/tests/e2e_pypi.rs @@ -15,13 +15,11 @@ #[path = "common/mod.rs"] mod common; -use common::{binary, git_sha256}; +use common::{binary, git_sha256, hermetic}; use std::path::{Path, PathBuf}; use std::process::{Command, Output}; -use socket_patch_cli::args::{GLOBAL_ARG_ENV_VARS, LOCAL_ARG_ENV_VARS}; - use common::cache_env; // --------------------------------------------------------------------------- @@ -52,30 +50,21 @@ fn git_sha256_file(path: &Path) -> String { /// Run the CLI binary with the given args, setting `cwd` as the working dir. /// -/// The environment is pinned hard: every env var the CLI binds (the canonical -/// `GLOBAL_ARG_ENV_VARS` / `LOCAL_ARG_ENV_VARS` lists) is scrubbed so ambient -/// developer/CI configuration can't change the lifecycle under test. Scrubbing `SOCKET_API_TOKEN` also forces the +/// The child gets the hermetic `SOCKET_*` environment, which forces the /// public-proxy (free-tier) path this suite relies on. The stakes are higher /// here than in the offline suites: an inherited `SOCKET_GLOBAL=true` takes /// `get`/`apply` out of the temp venv and patches the host's real /// site-packages, while `SOCKET_MANIFEST_PATH` relocates `.socket/` (verified /// to fail `get` outright) and `SOCKET_DRY_RUN` / `SOCKET_SAVE_ONLY` turn the -/// apply steps into silent no-ops. The hostile seeds below are cleared by the -/// scrub loop — the child never sees them — but if the scrub is ever dropped -/// the seeds (rather than a developer's ambient shell, which this suite can't -/// rely on) turn the tests red immediately. +/// apply steps into silent no-ops. An activated venv is scrubbed too, so the +/// crawler patches the suite's own `.venv` rather than the developer's, and +/// `HOME` points at a stand-in (`jvm_env::isolate_cli`) so the user site +/// under the real `~` stays out of the scans. fn run(cwd: &Path, args: &[&str]) -> (i32, String, String) { - let mut cmd = Command::new(binary()); - cmd.args(args) - .current_dir(cwd) - .env("SOCKET_GLOBAL", "true") - .env("SOCKET_GLOBAL_PREFIX", "/nonexistent") - .env("SOCKET_DRY_RUN", "true") - .env("SOCKET_SAVE_ONLY", "true") - .env("SOCKET_MANIFEST_PATH", "/nonexistent/manifest.json"); - for var in GLOBAL_ARG_ENV_VARS.iter().chain(LOCAL_ARG_ENV_VARS) { - cmd.env_remove(var); - } + let mut cmd = hermetic::command(&binary()); + hermetic::scrub_extra(&mut cmd, &[hermetic::Extra::Venv]); + common::jvm_env::isolate_cli(&mut cmd); + cmd.args(args).current_dir(cwd); let out: Output = cmd.output().expect("failed to execute socket-patch binary"); let code = out.status.code().unwrap_or(-1); diff --git a/crates/socket-patch-cli/tests/e2e_pypi_multi_copy.rs b/crates/socket-patch-cli/tests/e2e_pypi_multi_copy.rs index eb46b7c96..7054a39fb 100644 --- a/crates/socket-patch-cli/tests/e2e_pypi_multi_copy.rs +++ b/crates/socket-patch-cli/tests/e2e_pypi_multi_copy.rs @@ -16,7 +16,6 @@ mod common; use std::path::{Path, PathBuf}; -use std::process::Command; use serde_json::{json, Value}; @@ -48,19 +47,11 @@ const DISCOVERY_VARS: &[&str] = &[ /// Run the binary in `cwd` with the `SOCKET_*` and discovery env scrubbed /// and exactly `env` added. fn run(cwd: &Path, args: &[&str], env: &[(&str, &str)]) -> (i32, String, String) { - let mut cmd = Command::new(common::binary()); + let mut cmd = common::hermetic::command(&common::binary()); cmd.args(args).current_dir(cwd); - for (key, _) in std::env::vars_os() { - let name = key.to_string_lossy(); - if name.starts_with("SOCKET_") && !name.contains("TELEMETRY") { - cmd.env_remove(&key); - } - } for name in DISCOVERY_VARS { cmd.env_remove(name); } - cmd.env("SOCKET_NO_CONFIG", "1") - .env("SOCKET_NO_UPDATE_CHECK", "1"); for (k, v) in env { cmd.env(k, v); } diff --git a/crates/socket-patch-cli/tests/spawn_env_hygiene.rs b/crates/socket-patch-cli/tests/spawn_env_hygiene.rs index 713a490b5..ee22be71e 100644 --- a/crates/socket-patch-cli/tests/spawn_env_hygiene.rs +++ b/crates/socket-patch-cli/tests/spawn_env_hygiene.rs @@ -40,10 +40,6 @@ const PENDING_RAW_SPAWNS: &[&str] = &[ "e2e_hosted_production.rs", "e2e_maven.rs", "e2e_npm.rs", - "e2e_nuget.rs", - "e2e_nuget_dotnet_build.rs", - "e2e_pypi.rs", - "e2e_pypi_multi_copy.rs", "e2e_redirect_bun_build.rs", "e2e_redirect_cargo_shapes.rs", "e2e_scan.rs", @@ -275,19 +271,7 @@ fn workflow_env_copies_carry_every_opt_out() { #[serial] fn command_never_leaks_its_hostile_seeds() { let env = effective_env(&hermetic::command(Path::new("socket-patch"))); - for key in [ - "SOCKET_CWD", - "SOCKET_GLOBAL", - "SOCKET_GLOBAL_PREFIX", - "SOCKET_DRY_RUN", - "SOCKET_ECOSYSTEMS", - "SOCKET_MANIFEST_PATH", - "SOCKET_JSON", - "SOCKET_SILENT", - "SOCKET_VERBOSE", - "SOCKET_UPDATE_BASE_URL", - "SOCKET_UPDATE_STATE_DIR", - ] { + for (key, _) in hermetic::HOSTILE_SEEDS { assert_eq!(get(&env, key), None, "{key} seed must be scrubbed"); } }