fix ui
This commit is contained in:
@@ -205,6 +205,7 @@ def run_monitor(args: argparse.Namespace) -> int:
|
||||
llm_timeout=args.llm_timeout,
|
||||
baseline_path=args.baseline_db,
|
||||
config_path=args.config_file,
|
||||
history_path=args.history_db,
|
||||
)
|
||||
return 0
|
||||
|
||||
@@ -289,6 +290,7 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
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("--config-file", default="state/fgai-config.json", help="Runtime configuration written by dashboard")
|
||||
monitor.add_argument("--history-db", default="state/signalscope-history.sqlite3", help="Aggregate history database for dashboard trends")
|
||||
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")
|
||||
|
||||
@@ -54,6 +54,7 @@ HTML = """<!doctype html>
|
||||
.field-row code { width: fit-content; }
|
||||
.field-controls { display: flex; flex-wrap: wrap; gap: 10px; }
|
||||
.field-controls label { white-space: nowrap; }
|
||||
.chart { width: 100%; height: 220px; background: #04182d; border: 1px solid #163b59; }
|
||||
@media (max-width: 860px) { .hero, .split { grid-template-columns: 1fr; } .hero img { display: none; } }
|
||||
</style>
|
||||
</head>
|
||||
@@ -68,7 +69,7 @@ HTML = """<!doctype html>
|
||||
</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><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="split"><div class="panel"><h2>Events and Anomalies</h2><canvas id="trendChart" class="chart"></canvas></div><div class="panel"><h2>Baseline and Stream Health</h2><div id="health"></div></div></section><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="panel"><h2>Field Baseline Deviations</h2><div id="fieldDeviations"></div></section><section class="panel"><h2>Related Activity Across Sources</h2><div id="relatedActivity"></div></section><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 streams<br><button type="button" id="loadStreams">Load streams</button><div id="streamPicker" class="muted">Load streams after URL and token are saved.</div></label><label>Discover fields<br><button type="button" id="loadFields">Load selected stream fields</button><div id="fieldPicker" class="muted">Select a stream first.</div></label><label>Graylog query<br><input name="graylog_query" placeholder="*"></label><label>Graylog field mapping (JSON)<br><textarea name="graylog_field_mapping" placeholder='{"srcip":"client_ip","dstip":"server_ip","action":"event_action"}'></textarea></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>
|
||||
@@ -87,6 +88,7 @@ function table(rows, columns) {
|
||||
return `<div class="table-wrap"><table><thead><tr>${head}</tr></thead><tbody>${body}</tbody></table></div>`;
|
||||
}
|
||||
function capability(label, state, detail) { return `<span class="capability ${state}">${esc(label)}: ${esc(detail)}</span>`; }
|
||||
function drawTrend(history) { const canvas=document.getElementById('trendChart'), ctx=canvas.getContext('2d'), w=canvas.width=canvas.clientWidth*devicePixelRatio, h=canvas.height=canvas.clientHeight*devicePixelRatio; ctx.scale(devicePixelRatio,devicePixelRatio); const cw=canvas.clientWidth,ch=canvas.clientHeight; ctx.clearRect(0,0,cw,ch); const max=Math.max(1,...history.map(item=>item.events||0)); const line=(key,color)=>{ctx.strokeStyle=color;ctx.lineWidth=2;ctx.beginPath();history.forEach((item,index)=>{const x=12+index*Math.max(1,(cw-24)/Math.max(1,history.length-1));const y=ch-18-((item[key]||0)/max)*(ch-36);index?ctx.lineTo(x,y):ctx.moveTo(x,y)});ctx.stroke()}; line('events','#1ea9ff');line('anomalies','#ff5656'); }
|
||||
async function refresh() {
|
||||
const res = await fetch('/api/status', {cache: 'no-store'});
|
||||
const data = await res.json();
|
||||
@@ -96,6 +98,7 @@ async function refresh() {
|
||||
const threat = (data.capabilities || {}).threat_intel || {};
|
||||
const mcp = (data.capabilities || {}).graylog_mcp || {};
|
||||
const configuration = data.configuration || {};
|
||||
drawTrend(data.history || []);
|
||||
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),
|
||||
@@ -117,6 +120,9 @@ async function refresh() {
|
||||
`High anomalies: ${esc((a.high || 0))}`,
|
||||
`Baseline sources ready: ${esc((data.baseline || {}).sources_ready || 0)}`
|
||||
].join('<br>');
|
||||
document.getElementById('health').innerHTML = [
|
||||
metric('Baseline sources ready', baseline.sources_ready || 0), metric('MCP events fetched', mcp.events_fetched || 0), metric('Profiles active', (data.stream_profiles || []).length), metric('Correlated entities', (data.cross_source_correlations || []).length)
|
||||
].join('');
|
||||
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 || [], [
|
||||
|
||||
28
src/fgai/history.py
Normal file
28
src/fgai/history.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class HistoryStore:
|
||||
def __init__(self, path: str) -> None:
|
||||
self.path = Path(path)
|
||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with sqlite3.connect(self.path) as connection:
|
||||
connection.execute("create table if not exists snapshots (at integer primary key, events integer, anomalies integer, critical integer, high integer, baseline_ready integer, mcp_events integer)")
|
||||
|
||||
def record(self, status: dict[str, object]) -> None:
|
||||
summary = status.get("summary", {}) if isinstance(status.get("summary"), dict) else {}
|
||||
anomalies = status.get("anomaly_summary", {}) if isinstance(status.get("anomaly_summary"), dict) else {}
|
||||
baseline = status.get("baseline", {}) if isinstance(status.get("baseline"), dict) else {}
|
||||
capabilities = status.get("capabilities", {}) if isinstance(status.get("capabilities"), dict) else {}
|
||||
mcp = capabilities.get("graylog_mcp", {}) if isinstance(capabilities.get("graylog_mcp"), dict) else {}
|
||||
with sqlite3.connect(self.path) as connection:
|
||||
connection.execute("insert or replace into snapshots values (?, ?, ?, ?, ?, ?, ?)", (int(time.time()), int(summary.get("total", 0) or 0), int(anomalies.get("total", 0) or 0), int(anomalies.get("critical", 0) or 0), int(anomalies.get("high", 0) or 0), int(baseline.get("sources_ready", 0) or 0), int(mcp.get("events_fetched", 0) or 0)))
|
||||
connection.execute("delete from snapshots where at < ?", (int(time.time()) - 7 * 86400,))
|
||||
|
||||
def recent(self, limit: int = 288) -> list[dict[str, int]]:
|
||||
with sqlite3.connect(self.path) as connection:
|
||||
rows = connection.execute("select at, events, anomalies, critical, high, baseline_ready, mcp_events from snapshots order by at desc limit ?", (limit,)).fetchall()
|
||||
return [dict(zip(("at", "events", "anomalies", "critical", "high", "baseline_ready", "mcp_events"), row)) for row in reversed(rows)]
|
||||
@@ -11,6 +11,7 @@ from .correlation import correlate_source_ips
|
||||
from .event_context import build_event_context
|
||||
from .graylog_mcp import GraylogMcpClient
|
||||
from .graylog_source import GraylogStreamSource
|
||||
from .history import HistoryStore
|
||||
from .llm import ollama_dashboard_assessment
|
||||
from .logs import local_in_failures, read_events, summarize_events, top_field_values
|
||||
from .mitigation import parse_allowlist, suggest_block_candidates
|
||||
@@ -29,6 +30,7 @@ def build_status(
|
||||
anomaly_limit: int = 20,
|
||||
baseline_path: str | None = None,
|
||||
config_path: str | None = None,
|
||||
history_path: str | None = None,
|
||||
) -> dict[str, object]:
|
||||
config_store = ConfigStore(config_path) if config_path else None
|
||||
config_exists = bool(config_store and config_store.path.exists())
|
||||
@@ -93,7 +95,7 @@ def build_status(
|
||||
except Exception as exc:
|
||||
policy_error = str(exc)
|
||||
|
||||
return {
|
||||
status = {
|
||||
"generated_at": int(time.time()),
|
||||
"log_path": log_path,
|
||||
"policy_path": policy_path,
|
||||
@@ -149,6 +151,11 @@ def build_status(
|
||||
"policy_findings": policy_findings,
|
||||
"policy_error": policy_error,
|
||||
}
|
||||
if history_path:
|
||||
history = HistoryStore(history_path)
|
||||
history.record(status)
|
||||
status["history"] = history.recent()
|
||||
return status
|
||||
|
||||
|
||||
def add_llm_assessment(status: dict[str, object], *, previous: str | None = None, model: str | None = None, timeout: int | None = None) -> None:
|
||||
@@ -190,6 +197,7 @@ def monitor_loop(
|
||||
llm_timeout: int | None = None,
|
||||
baseline_path: str | None = None,
|
||||
config_path: str | None = None,
|
||||
history_path: str | None = None,
|
||||
) -> None:
|
||||
print(f"Monitoring {log_path}")
|
||||
print(f"Writing status to {output}")
|
||||
@@ -201,7 +209,7 @@ def monitor_loop(
|
||||
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,
|
||||
baseline_path=baseline_path, config_path=config_path, history_path=history_path,
|
||||
)
|
||||
if effective_llm:
|
||||
now = int(time.time())
|
||||
|
||||
3
start.sh
3
start.sh
@@ -13,6 +13,7 @@ POLICY_FILE="${FGAI_POLICY_FILE:-$ROOT_DIR/exports/policies.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}"
|
||||
HISTORY_DB="${FGAI_HISTORY_DB:-$ROOT_DIR/state/signalscope-history.sqlite3}"
|
||||
MONITOR_INTERVAL="${FGAI_MONITOR_INTERVAL:-10}"
|
||||
LLM_ENABLED="${FGAI_LLM:-0}"
|
||||
LLM_INTERVAL="${FGAI_LLM_INTERVAL:-300}"
|
||||
@@ -120,7 +121,7 @@ start_monitor() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
monitor_args=(monitor --logs "$LOG_FILE" --output "$STATE_FILE" --interval "$MONITOR_INTERVAL" --baseline-db "$BASELINE_DB" --config-file "$CONFIG_FILE")
|
||||
monitor_args=(monitor --logs "$LOG_FILE" --output "$STATE_FILE" --interval "$MONITOR_INTERVAL" --baseline-db "$BASELINE_DB" --history-db "$HISTORY_DB" --config-file "$CONFIG_FILE")
|
||||
if [ -f "$POLICY_FILE" ]; then
|
||||
monitor_args+=(--policies "$POLICY_FILE")
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user