#!/usr/bin/env python3 """Collect focused TLS evidence that ZAP baseline does not enumerate.""" import argparse import json import subprocess from pathlib import Path from urllib.parse import urlparse def openssl_handshake(host: str, port: int, option: str, cipher: str | None = None) -> tuple[bool, str]: command = ["openssl", "s_client", "-connect", f"{host}:{port}", "-servername", host, option, "-brief"] if cipher: command.extend(["-cipher", cipher]) result = subprocess.run(command, input="", text=True, capture_output=True, timeout=20, check=False) evidence = (result.stdout + result.stderr).strip() return result.returncode == 0 and "Protocol version" in evidence, evidence[-2000:] def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("url") parser.add_argument("output", type=Path) args = parser.parse_args() parsed = urlparse(args.url) host = parsed.hostname port = parsed.port or 443 if not host: parser.error("URL must include a hostname") findings = [] for option, label in (("-tls1", "TLS 1.0"), ("-tls1_1", "TLS 1.1")): accepted, evidence = openssl_handshake(host, port, option, "AES128-SHA:@SECLEVEL=0") if accepted: findings.append( { "id": "TLS-OLD-PROTOCOL", "title": f"Server accepts {label}", "severity": "high", "description": "The endpoint accepts an obsolete TLS protocol.", "remediation": "Allow only TLS 1.2 and TLS 1.3.", "evidence": evidence, } ) weak_cipher, cipher_evidence = openssl_handshake(host, port, "-tls1_2", "AES128-SHA:@SECLEVEL=0") if weak_cipher: findings.append( { "id": "TLS-WEAK-CIPHER", "title": "Server accepts TLS_RSA_WITH_AES_128_CBC_SHA", "severity": "medium", "description": "The endpoint accepts a legacy RSA/CBC cipher suite.", "remediation": "Use forward-secret AEAD cipher suites.", "evidence": cipher_evidence, } ) verification = subprocess.run( [ "openssl", "s_client", "-connect", f"{host}:{port}", "-servername", host, "-verify_return_error", "-brief", ], input="", text=True, capture_output=True, timeout=20, check=False, ) verification_evidence = (verification.stdout + verification.stderr).strip() if verification.returncode != 0 and "certificate verify failed" in verification_evidence.lower(): findings.append( { "id": "TLS-SELF-SIGNED", "title": "TLS certificate is not publicly trusted", "severity": "medium", "description": "Default certificate verification rejected the endpoint certificate.", "remediation": "Install a certificate issued by a trusted CA for the environment.", "evidence": verification_evidence[-2000:], } ) args.output.parent.mkdir(parents=True, exist_ok=True) args.output.write_text(json.dumps({"target": args.url, "findings": findings}, indent=2) + "\n", encoding="utf-8") print(f"Collected {len(findings)} TLS findings") return 0 if __name__ == "__main__": raise SystemExit(main())