feat(casan): establish assurance kernel and harden control plane
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
"""Canonical CASAN wire-contract builders and lightweight validation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Mapping
|
||||
|
||||
|
||||
CONTRACT_VERSION = "1.0.0"
|
||||
|
||||
|
||||
def utc_now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
||||
|
||||
|
||||
def actor(actor_id: str, actor_type: str, issuer: str, trust_level: str, auth_evidence_ref: str | None = None) -> dict[str, Any]:
|
||||
return {
|
||||
"actor_id": actor_id,
|
||||
"actor_type": actor_type,
|
||||
"issuer": issuer,
|
||||
"trust_level": trust_level,
|
||||
"authentication_evidence_ref": auth_evidence_ref,
|
||||
}
|
||||
|
||||
|
||||
def outcome(execution: str, assurance: str, certification: str, business: str = "not_evaluated") -> dict[str, str]:
|
||||
return {
|
||||
"execution_result": execution,
|
||||
"assurance_result": assurance,
|
||||
"certification_result": certification,
|
||||
"business_result": business,
|
||||
}
|
||||
|
||||
|
||||
def evidence_digest(evidence: Mapping[str, Any]) -> str:
|
||||
"""Digest the claim-bearing evidence metadata, excluding its digest field."""
|
||||
|
||||
material = {
|
||||
key: evidence.get(key)
|
||||
for key in (
|
||||
"evidence_id", "claim_refs", "producer_identity", "timestamp",
|
||||
"artifact_ref", "validation_status", "retention_class", "category",
|
||||
)
|
||||
}
|
||||
payload = json.dumps(material, sort_keys=True, separators=(",", ":"), ensure_ascii=True)
|
||||
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def verify_evidence_integrity(evidence: Mapping[str, Any]) -> bool:
|
||||
integrity = evidence.get("integrity")
|
||||
if not isinstance(integrity, Mapping) or integrity.get("algorithm") != "sha256":
|
||||
return False
|
||||
digest = integrity.get("digest")
|
||||
return isinstance(digest, str) and digest == evidence_digest(evidence)
|
||||
|
||||
|
||||
def validate_bundle(bundle: Mapping[str, Any]) -> list[str]:
|
||||
"""Return deterministic contract errors; an empty list means valid.
|
||||
|
||||
JSON Schema is canonical. This small validator keeps runtime adapters stdlib-
|
||||
only and catches the invariants needed at enforcement boundaries.
|
||||
"""
|
||||
|
||||
errors: list[str] = []
|
||||
if bundle.get("schema_version") != CONTRACT_VERSION:
|
||||
errors.append("unsupported_schema_version")
|
||||
run = bundle.get("run")
|
||||
if not isinstance(run, Mapping):
|
||||
errors.append("missing_execution_run")
|
||||
return errors
|
||||
for key in ("run_id", "runtime", "mode", "status", "correlation_id", "outcome"):
|
||||
if not run.get(key):
|
||||
errors.append(f"run.{key}_required")
|
||||
actors = bundle.get("actors")
|
||||
actor_ids: set[object] = set()
|
||||
if not isinstance(actors, list) or not actors:
|
||||
errors.append("actors_required")
|
||||
else:
|
||||
actor_ids = {item.get("actor_id") for item in actors if isinstance(item, Mapping)}
|
||||
if "" in actor_ids or None in actor_ids:
|
||||
errors.append("actor_identity_required")
|
||||
steps = bundle.get("steps")
|
||||
if not isinstance(steps, list):
|
||||
errors.append("steps_required")
|
||||
else:
|
||||
for index, step in enumerate(steps):
|
||||
if not isinstance(step, Mapping):
|
||||
errors.append(f"steps.{index}_invalid")
|
||||
continue
|
||||
for key in ("step_id", "run_id", "sequence", "actor_ref", "action", "outcome"):
|
||||
if step.get(key) is None:
|
||||
errors.append(f"steps.{index}.{key}_required")
|
||||
if step.get("actor_ref") not in actor_ids:
|
||||
errors.append(f"steps.{index}.actor_ref_unknown")
|
||||
|
||||
evidence = bundle.get("evidence")
|
||||
evidence_ids: set[object] = set()
|
||||
if not isinstance(evidence, list):
|
||||
errors.append("evidence_required")
|
||||
else:
|
||||
for index, item in enumerate(evidence):
|
||||
if not isinstance(item, Mapping):
|
||||
errors.append(f"evidence.{index}_invalid")
|
||||
continue
|
||||
evidence_ids.add(item.get("evidence_id"))
|
||||
if not verify_evidence_integrity(item):
|
||||
errors.append(f"evidence.{index}.integrity_invalid")
|
||||
|
||||
claims = bundle.get("claims")
|
||||
claim_ids = {
|
||||
item.get("claim_id") for item in claims or [] if isinstance(item, Mapping)
|
||||
} if isinstance(claims, list) else set()
|
||||
if isinstance(claims, list):
|
||||
for index, claim in enumerate(claims):
|
||||
if not isinstance(claim, Mapping):
|
||||
errors.append(f"claims.{index}_invalid")
|
||||
continue
|
||||
for evidence_ref in claim.get("evidence_refs") or []:
|
||||
if evidence_ref not in evidence_ids:
|
||||
errors.append(f"claims.{index}.evidence_ref_unknown")
|
||||
if isinstance(evidence, list):
|
||||
for index, item in enumerate(evidence):
|
||||
if isinstance(item, Mapping):
|
||||
for claim_ref in item.get("claim_refs") or []:
|
||||
if claim_ref not in claim_ids:
|
||||
errors.append(f"evidence.{index}.claim_ref_unknown")
|
||||
|
||||
delegations = bundle.get("delegations")
|
||||
if delegations is not None:
|
||||
errors.extend(_validate_delegations(delegations, actor_ids))
|
||||
if run.get("mode") == "observe" and isinstance(run.get("outcome"), Mapping):
|
||||
if run["outcome"].get("certification_result") == "certified":
|
||||
errors.append("observed_only_cannot_be_certified")
|
||||
return errors
|
||||
|
||||
|
||||
def _validate_delegations(delegations: object, actor_ids: set[object]) -> list[str]:
|
||||
if not isinstance(delegations, list):
|
||||
return ["delegations_invalid"]
|
||||
errors: list[str] = []
|
||||
records = {
|
||||
item.get("delegation_id"): item
|
||||
for item in delegations if isinstance(item, Mapping) and item.get("delegation_id")
|
||||
}
|
||||
for index, item in enumerate(delegations):
|
||||
if not isinstance(item, Mapping):
|
||||
errors.append(f"delegations.{index}_invalid")
|
||||
continue
|
||||
for actor_key in ("delegator_ref", "delegate_ref"):
|
||||
if item.get(actor_key) not in actor_ids:
|
||||
errors.append(f"delegations.{index}.{actor_key}_unknown")
|
||||
current: Mapping[str, Any] = item
|
||||
visited: set[object] = set()
|
||||
depth = 0
|
||||
while current.get("parent_delegation_ref"):
|
||||
parent_ref = current.get("parent_delegation_ref")
|
||||
if parent_ref in visited:
|
||||
errors.append(f"delegations.{index}.cycle")
|
||||
break
|
||||
visited.add(parent_ref)
|
||||
parent = records.get(parent_ref)
|
||||
if not isinstance(parent, Mapping):
|
||||
errors.append(f"delegations.{index}.parent_unknown")
|
||||
break
|
||||
depth += 1
|
||||
current = parent
|
||||
maximum_depth = item.get("maximum_depth")
|
||||
if isinstance(maximum_depth, int) and depth > maximum_depth:
|
||||
errors.append(f"delegations.{index}.maximum_depth_exceeded")
|
||||
return errors
|
||||
Reference in New Issue
Block a user