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) logging.basicConfig(level=logging.INFO)
# TODO: # TODO:
# REPORT - make darkmode # LOGGING - make better format
# TLS SCANNING # TLS SCANNING
# TLS Version PROBE # TLS Version PROBE
# EMAIL # EMAIL
import time import time
from pathlib import Path from pathlib import Path
from dataclasses import asdict
from ipaddress import ip_address from ipaddress import ip_address
from typing import Any, Dict, List, Set from typing import Any, Dict, List, Set
@@ -50,33 +49,29 @@ def results_to_open_sets(
def build_reports( def build_reports(
scan_config: "ScanConfigFile", scan_config: "ScanConfigFile",
discovered: Dict[str, Dict[str, Set[int]]], 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` Build per-IP deltas and return a grouped, template-friendly result.
and discovered ports from `discovered`.
Output format: Returns:
{ GroupedReports:
ip: { - issues: hosts with any deltas (sorted by IP)
"unexpected_tcp": [...], - expected: hosts with no deltas (sorted by IP)
"missing_tcp": [...], - by_ip: mapping of ip -> HostReport for random access
"unexpected_udp": [...],
"missing_udp": [...]
}
}
Notes: Notes:
- If a host has no expected UDP ports in the config, `expected_udp` is empty here. - Works with `scan_config.scan_targets` where each target has:
(This function reflects *expectations*, not what to scan. Your scan logic can still ip, expected_tcp (List[int]), expected_udp (List[int]).
choose 'top UDP ports' for those hosts.) - `discovered` is expected to be { ip: { "tcp": Set[int], "udp": Set[int] } }.
- The `discovered` dict is expected to use keys "tcp" / "udp" per host. 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]]] = {} expected: Dict[str, Dict[str, Set[int]]] = {}
cfg_targets = getattr(scan_config, "scan_targets", []) or [] cfg_targets = getattr(scan_config, "scan_targets", []) or []
for t in cfg_targets: 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") ip = getattr(t, "ip", None) if hasattr(t, "ip") else t.get("ip")
if not ip: if not ip:
continue continue
@@ -92,38 +87,63 @@ def build_reports(
"expected_udp": exp_udp, "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()) all_ips = set(expected.keys()) | set(discovered.keys())
reports: Dict[str, Dict[str, List[int]]] = {} # ---- 3) Compute per-host deltas into HostReport objects ----
for ip in sorted(all_ips): by_ip: Dict[str, HostReport] = {}
# 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())
# 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_tcp = discovered.get(ip, {}).get("tcp", set()) or set()
disc_udp = discovered.get(ip, {}).get("udp", 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): if not isinstance(disc_tcp, set):
disc_tcp = set(disc_tcp) disc_tcp = set(disc_tcp)
if not isinstance(disc_udp, set): if not isinstance(disc_udp, set):
disc_udp = set(disc_udp) disc_udp = set(disc_udp)
reports[ip] = { hr = HostReport(
"unexpected_tcp": sorted(disc_tcp - exp_tcp), ip=ip,
"missing_tcp": sorted(exp_tcp - disc_tcp), unexpected_tcp=sorted(disc_tcp - exp_tcp),
"unexpected_udp": sorted(disc_udp - exp_udp), missing_tcp=sorted(exp_tcp - disc_tcp),
"missing_udp": sorted(exp_udp - disc_udp), 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): def run_repo_scan(scan_config:ScanConfigFile):
logger.info(f"Starting scan for {scan_config.name}") 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"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)) logger.info("Targets: %d hosts", len(scan_config.scan_targets))
# tack the filename on the end of our data path # 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: if scan_config.reporting.dark_mode:
template = DARK_TEMPLATE 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 Template Set to: {template}")
logger.info(f"Reporting Only Issues: {show_only_issues}")
scanner = nmap_scanner(scan_config) scanner = nmap_scanner(scan_config)
scan_results = scanner.scan_targets() scan_results = scanner.scan_targets()
@@ -150,7 +176,7 @@ def run_repo_scan(scan_config:ScanConfigFile):
out_path=file_out_path, out_path=file_out_path,
title=scan_config.reporting.report_name, title=scan_config.reporting.report_name,
template_name=template, template_name=template,
only_issues=True) only_issues=show_only_issues)
scanner.cleanup() scanner.cleanup()
def main(): def main():

View File

@@ -1,12 +1,14 @@
# utils/reporting_jinja.py
from __future__ import annotations from __future__ import annotations
from datetime import datetime from datetime import datetime
from html import escape from html import escape
from pathlib import Path 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 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: def fmt_ports(ports: List[int]) -> str:
if not ports: 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) env.globals.update(badge=badge, pill_state=pill_state, pill_proto=pill_proto, fmt_ports=fmt_ports)
return env 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( def render_html_report_jinja(
reports: Dict[str, Dict[str, List[int]]], reports: ReportsArg,
host_results: List[HostResult], host_results: List[HostResult],
templates_dir: Path, templates_dir: Path,
template_name: str = "report.html.j2", template_name: str = "report.html.j2",
@@ -56,30 +103,46 @@ def render_html_report_jinja(
env = _env(templates_dir) env = _env(templates_dir)
template = env.get_template(template_name) template = env.get_template(template_name)
total_hosts = len(reports) grouped = _coerce_to_grouped(reports)
hosts_with_issues = [ip for ip, r in reports.items() if any(r.values())]
ok_hosts = total_hosts - len(hosts_with_issues)
# No filtering — we'll show all, but only_issues changes template behavior. total_hosts = len(grouped.by_ip)
by_ip = {hr.address: hr for hr in host_results} ok_hosts = len(grouped.expected)
for hr in by_ip.values(): 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: if hr and hr.ports:
hr.ports.sort(key=lambda p: (p.protocol, p.port)) hr.ports.sort(key=lambda p: (p.protocol, p.port))
html = template.render( html = template.render(
title=title, title=title,
generated=datetime.now().strftime("%Y-%m-%d %H:%M:%S"), generated=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
# Summary bar
total_hosts=total_hosts, total_hosts=total_hosts,
ok_hosts=ok_hosts, ok_hosts=ok_hosts,
hosts_with_issues=hosts_with_issues, 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, 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, only_issues=only_issues,
) )
return html return html
def write_html_report_jinja( def write_html_report_jinja(
reports: Dict[str, Dict[str, List[int]]], reports: ReportsArg,
host_results: List[HostResult], host_results: List[HostResult],
out_path: Path, out_path: Path,
templates_dir: Path = Path("templates"), templates_dir: Path = Path("templates"),

View File

@@ -54,32 +54,9 @@
</div> </div>
{% endif %} {% endif %}
{# ===== Host Sections ===== #} {# ===== Host Sections (Issues first, then Expected) ===== #}
{% for ip_key, r in reports|dictsort %}
{% set has_issues = r.unexpected_tcp or r.missing_tcp or r.unexpected_udp or r.missing_udp %}
{% set hr = host_results_by_ip.get(ip_key) %}
{% set header_title = ip_key ~ ((' (' ~ hr.host ~ ')') if hr and hr.host else '') %}
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="margin:0 0 18px 0;border-collapse:separate;border-spacing:0">
<tr>
<td colspan="5" style="padding:12px 10px;background:#1e293b;color:#f1f5f9;font-weight:600;font-size:14px;border-radius:8px;border:1px solid #334155">
{{ header_title }}
{% if has_issues %}
{{ badge('ISSUES', '#ef4444') }}
{% else %}
{{ badge('OK', '#16a34a') }}
{% endif %}
</td>
</tr>
{% if has_issues %}
<tr>
<td colspan="5" style="padding:10px 10px 6px 10px;font-size:13px">
{{ badge('ISSUES', '#ef4444') }}
</td>
</tr>
{# A small macro reused for delta lines #}
{% macro delta_row(label, ports) -%} {% macro delta_row(label, ports) -%}
<tr> <tr>
<td colspan="5" style="padding:4px 10px 6px 10px;font-size:13px"> <td colspan="5" style="padding:4px 10px 6px 10px;font-size:13px">
@@ -89,6 +66,27 @@
</tr> </tr>
{%- endmacro %} {%- endmacro %}
{# ---------- 1) Issues (already sorted by IP in Python) ---------- #}
{% for hr in issues %}
{% set ip_key = hr.ip %}
{% set r = hr %} {# keep "r" alias so the rest of the block looks familiar #}
{% set has_issues = true %}
{% set host_row = host_results_by_ip.get(ip_key) %}
{% set header_title = ip_key ~ ((' (' ~ host_row.host ~ ')') if host_row and host_row.host else '') %}
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="margin:0 0 18px 0;border-collapse:separate;border-spacing:0">
<tr>
<td colspan="5" style="padding:12px 10px;background:#1e293b;color:#f1f5f9;font-weight:600;font-size:14px;border-radius:8px;border:1px solid #334155">
{{ header_title }} {{ badge('ISSUES', '#ef4444') }}
</td>
</tr>
<tr>
<td colspan="5" style="padding:10px 10px 6px 10px;font-size:13px">
{{ badge('ISSUES', '#ef4444') }}
</td>
</tr>
{{ delta_row('Unexpected TCP open ports', r.unexpected_tcp) }} {{ delta_row('Unexpected TCP open ports', r.unexpected_tcp) }}
{{ delta_row('Expected TCP ports not seen', r.missing_tcp) }} {{ delta_row('Expected TCP ports not seen', r.missing_tcp) }}
{{ delta_row('Unexpected UDP open ports', r.unexpected_udp) }} {{ delta_row('Unexpected UDP open ports', r.unexpected_udp) }}
@@ -107,19 +105,14 @@
<td style="padding:8px 10px;border:1px solid #1f2937;background:#111827;font-weight:600;color:#e5e7eb">Expectation</td> <td style="padding:8px 10px;border:1px solid #1f2937;background:#111827;font-weight:600;color:#e5e7eb">Expectation</td>
</tr> </tr>
{% if hr and hr.ports %} {% if host_row and host_row.ports %}
{# Track visible rows after filtering (e.g., skip closed UDP) #}
{% set ns = namespace(visible_rows=0) %} {% set ns = namespace(visible_rows=0) %}
{% for p in hr.ports %} {% for p in host_row.ports %}
{# --- Normalize helpers --- #}
{% set proto = (p.protocol|string|lower) %} {% set proto = (p.protocol|string|lower) %}
{% set state = (p.state|string|lower) %} {% set state = (p.state|string|lower) %}
{# --- Skip rule: hide UDP rows that are "closed" (substring match) --- #}
{% set skip_row = (proto == 'udp' and ('closed' in state)) %} {% set skip_row = (proto == 'udp' and ('closed' in state)) %}
{# --- Expectation labeling --- #}
{% set is_tcp = (proto == 'tcp') %} {% set is_tcp = (proto == 'tcp') %}
{% set is_udp = (proto == 'udp') %} {% set is_udp = (proto == 'udp') %}
{% set is_open = ('open' in state) %} {% set is_open = ('open' in state) %}
@@ -158,16 +151,34 @@
</td> </td>
</tr> </tr>
{% endif %} {% endif %}
{% else %} </table>
{# Host has no issues #} {% endfor %}
{# ---------- 2) Expected / OK hosts (only if not only_issues) ---------- #}
{% if not only_issues %}
{% for hr in expected %}
{% set ip_key = hr.ip %}
{% set r = hr %}
{% set has_issues = false %}
{% set host_row = host_results_by_ip.get(ip_key) %}
{% set header_title = ip_key ~ ((' (' ~ host_row.host ~ ')') if host_row and host_row.host else '') %}
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="margin:0 0 18px 0;border-collapse:separate;border-spacing:0">
<tr>
<td colspan="5" style="padding:12px 10px;background:#1e293b;color:#f1f5f9;font-weight:600;font-size:14px;border-radius:8px;border:1px solid #334155">
{{ header_title }} {{ badge('OK', '#16a34a') }}
</td>
</tr>
<tr> <tr>
<td colspan="5" style="padding:10px 10px 8px 10px;font-size:13px"> <td colspan="5" style="padding:10px 10px 8px 10px;font-size:13px">
{{ badge('OK', '#16a34a') }} &nbsp; <span style="color:#cbd5e1">Matches expected ports.</span> {{ badge('OK', '#16a34a') }} &nbsp; <span style="color:#cbd5e1">Matches expected ports.</span>
</td> </td>
</tr> </tr>
{% endif %}
</table> </table>
{% endfor %} {% endfor %}
{% endif %}
<div style="margin-top:18px;font-size:11px;color:#9ca3af"> <div style="margin-top:18px;font-size:11px;color:#9ca3af">
Report generated by mass-scan-v2 • {{ generated }} Report generated by mass-scan-v2 • {{ generated }}

View File

@@ -38,41 +38,44 @@
</div> </div>
{% endif %} {% endif %}
{# Host sections #} {# ===== Host Sections (LIGHT theme) ===== #}
{% for ip_key, r in reports|dictsort %}
{% set has_issues = r.unexpected_tcp or r.missing_tcp or r.unexpected_udp or r.missing_udp %}
{% set hr = host_results_by_ip.get(ip_key) %}
{% set header_title = ip_key ~ ((' (' ~ hr.host ~ ')') if hr and hr.host else '') %}
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="margin:0 0 18px 0">
<tr>
<td colspan="4" style="padding:12px 10px;background:#0f172a;color:#e2e8f0;font-weight:600;font-size:14px;border-radius:8px">
{{ header_title }} {% if has_issues %} {{ badge('ISSUES', '#ef4444') }} {% else %} {{ badge('OK', '#16a34a') }} {% endif %}
</td>
</tr>
{% if has_issues %}
<tr>
<td colspan="4" style="padding:10px 10px 6px 10px;font-size:13px">
{{ badge('ISSUES', '#ef4444') }}
</td>
</tr>
{# Reusable delta row #}
{% macro delta_row(label, ports) -%} {% macro delta_row(label, ports) -%}
<tr> <tr>
<td colspan="4" style="padding:4px 10px 6px 10px;font-size:13px"> <td colspan="5" style="padding:4px 10px 6px 10px;font-size:13px">
<strong>{{ label }}:</strong> {{ fmt_ports(ports) }} <strong>{{ label }}:</strong> {{ fmt_ports(ports) }}
</td> </td>
</tr> </tr>
{%- endmacro %} {%- endmacro %}
{# ---------- 1) Issues first (already IP-sorted in Python) ---------- #}
{% for hr in issues %}
{% set ip_key = hr.ip %}
{% set r = hr %}
{% set host_row = host_results_by_ip.get(ip_key) %}
{% set header_title = ip_key ~ ((' (' ~ host_row.host ~ ')') if host_row and host_row.host else '') %}
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="margin:0 0 18px 0">
<tr>
<td colspan="5" style="padding:12px 10px;background:#0f172a;color:#e2e8f0;font-weight:600;font-size:14px;border-radius:8px">
{{ header_title }} {{ badge('ISSUES', '#ef4444') }}
</td>
</tr>
<tr>
<td colspan="5" style="padding:10px 10px 6px 10px;font-size:13px">
{{ badge('ISSUES', '#ef4444') }}
</td>
</tr>
{{ delta_row('Unexpected TCP open ports', r.unexpected_tcp) }} {{ delta_row('Unexpected TCP open ports', r.unexpected_tcp) }}
{{ delta_row('Expected TCP ports not seen', r.missing_tcp) }} {{ delta_row('Expected TCP ports not seen', r.missing_tcp) }}
{{ delta_row('Unexpected UDP open ports', r.unexpected_udp) }} {{ delta_row('Unexpected UDP open ports', r.unexpected_udp) }}
{{ delta_row('Expected UDP ports not seen', r.missing_udp) }} {{ delta_row('Expected UDP ports not seen', r.missing_udp) }}
<tr> <tr>
<td colspan="4" style="padding:8px 10px 6px 10px;font-size:13px"> <td colspan="5" style="padding:8px 10px 6px 10px;font-size:13px">
<div style="font-weight:600;margin:8px 0">Discovered Ports</div> <div style="font-weight:600;margin:8px 0">Discovered Ports</div>
</td> </td>
</tr> </tr>
@@ -84,19 +87,14 @@
<td style="padding:8px 10px;border:1px solid #e5e7eb;background:#f8fafc;font-weight:600">Expectation</td> <td style="padding:8px 10px;border:1px solid #e5e7eb;background:#f8fafc;font-weight:600">Expectation</td>
</tr> </tr>
{% if hr and hr.ports %} {% if host_row and host_row.ports %}
{# Track whether we rendered any visible rows after filtering #}
{% set ns = namespace(visible_rows=0) %} {% set ns = namespace(visible_rows=0) %}
{% for p in hr.ports %} {% for p in host_row.ports %}
{# Normalize helpers #}
{% set proto = (p.protocol|string|lower) %} {% set proto = (p.protocol|string|lower) %}
{% set state = (p.state|string|lower) %} {% set state = (p.state|string|lower) %}
{# Skip rule: hide UDP rows that are 'closed' #}
{% set skip_row = (proto == 'udp' and ('closed' in state)) %} {% set skip_row = (proto == 'udp' and ('closed' in state)) %}
{# Compute expectation labeling (only matters for displayed rows) #}
{% set is_tcp = (proto == 'tcp') %} {% set is_tcp = (proto == 'tcp') %}
{% set is_udp = (proto == 'udp') %} {% set is_udp = (proto == 'udp') %}
{% set is_open = ('open' in state) %} {% set is_open = ('open' in state) %}
@@ -121,7 +119,6 @@
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{# If everything was filtered out, show a friendly note #}
{% if ns.visible_rows == 0 %} {% if ns.visible_rows == 0 %}
<tr> <tr>
<td colspan="5" style="padding:8px 10px;border:1px solid #e5e7eb;color:#64748b"> <td colspan="5" style="padding:8px 10px;border:1px solid #e5e7eb;color:#64748b">
@@ -136,18 +133,31 @@
</td> </td>
</tr> </tr>
{% endif %} {% endif %}
{% else %}
{# Host has no issues #}
<tr>
<td colspan="4" style="padding:10px 10px 8px 10px;font-size:13px">
{{ badge('OK', '#16a34a') }} &nbsp; Matches expected ports.
</td>
</tr>
{% endif %}
</table> </table>
{% endfor %} {% endfor %}
{# ---------- 2) Expected / OK (only if not only_issues) ---------- #}
{% if not only_issues %}
{% for hr in expected %}
{% set ip_key = hr.ip %}
{% set host_row = host_results_by_ip.get(ip_key) %}
{% set header_title = ip_key ~ ((' (' ~ host_row.host ~ ')') if host_row and host_row.host else '') %}
<div style="margin-top:18px;font-size:11px;color:#94a3b8"> <table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="margin:0 0 18px 0">
<tr>
<td colspan="5" style="padding:12px 10px;background:#0f172a;color:#e2e8f0;font-weight:600;font-size:14px;border-radius:8px">
{{ header_title }} {{ badge('OK', '#16a34a') }}
</td>
</tr>
<tr>
<td colspan="5" style="padding:10px 10px 8px 10px;font-size:13px">
{{ badge('OK', '#16a34a') }} &nbsp; Matches expected ports.
</td>
</tr>
</table>
{% endfor %}
{% endif %}
<div style="margin-top:18px;font-size:11px;color:#94a3b8">
Report generated by mass-scan-v2 • {{ generated }} Report generated by mass-scan-v2 • {{ generated }}
</div> </div>

View File

@@ -162,6 +162,7 @@ class ScanConfigRepository:
report_name=str(rep_raw.get("report_name", "Scan Report")), report_name=str(rep_raw.get("report_name", "Scan Report")),
report_filename=str(rep_raw.get("report_filename", "report.html")), report_filename=str(rep_raw.get("report_filename", "report.html")),
full_details=bool(rep_raw.get("full_details", False)), full_details=bool(rep_raw.get("full_details", False)),
dark_mode = bool(rep_raw.get("dark_mode", False)),
email_to=self._as_str_list(rep_raw.get("email_to", []), "email_to"), email_to=self._as_str_list(rep_raw.get("email_to", []), "email_to"),
email_cc=self._as_str_list(rep_raw.get("email_cc", []), "email_cc"), email_cc=self._as_str_list(rep_raw.get("email_cc", []), "email_cc"),
) )
@@ -236,7 +237,6 @@ class ScanConfigRepository:
raise TypeError(f"'{field_name}' must be a string or a list of strings.") raise TypeError(f"'{field_name}' must be a string or a list of strings.")
# helpers # helpers
def list_configs(self) -> List[str]: def list_configs(self) -> List[str]:
""" """
Return names of loaded configs for UI selection. Return names of loaded configs for UI selection.

View File

@@ -268,7 +268,7 @@ class nmap_scanner:
Run a command and raise on non-zero exit with a readable message. Run a command and raise on non-zero exit with a readable message.
""" """
try: try:
subprocess.run(cmd, check=True) subprocess.run(cmd, check=True,stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
except subprocess.CalledProcessError as exc: except subprocess.CalledProcessError as exc:
raise RuntimeError(f"Command failed ({exc.returncode}): {' '.join(cmd)}") from exc raise RuntimeError(f"Command failed ({exc.returncode}): {' '.join(cmd)}") from exc

View File

@@ -5,35 +5,23 @@
<title>Corporate WAN Perimeter</title> <title>Corporate WAN Perimeter</title>
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
</head> </head>
<body style="margin:0;padding:16px;background:#0b1020;color:#e5e7eb"> <body style="margin:0;padding:16px;background:#ffffff">
<div style="max-width:860px;margin:0 auto;font-family:Segoe UI,Arial,sans-serif"> <div style="max-width:860px;margin:0 auto;font-family:Segoe UI,Arial,sans-serif">
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="margin:0 0 12px 0"> <table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="margin:0 0 12px 0">
<tr> <tr>
<td style="padding:14px 16px;background:#1e293b;border:1px solid #334155;border-radius:10px;color:#f1f5f9"> <td style="padding:10px;background:#0f172a;border-radius:10px;color:#e2e8f0">
<div style="font-size:20px;font-weight:700;margin-bottom:4px">Corporate WAN Perimeter</div> <div style="font-size:18px;font-weight:700;margin-bottom:4px">Corporate WAN Perimeter</div>
<div style="font-size:12px;color:#94a3b8">Generated: 2025-10-22 01:45:32</div> <div style="font-size:12px;color:#94a3b8">Generated: 2025-10-22 02:33:35</div>
</td> </td>
</tr> </tr>
</table> </table>
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="margin:0 0 16px 0"> <table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="margin:0 0 16px 0">
<tr> <tr>
<td style=" <td style="padding:10px;border:1px solid #e5e7eb;border-radius:8px">
padding:12px 16px; Total hosts: <strong>3</strong>&nbsp;
border:1px solid #334155; Matching expected: <strong>2</strong>&nbsp;
border-radius:8px; With issues: <strong>1</strong>
background:#1e293b;
color:#f1f5f9;
font-size:14px;
">
Total hosts:
<strong style="color:#f8fafc">3</strong>&nbsp;&nbsp;
Matching expected:
<strong style="color:#4ade80">2</strong>&nbsp;&nbsp;
With issues:
<strong style="color:#f87171">1</strong>
</td> </td>
</tr> </tr>
</table> </table>
@@ -42,12 +30,11 @@
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="margin:0 0 18px 0;border-collapse:separate;border-spacing:0">
<tr>
<td colspan="5" style="padding:12px 10px;background:#1e293b;color:#f1f5f9;font-weight:600;font-size:14px;border-radius:8px;border:1px solid #334155">
10.10.20.12 <table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="margin:0 0 18px 0">
<span style="display:inline-block;padding:2px 6px;border-radius:12px;font-size:12px;line-height:16px;background:#ef4444;color:#ffffff;font-family:Segoe UI,Arial,sans-serif">ISSUES</span> <tr>
<td colspan="5" style="padding:12px 10px;background:#0f172a;color:#e2e8f0;font-weight:600;font-size:14px;border-radius:8px">
10.10.20.12 <span style="display:inline-block;padding:2px 6px;border-radius:12px;font-size:12px;line-height:16px;background:#ef4444;color:#ffffff;font-family:Segoe UI,Arial,sans-serif">ISSUES</span>
</td> </td>
</tr> </tr>
@@ -57,134 +44,116 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<td colspan="5" style="padding:4px 10px 6px 10px;font-size:13px"> <td colspan="5" style="padding:4px 10px 6px 10px;font-size:13px">
<strong style="color:#e5e7eb">Unexpected TCP open ports:</strong> <strong>Unexpected TCP open ports:</strong> 3128
<span style="color:#cbd5e1">3128</span>
</td> </td>
</tr> </tr>
<tr> <tr>
<td colspan="5" style="padding:4px 10px 6px 10px;font-size:13px"> <td colspan="5" style="padding:4px 10px 6px 10px;font-size:13px">
<strong style="color:#e5e7eb">Expected TCP ports not seen:</strong> <strong>Expected TCP ports not seen:</strong> none
<span style="color:#cbd5e1">none</span>
</td> </td>
</tr> </tr>
<tr> <tr>
<td colspan="5" style="padding:4px 10px 6px 10px;font-size:13px"> <td colspan="5" style="padding:4px 10px 6px 10px;font-size:13px">
<strong style="color:#e5e7eb">Unexpected UDP open ports:</strong> <strong>Unexpected UDP open ports:</strong> none
<span style="color:#cbd5e1">none</span>
</td> </td>
</tr> </tr>
<tr> <tr>
<td colspan="5" style="padding:4px 10px 6px 10px;font-size:13px"> <td colspan="5" style="padding:4px 10px 6px 10px;font-size:13px">
<strong style="color:#e5e7eb">Expected UDP ports not seen:</strong> <strong>Expected UDP ports not seen:</strong> none
<span style="color:#cbd5e1">none</span>
</td> </td>
</tr> </tr>
<tr> <tr>
<td colspan="5" style="padding:8px 10px 6px 10px;font-size:13px"> <td colspan="5" style="padding:8px 10px 6px 10px;font-size:13px">
<div style="font-weight:600;margin:8px 0;color:#e5e7eb">Discovered Ports</div> <div style="font-weight:600;margin:8px 0">Discovered Ports</div>
</td> </td>
</tr> </tr>
<tr> <tr>
<td style="padding:8px 10px;border:1px solid #1f2937;background:#111827;font-weight:600;color:#e5e7eb">Protocol</td> <td style="padding:8px 10px;border:1px solid #e5e7eb;background:#f8fafc;font-weight:600">Protocol</td>
<td style="padding:8px 10px;border:1px solid #1f2937;background:#111827;font-weight:600;color:#e5e7eb">Port</td> <td style="padding:8px 10px;border:1px solid #e5e7eb;background:#f8fafc;font-weight:600">Port</td>
<td style="padding:8px 10px;border:1px solid #1f2937;background:#111827;font-weight:600;color:#e5e7eb">State</td> <td style="padding:8px 10px;border:1px solid #e5e7eb;background:#f8fafc;font-weight:600">State</td>
<td style="padding:8px 10px;border:1px solid #1f2937;background:#111827;font-weight:600;color:#e5e7eb">Service</td> <td style="padding:8px 10px;border:1px solid #e5e7eb;background:#f8fafc;font-weight:600">Service</td>
<td style="padding:8px 10px;border:1px solid #1f2937;background:#111827;font-weight:600;color:#e5e7eb">Expectation</td> <td style="padding:8px 10px;border:1px solid #e5e7eb;background:#f8fafc;font-weight:600">Expectation</td>
</tr> </tr>
<tr> <tr>
<td style="padding:6px 10px;border:1px solid #1f2937;background:#0b1020"><span style="display:inline-block;padding:2px 6px;border-radius:12px;font-size:12px;line-height:16px;background:#334155;color:#ffffff;font-family:Segoe UI,Arial,sans-serif">tcp</span></td> <td style="padding:6px 10px;border:1px solid #e5e7eb"><span style="display:inline-block;padding:2px 6px;border-radius:12px;font-size:12px;line-height:16px;background:#334155;color:#ffffff;font-family:Segoe UI,Arial,sans-serif">tcp</span></td>
<td style="padding:6px 10px;border:1px solid #1f2937;background:#0b1020;color:#e5e7eb">22</td> <td style="padding:6px 10px;border:1px solid #e5e7eb">22</td>
<td style="padding:6px 10px;border:1px solid #1f2937;background:#0b1020"><span style="display:inline-block;padding:2px 6px;border-radius:12px;font-size:12px;line-height:16px;background:#16a34a;color:#ffffff;font-family:Segoe UI,Arial,sans-serif">open</span></td> <td style="padding:6px 10px;border:1px solid #e5e7eb"><span style="display:inline-block;padding:2px 6px;border-radius:12px;font-size:12px;line-height:16px;background:#16a34a;color:#ffffff;font-family:Segoe UI,Arial,sans-serif">open</span></td>
<td style="padding:6px 10px;border:1px solid #1f2937;background:#0b1020;color:#cbd5e1">ssh</td> <td style="padding:6px 10px;border:1px solid #e5e7eb">ssh</td>
<td style="padding:6px 10px;border:1px solid #1f2937;background:#0b1020"><span style="display:inline-block;padding:2px 6px;border-radius:12px;font-size:12px;line-height:16px;background:#16a34a;color:#ffffff;font-family:Segoe UI,Arial,sans-serif">Expected</span></td> <td style="padding:6px 10px;border:1px solid #e5e7eb"><span style="display:inline-block;padding:2px 6px;border-radius:12px;font-size:12px;line-height:16px;background:#16a34a;color:#ffffff;font-family:Segoe UI,Arial,sans-serif">Expected</span></td>
</tr> </tr>
<tr> <tr>
<td style="padding:6px 10px;border:1px solid #1f2937;background:#0b1020"><span style="display:inline-block;padding:2px 6px;border-radius:12px;font-size:12px;line-height:16px;background:#334155;color:#ffffff;font-family:Segoe UI,Arial,sans-serif">tcp</span></td> <td style="padding:6px 10px;border:1px solid #e5e7eb"><span style="display:inline-block;padding:2px 6px;border-radius:12px;font-size:12px;line-height:16px;background:#334155;color:#ffffff;font-family:Segoe UI,Arial,sans-serif">tcp</span></td>
<td style="padding:6px 10px;border:1px solid #1f2937;background:#0b1020;color:#e5e7eb">111</td> <td style="padding:6px 10px;border:1px solid #e5e7eb">111</td>
<td style="padding:6px 10px;border:1px solid #1f2937;background:#0b1020"><span style="display:inline-block;padding:2px 6px;border-radius:12px;font-size:12px;line-height:16px;background:#16a34a;color:#ffffff;font-family:Segoe UI,Arial,sans-serif">open</span></td> <td style="padding:6px 10px;border:1px solid #e5e7eb"><span style="display:inline-block;padding:2px 6px;border-radius:12px;font-size:12px;line-height:16px;background:#16a34a;color:#ffffff;font-family:Segoe UI,Arial,sans-serif">open</span></td>
<td style="padding:6px 10px;border:1px solid #1f2937;background:#0b1020;color:#cbd5e1">rpcbind</td> <td style="padding:6px 10px;border:1px solid #e5e7eb">rpcbind</td>
<td style="padding:6px 10px;border:1px solid #1f2937;background:#0b1020"><span style="display:inline-block;padding:2px 6px;border-radius:12px;font-size:12px;line-height:16px;background:#16a34a;color:#ffffff;font-family:Segoe UI,Arial,sans-serif">Expected</span></td> <td style="padding:6px 10px;border:1px solid #e5e7eb"><span style="display:inline-block;padding:2px 6px;border-radius:12px;font-size:12px;line-height:16px;background:#16a34a;color:#ffffff;font-family:Segoe UI,Arial,sans-serif">Expected</span></td>
</tr> </tr>
<tr> <tr>
<td style="padding:6px 10px;border:1px solid #1f2937;background:#0b1020"><span style="display:inline-block;padding:2px 6px;border-radius:12px;font-size:12px;line-height:16px;background:#334155;color:#ffffff;font-family:Segoe UI,Arial,sans-serif">tcp</span></td> <td style="padding:6px 10px;border:1px solid #e5e7eb"><span style="display:inline-block;padding:2px 6px;border-radius:12px;font-size:12px;line-height:16px;background:#334155;color:#ffffff;font-family:Segoe UI,Arial,sans-serif">tcp</span></td>
<td style="padding:6px 10px;border:1px solid #1f2937;background:#0b1020;color:#e5e7eb">3128</td> <td style="padding:6px 10px;border:1px solid #e5e7eb">3128</td>
<td style="padding:6px 10px;border:1px solid #1f2937;background:#0b1020"><span style="display:inline-block;padding:2px 6px;border-radius:12px;font-size:12px;line-height:16px;background:#16a34a;color:#ffffff;font-family:Segoe UI,Arial,sans-serif">open</span></td> <td style="padding:6px 10px;border:1px solid #e5e7eb"><span style="display:inline-block;padding:2px 6px;border-radius:12px;font-size:12px;line-height:16px;background:#16a34a;color:#ffffff;font-family:Segoe UI,Arial,sans-serif">open</span></td>
<td style="padding:6px 10px;border:1px solid #1f2937;background:#0b1020;color:#cbd5e1">squid-http</td> <td style="padding:6px 10px;border:1px solid #e5e7eb">squid-http</td>
<td style="padding:6px 10px;border:1px solid #1f2937;background:#0b1020"><span style="display:inline-block;padding:2px 6px;border-radius:12px;font-size:12px;line-height:16px;background:#ef4444;color:#ffffff;font-family:Segoe UI,Arial,sans-serif">Issue</span></td> <td style="padding:6px 10px;border:1px solid #e5e7eb"><span style="display:inline-block;padding:2px 6px;border-radius:12px;font-size:12px;line-height:16px;background:#ef4444;color:#ffffff;font-family:Segoe UI,Arial,sans-serif">Issue</span></td>
</tr> </tr>
<tr> <tr>
<td style="padding:6px 10px;border:1px solid #1f2937;background:#0b1020"><span style="display:inline-block;padding:2px 6px;border-radius:12px;font-size:12px;line-height:16px;background:#334155;color:#ffffff;font-family:Segoe UI,Arial,sans-serif">tcp</span></td> <td style="padding:6px 10px;border:1px solid #e5e7eb"><span style="display:inline-block;padding:2px 6px;border-radius:12px;font-size:12px;line-height:16px;background:#334155;color:#ffffff;font-family:Segoe UI,Arial,sans-serif">tcp</span></td>
<td style="padding:6px 10px;border:1px solid #1f2937;background:#0b1020;color:#e5e7eb">8006</td> <td style="padding:6px 10px;border:1px solid #e5e7eb">8006</td>
<td style="padding:6px 10px;border:1px solid #1f2937;background:#0b1020"><span style="display:inline-block;padding:2px 6px;border-radius:12px;font-size:12px;line-height:16px;background:#16a34a;color:#ffffff;font-family:Segoe UI,Arial,sans-serif">open</span></td> <td style="padding:6px 10px;border:1px solid #e5e7eb"><span style="display:inline-block;padding:2px 6px;border-radius:12px;font-size:12px;line-height:16px;background:#16a34a;color:#ffffff;font-family:Segoe UI,Arial,sans-serif">open</span></td>
<td style="padding:6px 10px;border:1px solid #1f2937;background:#0b1020;color:#cbd5e1">wpl-analytics</td> <td style="padding:6px 10px;border:1px solid #e5e7eb">wpl-analytics</td>
<td style="padding:6px 10px;border:1px solid #1f2937;background:#0b1020"><span style="display:inline-block;padding:2px 6px;border-radius:12px;font-size:12px;line-height:16px;background:#16a34a;color:#ffffff;font-family:Segoe UI,Arial,sans-serif">Expected</span></td> <td style="padding:6px 10px;border:1px solid #e5e7eb"><span style="display:inline-block;padding:2px 6px;border-radius:12px;font-size:12px;line-height:16px;background:#16a34a;color:#ffffff;font-family:Segoe UI,Arial,sans-serif">Expected</span></td>
</tr> </tr>
<tr> <tr>
<td style="padding:6px 10px;border:1px solid #1f2937;background:#0b1020"><span style="display:inline-block;padding:2px 6px;border-radius:12px;font-size:12px;line-height:16px;background:#334155;color:#ffffff;font-family:Segoe UI,Arial,sans-serif">udp</span></td> <td style="padding:6px 10px;border:1px solid #e5e7eb"><span style="display:inline-block;padding:2px 6px;border-radius:12px;font-size:12px;line-height:16px;background:#334155;color:#ffffff;font-family:Segoe UI,Arial,sans-serif">udp</span></td>
<td style="padding:6px 10px;border:1px solid #1f2937;background:#0b1020;color:#e5e7eb">111</td> <td style="padding:6px 10px;border:1px solid #e5e7eb">111</td>
<td style="padding:6px 10px;border:1px solid #1f2937;background:#0b1020"><span style="display:inline-block;padding:2px 6px;border-radius:12px;font-size:12px;line-height:16px;background:#16a34a;color:#ffffff;font-family:Segoe UI,Arial,sans-serif">open</span></td> <td style="padding:6px 10px;border:1px solid #e5e7eb"><span style="display:inline-block;padding:2px 6px;border-radius:12px;font-size:12px;line-height:16px;background:#16a34a;color:#ffffff;font-family:Segoe UI,Arial,sans-serif">open</span></td>
<td style="padding:6px 10px;border:1px solid #1f2937;background:#0b1020;color:#cbd5e1">rpcbind</td> <td style="padding:6px 10px;border:1px solid #e5e7eb">rpcbind</td>
<td style="padding:6px 10px;border:1px solid #1f2937;background:#0b1020"><span style="display:inline-block;padding:2px 6px;border-radius:12px;font-size:12px;line-height:16px;background:#16a34a;color:#ffffff;font-family:Segoe UI,Arial,sans-serif">Expected</span></td> <td style="padding:6px 10px;border:1px solid #e5e7eb"><span style="display:inline-block;padding:2px 6px;border-radius:12px;font-size:12px;line-height:16px;background:#16a34a;color:#ffffff;font-family:Segoe UI,Arial,sans-serif">Expected</span></td>
</tr> </tr>
</table> </table>
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="margin:0 0 18px 0;border-collapse:separate;border-spacing:0">
<tr>
<td colspan="5" style="padding:12px 10px;background:#1e293b;color:#f1f5f9;font-weight:600;font-size:14px;border-radius:8px;border:1px solid #334155">
10.10.20.4 <table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="margin:0 0 18px 0">
<span style="display:inline-block;padding:2px 6px;border-radius:12px;font-size:12px;line-height:16px;background:#16a34a;color:#ffffff;font-family:Segoe UI,Arial,sans-serif">OK</span> <tr>
<td colspan="5" style="padding:12px 10px;background:#0f172a;color:#e2e8f0;font-weight:600;font-size:14px;border-radius:8px">
10.10.20.4 <span style="display:inline-block;padding:2px 6px;border-radius:12px;font-size:12px;line-height:16px;background:#16a34a;color:#ffffff;font-family:Segoe UI,Arial,sans-serif">OK</span>
</td> </td>
</tr> </tr>
<tr> <tr>
<td colspan="5" style="padding:10px 10px 8px 10px;font-size:13px"> <td colspan="5" style="padding:10px 10px 8px 10px;font-size:13px">
<span style="display:inline-block;padding:2px 6px;border-radius:12px;font-size:12px;line-height:16px;background:#16a34a;color:#ffffff;font-family:Segoe UI,Arial,sans-serif">OK</span> &nbsp; <span style="color:#cbd5e1">Matches expected ports.</span> <span style="display:inline-block;padding:2px 6px;border-radius:12px;font-size:12px;line-height:16px;background:#16a34a;color:#ffffff;font-family:Segoe UI,Arial,sans-serif">OK</span> &nbsp; Matches expected ports.
</td> </td>
</tr> </tr>
</table> </table>
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="margin:0 0 18px 0;border-collapse:separate;border-spacing:0"> <table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="margin:0 0 18px 0">
<tr> <tr>
<td colspan="5" style="padding:12px 10px;background:#1e293b;color:#f1f5f9;font-weight:600;font-size:14px;border-radius:8px;border:1px solid #334155"> <td colspan="5" style="padding:12px 10px;background:#0f172a;color:#e2e8f0;font-weight:600;font-size:14px;border-radius:8px">
10.10.20.5 <span style="display:inline-block;padding:2px 6px;border-radius:12px;font-size:12px;line-height:16px;background:#16a34a;color:#ffffff;font-family:Segoe UI,Arial,sans-serif">OK</span>
10.10.20.5
<span style="display:inline-block;padding:2px 6px;border-radius:12px;font-size:12px;line-height:16px;background:#16a34a;color:#ffffff;font-family:Segoe UI,Arial,sans-serif">OK</span>
</td> </td>
</tr> </tr>
<tr> <tr>
<td colspan="5" style="padding:10px 10px 8px 10px;font-size:13px"> <td colspan="5" style="padding:10px 10px 8px 10px;font-size:13px">
<span style="display:inline-block;padding:2px 6px;border-radius:12px;font-size:12px;line-height:16px;background:#16a34a;color:#ffffff;font-family:Segoe UI,Arial,sans-serif">OK</span> &nbsp; <span style="color:#cbd5e1">Matches expected ports.</span> <span style="display:inline-block;padding:2px 6px;border-radius:12px;font-size:12px;line-height:16px;background:#16a34a;color:#ffffff;font-family:Segoe UI,Arial,sans-serif">OK</span> &nbsp; Matches expected ports.
</td> </td>
</tr> </tr>
</table> </table>
<div style="margin-top:18px;font-size:11px;color:#9ca3af"> <div style="margin-top:18px;font-size:11px;color:#94a3b8">
Report generated by mass-scan-v2 • 2025-10-22 01:45:32 Report generated by mass-scan-v2 • 2025-10-22 02:33:35
</div> </div>
</div>
</body>
</html>

View File

@@ -8,8 +8,8 @@ scan_options:
reporting: reporting:
report_name: Corporate WAN Perimeter report_name: Corporate WAN Perimeter
report_filename: corp-wan.html report_filename: corp-wan.html
dark_mode: true
full_details: true full_details: true
dark_mode: false
email_to: soc@example.com # single string is fine; or a list email_to: soc@example.com # single string is fine; or a list
email_cc: [] # explicitly none email_cc: [] # explicitly none