/** * Secret detection and env filtering utilities * * subprocess env filtering: default-deny allowlist model. * only vars in the safe set or user allowlist are passed to child processes. * * log redaction: SENSITIVE_PATTERNS are used to identify secret values * for redaction in logs and GHA masking (independent of subprocess filtering). */ // --- log redaction (unchanged, independent of subprocess filtering) --- // patterns for sensitive env var names (used by normalizeEnv) export const SENSITIVE_PATTERNS = [ /_KEY$/i, /_SECRET$/i, /_TOKEN$/i, /_PASSWORD$/i, /_CREDENTIAL$/i, ]; export function isSensitiveEnvName(key: string): boolean { return SENSITIVE_PATTERNS.some((p) => p.test(key)); } // --- subprocess env filtering --- // vars that are never passed to subprocesses, even if prefix-matched const BLOCKED_ENV_NAMES = new Set(["GITHUB_TOKEN", "GH_TOKEN"]); // prefixes whose vars are safe to pass through (runner metadata, workflow context) const SAFE_ENV_PREFIXES = ["GITHUB_", "RUNNER_", "JAVA_HOME_", "GOROOT_", "PULLFROG_"]; // exact var names safe to pass through (system + runner image toolchain) const SAFE_ENV_NAMES = new Set([ // system "CI", "HOME", "LANG", "LOGNAME", "PATH", "SHELL", "SHLVL", "TERM", "TMPDIR", "TZ", "USER", "XDG_CONFIG_HOME", "XDG_RUNTIME_DIR", "DEBIAN_FRONTEND", // runner image toolchain "ACCEPT_EULA", "AGENT_TOOLSDIRECTORY", "ANDROID_HOME", "ANDROID_NDK", "ANDROID_NDK_HOME", "ANDROID_NDK_LATEST_HOME", "ANDROID_NDK_ROOT", "ANDROID_SDK_ROOT", "ANT_HOME", "AZURE_EXTENSION_DIR", "BOOTSTRAP_HASKELL_NONINTERACTIVE", "CHROME_BIN", "CHROMEWEBDRIVER", "CONDA", "DOTNET_MULTILEVEL_LOOKUP", "DOTNET_NOLOGO", "DOTNET_SKIP_FIRST_TIME_EXPERIENCE", "EDGEWEBDRIVER", "GECKOWEBDRIVER", "GHCUP_INSTALL_BASE_PREFIX", "GRADLE_HOME", "JAVA_HOME", "HOMEBREW_CLEANUP_PERIODIC_FULL_DAYS", "HOMEBREW_NO_AUTO_UPDATE", "ImageOS", "ImageVersion", "NVM_DIR", "PIPX_BIN_DIR", "PIPX_HOME", "PSModulePath", "SELENIUM_JAR_PATH", "SGX_AESM_ADDR", "SWIFT_PATH", "VCPKG_INSTALLATION_ROOT", ]); let _userAllowlist: Set | null = null; export function setEnvAllowlist(raw: string): string[] { const names = raw .split("\n") .map((line) => line.trim()) .filter(Boolean); const blocked = names.filter((n) => BLOCKED_ENV_NAMES.has(n)); _userAllowlist = new Set(names.filter((n) => !BLOCKED_ENV_NAMES.has(n))); return blocked; } function isSafeEnvVar(key: string): boolean { if (BLOCKED_ENV_NAMES.has(key)) return false; if (SAFE_ENV_NAMES.has(key)) return true; return SAFE_ENV_PREFIXES.some((p) => key.startsWith(p)); } /** filter env vars using default-deny allowlist: safe set + user allowlist */ export function filterEnv(): Record { const filtered: Record = {}; for (const [key, value] of Object.entries(process.env)) { if (value === undefined) continue; if (BLOCKED_ENV_NAMES.has(key)) continue; if (isSafeEnvVar(key) || _userAllowlist?.has(key)) { filtered[key] = value; } } return filtered; } export type EnvMode = "restricted" | "inherit" | Record; /** * resolve env mode to actual env object * - "restricted" (default): filterEnv() — only safe set + user allowlist * - "inherit": full process.env * - object: custom env merged with restricted base */ export function resolveEnv(mode: EnvMode | undefined): Record { if (mode === "inherit") { return process.env; } if (mode === "restricted" || mode === undefined) { return filterEnv(); } // custom env object - merge with restricted base return { ...filterEnv(), ...mode }; }