603 lines
23 KiB
Python
Executable File
603 lines
23 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Trusted-snapshot entry point for Nono evidence production.
|
|
|
|
The checkout copy is source only. A future maintenance installer must promote
|
|
this file and its harness into an immutable snapshot, add the marker file, and
|
|
invoke this command with that snapshot's digest. Running this source directly
|
|
therefore fails closed.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import platform
|
|
import re
|
|
import shutil
|
|
import stat
|
|
import subprocess
|
|
import sys
|
|
import uuid
|
|
from pathlib import Path
|
|
from typing import Any, Callable
|
|
|
|
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
_MARKER_CONTENT = "pi-sbx-trusted-snapshot-v1\n"
|
|
_DIGEST = re.compile(r"^[0-9a-f]{64}$")
|
|
_RAW_OUTPUT_FIELDS = frozenset({"stdout", "stderr", "argv", "env", "environment", "exception", "output"})
|
|
_LIVE_TESTS = frozenset({"git-live", "github-cli-live"})
|
|
|
|
|
|
def _sha256_bytes(value: bytes) -> str:
|
|
return hashlib.sha256(value).hexdigest()
|
|
|
|
|
|
def _regular_file_identity(path: Path, *, max_size: int) -> tuple[int, int, int, int, str]:
|
|
if path.is_symlink():
|
|
raise EvidenceCliError(f"identity input cannot be a symlink: {path}")
|
|
flags = (
|
|
os.O_RDONLY
|
|
| getattr(os, "O_NOFOLLOW", 0)
|
|
| getattr(os, "O_NONBLOCK", 0)
|
|
)
|
|
try:
|
|
descriptor = os.open(path, flags)
|
|
except OSError as error:
|
|
raise EvidenceCliError(f"identity input is unavailable: {path}: {error}") from error
|
|
try:
|
|
file_stat = os.fstat(descriptor)
|
|
if not stat.S_ISREG(file_stat.st_mode) or file_stat.st_size > max_size:
|
|
raise EvidenceCliError(
|
|
f"identity input must be a bounded regular file: {path}"
|
|
)
|
|
digest = hashlib.sha256()
|
|
remaining = file_stat.st_size
|
|
while remaining:
|
|
chunk = os.read(descriptor, min(65536, remaining))
|
|
if not chunk:
|
|
break
|
|
digest.update(chunk)
|
|
remaining -= len(chunk)
|
|
final_stat = os.fstat(descriptor)
|
|
if remaining or final_stat.st_size != file_stat.st_size:
|
|
raise EvidenceCliError(f"identity input changed while reading: {path}")
|
|
return (
|
|
file_stat.st_dev,
|
|
file_stat.st_ino,
|
|
file_stat.st_size,
|
|
file_stat.st_mtime_ns,
|
|
digest.hexdigest(),
|
|
)
|
|
finally:
|
|
os.close(descriptor)
|
|
|
|
|
|
def _git_head(git: str, repository_root: Path) -> str:
|
|
try:
|
|
result = subprocess.run(
|
|
[git, "-C", str(repository_root), "rev-parse", "HEAD"],
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=10,
|
|
env={"PATH": "/usr/bin:/bin", "LANG": "C", "LC_ALL": "C"},
|
|
)
|
|
except (OSError, subprocess.SubprocessError) as error:
|
|
raise EvidenceCliError(f"could not resolve repository commit: {error}") from error
|
|
return result.stdout.strip()
|
|
|
|
|
|
class TrustedSnapshotError(RuntimeError):
|
|
"""The source has not been authorized as an installed producer."""
|
|
|
|
|
|
class EvidenceCliError(RuntimeError):
|
|
"""Invalid evidence CLI request."""
|
|
|
|
|
|
def _snapshot_digest(snapshot: Path) -> str:
|
|
if snapshot.is_file():
|
|
return sha256_bytes(snapshot.read_bytes())
|
|
if not snapshot.is_dir():
|
|
raise TrustedSnapshotError(f"trusted snapshot is not a file or directory: {snapshot}")
|
|
records: list[bytes] = []
|
|
for path in sorted(snapshot.rglob("*")):
|
|
relative = path.relative_to(snapshot).as_posix()
|
|
if relative == ".pi-sbx-trusted-snapshot":
|
|
continue
|
|
if path.is_symlink():
|
|
raise TrustedSnapshotError(f"trusted snapshot contains symlink: {relative}")
|
|
if path.is_file():
|
|
records.append(
|
|
relative.encode("utf-8")
|
|
+ b"\0"
|
|
+ _sha256_bytes(path.read_bytes()).encode("ascii")
|
|
+ b"\n"
|
|
)
|
|
return _sha256_bytes(b"".join(records))
|
|
|
|
|
|
def verify_trusted_snapshot(snapshot: str | os.PathLike[str], expected_digest: str) -> Path:
|
|
"""Verify an explicit marker and immutable-content digest.
|
|
|
|
A path inside this source checkout is rejected even if a marker is added to
|
|
it. This prevents a repository write from becoming a trusted producer.
|
|
"""
|
|
path = Path(snapshot)
|
|
if not path.is_absolute():
|
|
raise TrustedSnapshotError("--trusted-snapshot must be an absolute path")
|
|
if _DIGEST.fullmatch(expected_digest) is None:
|
|
raise TrustedSnapshotError("trusted snapshot digest must be lowercase SHA-256")
|
|
if path.is_symlink():
|
|
raise TrustedSnapshotError("trusted snapshot path itself must not be a symlink")
|
|
try:
|
|
resolved = path.resolve(strict=True)
|
|
except OSError as error:
|
|
raise TrustedSnapshotError(f"cannot resolve trusted snapshot: {error}") from error
|
|
source = REPOSITORY_ROOT.resolve()
|
|
producer = Path(__file__).resolve()
|
|
source_is_checkout = (source / ".git").exists()
|
|
if source_is_checkout:
|
|
raise TrustedSnapshotError("source checkout cannot be a trusted production snapshot")
|
|
if not resolved.is_dir() or resolved != source or resolved not in producer.parents:
|
|
raise TrustedSnapshotError(
|
|
"trusted snapshot must equal the executing producer's repository root"
|
|
)
|
|
if resolved.is_dir():
|
|
if resolved.stat().st_uid != os.geteuid() or resolved.stat().st_mode & 0o022:
|
|
raise TrustedSnapshotError("trusted snapshot directory must be caller-owned and private")
|
|
for candidate in resolved.rglob("*"):
|
|
if candidate.is_symlink():
|
|
raise TrustedSnapshotError(f"trusted snapshot contains symlink: {candidate}")
|
|
if not candidate.is_dir() and not candidate.is_file():
|
|
raise TrustedSnapshotError(f"trusted snapshot contains a non-file entry: {candidate}")
|
|
candidate_stat = candidate.stat()
|
|
if candidate_stat.st_uid != os.geteuid() or candidate_stat.st_mode & 0o022:
|
|
raise TrustedSnapshotError(
|
|
f"trusted snapshot entries must be caller-owned and not group/other writable: {candidate}"
|
|
)
|
|
marker = resolved / ".pi-sbx-trusted-snapshot"
|
|
try:
|
|
marker_contents = marker.read_text(encoding="utf-8")
|
|
except OSError as error:
|
|
raise TrustedSnapshotError(f"trusted snapshot marker is missing: {marker}") from error
|
|
if marker_contents != _MARKER_CONTENT:
|
|
raise TrustedSnapshotError("trusted snapshot marker has an invalid contract")
|
|
observed = _snapshot_digest(resolved)
|
|
if observed != expected_digest:
|
|
raise TrustedSnapshotError(
|
|
f"trusted snapshot digest mismatch: expected {expected_digest}, observed {observed}"
|
|
)
|
|
return resolved
|
|
|
|
|
|
def _load_trusted_modules() -> None:
|
|
"""Import producer code only after the executing snapshot is verified."""
|
|
if str(REPOSITORY_ROOT) not in sys.path:
|
|
sys.path.insert(0, str(REPOSITORY_ROOT))
|
|
global EvidenceStore, EvidenceValidationError, create_summary
|
|
global sha256_bytes, sha256_json, utc_timestamp, redact
|
|
global isolated_environment, HostRequirementError, NonoAdapter
|
|
global run_bounded_command
|
|
|
|
from tests.nono.harness.evidence import (
|
|
EvidenceStore,
|
|
EvidenceValidationError,
|
|
create_summary,
|
|
sha256_bytes,
|
|
sha256_json,
|
|
utc_timestamp,
|
|
)
|
|
from tests.nono.harness.fixtures import isolated_environment
|
|
from tests.nono.harness.nono import (
|
|
HostRequirementError,
|
|
NonoAdapter,
|
|
run_bounded_command,
|
|
)
|
|
from tests.nono.harness.redaction import redact
|
|
|
|
|
|
def _observation(
|
|
observation_id: str,
|
|
capability: str,
|
|
expected: str,
|
|
actual: str,
|
|
status: str,
|
|
) -> dict[str, str]:
|
|
body = {
|
|
"id": observation_id,
|
|
"capability": capability,
|
|
"expected": expected,
|
|
"actual": actual,
|
|
"status": status,
|
|
}
|
|
body["hash"] = sha256_json(body)
|
|
return body
|
|
|
|
|
|
def _run_fixed_harness(arguments: list[str]) -> dict[str, Any]:
|
|
if arguments:
|
|
command = [
|
|
sys.executable,
|
|
str(REPOSITORY_ROOT / "scripts" / "test_nono.py"),
|
|
*arguments,
|
|
]
|
|
else:
|
|
command = [
|
|
sys.executable,
|
|
"-m",
|
|
"unittest",
|
|
"discover",
|
|
"-s",
|
|
"tests/nono/harness",
|
|
"-p",
|
|
"test_*.py",
|
|
"-t",
|
|
".",
|
|
]
|
|
with isolated_environment(prefix="nono-evidence-runner-") as fixture:
|
|
result = run_bounded_command(
|
|
command,
|
|
cwd=REPOSITORY_ROOT,
|
|
environment=fixture.env,
|
|
timeout=180,
|
|
max_output=65536,
|
|
)
|
|
return {
|
|
"returncode": result.returncode,
|
|
"stdout": result.stdout,
|
|
"stderr": result.stderr,
|
|
"timedOut": result.timed_out,
|
|
}
|
|
|
|
|
|
def _hermetic_core(_: argparse.Namespace) -> dict[str, Any]:
|
|
execution = _run_fixed_harness([])
|
|
passed = execution["returncode"] == 0 and not execution["timedOut"]
|
|
status = "passed" if passed else "failed"
|
|
actual = "allowed" if passed else "unknown"
|
|
return {
|
|
"status": status,
|
|
"observations": [
|
|
_observation("hermetic-suite", "harness", "allowed", actual, status)
|
|
],
|
|
"hashes": {},
|
|
"stdout": execution["stdout"],
|
|
"stderr": execution["stderr"],
|
|
}
|
|
|
|
|
|
def _negative_matrix(_: argparse.Namespace) -> dict[str, Any]:
|
|
capabilities = (
|
|
"tmp", "runtime-parent", "sibling-worktree", "direct-ssh", "cross-context-key",
|
|
"unrelated-pi-state", "trusted-instructions-write", "docker", "browser",
|
|
"dictation", "update", "production-credentials", "nonselected-provider",
|
|
)
|
|
observations = [
|
|
_observation(f"negative-{name}", name, "denied", "not-run", "blocked")
|
|
for name in capabilities
|
|
]
|
|
return {"status": "blocked", "observations": observations, "hashes": {}}
|
|
|
|
|
|
def _positive_contract(_: argparse.Namespace) -> dict[str, Any]:
|
|
capabilities = ("git", "gh-cli", "package-cache", "provider-port", "unix-socket")
|
|
observations = [
|
|
_observation(f"positive-{name}", name, "allowed", "not-run", "blocked")
|
|
for name in capabilities
|
|
]
|
|
return {"status": "blocked", "observations": observations, "hashes": {}}
|
|
|
|
|
|
def _broad_fixture(args: argparse.Namespace) -> dict[str, Any]:
|
|
if args.suite != "host":
|
|
return {
|
|
"status": "blocked",
|
|
"observations": [
|
|
_observation(
|
|
"broad-workdir",
|
|
"workdir",
|
|
"denied",
|
|
"not-run",
|
|
"blocked",
|
|
)
|
|
],
|
|
"hashes": {},
|
|
}
|
|
execution = _run_fixed_harness(["--host", "--nono", args.nono])
|
|
passed = execution["returncode"] == 0 and not execution["timedOut"]
|
|
status = "passed" if passed else "failed"
|
|
return {
|
|
"status": status,
|
|
"observations": [
|
|
_observation(
|
|
"minimal-cross-session",
|
|
"cross-session-filesystem",
|
|
"denied",
|
|
"denied" if passed else "unknown",
|
|
status,
|
|
),
|
|
_observation(
|
|
"broad-workdir",
|
|
"workdir",
|
|
"allowed",
|
|
"allowed" if passed else "unknown",
|
|
status,
|
|
),
|
|
],
|
|
"hashes": {},
|
|
"stdout": execution["stdout"],
|
|
"stderr": execution["stderr"],
|
|
}
|
|
|
|
|
|
def _live_result(_: argparse.Namespace) -> dict[str, Any]:
|
|
# Credentialed live work is intentionally not attempted by source code. A
|
|
# host procedure must replace this fixed blocked observation in a future
|
|
# installed snapshot after a human has approved it.
|
|
return {
|
|
"status": "blocked",
|
|
"observations": [
|
|
_observation("live-manual-check", "live-credentials", "allowed", "not-run", "blocked")
|
|
],
|
|
"hashes": {},
|
|
}
|
|
|
|
|
|
BUILTIN_TESTS: dict[str, Callable[[argparse.Namespace], dict[str, Any]]] = {
|
|
"hermetic-core": _hermetic_core,
|
|
"negative-capability-matrix": _negative_matrix,
|
|
"positive-contract-matrix": _positive_contract,
|
|
"broad-fixture": _broad_fixture,
|
|
"git-live": _live_result,
|
|
"github-cli-live": _live_result,
|
|
}
|
|
|
|
|
|
def _parser() -> argparse.ArgumentParser:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--trusted-snapshot", required=True, help="installed snapshot directory")
|
|
parser.add_argument(
|
|
"--trusted-snapshot-digest",
|
|
"--snapshot-digest",
|
|
dest="trusted_snapshot_digest",
|
|
required=True,
|
|
help="SHA-256 digest of the installed snapshot manifest",
|
|
)
|
|
parser.add_argument(
|
|
"--state-root", required=True, help="injected XDG_STATE_HOME-compatible evidence root"
|
|
)
|
|
parser.add_argument(
|
|
"--summary-root", required=True, help="injected root for redacted summaries"
|
|
)
|
|
subparsers = parser.add_subparsers(dest="action", required=True)
|
|
run = subparsers.add_parser("run", help="run one fixed registry test")
|
|
run.add_argument("test_id")
|
|
run.add_argument("--suite", choices=("hermetic", "host", "live"), default="hermetic")
|
|
run.add_argument("--approver")
|
|
run.add_argument("--approve-live", action="store_true")
|
|
run.add_argument("--repository-root", required=True)
|
|
run.add_argument("--repository-commit", required=True)
|
|
run.add_argument("--policy-file", required=True)
|
|
run.add_argument("--policy-digest", required=True)
|
|
run.add_argument("--nono", required=True, help="absolute Nono 0.74 executable")
|
|
run.add_argument("--canary", action="append", default=[])
|
|
summarize = subparsers.add_parser("summarize", help="summarize retained raw evidence")
|
|
summarize.add_argument("run_id")
|
|
summarize.add_argument("--repository-id", default="fixture-repository")
|
|
summarize.add_argument("--canary", action="append", default=[])
|
|
summarize.add_argument(
|
|
"--credential-name-pattern",
|
|
action="append",
|
|
default=[],
|
|
help="trusted credential-name regex that must be redacted from the summary",
|
|
)
|
|
return parser
|
|
|
|
|
|
def _run(args: argparse.Namespace, snapshot_digest: str) -> int:
|
|
if args.test_id not in BUILTIN_TESTS:
|
|
raise EvidenceCliError(f"unknown test ID {args.test_id!r}; arbitrary commands are forbidden")
|
|
selected_nono = Path(args.nono)
|
|
if not selected_nono.is_absolute():
|
|
raise EvidenceCliError("Nono executable must be an absolute path")
|
|
nono_identity = _regular_file_identity(selected_nono, max_size=256 * 1024 * 1024)
|
|
try:
|
|
with isolated_environment(prefix="nono-evidence-host-") as fixture:
|
|
session = fixture.session("one")
|
|
adapter = NonoAdapter(
|
|
selected_nono,
|
|
cwd=session.repo,
|
|
environment=session.environment(fixture.env),
|
|
)
|
|
args.nono_version = adapter.require_host_0_74()
|
|
if args.suite == "host":
|
|
profile = (
|
|
REPOSITORY_ROOT
|
|
/ "tests"
|
|
/ "nono"
|
|
/ "fixtures"
|
|
/ "core"
|
|
/ "minimal.json"
|
|
)
|
|
if not adapter.validate(profile).ok or not adapter.run(
|
|
profile,
|
|
["/usr/bin/true"],
|
|
).ok:
|
|
raise EvidenceCliError(
|
|
"host suite fixture validation or execution failed"
|
|
)
|
|
except (HostRequirementError, OSError, ValueError) as error:
|
|
raise EvidenceCliError(f"Nono 0.74 verification failed: {error}") from error
|
|
if args.suite == "live":
|
|
if args.test_id not in _LIVE_TESTS:
|
|
raise EvidenceCliError("live mode is available only for the fixed live test registry")
|
|
if not args.approve_live or not isinstance(args.approver, str) or not args.approver.strip():
|
|
raise EvidenceCliError("live mode requires explicit approval via --approve-live and a named --approver")
|
|
elif args.test_id in _LIVE_TESTS:
|
|
raise EvidenceCliError("credentialed tests require explicit --suite live approval")
|
|
repository_root = Path(args.repository_root)
|
|
if not repository_root.is_absolute() or repository_root.is_symlink():
|
|
raise EvidenceCliError("repository root must be an absolute non-symlink path")
|
|
try:
|
|
repository_root = repository_root.resolve(strict=True)
|
|
except OSError as error:
|
|
raise EvidenceCliError(f"repository root is unavailable: {error}") from error
|
|
if not (repository_root / ".git").exists():
|
|
raise EvidenceCliError("repository root is not a Git checkout")
|
|
git = shutil.which("git", path="/usr/bin:/bin")
|
|
if git is None:
|
|
raise EvidenceCliError("Git is unavailable for repository identity verification")
|
|
observed_commit = _git_head(git, repository_root)
|
|
if observed_commit != args.repository_commit:
|
|
raise EvidenceCliError("repository commit does not match the tested checkout")
|
|
|
|
policy_file = Path(args.policy_file)
|
|
if not policy_file.is_absolute() or policy_file.is_symlink():
|
|
raise EvidenceCliError("policy file must be an absolute non-symlink path")
|
|
try:
|
|
policy_file = policy_file.resolve(strict=True)
|
|
except OSError as error:
|
|
raise EvidenceCliError(f"policy file is unavailable: {error}") from error
|
|
policy_identity = _regular_file_identity(policy_file, max_size=1024 * 1024)
|
|
policy_digest = policy_identity[-1]
|
|
if policy_digest != args.policy_digest:
|
|
raise EvidenceCliError("policy digest does not match the tested policy file")
|
|
run_id = "run-" + uuid.uuid4().hex
|
|
started = utc_timestamp()
|
|
result = BUILTIN_TESTS[args.test_id](args)
|
|
if _regular_file_identity(
|
|
selected_nono,
|
|
max_size=256 * 1024 * 1024,
|
|
) != nono_identity:
|
|
raise EvidenceCliError("Nono executable changed during evidence execution")
|
|
if _git_head(git, repository_root) != observed_commit:
|
|
raise EvidenceCliError("repository HEAD changed during evidence execution")
|
|
if _regular_file_identity(policy_file, max_size=1024 * 1024) != policy_identity:
|
|
raise EvidenceCliError("policy file changed during evidence execution")
|
|
finished = utc_timestamp()
|
|
payload = {
|
|
"schemaVersion": 1,
|
|
"runId": run_id,
|
|
"testId": args.test_id,
|
|
"suite": args.suite,
|
|
"metadata": {
|
|
"repositoryCommit": args.repository_commit,
|
|
"policySnapshotDigest": policy_digest,
|
|
"installedSnapshotDigest": snapshot_digest,
|
|
"nonoVersion": args.nono_version,
|
|
"approver": args.approver if args.suite == "live" else None,
|
|
"startedAt": started,
|
|
"finishedAt": finished,
|
|
},
|
|
"result": result,
|
|
}
|
|
store = EvidenceStore(args.state_root, args.summary_root)
|
|
store.purge_expired()
|
|
reference = store.write_raw_evidence(run_id, payload)
|
|
print(
|
|
json.dumps(
|
|
{
|
|
"runId": run_id,
|
|
"status": result["status"],
|
|
"rawEvidenceSha256": reference.sha256,
|
|
"rawEvidence": str(reference.path),
|
|
},
|
|
sort_keys=True,
|
|
)
|
|
)
|
|
return {"passed": 0, "failed": 1, "blocked": 2, "not-run": 2}.get(
|
|
str(result["status"]), 2
|
|
)
|
|
|
|
|
|
def _summarize(args: argparse.Namespace, snapshot_digest: str) -> int:
|
|
store = EvidenceStore(args.state_root, args.summary_root)
|
|
payload, raw_metadata = store.read_raw_evidence(args.run_id)
|
|
if not isinstance(payload, dict) or not isinstance(payload.get("metadata"), dict):
|
|
raise EvidenceCliError("retained evidence has no trusted metadata")
|
|
metadata = payload["metadata"]
|
|
if payload.get("runId") != args.run_id:
|
|
raise EvidenceCliError("retained evidence run ID mismatch")
|
|
if metadata.get("installedSnapshotDigest") != snapshot_digest:
|
|
raise EvidenceCliError("evidence was produced by a different installed snapshot")
|
|
result = payload.get("result")
|
|
if not isinstance(result, dict):
|
|
raise EvidenceCliError("retained evidence result is not an object")
|
|
cleaned = redact(
|
|
payload,
|
|
canaries=args.canary,
|
|
credential_name_patterns=args.credential_name_pattern,
|
|
)
|
|
cleaned_result = cleaned.get("result")
|
|
if not isinstance(cleaned_result, dict):
|
|
raise EvidenceCliError("redacted result is not an object")
|
|
result_hashes: dict[str, str] = {}
|
|
raw_hashes = cleaned_result.get("hashes", {})
|
|
if not isinstance(raw_hashes, dict):
|
|
raise EvidenceCliError("result hashes are not an object")
|
|
for key, digest in raw_hashes.items():
|
|
if not isinstance(key, str) or key in _RAW_OUTPUT_FIELDS or _DIGEST.fullmatch(str(digest)) is None:
|
|
raise EvidenceCliError("result contains an invalid or raw hash field")
|
|
result_hashes[key] = str(digest)
|
|
# Output fields are represented only by their digest in a summary.
|
|
for key, value in result.items():
|
|
if key in _RAW_OUTPUT_FIELDS:
|
|
result_hashes[key + "Hash"] = sha256_json(value)
|
|
observations = cleaned_result.get("observations", [])
|
|
if not isinstance(observations, list):
|
|
raise EvidenceCliError("observations are not an array")
|
|
summary = create_summary(
|
|
run_id=args.run_id,
|
|
test_id=str(payload.get("testId", "")),
|
|
suite=str(payload.get("suite", "")),
|
|
repository_commit=str(metadata.get("repositoryCommit", "")),
|
|
policy_snapshot_digest=str(metadata.get("policySnapshotDigest", "")),
|
|
installed_snapshot_digest=snapshot_digest,
|
|
nono_version=str(metadata.get("nonoVersion", "")),
|
|
status=str(cleaned_result.get("status", "")),
|
|
approver=metadata.get("approver"),
|
|
started_at=str(metadata.get("startedAt", "")),
|
|
finished_at=str(metadata.get("finishedAt", "")),
|
|
raw_evidence_sha256=str(raw_metadata.get("sha256", "")),
|
|
raw_evidence_retained_until=str(raw_metadata.get("retainedUntil", "")),
|
|
result_hashes=result_hashes,
|
|
observations=observations,
|
|
canaries=args.canary,
|
|
credential_name_patterns=args.credential_name_pattern,
|
|
)
|
|
policy_digest = str(metadata["policySnapshotDigest"])
|
|
path = store.write_summary(
|
|
summary,
|
|
repository_id=args.repository_id,
|
|
policy_digest=policy_digest,
|
|
)
|
|
print(json.dumps({"runId": args.run_id, "summary": str(path), "status": summary["status"]}, sort_keys=True))
|
|
return {"passed": 0, "failed": 1, "blocked": 2, "not-run": 2}.get(
|
|
str(summary["status"]), 2
|
|
)
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
args = _parser().parse_args(argv)
|
|
snapshot = verify_trusted_snapshot(args.trusted_snapshot, args.trusted_snapshot_digest)
|
|
del snapshot
|
|
sys.dont_write_bytecode = True
|
|
_load_trusted_modules()
|
|
if args.action == "run":
|
|
return _run(args, args.trusted_snapshot_digest)
|
|
if args.action == "summarize":
|
|
return _summarize(args, args.trusted_snapshot_digest)
|
|
raise EvidenceCliError("unknown action")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
raise SystemExit(main())
|
|
except (EvidenceCliError, TrustedSnapshotError, ValueError) as error:
|
|
print(f"nono-evidence: {error}", file=sys.stderr)
|
|
raise SystemExit(2)
|