style(graph): reformat graph template and switch canvas background to dark

This commit is contained in:
zwitschi committed 2026-09-12 14:07:21 +02:00
1 parent 4ba40dfef2
commit 0137264fe4
1 file changed
+210 -227
+210 -227
View File
@@ -1,227 +1,210 @@
{% extends "base.html" %} {% block title %}Graph — Intrusion Detection{% {% extends "base.html" %} {% block title %}Graph — Intrusion Detection{% endblock %} {% block content %}
endblock %} {% block content %} <h1>IPv4 address space map</h1>
<h1>IPv4 address space map</h1> <p class="muted">
<p class="muted"> One pixel per /24 network, arranged as 16×16 tiles of /8 (first octet). Blocked targets are red, whitelisted ranges
One pixel per /24 network, arranged as 16×16 tiles of /8 (first octet). are green.
Blocked targets are red, whitelisted ranges are green. </p>
</p>
<div class="panel">
<div class="panel"> <div class="panel-head">
<div class="panel-head"> <div class="graph-controls">
<div class="graph-controls"> <label class="check" style="margin-top: 0">
<label class="check" style="margin-top: 0"> <input type="checkbox" id="show-whitelisted" checked />
<input type="checkbox" id="show-whitelisted" checked /> Show whitelisted
Show whitelisted </label>
</label> <span class="legend">
<span class="legend"> <span class="sw red"></span> Blocked <span class="sw green"></span> Whitelisted
<span class="sw red"></span> Blocked <span class="sw plain"></span> Unlisted
<span class="sw green"></span> Whitelisted </span>
<span class="sw plain"></span> Unlisted </div>
</span> <button class="btn ghost" id="graph-refresh">↻ Refresh</button>
</div> </div>
<button class="btn ghost" id="graph-refresh">↻ Refresh</button> <div class="graph-wrap" id="graph-wrap">
</div> <canvas id="graph-canvas" width="4096" height="4096"></canvas>
<div class="graph-wrap" id="graph-wrap"> <div class="graph-overlay" id="graph-overlay"></div>
<canvas id="graph-canvas" width="4096" height="4096"></canvas> <div class="graph-tooltip hidden" id="graph-tooltip"></div>
<div class="graph-overlay" id="graph-overlay"></div> </div>
<div class="graph-tooltip hidden" id="graph-tooltip"></div> <p class="muted mono" id="graph-stats">Loading…</p>
</div> </div>
<p class="muted mono" id="graph-stats">Loading…</p> {% endblock %} {% block scripts %}
</div> <script>
{% endblock %} {% block scripts %} // Layout mirrors plot.py: 16x16 grid of /8 tiles, each 256x256 px (one pixel
<script> // per /24). Blocked ranges override whitelisted ranges.
// Layout mirrors plot.py: 16x16 grid of /8 tiles, each 256x256 px (one pixel const TILE = 16;
// per /24). Blocked ranges override whitelisted ranges. const SIZE = 256;
const TILE = 16; const TOTAL = TILE * SIZE; // 4096
const SIZE = 256;
const TOTAL = TILE * SIZE; // 4096 const V_UNLISTED = 0;
const V_WHITELISTED = 1;
const V_UNLISTED = 0; const V_BLOCKED = 2;
const V_WHITELISTED = 1; const COLOR_WHITELISTED = "#43a047";
const V_BLOCKED = 2; const COLOR_BLOCKED = "#e53935";
const COLOR_WHITELISTED = "#43a047";
const COLOR_BLOCKED = "#e53935"; const canvas = document.getElementById("graph-canvas");
const ctx = canvas.getContext("2d");
const canvas = document.getElementById("graph-canvas"); const overlay = document.getElementById("graph-overlay");
const ctx = canvas.getContext("2d"); const tooltip = document.getElementById("graph-tooltip");
const overlay = document.getElementById("graph-overlay"); const stats = document.getElementById("graph-stats");
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();
// /24 index (0..2^24-1) -> value (whitelisted/blocked)
const marked = new Map(); function parseCIDR(s) {
const m = String(s)
function parseCIDR(s) { .trim()
const m = String(s) .match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})(?:\/(\d{1,2}))?$/);
.trim() if (!m) return null;
.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})(?:\/(\d{1,2}))?$/); const o = [m[1], m[2], m[3], m[4]].map(Number);
if (!m) return null; if (o.some((n) => n > 255)) return null;
const o = [m[1], m[2], m[3], m[4]].map(Number); const prefix = m[5] === undefined ? 32 : Number(m[5]);
if (o.some((n) => n > 255)) return null; if (prefix < 0 || prefix > 32) return null;
const prefix = m[5] === undefined ? 32 : Number(m[5]); const ip = ((o[0] << 24) | (o[1] << 16) | (o[2] << 8) | o[3]) >>> 0;
if (prefix < 0 || prefix > 32) return null; const mask = prefix === 0 ? 0 : (0xffffffff << (32 - prefix)) >>> 0;
const ip = ((o[0] << 24) | (o[1] << 16) | (o[2] << 8) | o[3]) >>> 0; const network = (ip & mask) >>> 0;
const mask = prefix === 0 ? 0 : (0xffffffff << (32 - prefix)) >>> 0; const broadcast = (ip | (~mask >>> 0)) >>> 0;
const network = (ip & mask) >>> 0; return {
const broadcast = (ip | (~mask >>> 0)) >>> 0; first24: network >>> 8,
return { last24: broadcast >>> 8,
first24: network >>> 8, label: `${o[0]}.${o[1]}.${o[2]}.${o[3]}/${prefix}`,
last24: broadcast >>> 8, };
label: `${o[0]}.${o[1]}.${o[2]}.${o[3]}/${prefix}`, }
};
} function idxToPos(idx) {
const a = (idx >>> 16) & 0xff;
function idxToPos(idx) { const b = (idx >>> 8) & 0xff;
const a = (idx >>> 16) & 0xff; const c = idx & 0xff;
const b = (idx >>> 8) & 0xff; return {
const c = idx & 0xff; x: (a % TILE) * SIZE + b,
return { y: Math.floor(a / TILE) * SIZE + c,
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++) {
function markRange(range, value) { const prev = marked.get(idx);
for (let idx = range.first24; idx <= range.last24; idx++) { if (prev === undefined || value > prev) marked.set(idx, value);
const prev = marked.get(idx); }
if (prev === undefined || value > prev) marked.set(idx, value); }
}
} function drawOverlay() {
overlay.innerHTML = "";
function drawOverlay() { // /8 tile borders
overlay.innerHTML = ""; for (let i = 0; i <= TILE; i++) {
// /8 tile borders const pct = (i / TILE) * 100;
for (let i = 0; i <= TILE; i++) { const v = el("div", { class: "g-line g-v", style: `left:${pct}%` });
const pct = (i / TILE) * 100; const h = el("div", { class: "g-line g-h", style: `top:${pct}%` });
const v = el("div", { class: "g-line g-v", style: `left:${pct}%` }); overlay.append(v, h);
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++) {
// first-octet labels, one per /8 tile const col = a % TILE;
for (let a = 0; a < 256; a++) { const row = Math.floor(a / TILE);
const col = a % TILE; overlay.append(
const row = Math.floor(a / TILE); el("div", {
overlay.append( class: "g-label",
el("div", { style: `left:${(col / TILE) * 100}%;top:${(row / TILE) * 100}%`,
class: "g-label", text: `${a}.0.0.0/8`,
style: `left:${(col / TILE) * 100}%;top:${(row / TILE) * 100}%`, }),
text: `${a}.0.0.0/8`, );
}), }
); }
}
} function render() {
const showWl = document.getElementById("show-whitelisted").checked;
function render() { ctx.fillStyle = "#0c0c0c";
const showWl = document.getElementById("show-whitelisted").checked; ctx.fillRect(0, 0, TOTAL, TOTAL);
ctx.fillStyle = "#ffffff"; for (const [idx, value] of marked) {
ctx.fillRect(0, 0, TOTAL, TOTAL); if (!showWl && value === V_WHITELISTED) continue;
for (const [idx, value] of marked) { const p = idxToPos(idx);
if (!showWl && value === V_WHITELISTED) continue; ctx.fillStyle = value === V_BLOCKED ? COLOR_BLOCKED : COLOR_WHITELISTED;
const p = idxToPos(idx); ctx.fillRect(p.x, p.y, 1, 1);
ctx.fillStyle = value === V_BLOCKED ? COLOR_BLOCKED : COLOR_WHITELISTED; }
ctx.fillRect(p.x, p.y, 1, 1); }
}
} // --- hover tooltip ------------------------------------------------------
// --- hover tooltip ------------------------------------------------------ function matchesIn(ranges, idx) {
const out = [];
function matchesIn(ranges, idx) { for (const r of ranges) {
const out = []; if (idx >= r.first24 && idx <= r.last24) out.push(r.label);
for (const r of ranges) { if (out.length >= 12) break;
if (idx >= r.first24 && idx <= r.last24) out.push(r.label); }
if (out.length >= 12) break; return out;
} }
return out;
} function showTooltip(e, x, y) {
const tileCol = Math.floor(x / SIZE);
function showTooltip(e, x, y) { const tileRow = Math.floor(y / SIZE);
const tileCol = Math.floor(x / SIZE); const a = tileRow * TILE + tileCol;
const tileRow = Math.floor(y / SIZE); const b = x % SIZE;
const a = tileRow * TILE + tileCol; const c = y % SIZE;
const b = x % SIZE; const idx = (a << 16) | (b << 8) | c;
const c = y % SIZE; const value = marked.get(idx) ?? V_UNLISTED;
const idx = (a << 16) | (b << 8) | c; const status = value === V_BLOCKED ? "blocked" : value === V_WHITELISTED ? "whitelisted" : "unlisted";
const value = marked.get(idx) ?? V_UNLISTED; const entries = matchesIn(blockedRanges, idx).concat(matchesIn(whitelistedRanges, idx));
const status = tooltip.innerHTML = "";
value === V_BLOCKED tooltip.append(
? "blocked" el("div", { class: "tt-head mono", text: `${a}.${b}.${c}.0/24` }),
: value === V_WHITELISTED el("div", { class: `tt-status ${status}`, text: status }),
? "whitelisted" );
: "unlisted"; if (entries.length) {
const entries = matchesIn(blockedRanges, idx).concat( tooltip.append(el("div", { class: "tt-list mono", text: entries.join("\n") }));
matchesIn(whitelistedRanges, idx), } else {
); tooltip.append(el("div", { class: "tt-list muted", text: "no matching entry" }));
tooltip.innerHTML = ""; }
tooltip.append( tooltip.classList.remove("hidden");
el("div", { class: "tt-head mono", text: `${a}.${b}.${c}.0/24` }), const rect = canvas.getBoundingClientRect();
el("div", { class: `tt-status ${status}`, text: status }), let left = e.clientX - rect.left + 14;
); let top = e.clientY - rect.top + 14;
if (entries.length) { if (left + tooltip.offsetWidth > rect.width) left = e.clientX - rect.left - tooltip.offsetWidth - 14;
tooltip.append( if (top + tooltip.offsetHeight > rect.height) top = e.clientY - rect.top - tooltip.offsetHeight - 14;
el("div", { class: "tt-list mono", text: entries.join("\n") }), tooltip.style.left = `${Math.max(0, left)}px`;
); tooltip.style.top = `${Math.max(0, top)}px`;
} else { }
tooltip.append(
el("div", { class: "tt-list muted", text: "no matching entry" }), canvas.addEventListener("mousemove", (e) => {
); const rect = canvas.getBoundingClientRect();
} const lx = ((e.clientX - rect.left) * TOTAL) / rect.width;
tooltip.classList.remove("hidden"); const ly = ((e.clientY - rect.top) * TOTAL) / rect.height;
const rect = canvas.getBoundingClientRect(); const x = Math.floor(lx);
let left = e.clientX - rect.left + 14; const y = Math.floor(ly);
let top = e.clientY - rect.top + 14; if (x < 0 || x >= TOTAL || y < 0 || y >= TOTAL) {
if (left + tooltip.offsetWidth > rect.width) tooltip.classList.add("hidden");
left = e.clientX - rect.left - tooltip.offsetWidth - 14; return;
if (top + tooltip.offsetHeight > rect.height) }
top = e.clientY - rect.top - tooltip.offsetHeight - 14; showTooltip(e, x, y);
tooltip.style.left = `${Math.max(0, left)}px`; });
tooltip.style.top = `${Math.max(0, top)}px`;
} canvas.addEventListener("mouseleave", () => {
tooltip.classList.add("hidden");
canvas.addEventListener("mousemove", (e) => { });
const rect = canvas.getBoundingClientRect();
const lx = ((e.clientX - rect.left) * TOTAL) / rect.width; // --- load ---------------------------------------------------------------
const ly = ((e.clientY - rect.top) * TOTAL) / rect.height;
const x = Math.floor(lx); let blockedRanges = [];
const y = Math.floor(ly); let whitelistedRanges = [];
if (x < 0 || x >= TOTAL || y < 0 || y >= TOTAL) {
tooltip.classList.add("hidden"); async function loadGraph() {
return; try {
} const d = await api("/api/graph/ipv4");
showTooltip(e, x, y); blockedRanges = (d.blocked || []).map(parseCIDR).filter(Boolean);
}); whitelistedRanges = (d.whitelisted || []).map(parseCIDR).filter(Boolean);
marked.clear();
canvas.addEventListener("mouseleave", () => { for (const r of whitelistedRanges) markRange(r, V_WHITELISTED);
tooltip.classList.add("hidden"); for (const r of blockedRanges) markRange(r, V_BLOCKED);
}); render();
drawOverlay();
// --- load --------------------------------------------------------------- const c = d.counts || {};
stats.textContent = `blocked targets: ${c.blocked ?? blockedRanges.length} · whitelisted ranges: ${c.whitelisted ?? whitelistedRanges.length} · marked /24 pixels: ${marked.size}`;
let blockedRanges = []; } catch (err) {
let whitelistedRanges = []; stats.textContent = `failed to load graph data: ${err.message}`;
toast(err.message, "error");
async function loadGraph() { }
try { }
const d = await api("/api/graph/ipv4");
blockedRanges = (d.blocked || []).map(parseCIDR).filter(Boolean); document.getElementById("show-whitelisted").addEventListener("change", render);
whitelistedRanges = (d.whitelisted || []).map(parseCIDR).filter(Boolean); document.getElementById("graph-refresh").addEventListener("click", loadGraph);
marked.clear();
for (const r of whitelistedRanges) markRange(r, V_WHITELISTED); loadGraph();
for (const r of blockedRanges) markRange(r, V_BLOCKED); </script>
render(); {% endblock %}
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 %}