""" Comprehensive SSL/TLS security scanning and analysis. All blocking socket calls run in a thread pool; user input is sanitised. """ import asyncio import logging import socket import ssl import OpenSSL import datetime import simplematrixbotlib as botlib from plugins.common import is_public_destination, html_escape, collapsible_summary # SSL/TLS configuration – handle missing protocols in modern Python TLS_VERSIONS = { 'TLSv1.2': ssl.PROTOCOL_TLSv1_2, 'TLSv1.3': ssl.PROTOCOL_TLS } try: TLS_VERSIONS['TLSv1.1'] = ssl.PROTOCOL_TLSv1_1 except AttributeError: pass try: TLS_VERSIONS['TLSv1'] = ssl.PROTOCOL_TLSv1 except AttributeError: pass CIPHER_CATEGORIES = { 'STRONG': [ 'TLS_AES_256_GCM_SHA384', 'TLS_CHACHA20_POLY1305_SHA256', 'TLS_AES_128_GCM_SHA256', 'ECDHE-RSA-AES256-GCM-SHA384', 'ECDHE-ECDSA-AES256-GCM-SHA384', 'ECDHE-RSA-CHACHA20-POLY1305', 'ECDHE-ECDSA-CHACHA20-POLY1305', 'DHE-RSA-AES256-GCM-SHA384' ], 'WEAK': ['RC4', 'DES', '3DES', 'MD5', 'EXPORT', 'NULL', 'ANON', 'ADH', 'CBC'], } async def handle_command(room, message, bot, prefix, config): """ Handle !sslscan command for comprehensive SSL/TLS analysis. """ match = botlib.MessageMatch(room, message, bot, prefix) if match.is_not_from_this_bot() and match.prefix() and match.command("sslscan"): args = match.args() if len(args) < 1: await show_usage(room, bot) return target = args[0].strip() port = 443 if ':' in target: parts = target.split(':') target = parts[0] try: port = int(parts[1]) except ValueError: await bot.api.send_text_message(room.room_id, "Invalid port number") return if not is_public_destination(target): await bot.api.send_text_message(room.room_id, "❌ Scanning of private/internal addresses is not allowed.") return await perform_ssl_scan(room, bot, target, port) async def show_usage(room, bot): """Display sslscan command usage.""" usage = """ 🔐 SSL/TLS Security Scanner !sslscan <domain[:port]> - Comprehensive SSL/TLS security analysis Examples:!sslscan example.com!sslscan github.com:443 Tests Performed: • SSL/TLS protocol support and versions • Certificate chain validation and expiration • Cipher suite strength and configuration • Security headers and extensions • Handshake simulation and vulnerabilities • PCI DSS compliance checking • SSL/TLS best practices assessment """ await bot.api.send_markdown_message(room.room_id, usage) # ----- async wrappers for blocking socket calls ----- async def _run_blocking(func, *args, **kwargs): loop = asyncio.get_running_loop() return await loop.run_in_executor(None, lambda: func(*args, **kwargs)) def _test_connectivity(target, port): """Test basic connectivity.""" try: with socket.create_connection((target, port), timeout=10): return True except: return False def _get_certificate_info(target, port): """Retrieve detailed certificate info.""" context = ssl.create_default_context() context.check_hostname = False context.verify_mode = ssl.CERT_NONE with socket.create_connection((target, port), timeout=10) as sock: with context.wrap_socket(sock, server_hostname=target) as ssock: cert_bin = ssock.getpeercert(binary_form=True) cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_ASN1, cert_bin) subject = cert.get_subject() issuer = cert.get_issuer() not_before = cert.get_notBefore().decode('utf-8') not_after = cert.get_notAfter().decode('utf-8') sig_alg = cert.get_signature_algorithm().decode('utf-8') not_after_dt = datetime.datetime.strptime(not_after, '%Y%m%d%H%M%SZ') days_remaining = (not_after_dt - datetime.datetime.utcnow()).days # Extensions summary extensions = [] for i in range(cert.get_extension_count()): ext = cert.get_extension(i) extensions.append({ 'name': ext.get_short_name().decode('utf-8'), 'value': str(ext) }) return { 'subject': { 'common_name': subject.CN, 'organization': subject.O, 'organizational_unit': subject.OU, 'country': subject.C, 'state': subject.ST, 'locality': subject.L }, 'issuer': { 'common_name': issuer.CN, 'organization': issuer.O, 'organizational_unit': issuer.OU }, 'serial_number': cert.get_serial_number(), 'version': cert.get_version(), 'not_before': not_before, 'not_after': not_after, 'signature_algorithm': sig_alg, 'days_until_expiry': days_remaining, 'extensions': extensions } return None def _test_protocols(target, port): """Test support for various SSL/TLS protocols.""" protocols = {} for proto_name in ['SSLv2', 'SSLv3', 'TLSv1', 'TLSv1.1', 'TLSv1.2', 'TLSv1.3']: if proto_name not in TLS_VERSIONS: protocols[proto_name] = False continue try: ctx = ssl.SSLContext(TLS_VERSIONS[proto_name]) ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE with socket.create_connection((target, port), timeout=5) as sock: with ctx.wrap_socket(sock, server_hostname=target): protocols[proto_name] = True except: protocols[proto_name] = False return protocols def _test_cipher_suites(target, port): """Return list of supported cipher suite names.""" test_ciphers = [ 'ECDHE-RSA-AES256-GCM-SHA384', 'ECDHE-ECDSA-AES256-GCM-SHA384', 'ECDHE-RSA-AES256-SHA384', 'ECDHE-ECDSA-AES256-SHA384', 'ECDHE-RSA-AES256-SHA', 'ECDHE-ECDSA-AES256-SHA', 'AES256-GCM-SHA384', 'AES256-SHA256', 'AES256-SHA', 'CAMELLIA256-SHA', 'PSK-AES256-CBC-SHA', 'ECDHE-RSA-AES128-GCM-SHA256', 'ECDHE-ECDSA-AES128-GCM-SHA256', 'ECDHE-RSA-AES128-SHA256', 'ECDHE-ECDSA-AES128-SHA256', 'ECDHE-RSA-AES128-SHA', 'ECDHE-ECDSA-AES128-SHA', 'AES128-GCM-SHA256', 'AES128-SHA256', 'AES128-SHA', 'CAMELLIA128-SHA', 'PSK-AES128-CBC-SHA', 'DES-CBC3-SHA', 'RC4-SHA', 'RC4-MD5' ] supported = [] for cipher in test_ciphers: try: ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE ctx.set_ciphers(cipher) with socket.create_connection((target, port), timeout=5) as sock: with ctx.wrap_socket(sock, server_hostname=target): supported.append(cipher) except: pass return supported # ----- analysis helpers (same logic as original) ----- def _check_vulnerabilities(protocols, cert_info, supported_ciphers): vulns = [] if protocols.get('SSLv2'): vulns.append({ 'name': 'SSLv2 Support', 'severity': 'CRITICAL', 'description': 'SSLv2 is obsolete and contains critical vulnerabilities', 'cve': 'Multiple CVEs' }) if protocols.get('SSLv3'): vulns.append({ 'name': 'SSLv3 Support', 'severity': 'HIGH', 'description': 'SSLv3 is vulnerable to POODLE attack', 'cve': 'CVE-2014-3566' }) if cert_info and cert_info.get('days_until_expiry', 0) < 30: vulns.append({ 'name': 'Certificate Expiring Soon', 'severity': 'MEDIUM', 'description': f"Certificate expires in {cert_info['days_until_expiry']} days", 'cve': 'N/A' }) weak_ciphers = [c for c in supported_ciphers if any(weak in c.upper() for weak in CIPHER_CATEGORIES['WEAK'])] if weak_ciphers: vulns.append({ 'name': 'Weak Cipher Suites', 'severity': 'HIGH', 'description': f'Weak ciphers supported: {", ".join(weak_ciphers[:3])}', 'cve': 'Multiple CVEs' }) if not protocols.get('TLSv1.2', False): vulns.append({ 'name': 'TLS 1.2 Not Supported', 'severity': 'HIGH', 'description': 'TLS 1.2 is required for modern security', 'cve': 'N/A' }) if not protocols.get('TLSv1.3', False): vulns.append({ 'name': 'TLS 1.3 Not Supported', 'severity': 'MEDIUM', 'description': 'TLS 1.3 provides improved security and performance', 'cve': 'N/A' }) return vulns def _calculate_score(protocols, cert_info, supported_ciphers, vulnerabilities): score = 100 if protocols.get('SSLv2'): score -= 30 if protocols.get('SSLv3'): score -= 20 if not protocols.get('TLSv1.2'): score -= 15 if not protocols.get('TLSv1.3'): score -= 10 if cert_info and cert_info.get('days_until_expiry', 0) < 30: score -= 10 if cert_info and cert_info.get('days_until_expiry', 0) < 7: score -= 20 weak_cipher_count = sum(1 for c in supported_ciphers if any(w in c.upper() for w in CIPHER_CATEGORIES['WEAK'])) score -= min(weak_cipher_count * 5, 25) for vuln in vulnerabilities: if vuln['severity'] == 'CRITICAL': score -= 20 elif vuln['severity'] == 'HIGH': score -= 15 elif vuln['severity'] == 'MEDIUM': score -= 10 elif vuln['severity'] == 'LOW': score -= 5 return max(0, score) def _generate_recommendations(protocols, cert_info, supported_ciphers, score): recs = [] if protocols.get('SSLv2'): recs.append("🔴 IMMEDIATELY disable SSLv2 - critically vulnerable") if protocols.get('SSLv3'): recs.append("🔴 Disable SSLv3 - vulnerable to POODLE attack") if not protocols.get('TLSv1.3'): recs.append("🟡 Enable TLSv1.3 for best security and performance") if cert_info and cert_info.get('days_until_expiry', 0) < 30: recs.append("🟡 Renew SSL certificate - expiring soon") weak_ciphers = [c for c in supported_ciphers if any(w in c.upper() for w in CIPHER_CATEGORIES['WEAK'])] if weak_ciphers: recs.append("🔴 Remove weak cipher suites (RC4, DES, 3DES, NULL)") if score < 80: recs.append("🛡️ Implement modern TLS configuration following Mozilla guidelines") if not any('ECDHE' in c for c in supported_ciphers): recs.append("🟡 Enable Forward Secrecy with ECDHE cipher suites") recs.append("ℹ️ Note: SSLv2/SSLv3 testing limited by Python security features") return recs def _format_cert_date(date_str): try: dt = datetime.datetime.strptime(date_str, '%Y%m%d%H%M%SZ') return dt.strftime('%Y-%m-%d %H:%M:%S UTC') except: return date_str # ----- main scan orchestration ----- async def perform_ssl_scan(room, bot, target, port): safe_target = html_escape(target) await bot.api.send_text_message(room.room_id, f"🔍 Starting comprehensive SSL/TLS scan for {safe_target}:{port}...") if not await _run_blocking(_test_connectivity, target, port): await bot.api.send_text_message(room.room_id, f"❌ Cannot connect to {safe_target}:{port}") return # Run blocking checks in parallel cert_task = _run_blocking(_get_certificate_info, target, port) proto_task = _run_blocking(_test_protocols, target, port) cipher_task = _run_blocking(_test_cipher_suites, target, port) cert_info, protocols, supported_ciphers = await asyncio.gather(cert_task, proto_task, cipher_task) vulnerabilities = _check_vulnerabilities(protocols, cert_info, supported_ciphers) score = _calculate_score(protocols, cert_info, supported_ciphers, vulnerabilities) recommendations = _generate_recommendations(protocols, cert_info, supported_ciphers, score) # Build output (using safe domain/port) output = await _format_results(target, port, cert_info, protocols, supported_ciphers, vulnerabilities, score, recommendations) await bot.api.send_markdown_message(room.room_id, output) logging.info(f"Completed SSL scan for {target}:{port}") async def _format_results(target, port, cert_info, protocols, supported_ciphers, vulnerabilities, score, recommendations): safe_target = html_escape(target) score_emoji = "🟢" if score >= 90 else "🟡" if score >= 80 else "🟠" if score >= 60 else "🔴" rating = "Excellent" if score >= 90 else "Good" if score >= 80 else "Fair" if score >= 60 else "Poor" body = f"🔐 SSL/TLS Security Scan: {safe_target}:{port}

" body += f"{score_emoji} Security Score: {score}/100 ({rating})

" # Certificate Information if cert_info: body += "📜 Certificate Information
" body += f" • Subject: {html_escape(cert_info['subject'].get('common_name', 'N/A'))}
" body += f" • Issuer: {html_escape(cert_info['issuer'].get('common_name', 'N/A'))}
" body += f" • Valid From: {_format_cert_date(cert_info['not_before'])}
" body += f" • Valid Until: {_format_cert_date(cert_info['not_after'])}
" days = cert_info.get('days_until_expiry', 'N/A') body += f" • Expires In: {days} days
" body += f" • Signature Algorithm: {html_escape(cert_info['signature_algorithm'])}
" body += "
" # Protocol Support body += "🔌 Protocol Support
" for proto in ['SSLv2', 'SSLv3', 'TLSv1', 'TLSv1.1', 'TLSv1.2', 'TLSv1.3']: supported = protocols.get(proto, False) if proto in ['SSLv2', 'SSLv3'] and supported: emoji = "🔴" elif proto == 'TLSv1.3' and supported: emoji = "✅" else: emoji = "✅" if supported else "❌" status = "Supported" if supported else "Not Supported" if proto in ['SSLv2', 'SSLv3'] and proto not in TLS_VERSIONS: status = "Cannot test (Python security)" emoji = "⚫" body += f" • {emoji} {proto}: {status}
" body += "
" # Cipher Suites body += "🔐 Cipher Suites
" body += f" • Total Supported: {len(supported_ciphers)}
" weak_ciphers = [c for c in supported_ciphers if any(w in c.upper() for w in CIPHER_CATEGORIES['WEAK'])] if weak_ciphers: body += f" • Weak Ciphers: {len(weak_ciphers)} found
" for cipher in weak_ciphers[:3]: body += f" └─ 🔴 {html_escape(cipher)}
" strong_ciphers = [c for c in supported_ciphers if any(s in c.upper() for s in CIPHER_CATEGORIES['STRONG'])] if strong_ciphers: body += f" • Strong Ciphers: {len(strong_ciphers)} found
" body += "
" # Vulnerabilities if vulnerabilities: body += "⚠️ Security Vulnerabilities
" for vuln in vulnerabilities[:5]: sev_emoji = "🔴" if vuln['severity'] == 'CRITICAL' else "🟠" if vuln['severity'] == 'HIGH' else "🟡" body += f" • {sev_emoji} {html_escape(vuln['name'])} ({vuln['severity']})
" body += f" └─ {html_escape(vuln['description'])}
" body += "
" # Recommendations if recommendations: body += "💡 Security Recommendations
" for rec in recommendations[:8]: body += f" • {rec}
" body += "
" # Quick Assessment body += "📊 Quick Assessment
" if score >= 90: body += " • ✅ Excellent TLS configuration
" body += " • ✅ Modern protocols and ciphers
" body += " • ✅ Good certificate management
" elif score >= 70: body += " • ⚠️ Good configuration with minor issues
" body += " • 🔧 Some improvements recommended
" else: body += " • 🚨 Significant security issues found
" body += " • 🔴 Immediate action required
" body += "
ℹ️ Note: Some protocol tests limited by Python security features" return collapsible_summary(f"🔐 SSL/TLS Scan: {safe_target}:{port} (Score: {score}/100)", body) # --------------------------------------------------------------------------- # Plugin Metadata # --------------------------------------------------------------------------- __version__ = "1.0.2" __author__ = "Funguy Bot" __description__ = "SSL/TLS security scanner (SSRF‑safe, async)" __help__ = """
!sslscan – SSL/TLS analysis

!sslscan <domain[:port]> – Tests protocols, cipher suites, certificate validity, vulnerabilities.
Provides a security score (0-100) and actionable recommendations.

"""