2026-08-23 12:27:13 +02:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""Validate security-zone exception records at an explicit instant."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
|
import json
|
|
|
|
|
import sys
|
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
from typing import Any, Mapping
|
|
|
|
|
|
|
|
|
|
import yaml
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
POLICY_STANDARD = "security-zone-exception-policy_v0.1"
|
|
|
|
|
RECORD_STANDARD = "security-zone-exceptions_v0.1"
|
2026-08-23 12:35:01 +02:00
|
|
|
ZONE_IDS = frozenset(
|
|
|
|
|
{
|
|
|
|
|
"z0-experimental",
|
|
|
|
|
"z1-operational",
|
|
|
|
|
"z2-protected",
|
|
|
|
|
"z2-continuity",
|
|
|
|
|
"z3-critical",
|
|
|
|
|
}
|
|
|
|
|
)
|
2026-08-23 12:27:13 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class ExceptionConformanceError(ValueError):
|
|
|
|
|
"""The exception input or policy is structurally unusable."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _required(mapping: Mapping[str, Any], key: str, where: str) -> Any:
|
|
|
|
|
value = mapping.get(key)
|
|
|
|
|
if value is None or value == "" or value == []:
|
|
|
|
|
raise ExceptionConformanceError(f"{where}.{key} is required")
|
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _instant(value: Any, where: str) -> datetime:
|
|
|
|
|
text = str(value)
|
|
|
|
|
if text.endswith("Z"):
|
|
|
|
|
text = text[:-1] + "+00:00"
|
|
|
|
|
try:
|
|
|
|
|
parsed = datetime.fromisoformat(text)
|
|
|
|
|
except ValueError as exc:
|
|
|
|
|
raise ExceptionConformanceError(f"{where} must be an ISO timestamp") from exc
|
|
|
|
|
if parsed.tzinfo is None:
|
|
|
|
|
raise ExceptionConformanceError(f"{where} must include a timezone")
|
|
|
|
|
return parsed.astimezone(timezone.utc)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _policy(policy: Any) -> dict[str, Any]:
|
|
|
|
|
if not isinstance(policy, Mapping):
|
|
|
|
|
raise ExceptionConformanceError("exception policy must be a mapping")
|
|
|
|
|
if policy.get("standard") != POLICY_STANDARD:
|
|
|
|
|
raise ExceptionConformanceError(
|
|
|
|
|
f"exception policy standard must be {POLICY_STANDARD}"
|
|
|
|
|
)
|
|
|
|
|
policy_id = str(_required(policy, "policy_id", "policy"))
|
|
|
|
|
version = str(_required(policy, "version", "policy"))
|
|
|
|
|
controls = _required(policy, "controls", "policy")
|
|
|
|
|
if not isinstance(controls, Mapping) or not controls:
|
|
|
|
|
raise ExceptionConformanceError("policy.controls must be a non-empty mapping")
|
|
|
|
|
normalized: dict[str, Any] = {}
|
|
|
|
|
for control_id, control in controls.items():
|
|
|
|
|
where = f"policy.controls.{control_id}"
|
|
|
|
|
if not isinstance(control, Mapping):
|
|
|
|
|
raise ExceptionConformanceError(f"{where} must be a mapping")
|
|
|
|
|
if "/" not in str(control_id):
|
|
|
|
|
raise ExceptionConformanceError(f"{where} id must be owner-qualified")
|
|
|
|
|
authorities = _required(control, "grant_authorities", where)
|
|
|
|
|
if not isinstance(authorities, list) or not all(
|
|
|
|
|
isinstance(authority, str) and authority for authority in authorities
|
|
|
|
|
):
|
|
|
|
|
raise ExceptionConformanceError(
|
|
|
|
|
f"{where}.grant_authorities must be a non-empty string list"
|
|
|
|
|
)
|
|
|
|
|
maximum = _required(control, "maximum_duration_seconds", where)
|
|
|
|
|
if not isinstance(maximum, int) or maximum <= 0:
|
|
|
|
|
raise ExceptionConformanceError(
|
|
|
|
|
f"{where}.maximum_duration_seconds must be a positive integer"
|
|
|
|
|
)
|
|
|
|
|
normalized[str(control_id)] = {
|
|
|
|
|
"grant_authorities": set(authorities),
|
|
|
|
|
"maximum_duration_seconds": maximum,
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
"policy_id": policy_id,
|
|
|
|
|
"version": version,
|
|
|
|
|
"policy_ref": f"{policy_id}@{version}",
|
|
|
|
|
"controls": normalized,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _validate_relaxation(record: Mapping[str, Any], errors: list[str]) -> None:
|
|
|
|
|
base = record.get("base")
|
|
|
|
|
relaxation = record.get("relaxation")
|
|
|
|
|
if not isinstance(base, Mapping) or not isinstance(relaxation, Mapping):
|
|
|
|
|
errors.append("base and relaxation must be mappings")
|
|
|
|
|
return
|
|
|
|
|
changed = False
|
|
|
|
|
if "stance" in relaxation:
|
|
|
|
|
base_stance = base.get("stance")
|
|
|
|
|
relaxed_stance = relaxation.get("stance")
|
|
|
|
|
allowed = {
|
|
|
|
|
"enforced": {"advisory", "exempt"},
|
|
|
|
|
"advisory": {"exempt"},
|
|
|
|
|
}
|
2026-08-23 12:35:01 +02:00
|
|
|
if relaxed_stance not in allowed.get(str(base_stance), set()):
|
2026-08-23 12:27:13 +02:00
|
|
|
errors.append("relaxation.stance must strictly relax the base stance")
|
|
|
|
|
else:
|
|
|
|
|
changed = True
|
|
|
|
|
if "failure_mode" in relaxation:
|
|
|
|
|
if base.get("failure_mode") != "fail_closed" or relaxation.get(
|
|
|
|
|
"failure_mode"
|
|
|
|
|
) != "fail_open":
|
|
|
|
|
errors.append(
|
|
|
|
|
"failure-mode relaxation must change fail_closed to fail_open"
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
changed = True
|
|
|
|
|
if not changed and not errors:
|
|
|
|
|
errors.append("relaxation must change stance or failure_mode")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _record_result(
|
|
|
|
|
record: Any,
|
|
|
|
|
policy: Mapping[str, Any],
|
|
|
|
|
at: datetime,
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
errors: list[str] = []
|
|
|
|
|
if not isinstance(record, Mapping):
|
|
|
|
|
return {
|
|
|
|
|
"exception_id": None,
|
|
|
|
|
"valid": False,
|
|
|
|
|
"active": False,
|
|
|
|
|
"state": "invalid",
|
|
|
|
|
"errors": ["exception record must be a mapping"],
|
|
|
|
|
}
|
|
|
|
|
exception_id = record.get("exception_id")
|
|
|
|
|
for key in (
|
|
|
|
|
"exception_id",
|
|
|
|
|
"security_zone",
|
|
|
|
|
"control",
|
|
|
|
|
"workloads",
|
|
|
|
|
"base",
|
|
|
|
|
"relaxation",
|
|
|
|
|
"justification",
|
|
|
|
|
"requested_by",
|
|
|
|
|
"granted_by",
|
|
|
|
|
"issued_at",
|
|
|
|
|
"not_before",
|
|
|
|
|
"not_after",
|
|
|
|
|
"maximum_duration_policy",
|
|
|
|
|
"change_ref",
|
|
|
|
|
):
|
|
|
|
|
value = record.get(key)
|
|
|
|
|
if value is None or value == "" or value == () or value == []:
|
|
|
|
|
errors.append(f"{key} is required")
|
|
|
|
|
zone = record.get("security_zone")
|
2026-08-23 12:35:01 +02:00
|
|
|
if zone not in ZONE_IDS:
|
2026-08-23 12:27:13 +02:00
|
|
|
errors.append("security_zone must be a named zone")
|
|
|
|
|
control_id = record.get("control")
|
2026-08-23 12:35:01 +02:00
|
|
|
control_policy = (
|
|
|
|
|
policy["controls"].get(str(control_id)) if control_id is not None else None
|
|
|
|
|
)
|
2026-08-23 12:27:13 +02:00
|
|
|
if control_policy is None:
|
|
|
|
|
errors.append("control is absent from the owner exception policy")
|
|
|
|
|
workloads = record.get("workloads")
|
|
|
|
|
if not isinstance(workloads, list) or not workloads:
|
|
|
|
|
errors.append("workloads must be a non-empty list")
|
|
|
|
|
workloads = []
|
|
|
|
|
elif any(
|
|
|
|
|
not isinstance(workload, str)
|
|
|
|
|
or not workload
|
|
|
|
|
or workload in {"*", "unknown"}
|
|
|
|
|
for workload in workloads
|
|
|
|
|
):
|
|
|
|
|
errors.append("workloads must contain exact resolved workload ids")
|
|
|
|
|
elif len(set(workloads)) != len(workloads):
|
|
|
|
|
errors.append("workloads must not contain duplicates")
|
|
|
|
|
_validate_relaxation(record, errors)
|
|
|
|
|
|
|
|
|
|
issued = before = after = None
|
|
|
|
|
for key in ("issued_at", "not_before", "not_after"):
|
|
|
|
|
try:
|
|
|
|
|
parsed = _instant(record.get(key), key)
|
|
|
|
|
if key == "issued_at":
|
|
|
|
|
issued = parsed
|
|
|
|
|
elif key == "not_before":
|
|
|
|
|
before = parsed
|
|
|
|
|
else:
|
|
|
|
|
after = parsed
|
|
|
|
|
except ExceptionConformanceError as exc:
|
|
|
|
|
errors.append(str(exc))
|
|
|
|
|
if issued and before and after:
|
|
|
|
|
if issued > before:
|
|
|
|
|
errors.append("issued_at must be at or before not_before")
|
|
|
|
|
if before >= after:
|
|
|
|
|
errors.append("not_before must be before not_after")
|
|
|
|
|
if control_policy and (after - before).total_seconds() > control_policy[
|
|
|
|
|
"maximum_duration_seconds"
|
|
|
|
|
]:
|
|
|
|
|
errors.append("exception duration exceeds owner maximum")
|
|
|
|
|
if control_policy and record.get("granted_by") not in control_policy[
|
|
|
|
|
"grant_authorities"
|
|
|
|
|
]:
|
|
|
|
|
errors.append("granted_by is not a designated control authority")
|
|
|
|
|
if record.get("maximum_duration_policy") != policy["policy_ref"]:
|
|
|
|
|
errors.append("maximum_duration_policy does not match evaluated owner policy")
|
|
|
|
|
|
|
|
|
|
durable = record.get("durable_authorities") or []
|
|
|
|
|
if not isinstance(durable, list):
|
|
|
|
|
errors.append("durable_authorities must be a list")
|
|
|
|
|
else:
|
|
|
|
|
for index, authority in enumerate(durable):
|
|
|
|
|
if not isinstance(authority, Mapping) or not authority.get("id"):
|
|
|
|
|
errors.append(f"durable_authorities[{index}] requires id and not_after")
|
|
|
|
|
continue
|
|
|
|
|
try:
|
|
|
|
|
authority_after = _instant(
|
|
|
|
|
authority.get("not_after"),
|
|
|
|
|
f"durable_authorities[{index}].not_after",
|
|
|
|
|
)
|
|
|
|
|
except ExceptionConformanceError as exc:
|
|
|
|
|
errors.append(str(exc))
|
|
|
|
|
continue
|
|
|
|
|
if after and authority_after > after:
|
|
|
|
|
errors.append(
|
|
|
|
|
f"durable_authorities[{index}] outlives the exception"
|
|
|
|
|
)
|
|
|
|
|
valid = not errors
|
|
|
|
|
active = bool(valid and before and after and before <= at < after)
|
2026-08-23 12:35:01 +02:00
|
|
|
if not valid or before is None or after is None:
|
2026-08-23 12:27:13 +02:00
|
|
|
state = "invalid"
|
|
|
|
|
elif at < before:
|
|
|
|
|
state = "future"
|
|
|
|
|
elif at >= after:
|
|
|
|
|
state = "expired"
|
|
|
|
|
else:
|
|
|
|
|
state = "active"
|
|
|
|
|
return {
|
|
|
|
|
"exception_id": exception_id,
|
|
|
|
|
"control": control_id,
|
|
|
|
|
"workloads": sorted(workloads),
|
|
|
|
|
"not_before": before.isoformat() if before else None,
|
|
|
|
|
"not_after": after.isoformat() if after else None,
|
|
|
|
|
"valid": valid,
|
|
|
|
|
"active": active,
|
|
|
|
|
"state": state,
|
|
|
|
|
"errors": errors,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _overlap(left: Mapping[str, Any], right: Mapping[str, Any]) -> bool:
|
|
|
|
|
if left.get("control") != right.get("control"):
|
|
|
|
|
return False
|
|
|
|
|
if not set(left.get("workloads", [])).intersection(right.get("workloads", [])):
|
|
|
|
|
return False
|
|
|
|
|
if not all((left.get("not_before"), left.get("not_after"), right.get("not_before"), right.get("not_after"))):
|
|
|
|
|
return False
|
|
|
|
|
left_before = _instant(left["not_before"], "left.not_before")
|
|
|
|
|
left_after = _instant(left["not_after"], "left.not_after")
|
|
|
|
|
right_before = _instant(right["not_before"], "right.not_before")
|
|
|
|
|
right_after = _instant(right["not_after"], "right.not_after")
|
|
|
|
|
return max(left_before, right_before) < min(left_after, right_after)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def evaluate_exceptions(
|
|
|
|
|
document: Any,
|
|
|
|
|
policy_document: Any,
|
|
|
|
|
*,
|
|
|
|
|
at: datetime,
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
policy = _policy(policy_document)
|
|
|
|
|
if not isinstance(document, Mapping) or document.get("standard") != RECORD_STANDARD:
|
|
|
|
|
raise ExceptionConformanceError(
|
|
|
|
|
f"exception document standard must be {RECORD_STANDARD}"
|
|
|
|
|
)
|
|
|
|
|
records = document.get("exceptions")
|
|
|
|
|
if not isinstance(records, list):
|
|
|
|
|
raise ExceptionConformanceError("exceptions must be a list")
|
|
|
|
|
results = [_record_result(record, policy, at) for record in records]
|
|
|
|
|
ids: dict[str, list[int]] = {}
|
|
|
|
|
for index, result in enumerate(results):
|
|
|
|
|
if result["exception_id"]:
|
|
|
|
|
ids.setdefault(str(result["exception_id"]), []).append(index)
|
|
|
|
|
for exception_id, indexes in ids.items():
|
|
|
|
|
if len(indexes) > 1:
|
|
|
|
|
for index in indexes:
|
|
|
|
|
results[index]["errors"].append(
|
|
|
|
|
f"duplicate exception_id {exception_id}"
|
|
|
|
|
)
|
|
|
|
|
results[index].update(valid=False, active=False, state="invalid")
|
|
|
|
|
source_by_id = {
|
|
|
|
|
str(record.get("exception_id")): record
|
|
|
|
|
for record in records
|
|
|
|
|
if isinstance(record, Mapping) and record.get("exception_id")
|
|
|
|
|
}
|
|
|
|
|
for index, record in enumerate(records):
|
|
|
|
|
if not isinstance(record, Mapping) or not record.get("renews"):
|
|
|
|
|
continue
|
|
|
|
|
renewed = str(record["renews"])
|
|
|
|
|
if renewed == str(record.get("exception_id")) or renewed not in source_by_id:
|
|
|
|
|
results[index]["errors"].append(
|
|
|
|
|
"renews must name a different existing exception id"
|
|
|
|
|
)
|
|
|
|
|
results[index].update(valid=False, active=False, state="invalid")
|
|
|
|
|
for left in range(len(records)):
|
|
|
|
|
for right in range(left + 1, len(records)):
|
|
|
|
|
if not results[left]["valid"] or not results[right]["valid"]:
|
|
|
|
|
continue
|
|
|
|
|
try:
|
|
|
|
|
overlapping = _overlap(records[left], records[right])
|
|
|
|
|
except ExceptionConformanceError:
|
|
|
|
|
overlapping = False
|
|
|
|
|
if overlapping:
|
|
|
|
|
for index in (left, right):
|
|
|
|
|
results[index]["errors"].append(
|
|
|
|
|
f"overlaps exception {results[right if index == left else left]['exception_id']}"
|
|
|
|
|
)
|
|
|
|
|
results[index].update(valid=False, active=False, state="invalid")
|
|
|
|
|
return {
|
|
|
|
|
"ok": all(result["valid"] for result in results),
|
|
|
|
|
"standard": RECORD_STANDARD,
|
|
|
|
|
"evaluated_at": at.astimezone(timezone.utc).isoformat(),
|
|
|
|
|
"policy": {"id": policy["policy_id"], "version": policy["version"]},
|
|
|
|
|
"results": sorted(results, key=lambda result: str(result["exception_id"])),
|
|
|
|
|
"active_exception_ids": sorted(
|
|
|
|
|
str(result["exception_id"])
|
|
|
|
|
for result in results
|
|
|
|
|
if result["active"]
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _load(path: Path) -> dict[str, Any]:
|
|
|
|
|
value = yaml.safe_load(path.read_text()) or {}
|
|
|
|
|
if not isinstance(value, dict):
|
|
|
|
|
raise ExceptionConformanceError(f"{path} must contain a mapping")
|
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main() -> int:
|
|
|
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
|
|
|
parser.add_argument("exceptions", type=Path)
|
|
|
|
|
parser.add_argument("--policy", required=True, type=Path)
|
|
|
|
|
parser.add_argument("--at", required=True)
|
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
try:
|
|
|
|
|
result = evaluate_exceptions(
|
|
|
|
|
_load(args.exceptions),
|
|
|
|
|
_load(args.policy),
|
|
|
|
|
at=_instant(args.at, "--at"),
|
|
|
|
|
)
|
|
|
|
|
except (OSError, yaml.YAMLError, ExceptionConformanceError) as exc:
|
|
|
|
|
result = {"ok": False, "errors": [str(exc)], "results": []}
|
|
|
|
|
print(json.dumps(result, indent=2, sort_keys=True))
|
|
|
|
|
return 0 if result["ok"] else 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
sys.exit(main())
|