mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-07-13 19:05:17 +03:00
chore: fix license, add disclaimer, quick start, GitHub topics, issue templates
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user