templates and data classes are done
This commit is contained in:
102
app/main.py
102
app/main.py
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user