Files
NeuroSploit/core/report_generator.py
2026-02-11 10:50:37 -03:00

699 lines
23 KiB
Python

#!/usr/bin/env python3
"""
Professional Pentest Report Generator
Generates detailed reports with PoCs, CVSS scores, requests/responses
"""
import base64
import json
import os
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Any
import html
import logging
logger = logging.getLogger(__name__)
class ReportGenerator:
"""Generates professional penetration testing reports"""
def __init__(self, scan_results: Dict, llm_analysis: str = ""):
self.scan_results = scan_results
self.llm_analysis = llm_analysis
self.timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
def _get_severity_color(self, severity: str) -> str:
"""Get color for severity level"""
colors = {
"critical": "#dc3545",
"high": "#fd7e14",
"medium": "#ffc107",
"low": "#17a2b8",
"info": "#6c757d"
}
return colors.get(severity.lower(), "#6c757d")
def _get_severity_badge(self, severity: str) -> str:
"""Get HTML badge for severity"""
color = self._get_severity_color(severity)
return f'<span class="badge" style="background-color: {color}; color: white; padding: 5px 10px; border-radius: 4px;">{severity.upper()}</span>'
def _escape_html(self, text: str) -> str:
"""Escape HTML characters"""
if not text:
return ""
return html.escape(str(text))
def _format_code_block(self, code: str, language: str = "") -> str:
"""Format code block with syntax highlighting"""
escaped = self._escape_html(code)
return f'<pre><code class="language-{language}">{escaped}</code></pre>'
def embed_screenshot(self, filepath: str) -> str:
"""Convert a screenshot file to a base64 data URI for HTML embedding."""
path = Path(filepath)
if not path.exists():
return ""
try:
with open(path, 'rb') as f:
data = base64.b64encode(f.read()).decode('ascii')
return f"data:image/png;base64,{data}"
except Exception:
return ""
def build_screenshots_html(self, finding_id: str, screenshots_dir: str = "reports/screenshots") -> str:
"""Build screenshot grid HTML for a finding, embedding images as base64."""
finding_dir = Path(screenshots_dir) / finding_id
if not finding_dir.exists():
return ""
screenshots = sorted(finding_dir.glob("*.png"))[:3]
if not screenshots:
return ""
cards = ""
for ss in screenshots:
data_uri = self.embed_screenshot(str(ss))
if data_uri:
caption = ss.stem.replace('_', ' ').title()
cards += f"""
<div class="screenshot-card">
<img src="{data_uri}" alt="{caption}" />
<div class="screenshot-caption">{caption}</div>
</div>"""
return f'<div class="screenshot-grid">{cards}</div>' if cards else ""
def generate_executive_summary(self) -> str:
"""Generate executive summary section"""
summary = self.scan_results.get("summary", {})
severity = summary.get("severity_breakdown", {})
total = summary.get("total_vulnerabilities", 0)
critical = severity.get("Critical", 0)
high = severity.get("High", 0)
medium = severity.get("Medium", 0)
low = severity.get("Low", 0)
risk_level = "Critical" if critical > 0 else "High" if high > 0 else "Medium" if medium > 0 else "Low"
return f"""
<div class="card executive-summary">
<div class="card-header">
<h2>Executive Summary</h2>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h4>Assessment Overview</h4>
<table class="table">
<tr><td><strong>Target:</strong></td><td>{self._escape_html(self.scan_results.get('target', 'N/A'))}</td></tr>
<tr><td><strong>Scan Started:</strong></td><td>{self.scan_results.get('scan_started', 'N/A')}</td></tr>
<tr><td><strong>Scan Completed:</strong></td><td>{self.scan_results.get('scan_completed', 'N/A')}</td></tr>
<tr><td><strong>Overall Risk Level:</strong></td><td>{self._get_severity_badge(risk_level)}</td></tr>
</table>
</div>
<div class="col-md-6">
<h4>Findings Summary</h4>
<div class="severity-chart">
<div class="severity-bar critical" style="width: {critical * 20}%">{critical} Critical</div>
<div class="severity-bar high" style="width: {high * 20}%">{high} High</div>
<div class="severity-bar medium" style="width: {medium * 20}%">{medium} Medium</div>
<div class="severity-bar low" style="width: {low * 20}%">{low} Low</div>
</div>
<p class="mt-3"><strong>Total Vulnerabilities:</strong> {total}</p>
<p><strong>Open Ports Found:</strong> {summary.get('open_ports', 0)}</p>
<p><strong>Tools Executed:</strong> {summary.get('tools_executed', 0)}</p>
</div>
</div>
</div>
</div>
"""
def generate_vulnerability_card(self, vuln: Dict, index: int) -> str:
"""Generate HTML card for a single vulnerability"""
severity = vuln.get("severity", "Unknown")
color = self._get_severity_color(severity)
# Build references list
refs_html = ""
if vuln.get("references"):
refs_html = "<ul>"
for ref in vuln.get("references", [])[:5]:
refs_html += f'<li><a href="{self._escape_html(ref)}" target="_blank">{self._escape_html(ref)}</a></li>'
refs_html += "</ul>"
return f"""
<div class="vulnerability-card" id="vuln-{index}">
<div class="vuln-header" style="border-left: 5px solid {color};">
<div class="vuln-title">
<h3>{self._escape_html(vuln.get('title', 'Unknown Vulnerability'))}</h3>
<div class="vuln-meta">
{self._get_severity_badge(severity)}
<span class="cvss-score">CVSS: {vuln.get('cvss_score', 'N/A')}</span>
{f'<span class="cwe-id">CWE: {vuln.get("cwe_id")}</span>' if vuln.get('cwe_id') else ''}
</div>
</div>
</div>
<div class="vuln-body">
<div class="vuln-section">
<h4>Description</h4>
<p>{self._escape_html(vuln.get('description', 'No description available'))}</p>
</div>
<div class="vuln-section">
<h4>Affected Endpoint</h4>
<code class="endpoint">{self._escape_html(vuln.get('affected_endpoint', 'N/A'))}</code>
</div>
<div class="vuln-section">
<h4>Impact</h4>
<p>{self._escape_html(vuln.get('impact', 'Impact not assessed'))}</p>
</div>
<div class="vuln-section poc-section">
<h4>Proof of Concept (PoC)</h4>
<div class="poc-item">
<h5>Request</h5>
{self._format_code_block(vuln.get('poc_request', 'N/A'), 'http')}
</div>
<div class="poc-item">
<h5>Payload</h5>
{self._format_code_block(vuln.get('poc_payload', 'N/A'), 'text')}
</div>
<div class="poc-item">
<h5>Response</h5>
{self._format_code_block(vuln.get('poc_response', 'N/A')[:1000], 'http')}
</div>
</div>
{f'''<div class="vuln-section">
<h4>CVSS Vector</h4>
<code>{self._escape_html(vuln.get('cvss_vector', 'N/A'))}</code>
</div>''' if vuln.get('cvss_vector') else ''}
<div class="vuln-section remediation">
<h4>Remediation</h4>
<p>{self._escape_html(vuln.get('remediation', 'Consult vendor documentation for patches'))}</p>
</div>
{f'''<div class="vuln-section">
<h4>References</h4>
{refs_html}
</div>''' if refs_html else ''}
{f'''<div class="vuln-section tool-output">
<h4>Raw Tool Output</h4>
{self._format_code_block(vuln.get('tool_output', '')[:2000], 'text')}
</div>''' if vuln.get('tool_output') else ''}
</div>
</div>
"""
def generate_open_ports_section(self) -> str:
"""Generate open ports section"""
ports = self.scan_results.get("open_ports", [])
if not ports:
return ""
rows = ""
for port in ports:
rows += f"""
<tr>
<td>{port.get('port', 'N/A')}</td>
<td>{port.get('protocol', 'N/A')}</td>
<td>{self._escape_html(port.get('service', 'N/A'))}</td>
<td>{self._escape_html(port.get('version', 'N/A'))}</td>
</tr>
"""
return f"""
<div class="card">
<div class="card-header">
<h2>Open Ports & Services</h2>
</div>
<div class="card-body">
<table class="table table-striped">
<thead>
<tr>
<th>Port</th>
<th>Protocol</th>
<th>Service</th>
<th>Version</th>
</tr>
</thead>
<tbody>
{rows}
</tbody>
</table>
</div>
</div>
"""
def generate_tools_executed_section(self) -> str:
"""Generate tools executed section"""
tools = self.scan_results.get("tools_executed", [])
if not tools:
return ""
rows = ""
for tool in tools:
status = "Success" if tool.get("success") else "Failed"
status_class = "text-success" if tool.get("success") else "text-danger"
rows += f"""
<tr>
<td>{self._escape_html(tool.get('tool', 'N/A'))}</td>
<td><code>{self._escape_html(tool.get('command', 'N/A')[:100])}</code></td>
<td class="{status_class}">{status}</td>
<td>{tool.get('timestamp', 'N/A')}</td>
</tr>
"""
return f"""
<div class="card">
<div class="card-header">
<h2>Tools Executed</h2>
</div>
<div class="card-body">
<table class="table table-striped">
<thead>
<tr>
<th>Tool</th>
<th>Command</th>
<th>Status</th>
<th>Timestamp</th>
</tr>
</thead>
<tbody>
{rows}
</tbody>
</table>
</div>
</div>
"""
def generate_llm_analysis_section(self) -> str:
"""Generate AI analysis section"""
if not self.llm_analysis:
return ""
import mistune
analysis_html = mistune.html(self.llm_analysis)
return f"""
<div class="card">
<div class="card-header">
<h2>AI Security Analysis</h2>
</div>
<div class="card-body llm-analysis">
{analysis_html}
</div>
</div>
"""
def generate_html_report(self) -> str:
"""Generate complete HTML report"""
vulnerabilities = self.scan_results.get("vulnerabilities", [])
# Sort vulnerabilities by severity
severity_order = {"Critical": 0, "High": 1, "Medium": 2, "Low": 3, "Info": 4}
vulnerabilities.sort(key=lambda x: severity_order.get(x.get("severity", "Info").capitalize(), 5))
vuln_cards = ""
for i, vuln in enumerate(vulnerabilities, 1):
vuln_cards += self.generate_vulnerability_card(vuln, i)
# Table of contents
toc_items = ""
for i, vuln in enumerate(vulnerabilities, 1):
severity = vuln.get("severity", "Unknown")
color = self._get_severity_color(severity)
toc_items += f'<li><a href="#vuln-{i}" style="color: {color};">[{severity.upper()}] {self._escape_html(vuln.get("title", "Unknown")[:50])}</a></li>'
html = f"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NeuroSploitv2 - Penetration Test Report</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css">
<style>
:root {{
--bg-dark: #0d1117;
--bg-card: #161b22;
--border-color: #30363d;
--text-primary: #c9d1d9;
--text-secondary: #8b949e;
--accent-green: #00ff00;
--critical-color: #dc3545;
--high-color: #fd7e14;
--medium-color: #ffc107;
--low-color: #17a2b8;
}}
body {{
background-color: var(--bg-dark);
color: var(--text-primary);
font-family: 'Segoe UI', system-ui, sans-serif;
}}
.container {{
max-width: 1200px;
padding: 20px;
}}
.report-header {{
text-align: center;
padding: 40px 0;
border-bottom: 2px solid var(--accent-green);
margin-bottom: 30px;
}}
.report-header h1 {{
font-size: 2.5rem;
color: var(--accent-green);
text-shadow: 0 0 10px var(--accent-green);
margin-bottom: 10px;
}}
.card {{
background-color: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 8px;
margin-bottom: 20px;
}}
.card-header {{
background-color: rgba(0, 255, 0, 0.1);
border-bottom: 1px solid var(--border-color);
padding: 15px 20px;
}}
.card-header h2 {{
margin: 0;
color: var(--accent-green);
font-size: 1.3rem;
}}
.card-body {{
padding: 20px;
}}
.table {{
color: var(--text-primary);
}}
.table th {{
border-color: var(--border-color);
color: var(--accent-green);
}}
.table td {{
border-color: var(--border-color);
}}
.vulnerability-card {{
background-color: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 8px;
margin-bottom: 25px;
overflow: hidden;
}}
.vuln-header {{
padding: 20px;
background-color: rgba(0, 0, 0, 0.3);
}}
.vuln-title h3 {{
margin: 0 0 10px 0;
font-size: 1.2rem;
}}
.vuln-meta {{
display: flex;
gap: 15px;
align-items: center;
flex-wrap: wrap;
}}
.cvss-score {{
background-color: #333;
padding: 5px 10px;
border-radius: 4px;
font-family: monospace;
}}
.cwe-id {{
background-color: #1a365d;
padding: 5px 10px;
border-radius: 4px;
font-family: monospace;
}}
.vuln-body {{
padding: 20px;
}}
.vuln-section {{
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid var(--border-color);
}}
.vuln-section:last-child {{
border-bottom: none;
margin-bottom: 0;
}}
.vuln-section h4 {{
color: var(--accent-green);
font-size: 1rem;
margin-bottom: 10px;
}}
.vuln-section h5 {{
color: var(--text-secondary);
font-size: 0.9rem;
margin: 10px 0 5px 0;
}}
.poc-section {{
background-color: rgba(0, 0, 0, 0.2);
padding: 15px;
border-radius: 8px;
}}
.poc-item {{
margin-bottom: 15px;
}}
pre {{
background-color: #1e1e1e;
padding: 15px;
border-radius: 6px;
overflow-x: auto;
margin: 0;
}}
code {{
font-family: 'Fira Code', 'Consolas', monospace;
font-size: 0.85rem;
}}
.endpoint {{
background-color: #333;
padding: 8px 12px;
border-radius: 4px;
display: inline-block;
word-break: break-all;
}}
.remediation {{
background-color: rgba(0, 255, 0, 0.05);
border-left: 3px solid var(--accent-green);
padding-left: 15px;
}}
.severity-chart {{
display: flex;
flex-direction: column;
gap: 5px;
}}
.severity-bar {{
padding: 8px 15px;
border-radius: 4px;
font-weight: bold;
min-width: 100px;
}}
.severity-bar.critical {{ background-color: var(--critical-color); }}
.severity-bar.high {{ background-color: var(--high-color); color: #000; }}
.severity-bar.medium {{ background-color: var(--medium-color); color: #000; }}
.severity-bar.low {{ background-color: var(--low-color); }}
.toc {{
background-color: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 20px;
margin-bottom: 30px;
}}
.toc h3 {{
color: var(--accent-green);
margin-bottom: 15px;
}}
.toc ul {{
list-style: none;
padding: 0;
margin: 0;
}}
.toc li {{
padding: 5px 0;
}}
.toc a {{
text-decoration: none;
}}
.toc a:hover {{
text-decoration: underline;
}}
.llm-analysis {{
line-height: 1.8;
}}
.llm-analysis h2 {{
color: var(--accent-green);
border-bottom: 1px solid var(--border-color);
padding-bottom: 10px;
}}
.footer {{
text-align: center;
padding: 30px;
border-top: 1px solid var(--border-color);
margin-top: 30px;
color: var(--text-secondary);
}}
@media print {{
body {{
background-color: white;
color: black;
}}
.vulnerability-card {{
page-break-inside: avoid;
}}
}}
</style>
</head>
<body>
<div class="container">
<div class="report-header">
<h1>NeuroSploitv2</h1>
<p class="lead">Penetration Test Report</p>
<p class="text-muted">Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
</div>
{self.generate_executive_summary()}
<div class="toc">
<h3>Table of Contents - Vulnerabilities ({len(vulnerabilities)})</h3>
<ul>
{toc_items}
</ul>
</div>
{self.generate_open_ports_section()}
{self.generate_tools_executed_section()}
<div class="card">
<div class="card-header">
<h2>Vulnerability Details</h2>
</div>
<div class="card-body">
{vuln_cards if vuln_cards else '<p class="text-muted">No vulnerabilities found during the assessment.</p>'}
</div>
</div>
{self.generate_llm_analysis_section()}
<div class="footer">
<p>Report generated by <strong>NeuroSploitv2</strong> - AI-Powered Penetration Testing Framework</p>
<p class="small">This report is confidential and intended for authorized personnel only.</p>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
</body>
</html>
"""
return html
def save_report(self, output_dir: str = "reports") -> str:
"""Save HTML report to a per-report folder with screenshots."""
import shutil
# Create per-report folder
target = self.scan_results.get("target_url", self.scan_results.get("target", "unknown"))
target_name = target.replace("://", "_").replace("/", "_").rstrip("_")[:40]
report_folder = f"report_{target_name}_{self.timestamp}"
report_dir = os.path.join(output_dir, report_folder)
os.makedirs(report_dir, exist_ok=True)
filename = f"pentest_report_{self.timestamp}.html"
filepath = os.path.join(report_dir, filename)
html_content = self.generate_html_report()
with open(filepath, 'w', encoding='utf-8') as f:
f.write(html_content)
# Copy screenshots into report folder
screenshots_src = os.path.join("reports", "screenshots")
if os.path.exists(screenshots_src):
screenshots_dest = os.path.join(report_dir, "screenshots")
vulns = self.scan_results.get("vulnerabilities", [])
for vuln in vulns:
fid = vuln.get("id", "")
if fid:
src_dir = os.path.join(screenshots_src, str(fid))
if os.path.exists(src_dir):
dest_dir = os.path.join(screenshots_dest, str(fid))
os.makedirs(dest_dir, exist_ok=True)
for ss_file in Path(src_dir).glob("*.png"):
shutil.copy2(str(ss_file), os.path.join(dest_dir, ss_file.name))
logger.info(f"Report saved to: {filepath}")
return filepath
def save_json_report(self, output_dir: str = "results") -> str:
"""Save JSON report to file"""
os.makedirs(output_dir, exist_ok=True)
filename = f"pentest_results_{self.timestamp}.json"
filepath = os.path.join(output_dir, filename)
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(self.scan_results, f, indent=2, default=str)
logger.info(f"JSON results saved to: {filepath}")
return filepath