From 16330145935d4e148d5f9d80b7c1b2f382ee65ab1d42a98cf2afdd227a49c5d5 Mon Sep 17 00:00:00 2001 From: larssand Date: Tue, 30 Jun 2026 09:49:35 +0200 Subject: [PATCH] Fixed the graph regression in --- src/fgai/dashboard.py | 70 ++++++++++++++++++++++++++++++++----------- 1 file changed, 53 insertions(+), 17 deletions(-) diff --git a/src/fgai/dashboard.py b/src/fgai/dashboard.py index 51c4939..78cf61d 100644 --- a/src/fgai/dashboard.py +++ b/src/fgai/dashboard.py @@ -159,33 +159,69 @@ function drawTrend(history) { canvas.width=cw*ratio; canvas.height=ch*ratio; ctx.scale(ratio,ratio); ctx.clearRect(0,0,cw,ch); const rows=(history||[]).slice(-72); if (!rows.length) { ctx.fillStyle='#91abc4'; ctx.font='14px Arial'; ctx.fillText('Waiting for monitor history.', 16, 28); return; } - const left=54,right=54,top=34,bottom=34,w=cw-left-right,h=ch-top-bottom; + const left=58,right=58,top=34,bottom=36,w=cw-left-right,h=ch-top-bottom; const eventsMax=Math.max(1,...rows.map(item=>Number(item.events)||0)); const anomalyMax=Math.max(1,...rows.map(item=>Number(item.anomalies)||0),...rows.map(item=>Number(item.high||0)+Number(item.critical||0))); ctx.strokeStyle='#163b59'; ctx.lineWidth=1; ctx.font='11px Arial'; ctx.textAlign='right'; ctx.fillStyle='#91abc4'; - for(let tick=0; tick<=4; tick++){ const y=top+h-(tick/4)*h; ctx.beginPath(); ctx.moveTo(left,y); ctx.lineTo(cw-right,y); ctx.stroke(); ctx.fillText(Math.round(eventsMax*tick/4).toLocaleString(), left-8, y+4); } - ctx.textAlign='left'; ctx.fillStyle='#1ea9ff'; ctx.fillText('events', left, 18); ctx.fillStyle='#ffcf5a'; ctx.fillText('high', left+64, 18); ctx.fillStyle='#ff6666'; ctx.fillText('critical/anomalies', left+112, 18); - const barGap=2, barW=Math.max(2, w/rows.length-barGap); - rows.forEach((item,index)=>{ const x=left+index*w/rows.length; const eventH=((Number(item.events)||0)/eventsMax)*h; ctx.fillStyle='rgba(30,169,255,.42)'; ctx.fillRect(x, top+h-eventH, barW, eventH); const high=Number(item.high)||0, critical=Number(item.critical)||0; const highH=(high/anomalyMax)*h, critH=(critical/anomalyMax)*h; ctx.fillStyle='rgba(255,207,90,.85)'; ctx.fillRect(x, top+h-highH, Math.max(1,barW*.45), highH); ctx.fillStyle='rgba(255,83,83,.9)'; ctx.fillRect(x+Math.max(1,barW*.45), top+h-critH, Math.max(1,barW*.45), critH); }); - const line=(key,color,max)=>{ ctx.strokeStyle=color; ctx.lineWidth=2.5; ctx.beginPath(); rows.forEach((item,index)=>{ const x=left+(index+.5)*w/rows.length; const y=top+h-((Number(item[key])||0)/max)*h; index?ctx.lineTo(x,y):ctx.moveTo(x,y); }); ctx.stroke(); }; - line('anomalies','#ff6666',anomalyMax); - ctx.strokeStyle='#285071'; ctx.strokeRect(left,top,w,h); ctx.textAlign='right'; ctx.fillStyle='#ff9a9a'; ctx.fillText(`anomaly max ${anomalyMax}`, cw-8, 18); ctx.fillStyle='#91abc4'; ctx.fillText(`${rows.length} samples`, cw-right, ch-10); ctx.textAlign='left'; + for(let tick=0; tick<=4; tick++){ + const y=top+h-(tick/4)*h; + ctx.beginPath(); ctx.moveTo(left,y); ctx.lineTo(cw-right,y); ctx.stroke(); + ctx.fillText(Math.round(eventsMax*tick/4).toLocaleString(), left-8, y+4); + ctx.textAlign='left'; ctx.fillText(Math.round(anomalyMax*tick/4).toLocaleString(), cw-right+8, y+4); ctx.textAlign='right'; + } + const points=(key,max) => rows.map((item,index)=>({x:left+index*w/Math.max(1,rows.length-1), y:top+h-((Number(item[key])||0)/max)*h})); + const drawLine=(pts,color,width=2.5) => { ctx.strokeStyle=color; ctx.lineWidth=width; ctx.beginPath(); pts.forEach((point,index)=>index?ctx.lineTo(point.x,point.y):ctx.moveTo(point.x,point.y)); ctx.stroke(); }; + const eventsPts=points('events',eventsMax), anomaliesPts=points('anomalies',anomalyMax), highPts=rows.map((item,index)=>({x:left+index*w/Math.max(1,rows.length-1), y:top+h-(((Number(item.high)||0)+(Number(item.critical)||0))/anomalyMax)*h})); + ctx.fillStyle='rgba(30,169,255,.12)'; + ctx.beginPath(); eventsPts.forEach((point,index)=>index?ctx.lineTo(point.x,point.y):ctx.moveTo(point.x,point.y)); ctx.lineTo(left+w,top+h); ctx.lineTo(left,top+h); ctx.closePath(); ctx.fill(); + drawLine(eventsPts,'#1ea9ff',2.5); + drawLine(highPts,'#ffcf5a',2); + drawLine(anomaliesPts,'#ff6666',2.5); + const last=rows[rows.length-1] || {}; + ctx.strokeStyle='#285071'; ctx.strokeRect(left,top,w,h); + ctx.textAlign='left'; ctx.fillStyle='#1ea9ff'; ctx.fillText(`events max ${eventsMax.toLocaleString()}`, left, 18); + ctx.fillStyle='#ffcf5a'; ctx.fillText(`high+ max ${anomalyMax.toLocaleString()}`, left+150, 18); + ctx.fillStyle='#ff6666'; ctx.fillText(`anomalies latest ${Number(last.anomalies||0).toLocaleString()}`, left+280, 18); + ctx.textAlign='right'; ctx.fillStyle='#91abc4'; ctx.fillText(`${rows.length} samples`, cw-right, ch-10); ctx.textAlign='left'; } function drawCorrelationGraph(correlations) { const canvas=document.getElementById('correlationGraph'), ctx=canvas.getContext('2d'), ratio=window.devicePixelRatio||1, cw=canvas.clientWidth, ch=canvas.clientHeight; canvas.width=cw*ratio; canvas.height=ch*ratio; ctx.scale(ratio,ratio); ctx.clearRect(0,0,cw,ch); const short=(value,max=22)=>String(value||'-').length>max?`${String(value).slice(0,max-3)}...`:String(value||'-'); - const items=[...(correlations||[])].sort((a,b)=>(Number(b.security_events)||0)-(Number(a.security_events)||0) || (Number(b.events)||0)-(Number(a.events)||0) || (b.streams||[]).length-(a.streams||[]).length).slice(0,10); + const items=[...(correlations||[])].sort((a,b)=>(Number(b.security_events)||0)-(Number(a.security_events)||0) || (Number(b.events)||0)-(Number(a.events)||0) || (b.streams||[]).length-(a.streams||[]).length).slice(0,8); if (!items.length) { ctx.fillStyle='#91abc4'; ctx.font='14px Arial'; ctx.fillText('No multi-stream entities in the current analysis window.', 16, 28); return; } const streamScores=new Map(); items.forEach(item => (item.streams||[]).forEach(stream => streamScores.set(stream, (streamScores.get(stream)||0)+Number(item.security_events||0)+1))); - const streams=[...streamScores.entries()].sort((a,b)=>b[1]-a[1]).map(([name])=>name).slice(0,8); - const left=Math.min(210, Math.max(130, cw*.24)), top=54, right=20, bottom=34, rowH=Math.max(24, Math.min(34, (ch-top-bottom)/items.length)), cellW=(cw-left-right)/Math.max(1,streams.length); - ctx.font='12px Arial'; ctx.textAlign='center'; ctx.fillStyle='#83bce9'; - streams.forEach((stream,index)=>{ const x=left+index*cellW+cellW/2; ctx.save(); ctx.translate(x, top-8); ctx.rotate(-Math.PI/6); ctx.fillText(short(stream,18),0,0); ctx.restore(); }); - ctx.textAlign='right'; ctx.fillStyle='#83bce9'; ctx.fillText('Entity', left-12, top-14); - items.forEach((item,row)=>{ const y=top+row*rowH; const severity=Number(item.security_events)||0; ctx.fillStyle=severity?'#ff9a9a':'#d9e8f7'; ctx.textAlign='right'; ctx.fillText(short(item.entity||item.source_ip,24), left-12, y+rowH*.65); ctx.fillStyle='#91abc4'; ctx.fillText(`${Number(item.events)||0}`, left-12, y+rowH*.95); streams.forEach((stream,col)=>{ const x=left+col*cellW+4; const active=(item.streams||[]).includes(stream); ctx.fillStyle=active ? (severity ? `rgba(255,86,86,${Math.min(.95,.28+severity*.08)})` : 'rgba(30,169,255,.48)') : 'rgba(12,42,68,.7)'; ctx.fillRect(x,y+4,Math.max(8,cellW-8),rowH-8); }); }); - ctx.textAlign='left'; ctx.fillStyle='#91abc4'; ctx.fillText('cell = entity observed in stream; red intensity = security events; number below entity = events', 8, ch-10); + const streams=[...streamScores.entries()].sort((a,b)=>b[1]-a[1]).map(([name])=>name).slice(0,7); + const top=34,bottom=42,entityX=Math.max(170,Math.min(260,cw*.24)),streamX=Math.min(cw-190,Math.max(cw*.72,entityX+260)); + const entityY=index => top+(index+.5)*(ch-top-bottom)/items.length; + const streamY=index => top+(index+.5)*(ch-top-bottom)/streams.length; + const streamIndex=Object.fromEntries(streams.map((stream,index)=>[stream,index])); + items.forEach((item,index)=>{ + const y1=entityY(index); + (item.streams||[]).filter(stream => stream in streamIndex).forEach(stream => { + const y2=streamY(streamIndex[stream]); + const security=Number(item.security_events)||0; + ctx.strokeStyle=security ? 'rgba(255,95,95,.55)' : 'rgba(51,145,202,.45)'; + ctx.lineWidth=Math.min(4,1.2+Math.log10(Math.max(1,Number(item.events)||1))); + ctx.beginPath(); + ctx.moveTo(entityX,y1); + ctx.bezierCurveTo(entityX+120,y1,streamX-120,y2,streamX,y2); + ctx.stroke(); + }); + }); + items.forEach((item,index)=>{ + const y=entityY(index), security=Number(item.security_events)||0, radius=Math.min(16,8+Math.log10(Math.max(1,Number(item.events)||1))*3); + ctx.fillStyle=security ? '#d95f5f' : '#2389cc'; ctx.beginPath(); ctx.arc(entityX,y,radius,0,Math.PI*2); ctx.fill(); + ctx.textAlign='right'; ctx.font='12px Arial'; ctx.fillStyle='#d9e8f7'; ctx.fillText(short(item.entity||item.source_ip,22), entityX-radius-10, y-2); + ctx.fillStyle='#91abc4'; ctx.fillText(`${Number(item.events)||0} events`, entityX-radius-10, y+12); + }); + streams.forEach((stream,index)=>{ + const y=streamY(index); + ctx.fillStyle='#238b5d'; ctx.fillRect(streamX-10,y-10,20,20); + ctx.textAlign='left'; ctx.font='12px Arial'; ctx.fillStyle='#d9e8f7'; ctx.fillText(short(stream,26), streamX+16, y+4); + }); + ctx.textAlign='left'; ctx.fillStyle='#91abc4'; ctx.font='12px Arial'; ctx.fillText('Top correlated entities and streams. Red links/entities include security-event activity; circle size follows event volume.', 8, ch-14); } async function refresh() { const openDetails = new Set([...document.querySelectorAll('details[open][data-detail-id]')].map(item => item.dataset.detailId)); @@ -206,7 +242,7 @@ async function refresh() { if (rawCorrelations.length) uiCache.correlations = rawCorrelations; drawTrend(data.history || []); drawCorrelationGraph(correlations); - document.getElementById('correlationGraphInfo').textContent = `${correlations.length} entities correlated across enabled streams${correlationsCached ? ' (cached from previous non-empty poll)' : ''}. Matrix shows top entities by security activity; colored cells mean the entity was observed in that stream, red intensity marks security-event activity.`; + document.getElementById('correlationGraphInfo').textContent = `${correlations.length} entities correlated across enabled streams${correlationsCached ? ' (cached from previous non-empty poll)' : ''}. Graph shows the highest-signal entities linked to the streams where they were observed.`; document.getElementById('stamp').textContent = data.generated_at ? `Updated ${new Date(data.generated_at * 1000).toLocaleString()}` : 'Waiting for monitor data'; document.getElementById('metrics').innerHTML = [ metric('Total events', s.total || 0),