mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-07-30 16:06:52 +03:00
Complete folder anatomy for all 649 cybersecurity skills + update LICENSE to Mahipal
- Add scripts/agent.py and references/api-reference.md to all remaining skills - Update all 648 LICENSE files: copyright now reads 'Mahipal' - Add implementing-security-monitoring-with-datadog (new skill with full anatomy) - All 649 skills now have: SKILL.md, LICENSE, scripts/agent.py, references/api-reference.md
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Agent for implementing and auditing SAML SSO with Okta.
|
||||
|
||||
Validates SAML configuration, checks certificate expiry, tests
|
||||
assertion encryption, audits attribute mappings, and verifies
|
||||
signature algorithms for enterprise SSO deployments.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import ssl
|
||||
import socket
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
try:
|
||||
import requests
|
||||
except ImportError:
|
||||
requests = None
|
||||
|
||||
|
||||
SAML_CHECKS = {
|
||||
"sha256_signature": "SignatureMethod must use SHA-256 (not SHA-1)",
|
||||
"assertion_encrypted": "Assertions should be encrypted in transit",
|
||||
"audience_restriction": "AudienceRestriction element must be present",
|
||||
"conditions_notbefore": "Conditions NotBefore/NotOnOrAfter must be set",
|
||||
"single_logout": "SingleLogoutService should be configured",
|
||||
}
|
||||
|
||||
|
||||
class SAMLSSOAgent:
|
||||
"""Audits and validates SAML SSO configurations with Okta."""
|
||||
|
||||
def __init__(self, okta_domain, api_token=None, output_dir="./saml_sso_audit"):
|
||||
self.okta_domain = okta_domain.rstrip("/")
|
||||
self.api_token = api_token
|
||||
self.output_dir = Path(output_dir)
|
||||
self.output_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.findings = []
|
||||
|
||||
def _okta_get(self, path):
|
||||
if not requests or not self.api_token:
|
||||
return None
|
||||
try:
|
||||
return requests.get(
|
||||
f"https://{self.okta_domain}/api/v1{path}",
|
||||
headers={"Authorization": f"SSWS {self.api_token}", "Accept": "application/json"},
|
||||
timeout=10,
|
||||
)
|
||||
except requests.RequestException:
|
||||
return None
|
||||
|
||||
def list_saml_apps(self):
|
||||
"""List SAML applications configured in Okta."""
|
||||
resp = self._okta_get("/apps?filter=status eq \"ACTIVE\"&limit=50")
|
||||
if not resp or resp.status_code != 200:
|
||||
return []
|
||||
apps = []
|
||||
for app in resp.json():
|
||||
sign_on = app.get("signOnMode", "")
|
||||
if "SAML" in sign_on.upper():
|
||||
apps.append({
|
||||
"id": app["id"], "label": app.get("label"),
|
||||
"sign_on_mode": sign_on,
|
||||
"status": app.get("status"),
|
||||
})
|
||||
return apps
|
||||
|
||||
def get_saml_metadata(self, app_id):
|
||||
"""Retrieve SAML metadata XML for an application."""
|
||||
resp = self._okta_get(f"/apps/{app_id}/sso/saml/metadata")
|
||||
if resp and resp.status_code == 200:
|
||||
return resp.text
|
||||
return None
|
||||
|
||||
def validate_metadata(self, metadata_xml):
|
||||
"""Validate SAML metadata for security best practices."""
|
||||
issues = []
|
||||
try:
|
||||
root = ET.fromstring(metadata_xml)
|
||||
except ET.ParseError:
|
||||
return [{"severity": "high", "issue": "Invalid SAML metadata XML"}]
|
||||
|
||||
ns = {"md": "urn:oasis:names:tc:SAML:2.0:metadata",
|
||||
"ds": "http://www.w3.org/2000/09/xmldsig#"}
|
||||
|
||||
sig_methods = root.findall(".//ds:SignatureMethod", ns)
|
||||
for sm in sig_methods:
|
||||
alg = sm.get("Algorithm", "")
|
||||
if "sha1" in alg.lower():
|
||||
issues.append({"severity": "high", "issue": "SHA-1 signature detected - upgrade to SHA-256"})
|
||||
self.findings.append({"severity": "high", "type": "Weak Signature",
|
||||
"detail": f"Algorithm: {alg}"})
|
||||
|
||||
slo = root.findall(".//md:SingleLogoutService", ns)
|
||||
if not slo:
|
||||
issues.append({"severity": "medium", "issue": "SingleLogoutService not configured"})
|
||||
|
||||
certs = root.findall(".//ds:X509Certificate", ns)
|
||||
if not certs:
|
||||
issues.append({"severity": "high", "issue": "No X.509 certificate in metadata"})
|
||||
|
||||
name_id = root.findall(".//{urn:oasis:names:tc:SAML:2.0:metadata}NameIDFormat")
|
||||
for nid in name_id:
|
||||
if "unspecified" in (nid.text or "").lower():
|
||||
issues.append({"severity": "medium", "issue": "NameIDFormat is unspecified"})
|
||||
|
||||
return issues
|
||||
|
||||
def check_certificate_expiry(self, host, port=443):
|
||||
"""Check SAML signing certificate expiration."""
|
||||
try:
|
||||
ctx = ssl.create_default_context()
|
||||
with ctx.wrap_socket(socket.socket(), server_hostname=host) as s:
|
||||
s.settimeout(5)
|
||||
s.connect((host, port))
|
||||
cert = s.getpeercert()
|
||||
not_after = datetime.strptime(cert["notAfter"], "%b %d %H:%M:%S %Y %Z")
|
||||
days_left = (not_after - datetime.utcnow()).days
|
||||
if days_left < 30:
|
||||
self.findings.append({"severity": "critical", "type": "Certificate Expiring",
|
||||
"detail": f"Certificate expires in {days_left} days"})
|
||||
return {"host": host, "expires": cert["notAfter"], "days_left": days_left}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
def audit_app_assignments(self, app_id):
|
||||
"""Audit user/group assignments for a SAML app."""
|
||||
resp = self._okta_get(f"/apps/{app_id}/users?limit=100")
|
||||
users = resp.json() if resp and resp.status_code == 200 else []
|
||||
resp_g = self._okta_get(f"/apps/{app_id}/groups?limit=100")
|
||||
groups = resp_g.json() if resp_g and resp_g.status_code == 200 else []
|
||||
if len(users) > 50:
|
||||
self.findings.append({"severity": "info", "type": "Large User Assignment",
|
||||
"detail": f"App {app_id} has {len(users)} direct user assignments"})
|
||||
return {"users": len(users), "groups": len(groups)}
|
||||
|
||||
def check_mfa_policy(self):
|
||||
"""Check if MFA is enforced for SAML authentication."""
|
||||
resp = self._okta_get("/policies?type=OKTA_SIGN_ON")
|
||||
if not resp or resp.status_code != 200:
|
||||
return {"error": "Cannot retrieve policies"}
|
||||
policies = resp.json()
|
||||
mfa_enforced = False
|
||||
for policy in policies:
|
||||
if policy.get("status") == "ACTIVE":
|
||||
for rule in policy.get("_embedded", {}).get("rules", []):
|
||||
actions = rule.get("actions", {}).get("signon", {})
|
||||
if actions.get("requireFactor"):
|
||||
mfa_enforced = True
|
||||
if not mfa_enforced:
|
||||
self.findings.append({"severity": "high", "type": "No MFA",
|
||||
"detail": "MFA not enforced in sign-on policies"})
|
||||
return {"mfa_enforced": mfa_enforced}
|
||||
|
||||
def generate_report(self):
|
||||
apps = self.list_saml_apps()
|
||||
metadata_issues = {}
|
||||
for app in apps:
|
||||
meta = self.get_saml_metadata(app["id"])
|
||||
if meta:
|
||||
metadata_issues[app["id"]] = self.validate_metadata(meta)
|
||||
cert = self.check_certificate_expiry(self.okta_domain)
|
||||
mfa = self.check_mfa_policy()
|
||||
|
||||
report = {
|
||||
"report_date": datetime.utcnow().isoformat(),
|
||||
"okta_domain": self.okta_domain,
|
||||
"saml_apps": apps,
|
||||
"metadata_validation": metadata_issues,
|
||||
"certificate_status": cert,
|
||||
"mfa_status": mfa,
|
||||
"findings": self.findings,
|
||||
"total_findings": len(self.findings),
|
||||
}
|
||||
out = self.output_dir / "saml_sso_report.json"
|
||||
with open(out, "w") as f:
|
||||
json.dump(report, f, indent=2)
|
||||
print(json.dumps(report, indent=2))
|
||||
return report
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: agent.py <okta_domain> [--token <api_token>]")
|
||||
sys.exit(1)
|
||||
domain = sys.argv[1]
|
||||
token = None
|
||||
if "--token" in sys.argv:
|
||||
token = sys.argv[sys.argv.index("--token") + 1]
|
||||
agent = SAMLSSOAgent(domain, token)
|
||||
agent.generate_report()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user