add settings in UI
This commit is contained in:
@@ -4,6 +4,8 @@ import json
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
|
||||
from .config import ConfigStore
|
||||
|
||||
|
||||
HTML = """<!doctype html>
|
||||
<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>
|
||||
</div>
|
||||
</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="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="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>
|
||||
<script>
|
||||
function esc(value) {
|
||||
@@ -82,6 +85,7 @@ async function refresh() {
|
||||
const a = data.anomaly_summary || {};
|
||||
const baseline = data.baseline || {};
|
||||
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('metrics').innerHTML = [
|
||||
metric('Total events', s.total || 0),
|
||||
@@ -93,7 +97,8 @@ async function refresh() {
|
||||
document.getElementById('capabilities').innerHTML = [
|
||||
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('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('');
|
||||
document.getElementById('liveStatus').innerHTML = [
|
||||
`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>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(item => item.classList.toggle('active', item === button));
|
||||
document.querySelectorAll('[data-view]').forEach(view => view.classList.toggle('active', view.dataset.view === button.dataset.tab));
|
||||
}));
|
||||
refresh();
|
||||
loadSettings();
|
||||
setInterval(refresh, 5000);
|
||||
</script>
|
||||
</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)
|
||||
image_root = Path(image_dir) if image_dir else None
|
||||
config_store = ConfigStore(config_file)
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def do_GET(self) -> None:
|
||||
if self.path == "/":
|
||||
self._send(200, "text/html; charset=utf-8", HTML.encode("utf-8"))
|
||||
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 status_path.exists():
|
||||
body = status_path.read_bytes()
|
||||
@@ -191,6 +222,24 @@ def serve_dashboard(host: str, port: int, status_file: str, *, image_dir: str |
|
||||
return
|
||||
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:
|
||||
return
|
||||
|
||||
|
||||
Reference in New Issue
Block a user