Refactor web_scan.py for improved readability and maintainability; add cron job script for npm log backups; update start.ps1 for consistent formatting.

This commit is contained in:
zwitschi committed 2026-09-08 19:49:11 +02:00
1 parent 0d1cd21468
commit c2eba88ac8
113 files changed
+772606 -772579

No files matched your search

+50 -50
View File
@@ -1,50 +1,50 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{% block title %}Intrusion Detection{% endblock %}</title>
<link
rel="stylesheet"
href="{{ url_for('static', filename='style.css') }}"
/>
</head>
<body>
<header class="topbar">
<div class="brand">🛡 Intrusion Detection</div>
<nav>
<a href="/" class="{{ 'active' if request.path == '/' }}">Dashboard</a>
<a href="/scan" class="{{ 'active' if request.path == '/scan' }}"
>Scan</a
>
<a href="/reports" class="{{ 'active' if request.path == '/reports' }}"
>Reports</a
>
<a
href="/blocklist"
class="{{ 'active' if request.path == '/blocklist' }}"
>Blocklist</a
>
<a
href="/firewall"
class="{{ 'active' if request.path == '/firewall' }}"
>Firewall</a
>
<a
href="/whitelist"
class="{{ 'active' if request.path == '/whitelist' }}"
>Whitelist</a
>
<a
href="/graph"
class="{{ 'active' if request.path == '/graph' }}"
>Graph</a
>
</nav>
</header>
<main class="container">{% block content %}{% endblock %}</main>
<div id="toasts"></div>
<script src="{{ url_for('static', filename='app.js') }}"></script>
{% block scripts %}{% endblock %}
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{% block title %}Intrusion Detection{% endblock %}</title>
<link
rel="stylesheet"
href="{{ url_for('static', filename='style.css') }}"
/>
</head>
<body>
<header class="topbar">
<div class="brand">🛡 Intrusion Detection</div>
<nav>
<a href="/" class="{{ 'active' if request.path == '/' }}">Dashboard</a>
<a href="/scan" class="{{ 'active' if request.path == '/scan' }}"
>Scan</a
>
<a href="/reports" class="{{ 'active' if request.path == '/reports' }}"
>Reports</a
>
<a
href="/blocklist"
class="{{ 'active' if request.path == '/blocklist' }}"
>Blocklist</a
>
<a
href="/firewall"
class="{{ 'active' if request.path == '/firewall' }}"
>Firewall</a
>
<a
href="/whitelist"
class="{{ 'active' if request.path == '/whitelist' }}"
>Whitelist</a
>
<a
href="/graph"
class="{{ 'active' if request.path == '/graph' }}"
>Graph</a
>
</nav>
</header>
<main class="container">{% block content %}{% endblock %}</main>
<div id="toasts"></div>
<script src="{{ url_for('static', filename='app.js') }}"></script>
{% block scripts %}{% endblock %}
</body>
</html>
+134 -134
View File
@@ -1,134 +1,134 @@
{% extends "base.html" %} {% block title %}Blocklist — Intrusion Detection{%
endblock %} {% block content %}
<h1>Blocklist</h1>
<p class="muted">
Current consolidated blocklist (from the latest scan). Adding/removing touches
the MikroTik router.
</p>
<div class="panel">
<div class="panel-head"><h2>Add / remove entry</h2></div>
<div class="panel-actions" style="margin-top: 0">
<input
id="ip"
placeholder="IP or CIDR, e.g. 1.2.3.4 or 1.2.0.0/16"
style="min-width: 260px"
/>
<button class="btn primary" id="add-btn">Add to Blocked</button>
<button class="btn danger" id="remove-btn">Remove from Blocked</button>
</div>
</div>
<div class="panel">
<div class="panel-head">
<h2>IPv4 <span id="v4-count"></span></h2>
</div>
<table class="table">
<thead>
<tr>
<th>Address</th>
<th></th>
</tr>
</thead>
<tbody id="v4"></tbody>
</table>
</div>
<div class="panel">
<div class="panel-head">
<h2>IPv6 <span id="v6-count"></span></h2>
</div>
<table class="table">
<thead>
<tr>
<th>Address</th>
<th></th>
</tr>
</thead>
<tbody id="v6"></tbody>
</table>
</div>
{% endblock %} {% block scripts %}
<script>
async function loadBlocklist() {
try {
const data = await api("/api/blocklist");
render("v4", data.ipv4 || [], "v4-count");
render("v6", data.ipv6 || [], "v6-count");
} catch (e) {
toast(e.message, "error");
}
}
function render(tbodyId, entries, countId) {
const body = document.getElementById(tbodyId);
body.innerHTML = "";
document.getElementById(countId).textContent = `(${entries.length})`;
if (!entries.length) {
body.innerHTML =
'<tr><td colspan="2" class="muted">No entries.</td></tr>';
return;
}
for (const ip of entries) {
body.append(
el(
"tr",
el("td", { class: "mono", text: ip }),
el(
"td",
{ style: "text-align:right" },
el("button", {
class: "btn danger",
text: "Remove",
onclick: () => remove(ip),
}),
),
),
);
}
}
async function act(action) {
const ip = document.getElementById("ip").value.trim();
if (!ip) {
toast("Enter an IP or CIDR", "warn");
return;
}
try {
const data = await api(`/api/blocklist/${action}`, {
method: "POST",
body: { ip },
});
toast(
`Removed ${ip}`.replace(
"Removed",
action === "add" ? "Added" : "Removed",
),
"ok",
);
document.getElementById("ip").value = "";
loadBlocklist();
} catch (e) {
toast(e.message, "error");
}
}
async function remove(ip) {
try {
await api("/api/blocklist/remove", { method: "POST", body: { ip } });
toast(`Removed ${ip}`, "ok");
loadBlocklist();
} catch (e) {
toast(e.message, "error");
}
}
document
.getElementById("add-btn")
.addEventListener("click", () => act("add"));
document
.getElementById("remove-btn")
.addEventListener("click", () => act("remove"));
loadBlocklist();
</script>
{% endblock %}
{% extends "base.html" %} {% block title %}Blocklist — Intrusion Detection{%
endblock %} {% block content %}
<h1>Blocklist</h1>
<p class="muted">
Current consolidated blocklist (from the latest scan). Adding/removing touches
the MikroTik router.
</p>
<div class="panel">
<div class="panel-head"><h2>Add / remove entry</h2></div>
<div class="panel-actions" style="margin-top: 0">
<input
id="ip"
placeholder="IP or CIDR, e.g. 1.2.3.4 or 1.2.0.0/16"
style="min-width: 260px"
/>
<button class="btn primary" id="add-btn">Add to Blocked</button>
<button class="btn danger" id="remove-btn">Remove from Blocked</button>
</div>
</div>
<div class="panel">
<div class="panel-head">
<h2>IPv4 <span id="v4-count"></span></h2>
</div>
<table class="table">
<thead>
<tr>
<th>Address</th>
<th></th>
</tr>
</thead>
<tbody id="v4"></tbody>
</table>
</div>
<div class="panel">
<div class="panel-head">
<h2>IPv6 <span id="v6-count"></span></h2>
</div>
<table class="table">
<thead>
<tr>
<th>Address</th>
<th></th>
</tr>
</thead>
<tbody id="v6"></tbody>
</table>
</div>
{% endblock %} {% block scripts %}
<script>
async function loadBlocklist() {
try {
const data = await api("/api/blocklist");
render("v4", data.ipv4 || [], "v4-count");
render("v6", data.ipv6 || [], "v6-count");
} catch (e) {
toast(e.message, "error");
}
}
function render(tbodyId, entries, countId) {
const body = document.getElementById(tbodyId);
body.innerHTML = "";
document.getElementById(countId).textContent = `(${entries.length})`;
if (!entries.length) {
body.innerHTML =
'<tr><td colspan="2" class="muted">No entries.</td></tr>';
return;
}
for (const ip of entries) {
body.append(
el(
"tr",
el("td", { class: "mono", text: ip }),
el(
"td",
{ style: "text-align:right" },
el("button", {
class: "btn danger",
text: "Remove",
onclick: () => remove(ip),
}),
),
),
);
}
}
async function act(action) {
const ip = document.getElementById("ip").value.trim();
if (!ip) {
toast("Enter an IP or CIDR", "warn");
return;
}
try {
const data = await api(`/api/blocklist/${action}`, {
method: "POST",
body: { ip },
});
toast(
`Removed ${ip}`.replace(
"Removed",
action === "add" ? "Added" : "Removed",
),
"ok",
);
document.getElementById("ip").value = "";
loadBlocklist();
} catch (e) {
toast(e.message, "error");
}
}
async function remove(ip) {
try {
await api("/api/blocklist/remove", { method: "POST", body: { ip } });
toast(`Removed ${ip}`, "ok");
loadBlocklist();
} catch (e) {
toast(e.message, "error");
}
}
document
.getElementById("add-btn")
.addEventListener("click", () => act("add"));
document
.getElementById("remove-btn")
.addEventListener("click", () => act("remove"));
loadBlocklist();
</script>
{% endblock %}
+102 -102
View File
@@ -1,102 +1,102 @@
{% extends "base.html" %} {% block title %}Dashboard — Intrusion Detection{%
endblock %} {% block content %}
<h1>Dashboard</h1>
<p class="muted">
Localhost-only control panel for the intrusion detection scanner.
</p>
<div class="cards" id="status-cards"></div>
<div class="panel">
<div class="panel-head">
<h2>Latest scans</h2>
<a class="btn ghost" href="/scan">Run a scan →</a>
</div>
<table class="table">
<thead>
<tr>
<th>Job</th>
<th>Kind</th>
<th>State</th>
<th>Started</th>
<th>Finished</th>
<th class="num">Blocked</th>
</tr>
</thead>
<tbody id="latest-scans"></tbody>
</table>
</div>
{% endblock %} {% block scripts %}
<script>
async function loadDashboard() {
try {
const s = await api("/api/status");
renderCards(s);
renderScans(s.latest_scans || []);
} catch (e) {
toast(e.message, "error");
}
}
function renderCards(s) {
const cards = [
{
label: "Scan running",
value: s.scan_running ? "Yes" : "No",
tone: s.scan_running ? "warn" : "ok",
},
{ label: "Blocklist IPv4", value: s.blocklist_ipv4_count ?? "—" },
{ label: "Blocklist IPv6", value: s.blocklist_ipv6_count ?? "—" },
{
label: "Firewall snapshot",
value:
s.firewall_snapshot && s.firewall_snapshot.exists
? "Present"
: "None",
tone:
s.firewall_snapshot && s.firewall_snapshot.exists ? "ok" : "muted",
},
];
const box = document.getElementById("status-cards");
box.innerHTML = "";
for (const c of cards) {
box.append(
el(
"div",
{ class: "card" },
el("div", { class: "card-label", text: c.label }),
el("div", { class: `card-value ${c.tone || ""}`, text: c.value }),
),
);
}
}
function renderScans(scans) {
const body = document.getElementById("latest-scans");
body.innerHTML = "";
if (!scans.length) {
body.innerHTML =
'<tr><td colspan="6" class="muted">No scans yet — run one from the Scan page.</td></tr>';
return;
}
for (const j of scans) {
const r = j.result || {};
body.append(
el(
"tr",
el("td", { class: "mono", text: j.job_id }),
el("td", { text: j.kind }),
el("td", {}, badge(j.state)),
el("td", { text: fmtTime(j.started) }),
el("td", { text: fmtTime(j.finished) }),
el("td", { class: "num", text: r.blocked_targets ?? "—" }),
),
);
}
}
loadDashboard();
// Keep the latest-scans table fresh without hammering /api/scan/status.
setInterval(loadDashboard, 15000);
</script>
{% endblock %}
{% extends "base.html" %} {% block title %}Dashboard — Intrusion Detection{%
endblock %} {% block content %}
<h1>Dashboard</h1>
<p class="muted">
Localhost-only control panel for the intrusion detection scanner.
</p>
<div class="cards" id="status-cards"></div>
<div class="panel">
<div class="panel-head">
<h2>Latest scans</h2>
<a class="btn ghost" href="/scan">Run a scan →</a>
</div>
<table class="table">
<thead>
<tr>
<th>Job</th>
<th>Kind</th>
<th>State</th>
<th>Started</th>
<th>Finished</th>
<th class="num">Blocked</th>
</tr>
</thead>
<tbody id="latest-scans"></tbody>
</table>
</div>
{% endblock %} {% block scripts %}
<script>
async function loadDashboard() {
try {
const s = await api("/api/status");
renderCards(s);
renderScans(s.latest_scans || []);
} catch (e) {
toast(e.message, "error");
}
}
function renderCards(s) {
const cards = [
{
label: "Scan running",
value: s.scan_running ? "Yes" : "No",
tone: s.scan_running ? "warn" : "ok",
},
{ label: "Blocklist IPv4", value: s.blocklist_ipv4_count ?? "—" },
{ label: "Blocklist IPv6", value: s.blocklist_ipv6_count ?? "—" },
{
label: "Firewall snapshot",
value:
s.firewall_snapshot && s.firewall_snapshot.exists
? "Present"
: "None",
tone:
s.firewall_snapshot && s.firewall_snapshot.exists ? "ok" : "muted",
},
];
const box = document.getElementById("status-cards");
box.innerHTML = "";
for (const c of cards) {
box.append(
el(
"div",
{ class: "card" },
el("div", { class: "card-label", text: c.label }),
el("div", { class: `card-value ${c.tone || ""}`, text: c.value }),
),
);
}
}
function renderScans(scans) {
const body = document.getElementById("latest-scans");
body.innerHTML = "";
if (!scans.length) {
body.innerHTML =
'<tr><td colspan="6" class="muted">No scans yet — run one from the Scan page.</td></tr>';
return;
}
for (const j of scans) {
const r = j.result || {};
body.append(
el(
"tr",
el("td", { class: "mono", text: j.job_id }),
el("td", { text: j.kind }),
el("td", {}, badge(j.state)),
el("td", { text: fmtTime(j.started) }),
el("td", { text: fmtTime(j.finished) }),
el("td", { class: "num", text: r.blocked_targets ?? "—" }),
),
);
}
}
loadDashboard();
// Keep the latest-scans table fresh without hammering /api/scan/status.
setInterval(loadDashboard, 15000);
</script>
{% endblock %}
+176 -176
View File
@@ -1,176 +1,176 @@
{% extends "base.html" %} {% block title %}Firewall — Intrusion Detection{%
endblock %} {% block content %}
<h1>Firewall</h1>
<p class="muted">
Current MikroTik state (from the latest <code>data/mikrotik</code> snapshot).
Sync previews or applies the blocklist diff.
</p>
<div class="panel">
<div class="panel-actions" style="margin-top: 0">
<button class="btn" id="preview-btn">Preview sync (dry-run)</button>
<button class="btn danger" id="apply-btn">Apply sync</button>
<span class="muted" id="fw-result"></span>
</div>
</div>
<div class="panel">
<div class="panel-head"><h2>Address lists</h2></div>
<div id="address-lists"></div>
</div>
<div class="panel">
<div class="panel-head"><h2>Firewall rules</h2></div>
<div id="firewall-rules"></div>
</div>
{% endblock %} {% block scripts %}
<script>
async function loadState() {
try {
const s = await api("/api/firewall/state");
renderAddressLists(s.address_lists || {});
renderRules("firewall-rules", s.firewall_rules || {});
} catch (e) {
toast(e.message, "error");
}
}
function renderAddressLists(addr) {
const box = document.getElementById("address-lists");
box.innerHTML = "";
const byList = {};
for (const version of ["ipv4", "ipv6"]) {
for (const e of addr[version] || []) {
const name = e.list || "?";
(byList[name] = byList[name] || { ipv4: [], ipv6: [] })[version].push(
e.address,
);
}
}
const names = Object.keys(byList);
if (!names.length) {
box.innerHTML =
'<p class="muted">No address-list snapshot available. Run <code>app.py --update-firewall</code> first.</p>';
return;
}
for (const name of names) {
const rows = [];
for (const version of ["ipv4", "ipv6"]) {
const entries = (byList[name][version] || []).sort();
rows.push(
el(
"tr",
el("td", { text: version }),
el("td", { class: "mono", text: entries.join(", ") || "—" }),
),
);
}
box.append(
el(
"div",
{ style: "margin-bottom:1rem" },
el("div", {
style: "font-weight:600;margin-bottom:.4rem",
text: name,
}),
el(
"table",
{ class: "table" },
el(
"thead",
{},
el(
"tr",
{},
el("th", { text: "Version" }),
el("th", { text: "Addresses" }),
),
),
el("tbody", {}, ...rows),
),
),
);
}
}
function renderRules(boxId, rules) {
const box = document.getElementById(boxId);
box.innerHTML = "";
for (const version of ["ipv4", "ipv6"]) {
const list = rules[version] || [];
const rows = list.map((r) =>
el(
"tr",
el("td", {
class: "mono",
text: r.address ? r.address : r["src-address-list"] || "",
}),
el("td", { text: r.chain || "—" }),
el("td", { text: r.action || "—" }),
el("td", { text: r.comment || "" }),
),
);
box.append(
el(
"div",
{ style: "margin-bottom:1rem" },
el("div", {
style: "font-weight:600;margin-bottom:.4rem",
text: `IPv${version} (${list.length})`,
}),
rows.length
? el(
"table",
{ class: "table" },
el(
"thead",
{},
el(
"tr",
{},
el("th", { text: "Address / List" }),
el("th", { text: "Chain" }),
el("th", { text: "Action" }),
el("th", { text: "Comment" }),
),
),
el("tbody", {}, ...rows),
)
: el("p", { class: "muted", text: "No rules in snapshot." }),
),
);
}
}
async function sync(dryRun) {
const out = document.getElementById("fw-result");
out.textContent = "Working…";
try {
const changes = await api(`/api/firewall/${dryRun ? "sync" : "apply"}`, {
method: "POST",
body: {},
});
const verb = dryRun ? "Would" : "";
out.textContent =
`${verb} add ${(changes.added_ipv4 || []).length} IPv4 + ${(changes.added_ipv6 || []).length} IPv6; ` +
`remove ${(changes.removed_ipv4 || []).length} IPv4 + ${(changes.removed_ipv6 || []).length} IPv6.`;
toast(
(dryRun ? "Preview" : "Applied") + " firewall sync",
dryRun ? "warn" : "ok",
);
if (!dryRun) loadState();
} catch (e) {
out.textContent = "";
toast(e.message, "error");
}
}
document
.getElementById("preview-btn")
.addEventListener("click", () => sync(true));
document
.getElementById("apply-btn")
.addEventListener("click", () => sync(false));
loadState();
</script>
{% endblock %}
{% extends "base.html" %} {% block title %}Firewall — Intrusion Detection{%
endblock %} {% block content %}
<h1>Firewall</h1>
<p class="muted">
Current MikroTik state (from the latest <code>data/mikrotik</code> snapshot).
Sync previews or applies the blocklist diff.
</p>
<div class="panel">
<div class="panel-actions" style="margin-top: 0">
<button class="btn" id="preview-btn">Preview sync (dry-run)</button>
<button class="btn danger" id="apply-btn">Apply sync</button>
<span class="muted" id="fw-result"></span>
</div>
</div>
<div class="panel">
<div class="panel-head"><h2>Address lists</h2></div>
<div id="address-lists"></div>
</div>
<div class="panel">
<div class="panel-head"><h2>Firewall rules</h2></div>
<div id="firewall-rules"></div>
</div>
{% endblock %} {% block scripts %}
<script>
async function loadState() {
try {
const s = await api("/api/firewall/state");
renderAddressLists(s.address_lists || {});
renderRules("firewall-rules", s.firewall_rules || {});
} catch (e) {
toast(e.message, "error");
}
}
function renderAddressLists(addr) {
const box = document.getElementById("address-lists");
box.innerHTML = "";
const byList = {};
for (const version of ["ipv4", "ipv6"]) {
for (const e of addr[version] || []) {
const name = e.list || "?";
(byList[name] = byList[name] || { ipv4: [], ipv6: [] })[version].push(
e.address,
);
}
}
const names = Object.keys(byList);
if (!names.length) {
box.innerHTML =
'<p class="muted">No address-list snapshot available. Run <code>app.py --update-firewall</code> first.</p>';
return;
}
for (const name of names) {
const rows = [];
for (const version of ["ipv4", "ipv6"]) {
const entries = (byList[name][version] || []).sort();
rows.push(
el(
"tr",
el("td", { text: version }),
el("td", { class: "mono", text: entries.join(", ") || "—" }),
),
);
}
box.append(
el(
"div",
{ style: "margin-bottom:1rem" },
el("div", {
style: "font-weight:600;margin-bottom:.4rem",
text: name,
}),
el(
"table",
{ class: "table" },
el(
"thead",
{},
el(
"tr",
{},
el("th", { text: "Version" }),
el("th", { text: "Addresses" }),
),
),
el("tbody", {}, ...rows),
),
),
);
}
}
function renderRules(boxId, rules) {
const box = document.getElementById(boxId);
box.innerHTML = "";
for (const version of ["ipv4", "ipv6"]) {
const list = rules[version] || [];
const rows = list.map((r) =>
el(
"tr",
el("td", {
class: "mono",
text: r.address ? r.address : r["src-address-list"] || "",
}),
el("td", { text: r.chain || "—" }),
el("td", { text: r.action || "—" }),
el("td", { text: r.comment || "" }),
),
);
box.append(
el(
"div",
{ style: "margin-bottom:1rem" },
el("div", {
style: "font-weight:600;margin-bottom:.4rem",
text: `IPv${version} (${list.length})`,
}),
rows.length
? el(
"table",
{ class: "table" },
el(
"thead",
{},
el(
"tr",
{},
el("th", { text: "Address / List" }),
el("th", { text: "Chain" }),
el("th", { text: "Action" }),
el("th", { text: "Comment" }),
),
),
el("tbody", {}, ...rows),
)
: el("p", { class: "muted", text: "No rules in snapshot." }),
),
);
}
}
async function sync(dryRun) {
const out = document.getElementById("fw-result");
out.textContent = "Working…";
try {
const changes = await api(`/api/firewall/${dryRun ? "sync" : "apply"}`, {
method: "POST",
body: {},
});
const verb = dryRun ? "Would" : "";
out.textContent =
`${verb} add ${(changes.added_ipv4 || []).length} IPv4 + ${(changes.added_ipv6 || []).length} IPv6; ` +
`remove ${(changes.removed_ipv4 || []).length} IPv4 + ${(changes.removed_ipv6 || []).length} IPv6.`;
toast(
(dryRun ? "Preview" : "Applied") + " firewall sync",
dryRun ? "warn" : "ok",
);
if (!dryRun) loadState();
} catch (e) {
out.textContent = "";
toast(e.message, "error");
}
}
document
.getElementById("preview-btn")
.addEventListener("click", () => sync(true));
document
.getElementById("apply-btn")
.addEventListener("click", () => sync(false));
loadState();
</script>
{% endblock %}
+227 -227
View File
@@ -1,227 +1,227 @@
{% extends "base.html" %} {% block title %}Graph — Intrusion Detection{%
endblock %} {% block content %}
<h1>IPv4 address space map</h1>
<p class="muted">
One pixel per /24 network, arranged as 16×16 tiles of /8 (first octet).
Blocked targets are red, whitelisted ranges are green.
</p>
<div class="panel">
<div class="panel-head">
<div class="graph-controls">
<label class="check" style="margin-top: 0">
<input type="checkbox" id="show-whitelisted" checked />
Show whitelisted
</label>
<span class="legend">
<span class="sw red"></span> Blocked
<span class="sw green"></span> Whitelisted
<span class="sw plain"></span> Unlisted
</span>
</div>
<button class="btn ghost" id="graph-refresh">↻ Refresh</button>
</div>
<div class="graph-wrap" id="graph-wrap">
<canvas id="graph-canvas" width="4096" height="4096"></canvas>
<div class="graph-overlay" id="graph-overlay"></div>
<div class="graph-tooltip hidden" id="graph-tooltip"></div>
</div>
<p class="muted mono" id="graph-stats">Loading…</p>
</div>
{% endblock %} {% block scripts %}
<script>
// Layout mirrors plot.py: 16x16 grid of /8 tiles, each 256x256 px (one pixel
// per /24). Blocked ranges override whitelisted ranges.
const TILE = 16;
const SIZE = 256;
const TOTAL = TILE * SIZE; // 4096
const V_UNLISTED = 0;
const V_WHITELISTED = 1;
const V_BLOCKED = 2;
const COLOR_WHITELISTED = "#43a047";
const COLOR_BLOCKED = "#e53935";
const canvas = document.getElementById("graph-canvas");
const ctx = canvas.getContext("2d");
const overlay = document.getElementById("graph-overlay");
const tooltip = document.getElementById("graph-tooltip");
const stats = document.getElementById("graph-stats");
// /24 index (0..2^24-1) -> value (whitelisted/blocked)
const marked = new Map();
function parseCIDR(s) {
const m = String(s)
.trim()
.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})(?:\/(\d{1,2}))?$/);
if (!m) return null;
const o = [m[1], m[2], m[3], m[4]].map(Number);
if (o.some((n) => n > 255)) return null;
const prefix = m[5] === undefined ? 32 : Number(m[5]);
if (prefix < 0 || prefix > 32) return null;
const ip = ((o[0] << 24) | (o[1] << 16) | (o[2] << 8) | o[3]) >>> 0;
const mask = prefix === 0 ? 0 : (0xffffffff << (32 - prefix)) >>> 0;
const network = (ip & mask) >>> 0;
const broadcast = (ip | (~mask >>> 0)) >>> 0;
return {
first24: network >>> 8,
last24: broadcast >>> 8,
label: `${o[0]}.${o[1]}.${o[2]}.${o[3]}/${prefix}`,
};
}
function idxToPos(idx) {
const a = (idx >>> 16) & 0xff;
const b = (idx >>> 8) & 0xff;
const c = idx & 0xff;
return {
x: (a % TILE) * SIZE + b,
y: Math.floor(a / TILE) * SIZE + c,
};
}
function markRange(range, value) {
for (let idx = range.first24; idx <= range.last24; idx++) {
const prev = marked.get(idx);
if (prev === undefined || value > prev) marked.set(idx, value);
}
}
function drawOverlay() {
overlay.innerHTML = "";
// /8 tile borders
for (let i = 0; i <= TILE; i++) {
const pct = (i / TILE) * 100;
const v = el("div", { class: "g-line g-v", style: `left:${pct}%` });
const h = el("div", { class: "g-line g-h", style: `top:${pct}%` });
overlay.append(v, h);
}
// first-octet labels, one per /8 tile
for (let a = 0; a < 256; a++) {
const col = a % TILE;
const row = Math.floor(a / TILE);
overlay.append(
el("div", {
class: "g-label",
style: `left:${(col / TILE) * 100}%;top:${(row / TILE) * 100}%`,
text: `${a}.0.0.0/8`,
}),
);
}
}
function render() {
const showWl = document.getElementById("show-whitelisted").checked;
ctx.fillStyle = "#ffffff";
ctx.fillRect(0, 0, TOTAL, TOTAL);
for (const [idx, value] of marked) {
if (!showWl && value === V_WHITELISTED) continue;
const p = idxToPos(idx);
ctx.fillStyle = value === V_BLOCKED ? COLOR_BLOCKED : COLOR_WHITELISTED;
ctx.fillRect(p.x, p.y, 1, 1);
}
}
// --- hover tooltip ------------------------------------------------------
function matchesIn(ranges, idx) {
const out = [];
for (const r of ranges) {
if (idx >= r.first24 && idx <= r.last24) out.push(r.label);
if (out.length >= 12) break;
}
return out;
}
function showTooltip(e, x, y) {
const tileCol = Math.floor(x / SIZE);
const tileRow = Math.floor(y / SIZE);
const a = tileRow * TILE + tileCol;
const b = x % SIZE;
const c = y % SIZE;
const idx = (a << 16) | (b << 8) | c;
const value = marked.get(idx) ?? V_UNLISTED;
const status =
value === V_BLOCKED
? "blocked"
: value === V_WHITELISTED
? "whitelisted"
: "unlisted";
const entries = matchesIn(blockedRanges, idx).concat(
matchesIn(whitelistedRanges, idx),
);
tooltip.innerHTML = "";
tooltip.append(
el("div", { class: "tt-head mono", text: `${a}.${b}.${c}.0/24` }),
el("div", { class: `tt-status ${status}`, text: status }),
);
if (entries.length) {
tooltip.append(
el("div", { class: "tt-list mono", text: entries.join("\n") }),
);
} else {
tooltip.append(
el("div", { class: "tt-list muted", text: "no matching entry" }),
);
}
tooltip.classList.remove("hidden");
const rect = canvas.getBoundingClientRect();
let left = e.clientX - rect.left + 14;
let top = e.clientY - rect.top + 14;
if (left + tooltip.offsetWidth > rect.width)
left = e.clientX - rect.left - tooltip.offsetWidth - 14;
if (top + tooltip.offsetHeight > rect.height)
top = e.clientY - rect.top - tooltip.offsetHeight - 14;
tooltip.style.left = `${Math.max(0, left)}px`;
tooltip.style.top = `${Math.max(0, top)}px`;
}
canvas.addEventListener("mousemove", (e) => {
const rect = canvas.getBoundingClientRect();
const lx = ((e.clientX - rect.left) * TOTAL) / rect.width;
const ly = ((e.clientY - rect.top) * TOTAL) / rect.height;
const x = Math.floor(lx);
const y = Math.floor(ly);
if (x < 0 || x >= TOTAL || y < 0 || y >= TOTAL) {
tooltip.classList.add("hidden");
return;
}
showTooltip(e, x, y);
});
canvas.addEventListener("mouseleave", () => {
tooltip.classList.add("hidden");
});
// --- load ---------------------------------------------------------------
let blockedRanges = [];
let whitelistedRanges = [];
async function loadGraph() {
try {
const d = await api("/api/graph/ipv4");
blockedRanges = (d.blocked || []).map(parseCIDR).filter(Boolean);
whitelistedRanges = (d.whitelisted || []).map(parseCIDR).filter(Boolean);
marked.clear();
for (const r of whitelistedRanges) markRange(r, V_WHITELISTED);
for (const r of blockedRanges) markRange(r, V_BLOCKED);
render();
drawOverlay();
const c = d.counts || {};
stats.textContent = `blocked targets: ${c.blocked ?? blockedRanges.length} · whitelisted ranges: ${c.whitelisted ?? whitelistedRanges.length} · marked /24 pixels: ${marked.size}`;
} catch (err) {
stats.textContent = `failed to load graph data: ${err.message}`;
toast(err.message, "error");
}
}
document
.getElementById("show-whitelisted")
.addEventListener("change", render);
document.getElementById("graph-refresh").addEventListener("click", loadGraph);
loadGraph();
</script>
{% endblock %}
{% extends "base.html" %} {% block title %}Graph — Intrusion Detection{%
endblock %} {% block content %}
<h1>IPv4 address space map</h1>
<p class="muted">
One pixel per /24 network, arranged as 16×16 tiles of /8 (first octet).
Blocked targets are red, whitelisted ranges are green.
</p>
<div class="panel">
<div class="panel-head">
<div class="graph-controls">
<label class="check" style="margin-top: 0">
<input type="checkbox" id="show-whitelisted" checked />
Show whitelisted
</label>
<span class="legend">
<span class="sw red"></span> Blocked
<span class="sw green"></span> Whitelisted
<span class="sw plain"></span> Unlisted
</span>
</div>
<button class="btn ghost" id="graph-refresh">↻ Refresh</button>
</div>
<div class="graph-wrap" id="graph-wrap">
<canvas id="graph-canvas" width="4096" height="4096"></canvas>
<div class="graph-overlay" id="graph-overlay"></div>
<div class="graph-tooltip hidden" id="graph-tooltip"></div>
</div>
<p class="muted mono" id="graph-stats">Loading…</p>
</div>
{% endblock %} {% block scripts %}
<script>
// Layout mirrors plot.py: 16x16 grid of /8 tiles, each 256x256 px (one pixel
// per /24). Blocked ranges override whitelisted ranges.
const TILE = 16;
const SIZE = 256;
const TOTAL = TILE * SIZE; // 4096
const V_UNLISTED = 0;
const V_WHITELISTED = 1;
const V_BLOCKED = 2;
const COLOR_WHITELISTED = "#43a047";
const COLOR_BLOCKED = "#e53935";
const canvas = document.getElementById("graph-canvas");
const ctx = canvas.getContext("2d");
const overlay = document.getElementById("graph-overlay");
const tooltip = document.getElementById("graph-tooltip");
const stats = document.getElementById("graph-stats");
// /24 index (0..2^24-1) -> value (whitelisted/blocked)
const marked = new Map();
function parseCIDR(s) {
const m = String(s)
.trim()
.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})(?:\/(\d{1,2}))?$/);
if (!m) return null;
const o = [m[1], m[2], m[3], m[4]].map(Number);
if (o.some((n) => n > 255)) return null;
const prefix = m[5] === undefined ? 32 : Number(m[5]);
if (prefix < 0 || prefix > 32) return null;
const ip = ((o[0] << 24) | (o[1] << 16) | (o[2] << 8) | o[3]) >>> 0;
const mask = prefix === 0 ? 0 : (0xffffffff << (32 - prefix)) >>> 0;
const network = (ip & mask) >>> 0;
const broadcast = (ip | (~mask >>> 0)) >>> 0;
return {
first24: network >>> 8,
last24: broadcast >>> 8,
label: `${o[0]}.${o[1]}.${o[2]}.${o[3]}/${prefix}`,
};
}
function idxToPos(idx) {
const a = (idx >>> 16) & 0xff;
const b = (idx >>> 8) & 0xff;
const c = idx & 0xff;
return {
x: (a % TILE) * SIZE + b,
y: Math.floor(a / TILE) * SIZE + c,
};
}
function markRange(range, value) {
for (let idx = range.first24; idx <= range.last24; idx++) {
const prev = marked.get(idx);
if (prev === undefined || value > prev) marked.set(idx, value);
}
}
function drawOverlay() {
overlay.innerHTML = "";
// /8 tile borders
for (let i = 0; i <= TILE; i++) {
const pct = (i / TILE) * 100;
const v = el("div", { class: "g-line g-v", style: `left:${pct}%` });
const h = el("div", { class: "g-line g-h", style: `top:${pct}%` });
overlay.append(v, h);
}
// first-octet labels, one per /8 tile
for (let a = 0; a < 256; a++) {
const col = a % TILE;
const row = Math.floor(a / TILE);
overlay.append(
el("div", {
class: "g-label",
style: `left:${(col / TILE) * 100}%;top:${(row / TILE) * 100}%`,
text: `${a}.0.0.0/8`,
}),
);
}
}
function render() {
const showWl = document.getElementById("show-whitelisted").checked;
ctx.fillStyle = "#ffffff";
ctx.fillRect(0, 0, TOTAL, TOTAL);
for (const [idx, value] of marked) {
if (!showWl && value === V_WHITELISTED) continue;
const p = idxToPos(idx);
ctx.fillStyle = value === V_BLOCKED ? COLOR_BLOCKED : COLOR_WHITELISTED;
ctx.fillRect(p.x, p.y, 1, 1);
}
}
// --- hover tooltip ------------------------------------------------------
function matchesIn(ranges, idx) {
const out = [];
for (const r of ranges) {
if (idx >= r.first24 && idx <= r.last24) out.push(r.label);
if (out.length >= 12) break;
}
return out;
}
function showTooltip(e, x, y) {
const tileCol = Math.floor(x / SIZE);
const tileRow = Math.floor(y / SIZE);
const a = tileRow * TILE + tileCol;
const b = x % SIZE;
const c = y % SIZE;
const idx = (a << 16) | (b << 8) | c;
const value = marked.get(idx) ?? V_UNLISTED;
const status =
value === V_BLOCKED
? "blocked"
: value === V_WHITELISTED
? "whitelisted"
: "unlisted";
const entries = matchesIn(blockedRanges, idx).concat(
matchesIn(whitelistedRanges, idx),
);
tooltip.innerHTML = "";
tooltip.append(
el("div", { class: "tt-head mono", text: `${a}.${b}.${c}.0/24` }),
el("div", { class: `tt-status ${status}`, text: status }),
);
if (entries.length) {
tooltip.append(
el("div", { class: "tt-list mono", text: entries.join("\n") }),
);
} else {
tooltip.append(
el("div", { class: "tt-list muted", text: "no matching entry" }),
);
}
tooltip.classList.remove("hidden");
const rect = canvas.getBoundingClientRect();
let left = e.clientX - rect.left + 14;
let top = e.clientY - rect.top + 14;
if (left + tooltip.offsetWidth > rect.width)
left = e.clientX - rect.left - tooltip.offsetWidth - 14;
if (top + tooltip.offsetHeight > rect.height)
top = e.clientY - rect.top - tooltip.offsetHeight - 14;
tooltip.style.left = `${Math.max(0, left)}px`;
tooltip.style.top = `${Math.max(0, top)}px`;
}
canvas.addEventListener("mousemove", (e) => {
const rect = canvas.getBoundingClientRect();
const lx = ((e.clientX - rect.left) * TOTAL) / rect.width;
const ly = ((e.clientY - rect.top) * TOTAL) / rect.height;
const x = Math.floor(lx);
const y = Math.floor(ly);
if (x < 0 || x >= TOTAL || y < 0 || y >= TOTAL) {
tooltip.classList.add("hidden");
return;
}
showTooltip(e, x, y);
});
canvas.addEventListener("mouseleave", () => {
tooltip.classList.add("hidden");
});
// --- load ---------------------------------------------------------------
let blockedRanges = [];
let whitelistedRanges = [];
async function loadGraph() {
try {
const d = await api("/api/graph/ipv4");
blockedRanges = (d.blocked || []).map(parseCIDR).filter(Boolean);
whitelistedRanges = (d.whitelisted || []).map(parseCIDR).filter(Boolean);
marked.clear();
for (const r of whitelistedRanges) markRange(r, V_WHITELISTED);
for (const r of blockedRanges) markRange(r, V_BLOCKED);
render();
drawOverlay();
const c = d.counts || {};
stats.textContent = `blocked targets: ${c.blocked ?? blockedRanges.length} · whitelisted ranges: ${c.whitelisted ?? whitelistedRanges.length} · marked /24 pixels: ${marked.size}`;
} catch (err) {
stats.textContent = `failed to load graph data: ${err.message}`;
toast(err.message, "error");
}
}
document
.getElementById("show-whitelisted")
.addEventListener("change", render);
document.getElementById("graph-refresh").addEventListener("click", loadGraph);
loadGraph();
</script>
{% endblock %}
+76 -76
View File
@@ -1,76 +1,76 @@
{% extends "base.html" %} {% block title %}Reports — Intrusion Detection{%
endblock %} {% block content %}
<h1>Reports</h1>
<p class="muted">Browse the timestamped reports written by each scanner.</p>
<div class="panel">
<div class="panel-actions" style="margin-top: 0">
<label style="flex-direction: row; align-items: center; gap: 0.5rem"
>Source
<select id="source">
<option value="copyparty">copyparty</option>
<option value="npm">npm</option>
<option value="app">app</option>
<option value="mikrotik">mikrotik</option>
</select>
</label>
<button class="btn" id="refresh-btn">Refresh</button>
</div>
<div class="list" id="files"></div>
</div>
<div class="panel">
<div class="panel-head"><h2 id="content-title">Report content</h2></div>
<pre class="content" id="content">
Select a report on the left to view it.</pre
>
</div>
{% endblock %} {% block scripts %}
<script>
async function loadFiles() {
const source = document.getElementById("source").value;
const list = document.getElementById("files");
list.innerHTML = '<div class="muted">Loading…</div>';
try {
const data = await api(`/api/reports/${source}`);
list.innerHTML = "";
const files = data.files || [];
if (!files.length) {
list.innerHTML = '<div class="muted">No reports in this source.</div>';
return;
}
for (const name of files) {
list.append(
el("a", {
href: "#",
text: name,
onclick: (ev) => {
ev.preventDefault();
openReport(name);
},
}),
);
}
} catch (e) {
list.innerHTML = `<div class="muted">${escapeHtml(e.message)}</div>`;
}
}
async function openReport(name) {
const source = document.getElementById("source").value;
document.getElementById("content-title").textContent = name;
const pre = document.getElementById("content");
pre.textContent = "Loading…";
try {
const data = await api(`/api/reports/${source}/${name}`);
pre.textContent = data.content;
} catch (e) {
pre.textContent = `Error: ${e.message}`;
}
}
document.getElementById("refresh-btn").addEventListener("click", loadFiles);
document.getElementById("source").addEventListener("change", loadFiles);
loadFiles();
</script>
{% endblock %}
{% extends "base.html" %} {% block title %}Reports — Intrusion Detection{%
endblock %} {% block content %}
<h1>Reports</h1>
<p class="muted">Browse the timestamped reports written by each scanner.</p>
<div class="panel">
<div class="panel-actions" style="margin-top: 0">
<label style="flex-direction: row; align-items: center; gap: 0.5rem"
>Source
<select id="source">
<option value="copyparty">copyparty</option>
<option value="npm">npm</option>
<option value="app">app</option>
<option value="mikrotik">mikrotik</option>
</select>
</label>
<button class="btn" id="refresh-btn">Refresh</button>
</div>
<div class="list" id="files"></div>
</div>
<div class="panel">
<div class="panel-head"><h2 id="content-title">Report content</h2></div>
<pre class="content" id="content">
Select a report on the left to view it.</pre
>
</div>
{% endblock %} {% block scripts %}
<script>
async function loadFiles() {
const source = document.getElementById("source").value;
const list = document.getElementById("files");
list.innerHTML = '<div class="muted">Loading…</div>';
try {
const data = await api(`/api/reports/${source}`);
list.innerHTML = "";
const files = data.files || [];
if (!files.length) {
list.innerHTML = '<div class="muted">No reports in this source.</div>';
return;
}
for (const name of files) {
list.append(
el("a", {
href: "#",
text: name,
onclick: (ev) => {
ev.preventDefault();
openReport(name);
},
}),
);
}
} catch (e) {
list.innerHTML = `<div class="muted">${escapeHtml(e.message)}</div>`;
}
}
async function openReport(name) {
const source = document.getElementById("source").value;
document.getElementById("content-title").textContent = name;
const pre = document.getElementById("content");
pre.textContent = "Loading…";
try {
const data = await api(`/api/reports/${source}/${name}`);
pre.textContent = data.content;
} catch (e) {
pre.textContent = `Error: ${e.message}`;
}
}
document.getElementById("refresh-btn").addEventListener("click", loadFiles);
document.getElementById("source").addEventListener("change", loadFiles);
loadFiles();
</script>
{% endblock %}
+185 -185
View File
@@ -1,185 +1,185 @@
{% extends "base.html" %} {% block title %}Scan — Intrusion Detection{% endblock
%} {% block content %}
<h1>Run a scan</h1>
<p class="muted">
Scans run in the background; only one at a time. You can leave this page and
come back.
</p>
<div class="panel">
<form id="scan-form" class="form-grid">
<label
>Type
<select id="kind">
<option value="consolidated">Consolidated (copyparty + npm)</option>
<option value="copyparty">copyparty only</option>
<option value="npm">npm only</option>
</select>
</label>
<label>Top IPs <input id="top" type="number" value="30" min="1" /></label>
<label
>Min denied (copyparty)
<input id="min_denied" type="number" value="1" min="0"
/></label>
<label
>Min errors (npm) <input id="min_errors" type="number" value="5" min="0"
/></label>
<label
>Error ratio (npm)
<input
id="error_ratio"
type="number"
step="0.05"
value="0.5"
min="0"
max="1"
/></label>
<label class="check"
><input id="update_firewall" type="checkbox" /> Update firewall after
scan</label
>
<label class="check"
><input id="dry_run" type="checkbox" /> Dry-run (preview, no
changes)</label
>
<label class="check"
><input id="full" type="checkbox" /> Full scan (ignore incremental
state)</label
>
</form>
<div class="panel-actions">
<button class="btn primary" id="run-btn">Run scan</button>
<span class="muted" id="busy-hint"></span>
</div>
<div id="job-status" class="job-status"></div>
</div>
<div class="panel">
<div class="panel-head"><h2>Recent jobs</h2></div>
<table class="table">
<thead>
<tr>
<th>Job</th>
<th>Kind</th>
<th>State</th>
<th>Started</th>
<th>Finished</th>
<th class="num">Blocked</th>
</tr>
</thead>
<tbody id="jobs"></tbody>
</table>
</div>
{% endblock %} {% block scripts %}
<script>
let pollTimer = null;
const form = document.getElementById("scan-form");
const runBtn = document.getElementById("run-btn");
const hint = document.getElementById("busy-hint");
async function runScan() {
const kind = document.getElementById("kind").value;
const params = {
top: parseInt(document.getElementById("top").value, 10) || 30,
min_denied:
parseInt(document.getElementById("min_denied").value, 10) || 1,
min_errors:
parseInt(document.getElementById("min_errors").value, 10) || 5,
error_ratio:
parseFloat(document.getElementById("error_ratio").value) || 0.5,
update_firewall: document.getElementById("update_firewall").checked,
dry_run: document.getElementById("dry_run").checked,
full: document.getElementById("full").checked,
quiet: true,
};
runBtn.disabled = true;
try {
const job = await startScan(kind, params);
setStatus(`Job ${job.job_id} started (${job.state}).`, "ok");
poll(job.job_id);
} catch (e) {
setStatus(e.message, "error");
runBtn.disabled = false;
}
}
function setStatus(msg, type) {
const box = document.getElementById("job-status");
box.textContent = msg;
box.style.color =
type === "error"
? "var(--danger)"
: type === "ok"
? "var(--ok)"
: "inherit";
}
async function poll(jobId) {
clearTimeout(pollTimer);
let job;
try {
job = await api(`/api/scan/${jobId}`);
} catch (e) {
setStatus(e.message, "error");
runBtn.disabled = false;
return;
}
const r = job.result || {};
setStatus(
`[${job.state}] ${job.kind} — started ${fmtTime(job.started)}` +
(job.finished ? `, finished ${fmtTime(job.finished)}` : "") +
(r.blocked_targets != null
? `, blocked targets: ${r.blocked_targets}`
: "") +
(job.error ? `, error: ${job.error}` : ""),
job.state === "error" ? "error" : job.state === "done" ? "ok" : "",
);
if (job.state === "running" || job.state === "queued") {
pollTimer = setTimeout(() => poll(jobId), 2000);
} else {
runBtn.disabled = false;
refreshJobs();
const running = (await api("/api/scan/status")).running;
hint.textContent = running ? "" : "";
}
}
async function refreshJobs() {
try {
const s = await api("/api/scan/status");
hint.textContent = s.running ? "A scan is currently running…" : "";
const body = document.getElementById("jobs");
body.innerHTML = "";
const jobs = s.latest_scans || [];
if (!jobs.length) {
body.innerHTML =
'<tr><td colspan="6" class="muted">No jobs yet.</td></tr>';
return;
}
for (const j of jobs) {
const r = j.result || {};
body.append(
el(
"tr",
el("td", { class: "mono", text: j.job_id }),
el("td", { text: j.kind }),
el("td", {}, badge(j.state)),
el("td", { text: fmtTime(j.started) }),
el("td", { text: fmtTime(j.finished) }),
el("td", { class: "num", text: r.blocked_targets ?? "—" }),
),
);
}
} catch (e) {
toast(e.message, "error");
}
}
runBtn.addEventListener("click", runScan);
refreshJobs();
// /api/scan/status is only polled every 15 s; the active job's own
// 2 s poll keeps live progress without hammering the status endpoint.
setInterval(refreshJobs, 15000);
</script>
{% endblock %}
{% extends "base.html" %} {% block title %}Scan — Intrusion Detection{% endblock
%} {% block content %}
<h1>Run a scan</h1>
<p class="muted">
Scans run in the background; only one at a time. You can leave this page and
come back.
</p>
<div class="panel">
<form id="scan-form" class="form-grid">
<label
>Type
<select id="kind">
<option value="consolidated">Consolidated (copyparty + npm)</option>
<option value="copyparty">copyparty only</option>
<option value="npm">npm only</option>
</select>
</label>
<label>Top IPs <input id="top" type="number" value="30" min="1" /></label>
<label
>Min denied (copyparty)
<input id="min_denied" type="number" value="1" min="0"
/></label>
<label
>Min errors (npm) <input id="min_errors" type="number" value="5" min="0"
/></label>
<label
>Error ratio (npm)
<input
id="error_ratio"
type="number"
step="0.05"
value="0.5"
min="0"
max="1"
/></label>
<label class="check"
><input id="update_firewall" type="checkbox" /> Update firewall after
scan</label
>
<label class="check"
><input id="dry_run" type="checkbox" /> Dry-run (preview, no
changes)</label
>
<label class="check"
><input id="full" type="checkbox" /> Full scan (ignore incremental
state)</label
>
</form>
<div class="panel-actions">
<button class="btn primary" id="run-btn">Run scan</button>
<span class="muted" id="busy-hint"></span>
</div>
<div id="job-status" class="job-status"></div>
</div>
<div class="panel">
<div class="panel-head"><h2>Recent jobs</h2></div>
<table class="table">
<thead>
<tr>
<th>Job</th>
<th>Kind</th>
<th>State</th>
<th>Started</th>
<th>Finished</th>
<th class="num">Blocked</th>
</tr>
</thead>
<tbody id="jobs"></tbody>
</table>
</div>
{% endblock %} {% block scripts %}
<script>
let pollTimer = null;
const form = document.getElementById("scan-form");
const runBtn = document.getElementById("run-btn");
const hint = document.getElementById("busy-hint");
async function runScan() {
const kind = document.getElementById("kind").value;
const params = {
top: parseInt(document.getElementById("top").value, 10) || 30,
min_denied:
parseInt(document.getElementById("min_denied").value, 10) || 1,
min_errors:
parseInt(document.getElementById("min_errors").value, 10) || 5,
error_ratio:
parseFloat(document.getElementById("error_ratio").value) || 0.5,
update_firewall: document.getElementById("update_firewall").checked,
dry_run: document.getElementById("dry_run").checked,
full: document.getElementById("full").checked,
quiet: true,
};
runBtn.disabled = true;
try {
const job = await startScan(kind, params);
setStatus(`Job ${job.job_id} started (${job.state}).`, "ok");
poll(job.job_id);
} catch (e) {
setStatus(e.message, "error");
runBtn.disabled = false;
}
}
function setStatus(msg, type) {
const box = document.getElementById("job-status");
box.textContent = msg;
box.style.color =
type === "error"
? "var(--danger)"
: type === "ok"
? "var(--ok)"
: "inherit";
}
async function poll(jobId) {
clearTimeout(pollTimer);
let job;
try {
job = await api(`/api/scan/${jobId}`);
} catch (e) {
setStatus(e.message, "error");
runBtn.disabled = false;
return;
}
const r = job.result || {};
setStatus(
`[${job.state}] ${job.kind} — started ${fmtTime(job.started)}` +
(job.finished ? `, finished ${fmtTime(job.finished)}` : "") +
(r.blocked_targets != null
? `, blocked targets: ${r.blocked_targets}`
: "") +
(job.error ? `, error: ${job.error}` : ""),
job.state === "error" ? "error" : job.state === "done" ? "ok" : "",
);
if (job.state === "running" || job.state === "queued") {
pollTimer = setTimeout(() => poll(jobId), 2000);
} else {
runBtn.disabled = false;
refreshJobs();
const running = (await api("/api/scan/status")).running;
hint.textContent = running ? "" : "";
}
}
async function refreshJobs() {
try {
const s = await api("/api/scan/status");
hint.textContent = s.running ? "A scan is currently running…" : "";
const body = document.getElementById("jobs");
body.innerHTML = "";
const jobs = s.latest_scans || [];
if (!jobs.length) {
body.innerHTML =
'<tr><td colspan="6" class="muted">No jobs yet.</td></tr>';
return;
}
for (const j of jobs) {
const r = j.result || {};
body.append(
el(
"tr",
el("td", { class: "mono", text: j.job_id }),
el("td", { text: j.kind }),
el("td", {}, badge(j.state)),
el("td", { text: fmtTime(j.started) }),
el("td", { text: fmtTime(j.finished) }),
el("td", { class: "num", text: r.blocked_targets ?? "—" }),
),
);
}
} catch (e) {
toast(e.message, "error");
}
}
runBtn.addEventListener("click", runScan);
refreshJobs();
// /api/scan/status is only polled every 15 s; the active job's own
// 2 s poll keeps live progress without hammering the status endpoint.
setInterval(refreshJobs, 15000);
</script>
{% endblock %}
+110 -110
View File
@@ -1,110 +1,110 @@
{% extends "base.html" %} {% block title %}Whitelist — Intrusion Detection{%
endblock %} {% block content %}
<h1>Whitelist</h1>
<p class="muted">
Provider whitelists (from the router snapshot). Never add a whole cloud
provider to the blocklist.
</p>
<div class="panel">
<div class="panel-head"><h2>Add / remove CIDR</h2></div>
<div class="panel-actions" style="margin-top: 0">
<select id="wl-name">
{% for name in whitelist_names %}
<option value="{{ name }}">{{ name }}</option>
{% endfor %}
</select>
<input
id="wl-address"
placeholder="CIDR, e.g. 1.2.3.0/24"
style="min-width: 220px"
/>
<button class="btn primary" id="wl-add-btn">Add</button>
<button class="btn danger" id="wl-remove-btn">Remove</button>
</div>
</div>
<div id="whitelists"></div>
{% endblock %} {% block scripts %}
<script>
async function loadWhitelists() {
try {
const data = await api("/api/whitelist");
const box = document.getElementById("whitelists");
box.innerHTML = "";
for (const name of data.names || []) {
const w = data.whitelists[name] || {};
const rows = [];
for (const version of ["ipv4", "ipv6"]) {
const entries = (w[version] || []).sort();
rows.push(
el(
"tr",
el("td", { text: version }),
el("td", { class: "mono", text: entries.join(", ") || "—" }),
),
);
}
box.append(
el(
"div",
{ class: "panel" },
el(
"div",
{ class: "panel-head" },
el("h2", {
text: `${name} (${(w.ipv4 || []).length + (w.ipv6 || []).length})`,
}),
),
el(
"table",
{ class: "table" },
el(
"thead",
{},
el(
"tr",
{},
el("th", { text: "Version" }),
el("th", { text: "Addresses" }),
),
),
el("tbody", {}, ...rows),
),
),
);
}
} catch (e) {
toast(e.message, "error");
}
}
async function act(action) {
const name = document.getElementById("wl-name").value;
const address = document.getElementById("wl-address").value.trim();
if (!address) {
toast("Enter a CIDR", "warn");
return;
}
try {
await api("/api/whitelist/update", {
method: "POST",
body: { name, address, action },
});
toast(`${action} ${address} on ${name}`, "ok");
document.getElementById("wl-address").value = "";
loadWhitelists();
} catch (e) {
toast(e.message, "error");
}
}
document
.getElementById("wl-add-btn")
.addEventListener("click", () => act("add"));
document
.getElementById("wl-remove-btn")
.addEventListener("click", () => act("remove"));
loadWhitelists();
</script>
{% endblock %}
{% extends "base.html" %} {% block title %}Whitelist — Intrusion Detection{%
endblock %} {% block content %}
<h1>Whitelist</h1>
<p class="muted">
Provider whitelists (from the router snapshot). Never add a whole cloud
provider to the blocklist.
</p>
<div class="panel">
<div class="panel-head"><h2>Add / remove CIDR</h2></div>
<div class="panel-actions" style="margin-top: 0">
<select id="wl-name">
{% for name in whitelist_names %}
<option value="{{ name }}">{{ name }}</option>
{% endfor %}
</select>
<input
id="wl-address"
placeholder="CIDR, e.g. 1.2.3.0/24"
style="min-width: 220px"
/>
<button class="btn primary" id="wl-add-btn">Add</button>
<button class="btn danger" id="wl-remove-btn">Remove</button>
</div>
</div>
<div id="whitelists"></div>
{% endblock %} {% block scripts %}
<script>
async function loadWhitelists() {
try {
const data = await api("/api/whitelist");
const box = document.getElementById("whitelists");
box.innerHTML = "";
for (const name of data.names || []) {
const w = data.whitelists[name] || {};
const rows = [];
for (const version of ["ipv4", "ipv6"]) {
const entries = (w[version] || []).sort();
rows.push(
el(
"tr",
el("td", { text: version }),
el("td", { class: "mono", text: entries.join(", ") || "—" }),
),
);
}
box.append(
el(
"div",
{ class: "panel" },
el(
"div",
{ class: "panel-head" },
el("h2", {
text: `${name} (${(w.ipv4 || []).length + (w.ipv6 || []).length})`,
}),
),
el(
"table",
{ class: "table" },
el(
"thead",
{},
el(
"tr",
{},
el("th", { text: "Version" }),
el("th", { text: "Addresses" }),
),
),
el("tbody", {}, ...rows),
),
),
);
}
} catch (e) {
toast(e.message, "error");
}
}
async function act(action) {
const name = document.getElementById("wl-name").value;
const address = document.getElementById("wl-address").value.trim();
if (!address) {
toast("Enter a CIDR", "warn");
return;
}
try {
await api("/api/whitelist/update", {
method: "POST",
body: { name, address, action },
});
toast(`${action} ${address} on ${name}`, "ok");
document.getElementById("wl-address").value = "";
loadWhitelists();
} catch (e) {
toast(e.message, "error");
}
}
document
.getElementById("wl-add-btn")
.addEventListener("click", () => act("add"));
document
.getElementById("wl-remove-btn")
.addEventListener("click", () => act("remove"));
loadWhitelists();
</script>
{% endblock %}