Files
mukul975 c47eed6a64 Production hardening: security fixes, code quality, 724 skills complete
- 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
2026-03-19 13:26:49 +01:00

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()