feat(nono): add isolated evidence harness
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import stat
|
||||
import tempfile
|
||||
import textwrap
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from .fixtures import isolated_environment
|
||||
from .nono import NonoAdapter
|
||||
|
||||
|
||||
class NonoAdapterTests(unittest.TestCase):
|
||||
def _environment(self, root: Path, *, home: Path | None = None) -> dict[str, str]:
|
||||
return {
|
||||
"HOME": str(home or root),
|
||||
"XDG_CONFIG_HOME": str(root),
|
||||
"XDG_STATE_HOME": str(root),
|
||||
"XDG_CACHE_HOME": str(root),
|
||||
"XDG_RUNTIME_DIR": str(root),
|
||||
"TMPDIR": str(root),
|
||||
"PATH": "/usr/bin:/bin",
|
||||
}
|
||||
|
||||
def _fake_nono(self, directory: Path, log: Path) -> Path:
|
||||
executable = directory / "fake-nono"
|
||||
executable.write_text(
|
||||
textwrap.dedent(
|
||||
f"""\
|
||||
#!/usr/bin/env python3
|
||||
import pathlib, sys, time
|
||||
pathlib.Path({str(log)!r}).write_text('\\n'.join(sys.argv[1:]), encoding='utf-8')
|
||||
if '--version' in sys.argv:
|
||||
print('nono 0.74.0')
|
||||
if 'slow' in sys.argv:
|
||||
print('x' * (2 * 1024 * 1024), flush=True)
|
||||
print('y' * (2 * 1024 * 1024), file=sys.stderr, flush=True)
|
||||
time.sleep(2)
|
||||
print('stdout:' + ('x' * 500))
|
||||
print('stderr:' + ('y' * 500), file=sys.stderr)
|
||||
"""
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
executable.chmod(0o700)
|
||||
return executable
|
||||
|
||||
def test_validate_show_and_run_are_exact_argv_and_no_active_namespace(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
log = root / "argv.log"
|
||||
executable = self._fake_nono(root, log)
|
||||
profile = root / "temporary.json"
|
||||
profile.write_text("{}", encoding="utf-8")
|
||||
adapter = NonoAdapter(
|
||||
executable,
|
||||
cwd=root,
|
||||
environment=self._environment(root),
|
||||
)
|
||||
|
||||
adapter.validate_temporary_profile(profile)
|
||||
validation_argv = log.read_text(encoding="utf-8").splitlines()
|
||||
self.assertEqual(validation_argv[:3], ["profile", "validate", "--strict"])
|
||||
self.assertEqual(Path(validation_argv[3]).name, "profile.json")
|
||||
self.assertNotEqual(validation_argv[3], str(profile))
|
||||
adapter.show_temporary_profile(profile)
|
||||
show_argv = log.read_text(encoding="utf-8").splitlines()
|
||||
self.assertEqual(show_argv[:2], ["profile", "show"])
|
||||
self.assertEqual(Path(show_argv[2]).name, "profile.json")
|
||||
self.assertEqual(show_argv[3:], ["--json"])
|
||||
adapter.run_temporary_profile(profile, ["/usr/bin/true", "--fixed"])
|
||||
run_argv = log.read_text(encoding="utf-8").splitlines()
|
||||
self.assertEqual(run_argv[:2], ["run", "--profile"])
|
||||
self.assertEqual(Path(run_argv[2]).name, "profile.json")
|
||||
self.assertEqual(run_argv[3:], ["--", "/usr/bin/true", "--fixed"])
|
||||
adapter.run_temporary_profile(
|
||||
profile,
|
||||
["/usr/bin/true"],
|
||||
allow_cwd=True,
|
||||
)
|
||||
cwd_argv = log.read_text(encoding="utf-8").splitlines()
|
||||
self.assertEqual(cwd_argv[:2], ["run", "--profile"])
|
||||
self.assertEqual(Path(cwd_argv[2]).name, "profile.json")
|
||||
self.assertEqual(cwd_argv[3:], ["--allow-cwd", "--", "/usr/bin/true"])
|
||||
|
||||
active = root / "active" / ".config" / "nono" / "profiles" / "name.json"
|
||||
# An explicitly injected active namespace is rejected by the same
|
||||
# path guard used for HOME's conventional namespace.
|
||||
active.parent.mkdir(parents=True)
|
||||
os.chmod(root / "active", 0o700)
|
||||
adapter_with_home = NonoAdapter(
|
||||
executable,
|
||||
cwd=root,
|
||||
environment=self._environment(root, home=root / "active"),
|
||||
)
|
||||
active.write_text("{}", encoding="utf-8")
|
||||
with self.assertRaises(ValueError):
|
||||
adapter_with_home.validate(active)
|
||||
hardlink = root / "active-hardlink.json"
|
||||
os.link(active, hardlink)
|
||||
with self.assertRaisesRegex(ValueError, "cannot alias"):
|
||||
adapter_with_home.validate(hardlink)
|
||||
|
||||
def test_result_output_is_bounded_and_timeout_is_recorded(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
executable = self._fake_nono(root, root / "argv.log")
|
||||
profile = root / "temporary.json"
|
||||
profile.write_text("{}", encoding="utf-8")
|
||||
adapter = NonoAdapter(
|
||||
executable,
|
||||
cwd=root,
|
||||
timeout=0.1,
|
||||
max_output=32,
|
||||
environment=self._environment(root),
|
||||
)
|
||||
result = adapter.run(profile, ["slow"])
|
||||
self.assertTrue(result.timed_out)
|
||||
self.assertLessEqual(len(result.stdout), 32)
|
||||
self.assertLessEqual(len(result.stderr), 32)
|
||||
|
||||
def test_commands_are_argv_only(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
executable = self._fake_nono(root, root / "argv.log")
|
||||
profile = root / "temporary.json"
|
||||
profile.write_text("{}", encoding="utf-8")
|
||||
adapter = NonoAdapter(
|
||||
executable,
|
||||
cwd=root,
|
||||
environment=self._environment(root),
|
||||
)
|
||||
with self.assertRaises(TypeError):
|
||||
adapter.run(profile, "echo unsafe") # type: ignore[arg-type]
|
||||
|
||||
def test_environment_is_required_and_symlinked_profiles_are_rejected(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
executable = self._fake_nono(root, root / "argv.log")
|
||||
with self.assertRaisesRegex(ValueError, "explicit isolated environment"):
|
||||
NonoAdapter(executable, cwd=root)
|
||||
for unsafe_name in ("GITHUB_TOKEN", "AWS_ACCESS_KEY_ID", "LD_PRELOAD"):
|
||||
with self.subTest(environment=unsafe_name):
|
||||
unsafe_environment = self._environment(root)
|
||||
unsafe_environment[unsafe_name] = "must-not-pass"
|
||||
with self.assertRaisesRegex(ValueError, "allowlist"):
|
||||
NonoAdapter(
|
||||
executable,
|
||||
cwd=root,
|
||||
environment=unsafe_environment,
|
||||
)
|
||||
profile = root / "profile.json"
|
||||
profile.write_text("{}", encoding="utf-8")
|
||||
linked = root / "linked.json"
|
||||
linked.symlink_to(profile)
|
||||
adapter = NonoAdapter(
|
||||
executable,
|
||||
cwd=root,
|
||||
environment=self._environment(root),
|
||||
)
|
||||
with self.assertRaisesRegex(ValueError, "symlinked"):
|
||||
adapter.validate(linked)
|
||||
fifo = root / "fifo-profile.json"
|
||||
os.mkfifo(fifo, mode=0o600)
|
||||
with self.assertRaisesRegex(ValueError, "regular file"):
|
||||
adapter.validate(fifo)
|
||||
|
||||
|
||||
class FixtureProfileValidationTests(unittest.TestCase):
|
||||
def test_fixture_directories_are_not_install_targets(self) -> None:
|
||||
with isolated_environment() as fixture:
|
||||
profile = Path(__file__).parents[1] / "fixtures" / "core" / "minimal.json"
|
||||
self.assertNotIn(str(profile), fixture.env.values())
|
||||
self.assertTrue(profile.is_file())
|
||||
self.assertNotEqual(stat.S_IMODE(profile.stat().st_mode), 0o600)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user