#!/usr/bin/env python3
"""Azure Service Principal abuse detection agent."""

import json
import sys
import argparse
from datetime import datetime, timedelta

try:
    from azure.identity import ClientSecretCredential
except ImportError:
    ClientSecretCredential = None

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


class AzureGraphClient:
    """Client for Microsoft Graph API to audit service principals."""

    def __init__(self, tenant_id, client_id, client_secret):
        self.tenant_id = tenant_id
        self.token = self._get_token(tenant_id, client_id, client_secret)
        self.headers = {"Authorization": f"Bearer {self.token}"}

    def _get_token(self, tenant_id, client_id, client_secret):
        resp = requests.post(
            f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token",
            data={
                "grant_type": "client_credentials",
                "client_id": client_id,
                "client_secret": client_secret,
                "scope": "https://graph.microsoft.com/.default",
            },
            timeout=15,
        )
        resp.raise_for_status()
        return resp.json()["access_token"]

    def _get(self, endpoint, params=None):
        resp = requests.get(f"https://graph.microsoft.com/v1.0{endpoint}",
                            headers=self.headers, params=params, timeout=15)
        resp.raise_for_status()
        return resp.json()

    def list_service_principals(self):
        return self._get("/servicePrincipals", params={"$top": 200}).get("value", [])

    def get_sp_credentials(self, sp_id):
        return self._get(f"/servicePrincipals/{sp_id}").get("passwordCredentials", [])

    def get_sp_app_roles(self, sp_id):
        return self._get(f"/servicePrincipals/{sp_id}/appRoleAssignments").get("value", [])

    def get_sign_in_logs(self, sp_id, days=7):
        since = (datetime.utcnow() - timedelta(days=days)).strftime("%Y-%m-%dT%H:%M:%SZ")
        return self._get("/auditLogs/signIns", params={
            "$filter": f"appId eq '{sp_id}' and createdDateTime ge {since}",
            "$top": 100,
        }).get("value", [])

    def get_directory_roles(self):
        return self._get("/directoryRoles").get("value", [])

    def get_role_members(self, role_id):
        return self._get(f"/directoryRoles/{role_id}/members").get("value", [])


def audit_credential_expiry(client, principals):
    """Check for expired or soon-to-expire service principal credentials."""
    findings = []
    now = datetime.utcnow()
    for sp in principals:
        sp_id = sp.get("id", "")
        display = sp.get("displayName", "")
        creds = sp.get("passwordCredentials", [])
        key_creds = sp.get("keyCredentials", [])

        for cred in creds + key_creds:
            end_str = cred.get("endDateTime", "")
            if not end_str:
                continue
            try:
                end = datetime.fromisoformat(end_str.rstrip("Z"))
            except ValueError:
                continue

            if end < now:
                findings.append({
                    "sp_name": display, "sp_id": sp_id,
                    "credential_id": cred.get("keyId", ""),
                    "issue": "Credential expired but not removed",
                    "severity": "MEDIUM", "expired": end_str,
                })
            elif end < now + timedelta(days=30):
                findings.append({
                    "sp_name": display, "sp_id": sp_id,
                    "issue": f"Credential expires within 30 days ({end_str})",
                    "severity": "LOW",
                })

        if len(creds) > 2:
            findings.append({
                "sp_name": display, "sp_id": sp_id,
                "issue": f"Multiple password credentials ({len(creds)}) — possible backdoor",
                "severity": "HIGH",
            })

    return findings


def audit_privileged_sp_roles(client):
    """Find service principals with high-privilege directory roles."""
    findings = []
    high_priv_roles = {
        "Global Administrator", "Application Administrator",
        "Cloud Application Administrator", "Privileged Role Administrator",
    }
    roles = client.get_directory_roles()
    for role in roles:
        role_name = role.get("displayName", "")
        if role_name not in high_priv_roles:
            continue
        members = client.get_role_members(role["id"])
        for m in members:
            if m.get("@odata.type") == "#microsoft.graph.servicePrincipal":
                findings.append({
                    "sp_name": m.get("displayName", ""),
                    "sp_id": m.get("id", ""),
                    "role": role_name,
                    "issue": f"Service principal has {role_name} role",
                    "severity": "CRITICAL",
                })
    return findings


def run_audit(args):
    """Execute Azure service principal abuse detection audit."""
    print(f"\n{'='*60}")
    print(f"  AZURE SERVICE PRINCIPAL ABUSE DETECTION")
    print(f"  Generated: {datetime.utcnow().isoformat()} UTC")
    print(f"{'='*60}\n")

    client = AzureGraphClient(args.tenant_id, args.client_id, args.client_secret)
    report = {}

    principals = client.list_service_principals()
    report["total_service_principals"] = len(principals)
    print(f"--- SERVICE PRINCIPALS ({len(principals)}) ---")
    for sp in principals[:10]:
        print(f"  {sp.get('displayName','')}: {sp.get('appId','')[:20]}...")

    cred_findings = audit_credential_expiry(client, principals)
    report["credential_findings"] = cred_findings
    print(f"\n--- CREDENTIAL AUDIT ({len(cred_findings)} findings) ---")
    for f in cred_findings[:15]:
        print(f"  [{f['severity']}] {f['sp_name']}: {f['issue']}")

    role_findings = audit_privileged_sp_roles(client)
    report["privileged_role_findings"] = role_findings
    print(f"\n--- PRIVILEGED ROLE ASSIGNMENTS ({len(role_findings)}) ---")
    for f in role_findings:
        print(f"  [{f['severity']}] {f['sp_name']}: {f['role']}")

    return report


def main():
    parser = argparse.ArgumentParser(description="Azure SP Abuse Detection 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("--output", help="Save report to JSON file")
    args = parser.parse_args()

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


if __name__ == "__main__":
    main()
