mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-12 22:24:56 +03:00
350 lines
12 KiB
Python
350 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
SSL Certificate Lifecycle Management Tool
|
|
|
|
Implements certificate generation, parsing, monitoring, chain validation,
|
|
and OCSP checking for managing TLS certificate lifecycles.
|
|
|
|
Requirements:
|
|
pip install cryptography requests
|
|
|
|
Usage:
|
|
python process.py generate-csr --domain example.com --output ./certs
|
|
python process.py check-expiry --host example.com
|
|
python process.py parse-cert --cert ./server.crt
|
|
python process.py monitor --domains domains.txt --threshold 30
|
|
python process.py verify-chain --cert ./server.crt --ca-bundle ./ca-bundle.crt
|
|
"""
|
|
|
|
import os
|
|
import ssl
|
|
import sys
|
|
import json
|
|
import socket
|
|
import argparse
|
|
import logging
|
|
import datetime
|
|
from pathlib import Path
|
|
from typing import Dict, List, Optional
|
|
|
|
from cryptography import x509
|
|
from cryptography.x509.oid import NameOID, ExtensionOID
|
|
from cryptography.hazmat.primitives import hashes, serialization
|
|
from cryptography.hazmat.primitives.asymmetric import ec, rsa
|
|
from cryptography.hazmat.backends import default_backend
|
|
|
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
|
logger = logging.getLogger(__name__)
|
|
|
|
EXPIRY_WARNING_DAYS = 30
|
|
EXPIRY_CRITICAL_DAYS = 15
|
|
|
|
|
|
def generate_csr(
|
|
domain: str,
|
|
output_dir: str,
|
|
key_type: str = "ecdsa",
|
|
san_domains: Optional[List[str]] = None,
|
|
organization: Optional[str] = None,
|
|
) -> Dict:
|
|
"""Generate a private key and CSR for a domain."""
|
|
output_path = Path(output_dir)
|
|
output_path.mkdir(parents=True, exist_ok=True)
|
|
|
|
if key_type == "ecdsa":
|
|
private_key = ec.generate_private_key(ec.SECP256R1(), default_backend())
|
|
else:
|
|
private_key = rsa.generate_private_key(
|
|
public_exponent=65537, key_size=4096, backend=default_backend()
|
|
)
|
|
|
|
subject_attrs = [
|
|
x509.NameAttribute(NameOID.COMMON_NAME, domain),
|
|
]
|
|
if organization:
|
|
subject_attrs.insert(0, x509.NameAttribute(NameOID.ORGANIZATION_NAME, organization))
|
|
|
|
subject = x509.Name(subject_attrs)
|
|
|
|
san_list = [x509.DNSName(domain)]
|
|
if san_domains:
|
|
for d in san_domains:
|
|
san_list.append(x509.DNSName(d))
|
|
|
|
csr = (
|
|
x509.CertificateSigningRequestBuilder()
|
|
.subject_name(subject)
|
|
.add_extension(x509.SubjectAlternativeName(san_list), critical=False)
|
|
.sign(private_key, hashes.SHA256(), default_backend())
|
|
)
|
|
|
|
key_path = output_path / f"{domain}.key"
|
|
csr_path = output_path / f"{domain}.csr"
|
|
|
|
key_path.write_bytes(
|
|
private_key.private_bytes(
|
|
encoding=serialization.Encoding.PEM,
|
|
format=serialization.PrivateFormat.PKCS8,
|
|
encryption_algorithm=serialization.NoEncryption(),
|
|
)
|
|
)
|
|
|
|
csr_path.write_bytes(csr.public_bytes(serialization.Encoding.PEM))
|
|
|
|
logger.info(f"Generated CSR for {domain}")
|
|
|
|
return {
|
|
"domain": domain,
|
|
"key_type": key_type,
|
|
"key_path": str(key_path),
|
|
"csr_path": str(csr_path),
|
|
"san_domains": [d.value for d in san_list],
|
|
}
|
|
|
|
|
|
def parse_certificate(cert_path: str) -> Dict:
|
|
"""Parse an X.509 certificate and extract key information."""
|
|
cert_data = Path(cert_path).read_bytes()
|
|
|
|
if b"-----BEGIN CERTIFICATE-----" in cert_data:
|
|
cert = x509.load_pem_x509_certificate(cert_data, default_backend())
|
|
else:
|
|
cert = x509.load_der_x509_certificate(cert_data, default_backend())
|
|
|
|
subject_attrs = {}
|
|
for attr in cert.subject:
|
|
subject_attrs[attr.oid._name] = attr.value
|
|
|
|
issuer_attrs = {}
|
|
for attr in cert.issuer:
|
|
issuer_attrs[attr.oid._name] = attr.value
|
|
|
|
san_names = []
|
|
try:
|
|
san_ext = cert.extensions.get_extension_for_oid(ExtensionOID.SUBJECT_ALTERNATIVE_NAME)
|
|
san_names = [name.value for name in san_ext.value.get_values_for_type(x509.DNSName)]
|
|
except x509.ExtensionNotFound:
|
|
pass
|
|
|
|
now = datetime.datetime.utcnow()
|
|
not_after = cert.not_valid_after_utc.replace(tzinfo=None)
|
|
days_remaining = (not_after - now).days
|
|
|
|
pub_key = cert.public_key()
|
|
if isinstance(pub_key, rsa.RSAPublicKey):
|
|
key_info = {"type": "RSA", "size": pub_key.key_size}
|
|
elif isinstance(pub_key, ec.EllipticCurvePublicKey):
|
|
key_info = {"type": "ECDSA", "curve": pub_key.curve.name, "size": pub_key.key_size}
|
|
else:
|
|
key_info = {"type": "Unknown"}
|
|
|
|
return {
|
|
"subject": subject_attrs,
|
|
"issuer": issuer_attrs,
|
|
"serial_number": hex(cert.serial_number),
|
|
"not_valid_before": cert.not_valid_before_utc.isoformat(),
|
|
"not_valid_after": cert.not_valid_after_utc.isoformat(),
|
|
"days_remaining": days_remaining,
|
|
"san_domains": san_names,
|
|
"signature_algorithm": cert.signature_algorithm_oid._name,
|
|
"public_key": key_info,
|
|
"version": cert.version.value,
|
|
"is_expired": days_remaining < 0,
|
|
"fingerprint_sha256": cert.fingerprint(hashes.SHA256()).hex(),
|
|
}
|
|
|
|
|
|
def check_remote_certificate(host: str, port: int = 443, timeout: int = 10) -> Dict:
|
|
"""Check the TLS certificate of a remote host."""
|
|
result = {
|
|
"host": host,
|
|
"port": port,
|
|
"status": "unknown",
|
|
"days_remaining": None,
|
|
"certificate": None,
|
|
"errors": [],
|
|
}
|
|
|
|
try:
|
|
ctx = ssl.create_default_context()
|
|
with socket.create_connection((host, port), timeout=timeout) as sock:
|
|
with ctx.wrap_socket(sock, server_hostname=host) as ssock:
|
|
cert_der = ssock.getpeercert(binary_form=True)
|
|
cert = x509.load_der_x509_certificate(cert_der, default_backend())
|
|
|
|
now = datetime.datetime.utcnow()
|
|
not_after = cert.not_valid_after_utc.replace(tzinfo=None)
|
|
days_remaining = (not_after - now).days
|
|
|
|
result["days_remaining"] = days_remaining
|
|
result["not_after"] = not_after.isoformat()
|
|
result["protocol"] = ssock.version()
|
|
result["cipher"] = ssock.cipher()[0]
|
|
|
|
subject_cn = None
|
|
for attr in cert.subject:
|
|
if attr.oid == NameOID.COMMON_NAME:
|
|
subject_cn = attr.value
|
|
break
|
|
|
|
result["common_name"] = subject_cn
|
|
result["fingerprint_sha256"] = cert.fingerprint(hashes.SHA256()).hex()
|
|
|
|
if days_remaining < 0:
|
|
result["status"] = "EXPIRED"
|
|
elif days_remaining < EXPIRY_CRITICAL_DAYS:
|
|
result["status"] = "CRITICAL"
|
|
elif days_remaining < EXPIRY_WARNING_DAYS:
|
|
result["status"] = "WARNING"
|
|
else:
|
|
result["status"] = "OK"
|
|
|
|
except ssl.SSLCertVerificationError as e:
|
|
result["status"] = "INVALID"
|
|
result["errors"].append(f"Certificate verification failed: {e}")
|
|
except socket.timeout:
|
|
result["status"] = "TIMEOUT"
|
|
result["errors"].append("Connection timed out")
|
|
except Exception as e:
|
|
result["status"] = "ERROR"
|
|
result["errors"].append(str(e))
|
|
|
|
return result
|
|
|
|
|
|
def monitor_domains(domains: List[str], threshold_days: int = 30) -> Dict:
|
|
"""Monitor certificate expiration for multiple domains."""
|
|
results = {
|
|
"scan_time": datetime.datetime.utcnow().isoformat() + "Z",
|
|
"threshold_days": threshold_days,
|
|
"total_domains": len(domains),
|
|
"ok": 0,
|
|
"warning": 0,
|
|
"critical": 0,
|
|
"expired": 0,
|
|
"errors": 0,
|
|
"domains": [],
|
|
}
|
|
|
|
for domain in domains:
|
|
domain = domain.strip()
|
|
if not domain or domain.startswith("#"):
|
|
continue
|
|
|
|
host = domain.split(":")[0]
|
|
port = int(domain.split(":")[1]) if ":" in domain else 443
|
|
|
|
logger.info(f"Checking {host}:{port}...")
|
|
check = check_remote_certificate(host, port)
|
|
results["domains"].append(check)
|
|
|
|
status = check["status"]
|
|
if status == "OK":
|
|
results["ok"] += 1
|
|
elif status == "WARNING":
|
|
results["warning"] += 1
|
|
elif status == "CRITICAL":
|
|
results["critical"] += 1
|
|
elif status == "EXPIRED":
|
|
results["expired"] += 1
|
|
else:
|
|
results["errors"] += 1
|
|
|
|
return results
|
|
|
|
|
|
def verify_certificate_chain(cert_path: str, ca_bundle_path: str) -> Dict:
|
|
"""Verify a certificate chain against a CA bundle."""
|
|
cert_data = Path(cert_path).read_bytes()
|
|
ca_data = Path(ca_bundle_path).read_bytes()
|
|
|
|
cert = x509.load_pem_x509_certificate(cert_data, default_backend())
|
|
|
|
ca_certs = []
|
|
pem_blocks = ca_data.split(b"-----END CERTIFICATE-----")
|
|
for block in pem_blocks:
|
|
block = block.strip()
|
|
if block and b"-----BEGIN CERTIFICATE-----" in block:
|
|
pem = block + b"\n-----END CERTIFICATE-----\n"
|
|
ca_certs.append(x509.load_pem_x509_certificate(pem, default_backend()))
|
|
|
|
chain = []
|
|
current = cert
|
|
chain.append({
|
|
"subject": current.subject.rfc4514_string(),
|
|
"issuer": current.issuer.rfc4514_string(),
|
|
})
|
|
|
|
for ca in ca_certs:
|
|
if current.issuer == ca.subject:
|
|
chain.append({
|
|
"subject": ca.subject.rfc4514_string(),
|
|
"issuer": ca.issuer.rfc4514_string(),
|
|
})
|
|
current = ca
|
|
if ca.issuer == ca.subject:
|
|
break
|
|
|
|
is_self_signed = cert.issuer == cert.subject
|
|
chain_complete = len(chain) > 1 or is_self_signed
|
|
|
|
return {
|
|
"certificate": cert.subject.rfc4514_string(),
|
|
"chain_length": len(chain),
|
|
"chain": chain,
|
|
"chain_complete": chain_complete,
|
|
"is_self_signed": is_self_signed,
|
|
}
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="SSL Certificate Lifecycle Tool")
|
|
subparsers = parser.add_subparsers(dest="command")
|
|
|
|
csr = subparsers.add_parser("generate-csr", help="Generate CSR")
|
|
csr.add_argument("--domain", required=True, help="Primary domain")
|
|
csr.add_argument("--output", default="./certs", help="Output directory")
|
|
csr.add_argument("--key-type", choices=["ecdsa", "rsa"], default="ecdsa")
|
|
csr.add_argument("--san", nargs="*", help="Additional SAN domains")
|
|
csr.add_argument("--org", help="Organization name")
|
|
|
|
parse = subparsers.add_parser("parse-cert", help="Parse certificate")
|
|
parse.add_argument("--cert", required=True, help="Certificate file path")
|
|
|
|
check = subparsers.add_parser("check-expiry", help="Check remote cert expiry")
|
|
check.add_argument("--host", required=True, help="Hostname")
|
|
check.add_argument("--port", type=int, default=443, help="Port")
|
|
|
|
mon = subparsers.add_parser("monitor", help="Monitor multiple domains")
|
|
mon.add_argument("--domains", required=True, help="File with domain list")
|
|
mon.add_argument("--threshold", type=int, default=30, help="Warning threshold (days)")
|
|
|
|
chain = subparsers.add_parser("verify-chain", help="Verify certificate chain")
|
|
chain.add_argument("--cert", required=True, help="Certificate file")
|
|
chain.add_argument("--ca-bundle", required=True, help="CA bundle file")
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.command == "generate-csr":
|
|
result = generate_csr(args.domain, args.output, args.key_type, args.san, args.org)
|
|
print(json.dumps(result, indent=2))
|
|
elif args.command == "parse-cert":
|
|
result = parse_certificate(args.cert)
|
|
print(json.dumps(result, indent=2, default=str))
|
|
elif args.command == "check-expiry":
|
|
result = check_remote_certificate(args.host, args.port)
|
|
print(json.dumps(result, indent=2, default=str))
|
|
elif args.command == "monitor":
|
|
domains = Path(args.domains).read_text().strip().split("\n")
|
|
result = monitor_domains(domains, args.threshold)
|
|
print(json.dumps(result, indent=2, default=str))
|
|
elif args.command == "verify-chain":
|
|
result = verify_certificate_chain(args.cert, args.ca_bundle)
|
|
print(json.dumps(result, indent=2))
|
|
else:
|
|
parser.print_help()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|