correlation-grafen till mer “entity graph

This commit is contained in:
larssand
2026-07-02 20:02:14 +02:00
parent 19c3dd7e54
commit fd0c1bf05d

View File

@@ -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); 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 = []; 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; } 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(); if (!items.some(item => correlationKey(item) === uiCache.selectedCorrelationKey)) {
items.forEach(item => (item.streams||[]).forEach(stream => streamScores.set(stream, (streamScores.get(stream)||0)+Number(item.security_events||0)+1))); uiCache.selectedCorrelationKey = correlationKey(items[0]);
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 selected = items.find(item => correlationKey(item) === uiCache.selectedCorrelationKey) || items[0];
const entityY=index => top+(index+.5)*(ch-top-bottom)/items.length; const center={x:cw*.48,y:ch*.48};
const streamY=index => top+(index+.5)*(ch-top-bottom)/streams.length; const streams=(selected.streams||[]).slice(0,7);
const streamIndex=Object.fromEntries(streams.map((stream,index)=>[stream,index])); const samples=(selected.samples||[]).slice(0,8);
items.forEach((item,index)=>{ const resources=[...new Set(samples.map(item => item.destination || item.service || item.type || '').filter(Boolean))].slice(0,5);
const y1=entityY(index); const outer=[...streams.map(name=>({kind:'stream', name})), ...resources.map(name=>({kind:'resource', name}))];
(item.streams||[]).filter(stream => stream in streamIndex).forEach(stream => { const radius=Math.max(86, Math.min(cw,ch)*.32);
const y2=streamY(streamIndex[stream]); ctx.strokeStyle='#163b59';
const security=Number(item.security_events)||0; ctx.lineWidth=1;
ctx.strokeStyle=security ? 'rgba(255,95,95,.55)' : 'rgba(51,145,202,.45)'; for (let ring=1; ring<=2; ring++) {
ctx.lineWidth=Math.min(4,1.2+Math.log10(Math.max(1,Number(item.events)||1))); ctx.beginPath();
ctx.beginPath(); ctx.arc(center.x, center.y, radius*ring/2, 0, Math.PI*2);
ctx.moveTo(entityX,y1); ctx.stroke();
ctx.bezierCurveTo(entityX+120,y1,streamX-120,y2,streamX,y2); }
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 drawNode=(x,y,r,color,label,sub,key='') => {
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=color;
ctx.fillStyle=security ? '#d95f5f' : '#2389cc'; ctx.beginPath(); ctx.arc(entityX,y,radius,0,Math.PI*2); ctx.fill(); ctx.beginPath();
uiCache.correlationHitboxes.push({key: correlationKey(item), x: entityX, y, radius: radius + 8}); ctx.arc(x,y,r,0,Math.PI*2);
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.fill();
ctx.fillStyle='#91abc4'; ctx.fillText(`${Number(item.events)||0} events`, entityX-radius-10, y+12); 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 sideX=20, sideY=28;
const y=streamY(index); ctx.textAlign='left';
ctx.fillStyle='#238b5d'; ctx.fillRect(streamX-10,y-10,20,20); ctx.font='12px Arial';
ctx.textAlign='left'; ctx.font='12px Arial'; ctx.fillStyle='#d9e8f7'; ctx.fillText(short(stream,26), streamX+16, y+4); 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) { function showRefreshError(error) {
const message = error && error.stack ? error.stack : String(error || 'unknown refresh error'); const message = error && error.stack ? error.stack : String(error || 'unknown refresh error');
@@ -450,7 +482,7 @@ async function refresh() {
drawTrend(data.history || []); drawTrend(data.history || []);
drawCorrelationGraph(correlations); drawCorrelationGraph(correlations);
renderCorrelationExplorer(correlations, configuration); 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('stamp').textContent = data.generated_at ? `Updated ${new Date(data.generated_at * 1000).toLocaleString()}` : 'Waiting for monitor data';
document.getElementById('metrics').innerHTML = [ document.getElementById('metrics').innerHTML = [
metric('Total events', s.total || 0), metric('Total events', s.total || 0),
@@ -891,7 +923,10 @@ document.getElementById('correlationGraph')?.addEventListener('click', event =>
const rect = event.currentTarget.getBoundingClientRect(); const rect = event.currentTarget.getBoundingClientRect();
const x = event.clientX - rect.left; const x = event.clientX - rect.left;
const y = event.clientY - rect.top; 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; if (!hit) return;
uiCache.selectedCorrelationKey = hit.key; uiCache.selectedCorrelationKey = hit.key;
renderCorrelationExplorer(window.currentCorrelations || [], window.currentConfiguration || {}); renderCorrelationExplorer(window.currentCorrelations || [], window.currentConfiguration || {});