fix clear investi

This commit is contained in:
larssand
2026-07-06 16:49:30 +02:00
parent 49ac7e2958
commit 649c29a8c2
8 changed files with 88 additions and 7 deletions

View File

@@ -460,6 +460,8 @@ FGAI_LLM=1 OLLAMA_MODEL=llama3.1 ./start.sh restart
```
The monitor refreshes deterministic detections every `FGAI_MONITOR_INTERVAL` seconds and refreshes the LLM note every `FGAI_LLM_INTERVAL` seconds, default `300`.
The dashboard also exposes `Ollama assessment timeout seconds`; raise this when
Ollama is running but large multi-stream summaries still time out.
The script activates `.venv` inside the script process. If you also want your current shell prompt to show the venv, run:
@@ -687,6 +689,8 @@ end
- `OLLAMA_TIMEOUT`: Ollama request timeout in seconds, defaults to `180`.
- `FGAI_LLM`: set to `1` to enable dashboard Ollama analyst notes.
- `FGAI_LLM_INTERVAL`: seconds between dashboard LLM notes, defaults to `300`.
- `llm_timeout` in the dashboard config controls the dashboard assessment
timeout after startup; it defaults to `180`.
- `FGAI_THREAT_INTEL`: set to `1` to enable external threat intelligence lookups.
- `ABUSEIPDB_API_KEY`: AbuseIPDB API key for public IP reputation enrichment.
- `ABUSEIPDB_MAX_AGE_DAYS`: report age window for AbuseIPDB, defaults to `90`.

View File

@@ -26,6 +26,7 @@ DEFAULT_CONFIG: dict[str, object] = {
"graylog_field_mapping": "",
"llm_enabled": False,
"llm_model": "",
"llm_timeout": 180,
"profile_advisor_enabled": False,
"profile_advisor_model": "qwen3:8b",
"profile_advisor_timeout": 240,
@@ -73,7 +74,7 @@ class ConfigStore:
current[key] = value
elif key == "graylog_fetch_mode" and value in {"auto", "raw", "aggregate"}:
current[key] = value
elif key in {"graylog_range_seconds", "graylog_max_events_per_stream", "graylog_raw_sample_events", "graylog_mcp_call_timeout_seconds", "graylog_mcp_poll_timeout_seconds", "baseline_retention_days", "baseline_value_retention_days", "baseline_max_values_per_field", "baseline_training_days", "profile_advisor_timeout", "threat_intel_daily_limit", "threat_intel_ttl_seconds", "threat_intel_error_ttl_seconds", "abuseipdb_max_age_days"}:
elif key in {"graylog_range_seconds", "graylog_max_events_per_stream", "graylog_raw_sample_events", "graylog_mcp_call_timeout_seconds", "graylog_mcp_poll_timeout_seconds", "baseline_retention_days", "baseline_value_retention_days", "baseline_max_values_per_field", "baseline_training_days", "llm_timeout", "profile_advisor_timeout", "threat_intel_daily_limit", "threat_intel_ttl_seconds", "threat_intel_error_ttl_seconds", "abuseipdb_max_age_days"}:
try:
minimum = 60 if key in {"graylog_range_seconds", "graylog_mcp_poll_timeout_seconds"} else 1
current[key] = max(minimum, int(value))

File diff suppressed because one or more lines are too long

View File

@@ -112,6 +112,21 @@ class IncidentStore:
self._write(states)
return {"id": incident_id, **entry}
def clear(self, *, status: str | None = None) -> dict[str, object]:
states = self.entries()
if status is None:
removed = len(states)
states = {}
else:
status = status.lower()
if status not in {"open", "acknowledged", "resolved"}:
raise ValueError("invalid incident status")
before = len(states)
states = {key: value for key, value in states.items() if str(value.get("status", "open")) != status}
removed = before - len(states)
self._write(states)
return {"removed": removed, "remaining": len(states), "status": status or "all"}
def _write(self, states: dict[str, dict[str, object]]) -> None:
self.path.parent.mkdir(parents=True, exist_ok=True)
self.path.write_text(json.dumps(states, indent=2, sort_keys=True), encoding="utf-8")

View File

@@ -742,6 +742,7 @@ def monitor_loop(
runtime = ConfigStore(config_path).read() if config_path and Path(config_path).exists() else {}
effective_llm = bool(runtime.get("llm_enabled")) if runtime else llm
effective_model = str(runtime.get("llm_model") or llm_model or "")
effective_llm_timeout = int(runtime.get("llm_timeout") or llm_timeout or 180)
mcp_call_timeout = max(1, int(runtime.get("graylog_mcp_call_timeout_seconds", 8) or 8))
mcp_poll_timeout = max(60, int(runtime.get("graylog_mcp_poll_timeout_seconds", 240) or 240))
if runtime.get("log_source") == "graylog_mcp":
@@ -779,15 +780,13 @@ def monitor_loop(
StatusSnapshotStore(status_cache_path).save("last_good", status)
write_status(status, output)
now = int(time.time())
if runtime.get("profile_advisor_enabled"):
add_profile_advisor(status, runtime)
write_status(status, output)
if now - last_llm_at >= llm_interval:
add_llm_assessment(status, previous=last_llm_text, model=effective_model or None, timeout=llm_timeout)
add_llm_assessment(status, previous=last_llm_text, model=effective_model or None, timeout=effective_llm_timeout)
assessment = status.get("llm_assessment", {})
if isinstance(assessment, dict):
last_llm_text = str(assessment.get("text", "") or last_llm_text or "")
last_llm_at = now
write_status(status, output)
else:
status["llm_assessment"] = {
"enabled": True,
@@ -795,6 +794,9 @@ def monitor_loop(
"generated_at": last_llm_at,
"text": last_llm_text or "",
}
if runtime.get("profile_advisor_enabled"):
add_profile_advisor(status, runtime)
write_status(status, output)
else:
status["llm_assessment"] = {"enabled": False, "status": "disabled", "text": ""}
if runtime.get("profile_advisor_enabled"):

View File

@@ -48,6 +48,14 @@ class ConfigTests(unittest.TestCase):
self.assertEqual(public["graylog_mcp_call_timeout_seconds"], 6)
self.assertEqual(public["graylog_mcp_poll_timeout_seconds"], 180)
def test_llm_timeout_setting_is_numeric_and_bounded(self):
with tempfile.TemporaryDirectory() as directory:
store = ConfigStore(str(Path(directory) / "config.json"))
public = store.update({"llm_timeout": "0"})
self.assertEqual(public["llm_timeout"], 1)
public = store.update({"llm_timeout": "300"})
self.assertEqual(public["llm_timeout"], 300)
def test_graylog_tls_verify_can_be_disabled(self):
with tempfile.TemporaryDirectory() as directory:
store = ConfigStore(str(Path(directory) / "config.json"))

View File

@@ -57,6 +57,11 @@ class DashboardTests(unittest.TestCase):
self.assertIn("incidentNotice", HTML)
self.assertIn("Incident marked", HTML)
def test_dashboard_can_clear_stored_incident_states(self):
self.assertIn('id="clearResolvedIncidents"', HTML)
self.assertIn('id="clearAllIncidents"', HTML)
self.assertIn("/api/incidents/clear", HTML)
def test_dashboard_does_not_clear_streams_when_picker_is_unloaded(self):
self.assertIn("const streamValues = Object.values(window.streamSelection || {})", HTML)
self.assertIn("if (streamValues.length)", HTML)
@@ -76,6 +81,10 @@ class DashboardTests(unittest.TestCase):
self.assertIn("assessment unavailable", HTML)
self.assertIn("Ollama assessment: unavailable", HTML)
def test_dashboard_exposes_ollama_assessment_timeout_setting(self):
self.assertIn('name="llm_timeout"', HTML)
self.assertIn("llm_timeout: config.llm_timeout || 180", HTML)
if __name__ == "__main__":
unittest.main()

View File

@@ -37,3 +37,17 @@ class IncidentTests(unittest.TestCase):
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"])
def test_incident_store_can_clear_resolved_or_all_states(self):
with tempfile.TemporaryDirectory() as directory:
store = IncidentStore(str(Path(directory) / "incidents.json"))
store.update("one", "resolved")
store.update("two", "acknowledged")
resolved = store.clear(status="resolved")
self.assertEqual(resolved, {"removed": 1, "remaining": 1, "status": "resolved"})
self.assertEqual(set(store.entries()), {"two"})
all_items = store.clear()
self.assertEqual(all_items, {"removed": 1, "remaining": 0, "status": "all"})
self.assertEqual(store.entries(), {})