From 8d516b980a441ef975e55fbf36c400d536d916bb8fcd6c8cf25db25cab6e95a6 Mon Sep 17 00:00:00 2001 From: larssand Date: Tue, 23 Jun 2026 20:22:57 +0200 Subject: [PATCH] Added per-profile baseline readiness. --- src/fgai/baseline.py | 10 ++++++++++ src/fgai/dashboard.py | 2 ++ src/fgai/monitor.py | 2 ++ 3 files changed, 14 insertions(+) diff --git a/src/fgai/baseline.py b/src/fgai/baseline.py index 3ce3a91..fde084a 100644 --- a/src/fgai/baseline.py +++ b/src/fgai/baseline.py @@ -182,6 +182,16 @@ class BaselineStore: output[entity].append(evidence) return output + def profile_readiness(self, profiles: dict[str, object]) -> list[dict[str, object]]: + rows: list[dict[str, object]] = [] + with self._connect() as connection: + for stream_id, profile in profiles.items(): + fields = [*getattr(profile, "categorical_fields", ()), *getattr(profile, "numeric_fields", ())] + for field in fields: + count = connection.execute("select count(distinct bucket_start) from profile_buckets where stream_id=? and field=?", (stream_id, str(field).lower())).fetchone()[0] + rows.append({"stream_id": stream_id, "field": str(field), "buckets": count, "ready": count >= 12}) + return rows + def profiles(self, source_ips: set[str]) -> dict[str, dict[str, object]]: profiles: dict[str, dict[str, object]] = {} with self._connect() as connection: diff --git a/src/fgai/dashboard.py b/src/fgai/dashboard.py index 843ff6e..3ff180f 100644 --- a/src/fgai/dashboard.py +++ b/src/fgai/dashboard.py @@ -186,10 +186,12 @@ async function refresh() { ]); const d = data.diagnostics || {}; const context = data.event_context || {}; + const profileReadiness = data.profile_readiness || []; const correlations = data.cross_source_correlations || []; document.getElementById('diagnostics').innerHTML = '

Cross-Source Correlations

' + table(correlations, [{label:'Source IP', key:'source_ip'}, {label:'Streams', render:r => esc((r.streams || []).join(', '))}, {label:'Events', key:'events'}, {label:'Security Events', key:'security_events'}]) + '

Entities

' + table(context.source_profiles || [], [{label:'Entity', key:'entity'}, {label:'Events', key:'events'}, {label:'UTM', key:'utm_events'}, {label:'Deny', key:'deny_or_threat_actions'}, {label:'Destinations', key:'distinct_destinations'}, {label:'Actions', render:r => esc((r.top_actions || []).join(', '))}]) + + '

Profile Baseline Readiness

' + table(profileReadiness, [{label:'Stream', key:'stream_id'}, {label:'Field', key:'field'}, {label:'Buckets', key:'buckets'}, {label:'Ready', render:r => r.ready ? 'ready' : 'learning'}]) + '

Security Event Samples

' + table(context.security_event_samples || [], [{label:'Entity', key:'entity'}, {label:'Type', key:'type'}, {label:'Action', key:'action'}, {label:'Severity', key:'severity'}, {label:'Destination', key:'dst'}, {label:'Service', key:'service'}]) + '

Top Sources

' + table(d.top_source_ips || [], [{label:'Value', key:'value'}, {label:'Count', key:'count'}]) + '

Top Destinations

' + table(d.top_destination_ips || [], [{label:'Value', key:'value'}, {label:'Count', key:'count'}]) + diff --git a/src/fgai/monitor.py b/src/fgai/monitor.py index ec42567..d134d90 100644 --- a/src/fgai/monitor.py +++ b/src/fgai/monitor.py @@ -77,6 +77,7 @@ def build_status( anomalies = detect_source_anomalies(events, limit=anomaly_limit, baselines=profiles, field_deviations=field_deviations) 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 [] intel_ips = sorted( { ip @@ -114,6 +115,7 @@ def build_status( "capabilities": {"threat_intel": threat_intel_status, "graylog_mcp": mcp_status}, "configuration": runtime_config, "stream_profiles": [{"stream_id": item.stream_id, "entity_field": item.entity_field, "timestamp_field": item.timestamp_field, "categorical_fields": list(item.categorical_fields), "numeric_fields": list(item.numeric_fields)} for item in stream_profiles.values()], + "profile_readiness": profile_readiness, "diagnostics": { "top_source_ips": top_field_values(events, "srcip", limit=10), "top_destination_ips": top_field_values(events, "dstip", limit=10),