diff --git a/src/fgai/dashboard.py b/src/fgai/dashboard.py index 68f6010..83b9371 100644 --- a/src/fgai/dashboard.py +++ b/src/fgai/dashboard.py @@ -414,6 +414,14 @@ 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 short=(value,max=22)=>String(value||'-').length>max?`${String(value).slice(0,max-3)}...`:String(value||'-'); + const usefulResource = value => { + const text = String(value || '').trim(); + if (!text || ['-', '0', 'unknown', 'n/a', 'none', 'null'].includes(text.toLowerCase())) return ''; + if (/^\\d+(\\.\\d+)?$/.test(text)) return ''; + if (/^\\d{4}-\\d{2}-\\d{2}t/i.test(text)) return ''; + return text; + }; + const sampleResource = item => usefulResource(item.destination) || usefulResource(item.service) || usefulResource(item.type) || usefulResource(item.context); 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; } @@ -424,7 +432,7 @@ function drawCorrelationGraph(correlations) { 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 resources=[...new Set(samples.map(sampleResource).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'; @@ -466,7 +474,7 @@ function drawCorrelationGraph(correlations) { drawNode(center.x, center.y, 24, Number(selected.security_events||0) ? '#d95f5f' : '#2389cc', correlationShortLabel(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; + const count=samples.filter(sample => sample.stream===node.name || sampleResource(sample)===node.name).length; drawNode(node.x,node.y,node.kind==='stream'?15:13,color,node.name,count?`${count} samples`:node.kind,''); }); const sideX=20, sideY=28; @@ -532,9 +540,10 @@ async function refresh() { metric('Anomalies high+', (a.high || 0) + (a.critical || 0)) ].join(''); const llm = data.llm_assessment || {}; + const llmEnabled = Boolean(llm.enabled || configuration.llm_enabled); document.getElementById('capabilities').innerHTML = [ capability('Baseline', baseline.enabled ? 'on' : 'warn', baseline.enabled ? `${baseline.sources_ready || 0} sources ready` : 'disabled'), - capability('Ollama', llm.enabled && llm.status !== 'error' ? 'on' : 'warn', llm.enabled ? (llm.status || 'starting') : 'disabled'), + capability('Ollama', llmEnabled && llm.status !== 'error' ? 'on' : 'warn', llmEnabled ? (llm.status || 'starting') : 'disabled'), capability('Threat Intel', threat.enabled && threat.configured ? 'on' : 'warn', threat.enabled ? `${threat.provider || 'unknown'}${threat.configured ? '' : ', key missing'}` : 'disabled'), capability('Graylog MCP', mcp.status === 'connected' ? 'on' : 'warn', configuration.log_source === 'graylog_mcp' ? (mcp.status || 'checking') : 'not selected') ].join(''); diff --git a/src/fgai/monitor.py b/src/fgai/monitor.py index 6a827e8..050ca07 100644 --- a/src/fgai/monitor.py +++ b/src/fgai/monitor.py @@ -578,5 +578,7 @@ def monitor_loop( } else: status["llm_assessment"] = {"enabled": False, "status": "disabled", "text": ""} + if status_cache_path and isinstance(mcp, dict) and mcp.get("status") not in {"error", "refreshing"}: + StatusSnapshotStore(status_cache_path).save("last_good", status) write_status(status, output) time.sleep(interval) diff --git a/tests/test_monitor.py b/tests/test_monitor.py index 1d7616e..8632cf8 100644 --- a/tests/test_monitor.py +++ b/tests/test_monitor.py @@ -82,6 +82,7 @@ class MonitorTests(unittest.TestCase): "summary": {"total": 1234}, "capabilities": {"graylog_mcp": {"status": "connected", "events_fetched": 99, "raw_events_fetched": 88, "aggregate_events": 1234, "fetch_mode": "aggregate", "coverage_status": "complete_window"}}, "cross_source_correlations": [{"entity": "10.0.0.1", "entity_label": "host01 (10.0.0.1)"}], + "llm_assessment": {"enabled": True, "status": "cached", "text": "previous assessment"}, }, ) @@ -95,6 +96,7 @@ class MonitorTests(unittest.TestCase): self.assertEqual(mcp["raw_events_fetched"], 88) self.assertEqual(mcp["aggregate_events"], 1234) self.assertEqual(mcp["previous_status"], "connected") + self.assertTrue(status["llm_assessment"]["enabled"]) self.assertTrue(status["status_cache"]["served_from_cache"]) def test_profile_readiness_includes_stream_and_profile_names(self):