Added per-profile baseline readiness.

This commit is contained in:
larssand
2026-06-23 20:22:57 +02:00
parent 16881620e9
commit 8d516b980a
3 changed files with 14 additions and 0 deletions

View File

@@ -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:

View File

@@ -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 =
'<h3>Cross-Source Correlations</h3>' + 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'}]) +
'<h3>Entities</h3>' + 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(', '))}]) +
'<h3>Profile Baseline Readiness</h3>' + table(profileReadiness, [{label:'Stream', key:'stream_id'}, {label:'Field', key:'field'}, {label:'Buckets', key:'buckets'}, {label:'Ready', render:r => r.ready ? 'ready' : 'learning'}]) +
'<h3>Security Event Samples</h3>' + 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'}]) +
'<h3>Top Sources</h3>' + table(d.top_source_ips || [], [{label:'Value', key:'value'}, {label:'Count', key:'count'}]) +
'<h3>Top Destinations</h3>' + table(d.top_destination_ips || [], [{label:'Value', key:'value'}, {label:'Count', key:'count'}]) +

View File

@@ -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),