mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-14 15:04:56 +03:00
231 lines
8.5 KiB
Python
231 lines
8.5 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Saviynt Access Recertification Campaign Manager
|
|
|
|
Manages access recertification campaigns via Saviynt REST API,
|
|
tracks campaign progress, and generates compliance reports.
|
|
|
|
Requirements:
|
|
pip install requests pandas
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
import sys
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
try:
|
|
import requests
|
|
except ImportError:
|
|
print("[ERROR] requests required: pip install requests")
|
|
sys.exit(1)
|
|
|
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
|
logger = logging.getLogger("saviynt_recert")
|
|
|
|
|
|
class SaviyntCampaignManager:
|
|
"""Manage Saviynt access recertification campaigns."""
|
|
|
|
def __init__(self, base_url, username, password):
|
|
self.base_url = base_url.rstrip("/")
|
|
self.token = self._authenticate(username, password)
|
|
|
|
def _authenticate(self, username, password):
|
|
"""Authenticate and retrieve API token."""
|
|
resp = requests.post(
|
|
f"{self.base_url}/ECM/api/login",
|
|
json={"username": username, "password": password},
|
|
)
|
|
resp.raise_for_status()
|
|
token = resp.json().get("access_token")
|
|
if not token:
|
|
raise Exception("Authentication failed: no token returned")
|
|
logger.info("Authenticated to Saviynt EIC")
|
|
return token
|
|
|
|
def _api_call(self, method, endpoint, json_data=None):
|
|
headers = {
|
|
"Authorization": f"Bearer {self.token}",
|
|
"Content-Type": "application/json",
|
|
}
|
|
url = f"{self.base_url}{endpoint}"
|
|
resp = requests.request(method, url, headers=headers, json=json_data)
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
def create_campaign(self, name, campaign_type, certifier_type,
|
|
due_days=14, scope=None):
|
|
"""Create a new certification campaign."""
|
|
due_date = (datetime.now(timezone.utc) + timedelta(days=due_days)).strftime(
|
|
"%Y-%m-%d"
|
|
)
|
|
payload = {
|
|
"campaignname": name,
|
|
"campaigntype": campaign_type, # UserManager, EntitlementOwner, Application
|
|
"certifier": certifier_type,
|
|
"duedate": due_date,
|
|
"reminderdays": "7,10,13",
|
|
"autorevoke": True,
|
|
"autorevokedays": due_days + 1,
|
|
"status": "New",
|
|
}
|
|
if scope:
|
|
payload["scope"] = scope
|
|
|
|
result = self._api_call("POST", "/ECM/api/v5/createCampaign", payload)
|
|
campaign_id = result.get("campaignId")
|
|
logger.info(f"Campaign created: {name} (ID: {campaign_id})")
|
|
return result
|
|
|
|
def launch_campaign(self, campaign_id):
|
|
"""Launch an existing campaign, sending notifications to certifiers."""
|
|
result = self._api_call(
|
|
"POST", "/ECM/api/v5/launchCampaign",
|
|
{"campaignId": campaign_id}
|
|
)
|
|
logger.info(f"Campaign {campaign_id} launched")
|
|
return result
|
|
|
|
def get_campaign_status(self, campaign_id):
|
|
"""Get campaign progress and statistics."""
|
|
result = self._api_call(
|
|
"GET", f"/ECM/api/v5/getCampaignDetails?campaignId={campaign_id}"
|
|
)
|
|
return {
|
|
"campaign_id": campaign_id,
|
|
"name": result.get("campaignname"),
|
|
"status": result.get("status"),
|
|
"total_items": result.get("totalLineItems", 0),
|
|
"certified": result.get("certifiedCount", 0),
|
|
"revoked": result.get("revokedCount", 0),
|
|
"pending": result.get("pendingCount", 0),
|
|
"completion_pct": result.get("completionPercentage", 0),
|
|
"due_date": result.get("duedate"),
|
|
}
|
|
|
|
def get_all_campaigns(self, status=None):
|
|
"""List all certification campaigns."""
|
|
params = {}
|
|
if status:
|
|
params["status"] = status
|
|
result = self._api_call("GET", "/ECM/api/v5/getCampaigns")
|
|
campaigns = result.get("campaigns", [])
|
|
if status:
|
|
campaigns = [c for c in campaigns if c.get("status") == status]
|
|
return campaigns
|
|
|
|
def get_certification_items(self, campaign_id, status_filter=None):
|
|
"""Get individual certification line items for a campaign."""
|
|
result = self._api_call(
|
|
"POST", "/ECM/api/v5/getCertificationDetails",
|
|
{"campaignId": campaign_id, "max": 1000}
|
|
)
|
|
items = result.get("certifications", [])
|
|
if status_filter:
|
|
items = [i for i in items if i.get("status") == status_filter]
|
|
return items
|
|
|
|
def generate_compliance_report(self, campaign_id):
|
|
"""Generate a compliance-ready report for a completed campaign."""
|
|
status = self.get_campaign_status(campaign_id)
|
|
items = self.get_certification_items(campaign_id)
|
|
|
|
certified_items = [i for i in items if i.get("decision") == "Certify"]
|
|
revoked_items = [i for i in items if i.get("decision") == "Revoke"]
|
|
pending_items = [i for i in items if not i.get("decision")]
|
|
|
|
report = {
|
|
"report_title": "Access Recertification Compliance Report",
|
|
"campaign_name": status.get("name"),
|
|
"campaign_id": campaign_id,
|
|
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
"metrics": {
|
|
"total_items_reviewed": status["total_items"],
|
|
"certified": status["certified"],
|
|
"revoked": status["revoked"],
|
|
"pending": status["pending"],
|
|
"completion_rate": f"{status['completion_pct']}%",
|
|
"certification_rate": (
|
|
f"{(status['certified'] / status['total_items'] * 100):.1f}%"
|
|
if status["total_items"] else "N/A"
|
|
),
|
|
"revocation_rate": (
|
|
f"{(status['revoked'] / status['total_items'] * 100):.1f}%"
|
|
if status["total_items"] else "N/A"
|
|
),
|
|
},
|
|
"findings": [],
|
|
}
|
|
|
|
if pending_items:
|
|
report["findings"].append({
|
|
"severity": "High",
|
|
"finding": f"{len(pending_items)} items not reviewed by due date",
|
|
"action": "Auto-revoke or manual follow-up required",
|
|
})
|
|
|
|
revoke_rate = status["revoked"] / status["total_items"] if status["total_items"] else 0
|
|
if revoke_rate > 0.20:
|
|
report["findings"].append({
|
|
"severity": "Medium",
|
|
"finding": f"High revocation rate ({revoke_rate:.0%}) suggests over-provisioning",
|
|
"action": "Review provisioning policies and role definitions",
|
|
})
|
|
|
|
return report
|
|
|
|
|
|
class RecertificationScheduler:
|
|
"""Schedule recurring certification campaigns based on compliance needs."""
|
|
|
|
def __init__(self):
|
|
self.schedules = []
|
|
|
|
def add_schedule(self, name, frequency_days, campaign_type,
|
|
certifier_type, scope=None):
|
|
"""Add a recurring certification schedule."""
|
|
self.schedules.append({
|
|
"name": name,
|
|
"frequency_days": frequency_days,
|
|
"campaign_type": campaign_type,
|
|
"certifier_type": certifier_type,
|
|
"scope": scope,
|
|
"last_run": None,
|
|
})
|
|
|
|
def get_due_campaigns(self):
|
|
"""Check which campaigns are due to run."""
|
|
now = datetime.now(timezone.utc)
|
|
due = []
|
|
for schedule in self.schedules:
|
|
if schedule["last_run"] is None:
|
|
due.append(schedule)
|
|
else:
|
|
next_run = schedule["last_run"] + timedelta(days=schedule["frequency_days"])
|
|
if now >= next_run:
|
|
due.append(schedule)
|
|
return due
|
|
|
|
def export_schedule(self, output_path):
|
|
"""Export certification schedule for documentation."""
|
|
with open(output_path, "w") as f:
|
|
json.dump({
|
|
"schedules": self.schedules,
|
|
"exported_at": datetime.now(timezone.utc).isoformat(),
|
|
}, f, indent=2, default=str)
|
|
logger.info(f"Schedule exported to {output_path}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
print("=" * 60)
|
|
print("Saviynt Access Recertification Campaign Manager")
|
|
print("=" * 60)
|
|
print()
|
|
print("Usage:")
|
|
print(" mgr = SaviyntCampaignManager('https://tenant.saviyntcloud.com',")
|
|
print(" 'admin', 'password')")
|
|
print(" mgr.create_campaign('Q1 Review', 'UserManager', 'Manager')")
|
|
print(" mgr.launch_campaign(campaign_id)")
|
|
print(" report = mgr.generate_compliance_report(campaign_id)")
|