Files
vm-cloudflare/observatory/drift-visualizer.py
Vault Sovereign 37a867c485 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
2025-12-16 18:31:53 +00:00

345 lines
12 KiB
Python

#!/usr/bin/env python3
"""
Drift Visualizer
Compares Terraform state, DNS manifest, and live Cloudflare configuration.
Outputs JSON diff and HTML report.
Usage:
python3 drift-visualizer.py --snapshot <path> --manifest <path> --output <dir>
"""
import argparse
import html
import json
import os
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional, Set, Tuple
OUTPUT_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "reports")
class DriftAnalyzer:
"""Analyzes drift between different state sources."""
def __init__(self):
self.diffs: List[Dict[str, Any]] = []
def compare_dns_records(
self,
source_name: str,
source_records: List[Dict],
target_name: str,
target_records: List[Dict]
) -> List[Dict[str, Any]]:
"""Compare DNS records between two sources."""
diffs = []
# Build lookup maps
source_map = {(r.get("type"), r.get("name")): r for r in source_records}
target_map = {(r.get("type"), r.get("name")): r for r in target_records}
all_keys = set(source_map.keys()) | set(target_map.keys())
for key in all_keys:
rtype, name = key
source_rec = source_map.get(key)
target_rec = target_map.get(key)
if source_rec and not target_rec:
diffs.append({
"type": "missing",
"source": source_name,
"target": target_name,
"record_type": rtype,
"record_name": name,
"detail": f"Record exists in {source_name} but not in {target_name}",
"severity": "high",
})
elif target_rec and not source_rec:
diffs.append({
"type": "extra",
"source": source_name,
"target": target_name,
"record_type": rtype,
"record_name": name,
"detail": f"Record exists in {target_name} but not in {source_name}",
"severity": "medium",
})
else:
# Both exist - check for content/config drift
content_diff = self._compare_record_content(source_rec, target_rec)
if content_diff:
diffs.append({
"type": "modified",
"source": source_name,
"target": target_name,
"record_type": rtype,
"record_name": name,
"detail": content_diff,
"source_value": source_rec,
"target_value": target_rec,
"severity": "medium",
})
return diffs
def _compare_record_content(self, rec1: Dict, rec2: Dict) -> Optional[str]:
"""Compare record content and return diff description."""
diffs = []
if rec1.get("content") != rec2.get("content"):
diffs.append(f"content: {rec1.get('content')} -> {rec2.get('content')}")
if rec1.get("proxied") != rec2.get("proxied"):
diffs.append(f"proxied: {rec1.get('proxied')} -> {rec2.get('proxied')}")
if rec1.get("ttl") != rec2.get("ttl"):
diffs.append(f"ttl: {rec1.get('ttl')} -> {rec2.get('ttl')}")
return "; ".join(diffs) if diffs else None
def compare_settings(
self,
source_name: str,
source_settings: Dict,
target_name: str,
target_settings: Dict
) -> List[Dict[str, Any]]:
"""Compare zone settings."""
diffs = []
all_keys = set(source_settings.keys()) | set(target_settings.keys())
for key in all_keys:
src_val = source_settings.get(key)
tgt_val = target_settings.get(key)
if src_val != tgt_val:
diffs.append({
"type": "setting_drift",
"source": source_name,
"target": target_name,
"setting": key,
"source_value": src_val,
"target_value": tgt_val,
"severity": "medium" if key in ("ssl", "min_tls_version") else "low",
})
return diffs
def analyze(
self,
snapshot: Optional[Dict] = None,
manifest: Optional[Dict] = None,
terraform_state: Optional[Dict] = None
) -> Dict[str, Any]:
"""Run full drift analysis."""
self.diffs = []
comparisons = []
# Snapshot vs Manifest
if snapshot and manifest:
snapshot_dns = snapshot.get("state", {}).get("dns", {}).get("records", [])
manifest_dns = manifest.get("records", [])
dns_diffs = self.compare_dns_records(
"manifest", manifest_dns,
"cloudflare", snapshot_dns
)
self.diffs.extend(dns_diffs)
comparisons.append("manifest_vs_cloudflare")
# Summary
high = len([d for d in self.diffs if d.get("severity") == "high"])
medium = len([d for d in self.diffs if d.get("severity") == "medium"])
low = len([d for d in self.diffs if d.get("severity") == "low"])
return {
"analysis_type": "drift_report",
"timestamp": datetime.now(timezone.utc).isoformat(),
"comparisons": comparisons,
"summary": {
"total_diffs": len(self.diffs),
"high_severity": high,
"medium_severity": medium,
"low_severity": low,
"drift_detected": len(self.diffs) > 0,
},
"diffs": self.diffs,
}
def generate_html_report(analysis: Dict[str, Any]) -> str:
"""Generate HTML visualization of drift report."""
timestamp = analysis.get("timestamp", "")
summary = analysis.get("summary", {})
diffs = analysis.get("diffs", [])
# CSS styles
css = """
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
max-width: 1200px; margin: 0 auto; padding: 20px; background: #0d1117; color: #c9d1d9; }
h1 { color: #58a6ff; border-bottom: 1px solid #30363d; padding-bottom: 10px; }
h2 { color: #8b949e; }
.summary { display: flex; gap: 20px; margin: 20px 0; }
.card { background: #161b22; padding: 20px; border-radius: 8px; border: 1px solid #30363d; flex: 1; }
.card h3 { margin-top: 0; color: #58a6ff; }
.stat { font-size: 2em; font-weight: bold; }
.high { color: #f85149; }
.medium { color: #d29922; }
.low { color: #3fb950; }
.ok { color: #3fb950; }
table { width: 100%; border-collapse: collapse; margin: 20px 0; }
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #30363d; }
th { background: #161b22; color: #8b949e; }
tr:hover { background: #161b22; }
.badge { padding: 4px 8px; border-radius: 4px; font-size: 0.8em; font-weight: bold; }
.badge-high { background: #f85149; color: white; }
.badge-medium { background: #d29922; color: black; }
.badge-low { background: #238636; color: white; }
.badge-missing { background: #f85149; }
.badge-extra { background: #d29922; }
.badge-modified { background: #1f6feb; color: white; }
.no-drift { text-align: center; padding: 40px; color: #3fb950; }
code { background: #21262d; padding: 2px 6px; border-radius: 4px; }
</style>
"""
# Header
html_parts = [
"<!DOCTYPE html>",
"<html><head>",
"<meta charset='utf-8'>",
"<title>Cloudflare Drift Report</title>",
css,
"</head><body>",
"<h1>Cloudflare Drift Report</h1>",
f"<p>Generated: {timestamp}</p>",
]
# Summary cards
html_parts.append("<div class='summary'>")
html_parts.append(f"""
<div class='card'>
<h3>Total Diffs</h3>
<div class='stat {"ok" if summary.get("total_diffs") == 0 else "high"}'>{summary.get("total_diffs", 0)}</div>
</div>
""")
html_parts.append(f"""
<div class='card'>
<h3>High Severity</h3>
<div class='stat high'>{summary.get("high_severity", 0)}</div>
</div>
""")
html_parts.append(f"""
<div class='card'>
<h3>Medium Severity</h3>
<div class='stat medium'>{summary.get("medium_severity", 0)}</div>
</div>
""")
html_parts.append(f"""
<div class='card'>
<h3>Low Severity</h3>
<div class='stat low'>{summary.get("low_severity", 0)}</div>
</div>
""")
html_parts.append("</div>")
# Diffs table
if diffs:
html_parts.append("<h2>Drift Details</h2>")
html_parts.append("<table>")
html_parts.append("""
<tr>
<th>Type</th>
<th>Severity</th>
<th>Record</th>
<th>Detail</th>
</tr>
""")
for diff in diffs:
dtype = diff.get("type", "unknown")
severity = diff.get("severity", "low")
record = f"{diff.get('record_type', '')} {diff.get('record_name', '')}"
detail = html.escape(str(diff.get("detail", "")))
html_parts.append(f"""
<tr>
<td><span class='badge badge-{dtype}'>{dtype}</span></td>
<td><span class='badge badge-{severity}'>{severity.upper()}</span></td>
<td><code>{html.escape(record)}</code></td>
<td>{detail}</td>
</tr>
""")
html_parts.append("</table>")
else:
html_parts.append("<div class='no-drift'>No drift detected. Configuration is in sync.</div>")
html_parts.append("</body></html>")
return "\n".join(html_parts)
def main():
parser = argparse.ArgumentParser(description="Drift Visualizer")
parser.add_argument("--snapshot", help="Path to state snapshot JSON")
parser.add_argument("--manifest", help="Path to DNS manifest JSON/YAML")
parser.add_argument("--output-dir", default=OUTPUT_DIR, help="Output directory")
parser.add_argument("--format", choices=["json", "html", "both"], default="both",
help="Output format")
args = parser.parse_args()
# Load files
snapshot = None
manifest = None
if args.snapshot:
with open(args.snapshot) as f:
snapshot = json.load(f)
if args.manifest:
with open(args.manifest) as f:
manifest = json.load(f)
if not snapshot and not manifest:
print("Error: At least one of --snapshot or --manifest required")
return 1
# Ensure output directory
os.makedirs(args.output_dir, exist_ok=True)
# Run analysis
analyzer = DriftAnalyzer()
analysis = analyzer.analyze(snapshot=snapshot, manifest=manifest)
# Output
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H-%M-%SZ")
if args.format in ("json", "both"):
json_path = os.path.join(args.output_dir, f"drift-report-{timestamp}.json")
with open(json_path, "w") as f:
json.dump(analysis, f, indent=2)
print(f"JSON report: {json_path}")
if args.format in ("html", "both"):
html_content = generate_html_report(analysis)
html_path = os.path.join(args.output_dir, f"drift-report-{timestamp}.html")
with open(html_path, "w") as f:
f.write(html_content)
print(f"HTML report: {html_path}")
# Summary
summary = analysis.get("summary", {})
print(f"\nDrift Summary:")
print(f" Total diffs: {summary.get('total_diffs', 0)}")
print(f" High: {summary.get('high_severity', 0)}")
print(f" Medium: {summary.get('medium_severity', 0)}")
print(f" Low: {summary.get('low_severity', 0)}")
return 0 if summary.get("total_diffs", 0) == 0 else 1
if __name__ == "__main__":
exit(main())