Files

184 lines
6.7 KiB
Python
Executable File

#!/usr/bin/env python3
"""Run the Nono Slice 2 hermetic suite and explicit security gates."""
from __future__ import annotations
import argparse
import json
import platform
import shutil
import sys
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from tests.nono.harness.fixtures import isolated_environment # noqa: E402
from tests.nono.harness.nono import HostRequirementError, NonoAdapter # noqa: E402
def _parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--host", action="store_true", help="run mandatory Linux/Nono 0.74 checks")
parser.add_argument(
"--live", action="store_true", help="run separately approved live gate (never implicit)"
)
parser.add_argument(
"--approve-live",
metavar="APPROVER",
help="required explicit human approval for --live; absence is an error",
)
parser.add_argument("--nono", default=None, help="absolute Nono executable for --host")
return parser
def run_hermetic() -> int:
suite = unittest.defaultTestLoader.discover(
str(ROOT / "tests" / "nono"), pattern="test_*.py", top_level_dir=str(ROOT)
)
result = unittest.TextTestRunner(verbosity=2).run(suite)
return 0 if result.wasSuccessful() else 1
def run_host(executable: str | None) -> int:
if platform.system() != "Linux":
print("host gate failed: Linux is required", file=sys.stderr)
return 2
selected = executable or shutil.which("nono")
if selected is None:
print("host gate failed: nono executable is missing", file=sys.stderr)
return 2
selected_path = Path(selected)
if not selected_path.is_absolute():
print("host gate failed: nono executable must resolve to an absolute path", file=sys.stderr)
return 2
fixture_root = ROOT / "tests" / "nono" / "fixtures" / "core"
minimal_profile = fixture_root / "minimal.json"
broad_profile = fixture_root / "broad.json"
try:
with isolated_environment(prefix="nono-host-") as fixture:
session = fixture.session("one")
adapter = NonoAdapter(
selected_path,
cwd=session.repo,
timeout=20,
environment=session.environment(fixture.env),
)
version = adapter.require_host_0_74()
validation = adapter.validate_temporary_profile(minimal_profile)
if not validation.ok:
print(
f"host gate failed: strict validation exited {validation.returncode}: "
f"{validation.stderr or validation.stdout}",
file=sys.stderr,
)
return 1
shown = adapter.show_temporary_profile(minimal_profile)
if not shown.ok:
print("host gate failed: profile show did not succeed", file=sys.stderr)
return 1
try:
resolved = json.loads(shown.stdout)
except json.JSONDecodeError as error:
print(f"host gate failed: profile show was not JSON: {error}", file=sys.stderr)
return 1
if resolved.get("network", {}).get("block") is not True:
print("host gate failed: fixture is not network-blocked", file=sys.stderr)
return 1
run = adapter.run_temporary_profile(minimal_profile, ["/usr/bin/true"])
if not run.ok:
print(
f"host gate failed: isolated execution exited {run.returncode}: "
f"{run.stderr or run.stdout}",
file=sys.stderr,
)
return 1
denied_target = fixture.session("two").tmp / "private.txt"
denied_target.write_text("denied fixture\n", encoding="utf-8")
probe = [
"/usr/bin/python3",
"-c",
"from pathlib import Path; Path(__import__('sys').argv[1]).read_text()",
str(denied_target),
]
denied = adapter.run_temporary_profile(minimal_profile, probe)
if denied.ok:
print(
"host gate failed: minimal profile read another session's temporary file",
file=sys.stderr,
)
return 1
broad_target = session.repo / "broad-readable.txt"
broad_target.write_text("broad fixture\n", encoding="utf-8")
broad_probe = [*probe[:-1], str(broad_target)]
allowed = adapter.run_temporary_profile(
broad_profile,
broad_probe,
allow_cwd=True,
)
if not allowed.ok:
print(
"host gate failed: broad fixture did not demonstrate the over-grant",
file=sys.stderr,
)
return 1
print(
json.dumps(
{
"suite": "host",
"nonoVersion": version,
"status": "passed",
"negativeChecks": 1,
"overgrantDetectionChecks": 1,
}
)
)
return 0
except HostRequirementError as error:
print(f"host gate failed: {error}", file=sys.stderr)
return 2
except (OSError, ValueError) as error:
print(f"host gate failed: {error}", file=sys.stderr)
return 2
def run_live(approver: str | None) -> int:
if not isinstance(approver, str) or not approver.strip():
print("live gate requires --approve-live APPROVER", file=sys.stderr)
return 2
# This source harness has no credential broker. A missing manual check is
# blocked, never represented as a pass or an implicit unittest skip.
print(
json.dumps(
{
"suite": "live",
"approver": approver,
"status": "blocked",
"reason": "manual credentialed evidence is not installed in Slice 2",
}
)
)
return 1
def main(argv: list[str] | None = None) -> int:
args = _parser().parse_args(argv)
if args.host and args.live:
print("choose exactly one of --host or --live", file=sys.stderr)
return 2
if args.approve_live is not None and not args.live:
print("--approve-live is valid only with --live", file=sys.stderr)
return 2
if args.host:
return run_host(args.nono)
if args.live:
return run_live(args.approve_live)
return run_hermetic()
if __name__ == "__main__":
raise SystemExit(main())