From fd0c1bf05d9615f461e83b15e3a525ac1a582152daa9837db455cd63fa0d633a Mon Sep 17 00:00:00 2001 From: larssand Date: Thu, 2 Jul 2026 20:02:14 +0200 Subject: [PATCH] =?UTF-8?q?correlation-grafen=20till=20mer=20=E2=80=9Centi?= =?UTF-8?q?ty=20graph?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/fgai/dashboard.py | 99 +++++++++++++++++++++++++++++-------------- 1 file changed, 67 insertions(+), 32 deletions(-) diff --git a/src/fgai/dashboard.py b/src/fgai/dashboard.py index 9422d86..2a69bc2 100644 --- a/src/fgai/dashboard.py +++ b/src/fgai/dashboard.py @@ -378,39 +378,71 @@ function drawCorrelationGraph(correlations) { 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); uiCache.correlationHitboxes = []; 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,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(); - }); + if (!items.some(item => correlationKey(item) === uiCache.selectedCorrelationKey)) { + uiCache.selectedCorrelationKey = correlationKey(items[0]); + } + const selected = items.find(item => correlationKey(item) === uiCache.selectedCorrelationKey) || items[0]; + const center={x:cw*.48,y:ch*.48}; + const streams=(selected.streams||[]).slice(0,7); + const samples=(selected.samples||[]).slice(0,8); + const resources=[...new Set(samples.map(item => item.destination || item.service || item.type || '').filter(Boolean))].slice(0,5); + const outer=[...streams.map(name=>({kind:'stream', name})), ...resources.map(name=>({kind:'resource', name}))]; + const radius=Math.max(86, Math.min(cw,ch)*.32); + ctx.strokeStyle='#163b59'; + ctx.lineWidth=1; + for (let ring=1; ring<=2; ring++) { + ctx.beginPath(); + ctx.arc(center.x, center.y, radius*ring/2, 0, Math.PI*2); + ctx.stroke(); + } + const nodePositions=[]; + outer.forEach((node,index)=>{ + const angle=(-Math.PI/2)+(index*Math.PI*2/Math.max(1,outer.length)); + const x=center.x+Math.cos(angle)*radius; + const y=center.y+Math.sin(angle)*radius; + nodePositions.push({...node,x,y}); + const security=samples.some(sample => sample.stream===node.name && (/(deny|block|fail|drop|reject)/i.test(String(sample.action||'')) || !['','-','info','notice','low'].includes(String(sample.severity||'').toLowerCase()))); + ctx.strokeStyle=security ? 'rgba(255,95,95,.72)' : 'rgba(51,145,202,.55)'; + ctx.lineWidth=security ? 2.2 : 1.2; + ctx.beginPath(); + ctx.moveTo(center.x, center.y); + ctx.lineTo(x, y); + 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(); - uiCache.correlationHitboxes.push({key: correlationKey(item), x: entityX, y, radius: radius + 8}); - 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); + const drawNode=(x,y,r,color,label,sub,key='') => { + ctx.fillStyle=color; + ctx.beginPath(); + ctx.arc(x,y,r,0,Math.PI*2); + ctx.fill(); + ctx.strokeStyle='#9ecfff'; + ctx.lineWidth=1.2; + ctx.stroke(); + if (key) uiCache.correlationHitboxes.push({key,x,y,radius:r+8}); + ctx.textAlign='center'; + ctx.font='12px Arial'; + ctx.fillStyle='#f2f8ff'; + ctx.fillText(short(label,24), x, y+r+14); + if (sub) { ctx.font='11px Arial'; ctx.fillStyle='#91abc4'; ctx.fillText(short(sub,28), x, y+r+28); } + }; + drawNode(center.x, center.y, 24, Number(selected.security_events||0) ? '#d95f5f' : '#2389cc', correlationKey(selected), `${Number(selected.events)||0} events / ${Number(selected.security_events)||0} security`, correlationKey(selected)); + nodePositions.forEach(node => { + const color=node.kind==='stream' ? '#238b5d' : '#6f55c8'; + const count=samples.filter(sample => sample.stream===node.name || sample.destination===node.name || sample.service===node.name || sample.type===node.name).length; + drawNode(node.x,node.y,node.kind==='stream'?15:13,color,node.name,count?`${count} samples`:node.kind,''); }); - 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); + const sideX=20, sideY=28; + ctx.textAlign='left'; + ctx.font='12px Arial'; + ctx.fillStyle='#91abc4'; + ctx.fillText('Focus entity graph', sideX, sideY); + items.slice(0,5).forEach((item,index)=>{ + const key=correlationKey(item); + const y=sideY+22+index*23; + ctx.fillStyle=key===correlationKey(selected)?'#1ea9ff':'#91abc4'; + ctx.fillText(`${short(key,18)} ${Number(item.security_events||0)}/${Number(item.events||0)}`, sideX, y); + uiCache.correlationHitboxes.push({key,x:sideX-4,y:y-14,w:150,h:20}); }); - 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); + ctx.textAlign='left'; ctx.fillStyle='#91abc4'; ctx.font='12px Arial'; ctx.fillText('Selected entity is centered. Green nodes are streams; purple nodes are destinations/services. Click a listed entity or center node evidence chip to change focus.', 8, ch-14); } function showRefreshError(error) { const message = error && error.stack ? error.stack : String(error || 'unknown refresh error'); @@ -450,7 +482,7 @@ async function refresh() { drawTrend(data.history || []); drawCorrelationGraph(correlations); renderCorrelationExplorer(correlations, configuration); - 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('correlationGraphInfo').textContent = `${correlations.length} entities correlated across enabled streams${correlationsCached ? ' (cached from previous non-empty poll)' : ''}. Focus graph shows the selected entity, connected streams, and sampled destinations/services; click entity labels or chips to inspect evidence.`; 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), @@ -891,7 +923,10 @@ document.getElementById('correlationGraph')?.addEventListener('click', event => const rect = event.currentTarget.getBoundingClientRect(); const x = event.clientX - rect.left; const y = event.clientY - rect.top; - const hit = (uiCache.correlationHitboxes || []).find(item => Math.hypot(item.x - x, item.y - y) <= item.radius); + const hit = (uiCache.correlationHitboxes || []).find(item => { + if (Number.isFinite(item.w) && Number.isFinite(item.h)) return x >= item.x && x <= item.x + item.w && y >= item.y && y <= item.y + item.h; + return Math.hypot(item.x - x, item.y - y) <= item.radius; + }); if (!hit) return; uiCache.selectedCorrelationKey = hit.key; renderCorrelationExplorer(window.currentCorrelations || [], window.currentConfiguration || {});