94 lines
2.5 KiB
Python
94 lines
2.5 KiB
Python
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,
|
|
} |