From 1ced15e08f1ffa3b493fbe434e3bb3903d67d2a2249a8a461e2e80d8bb5e1e98 Mon Sep 17 00:00:00 2001 From: larssand Date: Thu, 2 Jul 2026 21:20:13 +0200 Subject: [PATCH] Fixed the incident ack/resolve behavior. --- src/fgai/dashboard.py | 21 +++++++++++++++++---- src/fgai/incidents.py | 2 +- tests/test_incidents.py | 5 +++++ 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/fgai/dashboard.py b/src/fgai/dashboard.py index 93fa091..01328ea 100644 --- a/src/fgai/dashboard.py +++ b/src/fgai/dashboard.py @@ -181,7 +181,7 @@ HTML = """ -

Events and Anomalies

Baseline and Stream Health

Operator Guidance

Waiting for monitor data.

Correlation Map

Waiting for correlated entities.

Investigation Incidents

Anomalies

Recommendations

AI Assessment

LLM assessment disabled.
+

Events and Anomalies

Baseline and Stream Health

Operator Guidance

Waiting for monitor data.

Correlation Map

Waiting for correlated entities.

Investigation Incidents

Anomalies

Recommendations

AI Assessment

LLM assessment disabled.

Triage Queue

Field Baseline Deviations

Related Activity Across Sources

Block Candidates

Threat Intelligence

Policy Findings

Diagnostics

Recommended Stream Profiles

Waiting for observed stream data.

Installed Ollama Models

Loading local Ollama models.

Runtime Configuration

@@ -336,6 +336,15 @@ function refreshEntityLabels(correlations, context) { rememberEntityLabel(item.entity, item.entity_label || item.entity_display || item.entity, item.entity_detail || ''); } } +function incidentActions(row) { + const state = String(row.lifecycle_status || 'open'); + const id = esc(row.id); + const actions = []; + if (state !== 'acknowledged') actions.push(``); + if (state !== 'resolved') actions.push(``); + if (state !== 'open') actions.push(``); + return `
${actions.join('')}
${row.note ? `
${esc(row.note)}
` : ''}`; +} function renderCorrelationExplorer(correlations, configuration) { const target = document.getElementById('correlationExplorer'); const rows = [...(correlations || [])].sort((a,b)=>(Number(b.security_events)||0)-(Number(a.security_events)||0) || (Number(b.events)||0)-(Number(a.events)||0)).slice(0,10); @@ -664,7 +673,11 @@ async function refresh() { {label:'Policies', render:r => esc((r.related_policy_ids || []).join(', '))}, {label:'Services', render:r => esc((r.related_services || []).join(', '))} ], 'recommendations'); - document.getElementById('incidents').innerHTML = table(data.incidents || [], [ + const showResolvedIncidents = document.getElementById('showResolvedIncidents')?.checked; + const visibleIncidents = (data.incidents || []).filter(item => showResolvedIncidents || (item.lifecycle_status || 'open') !== 'resolved'); + const resolvedIncidentCount = (data.incidents || []).filter(item => (item.lifecycle_status || 'open') === 'resolved').length; + document.getElementById('incidentSummary').textContent = `${visibleIncidents.length} shown, ${resolvedIncidentCount} resolved hidden.`; + document.getElementById('incidents').innerHTML = table(visibleIncidents, [ {label:'Entity', key:'entity', render:r => esc(`${entityDisplay(r.entity, r)} (${r.entity_type || 'entity'})`)}, {label:'Score', key:'score'}, {label:'Severity', render:r => `${esc(r.severity)}`}, @@ -672,7 +685,7 @@ async function refresh() { {label:'Streams', render:r => esc((r.correlated_streams || []).join(', ') || 'single stream')}, {label:'Evidence', render:r => esc((r.evidence || []).join('; '))}, {label:'Timeline', render:r => { const rows=(r.timeline||[]).map(item => `${esc(`${item.timestamp || ''} | ${item.stream || ''} | ${item.action || ''} | ${item.destination || ''} | ${item.context || item.message || ''}`)}${item.graylog_query ? `
${esc(item.graylog_query)}${graylogEvidenceLink(item.graylog_query, configuration)}` : ''}`).join('
'); const id=`incident:${r.id || r.entity}:${r.first_seen || ''}`; return rows ? `
${esc(`${r.first_seen || '-'} to ${r.last_seen || '-'}`)}

${rows}

` : '-'; }}, - {label:'Action', render:r => `
${r.note ? `
${esc(r.note)}
` : ''}`} + {label:'Action', render:r => incidentActions(r)} ], 'incidents'); document.querySelectorAll('.incident-action').forEach(button => button.addEventListener('click', async () => { const note = prompt('Incident note (optional):') || ''; @@ -1011,7 +1024,7 @@ document.querySelectorAll('.tab').forEach(button => button.addEventListener('cli document.querySelectorAll('.tab').forEach(item => item.classList.toggle('active', item === button)); document.querySelectorAll('[data-view]').forEach(view => view.classList.toggle('active', view.dataset.view === button.dataset.tab)); })); -['showReviewedFindings','showLowFindings','showExistingProfiles'].forEach(id => document.getElementById(id)?.addEventListener('change', refresh)); +['showReviewedFindings','showLowFindings','showExistingProfiles','showResolvedIncidents'].forEach(id => document.getElementById(id)?.addEventListener('change', refresh)); document.getElementById('correlationGraph')?.addEventListener('click', event => { const rect = event.currentTarget.getBoundingClientRect(); const x = event.clientX - rect.left; diff --git a/src/fgai/incidents.py b/src/fgai/incidents.py index 73a72e2..b9c0fa4 100644 --- a/src/fgai/incidents.py +++ b/src/fgai/incidents.py @@ -52,7 +52,7 @@ def build_incidents(anomalies: list[AnomalyFinding], field_deviations: dict[str, severity = "critical" if score >= 85 else "high" if score >= 60 else "medium" if score >= 35 else "low" streams = sorted({stream for item in group["correlations"] for stream in item.get("streams", [])} | {str(item.get("stream_name") or item.get("stream_title") or item.get("stream_id", "")) for item in group["fields"] if item.get("stream_id") or item.get("stream_name") or item.get("stream_title")}) timeline = sorted(group["timeline"], key=lambda item: str(item.get("timestamp", "")))[:20] - incident_id = hashlib.sha256(json.dumps({"entity": entity, "streams": streams, "evidence": list(dict.fromkeys(str(item) for item in group["evidence"] if item))[:4]}, sort_keys=True).encode("utf-8")).hexdigest()[:16] + incident_id = hashlib.sha256(json.dumps({"entity": entity, "streams": streams}, sort_keys=True).encode("utf-8")).hexdigest()[:16] incidents.append({ "id": incident_id, "entity": entity, diff --git a/tests/test_incidents.py b/tests/test_incidents.py index 339b576..5b8b228 100644 --- a/tests/test_incidents.py +++ b/tests/test_incidents.py @@ -32,3 +32,8 @@ class IncidentTests(unittest.TestCase): self.assertEqual(applied_again["lifecycle_status"], "acknowledged") self.assertEqual(applied_again["note"], "checking vpn logs") + + def test_incident_id_is_stable_when_evidence_changes(self): + first = build_incidents([], {"alice": [{"score": 15, "reason": "new login country", "stream_id": "windows"}]}, [])[0] + second = build_incidents([], {"alice": [{"score": 25, "reason": "new source ip", "stream_id": "windows"}]}, [])[0] + self.assertEqual(first["id"], second["id"])