Files
Anthropic-Cybersecurity-Skills/skills/detecting-oauth-token-theft/scripts/agent.py
T

186 lines
7.1 KiB
Python

#!/usr/bin/env python3
"""OAuth token theft detection agent.
Analyzes sign-in logs for impossible travel, new device sign-ins,
token replay from unusual IPs, and anomalous scope requests.
"""
import argparse
import json
import math
import sys
import datetime
import collections
try:
import requests
HAS_REQUESTS = True
except ImportError:
HAS_REQUESTS = False
EARTH_RADIUS_KM = 6371
def haversine(lat1, lon1, lat2, lon2):
"""Calculate great-circle distance between two points in km."""
lat1, lon1, lat2, lon2 = map(math.radians, [lat1, lon1, lat2, lon2])
dlat = lat2 - lat1
dlon = lon2 - lon1
a = math.sin(dlat / 2) ** 2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2) ** 2
return 2 * EARTH_RADIUS_KM * math.asin(math.sqrt(a))
def detect_impossible_travel(sign_ins, max_speed_kmh=900):
"""Detect impossible travel based on geo and time between logins."""
alerts = []
by_user = collections.defaultdict(list)
for event in sign_ins:
by_user[event.get("user", "")].append(event)
for user, events in by_user.items():
sorted_events = sorted(events, key=lambda e: e.get("timestamp", ""))
for i in range(1, len(sorted_events)):
prev, curr = sorted_events[i - 1], sorted_events[i]
if not all(k in prev for k in ("lat", "lon")) or not all(k in curr for k in ("lat", "lon")):
continue
dist = haversine(prev["lat"], prev["lon"], curr["lat"], curr["lon"])
try:
t1 = datetime.datetime.fromisoformat(prev["timestamp"].replace("Z", "+00:00"))
t2 = datetime.datetime.fromisoformat(curr["timestamp"].replace("Z", "+00:00"))
hours = max((t2 - t1).total_seconds() / 3600, 0.001)
except (ValueError, KeyError):
continue
speed = dist / hours
if speed > max_speed_kmh and dist > 100:
alerts.append({
"type": "impossible_travel",
"user": user,
"from_ip": prev.get("ip", ""),
"to_ip": curr.get("ip", ""),
"distance_km": round(dist, 1),
"time_hours": round(hours, 2),
"speed_kmh": round(speed, 1),
"severity": "HIGH",
})
return alerts
def detect_token_replay(sign_ins):
"""Detect token replay from multiple IPs in short timeframe."""
alerts = []
by_user = collections.defaultdict(list)
for event in sign_ins:
by_user[event.get("user", "")].append(event)
for user, events in by_user.items():
sorted_events = sorted(events, key=lambda e: e.get("timestamp", ""))
window = []
for event in sorted_events:
try:
ts = datetime.datetime.fromisoformat(event["timestamp"].replace("Z", "+00:00"))
except (ValueError, KeyError):
continue
window = [e for e in window
if (ts - datetime.datetime.fromisoformat(
e["timestamp"].replace("Z", "+00:00"))).total_seconds() < 300]
window.append(event)
unique_ips = set(e.get("ip") for e in window if e.get("ip"))
if len(unique_ips) >= 3:
alerts.append({
"type": "token_replay",
"user": user,
"ips": list(unique_ips),
"window_seconds": 300,
"severity": "CRITICAL",
})
return alerts
def detect_new_device(sign_ins, known_devices=None):
"""Detect sign-ins from previously unseen devices."""
known = set(known_devices or [])
alerts = []
for event in sign_ins:
device_id = event.get("device_id", event.get("user_agent", ""))
if device_id and device_id not in known:
alerts.append({
"type": "new_device",
"user": event.get("user", ""),
"device": device_id,
"ip": event.get("ip", ""),
"timestamp": event.get("timestamp", ""),
"severity": "MEDIUM",
})
known.add(device_id)
return alerts
def detect_suspicious_scopes(sign_ins):
"""Detect OAuth requests with overly broad or sensitive scopes."""
sensitive_scopes = {
"Mail.ReadWrite", "Mail.Send", "Files.ReadWrite.All",
"Directory.ReadWrite.All", "User.ReadWrite.All",
"Application.ReadWrite.All", "RoleManagement.ReadWrite.Directory",
}
alerts = []
for event in sign_ins:
scopes = set(event.get("scopes", []))
dangerous = scopes & sensitive_scopes
if len(dangerous) >= 2:
alerts.append({
"type": "suspicious_scopes",
"user": event.get("user", ""),
"scopes": list(dangerous),
"app": event.get("app_name", ""),
"severity": "HIGH",
})
return alerts
def main():
parser = argparse.ArgumentParser(description="OAuth token theft detection agent")
parser.add_argument("--log-file", help="JSON file with sign-in events")
parser.add_argument("--max-speed", type=int, default=900, help="Max travel speed km/h (default: 900)")
parser.add_argument("--output", "-o", help="Output JSON report path")
args = parser.parse_args()
print("[*] OAuth Token Theft Detection Agent")
report = {"timestamp": datetime.datetime.utcnow().isoformat() + "Z", "alerts": []}
if args.log_file:
with open(args.log_file) as f:
sign_ins = json.load(f)
else:
sign_ins = [
{"user": "alice@corp.com", "ip": "203.0.113.10", "lat": 40.7128, "lon": -74.0060,
"timestamp": "2025-06-15T10:00:00Z", "device_id": "device-A"},
{"user": "alice@corp.com", "ip": "198.51.100.50", "lat": 51.5074, "lon": -0.1278,
"timestamp": "2025-06-15T10:30:00Z", "device_id": "device-B"},
{"user": "bob@corp.com", "ip": "10.0.0.1", "lat": 37.7749, "lon": -122.4194,
"timestamp": "2025-06-15T09:00:00Z", "device_id": "device-C",
"scopes": ["Mail.ReadWrite", "Mail.Send", "Files.ReadWrite.All"]},
]
print("[DEMO] Using sample sign-in events")
report["alerts"].extend(detect_impossible_travel(sign_ins, args.max_speed))
report["alerts"].extend(detect_token_replay(sign_ins))
report["alerts"].extend(detect_new_device(sign_ins))
report["alerts"].extend(detect_suspicious_scopes(sign_ins))
by_type = collections.Counter(a["type"] for a in report["alerts"])
print(f"[*] Total alerts: {len(report['alerts'])}")
for alert_type, count in by_type.items():
print(f" {alert_type}: {count}")
for a in report["alerts"]:
print(f" [{a['severity']}] {a['type']}: {a.get('user', '')} - {a.get('distance_km', a.get('ips', a.get('device', '')))}")
if args.output:
with open(args.output, "w") as f:
json.dump(report, f, indent=2)
print(json.dumps({"total_alerts": len(report["alerts"]), "by_type": dict(by_type)}, indent=2))
if __name__ == "__main__":
main()