#!/usr/bin/env bash # ============================================================================ # WAF + PLAN INVARIANTS CHECKER # ============================================================================ # Enforces security+plan gating invariants for VaultMesh Cloudflare IaC. # Run from repo root: bash scripts/waf-and-plan-invariants.sh # # Exit codes: # 0 = All invariants pass # 1 = One or more invariants violated # # Governed by: RED-BOOK.md # ============================================================================ set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$REPO_ROOT" # Colors RED='\033[0;31m' GREEN='\033[0;32m' NC='\033[0m' echo "============================================" echo " VaultMesh WAF + Plan Invariants Check" echo "============================================" echo "" FAILED=0 echo "── 0. Toolchain Versions ──" terraform version || true python3 --version || true python3 -m pip --version || true python3 -m pytest --version || true python3 -m mcp.waf_intelligence --version || true echo "" echo "── 1. WAF Intel Analyzer Regression ──" if python3 -m pytest -q tests/test_waf_intelligence_analyzer.py; then echo -e "${GREEN}✓${NC} 1.1 Analyzer regression test passed" else echo -e "${RED}✗${NC} 1.1 Analyzer regression test failed" FAILED=1 fi echo "" echo "── 2. WAF Intel CLI Contract ──" TMP_DIR="${TMPDIR:-/tmp}" WAF_JSON_FILE="$(mktemp -p "$TMP_DIR" waf-intel.XXXXXX.json)" if python3 -m mcp.waf_intelligence --file terraform/waf.tf --format json --limit 5 >"$WAF_JSON_FILE"; then if python3 - "$WAF_JSON_FILE" <<'PY' import json import sys path = sys.argv[1] with open(path, "r", encoding="utf-8") as f: payload = json.load(f) insights = payload.get("insights") if not isinstance(insights, list): raise SystemExit("waf_intel: insights is not a list") if insights: raise SystemExit(f"waf_intel: expected 0 insights, got {len(insights)}") print("ok") PY then echo -e "${GREEN}✓${NC} 2.1 WAF Intel JSON parses and insights are empty" else echo -e "${RED}✗${NC} 2.1 WAF Intel JSON contract violated" cat "$WAF_JSON_FILE" FAILED=1 fi else echo -e "${RED}✗${NC} 2.1 WAF Intel CLI failed" FAILED=1 fi rm -f "$WAF_JSON_FILE" echo "" echo "── 3. Terraform Format + Validate + Plan Gates ──" cd terraform if terraform fmt -check -recursive >/dev/null 2>&1; then echo -e "${GREEN}✓${NC} 3.1 Terraform formatting OK" else echo -e "${RED}✗${NC} 3.1 Terraform formatting required" echo " Run: cd terraform && terraform fmt -recursive" FAILED=1 fi terraform init -backend=false -input=false >/dev/null 2>&1 if terraform validate -no-color >/dev/null 2>&1; then echo -e "${GREEN}✓${NC} 3.2 Terraform validate OK" else echo -e "${RED}✗${NC} 3.2 Terraform validate failed" terraform validate -no-color FAILED=1 fi PLAN_FREE_OUT="$(mktemp -p "$TMP_DIR" tf-plan-free.XXXXXX.out)" PLAN_PRO_OUT="$(mktemp -p "$TMP_DIR" tf-plan-pro.XXXXXX.out)" PLAN_FREE_JSON="$(mktemp -p "$TMP_DIR" tf-plan-free.XXXXXX.json)" PLAN_PRO_JSON="$(mktemp -p "$TMP_DIR" tf-plan-pro.XXXXXX.json)" rm -f "$PLAN_FREE_OUT" "$PLAN_PRO_OUT" if terraform plan -no-color -input=false -lock=false -refresh=false -out="$PLAN_FREE_OUT" -var-file=assurance_free.tfvars >/dev/null; then if terraform show -json "$PLAN_FREE_OUT" >"$PLAN_FREE_JSON"; then if output="$( python3 - "$PLAN_FREE_JSON" <<'PY' import json import sys path = sys.argv[1] try: with open(path, "r", encoding="utf-8") as f: payload = json.load(f) except json.JSONDecodeError as e: print(f"json parse error: {e}") raise SystemExit(2) resource_changes = payload.get("resource_changes") planned_values = payload.get("planned_values") if not isinstance(resource_changes, list) or not isinstance(planned_values, dict): print("invalid plan json: missing resource_changes[] and/or planned_values{}") raise SystemExit(2) addresses = [ rc.get("address", "") for rc in resource_changes if isinstance(rc, dict) and isinstance(rc.get("address"), str) ] managed_waf = sum(1 for a in addresses if a.startswith("cloudflare_ruleset.managed_waf[")) bot_mgmt = sum(1 for a in addresses if a.startswith("cloudflare_bot_management.domains[")) if managed_waf != 0 or bot_mgmt != 0: print(f"expected managed_waf=0 bot_management=0, got managed_waf={managed_waf} bot_management={bot_mgmt}") for addr in sorted( a for a in addresses if a.startswith("cloudflare_ruleset.managed_waf[") or a.startswith("cloudflare_bot_management.domains[") ): print(f"- {addr}") raise SystemExit(2) PY )"; then echo -e "${GREEN}✓${NC} 3.3 Free-plan gate OK (managed_waf=0 bot_management=0)" else echo -e "${RED}✗${NC} 3.3 Free-plan gate violated" if [[ -n "${output:-}" ]]; then echo "$output" | sed 's/^/ /' fi FAILED=1 fi else echo -e "${RED}✗${NC} 3.3 terraform show -json failed (free)" FAILED=1 fi else echo -e "${RED}✗${NC} 3.3 Terraform plan failed (free)" terraform show -no-color "$PLAN_FREE_OUT" 2>/dev/null || true FAILED=1 fi if terraform plan -no-color -input=false -lock=false -refresh=false -out="$PLAN_PRO_OUT" -var-file=assurance_pro.tfvars >/dev/null; then if terraform show -json "$PLAN_PRO_OUT" >"$PLAN_PRO_JSON"; then if output="$( python3 - "$PLAN_PRO_JSON" <<'PY' import json import sys path = sys.argv[1] try: with open(path, "r", encoding="utf-8") as f: payload = json.load(f) except json.JSONDecodeError as e: print(f"json parse error: {e}") raise SystemExit(2) resource_changes = payload.get("resource_changes") planned_values = payload.get("planned_values") if not isinstance(resource_changes, list) or not isinstance(planned_values, dict): print("invalid plan json: missing resource_changes[] and/or planned_values{}") raise SystemExit(2) addresses = [ rc.get("address", "") for rc in resource_changes if isinstance(rc, dict) and isinstance(rc.get("address"), str) ] managed_waf = sum(1 for a in addresses if a.startswith("cloudflare_ruleset.managed_waf[")) bot_mgmt = sum(1 for a in addresses if a.startswith("cloudflare_bot_management.domains[")) if managed_waf != 1 or bot_mgmt != 1: print("expected managed_waf=1 bot_management=1") print(f"got managed_waf={managed_waf} bot_management={bot_mgmt}") print("observed:") for addr in sorted( a for a in addresses if a.startswith("cloudflare_ruleset.managed_waf[") or a.startswith("cloudflare_bot_management.domains[") ): print(f"- {addr}") raise SystemExit(2) PY )"; then echo -e "${GREEN}✓${NC} 3.4 Paid-plan gate OK (managed_waf=1 bot_management=1)" else echo -e "${RED}✗${NC} 3.4 Paid-plan gate violated" if [[ -n "${output:-}" ]]; then echo "$output" | sed 's/^/ /' fi FAILED=1 fi else echo -e "${RED}✗${NC} 3.4 terraform show -json failed (pro)" FAILED=1 fi else echo -e "${RED}✗${NC} 3.4 Terraform plan failed (pro)" terraform show -no-color "$PLAN_PRO_OUT" 2>/dev/null || true FAILED=1 fi PLAN_NEG_FREE_OUT="$(mktemp -p "$TMP_DIR" tf-plan-neg-free.XXXXXX.out)" PLAN_NEG_PRO_OUT="$(mktemp -p "$TMP_DIR" tf-plan-neg-pro.XXXXXX.out)" PLAN_NEG_FREE_JSON="$(mktemp -p "$TMP_DIR" tf-plan-neg-free.XXXXXX.json)" PLAN_NEG_PRO_JSON="$(mktemp -p "$TMP_DIR" tf-plan-neg-pro.XXXXXX.json)" rm -f "$PLAN_NEG_FREE_OUT" "$PLAN_NEG_PRO_OUT" echo "" echo "── 4. Negative Controls (Prove the gate bites) ──" if terraform plan -no-color -input=false -lock=false -refresh=false -out="$PLAN_NEG_FREE_OUT" -var-file=assurance_negative_free_should_fail.tfvars >/dev/null; then if terraform show -json "$PLAN_NEG_FREE_OUT" >"$PLAN_NEG_FREE_JSON"; then if output="$( python3 - "$PLAN_NEG_FREE_JSON" <<'PY' import json import sys path = sys.argv[1] try: with open(path, "r", encoding="utf-8") as f: payload = json.load(f) except json.JSONDecodeError as e: print(f"json parse error: {e}") raise SystemExit(2) resource_changes = payload.get("resource_changes") planned_values = payload.get("planned_values") if not isinstance(resource_changes, list) or not isinstance(planned_values, dict): print("invalid plan json: missing resource_changes[] and/or planned_values{}") raise SystemExit(2) addresses = [ rc.get("address", "") for rc in resource_changes if isinstance(rc, dict) and isinstance(rc.get("address"), str) ] managed_waf = sum(1 for a in addresses if a.startswith("cloudflare_ruleset.managed_waf[")) bot_mgmt = sum(1 for a in addresses if a.startswith("cloudflare_bot_management.domains[")) if managed_waf != 0 or bot_mgmt != 0: print(f"expected managed_waf=0 bot_management=0, got managed_waf={managed_waf} bot_management={bot_mgmt}") for addr in sorted( a for a in addresses if a.startswith("cloudflare_ruleset.managed_waf[") or a.startswith("cloudflare_bot_management.domains[") ): print(f"- {addr}") raise SystemExit(2) print("ok") PY )"; then echo -e "${RED}✗${NC} 4.1 Negative free-plan control unexpectedly passed" FAILED=1 else if [[ "${output:-}" == *"expected managed_waf=0 bot_management=0"* ]]; then echo -e "${GREEN}✓${NC} 4.1 Negative free-plan control failed as expected" else echo -e "${RED}✗${NC} 4.1 Negative free-plan control failed (unexpected error)" if [[ -n "${output:-}" ]]; then echo "$output" | sed 's/^/ /' fi FAILED=1 fi fi else echo -e "${RED}✗${NC} 4.1 terraform show -json failed (negative free)" FAILED=1 fi else echo -e "${RED}✗${NC} 4.1 Terraform plan failed (negative free)" FAILED=1 fi if terraform plan -no-color -input=false -lock=false -refresh=false -out="$PLAN_NEG_PRO_OUT" -var-file=assurance_negative_pro_should_fail.tfvars >/dev/null; then if terraform show -json "$PLAN_NEG_PRO_OUT" >"$PLAN_NEG_PRO_JSON"; then if output="$( python3 - "$PLAN_NEG_PRO_JSON" <<'PY' import json import sys path = sys.argv[1] try: with open(path, "r", encoding="utf-8") as f: payload = json.load(f) except json.JSONDecodeError as e: print(f"json parse error: {e}") raise SystemExit(2) resource_changes = payload.get("resource_changes") planned_values = payload.get("planned_values") if not isinstance(resource_changes, list) or not isinstance(planned_values, dict): print("invalid plan json: missing resource_changes[] and/or planned_values{}") raise SystemExit(2) addresses = [ rc.get("address", "") for rc in resource_changes if isinstance(rc, dict) and isinstance(rc.get("address"), str) ] managed_waf = sum(1 for a in addresses if a.startswith("cloudflare_ruleset.managed_waf[")) bot_mgmt = sum(1 for a in addresses if a.startswith("cloudflare_bot_management.domains[")) if managed_waf != 1 or bot_mgmt != 1: print("expected managed_waf=1 bot_management=1") print(f"got managed_waf={managed_waf} bot_management={bot_mgmt}") print("observed:") for addr in sorted( a for a in addresses if a.startswith("cloudflare_ruleset.managed_waf[") or a.startswith("cloudflare_bot_management.domains[") ): print(f"- {addr}") raise SystemExit(2) print("ok") PY )"; then echo -e "${RED}✗${NC} 4.2 Negative paid-plan control unexpectedly passed" FAILED=1 else if [[ "${output:-}" == *"expected managed_waf=1 bot_management=1"* ]]; then echo -e "${GREEN}✓${NC} 4.2 Negative paid-plan control failed as expected" else echo -e "${RED}✗${NC} 4.2 Negative paid-plan control failed (unexpected error)" if [[ -n "${output:-}" ]]; then echo "$output" | sed 's/^/ /' fi FAILED=1 fi fi else echo -e "${RED}✗${NC} 4.2 terraform show -json failed (negative pro)" FAILED=1 fi else echo -e "${RED}✗${NC} 4.2 Terraform plan failed (negative pro)" FAILED=1 fi rm -f "$PLAN_FREE_OUT" "$PLAN_PRO_OUT" "$PLAN_FREE_JSON" "$PLAN_PRO_JSON" "$PLAN_NEG_FREE_OUT" "$PLAN_NEG_PRO_OUT" "$PLAN_NEG_FREE_JSON" "$PLAN_NEG_PRO_JSON" cd "$REPO_ROOT" echo "" echo "============================================" echo " Summary" echo "============================================" if [[ $FAILED -gt 0 ]]; then echo -e "${RED}WAF + plan invariants violated. Fix before merging.${NC}" exit 1 fi echo -e "${GREEN}All WAF + plan invariants pass. ✓${NC}" exit 0