templates and data classes are done

This commit is contained in:
2025-10-21 21:57:43 -05:00
parent f394e268da
commit 68aa25993d
8 changed files with 407 additions and 328 deletions

View File

@@ -1,12 +1,14 @@
# utils/reporting_jinja.py
from __future__ import annotations
from datetime import datetime
from html import escape
from pathlib import Path
from typing import Dict, List
from typing import Any, Dict, List, Set, Union
from jinja2 import Environment, FileSystemLoader, select_autoescape
from utils.models import HostResult
from utils.models import HostResult, HostReport, GroupedReports # <-- add HostReport, GroupedReports
from ipaddress import ip_address
def fmt_ports(ports: List[int]) -> str:
if not ports:
@@ -45,8 +47,53 @@ def _env(templates_dir: Path) -> Environment:
env.globals.update(badge=badge, pill_state=pill_state, pill_proto=pill_proto, fmt_ports=fmt_ports)
return env
# --- NEW: compatibility shim so renderer can accept either shape ---
ReportsArg = Union[GroupedReports, Dict[str, Dict[str, List[int]]]]
def _coerce_to_grouped(reports: ReportsArg) -> GroupedReports:
"""
Accept either:
- GroupedReports (new, preferred), or
- legacy dict: { ip: {"unexpected_tcp":[], "missing_tcp":[], "unexpected_udp":[], "missing_udp":[] } }
and return a GroupedReports instance.
"""
if isinstance(reports, GroupedReports):
return reports
# Legacy dict -> build HostReport objects
by_ip: Dict[str, HostReport] = {}
for ip, d in reports.items():
hr = HostReport(
ip=str(ip),
unexpected_tcp=list(d.get("unexpected_tcp", []) or []),
missing_tcp=list(d.get("missing_tcp", []) or []),
unexpected_udp=list(d.get("unexpected_udp", []) or []),
missing_udp=list(d.get("missing_udp", []) or []),
)
by_ip[ip] = hr
# Split & sort by IP
issues: List[HostReport] = []
expected: List[HostReport] = []
for hr in by_ip.values():
(issues if hr.has_issues() else expected).append(hr)
def _ip_key(hr: HostReport):
try:
return ip_address(hr.ip)
except ValueError:
return hr.ip
issues.sort(key=_ip_key)
expected.sort(key=_ip_key)
return GroupedReports(issues=issues, expected=expected, by_ip=by_ip)
def render_html_report_jinja(
reports: Dict[str, Dict[str, List[int]]],
reports: ReportsArg,
host_results: List[HostResult],
templates_dir: Path,
template_name: str = "report.html.j2",
@@ -56,30 +103,46 @@ def render_html_report_jinja(
env = _env(templates_dir)
template = env.get_template(template_name)
total_hosts = len(reports)
hosts_with_issues = [ip for ip, r in reports.items() if any(r.values())]
ok_hosts = total_hosts - len(hosts_with_issues)
grouped = _coerce_to_grouped(reports)
# No filtering — we'll show all, but only_issues changes template behavior.
by_ip = {hr.address: hr for hr in host_results}
for hr in by_ip.values():
total_hosts = len(grouped.by_ip)
ok_hosts = len(grouped.expected)
hosts_with_issues = [hr.ip for hr in grouped.issues]
# Build a mapping of IP -> HostResult and sort port rows for stable output
by_ip_results = {hr.address: hr for hr in host_results}
for hr in by_ip_results.values():
if hr and hr.ports:
hr.ports.sort(key=lambda p: (p.protocol, p.port))
html = template.render(
title=title,
generated=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
# Summary bar
total_hosts=total_hosts,
ok_hosts=ok_hosts,
hosts_with_issues=hosts_with_issues,
# New grouped context for simple “issues first, expected later” loops
issues=grouped.issues, # list[HostReport]
expected=grouped.expected, # list[HostReport]
by_ip=grouped.by_ip, # dict[str, HostReport]
# Legacy context (kept for compatibility if your template still uses it)
reports=reports,
host_results_by_ip=by_ip,
# Host scan details
host_results_by_ip=by_ip_results,
# Existing behavior switch
only_issues=only_issues,
)
return html
def write_html_report_jinja(
reports: Dict[str, Dict[str, List[int]]],
reports: ReportsArg,
host_results: List[HostResult],
out_path: Path,
templates_dir: Path = Path("templates"),