style(graph): reformat graph template and switch canvas background to dark
This commit is contained in:
1 parent
4ba40dfef2
commit
0137264fe4
1 file changed
+210
-227
+210
-227
@@ -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 %}
|
|
||||||
Reference in new issue
Block a user