template refactor
This commit is contained in:
220
app/templates/base_report.html.j2
Normal file
220
app/templates/base_report.html.j2
Normal 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>
|
||||
30
app/templates/palettes/dark.css.j2
Normal file
30
app/templates/palettes/dark.css.j2
Normal 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 */
|
||||
}
|
||||
35
app/templates/palettes/light.css.j2
Normal file
35
app/templates/palettes/light.css.j2
Normal 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 */
|
||||
}
|
||||
164
app/templates/report_body.html.j2
Normal file
164
app/templates/report_body.html.j2
Normal 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>
|
||||
Matching expected:
|
||||
<strong class="text-ok">{{ ok_hosts }}</strong>
|
||||
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>
|
||||
<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 %}
|
||||
@@ -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>
|
||||
Matching expected:
|
||||
<strong style="color:#4ade80">{{ ok_hosts }}</strong>
|
||||
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') }} <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>
|
||||
@@ -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>
|
||||
Matching expected: <strong>{{ ok_hosts }}</strong>
|
||||
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') }} 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>
|
||||
Reference in New Issue
Block a user