Files
autarch/core/report_generator.py

1138 lines
39 KiB
Python
Raw Permalink Normal View History

2026-03-13 15:17:15 -07:00
"""
AUTARCH Report Generator
Generate HTML reports for scan results
"""
import json
import os
from datetime import datetime
from pathlib import Path
from typing import List, Dict, Optional
class ReportGenerator:
"""Generate HTML reports for OSINT scan results."""
def __init__(self, output_dir: str = None):
"""Initialize report generator.
Args:
output_dir: Directory to save reports. Defaults to results/reports.
"""
if output_dir:
self.output_dir = Path(output_dir)
else:
from core.paths import get_reports_dir
self.output_dir = get_reports_dir()
self.output_dir.mkdir(parents=True, exist_ok=True)
def _get_html_template(self) -> str:
"""Get base HTML template."""
return '''<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{title}</title>
<style>
:root {{
--bg-primary: #0d1117;
--bg-secondary: #161b22;
--bg-tertiary: #21262d;
--text-primary: #c9d1d9;
--text-secondary: #8b949e;
--accent-green: #3fb950;
--accent-red: #f85149;
--accent-yellow: #d29922;
--accent-blue: #58a6ff;
--accent-purple: #bc8cff;
--border-color: #30363d;
}}
* {{
margin: 0;
padding: 0;
box-sizing: border-box;
}}
body {{
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
background-color: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
padding: 20px;
}}
.container {{
max-width: 1200px;
margin: 0 auto;
}}
header {{
background: linear-gradient(135deg, var(--bg-secondary), var(--bg-tertiary));
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 30px;
margin-bottom: 30px;
}}
header h1 {{
color: var(--accent-red);
font-size: 2em;
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 15px;
}}
header h1::before {{
content: '';
display: inline-block;
width: 40px;
height: 40px;
background: var(--accent-red);
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z'/%3E%3C/svg%3E");
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z'/%3E%3C/svg%3E");
}}
.meta {{
color: var(--text-secondary);
font-size: 0.9em;
}}
.meta span {{
margin-right: 20px;
}}
.stats {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
margin: 30px 0;
}}
.stat-card {{
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 20px;
text-align: center;
}}
.stat-card .number {{
font-size: 2.5em;
font-weight: bold;
color: var(--accent-green);
}}
.stat-card .label {{
color: var(--text-secondary);
font-size: 0.9em;
}}
.stat-card.warning .number {{
color: var(--accent-yellow);
}}
.stat-card.info .number {{
color: var(--accent-blue);
}}
section {{
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 25px;
margin-bottom: 25px;
}}
section h2 {{
color: var(--accent-blue);
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid var(--border-color);
}}
table {{
width: 100%;
border-collapse: collapse;
}}
th, td {{
padding: 12px;
text-align: left;
border-bottom: 1px solid var(--border-color);
}}
th {{
background: var(--bg-tertiary);
color: var(--text-secondary);
font-weight: 600;
text-transform: uppercase;
font-size: 0.85em;
}}
tr:hover {{
background: var(--bg-tertiary);
}}
a {{
color: var(--accent-blue);
text-decoration: none;
}}
a:hover {{
text-decoration: underline;
}}
.confidence {{
display: inline-block;
padding: 2px 8px;
border-radius: 12px;
font-size: 0.85em;
font-weight: 600;
}}
.confidence.high {{
background: rgba(63, 185, 80, 0.2);
color: var(--accent-green);
}}
.confidence.medium {{
background: rgba(210, 153, 34, 0.2);
color: var(--accent-yellow);
}}
.confidence.low {{
background: rgba(248, 81, 73, 0.2);
color: var(--accent-red);
}}
.category-tag {{
display: inline-block;
padding: 2px 8px;
background: var(--bg-tertiary);
border-radius: 4px;
font-size: 0.8em;
color: var(--text-secondary);
}}
.footer {{
text-align: center;
padding: 20px;
color: var(--text-secondary);
font-size: 0.85em;
}}
.nsfw-warning {{
background: rgba(248, 81, 73, 0.1);
border: 1px solid var(--accent-red);
color: var(--accent-red);
padding: 10px 15px;
border-radius: 8px;
margin-bottom: 15px;
}}
.severity-critical {{
background: rgba(248, 81, 73, 0.2);
color: #f85149;
padding: 2px 8px;
border-radius: 12px;
font-weight: 600;
font-size: 0.85em;
}}
.severity-high {{
background: rgba(255, 100, 60, 0.2);
color: #ff6a3d;
padding: 2px 8px;
border-radius: 12px;
font-weight: 600;
font-size: 0.85em;
}}
.severity-medium {{
background: rgba(210, 153, 34, 0.2);
color: #d29922;
padding: 2px 8px;
border-radius: 12px;
font-weight: 600;
font-size: 0.85em;
}}
.severity-low {{
background: rgba(88, 166, 255, 0.2);
color: #58a6ff;
padding: 2px 8px;
border-radius: 12px;
font-weight: 600;
font-size: 0.85em;
}}
.score-gauge {{
width: 100%;
height: 30px;
background: var(--bg-tertiary);
border-radius: 15px;
overflow: hidden;
margin: 10px 0;
}}
.score-gauge .fill {{
height: 100%;
border-radius: 15px;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 0.9em;
}}
</style>
</head>
<body>
<div class="container">
{content}
</div>
</body>
</html>'''
def generate_username_report(
self,
username: str,
results: List[Dict],
total_checked: int,
scan_time: float = 0
) -> str:
"""Generate HTML report for username scan.
Args:
username: The username that was scanned.
results: List of found profile dictionaries.
total_checked: Total sites checked.
scan_time: Total scan time in seconds.
Returns:
Path to generated report file.
"""
# Categorize results
high_conf = [r for r in results if r.get('confidence', 0) >= 80 and r.get('status') != 'restricted']
med_conf = [r for r in results if 60 <= r.get('confidence', 0) < 80 and r.get('status') != 'restricted']
low_conf = [r for r in results if r.get('confidence', 0) < 60 and r.get('status') != 'restricted']
restricted = [r for r in results if r.get('status') == 'restricted']
# Group by category
by_category = {}
for r in results:
if r.get('status') != 'restricted' and r.get('confidence', 0) >= 60:
cat = r.get('category', 'other')
if cat not in by_category:
by_category[cat] = []
by_category[cat].append(r)
# Build stats section
stats_html = f'''
<div class="stats">
<div class="stat-card">
<div class="number">{total_checked}</div>
<div class="label">Sites Checked</div>
</div>
<div class="stat-card">
<div class="number">{len(results)}</div>
<div class="label">Total Found</div>
</div>
<div class="stat-card">
<div class="number">{len(high_conf)}</div>
<div class="label">High Confidence</div>
</div>
<div class="stat-card info">
<div class="number">{len(med_conf)}</div>
<div class="label">Medium Confidence</div>
</div>
<div class="stat-card warning">
<div class="number">{len(restricted)}</div>
<div class="label">Restricted</div>
</div>
</div>
'''
# Build results table
def get_confidence_class(conf):
if conf >= 80:
return 'high'
elif conf >= 60:
return 'medium'
return 'low'
confirmed_rows = ''
for r in sorted(high_conf + med_conf, key=lambda x: -x.get('confidence', 0)):
conf = r.get('confidence', 0)
conf_class = get_confidence_class(conf)
tracker_badge = ' <span style="color: var(--text-secondary);">[tracker]</span>' if r.get('is_tracker') else ''
confirmed_rows += f'''
<tr>
<td>{r.get('name', 'Unknown')}{tracker_badge}</td>
<td><a href="{r.get('url', '#')}" target="_blank">{r.get('url', '')}</a></td>
<td><span class="category-tag">{r.get('category', 'other')}</span></td>
<td><span class="confidence {conf_class}">{conf}%</span></td>
</tr>
'''
# Build category breakdown
category_rows = ''
for cat, items in sorted(by_category.items(), key=lambda x: -len(x[1])):
category_rows += f'''
<tr>
<td>{cat}</td>
<td>{len(items)}</td>
</tr>
'''
# Restricted section
restricted_rows = ''
for r in restricted[:30]:
restricted_rows += f'''
<tr>
<td>{r.get('name', 'Unknown')}</td>
<td><a href="{r.get('url', '#')}" target="_blank">{r.get('url', '')}</a></td>
<td><span class="category-tag">{r.get('category', 'other')}</span></td>
<td><span class="confidence low">Restricted</span></td>
</tr>
'''
# Build full content
content = f'''
<header>
<h1>AUTARCH Username Report</h1>
<div class="meta">
<span><strong>Target:</strong> {username}</span>
<span><strong>Date:</strong> {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</span>
<span><strong>Scan Time:</strong> {scan_time:.1f}s</span>
</div>
</header>
{stats_html}
<section>
<h2>Confirmed Profiles ({len(high_conf) + len(med_conf)})</h2>
<table>
<thead>
<tr>
<th>Site</th>
<th>URL</th>
<th>Category</th>
<th>Confidence</th>
</tr>
</thead>
<tbody>
{confirmed_rows if confirmed_rows else '<tr><td colspan="4" style="text-align: center; color: var(--text-secondary);">No confirmed profiles found</td></tr>'}
</tbody>
</table>
</section>
<section>
<h2>By Category</h2>
<table>
<thead>
<tr>
<th>Category</th>
<th>Count</th>
</tr>
</thead>
<tbody>
{category_rows if category_rows else '<tr><td colspan="2" style="text-align: center; color: var(--text-secondary);">No categories</td></tr>'}
</tbody>
</table>
</section>
<section>
<h2>Restricted Access ({len(restricted)})</h2>
<p style="color: var(--text-secondary); margin-bottom: 15px;">
These sites returned 403/401 errors - the profile may exist but requires authentication.
</p>
<table>
<thead>
<tr>
<th>Site</th>
<th>URL</th>
<th>Category</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{restricted_rows if restricted_rows else '<tr><td colspan="4" style="text-align: center; color: var(--text-secondary);">None</td></tr>'}
</tbody>
</table>
</section>
<div class="footer">
<p>Generated by AUTARCH Framework - darkHal Security Group</p>
<p>Report generated at {datetime.now().strftime('%Y-%m-%d %H:%M:%S UTC')}</p>
</div>
'''
# Generate HTML
html = self._get_html_template().format(
title=f"AUTARCH Report - {username}",
content=content
)
# Save report
filename = f"{username}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.html"
filepath = self.output_dir / filename
with open(filepath, 'w', encoding='utf-8') as f:
f.write(html)
return str(filepath)
def generate_geoip_report(self, results: List[Dict]) -> str:
"""Generate HTML report for GEO IP lookups.
Args:
results: List of GEO IP lookup result dictionaries.
Returns:
Path to generated report file.
"""
rows = ''
for r in results:
if 'error' in r:
rows += f'''
<tr>
<td>{r.get('target', 'Unknown')}</td>
<td colspan="5" style="color: var(--accent-red);">Error: {r['error']}</td>
</tr>
'''
else:
map_link = f'<a href="{r.get("map_osm", "#")}" target="_blank">View Map</a>' if r.get('map_osm') else '-'
rows += f'''
<tr>
<td>{r.get('target', '-')}</td>
<td>{r.get('ipv4', '-')}</td>
<td>{r.get('country_code', '-')}</td>
<td>{r.get('region', '-')}</td>
<td>{r.get('city', '-')}</td>
<td>{r.get('isp', '-')}</td>
<td>{map_link}</td>
</tr>
'''
content = f'''
<header>
<h1>AUTARCH GEO IP Report</h1>
<div class="meta">
<span><strong>Date:</strong> {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</span>
<span><strong>Total Lookups:</strong> {len(results)}</span>
</div>
</header>
<section>
<h2>GEO IP Results</h2>
<table>
<thead>
<tr>
<th>Target</th>
<th>IPv4</th>
<th>Country</th>
<th>Region</th>
<th>City</th>
<th>ISP</th>
<th>Map</th>
</tr>
</thead>
<tbody>
{rows}
</tbody>
</table>
</section>
<div class="footer">
<p>Generated by AUTARCH Framework - darkHal Security Group</p>
</div>
'''
html = self._get_html_template().format(
title="AUTARCH GEO IP Report",
content=content
)
filename = f"geoip_{datetime.now().strftime('%Y%m%d_%H%M%S')}.html"
filepath = self.output_dir / filename
with open(filepath, 'w', encoding='utf-8') as f:
f.write(html)
return str(filepath)
def generate_security_audit_report(
self,
system_info: Dict,
issues: List[Dict],
score: int
) -> str:
"""Generate HTML report for security audit.
Args:
system_info: System information dictionary.
issues: List of security issues found.
score: Security score 0-100.
Returns:
Path to generated report file.
"""
# Score color
if score >= 80:
score_color = "var(--accent-green)"
elif score >= 60:
score_color = "var(--accent-yellow)"
else:
score_color = "var(--accent-red)"
# System info rows
sys_rows = ''
for key, val in system_info.items():
sys_rows += f'<tr><td><strong>{key}</strong></td><td>{val}</td></tr>\n'
# Score gauge
score_html = f'''
<div class="score-gauge">
<div class="fill" style="width: {score}%; background: {score_color}; color: var(--bg-primary);">
{score}/100
</div>
</div>
'''
# Issues by severity
severity_counts = {'CRITICAL': 0, 'HIGH': 0, 'MEDIUM': 0, 'LOW': 0}
for issue in issues:
sev = issue.get('severity', 'LOW').upper()
if sev in severity_counts:
severity_counts[sev] += 1
# Issues table
issue_rows = ''
for issue in sorted(issues, key=lambda x: ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'].index(x.get('severity', 'LOW').upper())):
sev = issue.get('severity', 'LOW').upper()
sev_class = f'severity-{sev.lower()}'
issue_rows += f'''
<tr>
<td><span class="{sev_class}">{sev}</span></td>
<td>{issue.get('title', '')}</td>
<td>{issue.get('description', '')}</td>
<td>{issue.get('recommendation', '')}</td>
</tr>
'''
content = f'''
<header>
<h1>Security Audit Report</h1>
<div class="meta">
<span><strong>Date:</strong> {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</span>
<span><strong>Issues Found:</strong> {len(issues)}</span>
</div>
</header>
<div class="stats">
<div class="stat-card">
<div class="number" style="color: {score_color};">{score}</div>
<div class="label">Security Score</div>
</div>
<div class="stat-card" style="border-left: 3px solid #f85149;">
<div class="number" style="color: #f85149;">{severity_counts['CRITICAL']}</div>
<div class="label">Critical</div>
</div>
<div class="stat-card" style="border-left: 3px solid #ff6a3d;">
<div class="number" style="color: #ff6a3d;">{severity_counts['HIGH']}</div>
<div class="label">High</div>
</div>
<div class="stat-card" style="border-left: 3px solid #d29922;">
<div class="number" style="color: #d29922;">{severity_counts['MEDIUM']}</div>
<div class="label">Medium</div>
</div>
<div class="stat-card" style="border-left: 3px solid #58a6ff;">
<div class="number" style="color: #58a6ff;">{severity_counts['LOW']}</div>
<div class="label">Low</div>
</div>
</div>
{score_html}
<section>
<h2>System Information</h2>
<table>
<thead><tr><th>Property</th><th>Value</th></tr></thead>
<tbody>{sys_rows}</tbody>
</table>
</section>
<section>
<h2>Security Issues ({len(issues)})</h2>
<table>
<thead>
<tr>
<th>Severity</th>
<th>Issue</th>
<th>Description</th>
<th>Recommendation</th>
</tr>
</thead>
<tbody>
{issue_rows if issue_rows else '<tr><td colspan="4" style="text-align: center; color: var(--text-secondary);">No issues found</td></tr>'}
</tbody>
</table>
</section>
<div class="footer">
<p>Generated by AUTARCH Framework - darkHal Security Group</p>
</div>
'''
html = self._get_html_template().format(
title="AUTARCH Security Audit Report",
content=content
)
filename = f"audit_{datetime.now().strftime('%Y%m%d_%H%M%S')}.html"
filepath = self.output_dir / filename
with open(filepath, 'w', encoding='utf-8') as f:
f.write(html)
return str(filepath)
def generate_network_scan_report(
self,
target: str,
hosts: List[Dict],
scan_time: float = 0
) -> str:
"""Generate HTML report for network scan.
Args:
target: Target subnet/IP.
hosts: List of host dictionaries with ports/services.
scan_time: Total scan time in seconds.
Returns:
Path to generated report file.
"""
total_ports = sum(len(h.get('ports', [])) for h in hosts)
all_services = set()
for h in hosts:
for p in h.get('ports', []):
all_services.add(p.get('service', 'unknown'))
# Host rows
host_rows = ''
for h in hosts:
ports_str = ', '.join(str(p.get('port', '')) for p in h.get('ports', []))
services_str = ', '.join(set(p.get('service', '') for p in h.get('ports', [])))
host_rows += f'''
<tr>
<td>{h.get('ip', '')}</td>
<td>{h.get('hostname', '-')}</td>
<td>{h.get('os_guess', '-')}</td>
<td>{ports_str or '-'}</td>
<td>{services_str or '-'}</td>
</tr>
'''
# Service distribution
svc_count = {}
for h in hosts:
for p in h.get('ports', []):
svc = p.get('service', 'unknown')
svc_count[svc] = svc_count.get(svc, 0) + 1
svc_rows = ''
for svc, count in sorted(svc_count.items(), key=lambda x: -x[1]):
svc_rows += f'<tr><td>{svc}</td><td>{count}</td></tr>\n'
content = f'''
<header>
<h1>Network Scan Report</h1>
<div class="meta">
<span><strong>Target:</strong> {target}</span>
<span><strong>Date:</strong> {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</span>
<span><strong>Scan Time:</strong> {scan_time:.1f}s</span>
</div>
</header>
<div class="stats">
<div class="stat-card">
<div class="number">{len(hosts)}</div>
<div class="label">Hosts Found</div>
</div>
<div class="stat-card info">
<div class="number">{total_ports}</div>
<div class="label">Open Ports</div>
</div>
<div class="stat-card warning">
<div class="number">{len(all_services)}</div>
<div class="label">Unique Services</div>
</div>
</div>
<section>
<h2>Host Map ({len(hosts)} hosts)</h2>
<table>
<thead>
<tr>
<th>IP Address</th>
<th>Hostname</th>
<th>OS</th>
<th>Open Ports</th>
<th>Services</th>
</tr>
</thead>
<tbody>
{host_rows if host_rows else '<tr><td colspan="5" style="text-align: center; color: var(--text-secondary);">No hosts found</td></tr>'}
</tbody>
</table>
</section>
<section>
<h2>Service Distribution</h2>
<table>
<thead><tr><th>Service</th><th>Count</th></tr></thead>
<tbody>{svc_rows}</tbody>
</table>
</section>
<div class="footer">
<p>Generated by AUTARCH Framework - darkHal Security Group</p>
</div>
'''
html = self._get_html_template().format(
title=f"AUTARCH Network Scan - {target}",
content=content
)
safe_target = target.replace('/', '_').replace('.', '-')
filename = f"network_{safe_target}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.html"
filepath = self.output_dir / filename
with open(filepath, 'w', encoding='utf-8') as f:
f.write(html)
return str(filepath)
def generate_vulnerability_report(
self,
target: str,
correlations: List[Dict],
scan_time: float = 0
) -> str:
"""Generate HTML report for vulnerability scan.
Args:
target: Target IP/hostname.
correlations: List of service-CVE correlation dicts.
scan_time: Total scan time in seconds.
Returns:
Path to generated report file.
"""
total_cves = 0
severity_counts = {'CRITICAL': 0, 'HIGH': 0, 'MEDIUM': 0, 'LOW': 0}
for corr in correlations:
for cve in corr.get('cves', []):
total_cves += 1
score = cve.get('cvss', 0)
if score >= 9.0:
severity_counts['CRITICAL'] += 1
elif score >= 7.0:
severity_counts['HIGH'] += 1
elif score >= 4.0:
severity_counts['MEDIUM'] += 1
else:
severity_counts['LOW'] += 1
# Per-service CVE sections
service_sections = ''
for corr in correlations:
svc = corr.get('service', {})
cves = corr.get('cves', [])
svc_label = f"{svc.get('service', 'unknown')}:{svc.get('version', '?')} on port {svc.get('port', '?')}"
cve_rows = ''
for cve in sorted(cves, key=lambda x: -x.get('cvss', 0)):
score = cve.get('cvss', 0)
if score >= 9.0:
sev, sev_class = 'CRITICAL', 'severity-critical'
elif score >= 7.0:
sev, sev_class = 'HIGH', 'severity-high'
elif score >= 4.0:
sev, sev_class = 'MEDIUM', 'severity-medium'
else:
sev, sev_class = 'LOW', 'severity-low'
cve_rows += f'''
<tr>
<td><a href="https://nvd.nist.gov/vuln/detail/{cve.get('id', '')}" target="_blank">{cve.get('id', '')}</a></td>
<td><span class="{sev_class}">{sev} ({score})</span></td>
<td>{cve.get('description', '')[:200]}</td>
</tr>
'''
service_sections += f'''
<section>
<h2>{svc_label} ({len(cves)} CVEs)</h2>
<table>
<thead><tr><th>CVE ID</th><th>Severity</th><th>Description</th></tr></thead>
<tbody>{cve_rows if cve_rows else '<tr><td colspan="3" style="text-align:center; color:var(--text-secondary);">No CVEs found</td></tr>'}</tbody>
</table>
</section>
'''
content = f'''
<header>
<h1>Vulnerability Report</h1>
<div class="meta">
<span><strong>Target:</strong> {target}</span>
<span><strong>Date:</strong> {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</span>
<span><strong>Scan Time:</strong> {scan_time:.1f}s</span>
</div>
</header>
<div class="stats">
<div class="stat-card">
<div class="number">{total_cves}</div>
<div class="label">Total CVEs</div>
</div>
<div class="stat-card" style="border-left: 3px solid #f85149;">
<div class="number" style="color: #f85149;">{severity_counts['CRITICAL']}</div>
<div class="label">Critical</div>
</div>
<div class="stat-card" style="border-left: 3px solid #ff6a3d;">
<div class="number" style="color: #ff6a3d;">{severity_counts['HIGH']}</div>
<div class="label">High</div>
</div>
<div class="stat-card" style="border-left: 3px solid #d29922;">
<div class="number" style="color: #d29922;">{severity_counts['MEDIUM']}</div>
<div class="label">Medium</div>
</div>
<div class="stat-card" style="border-left: 3px solid #58a6ff;">
<div class="number" style="color: #58a6ff;">{severity_counts['LOW']}</div>
<div class="label">Low</div>
</div>
</div>
{service_sections}
<div class="footer">
<p>Generated by AUTARCH Framework - darkHal Security Group</p>
</div>
'''
html = self._get_html_template().format(
title=f"AUTARCH Vulnerability Report - {target}",
content=content
)
safe_target = target.replace('/', '_').replace('.', '-')
filename = f"vulns_{safe_target}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.html"
filepath = self.output_dir / filename
with open(filepath, 'w', encoding='utf-8') as f:
f.write(html)
return str(filepath)
def generate_pentest_report(
self,
target: str,
network_data: Optional[List[Dict]] = None,
vuln_data: Optional[List[Dict]] = None,
exploit_data: Optional[List[Dict]] = None,
audit_data: Optional[Dict] = None
) -> str:
"""Generate combined pentest report.
Args:
target: Target IP/hostname.
network_data: Network map host list (optional).
vuln_data: Vulnerability correlations (optional).
exploit_data: Exploit suggestions (optional).
audit_data: Security audit data with 'system_info', 'issues', 'score' (optional).
Returns:
Path to generated report file.
"""
sections_html = ''
# Executive summary
summary_items = []
if network_data:
summary_items.append(f"<li>{len(network_data)} hosts discovered</li>")
if vuln_data:
total_cves = sum(len(c.get('cves', [])) for c in vuln_data)
summary_items.append(f"<li>{total_cves} vulnerabilities identified across {len(vuln_data)} services</li>")
if exploit_data:
summary_items.append(f"<li>{len(exploit_data)} potential exploit paths identified</li>")
if audit_data:
summary_items.append(f"<li>Security score: {audit_data.get('score', 'N/A')}/100</li>")
sections_html += f'''
<section>
<h2>Executive Summary</h2>
<ul style="list-style: disc; padding-left: 20px; line-height: 2;">
{''.join(summary_items) if summary_items else '<li>No data collected</li>'}
</ul>
</section>
'''
# Network map section
if network_data:
net_rows = ''
for h in network_data:
ports_str = ', '.join(str(p.get('port', '')) for p in h.get('ports', []))
services_str = ', '.join(set(p.get('service', '') for p in h.get('ports', [])))
net_rows += f'''
<tr>
<td>{h.get('ip', '')}</td>
<td>{h.get('hostname', '-')}</td>
<td>{h.get('os_guess', '-')}</td>
<td>{ports_str or '-'}</td>
<td>{services_str or '-'}</td>
</tr>
'''
sections_html += f'''
<section>
<h2>Network Map ({len(network_data)} hosts)</h2>
<table>
<thead><tr><th>IP</th><th>Hostname</th><th>OS</th><th>Ports</th><th>Services</th></tr></thead>
<tbody>{net_rows}</tbody>
</table>
</section>
'''
# Vulnerabilities section
if vuln_data:
vuln_rows = ''
for corr in vuln_data:
svc = corr.get('service', {})
for cve in sorted(corr.get('cves', []), key=lambda x: -x.get('cvss', 0)):
score = cve.get('cvss', 0)
if score >= 9.0:
sev, sev_class = 'CRITICAL', 'severity-critical'
elif score >= 7.0:
sev, sev_class = 'HIGH', 'severity-high'
elif score >= 4.0:
sev, sev_class = 'MEDIUM', 'severity-medium'
else:
sev, sev_class = 'LOW', 'severity-low'
vuln_rows += f'''
<tr>
<td>{svc.get('service', '')}:{svc.get('port', '')}</td>
<td><a href="https://nvd.nist.gov/vuln/detail/{cve.get('id', '')}" target="_blank">{cve.get('id', '')}</a></td>
<td><span class="{sev_class}">{sev} ({score})</span></td>
<td>{cve.get('description', '')[:150]}</td>
</tr>
'''
sections_html += f'''
<section>
<h2>Vulnerabilities</h2>
<table>
<thead><tr><th>Service</th><th>CVE</th><th>Severity</th><th>Description</th></tr></thead>
<tbody>{vuln_rows}</tbody>
</table>
</section>
'''
# Exploit suggestions section
if exploit_data:
exploit_rows = ''
for i, exp in enumerate(exploit_data, 1):
exploit_rows += f'''
<tr>
<td>{i}</td>
<td><code>{exp.get('module', '')}</code></td>
<td>{exp.get('target', '')}</td>
<td>{exp.get('cve', '-')}</td>
<td>{exp.get('reasoning', '')}</td>
</tr>
'''
sections_html += f'''
<section>
<h2>Exploit Suggestions ({len(exploit_data)})</h2>
<table>
<thead><tr><th>#</th><th>Module</th><th>Target</th><th>CVE</th><th>Reasoning</th></tr></thead>
<tbody>{exploit_rows}</tbody>
</table>
</section>
'''
# Security audit section
if audit_data:
score = audit_data.get('score', 0)
if score >= 80:
score_color = "var(--accent-green)"
elif score >= 60:
score_color = "var(--accent-yellow)"
else:
score_color = "var(--accent-red)"
audit_issue_rows = ''
for issue in audit_data.get('issues', []):
sev = issue.get('severity', 'LOW').upper()
sev_class = f'severity-{sev.lower()}'
audit_issue_rows += f'''
<tr>
<td><span class="{sev_class}">{sev}</span></td>
<td>{issue.get('title', '')}</td>
<td>{issue.get('description', '')}</td>
</tr>
'''
sections_html += f'''
<section>
<h2>Security Audit (Score: {score}/100)</h2>
<div class="score-gauge">
<div class="fill" style="width: {score}%; background: {score_color}; color: var(--bg-primary);">
{score}/100
</div>
</div>
<table style="margin-top: 15px;">
<thead><tr><th>Severity</th><th>Issue</th><th>Description</th></tr></thead>
<tbody>{audit_issue_rows if audit_issue_rows else '<tr><td colspan="3" style="text-align:center; color:var(--text-secondary);">No issues</td></tr>'}</tbody>
</table>
</section>
'''
content = f'''
<header>
<h1>Penetration Test Report</h1>
<div class="meta">
<span><strong>Target:</strong> {target}</span>
<span><strong>Date:</strong> {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</span>
</div>
</header>
{sections_html}
<div class="footer">
<p>Generated by AUTARCH Framework - darkHal Security Group</p>
</div>
'''
html = self._get_html_template().format(
title=f"AUTARCH Pentest Report - {target}",
content=content
)
safe_target = target.replace('/', '_').replace('.', '-')
filename = f"pentest_{safe_target}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.html"
filepath = self.output_dir / filename
with open(filepath, 'w', encoding='utf-8') as f:
f.write(html)
return str(filepath)
def get_report_generator(output_dir: str = None) -> ReportGenerator:
"""Get a ReportGenerator instance.
Args:
output_dir: Optional output directory.
Returns:
ReportGenerator instance.
"""
return ReportGenerator(output_dir)