fix clear investi
This commit is contained in:
@@ -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 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:
|
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`.
|
- `OLLAMA_TIMEOUT`: Ollama request timeout in seconds, defaults to `180`.
|
||||||
- `FGAI_LLM`: set to `1` to enable dashboard Ollama analyst notes.
|
- `FGAI_LLM`: set to `1` to enable dashboard Ollama analyst notes.
|
||||||
- `FGAI_LLM_INTERVAL`: seconds between dashboard LLM notes, defaults to `300`.
|
- `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.
|
- `FGAI_THREAT_INTEL`: set to `1` to enable external threat intelligence lookups.
|
||||||
- `ABUSEIPDB_API_KEY`: AbuseIPDB API key for public IP reputation enrichment.
|
- `ABUSEIPDB_API_KEY`: AbuseIPDB API key for public IP reputation enrichment.
|
||||||
- `ABUSEIPDB_MAX_AGE_DAYS`: report age window for AbuseIPDB, defaults to `90`.
|
- `ABUSEIPDB_MAX_AGE_DAYS`: report age window for AbuseIPDB, defaults to `90`.
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ DEFAULT_CONFIG: dict[str, object] = {
|
|||||||
"graylog_field_mapping": "",
|
"graylog_field_mapping": "",
|
||||||
"llm_enabled": False,
|
"llm_enabled": False,
|
||||||
"llm_model": "",
|
"llm_model": "",
|
||||||
|
"llm_timeout": 180,
|
||||||
"profile_advisor_enabled": False,
|
"profile_advisor_enabled": False,
|
||||||
"profile_advisor_model": "qwen3:8b",
|
"profile_advisor_model": "qwen3:8b",
|
||||||
"profile_advisor_timeout": 240,
|
"profile_advisor_timeout": 240,
|
||||||
@@ -73,7 +74,7 @@ class ConfigStore:
|
|||||||
current[key] = value
|
current[key] = value
|
||||||
elif key == "graylog_fetch_mode" and value in {"auto", "raw", "aggregate"}:
|
elif key == "graylog_fetch_mode" and value in {"auto", "raw", "aggregate"}:
|
||||||
current[key] = value
|
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:
|
try:
|
||||||
minimum = 60 if key in {"graylog_range_seconds", "graylog_mcp_poll_timeout_seconds"} else 1
|
minimum = 60 if key in {"graylog_range_seconds", "graylog_mcp_poll_timeout_seconds"} else 1
|
||||||
current[key] = max(minimum, int(value))
|
current[key] = max(minimum, int(value))
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -112,6 +112,21 @@ class IncidentStore:
|
|||||||
self._write(states)
|
self._write(states)
|
||||||
return {"id": incident_id, **entry}
|
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:
|
def _write(self, states: dict[str, dict[str, object]]) -> None:
|
||||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
self.path.write_text(json.dumps(states, indent=2, sort_keys=True), encoding="utf-8")
|
self.path.write_text(json.dumps(states, indent=2, sort_keys=True), encoding="utf-8")
|
||||||
|
|||||||
@@ -742,6 +742,7 @@ def monitor_loop(
|
|||||||
runtime = ConfigStore(config_path).read() if config_path and Path(config_path).exists() else {}
|
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_llm = bool(runtime.get("llm_enabled")) if runtime else llm
|
||||||
effective_model = str(runtime.get("llm_model") or llm_model or "")
|
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_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))
|
mcp_poll_timeout = max(60, int(runtime.get("graylog_mcp_poll_timeout_seconds", 240) or 240))
|
||||||
if runtime.get("log_source") == "graylog_mcp":
|
if runtime.get("log_source") == "graylog_mcp":
|
||||||
@@ -779,15 +780,13 @@ def monitor_loop(
|
|||||||
StatusSnapshotStore(status_cache_path).save("last_good", status)
|
StatusSnapshotStore(status_cache_path).save("last_good", status)
|
||||||
write_status(status, output)
|
write_status(status, output)
|
||||||
now = int(time.time())
|
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:
|
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", {})
|
assessment = status.get("llm_assessment", {})
|
||||||
if isinstance(assessment, dict):
|
if isinstance(assessment, dict):
|
||||||
last_llm_text = str(assessment.get("text", "") or last_llm_text or "")
|
last_llm_text = str(assessment.get("text", "") or last_llm_text or "")
|
||||||
last_llm_at = now
|
last_llm_at = now
|
||||||
|
write_status(status, output)
|
||||||
else:
|
else:
|
||||||
status["llm_assessment"] = {
|
status["llm_assessment"] = {
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
@@ -795,6 +794,9 @@ def monitor_loop(
|
|||||||
"generated_at": last_llm_at,
|
"generated_at": last_llm_at,
|
||||||
"text": last_llm_text or "",
|
"text": last_llm_text or "",
|
||||||
}
|
}
|
||||||
|
if runtime.get("profile_advisor_enabled"):
|
||||||
|
add_profile_advisor(status, runtime)
|
||||||
|
write_status(status, output)
|
||||||
else:
|
else:
|
||||||
status["llm_assessment"] = {"enabled": False, "status": "disabled", "text": ""}
|
status["llm_assessment"] = {"enabled": False, "status": "disabled", "text": ""}
|
||||||
if runtime.get("profile_advisor_enabled"):
|
if runtime.get("profile_advisor_enabled"):
|
||||||
|
|||||||
@@ -48,6 +48,14 @@ class ConfigTests(unittest.TestCase):
|
|||||||
self.assertEqual(public["graylog_mcp_call_timeout_seconds"], 6)
|
self.assertEqual(public["graylog_mcp_call_timeout_seconds"], 6)
|
||||||
self.assertEqual(public["graylog_mcp_poll_timeout_seconds"], 180)
|
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):
|
def test_graylog_tls_verify_can_be_disabled(self):
|
||||||
with tempfile.TemporaryDirectory() as directory:
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
store = ConfigStore(str(Path(directory) / "config.json"))
|
store = ConfigStore(str(Path(directory) / "config.json"))
|
||||||
|
|||||||
@@ -57,6 +57,11 @@ class DashboardTests(unittest.TestCase):
|
|||||||
self.assertIn("incidentNotice", HTML)
|
self.assertIn("incidentNotice", HTML)
|
||||||
self.assertIn("Incident marked", 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):
|
def test_dashboard_does_not_clear_streams_when_picker_is_unloaded(self):
|
||||||
self.assertIn("const streamValues = Object.values(window.streamSelection || {})", HTML)
|
self.assertIn("const streamValues = Object.values(window.streamSelection || {})", HTML)
|
||||||
self.assertIn("if (streamValues.length)", HTML)
|
self.assertIn("if (streamValues.length)", HTML)
|
||||||
@@ -76,6 +81,10 @@ class DashboardTests(unittest.TestCase):
|
|||||||
self.assertIn("assessment unavailable", HTML)
|
self.assertIn("assessment unavailable", HTML)
|
||||||
self.assertIn("Ollama 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__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -37,3 +37,17 @@ class IncidentTests(unittest.TestCase):
|
|||||||
first = build_incidents([], {"alice": [{"score": 15, "reason": "new login country", "stream_id": "windows"}]}, [])[0]
|
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]
|
second = build_incidents([], {"alice": [{"score": 25, "reason": "new source ip", "stream_id": "windows"}]}, [])[0]
|
||||||
self.assertEqual(first["id"], second["id"])
|
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(), {})
|
||||||
|
|||||||
Reference in New Issue
Block a user