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

185 lines
6.8 KiB
Python

#!/usr/bin/env python3
"""Agent for identity governance and lifecycle management operations."""
import os
import json
import argparse
from datetime import datetime, timedelta
import requests
def get_graph_token(tenant_id, client_id, client_secret):
"""Authenticate to Microsoft Graph API via client credentials."""
url = f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token"
resp = requests.post(url, data={
"grant_type": "client_credentials",
"client_id": client_id,
"client_secret": client_secret,
"scope": "https://graph.microsoft.com/.default",
}, timeout=30)
resp.raise_for_status()
return resp.json()["access_token"]
def list_users(token, filter_query=None):
"""List users from Microsoft Entra ID with optional filter."""
headers = {"Authorization": f"Bearer {token}"}
url = "https://graph.microsoft.com/v1.0/users"
params = {"$select": "id,displayName,userPrincipalName,accountEnabled,employeeId,"
"department,jobTitle,createdDateTime,signInActivity",
"$top": "999"}
if filter_query:
params["$filter"] = filter_query
users = []
while url:
resp = requests.get(url, headers=headers, params=params, timeout=30)
resp.raise_for_status()
data = resp.json()
users.extend(data.get("value", []))
url = data.get("@odata.nextLink")
params = {}
return users
def detect_orphaned_accounts(token, days_inactive=90):
"""Find accounts with no sign-in activity for N days."""
users = list_users(token)
cutoff = datetime.utcnow() - timedelta(days=days_inactive)
orphaned = []
for u in users:
if not u.get("accountEnabled"):
continue
sign_in = u.get("signInActivity", {})
last_sign_in = sign_in.get("lastSignInDateTime")
if not last_sign_in:
orphaned.append({
"user": u["userPrincipalName"],
"department": u.get("department", ""),
"reason": "No sign-in recorded",
"risk": "HIGH",
})
else:
last_dt = datetime.fromisoformat(last_sign_in.rstrip("Z"))
if last_dt < cutoff:
orphaned.append({
"user": u["userPrincipalName"],
"department": u.get("department", ""),
"last_sign_in": last_sign_in,
"days_inactive": (datetime.utcnow() - last_dt).days,
"risk": "MEDIUM",
})
return orphaned
def detect_stale_guests(token, days_inactive=60):
"""Find guest accounts with no recent activity."""
guests = list_users(token, "userType eq 'Guest'")
cutoff = datetime.utcnow() - timedelta(days=days_inactive)
stale = []
for g in guests:
sign_in = g.get("signInActivity", {})
last = sign_in.get("lastSignInDateTime")
if not last or datetime.fromisoformat(last.rstrip("Z")) < cutoff:
stale.append({
"user": g["userPrincipalName"],
"created": g.get("createdDateTime", ""),
"last_sign_in": last,
})
return stale
def get_access_reviews(token):
"""List active access reviews from Entra ID Governance."""
headers = {"Authorization": f"Bearer {token}"}
url = "https://graph.microsoft.com/v1.0/identityGovernance/accessReviews/definitions"
resp = requests.get(url, headers=headers, timeout=30)
resp.raise_for_status()
reviews = []
for r in resp.json().get("value", []):
reviews.append({
"name": r["displayName"],
"status": r["status"],
"scope": r.get("scope", {}).get("query", ""),
"created": r.get("createdDateTime"),
})
return reviews
def check_users_without_mfa(token):
"""Identify users without registered MFA methods."""
headers = {"Authorization": f"Bearer {token}"}
url = "https://graph.microsoft.com/v1.0/reports/authenticationMethods/userRegistrationDetails"
resp = requests.get(url, headers=headers, params={"$top": "999"}, timeout=30)
resp.raise_for_status()
no_mfa = []
for u in resp.json().get("value", []):
if not u.get("isMfaRegistered"):
no_mfa.append({
"user": u["userPrincipalName"],
"mfa_registered": False,
"methods": u.get("methodsRegistered", []),
})
return no_mfa
def generate_lifecycle_report(token):
"""Generate comprehensive identity governance report."""
report = {"generated_at": datetime.utcnow().isoformat(), "findings": {}}
users = list_users(token)
enabled = [u for u in users if u.get("accountEnabled")]
disabled = [u for u in users if not u.get("accountEnabled")]
report["summary"] = {
"total_users": len(users),
"enabled": len(enabled),
"disabled": len(disabled),
}
report["findings"]["orphaned_accounts"] = detect_orphaned_accounts(token)
report["findings"]["stale_guests"] = detect_stale_guests(token)
report["findings"]["no_mfa"] = check_users_without_mfa(token)
report["findings"]["access_reviews"] = get_access_reviews(token)
return report
def main():
parser = argparse.ArgumentParser(description="Identity Governance Lifecycle 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("--output", default="iga_report.json")
parser.add_argument("--action", choices=[
"orphaned", "guests", "mfa", "reviews", "full_report"
], default="full_report")
args = parser.parse_args()
token = get_graph_token(args.tenant_id, args.client_id, args.client_secret)
report = {"scan_date": datetime.utcnow().isoformat(), "findings": {}}
if args.action in ("orphaned", "full_report"):
results = detect_orphaned_accounts(token)
report["findings"]["orphaned_accounts"] = results
print(f"[+] Orphaned accounts: {len(results)}")
if args.action in ("guests", "full_report"):
results = detect_stale_guests(token)
report["findings"]["stale_guests"] = results
print(f"[+] Stale guest accounts: {len(results)}")
if args.action in ("mfa", "full_report"):
results = check_users_without_mfa(token)
report["findings"]["no_mfa"] = results
print(f"[+] Users without MFA: {len(results)}")
if args.action in ("reviews", "full_report"):
results = get_access_reviews(token)
report["findings"]["access_reviews"] = results
print(f"[+] Active access reviews: {len(results)}")
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()