poc2_worked

This commit is contained in:
Mateusz Gruszczyński
2026-08-15 18:29:36 +02:00
parent fc3a2944b2
commit 71b6c0d86f
62 changed files with 9112 additions and 375 deletions
+207
View File
@@ -0,0 +1,207 @@
(() => {
'use strict';
const COLORS = {
grid:'#202126', text:'#777780', strong:'#d4d4d8', green:'#3ecf8e', blue:'#60a5fa', red:'#f87171', amber:'#fbbf24',
palette:['#3ecf8e','#60a5fa','#fbbf24','#a78bfa','#f87171','#22d3ee','#fb7185','#94a3b8']
};
function setup(canvas) {
if (!canvas || !canvas.isConnected || canvas.offsetParent === null) return null;
const rect = canvas.getBoundingClientRect();
if (rect.width < 20) return null;
const DPR = Math.max(1, Math.min(window.devicePixelRatio || 1, 2));
const width = Math.max(1, Math.floor(rect.width));
const cssHeight = Number.parseFloat(getComputedStyle(canvas).height) || 0;
const height = Math.max(170, Math.floor(cssHeight || Number(canvas.getAttribute('height')) || 220));
canvas.width = Math.floor(width * DPR);
canvas.height = Math.floor(height * DPR);
canvas.style.height = `${height}px`;
const ctx = canvas.getContext('2d');
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
return {ctx, width, height};
}
function seriesMax(rows, key) {
return Math.max(1, ...rows.map(row => Number(row?.[key] || 0)));
}
function formatTick(ts) {
const d = new Date(Number(ts || 0));
return d.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'});
}
function compactNumber(value) {
const n = Number(value || 0);
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
return String(Math.round(n));
}
function formatRate(value) {
let n = Math.max(0, Number(value || 0));
const units = ['bps','Kbps','Mbps','Gbps','Tbps'];
let i = 0;
while (n >= 1000 && i < units.length - 1) { n /= 1000; i++; }
return `${n < 10 && i ? n.toFixed(1) : Math.round(n)} ${units[i]}`;
}
function drawGrid(ctx, width, height, pad, rows) {
ctx.clearRect(0, 0, width, height);
ctx.lineWidth = 1;
ctx.strokeStyle = COLORS.grid;
ctx.fillStyle = COLORS.text;
ctx.font = '10px ui-sans-serif, system-ui';
for (let i = 0; i <= 4; i++) {
const y = pad.t + ((height - pad.t - pad.b) / 4) * i;
ctx.beginPath(); ctx.moveTo(pad.l, y); ctx.lineTo(width - pad.r, y); ctx.stroke();
}
const labelIndexes = [0, Math.floor((rows.length - 1) / 2), rows.length - 1];
ctx.textBaseline = 'bottom';
labelIndexes.forEach((idx, i) => {
if (!rows[idx]) return;
const x = pad.l + ((width - pad.l - pad.r) * idx / Math.max(rows.length - 1, 1));
ctx.textAlign = i === 0 ? 'left' : (i === labelIndexes.length - 1 ? 'right' : 'center');
ctx.fillText(formatTick(rows[idx].ts_ms), x, height - 2);
});
}
function drawLine(ctx, rows, key, max, width, height, pad, color, fillAlpha=0) {
if (!rows.length || !(Number(max) > 0)) return;
const iw = width - pad.l - pad.r;
const ih = height - pad.t - pad.b;
const points = rows.map((row, idx) => ({
x: pad.l + iw * idx / Math.max(rows.length - 1, 1),
y: pad.t + ih - (Number(row[key] || 0) / Number(max)) * ih,
}));
if (fillAlpha) {
const grad = ctx.createLinearGradient(0, pad.t, 0, height - pad.b);
grad.addColorStop(0, hexToRgba(color, fillAlpha)); grad.addColorStop(1, hexToRgba(color, 0));
ctx.fillStyle = grad; ctx.beginPath(); ctx.moveTo(points[0].x, height - pad.b);
points.forEach(p => ctx.lineTo(p.x, p.y)); ctx.lineTo(points.at(-1).x, height - pad.b); ctx.closePath(); ctx.fill();
}
ctx.strokeStyle = color; ctx.lineWidth = 1.6; ctx.lineJoin = 'round'; ctx.lineCap = 'round';
ctx.beginPath(); points.forEach((p, idx) => idx ? ctx.lineTo(p.x, p.y) : ctx.moveTo(p.x, p.y)); ctx.stroke();
}
function hexToRgba(hex, alpha) {
const value = hex.replace('#','');
const n = parseInt(value, 16);
return `rgba(${(n>>16)&255},${(n>>8)&255},${n&255},${alpha})`;
}
function normalizedDonutRows(rows) {
const positive = (rows || []).map(row => ({
name:String(row?.name || 'unknown'), count:Math.max(0, Number(row?.count || 0))
})).filter(row => row.count > 0);
if (positive.length <= 6) return positive;
const head = positive.slice(0, 5);
const other = positive.slice(5).reduce((sum, row) => sum + row.count, 0);
if (other) head.push({name:'other', count:other});
return head;
}
function drawDonut(canvas, rows) {
if (!canvas) return;
const prepared = setup(canvas); if (!prepared) return;
const {ctx,width,height} = prepared;
ctx.clearRect(0, 0, width, height);
const items = normalizedDonutRows(rows);
const total = items.reduce((sum, row) => sum + row.count, 0);
if (!total) {
ctx.fillStyle = COLORS.text; ctx.font = '11px ui-sans-serif, system-ui'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText('No data in this window', width / 2, height / 2);
return;
}
const wide = width >= 360;
const cx = wide ? Math.min(width * .34, 135) : width / 2;
const cy = wide ? height / 2 : Math.min(88, height * .42);
const radius = Math.min(70, Math.max(48, Math.min(width * .22, height * .32)));
const inner = radius * .64;
let angle = -Math.PI / 2;
items.forEach((row, idx) => {
const portion = row.count / total;
const end = angle + portion * Math.PI * 2;
ctx.beginPath(); ctx.arc(cx, cy, radius, angle, end); ctx.arc(cx, cy, inner, end, angle, true); ctx.closePath();
ctx.fillStyle = COLORS.palette[idx % COLORS.palette.length]; ctx.fill();
angle = end;
});
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillStyle = COLORS.strong; ctx.font = '600 18px ui-sans-serif, system-ui'; ctx.fillText(compactNumber(total), cx, cy - 5);
ctx.fillStyle = COLORS.text; ctx.font = '9px ui-sans-serif, system-ui'; ctx.fillText('events', cx, cy + 14);
const legendX = wide ? Math.min(width * .60, cx + radius + 35) : 14;
const legendY = wide ? Math.max(18, cy - Math.min(items.length * 16, 84) / 2) : Math.min(height - 74, cy + radius + 16);
const legendWidth = wide ? Math.max(90, width - legendX - 12) : width - 28;
items.forEach((row, idx) => {
const y = legendY + idx * 18;
ctx.fillStyle = COLORS.palette[idx % COLORS.palette.length]; ctx.fillRect(legendX, y + 3, 7, 7);
ctx.fillStyle = COLORS.text; ctx.font = '10px ui-sans-serif, system-ui'; ctx.textAlign = 'left'; ctx.textBaseline = 'top';
const label = row.name.length > 18 ? `${row.name.slice(0,17)}` : row.name;
ctx.fillText(label, legendX + 13, y, Math.max(40, legendWidth - 45));
ctx.textAlign = 'right';
ctx.fillText(`${Math.round(row.count / total * 100)}%`, legendX + legendWidth, y);
});
}
function drawLoading(canvas, label='Building selected range…') {
if (!canvas) return;
const prepared = setup(canvas); if (!prepared) return;
const {ctx,width,height} = prepared;
ctx.clearRect(0, 0, width, height);
ctx.strokeStyle = COLORS.grid; ctx.lineWidth = 1;
for (let i = 1; i <= 3; i++) {
const y = height * i / 4;
ctx.beginPath(); ctx.moveTo(12, y); ctx.lineTo(width - 12, y); ctx.stroke();
}
ctx.fillStyle = COLORS.text; ctx.font = '11px ui-sans-serif, system-ui'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText(label, width / 2, height / 2);
}
function drawTraffic(canvas, rows) {
if (!canvas) return;
const prepared = setup(canvas); if (!prepared) return;
const {ctx,width,height} = prepared; const pad={l:8,r:8,t:12,b:22};
drawGrid(ctx,width,height,pad,rows);
drawLine(ctx, rows, 'bytes', seriesMax(rows,'bytes'), width,height,pad,COLORS.blue,.10);
drawLine(ctx, rows, 'events', seriesMax(rows,'events'), width,height,pad,COLORS.green,.12);
}
function drawThroughput(canvas, rows) {
if (!canvas) return;
const prepared = setup(canvas); if (!prepared) return;
const {ctx,width,height} = prepared; const pad={l:56,r:10,t:12,b:22};
drawGrid(ctx,width,height,pad,rows);
const max = Math.max(seriesMax(rows,'bps'), seriesMax(rows,'in_bps'), seriesMax(rows,'out_bps'));
ctx.fillStyle = COLORS.text; ctx.font = '10px ui-sans-serif, system-ui'; ctx.textAlign = 'right'; ctx.textBaseline = 'middle';
if (!(max > 0)) {
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText('No TZSP throughput samples in this window', width / 2, height / 2);
return;
}
for (let i = 0; i <= 4; i++) {
const value = max * (4 - i) / 4;
const y = pad.t + ((height - pad.t - pad.b) / 4) * i;
ctx.fillText(formatRate(value), pad.l - 7, y);
}
// Total is always available from raw TZSP byte counters, even when LAN
// direction classification is not configured correctly.
drawLine(ctx, rows, 'bps', max, width,height,pad,COLORS.amber,.04);
drawLine(ctx, rows, 'in_bps', max, width,height,pad,COLORS.blue,.05);
drawLine(ctx, rows, 'out_bps', max, width,height,pad,COLORS.green,.04);
}
function drawEvents(canvas, rows) {
if (!canvas) return;
const prepared = setup(canvas); if (!prepared) return;
const {ctx,width,height} = prepared; const pad={l:8,r:8,t:12,b:22};
drawGrid(ctx,width,height,pad,rows);
const max = Math.max(seriesMax(rows,'events'), seriesMax(rows,'alerts'));
drawLine(ctx, rows, 'events', max, width,height,pad,COLORS.green,.10);
drawLine(ctx, rows, 'alerts', max, width,height,pad,COLORS.red,0);
}
window.MikroSuricataCharts = {drawTraffic, drawThroughput, drawEvents, drawDonut, drawLoading};
})();