mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-07-14 19:55:16 +03:00
290 lines
11 KiB
Python
290 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
AiTM Phishing Detection Engine
|
|
|
|
Analyzes Azure AD sign-in logs and session data to detect
|
|
Adversary-in-the-Middle phishing attacks including session
|
|
cookie replay and impossible travel patterns.
|
|
|
|
Usage:
|
|
python process.py detect --signin-log signins.json
|
|
python process.py check-session --session-log sessions.json
|
|
python process.py analyze-domain --domain suspicious-login.com
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
import sys
|
|
import math
|
|
from dataclasses import dataclass, field, asdict
|
|
from datetime import datetime, timedelta
|
|
from collections import defaultdict
|
|
|
|
|
|
@dataclass
|
|
class AiTMIndicator:
|
|
"""An AiTM detection indicator."""
|
|
indicator_type: str = ""
|
|
description: str = ""
|
|
severity: str = "medium"
|
|
confidence: float = 0.0
|
|
user: str = ""
|
|
timestamp: str = ""
|
|
details: dict = field(default_factory=dict)
|
|
|
|
|
|
@dataclass
|
|
class AiTMAnalysis:
|
|
"""Complete AiTM analysis result."""
|
|
total_signins: int = 0
|
|
suspicious_signins: int = 0
|
|
session_replays_detected: int = 0
|
|
impossible_travel_detected: int = 0
|
|
post_compromise_indicators: int = 0
|
|
indicators: list = field(default_factory=list)
|
|
affected_users: list = field(default_factory=list)
|
|
recommended_actions: list = field(default_factory=list)
|
|
|
|
|
|
# Known AiTM infrastructure patterns
|
|
AITM_DOMAIN_PATTERNS = [
|
|
r'login.*microsoft.*\.(top|xyz|info|click|online)',
|
|
r'auth.*office.*\.(top|xyz|info|click|online)',
|
|
r'sso.*\.(top|xyz|info|click|online)',
|
|
r'verify.*account.*\.(top|xyz|info|click|online)',
|
|
r'.*\.workers\.dev$',
|
|
r'.*\.pages\.dev$',
|
|
r'.*-login-.*\.(com|net|org)',
|
|
]
|
|
|
|
# Known PhaaS hosting patterns
|
|
PHAAS_INFRA = [
|
|
'cloudflare-ipfs.com',
|
|
'workers.dev',
|
|
'pages.dev',
|
|
'web.app',
|
|
'firebaseapp.com',
|
|
'glitch.me',
|
|
'netlify.app',
|
|
'vercel.app',
|
|
]
|
|
|
|
|
|
def haversine_distance(lat1, lon1, lat2, lon2):
|
|
"""Calculate distance in km between two coordinates."""
|
|
R = 6371 # Earth radius in km
|
|
lat1_r, lat2_r = math.radians(lat1), math.radians(lat2)
|
|
dlat = math.radians(lat2 - lat1)
|
|
dlon = math.radians(lon2 - lon1)
|
|
a = (math.sin(dlat / 2) ** 2 +
|
|
math.cos(lat1_r) * math.cos(lat2_r) * math.sin(dlon / 2) ** 2)
|
|
c = 2 * math.asin(math.sqrt(a))
|
|
return R * c
|
|
|
|
|
|
def detect_aitm_signins(signins: list) -> AiTMAnalysis:
|
|
"""Analyze sign-in logs for AiTM indicators."""
|
|
analysis = AiTMAnalysis()
|
|
analysis.total_signins = len(signins)
|
|
|
|
user_signins = defaultdict(list)
|
|
for signin in signins:
|
|
user = signin.get("userPrincipalName", "")
|
|
user_signins[user].append(signin)
|
|
|
|
affected = set()
|
|
|
|
for user, events in user_signins.items():
|
|
sorted_events = sorted(events, key=lambda x: x.get("createdDateTime", ""))
|
|
|
|
for i in range(len(sorted_events)):
|
|
event = sorted_events[i]
|
|
ip = event.get("ipAddress", "")
|
|
location = event.get("location", {})
|
|
risk_level = event.get("riskLevelDuringSignIn", "none")
|
|
is_interactive = event.get("isInteractive", True)
|
|
app = event.get("appDisplayName", "")
|
|
timestamp = event.get("createdDateTime", "")
|
|
|
|
# Check for anonymous proxy
|
|
if event.get("isFromAnonymousProxy", False):
|
|
analysis.indicators.append(AiTMIndicator(
|
|
indicator_type="anonymous_proxy",
|
|
description=f"Sign-in from anonymous proxy/VPN",
|
|
severity="high",
|
|
confidence=0.7,
|
|
user=user,
|
|
timestamp=timestamp,
|
|
details={"ip": ip}
|
|
))
|
|
analysis.suspicious_signins += 1
|
|
affected.add(user)
|
|
|
|
# Check for impossible travel
|
|
if i > 0:
|
|
prev = sorted_events[i - 1]
|
|
prev_loc = prev.get("location", {})
|
|
prev_time = prev.get("createdDateTime", "")
|
|
|
|
if (location.get("geoCoordinates") and
|
|
prev_loc.get("geoCoordinates")):
|
|
lat1 = prev_loc["geoCoordinates"].get("latitude", 0)
|
|
lon1 = prev_loc["geoCoordinates"].get("longitude", 0)
|
|
lat2 = location["geoCoordinates"].get("latitude", 0)
|
|
lon2 = location["geoCoordinates"].get("longitude", 0)
|
|
|
|
distance = haversine_distance(lat1, lon1, lat2, lon2)
|
|
|
|
try:
|
|
t1 = datetime.fromisoformat(prev_time.replace('Z', '+00:00'))
|
|
t2 = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
|
|
hours = (t2 - t1).total_seconds() / 3600
|
|
|
|
if hours > 0 and distance > 0:
|
|
speed = distance / hours
|
|
if speed > 900: # Faster than commercial flight
|
|
analysis.indicators.append(AiTMIndicator(
|
|
indicator_type="impossible_travel",
|
|
description=(
|
|
f"Impossible travel: {distance:.0f}km in "
|
|
f"{hours:.1f}h ({speed:.0f}km/h)"
|
|
),
|
|
severity="high",
|
|
confidence=0.85,
|
|
user=user,
|
|
timestamp=timestamp,
|
|
details={
|
|
"from_ip": prev.get("ipAddress"),
|
|
"to_ip": ip,
|
|
"distance_km": round(distance),
|
|
"speed_kmh": round(speed)
|
|
}
|
|
))
|
|
analysis.impossible_travel_detected += 1
|
|
affected.add(user)
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
# Check for session from different IP shortly after auth
|
|
if i < len(sorted_events) - 1:
|
|
next_event = sorted_events[i + 1]
|
|
next_ip = next_event.get("ipAddress", "")
|
|
next_time = next_event.get("createdDateTime", "")
|
|
|
|
if ip and next_ip and ip != next_ip:
|
|
try:
|
|
t1 = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
|
|
t2 = datetime.fromisoformat(next_time.replace('Z', '+00:00'))
|
|
minutes = (t2 - t1).total_seconds() / 60
|
|
|
|
if 0 < minutes < 10:
|
|
analysis.indicators.append(AiTMIndicator(
|
|
indicator_type="session_ip_switch",
|
|
description=(
|
|
f"Session IP changed within {minutes:.0f}min "
|
|
f"({ip} -> {next_ip})"
|
|
),
|
|
severity="critical",
|
|
confidence=0.9,
|
|
user=user,
|
|
timestamp=timestamp,
|
|
details={
|
|
"auth_ip": ip,
|
|
"session_ip": next_ip,
|
|
"time_delta_min": round(minutes)
|
|
}
|
|
))
|
|
analysis.session_replays_detected += 1
|
|
affected.add(user)
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
analysis.affected_users = list(affected)
|
|
if affected:
|
|
analysis.recommended_actions = [
|
|
"Revoke all sessions for affected users",
|
|
"Force password reset with phishing-resistant MFA",
|
|
"Check for inbox forwarding rules created post-compromise",
|
|
"Review OAuth app consents for affected accounts",
|
|
"Block source IPs at firewall",
|
|
"Retract phishing email from all mailboxes"
|
|
]
|
|
|
|
return analysis
|
|
|
|
|
|
def analyze_domain(domain: str) -> dict:
|
|
"""Check if domain matches known AiTM/PhaaS patterns."""
|
|
result = {
|
|
"domain": domain,
|
|
"is_suspicious": False,
|
|
"indicators": [],
|
|
"risk_score": 0
|
|
}
|
|
|
|
domain_lower = domain.lower()
|
|
|
|
for pattern in AITM_DOMAIN_PATTERNS:
|
|
if re.search(pattern, domain_lower):
|
|
result["indicators"].append(f"Matches AiTM domain pattern: {pattern}")
|
|
result["risk_score"] += 30
|
|
result["is_suspicious"] = True
|
|
|
|
for infra in PHAAS_INFRA:
|
|
if domain_lower.endswith(infra):
|
|
result["indicators"].append(f"Hosted on known PhaaS infrastructure: {infra}")
|
|
result["risk_score"] += 25
|
|
result["is_suspicious"] = True
|
|
|
|
# Check for brand impersonation in domain
|
|
brands = ['microsoft', 'office', 'outlook', 'google', 'okta', 'azure']
|
|
for brand in brands:
|
|
if brand in domain_lower and not domain_lower.endswith(f'.{brand}.com'):
|
|
result["indicators"].append(f"Contains brand name '{brand}' in non-official domain")
|
|
result["risk_score"] += 20
|
|
result["is_suspicious"] = True
|
|
|
|
result["risk_score"] = min(result["risk_score"], 100)
|
|
return result
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="AiTM Phishing Detection")
|
|
subparsers = parser.add_subparsers(dest="command")
|
|
|
|
detect_parser = subparsers.add_parser("detect", help="Detect AiTM in sign-in logs")
|
|
detect_parser.add_argument("--signin-log", required=True)
|
|
|
|
domain_parser = subparsers.add_parser("analyze-domain", help="Check domain for AiTM")
|
|
domain_parser.add_argument("--domain", required=True)
|
|
|
|
parser.add_argument("--json", action="store_true")
|
|
args = parser.parse_args()
|
|
|
|
if args.command == "detect":
|
|
with open(args.signin_log) as f:
|
|
signins = json.load(f)
|
|
result = detect_aitm_signins(signins)
|
|
if args.json:
|
|
print(json.dumps(asdict(result), indent=2, default=str))
|
|
else:
|
|
print(f"Total sign-ins: {result.total_signins}")
|
|
print(f"Suspicious: {result.suspicious_signins}")
|
|
print(f"Session replays: {result.session_replays_detected}")
|
|
print(f"Impossible travel: {result.impossible_travel_detected}")
|
|
print(f"Affected users: {len(result.affected_users)}")
|
|
for ind in result.indicators:
|
|
print(f" [{ind.severity.upper()}] {ind.description} (user: {ind.user})")
|
|
|
|
elif args.command == "analyze-domain":
|
|
result = analyze_domain(args.domain)
|
|
print(json.dumps(result, indent=2))
|
|
|
|
else:
|
|
parser.print_help()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|