#!/usr/bin/env python3 """Agent for auditing SailPoint IdentityNow/IIQ identity governance.""" import json import argparse from datetime import datetime from collections import Counter try: import requests except ImportError: requests = None def sailpoint_api(base_url, token, endpoint, method="GET", params=None): """Call SailPoint IdentityNow API.""" headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} resp = requests.request(method, f"{base_url}{endpoint}", headers=headers, params=params, timeout=60) resp.raise_for_status() return resp.json() def list_identities(base_url, token, limit=250): """List identities from SailPoint.""" return sailpoint_api(base_url, token, "/v3/search/identities", params={"limit": limit}) def list_access_profiles(base_url, token): """List access profiles.""" return sailpoint_api(base_url, token, "/v3/access-profiles", params={"limit": 250}) def list_certifications(base_url, token): """List active certification campaigns.""" return sailpoint_api(base_url, token, "/v3/campaigns", params={"limit": 50}) def audit_certification_campaigns(campaigns_data): """Audit certification campaign completion and compliance.""" campaigns = campaigns_data if isinstance(campaigns_data, list) else \ campaigns_data.get("campaigns", campaigns_data.get("items", [])) findings = [] for campaign in campaigns: status = campaign.get("status", "").lower() completion = campaign.get("completionPercentage", campaign.get("completion", 0)) deadline = campaign.get("deadline", campaign.get("due_date", "")) if status == "active" and completion < 50: findings.append({ "campaign": campaign.get("name", ""), "issue": f"Low completion: {completion}%", "severity": "HIGH", "deadline": deadline, }) if status == "overdue": findings.append({ "campaign": campaign.get("name", ""), "issue": "Campaign overdue", "severity": "CRITICAL", }) return findings def audit_sod_violations(sod_data): """Audit Separation of Duties policy violations.""" violations = sod_data if isinstance(sod_data, list) else \ sod_data.get("violations", []) by_policy = Counter(v.get("policy_name", "unknown") for v in violations) by_identity = Counter(v.get("identity", "unknown") for v in violations) critical = [v for v in violations if v.get("severity", "").lower() == "critical"] return { "total_violations": len(violations), "by_policy": dict(by_policy), "top_violators": dict(by_identity.most_common(10)), "critical_violations": len(critical), "details": critical[:20], } def audit_orphan_accounts(accounts_data): """Find orphan accounts (no identity correlation).""" accounts = accounts_data if isinstance(accounts_data, list) else \ accounts_data.get("accounts", []) orphans = [] for acct in accounts: if not acct.get("identityId") and not acct.get("identity"): orphans.append({ "account": acct.get("name", acct.get("nativeIdentity", "")), "source": acct.get("sourceName", acct.get("source", "")), "created": acct.get("created", ""), "last_login": acct.get("lastLogin", acct.get("last_login", "")), "severity": "HIGH", }) return { "total_accounts": len(accounts), "orphan_count": len(orphans), "orphans": orphans[:50], } def audit_access_reviews(reviews_path): """Audit access review data for over-provisioned users.""" with open(reviews_path) as f: reviews = json.load(f) items = reviews if isinstance(reviews, list) else reviews.get("reviews", []) over_provisioned = [] for review in items: entitlements = review.get("entitlements", review.get("access", [])) unused = [e for e in entitlements if not e.get("used_last_90_days", e.get("last_used", "") != "")] if len(unused) > 3: over_provisioned.append({ "identity": review.get("identity", review.get("user", "")), "total_entitlements": len(entitlements), "unused_entitlements": len(unused), "severity": "HIGH" if len(unused) > 10 else "MEDIUM", }) return { "total_reviewed": len(items), "over_provisioned": len(over_provisioned), "details": over_provisioned[:20], } def generate_lifecycle_policy(): """Generate identity lifecycle policy recommendations.""" return { "joiner": { "trigger": "HR system new hire event", "actions": ["Create identity", "Provision birthright access", "Assign department role", "Send welcome notification"], "sla": "Within 24 hours of start date", }, "mover": { "trigger": "Department or role change in HR system", "actions": ["Recalculate role entitlements", "Revoke old department access", "Provision new role access", "Trigger manager certification"], "sla": "Within 48 hours of change", }, "leaver": { "trigger": "Termination event from HR system", "actions": ["Disable all accounts immediately", "Revoke all access", "Transfer ownership of shared resources", "Archive mailbox", "Remove from all groups"], "sla": "Within 1 hour of termination (immediate for involuntary)", }, } def main(): parser = argparse.ArgumentParser(description="SailPoint Identity Governance Agent") parser.add_argument("--campaigns", help="Certification campaigns JSON") parser.add_argument("--sod", help="SOD violations JSON") parser.add_argument("--accounts", help="Accounts data JSON for orphan detection") parser.add_argument("--reviews", help="Access reviews JSON") parser.add_argument("--action", choices=["campaigns", "sod", "orphans", "reviews", "lifecycle", "full"], default="full") parser.add_argument("--output", default="sailpoint_governance_report.json") args = parser.parse_args() report = {"generated_at": datetime.utcnow().isoformat(), "results": {}} if args.action in ("campaigns", "full") and args.campaigns: with open(args.campaigns) as f: data = json.load(f) findings = audit_certification_campaigns(data) report["results"]["campaigns"] = findings print(f"[+] Campaign issues: {len(findings)}") if args.action in ("sod", "full") and args.sod: with open(args.sod) as f: data = json.load(f) result = audit_sod_violations(data) report["results"]["sod"] = result print(f"[+] SOD violations: {result['total_violations']}") if args.action in ("orphans", "full") and args.accounts: with open(args.accounts) as f: data = json.load(f) result = audit_orphan_accounts(data) report["results"]["orphans"] = result print(f"[+] Orphan accounts: {result['orphan_count']}") if args.action in ("reviews", "full") and args.reviews: result = audit_access_reviews(args.reviews) report["results"]["reviews"] = result print(f"[+] Over-provisioned: {result['over_provisioned']}") if args.action in ("lifecycle", "full"): policy = generate_lifecycle_policy() report["results"]["lifecycle"] = policy print("[+] Lifecycle policy generated") 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()