From e3938c2581bc8ce73462586b2a45120f8ae939ba28ee335950a186c407fa3e2c Mon Sep 17 00:00:00 2001 From: larssand Date: Tue, 23 Jun 2026 20:30:14 +0200 Subject: [PATCH] Started the roadmap with incident grouping. --- src/fgai/incidents.py | 17 +++++++++++++++++ src/fgai/monitor.py | 5 ++++- tests/test_incidents.py | 9 +++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 src/fgai/incidents.py create mode 100644 tests/test_incidents.py diff --git a/src/fgai/incidents.py b/src/fgai/incidents.py new file mode 100644 index 0000000..f48d2de --- /dev/null +++ b/src/fgai/incidents.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from .models import AnomalyFinding + + +def build_incidents(anomalies: list[AnomalyFinding], field_deviations: dict[str, list[dict[str, object]]], correlations: list[dict[str, object]]) -> list[dict[str, object]]: + correlation_by_ip = {str(item.get("source_ip")): item for item in correlations} + incidents = [] + for anomaly in anomalies: + fields = field_deviations.get(anomaly.subject, []) + correlation = correlation_by_ip.get(anomaly.subject) + evidence = [*anomaly.reasons, *[str(item.get("reason", "")) for item in fields]] + score = min(100, anomaly.score + min(15, sum(int(item.get("score", 0)) for item in fields))) + if correlation: + score = min(100, score + 10); evidence.append(f"observed across {len(correlation.get('streams', []))} streams") + incidents.append({"entity": anomaly.subject, "score": score, "severity": anomaly.severity, "evidence": evidence[:8], "field_deviations": len(fields), "correlated_streams": correlation.get("streams", []) if correlation else []}) + return sorted(incidents, key=lambda item: int(item["score"]), reverse=True) diff --git a/src/fgai/monitor.py b/src/fgai/monitor.py index d134d90..2b3ceca 100644 --- a/src/fgai/monitor.py +++ b/src/fgai/monitor.py @@ -13,6 +13,7 @@ from .feedback import FeedbackStore from .graylog_mcp import GraylogMcpClient from .graylog_source import GraylogStreamSource from .history import HistoryStore +from .incidents import build_incidents from .llm import ollama_dashboard_assessment from .logs import local_in_failures, read_events, summarize_events, top_field_values from .mitigation import parse_allowlist, suggest_block_candidates @@ -90,6 +91,7 @@ def build_status( reputation = enrich_ips(intel_ips, limit=25, enabled=threat_enabled) threat_intel_status = ThreatIntelClient(enabled=threat_enabled).status() recommendations = build_recommendations(events, anomalies, reputation) + correlations = correlate_source_ips(events) block_candidates = suggest_block_candidates( events, min_events=min_block_events, @@ -130,7 +132,8 @@ def build_status( "event_context": build_event_context(events), "field_deviations": field_deviations, "feedback": feedback, - "cross_source_correlations": correlate_source_ips(events), + "cross_source_correlations": correlations, + "incidents": build_incidents(anomalies, field_deviations, correlations), "anomalies": [ { "subject": finding.subject, diff --git a/tests/test_incidents.py b/tests/test_incidents.py new file mode 100644 index 0000000..a7d187d --- /dev/null +++ b/tests/test_incidents.py @@ -0,0 +1,9 @@ +import unittest +from fgai.incidents import build_incidents +from fgai.models import AnomalyFinding + +class IncidentTests(unittest.TestCase): + def test_merges_anomaly_and_correlation(self): + anomaly = AnomalyFinding("10.0.0.1", 60, "high", "high", ["burst"], {}) + result = build_incidents([anomaly], {"10.0.0.1": [{"score": 15, "reason": "new domain"}]}, [{"source_ip": "10.0.0.1", "streams": ["DNS", "Firewall"]}]) + self.assertEqual(result[0]["score"], 85)