diff --git a/crates/socket-patch-cli/tests/common/hermetic.rs b/crates/socket-patch-cli/tests/common/hermetic.rs index c54cf49b6..8f5e78057 100644 --- a/crates/socket-patch-cli/tests/common/hermetic.rs +++ b/crates/socket-patch-cli/tests/common/hermetic.rs @@ -89,6 +89,12 @@ pub fn command(bin: &Path) -> Command { cmd.env_remove(k); } cmd.env_remove("SOCKET_API_TOKEN"); + // The `SOCKET_CLI_*` peer names too: the binary promotes each onto its + // canonical `SOCKET_*` name, so a peer token set mid-run would put the + // child on the authenticated path. + for (_, peer) in socket_patch_core::utils::env_compat::PEER_ENV_ALIASES { + cmd.env_remove(peer); + } scrub_socket_vars(&mut cmd); // Belt-and-braces on top of the `.cargo/config.toml` `[env]` defaults // (see `FORCED_OPT_OUTS`): a developer's real `socket login` would flip diff --git a/crates/socket-patch-cli/tests/e2e_golang.rs b/crates/socket-patch-cli/tests/e2e_golang.rs index e2d49c309..017815f55 100644 --- a/crates/socket-patch-cli/tests/e2e_golang.rs +++ b/crates/socket-patch-cli/tests/e2e_golang.rs @@ -50,14 +50,27 @@ async fn mount_batch(server: &MockServer) { .await; } +/// The binary with the environment pinned hard: `hermetic::command` scrubs +/// every ambient `SOCKET_*` (so `SOCKET_OFFLINE`, `SOCKET_ECOSYSTEMS`, +/// `SOCKET_GLOBAL*` and the output-mode trio can't change what the crawler +/// discovers or whether it calls home), `VIRTUAL_ENV` is removed (the scans +/// are unscoped, so an activated venv would add its packages to the batch), +/// `GOPATH` is removed, and the token/url/org steer the API at the in-test +/// server. Callers pin the module cache. +fn golang_command(cwd: &Path, api_url: &str) -> std::process::Command { + let mut cmd = common::hermetic::command(&binary()); + common::hermetic::scrub_extra(&mut cmd, &[common::hermetic::Extra::Venv]); + cmd.current_dir(cwd) + .env("SOCKET_API_URL", api_url) + .env("SOCKET_API_TOKEN", "sktsec_dummy_e2e_golang_token_api") + .env("SOCKET_ORG_SLUG", ORG) + .env_remove("GOPATH"); + cmd +} + /// Run the binary as a blocking subprocess (off the async runtime so the -/// wiremock server can service the request concurrently). -/// -/// The environment is pinned hard: `GOMODCACHE` fixes the crawl root, the -/// token/url/org steer the API at the in-test server, and every variable that -/// could redirect the API elsewhere or disable it (`GOPATH`, `SOCKET_OFFLINE`, -/// the proxy URLs) is scrubbed so an ambient value in the test environment -/// can't quietly change what the crawler discovers or whether it calls home. +/// wiremock server can service the request concurrently), with +/// [`golang_command`]'s env and `GOMODCACHE` fixing the crawl root. async fn run(args: &[&str], cwd: &Path, gomodcache: &Path, api_url: &str) -> Output { let args: Vec = args.iter().map(|s| s.to_string()).collect(); let cwd = cwd.to_path_buf(); @@ -65,38 +78,9 @@ async fn run(args: &[&str], cwd: &Path, gomodcache: &Path, api_url: &str) -> Out let api_url = api_url.to_string(); tokio::task::spawn_blocking(move || { let arg_refs: Vec<&str> = args.iter().map(String::as_str).collect(); - std::process::Command::new(binary()) + golang_command(&cwd, &api_url) .args(&arg_refs) - .current_dir(&cwd) .env("GOMODCACHE", &gomodcache) - .env("SOCKET_API_URL", &api_url) - .env("SOCKET_API_TOKEN", "sktsec_dummy_e2e_golang_token_api") - .env("SOCKET_ORG_SLUG", ORG) - // Ambient-pollution regression guard: seed a hostile value for - // each scan-affecting variable, then scrub it below. `env_remove` - // clears explicitly-set values too, so the child never sees the - // seeds — but if a scrub line is ever dropped, the seed (rather - // than a developer's ambient shell, which this suite can't rely - // on) turns the tests red immediately. Each seed was verified to - // break the suite when inherited: `SOCKET_ECOSYSTEMS` filters the - // go crawler out entirely, `SOCKET_GLOBAL` / `SOCKET_GLOBAL_PREFIX` - // redirect the crawl away from the pinned `GOMODCACHE`, and - // `SOCKET_JSON` / `SOCKET_SILENT` replace or suppress the human - // output the tests assert on. - .env("SOCKET_ECOSYSTEMS", "npm") - .env("SOCKET_GLOBAL", "true") - .env("SOCKET_GLOBAL_PREFIX", "/nonexistent") - .env("SOCKET_JSON", "true") - .env("SOCKET_SILENT", "true") - .env_remove("SOCKET_ECOSYSTEMS") - .env_remove("SOCKET_GLOBAL") - .env_remove("SOCKET_GLOBAL_PREFIX") - .env_remove("SOCKET_JSON") - .env_remove("SOCKET_SILENT") - .env_remove("GOPATH") - .env_remove("SOCKET_OFFLINE") - .env_remove("SOCKET_PROXY_URL") - .env_remove("SOCKET_BATCH_SIZE") .output() .expect("Failed to run socket-patch binary") }) @@ -317,25 +301,12 @@ async fn scan_finds_the_cache_named_by_the_go_env_file() { let (project2, api) = (project.clone(), api_url.clone()); let output = tokio::task::spawn_blocking(move || { - common::hermetic::command(&binary()) + golang_command(&project2, &api) .args(["scan", "--json", "--cwd", project2.to_str().unwrap()]) - .current_dir(&project2) .env("GOENV", &goenv) .env("HOME", &home) .env("USERPROFILE", &home) - .env("SOCKET_API_URL", &api) - .env("SOCKET_API_TOKEN", "sktsec_dummy_e2e_golang_token_api") - .env("SOCKET_ORG_SLUG", ORG) .env_remove("GOMODCACHE") - .env_remove("GOPATH") - .env_remove("SOCKET_ECOSYSTEMS") - .env_remove("SOCKET_GLOBAL") - .env_remove("SOCKET_GLOBAL_PREFIX") - .env_remove("SOCKET_JSON") - .env_remove("SOCKET_SILENT") - .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_golang_build.rs b/crates/socket-patch-cli/tests/e2e_golang_build.rs index 2369e65e3..92a861bd8 100644 --- a/crates/socket-patch-cli/tests/e2e_golang_build.rs +++ b/crates/socket-patch-cli/tests/e2e_golang_build.rs @@ -60,26 +60,10 @@ fn go_env<'a>(modcache: &'a str, proxy_url: &'a str) -> Vec<(&'a str, &'a str)> ] } -/// Run socket-patch with ambient `SOCKET_*` scrubbed + the fixture GOMODCACHE -/// (the go crawler resolves installed modules through it). Every global flag -/// is env-backed (`SOCKET_DRY_RUN`, `SOCKET_GLOBAL`, `SOCKET_MANIFEST_PATH`, -/// …), so an unscrubbed ambient value would silently reconfigure `apply` / -/// `--check` out from under the assertions. +/// Run socket-patch through the shared hermetic runner with the fixture +/// GOMODCACHE (the go crawler resolves installed modules through it). fn run_socket(cwd: &Path, args: &[&str], modcache: &Path) -> (i32, String, String) { - let mut cmd = Command::new(binary()); - cmd.args(args).current_dir(cwd); - for (k, _) in std::env::vars_os() { - if k.to_string_lossy().starts_with("SOCKET_") && k.to_string_lossy() != "SOCKET_NO_CONFIG" { - cmd.env_remove(&k); - } - } - cmd.env("GOMODCACHE", modcache); - let out = cmd.output().expect("failed to run socket-patch binary"); - ( - out.status.code().unwrap_or(-1), - String::from_utf8_lossy(&out.stdout).into_owned(), - String::from_utf8_lossy(&out.stderr).into_owned(), - ) + common::run_with_env(cwd, args, &[("GOMODCACHE", modcache.to_str().unwrap())]) } /// Run `go` with its caches sandboxed, then the fixture's own env on top. @@ -206,14 +190,12 @@ fn go_build_links_patch_via_replace_redirect() { if !golang_e2e_matrix::toolchain_ready("e2e_golang_build") { return; } - // RED guards for the hermeticity pins: bake the hostile ambient values in - // so this suite fails deterministically if either leak returns. - // `GOTOOLCHAIN` must lose to go_env's `local` pin (or every `go` command - // chases a nonexistent toolchain through the file proxy); `SOCKET_DRY_RUN` - // must be scrubbed by `run_socket` (or every apply is a no-op that still - // exits 0 and the patched-symbol assert sees PRISTINE). + // RED guard for the toolchain pin: bake the hostile ambient value in so + // this suite fails deterministically if the leak returns. `GOTOOLCHAIN` + // must lose to go_env's `local` pin (or every `go` command chases a + // nonexistent toolchain through the file proxy). `SOCKET_DRY_RUN` needs + // no guard here: `hermetic::command` seeds and scrubs it itself. std::env::set_var("GOTOOLCHAIN", "go1.99.99"); - std::env::set_var("SOCKET_DRY_RUN", "true"); let tmp = tempfile::tempdir().unwrap(); let (consumer, modcache, proxy_url) = stage(tmp.path()); let cs = consumer.to_str().unwrap(); diff --git a/crates/socket-patch-cli/tests/e2e_golang_workspace_build.rs b/crates/socket-patch-cli/tests/e2e_golang_workspace_build.rs index fed6d3392..73122218a 100644 --- a/crates/socket-patch-cli/tests/e2e_golang_workspace_build.rs +++ b/crates/socket-patch-cli/tests/e2e_golang_workspace_build.rs @@ -38,7 +38,7 @@ mod golang_e2e_matrix; mod vex_e2e_common; use golang_e2e_matrix::{manifestless_vex, ManifestlessGo}; -use vex_e2e_common::{binary, git_sha256, patch_view, Marker, VexVia}; +use vex_e2e_common::{binary, git_sha256, hermetic, patch_view, Marker, VexVia}; const ORG: &str = "test-org"; const UMOD: &str = "example.com/upstream"; @@ -292,20 +292,12 @@ fn move_replace_to_go_work(root: &Path, sums_of: Option<&str>) -> String { line } -/// Run socket-patch (SOCKET_* scrubbed, hermetic config) in `cwd`. +/// Run socket-patch (hermetic `SOCKET_*` environment) in `cwd`. fn socket(cwd: &Path, args: &[&str], modcache: &Path) -> (i32, serde_json::Value, String) { - let mut cmd = Command::new(binary()); + let mut cmd = hermetic::command(&binary()); + hermetic::scrub_extra(&mut cmd, &[hermetic::Extra::Venv]); cmd.current_dir(cwd); - for (k, _) in std::env::vars_os() { - if k.to_string_lossy().starts_with("SOCKET_") { - cmd.env_remove(&k); - } - } - cmd.env("SOCKET_NO_CONFIG", "1") - .env("SOCKET_TELEMETRY_DISABLED", "1") - .env("GOMODCACHE", modcache) - .env("GOFLAGS", "") - .env_remove("VIRTUAL_ENV"); + cmd.env("GOMODCACHE", modcache).env("GOFLAGS", ""); let _fixture = prebuilt_common::prepare_command( &mut cmd, cwd, diff --git a/crates/socket-patch-cli/tests/spawn_env_hygiene.rs b/crates/socket-patch-cli/tests/spawn_env_hygiene.rs index 713a490b5..55f4bbb1b 100644 --- a/crates/socket-patch-cli/tests/spawn_env_hygiene.rs +++ b/crates/socket-patch-cli/tests/spawn_env_hygiene.rs @@ -34,9 +34,6 @@ const PENDING_RAW_SPAWNS: &[&str] = &[ "e2e_composer.rs", "e2e_composer_version_identity.rs", "e2e_gem.rs", - "e2e_golang.rs", - "e2e_golang_build.rs", - "e2e_golang_workspace_build.rs", "e2e_hosted_production.rs", "e2e_maven.rs", "e2e_npm.rs", @@ -51,7 +48,6 @@ const PENDING_RAW_SPAWNS: &[&str] = &[ "e2e_vendored_production.rs", "e2e_vex_vendor.rs", "rollback/rollback_multicopy_blob_gate.rs", - "vendor/e2e_golang_redirect.rs", "vendor/in_process_vendor_bun.rs", "vendor/vendor_gem_lockfile_only_e2e.rs", "vendor/vendor_rerun_no_network_e2e.rs", @@ -305,6 +301,11 @@ fn command_drops_flag_bound_vars_set_after_it_is_built() { ("SOCKET_DEBUG", "1"), ("SOCKET_PROXY_URL", "http://ambient.invalid"), ("SOCKET_SKIP_ROLLBACK", "true"), + // Peer aliases the binary promotes onto `SOCKET_API_TOKEN` & co. + ("SOCKET_CLI_API_TOKEN", "late-token"), + ("SOCKET_CLI_ORG_SLUG", "late-org"), + ("SOCKET_CLI_API_BASE_URL", "http://ambient.invalid"), + ("SOCKET_CLI_NO_API_TOKEN", "1"), ], || { let env = effective_env(&cmd); @@ -313,6 +314,10 @@ fn command_drops_flag_bound_vars_set_after_it_is_built() { "SOCKET_DEBUG", "SOCKET_PROXY_URL", "SOCKET_SKIP_ROLLBACK", + "SOCKET_CLI_API_TOKEN", + "SOCKET_CLI_ORG_SLUG", + "SOCKET_CLI_API_BASE_URL", + "SOCKET_CLI_NO_API_TOKEN", ] { assert_eq!(get(&env, key), None, "{key} set after build leaked"); } diff --git a/crates/socket-patch-cli/tests/vendor/e2e_golang_redirect.rs b/crates/socket-patch-cli/tests/vendor/e2e_golang_redirect.rs index 12c8ada3e..f257cc636 100644 --- a/crates/socket-patch-cli/tests/vendor/e2e_golang_redirect.rs +++ b/crates/socket-patch-cli/tests/vendor/e2e_golang_redirect.rs @@ -64,42 +64,17 @@ fn stage(root: &Path) -> (std::path::PathBuf, std::path::PathBuf) { (gomodcache, cache_dir) } -/// Run the CLI binary with the environment pinned hard (mirrors the -/// seed-then-scrub runner in `e2e_golang.rs`). Each variable below was -/// verified to break this suite when inherited from the ambient shell, so it -/// is seeded with a hostile value and then scrubbed — `env_remove` clears the -/// seed too, so the child never sees it, but if a scrub line is ever dropped -/// the seed (rather than a developer's ambient shell, which this suite can't -/// rely on) turns the tests red immediately. `SOCKET_GLOBAL` / -/// `SOCKET_GLOBAL_PREFIX` take the redirect backend out of local scope -/// (apply patches the fake module cache IN PLACE and `--check` exits 0 -/// having checked nothing), `SOCKET_DRY_RUN` makes every apply a no-op, and -/// `SOCKET_MANIFEST_PATH` points apply/check at a manifest that isn't there. -/// `--offline`, `--ecosystems`, and `--cwd` are passed as flags, which -/// outrank env, so their env twins can't bite; `GOMODCACHE` pins the crawl -/// root (so `GOPATH` is never consulted). +/// Run the CLI binary through the shared hermetic runner. Its hostile seeds +/// cover the vars verified to break this suite when inherited: +/// `SOCKET_GLOBAL` / `SOCKET_GLOBAL_PREFIX` take the redirect backend out of +/// local scope (apply patches the fake module cache IN PLACE and `--check` +/// exits 0 having checked nothing), `SOCKET_DRY_RUN` makes every apply a +/// no-op, and `SOCKET_MANIFEST_PATH` points apply/check at a manifest that +/// isn't there. `--offline`, `--ecosystems`, and `--cwd` are passed as flags, +/// which outrank env, so their env twins can't bite; `GOMODCACHE` pins the +/// crawl root (so `GOPATH` is never consulted). fn run_cli(root: &Path, gomodcache: &Path, args: &[&str]) -> (i32, String, String) { - let out = std::process::Command::new(binary()) - .args(args) - .current_dir(root) - .env("GOMODCACHE", gomodcache) - .env("SOCKET_GLOBAL", "true") - .env("SOCKET_GLOBAL_PREFIX", "/nonexistent") - .env("SOCKET_DRY_RUN", "true") - .env("SOCKET_MANIFEST_PATH", "/nonexistent/manifest.json") - .env_remove("SOCKET_GLOBAL") - .env_remove("SOCKET_GLOBAL_PREFIX") - .env_remove("SOCKET_DRY_RUN") - .env_remove("SOCKET_MANIFEST_PATH") - .env_remove("SOCKET_API_TOKEN") - .env_remove("SOCKET_CLI_API_TOKEN") - .output() - .expect("failed to execute socket-patch binary"); - ( - out.status.code().unwrap_or(-1), - String::from_utf8_lossy(&out.stdout).to_string(), - String::from_utf8_lossy(&out.stderr).to_string(), - ) + common::run_with_env(root, args, &[("GOMODCACHE", gomodcache.to_str().unwrap())]) } fn apply(root: &Path, gomodcache: &Path) -> (i32, String, String) {