deer-flow/backend/tests/_windows_acl_helpers.py
xiaodu55 2536f24f81
fix(sandbox): platform-aware Lark CLI runtime validation for Windows hosts (#5442)
* fix(sandbox): platform-aware Lark CLI runtime validation for Windows hosts

The managed Lark CLI sandbox runtime validation and its tests assumed POSIX
semantics that Windows hosts cannot satisfy, breaking the focused AIO/Lark CLI
suites (5 failures on current main).

- _validate_lark_cli_sandbox_runtime keeps the strict executable-bit contract
  on POSIX; on Windows it validates the Linux-only artifacts by content
  instead (ELF/PE/Mach-O image magic for linux-*/lark-cli, shebang for the
  bin/lark-cli launcher), since NTFS cannot represent the exec bit.
- The AIO runtime-mounts test now asserts the explicit Windows credential
  contract: an owner-only inheritable DACL (via the existing PowerShell ACL
  resolvers) instead of exact 0o700 modes, which remain asserted on POSIX.
- Accept-path runtime tests stage ELF-prefixed payloads so both platforms
  exercise realistic artifact content; the extractor's mode assertion is
  POSIX-only with a writability check on Windows.

Focused suites: 5 failed / 162 passed -> 167 passed, 3 skipped on Windows 11;
POSIX behavior unchanged (POSIX branches keep the original assertions).

* refactor(tests): share Windows ACL resolvers via a helper module and cover the Windows shebang gate

Review follow-ups on #5442:

- Move the PowerShell ACL resolvers (_windows_acl_env/_windows_acl_sids/
  _windows_acl_protected/_windows_acl_owner_sid) from
  tests/test_lark_cli_integration.py into tests/_windows_acl_helpers.py,
  following the existing shared-helper convention, so the aio suite no longer
  imports the full lark-cli integration module (which drags in app.gateway
  routers and the FastAPI TestClient at collection time).
- Add test_managed_sandbox_runtime_rejects_launcher_without_shebang_on_windows:
  a launcher without a shebang plus ELF-magic binaries, with lark_cli.os
  monkeypatched via the existing Windows stub so the shebang-missing reject
  branch of _runtime_artifact_is_executable is covered on every platform.
- Comment the rejects-non-executable prestaged-binary test to record that on
  Windows the rejection comes from the payload's non-magic content, since
  chmod() cannot clear the exec bit there.

Focused suites: 168 passed / 3 skipped on Windows 11.
2026-09-15 07:26:08 +08:00

71 lines
2.8 KiB
Python

"""Shared Windows ACL inspection helpers for lark-cli credential tests.
These resolvers shell out to Windows PowerShell ``Get-Acl`` and translate ACEs
to raw SIDs so tests can assert the owner-only DACL contract that the
credential-tree hardener establishes on NTFS (where POSIX modes are not
representable). They are Windows-only; callers gate them on ``os.name == "nt"``.
"""
from __future__ import annotations
import os
import subprocess
from pathlib import Path
def _windows_acl_env() -> dict[str, str]:
"""Return a PowerShell environment with a clean, ordered ``PSModulePath``.
The Codex runtime prepends a bundled PowerShell module path that shadows the
stock ``Microsoft.PowerShell.Security`` module, which makes ``Get-Acl`` fail
to autoload under ``-NoProfile``. Use the stock Windows PowerShell module path
so ACL inspection is reliable on any host.
"""
system_root = os.environ.get("SystemRoot", r"C:\Windows")
program_files = os.environ.get("ProgramFiles", r"C:\Program Files")
modules = f"{system_root}\\system32\\WindowsPowerShell\\v1.0\\Modules;{program_files}\\WindowsPowerShell\\Modules"
return {**os.environ, "PSModulePath": modules}
def _windows_acl_sids(path: Path) -> set[str]:
"""Return the SIDs granted on *path* (Windows-only, PowerShell resolver).
``icacls`` displays localized account names rather than raw SIDs, so we
translate each ACE IdentityReference back to a SID before asserting.
"""
cmd = "(Get-Acl -LiteralPath '" + str(path) + "').Access | ForEach-Object { $_.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value }"
out = subprocess.run(
["powershell", "-NoProfile", "-Command", cmd],
capture_output=True,
text=True,
check=True,
env=_windows_acl_env(),
)
return {line.strip() for line in out.stdout.splitlines() if line.strip()}
def _windows_acl_protected(path: Path) -> bool:
"""Return whether *path*'s DACL is protected from inheritance (Windows-only)."""
cmd = "(Get-Acl -LiteralPath '" + str(path) + "').AreAccessRulesProtected"
out = subprocess.run(
["powershell", "-NoProfile", "-Command", cmd],
capture_output=True,
text=True,
check=True,
env=_windows_acl_env(),
)
return out.stdout.strip() == "True"
def _windows_acl_owner_sid(path: Path) -> str:
"""Return *path*'s object owner as a raw SID (Windows-only)."""
cmd = "$acl = Get-Acl -LiteralPath $env:DEER_FLOW_TEST_ACL_PATH; $acl.GetOwner([System.Security.Principal.SecurityIdentifier]).Value"
out = subprocess.run(
["powershell", "-NoProfile", "-Command", cmd],
capture_output=True,
text=True,
check=True,
env={**_windows_acl_env(), "DEER_FLOW_TEST_ACL_PATH": str(path)},
)
return out.stdout.strip()