feat(scripts): add run_tests_sandboxed.ps1 (FR5 OS-level sandbox) + smoke test
This commit is contained in:
@@ -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_<RUN_ID>/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_<RUN_ID>/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_<RUN_ID>/)"
|
||||
}
|
||||
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
|
||||
@@ -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")
|
||||
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
|
||||
Reference in New Issue
Block a user