Files
NeuroSploit/backend/core/report_generator.py
2026-02-11 10:47:33 -03:00

1129 lines
41 KiB
Python

"""
NeuroSploit v3 - Professional HTML Report Generator
Generates beautiful, comprehensive security assessment reports
"""
import json
import base64
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Any, Optional
from dataclasses import dataclass
import html
@dataclass
class ReportConfig:
"""Report generation configuration"""
company_name: str = "NeuroSploit Security"
logo_base64: Optional[str] = None
include_executive_summary: bool = True
include_methodology: bool = True
include_recommendations: bool = True
theme: str = "dark" # "dark" or "light"
class HTMLReportGenerator:
"""Generate professional HTML security reports"""
SEVERITY_COLORS = {
"critical": {"bg": "#dc2626", "text": "#ffffff", "border": "#991b1b"},
"high": {"bg": "#ea580c", "text": "#ffffff", "border": "#c2410c"},
"medium": {"bg": "#ca8a04", "text": "#ffffff", "border": "#a16207"},
"low": {"bg": "#2563eb", "text": "#ffffff", "border": "#1d4ed8"},
"info": {"bg": "#6b7280", "text": "#ffffff", "border": "#4b5563"}
}
SEVERITY_ORDER = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}
def __init__(self, config: Optional[ReportConfig] = None):
self.config = config or ReportConfig()
def generate_report(
self,
session_data: Dict,
findings: List[Dict],
scan_results: Optional[List[Dict]] = None
) -> str:
"""Generate complete HTML report"""
# Sort findings by severity
sorted_findings = sorted(
findings,
key=lambda x: self.SEVERITY_ORDER.get(x.get('severity', 'info'), 4)
)
# Calculate statistics
stats = self._calculate_stats(sorted_findings)
# Generate report sections
html_content = f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Security Assessment Report - {html.escape(session_data.get('name', 'Unknown'))}</title>
{self._get_styles()}
</head>
<body>
<div class="report-container">
{self._generate_header(session_data)}
{self._generate_executive_summary(session_data, stats, sorted_findings)}
{self._generate_scope_section(session_data)}
{self._generate_findings_summary(stats)}
{self._generate_findings_detail(sorted_findings)}
{self._generate_scan_results(scan_results) if scan_results else ''}
{self._generate_recommendations(sorted_findings)}
{self._generate_methodology()}
{self._generate_footer(session_data)}
</div>
{self._get_scripts()}
</body>
</html>"""
return html_content
def _get_styles(self) -> str:
"""Get CSS styles for the report"""
is_dark = self.config.theme == "dark"
bg_color = "#0f172a" if is_dark else "#ffffff"
card_bg = "#1e293b" if is_dark else "#f8fafc"
text_color = "#e2e8f0" if is_dark else "#1e293b"
text_muted = "#94a3b8" if is_dark else "#64748b"
border_color = "#334155" if is_dark else "#e2e8f0"
accent = "#3b82f6"
return f"""
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
* {{
margin: 0;
padding: 0;
box-sizing: border-box;
}}
body {{
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
background: {bg_color};
color: {text_color};
line-height: 1.6;
font-size: 14px;
}}
.report-container {{
max-width: 1200px;
margin: 0 auto;
padding: 40px 20px;
}}
/* Header */
.report-header {{
text-align: center;
padding: 60px 40px;
background: linear-gradient(135deg, #1e40af 0%, #7c3aed 100%);
border-radius: 16px;
margin-bottom: 40px;
position: relative;
overflow: hidden;
}}
.report-header::before {{
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.05'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
opacity: 0.5;
}}
.report-header h1 {{
font-size: 2.5rem;
font-weight: 700;
color: white;
margin-bottom: 8px;
position: relative;
}}
.report-header .subtitle {{
font-size: 1.1rem;
color: rgba(255,255,255,0.9);
position: relative;
}}
.report-header .meta {{
margin-top: 24px;
display: flex;
justify-content: center;
gap: 40px;
color: rgba(255,255,255,0.8);
font-size: 0.9rem;
position: relative;
}}
.report-header .meta-item {{
display: flex;
align-items: center;
gap: 8px;
}}
/* Cards */
.card {{
background: {card_bg};
border: 1px solid {border_color};
border-radius: 12px;
padding: 24px;
margin-bottom: 24px;
}}
.card-header {{
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid {border_color};
}}
.card-header h2 {{
font-size: 1.25rem;
font-weight: 600;
color: {text_color};
}}
.card-header .icon {{
width: 32px;
height: 32px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
background: {accent};
color: white;
}}
/* Stats Grid */
.stats-grid {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 16px;
margin-bottom: 32px;
}}
.stat-card {{
padding: 20px;
border-radius: 12px;
text-align: center;
transition: transform 0.2s;
}}
.stat-card:hover {{
transform: translateY(-2px);
}}
.stat-card .number {{
font-size: 2.5rem;
font-weight: 700;
line-height: 1;
}}
.stat-card .label {{
font-size: 0.875rem;
margin-top: 8px;
text-transform: uppercase;
letter-spacing: 0.05em;
}}
.stat-critical {{ background: linear-gradient(135deg, #dc2626, #991b1b); color: white; }}
.stat-high {{ background: linear-gradient(135deg, #ea580c, #c2410c); color: white; }}
.stat-medium {{ background: linear-gradient(135deg, #ca8a04, #a16207); color: white; }}
.stat-low {{ background: linear-gradient(135deg, #2563eb, #1d4ed8); color: white; }}
.stat-info {{ background: linear-gradient(135deg, #6b7280, #4b5563); color: white; }}
.stat-total {{ background: linear-gradient(135deg, #7c3aed, #5b21b6); color: white; }}
/* Findings */
.finding {{
border: 1px solid {border_color};
border-radius: 12px;
margin-bottom: 16px;
overflow: hidden;
transition: box-shadow 0.2s;
}}
.finding:hover {{
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
}}
.finding-header {{
padding: 16px 20px;
display: flex;
align-items: center;
gap: 16px;
cursor: pointer;
background: {card_bg};
}}
.finding-header:hover {{
background: {'#293548' if is_dark else '#f1f5f9'};
}}
.severity-badge {{
padding: 6px 12px;
border-radius: 6px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
min-width: 80px;
text-align: center;
}}
.finding-title {{
flex: 1;
font-weight: 500;
color: {text_color};
}}
.finding-endpoint {{
font-size: 0.875rem;
color: {text_muted};
max-width: 300px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}}
.finding-content {{
padding: 20px;
background: {'#151f2e' if is_dark else '#ffffff'};
display: none;
}}
.finding-content.active {{
display: block;
}}
.finding-section {{
margin-bottom: 16px;
}}
.finding-section:last-child {{
margin-bottom: 0;
}}
.finding-section h4 {{
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.1em;
color: {text_muted};
margin-bottom: 8px;
}}
.finding-section p {{
color: {text_color};
}}
.evidence-box {{
background: {'#0f172a' if is_dark else '#f1f5f9'};
border: 1px solid {border_color};
border-radius: 8px;
padding: 12px 16px;
font-family: 'Fira Code', monospace;
font-size: 0.875rem;
overflow-x: auto;
white-space: pre-wrap;
word-break: break-all;
}}
.remediation-box {{
background: rgba(34, 197, 94, 0.1);
border: 1px solid rgba(34, 197, 94, 0.3);
border-radius: 8px;
padding: 12px 16px;
color: #22c55e;
}}
/* Executive Summary */
.exec-summary {{
display: grid;
grid-template-columns: 2fr 1fr;
gap: 24px;
}}
.risk-meter {{
height: 12px;
background: {border_color};
border-radius: 6px;
overflow: hidden;
margin: 16px 0;
}}
.risk-meter-fill {{
height: 100%;
border-radius: 6px;
transition: width 0.5s ease;
}}
.risk-high {{ background: linear-gradient(90deg, #dc2626, #ea580c); }}
.risk-medium {{ background: linear-gradient(90deg, #ea580c, #ca8a04); }}
.risk-low {{ background: linear-gradient(90deg, #ca8a04, #22c55e); }}
/* Table */
table {{
width: 100%;
border-collapse: collapse;
}}
th, td {{
padding: 12px 16px;
text-align: left;
border-bottom: 1px solid {border_color};
}}
th {{
font-weight: 600;
color: {text_muted};
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.1em;
}}
/* Footer */
.report-footer {{
text-align: center;
padding: 40px;
color: {text_muted};
border-top: 1px solid {border_color};
margin-top: 40px;
}}
.report-footer .logo {{
font-size: 1.5rem;
font-weight: 700;
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 8px;
}}
/* Print styles */
@media print {{
body {{
background: white;
color: black;
}}
.card {{
break-inside: avoid;
}}
.finding {{
break-inside: avoid;
}}
.report-header {{
background: #1e40af !important;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}}
}}
/* Responsive */
@media (max-width: 768px) {{
.exec-summary {{
grid-template-columns: 1fr;
}}
.stats-grid {{
grid-template-columns: repeat(2, 1fr);
}}
.report-header .meta {{
flex-direction: column;
gap: 12px;
}}
}}
/* Animations */
@keyframes fadeIn {{
from {{ opacity: 0; transform: translateY(10px); }}
to {{ opacity: 1; transform: translateY(0); }}
}}
.card {{
animation: fadeIn 0.3s ease;
}}
/* Screenshot grid */
.screenshot-grid {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 16px;
margin-top: 12px;
}}
.screenshot-card {{
border: 1px solid {border_color};
border-radius: 8px;
overflow: hidden;
background: {'#0f172a' if is_dark else '#f1f5f9'};
transition: transform 0.2s, box-shadow 0.2s;
}}
.screenshot-card:hover {{
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0,0,0,0.3);
}}
.screenshot-card img {{
width: 100%;
height: auto;
display: block;
cursor: pointer;
}}
.screenshot-caption {{
padding: 8px 12px;
font-size: 0.75rem;
color: {text_muted};
text-align: center;
border-top: 1px solid {border_color};
text-transform: uppercase;
letter-spacing: 0.05em;
}}
/* Screenshot modal (fullscreen view) */
.screenshot-modal {{
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.9);
z-index: 10000;
justify-content: center;
align-items: center;
cursor: pointer;
}}
.screenshot-modal.active {{
display: flex;
}}
.screenshot-modal img {{
max-width: 90%;
max-height: 90%;
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
}}
</style>"""
def _get_scripts(self) -> str:
"""Get JavaScript for interactivity"""
return """
<script>
document.querySelectorAll('.finding-header').forEach(header => {
header.addEventListener('click', () => {
const content = header.nextElementSibling;
const isActive = content.classList.contains('active');
// Close all others
document.querySelectorAll('.finding-content').forEach(c => {
c.classList.remove('active');
});
// Toggle current
if (!isActive) {
content.classList.add('active');
}
});
});
// Expand all button functionality
function expandAll() {
document.querySelectorAll('.finding-content').forEach(c => {
c.classList.add('active');
});
}
function collapseAll() {
document.querySelectorAll('.finding-content').forEach(c => {
c.classList.remove('active');
});
}
// Print functionality
function printReport() {
window.print();
}
// Screenshot zoom modal
(function() {
var modal = document.createElement('div');
modal.className = 'screenshot-modal';
modal.innerHTML = '<img />';
document.body.appendChild(modal);
document.addEventListener('click', function(e) {
if (e.target.closest('.screenshot-card img')) {
var src = e.target.src;
modal.querySelector('img').src = src;
modal.classList.add('active');
}
if (e.target.closest('.screenshot-modal')) {
modal.classList.remove('active');
}
});
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') modal.classList.remove('active');
});
})();
</script>"""
def _generate_header(self, session_data: Dict) -> str:
"""Generate report header"""
target = session_data.get('target', 'Unknown Target')
name = session_data.get('name', 'Security Assessment')
created = session_data.get('created_at', datetime.utcnow().isoformat())
try:
created_dt = datetime.fromisoformat(created.replace('Z', '+00:00'))
created_str = created_dt.strftime('%B %d, %Y')
except:
created_str = created
return f"""
<header class="report-header">
<h1>🛡️ Security Assessment Report</h1>
<p class="subtitle">{html.escape(name)}</p>
<div class="meta">
<div class="meta-item">
<span>🎯</span>
<span>{html.escape(target)}</span>
</div>
<div class="meta-item">
<span>📅</span>
<span>{created_str}</span>
</div>
<div class="meta-item">
<span>🔬</span>
<span>NeuroSploit AI Scanner</span>
</div>
</div>
</header>"""
def _calculate_stats(self, findings: List[Dict]) -> Dict:
"""Calculate finding statistics"""
stats = {
"total": len(findings),
"critical": 0,
"high": 0,
"medium": 0,
"low": 0,
"info": 0
}
for finding in findings:
severity = finding.get('severity', 'info').lower()
if severity in stats:
stats[severity] += 1
# Calculate risk score (0-100)
risk_score = (
stats['critical'] * 25 +
stats['high'] * 15 +
stats['medium'] * 8 +
stats['low'] * 3 +
stats['info'] * 1
)
stats['risk_score'] = min(100, risk_score)
# Risk level
if stats['risk_score'] >= 70 or stats['critical'] > 0:
stats['risk_level'] = 'HIGH'
stats['risk_class'] = 'risk-high'
elif stats['risk_score'] >= 40 or stats['high'] > 1:
stats['risk_level'] = 'MEDIUM'
stats['risk_class'] = 'risk-medium'
else:
stats['risk_level'] = 'LOW'
stats['risk_class'] = 'risk-low'
return stats
def _generate_executive_summary(self, session_data: Dict, stats: Dict, findings: List[Dict]) -> str:
"""Generate executive summary section"""
target = session_data.get('target', 'the target')
# Generate summary text based on findings
if stats['critical'] > 0:
summary = f"The security assessment of {html.escape(target)} revealed <strong>{stats['critical']} critical</strong> vulnerabilities that require immediate attention. These findings pose significant risk to the application's security posture and could lead to severe data breaches or system compromise."
elif stats['high'] > 0:
summary = f"The security assessment identified <strong>{stats['high']} high-severity</strong> issues that should be addressed promptly. While no critical vulnerabilities were found, the identified issues could be exploited by attackers to gain unauthorized access or compromise sensitive data."
elif stats['medium'] > 0:
summary = f"The assessment found <strong>{stats['medium']} medium-severity</strong> findings that represent moderate risk. These issues should be included in the remediation roadmap and addressed according to priority."
else:
summary = f"The security assessment completed with <strong>{stats['total']} findings</strong>, primarily informational in nature. The overall security posture appears reasonable, though continuous monitoring is recommended."
return f"""
<section class="card">
<div class="card-header">
<div class="icon">📊</div>
<h2>Executive Summary</h2>
</div>
<div class="exec-summary">
<div>
<p style="margin-bottom: 16px;">{summary}</p>
<div class="risk-meter">
<div class="risk-meter-fill {stats['risk_class']}" style="width: {stats['risk_score']}%"></div>
</div>
<p style="font-size: 0.875rem; color: var(--text-muted);">
Overall Risk Score: <strong>{stats['risk_score']}/100</strong> ({stats['risk_level']})
</p>
</div>
<div style="text-align: center; padding: 20px; background: rgba(59, 130, 246, 0.1); border-radius: 12px;">
<div style="font-size: 3rem; font-weight: 700; color: #3b82f6;">{stats['total']}</div>
<div style="text-transform: uppercase; letter-spacing: 0.1em; font-size: 0.75rem; color: #94a3b8;">Total Findings</div>
</div>
</div>
</section>"""
def _generate_scope_section(self, session_data: Dict) -> str:
"""Generate scope section"""
target = session_data.get('target', 'Unknown')
recon = session_data.get('recon_data', {})
technologies = recon.get('technologies', [])
endpoints = recon.get('endpoints', [])
tech_html = ""
if technologies:
tech_html = f"""
<div style="margin-top: 16px;">
<h4 style="font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.1em; color: #94a3b8; margin-bottom: 8px;">Detected Technologies</h4>
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
{"".join(f'<span style="background: rgba(59,130,246,0.2); color: #60a5fa; padding: 4px 12px; border-radius: 20px; font-size: 0.875rem;">{html.escape(t)}</span>' for t in technologies[:15])}
</div>
</div>"""
return f"""
<section class="card">
<div class="card-header">
<div class="icon">🎯</div>
<h2>Assessment Scope</h2>
</div>
<table>
<tr>
<td style="width: 150px; font-weight: 500;">Target URL</td>
<td><a href="{html.escape(target)}" style="color: #3b82f6;">{html.escape(target)}</a></td>
</tr>
<tr>
<td style="font-weight: 500;">Endpoints Tested</td>
<td>{len(endpoints)}</td>
</tr>
<tr>
<td style="font-weight: 500;">Assessment Type</td>
<td>Automated Security Scan + AI Analysis</td>
</tr>
</table>
{tech_html}
</section>"""
def _generate_findings_summary(self, stats: Dict) -> str:
"""Generate findings summary with stats cards"""
return f"""
<section class="card">
<div class="card-header">
<div class="icon">📈</div>
<h2>Findings Overview</h2>
</div>
<div class="stats-grid">
<div class="stat-card stat-critical">
<div class="number">{stats['critical']}</div>
<div class="label">Critical</div>
</div>
<div class="stat-card stat-high">
<div class="number">{stats['high']}</div>
<div class="label">High</div>
</div>
<div class="stat-card stat-medium">
<div class="number">{stats['medium']}</div>
<div class="label">Medium</div>
</div>
<div class="stat-card stat-low">
<div class="number">{stats['low']}</div>
<div class="label">Low</div>
</div>
<div class="stat-card stat-info">
<div class="number">{stats['info']}</div>
<div class="label">Info</div>
</div>
<div class="stat-card stat-total">
<div class="number">{stats['total']}</div>
<div class="label">Total</div>
</div>
</div>
</section>"""
def _generate_findings_detail(self, findings: List[Dict]) -> str:
"""Generate detailed findings section with CVSS, CWE, and OWASP data"""
if not findings:
return """
<section class="card">
<div class="card-header">
<div class="icon">🔍</div>
<h2>Detailed Findings</h2>
</div>
<p style="text-align: center; padding: 40px; color: #94a3b8;">
No vulnerabilities were identified during this assessment.
</p>
</section>"""
findings_html = ""
for i, finding in enumerate(findings):
severity = finding.get('severity', 'info').lower()
colors = self.SEVERITY_COLORS.get(severity, self.SEVERITY_COLORS['info'])
# Get CVSS, CWE, and OWASP data
cvss_score = finding.get('cvss_score', self._get_default_cvss(severity))
cvss_vector = finding.get('cvss_vector', '')
cwe_id = finding.get('cwe_id', '')
owasp = finding.get('owasp', '')
# Generate technical info section
tech_info_html = ""
if cvss_score or cwe_id or owasp:
tech_items = []
if cvss_score:
cvss_color = self._get_cvss_color(cvss_score)
tech_items.append(f'''
<div style="flex: 1; min-width: 150px;">
<div style="font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.05em; color: #94a3b8; margin-bottom: 4px;">CVSS Score</div>
<div style="display: flex; align-items: center; gap: 8px;">
<span style="font-size: 1.5rem; font-weight: 700; color: {cvss_color};">{cvss_score}</span>
<span style="font-size: 0.75rem; color: #94a3b8;">{self._get_cvss_rating(cvss_score)}</span>
</div>
{f'<div style="font-size: 0.7rem; color: #64748b; margin-top: 2px; font-family: monospace;">{html.escape(cvss_vector)}</div>' if cvss_vector else ''}
</div>''')
if cwe_id:
cwe_link = f"https://cwe.mitre.org/data/definitions/{cwe_id.replace('CWE-', '')}.html" if cwe_id.startswith('CWE-') else '#'
tech_items.append(f'''
<div style="flex: 1; min-width: 150px;">
<div style="font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.05em; color: #94a3b8; margin-bottom: 4px;">CWE Reference</div>
<a href="{cwe_link}" target="_blank" style="color: #60a5fa; text-decoration: none; font-weight: 500;">
{html.escape(cwe_id)}
</a>
</div>''')
if owasp:
tech_items.append(f'''
<div style="flex: 1; min-width: 150px;">
<div style="font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.05em; color: #94a3b8; margin-bottom: 4px;">OWASP Top 10</div>
<span style="color: #fbbf24; font-weight: 500;">{html.escape(owasp)}</span>
</div>''')
tech_info_html = f'''
<div style="display: flex; flex-wrap: wrap; gap: 24px; padding: 16px; background: rgba(59, 130, 246, 0.05); border: 1px solid rgba(59, 130, 246, 0.1); border-radius: 8px; margin-bottom: 16px;">
{''.join(tech_items)}
</div>'''
findings_html += f"""
<div class="finding">
<div class="finding-header">
<span class="severity-badge" style="background: {colors['bg']}; color: {colors['text']};">
{severity.upper()}
</span>
<span class="finding-title">{html.escape(finding.get('title', 'Unknown'))}</span>
<span class="finding-endpoint">{html.escape(finding.get('affected_endpoint', ''))}</span>
<span style="color: #94a3b8;">▼</span>
</div>
<div class="finding-content">
{tech_info_html}
<div class="finding-section">
<h4>Vulnerability Type</h4>
<p>{html.escape(finding.get('vulnerability_type', 'Unknown'))}</p>
</div>
<div class="finding-section">
<h4>Description</h4>
<p>{html.escape(finding.get('description', 'No description available'))}</p>
</div>
{f'''<div class="finding-section">
<h4>Affected Endpoint</h4>
<div class="evidence-box">{html.escape(finding.get('affected_endpoint', ''))}</div>
</div>''' if finding.get('affected_endpoint') else ''}
{f'''<div class="finding-section">
<h4>Evidence / Proof of Concept</h4>
<div class="evidence-box">{html.escape(finding.get('evidence', ''))}</div>
</div>''' if finding.get('evidence') else ''}
{self._generate_screenshots_html(finding)}
{f'''<div class="finding-section">
<h4>Impact</h4>
<p>{html.escape(finding.get('impact', ''))}</p>
</div>''' if finding.get('impact') else ''}
<div class="finding-section">
<h4>Remediation</h4>
<div class="remediation-box">{html.escape(finding.get('remediation', 'Review and address this finding'))}</div>
</div>
{self._generate_references_html(finding.get('references', []))}
</div>
</div>"""
return f"""
<section class="card">
<div class="card-header">
<div class="icon">🔍</div>
<h2>Detailed Findings</h2>
<div style="margin-left: auto; display: flex; gap: 8px;">
<button onclick="expandAll()" style="padding: 6px 12px; border-radius: 6px; border: 1px solid #334155; background: transparent; color: #94a3b8; cursor: pointer; font-size: 0.75rem;">Expand All</button>
<button onclick="collapseAll()" style="padding: 6px 12px; border-radius: 6px; border: 1px solid #334155; background: transparent; color: #94a3b8; cursor: pointer; font-size: 0.75rem;">Collapse All</button>
</div>
</div>
{findings_html}
</section>"""
def _get_default_cvss(self, severity: str) -> float:
"""Get default CVSS score based on severity"""
defaults = {
'critical': 9.5,
'high': 7.5,
'medium': 5.0,
'low': 3.0,
'info': 0.0
}
return defaults.get(severity.lower(), 5.0)
def _get_cvss_color(self, score: float) -> str:
"""Get color based on CVSS score"""
if score >= 9.0:
return '#dc2626' # Critical - Red
elif score >= 7.0:
return '#ea580c' # High - Orange
elif score >= 4.0:
return '#ca8a04' # Medium - Yellow
elif score > 0:
return '#2563eb' # Low - Blue
else:
return '#6b7280' # Info - Gray
def _get_cvss_rating(self, score: float) -> str:
"""Get CVSS rating text"""
if score >= 9.0:
return 'Critical'
elif score >= 7.0:
return 'High'
elif score >= 4.0:
return 'Medium'
elif score > 0:
return 'Low'
else:
return 'None'
def _generate_references_html(self, references: List[str]) -> str:
"""Generate references section HTML"""
if not references:
return ''
refs_html = ''
for ref in references[:5]: # Limit to 5 references
if ref.startswith('http'):
refs_html += f'<li><a href="{html.escape(ref)}" target="_blank" style="color: #60a5fa; text-decoration: none;">{html.escape(ref[:60])}{"..." if len(ref) > 60 else ""}</a></li>'
else:
refs_html += f'<li>{html.escape(ref)}</li>'
return f'''
<div class="finding-section">
<h4>References</h4>
<ul style="margin-left: 16px; color: #94a3b8; font-size: 0.875rem;">
{refs_html}
</ul>
</div>'''
def _generate_screenshots_html(self, finding: Dict) -> str:
"""Generate screenshot grid HTML for a finding.
Supports two sources:
1. finding['screenshots'] list with base64 data URIs (from agent capture)
2. Filesystem lookup in reports/screenshots/{finding_id}/ (from BrowserValidator)
"""
screenshots = finding.get('screenshots', [])
# Also check filesystem for screenshots stored by BrowserValidator
finding_id = finding.get('id', '')
if finding_id and not screenshots:
ss_dir = Path('reports/screenshots') / finding_id
if ss_dir.exists():
for ss_file in sorted(ss_dir.glob('*.png'))[:5]:
try:
with open(ss_file, 'rb') as f:
data = base64.b64encode(f.read()).decode('ascii')
screenshots.append(f"data:image/png;base64,{data}")
except Exception:
pass
if not screenshots:
return ''
cards = ''
for i, ss in enumerate(screenshots[:5]): # Cap at 5 screenshots
label = f"Screenshot {i + 1}"
if i == 0:
label = "Evidence Capture"
elif i == 1:
label = "Exploitation Proof"
cards += f'''
<div class="screenshot-card">
<img src="{ss}" alt="{label}" loading="lazy" />
<div class="screenshot-caption">{label}</div>
</div>'''
return f'''
<div class="finding-section">
<h4>Screenshots</h4>
<div class="screenshot-grid">{cards}</div>
</div>'''
def _generate_scan_results(self, scan_results: List[Dict]) -> str:
"""Generate tool scan results section"""
if not scan_results:
return ""
results_html = ""
for result in scan_results:
tool = result.get('tool', 'Unknown')
status = result.get('status', 'unknown')
output = result.get('output', '')[:2000] # Limit output size
status_color = "#22c55e" if status == "completed" else "#ef4444"
results_html += f"""
<div style="margin-bottom: 16px;">
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 8px;">
<strong>{html.escape(tool)}</strong>
<span style="color: {status_color}; font-size: 0.75rem; text-transform: uppercase;">{status}</span>
</div>
<div class="evidence-box" style="max-height: 200px; overflow-y: auto;">
{html.escape(output)}
</div>
</div>"""
return f"""
<section class="card">
<div class="card-header">
<div class="icon">🔧</div>
<h2>Tool Scan Results</h2>
</div>
{results_html}
</section>"""
def _generate_recommendations(self, findings: List[Dict]) -> str:
"""Generate prioritized recommendations"""
recommendations = []
# Group findings by severity
critical = [f for f in findings if f.get('severity') == 'critical']
high = [f for f in findings if f.get('severity') == 'high']
medium = [f for f in findings if f.get('severity') == 'medium']
if critical:
recommendations.append({
"priority": "Immediate",
"color": "#dc2626",
"items": [f"Fix: {f.get('title', 'Unknown')} - {f.get('remediation', 'Review and fix')}" for f in critical]
})
if high:
recommendations.append({
"priority": "Short-term (1-2 weeks)",
"color": "#ea580c",
"items": [f"Address: {f.get('title', 'Unknown')}" for f in high]
})
if medium:
recommendations.append({
"priority": "Medium-term (1 month)",
"color": "#ca8a04",
"items": [f"Plan fix for: {f.get('title', 'Unknown')}" for f in medium[:5]]
})
# Always add general recommendations
recommendations.append({
"priority": "Ongoing",
"color": "#3b82f6",
"items": [
"Implement regular security scanning",
"Keep all software and dependencies updated",
"Review and strengthen authentication mechanisms",
"Implement proper logging and monitoring",
"Conduct periodic penetration testing"
]
})
rec_html = ""
for rec in recommendations:
items_html = "".join(f"<li>{html.escape(item)}</li>" for item in rec['items'])
rec_html += f"""
<div style="margin-bottom: 24px;">
<h4 style="color: {rec['color']}; margin-bottom: 12px; display: flex; align-items: center; gap: 8px;">
<span style="width: 8px; height: 8px; background: {rec['color']}; border-radius: 50%;"></span>
{rec['priority']}
</h4>
<ul style="margin-left: 24px; color: #94a3b8;">
{items_html}
</ul>
</div>"""
return f"""
<section class="card">
<div class="card-header">
<div class="icon">✅</div>
<h2>Recommendations</h2>
</div>
{rec_html}
</section>"""
def _generate_methodology(self) -> str:
"""Generate methodology section"""
return """
<section class="card">
<div class="card-header">
<div class="icon">📋</div>
<h2>Methodology</h2>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px;">
<div>
<h4 style="color: #3b82f6; margin-bottom: 8px;">1. Reconnaissance</h4>
<p style="color: #94a3b8; font-size: 0.875rem;">Technology fingerprinting, endpoint discovery, and information gathering</p>
</div>
<div>
<h4 style="color: #8b5cf6; margin-bottom: 8px;">2. Vulnerability Scanning</h4>
<p style="color: #94a3b8; font-size: 0.875rem;">Automated scanning for known vulnerabilities and misconfigurations</p>
</div>
<div>
<h4 style="color: #ec4899; margin-bottom: 8px;">3. AI Analysis</h4>
<p style="color: #94a3b8; font-size: 0.875rem;">LLM-powered analysis of findings for context and remediation</p>
</div>
<div>
<h4 style="color: #22c55e; margin-bottom: 8px;">4. Verification</h4>
<p style="color: #94a3b8; font-size: 0.875rem;">Manual verification of critical findings to eliminate false positives</p>
</div>
</div>
</section>"""
def _generate_footer(self, session_data: Dict) -> str:
"""Generate report footer"""
return f"""
<footer class="report-footer">
<div class="logo">⚡ NeuroSploit</div>
<p>AI-Powered Security Assessment Platform</p>
<p style="margin-top: 16px; font-size: 0.75rem;">
Report generated on {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC')}
</p>
<p style="margin-top: 8px; font-size: 0.75rem;">
This report contains confidential security information. Handle with care.
</p>
</footer>"""