from __future__ import annotations from dataclasses import dataclass, field, asdict from typing import Dict, List, Set, Optional, Any from ipaddress import ip_address @dataclass class PortFinding: """ A single discovered port on a host. protocol: 'tcp' or 'udp' state: 'open', 'closed', 'filtered', 'open|filtered', etc. service: optional nmap-reported service name (e.g., 'ssh', 'http') """ port: int protocol: str state: str service: Optional[str] = None @dataclass class HostResult: """ Results for a single host. address: IP address (e.g., '192.0.2.10') host: primary hostname if reported by nmap (may be None) ports: list of PortFinding instances for this host """ address: str host: Optional[str] = None ports: List[PortFinding] = field(default_factory=list) @dataclass class HostReport: """ Delta result for a single host. """ ip: str unexpected_tcp: List[int] missing_tcp: List[int] unexpected_udp: List[int] missing_udp: List[int] def has_issues(self) -> bool: """ Returns True if any delta list is non-empty. """ if self.unexpected_tcp: return True if self.missing_tcp: return True if self.unexpected_udp: return True if self.missing_udp: return True return False def to_dict(self) -> Dict[str, Any]: """ Convert to a plain dict for JSON/Jinja contexts. """ return asdict(self) @dataclass class GroupedReports: """ Final, template-friendly structure: - issues: list of HostReport with any deltas (sorted by IP) - expected: list of HostReport with no deltas (sorted by IP) - by_ip: mapping for random access if needed """ issues: List[HostReport] expected: List[HostReport] by_ip: Dict[str, HostReport] def to_context(self) -> Dict[str, Any]: """ Produce plain-dict context for Jinja render() if you prefer dicts. """ issues_dicts: List[Dict[str, Any]] = [] for hr in self.issues: issues_dicts.append(hr.to_dict()) expected_dicts: List[Dict[str, Any]] = [] for hr in self.expected: expected_dicts.append(hr.to_dict()) by_ip_dict: Dict[str, Dict[str, Any]] = {} for ip, hr in self.by_ip.items(): by_ip_dict[ip] = hr.to_dict() return { "issues": issues_dicts, "expected": expected_dicts, "by_ip": by_ip_dict, }