394 lines
12 KiB
Bash
394 lines
12 KiB
Bash
#!/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
|