diff --git a/src/fgai/dashboard.py b/src/fgai/dashboard.py index 3039b51..45a2ea2 100644 --- a/src/fgai/dashboard.py +++ b/src/fgai/dashboard.py @@ -211,7 +211,7 @@ async function refresh() { const context = data.event_context || {}; const quality = data.data_quality || {}; const profileNames = Object.fromEntries((data.stream_profiles || []).map(item => [item.stream_id, item.name || item.stream_id])); - const profileReadiness = (data.profile_readiness || []).map(item => ({...item, profile_name: profileNames[item.stream_id] || item.stream_id, stream_title: streamTitles[item.stream_id] || item.stream_id})); + const profileReadiness = (data.profile_readiness || []).map(item => ({...item, profile_name: item.profile_name || profileNames[item.stream_id] || item.stream_id, stream_title: item.stream_name || item.stream_title || streamTitles[item.stream_id] || item.stream_id})); const correlations = data.cross_source_correlations || []; document.getElementById('diagnostics').innerHTML = '

Cross-Source Correlations

' + table(correlations, [{label:'Entity', key:'entity', render:r => esc(`${r.entity || r.source_ip} (${r.entity_type || 'ip'})`)}, {label:'Streams', render:r => esc((r.streams || []).join(', '))}, {label:'Events', key:'events'}, {label:'Security Events', key:'security_events'}], 'correlations') + diff --git a/src/fgai/monitor.py b/src/fgai/monitor.py index 8d16eaf..f8105a5 100644 --- a/src/fgai/monitor.py +++ b/src/fgai/monitor.py @@ -93,6 +93,19 @@ def build_status( baseline_events = baseline.ingest(events) if baseline else 0 profile_baseline_fields = baseline.ingest_profile_fields(events, stream_profiles) if baseline else 0 profile_readiness = baseline.profile_readiness(stream_profiles) if baseline else [] + stream_titles = { + str(item.get("id", "")): str(item.get("title", "")) + for item in runtime_values.get("graylog_streams", []) + if isinstance(item, dict) and item.get("id") + } + profile_readiness = [ + { + **item, + "profile_name": getattr(stream_profiles.get(str(item.get("stream_id", ""))), "name", str(item.get("stream_id", ""))), + "stream_name": stream_titles.get(str(item.get("stream_id", ""))) or getattr(stream_profiles.get(str(item.get("stream_id", ""))), "name", str(item.get("stream_id", ""))), + } + for item in profile_readiness + ] intel_ips = sorted( { ip diff --git a/tests/test_monitor.py b/tests/test_monitor.py index f40421f..a9e6707 100644 --- a/tests/test_monitor.py +++ b/tests/test_monitor.py @@ -1,4 +1,5 @@ import tempfile +import json import unittest from pathlib import Path from unittest.mock import patch @@ -34,6 +35,24 @@ class MonitorTests(unittest.TestCase): self.assertTrue(output.exists()) + def test_profile_readiness_includes_stream_and_profile_names(self): + with tempfile.TemporaryDirectory() as tmp: + config_path = Path(tmp) / "config.json" + config_path.write_text( + json.dumps( + { + "graylog_streams": [{"id": "66fe", "title": "Fortigate", "enabled": True}], + "graylog_stream_profiles": [{"stream_id": "66fe", "name": "Firewall baseline", "entity_field": "srcip", "categorical_fields": ["action"]}], + } + ), + encoding="utf-8", + ) + with patch("fgai.monitor.BaselineStore.profile_readiness", return_value=[{"stream_id": "66fe", "field": "action", "buckets": 12, "ready": True}]): + status = build_status(str(Path(tmp) / "missing.log"), baseline_path=str(Path(tmp) / "baseline.sqlite3"), config_path=str(config_path)) + + self.assertEqual(status["profile_readiness"][0]["stream_name"], "Fortigate") + self.assertEqual(status["profile_readiness"][0]["profile_name"], "Firewall baseline") + def test_add_llm_assessment_records_error_without_ollama(self): status = {"summary": {}, "anomalies": []}