Files

253 lines
9.6 KiB
Python

#!/usr/bin/env python3
"""CyberArk PAM configuration audit agent.
Audits CyberArk Privileged Access Management via the REST API to
verify safe configurations, privileged account inventory, platform
assignments, and password rotation compliance.
"""
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_cyberark_config():
"""Return CyberArk PVWA URL."""
pvwa_url = os.environ.get("CYBERARK_PVWA_URL", "").rstrip("/")
if not pvwa_url:
print("[!] Set CYBERARK_PVWA_URL env var", file=sys.stderr)
sys.exit(1)
return pvwa_url
def authenticate(pvwa_url, username, password, auth_type="CyberArk"):
"""Authenticate and get session token."""
url = f"{pvwa_url}/PasswordVault/API/Auth/{auth_type}/Logon"
resp = requests.post(url, json={"username": username, "password": password},
verify=not os.environ.get("SKIP_TLS_VERIFY", "").lower() == "true", timeout=30) # Set SKIP_TLS_VERIFY=true for self-signed certs in lab environments
resp.raise_for_status()
token = resp.json().strip('"')
print(f"[+] Authenticated as {username}")
return token
def api_call(pvwa_url, token, endpoint, method="GET", data=None, params=None):
"""Make authenticated API call."""
url = f"{pvwa_url}/PasswordVault/API{endpoint}"
headers = {"Authorization": token, "Content-Type": "application/json"}
if method == "POST":
resp = requests.post(url, headers=headers, json=data, params=params,
verify=not os.environ.get("SKIP_TLS_VERIFY", "").lower() == "true", timeout=30) # Set SKIP_TLS_VERIFY=true for self-signed certs in lab environments
else:
resp = requests.get(url, headers=headers, params=params,
verify=not os.environ.get("SKIP_TLS_VERIFY", "").lower() == "true", timeout=30) # Set SKIP_TLS_VERIFY=true for self-signed certs in lab environments
resp.raise_for_status()
return resp.json()
def audit_safes(pvwa_url, token):
"""Audit safe configurations."""
findings = []
print("[*] Auditing safes...")
data = api_call(pvwa_url, token, "/Safes", params={"limit": 1000})
safes = data.get("value", data.get("Safes", []))
for safe in safes:
name = safe.get("safeName", safe.get("SafeName", ""))
member_count = safe.get("numberOfMembers", safe.get("NumberOfMembers", 0))
days_retention = safe.get("numberOfDaysRetention", safe.get("NumberOfDaysRetention", 0))
versions = safe.get("numberOfVersionsRetention", safe.get("NumberOfVersionsRetention", 0))
if member_count == 0:
findings.append({
"safe": name, "check": "Empty safe (no members)",
"severity": "MEDIUM", "detail": "Safe has no members assigned",
})
if days_retention == 0 and versions == 0:
findings.append({
"safe": name, "check": "No retention policy",
"severity": "HIGH",
"detail": "No password history retention configured",
})
print(f"[+] Audited {len(safes)} safes")
return findings, safes
def audit_accounts(pvwa_url, token, safe_name=None):
"""Audit privileged accounts."""
findings = []
print("[*] Auditing privileged accounts...")
params = {"limit": 1000}
if safe_name:
params["filter"] = f"safeName eq {safe_name}"
data = api_call(pvwa_url, token, "/Accounts", params=params)
accounts = data.get("value", [])
now = datetime.now(timezone.utc)
for acct in accounts:
acct_name = acct.get("name", "")
platform = acct.get("platformId", "")
safe = acct.get("safeName", "")
secret_mgmt = acct.get("secretManagement", {})
last_modified = secret_mgmt.get("lastModifiedTime", 0)
auto_mgmt = secret_mgmt.get("automaticManagementEnabled", False)
status = secret_mgmt.get("status", "")
if not auto_mgmt:
findings.append({
"account": acct_name, "safe": safe, "platform": platform,
"check": "Automatic password management disabled",
"severity": "HIGH",
"detail": "Password not managed by CyberArk CPM",
})
if last_modified:
last_mod_dt = datetime.fromtimestamp(last_modified, tz=timezone.utc)
age_days = (now - last_mod_dt).days
if age_days > 90:
findings.append({
"account": acct_name, "safe": safe,
"check": "Password age exceeds 90 days",
"severity": "HIGH",
"detail": f"Last rotated {age_days} days ago",
})
if status and status != "success":
findings.append({
"account": acct_name, "safe": safe,
"check": f"CPM status: {status}",
"severity": "CRITICAL" if "fail" in status.lower() else "MEDIUM",
"detail": f"Password management status: {status}",
})
print(f"[+] Audited {len(accounts)} accounts")
return findings, accounts
def audit_platforms(pvwa_url, token):
"""Audit platform configurations."""
findings = []
print("[*] Auditing platforms...")
data = api_call(pvwa_url, token, "/Platforms")
platforms = data.get("Platforms", data.get("value", []))
for plat in platforms:
name = plat.get("Name", plat.get("PlatformID", ""))
active = plat.get("Active", True)
if not active:
continue
priv_session = plat.get("PrivilegedSessionManagement", {})
if not priv_session.get("PSMServerId"):
findings.append({
"platform": name,
"check": "No PSM configured",
"severity": "MEDIUM",
"detail": "Platform missing privileged session management",
})
print(f"[+] Audited {len(platforms)} platforms")
return findings, platforms
def logoff(pvwa_url, token):
"""End the CyberArk session."""
try:
requests.post(f"{pvwa_url}/PasswordVault/API/Auth/Logoff",
headers={"Authorization": token},
verify=not os.environ.get("SKIP_TLS_VERIFY", "").lower() == "true", timeout=10) # Set SKIP_TLS_VERIFY=true for self-signed certs in lab environments
print("[+] Session ended")
except requests.RequestException:
pass
def format_summary(safe_findings, acct_findings, plat_findings, safes, accounts):
"""Print audit summary."""
all_findings = safe_findings + acct_findings + plat_findings
print(f"\n{'='*60}")
print(f" CyberArk PAM Audit Report")
print(f"{'='*60}")
print(f" Safes : {len(safes)}")
print(f" Accounts : {len(accounts)}")
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 all_findings:
print(f"\n Top Issues:")
for f in all_findings[:15]:
if f["severity"] in ("CRITICAL", "HIGH"):
print(f" [{f['severity']:8s}] {f['check']}: "
f"{f.get('account', f.get('safe', f.get('platform', '')))}")
return severity_counts
def main():
parser = argparse.ArgumentParser(description="CyberArk PAM configuration audit agent")
parser.add_argument("--pvwa-url", help="PVWA URL (or CYBERARK_PVWA_URL env)")
parser.add_argument("--username", required=True, help="CyberArk username")
parser.add_argument("--password", required=True, help="CyberArk password")
parser.add_argument("--auth-type", default="CyberArk",
choices=["CyberArk", "LDAP", "RADIUS", "Windows"])
parser.add_argument("--safe", help="Audit specific safe only")
parser.add_argument("--output", "-o", help="Output JSON report")
parser.add_argument("--verbose", "-v", action="store_true")
args = parser.parse_args()
if args.pvwa_url:
os.environ["CYBERARK_PVWA_URL"] = args.pvwa_url
pvwa_url = get_cyberark_config()
token = authenticate(pvwa_url, args.username, args.password, args.auth_type)
try:
safe_findings, safes = audit_safes(pvwa_url, token)
acct_findings, accounts = audit_accounts(pvwa_url, token, args.safe)
plat_findings, platforms = audit_platforms(pvwa_url, token)
finally:
logoff(pvwa_url, token)
severity_counts = format_summary(safe_findings, acct_findings, plat_findings, safes, accounts)
report = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"tool": "CyberArk PAM Audit",
"safes_count": len(safes),
"accounts_count": len(accounts),
"findings": safe_findings + acct_findings + plat_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()