#!/usr/bin/env python3
"""Okta SCIM provisioning audit agent.

Audits Okta SCIM provisioning configuration by querying the Okta API
for provisioned applications, user assignments, group memberships, and
deprovisioning status. Identifies orphaned accounts, mismatched
assignments, and provisioning failures.
"""
import argparse
import json
import os
import sys
from datetime import datetime, timezone

try:
    import requests
except ImportError:
    print("[!] 'requests' required: pip install requests", file=sys.stderr)
    sys.exit(1)


def get_okta_config():
    """Return Okta org URL and API token."""
    org_url = os.environ.get("OKTA_ORG_URL", "").rstrip("/")
    api_token = os.environ.get("OKTA_API_TOKEN", "")
    if not org_url or not api_token:
        print("[!] Set OKTA_ORG_URL and OKTA_API_TOKEN env vars", file=sys.stderr)
        sys.exit(1)
    return org_url, api_token


def okta_api(org_url, token, endpoint, params=None):
    """Make authenticated Okta API call with pagination."""
    url = f"{org_url}/api/v1{endpoint}"
    headers = {"Authorization": f"SSWS {token}", "Accept": "application/json"}
    all_results = []
    while url:
        resp = requests.get(url, headers=headers, params=params, timeout=30)
        resp.raise_for_status()
        all_results.extend(resp.json())
        links = resp.links
        url = links.get("next", {}).get("url")
        params = None  # Pagination URL includes params
    return all_results


def list_provisioning_apps(org_url, token):
    """List applications with SCIM provisioning enabled."""
    print("[*] Fetching provisioning-enabled applications...")
    apps = okta_api(org_url, token, "/apps", params={"limit": 200})
    scim_apps = []
    for app in apps:
        features = app.get("features", [])
        if any(f in features for f in [
            "PUSH_NEW_USERS", "PUSH_USER_DEACTIVATION",
            "IMPORT_NEW_USERS", "PUSH_PROFILE_UPDATES"
        ]):
            scim_apps.append({
                "id": app.get("id"),
                "name": app.get("name", ""),
                "label": app.get("label", ""),
                "status": app.get("status", ""),
                "features": features,
                "sign_on_mode": app.get("signOnMode", ""),
                "created": app.get("created", ""),
            })
    print(f"[+] Found {len(scim_apps)} SCIM-enabled apps")
    return scim_apps


def audit_app_assignments(org_url, token, app_id, app_label):
    """Audit user assignments for a provisioning app."""
    findings = []
    print(f"[*] Auditing assignments for: {app_label}")
    users = okta_api(org_url, token, f"/apps/{app_id}/users", params={"limit": 200})

    status_counts = {}
    for user in users:
        status = user.get("status", "unknown")
        status_counts[status] = status_counts.get(status, 0) + 1
        scope = user.get("scope", "")
        sync_state = user.get("syncState", "")

        if status == "PROVISIONED" and sync_state == "ERROR":
            findings.append({
                "app": app_label,
                "user": user.get("credentials", {}).get("userName", "unknown"),
                "check": "Provisioning sync error",
                "severity": "HIGH",
                "detail": f"User sync state: ERROR (status: {status})",
            })
        if status == "DEPROVISIONED":
            findings.append({
                "app": app_label,
                "user": user.get("credentials", {}).get("userName", "unknown"),
                "check": "Deprovisioned user still assigned",
                "severity": "MEDIUM",
                "detail": "User is deprovisioned but assignment exists",
            })

    findings.append({
        "app": app_label,
        "check": "Assignment summary",
        "severity": "INFO",
        "detail": f"Total: {len(users)}, Status: {json.dumps(status_counts)}",
    })
    return findings, users


def audit_group_assignments(org_url, token, app_id, app_label):
    """Audit group-based provisioning assignments."""
    findings = []
    groups = okta_api(org_url, token, f"/apps/{app_id}/groups", params={"limit": 200})
    if not groups:
        findings.append({
            "app": app_label,
            "check": "Group assignments",
            "severity": "MEDIUM",
            "detail": "No group assignments found (user-level only)",
        })
    else:
        for group in groups:
            group_id = group.get("id", "")
            priority = group.get("priority", 0)
            findings.append({
                "app": app_label,
                "check": f"Group assignment: {group_id}",
                "severity": "INFO",
                "detail": f"Priority: {priority}",
            })
    return findings


def check_deprovisioning(org_url, token):
    """Check for deactivated Okta users that still have active app assignments."""
    findings = []
    print("[*] Checking for orphaned provisioning assignments...")
    deactivated = okta_api(org_url, token, "/users",
                           params={"filter": 'status eq "DEPROVISIONED"', "limit": 200})
    for user in deactivated[:50]:  # Check first 50
        user_id = user.get("id")
        login = user.get("profile", {}).get("login", "unknown")
        try:
            apps = okta_api(org_url, token, f"/users/{user_id}/appLinks")
            if apps:
                findings.append({
                    "check": "Orphaned app assignment",
                    "user": login,
                    "severity": "HIGH",
                    "detail": f"Deactivated user has {len(apps)} active app link(s)",
                    "apps": [a.get("appName", "") for a in apps[:5]],
                })
        except requests.RequestException:
            pass

    if not findings:
        findings.append({
            "check": "Deprovisioning audit",
            "severity": "INFO",
            "detail": f"No orphaned assignments found ({len(deactivated)} deactivated users checked)",
        })
    return findings


def format_summary(scim_apps, all_findings):
    """Print audit summary."""
    print(f"\n{'='*60}")
    print(f"  Okta SCIM Provisioning Audit Report")
    print(f"{'='*60}")
    print(f"  SCIM Apps    : {len(scim_apps)}")
    print(f"  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", "INFO"]:
        count = severity_counts.get(sev, 0)
        if count:
            print(f"    {sev:10s}: {count}")

    if scim_apps:
        print(f"\n  Provisioning-Enabled Apps:")
        for app in scim_apps:
            print(f"    {app['label']:30s} | {app['status']:10s} | "
                  f"Features: {', '.join(app['features'][:3])}")

    issues = [f for f in all_findings if f["severity"] in ("CRITICAL", "HIGH")]
    if issues:
        print(f"\n  Issues Requiring Attention:")
        for f in issues[:15]:
            print(f"    [{f['severity']:8s}] {f['check']}: {f.get('detail', '')[:50]}")

    return severity_counts


def main():
    parser = argparse.ArgumentParser(description="Okta SCIM provisioning audit agent")
    parser.add_argument("--org-url", help="Okta org URL (or OKTA_ORG_URL env)")
    parser.add_argument("--token", help="API token (or OKTA_API_TOKEN env)")
    parser.add_argument("--app-id", help="Audit a specific app ID")
    parser.add_argument("--skip-deprovisioning", 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.org_url:
        os.environ["OKTA_ORG_URL"] = args.org_url
    if args.token:
        os.environ["OKTA_API_TOKEN"] = args.token

    org_url, token = get_okta_config()
    all_findings = []

    scim_apps = list_provisioning_apps(org_url, token)
    for app in scim_apps:
        if args.app_id and app["id"] != args.app_id:
            continue
        findings, _ = audit_app_assignments(org_url, token, app["id"], app["label"])
        all_findings.extend(findings)
        group_findings = audit_group_assignments(org_url, token, app["id"], app["label"])
        all_findings.extend(group_findings)

    if not args.skip_deprovisioning:
        all_findings.extend(check_deprovisioning(org_url, token))

    severity_counts = format_summary(scim_apps, all_findings)

    report = {
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "tool": "Okta SCIM Audit",
        "scim_apps": scim_apps,
        "findings": all_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()
