From 68aa25993d7a32e4014b7f53253ab574c3a401f5 Mon Sep 17 00:00:00 2001 From: Phillip Tarrant Date: Tue, 21 Oct 2025 21:57:43 -0500 Subject: [PATCH] templates and data classes are done --- app/main.py | 102 ++++++++----- app/reporting_jinja.py | 85 +++++++++-- app/templates/report_dark.html.j2 | 183 +++++++++++----------- app/templates/report_light.html.j2 | 122 ++++++++------- app/utils/scan_config_loader.py | 2 +- app/utils/scanner.py | 2 +- data/output/corp-wan.html | 237 +++++++++++++---------------- data/scan_targets/corp-wan.yaml | 2 +- 8 files changed, 407 insertions(+), 328 deletions(-) diff --git a/app/main.py b/app/main.py index e3a02cb..204b066 100644 --- a/app/main.py +++ b/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(): diff --git a/app/reporting_jinja.py b/app/reporting_jinja.py index 1ee7f50..b236728 100644 --- a/app/reporting_jinja.py +++ b/app/reporting_jinja.py @@ -1,12 +1,14 @@ +# utils/reporting_jinja.py from __future__ import annotations from datetime import datetime from html import escape 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 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: 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) 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( - reports: Dict[str, Dict[str, List[int]]], + reports: ReportsArg, host_results: List[HostResult], templates_dir: Path, template_name: str = "report.html.j2", @@ -56,30 +103,46 @@ def render_html_report_jinja( env = _env(templates_dir) template = env.get_template(template_name) - total_hosts = len(reports) - hosts_with_issues = [ip for ip, r in reports.items() if any(r.values())] - ok_hosts = total_hosts - len(hosts_with_issues) + grouped = _coerce_to_grouped(reports) - # No filtering — we'll show all, but only_issues changes template behavior. - by_ip = {hr.address: hr for hr in host_results} - for hr in by_ip.values(): + total_hosts = len(grouped.by_ip) + ok_hosts = len(grouped.expected) + 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: hr.ports.sort(key=lambda p: (p.protocol, p.port)) html = template.render( title=title, generated=datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + + # Summary bar total_hosts=total_hosts, ok_hosts=ok_hosts, 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, - host_results_by_ip=by_ip, + + # Host scan details + host_results_by_ip=by_ip_results, + + # Existing behavior switch only_issues=only_issues, ) return html + def write_html_report_jinja( - reports: Dict[str, Dict[str, List[int]]], + reports: ReportsArg, host_results: List[HostResult], out_path: Path, templates_dir: Path = Path("templates"), diff --git a/app/templates/report_dark.html.j2 b/app/templates/report_dark.html.j2 index 242e030..fc8a823 100644 --- a/app/templates/report_dark.html.j2 +++ b/app/templates/report_dark.html.j2 @@ -54,121 +54,132 @@ {% endif %} - {# ===== Host Sections ===== #} - {% 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 '') %} + {# ===== Host Sections (Issues first, then Expected) ===== #} + + {# A small macro reused for delta lines #} + {% macro delta_row(label, ports) -%} + + + {{ label }}: + {{ fmt_ports(ports) }} + + + {%- 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 '') %} - {% if has_issues %} - - - + + + - {% macro delta_row(label, ports) -%} - - - - {%- endmacro %} + {{ delta_row('Unexpected TCP open ports', r.unexpected_tcp) }} + {{ delta_row('Expected TCP ports not seen', r.missing_tcp) }} + {{ delta_row('Unexpected UDP open ports', r.unexpected_udp) }} + {{ delta_row('Expected UDP ports not seen', r.missing_udp) }} - {{ delta_row('Unexpected TCP open ports', r.unexpected_tcp) }} - {{ delta_row('Expected TCP ports not seen', r.missing_tcp) }} - {{ delta_row('Unexpected UDP open ports', r.unexpected_udp) }} - {{ delta_row('Expected UDP ports not seen', r.missing_udp) }} + + + + + + + + + + - - - - - - - - - - + {% if host_row and host_row.ports %} + {% set ns = namespace(visible_rows=0) %} - {% if hr and hr.ports %} - {# Track visible rows after filtering (e.g., skip closed UDP) #} - {% set ns = namespace(visible_rows=0) %} + {% for p in host_row.ports %} + {% set proto = (p.protocol|string|lower) %} + {% set state = (p.state|string|lower) %} + {% set skip_row = (proto == 'udp' and ('closed' in state)) %} - {% for p in hr.ports %} - {# --- Normalize helpers --- #} - {% set proto = (p.protocol|string|lower) %} - {% set state = (p.state|string|lower) %} + {% set is_tcp = (proto == 'tcp') %} + {% set is_udp = (proto == 'udp') %} + {% set is_open = ('open' in state) %} + {% set is_issue = (is_open and ( + (is_tcp and (p.port in (r.unexpected_tcp or []))) or + (is_udp and (p.port in (r.unexpected_udp or []))) + )) %} + {% set expectation_badge = ( + badge('Issue', '#ef4444') if is_issue + else (badge('Expected', '#16a34a') if is_open else '') + ) %} - {# --- Skip rule: hide UDP rows that are "closed" (substring match) --- #} - {% set skip_row = (proto == 'udp' and ('closed' in state)) %} - - {# --- Expectation labeling --- #} - {% set is_tcp = (proto == 'tcp') %} - {% set is_udp = (proto == 'udp') %} - {% set is_open = ('open' in state) %} - {% set is_issue = (is_open and ( - (is_tcp and (p.port in (r.unexpected_tcp or []))) or - (is_udp and (p.port in (r.unexpected_udp or []))) - )) %} - {% set expectation_badge = ( - badge('Issue', '#ef4444') if is_issue - else (badge('Expected', '#16a34a') if is_open else '') - ) %} - - {% if not skip_row %} - {% set ns.visible_rows = ns.visible_rows + 1 %} - - - - - - - - {% endif %} - {% endfor %} - - {% if ns.visible_rows == 0 %} + {% if not skip_row %} + {% set ns.visible_rows = ns.visible_rows + 1 %} - + + + + + {% endif %} - {% else %} + {% endfor %} + + {% if ns.visible_rows == 0 %} {% endif %} {% else %} - {# Host has no issues #} - {% endif %}
- - {{ header_title }} - {% if has_issues %} - {{ badge('ISSUES', '#ef4444') }} - {% else %} - {{ badge('OK', '#16a34a') }} - {% endif %} + {{ header_title }} {{ badge('ISSUES', '#ef4444') }}
- {{ badge('ISSUES', '#ef4444') }} -
+ {{ badge('ISSUES', '#ef4444') }} +
- {{ label }}: - {{ fmt_ports(ports) }} -
+
Discovered Ports
+
ProtocolPortStateServiceExpectation
-
Discovered Ports
-
ProtocolPortStateServiceExpectation
{{ pill_proto(p.protocol) }}{{ p.port }}{{ pill_state(p.state) }}{{ p.service or '-' }}{{ expectation_badge | safe }}
- No per-port details to display after filtering closed UDP results. - {{ pill_proto(p.protocol) }}{{ p.port }}{{ pill_state(p.state) }}{{ p.service or '-' }}{{ expectation_badge | safe }}
- No per-port details available for this host. + No per-port details to display after filtering closed UDP results.
- {{ badge('OK', '#16a34a') }}   Matches expected ports. + + No per-port details available for this host.
{% 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 '') %} + + + + + + + + + +
+ {{ header_title }} {{ badge('OK', '#16a34a') }} +
+ {{ badge('OK', '#16a34a') }}   Matches expected ports. +
+ {% endfor %} + {% endif %} + +
Report generated by mass-scan-v2 • {{ generated }}
diff --git a/app/templates/report_light.html.j2 b/app/templates/report_light.html.j2 index 736ce22..da283ad 100644 --- a/app/templates/report_light.html.j2 +++ b/app/templates/report_light.html.j2 @@ -38,65 +38,63 @@ {% endif %} - {# Host sections #} - {% 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 '') %} + {# ===== Host Sections (LIGHT theme) ===== #} + + {# Reusable delta row #} + {% macro delta_row(label, ports) -%} + + + {{ label }}: {{ fmt_ports(ports) }} + + + {%- 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 '') %} - - {% if has_issues %} - - - + + + - {% macro delta_row(label, ports) -%} - - - - {%- endmacro %} + {{ delta_row('Unexpected TCP open ports', r.unexpected_tcp) }} + {{ delta_row('Expected TCP ports not seen', r.missing_tcp) }} + {{ delta_row('Unexpected UDP open ports', r.unexpected_udp) }} + {{ delta_row('Expected UDP ports not seen', r.missing_udp) }} - {{ delta_row('Unexpected TCP open ports', r.unexpected_tcp) }} - {{ delta_row('Expected TCP ports not seen', r.missing_tcp) }} - {{ delta_row('Unexpected UDP open ports', r.unexpected_udp) }} - {{ delta_row('Expected UDP ports not seen', r.missing_udp) }} + + + + + + + + + + - - - - - - - - - - - - {% if hr and hr.ports %} - {# Track whether we rendered any visible rows after filtering #} + {% if host_row and host_row.ports %} {% set ns = namespace(visible_rows=0) %} - {% for p in hr.ports %} - {# Normalize helpers #} + {% for p in host_row.ports %} {% set proto = (p.protocol|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)) %} - {# Compute expectation labeling (only matters for displayed rows) #} {% set is_tcp = (proto == 'tcp') %} {% set is_udp = (proto == 'udp') %} {% set is_open = ('open' in state) %} @@ -121,7 +119,6 @@ {% endif %} {% endfor %} - {# If everything was filtered out, show a friendly note #} {% if ns.visible_rows == 0 %} {% endif %} - {% else %} - {# Host has no issues #} - - - - {% endif %}
- {{ header_title }} {% if has_issues %} {{ badge('ISSUES', '#ef4444') }} {% else %} {{ badge('OK', '#16a34a') }} {% endif %} + + {{ header_title }} {{ badge('ISSUES', '#ef4444') }}
- {{ badge('ISSUES', '#ef4444') }} -
+ {{ badge('ISSUES', '#ef4444') }} +
- {{ label }}: {{ fmt_ports(ports) }} -
+
Discovered Ports
+
ProtocolPortStateServiceExpectation
-
Discovered Ports
-
ProtocolPortStateServiceExpectation
@@ -136,18 +133,31 @@
- {{ badge('OK', '#16a34a') }}   Matches expected ports. -
{% 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 '') %} -
- Report generated by mass-scan-v2 • {{ generated }} -
+ + + + + + + +
+ {{ header_title }} {{ badge('OK', '#16a34a') }} +
+ {{ badge('OK', '#16a34a') }}   Matches expected ports. +
+ {% endfor %} + {% endif %} + +
+ Report generated by mass-scan-v2 • {{ generated }} +
diff --git a/app/utils/scan_config_loader.py b/app/utils/scan_config_loader.py index 78efbe5..3e07bd8 100644 --- a/app/utils/scan_config_loader.py +++ b/app/utils/scan_config_loader.py @@ -162,6 +162,7 @@ class ScanConfigRepository: report_name=str(rep_raw.get("report_name", "Scan Report")), report_filename=str(rep_raw.get("report_filename", "report.html")), 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_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.") # helpers - def list_configs(self) -> List[str]: """ Return names of loaded configs for UI selection. diff --git a/app/utils/scanner.py b/app/utils/scanner.py index 248e728..8f0bb14 100644 --- a/app/utils/scanner.py +++ b/app/utils/scanner.py @@ -268,7 +268,7 @@ class nmap_scanner: Run a command and raise on non-zero exit with a readable message. """ try: - subprocess.run(cmd, check=True) + subprocess.run(cmd, check=True,stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) except subprocess.CalledProcessError as exc: raise RuntimeError(f"Command failed ({exc.returncode}): {' '.join(cmd)}") from exc diff --git a/data/output/corp-wan.html b/data/output/corp-wan.html index 7ec1e13..286fc01 100644 --- a/data/output/corp-wan.html +++ b/data/output/corp-wan.html @@ -5,35 +5,23 @@ Corporate WAN Perimeter - +
- -
-
Corporate WAN Perimeter
-
Generated: 2025-10-22 01:45:32
+
+
Corporate WAN Perimeter
+
Generated: 2025-10-22 02:33:35
- -
- Total hosts: - 3   - Matching expected: - 2   - With issues: - 1 + + Total hosts: 3  + Matching expected: 2  + With issues: 1
@@ -42,149 +30,130 @@ - - -
- 10.10.20.12 - ISSUES + + + - - - + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + - - - - - - - - + + + + + + + - - - - - - - - + + + + + + + - - - - - - - - + + + + + + + - - - - - - - - + + + + + + + - - - - - - - - + + + + + + +
+ 10.10.20.12 ISSUES
- ISSUES -
+ ISSUES +
+ Unexpected TCP open ports: 3128 +
+ Expected TCP ports not seen: none +
+ Unexpected UDP open ports: none +
+ Expected UDP ports not seen: none +
- Unexpected TCP open ports: - 3128 -
- Expected TCP ports not seen: - none -
- Unexpected UDP open ports: - none -
- Expected UDP ports not seen: - none -
-
Discovered Ports
-
ProtocolPortStateServiceExpectation
+
Discovered Ports
+
ProtocolPortStateServiceExpectation
tcp22opensshExpected
tcp22opensshExpected
tcp111openrpcbindExpected
tcp111openrpcbindExpected
tcp3128opensquid-httpIssue
tcp3128opensquid-httpIssue
tcp8006openwpl-analyticsExpected
tcp8006openwpl-analyticsExpected
udp111openrpcbindExpected
udp111openrpcbindExpected
- - - - +
- - 10.10.20.4 - OK -
+ + + -
+ 10.10.20.4 OK +
- OK   Matches expected ports. + OK   Matches expected ports.
- - - - - +
- - 10.10.20.5 - OK -
+ + + + -
+ 10.10.20.5 OK +
- OK   Matches expected ports. + OK   Matches expected ports.
+
-
- Report generated by mass-scan-v2 • 2025-10-22 01:45:32 -
-
- - \ No newline at end of file +
+ Report generated by mass-scan-v2 • 2025-10-22 02:33:35 +
\ No newline at end of file diff --git a/data/scan_targets/corp-wan.yaml b/data/scan_targets/corp-wan.yaml index 104971c..3d41e80 100644 --- a/data/scan_targets/corp-wan.yaml +++ b/data/scan_targets/corp-wan.yaml @@ -8,8 +8,8 @@ scan_options: reporting: report_name: Corporate WAN Perimeter report_filename: corp-wan.html - dark_mode: true full_details: true + dark_mode: false email_to: soc@example.com # single string is fine; or a list email_cc: [] # explicitly none