Autarch/modules/report_engine.py
DigiJ 2322f69516 v2.2.0 — Full arsenal expansion: 16 new security modules
Add WiFi Audit, API Fuzzer, Cloud Scanner, Threat Intel, Log Correlator,
Steganography, Anti-Forensics, BLE Scanner, Forensics, RFID/NFC, Malware
Sandbox, Password Toolkit, Web Scanner, Report Engine, Net Mapper, and
C2 Framework. Each module includes CLI interface, Flask routes, and web
UI template. Also includes Go DNS server source + binary, IP Capture
service, SYN Flood, Gone Fishing mail server, and hack hijack modules
from v2.0 work.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 05:20:39 -08:00

500 lines
22 KiB
Python

"""AUTARCH Reporting Engine
Structured pentest report builder with findings, CVSS scoring, evidence,
and export to HTML/Markdown/JSON.
"""
DESCRIPTION = "Pentest report builder & exporter"
AUTHOR = "darkHal"
VERSION = "1.0"
CATEGORY = "analyze"
import os
import json
import time
import uuid
from pathlib import Path
from datetime import datetime, timezone
from typing import Dict, List, Optional, Any
from dataclasses import dataclass, field, asdict
import threading
try:
from core.paths import get_data_dir
except ImportError:
def get_data_dir():
return str(Path(__file__).parent.parent / 'data')
# ── Finding Severity & CVSS ──────────────────────────────────────────────────
SEVERITY_MAP = {
'critical': {'color': '#dc2626', 'score_range': '9.0-10.0', 'order': 0},
'high': {'color': '#ef4444', 'score_range': '7.0-8.9', 'order': 1},
'medium': {'color': '#f59e0b', 'score_range': '4.0-6.9', 'order': 2},
'low': {'color': '#22c55e', 'score_range': '0.1-3.9', 'order': 3},
'info': {'color': '#6366f1', 'score_range': '0.0', 'order': 4},
}
FINDING_TEMPLATES = [
{
'id': 'sqli',
'title': 'SQL Injection',
'severity': 'critical',
'cvss': 9.8,
'description': 'The application is vulnerable to SQL injection, allowing an attacker to manipulate database queries.',
'impact': 'Complete database compromise, data exfiltration, authentication bypass, potential remote code execution.',
'remediation': 'Use parameterized queries/prepared statements. Implement input validation and WAF rules.',
'references': ['OWASP Top 10: A03:2021', 'CWE-89'],
},
{
'id': 'xss',
'title': 'Cross-Site Scripting (XSS)',
'severity': 'high',
'cvss': 7.5,
'description': 'The application reflects user input without proper sanitization, enabling script injection.',
'impact': 'Session hijacking, credential theft, defacement, malware distribution.',
'remediation': 'Encode all output, implement Content-Security-Policy, use framework auto-escaping.',
'references': ['OWASP Top 10: A03:2021', 'CWE-79'],
},
{
'id': 'broken_auth',
'title': 'Broken Authentication',
'severity': 'critical',
'cvss': 9.1,
'description': 'Authentication mechanisms can be bypassed or abused to gain unauthorized access.',
'impact': 'Account takeover, privilege escalation, unauthorized data access.',
'remediation': 'Implement MFA, rate limiting, secure session management, strong password policies.',
'references': ['OWASP Top 10: A07:2021', 'CWE-287'],
},
{
'id': 'idor',
'title': 'Insecure Direct Object Reference (IDOR)',
'severity': 'high',
'cvss': 7.5,
'description': 'The application exposes internal object references that can be manipulated to access unauthorized resources.',
'impact': 'Unauthorized access to other users\' data, horizontal privilege escalation.',
'remediation': 'Implement proper access control checks, use indirect references.',
'references': ['OWASP Top 10: A01:2021', 'CWE-639'],
},
{
'id': 'missing_headers',
'title': 'Missing Security Headers',
'severity': 'low',
'cvss': 3.1,
'description': 'The application does not implement recommended security headers.',
'impact': 'Increased attack surface for clickjacking, MIME sniffing, and XSS attacks.',
'remediation': 'Implement CSP, X-Frame-Options, X-Content-Type-Options, HSTS headers.',
'references': ['OWASP Secure Headers Project'],
},
{
'id': 'weak_ssl',
'title': 'Weak SSL/TLS Configuration',
'severity': 'medium',
'cvss': 5.3,
'description': 'The server supports weak SSL/TLS protocols or cipher suites.',
'impact': 'Potential for traffic interception via downgrade attacks.',
'remediation': 'Disable TLS 1.0/1.1, remove weak ciphers, enable HSTS.',
'references': ['CWE-326', 'NIST SP 800-52'],
},
{
'id': 'info_disclosure',
'title': 'Information Disclosure',
'severity': 'medium',
'cvss': 5.0,
'description': 'The application reveals sensitive information such as server versions, stack traces, or internal paths.',
'impact': 'Aids attackers in fingerprinting and planning targeted attacks.',
'remediation': 'Remove version headers, disable debug modes, implement custom error pages.',
'references': ['CWE-200'],
},
{
'id': 'default_creds',
'title': 'Default Credentials',
'severity': 'critical',
'cvss': 9.8,
'description': 'The system uses default or well-known credentials that have not been changed.',
'impact': 'Complete system compromise with minimal effort.',
'remediation': 'Enforce password change on first login, remove default accounts.',
'references': ['CWE-798'],
},
{
'id': 'eternalblue',
'title': 'MS17-010 (EternalBlue)',
'severity': 'critical',
'cvss': 9.8,
'description': 'The target is vulnerable to the EternalBlue SMB exploit (MS17-010).',
'impact': 'Remote code execution with SYSTEM privileges, wormable exploit.',
'remediation': 'Apply Microsoft patch MS17-010, disable SMBv1.',
'references': ['CVE-2017-0144', 'MS17-010'],
},
{
'id': 'open_ports',
'title': 'Unnecessary Open Ports',
'severity': 'low',
'cvss': 3.0,
'description': 'The target exposes network services that are not required for operation.',
'impact': 'Increased attack surface, potential exploitation of exposed services.',
'remediation': 'Close unnecessary ports, implement firewall rules, use network segmentation.',
'references': ['CIS Benchmarks'],
},
]
# ── Report Engine ─────────────────────────────────────────────────────────────
class ReportEngine:
"""Pentest report builder with findings management and export."""
def __init__(self):
self._data_dir = os.path.join(get_data_dir(), 'reports')
os.makedirs(self._data_dir, exist_ok=True)
# ── Report CRUD ───────────────────────────────────────────────────────
def create_report(self, title: str, client: str = '',
scope: str = '', methodology: str = '') -> dict:
"""Create a new report."""
report_id = str(uuid.uuid4())[:8]
report = {
'id': report_id,
'title': title,
'client': client,
'scope': scope,
'methodology': methodology or 'OWASP Testing Guide v4.2 / PTES',
'executive_summary': '',
'findings': [],
'created_at': datetime.now(timezone.utc).isoformat(),
'updated_at': datetime.now(timezone.utc).isoformat(),
'status': 'draft',
'author': 'AUTARCH',
}
self._save_report(report)
return {'ok': True, 'report': report}
def get_report(self, report_id: str) -> Optional[dict]:
path = os.path.join(self._data_dir, f'{report_id}.json')
if not os.path.exists(path):
return None
with open(path, 'r') as f:
return json.load(f)
def update_report(self, report_id: str, updates: dict) -> dict:
report = self.get_report(report_id)
if not report:
return {'ok': False, 'error': 'Report not found'}
for k, v in updates.items():
if k in report and k not in ('id', 'created_at'):
report[k] = v
report['updated_at'] = datetime.now(timezone.utc).isoformat()
self._save_report(report)
return {'ok': True, 'report': report}
def delete_report(self, report_id: str) -> dict:
path = os.path.join(self._data_dir, f'{report_id}.json')
if os.path.exists(path):
os.remove(path)
return {'ok': True}
return {'ok': False, 'error': 'Report not found'}
def list_reports(self) -> List[dict]:
reports = []
for f in Path(self._data_dir).glob('*.json'):
try:
with open(f, 'r') as fh:
r = json.load(fh)
reports.append({
'id': r['id'],
'title': r['title'],
'client': r.get('client', ''),
'status': r.get('status', 'draft'),
'findings_count': len(r.get('findings', [])),
'created_at': r.get('created_at', ''),
'updated_at': r.get('updated_at', ''),
})
except Exception:
continue
reports.sort(key=lambda r: r.get('updated_at', ''), reverse=True)
return reports
# ── Finding Management ────────────────────────────────────────────────
def add_finding(self, report_id: str, finding: dict) -> dict:
report = self.get_report(report_id)
if not report:
return {'ok': False, 'error': 'Report not found'}
finding['id'] = str(uuid.uuid4())[:8]
finding.setdefault('severity', 'medium')
finding.setdefault('cvss', 5.0)
finding.setdefault('status', 'open')
finding.setdefault('evidence', [])
report['findings'].append(finding)
report['updated_at'] = datetime.now(timezone.utc).isoformat()
self._save_report(report)
return {'ok': True, 'finding': finding}
def update_finding(self, report_id: str, finding_id: str,
updates: dict) -> dict:
report = self.get_report(report_id)
if not report:
return {'ok': False, 'error': 'Report not found'}
for f in report['findings']:
if f['id'] == finding_id:
for k, v in updates.items():
if k != 'id':
f[k] = v
report['updated_at'] = datetime.now(timezone.utc).isoformat()
self._save_report(report)
return {'ok': True, 'finding': f}
return {'ok': False, 'error': 'Finding not found'}
def delete_finding(self, report_id: str, finding_id: str) -> dict:
report = self.get_report(report_id)
if not report:
return {'ok': False, 'error': 'Report not found'}
report['findings'] = [f for f in report['findings']
if f['id'] != finding_id]
report['updated_at'] = datetime.now(timezone.utc).isoformat()
self._save_report(report)
return {'ok': True}
def get_finding_templates(self) -> List[dict]:
return FINDING_TEMPLATES
# ── Export ────────────────────────────────────────────────────────────
def export_html(self, report_id: str) -> Optional[str]:
"""Export report as styled HTML."""
report = self.get_report(report_id)
if not report:
return None
findings_html = ''
sorted_findings = sorted(report.get('findings', []),
key=lambda f: SEVERITY_MAP.get(f.get('severity', 'info'), {}).get('order', 5))
for i, f in enumerate(sorted_findings, 1):
sev = f.get('severity', 'info')
color = SEVERITY_MAP.get(sev, {}).get('color', '#666')
findings_html += f'''
<div class="finding">
<h3>{i}. {_esc(f.get('title', 'Untitled'))}</h3>
<div class="finding-meta">
<span class="severity" style="background:{color}">{sev.upper()}</span>
<span>CVSS: {f.get('cvss', 'N/A')}</span>
<span>Status: {f.get('status', 'open')}</span>
</div>
<h4>Description</h4><p>{_esc(f.get('description', ''))}</p>
<h4>Impact</h4><p>{_esc(f.get('impact', ''))}</p>
<h4>Remediation</h4><p>{_esc(f.get('remediation', ''))}</p>
{'<h4>Evidence</h4><pre>' + _esc(chr(10).join(f.get('evidence', []))) + '</pre>' if f.get('evidence') else ''}
{'<h4>References</h4><ul>' + ''.join('<li>' + _esc(r) + '</li>' for r in f.get('references', [])) + '</ul>' if f.get('references') else ''}
</div>'''
# Summary stats
severity_counts = {}
for f in report.get('findings', []):
s = f.get('severity', 'info')
severity_counts[s] = severity_counts.get(s, 0) + 1
summary_html = '<div class="severity-summary">'
for sev in ['critical', 'high', 'medium', 'low', 'info']:
count = severity_counts.get(sev, 0)
color = SEVERITY_MAP.get(sev, {}).get('color', '#666')
summary_html += f'<div class="sev-box" style="border-color:{color}"><span class="sev-count" style="color:{color}">{count}</span><span class="sev-label">{sev.upper()}</span></div>'
summary_html += '</div>'
html = f'''<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>{_esc(report.get('title', 'Report'))}</title>
<style>
body{{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;max-width:900px;margin:0 auto;padding:40px;color:#1a1a2e;line-height:1.6}}
h1{{color:#0f1117;border-bottom:3px solid #6366f1;padding-bottom:10px}}
h2{{color:#333;margin-top:2rem}}
.meta{{color:#666;font-size:0.9rem;margin:1rem 0}}
.finding{{border:1px solid #ddd;border-radius:8px;padding:1.5rem;margin:1rem 0;page-break-inside:avoid}}
.finding h3{{margin-top:0;color:#1a1a2e}}
.finding h4{{color:#555;margin:1rem 0 0.3rem;font-size:0.95rem}}
.finding-meta{{display:flex;gap:1rem;margin:0.5rem 0}}
.severity{{color:#fff;padding:2px 10px;border-radius:4px;font-size:0.8rem;font-weight:700}}
pre{{background:#f5f5f5;padding:1rem;border-radius:4px;overflow-x:auto;font-size:0.85rem}}
.severity-summary{{display:flex;gap:1rem;margin:1.5rem 0}}
.sev-box{{border:2px solid;border-radius:8px;padding:0.75rem 1.5rem;text-align:center}}
.sev-count{{font-size:1.5rem;font-weight:700;display:block}}
.sev-label{{font-size:0.7rem;text-transform:uppercase;letter-spacing:0.05em}}
.footer{{margin-top:3rem;padding-top:1rem;border-top:1px solid #ddd;font-size:0.8rem;color:#999}}
</style></head><body>
<h1>{_esc(report.get('title', 'Penetration Test Report'))}</h1>
<div class="meta">
<div><strong>Client:</strong> {_esc(report.get('client', 'N/A'))}</div>
<div><strong>Date:</strong> {report.get('created_at', '')[:10]}</div>
<div><strong>Author:</strong> {_esc(report.get('author', 'AUTARCH'))}</div>
<div><strong>Status:</strong> {report.get('status', 'draft').upper()}</div>
</div>
<h2>Executive Summary</h2>
<p>{_esc(report.get('executive_summary', 'No executive summary provided.'))}</p>
<h2>Scope</h2>
<p>{_esc(report.get('scope', 'No scope defined.'))}</p>
<h2>Methodology</h2>
<p>{_esc(report.get('methodology', ''))}</p>
<h2>Findings Overview</h2>
{summary_html}
<h2>Detailed Findings</h2>
{findings_html if findings_html else '<p>No findings recorded.</p>'}
<div class="footer">
Generated by AUTARCH Security Platform — {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}
</div>
</body></html>'''
return html
def export_markdown(self, report_id: str) -> Optional[str]:
"""Export report as Markdown."""
report = self.get_report(report_id)
if not report:
return None
md = f"# {report.get('title', 'Report')}\n\n"
md += f"**Client:** {report.get('client', 'N/A')} \n"
md += f"**Date:** {report.get('created_at', '')[:10]} \n"
md += f"**Author:** {report.get('author', 'AUTARCH')} \n"
md += f"**Status:** {report.get('status', 'draft')} \n\n"
md += "## Executive Summary\n\n"
md += report.get('executive_summary', 'N/A') + "\n\n"
md += "## Scope\n\n"
md += report.get('scope', 'N/A') + "\n\n"
md += "## Findings\n\n"
sorted_findings = sorted(report.get('findings', []),
key=lambda f: SEVERITY_MAP.get(f.get('severity', 'info'), {}).get('order', 5))
for i, f in enumerate(sorted_findings, 1):
md += f"### {i}. [{f.get('severity', 'info').upper()}] {f.get('title', 'Untitled')}\n\n"
md += f"**CVSS:** {f.get('cvss', 'N/A')} | **Status:** {f.get('status', 'open')}\n\n"
md += f"**Description:** {f.get('description', '')}\n\n"
md += f"**Impact:** {f.get('impact', '')}\n\n"
md += f"**Remediation:** {f.get('remediation', '')}\n\n"
if f.get('evidence'):
md += "**Evidence:**\n```\n" + '\n'.join(f['evidence']) + "\n```\n\n"
if f.get('references'):
md += "**References:** " + ', '.join(f['references']) + "\n\n"
md += "---\n\n"
md += f"\n*Generated by AUTARCH — {datetime.now(timezone.utc).strftime('%Y-%m-%d')}*\n"
return md
def export_json(self, report_id: str) -> Optional[str]:
report = self.get_report(report_id)
if not report:
return None
return json.dumps(report, indent=2)
# ── Internal ──────────────────────────────────────────────────────────
def _save_report(self, report: dict):
path = os.path.join(self._data_dir, f'{report["id"]}.json')
with open(path, 'w') as f:
json.dump(report, f, indent=2)
def _esc(s: str) -> str:
return (s or '').replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
# ── Singleton ─────────────────────────────────────────────────────────────────
_instance = None
_lock = threading.Lock()
def get_report_engine() -> ReportEngine:
global _instance
if _instance is None:
with _lock:
if _instance is None:
_instance = ReportEngine()
return _instance
# ── CLI ───────────────────────────────────────────────────────────────────────
def run():
"""Interactive CLI for Reporting Engine."""
svc = get_report_engine()
while True:
print("\n╔═══════════════════════════════════════╗")
print("║ REPORTING ENGINE ║")
print("╠═══════════════════════════════════════╣")
print("║ 1 — List Reports ║")
print("║ 2 — Create Report ║")
print("║ 3 — Add Finding ║")
print("║ 4 — Export Report ║")
print("║ 5 — Finding Templates ║")
print("║ 0 — Back ║")
print("╚═══════════════════════════════════════╝")
choice = input("\n Select: ").strip()
if choice == '0':
break
elif choice == '1':
reports = svc.list_reports()
if not reports:
print("\n No reports.")
continue
for r in reports:
print(f" [{r['id']}] {r['title']}{r['findings_count']} findings "
f"({r['status']}) {r['updated_at'][:10]}")
elif choice == '2':
title = input(" Report title: ").strip()
client = input(" Client name: ").strip()
scope = input(" Scope: ").strip()
r = svc.create_report(title, client, scope)
print(f" Created report: {r['report']['id']}")
elif choice == '3':
rid = input(" Report ID: ").strip()
print(" Available templates:")
for i, t in enumerate(FINDING_TEMPLATES, 1):
print(f" {i}. [{t['severity'].upper()}] {t['title']}")
sel = input(" Template # (0 for custom): ").strip()
if sel and sel != '0':
idx = int(sel) - 1
if 0 <= idx < len(FINDING_TEMPLATES):
f = FINDING_TEMPLATES[idx].copy()
f.pop('id', None)
r = svc.add_finding(rid, f)
if r['ok']:
print(f" Added: {f['title']}")
else:
title = input(" Title: ").strip()
severity = input(" Severity (critical/high/medium/low/info): ").strip()
desc = input(" Description: ").strip()
r = svc.add_finding(rid, {'title': title, 'severity': severity,
'description': desc})
if r['ok']:
print(f" Added finding: {r['finding']['id']}")
elif choice == '4':
rid = input(" Report ID: ").strip()
fmt = input(" Format (html/markdown/json): ").strip() or 'html'
if fmt == 'html':
content = svc.export_html(rid)
elif fmt == 'markdown':
content = svc.export_markdown(rid)
else:
content = svc.export_json(rid)
if content:
ext = {'html': 'html', 'markdown': 'md', 'json': 'json'}.get(fmt, 'txt')
outpath = os.path.join(svc._data_dir, f'{rid}.{ext}')
with open(outpath, 'w') as f:
f.write(content)
print(f" Exported to: {outpath}")
else:
print(" Report not found.")
elif choice == '5':
for t in FINDING_TEMPLATES:
print(f" [{t['severity'].upper():8s}] {t['title']} (CVSS {t['cvss']})")