"""Deterministic security policy primitives shared by every CASAN adapter. This module is deliberately stdlib-only. It never calls a model and never treats model text as an enforcement decision. The JSON action registry is the canonical machine-readable source; callers receive structured decisions rather than booleans. """ from __future__ import annotations import json import os import re import shutil import stat import subprocess from dataclasses import dataclass from datetime import datetime, timezone from pathlib import Path from typing import Any, Mapping, MutableMapping, Optional POLICY_VERSION = "1.0.0" RISK_ORDER = {"low": 0, "medium": 1, "high": 2, "critical": 3} PRODUCTION_PROFILES = {"prod", "production", "strict"} DEVELOPMENT_PROFILES = {"", "dev", "development", "local", "test", "testing"} FAILURE_POLICIES = {"halt", "quarantine", "require_approval", "record_only"} def _harness_root() -> Path: override = os.environ.get("CASAN_HARNESS_ROOT") return Path(override).resolve() if override else Path(__file__).resolve().parents[1] def _action_registry_path() -> Path: override = os.environ.get("CASAN_ACTION_CLASS_REGISTRY") return Path(override).resolve() if override else _harness_root() / "config" / "action-classes.json" def _load_action_registry() -> dict[str, Any]: with _action_registry_path().open(encoding="utf-8") as handle: payload = json.load(handle) if not isinstance(payload, dict) or not isinstance(payload.get("classes"), dict): raise ValueError("action class registry is malformed") return payload def _normal(value: object) -> str: return re.sub(r"[^a-z0-9]+", "_", str(value or "").strip().lower()).strip("_") def _higher(left: str, right: str) -> str: return right if RISK_ORDER.get(right, 3) > RISK_ORDER.get(left, 3) else left def _content_risk(text: str) -> str: value = (text or "").lower() if re.search(r"\b(drop\s+table|truncate|password|api[_-]?key|secret|credential|shutdown|rm\s+-rf)\b", value): return "high" if re.search(r"\b(deploy|release|migration|permission|policy|external\s+api|database)\b", value): return "medium" return "low" def _resource_risk(resource: str) -> str: value = (resource or "").lower() if re.search(r"(^|/)(\.env|\.ssh|\.aws)(/|$)|\.(pem|key|p12|pfx)$|credential|secret", value): return "high" if re.search(r"\b(prod|production|customer|tenant)\b", value): return "medium" return "low" def _environment_risk(environment: str) -> str: return "medium" if _normal(environment) in PRODUCTION_PROFILES else "low" def classify_action( action: str = "", tool: str = "", resource: str = "", command: str = "", ) -> dict[str, Any]: """Classify an action using deterministic aliases and command patterns. When several signals match, the class with the highest risk floor wins. An unknown side-effect-shaped tool is therefore never downgraded by benign text. """ registry = _load_action_registry() classes = registry["classes"] candidates: list[tuple[str, str]] = [] action_key = _normal(action) tool_key = _normal(tool) action_class = registry.get("action_aliases", {}).get(action_key) if action_class: candidates.append(("action", action_class)) tool_class = registry.get("tool_aliases", {}).get(tool_key) if tool_class: candidates.append(("tool", tool_class)) combined = " ".join(part for part in (command, resource) if part) for rule in registry.get("command_patterns", []): try: if re.search(str(rule["pattern"]), combined, re.IGNORECASE): candidates.append(("command_or_resource", str(rule["class"]))) except (KeyError, re.error, TypeError) as exc: raise ValueError("invalid action class pattern") from exc shell_like = tool_key in {"bash", "shell", "exec", "run", "run_command", "terminal"} if not candidates and shell_like: candidates.append(("unknown_shell_command", "unknown")) if not candidates: candidates.append(("unclassified", "unknown")) selected_source, selected_class = candidates[0] selected_risk = str(classes[selected_class]["risk_floor"]) for source, candidate in candidates[1:]: risk = str(classes[candidate]["risk_floor"]) if RISK_ORDER.get(risk, 3) > RISK_ORDER.get(selected_risk, 3): selected_source, selected_class, selected_risk = source, candidate, risk metadata = classes[selected_class] return { "schema_version": registry.get("schema_version", POLICY_VERSION), "action_class": selected_class, "classification_source": selected_source, "risk_floor": selected_risk, "side_effect_level": metadata["side_effect_level"], "side_effecting": metadata["side_effect_level"] != "none", "requires_approval": bool(metadata["requires_approval"]), "actor_required": bool(metadata["actor_required"]), "evidence_required": bool(metadata["evidence_required"]), "matched_classes": [candidate for _source, candidate in candidates], } def evaluate_risk( action: str = "", tool: str = "", resource: str = "", command: str = "", content: str = "", actor: str = "", environment: str = "", ) -> dict[str, Any]: classification = classify_action(action, tool, resource, command) actor_present = bool(str(actor or "").strip()) identity_risk = "high" if classification["actor_required"] and not actor_present else "low" factors = { "content_risk": _content_risk(content), "action_risk": classification["risk_floor"], "resource_risk": _resource_risk(resource), "identity_risk": identity_risk, "environment_risk": _environment_risk(environment), } effective = "low" for factor in factors.values(): effective = _higher(effective, factor) reason_codes: list[str] = ["action_risk_floor_applied"] if classification["actor_required"] and not actor_present: decision = "deny" reason_codes.append("actor_identity_required") elif classification["requires_approval"] or effective in {"high", "critical"}: decision = "require_approval" reason_codes.append("explicit_approval_required") else: decision = "allow" reason_codes.append("risk_within_auto_approval_policy") return { "schema_version": POLICY_VERSION, "policy_id": "casan.action-risk-floor", "policy_version": POLICY_VERSION, "decision": decision, "reason_codes": reason_codes, "effective_risk": effective, "risk_factors": factors, "actor_identity_present": actor_present, "evidence_requirement": "required" if classification["evidence_required"] else "standard", **classification, } def evaluate_registry_configuration( mode: str, profile: str, explicit_value: Optional[str], ) -> dict[str, Any]: normalized_mode = _normal(mode) normalized_profile = _normal(profile) explicit = None if explicit_value is None else _normal(explicit_value) in {"1", "true", "yes", "on", "enabled"} production = normalized_profile in PRODUCTION_PROFILES if explicit is False and production: return { "enabled": True, "valid": False, "unsafe_bypass": False, "severity": "critical", "reason_code": "h2_registry_bypass_forbidden", } if explicit is False: return { "enabled": False, "valid": normalized_profile in DEVELOPMENT_PROFILES, "unsafe_bypass": True, "severity": "high", "reason_code": "h2_registry_unsafe_development_bypass", } return { "enabled": True if explicit is None else explicit, "valid": True, "unsafe_bypass": False, "severity": "info", "reason_code": "h2_registry_default_enabled" if explicit is None and normalized_mode == "enforce" else "h2_registry_enabled", } def evaluate_failure_policy( *, side_effecting: bool, mode: str, profile: str, configured: Optional[str] = None, ) -> dict[str, Any]: """Select single-step failure handling without conflating telemetry success. Invalid configuration fails closed. Production side effects may be made stricter, but they may never be configured as record-only. """ normalized = _normal(configured or "") production = _normal(profile) in PRODUCTION_PROFILES enforce = _normal(mode) == "enforce" if normalized and normalized not in FAILURE_POLICIES: policy, source, valid = "halt", "invalid_policy_fail_closed", False elif normalized == "record_only" and side_effecting and production: policy, source, valid = "halt", "production_record_only_forbidden", False elif normalized: policy, source, valid = normalized, "configured", True elif side_effecting and (enforce or production): policy, source, valid = "halt", "safe_enforce_default", True else: policy, source, valid = "record_only", "observe_or_read_only_default", True return { "schema_version": POLICY_VERSION, "policy_id": "casan.single-step-failure", "policy_version": POLICY_VERSION, "failure_policy": policy, "source": source, "valid": valid, "side_effecting": side_effecting, "mode": _normal(mode) or "observe", "profile": _normal(profile) or "development", "execution_result": "failed", "telemetry_result": "recorded", "assurance_may_continue": policy == "record_only", "reason_codes": [source], } @dataclass(frozen=True) class H2GateContext: mode: str actor: str action: str tool: str execution_id: str enforcement_path: str idempotency_key: str timeout_seconds: float = 8.0 def _h2_result(context: H2GateContext, underlying: str, reason_code: str, detail: str) -> dict[str, Any]: enforce = _normal(context.mode) == "enforce" actual_deny = underlying == "deny" dependency_failure = underlying == "error" execution_allowed = not enforce or (not actual_deny and not dependency_failure) if enforce: decision = "allow" if execution_allowed else "deny" else: decision = "observe_only" return { "schema_version": POLICY_VERSION, "policy_id": "casan.h2.tool-registry", "policy_version": POLICY_VERSION, "decision": decision, "underlying_decision": underlying, "reason_codes": [reason_code], "reason_code": reason_code, "mode": _normal(context.mode) or "observe", "actor": context.actor or "unidentified", "action": context.action, "tool": context.tool, "execution_id": context.execution_id, "enforcement_path": context.enforcement_path, "execution_allowed": execution_allowed, "certifiable": enforce and execution_allowed, "assurance_status": "enforced" if enforce and execution_allowed else "denied" if enforce else "degraded", "severity": "high" if reason_code != "h2_ok" else "info", "detail": detail[:400], "timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"), } def evaluate_h2_gate( gate_path: str, context: H2GateContext, env: Optional[Mapping[str, str]] = None, bash_path: str = "bash", ) -> dict[str, Any]: """Execute the H2 dependency and convert every outcome into one policy decision.""" path = Path(gate_path) if not path.is_file(): return _h2_result(context, "error", "h2_gate_unavailable", "gate file is missing") try: mode_bits = stat.S_IMODE(path.stat().st_mode) except OSError as exc: return _h2_result(context, "error", "h2_gate_unavailable", f"gate stat failed: {type(exc).__name__}") if mode_bits & 0o444 == 0: return _h2_result(context, "error", "h2_gate_permission_denied", "gate file is not readable") resolved_bash = shutil.which(bash_path) if not os.path.isabs(bash_path) else bash_path if not resolved_bash or not Path(resolved_bash).is_file(): return _h2_result(context, "error", "h2_gate_unavailable", "bash runtime is unavailable") child_env: MutableMapping[str, str] = dict(os.environ) if env: child_env.update({str(key): str(value) for key, value in env.items()}) child_env["CASAN_IDEMPOTENCY_KEY"] = context.idempotency_key if context.actor and not child_env.get("CASAN_AGENT"): child_env["CASAN_AGENT"] = context.actor try: proc = subprocess.run( [resolved_bash, str(path), context.action], stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=max(0.001, context.timeout_seconds), env=child_env, check=False, text=True, ) except subprocess.TimeoutExpired: return _h2_result(context, "error", "h2_gate_timeout", "gate exceeded internal timeout") except (OSError, ValueError, subprocess.SubprocessError) as exc: return _h2_result(context, "error", "h2_gate_internal_error", type(exc).__name__) stdout = proc.stdout.strip()[:4096] stderr = proc.stderr.strip()[:4096] if proc.returncode == 0 and re.search(r"\bTOOL_APPROVED\b", stdout): return _h2_result(context, "allow", "h2_ok", stdout) if re.search(r"\bTOOL_DENIED\b", stdout + "\n" + stderr): reason_match = re.search(r"reason=([^\s]+)", stdout + "\n" + stderr) reason = "h2_denied:%s" % (reason_match.group(1) if reason_match else "policy") return _h2_result(context, "deny", reason, stderr or stdout) if proc.returncode in {126, 127}: return _h2_result(context, "error", "h2_gate_unavailable", stderr or "gate runtime unavailable") if proc.returncode != 0: return _h2_result(context, "error", "h2_gate_internal_error", stderr or stdout or f"exit={proc.returncode}") return _h2_result(context, "error", "h2_gate_malformed_response", stdout or "empty response") def evaluate_trust_capabilities(env: Optional[Mapping[str, str]] = None) -> dict[str, Any]: values = dict(os.environ if env is None else env) profile = _normal(values.get("CASAN_PROFILE", "")) production = profile in PRODUCTION_PROFILES signing_provider = _normal(values.get("CASAN_SIGNING_PROVIDER", "local_openssl")) anchor_provider = _normal(values.get("CASAN_IMMUTABLE_ANCHOR_PROVIDER", "local_hash_chain")) emergency = _normal(values.get("CASAN_TRUST_EMERGENCY_OVERRIDE", "")) in {"1", "true", "yes", "on"} signing_external = signing_provider in {"vault", "vault_kms", "kms", "hsm"} anchor_external = anchor_provider in {"s3_object_lock", "qldb", "external_worm"} vault_addr = str(values.get("VAULT_ADDR") or "") signing_configured = signing_external and vault_addr.startswith("https://") and bool(values.get("VAULT_TOKEN")) anchor_configured = anchor_external and bool(values.get("CASAN_S3_BUCKET")) and bool(values.get("CASAN_S3_KMS_KEY_ID")) ready = (not production) or (signing_configured and anchor_configured) certifiable = ready and not emergency reasons: list[str] = [] if production and not signing_configured: reasons.append("external_signing_trust_root_required") if production and not anchor_configured: reasons.append("external_immutable_anchor_required") if emergency: reasons.append("emergency_local_trust_override_active") return { "schema_version": POLICY_VERSION, "profile": profile or "development", "production": production, "signing_provider": signing_provider or "local_openssl", "signing_capability": "external" if signing_configured else "local_or_unavailable", "immutable_anchor_provider": anchor_provider or "local_hash_chain", "immutable_anchor_capability": "external" if anchor_configured else "local_or_unavailable", "ready": ready, "certifiable": certifiable, "emergency_override": emergency, "severity": "critical" if emergency or not ready else "info", "reason_codes": reasons or ["trust_capabilities_satisfied"], }