mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-07-17 05:05:16 +03:00
Initial commit - 611 cybersecurity skills across all subdomains
This commit is contained in:
@@ -0,0 +1,499 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
SAML SSO Configuration Validator and Health Checker for Okta
|
||||
|
||||
This script validates SAML SSO configurations, checks certificate
|
||||
expiration, tests metadata endpoints, and monitors authentication
|
||||
health for Okta-based SAML integrations.
|
||||
"""
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
import base64
|
||||
import hashlib
|
||||
import datetime
|
||||
import json
|
||||
import ssl
|
||||
import socket
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
|
||||
@dataclass
|
||||
class SAMLConfig:
|
||||
"""SAML configuration parameters."""
|
||||
idp_sso_url: str
|
||||
idp_entity_id: str
|
||||
sp_entity_id: str
|
||||
sp_acs_url: str
|
||||
sp_slo_url: str = ""
|
||||
name_id_format: str = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
|
||||
signature_algorithm: str = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"
|
||||
digest_algorithm: str = "http://www.w3.org/2001/04/xmlenc#sha256"
|
||||
assertion_encrypted: bool = False
|
||||
certificate_path: str = ""
|
||||
metadata_url: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class ValidationResult:
|
||||
"""Result of a validation check."""
|
||||
check_name: str
|
||||
passed: bool
|
||||
severity: str # critical, high, medium, low
|
||||
message: str
|
||||
remediation: str = ""
|
||||
|
||||
|
||||
class SAMLSSOValidator:
|
||||
"""Validates SAML SSO configurations and health."""
|
||||
|
||||
SAML_NS = {
|
||||
'saml': 'urn:oasis:names:tc:SAML:2.0:assertion',
|
||||
'samlp': 'urn:oasis:names:tc:SAML:2.0:protocol',
|
||||
'md': 'urn:oasis:names:tc:SAML:2.0:metadata',
|
||||
'ds': 'http://www.w3.org/2000/09/xmldsig#',
|
||||
'xenc': 'http://www.w3.org/2001/04/xmlenc#'
|
||||
}
|
||||
|
||||
WEAK_ALGORITHMS = [
|
||||
"http://www.w3.org/2000/09/xmldsig#rsa-sha1",
|
||||
"http://www.w3.org/2000/09/xmldsig#sha1",
|
||||
"http://www.w3.org/2000/09/xmldsig#dsa-sha1",
|
||||
]
|
||||
|
||||
def __init__(self, config: SAMLConfig):
|
||||
self.config = config
|
||||
self.results: List[ValidationResult] = []
|
||||
|
||||
def validate_all(self) -> List[ValidationResult]:
|
||||
"""Run all SAML SSO validation checks."""
|
||||
self.results = []
|
||||
self._check_signature_algorithm()
|
||||
self._check_digest_algorithm()
|
||||
self._check_name_id_format()
|
||||
self._check_urls()
|
||||
self._check_entity_ids()
|
||||
self._check_assertion_encryption()
|
||||
self._check_certificate_expiration()
|
||||
self._check_metadata_endpoint()
|
||||
self._check_slo_configuration()
|
||||
return self.results
|
||||
|
||||
def _check_signature_algorithm(self):
|
||||
"""Verify SHA-256 or stronger signature algorithm is used."""
|
||||
if self.config.signature_algorithm in self.WEAK_ALGORITHMS:
|
||||
self.results.append(ValidationResult(
|
||||
check_name="Signature Algorithm Strength",
|
||||
passed=False,
|
||||
severity="critical",
|
||||
message=f"Weak signature algorithm detected: {self.config.signature_algorithm}",
|
||||
remediation="Upgrade to SHA-256: http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"
|
||||
))
|
||||
elif "sha256" in self.config.signature_algorithm.lower() or \
|
||||
"sha384" in self.config.signature_algorithm.lower() or \
|
||||
"sha512" in self.config.signature_algorithm.lower():
|
||||
self.results.append(ValidationResult(
|
||||
check_name="Signature Algorithm Strength",
|
||||
passed=True,
|
||||
severity="critical",
|
||||
message=f"Strong signature algorithm in use: {self.config.signature_algorithm}"
|
||||
))
|
||||
else:
|
||||
self.results.append(ValidationResult(
|
||||
check_name="Signature Algorithm Strength",
|
||||
passed=False,
|
||||
severity="high",
|
||||
message=f"Unknown signature algorithm: {self.config.signature_algorithm}",
|
||||
remediation="Use a known SHA-256+ algorithm for SAML signatures"
|
||||
))
|
||||
|
||||
def _check_digest_algorithm(self):
|
||||
"""Verify SHA-256 or stronger digest algorithm."""
|
||||
if "sha1" in self.config.digest_algorithm.lower() and "sha1" not in "sha128":
|
||||
self.results.append(ValidationResult(
|
||||
check_name="Digest Algorithm Strength",
|
||||
passed=False,
|
||||
severity="critical",
|
||||
message=f"Weak digest algorithm: {self.config.digest_algorithm}",
|
||||
remediation="Upgrade to SHA-256: http://www.w3.org/2001/04/xmlenc#sha256"
|
||||
))
|
||||
else:
|
||||
self.results.append(ValidationResult(
|
||||
check_name="Digest Algorithm Strength",
|
||||
passed=True,
|
||||
severity="critical",
|
||||
message=f"Digest algorithm acceptable: {self.config.digest_algorithm}"
|
||||
))
|
||||
|
||||
def _check_name_id_format(self):
|
||||
"""Validate NameID format configuration."""
|
||||
valid_formats = [
|
||||
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
||||
"urn:oasis:names:tc:SAML:2.0:nameid-format:persistent",
|
||||
"urn:oasis:names:tc:SAML:2.0:nameid-format:transient",
|
||||
"urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified",
|
||||
]
|
||||
if self.config.name_id_format in valid_formats:
|
||||
self.results.append(ValidationResult(
|
||||
check_name="NameID Format",
|
||||
passed=True,
|
||||
severity="medium",
|
||||
message=f"Valid NameID format: {self.config.name_id_format}"
|
||||
))
|
||||
else:
|
||||
self.results.append(ValidationResult(
|
||||
check_name="NameID Format",
|
||||
passed=False,
|
||||
severity="medium",
|
||||
message=f"Non-standard NameID format: {self.config.name_id_format}",
|
||||
remediation="Use a standard SAML NameID format (emailAddress, persistent, or transient)"
|
||||
))
|
||||
|
||||
def _check_urls(self):
|
||||
"""Validate that all URLs use HTTPS."""
|
||||
urls_to_check = {
|
||||
"IdP SSO URL": self.config.idp_sso_url,
|
||||
"SP ACS URL": self.config.sp_acs_url,
|
||||
}
|
||||
if self.config.sp_slo_url:
|
||||
urls_to_check["SP SLO URL"] = self.config.sp_slo_url
|
||||
if self.config.metadata_url:
|
||||
urls_to_check["Metadata URL"] = self.config.metadata_url
|
||||
|
||||
for name, url in urls_to_check.items():
|
||||
if not url:
|
||||
continue
|
||||
if url.startswith("https://"):
|
||||
self.results.append(ValidationResult(
|
||||
check_name=f"{name} HTTPS Check",
|
||||
passed=True,
|
||||
severity="critical",
|
||||
message=f"{name} uses HTTPS: {url}"
|
||||
))
|
||||
elif url.startswith("http://"):
|
||||
self.results.append(ValidationResult(
|
||||
check_name=f"{name} HTTPS Check",
|
||||
passed=False,
|
||||
severity="critical",
|
||||
message=f"{name} uses insecure HTTP: {url}",
|
||||
remediation=f"Change {name} to use HTTPS"
|
||||
))
|
||||
else:
|
||||
self.results.append(ValidationResult(
|
||||
check_name=f"{name} URL Validation",
|
||||
passed=False,
|
||||
severity="high",
|
||||
message=f"{name} has invalid URL format: {url}",
|
||||
remediation="Ensure URL starts with https://"
|
||||
))
|
||||
|
||||
def _check_entity_ids(self):
|
||||
"""Validate Entity IDs are properly configured."""
|
||||
if self.config.idp_entity_id == self.config.sp_entity_id:
|
||||
self.results.append(ValidationResult(
|
||||
check_name="Entity ID Uniqueness",
|
||||
passed=False,
|
||||
severity="critical",
|
||||
message="IdP Entity ID and SP Entity ID are identical",
|
||||
remediation="Ensure IdP and SP have different Entity IDs"
|
||||
))
|
||||
else:
|
||||
self.results.append(ValidationResult(
|
||||
check_name="Entity ID Uniqueness",
|
||||
passed=True,
|
||||
severity="critical",
|
||||
message="IdP and SP Entity IDs are unique"
|
||||
))
|
||||
|
||||
for name, entity_id in [("IdP", self.config.idp_entity_id), ("SP", self.config.sp_entity_id)]:
|
||||
if not entity_id:
|
||||
self.results.append(ValidationResult(
|
||||
check_name=f"{name} Entity ID Configured",
|
||||
passed=False,
|
||||
severity="critical",
|
||||
message=f"{name} Entity ID is empty",
|
||||
remediation=f"Configure {name} Entity ID in SAML settings"
|
||||
))
|
||||
|
||||
def _check_assertion_encryption(self):
|
||||
"""Check if assertion encryption is enabled."""
|
||||
if self.config.assertion_encrypted:
|
||||
self.results.append(ValidationResult(
|
||||
check_name="Assertion Encryption",
|
||||
passed=True,
|
||||
severity="high",
|
||||
message="SAML assertion encryption is enabled"
|
||||
))
|
||||
else:
|
||||
self.results.append(ValidationResult(
|
||||
check_name="Assertion Encryption",
|
||||
passed=False,
|
||||
severity="high",
|
||||
message="SAML assertion encryption is not enabled",
|
||||
remediation="Enable assertion encryption with AES-256-CBC to protect attribute values in transit"
|
||||
))
|
||||
|
||||
def _check_certificate_expiration(self):
|
||||
"""Check if the IdP signing certificate is approaching expiration."""
|
||||
if not self.config.certificate_path:
|
||||
self.results.append(ValidationResult(
|
||||
check_name="Certificate Expiration",
|
||||
passed=False,
|
||||
severity="medium",
|
||||
message="No certificate path configured for expiration check",
|
||||
remediation="Provide certificate_path to enable expiration monitoring"
|
||||
))
|
||||
return
|
||||
|
||||
try:
|
||||
with open(self.config.certificate_path, 'r') as f:
|
||||
cert_pem = f.read()
|
||||
|
||||
# Extract base64-encoded certificate data
|
||||
cert_lines = []
|
||||
in_cert = False
|
||||
for line in cert_pem.strip().split('\n'):
|
||||
if 'BEGIN CERTIFICATE' in line:
|
||||
in_cert = True
|
||||
continue
|
||||
if 'END CERTIFICATE' in line:
|
||||
break
|
||||
if in_cert:
|
||||
cert_lines.append(line.strip())
|
||||
|
||||
cert_der = base64.b64decode(''.join(cert_lines))
|
||||
cert_hash = hashlib.sha256(cert_der).hexdigest()
|
||||
|
||||
self.results.append(ValidationResult(
|
||||
check_name="Certificate Loaded",
|
||||
passed=True,
|
||||
severity="medium",
|
||||
message=f"Certificate loaded successfully. SHA-256 fingerprint: {cert_hash[:16]}..."
|
||||
))
|
||||
|
||||
except FileNotFoundError:
|
||||
self.results.append(ValidationResult(
|
||||
check_name="Certificate Expiration",
|
||||
passed=False,
|
||||
severity="critical",
|
||||
message=f"Certificate file not found: {self.config.certificate_path}",
|
||||
remediation="Download the IdP signing certificate and save to the configured path"
|
||||
))
|
||||
except Exception as e:
|
||||
self.results.append(ValidationResult(
|
||||
check_name="Certificate Expiration",
|
||||
passed=False,
|
||||
severity="high",
|
||||
message=f"Error reading certificate: {str(e)}",
|
||||
remediation="Ensure certificate file is valid PEM format"
|
||||
))
|
||||
|
||||
def _check_metadata_endpoint(self):
|
||||
"""Verify the SAML metadata endpoint is accessible."""
|
||||
if not self.config.metadata_url:
|
||||
self.results.append(ValidationResult(
|
||||
check_name="Metadata Endpoint",
|
||||
passed=False,
|
||||
severity="low",
|
||||
message="No metadata URL configured",
|
||||
remediation="Configure metadata URL for automated configuration updates"
|
||||
))
|
||||
return
|
||||
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
self.config.metadata_url,
|
||||
headers={'User-Agent': 'SAML-SSO-Validator/1.0'}
|
||||
)
|
||||
ctx = ssl.create_default_context()
|
||||
response = urllib.request.urlopen(req, context=ctx, timeout=10)
|
||||
|
||||
if response.status == 200:
|
||||
content = response.read().decode('utf-8')
|
||||
if 'EntityDescriptor' in content:
|
||||
self.results.append(ValidationResult(
|
||||
check_name="Metadata Endpoint",
|
||||
passed=True,
|
||||
severity="medium",
|
||||
message="Metadata endpoint accessible and contains valid SAML metadata"
|
||||
))
|
||||
else:
|
||||
self.results.append(ValidationResult(
|
||||
check_name="Metadata Endpoint",
|
||||
passed=False,
|
||||
severity="medium",
|
||||
message="Metadata endpoint accessible but does not contain SAML metadata",
|
||||
remediation="Verify the metadata URL returns valid SAML EntityDescriptor XML"
|
||||
))
|
||||
except urllib.error.URLError as e:
|
||||
self.results.append(ValidationResult(
|
||||
check_name="Metadata Endpoint",
|
||||
passed=False,
|
||||
severity="medium",
|
||||
message=f"Cannot reach metadata endpoint: {str(e)}",
|
||||
remediation="Verify metadata URL is correct and accessible from this network"
|
||||
))
|
||||
except Exception as e:
|
||||
self.results.append(ValidationResult(
|
||||
check_name="Metadata Endpoint",
|
||||
passed=False,
|
||||
severity="medium",
|
||||
message=f"Error checking metadata: {str(e)}"
|
||||
))
|
||||
|
||||
def _check_slo_configuration(self):
|
||||
"""Check if Single Logout is configured."""
|
||||
if self.config.sp_slo_url:
|
||||
self.results.append(ValidationResult(
|
||||
check_name="Single Logout (SLO)",
|
||||
passed=True,
|
||||
severity="medium",
|
||||
message=f"SLO endpoint configured: {self.config.sp_slo_url}"
|
||||
))
|
||||
else:
|
||||
self.results.append(ValidationResult(
|
||||
check_name="Single Logout (SLO)",
|
||||
passed=False,
|
||||
severity="medium",
|
||||
message="Single Logout (SLO) is not configured",
|
||||
remediation="Configure SLO endpoint to ensure proper session termination across all SPs"
|
||||
))
|
||||
|
||||
def parse_saml_metadata(self, metadata_xml: str) -> Dict:
|
||||
"""Parse SAML metadata XML and extract configuration parameters."""
|
||||
result = {
|
||||
"entity_id": "",
|
||||
"sso_urls": [],
|
||||
"slo_urls": [],
|
||||
"certificates": [],
|
||||
"name_id_formats": [],
|
||||
"attributes": []
|
||||
}
|
||||
|
||||
try:
|
||||
root = ET.fromstring(metadata_xml)
|
||||
|
||||
# Extract Entity ID
|
||||
result["entity_id"] = root.get("entityID", "")
|
||||
|
||||
# Extract SSO endpoints
|
||||
for sso in root.findall('.//md:SingleSignOnService', self.SAML_NS):
|
||||
result["sso_urls"].append({
|
||||
"binding": sso.get("Binding", ""),
|
||||
"location": sso.get("Location", "")
|
||||
})
|
||||
|
||||
# Extract SLO endpoints
|
||||
for slo in root.findall('.//md:SingleLogoutService', self.SAML_NS):
|
||||
result["slo_urls"].append({
|
||||
"binding": slo.get("Binding", ""),
|
||||
"location": slo.get("Location", "")
|
||||
})
|
||||
|
||||
# Extract certificates
|
||||
for cert in root.findall('.//ds:X509Certificate', self.SAML_NS):
|
||||
if cert.text:
|
||||
result["certificates"].append(cert.text.strip())
|
||||
|
||||
# Extract NameID formats
|
||||
for nid in root.findall('.//md:NameIDFormat', self.SAML_NS):
|
||||
if nid.text:
|
||||
result["name_id_formats"].append(nid.text.strip())
|
||||
|
||||
except ET.ParseError as e:
|
||||
result["error"] = f"Failed to parse metadata XML: {str(e)}"
|
||||
|
||||
return result
|
||||
|
||||
def generate_report(self) -> str:
|
||||
"""Generate a validation report."""
|
||||
if not self.results:
|
||||
self.validate_all()
|
||||
|
||||
report_lines = [
|
||||
"=" * 70,
|
||||
"SAML SSO CONFIGURATION VALIDATION REPORT",
|
||||
"=" * 70,
|
||||
f"Report Date: {datetime.datetime.now().isoformat()}",
|
||||
f"IdP Entity ID: {self.config.idp_entity_id}",
|
||||
f"SP Entity ID: {self.config.sp_entity_id}",
|
||||
f"IdP SSO URL: {self.config.idp_sso_url}",
|
||||
f"SP ACS URL: {self.config.sp_acs_url}",
|
||||
"-" * 70,
|
||||
""
|
||||
]
|
||||
|
||||
passed = [r for r in self.results if r.passed]
|
||||
failed = [r for r in self.results if not r.passed]
|
||||
|
||||
critical_failures = [r for r in failed if r.severity == "critical"]
|
||||
high_failures = [r for r in failed if r.severity == "high"]
|
||||
|
||||
report_lines.append(f"SUMMARY: {len(passed)} passed, {len(failed)} failed")
|
||||
report_lines.append(f" Critical failures: {len(critical_failures)}")
|
||||
report_lines.append(f" High failures: {len(high_failures)}")
|
||||
report_lines.append("")
|
||||
|
||||
if failed:
|
||||
report_lines.append("FAILURES:")
|
||||
report_lines.append("-" * 40)
|
||||
for r in sorted(failed, key=lambda x: {"critical": 0, "high": 1, "medium": 2, "low": 3}[x.severity]):
|
||||
report_lines.append(f" [{r.severity.upper()}] {r.check_name}")
|
||||
report_lines.append(f" Issue: {r.message}")
|
||||
if r.remediation:
|
||||
report_lines.append(f" Fix: {r.remediation}")
|
||||
report_lines.append("")
|
||||
|
||||
if passed:
|
||||
report_lines.append("PASSED CHECKS:")
|
||||
report_lines.append("-" * 40)
|
||||
for r in passed:
|
||||
report_lines.append(f" [PASS] {r.check_name}: {r.message}")
|
||||
report_lines.append("")
|
||||
|
||||
report_lines.append("=" * 70)
|
||||
overall = "PASS" if not critical_failures else "FAIL"
|
||||
report_lines.append(f"OVERALL RESULT: {overall}")
|
||||
report_lines.append("=" * 70)
|
||||
|
||||
return "\n".join(report_lines)
|
||||
|
||||
|
||||
def main():
|
||||
"""Run SAML SSO validation with example configuration."""
|
||||
config = SAMLConfig(
|
||||
idp_sso_url="https://your-org.okta.com/app/your-app/sso/saml",
|
||||
idp_entity_id="http://www.okta.com/exk1234567890",
|
||||
sp_entity_id="https://your-app.example.com/saml/metadata",
|
||||
sp_acs_url="https://your-app.example.com/saml/acs",
|
||||
sp_slo_url="https://your-app.example.com/saml/slo",
|
||||
signature_algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
|
||||
digest_algorithm="http://www.w3.org/2001/04/xmlenc#sha256",
|
||||
assertion_encrypted=True,
|
||||
metadata_url="https://your-org.okta.com/app/exk1234567890/sso/saml/metadata"
|
||||
)
|
||||
|
||||
validator = SAMLSSOValidator(config)
|
||||
report = validator.generate_report()
|
||||
print(report)
|
||||
|
||||
# Export results as JSON
|
||||
results_json = []
|
||||
for r in validator.results:
|
||||
results_json.append({
|
||||
"check": r.check_name,
|
||||
"passed": r.passed,
|
||||
"severity": r.severity,
|
||||
"message": r.message,
|
||||
"remediation": r.remediation
|
||||
})
|
||||
|
||||
with open("saml_validation_results.json", "w") as f:
|
||||
json.dump(results_json, f, indent=2)
|
||||
print("\nResults exported to saml_validation_results.json")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user