diff --git a/src/fgai/dashboard.py b/src/fgai/dashboard.py index 085b63a..3d1963f 100644 --- a/src/fgai/dashboard.py +++ b/src/fgai/dashboard.py @@ -110,6 +110,23 @@ function table(rows, columns, id = '') { return `
${head}${body}
`; } function capability(label, state, detail) { return `${esc(label)}: ${esc(detail)}`; } +function compactRelatedActivity(rows) { + const groups = new Map(); + for (const row of rows || []) { + const key = `${row.entity || row.source_ip || '-'}|${row.stream || '-'}|${row.type || '-'}`; + const group = groups.get(key) || {entity: row.entity || row.source_ip || '-', stream: row.stream || '-', type: row.type || '-', count: 0, security: 0, actions: new Set(), destinations: new Set(), services: new Set(), first: row.timestamp || '', last: row.timestamp || '', samples: []}; + group.count += 1; + if (row.severity && !['-','info','notice','low'].includes(String(row.severity).toLowerCase())) group.security += 1; + if (row.action && row.action !== '-') group.actions.add(row.action); + if (row.destination && row.destination !== '-') group.destinations.add(row.destination); + if (row.service && row.service !== '-') group.services.add(row.service); + if (row.timestamp && (!group.first || row.timestamp < group.first)) group.first = row.timestamp; + if (row.timestamp && (!group.last || row.timestamp > group.last)) group.last = row.timestamp; + if (group.samples.length < 6) group.samples.push(row); + groups.set(key, group); + } + return [...groups.values()].sort((left,right) => (right.security-left.security) || (right.count-left.count)).slice(0,25); +} function drawTrend(history) { const canvas=document.getElementById('trendChart'), 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); if (!history.length) { ctx.fillStyle='#91abc4'; ctx.font='14px Arial'; ctx.fillText('Waiting for monitor history.', 16, 28); return; } const eventsMax=Math.max(1,...history.map(item=>Number(item.events)||0)), anomaliesMax=Math.max(1,...history.map(item=>Number(item.anomalies)||0)), left=40,right=40,top=28,bottom=24; ctx.strokeStyle='#163b59'; ctx.lineWidth=1; for(let index=0;index<4;index++){const y=top+index*(ch-top-bottom)/3;ctx.beginPath();ctx.moveTo(left,y);ctx.lineTo(cw-right,y);ctx.stroke();} const line=(key,max,color)=>{ctx.strokeStyle=color;ctx.lineWidth=2;ctx.beginPath();history.forEach((item,index)=>{const x=left+index*(cw-left-right)/Math.max(1,history.length-1);const y=ch-bottom-((Number(item[key])||0)/max)*(ch-top-bottom);index?ctx.lineTo(x,y):ctx.moveTo(x,y)});ctx.stroke();}; line('events',eventsMax,'#1ea9ff');line('anomalies',anomaliesMax,'#ff5656'); ctx.font='11px Arial';ctx.fillStyle='#1ea9ff';ctx.fillText(`Events max ${eventsMax}`,4,14);ctx.fillStyle='#ff5656';ctx.textAlign='right';ctx.fillText(`Anomalies max ${anomaliesMax}`,cw-4,14);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 items=(correlations||[]).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 streams=[...new Set(items.flatMap(item=>item.streams||[]))].slice(0,8); const entityPoint=(index,total)=>({x:Math.max(86,cw*.25),y:42+(index+0.5)*Math.max(36,(ch-84)/total)}); const streamPoint=(index,total)=>({x:Math.min(cw-96,cw*.75),y:42+(index+0.5)*Math.max(36,(ch-84)/total)}); const streamPositions=Object.fromEntries(streams.map((name,index)=>[name,streamPoint(index,streams.length)])); const short=value=>String(value).length>22?`${String(value).slice(0,19)}...`:String(value); items.forEach((item,index)=>{const point=entityPoint(index,items.length); (item.streams||[]).filter(name=>streamPositions[name]).forEach(name=>{const target=streamPositions[name]; ctx.strokeStyle=item.security_events?'#e66b6b':'#347fae'; ctx.lineWidth=Math.min(5,1+Number(item.security_events||0)); ctx.beginPath(); ctx.moveTo(point.x,point.y); ctx.lineTo(target.x,target.y); ctx.stroke();});}); items.forEach((item,index)=>{const point=entityPoint(index,items.length); ctx.fillStyle='#1388cc'; ctx.beginPath(); ctx.arc(point.x,point.y,12,0,Math.PI*2); ctx.fill(); ctx.fillStyle='#d9e8f7'; ctx.font='12px Arial'; ctx.textAlign='right'; ctx.fillText(short(item.entity||item.source_ip),point.x-18,point.y+4);}); streams.forEach((name,index)=>{const point=streamPositions[name]; ctx.fillStyle='#218957'; ctx.fillRect(point.x-10,point.y-10,20,20); ctx.fillStyle='#d9e8f7'; ctx.font='12px Arial'; ctx.textAlign='left'; ctx.fillText(short(name),point.x+16,point.y+4);}); ctx.textAlign='left'; } async function refresh() { @@ -238,10 +255,17 @@ async function refresh() { document.getElementById('feedbackNotice').textContent = response.ok ? 'Review saved. The matching pattern will be labeled on the next refresh.' : 'Could not save review.'; refresh(); })); - document.getElementById('relatedActivity').innerHTML = table(relatedRows, [ - {label:'Entity', key:'entity', render:r => esc(r.entity || r.source_ip)}, {label:'Stream', key:'stream'}, {label:'Time', key:'timestamp'}, - {label:'Type', key:'type'}, {label:'Action', key:'action'}, {label:'Severity', key:'severity'}, - {label:'Destination', key:'destination'}, {label:'Service', key:'service'}, {label:'Context', key:'context'}, {label:'Graylog Query', render:r => r.graylog_query ? `${esc(r.graylog_query)}` : '-'} + const relatedGroups = compactRelatedActivity(relatedRows); + document.getElementById('relatedActivity').innerHTML = `

${relatedGroups.length} grouped rows shown from ${relatedRows.length} raw related events.

` + table(relatedGroups, [ + {label:'Entity', key:'entity'}, + {label:'Stream', key:'stream'}, + {label:'Type', key:'type'}, + {label:'Events', key:'count'}, + {label:'Security', key:'security'}, + {label:'Time range', render:r => esc(`${r.first || '-'} to ${r.last || '-'}`)}, + {label:'Actions', render:r => esc([...r.actions].slice(0,4).join(', ') || '-')}, + {label:'Destinations', render:r => esc([...r.destinations].slice(0,4).join(', ') || '-')}, + {label:'Samples', render:r => { const rows=(r.samples || []).map(item => `${esc(`${item.timestamp || ''} | ${item.action || ''} | ${item.destination || ''} | ${item.service || ''} | ${item.context || ''}`)}${item.graylog_query ? `
${esc(item.graylog_query)}` : ''}`).join('
'); const id=`related:${r.entity}:${r.stream}:${r.type}`; return rows ? `
show samples

${rows}

` : '-'; }} ], 'related-activity'); document.getElementById('reputation').innerHTML = table(reputationRows, [ {label:'IP', key:'ip'},