feat(nono): add isolated evidence harness
This commit is contained in:
Executable
+183
@@ -0,0 +1,183 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user