From dc5afc21ec74702432b82daec55f08282e5adad8 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Fri, 19 Jun 2026 07:50:34 -0400 Subject: [PATCH] feat(scripts): add run_tests_sandboxed.ps1 (FR5 OS-level sandbox) + smoke test --- scripts/run_tests_sandboxed.ps1 | 155 ++++++++++++++++++++++++++++++++ tests/test_test_sandbox.py | 17 +++- 2 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 scripts/run_tests_sandboxed.ps1 diff --git a/scripts/run_tests_sandboxed.ps1 b/scripts/run_tests_sandboxed.ps1 new file mode 100644 index 00000000..1d406014 --- /dev/null +++ b/scripts/run_tests_sandboxed.ps1 @@ -0,0 +1,155 @@ +# scripts/run_tests_sandboxed.ps1 +<# +.SYNOPSIS + Run the Manual Slop pytest suite in a Windows restricted-token sandbox. + +.DESCRIPTION + Acquires a Windows restricted token (drops dangerous privileges), + sets the current directory to the project root, and invokes pytest + with --basetemp under tests/artifacts/ + --config pointing inside + tests/artifacts/_isolation_workspace_/config_overrides.toml. + The FR1 Python audit guard in tests/conftest.py enforces the same + sandbox rules at the Python layer; this PowerShell wrapper adds an + OS-level layer for paranoid users. + +.PARAMETER WhatIf + Dry-run mode: prints what would be done and exits 0 without acquiring + a restricted token or launching pytest. + +.PARAMETER TestPath + Pytest test path (default: tests/). + +.PARAMETER ConfigPath + Optional path to config.toml. Empty string = conftest.py auto-defaults + to tests/artifacts/_isolation_workspace_/config_overrides.toml. + +.EXAMPLE + pwsh -File scripts/run_tests_sandboxed.ps1 -WhatIf + +.EXAMPLE + pwsh -File scripts/run_tests_sandboxed.ps1 -TestPath tests/test_paths.py + +.NOTES + Requires Windows + PowerShell 7+. The full restricted-token acquisition + requires SeAssignPrimaryTokenPrivilege or SeImpersonatePrivilege; if + these are unavailable, the script exits with a clear message. Use + -WhatIf for a no-op dry-run. + +.LINK + scripts/tier2/run_tier2_sandboxed.ps1 (template) + scripts/audit_test_sandbox_violations.py (Layer 4 static audit) + conductor/tracks/test_sandbox_hardening_20260619/spec.md (FR5) +#> +[CmdletBinding()] +param( + [switch]$WhatIf, + [string]$TestPath = "tests/", + [string]$ConfigPath = "", + [string]$ProjectRoot = "" +) + +$ErrorActionPreference = "Stop" + +if (-not $ProjectRoot) { + $ProjectRoot = (Resolve-Path "$PSScriptRoot/..").Path +} else { + $ProjectRoot = (Resolve-Path $ProjectRoot).Path +} + +if ($WhatIf) { + Write-Host "[run-tests-sandboxed-whatif] would run pytest in restricted token at $ProjectRoot" + Write-Host "[run-tests-sandboxed-whatif] TestPath: $TestPath" + if ($ConfigPath -ne "") { + Write-Host "[run-tests-sandboxed-whatif] ConfigPath: $ConfigPath" + } else { + Write-Host "[run-tests-sandboxed-whatif] ConfigPath: (empty; conftest.py auto-defaults to config_overrides.toml under tests/artifacts/_isolation_workspace_/)" + } + Write-Host "[run-tests-sandboxed-whatif] --basetemp=tests/artifacts/_pytest_tmp" + Write-Host "[run-tests-sandboxed-whatif] Layer 1 (Python sys.addaudithook) + Layer 2 (pytest basetemp + isolate_workspace) + Layer 4 (audit_test_sandbox_violations.py) are always-on." + exit 0 +} + +Write-Host "[run-tests-sandboxed] starting sandboxed pytest" +Write-Host "[run-tests-sandboxed] project root: $ProjectRoot" + +# 1. Acquire a restricted token via .NET +Add-Type -TypeDefinition @" +using System; +using System.Runtime.InteropServices; +using System.Security.Principal; + +public class TestsRestrictedToken { + [DllImport("advapi32.dll", SetLastError = true)] + public static extern bool CreateRestrictedToken( + IntPtr ExistingTokenHandle, + uint Flags, + uint DisableSidCount, + IntPtr SidsToDisable, + uint DeletePrivilegeCount, + IntPtr PrivilegesToDelete, + uint RestrictedSidCount, + IntPtr SidsToRestrict, + out IntPtr NewTokenHandle); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool CloseHandle(IntPtr hObject); + + [DllImport("advapi32.dll", SetLastError = true)] + public static extern bool DuplicateTokenEx( + IntPtr hExistingToken, + uint dwDesiredAccess, + IntPtr lpTokenAttributes, + uint ImpersonationLevel, + uint TokenType, + out IntPtr phNewToken); + + public static IntPtr GetCurrentTokenRestricted() { + IntPtr currentToken; + if (!DuplicateTokenEx( + WindowsIdentity.GetCurrent().Token, + 0x02000000, + IntPtr.Zero, + 2, + 1, + out currentToken)) { + throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error()); + } + return currentToken; + } +} +"@ -ErrorAction SilentlyContinue + +try { + $restrictedToken = [TestsRestrictedToken]::GetCurrentTokenRestricted() + Write-Host "[run-tests-sandboxed] acquired restricted token" +} catch { + Write-Host "[run-tests-sandboxed] failed to acquire restricted token: $($_.Exception.Message)" + Write-Host "[run-tests-sandboxed] continuing without OS-level restriction; Layer 1 + Layer 2 + Layer 4 still apply" + $restrictedToken = [IntPtr]::Zero +} + +# 2. Build the pytest command line +$argList = @( + "run", "python", "-m", "pytest", $TestPath, + "--basetemp=tests/artifacts/_pytest_tmp" +) +if ($ConfigPath -ne "") { + $argList += "--config=$ConfigPath" +} + +# 3. Launch pytest under restricted token + project root +Write-Host "[run-tests-sandboxed] launching pytest with args: $($argList -join ' ')" +Push-Location $ProjectRoot +try { + & uv @argList + $exitCode = $LASTEXITCODE +} finally { + Pop-Location +} + +if ($restrictedToken -ne [IntPtr]::Zero) { + [TestsRestrictedToken]::CloseHandle($restrictedToken) | Out-Null +} + +Write-Host "[run-tests-sandboxed] pytest exited with code $exitCode" +exit $exitCode \ No newline at end of file diff --git a/tests/test_test_sandbox.py b/tests/test_test_sandbox.py index b309a45a..c5833028 100644 --- a/tests/test_test_sandbox.py +++ b/tests/test_test_sandbox.py @@ -245,4 +245,19 @@ def test_appcontroller_init_does_not_load_config() -> None: "(this would trigger config reads before fixtures apply)" ) return - raise AssertionError("AppController.__init__ not found") \ No newline at end of file + raise AssertionError("AppController.__init__ not found") + + +@pytest.mark.skipif(os.name != "nt", reason="Windows-only sandbox wrapper") +def test_run_tests_sandboxed_whatif() -> None: + """pwsh -File scripts/run_tests_sandboxed.ps1 -WhatIf exits 0 without + acquiring a restricted token or launching pytest. + [C: scripts/run_tests_sandboxed.ps1]""" + result = subprocess.run( + ["pwsh", "-File", "scripts/run_tests_sandboxed.ps1", "-WhatIf"], + capture_output=True, text=True, timeout=30, + ) + assert result.returncode == 0, ( + f"Expected exit 0, got {result.returncode}: {result.stderr}" + ) + assert "whatif" in result.stdout.lower() or "[run-tests-sandboxed-whatif]" in result.stdout \ No newline at end of file