#!/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'{severity.upper()}' 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'
{escaped}
' 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"""
{caption}
{caption}
""" return f'
{cards}
' 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"""

Executive Summary

Assessment Overview

Target:{self._escape_html(self.scan_results.get('target', 'N/A'))}
Scan Started:{self.scan_results.get('scan_started', 'N/A')}
Scan Completed:{self.scan_results.get('scan_completed', 'N/A')}
Overall Risk Level:{self._get_severity_badge(risk_level)}

Findings Summary

{critical} Critical
{high} High
{medium} Medium
{low} Low

Total Vulnerabilities: {total}

Open Ports Found: {summary.get('open_ports', 0)}

Tools Executed: {summary.get('tools_executed', 0)}

""" 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 = "" return f"""

{self._escape_html(vuln.get('title', 'Unknown Vulnerability'))}

{self._get_severity_badge(severity)} CVSS: {vuln.get('cvss_score', 'N/A')} {f'CWE: {vuln.get("cwe_id")}' if vuln.get('cwe_id') else ''}

Description

{self._escape_html(vuln.get('description', 'No description available'))}

Affected Endpoint

{self._escape_html(vuln.get('affected_endpoint', 'N/A'))}

Impact

{self._escape_html(vuln.get('impact', 'Impact not assessed'))}

Proof of Concept (PoC)

Request
{self._format_code_block(vuln.get('poc_request', 'N/A'), 'http')}
Payload
{self._format_code_block(vuln.get('poc_payload', 'N/A'), 'text')}
Response
{self._format_code_block(vuln.get('poc_response', 'N/A')[:1000], 'http')}
{f'''

CVSS Vector

{self._escape_html(vuln.get('cvss_vector', 'N/A'))}
''' if vuln.get('cvss_vector') else ''}

Remediation

{self._escape_html(vuln.get('remediation', 'Consult vendor documentation for patches'))}

{f'''

References

{refs_html}
''' if refs_html else ''} {f'''

Raw Tool Output

{self._format_code_block(vuln.get('tool_output', '')[:2000], 'text')}
''' if vuln.get('tool_output') else ''}
""" 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""" {port.get('port', 'N/A')} {port.get('protocol', 'N/A')} {self._escape_html(port.get('service', 'N/A'))} {self._escape_html(port.get('version', 'N/A'))} """ return f"""

Open Ports & Services

{rows}
Port Protocol Service Version
""" 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""" {self._escape_html(tool.get('tool', 'N/A'))} {self._escape_html(tool.get('command', 'N/A')[:100])} {status} {tool.get('timestamp', 'N/A')} """ return f"""

Tools Executed

{rows}
Tool Command Status Timestamp
""" 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"""

AI Security Analysis

{analysis_html}
""" 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'
  • [{severity.upper()}] {self._escape_html(vuln.get("title", "Unknown")[:50])}
  • ' html = f""" NeuroSploitv2 - Penetration Test Report

    NeuroSploitv2

    Penetration Test Report

    Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}

    {self.generate_executive_summary()}

    Table of Contents - Vulnerabilities ({len(vulnerabilities)})

    {self.generate_open_ports_section()} {self.generate_tools_executed_section()}

    Vulnerability Details

    {vuln_cards if vuln_cards else '

    No vulnerabilities found during the assessment.

    '}
    {self.generate_llm_analysis_section()}
    """ 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