Files

219 lines
7.7 KiB
Python

#!/usr/bin/env python3
"""
Google Cloud IAP - Configuration Audit Tool
Audits IAP-enabled backend services, IAM bindings, access levels,
session settings, and access logs for compliance validation.
Requirements:
pip install google-cloud-compute google-auth requests
"""
import json
import subprocess
import sys
from datetime import datetime, timedelta, timezone
from typing import Any
def run_gcloud(args: list[str]) -> Any:
"""Execute gcloud command and return JSON output."""
cmd = ["gcloud"] + args + ["--format=json"]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
if result.returncode != 0:
return []
try:
return json.loads(result.stdout) if result.stdout.strip() else []
except json.JSONDecodeError:
return []
def audit_iap_backends(project_id: str) -> list[dict]:
"""Audit all backend services for IAP configuration."""
print("\n[1/5] Auditing IAP-enabled backend services...")
backends = run_gcloud(["compute", "backend-services", "list", "--project", project_id])
if not isinstance(backends, list):
return []
results = []
for backend in backends:
name = backend.get("name", "unknown")
iap = backend.get("iap", {})
enabled = iap.get("enabled", False)
info = {"name": name, "iap_enabled": enabled, "bindings": [], "has_conditions": False}
if enabled:
policy = run_gcloud([
"iap", "web", "get-iam-policy",
"--resource-type=backend-services",
"--service", name, "--project", project_id
])
bindings = policy.get("bindings", []) if isinstance(policy, dict) else []
info["bindings"] = bindings
info["bindings_count"] = len(bindings)
info["has_conditions"] = any(b.get("condition") for b in bindings)
print(f" [IAP ON] {name}: {len(bindings)} bindings, conditions: {info['has_conditions']}")
else:
print(f" [IAP OFF] {name}")
results.append(info)
return results
def audit_access_levels(policy_id: str) -> list[dict]:
"""Audit Access Context Manager levels."""
print("\n[2/5] Auditing access levels...")
if not policy_id:
print(" Skipped - no policy ID provided")
return []
levels = run_gcloud(["access-context-manager", "levels", "list", "--policy", policy_id])
if not isinstance(levels, list):
return []
results = []
for level in levels:
name = level.get("name", "").split("/")[-1]
title = level.get("title", "")
basic = level.get("basic", {})
has_device = any(
c.get("devicePolicy") for c in basic.get("conditions", [])
)
has_ip = any(
c.get("ipSubnetworks") for c in basic.get("conditions", [])
)
results.append({"name": name, "title": title, "has_device": has_device, "has_ip": has_ip})
print(f" {title}: device_policy={has_device}, ip_restriction={has_ip}")
return results
def audit_iap_tunnel_access(project_id: str) -> dict:
"""Audit IAP TCP tunnel permissions."""
print("\n[3/5] Auditing IAP tunnel access...")
instances = run_gcloud(["compute", "instances", "list", "--project", project_id])
if not isinstance(instances, list):
return {"total_vms": 0}
stats = {"total_vms": len(instances), "with_external_ip": 0, "tunnel_accessible": 0}
for vm in instances:
name = vm.get("name", "unknown")
interfaces = vm.get("networkInterfaces", [])
has_ext_ip = any(
iface.get("accessConfigs") for iface in interfaces
)
if has_ext_ip:
stats["with_external_ip"] += 1
print(f" Total VMs: {stats['total_vms']}, With external IP: {stats['with_external_ip']}")
if stats["with_external_ip"] > 0:
print(f" [WARN] {stats['with_external_ip']} VMs have external IPs - consider removing for IAP-only access")
return stats
def audit_iap_logs(project_id: str) -> dict:
"""Analyze recent IAP access logs."""
print("\n[4/5] Analyzing IAP access logs (24h)...")
start = (datetime.now(timezone.utc) - timedelta(hours=24)).strftime("%Y-%m-%dT%H:%M:%SZ")
logs = run_gcloud([
"logging", "read",
f'resource.type="gce_backend_service" AND protoPayload.serviceName="iap.googleapis.com" AND timestamp>="{start}"',
"--project", project_id, "--limit=500"
])
if not isinstance(logs, list):
return {"total": 0, "allowed": 0, "denied": 0}
stats = {"total": len(logs), "allowed": 0, "denied": 0, "users": set()}
for entry in logs:
payload = entry.get("protoPayload", {})
status = payload.get("status", {}).get("code", 0)
user = payload.get("authenticationInfo", {}).get("principalEmail", "")
if status == 0:
stats["allowed"] += 1
else:
stats["denied"] += 1
if user:
stats["users"].add(user)
stats["unique_users"] = len(stats["users"])
del stats["users"]
print(f" Requests: {stats['total']}, Allowed: {stats['allowed']}, Denied: {stats['denied']}")
return stats
def generate_report(project_id: str, backends: list, levels: list,
tunnels: dict, logs: dict) -> str:
"""Generate IAP audit report."""
print("\n[5/5] Generating report...")
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
iap_on = [b for b in backends if b["iap_enabled"]]
iap_off = [b for b in backends if not b["iap_enabled"]]
with_conditions = [b for b in iap_on if b.get("has_conditions")]
report = f"""
Google Cloud IAP Audit Report
{'=' * 55}
Project: {project_id}
Generated: {now}
1. BACKEND SERVICES
Total backend services: {len(backends)}
IAP enabled: {len(iap_on)}
IAP disabled: {len(iap_off)}
With access level conditions: {len(with_conditions)} / {len(iap_on)}
2. ACCESS LEVELS
Total defined: {len(levels)}
With device policy: {sum(1 for l in levels if l['has_device'])}
With IP restrictions: {sum(1 for l in levels if l['has_ip'])}
3. VM ACCESS
Total VMs: {tunnels['total_vms']}
With external IP: {tunnels['with_external_ip']}
4. ACCESS LOGS (24h)
Total requests: {logs['total']}
Allowed: {logs['allowed']}
Denied: {logs['denied']}
Unique users: {logs.get('unique_users', 0)}
5. RECOMMENDATIONS
"""
recs = []
if iap_off:
recs.append(f" - Enable IAP on {len(iap_off)} unprotected backend service(s)")
if len(with_conditions) < len(iap_on):
recs.append(f" - Add access level conditions to {len(iap_on) - len(with_conditions)} IAP service(s)")
if tunnels["with_external_ip"] > 0:
recs.append(f" - Remove external IPs from {tunnels['with_external_ip']} VM(s) for IAP-only access")
if not recs:
recs.append(" - Configuration meets best practices")
report += "\n".join(recs)
return report
def main():
if len(sys.argv) < 2:
print("Usage: python process.py <project-id> [access-policy-id]")
sys.exit(1)
project_id = sys.argv[1]
policy_id = sys.argv[2] if len(sys.argv) > 2 else None
backends = audit_iap_backends(project_id)
levels = audit_access_levels(policy_id)
tunnels = audit_iap_tunnel_access(project_id)
logs = audit_iap_logs(project_id)
report = generate_report(project_id, backends, levels, tunnels, logs)
print(report)
filename = f"iap_audit_{project_id}_{datetime.now().strftime('%Y%m%d')}.txt"
with open(filename, "w") as f:
f.write(report)
print(f"\nReport saved to: {filename}")
if __name__ == "__main__":
main()