mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-12 14:14:56 +03:00
255 lines
9.0 KiB
Python
255 lines
9.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Okta SCIM provisioning audit agent.
|
|
|
|
Audits Okta SCIM provisioning configuration by querying the Okta API
|
|
for provisioned applications, user assignments, group memberships, and
|
|
deprovisioning status. Identifies orphaned accounts, mismatched
|
|
assignments, and provisioning failures.
|
|
"""
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
from datetime import datetime, timezone
|
|
|
|
try:
|
|
import requests
|
|
except ImportError:
|
|
print("[!] 'requests' required: pip install requests", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
def get_okta_config():
|
|
"""Return Okta org URL and API token."""
|
|
org_url = os.environ.get("OKTA_ORG_URL", "").rstrip("/")
|
|
api_token = os.environ.get("OKTA_API_TOKEN", "")
|
|
if not org_url or not api_token:
|
|
print("[!] Set OKTA_ORG_URL and OKTA_API_TOKEN env vars", file=sys.stderr)
|
|
sys.exit(1)
|
|
return org_url, api_token
|
|
|
|
|
|
def okta_api(org_url, token, endpoint, params=None):
|
|
"""Make authenticated Okta API call with pagination."""
|
|
url = f"{org_url}/api/v1{endpoint}"
|
|
headers = {"Authorization": f"SSWS {token}", "Accept": "application/json"}
|
|
all_results = []
|
|
while url:
|
|
resp = requests.get(url, headers=headers, params=params, timeout=30)
|
|
resp.raise_for_status()
|
|
all_results.extend(resp.json())
|
|
links = resp.links
|
|
url = links.get("next", {}).get("url")
|
|
params = None # Pagination URL includes params
|
|
return all_results
|
|
|
|
|
|
def list_provisioning_apps(org_url, token):
|
|
"""List applications with SCIM provisioning enabled."""
|
|
print("[*] Fetching provisioning-enabled applications...")
|
|
apps = okta_api(org_url, token, "/apps", params={"limit": 200})
|
|
scim_apps = []
|
|
for app in apps:
|
|
features = app.get("features", [])
|
|
if any(f in features for f in [
|
|
"PUSH_NEW_USERS", "PUSH_USER_DEACTIVATION",
|
|
"IMPORT_NEW_USERS", "PUSH_PROFILE_UPDATES"
|
|
]):
|
|
scim_apps.append({
|
|
"id": app.get("id"),
|
|
"name": app.get("name", ""),
|
|
"label": app.get("label", ""),
|
|
"status": app.get("status", ""),
|
|
"features": features,
|
|
"sign_on_mode": app.get("signOnMode", ""),
|
|
"created": app.get("created", ""),
|
|
})
|
|
print(f"[+] Found {len(scim_apps)} SCIM-enabled apps")
|
|
return scim_apps
|
|
|
|
|
|
def audit_app_assignments(org_url, token, app_id, app_label):
|
|
"""Audit user assignments for a provisioning app."""
|
|
findings = []
|
|
print(f"[*] Auditing assignments for: {app_label}")
|
|
users = okta_api(org_url, token, f"/apps/{app_id}/users", params={"limit": 200})
|
|
|
|
status_counts = {}
|
|
for user in users:
|
|
status = user.get("status", "unknown")
|
|
status_counts[status] = status_counts.get(status, 0) + 1
|
|
scope = user.get("scope", "")
|
|
sync_state = user.get("syncState", "")
|
|
|
|
if status == "PROVISIONED" and sync_state == "ERROR":
|
|
findings.append({
|
|
"app": app_label,
|
|
"user": user.get("credentials", {}).get("userName", "unknown"),
|
|
"check": "Provisioning sync error",
|
|
"severity": "HIGH",
|
|
"detail": f"User sync state: ERROR (status: {status})",
|
|
})
|
|
if status == "DEPROVISIONED":
|
|
findings.append({
|
|
"app": app_label,
|
|
"user": user.get("credentials", {}).get("userName", "unknown"),
|
|
"check": "Deprovisioned user still assigned",
|
|
"severity": "MEDIUM",
|
|
"detail": "User is deprovisioned but assignment exists",
|
|
})
|
|
|
|
findings.append({
|
|
"app": app_label,
|
|
"check": "Assignment summary",
|
|
"severity": "INFO",
|
|
"detail": f"Total: {len(users)}, Status: {json.dumps(status_counts)}",
|
|
})
|
|
return findings, users
|
|
|
|
|
|
def audit_group_assignments(org_url, token, app_id, app_label):
|
|
"""Audit group-based provisioning assignments."""
|
|
findings = []
|
|
groups = okta_api(org_url, token, f"/apps/{app_id}/groups", params={"limit": 200})
|
|
if not groups:
|
|
findings.append({
|
|
"app": app_label,
|
|
"check": "Group assignments",
|
|
"severity": "MEDIUM",
|
|
"detail": "No group assignments found (user-level only)",
|
|
})
|
|
else:
|
|
for group in groups:
|
|
group_id = group.get("id", "")
|
|
priority = group.get("priority", 0)
|
|
findings.append({
|
|
"app": app_label,
|
|
"check": f"Group assignment: {group_id}",
|
|
"severity": "INFO",
|
|
"detail": f"Priority: {priority}",
|
|
})
|
|
return findings
|
|
|
|
|
|
def check_deprovisioning(org_url, token):
|
|
"""Check for deactivated Okta users that still have active app assignments."""
|
|
findings = []
|
|
print("[*] Checking for orphaned provisioning assignments...")
|
|
deactivated = okta_api(org_url, token, "/users",
|
|
params={"filter": 'status eq "DEPROVISIONED"', "limit": 200})
|
|
for user in deactivated[:50]: # Check first 50
|
|
user_id = user.get("id")
|
|
login = user.get("profile", {}).get("login", "unknown")
|
|
try:
|
|
apps = okta_api(org_url, token, f"/users/{user_id}/appLinks")
|
|
if apps:
|
|
findings.append({
|
|
"check": "Orphaned app assignment",
|
|
"user": login,
|
|
"severity": "HIGH",
|
|
"detail": f"Deactivated user has {len(apps)} active app link(s)",
|
|
"apps": [a.get("appName", "") for a in apps[:5]],
|
|
})
|
|
except requests.RequestException:
|
|
pass
|
|
|
|
if not findings:
|
|
findings.append({
|
|
"check": "Deprovisioning audit",
|
|
"severity": "INFO",
|
|
"detail": f"No orphaned assignments found ({len(deactivated)} deactivated users checked)",
|
|
})
|
|
return findings
|
|
|
|
|
|
def format_summary(scim_apps, all_findings):
|
|
"""Print audit summary."""
|
|
print(f"\n{'='*60}")
|
|
print(f" Okta SCIM Provisioning Audit Report")
|
|
print(f"{'='*60}")
|
|
print(f" SCIM Apps : {len(scim_apps)}")
|
|
print(f" Findings : {len(all_findings)}")
|
|
|
|
severity_counts = {}
|
|
for f in all_findings:
|
|
sev = f.get("severity", "INFO")
|
|
severity_counts[sev] = severity_counts.get(sev, 0) + 1
|
|
|
|
print(f"\n By Severity:")
|
|
for sev in ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"]:
|
|
count = severity_counts.get(sev, 0)
|
|
if count:
|
|
print(f" {sev:10s}: {count}")
|
|
|
|
if scim_apps:
|
|
print(f"\n Provisioning-Enabled Apps:")
|
|
for app in scim_apps:
|
|
print(f" {app['label']:30s} | {app['status']:10s} | "
|
|
f"Features: {', '.join(app['features'][:3])}")
|
|
|
|
issues = [f for f in all_findings if f["severity"] in ("CRITICAL", "HIGH")]
|
|
if issues:
|
|
print(f"\n Issues Requiring Attention:")
|
|
for f in issues[:15]:
|
|
print(f" [{f['severity']:8s}] {f['check']}: {f.get('detail', '')[:50]}")
|
|
|
|
return severity_counts
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Okta SCIM provisioning audit agent")
|
|
parser.add_argument("--org-url", help="Okta org URL (or OKTA_ORG_URL env)")
|
|
parser.add_argument("--token", help="API token (or OKTA_API_TOKEN env)")
|
|
parser.add_argument("--app-id", help="Audit a specific app ID")
|
|
parser.add_argument("--skip-deprovisioning", action="store_true")
|
|
parser.add_argument("--output", "-o", help="Output JSON report")
|
|
parser.add_argument("--verbose", "-v", action="store_true")
|
|
args = parser.parse_args()
|
|
|
|
if args.org_url:
|
|
os.environ["OKTA_ORG_URL"] = args.org_url
|
|
if args.token:
|
|
os.environ["OKTA_API_TOKEN"] = args.token
|
|
|
|
org_url, token = get_okta_config()
|
|
all_findings = []
|
|
|
|
scim_apps = list_provisioning_apps(org_url, token)
|
|
for app in scim_apps:
|
|
if args.app_id and app["id"] != args.app_id:
|
|
continue
|
|
findings, _ = audit_app_assignments(org_url, token, app["id"], app["label"])
|
|
all_findings.extend(findings)
|
|
group_findings = audit_group_assignments(org_url, token, app["id"], app["label"])
|
|
all_findings.extend(group_findings)
|
|
|
|
if not args.skip_deprovisioning:
|
|
all_findings.extend(check_deprovisioning(org_url, token))
|
|
|
|
severity_counts = format_summary(scim_apps, all_findings)
|
|
|
|
report = {
|
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
"tool": "Okta SCIM Audit",
|
|
"scim_apps": scim_apps,
|
|
"findings": all_findings,
|
|
"severity_counts": severity_counts,
|
|
"risk_level": (
|
|
"CRITICAL" if severity_counts.get("CRITICAL", 0) > 0
|
|
else "HIGH" if severity_counts.get("HIGH", 0) > 0
|
|
else "MEDIUM" if severity_counts.get("MEDIUM", 0) > 0
|
|
else "LOW"
|
|
),
|
|
}
|
|
|
|
if args.output:
|
|
with open(args.output, "w") as f:
|
|
json.dump(report, f, indent=2)
|
|
print(f"\n[+] Report saved to {args.output}")
|
|
elif args.verbose:
|
|
print(json.dumps(report, indent=2))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|