Initial commit: Cloudflare infrastructure with WAF Intelligence

- Complete Cloudflare Terraform configuration (DNS, WAF, tunnels, access)
- WAF Intelligence MCP server with threat analysis and ML classification
- GitOps automation with PR workflows and drift detection
- Observatory monitoring stack with Prometheus/Grafana
- IDE operator rules for governed development
- Security playbooks and compliance frameworks
- Autonomous remediation and state reconciliation
This commit is contained in:
Vault Sovereign
2025-12-16 18:31:53 +00:00
commit 37a867c485
123 changed files with 25407 additions and 0 deletions

View File

@@ -0,0 +1,209 @@
#!/bin/bash
#
# Cloudflare State Anchor
# Orchestrates state reconciliation, invariant checking, and ProofChain anchoring.
#
# Usage:
# ./anchor-cloudflare-state.sh [--zone-id ZONE_ID] [--account-id ACCOUNT_ID]
#
# Environment Variables:
# CLOUDFLARE_API_TOKEN - Required
# CLOUDFLARE_ZONE_ID - Zone ID (or use --zone-id)
# CLOUDFLARE_ACCOUNT_ID - Account ID (or use --account-id)
# VAULTMESH_ANCHORS_PATH - Path to ProofChain anchors file (optional)
#
# Exit Codes:
# 0 - Success, all invariants passed
# 1 - Success, but invariants failed (anomalies detected)
# 2 - Error during execution
set -euo pipefail
# Configuration
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BASE_DIR="$(dirname "$SCRIPT_DIR")"
SNAPSHOTS_DIR="${BASE_DIR}/snapshots"
RECEIPTS_DIR="${BASE_DIR}/receipts"
ANOMALIES_DIR="${BASE_DIR}/anomalies"
ANCHORS_PATH="${VAULTMESH_ANCHORS_PATH:-${BASE_DIR}/proofchain-anchors.jsonl}"
# Parse arguments
ZONE_ID="${CLOUDFLARE_ZONE_ID:-}"
ACCOUNT_ID="${CLOUDFLARE_ACCOUNT_ID:-}"
while [[ $# -gt 0 ]]; do
case $1 in
--zone-id)
ZONE_ID="$2"
shift 2
;;
--account-id)
ACCOUNT_ID="$2"
shift 2
;;
*)
echo "Unknown argument: $1"
exit 2
;;
esac
done
# Validate
if [[ -z "${CLOUDFLARE_API_TOKEN:-}" ]]; then
echo "Error: CLOUDFLARE_API_TOKEN environment variable required"
exit 2
fi
if [[ -z "$ZONE_ID" ]]; then
echo "Error: Zone ID required (--zone-id or CLOUDFLARE_ZONE_ID)"
exit 2
fi
if [[ -z "$ACCOUNT_ID" ]]; then
echo "Error: Account ID required (--account-id or CLOUDFLARE_ACCOUNT_ID)"
exit 2
fi
# Ensure directories exist
mkdir -p "$SNAPSHOTS_DIR" "$RECEIPTS_DIR" "$ANOMALIES_DIR"
# Timestamp for this run
TIMESTAMP=$(date -u +%Y-%m-%dT%H-%M-%SZ)
echo "======================================"
echo "Cloudflare State Anchor"
echo "======================================"
echo "Timestamp: $TIMESTAMP"
echo "Zone ID: $ZONE_ID"
echo "Account ID: $ACCOUNT_ID"
echo ""
# Step 1: Run State Reconciler
echo ">>> Step 1: Fetching Cloudflare state..."
python3 "${SCRIPT_DIR}/state-reconciler.py" \
--zone-id "$ZONE_ID" \
--account-id "$ACCOUNT_ID" \
--output-dir "$SNAPSHOTS_DIR" \
--receipt-dir "$RECEIPTS_DIR"
# Find the latest snapshot
LATEST_SNAPSHOT=$(ls -t "${SNAPSHOTS_DIR}"/cloudflare-*.json 2>/dev/null | head -1)
if [[ -z "$LATEST_SNAPSHOT" ]]; then
echo "Error: No snapshot found"
exit 2
fi
echo "Snapshot: $LATEST_SNAPSHOT"
# Extract Merkle root from snapshot
MERKLE_ROOT=$(python3 -c "
import json
with open('$LATEST_SNAPSHOT') as f:
data = json.load(f)
print(data['integrity']['merkle_root'])
")
echo "Merkle Root: $MERKLE_ROOT"
echo ""
# Step 2: Run Invariant Checker
echo ">>> Step 2: Checking invariants..."
INVARIANT_EXIT=0
python3 "${SCRIPT_DIR}/invariant-checker.py" \
--snapshot "$LATEST_SNAPSHOT" \
--output-dir "$ANOMALIES_DIR" || INVARIANT_EXIT=$?
# Find latest report
LATEST_REPORT=$(ls -t "${ANOMALIES_DIR}"/invariant-report-*.json 2>/dev/null | head -1)
echo "Invariant Report: $LATEST_REPORT"
echo ""
# Extract summary
if [[ -n "$LATEST_REPORT" ]]; then
PASSED=$(python3 -c "import json; print(json.load(open('$LATEST_REPORT'))['summary']['passed'])")
FAILED=$(python3 -c "import json; print(json.load(open('$LATEST_REPORT'))['summary']['failed'])")
echo "Passed: $PASSED"
echo "Failed: $FAILED"
fi
# Step 3: Create ProofChain Anchor
echo ""
echo ">>> Step 3: Creating ProofChain anchor..."
# Compute combined hash
COMBINED_HASH=$(cat "$LATEST_SNAPSHOT" "$LATEST_REPORT" 2>/dev/null | sha256sum | cut -d' ' -f1)
# Create anchor JSON
ANCHOR_JSON=$(cat <<EOF
{
"anchor_type": "cf_state_anchor",
"schema_version": "vm_anchor_v1",
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"zone_id": "$ZONE_ID",
"account_id": "$ACCOUNT_ID",
"snapshot_path": "$LATEST_SNAPSHOT",
"report_path": "$LATEST_REPORT",
"merkle_root": "$MERKLE_ROOT",
"combined_hash": "$COMBINED_HASH",
"invariants_passed": $PASSED,
"invariants_failed": $FAILED,
"status": "$([ $INVARIANT_EXIT -eq 0 ] && echo 'clean' || echo 'anomalies_detected')"
}
EOF
)
# Append to anchors file
echo "$ANCHOR_JSON" >> "$ANCHORS_PATH"
echo "Anchor appended to: $ANCHORS_PATH"
# Step 4: Create combined receipt
echo ""
echo ">>> Step 4: Creating combined receipt..."
RECEIPT_PATH="${RECEIPTS_DIR}/cf-anchor-${TIMESTAMP}.json"
cat > "$RECEIPT_PATH" <<EOF
{
"receipt_type": "cf_state_anchor_complete",
"schema_version": "vm_cf_anchor_v1",
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"zone_id": "$ZONE_ID",
"account_id": "$ACCOUNT_ID",
"artifacts": {
"snapshot": "$LATEST_SNAPSHOT",
"invariant_report": "$LATEST_REPORT",
"anchors_file": "$ANCHORS_PATH"
},
"integrity": {
"merkle_root": "$MERKLE_ROOT",
"combined_hash": "$COMBINED_HASH",
"hash_algorithm": "sha256"
},
"invariants": {
"passed": $PASSED,
"failed": $FAILED,
"status": "$([ $INVARIANT_EXIT -eq 0 ] && echo 'all_passed' || echo 'failures_detected')"
}
}
EOF
echo "Receipt: $RECEIPT_PATH"
# Summary
echo ""
echo "======================================"
echo "Anchor Complete"
echo "======================================"
echo "Snapshot: $LATEST_SNAPSHOT"
echo "Report: $LATEST_REPORT"
echo "Receipt: $RECEIPT_PATH"
echo "Merkle Root: $MERKLE_ROOT"
echo "Status: $([ $INVARIANT_EXIT -eq 0 ] && echo 'CLEAN' || echo 'ANOMALIES DETECTED')"
echo "======================================"
# Output for CI pipelines
echo ""
echo "CI_MERKLE_ROOT=$MERKLE_ROOT"
echo "CI_SNAPSHOT_PATH=$LATEST_SNAPSHOT"
echo "CI_REPORT_PATH=$LATEST_REPORT"
echo "CI_RECEIPT_PATH=$RECEIPT_PATH"
echo "CI_INVARIANTS_STATUS=$([ $INVARIANT_EXIT -eq 0 ] && echo 'passed' || echo 'failed')"
# Exit with invariant check result
exit $INVARIANT_EXIT

View File

@@ -0,0 +1,173 @@
#!/usr/bin/env python3
"""
Autonomous Remediator — Cloudflare Autonomic Mesh Engine
Pure technical (D1) implementation
Runs continuously (systemd service) and performs:
- DNS auto-remediation (re-proxy, restore records)
- WAF baseline enforcement
- Access policy enforcement (MFA, no bypass)
- Tunnel health remediation (restart, rekey optional)
- Drift correction using Terraform
Outputs VaultMesh receipts for each correction.
"""
import os
import json
import time
import subprocess
import requests
from datetime import datetime, timezone
CF_API = "https://api.cloudflare.com/client/v4"
CF_TOKEN = os.getenv("CF_API_TOKEN")
CF_ACCOUNT = os.getenv("CF_ACCOUNT_ID")
TF_DIR = os.getenv("TF_DIR", "./terraform")
RECEIPT_DIR = os.getenv("VM_RECEIPT_DIR", "./receipts")
HEADERS = {
"Authorization": f"Bearer {CF_TOKEN}",
"Content-Type": "application/json",
}
os.makedirs(RECEIPT_DIR, exist_ok=True)
def cf(endpoint, method="GET", data=None):
url = f"{CF_API}{endpoint}"
if method == "GET":
r = requests.get(url, headers=HEADERS)
else:
r = requests.request(method, url, headers=HEADERS, json=data)
r.raise_for_status()
return r.json().get("result", {})
def emit_receipt(action, details):
ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
path = f"{RECEIPT_DIR}/auto-{action}-{ts}.json"
with open(path, "w") as f:
json.dump({"ts": ts, "action": action, "details": details}, f, indent=2)
print(f"[REMEDIATOR] Receipt emitted: {path}")
# ------------------------------
# DNS Remediation
# ------------------------------
def fix_dns():
zones = cf("/zones")
for z in zones:
zid = z["id"]
zname = z["name"]
recs = cf(f"/zones/{zid}/dns_records")
for r in recs:
# Re-proxy unproxied A/AAAA
if r["type"] in ("A", "AAAA") and not r.get("proxied"):
print(f"[DNS] Re-proxying {r['name']} in {zname}")
cf(f"/zones/{zid}/dns_records/{r['id']}", method="PUT",
data={"type": r["type"], "name": r["name"], "content": r["content"], "proxied": True})
emit_receipt("dns_reproxy", {"zone": zname, "record": r})
# Enforce DNSSEC
dnssec = cf(f"/zones/{zid}/dnssec")
if dnssec.get("status") != "active":
print(f"[DNS] Enabling DNSSEC for {zname}")
cf(f"/zones/{zid}/dnssec", method="PATCH", data={"status": "active"})
emit_receipt("dnssec_enable", {"zone": zname})
# ------------------------------
# WAF Enforcement
# ------------------------------
def enforce_waf():
zones = cf("/zones")
for z in zones:
zid = z["id"]
zname = z["name"]
pkgs = cf(f"/zones/{zid}/firewall/waf/packages")
# Ensure OWASP ruleset is present
if not any("owasp" in p.get("name", "").lower() for p in pkgs):
emit_receipt("missing_owasp", {"zone": zname})
print(f"[WAF] Missing OWASP ruleset in {zname}")
# ------------------------------
# Access Policy Enforcement
# ------------------------------
def enforce_access():
policies = cf(f"/accounts/{CF_ACCOUNT}/access/policies")
for p in policies:
changed = False
pid = p["id"]
# Enforce MFA requirement
for rule in p.get("rules", []):
if not rule.get("require_mfa"):
rule["require_mfa"] = True
changed = True
# No bypass allowed
if p.get("decision") == "bypass":
p["decision"] = "allow"
changed = True
if changed:
print(f"[ACCESS] Correcting policy {pid}")
cf(f"/accounts/{CF_ACCOUNT}/access/policies/{pid}", method="PUT", data=p)
emit_receipt("access_policy_fix", {"policy_id": pid})
# ------------------------------
# Tunnel Health Remediation
# ------------------------------
def fix_tunnels():
tunnels = cf(f"/accounts/{CF_ACCOUNT}/cfd_tunnel")
for t in tunnels:
if t.get("status") in ("degraded", "reconnecting", "down"):
tid = t["id"]
print(f"[TUNNEL] Restart recommended for {tid}")
# Informational only — actual restart is manual or via systemd
emit_receipt("tunnel_unhealthy", t)
# ------------------------------
# Terraform Drift Correction
# ------------------------------
def correct_terraform_drift():
print("[TF] Running terraform plan to detect drift...")
proc = subprocess.run(["terraform", "-chdir", TF_DIR, "plan"], capture_output=True, text=True)
if "No changes" not in proc.stdout:
print("[TF] Drift detected — applying corrective action")
subprocess.run(["terraform", "-chdir", TF_DIR, "apply", "-auto-approve"])
emit_receipt("terraform_drift_fix", {"output": proc.stdout})
# ------------------------------
# Main Loop
# ------------------------------
def main():
print("[REMEDIATOR] Autonomic Mesh running...")
while True:
fix_dns()
enforce_waf()
enforce_access()
fix_tunnels()
correct_terraform_drift()
print("[REMEDIATOR] Cycle complete. Sleeping 5 minutes...")
time.sleep(300)
if __name__ == "__main__":
main()

259
scripts/doc-invariants.sh Executable file
View File

@@ -0,0 +1,259 @@
#!/usr/bin/env bash
# ============================================================================
# DOC INVARIANTS CHECKER
# ============================================================================
# Enforces documentation law for VaultMesh.
# Run from repo root: bash scripts/doc-invariants.sh
#
# Exit codes:
# 0 = All invariants pass
# 1 = One or more invariants violated
#
# Governed by: RED-BOOK.md
# ============================================================================
set -uo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$REPO_ROOT"
FAILED=0
PASSED=0
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
NC='\033[0m' # No Color
pass() {
echo -e "${GREEN}${NC} $1"
((PASSED++))
}
fail() {
echo -e "${RED}${NC} $1"
((FAILED++))
}
warn() {
echo -e "${YELLOW}${NC} $1"
}
echo "============================================"
echo " VaultMesh Documentation Invariants Check"
echo "============================================"
echo ""
# ============================================================================
# 1. STRUCTURAL INVARIANTS
# ============================================================================
echo "── 1. Structural Invariants ──"
# 1.1 Single Front Door
if [[ -f "README.md" ]]; then
if grep -q "STRUCTURE.md" README.md && grep -q "FIRST_RUN.md\|DEPLOYMENT_GUIDE.md" README.md; then
pass "1.1 Single Front Door: README.md exists and links to STRUCTURE.md + guides"
else
fail "1.1 Single Front Door: README.md missing links to STRUCTURE.md or guides"
fi
else
fail "1.1 Single Front Door: README.md does not exist"
fi
# 1.2 Single Index
if [[ -f "STRUCTURE.md" ]]; then
pass "1.2 Single Index: STRUCTURE.md exists"
else
fail "1.2 Single Index: STRUCTURE.md does not exist"
fi
# Check for competing indexes (forbidden patterns)
COMPETING_INDEXES=$(find . -maxdepth 1 -name "README_STRUCTURE*.md" -o -name "INDEX*.md" 2>/dev/null | grep -v archive_docs || true)
if [[ -z "$COMPETING_INDEXES" ]]; then
pass "1.2 Single Index: No competing index files found"
else
fail "1.2 Single Index: Found competing index files: $COMPETING_INDEXES"
fi
# 1.3 Archive Boundary
if [[ -d "archive_docs" ]]; then
pass "1.3 Archive Boundary: archive_docs/ directory exists"
else
warn "1.3 Archive Boundary: archive_docs/ directory does not exist (optional)"
fi
echo ""
# ============================================================================
# 2. CONTENT INVARIANTS
# ============================================================================
echo "── 2. Content Invariants ──"
# 2.1 Multi-Account Single Source of Truth
if [[ -f "MULTI_ACCOUNT_AUTH.md" ]]; then
pass "2.1 Multi-Account SSOT: MULTI_ACCOUNT_AUTH.md exists"
else
fail "2.1 Multi-Account SSOT: MULTI_ACCOUNT_AUTH.md does not exist"
fi
# 2.2 One Doctrine
if [[ -f "RED-BOOK.md" ]]; then
pass "2.2 One Doctrine: RED-BOOK.md exists"
else
fail "2.2 One Doctrine: RED-BOOK.md does not exist"
fi
# 2.3 Playbooks Own Incidents
REQUIRED_PLAYBOOKS=(
"playbooks/DNS-COMPROMISE-PLAYBOOK.md"
"playbooks/TUNNEL-ROTATION-PROTOCOL.md"
"playbooks/waf_incident_playbook.md"
)
ALL_PLAYBOOKS_EXIST=true
for pb in "${REQUIRED_PLAYBOOKS[@]}"; do
if [[ ! -f "$pb" ]]; then
fail "2.3 Playbooks: Missing $pb"
ALL_PLAYBOOKS_EXIST=false
fi
done
if $ALL_PLAYBOOKS_EXIST; then
pass "2.3 Playbooks: All required playbooks exist"
fi
echo ""
# ============================================================================
# 3. LINK & REFERENCE INVARIANTS
# ============================================================================
echo "── 3. Link & Reference Invariants ──"
# 3.1 No Dead Links in Active Space
# Check for known deprecated filenames outside archive_docs/
DEPRECATED_PATTERNS=(
"dns_compromise_playbook\.md"
"tunnel_rotation_protocol\.md"
"ONE-PAGE-SECURITY-SHEET\.md"
"README_STRUCTURE\.md"
)
DEAD_LINK_FOUND=false
for pattern in "${DEPRECATED_PATTERNS[@]}"; do
# Search for pattern, excluding archive_docs/
HITS=$(grep -r "$pattern" . --include="*.md" --include="*.yml" --include="*.yaml" --include="*.py" 2>/dev/null | grep -v "archive_docs/" | grep -v "doc-invariants.sh" || true)
if [[ -n "$HITS" ]]; then
fail "3.1 Dead Links: Found deprecated reference '$pattern' outside archive_docs/"
echo " $HITS" | head -3
DEAD_LINK_FOUND=true
fi
done
if ! $DEAD_LINK_FOUND; then
pass "3.1 Dead Links: No deprecated references found in active space"
fi
# 3.2 Case-Exact Playbook Paths
# Check for WRONG casing - lowercase variants when they should be uppercase
# DNS-COMPROMISE-PLAYBOOK.md should NOT appear as dns-compromise-playbook.md
CASE_VIOLATIONS=$(grep -r "dns-compromise-playbook\.md\|dns_compromise_playbook\.md" . --include="*.md" --include="*.yml" --include="*.yaml" 2>/dev/null | grep -v archive_docs/ | grep -v "DNS-COMPROMISE-PLAYBOOK" || true)
if [[ -z "$CASE_VIOLATIONS" ]]; then
pass "3.2 Case-Exact Paths: Playbook references use correct casing"
else
fail "3.2 Case-Exact Paths: Found lowercase playbook references (should be UPPERCASE)"
echo " $CASE_VIOLATIONS" | head -3
fi
echo ""
# ============================================================================
# 4. COGNITIVE / AI LAYER INVARIANTS
# ============================================================================
echo "── 4. Cognitive Layer Invariants ──"
# 4.1 Cognition ≈ Fourfold Work
COGNITION_DOCS=("COGNITION_FLOW.md" "DEMO_COGNITION.md")
for doc in "${COGNITION_DOCS[@]}"; do
if [[ -f "$doc" ]]; then
if grep -qi "RED-BOOK\|Fourfold Work\|Nigredo.*Albedo.*Citrinitas.*Rubedo" "$doc"; then
pass "4.1 Cognition Doctrine: $doc references Red Book"
else
fail "4.1 Cognition Doctrine: $doc does not reference Red Book / Fourfold Work"
fi
fi
done
# 4.2 Guardrails Reference Doctrine
if [[ -f "AGENT_GUARDRAILS.md" ]]; then
if grep -qi "RED-BOOK" "AGENT_GUARDRAILS.md"; then
pass "4.2 Guardrails Doctrine: AGENT_GUARDRAILS.md references Red Book"
else
fail "4.2 Guardrails Doctrine: AGENT_GUARDRAILS.md does not reference Red Book"
fi
fi
echo ""
# ============================================================================
# 5. PLAYBOOK REGISTRATION
# ============================================================================
echo "── 5. Playbook Registration ──"
# Check that all playbooks are registered in STRUCTURE.md
for pb in "${REQUIRED_PLAYBOOKS[@]}"; do
pb_name=$(basename "$pb")
if grep -q "$pb_name" STRUCTURE.md 2>/dev/null; then
pass "5.1 Registration: $pb_name listed in STRUCTURE.md"
else
fail "5.1 Registration: $pb_name NOT listed in STRUCTURE.md"
fi
done
echo ""
# ============================================================================
# 6. TOP-LEVEL DOC REGISTRY
# ============================================================================
echo "── 6. Doc Registry ──"
# Every top-level .md (except README.md, STRUCTURE.md, LICENSE) must be in STRUCTURE.md
UNREGISTERED_DOCS=false
for f in *.md; do
[[ "$f" == "README.md" || "$f" == "STRUCTURE.md" || "$f" == "LICENSE.md" ]] && continue
if ! grep -q "$f" STRUCTURE.md 2>/dev/null; then
fail "6.1 Registry: $f not listed in STRUCTURE.md"
UNREGISTERED_DOCS=true
fi
done
if ! $UNREGISTERED_DOCS; then
pass "6.1 Registry: All top-level docs are indexed in STRUCTURE.md"
fi
echo ""
# ============================================================================
# SUMMARY
# ============================================================================
echo "============================================"
echo " Summary"
echo "============================================"
echo -e " ${GREEN}Passed:${NC} $PASSED"
echo -e " ${RED}Failed:${NC} $FAILED"
echo ""
if [[ $FAILED -gt 0 ]]; then
echo -e "${RED}Doc invariants violated. Fix before merging.${NC}"
exit 1
else
echo -e "${GREEN}All doc invariants pass. ✓${NC}"
exit 0
fi

View File

@@ -0,0 +1,208 @@
#!/usr/bin/env python3
"""
Drift Guardian — Real-Time Cloudflare Drift Detection
Pure technical (D1)
Purpose:
• Poll Cloudflare state at short intervals
• Compare live state → latest snapshot → invariants
• Detect unauthorized modifications
• Trigger remediation (optional hook)
• Emit VaultMesh anomaly receipts
The Guardian = fast, reactive layer.
The Remediator = corrective, authoritative layer.
The Reconciler = canonical truth layer.
"""
import os
import json
import time
import hashlib
import requests
from datetime import datetime, timezone
CF_API = "https://api.cloudflare.com/client/v4"
CF_TOKEN = os.getenv("CF_API_TOKEN")
CF_ACCOUNT = os.getenv("CF_ACCOUNT_ID")
STATE_ROOT = os.getenv("VM_STATE_ROOT", "./cloudflare_state")
SNAP_DIR = f"{STATE_ROOT}/snapshots"
RECEIPT_DIR = f"{STATE_ROOT}/receipts"
ANOM_DIR = f"{STATE_ROOT}/anomalies"
HEADERS = {
"Authorization": f"Bearer {CF_TOKEN}",
"Content-Type": "application/json",
}
os.makedirs(RECEIPT_DIR, exist_ok=True)
os.makedirs(ANOM_DIR, exist_ok=True)
# -----------------------------
# Helpers
# -----------------------------
def cf(endpoint):
r = requests.get(f"{CF_API}{endpoint}", headers=HEADERS)
r.raise_for_status()
return r.json().get("result", {})
def load_latest_snapshot():
snaps = sorted(os.listdir(SNAP_DIR))
if not snaps:
return None
latest = snaps[-1]
with open(f"{SNAP_DIR}/{latest}") as f:
return json.load(f)
def emit_anomaly(event_type, details):
ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
anomaly = {"ts": ts, "event_type": event_type, "details": details}
h = hashlib.sha256(json.dumps(anomaly, sort_keys=True).encode()).hexdigest()
file_path = f"{ANOM_DIR}/drift-{ts}-{h[:8]}.json"
with open(file_path, "w") as f:
json.dump(anomaly, f, indent=2)
print(f"[GUARDIAN] Drift detected → {file_path}")
return file_path
# -----------------------------
# Drift Detection Logic
# -----------------------------
def detect_dns_drift(snapshot):
anomalies = []
zones_live = cf("/zones")
# index snapshot zones by name
snap_zones = {z["name"]: z for z in snapshot.get("zones", [])}
for z in zones_live:
name = z["name"]
zid = z["id"]
if name not in snap_zones:
anomalies.append({"type": "zone_added", "zone": name})
continue
# DNS record diff
live_recs = cf(f"/zones/{zid}/dns_records")
snap_recs = snapshot.get("dns", {}).get(name, [])
live_set = {(r["type"], r["name"], r.get("content")) for r in live_recs}
snap_set = {(r["type"], r["name"], r.get("content")) for r in snap_recs}
added = live_set - snap_set
removed = snap_set - live_set
if added:
anomalies.append({"type": "dns_added", "zone": name, "records": list(added)})
if removed:
anomalies.append({"type": "dns_removed", "zone": name, "records": list(removed)})
return anomalies
def detect_waf_drift(snapshot):
anomalies = []
zones_live = cf("/zones")
snap_waf = snapshot.get("waf", {})
for z in zones_live:
zname = z["name"]
zid = z["id"]
live_pkgs = cf(f"/zones/{zid}/firewall/waf/packages")
snap_pkgs = snap_waf.get(zname, [])
live_names = {p.get("name") for p in live_pkgs}
snap_names = {p.get("name") for p in snap_pkgs}
if live_names != snap_names:
anomalies.append({
"type": "waf_ruleset_drift",
"zone": zname,
"expected": list(snap_names),
"found": list(live_names)
})
return anomalies
def detect_access_drift(snapshot):
anomalies = []
live_apps = cf(f"/accounts/{CF_ACCOUNT}/access/apps")
snap_apps = snapshot.get("access_apps", [])
live_set = {(a.get("name"), a.get("type")) for a in live_apps}
snap_set = {(a.get("name"), a.get("type")) for a in snap_apps}
if live_set != snap_set:
anomalies.append({
"type": "access_app_drift",
"expected": list(snap_set),
"found": list(live_set)
})
return anomalies
def detect_tunnel_drift(snapshot):
anomalies = []
live = cf(f"/accounts/{CF_ACCOUNT}/cfd_tunnel")
snap = snapshot.get("tunnels", [])
live_ids = {t.get("id") for t in live}
snap_ids = {t.get("id") for t in snap}
if live_ids != snap_ids:
anomalies.append({
"type": "tunnel_id_drift",
"expected": list(snap_ids),
"found": list(live_ids)
})
# health drift
for t in live:
if t.get("status") not in ("active", "healthy"):
anomalies.append({"type": "tunnel_unhealthy", "tunnel": t})
return anomalies
# -----------------------------
# Main Guardian Loop
# -----------------------------
def main():
print("[GUARDIAN] Drift Guardian active…")
while True:
snapshot = load_latest_snapshot()
if not snapshot:
print("[GUARDIAN] No snapshot found — run state-reconciler first.")
time.sleep(60)
continue
anomalies = []
anomalies += detect_dns_drift(snapshot)
anomalies += detect_waf_drift(snapshot)
anomalies += detect_access_drift(snapshot)
anomalies += detect_tunnel_drift(snapshot)
if anomalies:
for a in anomalies:
emit_anomaly(a.get("type"), a)
else:
print("[GUARDIAN] No drift detected.")
time.sleep(120) # check every 2 minutes
if __name__ == "__main__":
main()

101
scripts/infra-invariants.sh Executable file
View File

@@ -0,0 +1,101 @@
#!/usr/bin/env bash
# ============================================================================
# INFRA INVARIANTS CHECKER
# ============================================================================
# Enforces infrastructure law for VaultMesh.
# Run from repo root: bash scripts/infra-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 Infrastructure Invariants Check"
echo "============================================"
echo ""
FAILED=0
# ============================================================================
# 1. TERRAFORM FORMAT CHECK
# ============================================================================
echo "── 1. Terraform Formatting ──"
cd terraform
if terraform fmt -check -recursive > /dev/null 2>&1; then
echo -e "${GREEN}${NC} 1.1 All .tf files are properly formatted"
else
echo -e "${RED}${NC} 1.1 Terraform files need formatting"
echo " Run: cd terraform && terraform fmt -recursive"
FAILED=1
fi
# ============================================================================
# 2. TERRAFORM VALIDATE
# ============================================================================
echo ""
echo "── 2. Terraform Validation ──"
terraform init -backend=false > /dev/null 2>&1
if terraform validate > /dev/null 2>&1; then
echo -e "${GREEN}${NC} 2.1 Terraform configuration is valid"
else
echo -e "${RED}${NC} 2.1 Terraform validation failed"
terraform validate
FAILED=1
fi
cd "$REPO_ROOT"
# ============================================================================
# 3. REQUIRED FILES
# ============================================================================
echo ""
echo "── 3. Required Terraform Files ──"
REQUIRED_TF_FILES=(
"terraform/main.tf"
"terraform/variables.tf"
)
for tf in "${REQUIRED_TF_FILES[@]}"; do
if [[ -f "$tf" ]]; then
echo -e "${GREEN}${NC} 3.1 $tf exists"
else
echo -e "${RED}${NC} 3.1 Missing required file: $tf"
FAILED=1
fi
done
# ============================================================================
# SUMMARY
# ============================================================================
echo ""
echo "============================================"
echo " Summary"
echo "============================================"
if [[ $FAILED -gt 0 ]]; then
echo -e "${RED}Infra invariants violated. Fix before merging.${NC}"
exit 1
else
echo -e "${GREEN}All infra invariants pass. ✓${NC}"
exit 0
fi

View File

@@ -0,0 +1,427 @@
#!/usr/bin/env python3
"""
Cloudflare Invariant Checker
Tests state snapshots against defined invariants and produces anomaly reports.
Usage:
python3 invariant-checker.py --snapshot <path/to/snapshot.json>
Environment Variables:
MANIFEST_PATH - Path to DNS manifest (optional)
TERRAFORM_STATE_PATH - Path to Terraform state (optional)
Output:
- anomalies/invariant-report-<timestamp>.json
- Exit code 0 if all pass, 1 if any fail
"""
import argparse
import hashlib
import json
import os
import sys
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional, Tuple
ANOMALY_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "anomalies")
class InvariantResult:
"""Result of an invariant check."""
def __init__(self, name: str, passed: bool, message: str, details: Optional[Dict] = None):
self.name = name
self.passed = passed
self.message = message
self.details = details or {}
def to_dict(self) -> Dict[str, Any]:
return {
"invariant": self.name,
"passed": self.passed,
"message": self.message,
"details": self.details,
}
class InvariantChecker:
"""Checks Cloudflare state against defined invariants."""
def __init__(self, snapshot: Dict[str, Any], manifest: Optional[Dict] = None, tf_state: Optional[Dict] = None):
self.snapshot = snapshot
self.state = snapshot.get("state", {})
self.manifest = manifest
self.tf_state = tf_state
self.results: List[InvariantResult] = []
def check_all(self) -> List[InvariantResult]:
"""Run all invariant checks."""
self._check_dns_invariants()
self._check_waf_invariants()
self._check_access_invariants()
self._check_tunnel_invariants()
self._check_zone_settings_invariants()
if self.manifest:
self._check_manifest_drift()
return self.results
# === DNS Invariants ===
def _check_dns_invariants(self):
"""Check DNS-related invariants."""
dns = self.state.get("dns", {})
records = dns.get("records", [])
# INV-DNS-001: No unproxied A/AAAA records (unless explicitly internal)
unproxied = [
r for r in records
if r.get("type") in ("A", "AAAA")
and not r.get("proxied", False)
and not r.get("name", "").startswith("_") # Allow service records
]
self.results.append(InvariantResult(
"INV-DNS-001",
len(unproxied) == 0,
f"No unproxied A/AAAA records" if len(unproxied) == 0 else f"Found {len(unproxied)} unproxied A/AAAA records",
{"unproxied_records": [r.get("name") for r in unproxied]}
))
# INV-DNS-002: DNSSEC must be enabled
dnssec = dns.get("dnssec", {})
dnssec_enabled = dnssec.get("status") == "active"
self.results.append(InvariantResult(
"INV-DNS-002",
dnssec_enabled,
"DNSSEC is active" if dnssec_enabled else "DNSSEC is not active",
{"dnssec_status": dnssec.get("status")}
))
# INV-DNS-003: SPF record must exist
spf_records = [r for r in records if r.get("type") == "TXT" and "v=spf1" in r.get("content", "")]
self.results.append(InvariantResult(
"INV-DNS-003",
len(spf_records) > 0,
"SPF record exists" if len(spf_records) > 0 else "No SPF record found",
{"spf_count": len(spf_records)}
))
# INV-DNS-004: DMARC record must exist
dmarc_records = [r for r in records if r.get("name", "").startswith("_dmarc") and r.get("type") == "TXT"]
self.results.append(InvariantResult(
"INV-DNS-004",
len(dmarc_records) > 0,
"DMARC record exists" if len(dmarc_records) > 0 else "No DMARC record found",
{"dmarc_count": len(dmarc_records)}
))
# INV-DNS-005: No wildcard records (unless explicitly allowed)
wildcards = [r for r in records if "*" in r.get("name", "")]
self.results.append(InvariantResult(
"INV-DNS-005",
len(wildcards) == 0,
"No wildcard records" if len(wildcards) == 0 else f"Found {len(wildcards)} wildcard records",
{"wildcard_records": [r.get("name") for r in wildcards]}
))
# === WAF Invariants ===
def _check_waf_invariants(self):
"""Check WAF-related invariants."""
waf = self.state.get("waf", {})
rulesets = waf.get("rulesets", [])
# INV-WAF-001: Managed ruleset must be enabled
managed_rulesets = [rs for rs in rulesets if rs.get("kind") == "managed"]
self.results.append(InvariantResult(
"INV-WAF-001",
len(managed_rulesets) > 0,
"Managed WAF ruleset enabled" if len(managed_rulesets) > 0 else "No managed WAF ruleset found",
{"managed_ruleset_count": len(managed_rulesets)}
))
# INV-WAF-002: Firewall rules must exist
firewall_rules = waf.get("firewall_rules", [])
self.results.append(InvariantResult(
"INV-WAF-002",
len(firewall_rules) > 0,
f"Found {len(firewall_rules)} firewall rules" if len(firewall_rules) > 0 else "No firewall rules configured",
{"firewall_rule_count": len(firewall_rules)}
))
# === Zone Settings Invariants ===
def _check_zone_settings_invariants(self):
"""Check zone settings invariants."""
settings = self.state.get("zone_settings", {})
# INV-ZONE-001: TLS must be strict
ssl_mode = settings.get("ssl")
self.results.append(InvariantResult(
"INV-ZONE-001",
ssl_mode in ("strict", "full_strict"),
f"TLS mode is {ssl_mode}" if ssl_mode in ("strict", "full_strict") else f"TLS mode is {ssl_mode}, should be strict",
{"ssl_mode": ssl_mode}
))
# INV-ZONE-002: Minimum TLS version must be 1.2+
min_tls = settings.get("min_tls_version")
valid_tls = min_tls in ("1.2", "1.3")
self.results.append(InvariantResult(
"INV-ZONE-002",
valid_tls,
f"Minimum TLS version is {min_tls}" if valid_tls else f"Minimum TLS version is {min_tls}, should be 1.2+",
{"min_tls_version": min_tls}
))
# INV-ZONE-003: Always Use HTTPS must be on
always_https = settings.get("always_use_https") == "on"
self.results.append(InvariantResult(
"INV-ZONE-003",
always_https,
"Always Use HTTPS is enabled" if always_https else "Always Use HTTPS is disabled",
{"always_use_https": settings.get("always_use_https")}
))
# INV-ZONE-004: Browser check must be on
browser_check = settings.get("browser_check") == "on"
self.results.append(InvariantResult(
"INV-ZONE-004",
browser_check,
"Browser Check is enabled" if browser_check else "Browser Check is disabled",
{"browser_check": settings.get("browser_check")}
))
# === Access Invariants ===
def _check_access_invariants(self):
"""Check Zero Trust Access invariants."""
access = self.state.get("access", {})
apps = access.get("apps", [])
# INV-ACCESS-001: All Access apps must have at least one policy
apps_without_policies = [a for a in apps if len(a.get("policies", [])) == 0]
self.results.append(InvariantResult(
"INV-ACCESS-001",
len(apps_without_policies) == 0,
"All Access apps have policies" if len(apps_without_policies) == 0 else f"{len(apps_without_policies)} apps have no policies",
{"apps_without_policies": [a.get("name") for a in apps_without_policies]}
))
# INV-ACCESS-002: No Access app in bypass mode
bypass_apps = [a for a in apps if any(
p.get("decision") == "bypass" for p in a.get("policies", [])
)]
self.results.append(InvariantResult(
"INV-ACCESS-002",
len(bypass_apps) == 0,
"No Access apps in bypass mode" if len(bypass_apps) == 0 else f"{len(bypass_apps)} apps have bypass policies",
{"bypass_apps": [a.get("name") for a in bypass_apps]}
))
# INV-ACCESS-003: Session duration should not exceed 24h
long_session_apps = [
a for a in apps
if self._parse_duration(a.get("session_duration", "24h")) > 86400
]
self.results.append(InvariantResult(
"INV-ACCESS-003",
len(long_session_apps) == 0,
"All sessions <= 24h" if len(long_session_apps) == 0 else f"{len(long_session_apps)} apps have sessions > 24h",
{"long_session_apps": [a.get("name") for a in long_session_apps]}
))
def _parse_duration(self, duration: str) -> int:
"""Parse duration string to seconds."""
if not duration:
return 0
try:
if duration.endswith("h"):
return int(duration[:-1]) * 3600
elif duration.endswith("m"):
return int(duration[:-1]) * 60
elif duration.endswith("s"):
return int(duration[:-1])
else:
return int(duration)
except (ValueError, TypeError):
return 0
# === Tunnel Invariants ===
def _check_tunnel_invariants(self):
"""Check Cloudflare Tunnel invariants."""
tunnels = self.state.get("tunnels", {})
tunnel_list = tunnels.get("list", [])
# INV-TUN-001: All tunnels must be healthy (not deleted, has connections)
active_tunnels = [t for t in tunnel_list if not t.get("deleted_at")]
unhealthy = [
t for t in active_tunnels
if len(t.get("connections", [])) == 0
]
self.results.append(InvariantResult(
"INV-TUN-001",
len(unhealthy) == 0,
f"All {len(active_tunnels)} tunnels healthy" if len(unhealthy) == 0 else f"{len(unhealthy)} tunnels have no connections",
{"unhealthy_tunnels": [t.get("name") for t in unhealthy]}
))
# INV-TUN-002: No stale/orphan tunnels (deleted but still present)
deleted_tunnels = [t for t in tunnel_list if t.get("deleted_at")]
self.results.append(InvariantResult(
"INV-TUN-002",
len(deleted_tunnels) == 0,
"No stale tunnels" if len(deleted_tunnels) == 0 else f"{len(deleted_tunnels)} deleted tunnels still present",
{"stale_tunnels": [t.get("name") for t in deleted_tunnels]}
))
# === Manifest Drift ===
def _check_manifest_drift(self):
"""Check for drift between live state and manifest."""
if not self.manifest:
return
dns = self.state.get("dns", {})
records = dns.get("records", [])
manifest_records = self.manifest.get("records", [])
# Build lookup maps
live_map = {(r.get("type"), r.get("name")): r for r in records}
manifest_map = {(r.get("type"), r.get("name")): r for r in manifest_records}
# Find drift
missing_in_live = set(manifest_map.keys()) - set(live_map.keys())
extra_in_live = set(live_map.keys()) - set(manifest_map.keys())
# INV-DRIFT-001: All manifest records must exist in live
self.results.append(InvariantResult(
"INV-DRIFT-001",
len(missing_in_live) == 0,
"All manifest records present" if len(missing_in_live) == 0 else f"{len(missing_in_live)} records missing from live",
{"missing_records": list(missing_in_live)}
))
# INV-DRIFT-002: No unexpected records in live
self.results.append(InvariantResult(
"INV-DRIFT-002",
len(extra_in_live) == 0,
"No unexpected records" if len(extra_in_live) == 0 else f"{len(extra_in_live)} unexpected records in live",
{"extra_records": list(extra_in_live)}
))
def generate_report(results: List[InvariantResult], snapshot_path: str) -> Dict[str, Any]:
"""Generate invariant check report."""
passed = [r for r in results if r.passed]
failed = [r for r in results if not r.passed]
return {
"report_type": "invariant_check",
"schema_version": "vm_invariant_v1",
"timestamp": datetime.now(timezone.utc).isoformat(),
"snapshot_path": snapshot_path,
"summary": {
"total": len(results),
"passed": len(passed),
"failed": len(failed),
"pass_rate": len(passed) / len(results) if results else 0,
},
"results": [r.to_dict() for r in results],
"failed_invariants": [r.to_dict() for r in failed],
}
def create_anomaly_receipt(failed: List[InvariantResult], snapshot_path: str) -> Optional[Dict[str, Any]]:
"""Create VaultMesh anomaly receipt for failed invariants."""
if not failed:
return None
return {
"receipt_type": "cf_invariant_anomaly",
"schema_version": "vm_cf_anomaly_v1",
"timestamp": datetime.now(timezone.utc).isoformat(),
"snapshot_path": snapshot_path,
"anomaly_count": len(failed),
"anomalies": [
{
"invariant": r.name,
"message": r.message,
"details": r.details,
}
for r in failed
],
"severity": "CRITICAL" if any(r.name.startswith("INV-DNS-002") or r.name.startswith("INV-ZONE-001") for r in failed) else "WARNING",
}
def main():
parser = argparse.ArgumentParser(description="Cloudflare Invariant Checker")
parser.add_argument("--snapshot", required=True, help="Path to state snapshot JSON")
parser.add_argument("--manifest", default=os.environ.get("MANIFEST_PATH"),
help="Path to DNS manifest")
parser.add_argument("--output-dir", default=ANOMALY_DIR,
help="Output directory for reports")
args = parser.parse_args()
# Load snapshot
with open(args.snapshot) as f:
snapshot = json.load(f)
# Load manifest if provided
manifest = None
if args.manifest and os.path.exists(args.manifest):
with open(args.manifest) as f:
manifest = json.load(f)
# Ensure output directory exists
os.makedirs(args.output_dir, exist_ok=True)
# Run checks
print(f"Checking invariants for snapshot: {args.snapshot}")
checker = InvariantChecker(snapshot, manifest)
results = checker.check_all()
# Generate report
report = generate_report(results, args.snapshot)
# Write report
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H-%M-%SZ")
report_filename = f"invariant-report-{timestamp}.json"
report_path = os.path.join(args.output_dir, report_filename)
with open(report_path, "w") as f:
json.dump(report, f, indent=2, sort_keys=True)
print(f"Report written to: {report_path}")
# Create anomaly receipt if failures
failed = [r for r in results if not r.passed]
if failed:
anomaly_receipt = create_anomaly_receipt(failed, args.snapshot)
anomaly_filename = f"anomaly-{timestamp}.json"
anomaly_path = os.path.join(args.output_dir, anomaly_filename)
with open(anomaly_path, "w") as f:
json.dump(anomaly_receipt, f, indent=2, sort_keys=True)
print(f"Anomaly receipt written to: {anomaly_path}")
# Summary
print("\n=== Invariant Check Summary ===")
print(f"Total: {report['summary']['total']}")
print(f"Passed: {report['summary']['passed']}")
print(f"Failed: {report['summary']['failed']}")
print(f"Pass Rate: {report['summary']['pass_rate']:.1%}")
if failed:
print("\n=== Failed Invariants ===")
for r in failed:
print(f" [{r.name}] {r.message}")
# Exit with appropriate code
return 0 if len(failed) == 0 else 1
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,182 @@
#!/usr/bin/env python3
"""
Cloudflare Invariant Checker (Pure Technical)
Evaluates whether Cloudflare's live state satisfies required invariants:
- DNS integrity (proxied, no wildcards, SPF/DKIM/DMARC match manifest)
- DNSSEC + registrar lock enabled
- WAF baseline compliance
- Access policies enforce MFA and no-bypass rules
- Tunnel health and credential age
- Drift vs DNS Manifest
- Drift vs Terraform (.tf files)
Outputs:
anomalies/cf-invariants-<ts>.json
receipts/cf-invariants-<ts>-<hash>.json
"""
import os
import json
import hashlib
import requests
from datetime import datetime, timezone
CF_API = "https://api.cloudflare.com/client/v4"
CF_TOKEN = os.getenv("CF_API_TOKEN")
CF_ACCOUNT = os.getenv("CF_ACCOUNT_ID")
ROOT = os.getenv("VM_STATE_ROOT", "./cloudflare_state")
MANIFEST_PATH = os.getenv("DNS_MANIFEST", "./cloudflare_dns_manifest.json")
TF_DIR = os.getenv("TF_DIR", "./terraform")
HEADERS = {
"Authorization": f"Bearer {CF_TOKEN}",
"Content-Type": "application/json",
}
os.makedirs(f"{ROOT}/anomalies", exist_ok=True)
os.makedirs(f"{ROOT}/receipts", exist_ok=True)
def merkle_root(obj):
return hashlib.sha256(json.dumps(obj, sort_keys=True).encode()).hexdigest()
def cf(endpoint):
r = requests.get(f"{CF_API}{endpoint}", headers=HEADERS)
r.raise_for_status()
return r.json().get("result", {})
# -------------------------------
# Helper: Load DNS Manifest
# -------------------------------
def load_manifest():
if not os.path.exists(MANIFEST_PATH):
return None
with open(MANIFEST_PATH, "r") as f:
try:
return json.load(f)
except:
return None
# -------------------------------
# Invariant Checks
# -------------------------------
def check_dns(zones, manifest):
anomalies = []
for z in zones:
zid = z["id"]
zname = z["name"]
recs = cf(f"/zones/{zid}/dns_records")
for r in recs:
# 1 — No wildcards
if r["name"].startswith("*"):
anomalies.append({"zone": zname, "type": "wildcard_record", "record": r})
# 2 — Must be proxied unless manifest says internal
internal = False
if manifest and zname in manifest.get("internal_records", {}):
internal_list = manifest["internal_records"][zname]
if r["name"] in internal_list:
internal = True
if not internal and r.get("proxied") is False:
anomalies.append({"zone": zname, "type": "unproxied_record", "record": r})
# 3 — DNSSEC required
dnssec = cf(f"/zones/{zid}/dnssec")
if dnssec.get("status") != "active":
anomalies.append({"zone": zname, "type": "dnssec_disabled"})
return anomalies
def check_zone_security(zones):
anomalies = []
for z in zones:
zid = z["id"]
settings = cf(f"/zones/{zid}/settings/security_header")
hsts = settings.get("value", {}).get("strict_transport_security")
if not hsts or not hsts.get("enabled"):
anomalies.append({"zone": z["name"], "type": "hsts_disabled"})
return anomalies
def check_waf(zones):
anomalies = []
for z in zones:
zid = z["id"]
waf = cf(f"/zones/{zid}/firewall/waf/packages")
if not waf:
anomalies.append({"zone": z["name"], "type": "waf_missing"})
continue
# Require OWASP ruleset
if not any("owasp" in pkg.get("name", "").lower() for pkg in waf):
anomalies.append({"zone": z["name"], "type": "owasp_ruleset_missing"})
return anomalies
def check_access_policies():
anomalies = []
apps = cf(f"/accounts/{CF_ACCOUNT}/access/apps")
policies = cf(f"/accounts/{CF_ACCOUNT}/access/policies")
for p in policies:
if p.get("decision") == "bypass":
anomalies.append({"type": "access_policy_bypass", "policy": p})
if not any(r.get("require_mfa") for r in p.get("rules", [])):
anomalies.append({"type": "access_policy_missing_mfa", "policy": p})
return anomalies
def check_tunnels():
anomalies = []
tunnels = cf(f"/accounts/{CF_ACCOUNT}/cfd_tunnel")
for t in tunnels:
if t.get("status") not in ("healthy", "active"):
anomalies.append({"type": "tunnel_unhealthy", "tunnel": t})
return anomalies
# -------------------------------
# Main
# -------------------------------
def main():
anomalies = []
ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
zones = cf("/zones")
manifest = load_manifest()
anomalies += check_dns(zones, manifest)
anomalies += check_zone_security(zones)
anomalies += check_waf(zones)
anomalies += check_access_policies()
anomalies += check_tunnels()
anomaly_file = f"{ROOT}/anomalies/cf-invariants-{ts}.json"
with open(anomaly_file, "w") as f:
json.dump(anomalies, f, indent=2)
root = merkle_root(anomalies)
receipt_file = f"{ROOT}/receipts/cf-invariants-{ts}-{root[:8]}.json"
with open(receipt_file, "w") as f:
json.dump({"ts": ts, "merkle_root": root, "anomalies_file": anomaly_file}, f, indent=2)
print("Anomaly report:", anomaly_file)
print("Receipt:", receipt_file)
print("Merkle root:", root)
if __name__ == "__main__":
main()

400
scripts/seed_ide_rules.py Normal file
View File

@@ -0,0 +1,400 @@
#!/usr/bin/env python3
"""
IDE Operator Rules Seeder
Seeds operator rules into VS Code extension folders to provide
policy-aware guidance for AI assistants and code generation.
This script:
1. Finds VS Code extension directories
2. Copies/symlinks operator rules to the appropriate locations
3. Works across Mac, Linux, and Windows
4. Can watch for extension updates and auto-reseed
5. Verifies symlink integrity
Usage:
python seed_ide_rules.py # Auto-detect and seed
python seed_ide_rules.py --list # List target directories
python seed_ide_rules.py --symlink # Use symlinks instead of copy
python seed_ide_rules.py --dry-run # Show what would be done
python seed_ide_rules.py --watch # Watch for extension updates and auto-reseed
python seed_ide_rules.py --verify # Verify all symlinks are intact
"""
from __future__ import annotations
import argparse
import os
import platform
import shutil
import sys
import time
from pathlib import Path
from typing import List, Optional, Set, Tuple
# Source rules files to seed
RULES_FILES = [
"IDE_OPERATOR_RULES.md",
"AGENT_GUARDRAILS.md",
]
# Target extension patterns and their rule directories
EXTENSION_TARGETS = [
# Azure GitHub Copilot extension
{
"pattern": "ms-azuretools.vscode-azure-github-copilot-*",
"subdir": "resources/azureRules",
"target_name": "cloudflare.instructions.md",
},
# GitHub Copilot extension (if it has a rules dir)
{
"pattern": "github.copilot-*",
"subdir": "resources",
"target_name": "operator.instructions.md",
},
]
def get_vscode_extensions_dirs() -> List[Path]:
"""Get VS Code extension directories for the current platform."""
system = platform.system()
home = Path.home()
dirs: List[Path] = []
if system == "Darwin": # macOS
dirs = [
home / ".vscode" / "extensions",
home / ".vscode-insiders" / "extensions",
home / ".cursor" / "extensions", # Cursor editor
]
elif system == "Linux":
dirs = [
home / ".vscode" / "extensions",
home / ".vscode-server" / "extensions", # Remote SSH
home / ".vscode-insiders" / "extensions",
]
elif system == "Windows":
dirs = [
home / ".vscode" / "extensions",
home / ".vscode-insiders" / "extensions",
Path(os.environ.get("APPDATA", "")) / "Code" / "User" / "extensions",
]
return [d for d in dirs if d.exists()]
def find_target_extensions(base_dirs: List[Path]) -> List[Tuple[Path, dict]]:
"""Find matching extension directories."""
targets: List[Tuple[Path, dict]] = []
for base_dir in base_dirs:
for ext_config in EXTENSION_TARGETS:
pattern = ext_config["pattern"]
# Use glob to find matching extensions
for ext_path in base_dir.glob(pattern):
if ext_path.is_dir():
targets.append((ext_path, ext_config))
return targets
def get_source_rules_path() -> Path:
"""Get the path to the source rules file."""
# Try relative to this script first
script_dir = Path(__file__).parent.parent
for rules_file in RULES_FILES:
source = script_dir / rules_file
if source.exists():
return source
# Try current working directory
for rules_file in RULES_FILES:
source = Path.cwd() / rules_file
if source.exists():
return source
# Try parent of cwd (in case running from scripts/)
for rules_file in RULES_FILES:
source = Path.cwd().parent / rules_file
if source.exists():
return source
raise FileNotFoundError(
f"Could not find any of {RULES_FILES}. "
"Run this script from the CLOUDFLARE repo root."
)
def seed_rules(
source: Path,
targets: List[Tuple[Path, dict]],
use_symlink: bool = False,
dry_run: bool = False,
) -> List[str]:
"""Seed rules to target directories."""
results: List[str] = []
for ext_path, config in targets:
subdir = config["subdir"]
target_name = config["target_name"]
target_dir = ext_path / subdir
target_file = target_dir / target_name
# Create target directory if needed
if not dry_run:
target_dir.mkdir(parents=True, exist_ok=True)
action = "symlink" if use_symlink else "copy"
if dry_run:
results.append(f"[DRY RUN] Would {action}: {source}{target_file}")
continue
try:
# Remove existing file/symlink
if target_file.exists() or target_file.is_symlink():
target_file.unlink()
if use_symlink:
target_file.symlink_to(source.resolve())
results.append(f"✅ Symlinked: {target_file}")
else:
shutil.copy2(source, target_file)
results.append(f"✅ Copied: {target_file}")
except PermissionError:
results.append(f"❌ Permission denied: {target_file}")
except Exception as e:
results.append(f"❌ Failed: {target_file}{e}")
return results
def list_targets(targets: List[Tuple[Path, dict]]) -> None:
"""List all target directories."""
print("\n📁 Found VS Code extension targets:\n")
if not targets:
print(" No matching extensions found.")
print(" Install ms-azuretools.vscode-azure-github-copilot to enable seeding.")
return
for ext_path, config in targets:
print(f" 📦 {ext_path.name}")
print(f" Path: {ext_path}")
print(f" Target: {config['subdir']}/{config['target_name']}")
print()
def verify_symlinks(
targets: List[Tuple[Path, dict]],
source: Path,
) -> List[str]:
"""Verify all symlinks point to correct source."""
results: List[str] = []
for ext_path, config in targets:
target_file = ext_path / config["subdir"] / config["target_name"]
if target_file.is_symlink():
try:
if target_file.resolve() == source.resolve():
results.append(f"✅ Valid: {config['target_name']} in {ext_path.name}")
else:
results.append(
f"⚠️ Stale: {target_file.name}{target_file.resolve()}"
)
except OSError:
results.append(f"💀 Broken symlink: {target_file}")
elif target_file.exists():
results.append(f"📄 Copy (not symlink): {target_file.name} in {ext_path.name}")
else:
results.append(f"❌ Missing: {config['target_name']} in {ext_path.name}")
return results
def watch_and_reseed(
source: Path,
use_symlink: bool = True,
interval: int = 60,
) -> None:
"""Watch for new extensions and auto-reseed."""
print(f"👁️ Watching for extension updates (every {interval}s)...")
print(" Press Ctrl+C to stop\n")
known_extensions: Set[str] = set()
# Initial seed
base_dirs = get_vscode_extensions_dirs()
targets = find_target_extensions(base_dirs)
known_extensions = {str(t[0]) for t in targets}
results = seed_rules(source, targets, use_symlink=use_symlink)
seeded = sum(1 for r in results if r.startswith(""))
print(f"📊 Initial seed: {seeded}/{len(results)} targets")
while True:
try:
time.sleep(interval)
base_dirs = get_vscode_extensions_dirs()
targets = find_target_extensions(base_dirs)
current = {str(t[0]) for t in targets}
new_extensions = current - known_extensions
removed_extensions = known_extensions - current
if new_extensions:
print(f"\n🆕 {len(new_extensions)} new extension(s) detected")
# Only seed new ones
new_targets = [(p, c) for p, c in targets if str(p) in new_extensions]
results = seed_rules(source, new_targets, use_symlink=use_symlink)
for r in results:
print(f" {r}")
if removed_extensions:
print(f"\n🗑️ {len(removed_extensions)} extension(s) removed")
known_extensions = current
except KeyboardInterrupt:
print("\n\n👋 Stopped watching")
break
def main() -> int:
parser = argparse.ArgumentParser(
description="Seed IDE operator rules into VS Code extensions"
)
parser.add_argument(
"--list", "-l",
action="store_true",
help="List target extension directories",
)
parser.add_argument(
"--symlink", "-s",
action="store_true",
help="Use symlinks instead of copying files",
)
parser.add_argument(
"--dry-run", "-n",
action="store_true",
help="Show what would be done without making changes",
)
parser.add_argument(
"--watch", "-w",
action="store_true",
help="Watch for extension updates and auto-reseed (runs in foreground)",
)
parser.add_argument(
"--verify", "-v",
action="store_true",
help="Verify all symlinks are intact",
)
parser.add_argument(
"--interval",
type=int,
default=60,
help="Watch interval in seconds (default: 60)",
)
parser.add_argument(
"--source",
type=Path,
help="Source rules file (default: auto-detect)",
)
args = parser.parse_args()
# Find VS Code extension directories
base_dirs = get_vscode_extensions_dirs()
if not base_dirs:
print("❌ No VS Code extension directories found.")
print(" Make sure VS Code is installed.")
return 1
print(f"🔍 Searching in {len(base_dirs)} VS Code extension directories...")
# Find target extensions
targets = find_target_extensions(base_dirs)
if args.list:
list_targets(targets)
return 0
if not targets:
print("\n⚠️ No matching extensions found.")
print(" Install one of these extensions to enable rule seeding:")
print(" - ms-azuretools.vscode-azure-github-copilot")
print(" - github.copilot")
return 1
# Get source file
try:
source = args.source or get_source_rules_path()
except FileNotFoundError as e:
print(f"{e}")
return 1
# Handle --verify
if args.verify:
print(f"📄 Source: {source}")
print(f"🔍 Verifying {len(targets)} target(s)...\n")
results = verify_symlinks(targets, source)
print("\n".join(results))
valid = sum(1 for r in results if r.startswith(""))
stale = sum(1 for r in results if r.startswith("⚠️"))
missing = sum(1 for r in results if r.startswith(""))
broken = sum(1 for r in results if r.startswith("💀"))
print(f"\n📊 {valid}/{len(results)} symlinks valid")
if stale:
print(f" ⚠️ {stale} stale (run --symlink to fix)")
if missing:
print(f"{missing} missing (run --symlink to create)")
if broken:
print(f" 💀 {broken} broken (run --symlink to fix)")
return 0 if (missing == 0 and broken == 0) else 1
# Handle --watch
if args.watch:
print(f"📄 Source: {source}")
watch_and_reseed(source, use_symlink=True, interval=args.interval)
return 0
print(f"📄 Source: {source}")
print(f"🎯 Found {len(targets)} target extension(s)")
if args.dry_run:
print("\n🔍 Dry run mode — no changes will be made\n")
# Seed the rules
results = seed_rules(
source=source,
targets=targets,
use_symlink=args.symlink,
dry_run=args.dry_run,
)
print("\n" + "\n".join(results))
# Summary
success = sum(1 for r in results if r.startswith(""))
failed = sum(1 for r in results if r.startswith(""))
if not args.dry_run:
print(f"\n📊 Seeded {success}/{len(results)} targets")
if failed:
print(f" ⚠️ {failed} failed — check permissions")
return 0 if failed == 0 else 1
if __name__ == "__main__":
sys.exit(main())

408
scripts/state-reconciler.py Normal file
View File

@@ -0,0 +1,408 @@
#!/usr/bin/env python3
"""
Cloudflare State Reconciler
Fetches live Cloudflare configuration and produces cryptographically verifiable snapshots.
Usage:
python3 state-reconciler.py --zone-id <ZONE_ID> --account-id <ACCOUNT_ID>
Environment Variables:
CLOUDFLARE_API_TOKEN - API token with read permissions
CLOUDFLARE_ZONE_ID - Zone ID (optional, can use --zone-id)
CLOUDFLARE_ACCOUNT_ID - Account ID (optional, can use --account-id)
Output:
- snapshots/cloudflare-<timestamp>.json
- receipts/cf-state-<timestamp>.json
"""
import argparse
import hashlib
import json
import os
import sys
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional
import requests
# Configuration
CF_API_BASE = "https://api.cloudflare.com/client/v4"
SNAPSHOT_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "snapshots")
RECEIPT_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "receipts")
class CloudflareClient:
"""Cloudflare API client for state fetching."""
def __init__(self, api_token: str):
self.api_token = api_token
self.session = requests.Session()
self.session.headers.update({
"Authorization": f"Bearer {api_token}",
"Content-Type": "application/json"
})
def _request(self, method: str, endpoint: str, **kwargs) -> Dict[str, Any]:
"""Make API request with error handling."""
url = f"{CF_API_BASE}{endpoint}"
response = self.session.request(method, url, **kwargs)
response.raise_for_status()
data = response.json()
if not data.get("success", False):
errors = data.get("errors", [])
raise Exception(f"Cloudflare API error: {errors}")
return data
def _paginate(self, endpoint: str) -> List[Dict[str, Any]]:
"""Fetch all pages of a paginated endpoint."""
results = []
page = 1
per_page = 100
while True:
data = self._request("GET", endpoint, params={"page": page, "per_page": per_page})
results.extend(data.get("result", []))
result_info = data.get("result_info", {})
total_pages = result_info.get("total_pages", 1)
if page >= total_pages:
break
page += 1
return results
# DNS
def get_dns_records(self, zone_id: str) -> List[Dict[str, Any]]:
"""Fetch all DNS records for a zone."""
return self._paginate(f"/zones/{zone_id}/dns_records")
def get_dnssec(self, zone_id: str) -> Dict[str, Any]:
"""Fetch DNSSEC status for a zone."""
data = self._request("GET", f"/zones/{zone_id}/dnssec")
return data.get("result", {})
# Zone Settings
def get_zone_settings(self, zone_id: str) -> List[Dict[str, Any]]:
"""Fetch all zone settings."""
data = self._request("GET", f"/zones/{zone_id}/settings")
return data.get("result", [])
def get_zone_info(self, zone_id: str) -> Dict[str, Any]:
"""Fetch zone information."""
data = self._request("GET", f"/zones/{zone_id}")
return data.get("result", {})
# WAF / Firewall
def get_firewall_rules(self, zone_id: str) -> List[Dict[str, Any]]:
"""Fetch firewall rules."""
return self._paginate(f"/zones/{zone_id}/firewall/rules")
def get_rulesets(self, zone_id: str) -> List[Dict[str, Any]]:
"""Fetch zone rulesets."""
data = self._request("GET", f"/zones/{zone_id}/rulesets")
return data.get("result", [])
# Access
def get_access_apps(self, account_id: str) -> List[Dict[str, Any]]:
"""Fetch Access applications."""
return self._paginate(f"/accounts/{account_id}/access/apps")
def get_access_policies(self, account_id: str, app_id: str) -> List[Dict[str, Any]]:
"""Fetch policies for an Access application."""
return self._paginate(f"/accounts/{account_id}/access/apps/{app_id}/policies")
# Tunnels
def get_tunnels(self, account_id: str) -> List[Dict[str, Any]]:
"""Fetch Cloudflare Tunnels."""
return self._paginate(f"/accounts/{account_id}/cfd_tunnel")
def get_tunnel_connections(self, account_id: str, tunnel_id: str) -> List[Dict[str, Any]]:
"""Fetch tunnel connections."""
data = self._request("GET", f"/accounts/{account_id}/cfd_tunnel/{tunnel_id}/connections")
return data.get("result", [])
# Logpush
def get_logpush_jobs(self, zone_id: str) -> List[Dict[str, Any]]:
"""Fetch Logpush jobs."""
data = self._request("GET", f"/zones/{zone_id}/logpush/jobs")
return data.get("result", [])
# API Tokens (metadata only)
def get_api_tokens(self) -> List[Dict[str, Any]]:
"""Fetch API token metadata (not secrets)."""
data = self._request("GET", "/user/tokens")
return data.get("result", [])
def compute_sha256(data: Any) -> str:
"""Compute SHA-256 hash of JSON-serialized data."""
serialized = json.dumps(data, sort_keys=True, separators=(",", ":"))
return hashlib.sha256(serialized.encode()).hexdigest()
def compute_merkle_root(hashes: List[str]) -> str:
"""Compute Merkle root from list of hashes."""
if not hashes:
return hashlib.sha256(b"").hexdigest()
# Pad to power of 2
while len(hashes) & (len(hashes) - 1) != 0:
hashes.append(hashes[-1])
while len(hashes) > 1:
new_level = []
for i in range(0, len(hashes), 2):
combined = hashes[i] + hashes[i + 1]
new_level.append(hashlib.sha256(combined.encode()).hexdigest())
hashes = new_level
return hashes[0]
def normalize_dns_record(record: Dict[str, Any]) -> Dict[str, Any]:
"""Normalize DNS record for consistent hashing."""
return {
"id": record.get("id"),
"type": record.get("type"),
"name": record.get("name"),
"content": record.get("content"),
"proxied": record.get("proxied"),
"ttl": record.get("ttl"),
"priority": record.get("priority"),
"created_on": record.get("created_on"),
"modified_on": record.get("modified_on"),
}
def normalize_tunnel(tunnel: Dict[str, Any]) -> Dict[str, Any]:
"""Normalize tunnel for consistent hashing."""
return {
"id": tunnel.get("id"),
"name": tunnel.get("name"),
"status": tunnel.get("status"),
"created_at": tunnel.get("created_at"),
"deleted_at": tunnel.get("deleted_at"),
"remote_config": tunnel.get("remote_config"),
}
def normalize_access_app(app: Dict[str, Any]) -> Dict[str, Any]:
"""Normalize Access app for consistent hashing."""
return {
"id": app.get("id"),
"name": app.get("name"),
"domain": app.get("domain"),
"type": app.get("type"),
"session_duration": app.get("session_duration"),
"auto_redirect_to_identity": app.get("auto_redirect_to_identity"),
"created_at": app.get("created_at"),
"updated_at": app.get("updated_at"),
}
def fetch_cloudflare_state(
client: CloudflareClient,
zone_id: str,
account_id: str
) -> Dict[str, Any]:
"""Fetch complete Cloudflare state."""
state = {
"metadata": {
"zone_id": zone_id,
"account_id": account_id,
"fetched_at": datetime.now(timezone.utc).isoformat(),
"schema_version": "cf_state_v1",
},
"dns": {},
"zone_settings": {},
"waf": {},
"access": {},
"tunnels": {},
"logpush": {},
"api_tokens": {},
}
print("Fetching zone info...")
state["zone_info"] = client.get_zone_info(zone_id)
print("Fetching DNS records...")
raw_dns = client.get_dns_records(zone_id)
state["dns"]["records"] = [normalize_dns_record(r) for r in raw_dns]
state["dns"]["dnssec"] = client.get_dnssec(zone_id)
print("Fetching zone settings...")
settings = client.get_zone_settings(zone_id)
state["zone_settings"] = {s["id"]: s["value"] for s in settings}
print("Fetching firewall rules...")
state["waf"]["firewall_rules"] = client.get_firewall_rules(zone_id)
state["waf"]["rulesets"] = client.get_rulesets(zone_id)
print("Fetching Access apps...")
access_apps = client.get_access_apps(account_id)
state["access"]["apps"] = []
for app in access_apps:
normalized = normalize_access_app(app)
normalized["policies"] = client.get_access_policies(account_id, app["id"])
state["access"]["apps"].append(normalized)
print("Fetching tunnels...")
tunnels = client.get_tunnels(account_id)
state["tunnels"]["list"] = []
for tunnel in tunnels:
normalized = normalize_tunnel(tunnel)
if tunnel.get("status") != "deleted":
normalized["connections"] = client.get_tunnel_connections(account_id, tunnel["id"])
state["tunnels"]["list"].append(normalized)
print("Fetching Logpush jobs...")
state["logpush"]["jobs"] = client.get_logpush_jobs(zone_id)
print("Fetching API token metadata...")
tokens = client.get_api_tokens()
# Remove sensitive fields
state["api_tokens"]["list"] = [
{
"id": t.get("id"),
"name": t.get("name"),
"status": t.get("status"),
"issued_on": t.get("issued_on"),
"modified_on": t.get("modified_on"),
"not_before": t.get("not_before"),
"expires_on": t.get("expires_on"),
}
for t in tokens
]
return state
def compute_state_hashes(state: Dict[str, Any]) -> Dict[str, str]:
"""Compute per-section hashes."""
sections = ["dns", "zone_settings", "waf", "access", "tunnels", "logpush", "api_tokens"]
hashes = {}
for section in sections:
if section in state:
hashes[section] = compute_sha256(state[section])
return hashes
def create_snapshot(state: Dict[str, Any], section_hashes: Dict[str, str], merkle_root: str) -> Dict[str, Any]:
"""Create complete snapshot with integrity data."""
return {
"snapshot_version": "1.0.0",
"created_at": datetime.now(timezone.utc).isoformat(),
"state": state,
"integrity": {
"section_hashes": section_hashes,
"merkle_root": merkle_root,
"hash_algorithm": "sha256",
}
}
def create_receipt(
snapshot_path: str,
merkle_root: str,
zone_id: str,
account_id: str
) -> Dict[str, Any]:
"""Create VaultMesh receipt for state snapshot."""
return {
"receipt_type": "cf_state_snapshot",
"schema_version": "vm_cf_snapshot_v1",
"timestamp": datetime.now(timezone.utc).isoformat(),
"zone_id": zone_id,
"account_id": account_id,
"snapshot_path": snapshot_path,
"merkle_root": merkle_root,
"hash_algorithm": "sha256",
}
def main():
parser = argparse.ArgumentParser(description="Cloudflare State Reconciler")
parser.add_argument("--zone-id", default=os.environ.get("CLOUDFLARE_ZONE_ID"),
help="Cloudflare Zone ID")
parser.add_argument("--account-id", default=os.environ.get("CLOUDFLARE_ACCOUNT_ID"),
help="Cloudflare Account ID")
parser.add_argument("--output-dir", default=SNAPSHOT_DIR,
help="Output directory for snapshots")
parser.add_argument("--receipt-dir", default=RECEIPT_DIR,
help="Output directory for receipts")
args = parser.parse_args()
# Validate inputs
api_token = os.environ.get("CLOUDFLARE_API_TOKEN")
if not api_token:
print("Error: CLOUDFLARE_API_TOKEN environment variable required", file=sys.stderr)
sys.exit(1)
if not args.zone_id:
print("Error: Zone ID required (--zone-id or CLOUDFLARE_ZONE_ID)", file=sys.stderr)
sys.exit(1)
if not args.account_id:
print("Error: Account ID required (--account-id or CLOUDFLARE_ACCOUNT_ID)", file=sys.stderr)
sys.exit(1)
# Ensure output directories exist
os.makedirs(args.output_dir, exist_ok=True)
os.makedirs(args.receipt_dir, exist_ok=True)
# Initialize client
client = CloudflareClient(api_token)
# Fetch state
print(f"Fetching Cloudflare state for zone {args.zone_id}...")
state = fetch_cloudflare_state(client, args.zone_id, args.account_id)
# Compute hashes
print("Computing integrity hashes...")
section_hashes = compute_state_hashes(state)
merkle_root = compute_merkle_root(list(section_hashes.values()))
# Create snapshot
snapshot = create_snapshot(state, section_hashes, merkle_root)
# Write snapshot
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H-%M-%SZ")
snapshot_filename = f"cloudflare-{timestamp}.json"
snapshot_path = os.path.join(args.output_dir, snapshot_filename)
with open(snapshot_path, "w") as f:
json.dump(snapshot, f, indent=2, sort_keys=True)
print(f"Snapshot written to: {snapshot_path}")
# Create and write receipt
receipt = create_receipt(snapshot_path, merkle_root, args.zone_id, args.account_id)
receipt_filename = f"cf-state-{timestamp}.json"
receipt_path = os.path.join(args.receipt_dir, receipt_filename)
with open(receipt_path, "w") as f:
json.dump(receipt, f, indent=2, sort_keys=True)
print(f"Receipt written to: {receipt_path}")
# Summary
print("\n=== State Reconciler Summary ===")
print(f"Zone ID: {args.zone_id}")
print(f"Account ID: {args.account_id}")
print(f"Merkle Root: {merkle_root}")
print(f"DNS Records: {len(state['dns'].get('records', []))}")
print(f"Access Apps: {len(state['access'].get('apps', []))}")
print(f"Tunnels: {len(state['tunnels'].get('list', []))}")
print(f"Snapshot: {snapshot_filename}")
print(f"Receipt: {receipt_filename}")
# Output merkle root for piping
print(f"\nMERKLE_ROOT={merkle_root}")
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,155 @@
#!/usr/bin/env python3
"""
Cloudflare State Reconciler (Pure Technical)
Generates a canonical JSON snapshot + Merkle root representing:
- DNS records
- DNSSEC + registrar lock status
- WAF rules
- Firewall rules
- Zero-Trust Access apps + policies
- Tunnels + status metadata
- API token metadata (non-secret)
Outputs:
snapshots/cloudflare-<ts>.json
receipts/cloudflare-state-<ts>-<merkle>.json
"""
import os
import json
import hashlib
import requests
from datetime import datetime, timezone
CF_API = "https://api.cloudflare.com/client/v4"
CF_TOKEN = os.getenv("CF_API_TOKEN")
CF_ACCOUNT = os.getenv("CF_ACCOUNT_ID")
OUT_ROOT = os.getenv("VM_STATE_ROOT", "./cloudflare_state")
HEADERS = {
"Authorization": f"Bearer {CF_TOKEN}",
"Content-Type": "application/json",
}
os.makedirs(f"{OUT_ROOT}/snapshots", exist_ok=True)
os.makedirs(f"{OUT_ROOT}/receipts", exist_ok=True)
def merkle_root(obj):
data = json.dumps(obj, sort_keys=True).encode()
return hashlib.sha256(data).hexdigest()
def cf(endpoint):
r = requests.get(f"{CF_API}{endpoint}", headers=HEADERS)
r.raise_for_status()
return r.json().get("result", {})
# -------------------------------
# Fetch Cloudflare State Sections
# -------------------------------
def fetch_dns(zones):
items = {}
for z in zones:
zid = z["id"]
rec = cf(f"/zones/{zid}/dns_records")
items[z["name"]] = rec
return items
def fetch_zones():
return cf(f"/zones")
def fetch_waf(zones):
rules = {}
for z in zones:
zid = z["id"]
waf = cf(f"/zones/{zid}/firewall/waf/packages")
rules[z["name"]] = waf
return rules
def fetch_firewall_rules(zones):
fr = {}
for z in zones:
zid = z["id"]
rules = cf(f"/zones/{zid}/firewall/rules")
fr[z["name"]] = rules
return fr
def fetch_tunnels():
return cf(f"/accounts/{CF_ACCOUNT}/cfd_tunnel")
def fetch_access_apps():
return cf(f"/accounts/{CF_ACCOUNT}/access/apps")
def fetch_access_policies():
return cf(f"/accounts/{CF_ACCOUNT}/access/policies")
def fetch_api_tokens():
# Metadata only, not secrets
r = requests.get(f"{CF_API}/user/tokens", headers=HEADERS)
if r.status_code != 200:
return []
return r.json().get("result", [])
# -------------------------------
# Snapshot Assembly
# -------------------------------
def build_snapshot():
zones = fetch_zones()
snapshot = {
"ts": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
"zones": zones,
"dns": fetch_dns(zones),
"waf": fetch_waf(zones),
"firewall_rules": fetch_firewall_rules(zones),
"access_apps": fetch_access_apps(),
"access_policies": fetch_access_policies(),
"tunnels": fetch_tunnels(),
"api_tokens": fetch_api_tokens(),
}
return snapshot
# -------------------------------
# Main
# -------------------------------
def main():
snap = build_snapshot()
root = merkle_root(snap)
ts = snap["ts"].replace(":", "-")
snap_path = f"{OUT_ROOT}/snapshots/cloudflare-{ts}.json"
with open(snap_path, "w") as f:
json.dump(snap, f, indent=2)
receipt = {
"ts": snap["ts"],
"merkle_root": root,
"snapshot_file": os.path.basename(snap_path)
}
receipt_path = f"{OUT_ROOT}/receipts/cloudflare-state-{ts}-{root[:8]}.json"
with open(receipt_path, "w") as f:
json.dump(receipt, f, indent=2)
print("Snapshot:", snap_path)
print("Receipt:", receipt_path)
print("Merkle root:", root)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,377 @@
#!/usr/bin/env python3
"""
Tunnel Rotation Scheduler
Automatically rotates Cloudflare Tunnel credentials based on age policy.
Usage:
python3 tunnel-rotation-scheduler.py --account-id <ACCOUNT_ID>
Environment Variables:
CLOUDFLARE_API_TOKEN - API token with Tunnel permissions
CLOUDFLARE_ACCOUNT_ID - Account ID (or use --account-id)
TUNNEL_MAX_AGE_DAYS - Maximum tunnel credential age (default: 90)
Output:
- Creates new tunnel with fresh credentials
- Updates DNS routes
- Destroys old tunnel
- Emits rotation receipts
"""
import argparse
import base64
import hashlib
import json
import os
import secrets
import subprocess
import sys
from datetime import datetime, timezone, timedelta
from typing import Any, Dict, List, Optional, Tuple
import requests
# Configuration
CF_API_BASE = "https://api.cloudflare.com/client/v4"
RECEIPT_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "receipts")
DEFAULT_MAX_AGE_DAYS = 90
class TunnelRotator:
"""Handles Cloudflare Tunnel credential rotation."""
def __init__(self, api_token: str, account_id: str, max_age_days: int = DEFAULT_MAX_AGE_DAYS):
self.api_token = api_token
self.account_id = account_id
self.max_age_days = max_age_days
self.session = requests.Session()
self.session.headers.update({
"Authorization": f"Bearer {api_token}",
"Content-Type": "application/json"
})
self.rotations: List[Dict[str, Any]] = []
def _request(self, method: str, endpoint: str, **kwargs) -> Dict[str, Any]:
"""Make API request with error handling."""
url = f"{CF_API_BASE}{endpoint}"
response = self.session.request(method, url, **kwargs)
response.raise_for_status()
data = response.json()
if not data.get("success", False):
errors = data.get("errors", [])
raise Exception(f"Cloudflare API error: {errors}")
return data
def get_tunnels(self) -> List[Dict[str, Any]]:
"""Fetch all tunnels for the account."""
data = self._request("GET", f"/accounts/{self.account_id}/cfd_tunnel")
return data.get("result", [])
def get_tunnel_by_name(self, name: str) -> Optional[Dict[str, Any]]:
"""Find tunnel by name."""
tunnels = self.get_tunnels()
for t in tunnels:
if t.get("name") == name and not t.get("deleted_at"):
return t
return None
def check_tunnel_age(self, tunnel: Dict[str, Any]) -> Tuple[int, bool]:
"""Check tunnel age and whether rotation is needed."""
created_at = tunnel.get("created_at")
if not created_at:
return 0, False
created = datetime.fromisoformat(created_at.replace("Z", "+00:00"))
age = datetime.now(timezone.utc) - created
age_days = age.days
needs_rotation = age_days >= self.max_age_days
return age_days, needs_rotation
def generate_tunnel_secret(self) -> str:
"""Generate cryptographically secure tunnel secret."""
return base64.b64encode(secrets.token_bytes(32)).decode()
def create_tunnel(self, name: str, secret: str) -> Dict[str, Any]:
"""Create a new tunnel."""
data = self._request(
"POST",
f"/accounts/{self.account_id}/cfd_tunnel",
json={
"name": name,
"tunnel_secret": secret,
}
)
return data.get("result", {})
def delete_tunnel(self, tunnel_id: str) -> bool:
"""Delete a tunnel."""
try:
self._request("DELETE", f"/accounts/{self.account_id}/cfd_tunnel/{tunnel_id}")
return True
except Exception as e:
print(f"Warning: Failed to delete tunnel {tunnel_id}: {e}")
return False
def get_tunnel_routes(self, tunnel_id: str) -> List[Dict[str, Any]]:
"""Get DNS routes for a tunnel."""
try:
data = self._request(
"GET",
f"/accounts/{self.account_id}/cfd_tunnel/{tunnel_id}/configurations"
)
config = data.get("result", {}).get("config", {})
return config.get("ingress", [])
except Exception:
return []
def update_dns_route(self, zone_id: str, hostname: str, tunnel_id: str) -> bool:
"""Update DNS CNAME to point to new tunnel."""
tunnel_cname = f"{tunnel_id}.cfargotunnel.com"
# Find existing record
records_data = self._request(
"GET",
f"/zones/{zone_id}/dns_records",
params={"name": hostname, "type": "CNAME"}
)
records = records_data.get("result", [])
if records:
# Update existing
record_id = records[0]["id"]
self._request(
"PATCH",
f"/zones/{zone_id}/dns_records/{record_id}",
json={"content": tunnel_cname}
)
else:
# Create new
self._request(
"POST",
f"/zones/{zone_id}/dns_records",
json={
"type": "CNAME",
"name": hostname,
"content": tunnel_cname,
"proxied": True
}
)
return True
def rotate_tunnel(
self,
old_tunnel: Dict[str, Any],
zone_id: Optional[str] = None,
hostnames: Optional[List[str]] = None,
dry_run: bool = False
) -> Dict[str, Any]:
"""Rotate a tunnel to fresh credentials."""
old_id = old_tunnel["id"]
old_name = old_tunnel["name"]
timestamp = datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S")
new_name = f"{old_name.split('-')[0]}-{timestamp}"
print(f"Rotating tunnel: {old_name} -> {new_name}")
rotation_record = {
"old_tunnel_id": old_id,
"old_tunnel_name": old_name,
"timestamp": datetime.now(timezone.utc).isoformat(),
"status": "pending",
}
if dry_run:
print(" [DRY RUN] Would create new tunnel and update routes")
rotation_record["status"] = "dry_run"
rotation_record["actions"] = ["create_tunnel", "update_routes", "delete_old"]
self.rotations.append(rotation_record)
return rotation_record
try:
# Generate new secret
new_secret = self.generate_tunnel_secret()
# Create new tunnel
new_tunnel = self.create_tunnel(new_name, new_secret)
new_id = new_tunnel["id"]
rotation_record["new_tunnel_id"] = new_id
rotation_record["new_tunnel_name"] = new_name
print(f" Created new tunnel: {new_id}")
# Update DNS routes if zone and hostnames provided
if zone_id and hostnames:
for hostname in hostnames:
try:
self.update_dns_route(zone_id, hostname, new_id)
print(f" Updated DNS route: {hostname}")
except Exception as e:
print(f" Warning: Failed to update route {hostname}: {e}")
# Wait for propagation (in production, would verify connectivity)
print(" Waiting for propagation...")
# Delete old tunnel
if self.delete_tunnel(old_id):
print(f" Deleted old tunnel: {old_id}")
rotation_record["old_tunnel_deleted"] = True
else:
rotation_record["old_tunnel_deleted"] = False
rotation_record["status"] = "success"
rotation_record["new_secret_hash"] = hashlib.sha256(new_secret.encode()).hexdigest()[:16]
except Exception as e:
rotation_record["status"] = "failed"
rotation_record["error"] = str(e)
print(f" Error: {e}")
self.rotations.append(rotation_record)
return rotation_record
def scan_and_rotate(
self,
zone_id: Optional[str] = None,
hostname_map: Optional[Dict[str, List[str]]] = None,
dry_run: bool = False
) -> List[Dict[str, Any]]:
"""Scan all tunnels and rotate those exceeding max age."""
print(f"Scanning tunnels (max age: {self.max_age_days} days)...")
tunnels = self.get_tunnels()
for tunnel in tunnels:
if tunnel.get("deleted_at"):
continue
name = tunnel.get("name", "unknown")
age_days, needs_rotation = self.check_tunnel_age(tunnel)
status = "NEEDS ROTATION" if needs_rotation else "OK"
print(f" {name}: {age_days} days old [{status}]")
if needs_rotation:
hostnames = hostname_map.get(name, []) if hostname_map else None
self.rotate_tunnel(tunnel, zone_id, hostnames, dry_run)
return self.rotations
def create_rotation_receipt(rotations: List[Dict[str, Any]], account_id: str) -> Dict[str, Any]:
"""Create VaultMesh receipt for rotation cycle."""
successful = [r for r in rotations if r.get("status") == "success"]
failed = [r for r in rotations if r.get("status") == "failed"]
return {
"receipt_type": "tunnel_rotation_cycle",
"schema_version": "vm_tunnel_rotation_v1",
"timestamp": datetime.now(timezone.utc).isoformat(),
"account_id": account_id,
"summary": {
"total_rotated": len(successful),
"failed": len(failed),
"skipped": len(rotations) - len(successful) - len(failed),
},
"rotations": rotations,
"cycle_hash": hashlib.sha256(
json.dumps(rotations, sort_keys=True).encode()
).hexdigest(),
}
def main():
parser = argparse.ArgumentParser(description="Tunnel Rotation Scheduler")
parser.add_argument("--account-id", default=os.environ.get("CLOUDFLARE_ACCOUNT_ID"),
help="Cloudflare Account ID")
parser.add_argument("--zone-id", default=os.environ.get("CLOUDFLARE_ZONE_ID"),
help="Zone ID for DNS route updates")
parser.add_argument("--max-age", type=int,
default=int(os.environ.get("TUNNEL_MAX_AGE_DAYS", DEFAULT_MAX_AGE_DAYS)),
help=f"Maximum tunnel age in days (default: {DEFAULT_MAX_AGE_DAYS})")
parser.add_argument("--tunnel-name", help="Rotate specific tunnel by name")
parser.add_argument("--dry-run", action="store_true", help="Simulate rotation without changes")
parser.add_argument("--force", action="store_true", help="Force rotation regardless of age")
parser.add_argument("--output-dir", default=RECEIPT_DIR, help="Output directory for receipts")
args = parser.parse_args()
# Validate inputs
api_token = os.environ.get("CLOUDFLARE_API_TOKEN")
if not api_token:
print("Error: CLOUDFLARE_API_TOKEN environment variable required", file=sys.stderr)
sys.exit(1)
if not args.account_id:
print("Error: Account ID required (--account-id or CLOUDFLARE_ACCOUNT_ID)", file=sys.stderr)
sys.exit(1)
# Ensure output directory exists
os.makedirs(args.output_dir, exist_ok=True)
# Initialize rotator
rotator = TunnelRotator(api_token, args.account_id, args.max_age)
print("=" * 50)
print("Tunnel Rotation Scheduler")
print("=" * 50)
print(f"Account ID: {args.account_id}")
print(f"Max Age: {args.max_age} days")
print(f"Dry Run: {args.dry_run}")
print(f"Force: {args.force}")
print("")
if args.tunnel_name:
# Rotate specific tunnel
tunnel = rotator.get_tunnel_by_name(args.tunnel_name)
if not tunnel:
print(f"Error: Tunnel '{args.tunnel_name}' not found", file=sys.stderr)
sys.exit(1)
if args.force:
rotator.rotate_tunnel(tunnel, args.zone_id, dry_run=args.dry_run)
else:
age_days, needs_rotation = rotator.check_tunnel_age(tunnel)
if needs_rotation:
rotator.rotate_tunnel(tunnel, args.zone_id, dry_run=args.dry_run)
else:
print(f"Tunnel '{args.tunnel_name}' is {age_days} days old, no rotation needed")
else:
# Scan and rotate all
rotator.scan_and_rotate(args.zone_id, dry_run=args.dry_run)
# Generate receipt
if rotator.rotations:
receipt = create_rotation_receipt(rotator.rotations, args.account_id)
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H-%M-%SZ")
receipt_filename = f"tunnel-rotation-{timestamp}.json"
receipt_path = os.path.join(args.output_dir, receipt_filename)
with open(receipt_path, "w") as f:
json.dump(receipt, f, indent=2, sort_keys=True)
print("")
print(f"Receipt written to: {receipt_path}")
# Summary
print("")
print("=" * 50)
print("Rotation Summary")
print("=" * 50)
successful = [r for r in rotator.rotations if r.get("status") == "success"]
failed = [r for r in rotator.rotations if r.get("status") == "failed"]
dry_runs = [r for r in rotator.rotations if r.get("status") == "dry_run"]
print(f"Successful: {len(successful)}")
print(f"Failed: {len(failed)}")
print(f"Dry Run: {len(dry_runs)}")
if failed:
print("")
print("Failed rotations:")
for r in failed:
print(f" - {r.get('old_tunnel_name')}: {r.get('error')}")
# Exit code
return 0 if len(failed) == 0 else 1
if __name__ == "__main__":
sys.exit(main())