correlation-grafen till mer “entity graph
This commit is contained in:
@@ -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 || {});
|
||||
|
||||
Reference in New Issue
Block a user