feat(nono): add isolated evidence harness
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
"""Hermetic filesystem and environment fixtures for the Nono evidence tests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import os
|
||||
import tempfile
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Iterator, Mapping
|
||||
|
||||
|
||||
_CREDENTIAL_NAME_HINTS = (
|
||||
"API_KEY",
|
||||
"TOKEN",
|
||||
"SECRET",
|
||||
"PASSWORD",
|
||||
"PRIVATE_KEY",
|
||||
"CREDENTIAL",
|
||||
"SSH_AUTH_SOCK",
|
||||
"AWS_ACCESS_KEY_ID",
|
||||
)
|
||||
|
||||
|
||||
def _mode(path: Path, permissions: int) -> None:
|
||||
path.mkdir(parents=True, exist_ok=True, mode=permissions)
|
||||
os.chmod(path, permissions)
|
||||
|
||||
|
||||
def _write_executable(path: Path, content: str) -> None:
|
||||
path.write_text(content, encoding="utf-8")
|
||||
os.chmod(path, 0o700)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FixtureSession:
|
||||
"""Paths belonging to one disposable, independent session."""
|
||||
|
||||
name: str
|
||||
root: Path
|
||||
home: Path
|
||||
state: Path
|
||||
cache: Path
|
||||
runtime: Path
|
||||
tmp: Path
|
||||
repo: Path
|
||||
|
||||
def environment(self, base: Mapping[str, str]) -> dict[str, str]:
|
||||
"""Return a minimal environment whose state paths point at this session."""
|
||||
return _minimal_environment(
|
||||
base,
|
||||
home=self.home,
|
||||
config=self.home / ".config",
|
||||
state=self.state,
|
||||
cache=self.cache,
|
||||
runtime=self.runtime,
|
||||
tmp=self.tmp,
|
||||
)
|
||||
|
||||
|
||||
class IsolatedFixture:
|
||||
"""Context-managed fixture with no dependency on or mutation of host state."""
|
||||
|
||||
def __init__(self, *, prefix: str = "nono-evidence-") -> None:
|
||||
self._temporary: tempfile.TemporaryDirectory[str] | None = None
|
||||
self.prefix = prefix
|
||||
self.root: Path | None = None
|
||||
self.home: Path | None = None
|
||||
self.xdg_config_home: Path | None = None
|
||||
self.xdg_state_home: Path | None = None
|
||||
self.xdg_cache_home: Path | None = None
|
||||
self.xdg_runtime_dir: Path | None = None
|
||||
self.tmpdir: Path | None = None
|
||||
self.repos: Path | None = None
|
||||
self.sessions: Path | None = None
|
||||
self.stub_bin: Path | None = None
|
||||
self.env: dict[str, str] = {}
|
||||
self._session_map: dict[str, FixtureSession] = {}
|
||||
|
||||
def __enter__(self) -> "IsolatedFixture":
|
||||
self._temporary = tempfile.TemporaryDirectory(prefix=self.prefix)
|
||||
self.root = Path(self._temporary.name)
|
||||
os.chmod(self.root, 0o700)
|
||||
|
||||
self.home = self.root / "home"
|
||||
self.xdg_config_home = self.root / "xdg-config"
|
||||
self.xdg_state_home = self.root / "xdg-state"
|
||||
self.xdg_cache_home = self.root / "xdg-cache"
|
||||
self.xdg_runtime_dir = self.root / "xdg-runtime"
|
||||
self.tmpdir = self.root / "tmp"
|
||||
self.repos = self.root / "repos"
|
||||
self.sessions = self.root / "sessions"
|
||||
self.stub_bin = self.root / "bin"
|
||||
for directory in (
|
||||
self.home,
|
||||
self.xdg_config_home,
|
||||
self.xdg_state_home,
|
||||
self.xdg_cache_home,
|
||||
self.xdg_runtime_dir,
|
||||
self.tmpdir,
|
||||
self.repos,
|
||||
self.sessions,
|
||||
self.stub_bin,
|
||||
):
|
||||
_mode(directory, 0o700)
|
||||
|
||||
self.env = _minimal_environment(
|
||||
os.environ,
|
||||
home=self.home,
|
||||
config=self.xdg_config_home,
|
||||
state=self.xdg_state_home,
|
||||
cache=self.xdg_cache_home,
|
||||
runtime=self.xdg_runtime_dir,
|
||||
tmp=self.tmpdir,
|
||||
stub_bin=self.stub_bin,
|
||||
)
|
||||
self._create_fixture_repositories()
|
||||
self._create_stub_bin()
|
||||
self.new_session("one")
|
||||
self.new_session("two")
|
||||
return self
|
||||
|
||||
def __exit__(self, *_: object) -> None:
|
||||
if self._temporary is not None:
|
||||
self._temporary.cleanup()
|
||||
self._temporary = None
|
||||
self.root = None
|
||||
|
||||
def new_session(self, name: str) -> FixtureSession:
|
||||
"""Create (or return) a distinct set of state and scratch directories."""
|
||||
if self.sessions is None or self.repos is None:
|
||||
raise RuntimeError("fixture is not active")
|
||||
if not name or name in {".", ".."} or "/" in name:
|
||||
raise ValueError("session names must be simple path components")
|
||||
if name in self._session_map:
|
||||
return self._session_map[name]
|
||||
root = self.sessions / name
|
||||
home = root / "home"
|
||||
state = root / "state"
|
||||
cache = root / "cache"
|
||||
runtime = root / "runtime"
|
||||
tmp = root / "tmp"
|
||||
repo = self.repos / name
|
||||
for directory in (root, home, home / ".config", state, cache, runtime, tmp, repo):
|
||||
_mode(directory, 0o700)
|
||||
session = FixtureSession(name, root, home, state, cache, runtime, tmp, repo)
|
||||
self._session_map[name] = session
|
||||
return session
|
||||
|
||||
def session(self, name: str) -> FixtureSession:
|
||||
if name not in self._session_map:
|
||||
return self.new_session(name)
|
||||
return self._session_map[name]
|
||||
|
||||
def path(self, *parts: str) -> Path:
|
||||
if self.root is None:
|
||||
raise RuntimeError("fixture is not active")
|
||||
return self.root.joinpath(*parts)
|
||||
|
||||
def _create_fixture_repositories(self) -> None:
|
||||
assert self.repos is not None
|
||||
for path in (
|
||||
self.repos / "primary",
|
||||
self.repos / "sibling-worktree",
|
||||
self.repos / "unrelated",
|
||||
):
|
||||
_mode(path, 0o700)
|
||||
(self.repos / "primary" / "README.txt").write_text(
|
||||
"fixture repository\n", encoding="utf-8"
|
||||
)
|
||||
(self.repos / "sibling-worktree" / "README.txt").write_text(
|
||||
"sibling fixture\n", encoding="utf-8"
|
||||
)
|
||||
|
||||
def _create_stub_bin(self) -> None:
|
||||
assert self.stub_bin is not None
|
||||
_write_executable(
|
||||
self.stub_bin / "safe-probe",
|
||||
"#!/bin/sh\nprintf '%s\\n' safe-probe\n",
|
||||
)
|
||||
_write_executable(
|
||||
self.stub_bin / "credential-name-probe",
|
||||
"#!/bin/sh\nprintf '%s\\n' GITHUB_TOKEN\n",
|
||||
)
|
||||
|
||||
|
||||
def _minimal_environment(
|
||||
inherited: Mapping[str, str],
|
||||
*,
|
||||
home: Path,
|
||||
config: Path,
|
||||
state: Path,
|
||||
cache: Path,
|
||||
runtime: Path,
|
||||
tmp: Path,
|
||||
stub_bin: Path | None = None,
|
||||
) -> dict[str, str]:
|
||||
"""Build an allowlist environment; no inherited credential-like names survive."""
|
||||
path = f"{stub_bin}:/usr/bin:/bin" if stub_bin is not None else "/usr/bin:/bin"
|
||||
environment = {
|
||||
"HOME": str(home),
|
||||
"XDG_CONFIG_HOME": str(config),
|
||||
"XDG_STATE_HOME": str(state),
|
||||
"XDG_CACHE_HOME": str(cache),
|
||||
"XDG_RUNTIME_DIR": str(runtime),
|
||||
"TMPDIR": str(tmp),
|
||||
"PATH": path,
|
||||
"LANG": inherited.get("LANG", "C"),
|
||||
"LC_ALL": inherited.get("LC_ALL", "C"),
|
||||
"PYTHONDONTWRITEBYTECODE": "1",
|
||||
}
|
||||
# Deliberately construct this mapping instead of copying os.environ. Keep this
|
||||
# assertion close to the allowlist so a newly added variable is reviewed.
|
||||
for name in environment:
|
||||
upper = name.upper()
|
||||
if any(hint in upper for hint in _CREDENTIAL_NAME_HINTS):
|
||||
raise AssertionError(f"credential-like variable accidentally allowlisted: {name}")
|
||||
return environment
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def isolated_environment(prefix: str = "nono-evidence-") -> Iterator[IsolatedFixture]:
|
||||
"""Yield an :class:`IsolatedFixture` and clean every path on exit."""
|
||||
with IsolatedFixture(prefix=prefix) as fixture:
|
||||
yield fixture
|
||||
|
||||
|
||||
__all__ = [
|
||||
"FixtureSession",
|
||||
"IsolatedFixture",
|
||||
"isolated_environment",
|
||||
]
|
||||
Reference in New Issue
Block a user