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

@@ -3,14 +3,13 @@ import logging
logging.basicConfig(level=logging.INFO)
# TODO:
# REPORT - make darkmode
# LOGGING - make better format
# TLS SCANNING
# TLS Version PROBE
# EMAIL
import time
from pathlib import Path
from dataclasses import asdict
from ipaddress import ip_address
from typing import Any, Dict, List, Set
@@ -50,33 +49,29 @@ def results_to_open_sets(
def build_reports(
scan_config: "ScanConfigFile",
discovered: Dict[str, Dict[str, Set[int]]],
) -> Dict[str, Dict[str, List[int]]]:
) -> GroupedReports:
"""
Create the per-IP delta structure using expected ports from `scan_config.scan_targets`
and discovered ports from `discovered`.
Build per-IP deltas and return a grouped, template-friendly result.
Output format:
{
ip: {
"unexpected_tcp": [...],
"missing_tcp": [...],
"unexpected_udp": [...],
"missing_udp": [...]
}
}
Returns:
GroupedReports:
- issues: hosts with any deltas (sorted by IP)
- expected: hosts with no deltas (sorted by IP)
- by_ip: mapping of ip -> HostReport for random access
Notes:
- If a host has no expected UDP ports in the config, `expected_udp` is empty here.
(This function reflects *expectations*, not what to scan. Your scan logic can still
choose 'top UDP ports' for those hosts.)
- The `discovered` dict is expected to use keys "tcp" / "udp" per host.
- Works with `scan_config.scan_targets` where each target has:
ip, expected_tcp (List[int]), expected_udp (List[int]).
- `discovered` is expected to be { ip: { "tcp": Set[int], "udp": Set[int] } }.
Lists are accepted and coerced to sets.
- Supports IPv4 and IPv6 sorting. Falls back to string compare if ip parsing fails.
"""
# Build `expected` from scan_config.scan_targets
# ---- 1) Build expectations from scan_config ----
expected: Dict[str, Dict[str, Set[int]]] = {}
cfg_targets = getattr(scan_config, "scan_targets", []) or []
for t in cfg_targets:
# Works whether ScanTarget is a dataclass or a dict-like object
# Support dataclass-like or dict-like objects
ip = getattr(t, "ip", None) if hasattr(t, "ip") else t.get("ip")
if not ip:
continue
@@ -92,38 +87,63 @@ def build_reports(
"expected_udp": exp_udp,
}
# Union of IPs present in either expectations or discoveries
# ---- 2) Union of IPs present in either expectations or discoveries ----
all_ips = set(expected.keys()) | set(discovered.keys())
reports: Dict[str, Dict[str, List[int]]] = {}
for ip in sorted(all_ips):
# Expected sets (default to empty sets if not present)
exp_tcp = expected.get(ip, {}).get("expected_tcp", set())
exp_udp = expected.get(ip, {}).get("expected_udp", set())
# ---- 3) Compute per-host deltas into HostReport objects ----
by_ip: Dict[str, HostReport] = {}
# Discovered sets (default to empty sets if not present)
for ip in all_ips:
# Expected sets (default to empty sets if not present)
exp_tcp: Set[int] = expected.get(ip, {}).get("expected_tcp", set()) or set()
exp_udp: Set[int] = expected.get(ip, {}).get("expected_udp", set()) or set()
# Discovered sets (default to empty sets if not present); coerce lists -> sets
disc_tcp = discovered.get(ip, {}).get("tcp", set()) or set()
disc_udp = discovered.get(ip, {}).get("udp", set()) or set()
# Ensure sets in case caller provided lists
if not isinstance(disc_tcp, set):
disc_tcp = set(disc_tcp)
if not isinstance(disc_udp, set):
disc_udp = set(disc_udp)
reports[ip] = {
"unexpected_tcp": sorted(disc_tcp - exp_tcp),
"missing_tcp": sorted(exp_tcp - disc_tcp),
"unexpected_udp": sorted(disc_udp - exp_udp),
"missing_udp": sorted(exp_udp - disc_udp),
}
hr = HostReport(
ip=ip,
unexpected_tcp=sorted(disc_tcp - exp_tcp),
missing_tcp=sorted(exp_tcp - disc_tcp),
unexpected_udp=sorted(disc_udp - exp_udp),
missing_udp=sorted(exp_udp - disc_udp),
)
by_ip[ip] = hr
return reports
# ---- 4) Split into issues vs expected ----
issues: List[HostReport] = []
expected_clean: List[HostReport] = []
for hr in by_ip.values():
if hr.has_issues():
issues.append(hr)
else:
expected_clean.append(hr)
# ---- 5) Sort both lists by numeric IP (IPv4/IPv6); fallback to string ----
def ip_sort_key(hr: HostReport):
try:
return ip_address(hr.ip)
except ValueError:
return hr.ip # non-IP strings (unlikely) fall back to lexical
issues.sort(key=ip_sort_key)
expected_clean.sort(key=ip_sort_key)
return GroupedReports(
issues=issues,
expected=expected_clean,
by_ip=by_ip,
)
def run_repo_scan(scan_config:ScanConfigFile):
logger.info(f"Starting scan for {scan_config.name}")
logger.info(f"Options: udp={scan_config.scan_options.udp_scan} tls_sec={scan_config.scan_options.tls_security_scan} tls_exp={scan_config.scan_options.tls_exp_check}",)
logger.info(f"Reporting Options: Dark Mode: {scan_config.reporting.dark_mode}")
logger.info("Targets: %d hosts", len(scan_config.scan_targets))
# tack the filename on the end of our data path
@@ -137,7 +157,13 @@ def run_repo_scan(scan_config:ScanConfigFile):
if scan_config.reporting.dark_mode:
template = DARK_TEMPLATE
if scan_config.reporting.full_details:
show_only_issues = False
else:
show_only_issues = True
logger.info(f"Reporting Template Set to: {template}")
logger.info(f"Reporting Only Issues: {show_only_issues}")
scanner = nmap_scanner(scan_config)
scan_results = scanner.scan_targets()
@@ -150,7 +176,7 @@ def run_repo_scan(scan_config:ScanConfigFile):
out_path=file_out_path,
title=scan_config.reporting.report_name,
template_name=template,
only_issues=True)
only_issues=show_only_issues)
scanner.cleanup()
def main():