Compare commits

...

5 Commits

Author SHA1 Message Date
Phillip Tarrant
d307a37a5b adding targets 2025-10-22 10:35:03 -05:00
d5f10e4bfb updating plans and save points 2025-10-21 23:14:21 -05:00
4d3599149f updating target.example 2025-10-21 23:07:10 -05:00
ed3b1de1cd template refactor 2025-10-21 23:05:43 -05:00
4846af66c4 adding punch list for improvement plan 2025-10-21 22:07:50 -05:00
13 changed files with 990 additions and 586 deletions

View File

@@ -4,6 +4,7 @@ FROM ubuntu:24.04
ENV DEBIAN_FRONTEND=noninteractive \ ENV DEBIAN_FRONTEND=noninteractive \
PYTHONUNBUFFERED=1 \ PYTHONUNBUFFERED=1 \
VIRTUAL_ENV=/opt/venv \ VIRTUAL_ENV=/opt/venv \
LANG=C.UTF-8 LC_ALL=C.UTF-8 \
PATH="/opt/venv/bin:$PATH" PATH="/opt/venv/bin:$PATH"
# copy only requirements first so pip install can be cached # copy only requirements first so pip install can be cached
@@ -13,9 +14,12 @@ COPY app/requirements.txt /app/requirements.txt
RUN apt-get update && \ RUN apt-get update && \
apt-get install -y --no-install-recommends \ apt-get install -y --no-install-recommends \
python3 python3-venv python3-pip \ python3 python3-venv python3-pip \
nmap ca-certificates tzdata && \ nmap ca-certificates tzdata wkhtmltopdf fonts-dejavu ca-certificates && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
# Debug Print version of wkhtmltopdf
RUN wkhtmltopdf --version
# ---- Create & activate venv ---- # ---- Create & activate venv ----
RUN python3 -m venv $VIRTUAL_ENV RUN python3 -m venv $VIRTUAL_ENV

View File

@@ -45,8 +45,8 @@ def results_to_open_sets(
out[hr.address] = {"tcp": tcp, "udp": udp} out[hr.address] = {"tcp": tcp, "udp": udp}
return out return out
# Build the "reports" dict (what the HTML renderer expects) # Build the grouped_reports (what the HTML renderer expects)
def build_reports( def build_grouped_reports(
scan_config: "ScanConfigFile", scan_config: "ScanConfigFile",
discovered: Dict[str, Dict[str, Set[int]]], discovered: Dict[str, Dict[str, Set[int]]],
) -> GroupedReports: ) -> GroupedReports:
@@ -149,34 +149,34 @@ def run_repo_scan(scan_config:ScanConfigFile):
# tack the filename on the end of our data path # tack the filename on the end of our data path
file_out_path = Path() / "data" / "output" / scan_config.reporting.report_filename file_out_path = Path() / "data" / "output" / scan_config.reporting.report_filename
LIGHT_TEMPLATE = "report_light.html.j2"
DARK_TEMPLATE = "report_dark.html.j2"
template = LIGHT_TEMPLATE
if scan_config.reporting.dark_mode:
template = DARK_TEMPLATE
if scan_config.reporting.full_details: if scan_config.reporting.full_details:
show_only_issues = False show_only_issues = False
else: else:
show_only_issues = True show_only_issues = True
logger.info(f"Reporting Template Set to: {template}") logger.info(f"Reporting Dark Mode set to: {scan_config.reporting.dark_mode}")
logger.info(f"Reporting Only Issues: {show_only_issues}") 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()
discovered_sets = results_to_open_sets(scan_results, count_as_open={"open", "open|filtered"}) discovered_sets = results_to_open_sets(scan_results, count_as_open={"open", "open|filtered"})
reports = build_reports(scan_config, discovered_sets) grouped_reports = build_grouped_reports(scan_config, discovered_sets)
# build the HTML report # build the HTML report
write_html_report_jinja(reports=reports, # write_html_report_jinja(grouped=grouped_reports,
# host_results=scan_results,
# out_path=file_out_path,
# title=scan_config.reporting.report_name,
# template_name=template,
# only_issues=show_only_issues)
write_html_report_jinja(grouped=grouped_reports,
host_results=scan_results, host_results=scan_results,
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, dark_mode=scan_config.reporting.dark_mode,
only_issues=show_only_issues) only_issues=show_only_issues)
scanner.cleanup() scanner.cleanup()
def main(): def main():

View File

@@ -1,157 +1,207 @@
# utils/reporting_jinja.py # utils/reporting_jinja.py
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime from datetime import datetime
from html import escape from html import escape
from ipaddress import ip_address
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Set, Union from typing import Dict, List
from jinja2 import Environment, FileSystemLoader, select_autoescape from jinja2 import Environment, FileSystemLoader, select_autoescape
from utils.models import HostResult, HostReport, GroupedReports # <-- add HostReport, GroupedReports from utils.models import HostResult, HostReport, GroupedReports
from ipaddress import ip_address
# ---------------------------
# Helper render-time functions
# ---------------------------
def fmt_ports(ports: List[int]) -> str: def fmt_ports(ports: List[int]) -> str:
"""
Render a simple, comma-separated list of port numbers, deduped and sorted.
"""
if not ports: if not ports:
return "none" return "none"
return ", ".join(str(p) for p in sorted(set(int(x) for x in ports))) unique_ports: List[int] = []
seen: set[int] = set()
for x in ports:
p = int(x)
if p not in seen:
seen.add(p)
unique_ports.append(p)
unique_ports.sort()
return ", ".join(str(p) for p in unique_ports)
def badge(text: str, kind: str = "") -> str:
"""
Return a class-based badge element (no inline color).
'kind' should map to CSS variants defined in your stylesheet (e.g., 'ok', 'issue').
Unknown kinds fall back to the base 'badge' styling.
Examples:
badge("OK", "ok") -> <span class="badge ok">OK</span>
badge("ISSUE", "issue") -> <span class="badge issue">ISSUE</span>
badge("info") -> <span class="badge">info</span>
"""
text_s = escape(str(text))
kind_s = str(kind).strip()
if kind_s:
return f'<span class="badge {kind_s}">{text_s}</span>'
return f'<span class="badge">{text_s}</span>'
def badge(text: str, bg: str, fg: str = "#ffffff") -> str:
return (
f'<span style="display:inline-block;padding:2px 6px;border-radius:12px;'
f'font-size:12px;line-height:16px;background:{bg};color:{fg};'
f'font-family:Segoe UI,Arial,sans-serif">{escape(text)}</span>'
)
def pill_state(state: str) -> str: def pill_state(state: str) -> str:
s = (state or "").lower() """
Map port states to neutral/semantic classes (no inline color).
Keeps semantics simple so CSS controls appearance.
"""
s = (state or "").strip().lower()
if s == "open": if s == "open":
return badge("open", "#16a34a") return badge("open", "ok") # green-ish in CSS
if s in ("open|filtered",):
return badge("open|filtered", "#0ea5e9")
if s == "filtered":
return badge("filtered", "#f59e0b", "#111111")
if s == "closed": if s == "closed":
return badge("closed", "#ef4444") return badge("closed", "issue") # red-ish in CSS
return badge(s, "#6b7280")
# For less common states, keep a neutral chip/badge so style stays consistent
if s in ("open|filtered", "filtered"):
return f'<span class="chip">{escape(s)}</span>'
# Fallback
return f'<span class="chip">{escape(s or "-")}</span>'
def pill_proto(proto: str) -> str: def pill_proto(proto: str) -> str:
return badge((proto or "").lower(), "#334155") """
Render protocol as a neutral 'chip'.
"""
return f'<span class="chip">{escape((proto or "").lower())}</span>'
def _env(templates_dir: Path) -> Environment: def _env(templates_dir: Path) -> Environment:
"""
Build a Jinja2 environment with our helpers registered as globals.
"""
env = Environment( env = Environment(
loader=FileSystemLoader(str(templates_dir)), loader=FileSystemLoader(str(templates_dir)),
autoescape=select_autoescape(["html", "xml"]), autoescape=select_autoescape(["html", "xml", "j2"]),
trim_blocks=True, trim_blocks=True,
lstrip_blocks=True, lstrip_blocks=True,
) )
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 --- # ---------------------------
# Rendering entry points
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: ReportsArg, grouped: GroupedReports,
host_results: List[HostResult], host_results: List[HostResult],
templates_dir: Path, templates_dir: Path,
template_name: str = "report.html.j2", *,
template_name: str = "report_body.html.j2",
title: str = "Port Compliance Report", title: str = "Port Compliance Report",
only_issues: bool = False, only_issues: bool = False,
dark_mode: bool = True,
) -> str: ) -> str:
"""
Render the HTML report using the new structure/palette templates.
Args:
grouped: GroupedReports with .issues, .expected, .by_ip already populated.
host_results: Raw scan results (per-host) used for the detailed port tables.
templates_dir: Path to the Jinja templates root (contains base_report.html.j2, palettes/, etc.).
template_name: Structure template to render (defaults to 'report_body.html.j2').
title: Report title.
only_issues: If True, suppresses the 'expected/OK' section.
dark_mode: Controls which palette is included by base_report.html.j2.
Returns:
Rendered HTML as a string.
"""
env = _env(templates_dir) env = _env(templates_dir)
template = env.get_template(template_name) template = env.get_template(template_name)
grouped = _coerce_to_grouped(reports)
total_hosts = len(grouped.by_ip) total_hosts = len(grouped.by_ip)
ok_hosts = len(grouped.expected) ok_hosts = len(grouped.expected)
hosts_with_issues = [hr.ip for hr in grouped.issues] hosts_with_issues = [hr.ip for hr in grouped.issues]
# Build a mapping of IP -> HostResult and sort port rows for stable output # Build mapping of IP -> HostResult and sort port rows for stable output
by_ip_results = {hr.address: hr for hr in host_results} by_ip_results: Dict[str, HostResult] = {}
for hr in host_results:
by_ip_results[hr.address] = hr
for hr in by_ip_results.values(): 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)) # Sort by (protocol, then port, then service)
hr.ports.sort(key=lambda p: (str(p.protocol).lower(), int(p.port), str(p.service or "")))
html = template.render( html = template.render(
# Base metadata
title=title, title=title,
generated=datetime.now().strftime("%Y-%m-%d %H:%M:%S"), generated=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
# Palette switch (consumed by base_report.html.j2)
dark_mode=dark_mode,
# Summary bar # 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 # Sections
issues=grouped.issues, # list[HostReport] issues=grouped.issues, # list[HostReport]
expected=grouped.expected, # list[HostReport] expected=grouped.expected, # list[HostReport]
by_ip=grouped.by_ip, # dict[str, HostReport] by_ip=grouped.by_ip, # dict[str, HostReport]
# Legacy context (kept for compatibility if your template still uses it) # Host scan details (used for port tables)
reports=reports,
# Host scan details
host_results_by_ip=by_ip_results, host_results_by_ip=by_ip_results,
# Existing behavior switch # Behavior flag
only_issues=only_issues, only_issues=only_issues,
) )
return html return html
def write_html_report_jinja( def write_html_report_jinja(
reports: ReportsArg, grouped: GroupedReports,
host_results: List[HostResult], host_results: List[HostResult],
out_path: Path, out_path: Path,
*,
templates_dir: Path = Path("templates"), templates_dir: Path = Path("templates"),
template_name: str = "report.html.j2", template_name: str = "report_body.html.j2",
title: str = "Port Compliance Report", title: str = "Port Compliance Report",
only_issues: bool = False, only_issues: bool = False,
dark_mode: bool = True,
) -> Path: ) -> Path:
"""
Render and write the HTML report to disk.
Notes:
- Assumes your templates directory contains:
base_report.html.j2
report_body.html.j2
palettes/light.css.j2
palettes/dark.css.j2
- 'dark_mode' toggles which palette is inlined by the base template.
"""
html = render_html_report_jinja( html = render_html_report_jinja(
reports, host_results, templates_dir, template_name, title, only_issues grouped=grouped,
host_results=host_results,
templates_dir=templates_dir,
template_name=template_name,
title=title,
only_issues=only_issues,
dark_mode=dark_mode,
) )
out_path.write_text(html, encoding="utf-8") out_path.write_text(html, encoding="utf-8")
return out_path return out_path

View File

@@ -0,0 +1,220 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="color-scheme" content="light dark" />
<title>{{ title or "Port Report" }}</title>
<style>
/* Reset-ish */
html, body { margin:0; padding:0; }
body {
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, "Helvetica Neue", Arial, "Noto Sans",
"Liberation Sans", sans-serif, "Apple Color Emoji","Segoe UI Emoji";
background: var(--bg);
color: var(--text);
line-height: 1.45;
font-size: 14px;
}
table { border-collapse: collapse; width: 100%; }
th, td { vertical-align: top; }
/* Include palette */
{% if dark_mode %}
{% include "palettes/dark.css.j2" %}
{% else %}
{% include "palettes/light.css.j2" %}
{% endif %}
/* Layout & components */
.container { max-width: 900px; margin: 24px auto; padding: 0 12px; }
.card {
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: 10px;
padding: 12px;
}
.header {
background: var(--card-bg);
border-radius: 10px;
padding: 12px;
font-weight: 600;
}
.muted { color: var(--muted); }
.dash { color: var(--muted); } /* for em-dash placeholder */
.badge {
display: inline-block;
font-size: 12px;
font-weight: 700;
padding: 2px 8px;
border-radius: 999px;
line-height: 1.6;
border: 1px solid transparent;
margin-left: 6px;
}
.badge.ok { background: var(--ok); color: var(--ok-on); }
.badge.issue { background: var(--issue); color: var(--issue-on); }
.chip { display:inline-block; background: var(--chip-bg); padding: 2px 6px; border-radius: 6px; }
.section { margin: 0 0 18px 0; }
.table-clean { width:100%; }
.table-clean td { padding: 8px 10px; }
.host-title { font-size: 14px; }
.summary-row { padding: 10px; }
/* Utility helpers referenced by report_body.html.j2 */
.title-xl { font-size: 20px; font-weight: 700; margin-bottom: 4px; }
.meta { font-size: 12px; }
.note { margin: 6px 0 12px 0; font-size: 12px; }
.subhead { font-weight: 600; margin: 8px 0; }
.pad-s { padding: 10px; }
.footer { margin-top: 18px; font-size: 11px; }
/* ===== Summary Bar (prominent) ===== */
.summary-row {
position: relative;
background: var(--summary-bg, var(--card-bg));
border: 1px solid var(--summary-border, var(--border));
border-radius: 12px;
padding: 14px 16px;
font-size: 14px;
color: var(--text);
box-shadow:
0 0 0 1px var(--summary-border, var(--border)) inset,
0 8px 24px var(--summary-shadow, rgba(0,0,0,0.25));
}
/* left accent bar */
.summary-row::before {
content: "";
position: absolute;
left: 0; top: 0; bottom: 0;
width: 6px;
border-radius: 12px 0 0 12px;
background: linear-gradient(
180deg,
color-mix(in srgb, var(--accent) 90%, transparent) 0%,
color-mix(in srgb, var(--accent) 60%, transparent) 100%
);
opacity: 0.9;
}
/* tighten spacing between metrics */
.summary-row strong { margin-right: 8px; }
/* KPI “chips” for the numbers (no HTML changes needed) */
.text-strong,
.text-ok,
.text-issue {
display: inline-block;
padding: 2px 8px;
border-radius: 999px;
font-weight: 700;
line-height: 1.6;
border: 1px solid transparent;
font-variant-numeric: tabular-nums;
}
/* Neutral total */
.text-strong {
background: var(--accent-soft, color-mix(in srgb, var(--accent) 12%, transparent));
border-color: color-mix(in srgb, var(--accent) 45%, var(--summary-border, var(--border)));
}
/* OK / Issue map to your existing status colors */
.text-ok {
color: var(--text);
background: var(--ok-bg-soft, color-mix(in srgb, var(--ok) 18%, transparent));
border-color: color-mix(in srgb, var(--ok) 45%, var(--summary-border, var(--border)));
}
.text-issue {
color: var(--text);
background: var(--issue-bg-soft, color-mix(in srgb, var(--issue) 18%, transparent));
border-color: color-mix(in srgb, var(--issue) 45%, var(--summary-border, var(--border)));
}
/* ===== Port Table Card (bordered) ===== */
.table-ports {
position: relative;
margin-bottom: 24px;
border: 1px solid var(--card-border, var(--border));
border-radius: 12px;
background: var(--card-bg);
box-shadow:
0 0 0 1px var(--card-border, var(--border)) inset,
0 4px 12px rgba(0,0,0,0.25);
overflow: hidden;
}
.table-ports th,
.table-ports td {
border: 1px solid var(--border);
padding: 6px 10px;
}
.table-ports .th {
background: var(--bg);
color: var(--text);
font-weight: 600;
font-size: 13px;
border-bottom: 2px solid var(--border);
}
.table-ports .td {
font-size: 13px;
}
.table-ports .td.num { white-space: nowrap; }
.table-ports .td.proto,
.table-ports .td.state,
.table-ports .td.expect {
text-align: center;
}
/* optional: subtle row separation */
.table-ports tr:nth-child(even) {
background: color-mix(in srgb, var(--card-bg) 92%, var(--border) 8%);
}
/* ===== Host Header Row (within table-ports) ===== */
.table-ports .header {
position: relative;
background: var(--header-bg, var(--card-bg));
color: var(--header-text, var(--text));
font-weight: 700;
font-size: 14px;
border-radius: 10px 10px 0 0;
padding: 12px 14px;
border-bottom: 2px solid var(--header-border, var(--border));
text-shadow: 0 1px 2px rgba(0,0,0,0.25);
box-shadow: 0 2px 8px var(--header-shadow, rgba(0,0,0,0.2));
letter-spacing: 0.2px;
}
/* Subtle left accent stripe (matches summary style) */
.table-ports .header::before {
content: "";
position: absolute;
top: 0; bottom: 0; left: 0;
width: 5px;
border-radius: 10px 0 0 0;
background: linear-gradient(
180deg,
color-mix(in srgb, var(--accent) 85%, transparent) 0%,
color-mix(in srgb, var(--accent) 55%, transparent) 100%
);
}
</style>
</head>
<body>
<div class="container">
{% block body %}{% endblock %}
</div>
</body>
</html>

View File

@@ -0,0 +1,30 @@
:root {
--bg: #0b1220; /* slate-950ish */
--text: #e5e7eb; /* slate-200 */
--muted: #94a3b8; /* slate-400 */
--card-bg: #0f172a; /* slate-900 */
--border: #334155; /* slate-700 */
--accent: #38bdf8;
--ok: #16a34a;
--ok-on: #052e16;
--issue: #ef4444;
--issue-on: #450a0a;
--chip-bg: #1f2937; /* gray-800 */
/* Summary bar emphasis */
--summary-bg: #101a33; /* a touch brighter than --card-bg */
--summary-border: #2c3a56; /* slightly brighter border */
--summary-shadow: rgba(0, 0, 0, 0.5);
/* KPI chip fills (derived from status colors) */
--ok-bg-soft: color-mix(in srgb, var(--ok) 18%, transparent);
--issue-bg-soft: color-mix(in srgb, var(--issue) 18%, transparent);
--accent-soft: color-mix(in srgb, var(--accent) 18%, transparent);
/* Port table border accent (dark) */
--card-border: #2b3b5c; /* slightly brighter than main border */
}

View File

@@ -0,0 +1,35 @@
:root {
/* Surface & text */
--bg: #f8fafc;
--text: #0f172a;
--muted: #334155;
/* Cards & borders */
--card-bg: #ffffff;
--border: #e2e8f0;
/* Accents */
--accent: #0ea5e9;
/* Status colors */
--ok: #16a34a;
--ok-on: #ecfdf5;
--issue: #ef4444;
--issue-on: #fef2f2;
/* Misc */
--chip-bg: #e5e7eb;
/* Summary bar emphasis (light) */
--summary-bg: #f1f5f9; /* slightly darker than page bg for contrast */
--summary-border: #cbd5e1; /* soft cool-gray border */
--summary-shadow: rgba(0, 0, 0, 0.08);
/* KPI chip fills (derived from status colors) */
--ok-bg-soft: color-mix(in srgb, var(--ok) 12%, white);
--issue-bg-soft: color-mix(in srgb, var(--issue) 12%, white);
--accent-soft: color-mix(in srgb, var(--accent) 12%, white);
/* Port table border accent (light) */
--card-border: #cbd5e1; /* cool gray for gentle contrast */
}

View File

@@ -0,0 +1,164 @@
{% extends "base_report.html.j2" %}
{% block body %}
{# ===== Title Card ===== #}
<table role="presentation" class="section table-clean">
<tr>
<td class="card">
<div class="title-xl">{{ title }}</div>
<div class="meta muted">Generated: {{ generated }}</div>
</td>
</tr>
</table>
{# ===== Summary Bar ===== #}
<table role="presentation" class="section table-clean">
<tr>
<td class="card summary-row">
Total hosts:
<strong class="text-strong">{{ total_hosts }}</strong>&nbsp;&nbsp;
Matching expected:
<strong class="text-ok">{{ ok_hosts }}</strong>&nbsp;&nbsp;
With issues:
<strong class="text-issue">{{ hosts_with_issues|length }}</strong>
</td>
</tr>
</table>
{% if not only_issues and ok_hosts == total_hosts and total_hosts > 0 %}
<div class="note muted">All hosts matched expected ports.</div>
{% endif %}
{% if only_issues and hosts_with_issues|length == 0 %}
<div class="note muted">No hosts with issues found. ✅</div>
{% endif %}
{# ===== Helpers ===== #}
{% macro delta_row(label, ports) -%}
<tr>
<td colspan="5" class="delta-row">
<strong>{{ label }}:</strong>
<span class="muted">{{ fmt_ports(ports) }}</span>
</td>
</tr>
{%- endmacro %}
{# ================= 1) Issues (already sorted by IP 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" class="section table-clean table-ports">
<tr>
<td colspan="5" class="header">
{{ header_title }} <span class="badge issue">ISSUES</span>
</td>
</tr>
{{ 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) }}
<tr>
<td colspan="5" class="pad-s">
<div class="subhead">Discovered Ports</div>
</td>
</tr>
<tr>
<td class="th">Protocol</td>
<td class="th">Port</td>
<td class="th">State</td>
<td class="th">Service</td>
<td class="th">Expectation</td>
</tr>
{% if host_row and host_row.ports %}
{% set ns = namespace(visible_rows=0) %}
{% for p in host_row.ports %}
{% set proto = (p.protocol|string|lower) %}
{% set state = (p.state|string|lower) %}
{# hide closed UDP lines to reduce noise #}
{% set skip_row = (proto == 'udp' and ('closed' in state)) %}
{% 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 [])))
)) %}
{% if is_issue %}
{% set expectation_badge %}<span class="badge issue">Issue</span>{% endset %}
{% elif is_open %}
{% set expectation_badge %}<span class="badge ok">Expected</span>{% endset %}
{% else %}
{% set expectation_badge %}<span class="dash">—</span>{% endset %}
{% endif %}
{% if not skip_row %}
{% set ns.visible_rows = ns.visible_rows + 1 %}
<tr>
<td class="td proto">{{ pill_proto(p.protocol) | safe }}</td>
<td class="td num">{{ p.port }}</td>
<td class="td state">{{ pill_state(p.state) | safe }}</td>
<td class="td svc">{{ p.service or '-' }}</td>
<td class="td expect">{{ expectation_badge | safe }}</td>
</tr>
{% endif %}
{% endfor %}
{% if ns.visible_rows == 0 %}
<tr>
<td colspan="5" class="td muted">
No per-port details to display after filtering closed UDP results.
</td>
</tr>
{% endif %}
{% else %}
<tr>
<td colspan="5" class="td muted">
No per-port details available for this host.
</td>
</tr>
{% endif %}
</table>
{% 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 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" class="section table-clean">
<tr>
<td colspan="5" class="header">
{{ header_title }} <span class="badge ok">OK</span>
</td>
</tr>
<tr>
<td colspan="5" class="pad-s">
<span class="badge ok">OK</span>
&nbsp; <span class="muted">Matches expected ports.</span>
</td>
</tr>
</table>
{% endfor %}
{% endif %}
<div class="footer muted">
Report generated by mass-scan-v2 • {{ generated }}
</div>
{% endblock %}

View File

@@ -1,188 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{{ title }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
{# Dark mode variant of compliance report. Filename: report_dark.html.j2 #}
</head>
<body style="margin:0;padding:16px;background:#0b1020;color:#e5e7eb">
<div style="max-width:860px;margin:0 auto;font-family:Segoe UI,Arial,sans-serif">
{# ===== Title Card ===== #}
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="margin:0 0 12px 0">
<tr>
<td style="padding:14px 16px;background:#1e293b;border:1px solid #334155;border-radius:10px;color:#f1f5f9">
<div style="font-size:20px;font-weight:700;margin-bottom:4px">{{ title }}</div>
<div style="font-size:12px;color:#94a3b8">Generated: {{ generated }}</div>
</td>
</tr>
</table>
{# ===== Summary Bar ===== #}
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="margin:0 0 16px 0">
<tr>
<td style="
padding:12px 16px;
border:1px solid #334155;
border-radius:8px;
background:#1e293b;
color:#f1f5f9;
font-size:14px;
">
Total hosts:
<strong style="color:#f8fafc">{{ total_hosts }}</strong>&nbsp;&nbsp;
Matching expected:
<strong style="color:#4ade80">{{ ok_hosts }}</strong>&nbsp;&nbsp;
With issues:
<strong style="color:#f87171">{{ hosts_with_issues|length }}</strong>
</td>
</tr>
</table>
{% if not only_issues and ok_hosts == total_hosts and total_hosts > 0 %}
<div style="margin:6px 0 12px 0;font-size:12px;color:#9ca3af">
All hosts matched expected ports.
</div>
{% endif %}
{% if only_issues and hosts_with_issues|length == 0 %}
<div style="margin:8px 0;color:#9ca3af">
No hosts with issues found. ✅
</div>
{% endif %}
{# ===== Host Sections (Issues first, then Expected) ===== #}
{# A small macro reused for delta lines #}
{% macro delta_row(label, ports) -%}
<tr>
<td colspan="5" style="padding:4px 10px 6px 10px;font-size:13px">
<strong style="color:#e5e7eb">{{ label }}:</strong>
<span style="color:#cbd5e1">{{ fmt_ports(ports) }}</span>
</td>
</tr>
{%- 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('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) }}
<tr>
<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>
</td>
</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 #1f2937;background:#111827;font-weight:600;color:#e5e7eb">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 #1f2937;background:#111827;font-weight:600;color:#e5e7eb">Service</td>
<td style="padding:8px 10px;border:1px solid #1f2937;background:#111827;font-weight:600;color:#e5e7eb">Expectation</td>
</tr>
{% if host_row and host_row.ports %}
{% 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)) %}
{% 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 '<span style="color:#9ca3af">—</span>')
) %}
{% if not skip_row %}
{% set ns.visible_rows = ns.visible_rows + 1 %}
<tr>
<td style="padding:6px 10px;border:1px solid #1f2937;background:#0b1020">{{ pill_proto(p.protocol) }}</td>
<td style="padding:6px 10px;border:1px solid #1f2937;background:#0b1020;color:#e5e7eb">{{ p.port }}</td>
<td style="padding:6px 10px;border:1px solid #1f2937;background:#0b1020">{{ pill_state(p.state) }}</td>
<td style="padding:6px 10px;border:1px solid #1f2937;background:#0b1020;color:#cbd5e1">{{ p.service or '-' }}</td>
<td style="padding:6px 10px;border:1px solid #1f2937;background:#0b1020">{{ expectation_badge | safe }}</td>
</tr>
{% endif %}
{% endfor %}
{% if ns.visible_rows == 0 %}
<tr>
<td colspan="5" style="padding:8px 10px;border:1px solid #1f2937;background:#0b1020;color:#9ca3af">
No per-port details to display after filtering closed UDP results.
</td>
</tr>
{% endif %}
{% else %}
<tr>
<td colspan="5" style="padding:8px 10px;border:1px solid #1f2937;background:#0b1020;color:#9ca3af">
No per-port details available for this host.
</td>
</tr>
{% endif %}
</table>
{% 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>
<td colspan="5" style="padding:10px 10px 8px 10px;font-size:13px">
{{ badge('OK', '#16a34a') }} &nbsp; <span style="color:#cbd5e1">Matches expected ports.</span>
</td>
</tr>
</table>
{% endfor %}
{% endif %}
<div style="margin-top:18px;font-size:11px;color:#9ca3af">
Report generated by mass-scan-v2 • {{ generated }}
</div>
</div>
</body>
</html>

View File

@@ -1,163 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{{ title }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body style="margin:0;padding:16px;background:#ffffff">
<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">
<tr>
<td style="padding:10px;background:#0f172a;border-radius:10px;color:#e2e8f0">
<div style="font-size:18px;font-weight:700;margin-bottom:4px">{{ title }}</div>
<div style="font-size:12px;color:#94a3b8">Generated: {{ generated }}</div>
</td>
</tr>
</table>
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="margin:0 0 16px 0">
<tr>
<td style="padding:10px;border:1px solid #e5e7eb;border-radius:8px">
Total hosts: <strong>{{ total_hosts }}</strong>&nbsp;
Matching expected: <strong>{{ ok_hosts }}</strong>&nbsp;
With issues: <strong>{{ hosts_with_issues|length }}</strong>
</td>
</tr>
</table>
{% if not only_issues and ok_hosts == total_hosts and total_hosts > 0 %}
<div style="margin:6px 0 12px 0;font-size:12px;color:#64748b">
All hosts matched expected ports.
</div>
{% endif %}
{% if only_issues and hosts_with_issues|length == 0 %}
<div style="margin:8px 0;color:#64748b">
No hosts with issues found. ✅
</div>
{% endif %}
{# ===== Host Sections (LIGHT theme) ===== #}
{# Reusable delta row #}
{% macro delta_row(label, ports) -%}
<tr>
<td colspan="5" style="padding:4px 10px 6px 10px;font-size:13px">
<strong>{{ label }}:</strong> {{ fmt_ports(ports) }}
</td>
</tr>
{%- 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('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) }}
<tr>
<td colspan="5" style="padding:8px 10px 6px 10px;font-size:13px">
<div style="font-weight:600;margin:8px 0">Discovered Ports</div>
</td>
</tr>
<tr>
<td style="padding:8px 10px;border:1px solid #e5e7eb;background:#f8fafc;font-weight:600">Protocol</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 #e5e7eb;background:#f8fafc;font-weight:600">State</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 #e5e7eb;background:#f8fafc;font-weight:600">Expectation</td>
</tr>
{% if host_row and host_row.ports %}
{% 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)) %}
{% 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 '<span style="color:#64748b">—</span>')
) %}
{% if not skip_row %}
{% set ns.visible_rows = ns.visible_rows + 1 %}
<tr>
<td style="padding:6px 10px;border:1px solid #e5e7eb">{{ pill_proto(p.protocol) }}</td>
<td style="padding:6px 10px;border:1px solid #e5e7eb">{{ p.port }}</td>
<td style="padding:6px 10px;border:1px solid #e5e7eb">{{ pill_state(p.state) }}</td>
<td style="padding:6px 10px;border:1px solid #e5e7eb">{{ p.service or '-' }}</td>
<td style="padding:6px 10px;border:1px solid #e5e7eb">{{ expectation_badge | safe }}</td>
</tr>
{% endif %}
{% endfor %}
{% if ns.visible_rows == 0 %}
<tr>
<td colspan="5" style="padding:8px 10px;border:1px solid #e5e7eb;color:#64748b">
No per-port details to display after filtering closed UDP results.
</td>
</tr>
{% endif %}
{% else %}
<tr>
<td colspan="5" style="padding:8px 10px;border:1px solid #e5e7eb;color:#64748b">
No per-port details available for this host.
</td>
</tr>
{% endif %}
</table>
{% 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 '') %}
<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 }}
</div>

View File

@@ -1,159 +1,410 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8" />
<meta name="color-scheme" content="light dark" />
<title>Corporate WAN Perimeter</title> <title>Corporate WAN Perimeter</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
/* Reset-ish */
html, body { margin:0; padding:0; }
body {
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, "Helvetica Neue", Arial, "Noto Sans",
"Liberation Sans", sans-serif, "Apple Color Emoji","Segoe UI Emoji";
background: var(--bg);
color: var(--text);
line-height: 1.45;
font-size: 14px;
}
table { border-collapse: collapse; width: 100%; }
th, td { vertical-align: top; }
/* Include palette */
:root {
--bg: #0b1220; /* slate-950ish */
--text: #e5e7eb; /* slate-200 */
--muted: #94a3b8; /* slate-400 */
--card-bg: #0f172a; /* slate-900 */
--border: #334155; /* slate-700 */
--accent: #38bdf8;
--ok: #16a34a;
--ok-on: #052e16;
--issue: #ef4444;
--issue-on: #450a0a;
--chip-bg: #1f2937; /* gray-800 */
/* Summary bar emphasis */
--summary-bg: #101a33; /* a touch brighter than --card-bg */
--summary-border: #2c3a56; /* slightly brighter border */
--summary-shadow: rgba(0, 0, 0, 0.5);
/* KPI chip fills (derived from status colors) */
--ok-bg-soft: color-mix(in srgb, var(--ok) 18%, transparent);
--issue-bg-soft: color-mix(in srgb, var(--issue) 18%, transparent);
--accent-soft: color-mix(in srgb, var(--accent) 18%, transparent);
/* Port table border accent (dark) */
--card-border: #2b3b5c; /* slightly brighter than main border */
}
/* Layout & components */
.container { max-width: 900px; margin: 24px auto; padding: 0 12px; }
.card {
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: 10px;
padding: 12px;
}
.header {
background: var(--card-bg);
border-radius: 10px;
padding: 12px;
font-weight: 600;
}
.muted { color: var(--muted); }
.dash { color: var(--muted); } /* for em-dash placeholder */
.badge {
display: inline-block;
font-size: 12px;
font-weight: 700;
padding: 2px 8px;
border-radius: 999px;
line-height: 1.6;
border: 1px solid transparent;
margin-left: 6px;
}
.badge.ok { background: var(--ok); color: var(--ok-on); }
.badge.issue { background: var(--issue); color: var(--issue-on); }
.chip { display:inline-block; background: var(--chip-bg); padding: 2px 6px; border-radius: 6px; }
.section { margin: 0 0 18px 0; }
.table-clean { width:100%; }
.table-clean td { padding: 8px 10px; }
.host-title { font-size: 14px; }
.summary-row { padding: 10px; }
/* Utility helpers referenced by report_body.html.j2 */
.title-xl { font-size: 20px; font-weight: 700; margin-bottom: 4px; }
.meta { font-size: 12px; }
.note { margin: 6px 0 12px 0; font-size: 12px; }
.subhead { font-weight: 600; margin: 8px 0; }
.pad-s { padding: 10px; }
.footer { margin-top: 18px; font-size: 11px; }
/* ===== Summary Bar (prominent) ===== */
.summary-row {
position: relative;
background: var(--summary-bg, var(--card-bg));
border: 1px solid var(--summary-border, var(--border));
border-radius: 12px;
padding: 14px 16px;
font-size: 14px;
color: var(--text);
box-shadow:
0 0 0 1px var(--summary-border, var(--border)) inset,
0 8px 24px var(--summary-shadow, rgba(0,0,0,0.25));
}
/* left accent bar */
.summary-row::before {
content: "";
position: absolute;
left: 0; top: 0; bottom: 0;
width: 6px;
border-radius: 12px 0 0 12px;
background: linear-gradient(
180deg,
color-mix(in srgb, var(--accent) 90%, transparent) 0%,
color-mix(in srgb, var(--accent) 60%, transparent) 100%
);
opacity: 0.9;
}
/* tighten spacing between metrics */
.summary-row strong { margin-right: 8px; }
/* KPI “chips” for the numbers (no HTML changes needed) */
.text-strong,
.text-ok,
.text-issue {
display: inline-block;
padding: 2px 8px;
border-radius: 999px;
font-weight: 700;
line-height: 1.6;
border: 1px solid transparent;
font-variant-numeric: tabular-nums;
}
/* Neutral total */
.text-strong {
background: var(--accent-soft, color-mix(in srgb, var(--accent) 12%, transparent));
border-color: color-mix(in srgb, var(--accent) 45%, var(--summary-border, var(--border)));
}
/* OK / Issue map to your existing status colors */
.text-ok {
color: var(--text);
background: var(--ok-bg-soft, color-mix(in srgb, var(--ok) 18%, transparent));
border-color: color-mix(in srgb, var(--ok) 45%, var(--summary-border, var(--border)));
}
.text-issue {
color: var(--text);
background: var(--issue-bg-soft, color-mix(in srgb, var(--issue) 18%, transparent));
border-color: color-mix(in srgb, var(--issue) 45%, var(--summary-border, var(--border)));
}
/* ===== Port Table Card (bordered) ===== */
.table-ports {
position: relative;
margin-bottom: 24px;
border: 1px solid var(--card-border, var(--border));
border-radius: 12px;
background: var(--card-bg);
box-shadow:
0 0 0 1px var(--card-border, var(--border)) inset,
0 4px 12px rgba(0,0,0,0.25);
overflow: hidden;
}
.table-ports th,
.table-ports td {
border: 1px solid var(--border);
padding: 6px 10px;
}
.table-ports .th {
background: var(--bg);
color: var(--text);
font-weight: 600;
font-size: 13px;
border-bottom: 2px solid var(--border);
}
.table-ports .td {
font-size: 13px;
}
.table-ports .td.num { white-space: nowrap; }
.table-ports .td.proto,
.table-ports .td.state,
.table-ports .td.expect {
text-align: center;
}
/* optional: subtle row separation */
.table-ports tr:nth-child(even) {
background: color-mix(in srgb, var(--card-bg) 92%, var(--border) 8%);
}
/* ===== Host Header Row (within table-ports) ===== */
.table-ports .header {
position: relative;
background: var(--header-bg, var(--card-bg));
color: var(--header-text, var(--text));
font-weight: 700;
font-size: 14px;
border-radius: 10px 10px 0 0;
padding: 12px 14px;
border-bottom: 2px solid var(--header-border, var(--border));
text-shadow: 0 1px 2px rgba(0,0,0,0.25);
box-shadow: 0 2px 8px var(--header-shadow, rgba(0,0,0,0.2));
letter-spacing: 0.2px;
}
/* Subtle left accent stripe (matches summary style) */
.table-ports .header::before {
content: "";
position: absolute;
top: 0; bottom: 0; left: 0;
width: 5px;
border-radius: 10px 0 0 0;
background: linear-gradient(
180deg,
color-mix(in srgb, var(--accent) 85%, transparent) 0%,
color-mix(in srgb, var(--accent) 55%, transparent) 100%
);
}
</style>
</head> </head>
<body style="margin:0;padding:16px;background:#ffffff"> <body>
<div style="max-width:860px;margin:0 auto;font-family:Segoe UI,Arial,sans-serif"> <div class="container">
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="margin:0 0 12px 0">
<tr>
<td style="padding:10px;background:#0f172a;border-radius:10px;color:#e2e8f0">
<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 02:33:35</div>
</td>
</tr>
</table>
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="margin:0 0 16px 0"> <table role="presentation" class="section table-clean">
<tr>
<td class="card">
<div class="title-xl">Corporate WAN Perimeter</div>
<div class="meta muted">Generated: 2025-10-22 04:02:59</div>
</td>
</tr>
</table>
<table role="presentation" class="section table-clean">
<tr>
<td class="card summary-row">
Total hosts:
<strong class="text-strong">3</strong>&nbsp;&nbsp;
Matching expected:
<strong class="text-ok">2</strong>&nbsp;&nbsp;
With issues:
<strong class="text-issue">1</strong>
</td>
</tr>
</table>
<table role="presentation" class="section table-clean table-ports">
<tr> <tr>
<td style="padding:10px;border:1px solid #e5e7eb;border-radius:8px"> <td colspan="5" class="header">
Total hosts: <strong>3</strong>&nbsp; 10.10.20.12 <span class="badge issue">ISSUES</span>
Matching expected: <strong>2</strong>&nbsp;
With issues: <strong>1</strong>
</td> </td>
</tr> </tr>
<tr>
<td colspan="5" class="delta-row">
<strong>Unexpected TCP open ports:</strong>
<span class="muted">3128</span>
</td>
</tr>
<tr>
<td colspan="5" class="delta-row">
<strong>Expected TCP ports not seen:</strong>
<span class="muted">none</span>
</td>
</tr>
<tr>
<td colspan="5" class="delta-row">
<strong>Unexpected UDP open ports:</strong>
<span class="muted">none</span>
</td>
</tr>
<tr>
<td colspan="5" class="delta-row">
<strong>Expected UDP ports not seen:</strong>
<span class="muted">none</span>
</td>
</tr>
<tr>
<td colspan="5" class="pad-s">
<div class="subhead">Discovered Ports</div>
</td>
</tr>
<tr>
<td class="th">Protocol</td>
<td class="th">Port</td>
<td class="th">State</td>
<td class="th">Service</td>
<td class="th">Expectation</td>
</tr>
<tr>
<td class="td proto"><span class="chip">tcp</span></td>
<td class="td num">22</td>
<td class="td state"><span class="badge ok">open</span></td>
<td class="td svc">ssh</td>
<td class="td expect"><span class="badge ok">Expected</span></td>
</tr>
<tr>
<td class="td proto"><span class="chip">tcp</span></td>
<td class="td num">111</td>
<td class="td state"><span class="badge ok">open</span></td>
<td class="td svc">rpcbind</td>
<td class="td expect"><span class="badge ok">Expected</span></td>
</tr>
<tr>
<td class="td proto"><span class="chip">tcp</span></td>
<td class="td num">3128</td>
<td class="td state"><span class="badge ok">open</span></td>
<td class="td svc">squid-http</td>
<td class="td expect"><span class="badge issue">Issue</span></td>
</tr>
<tr>
<td class="td proto"><span class="chip">tcp</span></td>
<td class="td num">8006</td>
<td class="td state"><span class="badge ok">open</span></td>
<td class="td svc">wpl-analytics</td>
<td class="td expect"><span class="badge ok">Expected</span></td>
</tr>
<tr>
<td class="td proto"><span class="chip">udp</span></td>
<td class="td num">111</td>
<td class="td state"><span class="badge ok">open</span></td>
<td class="td svc">rpcbind</td>
<td class="td expect"><span class="badge ok">Expected</span></td>
</tr>
</table> </table>
<table role="presentation" class="section table-clean">
<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:#0f172a;color:#e2e8f0;font-weight:600;font-size:14px;border-radius:8px"> <td colspan="5" class="header">
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> 10.10.20.4 <span class="badge ok">OK</span>
</td> </td>
</tr> </tr>
<tr> <tr>
<td colspan="5" style="padding:10px 10px 6px 10px;font-size:13px"> <td colspan="5" class="pad-s">
<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> <span class="badge ok">OK</span>
&nbsp; <span class="muted">Matches expected ports.</span>
</td> </td>
</tr> </tr>
<tr>
<td colspan="5" style="padding:4px 10px 6px 10px;font-size:13px">
<strong>Unexpected TCP open ports:</strong> 3128
</td>
</tr>
<tr>
<td colspan="5" style="padding:4px 10px 6px 10px;font-size:13px">
<strong>Expected TCP ports not seen:</strong> none
</td>
</tr>
<tr>
<td colspan="5" style="padding:4px 10px 6px 10px;font-size:13px">
<strong>Unexpected UDP open ports:</strong> none
</td>
</tr>
<tr>
<td colspan="5" style="padding:4px 10px 6px 10px;font-size:13px">
<strong>Expected UDP ports not seen:</strong> none
</td>
</tr>
<tr>
<td colspan="5" style="padding:8px 10px 6px 10px;font-size:13px">
<div style="font-weight:600;margin:8px 0">Discovered Ports</div>
</td>
</tr>
<tr>
<td style="padding:8px 10px;border:1px solid #e5e7eb;background:#f8fafc;font-weight:600">Protocol</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 #e5e7eb;background:#f8fafc;font-weight:600">State</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 #e5e7eb;background:#f8fafc;font-weight:600">Expectation</td>
</tr>
<tr>
<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 #e5e7eb">22</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 #e5e7eb">ssh</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>
<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 #e5e7eb">111</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 #e5e7eb">rpcbind</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>
<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 #e5e7eb">3128</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 #e5e7eb">squid-http</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>
<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 #e5e7eb">8006</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 #e5e7eb">wpl-analytics</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>
<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 #e5e7eb">111</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 #e5e7eb">rpcbind</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>
</table> </table>
<table role="presentation" class="section table-clean">
<tr>
<td colspan="5" class="header">
10.10.20.5 <span class="badge ok">OK</span>
</td>
</tr>
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="margin:0 0 18px 0"> <tr>
<tr> <td colspan="5" class="pad-s">
<td colspan="5" style="padding:12px 10px;background:#0f172a;color:#e2e8f0;font-weight:600;font-size:14px;border-radius:8px"> <span class="badge ok">OK</span>
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> &nbsp; <span class="muted">Matches expected ports.</span>
</td> </td>
</tr> </tr>
<tr> </table>
<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; Matches expected ports.
</td>
</tr>
</table>
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="margin:0 0 18px 0"> <div class="footer muted">
<tr> Report generated by mass-scan-v2 • 2025-10-22 04:02:59
<td colspan="5" style="padding:12px 10px;background:#0f172a;color:#e2e8f0;font-weight:600;font-size:14px;border-radius:8px"> </div>
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>
</tr>
<tr>
<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; Matches expected ports.
</td>
</tr>
</table>
<div style="margin-top:18px;font-size:11px;color:#94a3b8"> </div>
Report generated by mass-scan-v2 • 2025-10-22 02:33:35 </body>
</div> </html>

View File

@@ -9,7 +9,7 @@ reporting:
report_name: Corporate WAN Perimeter report_name: Corporate WAN Perimeter
report_filename: corp-wan.html report_filename: corp-wan.html
full_details: true full_details: true
dark_mode: false dark_mode: true
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

View File

@@ -8,6 +8,7 @@ reporting:
report_name: Corporate WAN Perimeter # Report Name report_name: Corporate WAN Perimeter # Report Name
report_filename: corp-wan.html # Report Filename report_filename: corp-wan.html # Report Filename
full_details: true # Show full details for ALL hosts (if nothing out of the ordinary is expected, still show ports) full_details: true # Show full details for ALL hosts (if nothing out of the ordinary is expected, still show ports)
dark_mode: true # Report generated will be in Dark colors or light colors theme
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

BIN
data/targets.xlsx Normal file

Binary file not shown.