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 '') %}
|
-
- {{ header_title }}
- {% if has_issues %}
- {{ badge('ISSUES', '#ef4444') }}
- {% else %}
- {{ badge('OK', '#16a34a') }}
- {% endif %}
+ {{ header_title }} {{ badge('ISSUES', '#ef4444') }}
|
- {% if has_issues %}
-
- |
- {{ badge('ISSUES', '#ef4444') }}
- |
-
+
+ |
+ {{ badge('ISSUES', '#ef4444') }}
+ |
+
- {% macro delta_row(label, ports) -%}
-
- |
- {{ label }}:
- {{ fmt_ports(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) }}
+
+ |
+ Discovered Ports
+ |
+
+
+ | Protocol |
+ Port |
+ State |
+ Service |
+ Expectation |
+
-
- |
- Discovered Ports
- |
-
-
- | Protocol |
- Port |
- State |
- Service |
- Expectation |
-
+ {% 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 %}
-
- | {{ pill_proto(p.protocol) }} |
- {{ p.port }} |
- {{ pill_state(p.state) }} |
- {{ p.service or '-' }} |
- {{ expectation_badge | safe }} |
-
- {% endif %}
- {% endfor %}
-
- {% if ns.visible_rows == 0 %}
+ {% if not skip_row %}
+ {% set ns.visible_rows = ns.visible_rows + 1 %}
- |
- 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 }} |
{% endif %}
- {% else %}
+ {% endfor %}
+
+ {% if ns.visible_rows == 0 %}
|
- No per-port details available for this host.
+ No per-port details to display after filtering closed UDP results.
|
{% endif %}
{% else %}
- {# Host has no issues #}
- |
- {{ badge('OK', '#16a34a') }} Matches expected ports.
+ |
+ No per-port details available for this host.
|
{% endif %}
{% 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 '') %}
- |
- {{ header_title }} {% if has_issues %} {{ badge('ISSUES', '#ef4444') }} {% else %} {{ badge('OK', '#16a34a') }} {% endif %}
+ |
+ {{ header_title }} {{ badge('ISSUES', '#ef4444') }}
|
- {% if has_issues %}
-
- |
- {{ badge('ISSUES', '#ef4444') }}
- |
-
+
+ |
+ {{ badge('ISSUES', '#ef4444') }}
+ |
+
- {% macro delta_row(label, ports) -%}
-
- |
- {{ label }}: {{ fmt_ports(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) }}
+
+ |
+ Discovered Ports
+ |
+
+
+ | Protocol |
+ Port |
+ State |
+ Service |
+ Expectation |
+
-
- |
- Discovered Ports
- |
-
-
- | Protocol |
- Port |
- State |
- Service |
- Expectation |
-
-
- {% 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 %}
|
@@ -136,18 +133,31 @@
|
{% endif %}
- {% else %}
- {# Host has no issues #}
-
- |
- {{ badge('OK', '#16a34a') }} Matches expected ports.
- |
-
- {% endif %}
{% 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
- |
-
-
- | Protocol |
- Port |
- State |
- Service |
- Expectation |
-
+
+ |
+ Discovered Ports
+ |
+
+
+ | Protocol |
+ Port |
+ State |
+ Service |
+ Expectation |
+
-
-
- | tcp |
- 22 |
- open |
- ssh |
- Expected |
-
+
+ | tcp |
+ 22 |
+ open |
+ ssh |
+ Expected |
+
-
-
- | tcp |
- 111 |
- open |
- rpcbind |
- Expected |
-
+
+ | tcp |
+ 111 |
+ open |
+ rpcbind |
+ Expected |
+
-
-
- | tcp |
- 3128 |
- open |
- squid-http |
- Issue |
-
+
+ | tcp |
+ 3128 |
+ open |
+ squid-http |
+ Issue |
+
-
-
- | tcp |
- 8006 |
- open |
- wpl-analytics |
- Expected |
-
+
+ | tcp |
+ 8006 |
+ open |
+ wpl-analytics |
+ Expected |
+
-
-
- | udp |
- 111 |
- open |
- rpcbind |
- Expected |
-
+
+ | udp |
+ 111 |
+ open |
+ rpcbind |
+ Expected |
+
-
-
- |
-
- 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
|