sots-re/tools/evidence.py

78 lines
4.4 KiB
Python

#!/usr/bin/env python3
"""Structural acceptance of gate evidence. Behavioral acceptance belongs to campaign review."""
import hashlib
import json
from pathlib import Path
import re
REQUIRED_CHECKS = frozenset((
"sourceCopy", "cleanRoom", "shimConfigs", "configure", "build", "inventory", "ctest",
"junitComplete", "outputComplete", "testStatuses", "skipClassification", "corpusExecution", "corpusNonzero",
"sourceUnchanged", "toolSourceUnchanged", "corpusUnchanged", "inputsUnchanged", "binaryExists"))
CORPUS_TESTS = {"mars_stream_save", "mars_stream_domains", "app_turn", "app_turn_record"}
def sha256(path):
return hashlib.sha256(Path(path).read_bytes()).hexdigest()
def require(value, message):
if not value:
raise ValueError(message)
def hash_valid(value):
return isinstance(value, str) and re.fullmatch(r"[0-9a-f]{64}", value) is not None
def validate_gate(manifest):
require(isinstance(manifest, dict) and manifest.get("schema") == "sots-gate/1" and
manifest.get("status") == "passed", "expected a passed sots-gate/1 manifest")
require(manifest.get("profile") in ("host", "full"), "invalid gate profile")
binary, source, tests = (manifest.get(k) for k in ("binary", "source", "tests"))
require(isinstance(binary, dict) and binary.get("path") and hash_valid(binary.get("sha256")),
"passed gate lacks a bound binary")
require(isinstance(source, dict) and set(source) == {"engine", "re"}, "missing source identities")
for name, identity in source.items():
require(isinstance(identity, dict) and identity.get("path") and identity.get("head") and
isinstance(identity.get("dirty"), bool) and identity.get("files"), f"incomplete {name} identity")
paths = []
for row in identity["files"]:
require(isinstance(row, dict) and row.get("path") and hash_valid(row.get("sha256")), "invalid source file")
relative = Path(row["path"])
require(not relative.is_absolute() and ".." not in relative.parts, "invalid source path")
paths.append(row["path"])
require(len(paths) == len(set(paths)), "duplicate source paths")
require(isinstance(tests, dict), "missing tests")
for key in ("expected", "passed", "skipped", "failed"):
values = tests.get(key)
require(isinstance(values, list) and all(isinstance(v, str) for v in values) and
len(values) == len(set(values)), f"invalid tests.{key}")
require(tests["expected"] and tests["passed"] and not tests["failed"] and
not set(tests["passed"]) & set(tests["skipped"]) and
sorted(tests["passed"] + tests["skipped"]) == sorted(tests["expected"]), "incomplete test execution")
require(CORPUS_TESTS <= set(tests["passed"]), "required corpus tests did not execute")
required, checks = manifest.get("requiredChecks"), manifest.get("checks")
require(isinstance(required, list) and all(isinstance(v, str) for v in required) and
REQUIRED_CHECKS <= set(required), "required gate checks were omitted")
require(isinstance(checks, dict) and all(checks.get(name) is True for name in required),
"required gate checks failed")
inputs = manifest.get("inputs", {})
corpus = inputs.get("SOTS_SAVES_DIR", {}) if isinstance(inputs, dict) else {}
require(isinstance(corpus, dict) and corpus.get("root") and corpus.get("files"), "missing corpus manifest")
for item in inputs.values():
require(isinstance(item, dict) and item.get("root") and isinstance(item.get("files"), list), "invalid input manifest")
require(all(isinstance(row, dict) and row.get("path") and hash_valid(row.get("sha256"))
for row in item["files"]), "invalid input hashes")
counts = tests.get("corpusSummaryCounts", {})
require(all(counts.get(name) == [len(corpus["files"])] for name in CORPUS_TESTS), "missing positive corpus counts")
require(isinstance(manifest.get("limitations"), list) and manifest.get("tools"), "missing tools/limitations")
if manifest["profile"] == "full":
require(not tests["skipped"] and "SOTS_DATA_DIR" in inputs and
checks.get("shimBuild") is True and checks.get("shimBinaryExists") is True and
hash_valid(manifest.get("shimBinary", {}).get("sha256")), "incomplete full gate")
return manifest
def load_passed_gate(path):
return validate_gate(json.loads(Path(path).read_text()))