mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-13 06:34:57 +03:00
c47eed6a64
- Fix 25 shell=True subprocess calls with list-based commands - Fix 49 verify=False in defensive skills (env-var override) - Add timeout to 231 HTTP/subprocess/socket calls - Fix 6 SQL injection patterns with whitelist validation - Replace 8 __import__() with standard imports - Remove 701 unused imports across 442 files - Add authorized-testing disclaimers to all offensive skills - Complete 11 incomplete skill directories - Expand 10 stub SKILL.md files with full content - Fix 2 YAML parse errors in frontmatter - Fix 5 pre-existing syntax errors - Convert 22 hardcoded paths/ports to environment variables - Back up 21 redundant skill pairs to .bak - Fix 2 global declaration errors - 724/724 skills with full folder anatomy (SKILL.md + agent.py + api-reference.md + LICENSE) - 0 compile errors across all 724 agent.py files
192 lines
6.7 KiB
Python
192 lines
6.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Agent for auditing Azure Active Directory (Entra ID) configuration."""
|
|
|
|
import os
|
|
import json
|
|
import argparse
|
|
from datetime import datetime, timedelta
|
|
|
|
from azure.identity import DefaultAzureCredential, ClientSecretCredential
|
|
import requests
|
|
|
|
|
|
def get_graph_token(credential):
|
|
"""Obtain a Microsoft Graph API access token."""
|
|
token = credential.get_token("https://graph.microsoft.com/.default")
|
|
return token.token
|
|
|
|
|
|
def graph_get(token, endpoint, params=None):
|
|
"""Make an authenticated GET request to Microsoft Graph API."""
|
|
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
|
url = f"https://graph.microsoft.com/v1.0{endpoint}"
|
|
resp = requests.get(url, headers=headers, params=params, timeout=30)
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
|
|
def get_tenant_info(token):
|
|
"""Get tenant organization details."""
|
|
data = graph_get(token, "/organization")
|
|
orgs = data.get("value", [])
|
|
if orgs:
|
|
org = orgs[0]
|
|
return {
|
|
"display_name": org.get("displayName"),
|
|
"tenant_id": org.get("id"),
|
|
"verified_domains": [d["name"] for d in org.get("verifiedDomains", [])],
|
|
}
|
|
return {}
|
|
|
|
|
|
def list_global_admins(token):
|
|
"""List all Global Administrator role assignments."""
|
|
roles = graph_get(token, "/directoryRoles")
|
|
ga_role = None
|
|
for role in roles.get("value", []):
|
|
if role["displayName"] == "Global Administrator":
|
|
ga_role = role["id"]
|
|
break
|
|
if not ga_role:
|
|
return []
|
|
members = graph_get(token, f"/directoryRoles/{ga_role}/members")
|
|
return [
|
|
{"displayName": m.get("displayName"), "upn": m.get("userPrincipalName"),
|
|
"type": m.get("@odata.type", "").split(".")[-1]}
|
|
for m in members.get("value", [])
|
|
]
|
|
|
|
|
|
def list_conditional_access_policies(token):
|
|
"""List all Conditional Access policies with their state and grant controls."""
|
|
policies = graph_get(token, "/identity/conditionalAccess/policies")
|
|
results = []
|
|
for p in policies.get("value", []):
|
|
results.append({
|
|
"name": p.get("displayName"),
|
|
"state": p.get("state"),
|
|
"grant_controls": p.get("grantControls", {}).get("builtInControls", []),
|
|
"excluded_groups": p.get("conditions", {}).get("users", {}).get("excludeGroups", []),
|
|
})
|
|
return results
|
|
|
|
|
|
def find_stale_users(token, days=90):
|
|
"""Find users who have not signed in for specified number of days."""
|
|
cutoff = (datetime.utcnow() - timedelta(days=days)).strftime("%Y-%m-%dT00:00:00Z")
|
|
users = graph_get(
|
|
token, "/users",
|
|
params={
|
|
"$select": "displayName,userPrincipalName,signInActivity,accountEnabled",
|
|
"$top": "999",
|
|
}
|
|
)
|
|
stale = []
|
|
for u in users.get("value", []):
|
|
sign_in = u.get("signInActivity", {})
|
|
last_sign_in = sign_in.get("lastSignInDateTime")
|
|
if last_sign_in and last_sign_in < cutoff:
|
|
stale.append({
|
|
"upn": u.get("userPrincipalName"),
|
|
"display_name": u.get("displayName"),
|
|
"last_sign_in": last_sign_in,
|
|
"enabled": u.get("accountEnabled"),
|
|
})
|
|
return stale
|
|
|
|
|
|
def list_guest_users(token):
|
|
"""List all guest users in the tenant."""
|
|
users = graph_get(
|
|
token, "/users",
|
|
params={"$filter": "userType eq 'Guest'", "$select": "displayName,userPrincipalName,createdDateTime"}
|
|
)
|
|
return [
|
|
{"upn": u.get("userPrincipalName"), "display_name": u.get("displayName"),
|
|
"created": u.get("createdDateTime")}
|
|
for u in users.get("value", [])
|
|
]
|
|
|
|
|
|
def check_mfa_registration(token):
|
|
"""Check users without MFA registered."""
|
|
try:
|
|
data = graph_get(token, "/reports/authenticationMethods/userRegistrationDetails")
|
|
no_mfa = [
|
|
{"upn": u.get("userPrincipalName"), "mfa_registered": u.get("isMfaRegistered")}
|
|
for u in data.get("value", []) if not u.get("isMfaRegistered")
|
|
]
|
|
return no_mfa
|
|
except Exception:
|
|
return []
|
|
|
|
|
|
def get_risky_signins(token, days=7):
|
|
"""Get risky sign-in events from the last N days."""
|
|
since = (datetime.utcnow() - timedelta(days=days)).strftime("%Y-%m-%dT00:00:00Z")
|
|
try:
|
|
data = graph_get(
|
|
token, "/auditLogs/signIns",
|
|
params={"$filter": f"riskLevelDuringSignIn ne 'none' and createdDateTime ge {since}"}
|
|
)
|
|
return [
|
|
{"user": s.get("userPrincipalName"), "risk": s.get("riskLevelDuringSignIn"),
|
|
"ip": s.get("ipAddress"), "app": s.get("appDisplayName")}
|
|
for s in data.get("value", [])
|
|
]
|
|
except Exception:
|
|
return []
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Azure AD Configuration Audit Agent")
|
|
parser.add_argument("--tenant-id", default=os.getenv("AZURE_TENANT_ID"))
|
|
parser.add_argument("--client-id", default=os.getenv("AZURE_CLIENT_ID"))
|
|
parser.add_argument("--client-secret", default=os.getenv("AZURE_CLIENT_SECRET"))
|
|
parser.add_argument("--stale-days", type=int, default=90)
|
|
parser.add_argument("--output", default="azure_ad_audit.json")
|
|
args = parser.parse_args()
|
|
|
|
if args.client_id and args.client_secret and args.tenant_id:
|
|
credential = ClientSecretCredential(args.tenant_id, args.client_id, args.client_secret)
|
|
else:
|
|
credential = DefaultAzureCredential()
|
|
|
|
token = get_graph_token(credential)
|
|
report = {"audit_date": datetime.utcnow().isoformat(), "findings": {}}
|
|
|
|
report["findings"]["tenant_info"] = get_tenant_info(token)
|
|
print(f"[+] Tenant: {report['findings']['tenant_info'].get('display_name')}")
|
|
|
|
admins = list_global_admins(token)
|
|
report["findings"]["global_admins"] = admins
|
|
print(f"[+] Global Admins: {len(admins)}")
|
|
|
|
ca_policies = list_conditional_access_policies(token)
|
|
report["findings"]["conditional_access"] = ca_policies
|
|
print(f"[+] Conditional Access policies: {len(ca_policies)}")
|
|
|
|
stale = find_stale_users(token, args.stale_days)
|
|
report["findings"]["stale_users"] = stale
|
|
print(f"[+] Stale users ({args.stale_days}+ days): {len(stale)}")
|
|
|
|
guests = list_guest_users(token)
|
|
report["findings"]["guest_users"] = guests
|
|
print(f"[+] Guest users: {len(guests)}")
|
|
|
|
no_mfa = check_mfa_registration(token)
|
|
report["findings"]["users_without_mfa"] = no_mfa
|
|
print(f"[+] Users without MFA: {len(no_mfa)}")
|
|
|
|
risky = get_risky_signins(token)
|
|
report["findings"]["risky_signins"] = risky
|
|
print(f"[+] Risky sign-ins (7d): {len(risky)}")
|
|
|
|
with open(args.output, "w") as f:
|
|
json.dump(report, f, indent=2, default=str)
|
|
print(f"[+] Report saved to {args.output}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|