remove infra.md.example, infra.md is the source of truth

This commit is contained in:
Azreen Jamal
2026-03-03 03:06:13 +08:00
parent 1ad3033cc1
commit a3c6d09350
86 changed files with 17093 additions and 39 deletions

View File

@@ -0,0 +1,535 @@
"""Report generator for AYN Antivirus.
Produces scan reports in plain-text, JSON, and HTML formats from
:class:`ScanResult` / :class:`FullScanResult` dataclasses.
"""
from __future__ import annotations
import html as html_mod
import json
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional, Union
from ayn_antivirus import __version__
from ayn_antivirus.core.engine import (
FullScanResult,
ScanResult,
ThreatInfo,
)
from ayn_antivirus.utils.helpers import format_duration, format_size, get_system_info
# Type alias for either result kind.
AnyResult = Union[ScanResult, FullScanResult]
class ReportGenerator:
"""Create scan reports in multiple output formats."""
# ------------------------------------------------------------------
# Plain text
# ------------------------------------------------------------------
@staticmethod
def generate_text(result: AnyResult) -> str:
"""Render a human-readable plain-text report."""
threats, meta = _extract(result)
lines: List[str] = []
lines.append("=" * 72)
lines.append(" AYN ANTIVIRUS — SCAN REPORT")
lines.append("=" * 72)
lines.append("")
lines.append(f" Generated : {datetime.utcnow().isoformat()}")
lines.append(f" Version : {__version__}")
lines.append(f" Scan ID : {meta.get('scan_id', 'N/A')}")
lines.append(f" Scan Type : {meta.get('scan_type', 'N/A')}")
lines.append(f" Duration : {format_duration(meta.get('duration', 0))}")
lines.append("")
# Summary.
sev_counts = _severity_counts(threats)
lines.append("-" * 72)
lines.append(" SUMMARY")
lines.append("-" * 72)
lines.append(f" Files scanned : {meta.get('files_scanned', 0)}")
lines.append(f" Files skipped : {meta.get('files_skipped', 0)}")
lines.append(f" Threats found : {len(threats)}")
lines.append(f" CRITICAL : {sev_counts.get('CRITICAL', 0)}")
lines.append(f" HIGH : {sev_counts.get('HIGH', 0)}")
lines.append(f" MEDIUM : {sev_counts.get('MEDIUM', 0)}")
lines.append(f" LOW : {sev_counts.get('LOW', 0)}")
lines.append("")
# Threat table.
if threats:
lines.append("-" * 72)
lines.append(" THREATS")
lines.append("-" * 72)
hdr = f" {'#':>3} {'Severity':<10} {'Threat Name':<30} {'File'}"
lines.append(hdr)
lines.append(" " + "-" * 68)
for idx, t in enumerate(threats, 1):
sev = _sev_str(t)
name = t.threat_name[:30]
fpath = t.path[:60]
lines.append(f" {idx:>3} {sev:<10} {name:<30} {fpath}")
lines.append("")
# System info.
try:
info = get_system_info()
lines.append("-" * 72)
lines.append(" SYSTEM INFORMATION")
lines.append("-" * 72)
lines.append(f" Hostname : {info['hostname']}")
lines.append(f" OS : {info['os_pretty']}")
lines.append(f" CPUs : {info['cpu_count']}")
lines.append(f" Memory : {info['memory_total_human']}")
lines.append(f" Uptime : {info['uptime_human']}")
lines.append("")
except Exception:
pass
lines.append("=" * 72)
lines.append(f" Report generated by AYN Antivirus v{__version__}")
lines.append("=" * 72)
return "\n".join(lines) + "\n"
# ------------------------------------------------------------------
# JSON
# ------------------------------------------------------------------
@staticmethod
def generate_json(result: AnyResult) -> str:
"""Render a machine-readable JSON report."""
threats, meta = _extract(result)
sev_counts = _severity_counts(threats)
try:
sys_info = get_system_info()
except Exception:
sys_info = {}
report: Dict[str, Any] = {
"generator": f"ayn-antivirus v{__version__}",
"generated_at": datetime.utcnow().isoformat(),
"scan": {
"scan_id": meta.get("scan_id"),
"scan_type": meta.get("scan_type"),
"start_time": meta.get("start_time"),
"end_time": meta.get("end_time"),
"duration_seconds": meta.get("duration"),
"files_scanned": meta.get("files_scanned", 0),
"files_skipped": meta.get("files_skipped", 0),
},
"summary": {
"total_threats": len(threats),
"by_severity": sev_counts,
},
"threats": [
{
"path": t.path,
"threat_name": t.threat_name,
"threat_type": t.threat_type.name if hasattr(t.threat_type, "name") else str(t.threat_type),
"severity": _sev_str(t),
"detector": t.detector_name,
"details": t.details,
"file_hash": t.file_hash,
"timestamp": t.timestamp.isoformat() if hasattr(t.timestamp, "isoformat") else str(t.timestamp),
}
for t in threats
],
"system": sys_info,
}
return json.dumps(report, indent=2, default=str)
# ------------------------------------------------------------------
# HTML
# ------------------------------------------------------------------
@staticmethod
def generate_html(result: AnyResult) -> str:
"""Render a professional HTML report with dark-theme CSS."""
threats, meta = _extract(result)
sev_counts = _severity_counts(threats)
now = datetime.utcnow()
esc = html_mod.escape
try:
sys_info = get_system_info()
except Exception:
sys_info = {}
total_threats = len(threats)
status_class = "clean" if total_threats == 0 else "infected"
# --- Build threat table rows ---
threat_rows = []
for idx, t in enumerate(threats, 1):
sev = _sev_str(t)
sev_lower = sev.lower()
ttype = t.threat_type.name if hasattr(t.threat_type, "name") else str(t.threat_type)
threat_rows.append(
f"<tr>"
f'<td class="idx">{idx}</td>'
f"<td>{esc(t.path)}</td>"
f"<td>{esc(t.threat_name)}</td>"
f"<td>{esc(ttype)}</td>"
f'<td><span class="badge badge-{sev_lower}">{sev}</span></td>'
f"<td>{esc(t.detector_name)}</td>"
f'<td class="hash">{esc(t.file_hash[:16])}{"" if len(t.file_hash) > 16 else ""}</td>'
f"</tr>"
)
threat_table = "\n".join(threat_rows) if threat_rows else (
'<tr><td colspan="7" class="empty">No threats detected ✅</td></tr>'
)
# --- System info rows ---
sys_rows = ""
if sys_info:
sys_rows = (
f"<tr><td>Hostname</td><td>{esc(str(sys_info.get('hostname', '')))}</td></tr>"
f"<tr><td>Operating System</td><td>{esc(str(sys_info.get('os_pretty', '')))}</td></tr>"
f"<tr><td>Architecture</td><td>{esc(str(sys_info.get('architecture', '')))}</td></tr>"
f"<tr><td>CPUs</td><td>{sys_info.get('cpu_count', '?')}</td></tr>"
f"<tr><td>Memory</td><td>{esc(str(sys_info.get('memory_total_human', '')))}"
f" ({sys_info.get('memory_percent', '?')}% used)</td></tr>"
f"<tr><td>Uptime</td><td>{esc(str(sys_info.get('uptime_human', '')))}</td></tr>"
)
html = f"""\
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AYN Antivirus — Scan Report</title>
<style>
{_CSS}
</style>
</head>
<body>
<!-- Header -->
<header>
<div class="logo">⚔️ AYN ANTIVIRUS</div>
<div class="subtitle">Scan Report — {esc(now.strftime("%Y-%m-%d %H:%M:%S"))}</div>
</header>
<!-- Summary cards -->
<section class="cards">
<div class="card">
<div class="card-value">{meta.get("files_scanned", 0)}</div>
<div class="card-label">Files Scanned</div>
</div>
<div class="card card-{status_class}">
<div class="card-value">{total_threats}</div>
<div class="card-label">Threats Found</div>
</div>
<div class="card card-critical">
<div class="card-value">{sev_counts.get("CRITICAL", 0)}</div>
<div class="card-label">Critical</div>
</div>
<div class="card card-high">
<div class="card-value">{sev_counts.get("HIGH", 0)}</div>
<div class="card-label">High</div>
</div>
<div class="card card-medium">
<div class="card-value">{sev_counts.get("MEDIUM", 0)}</div>
<div class="card-label">Medium</div>
</div>
<div class="card card-low">
<div class="card-value">{sev_counts.get("LOW", 0)}</div>
<div class="card-label">Low</div>
</div>
</section>
<!-- Scan details -->
<section class="details">
<h2>Scan Details</h2>
<table class="info-table">
<tr><td>Scan ID</td><td>{esc(str(meta.get("scan_id", "N/A")))}</td></tr>
<tr><td>Scan Type</td><td>{esc(str(meta.get("scan_type", "N/A")))}</td></tr>
<tr><td>Duration</td><td>{esc(format_duration(meta.get("duration", 0)))}</td></tr>
<tr><td>Files Scanned</td><td>{meta.get("files_scanned", 0)}</td></tr>
<tr><td>Files Skipped</td><td>{meta.get("files_skipped", 0)}</td></tr>
</table>
</section>
<!-- Threat table -->
<section class="threats">
<h2>Threat Details</h2>
<table class="threat-table">
<thead>
<tr>
<th>#</th>
<th>File Path</th>
<th>Threat Name</th>
<th>Type</th>
<th>Severity</th>
<th>Detector</th>
<th>Hash</th>
</tr>
</thead>
<tbody>
{threat_table}
</tbody>
</table>
</section>
<!-- System info -->
<section class="system">
<h2>System Information</h2>
<table class="info-table">
{sys_rows}
</table>
</section>
<!-- Footer -->
<footer>
Generated by AYN Antivirus v{__version__} &mdash; {esc(now.isoformat())}
</footer>
</body>
</html>
"""
return html
# ------------------------------------------------------------------
# File output
# ------------------------------------------------------------------
@staticmethod
def save_report(content: str, filepath: str | Path) -> None:
"""Write *content* to *filepath*, creating parent dirs if needed."""
fp = Path(filepath)
fp.parent.mkdir(parents=True, exist_ok=True)
fp.write_text(content, encoding="utf-8")
# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------
def _extract(result: AnyResult) -> tuple:
"""Return ``(threats_list, meta_dict)`` from either result type."""
if isinstance(result, FullScanResult):
sr = result.file_scan
threats = list(sr.threats)
elif isinstance(result, ScanResult):
sr = result
threats = list(sr.threats)
else:
sr = result
threats = []
meta: Dict[str, Any] = {
"scan_id": getattr(sr, "scan_id", None),
"scan_type": sr.scan_type.value if hasattr(sr, "scan_type") else None,
"start_time": sr.start_time.isoformat() if hasattr(sr, "start_time") and sr.start_time else None,
"end_time": sr.end_time.isoformat() if hasattr(sr, "end_time") and sr.end_time else None,
"duration": sr.duration_seconds if hasattr(sr, "duration_seconds") else 0,
"files_scanned": getattr(sr, "files_scanned", 0),
"files_skipped": getattr(sr, "files_skipped", 0),
}
return threats, meta
def _sev_str(threat: ThreatInfo) -> str:
"""Return the severity as an uppercase string."""
sev = threat.severity
if hasattr(sev, "name"):
return sev.name
return str(sev).upper()
def _severity_counts(threats: List[ThreatInfo]) -> Dict[str, int]:
counts: Dict[str, int] = {"CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0}
for t in threats:
key = _sev_str(t)
counts[key] = counts.get(key, 0) + 1
return counts
# ---------------------------------------------------------------------------
# Embedded CSS (dark theme)
# ---------------------------------------------------------------------------
_CSS = """\
:root {
--bg: #0f1117;
--surface: #1a1d27;
--border: #2a2d3a;
--text: #e0e0e0;
--text-dim: #8b8fa3;
--accent: #00bcd4;
--critical: #ff1744;
--high: #ff9100;
--medium: #ffea00;
--low: #00e676;
--clean: #00e676;
--infected: #ff1744;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', 'Inter', system-ui, -apple-system, sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.6;
padding: 0;
}
header {
background: linear-gradient(135deg, #1a1d27 0%, #0d1117 100%);
border-bottom: 2px solid var(--accent);
text-align: center;
padding: 2rem 1rem;
}
header .logo {
font-size: 2rem;
font-weight: 800;
color: var(--accent);
letter-spacing: 0.1em;
}
header .subtitle {
color: var(--text-dim);
font-size: 0.95rem;
margin-top: 0.3rem;
}
section {
max-width: 1200px;
margin: 2rem auto;
padding: 0 1.5rem;
}
h2 {
color: var(--accent);
font-size: 1.25rem;
margin-bottom: 1rem;
border-bottom: 1px solid var(--border);
padding-bottom: 0.4rem;
}
/* Summary cards */
.cards {
display: flex;
flex-wrap: wrap;
gap: 1rem;
max-width: 1200px;
margin: 2rem auto;
padding: 0 1.5rem;
}
.card {
flex: 1 1 140px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 1.2rem 1rem;
text-align: center;
}
.card-value {
font-size: 2rem;
font-weight: 700;
color: var(--text);
}
.card-label {
color: var(--text-dim);
font-size: 0.85rem;
margin-top: 0.2rem;
}
.card-clean .card-value { color: var(--clean); }
.card-infected .card-value { color: var(--infected); }
.card-critical .card-value { color: var(--critical); }
.card-high .card-value { color: var(--high); }
.card-medium .card-value { color: var(--medium); }
.card-low .card-value { color: var(--low); }
/* Tables */
table {
width: 100%;
border-collapse: collapse;
}
.info-table td {
padding: 0.5rem 0.75rem;
border-bottom: 1px solid var(--border);
}
.info-table td:first-child {
color: var(--text-dim);
width: 180px;
font-weight: 600;
}
.threat-table {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
overflow: hidden;
}
.threat-table thead th {
background: #12141c;
color: var(--accent);
padding: 0.7rem 0.75rem;
text-align: left;
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.threat-table tbody td {
padding: 0.6rem 0.75rem;
border-bottom: 1px solid var(--border);
font-size: 0.9rem;
word-break: break-all;
}
.threat-table tbody tr:hover {
background: rgba(0, 188, 212, 0.06);
}
.threat-table .idx { color: var(--text-dim); width: 40px; }
.threat-table .hash { font-family: monospace; color: var(--text-dim); font-size: 0.8rem; }
.threat-table .empty { text-align: center; color: var(--clean); padding: 2rem; font-size: 1.1rem; }
/* Severity badges */
.badge {
display: inline-block;
padding: 0.15rem 0.6rem;
border-radius: 4px;
font-size: 0.78rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.badge-critical { background: rgba(255,23,68,0.15); color: var(--critical); border: 1px solid var(--critical); }
.badge-high { background: rgba(255,145,0,0.15); color: var(--high); border: 1px solid var(--high); }
.badge-medium { background: rgba(255,234,0,0.12); color: var(--medium); border: 1px solid var(--medium); }
.badge-low { background: rgba(0,230,118,0.12); color: var(--low); border: 1px solid var(--low); }
/* Footer */
footer {
text-align: center;
color: var(--text-dim);
font-size: 0.8rem;
padding: 2rem 1rem;
border-top: 1px solid var(--border);
margin-top: 3rem;
}
/* System info */
.system { margin-bottom: 2rem; }
"""