various plugin refactors and fixes
This commit is contained in:
+65
-174
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Comprehensive SSL/TLS security scanning and analysis.
|
||||
All blocking socket calls run in a thread pool; user input is sanitised.
|
||||
Output is a clean code block with aligned columns.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
@@ -10,7 +11,7 @@ import ssl
|
||||
import OpenSSL
|
||||
import datetime
|
||||
import simplematrixbotlib as botlib
|
||||
from plugins.common import is_public_destination, html_escape, collapsible_summary
|
||||
from plugins.common import is_public_destination, html_escape, collapsible_summary, code_block
|
||||
|
||||
# SSL/TLS configuration – handle missing protocols in modern Python
|
||||
TLS_VERSIONS = {
|
||||
@@ -37,9 +38,6 @@ CIPHER_CATEGORIES = {
|
||||
}
|
||||
|
||||
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()
|
||||
@@ -49,7 +47,6 @@ async def handle_command(room, message, bot, prefix, config):
|
||||
|
||||
target = args[0].strip()
|
||||
port = 443
|
||||
|
||||
if ':' in target:
|
||||
parts = target.split(':')
|
||||
target = parts[0]
|
||||
@@ -65,12 +62,8 @@ async def handle_command(room, message, bot, prefix, config):
|
||||
|
||||
await perform_ssl_scan(room, bot, target, port)
|
||||
|
||||
|
||||
async def show_usage(room, bot):
|
||||
"""Display sslscan command usage."""
|
||||
usage = """
|
||||
<strong>🔐 SSL/TLS Security Scanner</strong>
|
||||
|
||||
usage = """<strong>🔐 SSL/TLS Security Scanner</strong>
|
||||
<strong>!sslscan <domain[:port]></strong> - Comprehensive SSL/TLS security analysis
|
||||
|
||||
<strong>Examples:</strong>
|
||||
@@ -88,28 +81,21 @@ async def show_usage(room, bot):
|
||||
"""
|
||||
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)
|
||||
@@ -117,15 +103,12 @@ def _get_certificate_info(target, port):
|
||||
|
||||
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)
|
||||
@@ -158,9 +141,7 @@ def _get_certificate_info(target, port):
|
||||
}
|
||||
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:
|
||||
@@ -177,9 +158,7 @@ def _test_protocols(target, port):
|
||||
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',
|
||||
@@ -207,130 +186,63 @@ def _test_cipher_suites(target, port):
|
||||
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'
|
||||
})
|
||||
|
||||
vulns.append(('SSLv2 Support', 'CRITICAL'))
|
||||
if protocols.get('SSLv3'):
|
||||
vulns.append({
|
||||
'name': 'SSLv3 Support',
|
||||
'severity': 'HIGH',
|
||||
'description': 'SSLv3 is vulnerable to POODLE attack',
|
||||
'cve': 'CVE-2014-3566'
|
||||
})
|
||||
|
||||
vulns.append(('SSLv3 Support', 'HIGH'))
|
||||
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'])]
|
||||
vulns.append(('Certificate Expiring Soon', 'MEDIUM'))
|
||||
weak_ciphers = [c for c in supported_ciphers if any(w in c.upper() for w 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'
|
||||
})
|
||||
|
||||
vulns.append(('Weak Cipher Suites', 'HIGH'))
|
||||
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'
|
||||
})
|
||||
|
||||
vulns.append(('TLS 1.2 Not Supported', 'HIGH'))
|
||||
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'
|
||||
})
|
||||
|
||||
vulns.append(('TLS 1.3 Not Supported', 'MEDIUM'))
|
||||
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']))
|
||||
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
|
||||
|
||||
for name, severity in vulnerabilities:
|
||||
if severity == 'CRITICAL': score -= 20
|
||||
elif severity == 'HIGH': score -= 15
|
||||
elif severity == 'MEDIUM': score -= 10
|
||||
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 protocols.get('SSLv2'): recs.append("🔴 Disable SSLv2")
|
||||
if protocols.get('SSLv3'): recs.append("🔴 Disable SSLv3")
|
||||
if not protocols.get('TLSv1.3'): recs.append("🟡 Enable TLSv1.3")
|
||||
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'])]
|
||||
recs.append("🟡 Renew certificate")
|
||||
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)")
|
||||
|
||||
recs.append("🔴 Remove weak ciphers")
|
||||
if score < 80:
|
||||
recs.append("🛡️ Implement modern TLS configuration following Mozilla guidelines")
|
||||
|
||||
recs.append("🛡️ Improve TLS configuration")
|
||||
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")
|
||||
recs.append("🟡 Enable Forward Secrecy")
|
||||
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}...")
|
||||
await bot.api.send_text_message(room.room_id, f"🔍 Starting 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)
|
||||
@@ -341,36 +253,25 @@ async def perform_ssl_scan(room, bot, target, port):
|
||||
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}")
|
||||
sections = []
|
||||
|
||||
|
||||
async def _format_results(target, port, cert_info, protocols, supported_ciphers,
|
||||
vulnerabilities, score, recommendations):
|
||||
safe_target = html_escape(target)
|
||||
# Score
|
||||
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"
|
||||
sections.append({"title": f"{score_emoji} Security Score", "rows": [("", "Score", f"{score}/100 ({rating})")]})
|
||||
|
||||
body = f"<strong>🔐 SSL/TLS Security Scan: {safe_target}:{port}</strong><br><br>"
|
||||
body += f"<strong>{score_emoji} Security Score: {score}/100 ({rating})</strong><br><br>"
|
||||
|
||||
# Certificate Information
|
||||
# Certificate
|
||||
if cert_info:
|
||||
body += "<strong>📜 Certificate Information</strong><br>"
|
||||
body += f" • <strong>Subject:</strong> {html_escape(cert_info['subject'].get('common_name', 'N/A'))}<br>"
|
||||
body += f" • <strong>Issuer:</strong> {html_escape(cert_info['issuer'].get('common_name', 'N/A'))}<br>"
|
||||
body += f" • <strong>Valid From:</strong> {_format_cert_date(cert_info['not_before'])}<br>"
|
||||
body += f" • <strong>Valid Until:</strong> {_format_cert_date(cert_info['not_after'])}<br>"
|
||||
days = cert_info.get('days_until_expiry', 'N/A')
|
||||
body += f" • <strong>Expires In:</strong> {days} days<br>"
|
||||
body += f" • <strong>Signature Algorithm:</strong> {html_escape(cert_info['signature_algorithm'])}<br>"
|
||||
body += "<br>"
|
||||
cert_rows = [
|
||||
("📜", "Subject", cert_info['subject'].get('common_name', 'N/A')),
|
||||
("🏢", "Issuer", cert_info['issuer'].get('common_name', 'N/A')),
|
||||
("📅", "Valid Until", cert_info['not_after']),
|
||||
("⏳", "Expires In", f"{cert_info['days_until_expiry']} days"),
|
||||
]
|
||||
sections.append({"title": "📜 Certificate", "rows": cert_rows})
|
||||
|
||||
# Protocol Support
|
||||
body += "<strong>🔌 Protocol Support</strong><br>"
|
||||
# Protocols
|
||||
proto_rows = []
|
||||
for proto in ['SSLv2', 'SSLv3', 'TLSv1', 'TLSv1.1', 'TLSv1.2', 'TLSv1.3']:
|
||||
supported = protocols.get(proto, False)
|
||||
if proto in ['SSLv2', 'SSLv3'] and supported:
|
||||
@@ -381,67 +282,57 @@ async def _format_results(target, port, cert_info, protocols, supported_ciphers,
|
||||
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)"
|
||||
status = "Cannot test"
|
||||
emoji = "⚫"
|
||||
body += f" • {emoji} <strong>{proto}:</strong> {status}<br>"
|
||||
body += "<br>"
|
||||
proto_rows.append((emoji, proto, status))
|
||||
sections.append({"title": "🔌 Protocols", "rows": proto_rows})
|
||||
|
||||
# Cipher Suites
|
||||
body += "<strong>🔐 Cipher Suites</strong><br>"
|
||||
body += f" • <strong>Total Supported:</strong> {len(supported_ciphers)}<br>"
|
||||
|
||||
weak_ciphers = [c for c in supported_ciphers
|
||||
if any(w in c.upper() for w in CIPHER_CATEGORIES['WEAK'])]
|
||||
weak_ciphers = [c for c in supported_ciphers if any(w in c.upper() for w in CIPHER_CATEGORIES['WEAK'])]
|
||||
strong_ciphers = [c for c in supported_ciphers if any(s in c.upper() for s in CIPHER_CATEGORIES['STRONG'])]
|
||||
cipher_rows = [("🔢", "Total Supported", str(len(supported_ciphers)))]
|
||||
if weak_ciphers:
|
||||
body += f" • <strong>Weak Ciphers:</strong> {len(weak_ciphers)} found<br>"
|
||||
for cipher in weak_ciphers[:3]:
|
||||
body += f" └─ 🔴 {html_escape(cipher)}<br>"
|
||||
strong_ciphers = [c for c in supported_ciphers
|
||||
if any(s in c.upper() for s in CIPHER_CATEGORIES['STRONG'])]
|
||||
cipher_rows.append(("🔴", "Weak Ciphers", str(len(weak_ciphers))))
|
||||
for c in weak_ciphers[:3]:
|
||||
cipher_rows.append(("", "", c))
|
||||
if strong_ciphers:
|
||||
body += f" • <strong>Strong Ciphers:</strong> {len(strong_ciphers)} found<br>"
|
||||
body += "<br>"
|
||||
cipher_rows.append(("🟢", "Strong Ciphers", str(len(strong_ciphers))))
|
||||
sections.append({"title": "🔐 Cipher Suites", "rows": cipher_rows})
|
||||
|
||||
# Vulnerabilities
|
||||
if vulnerabilities:
|
||||
body += "<strong>⚠️ Security Vulnerabilities</strong><br>"
|
||||
for vuln in vulnerabilities[:5]:
|
||||
sev_emoji = "🔴" if vuln['severity'] == 'CRITICAL' else "🟠" if vuln['severity'] == 'HIGH' else "🟡"
|
||||
body += f" • {sev_emoji} <strong>{html_escape(vuln['name'])}</strong> ({vuln['severity']})<br>"
|
||||
body += f" └─ {html_escape(vuln['description'])}<br>"
|
||||
body += "<br>"
|
||||
vuln_rows = []
|
||||
for name, sev in vulnerabilities:
|
||||
sev_emoji = "🔴" if sev == 'CRITICAL' else "🟠" if sev == 'HIGH' else "🟡"
|
||||
vuln_rows.append((sev_emoji, name, sev))
|
||||
sections.append({"title": "⚠️ Vulnerabilities", "rows": vuln_rows})
|
||||
|
||||
# Recommendations
|
||||
if recommendations:
|
||||
body += "<strong>💡 Security Recommendations</strong><br>"
|
||||
for rec in recommendations[:8]:
|
||||
body += f" • {rec}<br>"
|
||||
body += "<br>"
|
||||
rec_rows = [("💡", "Recommendation", rec) for rec in recommendations]
|
||||
sections.append({"title": "💡 Recommendations", "rows": rec_rows})
|
||||
|
||||
# Quick Assessment
|
||||
body += "<strong>📊 Quick Assessment</strong><br>"
|
||||
assessment_rows = []
|
||||
if score >= 90:
|
||||
body += " • ✅ Excellent TLS configuration<br>"
|
||||
body += " • ✅ Modern protocols and ciphers<br>"
|
||||
body += " • ✅ Good certificate management<br>"
|
||||
assessment_rows = [("", "Assessment", "✅ Excellent configuration")]
|
||||
elif score >= 70:
|
||||
body += " • ⚠️ Good configuration with minor issues<br>"
|
||||
body += " • 🔧 Some improvements recommended<br>"
|
||||
assessment_rows = [("", "Assessment", "⚠️ Good, minor improvements possible")]
|
||||
else:
|
||||
body += " • 🚨 Significant security issues found<br>"
|
||||
body += " • 🔴 Immediate action required<br>"
|
||||
|
||||
body += "<br><em>ℹ️ Note: Some protocol tests limited by Python security features</em>"
|
||||
|
||||
return collapsible_summary(f"🔐 SSL/TLS Scan: {safe_target}:{port} (Score: {score}/100)", body)
|
||||
assessment_rows = [("", "Assessment", "🚨 Significant issues found")]
|
||||
sections.append({"title": "📊 Quick Assessment", "rows": assessment_rows})
|
||||
|
||||
block = code_block(f"🔐 SSL/TLS Scan: {safe_target}:{port}", sections)
|
||||
output = collapsible_summary(f"🔐 SSL/TLS: {safe_target} (Score: {score}/100)", block)
|
||||
await bot.api.send_markdown_message(room.room_id, output)
|
||||
logging.info(f"Completed SSL scan for {target}:{port}")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Plugin Metadata
|
||||
# ---------------------------------------------------------------------------
|
||||
__version__ = "1.0.2"
|
||||
__author__ = "Funguy Bot"
|
||||
__description__ = "SSL/TLS security scanner (SSRF‑safe, async)"
|
||||
__description__ = "SSL/TLS security scanner"
|
||||
__help__ = """
|
||||
<details>
|
||||
<summary><strong>!sslscan</strong> – SSL/TLS analysis</summary>
|
||||
|
||||
Reference in New Issue
Block a user