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:
1 parent
0d1cd21468
commit
c2eba88ac8
113 files changed
+772606
-772579
No files matched your search
+50
-50
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 %}
|
||||
Reference in new issue
Block a user