Files
intrusion-detection/app/templates/firewall.html
T
zwitschi 2fb4032766 feat(db): backfill, DB-backed web scans, firewall snapshot view (R001/S001/F001)
- R001: app/db/backfill.py imports git-history blocklist -> ledger, firewall
  snapshots, and per-source requests.csv (idempotent; ran live migrating 705
  historical blocklist entries).
- S001: web_scan.latest_scans() now merges SQLite 'scans' records so the
  dashboard surfaces CLI/restart scans (_db_scans).
- F001: /api/firewall/snapshots + 'Snapshot history' panel; added
  recent_firewall_snapshots() query with entry counts.
2026-10-24 19:56:20 +02:00

222 lines
6.7 KiB
HTML

{% 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>
<div class="panel">
<div class="panel-head"><h2>Snapshot history (SQLite)</h2></div>
<div id="fw-snapshots"></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");
}
}
async function loadSnapshots() {
const box = document.getElementById("fw-snapshots");
try {
const d = await api("/api/firewall/snapshots?limit=10");
const snaps = d.snapshots || [];
if (!snaps.length) {
box.innerHTML =
'<p class="muted">No SQLite snapshots yet — they are recorded on each <code>--update-firewall</code> run.</p>';
return;
}
box.innerHTML = "";
const rows = snaps.map((s) =>
el(
"tr",
el("td", { class: "mono", text: `#${s.id}` }),
el("td", { text: fmtTime(s.captured_at) }),
el("td", { text: s.kind }),
el("td", { text: s.trigger }),
el("td", { class: "num", text: s.addr_entries }),
el("td", { class: "num", text: s.rule_entries }),
),
);
box.append(
el(
"table",
{ class: "table" },
el(
"thead",
el("tr", {}, el("th", { text: "ID" }), el("th", { text: "Captured" }), el("th", { text: "Kind" }), el("th", { text: "Trigger" }), el("th", { text: "Addr" }), el("th", { text: "Rules" })),
),
el("tbody", {}, ...rows),
),
);
} catch (e) {
box.innerHTML=
'<p class="muted">Snapshot history unavailable (SQLite).</p>';
}
}
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();
loadSnapshots();
</script>
{% endblock %}