railiance-infra/scripts/baseline_contract.py
codex b93af8cc78
Some checks failed
CI Smoke / source-contract (push) Failing after 2s
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Implement reproducible S1 handoff contracts
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a02994-7685-7940-bf34-3555b8256018
2026-08-23 12:02:23 +02:00

240 lines
9 KiB
Python

#!/usr/bin/env python3
"""Validate and resolve the executable S1 host-baseline contract."""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
from typing import Any
import yaml
SCHEMA_VERSION = "2.0"
PROFILE_MODES = {"ufw", "external"}
REQUIRED_PACKAGES = {"curl", "git", "vim", "htop", "ufw", "fail2ban"}
REQUIRED_SERVICES = {"fail2ban", "ssh.socket"}
REQUIRED_SSH = {
"PasswordAuthentication": "no",
"PermitRootLogin": "no",
"PubkeyAuthentication": "yes",
"ChallengeResponseAuthentication": "no",
}
CONSUMER_MARKERS = {
"ansible/roles/base/tasks/main.yml": [
"baseline_required_packages",
"baseline_ssh_directives",
"baseline_user",
"baseline_security",
"baseline_firewall",
],
"goss/baseline.yaml.j2": [
"baseline_required_packages",
"baseline_required_services",
"baseline_ssh_directives",
"baseline_user",
"baseline_security",
"baseline_firewall",
],
}
class BaselineError(ValueError):
"""The baseline model or its repository consumers are inconsistent."""
def _strings(
value: Any, label: str, errors: list[str], *, allow_empty: bool = False
) -> list[str]:
if not isinstance(value, list) or (not value and not allow_empty) or not all(
isinstance(item, str) and item.strip() for item in value
):
errors.append(f"{label} must be a non-empty string list")
return []
if len(set(value)) != len(value):
errors.append(f"{label} contains duplicates")
return value
def validate_spec(payload: Any) -> dict[str, Any]:
if not isinstance(payload, dict):
raise BaselineError("baseline must be a YAML object")
errors: list[str] = []
if str(payload.get("version")) != SCHEMA_VERSION:
errors.append(f"version must be {SCHEMA_VERSION!r}")
defaults = payload.get("defaults")
profiles = payload.get("profiles")
if not isinstance(defaults, dict):
errors.append("defaults must be an object")
defaults = {}
if not isinstance(profiles, dict) or not profiles:
errors.append("profiles must be a non-empty object")
profiles = {}
packages = _strings(defaults.get("packages"), "defaults.packages", errors)
services = _strings(defaults.get("services"), "defaults.services", errors)
missing_packages = sorted(REQUIRED_PACKAGES - set(packages))
missing_services = sorted(REQUIRED_SERVICES - set(services))
if missing_packages:
errors.append(f"defaults.packages omits governed packages {', '.join(missing_packages)}")
if missing_services:
errors.append(f"defaults.services omits governed services {', '.join(missing_services)}")
ssh = defaults.get("ssh_directives")
if not isinstance(ssh, dict) or not ssh or not all(
isinstance(key, str) and isinstance(value, str) for key, value in (ssh or {}).items()
):
errors.append("defaults.ssh_directives must be a non-empty string map")
elif {key: ssh.get(key) for key in REQUIRED_SSH} != REQUIRED_SSH:
errors.append("defaults.ssh_directives weakens a governed SSH directive")
user = defaults.get("user")
if not isinstance(user, dict):
errors.append("defaults.user must be an object")
else:
for key in ("name", "shell", "sudo"):
if not isinstance(user.get(key), str) or not user[key]:
errors.append(f"defaults.user.{key} must be a non-empty string")
if user.get("sudo") != "NOPASSWD":
errors.append("defaults.user.sudo must be NOPASSWD")
security = defaults.get("security")
if not isinstance(security, dict):
errors.append("defaults.security must be an object")
else:
if not isinstance(security.get("histcontrol"), str):
errors.append("defaults.security.histcontrol must be a string")
_strings(
security.get("fail2ban_jails"),
"defaults.security.fail2ban_jails",
errors,
)
for name, profile in profiles.items():
label = f"profiles.{name}"
if not isinstance(profile, dict):
errors.append(f"{label} must be an object")
continue
_strings(
profile.get("services", []),
f"{label}.services",
errors,
allow_empty=True,
)
firewall = profile.get("firewall")
if not isinstance(firewall, dict):
errors.append(f"{label}.firewall must be an object")
continue
mode = firewall.get("mode")
if mode not in PROFILE_MODES:
errors.append(f"{label}.firewall.mode must be one of {sorted(PROFILE_MODES)}")
expected_managed = mode == "ufw"
if firewall.get("managed") is not expected_managed:
errors.append(f"{label}.firewall.managed must be {expected_managed}")
verification = firewall.get("verification")
if not isinstance(verification, dict):
errors.append(f"{label}.firewall.verification must be an object")
else:
if not isinstance(verification.get("command"), str):
errors.append(f"{label}.firewall.verification.command must be a string")
_strings(
verification.get("stdout"),
f"{label}.firewall.verification.stdout",
errors,
)
if mode == "external":
replacement = firewall.get("replacement_control")
if not isinstance(replacement, dict):
errors.append(f"{label}.firewall.replacement_control must be an object")
else:
for key in ("description", "owner", "removal_condition"):
if not isinstance(replacement.get(key), str) or not replacement[key]:
errors.append(
f"{label}.firewall.replacement_control.{key} must be a string"
)
for required_profile, required_mode in {
"ufw-managed": "ufw",
"external-firewall": "external",
}.items():
if profiles.get(required_profile, {}).get("firewall", {}).get("mode") != required_mode:
errors.append(
f"profiles.{required_profile} must declare firewall mode {required_mode}"
)
if errors:
raise BaselineError("baseline contract failed:\n- " + "\n- ".join(errors))
return payload
def load_spec(path: Path) -> dict[str, Any]:
try:
payload = yaml.safe_load(path.read_text(encoding="utf-8"))
except (OSError, yaml.YAMLError) as exc:
raise BaselineError(f"cannot read {path}: {exc}") from exc
return validate_spec(payload)
def profile_hostvars(payload: dict[str, Any], profile_name: str) -> dict[str, Any]:
validate_spec(payload)
try:
profile = payload["profiles"][profile_name]
except KeyError as exc:
raise BaselineError(f"unknown baseline profile {profile_name!r}") from exc
defaults = payload["defaults"]
services = list(dict.fromkeys(defaults["services"] + profile["services"]))
return {
"baseline_profile": profile_name,
"baseline_required_packages": defaults["packages"],
"baseline_required_services": services,
"baseline_ssh_directives": defaults["ssh_directives"],
"baseline_user": defaults["user"],
"baseline_security": defaults["security"],
"baseline_firewall": profile["firewall"],
"ufw_manage": profile["firewall"]["managed"],
}
def validate_repo_consumers(root: Path) -> None:
errors: list[str] = []
for relative, markers in CONSUMER_MARKERS.items():
path = root / relative
try:
text = path.read_text(encoding="utf-8")
except OSError as exc:
errors.append(f"cannot read {relative}: {exc}")
continue
missing = [marker for marker in markers if marker not in text]
if missing:
errors.append(f"{relative} does not consume {', '.join(missing)}")
if errors:
raise BaselineError("baseline consumer parity failed:\n- " + "\n- ".join(errors))
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"spec", nargs="?", type=Path, default=Path("spec/server-baseline.yaml")
)
parser.add_argument("--profile", help="resolve one profile as Ansible hostvars")
parser.add_argument("--check-repo", action="store_true")
args = parser.parse_args()
try:
payload = load_spec(args.spec)
if args.check_repo:
validate_repo_consumers(Path(__file__).resolve().parents[1])
result: dict[str, Any] = {
"ok": True,
"version": payload["version"],
"profiles": sorted(payload["profiles"]),
}
if args.profile:
result["hostvars"] = profile_hostvars(payload, args.profile)
except BaselineError as exc:
print(exc, file=sys.stderr)
return 1
print(json.dumps(result, sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())