add settings in UI
This commit is contained in:
@@ -204,12 +204,13 @@ def run_monitor(args: argparse.Namespace) -> int:
|
|||||||
llm_model=args.model,
|
llm_model=args.model,
|
||||||
llm_timeout=args.llm_timeout,
|
llm_timeout=args.llm_timeout,
|
||||||
baseline_path=args.baseline_db,
|
baseline_path=args.baseline_db,
|
||||||
|
config_path=args.config_file,
|
||||||
)
|
)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def run_dashboard(args: argparse.Namespace) -> int:
|
def run_dashboard(args: argparse.Namespace) -> int:
|
||||||
serve_dashboard(args.host, args.port, args.status_file, image_dir=args.image_dir)
|
serve_dashboard(args.host, args.port, args.status_file, image_dir=args.image_dir, config_file=args.config_file)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
@@ -287,6 +288,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
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("--baseline-db", default="state/fgai-baseline.sqlite3", help="SQLite database for historical behavior baselines")
|
monitor.add_argument("--baseline-db", default="state/fgai-baseline.sqlite3", help="SQLite database for historical behavior baselines")
|
||||||
|
monitor.add_argument("--config-file", default="state/fgai-config.json", help="Runtime configuration written by dashboard")
|
||||||
monitor.add_argument("--llm", action="store_true", help="Generate cached Ollama analyst note for dashboard")
|
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("--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("--model", default=None, help="Ollama model name")
|
||||||
@@ -298,6 +300,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
dashboard.add_argument("--port", type=int, default=8088, help="Dashboard TCP port")
|
dashboard.add_argument("--port", type=int, default=8088, help="Dashboard TCP port")
|
||||||
dashboard.add_argument("--status-file", default="state/fgai-status.json", help="Status JSON produced by monitor")
|
dashboard.add_argument("--status-file", default="state/fgai-status.json", help="Status JSON produced by monitor")
|
||||||
dashboard.add_argument("--image-dir", default="images", help="Directory containing dashboard images")
|
dashboard.add_argument("--image-dir", default="images", help="Directory containing dashboard images")
|
||||||
|
dashboard.add_argument("--config-file", default="state/fgai-config.json", help="Local runtime configuration JSON")
|
||||||
dashboard.set_defaults(func=run_dashboard)
|
dashboard.set_defaults(func=run_dashboard)
|
||||||
|
|
||||||
return parser
|
return parser
|
||||||
|
|||||||
54
src/fgai/config.py
Normal file
54
src/fgai/config.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_CONFIG: dict[str, object] = {
|
||||||
|
"log_source": "local_syslog",
|
||||||
|
"graylog_mcp_url": "",
|
||||||
|
"graylog_stream": "",
|
||||||
|
"llm_enabled": False,
|
||||||
|
"llm_model": "",
|
||||||
|
"threat_intel_enabled": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
EDITABLE_FIELDS = set(DEFAULT_CONFIG) | {"graylog_mcp_token"}
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigStore:
|
||||||
|
def __init__(self, path: str) -> None:
|
||||||
|
self.path = Path(path)
|
||||||
|
|
||||||
|
def read(self) -> dict[str, object]:
|
||||||
|
try:
|
||||||
|
stored = json.loads(self.path.read_text(encoding="utf-8"))
|
||||||
|
except (FileNotFoundError, json.JSONDecodeError, OSError):
|
||||||
|
stored = {}
|
||||||
|
return {**DEFAULT_CONFIG, **{key: value for key, value in stored.items() if key in EDITABLE_FIELDS}}
|
||||||
|
|
||||||
|
def public(self) -> dict[str, object]:
|
||||||
|
config = self.read()
|
||||||
|
config["graylog_mcp_token_configured"] = bool(config.pop("graylog_mcp_token", ""))
|
||||||
|
return config
|
||||||
|
|
||||||
|
def update(self, values: dict[str, object]) -> dict[str, object]:
|
||||||
|
current = self.read()
|
||||||
|
for key, value in values.items():
|
||||||
|
if key not in EDITABLE_FIELDS:
|
||||||
|
continue
|
||||||
|
if key == "graylog_mcp_token" and value == "":
|
||||||
|
continue
|
||||||
|
if key in {"llm_enabled", "threat_intel_enabled"}:
|
||||||
|
current[key] = bool(value)
|
||||||
|
elif key == "log_source" and value in {"local_syslog", "graylog_mcp"}:
|
||||||
|
current[key] = value
|
||||||
|
elif isinstance(value, str):
|
||||||
|
current[key] = value.strip()
|
||||||
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
temporary = self.path.with_suffix(".tmp")
|
||||||
|
temporary.write_text(json.dumps(current, indent=2, sort_keys=True), encoding="utf-8")
|
||||||
|
os.chmod(temporary, 0o600)
|
||||||
|
temporary.replace(self.path)
|
||||||
|
return self.public()
|
||||||
@@ -4,6 +4,8 @@ import json
|
|||||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from .config import ConfigStore
|
||||||
|
|
||||||
|
|
||||||
HTML = """<!doctype html>
|
HTML = """<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
@@ -56,10 +58,11 @@ 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>
|
||||||
<nav class="tabs" aria-label="Dashboard views"><button class="tab active" data-tab="overview">Overview</button><button class="tab" data-tab="findings">Findings</button><button class="tab" data-tab="diagnostics">Diagnostics</button></nav>
|
<nav class="tabs" aria-label="Dashboard views"><button class="tab active" data-tab="overview">Overview</button><button class="tab" data-tab="findings">Findings</button><button class="tab" data-tab="diagnostics">Diagnostics</button><button class="tab" data-tab="settings">Settings</button></nav>
|
||||||
<div data-view="overview" class="active"><section class="panel"><h2>AI Assessment</h2><div id="llmAssessment" class="muted">LLM assessment disabled.</div></section><section class="split"><div class="panel"><h2>Anomalies</h2><div id="anomalies"></div></div><div class="panel"><h2>Recommendations</h2><div id="recommendations"></div></div></section></div>
|
<div data-view="overview" class="active"><section class="panel"><h2>AI Assessment</h2><div id="llmAssessment" class="muted">LLM assessment disabled.</div></section><section class="split"><div class="panel"><h2>Anomalies</h2><div id="anomalies"></div></div><div class="panel"><h2>Recommendations</h2><div id="recommendations"></div></div></section></div>
|
||||||
<div data-view="findings"><section class="split"><div class="panel"><h2>Block Candidates</h2><div id="blocks"></div></div><div class="panel"><h2>Threat Intelligence</h2><div id="reputation"></div></div></section><section class="panel"><h2>Policy Findings</h2><div id="policies"></div></section></div>
|
<div data-view="findings"><section class="split"><div class="panel"><h2>Block Candidates</h2><div id="blocks"></div></div><div class="panel"><h2>Threat Intelligence</h2><div id="reputation"></div></div></section><section class="panel"><h2>Policy Findings</h2><div id="policies"></div></section></div>
|
||||||
<div data-view="diagnostics"><section class="panel"><h2>Diagnostics</h2><div id="diagnostics"></div></section></div>
|
<div data-view="diagnostics"><section class="panel"><h2>Diagnostics</h2><div id="diagnostics"></div></section></div>
|
||||||
|
<div data-view="settings"><section class="panel"><h2>Runtime Configuration</h2><form id="settingsForm"><div class="grid"><label>Log source<br><select name="log_source"><option value="local_syslog">Local syslog</option><option value="graylog_mcp">Graylog MCP</option></select></label><label>Graylog MCP URL<br><input name="graylog_mcp_url" type="url" placeholder="https://graylog.example/api/mcp"></label><label>Graylog stream<br><input name="graylog_stream" placeholder="FortiGate stream ID or name"></label><label>Graylog MCP token<br><input name="graylog_mcp_token" type="password" placeholder="Leave blank to keep current token"></label><label>Ollama model<br><input name="llm_model" placeholder="llama3.1"></label><label><input name="llm_enabled" type="checkbox"> Enable Ollama analysis</label><label><input name="threat_intel_enabled" type="checkbox"> Enable threat intelligence</label></div><p><button type="submit">Save configuration</button> <span id="settingsResult" class="muted"></span></p></form></section></div>
|
||||||
</main>
|
</main>
|
||||||
<script>
|
<script>
|
||||||
function esc(value) {
|
function esc(value) {
|
||||||
@@ -82,6 +85,7 @@ async function refresh() {
|
|||||||
const a = data.anomaly_summary || {};
|
const a = data.anomaly_summary || {};
|
||||||
const baseline = data.baseline || {};
|
const baseline = data.baseline || {};
|
||||||
const threat = (data.capabilities || {}).threat_intel || {};
|
const threat = (data.capabilities || {}).threat_intel || {};
|
||||||
|
const configuration = data.configuration || {};
|
||||||
document.getElementById('stamp').textContent = data.generated_at ? `Updated ${new Date(data.generated_at * 1000).toLocaleString()}` : 'Waiting for monitor data';
|
document.getElementById('stamp').textContent = data.generated_at ? `Updated ${new Date(data.generated_at * 1000).toLocaleString()}` : 'Waiting for monitor data';
|
||||||
document.getElementById('metrics').innerHTML = [
|
document.getElementById('metrics').innerHTML = [
|
||||||
metric('Total events', s.total || 0),
|
metric('Total events', s.total || 0),
|
||||||
@@ -93,7 +97,8 @@ async function refresh() {
|
|||||||
document.getElementById('capabilities').innerHTML = [
|
document.getElementById('capabilities').innerHTML = [
|
||||||
capability('Baseline', baseline.enabled ? 'on' : 'warn', baseline.enabled ? `${baseline.sources_ready || 0} sources ready` : 'disabled'),
|
capability('Baseline', baseline.enabled ? 'on' : 'warn', baseline.enabled ? `${baseline.sources_ready || 0} sources ready` : 'disabled'),
|
||||||
capability('Ollama', llm.enabled && llm.status !== 'error' ? 'on' : 'warn', llm.enabled ? (llm.status || 'starting') : 'disabled'),
|
capability('Ollama', llm.enabled && llm.status !== 'error' ? 'on' : 'warn', llm.enabled ? (llm.status || 'starting') : 'disabled'),
|
||||||
capability('Threat Intel', threat.enabled && threat.configured ? 'on' : 'warn', threat.enabled ? `${threat.provider || 'unknown'}${threat.configured ? '' : ', key missing'}` : 'disabled')
|
capability('Threat Intel', threat.enabled && threat.configured ? 'on' : 'warn', threat.enabled ? `${threat.provider || 'unknown'}${threat.configured ? '' : ', key missing'}` : 'disabled'),
|
||||||
|
capability('Log source', configuration.log_source === 'graylog_mcp' ? 'warn' : 'on', configuration.log_source === 'graylog_mcp' ? 'Graylog MCP configured, connector pending' : 'local syslog')
|
||||||
].join('');
|
].join('');
|
||||||
document.getElementById('liveStatus').innerHTML = [
|
document.getElementById('liveStatus').innerHTML = [
|
||||||
`Log file: <code>${esc(data.log_path || '')}</code>`,
|
`Log file: <code>${esc(data.log_path || '')}</code>`,
|
||||||
@@ -155,11 +160,33 @@ async function refresh() {
|
|||||||
'<h3>Top Services</h3>' + table(d.top_services || [], [{label:'Value', key:'value'}, {label:'Count', key:'count'}]) +
|
'<h3>Top Services</h3>' + table(d.top_services || [], [{label:'Value', key:'value'}, {label:'Count', key:'count'}]) +
|
||||||
'<h3>Local-in Failures</h3>' + table(d.local_in_failures || [], [{label:'Source', key:'src_ip'}, {label:'Service', key:'service'}, {label:'Policy', key:'policy'}, {label:'Count', key:'count'}]);
|
'<h3>Local-in Failures</h3>' + table(d.local_in_failures || [], [{label:'Source', key:'src_ip'}, {label:'Service', key:'service'}, {label:'Policy', key:'policy'}, {label:'Count', key:'count'}]);
|
||||||
}
|
}
|
||||||
|
async function loadSettings() {
|
||||||
|
const config = await (await fetch('/api/config', {cache: 'no-store'})).json();
|
||||||
|
const form = document.getElementById('settingsForm');
|
||||||
|
for (const [key, value] of Object.entries(config)) {
|
||||||
|
const field = form.elements.namedItem(key);
|
||||||
|
if (!field) continue;
|
||||||
|
if (field.type === 'checkbox') field.checked = Boolean(value); else field.value = value || '';
|
||||||
|
}
|
||||||
|
const token = form.elements.namedItem('graylog_mcp_token');
|
||||||
|
token.placeholder = config.graylog_mcp_token_configured ? 'Token configured; leave blank to keep it' : 'Paste a read-only token';
|
||||||
|
}
|
||||||
|
document.getElementById('settingsForm').addEventListener('submit', async event => {
|
||||||
|
event.preventDefault();
|
||||||
|
const form = event.currentTarget;
|
||||||
|
const values = Object.fromEntries(new FormData(form));
|
||||||
|
values.llm_enabled = form.elements.llm_enabled.checked;
|
||||||
|
values.threat_intel_enabled = form.elements.threat_intel_enabled.checked;
|
||||||
|
const response = await fetch('/api/config', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(values)});
|
||||||
|
document.getElementById('settingsResult').textContent = response.ok ? 'Saved. Monitor applies supported settings on its next cycle.' : 'Could not save configuration.';
|
||||||
|
if (response.ok) loadSettings();
|
||||||
|
});
|
||||||
document.querySelectorAll('.tab').forEach(button => button.addEventListener('click', () => {
|
document.querySelectorAll('.tab').forEach(button => button.addEventListener('click', () => {
|
||||||
document.querySelectorAll('.tab').forEach(item => item.classList.toggle('active', item === button));
|
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));
|
document.querySelectorAll('[data-view]').forEach(view => view.classList.toggle('active', view.dataset.view === button.dataset.tab));
|
||||||
}));
|
}));
|
||||||
refresh();
|
refresh();
|
||||||
|
loadSettings();
|
||||||
setInterval(refresh, 5000);
|
setInterval(refresh, 5000);
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
@@ -167,15 +194,19 @@ setInterval(refresh, 5000);
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def serve_dashboard(host: str, port: int, status_file: str, *, image_dir: str | None = None) -> None:
|
def serve_dashboard(host: str, port: int, status_file: str, *, image_dir: str | None = None, config_file: str = "state/fgai-config.json") -> None:
|
||||||
status_path = Path(status_file)
|
status_path = Path(status_file)
|
||||||
image_root = Path(image_dir) if image_dir else None
|
image_root = Path(image_dir) if image_dir else None
|
||||||
|
config_store = ConfigStore(config_file)
|
||||||
|
|
||||||
class Handler(BaseHTTPRequestHandler):
|
class Handler(BaseHTTPRequestHandler):
|
||||||
def do_GET(self) -> None:
|
def do_GET(self) -> None:
|
||||||
if self.path == "/":
|
if self.path == "/":
|
||||||
self._send(200, "text/html; charset=utf-8", HTML.encode("utf-8"))
|
self._send(200, "text/html; charset=utf-8", HTML.encode("utf-8"))
|
||||||
return
|
return
|
||||||
|
if self.path == "/api/config":
|
||||||
|
self._send(200, "application/json", json.dumps(config_store.public()).encode("utf-8"))
|
||||||
|
return
|
||||||
if self.path == "/api/status":
|
if self.path == "/api/status":
|
||||||
if status_path.exists():
|
if status_path.exists():
|
||||||
body = status_path.read_bytes()
|
body = status_path.read_bytes()
|
||||||
@@ -191,6 +222,24 @@ def serve_dashboard(host: str, port: int, status_file: str, *, image_dir: str |
|
|||||||
return
|
return
|
||||||
self._send(404, "text/plain; charset=utf-8", b"not found")
|
self._send(404, "text/plain; charset=utf-8", b"not found")
|
||||||
|
|
||||||
|
def do_POST(self) -> None:
|
||||||
|
if self.path != "/api/config" or not self._is_loopback_client():
|
||||||
|
self._send(403, "application/json", b'{"error":"configuration is local-only"}')
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
length = int(self.headers.get("Content-Length", "0"))
|
||||||
|
payload = json.loads(self.rfile.read(min(length, 32_768)).decode("utf-8"))
|
||||||
|
if not isinstance(payload, dict):
|
||||||
|
raise ValueError("configuration must be an object")
|
||||||
|
body = json.dumps(config_store.update(payload)).encode("utf-8")
|
||||||
|
except (ValueError, json.JSONDecodeError) as exc:
|
||||||
|
self._send(400, "application/json", json.dumps({"error": str(exc)}).encode("utf-8"))
|
||||||
|
return
|
||||||
|
self._send(200, "application/json", body)
|
||||||
|
|
||||||
|
def _is_loopback_client(self) -> bool:
|
||||||
|
return self.client_address[0] in {"127.0.0.1", "::1"}
|
||||||
|
|
||||||
def log_message(self, format: str, *args: object) -> None:
|
def log_message(self, format: str, *args: object) -> None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
from .anomaly import anomaly_summary, detect_source_anomalies
|
from .anomaly import anomaly_summary, detect_source_anomalies
|
||||||
from .baseline import BaselineStore
|
from .baseline import BaselineStore
|
||||||
|
from .config import ConfigStore
|
||||||
from .llm import ollama_dashboard_assessment
|
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
|
||||||
@@ -22,12 +23,17 @@ def build_status(
|
|||||||
min_block_score: int = 7,
|
min_block_score: int = 7,
|
||||||
anomaly_limit: int = 20,
|
anomaly_limit: int = 20,
|
||||||
baseline_path: str | None = None,
|
baseline_path: str | None = None,
|
||||||
|
config_path: str | None = None,
|
||||||
) -> dict[str, object]:
|
) -> dict[str, object]:
|
||||||
events = read_events(log_path) if Path(log_path).exists() else []
|
events = read_events(log_path) if Path(log_path).exists() else []
|
||||||
baseline = BaselineStore(baseline_path) if baseline_path else None
|
baseline = BaselineStore(baseline_path) if baseline_path else None
|
||||||
profiles = baseline.profiles({event.src_ip for event in events if event.src_ip}) if baseline else {}
|
profiles = baseline.profiles({event.src_ip for event in events if event.src_ip}) if baseline else {}
|
||||||
anomalies = detect_source_anomalies(events, limit=anomaly_limit, baselines=profiles)
|
anomalies = detect_source_anomalies(events, limit=anomaly_limit, baselines=profiles)
|
||||||
baseline_events = baseline.ingest(events) if baseline else 0
|
baseline_events = baseline.ingest(events) if baseline else 0
|
||||||
|
config_store = ConfigStore(config_path) if config_path else None
|
||||||
|
config_exists = bool(config_store and config_store.path.exists())
|
||||||
|
runtime_values = config_store.read() if config_exists and config_store else {}
|
||||||
|
runtime_config = config_store.public() if config_store else {}
|
||||||
intel_ips = sorted(
|
intel_ips = sorted(
|
||||||
{
|
{
|
||||||
ip
|
ip
|
||||||
@@ -36,8 +42,9 @@ def build_status(
|
|||||||
if is_public_ip(ip)
|
if is_public_ip(ip)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
reputation = enrich_ips(intel_ips, limit=25)
|
threat_enabled = bool(runtime_values.get("threat_intel_enabled")) if runtime_values else None
|
||||||
threat_intel_status = ThreatIntelClient().status()
|
reputation = enrich_ips(intel_ips, limit=25, enabled=threat_enabled)
|
||||||
|
threat_intel_status = ThreatIntelClient(enabled=threat_enabled).status()
|
||||||
recommendations = build_recommendations(events, anomalies, reputation)
|
recommendations = build_recommendations(events, anomalies, reputation)
|
||||||
block_candidates = suggest_block_candidates(
|
block_candidates = suggest_block_candidates(
|
||||||
events,
|
events,
|
||||||
@@ -62,6 +69,7 @@ def build_status(
|
|||||||
"anomaly_summary": anomaly_summary(anomalies),
|
"anomaly_summary": anomaly_summary(anomalies),
|
||||||
"baseline": {"enabled": bool(baseline), "sources_ready": len(profiles), "new_events_recorded": baseline_events},
|
"baseline": {"enabled": bool(baseline), "sources_ready": len(profiles), "new_events_recorded": baseline_events},
|
||||||
"capabilities": {"threat_intel": threat_intel_status},
|
"capabilities": {"threat_intel": threat_intel_status},
|
||||||
|
"configuration": runtime_config,
|
||||||
"diagnostics": {
|
"diagnostics": {
|
||||||
"top_source_ips": top_field_values(events, "srcip", limit=10),
|
"top_source_ips": top_field_values(events, "srcip", limit=10),
|
||||||
"top_destination_ips": top_field_values(events, "dstip", limit=10),
|
"top_destination_ips": top_field_values(events, "dstip", limit=10),
|
||||||
@@ -145,17 +153,24 @@ def monitor_loop(
|
|||||||
llm_model: str | None = None,
|
llm_model: str | None = None,
|
||||||
llm_timeout: int | None = None,
|
llm_timeout: int | None = None,
|
||||||
baseline_path: str | None = None,
|
baseline_path: str | None = None,
|
||||||
|
config_path: str | 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_at = 0
|
||||||
last_llm_text: str | None = None
|
last_llm_text: str | None = None
|
||||||
while True:
|
while True:
|
||||||
status = build_status(log_path, policy_path=policy_path, anomaly_limit=anomaly_limit, baseline_path=baseline_path)
|
runtime = ConfigStore(config_path).read() if config_path and Path(config_path).exists() else {}
|
||||||
if llm:
|
effective_llm = bool(runtime.get("llm_enabled")) if runtime else llm
|
||||||
|
effective_model = str(runtime.get("llm_model") or llm_model or "")
|
||||||
|
status = build_status(
|
||||||
|
log_path, policy_path=policy_path, anomaly_limit=anomaly_limit,
|
||||||
|
baseline_path=baseline_path, config_path=config_path,
|
||||||
|
)
|
||||||
|
if effective_llm:
|
||||||
now = int(time.time())
|
now = int(time.time())
|
||||||
if now - last_llm_at >= llm_interval:
|
if now - last_llm_at >= llm_interval:
|
||||||
add_llm_assessment(status, previous=last_llm_text, model=llm_model, timeout=llm_timeout)
|
add_llm_assessment(status, previous=last_llm_text, model=effective_model or None, timeout=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 "")
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ def is_public_ip(value: str | None) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
class ThreatIntelClient:
|
class ThreatIntelClient:
|
||||||
def __init__(self, *, cache_file: str = "state/threat-intel-cache.json", ttl_seconds: int = 86400) -> None:
|
def __init__(self, *, cache_file: str = "state/threat-intel-cache.json", ttl_seconds: int = 86400, enabled: bool | None = None) -> None:
|
||||||
self.enabled = os.getenv("FGAI_THREAT_INTEL", "").lower() in {"1", "true", "yes", "on"}
|
self.enabled = os.getenv("FGAI_THREAT_INTEL", "").lower() in {"1", "true", "yes", "on"} if enabled is None else enabled
|
||||||
self.abuseipdb_key = os.getenv("ABUSEIPDB_API_KEY")
|
self.abuseipdb_key = os.getenv("ABUSEIPDB_API_KEY")
|
||||||
self.virustotal_key = os.getenv("VIRUSTOTAL_API_KEY")
|
self.virustotal_key = os.getenv("VIRUSTOTAL_API_KEY")
|
||||||
self.provider = os.getenv("FGAI_THREAT_INTEL_PROVIDER", "auto").lower()
|
self.provider = os.getenv("FGAI_THREAT_INTEL_PROVIDER", "auto").lower()
|
||||||
@@ -144,8 +144,10 @@ class ThreatIntelClient:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def enrich_ips(ips: list[str], *, cache_file: str = "state/threat-intel-cache.json", limit: int = 25) -> dict[str, dict[str, object]]:
|
def enrich_ips(
|
||||||
client = ThreatIntelClient(cache_file=cache_file)
|
ips: list[str], *, cache_file: str = "state/threat-intel-cache.json", limit: int = 25, enabled: bool | None = None
|
||||||
|
) -> dict[str, dict[str, object]]:
|
||||||
|
client = ThreatIntelClient(cache_file=cache_file, enabled=enabled)
|
||||||
enriched: dict[str, dict[str, object]] = {}
|
enriched: dict[str, dict[str, object]] = {}
|
||||||
for ip in ips[:limit]:
|
for ip in ips[:limit]:
|
||||||
enriched[ip] = client.lookup_ip(ip)
|
enriched[ip] = client.lookup_ip(ip)
|
||||||
|
|||||||
4
start.sh
4
start.sh
@@ -11,6 +11,7 @@ LOG_ROTATE_COUNT="${FGAI_LOG_ROTATE_COUNT:-14}"
|
|||||||
LISTENER_LOG="${FGAI_LISTENER_LOG:-$ROOT_DIR/logs/fgai-listener.log}"
|
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}"
|
||||||
|
CONFIG_FILE="${FGAI_CONFIG_FILE:-$ROOT_DIR/state/fgai-config.json}"
|
||||||
BASELINE_DB="${FGAI_BASELINE_DB:-$ROOT_DIR/state/fgai-baseline.sqlite3}"
|
BASELINE_DB="${FGAI_BASELINE_DB:-$ROOT_DIR/state/fgai-baseline.sqlite3}"
|
||||||
MONITOR_INTERVAL="${FGAI_MONITOR_INTERVAL:-10}"
|
MONITOR_INTERVAL="${FGAI_MONITOR_INTERVAL:-10}"
|
||||||
LLM_ENABLED="${FGAI_LLM:-0}"
|
LLM_ENABLED="${FGAI_LLM:-0}"
|
||||||
@@ -119,7 +120,7 @@ start_monitor() {
|
|||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
monitor_args=(monitor --logs "$LOG_FILE" --output "$STATE_FILE" --interval "$MONITOR_INTERVAL" --baseline-db "$BASELINE_DB")
|
monitor_args=(monitor --logs "$LOG_FILE" --output "$STATE_FILE" --interval "$MONITOR_INTERVAL" --baseline-db "$BASELINE_DB" --config-file "$CONFIG_FILE")
|
||||||
if [ -f "$POLICY_FILE" ]; then
|
if [ -f "$POLICY_FILE" ]; then
|
||||||
monitor_args+=(--policies "$POLICY_FILE")
|
monitor_args+=(--policies "$POLICY_FILE")
|
||||||
fi
|
fi
|
||||||
@@ -149,6 +150,7 @@ start_dashboard() {
|
|||||||
--host "$DASHBOARD_HOST" \
|
--host "$DASHBOARD_HOST" \
|
||||||
--port "$DASHBOARD_PORT" \
|
--port "$DASHBOARD_PORT" \
|
||||||
--status-file "$STATE_FILE" \
|
--status-file "$STATE_FILE" \
|
||||||
|
--config-file "$CONFIG_FILE" \
|
||||||
--image-dir "$ROOT_DIR/images" > "$DASHBOARD_LOG" 2>&1 &
|
--image-dir "$ROOT_DIR/images" > "$DASHBOARD_LOG" 2>&1 &
|
||||||
printf '%s\n' "$!" > "$DASHBOARD_PID_FILE"
|
printf '%s\n' "$!" > "$DASHBOARD_PID_FILE"
|
||||||
printf 'Started fgAI dashboard, pid %s\n' "$(cat "$DASHBOARD_PID_FILE")"
|
printf 'Started fgAI dashboard, pid %s\n' "$(cat "$DASHBOARD_PID_FILE")"
|
||||||
|
|||||||
20
tests/test_config.py
Normal file
20
tests/test_config.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from fgai.config import ConfigStore
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigTests(unittest.TestCase):
|
||||||
|
def test_public_config_hides_token_and_preserves_blank_update(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
store = ConfigStore(str(Path(directory) / "config.json"))
|
||||||
|
store.update({"log_source": "graylog_mcp", "graylog_mcp_token": "secret"})
|
||||||
|
public = store.update({"graylog_mcp_token": ""})
|
||||||
|
self.assertEqual(public["log_source"], "graylog_mcp")
|
||||||
|
self.assertTrue(public["graylog_mcp_token_configured"])
|
||||||
|
self.assertNotIn("graylog_mcp_token", public)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user