Fixed the graph regression in
This commit is contained in:
@@ -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);
|
canvas.width=cw*ratio; canvas.height=ch*ratio; ctx.scale(ratio,ratio); ctx.clearRect(0,0,cw,ch);
|
||||||
const rows=(history||[]).slice(-72);
|
const rows=(history||[]).slice(-72);
|
||||||
if (!rows.length) { ctx.fillStyle='#91abc4'; ctx.font='14px Arial'; ctx.fillText('Waiting for monitor history.', 16, 28); return; }
|
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 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)));
|
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';
|
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); }
|
for(let tick=0; tick<=4; tick++){
|
||||||
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 y=top+h-(tick/4)*h;
|
||||||
const barGap=2, barW=Math.max(2, w/rows.length-barGap);
|
ctx.beginPath(); ctx.moveTo(left,y); ctx.lineTo(cw-right,y); ctx.stroke();
|
||||||
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); });
|
ctx.fillText(Math.round(eventsMax*tick/4).toLocaleString(), left-8, y+4);
|
||||||
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(); };
|
ctx.textAlign='left'; ctx.fillText(Math.round(anomalyMax*tick/4).toLocaleString(), cw-right+8, y+4); ctx.textAlign='right';
|
||||||
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';
|
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) {
|
function drawCorrelationGraph(correlations) {
|
||||||
const canvas=document.getElementById('correlationGraph'), ctx=canvas.getContext('2d'), ratio=window.devicePixelRatio||1, cw=canvas.clientWidth, ch=canvas.clientHeight;
|
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);
|
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 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; }
|
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();
|
const streamScores=new Map();
|
||||||
items.forEach(item => (item.streams||[]).forEach(stream => streamScores.set(stream, (streamScores.get(stream)||0)+Number(item.security_events||0)+1)));
|
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 streams=[...streamScores.entries()].sort((a,b)=>b[1]-a[1]).map(([name])=>name).slice(0,7);
|
||||||
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);
|
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));
|
||||||
ctx.font='12px Arial'; ctx.textAlign='center'; ctx.fillStyle='#83bce9';
|
const entityY=index => top+(index+.5)*(ch-top-bottom)/items.length;
|
||||||
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(); });
|
const streamY=index => top+(index+.5)*(ch-top-bottom)/streams.length;
|
||||||
ctx.textAlign='right'; ctx.fillStyle='#83bce9'; ctx.fillText('Entity', left-12, top-14);
|
const streamIndex=Object.fromEntries(streams.map((stream,index)=>[stream,index]));
|
||||||
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); }); });
|
items.forEach((item,index)=>{
|
||||||
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 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() {
|
async function refresh() {
|
||||||
const openDetails = new Set([...document.querySelectorAll('details[open][data-detail-id]')].map(item => item.dataset.detailId));
|
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;
|
if (rawCorrelations.length) uiCache.correlations = rawCorrelations;
|
||||||
drawTrend(data.history || []);
|
drawTrend(data.history || []);
|
||||||
drawCorrelationGraph(correlations);
|
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('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),
|
||||||
|
|||||||
Reference in New Issue
Block a user