Files
mukul975 c47eed6a64 Production hardening: security fixes, code quality, 724 skills complete
- Fix 25 shell=True subprocess calls with list-based commands
- Fix 49 verify=False in defensive skills (env-var override)
- Add timeout to 231 HTTP/subprocess/socket calls
- Fix 6 SQL injection patterns with whitelist validation
- Replace 8 __import__() with standard imports
- Remove 701 unused imports across 442 files
- Add authorized-testing disclaimers to all offensive skills
- Complete 11 incomplete skill directories
- Expand 10 stub SKILL.md files with full content
- Fix 2 YAML parse errors in frontmatter
- Fix 5 pre-existing syntax errors
- Convert 22 hardcoded paths/ports to environment variables
- Back up 21 redundant skill pairs to .bak
- Fix 2 global declaration errors
- 724/724 skills with full folder anatomy (SKILL.md + agent.py + api-reference.md + LICENSE)
- 0 compile errors across all 724 agent.py files
2026-03-19 13:26:49 +01:00

164 lines
6.0 KiB
Python

#!/usr/bin/env python3
"""Agent for SSL/TLS certificate lifecycle management.
Generates CSRs, parses X.509 certificates using the cryptography
library, monitors expiration across infrastructure, checks OCSP
revocation status, and maintains a certificate inventory.
"""
import json
import sys
import ssl
import socket
from datetime import datetime
from pathlib import Path
try:
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec, rsa
HAS_CRYPTO = True
except ImportError:
HAS_CRYPTO = False
class CertLifecycleAgent:
"""Manages SSL/TLS certificate lifecycle operations."""
def __init__(self, output_dir="./cert_inventory"):
self.output_dir = Path(output_dir)
self.output_dir.mkdir(parents=True, exist_ok=True)
self.inventory = []
def generate_csr(self, common_name, org="", country="US",
san_names=None, key_type="ecdsa"):
"""Generate a private key and Certificate Signing Request."""
if not HAS_CRYPTO:
return {"error": "cryptography library required"}
if key_type == "ecdsa":
private_key = ec.generate_private_key(ec.SECP256R1())
else:
private_key = rsa.generate_private_key(
public_exponent=65537, key_size=2048)
subject = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, country),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, org or common_name),
x509.NameAttribute(NameOID.COMMON_NAME, common_name),
])
builder = x509.CertificateSigningRequestBuilder().subject_name(subject)
if san_names:
sans = [x509.DNSName(n) for n in san_names]
builder = builder.add_extension(
x509.SubjectAlternativeName(sans), critical=False)
csr = builder.sign(private_key, hashes.SHA256())
key_path = self.output_dir / f"{common_name}.key"
csr_path = self.output_dir / f"{common_name}.csr"
key_path.write_bytes(private_key.private_bytes(
serialization.Encoding.PEM,
serialization.PrivateFormat.PKCS8,
serialization.NoEncryption()))
csr_path.write_bytes(csr.public_bytes(serialization.Encoding.PEM))
return {"common_name": common_name, "key_file": str(key_path),
"csr_file": str(csr_path), "key_type": key_type}
def fetch_remote_cert(self, hostname, port=443):
"""Fetch and parse a certificate from a remote server."""
try:
ctx = ssl.create_default_context()
with ctx.wrap_socket(socket.socket(), server_hostname=hostname) as s:
s.settimeout(10)
s.connect((hostname, port))
der = s.getpeercert(binary_form=True)
pem_info = s.getpeercert()
not_after = datetime.strptime(
pem_info["notAfter"], "%b %d %H:%M:%S %Y %Z")
not_before = datetime.strptime(
pem_info["notBefore"], "%b %d %H:%M:%S %Y %Z")
days_remaining = (not_after - datetime.utcnow()).days
subject = dict(x[0] for x in pem_info.get("subject", ()))
issuer = dict(x[0] for x in pem_info.get("issuer", ()))
sans = [entry[1] for entry in pem_info.get("subjectAltName", ())]
entry = {
"hostname": hostname, "port": port,
"subject_cn": subject.get("commonName", ""),
"issuer_cn": issuer.get("commonName", ""),
"issuer_org": issuer.get("organizationName", ""),
"not_before": not_before.isoformat(),
"not_after": not_after.isoformat(),
"days_remaining": days_remaining,
"san": sans[:20],
"serial": pem_info.get("serialNumber", ""),
"version": pem_info.get("version", 0),
"expired": days_remaining < 0,
"expiring_soon": 0 < days_remaining <= 30,
}
self.inventory.append(entry)
return entry
except (socket.error, ssl.SSLError, OSError) as exc:
return {"hostname": hostname, "error": str(exc)}
def scan_hosts(self, hostnames, port=443):
"""Scan multiple hosts and collect certificate data."""
results = []
for host in hostnames:
result = self.fetch_remote_cert(host, port)
results.append(result)
return results
def check_expiring(self, threshold_days=30):
"""Return certificates expiring within threshold days."""
return [c for c in self.inventory
if c.get("days_remaining", 999) <= threshold_days
and "error" not in c]
def generate_report(self):
"""Generate certificate inventory report."""
expiring = self.check_expiring(30)
expired = [c for c in self.inventory if c.get("expired")]
report = {
"report_date": datetime.utcnow().isoformat(),
"total_certificates": len(self.inventory),
"expired": len(expired),
"expiring_30d": len(expiring),
"healthy": len(self.inventory) - len(expired) - len(expiring),
"certificates": self.inventory,
"alerts": [
{"hostname": c["hostname"],
"days_remaining": c["days_remaining"],
"severity": "critical" if c.get("expired") else "warning"}
for c in expired + expiring
],
}
report_path = self.output_dir / "cert_inventory_report.json"
with open(report_path, "w") as f:
json.dump(report, f, indent=2, default=str)
print(json.dumps(report, indent=2, default=str))
return report
def main():
hosts = sys.argv[1:] if len(sys.argv) > 1 else [
"google.com", "github.com", "expired.badssl.com"]
agent = CertLifecycleAgent()
agent.scan_hosts(hosts)
agent.generate_report()
if __name__ == "__main__":
main()