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 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`.
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(), {})
|
||||
|
||||
Reference in New Issue
Block a user