Claude 24f816b6a3
Consolidate 22 sibling repos into layered organism structure
Place useful parts of the surrounding repos into sica-fondt by layer, per the
body model (Ada = membrane; brain/endocrine/capabilities/knowledge non-Ada):

- brain/        LLM reasoning + providers (dapr, hermes, MoMoA)
- capabilities/ REPRAG sidecars: hermes tools/skills, dapr tools, parallel
                dispatch, A51 channels, and the OSINT cluster
- knowledge/    LORAG corpus: 754 cyber-skills, agency personas, secure-coding,
                MITRE ATT&CK data
- reference/    defensive threat-reference (C3, shhbruh doc) + AdaYaml parser

License handling: AGPL sources (worldosint, advanced_evolution, mercury,
Reticulum) and GPL DeTTECT are SPEC-only clean-room/port descriptions — no
copyleft code copied. MIT/Apache/data parts copied as working trees.

Safety: shhbruh escape/persistence material and C3 covert-C2 kept as reference
only, not wired into the running organism. See CONSOLIDATION.md.

https://claude.ai/code/session_01UehUqEXXJJCsHoA4voCU5c
2026-06-10 06:53:01 +00: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()