#!/usr/bin/env python3
"""
agent.py - Hunt Entra offensive-tool fingerprints in Graph activity logs.

Runs curated KQL hunts against an Azure Log Analytics / Microsoft Sentinel
workspace using the Azure Monitor Query REST API. Authentication uses an
AAD bearer token (pass --token, or set AZ_MONITOR_TOKEN). Obtain one with:

    az account get-access-token --resource https://api.loganalytics.io \
        --query accessToken -o tsv

Defensive/blue-team tool. Use with appropriate Log Analytics read permissions.

References:
  - Azure Monitor Query API  https://learn.microsoft.com/rest/api/loganalytics/dataaccess/query/get
  - AADGraphActivityLogs      https://learn.microsoft.com/entra/identity/monitoring-health/concept-aad-graph-activity-logs
"""
import argparse
import json
import os
import sys
import urllib.request
import urllib.error

API = "https://api.loganalytics.io/v1/workspaces/{ws}/query"

HUNTS = {
    "ingestion": """
union withsource=Tbl MicrosoftGraphActivityLogs, AADGraphActivityLogs
| where TimeGenerated > ago({days}d)
| summarize Records=count(), LastSeen=max(TimeGenerated) by Tbl
""",
    "roadtools-ua": """
AADGraphActivityLogs
| where TimeGenerated > ago({days}d)
| where RequestMethod == "GET"
| where UserAgent contains "python" and UserAgent contains "aiohttp"
| summarize RequestCount = count() by CallerIpAddress, AppId, UserAgent, UserId
| sort by RequestCount desc
""",
    "tool-agents": """
union MicrosoftGraphActivityLogs, AADGraphActivityLogs
| where TimeGenerated > ago({days}d)
| where UserAgent has_any ("AADInternals","aad-internals","azurehound","BloodHound","Go-http-client")
| project TimeGenerated, UserAgent, CallerIpAddress, AppId, UserId, RequestUri
| sort by TimeGenerated desc
""",
    "endpoint-sweep": """
AADGraphActivityLogs
| where TimeGenerated > ago({days}d)
| where RequestMethod == "GET"
| extend TopLevelResource = tolower(tostring(split(split(RequestUri, "?")[0], "/")[3]))
| summarize TopLevelResources = make_set(TopLevelResource), AppIds = make_set(AppId),
    CallerIPs = make_set(CallerIpAddress), UserAgents = make_set(UserAgent),
    StartTime = min(TimeGenerated), EndTime = max(TimeGenerated)
    by UserId, bin(TimeGenerated, 5m)
| where TopLevelResources has_all ("users","tenantdetails","groups","applications",
    "serviceprincipals","devices","directoryroles","roledefinitions","contacts",
    "oauth2permissiongrants","authorizationpolicy")
| project StartTime, EndTime, UserId, AppIds, CallerIPs, UserAgents
""",
    "volume-outlier": """
MicrosoftGraphActivityLogs
| where TimeGenerated > ago({days}d)
| where RequestMethod == "GET"
| where RequestUri has_any ("/users","/groups","/servicePrincipals","/applications","/directoryRoles","/roleManagement")
| summarize Reads=count(), Resources=dcount(RequestUri) by UserId, AppId, CallerIpAddress, bin(TimeGenerated, 10m)
| where Reads > {threshold}
| sort by Reads desc
""",
}


def run_query(workspace, token, kql):
    url = API.format(ws=workspace)
    body = json.dumps({"query": kql}).encode()
    req = urllib.request.Request(url, data=body, method="POST", headers={
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json",
    })
    try:
        with urllib.request.urlopen(req, timeout=120) as r:
            return json.loads(r.read().decode())
    except urllib.error.HTTPError as e:
        raise SystemExit(f"[!] query failed HTTP {e.code}: {e.read().decode(errors='replace')[:300]}")
    except urllib.error.URLError as e:
        raise SystemExit(f"[!] network error: {e.reason}")


def print_table(result):
    tables = result.get("tables", [])
    if not tables:
        print("    (no tables returned)")
        return
    t = tables[0]
    cols = [c["name"] for c in t.get("columns", [])]
    rows = t.get("rows", [])
    print("    " + " | ".join(cols))
    if not rows:
        print("    (0 rows)")
    for row in rows[:50]:
        print("    " + " | ".join(str(v) for v in row))
    if len(rows) > 50:
        print(f"    ... {len(rows) - 50} more rows")


def main():
    p = argparse.ArgumentParser(description="Hunt Entra offensive-tool fingerprints in Graph logs.")
    p.add_argument("--workspace", required=True, help="Log Analytics workspace ID (GUID)")
    p.add_argument("--token", default=os.environ.get("AZ_MONITOR_TOKEN"),
                   help="AAD bearer token for api.loganalytics.io (or set AZ_MONITOR_TOKEN)")
    p.add_argument("--hunt", choices=list(HUNTS) + ["all"], default="all",
                   help="Which hunt to run")
    p.add_argument("--days", type=int, default=7, help="Lookback window in days")
    p.add_argument("--threshold", type=int, default=200,
                   help="Read-count threshold for volume-outlier hunt")
    args = p.parse_args()

    if not args.token:
        raise SystemExit("[!] provide --token or set AZ_MONITOR_TOKEN "
                         "(az account get-access-token --resource https://api.loganalytics.io)")

    selected = list(HUNTS) if args.hunt == "all" else [args.hunt]
    for name in selected:
        kql = HUNTS[name].format(days=args.days, threshold=args.threshold)
        print(f"\n=== HUNT: {name} (last {args.days}d) ===")
        result = run_query(args.workspace, args.token, kql)
        print_table(result)
    return 0


if __name__ == "__main__":
    sys.exit(main())
