First version
This commit is contained in:
72
backend/graph/audit_logs.py
Normal file
72
backend/graph/audit_logs.py
Normal file
@@ -0,0 +1,72 @@
|
||||
import requests
|
||||
from datetime import datetime, timedelta
|
||||
from graph.auth import get_access_token
|
||||
from graph.resolve import resolve_directory_object, resolve_service_principal_owners
|
||||
|
||||
|
||||
def fetch_audit_logs(hours=24, max_pages=50):
|
||||
"""Fetch paginated directory audit logs from Microsoft Graph and enrich with resolved names."""
|
||||
token = get_access_token()
|
||||
start_time = (datetime.utcnow() - timedelta(hours=hours)).isoformat() + "Z"
|
||||
next_url = (
|
||||
"https://graph.microsoft.com/v1.0/"
|
||||
f"auditLogs/directoryAudits?$filter=activityDateTime ge {start_time}"
|
||||
)
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
events = []
|
||||
pages_fetched = 0
|
||||
|
||||
while next_url:
|
||||
if pages_fetched >= max_pages:
|
||||
raise RuntimeError(f"Aborting pagination after {max_pages} pages to avoid runaway fetch.")
|
||||
|
||||
try:
|
||||
res = requests.get(next_url, headers=headers, timeout=20)
|
||||
res.raise_for_status()
|
||||
body = res.json()
|
||||
except requests.RequestException as exc:
|
||||
raise RuntimeError(f"Failed to fetch audit logs page: {exc}") from exc
|
||||
except ValueError as exc:
|
||||
raise RuntimeError(f"Invalid JSON response from Graph: {exc}") from exc
|
||||
|
||||
events.extend(body.get("value", []))
|
||||
next_url = body.get("@odata.nextLink")
|
||||
pages_fetched += 1
|
||||
|
||||
return _enrich_events(events, token)
|
||||
|
||||
|
||||
def _enrich_events(events, token):
|
||||
"""
|
||||
Resolve actor/target IDs to readable names using Graph (requires Directory.Read.All).
|
||||
Adds _resolvedActor, _resolvedActorOwners, and per-target _resolved fields.
|
||||
"""
|
||||
cache = {}
|
||||
owner_cache = {}
|
||||
|
||||
for event in events:
|
||||
actor = event.get("initiatedBy", {}) or {}
|
||||
user = actor.get("user", {}) or {}
|
||||
sp = actor.get("servicePrincipal", {}) or {}
|
||||
app = actor.get("app", {}) or {}
|
||||
app_sp_id = app.get("servicePrincipalId") or app.get("servicePrincipalName")
|
||||
|
||||
actor_id = user.get("id") or sp.get("id") or app_sp_id
|
||||
|
||||
resolved_actor = resolve_directory_object(actor_id, token, cache) if actor_id else None
|
||||
actor_owners = []
|
||||
if resolved_actor and resolved_actor.get("type") == "servicePrincipal":
|
||||
actor_owners = resolve_service_principal_owners(resolved_actor.get("id"), token, owner_cache)
|
||||
|
||||
event["_resolvedActor"] = resolved_actor
|
||||
event["_resolvedActorOwners"] = actor_owners
|
||||
|
||||
for target in event.get("targetResources", []) or []:
|
||||
tid = target.get("id")
|
||||
if tid:
|
||||
resolved_target = resolve_directory_object(tid, token, cache)
|
||||
if resolved_target:
|
||||
target["_resolved"] = resolved_target
|
||||
|
||||
return events
|
||||
Reference in New Issue
Block a user