add ollama bagcround run and cache
This commit is contained in:
10
README.md
10
README.md
@@ -27,6 +27,14 @@ Or use the helper script, which creates/uses `.venv` automatically and runs `pip
|
|||||||
- Continuous monitor writing `state/fgai-status.json`
|
- Continuous monitor writing `state/fgai-status.json`
|
||||||
- Local dashboard at `http://127.0.0.1:8088`
|
- Local dashboard at `http://127.0.0.1:8088`
|
||||||
|
|
||||||
|
Enable cached Ollama analyst notes in the dashboard:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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 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:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -182,6 +190,8 @@ end
|
|||||||
- `OLLAMA_HOST`: defaults to `http://127.0.0.1:11434`.
|
- `OLLAMA_HOST`: defaults to `http://127.0.0.1:11434`.
|
||||||
- `OLLAMA_MODEL`: defaults to `llama3.3`.
|
- `OLLAMA_MODEL`: defaults to `llama3.3`.
|
||||||
- `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_INTERVAL`: seconds between dashboard LLM notes, defaults to `300`.
|
||||||
- `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`.
|
||||||
|
|||||||
@@ -192,6 +192,10 @@ def run_monitor(args: argparse.Namespace) -> int:
|
|||||||
policy_path=args.policies,
|
policy_path=args.policies,
|
||||||
interval=args.interval,
|
interval=args.interval,
|
||||||
anomaly_limit=args.anomaly_limit,
|
anomaly_limit=args.anomaly_limit,
|
||||||
|
llm=args.llm,
|
||||||
|
llm_interval=args.llm_interval,
|
||||||
|
llm_model=args.model,
|
||||||
|
llm_timeout=args.llm_timeout,
|
||||||
)
|
)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
@@ -272,6 +276,10 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
monitor.add_argument("--policies", default=None, help="Optional FortiGate policy JSON/config file to audit continuously")
|
monitor.add_argument("--policies", default=None, help="Optional FortiGate policy JSON/config file to audit continuously")
|
||||||
monitor.add_argument("--interval", type=int, default=10, help="Seconds between analysis runs")
|
monitor.add_argument("--interval", type=int, default=10, help="Seconds between analysis runs")
|
||||||
monitor.add_argument("--anomaly-limit", type=int, default=20, help="Maximum anomaly findings to include")
|
monitor.add_argument("--anomaly-limit", type=int, default=20, help="Maximum anomaly findings to include")
|
||||||
|
monitor.add_argument("--llm", action="store_true", help="Generate cached Ollama analyst note for dashboard")
|
||||||
|
monitor.add_argument("--llm-interval", type=int, default=300, help="Seconds between Ollama dashboard assessments")
|
||||||
|
monitor.add_argument("--model", default=None, help="Ollama model name")
|
||||||
|
monitor.add_argument("--llm-timeout", type=int, default=None, help="Ollama request timeout in seconds")
|
||||||
monitor.set_defaults(func=run_monitor)
|
monitor.set_defaults(func=run_monitor)
|
||||||
|
|
||||||
dashboard = subparsers.add_parser("dashboard", help="Serve local fgAI dashboard")
|
dashboard = subparsers.add_parser("dashboard", help="Serve local fgAI dashboard")
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ HTML = """<!doctype html>
|
|||||||
<section class="panel"><h2>Live Status</h2><div id="liveStatus" class="muted">Waiting for monitor data.</div></section>
|
<section class="panel"><h2>Live Status</h2><div id="liveStatus" class="muted">Waiting for monitor data.</div></section>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
<section class="panel"><h2>AI Assessment</h2><div id="llmAssessment" class="muted">LLM assessment disabled.</div></section>
|
||||||
<section class="panel"><h2>Anomalies</h2><div id="anomalies"></div></section>
|
<section class="panel"><h2>Anomalies</h2><div id="anomalies"></div></section>
|
||||||
<section class="panel"><h2>Recommendations</h2><div id="recommendations"></div></section>
|
<section class="panel"><h2>Recommendations</h2><div id="recommendations"></div></section>
|
||||||
<section class="panel"><h2>Block Candidates</h2><div id="blocks"></div></section>
|
<section class="panel"><h2>Block Candidates</h2><div id="blocks"></div></section>
|
||||||
@@ -84,6 +85,9 @@ async function refresh() {
|
|||||||
`Critical anomalies: ${esc((a.critical || 0))}`,
|
`Critical anomalies: ${esc((a.critical || 0))}`,
|
||||||
`High anomalies: ${esc((a.high || 0))}`
|
`High anomalies: ${esc((a.high || 0))}`
|
||||||
].join('<br>');
|
].join('<br>');
|
||||||
|
const llm = data.llm_assessment || {};
|
||||||
|
const llmText = llm.text ? esc(llm.text).replace(/\\n/g, '<br>') : esc(llm.error || 'LLM assessment disabled or waiting for first run.');
|
||||||
|
document.getElementById('llmAssessment').innerHTML = `<div>Status: <code>${esc(llm.status || 'unknown')}</code></div><p>${llmText}</p>`;
|
||||||
document.getElementById('anomalies').innerHTML = table(data.anomalies || [], [
|
document.getElementById('anomalies').innerHTML = table(data.anomalies || [], [
|
||||||
{label:'Source', key:'subject'},
|
{label:'Source', key:'subject'},
|
||||||
{label:'Score', key:'score'},
|
{label:'Score', key:'score'},
|
||||||
|
|||||||
@@ -46,3 +46,28 @@ def ollama_summary(
|
|||||||
with request.urlopen(req, timeout=selected_timeout) as response:
|
with request.urlopen(req, timeout=selected_timeout) as response:
|
||||||
data = json.loads(response.read().decode("utf-8"))
|
data = json.loads(response.read().decode("utf-8"))
|
||||||
return str(data.get("response", "")).strip()
|
return str(data.get("response", "")).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def ollama_dashboard_assessment(analysis: dict[str, object], model: str | None = None, timeout: int | None = None) -> str:
|
||||||
|
compact = {
|
||||||
|
"summary": analysis.get("summary", {}),
|
||||||
|
"anomaly_summary": analysis.get("anomaly_summary", {}),
|
||||||
|
"top_anomalies": analysis.get("anomalies", [])[:5],
|
||||||
|
"top_recommendations": analysis.get("recommendations", [])[:5],
|
||||||
|
"block_candidates": analysis.get("block_candidates", [])[:5],
|
||||||
|
"policy_findings": analysis.get("policy_findings", [])[:5],
|
||||||
|
}
|
||||||
|
return ollama_summary(
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
model,
|
||||||
|
analysis={
|
||||||
|
"task": (
|
||||||
|
"Write a concise dashboard analyst note for a FortiGate admin. "
|
||||||
|
"Explain likely cause, whether this looks malicious or noisy, and the next action. "
|
||||||
|
"Mention policyid=0 as implicit deny/drop, not an editable policy."
|
||||||
|
),
|
||||||
|
"data": compact,
|
||||||
|
},
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import time
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from .anomaly import anomaly_summary, detect_source_anomalies
|
from .anomaly import anomaly_summary, detect_source_anomalies
|
||||||
|
from .llm import ollama_dashboard_assessment
|
||||||
from .logs import local_in_failures, read_events, summarize_events, top_field_values
|
from .logs import local_in_failures, read_events, summarize_events, top_field_values
|
||||||
from .mitigation import parse_allowlist, suggest_block_candidates
|
from .mitigation import parse_allowlist, suggest_block_candidates
|
||||||
from .policies import audit_policies, read_policies
|
from .policies import audit_policies, read_policies
|
||||||
@@ -94,6 +95,24 @@ def build_status(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def add_llm_assessment(status: dict[str, object], *, previous: str | None = None, model: str | None = None, timeout: int | None = None) -> None:
|
||||||
|
try:
|
||||||
|
status["llm_assessment"] = {
|
||||||
|
"enabled": True,
|
||||||
|
"status": "ok",
|
||||||
|
"generated_at": int(time.time()),
|
||||||
|
"text": ollama_dashboard_assessment(status, model=model, timeout=timeout),
|
||||||
|
}
|
||||||
|
except Exception as exc:
|
||||||
|
status["llm_assessment"] = {
|
||||||
|
"enabled": True,
|
||||||
|
"status": "error",
|
||||||
|
"generated_at": int(time.time()),
|
||||||
|
"error": str(exc),
|
||||||
|
"text": previous or "",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def write_status(status: dict[str, object], output: str) -> None:
|
def write_status(status: dict[str, object], output: str) -> None:
|
||||||
output_path = Path(output)
|
output_path = Path(output)
|
||||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
@@ -109,10 +128,33 @@ def monitor_loop(
|
|||||||
policy_path: str | None = None,
|
policy_path: str | None = None,
|
||||||
interval: int = 10,
|
interval: int = 10,
|
||||||
anomaly_limit: int = 20,
|
anomaly_limit: int = 20,
|
||||||
|
llm: bool = False,
|
||||||
|
llm_interval: int = 300,
|
||||||
|
llm_model: str | None = None,
|
||||||
|
llm_timeout: int | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
print(f"Monitoring {log_path}")
|
print(f"Monitoring {log_path}")
|
||||||
print(f"Writing status to {output}")
|
print(f"Writing status to {output}")
|
||||||
|
last_llm_at = 0
|
||||||
|
last_llm_text: str | None = None
|
||||||
while True:
|
while True:
|
||||||
status = build_status(log_path, policy_path=policy_path, anomaly_limit=anomaly_limit)
|
status = build_status(log_path, policy_path=policy_path, anomaly_limit=anomaly_limit)
|
||||||
|
if llm:
|
||||||
|
now = int(time.time())
|
||||||
|
if now - last_llm_at >= llm_interval:
|
||||||
|
add_llm_assessment(status, previous=last_llm_text, model=llm_model, timeout=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
|
||||||
|
else:
|
||||||
|
status["llm_assessment"] = {
|
||||||
|
"enabled": True,
|
||||||
|
"status": "cached",
|
||||||
|
"generated_at": last_llm_at,
|
||||||
|
"text": last_llm_text or "",
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
status["llm_assessment"] = {"enabled": False, "status": "disabled", "text": ""}
|
||||||
write_status(status, output)
|
write_status(status, output)
|
||||||
time.sleep(interval)
|
time.sleep(interval)
|
||||||
|
|||||||
11
start.sh
11
start.sh
@@ -10,6 +10,10 @@ LISTENER_LOG="${FGAI_LISTENER_LOG:-$ROOT_DIR/logs/fgai-listener.log}"
|
|||||||
POLICY_FILE="${FGAI_POLICY_FILE:-$ROOT_DIR/exports/policies.json}"
|
POLICY_FILE="${FGAI_POLICY_FILE:-$ROOT_DIR/exports/policies.json}"
|
||||||
STATE_FILE="${FGAI_STATE_FILE:-$ROOT_DIR/state/fgai-status.json}"
|
STATE_FILE="${FGAI_STATE_FILE:-$ROOT_DIR/state/fgai-status.json}"
|
||||||
MONITOR_INTERVAL="${FGAI_MONITOR_INTERVAL:-10}"
|
MONITOR_INTERVAL="${FGAI_MONITOR_INTERVAL:-10}"
|
||||||
|
LLM_ENABLED="${FGAI_LLM:-0}"
|
||||||
|
LLM_INTERVAL="${FGAI_LLM_INTERVAL:-300}"
|
||||||
|
LLM_TIMEOUT="${OLLAMA_TIMEOUT:-180}"
|
||||||
|
LLM_MODEL="${OLLAMA_MODEL:-}"
|
||||||
DASHBOARD_HOST="${FGAI_DASHBOARD_HOST:-127.0.0.1}"
|
DASHBOARD_HOST="${FGAI_DASHBOARD_HOST:-127.0.0.1}"
|
||||||
DASHBOARD_PORT="${FGAI_DASHBOARD_PORT:-8088}"
|
DASHBOARD_PORT="${FGAI_DASHBOARD_PORT:-8088}"
|
||||||
LISTENER_PID_FILE="${FGAI_LISTENER_PID_FILE:-$ROOT_DIR/run/fgai-listener.pid}"
|
LISTENER_PID_FILE="${FGAI_LISTENER_PID_FILE:-$ROOT_DIR/run/fgai-listener.pid}"
|
||||||
@@ -27,6 +31,7 @@ usage() {
|
|||||||
printf ' FGAI_SYSLOG_FILE=%s\n' "$LOG_FILE"
|
printf ' FGAI_SYSLOG_FILE=%s\n' "$LOG_FILE"
|
||||||
printf ' FGAI_LISTENER_LOG=%s\n' "$LISTENER_LOG"
|
printf ' FGAI_LISTENER_LOG=%s\n' "$LISTENER_LOG"
|
||||||
printf ' FGAI_DASHBOARD_PORT=%s\n' "$DASHBOARD_PORT"
|
printf ' FGAI_DASHBOARD_PORT=%s\n' "$DASHBOARD_PORT"
|
||||||
|
printf ' FGAI_LLM=%s\n' "$LLM_ENABLED"
|
||||||
}
|
}
|
||||||
|
|
||||||
activate_venv_for_script() {
|
activate_venv_for_script() {
|
||||||
@@ -109,6 +114,12 @@ start_monitor() {
|
|||||||
if [ -f "$POLICY_FILE" ]; then
|
if [ -f "$POLICY_FILE" ]; then
|
||||||
monitor_args+=(--policies "$POLICY_FILE")
|
monitor_args+=(--policies "$POLICY_FILE")
|
||||||
fi
|
fi
|
||||||
|
if [ "$LLM_ENABLED" = "1" ] || [ "$LLM_ENABLED" = "true" ]; then
|
||||||
|
monitor_args+=(--llm --llm-interval "$LLM_INTERVAL" --llm-timeout "$LLM_TIMEOUT")
|
||||||
|
if [ -n "$LLM_MODEL" ]; then
|
||||||
|
monitor_args+=(--model "$LLM_MODEL")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
nohup "$VENV_DIR/bin/fgai" "${monitor_args[@]}" > "$MONITOR_LOG" 2>&1 &
|
nohup "$VENV_DIR/bin/fgai" "${monitor_args[@]}" > "$MONITOR_LOG" 2>&1 &
|
||||||
printf '%s\n' "$!" > "$MONITOR_PID_FILE"
|
printf '%s\n' "$!" > "$MONITOR_PID_FILE"
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
from fgai.monitor import build_status, write_status
|
from fgai.monitor import add_llm_assessment, build_status, write_status
|
||||||
|
|
||||||
|
|
||||||
class MonitorTests(unittest.TestCase):
|
class MonitorTests(unittest.TestCase):
|
||||||
@@ -33,6 +34,24 @@ class MonitorTests(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertTrue(output.exists())
|
self.assertTrue(output.exists())
|
||||||
|
|
||||||
|
def test_add_llm_assessment_records_error_without_ollama(self):
|
||||||
|
status = {"summary": {}, "anomalies": []}
|
||||||
|
|
||||||
|
with patch("fgai.monitor.ollama_dashboard_assessment", side_effect=TimeoutError("timeout")):
|
||||||
|
add_llm_assessment(status, previous="old text")
|
||||||
|
|
||||||
|
self.assertEqual(status["llm_assessment"]["status"], "error")
|
||||||
|
self.assertEqual(status["llm_assessment"]["text"], "old text")
|
||||||
|
|
||||||
|
def test_add_llm_assessment_records_text(self):
|
||||||
|
status = {"summary": {}, "anomalies": []}
|
||||||
|
|
||||||
|
with patch("fgai.monitor.ollama_dashboard_assessment", return_value="looks noisy"):
|
||||||
|
add_llm_assessment(status)
|
||||||
|
|
||||||
|
self.assertEqual(status["llm_assessment"]["status"], "ok")
|
||||||
|
self.assertEqual(status["llm_assessment"]["text"], "looks noisy")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user