#!/usr/bin/env python3
"""Microsoft Entra ID passwordless authentication audit agent using MS Graph API."""

import json
import sys
import argparse
from datetime import datetime

try:
    import requests
    from msal import ConfidentialClientApplication
except ImportError:
    print("Install: pip install msal requests")
    sys.exit(1)


GRAPH_URL = "https://graph.microsoft.com/v1.0"
GRAPH_BETA = "https://graph.microsoft.com/beta"


def get_access_token(tenant_id, client_id, client_secret):
    """Authenticate to Microsoft Graph using client credentials."""
    app = ConfidentialClientApplication(
        client_id, authority=f"https://login.microsoftonline.com/{tenant_id}",
        client_credential=client_secret)
    result = app.acquire_token_for_client(scopes=["https://graph.microsoft.com/.default"])
    if "access_token" in result:
        return result["access_token"]
    print(f"[!] Auth failed: {result.get('error_description', 'Unknown error')}")
    sys.exit(1)


def graph_get(token, endpoint, beta=False):
    """Make authenticated GET request to Microsoft Graph."""
    base = GRAPH_BETA if beta else GRAPH_URL
    headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
    resp = requests.get(f"{base}{endpoint}", headers=headers, timeout=30)
    resp.raise_for_status()
    return resp.json()


def get_auth_methods_policy(token):
    """Get authentication methods policy to check passwordless configuration."""
    return graph_get(token, "/policies/authenticationMethodsPolicy", beta=True)


def get_fido2_policy(token):
    """Get FIDO2 security key configuration."""
    policy = get_auth_methods_policy(token)
    for method in policy.get("authenticationMethodConfigurations", []):
        if method.get("id") == "fido2":
            return {
                "state": method.get("state"),
                "is_attestation_enforced": method.get("isAttestationEnforced"),
                "is_self_service_allowed": method.get("isSelfServiceRegistrationAllowed"),
                "key_restrictions": method.get("keyRestrictions", {}),
            }
    return {"state": "not_configured"}


def get_microsoft_authenticator_policy(token):
    """Get Microsoft Authenticator configuration."""
    policy = get_auth_methods_policy(token)
    for method in policy.get("authenticationMethodConfigurations", []):
        if method.get("id") == "microsoftAuthenticator":
            return {
                "state": method.get("state"),
                "feature_settings": method.get("featureSettings", {}),
            }
    return {"state": "not_configured"}


def get_windows_hello_policy(token):
    """Get Windows Hello for Business configuration."""
    policy = get_auth_methods_policy(token)
    for method in policy.get("authenticationMethodConfigurations", []):
        if method.get("id") == "windowsHelloForBusiness":
            return {"state": method.get("state")}
    return {"state": "not_configured"}


def list_user_auth_methods(token, user_id):
    """List authentication methods registered by a specific user."""
    try:
        methods = graph_get(token, f"/users/{user_id}/authentication/methods")
        return [{"id": m.get("id"), "type": m.get("@odata.type", "").split(".")[-1]}
                for m in methods.get("value", [])]
    except Exception as e:
        return [{"error": str(e)}]


def get_users_without_passwordless(token, max_users=100):
    """Identify users who have not registered any passwordless method."""
    users = graph_get(token, f"/users?$top={max_users}&$select=id,displayName,userPrincipalName")
    users_without = []
    for user in users.get("value", []):
        methods = list_user_auth_methods(token, user["id"])
        passwordless_types = {"fido2AuthenticationMethod", "microsoftAuthenticatorAuthenticationMethod",
                              "windowsHelloForBusinessAuthenticationMethod"}
        user_types = {m.get("type") for m in methods if not m.get("error")}
        if not user_types.intersection(passwordless_types):
            users_without.append({
                "user": user["userPrincipalName"],
                "name": user.get("displayName", ""),
                "methods": [m.get("type") for m in methods if not m.get("error")],
            })
    return users_without


def get_sign_in_logs(token, days=7, passwordless_only=False):
    """Get recent sign-in logs to analyze authentication methods used."""
    filter_str = ""
    if passwordless_only:
        filter_str = "?$filter=authenticationDetails/any(a:a/authenticationMethod eq 'FIDO2 security key')"
    try:
        logs = graph_get(token, f"/auditLogs/signIns{filter_str}", beta=True)
        return [{"user": log.get("userPrincipalName"),
                 "app": log.get("appDisplayName"),
                 "status": log.get("status", {}).get("errorCode", 0),
                 "auth_detail": [d.get("authenticationMethod") for d in log.get("authenticationDetails", [])],
                 "time": log.get("createdDateTime")}
                for log in logs.get("value", [])[:50]]
    except Exception as e:
        return [{"error": str(e)}]


def get_conditional_access_policies(token):
    """List conditional access policies related to authentication strength."""
    try:
        policies = graph_get(token, "/identity/conditionalAccess/policies", beta=True)
        auth_strength_policies = []
        for p in policies.get("value", []):
            grant = p.get("grantControls", {})
            if grant and "authenticationStrength" in json.dumps(grant):
                auth_strength_policies.append({
                    "name": p.get("displayName"),
                    "state": p.get("state"),
                    "grant_controls": grant,
                })
        return auth_strength_policies
    except Exception as e:
        return [{"error": str(e)}]


def run_passwordless_audit(token):
    """Run comprehensive passwordless authentication audit."""
    print(f"\n{'='*60}")
    print(f"  MICROSOFT ENTRA PASSWORDLESS AUTH AUDIT")
    print(f"  Generated: {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC")
    print(f"{'='*60}\n")

    fido2 = get_fido2_policy(token)
    print(f"--- FIDO2 SECURITY KEYS ---")
    print(f"  State: {fido2.get('state', 'unknown')}")
    print(f"  Attestation Enforced: {fido2.get('is_attestation_enforced', 'N/A')}")
    print(f"  Self-Service Registration: {fido2.get('is_self_service_allowed', 'N/A')}")

    authenticator = get_microsoft_authenticator_policy(token)
    print(f"\n--- MICROSOFT AUTHENTICATOR ---")
    print(f"  State: {authenticator.get('state', 'unknown')}")

    hello = get_windows_hello_policy(token)
    print(f"\n--- WINDOWS HELLO FOR BUSINESS ---")
    print(f"  State: {hello.get('state', 'unknown')}")

    ca_policies = get_conditional_access_policies(token)
    print(f"\n--- CONDITIONAL ACCESS (Auth Strength) ({len(ca_policies)}) ---")
    for p in ca_policies:
        print(f"  {p.get('name', 'N/A')}: {p.get('state', 'N/A')}")

    users_without = get_users_without_passwordless(token, max_users=50)
    print(f"\n--- USERS WITHOUT PASSWORDLESS ({len(users_without)}) ---")
    for u in users_without[:10]:
        print(f"  {u['user']} - Current methods: {', '.join(u['methods']) or 'none'}")

    print(f"\n{'='*60}\n")
    return {"fido2": fido2, "authenticator": authenticator, "hello": hello,
            "users_without_passwordless": len(users_without)}


def main():
    parser = argparse.ArgumentParser(description="Microsoft Entra Passwordless Auth Agent")
    parser.add_argument("--tenant-id", required=True, help="Azure AD tenant ID")
    parser.add_argument("--client-id", required=True, help="App registration client ID")
    parser.add_argument("--client-secret", required=True, help="App registration secret")
    parser.add_argument("--audit", action="store_true", help="Run passwordless audit")
    parser.add_argument("--user-methods", help="List auth methods for specific user")
    parser.add_argument("--output", help="Save report to JSON")
    args = parser.parse_args()

    token = get_access_token(args.tenant_id, args.client_id, args.client_secret)

    if args.audit:
        report = run_passwordless_audit(token)
        if args.output:
            with open(args.output, "w") as f:
                json.dump(report, f, indent=2, default=str)
    elif args.user_methods:
        methods = list_user_auth_methods(token, args.user_methods)
        print(json.dumps(methods, indent=2))
    else:
        parser.print_help()


if __name__ == "__main__":
    main()
