Files
Anthropic-Cybersecurity-Skills/skills/detecting-azure-lateral-movement/scripts/agent.py
T

179 lines
7.8 KiB
Python

#!/usr/bin/env python3
"""Azure Lateral Movement Detection Agent - hunts for lateral movement in Azure AD/Entra ID."""
import json
import argparse
import logging
import requests
from collections import defaultdict
from datetime import datetime, timedelta
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)
LATERAL_MOVEMENT_INDICATORS = {
"oauth_consent_grant": {"mitre": "T1550.001", "severity": "high"},
"service_principal_credential_add": {"mitre": "T1098.001", "severity": "critical"},
"cross_tenant_signin": {"mitre": "T1078.004", "severity": "high"},
"mailbox_delegation": {"mitre": "T1098.002", "severity": "high"},
"token_replay": {"mitre": "T1528", "severity": "critical"},
"conditional_access_bypass": {"mitre": "T1556.007", "severity": "critical"},
}
def get_graph_token(tenant_id, client_id, client_secret):
"""Authenticate to Microsoft Graph API via OAuth2 client credentials."""
url = 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",
}
resp = requests.post(url, data=data, timeout=30)
resp.raise_for_status()
return resp.json()["access_token"]
def query_audit_logs(token, hours=24):
"""Query Azure AD audit logs for suspicious directory changes."""
since = (datetime.utcnow() - timedelta(hours=hours)).strftime("%Y-%m-%dT%H:%M:%SZ")
url = "https://graph.microsoft.com/v1.0/auditLogs/directoryAudits"
headers = {"Authorization": f"Bearer {token}"}
params = {"$filter": f"activityDateTime ge {since}", "$top": 500, "$orderby": "activityDateTime desc"}
resp = requests.get(url, headers=headers, params=params, timeout=30)
resp.raise_for_status()
return resp.json().get("value", [])
def query_signin_logs(token, hours=24):
"""Query Azure AD sign-in logs for anomalous authentication patterns."""
since = (datetime.utcnow() - timedelta(hours=hours)).strftime("%Y-%m-%dT%H:%M:%SZ")
url = "https://graph.microsoft.com/v1.0/auditLogs/signIns"
headers = {"Authorization": f"Bearer {token}"}
params = {"$filter": f"createdDateTime ge {since}", "$top": 500}
resp = requests.get(url, headers=headers, params=params, timeout=30)
resp.raise_for_status()
return resp.json().get("value", [])
def detect_oauth_consent_abuse(audit_logs):
"""Detect suspicious OAuth application consent grants."""
findings = []
for log in audit_logs:
activity = log.get("activityDisplayName", "")
if "Consent to application" in activity:
target = log.get("targetResources", [{}])[0]
app_name = target.get("displayName", "")
actor = log.get("initiatedBy", {}).get("user", {}).get("userPrincipalName", "")
findings.append({
"indicator": "oauth_consent_grant", "actor": actor, "application": app_name,
"timestamp": log.get("activityDateTime", ""),
"severity": "high", "mitre": "T1550.001",
"detail": f"OAuth consent granted to '{app_name}' by {actor}",
})
return findings
def detect_service_principal_changes(audit_logs):
"""Detect credential additions to service principals."""
findings = []
suspicious_ops = ["Add service principal credentials", "Add service principal",
"Add app role assignment to service principal"]
for log in audit_logs:
activity = log.get("activityDisplayName", "")
if any(op in activity for op in suspicious_ops):
actor = log.get("initiatedBy", {}).get("user", {}).get("userPrincipalName", "unknown")
target = log.get("targetResources", [{}])[0].get("displayName", "")
findings.append({
"indicator": "service_principal_credential_add", "actor": actor,
"target_sp": target, "timestamp": log.get("activityDateTime", ""),
"severity": "critical", "mitre": "T1098.001",
"detail": f"Service principal '{target}' modified by {actor}",
})
return findings
def detect_cross_tenant_signins(signin_logs, home_tenant_id):
"""Detect sign-ins from external/unknown tenants."""
findings = []
for log in signin_logs:
resource_tenant = log.get("resourceTenantId", "")
if resource_tenant and resource_tenant != home_tenant_id:
findings.append({
"indicator": "cross_tenant_signin",
"user": log.get("userPrincipalName", ""),
"source_ip": log.get("ipAddress", ""),
"resource_tenant": resource_tenant,
"timestamp": log.get("createdDateTime", ""),
"severity": "high", "mitre": "T1078.004",
})
return findings
def detect_token_replay(signin_logs):
"""Detect potential token replay by finding same user from multiple IPs in short windows."""
user_sessions = defaultdict(list)
for log in signin_logs:
user = log.get("userPrincipalName", "")
ip = log.get("ipAddress", "")
ua = log.get("clientAppUsed", "")
if user and ip:
user_sessions[user].append({"ip": ip, "ua": ua, "time": log.get("createdDateTime", "")})
findings = []
for user, sessions in user_sessions.items():
unique_ips = set(s["ip"] for s in sessions)
if len(unique_ips) >= 5:
findings.append({
"indicator": "token_replay", "user": user,
"unique_ips": len(unique_ips), "session_count": len(sessions),
"severity": "critical", "mitre": "T1528",
"detail": f"User {user} signed in from {len(unique_ips)} different IPs",
})
return findings
def generate_report(all_findings, hours):
by_indicator = defaultdict(int)
for f in all_findings:
by_indicator[f["indicator"]] += 1
critical = sum(1 for f in all_findings if f.get("severity") == "critical")
return {
"timestamp": datetime.utcnow().isoformat(),
"lookback_hours": hours,
"total_findings": len(all_findings),
"critical_findings": critical,
"by_indicator": dict(by_indicator),
"findings": all_findings,
"risk_level": "critical" if critical > 0 else "high" if all_findings else "low",
}
def main():
parser = argparse.ArgumentParser(description="Azure AD Lateral Movement 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 client secret")
parser.add_argument("--hours", type=int, default=24, help="Lookback window in hours")
parser.add_argument("--output", default="azure_lateral_movement_report.json")
args = parser.parse_args()
token = get_graph_token(args.tenant_id, args.client_id, args.client_secret)
audit_logs = query_audit_logs(token, args.hours)
signin_logs = query_signin_logs(token, args.hours)
findings = []
findings.extend(detect_oauth_consent_abuse(audit_logs))
findings.extend(detect_service_principal_changes(audit_logs))
findings.extend(detect_cross_tenant_signins(signin_logs, args.tenant_id))
findings.extend(detect_token_replay(signin_logs))
report = generate_report(findings, args.hours)
with open(args.output, "w") as f:
json.dump(report, f, indent=2, default=str)
logger.info("Azure lateral movement: %d findings (%d critical) in %dh window",
report["total_findings"], report["critical_findings"], args.hours)
print(json.dumps(report, indent=2, default=str))
if __name__ == "__main__":
main()