various plugin refactors and fixes

This commit is contained in:
2026-05-09 04:51:50 -05:00
parent f822d6a450
commit 5c6234a317
25 changed files with 2044 additions and 3674 deletions
+166 -309
View File
@@ -1,5 +1,6 @@
"""
This plugin provides comprehensive HTTP security header analysis.
HTTP security header analysis plugin.
Outputs a structured code block with perfectly aligned columns.
"""
import logging
@@ -10,342 +11,198 @@ from urllib.parse import urlparse
import ssl
import socket
import datetime
from plugins.common import is_public_destination, collapsible_summary, html_escape
from plugins.common import is_public_destination, html_escape, collapsible_summary, code_block
async def _run_in_thread(func, *args, **kwargs):
loop = asyncio.get_running_loop()
return await loop.run_in_executor(None, lambda: func(*args, **kwargs))
async def analyze_http_response(url):
try:
async with aiohttp.ClientSession() as session:
async with session.get(url, allow_redirects=True, timeout=aiohttp.ClientTimeout(total=10)) as resp:
return str(resp.url), resp.status, dict(resp.headers), resp.url.scheme == 'https'
except aiohttp.ClientError as e:
logging.warning(f"HTTP analysis error: {e}")
return url, None, {}, False
async def analyze_https_response(url):
try:
async with aiohttp.ClientSession() as session:
async with session.get(url, allow_redirects=False, timeout=aiohttp.ClientTimeout(total=10)) as resp:
return resp.status, dict(resp.headers)
except aiohttp.ClientError as e:
logging.warning(f"HTTPS analysis error: {e}")
return None, {}
def _get_cert_info(domain):
try:
context = ssl.create_default_context()
with socket.create_connection((domain, 443), timeout=10) as sock:
with context.wrap_socket(sock, server_hostname=domain) as ssock:
cert = ssock.getpeercert()
return {
'subject': dict(x[0] for x in cert['subject']),
'issuer': dict(x[0] for x in cert['issuer']),
'not_before': cert['notBefore'],
'not_after': cert['notAfter'],
'san': cert.get('subjectAltName', []),
}
except Exception as e:
logging.warning(f"SSL cert error: {e}")
return None
def calculate_score(headers, redirects_to_https, cert_info):
score = 100
if 'Strict-Transport-Security' not in headers: score -= 15
if 'Content-Security-Policy' not in headers: score -= 15
if 'X-Content-Type-Options' not in headers: score -= 15
if 'X-Frame-Options' not in headers: score -= 15
if 'X-XSS-Protection' not in headers: score -= 15
hsts = headers.get('Strict-Transport-Security', '')
if 'max-age=31536000' not in hsts: score -= 10
if 'includeSubDomains' not in hsts: score -= 5
if 'preload' not in hsts: score -= 5
if headers.get('Referrer-Policy'): score += 5
if headers.get('Feature-Policy') or headers.get('Permissions-Policy'): score += 5
if headers.get('X-Content-Type-Options') == 'nosniff': score += 5
if headers.get('X-Frame-Options') in ['DENY', 'SAMEORIGIN']: score += 5
if redirects_to_https: score += 10
if cert_info and cert_info.get('not_after'):
try:
expires = datetime.datetime.strptime(cert_info['not_after'], '%b %d %H:%M:%S %Y %Z')
if (expires - datetime.datetime.utcnow()).days < 30: score -= 10
except: pass
return max(0, score)
def generate_recommendations(headers, redirects_to_https):
recs = []
if 'Strict-Transport-Security' not in headers:
recs.append("🔒 Implement HSTS with max-age=31536000, includeSubDomains, preload")
if 'Content-Security-Policy' not in headers:
recs.append("🛡️ Add Content-Security-Policy")
if 'X-Frame-Options' not in headers:
recs.append("🚫 Add X-Frame-Options (DENY or SAMEORIGIN)")
if 'X-Content-Type-Options' not in headers:
recs.append("📄 Add X-Content-Type-Options: nosniff")
if not redirects_to_https:
recs.append("🔐 Redirect HTTP to HTTPS")
if 'Server' in headers or 'X-Powered-By' in headers:
recs.append("🕵️ Remove info disclosure headers (Server, X-Powered-By)")
return recs
async def handle_command(room, message, bot, prefix, config):
"""
Function to handle !headers command for HTTP security header analysis.
"""
match = botlib.MessageMatch(room, message, bot, prefix)
if match.is_not_from_this_bot() and match.prefix() and match.command("headers"):
logging.info("Received !headers command")
if not (match.is_not_from_this_bot() and match.prefix() and match.command("headers")):
return
args = match.args()
args = match.args()
if len(args) < 1:
await bot.api.send_markdown_message(room.room_id,
"<strong>🔒 HTTP Security Headers Analysis</strong>\n<code>!headers &lt;url&gt;</code>")
return
if len(args) < 1:
await show_usage(room, bot)
return
original_input = args[0].strip()
url = original_input
if not url.startswith(('http://', 'https://')):
url = 'https://' + url
url = args[0].strip()
parsed = urlparse(url)
host = parsed.hostname
if not is_public_destination(host):
await bot.api.send_text_message(room.room_id, "❌ Private/internal addresses are not allowed.")
return
# Add protocol if missing
if not url.startswith(('http://', 'https://')):
url = 'https://' + url
safe_input = html_escape(original_input)
safe_host = html_escape(host)
await bot.api.send_text_message(room.room_id, f"🔍 Analyzing security headers for: {safe_input}...")
# SSRF protection: refuse internal hosts
parsed = urlparse(url)
host = parsed.hostname
if not is_public_destination(host):
await bot.api.send_text_message(room.room_id,
"❌ Scanning of private/internal addresses is not allowed.")
return
final_url, status_code, http_headers, redirects_to_https = await analyze_http_response(url)
_, https_headers = await analyze_https_response(url) if url.startswith('https://') else (None, {})
await analyze_headers(room, bot, url)
headers = https_headers or http_headers
cert_info = None
if url.startswith('https://'):
cert_info = await _run_in_thread(_get_cert_info, host)
async def show_usage(room, bot):
"""Display headers command usage."""
usage = """
<strong>🔒 HTTP Security Headers Analysis</strong>
score = calculate_score(headers, redirects_to_https, cert_info)
recommendations = generate_recommendations(headers, redirects_to_https)
<strong>!headers &lt;url&gt;</strong> - Comprehensive HTTP security header analysis
sections = []
<strong>Examples:</strong>
• <code>!headers example.com</code>
• <code>!headers https://github.com</code>
• <code>!headers http://localhost:8080</code>
<strong>Analyzes:</strong>
• Security headers presence and configuration
• SSL/TLS certificate information
• HTTP to HTTPS redirects
• Security scoring and recommendations
"""
await bot.api.send_markdown_message(room.room_id, usage)
async def analyze_headers(room, bot, url):
"""Perform comprehensive HTTP security header analysis."""
try:
await bot.api.send_text_message(room.room_id, f"🔍 Analyzing security headers for: {html_escape(url)}")
results = {
'url': url,
'http_headers': {},
'https_headers': {},
'redirect_chain': [],
'ssl_info': {},
'security_score': 0,
'recommendations': []
}
# Test HTTP first (if HTTPS was provided, we'll still check redirects)
parsed = urlparse(url)
http_url = f"http://{parsed.netloc or parsed.path}"
https_url = f"https://{parsed.netloc or parsed.path}"
# Analyze HTTP response and redirects
await analyze_http_response(results, http_url if not url.startswith('https://') else https_url)
# Analyze HTTPS response
if url.startswith('https://') or results.get('redirects_to_https'):
await analyze_https_response(results, https_url)
# Analyze SSL certificate if HTTPS
if url.startswith('https://') or results.get('redirects_to_https'):
await analyze_ssl_certificate(results, parsed.netloc or parsed.path)
# Calculate security score
await calculate_security_score(results)
# Generate recommendations
await generate_recommendations(results)
# Format and send results
output = await format_header_analysis(results)
await bot.api.send_markdown_message(room.room_id, output)
logging.info(f"Completed header analysis for {url}")
except Exception as e:
await bot.api.send_text_message(room.room_id, f"Error analyzing headers: {str(e)}")
logging.error(f"Error in analyze_headers: {e}")
async def analyze_http_response(results, url):
"""Analyze HTTP response and redirect chain."""
try:
async with aiohttp.ClientSession() as session:
async with session.get(url, allow_redirects=True, timeout=aiohttp.ClientTimeout(total=10)) as response:
results['final_url'] = str(response.url)
results['status_code'] = response.status
results['http_headers'] = dict(response.headers)
results['redirects_to_https'] = response.url.scheme == 'https'
# aiohttp doesn't give access to redirect history easily, so we'll mark if final URL differs
if str(response.url) != url:
results['redirect_chain'] = [{'url': url, 'status_code': 301}] # simplified
except aiohttp.ClientError as e:
results['http_error'] = str(e)
async def analyze_https_response(results, url):
"""Analyze HTTPS response headers."""
try:
async with aiohttp.ClientSession() as session:
async with session.get(url, allow_redirects=False, timeout=aiohttp.ClientTimeout(total=10)) as response:
results['https_headers'] = dict(response.headers)
results['https_status'] = response.status
except aiohttp.ClientError as e:
results['https_error'] = str(e)
async def analyze_ssl_certificate(results, domain):
"""Analyze SSL certificate information (run in thread to avoid event loop blocking)."""
def _get_cert():
try:
context = ssl.create_default_context()
with socket.create_connection((domain, 443), timeout=10) as sock:
with context.wrap_socket(sock, server_hostname=domain) as ssock:
cert = ssock.getpeercert()
return {
'subject': dict(x[0] for x in cert['subject']),
'issuer': dict(x[0] for x in cert['issuer']),
'not_before': cert['notBefore'],
'not_after': cert['notAfter'],
'san': cert.get('subjectAltName', []),
'version': cert.get('version'),
'serial_number': cert.get('serialNumber')
}
except Exception as e:
return f"Error: {e}"
loop = asyncio.get_running_loop()
ssl_data = await loop.run_in_executor(None, _get_cert)
if isinstance(ssl_data, str):
results['ssl_error'] = ssl_data
else:
results['ssl_info'] = ssl_data
async def calculate_security_score(results):
"""Calculate overall security score based on headers and configuration."""
score = 100
missing_headers = []
critical_headers = [
'Strict-Transport-Security',
'Content-Security-Policy',
'X-Content-Type-Options',
'X-Frame-Options',
'X-XSS-Protection'
]
headers = results.get('https_headers') or results.get('http_headers', {})
for header in critical_headers:
if header not in headers:
score -= 15
missing_headers.append(header)
# Check HSTS configuration
hsts = headers.get('Strict-Transport-Security', '')
if 'max-age=31536000' not in hsts:
score -= 10
if 'includeSubDomains' not in hsts:
score -= 5
if 'preload' not in hsts:
score -= 5
# Check CSP configuration
csp = headers.get('Content-Security-Policy', '')
if not csp:
score -= 10
elif "default-src 'none'" not in csp and "default-src 'self'" not in csp:
score -= 5
# Check for insecure headers
insecure_headers = ['Server', 'X-Powered-By', 'X-AspNet-Version']
for header in insecure_headers:
if header in headers:
score -= 5
# Bonus for good practices
if headers.get('Referrer-Policy'):
score += 5
if headers.get('Feature-Policy') or headers.get('Permissions-Policy'):
score += 5
if headers.get('X-Content-Type-Options') == 'nosniff':
score += 5
if headers.get('X-Frame-Options') in ['DENY', 'SAMEORIGIN']:
score += 5
# HTTPS enforcement bonus
if results.get('redirects_to_https'):
score += 10
results['security_score'] = max(0, score)
results['missing_headers'] = missing_headers
async def generate_recommendations(results):
"""Generate security recommendations based on analysis."""
recommendations = []
headers = results.get('https_headers') or results.get('http_headers', {})
if 'Strict-Transport-Security' not in headers:
recommendations.append("🔒 Implement HSTS header with max-age=31536000, includeSubDomains, and preload")
else:
hsts = headers['Strict-Transport-Security']
if 'max-age=31536000' not in hsts:
recommendations.append("🔒 Increase HSTS max-age to 31536000 (1 year)")
if 'includeSubDomains' not in hsts:
recommendations.append("🔒 Add includeSubDomains to HSTS header")
if 'preload' not in hsts:
recommendations.append("🔒 Consider adding preload directive to HSTS for browser preloading")
if 'Content-Security-Policy' not in headers:
recommendations.append("🛡️ Implement Content Security Policy to prevent XSS attacks")
if 'X-Frame-Options' not in headers:
recommendations.append("🚫 Add X-Frame-Options header to prevent clickjacking (DENY or SAMEORIGIN)")
if 'X-Content-Type-Options' not in headers:
recommendations.append("📄 Add X-Content-Type-Options: nosniff to prevent MIME type sniffing")
if 'Referrer-Policy' not in headers:
recommendations.append("🔗 Implement Referrer-Policy to control referrer information leakage")
if 'Server' in headers or 'X-Powered-By' in headers:
recommendations.append("🕵️ Remove Server and X-Powered-By headers to avoid information disclosure")
if not results.get('redirects_to_https') and not results['url'].startswith('https://'):
recommendations.append("🔐 Implement HTTP to HTTPS redirects")
results['recommendations'] = recommendations
async def format_header_analysis(results):
"""Format the header analysis results for display."""
safe_url = html_escape(results['url'])
output = f"<strong>🔒 Security Headers Analysis: {safe_url}</strong><br><br>"
# Security Score
score = results['security_score']
# Score
score_emoji = "🟢" if score >= 80 else "🟡" if score >= 60 else "🔴"
output += f"<strong>{score_emoji} Security Score: {score}/100</strong><br><br>"
sections.append({
"title": f"{score_emoji} Security Score",
"rows": [("", "Score", f"{score}/100")]
})
# Basic Information
output += "<strong>📊 Basic Information</strong><br>"
output += f" • <strong>Final URL:</strong> {html_escape(results.get('final_url', 'N/A'))}<br>"
output += f" • <strong>Status Code:</strong> {results.get('status_code', 'N/A')}<br>"
if results.get('redirects_to_https'):
output += f" • <strong>HTTPS Redirect:</strong> ✅ Enforced<br>"
else:
output += f" • <strong>HTTPS Redirect:</strong> ❌ Not enforced<br>"
output += "<br>"
# Security Headers Analysis
headers = results.get('https_headers') or results.get('http_headers', {})
output += "<strong>🛡️ Security Headers Analysis</strong><br>"
basic_rows = [
("🌐", "Final URL", final_url),
("📊", "Status Code", str(status_code) if status_code else "N/A"),
("🔐", "HTTPS Redirect", "✅ Yes" if redirects_to_https else "❌ No"),
]
sections.append({"title": "📊 Basic Information", "rows": basic_rows})
# Security Headers
security_headers = {
'Strict-Transport-Security': ('🔒', 'HSTS'),
'Content-Security-Policy': ('🛡️', 'CSP'),
'X-Frame-Options': ('🚫', 'Clickjacking Protection'),
'X-Content-Type-Options': ('📄', 'MIME Sniffing'),
'X-XSS-Protection': ('', 'XSS Protection (Deprecated)'),
'Referrer-Policy': ('🔗', 'Referrer Policy'),
'Feature-Policy': ('⚙️', 'Feature Policy'),
'Permissions-Policy': ('🔧', 'Permissions Policy'),
'Content-Security-Policy': ('🛡️', 'CSP'),
'X-Frame-Options': ('🚫', 'Frame Options'),
'X-Content-Type-Options': ('📄', 'Content Type'),
'X-XSS-Protection': ('', 'XSS Protection'),
'Referrer-Policy': ('🔗', 'Referrer Policy'),
'Permissions-Policy': ('🔧', 'Permissions Policy'),
'Feature-Policy': ('⚙️', 'Feature Policy'),
}
for header, (emoji, description) in security_headers.items():
if header in headers:
value = html_escape(str(headers[header]))[:100]
output += f"{emoji} <strong>{header}:</strong> {value}<br>"
header_rows = []
for hdr, (emoji, label) in security_headers.items():
if hdr in headers:
val = headers[hdr][:100]
header_rows.append((emoji, label, f"{val}"))
else:
output += f"{emoji} <strong>{header}:</strong> ❌ Missing<br>"
output += "<br>"
header_rows.append((emoji, label, "❌ Missing"))
sections.append({"title": "🛡️ Security Headers", "rows": header_rows})
# Other Headers (Information Disclosure)
output += "<strong>📋 Other Headers</strong><br>"
for header in ['Server', 'X-Powered-By']:
if header in headers:
output += f" • 🔍 <strong>{header}:</strong> {html_escape(str(headers[header]))}<br>"
output += "<br>"
# Other Headers
other_rows = []
for hdr in ['Server', 'X-Powered-By']:
if hdr in headers:
other_rows.append(("🔍", hdr, headers[hdr]))
if other_rows:
sections.append({"title": "📋 Other Headers", "rows": other_rows})
# SSL Certificate Information (if available)
if results.get('ssl_info') and 'subject' in results['ssl_info']:
output += "<strong>🔐 SSL Certificate</strong><br>"
ssl_info = results['ssl_info']
if ssl_info.get('subject'):
output += f" • <strong>Subject:</strong> {html_escape(ssl_info['subject'].get('commonName', 'N/A'))}<br>"
if ssl_info.get('issuer'):
output += f" • <strong>Issuer:</strong> {html_escape(ssl_info['issuer'].get('organizationName', 'N/A'))}<br>"
if ssl_info.get('not_after'):
output += f" • <strong>Expires:</strong> {html_escape(ssl_info['not_after'])}<br>"
output += "<br>"
# SSL Certificate
if cert_info:
ssl_rows = [
("📜", "Subject", cert_info['subject'].get('commonName', 'N/A')),
("🏢", "Issuer", cert_info['issuer'].get('organizationName', 'N/A')),
("📅", "Expires", cert_info.get('not_after', 'N/A')),
]
san = [san[1] for san in cert_info.get('san', []) if san[0] == 'DNS']
if san:
ssl_rows.append(("🌐", "SANs", ", ".join(san[:5])))
sections.append({"title": "🔐 SSL Certificate", "rows": ssl_rows})
# Recommendations
if results.get('recommendations'):
output += "<strong>💡 Security Recommendations</strong><br>"
for rec in results['recommendations'][:8]:
output += f"{rec}<br>"
output += "<br>"
if recommendations:
rec_rows = [("💡", "Recommendation", rec) for rec in recommendations]
sections.append({"title": "💡 Recommendations", "rows": rec_rows})
# Final rating
if score >= 80:
rating = "🟢 Excellent"
elif score >= 60:
rating = "🟡 Good"
elif score >= 40:
rating = "🟠 Fair"
else:
rating = "🔴 Poor"
output += f"<strong>📈 Security Rating:</strong> {rating}<br>"
block = code_block(f"🔒 Security Headers: {safe_host}", sections)
output = collapsible_summary(f"🔒 Headers: {safe_host}", block)
await bot.api.send_markdown_message(room.room_id, output)
# Wrap in collapsible details
return collapsible_summary(f"🔒 Security Headers Analysis: {safe_url} (Score: {score}/100)", output)
__version__ = "1.0.2"
# ---------------------------------------------------------------------------
# Plugin Metadata
# ---------------------------------------------------------------------------
__version__ = "1.1.2"
__author__ = "Funguy Bot"
__description__ = "HTTP security header analysis (SSRFsafe, async)"
__description__ = "HTTP security header analysis"
__help__ = """
<details>
<summary><strong>!headers</strong> HTTP security header scanner</summary>
<p><code>!headers &lt;url&gt;</code> Checks HSTS, CSP, X-Frame-Options, etc.<br>
Provides security score (0-100) and recommendations. Also shows SSL certificate info.</p>
<summary><strong>!headers</strong> HTTP security headers analysis</summary>
<p><code>!headers &lt;url&gt;</code> Analyzes security headers, SSL cert, gives score and recommendations in a clean, aligned table.</p>
</details>
"""