mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-07-16 12:45:17 +03:00
241 lines
8.5 KiB
Python
241 lines
8.5 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Azure Service Principal Abuse Detection Script
|
|
|
|
Queries Microsoft Graph API to detect suspicious service principal
|
|
activities including new credentials, privilege escalation, and
|
|
unauthorized ownership.
|
|
"""
|
|
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
from datetime import datetime, timedelta
|
|
|
|
|
|
def run_az_cli(args):
|
|
"""Execute Azure CLI command and return parsed JSON output."""
|
|
cmd = ["az"] + args + ["--output", "json"]
|
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
if result.returncode != 0:
|
|
return None, result.stderr
|
|
try:
|
|
return json.loads(result.stdout) if result.stdout.strip() else {}, None
|
|
except json.JSONDecodeError:
|
|
return result.stdout, None
|
|
|
|
|
|
def check_recent_credential_additions(days=7):
|
|
"""Check for service principals with recently added credentials."""
|
|
print(f"[*] Checking for SP credentials added in last {days} days...")
|
|
cutoff = (datetime.utcnow() - timedelta(days=days)).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
|
|
sps, err = run_az_cli(["ad", "sp", "list", "--all"])
|
|
if err:
|
|
print(f"[!] Error listing service principals: {err}")
|
|
return []
|
|
|
|
suspicious = []
|
|
for sp in sps or []:
|
|
display_name = sp.get("displayName", "Unknown")
|
|
app_id = sp.get("appId", "")
|
|
object_id = sp.get("id", "")
|
|
|
|
# Check password credentials
|
|
for cred in sp.get("passwordCredentials", []):
|
|
start_date = cred.get("startDateTime", "")
|
|
if start_date and start_date > cutoff:
|
|
suspicious.append({
|
|
"type": "password_credential",
|
|
"sp_name": display_name,
|
|
"app_id": app_id,
|
|
"object_id": object_id,
|
|
"credential_start": start_date,
|
|
"credential_end": cred.get("endDateTime", ""),
|
|
"key_id": cred.get("keyId", "")
|
|
})
|
|
|
|
# Check certificate credentials
|
|
for cert in sp.get("keyCredentials", []):
|
|
start_date = cert.get("startDateTime", "")
|
|
if start_date and start_date > cutoff:
|
|
suspicious.append({
|
|
"type": "certificate_credential",
|
|
"sp_name": display_name,
|
|
"app_id": app_id,
|
|
"object_id": object_id,
|
|
"credential_start": start_date,
|
|
"credential_end": cert.get("endDateTime", ""),
|
|
"key_id": cert.get("keyId", "")
|
|
})
|
|
|
|
if suspicious:
|
|
print(f"[!] Found {len(suspicious)} recently added credentials:")
|
|
for item in suspicious:
|
|
print(f" - [{item['type']}] {item['sp_name']} (AppId: {item['app_id']})")
|
|
print(f" Added: {item['credential_start']}")
|
|
else:
|
|
print("[+] No recently added credentials found")
|
|
|
|
return suspicious
|
|
|
|
|
|
def check_privileged_sp_roles():
|
|
"""Check for service principals with privileged directory roles."""
|
|
print("\n[*] Checking service principals with privileged roles...")
|
|
|
|
privileged_roles = [
|
|
"Global Administrator",
|
|
"Application Administrator",
|
|
"Cloud Application Administrator",
|
|
"Privileged Role Administrator",
|
|
"Exchange Administrator",
|
|
"SharePoint Administrator",
|
|
"User Administrator"
|
|
]
|
|
|
|
findings = []
|
|
roles, err = run_az_cli(["rest", "--method", "GET",
|
|
"--url", "https://graph.microsoft.com/v1.0/directoryRoles"])
|
|
if err:
|
|
print(f"[!] Error fetching roles: {err}")
|
|
return []
|
|
|
|
for role in (roles or {}).get("value", []):
|
|
role_name = role.get("displayName", "")
|
|
role_id = role.get("id", "")
|
|
|
|
if role_name not in privileged_roles:
|
|
continue
|
|
|
|
members, err = run_az_cli(["rest", "--method", "GET",
|
|
"--url", f"https://graph.microsoft.com/v1.0/directoryRoles/{role_id}/members"])
|
|
if err:
|
|
continue
|
|
|
|
for member in (members or {}).get("value", []):
|
|
odata_type = member.get("@odata.type", "")
|
|
if "servicePrincipal" in odata_type:
|
|
findings.append({
|
|
"role": role_name,
|
|
"sp_name": member.get("displayName", "Unknown"),
|
|
"sp_id": member.get("id", ""),
|
|
"app_id": member.get("appId", "")
|
|
})
|
|
|
|
if findings:
|
|
print(f"[!] Found {len(findings)} service principals with privileged roles:")
|
|
for f in findings:
|
|
print(f" - {f['sp_name']} has role: {f['role']}")
|
|
else:
|
|
print("[+] No service principals with privileged roles found")
|
|
|
|
return findings
|
|
|
|
|
|
def check_sp_ownership():
|
|
"""Identify applications with non-admin owners (potential abuse vector)."""
|
|
print("\n[*] Checking application ownership for potential abuse vectors...")
|
|
|
|
apps, err = run_az_cli(["ad", "app", "list", "--all"])
|
|
if err:
|
|
print(f"[!] Error listing applications: {err}")
|
|
return []
|
|
|
|
risky_ownership = []
|
|
for app in apps or []:
|
|
app_name = app.get("displayName", "Unknown")
|
|
app_id = app.get("appId", "")
|
|
object_id = app.get("id", "")
|
|
|
|
owners, err = run_az_cli(["rest", "--method", "GET",
|
|
"--url", f"https://graph.microsoft.com/v1.0/applications/{object_id}/owners"])
|
|
if err:
|
|
continue
|
|
|
|
owner_list = (owners or {}).get("value", [])
|
|
if len(owner_list) > 3: # Flag apps with many owners
|
|
risky_ownership.append({
|
|
"app_name": app_name,
|
|
"app_id": app_id,
|
|
"owner_count": len(owner_list),
|
|
"owners": [o.get("userPrincipalName", o.get("displayName", "Unknown")) for o in owner_list]
|
|
})
|
|
|
|
if risky_ownership:
|
|
print(f"[!] Found {len(risky_ownership)} applications with excessive owners:")
|
|
for item in risky_ownership:
|
|
print(f" - {item['app_name']}: {item['owner_count']} owners")
|
|
else:
|
|
print("[+] No applications with excessive ownership found")
|
|
|
|
return risky_ownership
|
|
|
|
|
|
def generate_detection_report(cred_findings, role_findings, ownership_findings):
|
|
"""Generate a consolidated detection report."""
|
|
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
report = f"""
|
|
{'='*60}
|
|
Azure Service Principal Abuse Detection Report
|
|
Generated: {timestamp}
|
|
{'='*60}
|
|
|
|
## Summary
|
|
- Recent Credential Additions: {len(cred_findings)}
|
|
- Privileged Role Assignments: {len(role_findings)}
|
|
- Risky Application Ownership: {len(ownership_findings)}
|
|
- Overall Risk: {'HIGH' if cred_findings or role_findings else 'LOW'}
|
|
"""
|
|
|
|
if cred_findings:
|
|
report += "\n## Recently Added Credentials\n"
|
|
for f in cred_findings:
|
|
report += f" [{f['type']}] {f['sp_name']} - Added {f['credential_start']}\n"
|
|
|
|
if role_findings:
|
|
report += "\n## Privileged Service Principals\n"
|
|
for f in role_findings:
|
|
report += f" {f['sp_name']} -> {f['role']}\n"
|
|
|
|
if ownership_findings:
|
|
report += "\n## Risky Application Ownership\n"
|
|
for f in ownership_findings:
|
|
report += f" {f['app_name']} - {f['owner_count']} owners\n"
|
|
|
|
print(report)
|
|
return report
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import argparse
|
|
|
|
parser = argparse.ArgumentParser(description="Azure Service Principal Abuse Detection")
|
|
parser.add_argument("--days", type=int, default=7, help="Lookback period in days")
|
|
parser.add_argument("--credentials", action="store_true", help="Check recent credential additions")
|
|
parser.add_argument("--roles", action="store_true", help="Check privileged role assignments")
|
|
parser.add_argument("--ownership", action="store_true", help="Check application ownership")
|
|
parser.add_argument("--full", action="store_true", help="Run all checks")
|
|
parser.add_argument("--output", type=str, help="Save report to file")
|
|
|
|
args = parser.parse_args()
|
|
|
|
cred_findings = []
|
|
role_findings = []
|
|
ownership_findings = []
|
|
|
|
if args.full or args.credentials:
|
|
cred_findings = check_recent_credential_additions(args.days)
|
|
if args.full or args.roles:
|
|
role_findings = check_privileged_sp_roles()
|
|
if args.full or args.ownership:
|
|
ownership_findings = check_sp_ownership()
|
|
|
|
if args.full or (args.credentials and args.roles):
|
|
report = generate_detection_report(cred_findings, role_findings, ownership_findings)
|
|
if args.output:
|
|
with open(args.output, "w") as f:
|
|
f.write(report)
|
|
print(f"\n[+] Report saved to {args.output}")
|