Files
intrusion-detection/app/templates/graph.html
T

211 lines
7.3 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% 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 = "#0c0c0c";
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 %}