Skip to content

Commit 19dfa2a

Browse files
committed
Add SourceOS SVF contract validator
1 parent 5454db9 commit 19dfa2a

1 file changed

Lines changed: 131 additions & 0 deletions

File tree

tools/validate_svf_contracts.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
#!/usr/bin/env python3
2+
"""Validate SourceOS SVF contract declarations.
3+
4+
This validator checks repo-local SVF contract shape and safety posture. It
5+
does not execute declared actions and does not issue receipts.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
import json
11+
import re
12+
import sys
13+
from pathlib import Path
14+
from typing import Any
15+
16+
ROOT = Path(__file__).resolve().parents[1]
17+
CONTRACT_PATH = ROOT / "svf" / "sourceos-contract-validation-basic.json"
18+
MAKEFILE_PATH = ROOT / "Makefile"
19+
20+
EXPECTED_POLICY_ID = "svf:policy:sourceos.contract-readonly"
21+
EXPECTED_PLAN_ID = "svf:plan:sourceos.contract-validation-basic"
22+
EXPECTED_PROFILE_REF = "svf:profile:sourceos.contracts"
23+
ALLOWED_CLAIMS = {"schema_conformant", "fixtures_validated", "policy_boundary_preserved", "non_production_only"}
24+
REQUIRED_ACTIONS = {
25+
"svf:action:sourceos.control-plane-examples",
26+
"svf:action:sourceos.nlboot-examples",
27+
"svf:action:sourceos.lattice-data-governai-examples",
28+
"svf:action:sourceos.ops-history-examples",
29+
"svf:action:sourceos.runtime-observability-examples",
30+
"svf:action:sourceos.lifecycle-boundary-examples",
31+
}
32+
33+
34+
def read_json(path: Path) -> dict[str, Any]:
35+
return json.loads(path.read_text(encoding="utf-8"))
36+
37+
38+
def make_targets() -> set[str]:
39+
text = MAKEFILE_PATH.read_text(encoding="utf-8")
40+
targets: set[str] = set()
41+
for line in text.splitlines():
42+
if not line or line.startswith("\t") or line.startswith("#"):
43+
continue
44+
match = re.match(r"^([A-Za-z0-9_.:-]+)\s*:", line)
45+
if match:
46+
targets.add(match.group(1))
47+
return targets
48+
49+
50+
def result(check_id: str, passed: bool, diagnostics: list[str] | None = None) -> dict[str, Any]:
51+
return {"check_id": check_id, "passed": passed, "diagnostics": diagnostics or []}
52+
53+
54+
def missing(actual: list[str], expected: set[str]) -> list[str]:
55+
actual_set = set(actual)
56+
return sorted(item for item in expected if item not in actual_set)
57+
58+
59+
def validate() -> dict[str, Any]:
60+
contract = read_json(CONTRACT_PATH)
61+
targets = make_targets()
62+
results: list[dict[str, Any]] = []
63+
64+
results.append(result("contract-id", contract.get("contract_id") == "svf:contract:sourceos.contract-validation-basic", [str(contract.get("contract_id"))]))
65+
results.append(result("profile-ref", contract.get("upstream_authority", {}).get("profile_ref") == EXPECTED_PROFILE_REF, [str(contract.get("upstream_authority", {}).get("profile_ref"))]))
66+
67+
policy = contract.get("capability_policy", {})
68+
results.append(result("policy-id", policy.get("policy_id") == EXPECTED_POLICY_ID, [str(policy.get("policy_id"))]))
69+
results.append(result("policy-production-disallowed", policy.get("production_environment_allowed") is False))
70+
results.append(result("policy-network-none", policy.get("network_modes") == ["network_none"], [str(policy.get("network_modes"))]))
71+
results.append(result("policy-credential-none", policy.get("credential_modes") == ["credential_none"], [str(policy.get("credential_modes"))]))
72+
results.append(result("policy-non-claims-present", isinstance(policy.get("non_claims"), list) and len(policy.get("non_claims", [])) >= 3))
73+
74+
actions = contract.get("actions", [])
75+
action_ids = [action.get("action_id") for action in actions]
76+
results.append(result("required-actions-present", not missing(action_ids, REQUIRED_ACTIONS), missing(action_ids, REQUIRED_ACTIONS)))
77+
results.append(result("action-ids-unique", len(action_ids) == len(set(action_ids))))
78+
79+
for action in actions:
80+
action_id = action.get("action_id", "unknown")
81+
prefix = f"action:{action_id}"
82+
binding = action.get("binding", {})
83+
capability = action.get("capability", {})
84+
claims = action.get("claim_scopes", [])
85+
bad_claims = [claim for claim in claims if claim not in ALLOWED_CLAIMS]
86+
87+
results.append(result(f"{prefix}:binding-kind-make-target", binding.get("kind") == "make_target", [str(binding.get("kind"))]))
88+
results.append(result(f"{prefix}:target-exists", isinstance(binding.get("entrypoint"), str) and binding.get("entrypoint") in targets, [str(binding.get("entrypoint"))]))
89+
results.append(result(f"{prefix}:network-none", capability.get("network_mode") == "network_none", [str(capability.get("network_mode"))]))
90+
results.append(result(f"{prefix}:credential-none", capability.get("credential_mode") == "credential_none", [str(capability.get("credential_mode"))]))
91+
results.append(result(f"{prefix}:backend-local", capability.get("backend") == "local", [str(capability.get("backend"))]))
92+
results.append(result(f"{prefix}:claims-allowed", not bad_claims, bad_claims))
93+
results.append(result(f"{prefix}:non-claims-present", isinstance(action.get("non_claims"), list) and len(action.get("non_claims", [])) >= 1))
94+
95+
plan = contract.get("plan", {})
96+
plan_actions = plan.get("actions", [])
97+
plan_refs = [step.get("action_ref") for step in plan_actions]
98+
missing_refs = [action_ref for action_ref in plan_refs if action_ref not in action_ids]
99+
missing_required_refs = missing(plan_refs, REQUIRED_ACTIONS)
100+
bad_plan_claims = [claim for claim in plan.get("claim_scopes", []) if claim not in ALLOWED_CLAIMS]
101+
102+
results.append(result("plan-id", plan.get("plan_id") == EXPECTED_PLAN_ID, [str(plan.get("plan_id"))]))
103+
results.append(result("plan-mode-advisory", plan.get("mode") == "advisory", [str(plan.get("mode"))]))
104+
results.append(result("plan-policy-ref", plan.get("policy_ref") == EXPECTED_POLICY_ID, [str(plan.get("policy_ref"))]))
105+
results.append(result("plan-action-refs-resolve", not missing_refs, [str(item) for item in missing_refs]))
106+
results.append(result("plan-includes-required-actions", not missing_required_refs, missing_required_refs))
107+
results.append(result("plan-claims-allowed", not bad_plan_claims, bad_plan_claims))
108+
results.append(result("plan-non-claims-present", isinstance(plan.get("non_claims"), list) and len(plan.get("non_claims", [])) >= 3))
109+
110+
passed = all(item["passed"] for item in results)
111+
return {
112+
"validator": "sourceos.svf-contracts.validator.v1",
113+
"passed": passed,
114+
"action_count": len(actions),
115+
"result_count": len(results),
116+
"results": results,
117+
}
118+
119+
120+
def main() -> int:
121+
validation = validate()
122+
print(json.dumps(validation, indent=2, sort_keys=True))
123+
if not validation["passed"]:
124+
print("FAIL: SourceOS SVF contracts", file=sys.stderr)
125+
return 1
126+
print("PASS: SourceOS SVF contracts")
127+
return 0
128+
129+
130+
if __name__ == "__main__":
131+
raise SystemExit(main())

0 commit comments

Comments
 (0)