feat: add localhost web UI for scans, blocklist, and firewall management
This commit is contained in:
1 parent
a9dc560f80
commit
4ef6892916
19 files changed
+1934
-117
No files matched your search
@@ -0,0 +1,45 @@
|
||||
<!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
|
||||
>
|
||||
</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>
|
||||
@@ -0,0 +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 %}
|
||||
@@ -0,0 +1,100 @@
|
||||
{% 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();
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +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 %}
|
||||
@@ -0,0 +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 %}
|
||||
@@ -0,0 +1,178 @@
|
||||
{% 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
|
||||
>
|
||||
</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,
|
||||
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();
|
||||
setInterval(refreshJobs, 5000);
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +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 %}
|
||||
Reference in new issue
Block a user