mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-11 21:54:56 +03:00
235 lines
8.1 KiB
Python
235 lines
8.1 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Google Workspace SSO Configuration Validator
|
|
|
|
Validates SAML SSO configuration between an IdP and Google Workspace by
|
|
checking SAML metadata, testing authentication flows, and verifying
|
|
certificate validity.
|
|
|
|
Requirements:
|
|
pip install requests cryptography lxml
|
|
"""
|
|
|
|
import base64
|
|
import json
|
|
import sys
|
|
import zlib
|
|
from datetime import datetime, timezone
|
|
from urllib.parse import urlencode, parse_qs, urlparse
|
|
|
|
try:
|
|
import requests
|
|
from cryptography import x509
|
|
from cryptography.hazmat.primitives import serialization
|
|
except ImportError:
|
|
print("[ERROR] Required: pip install requests cryptography")
|
|
sys.exit(1)
|
|
|
|
|
|
class GoogleWorkspaceSSOValidator:
|
|
"""Validate Google Workspace SAML SSO configuration."""
|
|
|
|
def __init__(self, domain):
|
|
self.domain = domain
|
|
self.acs_url = f"https://www.google.com/a/{domain}/acs"
|
|
self.entity_id = f"google.com/a/{domain}"
|
|
|
|
def validate_idp_metadata(self, metadata_url):
|
|
"""Fetch and validate IdP SAML metadata XML."""
|
|
try:
|
|
from lxml import etree
|
|
except ImportError:
|
|
return {"valid": False, "error": "lxml required for XML parsing"}
|
|
|
|
try:
|
|
resp = requests.get(metadata_url, timeout=15)
|
|
resp.raise_for_status()
|
|
except requests.RequestException as e:
|
|
return {"valid": False, "error": f"Cannot fetch metadata: {e}"}
|
|
|
|
try:
|
|
root = etree.fromstring(resp.content)
|
|
except etree.XMLSyntaxError as e:
|
|
return {"valid": False, "error": f"Invalid XML: {e}"}
|
|
|
|
ns = {
|
|
"md": "urn:oasis:names:tc:SAML:2.0:metadata",
|
|
"ds": "http://www.w3.org/2000/09/xmldsig#",
|
|
}
|
|
|
|
entity_id = root.get("entityID")
|
|
sso_elements = root.findall(
|
|
".//md:IDPSSODescriptor/md:SingleSignOnService", ns
|
|
)
|
|
sso_urls = [
|
|
{"binding": el.get("Binding"), "location": el.get("Location")}
|
|
for el in sso_elements
|
|
]
|
|
|
|
cert_elements = root.findall(
|
|
".//md:IDPSSODescriptor/md:KeyDescriptor/ds:KeyInfo/ds:X509Data/ds:X509Certificate",
|
|
ns,
|
|
)
|
|
certificates = [el.text.strip() for el in cert_elements if el.text]
|
|
|
|
return {
|
|
"valid": True,
|
|
"entity_id": entity_id,
|
|
"sso_endpoints": sso_urls,
|
|
"certificate_count": len(certificates),
|
|
"has_post_binding": any(
|
|
"HTTP-POST" in s["binding"] for s in sso_urls
|
|
),
|
|
"has_redirect_binding": any(
|
|
"HTTP-Redirect" in s["binding"] for s in sso_urls
|
|
),
|
|
}
|
|
|
|
def validate_certificate(self, cert_pem):
|
|
"""Validate the IdP signing certificate."""
|
|
try:
|
|
if "-----BEGIN CERTIFICATE-----" not in cert_pem:
|
|
cert_pem = (
|
|
"-----BEGIN CERTIFICATE-----\n"
|
|
+ cert_pem
|
|
+ "\n-----END CERTIFICATE-----"
|
|
)
|
|
|
|
cert = x509.load_pem_x509_certificate(cert_pem.encode())
|
|
now = datetime.now(timezone.utc)
|
|
|
|
return {
|
|
"valid": True,
|
|
"subject": cert.subject.rfc4514_string(),
|
|
"issuer": cert.issuer.rfc4514_string(),
|
|
"not_before": cert.not_valid_before_utc.isoformat(),
|
|
"not_after": cert.not_valid_after_utc.isoformat(),
|
|
"is_expired": now > cert.not_valid_after_utc,
|
|
"days_until_expiry": (cert.not_valid_after_utc - now).days,
|
|
"serial_number": str(cert.serial_number),
|
|
"signature_algorithm": cert.signature_algorithm_oid._name,
|
|
"key_size": cert.public_key().key_size,
|
|
}
|
|
except Exception as e:
|
|
return {"valid": False, "error": str(e)}
|
|
|
|
def validate_sso_configuration(self, idp_sso_url, idp_entity_id,
|
|
cert_pem):
|
|
"""Run complete SSO configuration validation."""
|
|
results = {
|
|
"domain": self.domain,
|
|
"acs_url": self.acs_url,
|
|
"entity_id": self.entity_id,
|
|
"checks": [],
|
|
}
|
|
|
|
# Check 1: ACS URL format
|
|
results["checks"].append({
|
|
"check": "ACS URL format",
|
|
"expected": f"https://www.google.com/a/{self.domain}/acs",
|
|
"status": "PASS",
|
|
})
|
|
|
|
# Check 2: Entity ID format
|
|
results["checks"].append({
|
|
"check": "Entity ID format",
|
|
"expected": f"google.com/a/{self.domain}",
|
|
"status": "PASS",
|
|
})
|
|
|
|
# Check 3: IdP SSO URL is HTTPS
|
|
is_https = idp_sso_url.startswith("https://")
|
|
results["checks"].append({
|
|
"check": "IdP SSO URL uses HTTPS",
|
|
"value": idp_sso_url,
|
|
"status": "PASS" if is_https else "FAIL",
|
|
})
|
|
|
|
# Check 4: IdP SSO URL is reachable
|
|
try:
|
|
resp = requests.head(idp_sso_url, timeout=10, allow_redirects=True)
|
|
reachable = resp.status_code < 500
|
|
except requests.RequestException:
|
|
reachable = False
|
|
results["checks"].append({
|
|
"check": "IdP SSO URL reachable",
|
|
"status": "PASS" if reachable else "FAIL",
|
|
})
|
|
|
|
# Check 5: Certificate validation
|
|
cert_result = self.validate_certificate(cert_pem)
|
|
results["checks"].append({
|
|
"check": "IdP certificate valid",
|
|
"status": "PASS" if cert_result.get("valid") and not cert_result.get("is_expired") else "FAIL",
|
|
"details": cert_result,
|
|
})
|
|
|
|
# Check 6: Certificate expiry warning
|
|
days_left = cert_result.get("days_until_expiry", 0)
|
|
results["checks"].append({
|
|
"check": "Certificate expiry > 30 days",
|
|
"days_remaining": days_left,
|
|
"status": "PASS" if days_left > 30 else "WARN",
|
|
})
|
|
|
|
# Check 7: Key size
|
|
key_size = cert_result.get("key_size", 0)
|
|
results["checks"].append({
|
|
"check": "Certificate key size >= 2048",
|
|
"key_size": key_size,
|
|
"status": "PASS" if key_size >= 2048 else "FAIL",
|
|
})
|
|
|
|
all_pass = all(
|
|
c["status"] in ("PASS", "WARN") for c in results["checks"]
|
|
)
|
|
results["overall_status"] = "PASS" if all_pass else "FAIL"
|
|
|
|
return results
|
|
|
|
def generate_saml_authn_request(self, idp_sso_url):
|
|
"""Generate a SAML AuthnRequest for testing SP-initiated SSO."""
|
|
request_id = f"_google_workspace_test_{int(datetime.now().timestamp())}"
|
|
issue_instant = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
|
|
authn_request = f"""<samlp:AuthnRequest
|
|
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
|
|
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
|
|
ID="{request_id}"
|
|
Version="2.0"
|
|
IssueInstant="{issue_instant}"
|
|
AssertionConsumerServiceURL="{self.acs_url}"
|
|
Destination="{idp_sso_url}"
|
|
ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST">
|
|
<saml:Issuer>{self.entity_id}</saml:Issuer>
|
|
<samlp:NameIDPolicy
|
|
Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
|
|
AllowCreate="true"/>
|
|
</samlp:AuthnRequest>"""
|
|
|
|
compressed = zlib.compress(authn_request.encode())[2:-4]
|
|
encoded = base64.b64encode(compressed).decode()
|
|
sso_redirect_url = f"{idp_sso_url}?{urlencode({'SAMLRequest': encoded})}"
|
|
|
|
return {
|
|
"request_id": request_id,
|
|
"authn_request_xml": authn_request,
|
|
"encoded_request": encoded,
|
|
"redirect_url": sso_redirect_url,
|
|
}
|
|
|
|
|
|
if __name__ == "__main__":
|
|
print("=" * 60)
|
|
print("Google Workspace SSO Configuration Validator")
|
|
print("=" * 60)
|
|
print()
|
|
print("Usage:")
|
|
print(" validator = GoogleWorkspaceSSOValidator('example.com')")
|
|
print(" result = validator.validate_sso_configuration(")
|
|
print(" idp_sso_url='https://idp.example.com/sso/saml',")
|
|
print(" idp_entity_id='https://idp.example.com',")
|
|
print(" cert_pem=open('idp_cert.pem').read()")
|
|
print(" )")
|
|
print(" print(json.dumps(result, indent=2))")
|