#!/usr/bin/env python3
"""Kubernetes RBAC hardening audit agent.

Audits Kubernetes Role-Based Access Control configuration for security
weaknesses including overly permissive ClusterRoles, wildcard permissions,
privilege escalation paths, and service account misconfigurations.
"""
import argparse
import json
import os
import subprocess
import sys
from datetime import datetime, timezone


def run_kubectl(args_list, timeout=60):
    """Execute kubectl and return parsed JSON."""
    cmd = ["kubectl"] + args_list + ["-o", "json"]
    result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
    if result.returncode != 0:
        return None
    try:
        return json.loads(result.stdout)
    except json.JSONDecodeError:
        return None


def audit_cluster_roles():
    """Audit ClusterRoles for dangerous permissions."""
    findings = []
    data = run_kubectl(["get", "clusterroles"])
    if not data:
        return findings

    dangerous_verbs = {"*", "create", "update", "patch", "delete"}
    dangerous_resources = {"secrets", "pods/exec", "clusterroles", "clusterrolebindings",
                           "roles", "rolebindings", "serviceaccounts", "nodes"}
    escalation_resources = {"clusterroles", "clusterrolebindings", "roles", "rolebindings"}

    for role in data.get("items", []):
        name = role.get("metadata", {}).get("name", "")
        if name.startswith("system:"):
            continue  # Skip system roles

        for rule in role.get("rules", []):
            verbs = set(rule.get("verbs", []))
            resources = set(rule.get("resources", []))
            api_groups = rule.get("apiGroups", [])

            # Wildcard everything
            if "*" in verbs and "*" in resources:
                findings.append({
                    "type": "wildcard_all", "role": name, "kind": "ClusterRole",
                    "severity": "CRITICAL",
                    "detail": "Full wildcard access (verbs: *, resources: *)",
                    "recommendation": "Replace with specific verbs and resources",
                })

            # Secrets access
            if resources & {"secrets", "*"} and verbs & {"get", "list", "watch", "*"}:
                findings.append({
                    "type": "secrets_access", "role": name, "kind": "ClusterRole",
                    "severity": "HIGH",
                    "detail": f"Can read secrets (verbs: {verbs & {'get', 'list', 'watch', '*'}})",
                })

            # Pod exec
            if "pods/exec" in resources or ("pods" in resources and "create" in verbs):
                findings.append({
                    "type": "pod_exec", "role": name, "kind": "ClusterRole",
                    "severity": "HIGH",
                    "detail": "Can exec into pods (container escape risk)",
                })

            # Privilege escalation via RBAC modification
            if resources & escalation_resources and verbs & {"create", "update", "patch", "*"}:
                findings.append({
                    "type": "rbac_escalation", "role": name, "kind": "ClusterRole",
                    "severity": "CRITICAL",
                    "detail": f"Can modify RBAC resources: {resources & escalation_resources}",
                })

            # Node access
            if "nodes" in resources and verbs & {"get", "list", "proxy", "*"}:
                findings.append({
                    "type": "node_access", "role": name, "kind": "ClusterRole",
                    "severity": "MEDIUM",
                    "detail": "Can access node resources",
                })

    return findings


def audit_cluster_role_bindings():
    """Audit ClusterRoleBindings for overly broad subject assignments."""
    findings = []
    data = run_kubectl(["get", "clusterrolebindings"])
    if not data:
        return findings

    for binding in data.get("items", []):
        name = binding.get("metadata", {}).get("name", "")
        if name.startswith("system:"):
            continue

        role_ref = binding.get("roleRef", {})
        role_name = role_ref.get("name", "")
        subjects = binding.get("subjects", [])

        for subject in subjects:
            kind = subject.get("kind", "")
            subj_name = subject.get("name", "")
            namespace = subject.get("namespace", "")

            # Cluster-admin binding
            if role_name == "cluster-admin":
                findings.append({
                    "type": "cluster_admin_binding",
                    "binding": name, "subject": f"{kind}/{subj_name}",
                    "severity": "CRITICAL",
                    "detail": f"cluster-admin bound to {kind} '{subj_name}'",
                })

            # Group bindings to all authenticated/unauthenticated
            if kind == "Group" and subj_name in ("system:authenticated", "system:unauthenticated"):
                findings.append({
                    "type": "broad_group_binding",
                    "binding": name, "subject": subj_name,
                    "severity": "CRITICAL" if subj_name == "system:unauthenticated" else "HIGH",
                    "detail": f"Role '{role_name}' bound to group '{subj_name}'",
                })

            # Default service account bindings
            if kind == "ServiceAccount" and subj_name == "default":
                findings.append({
                    "type": "default_sa_binding",
                    "binding": name, "subject": f"default SA in {namespace}",
                    "severity": "MEDIUM",
                    "detail": f"Role '{role_name}' bound to default service account",
                })

    return findings


def audit_service_accounts(namespace=None):
    """Audit service accounts for misconfigurations."""
    findings = []
    cmd = ["get", "serviceaccounts", "--all-namespaces"] if not namespace else ["get", "serviceaccounts", "-n", namespace]
    data = run_kubectl(cmd)
    if not data:
        return findings

    for sa in data.get("items", []):
        name = sa.get("metadata", {}).get("name", "")
        ns = sa.get("metadata", {}).get("namespace", "")
        automount = sa.get("automountServiceAccountToken", None)

        if name == "default" and automount is not False:
            findings.append({
                "type": "default_sa_automount",
                "namespace": ns, "service_account": name,
                "severity": "MEDIUM",
                "detail": f"Default SA in '{ns}' has automountServiceAccountToken enabled",
                "recommendation": "Set automountServiceAccountToken: false on default SA",
            })

        secrets = sa.get("secrets", [])
        if len(secrets) > 1:
            findings.append({
                "type": "sa_multiple_secrets",
                "namespace": ns, "service_account": name,
                "severity": "LOW",
                "detail": f"SA has {len(secrets)} token secrets",
            })

    return findings


def format_summary(role_findings, binding_findings, sa_findings):
    """Print RBAC audit summary."""
    all_findings = role_findings + binding_findings + sa_findings
    print(f"\n{'='*60}")
    print(f"  Kubernetes RBAC Hardening Audit")
    print(f"{'='*60}")
    print(f"  ClusterRole Issues    : {len(role_findings)}")
    print(f"  Binding Issues        : {len(binding_findings)}")
    print(f"  ServiceAccount Issues : {len(sa_findings)}")
    print(f"  Total Findings        : {len(all_findings)}")

    severity_counts = {}
    for f in all_findings:
        sev = f.get("severity", "INFO")
        severity_counts[sev] = severity_counts.get(sev, 0) + 1

    print(f"\n  By Severity:")
    for sev in ["CRITICAL", "HIGH", "MEDIUM", "LOW"]:
        count = severity_counts.get(sev, 0)
        if count:
            print(f"    {sev:10s}: {count}")

    if all_findings:
        print(f"\n  Top Findings:")
        for f in sorted(all_findings, key=lambda x: {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2}.get(x["severity"], 9))[:15]:
            print(f"    [{f['severity']:8s}] {f['type']:25s} | {f.get('detail', '')[:50]}")

    return severity_counts


def main():
    parser = argparse.ArgumentParser(description="Kubernetes RBAC hardening audit agent")
    parser.add_argument("--namespace", "-n", help="Specific namespace to audit")
    parser.add_argument("--kubeconfig", help="Path to kubeconfig")
    parser.add_argument("--skip-roles", action="store_true")
    parser.add_argument("--skip-bindings", action="store_true")
    parser.add_argument("--skip-sa", action="store_true")
    parser.add_argument("--output", "-o", help="Output JSON report")
    parser.add_argument("--verbose", "-v", action="store_true")
    args = parser.parse_args()

    if args.kubeconfig:
        os.environ["KUBECONFIG"] = args.kubeconfig

    role_findings = [] if args.skip_roles else audit_cluster_roles()
    binding_findings = [] if args.skip_bindings else audit_cluster_role_bindings()
    sa_findings = [] if args.skip_sa else audit_service_accounts(args.namespace)

    severity_counts = format_summary(role_findings, binding_findings, sa_findings)

    report = {
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "tool": "K8s RBAC Audit",
        "role_findings": role_findings,
        "binding_findings": binding_findings,
        "sa_findings": sa_findings,
        "severity_counts": severity_counts,
        "risk_level": (
            "CRITICAL" if severity_counts.get("CRITICAL", 0) > 0
            else "HIGH" if severity_counts.get("HIGH", 0) > 0
            else "MEDIUM" if severity_counts.get("MEDIUM", 0) > 0
            else "LOW"
        ),
    }

    if args.output:
        with open(args.output, "w") as f:
            json.dump(report, f, indent=2)
        print(f"\n[+] Report saved to {args.output}")
    elif args.verbose:
        print(json.dumps(report, indent=2))


if __name__ == "__main__":
    main()
