From 9c51b9230348cc44a2cc0daecb4f5525a48213420cf16cfa185577394f28eaa4 Mon Sep 17 00:00:00 2001 From: larssand Date: Mon, 22 Jun 2026 21:10:17 +0200 Subject: [PATCH] fix view in profile stream field and readme --- README.md | 38 +++++++++++++++++++++++++++++++++++++- src/fgai/dashboard.py | 11 ++++++++++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ec1a6d0..c53540e 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,42 @@ Or use the helper script, which creates/uses `.venv` automatically and runs `pip - Continuous monitor writing `state/fgai-status.json` - Local dashboard at `http://127.0.0.1:8088` +## Graylog MCP + +Graylog 7.1 MCP can be used as the active log source instead of the local JSONL +listener. In the dashboard, open `Settings`, select `Graylog MCP`, provide the +MCP URL and a read-only API token, then load and enable the streams to analyze. + +The token field accepts a raw Graylog API token, the Base64 value after `Basic `, +or a complete `Basic ` header. Tokens are stored only in the local runtime +configuration and are never returned by the dashboard API. + +Use `Load selected stream fields` after choosing a stream. The field table shows +Graylog datatype/capability metadata and lets you select an entity field, a time +field, and categorical/numeric fields for the stream profile. Profiles are stored +under `graylog_stream_profiles` in `state/fgai-config.json`. + +Enabled streams are normalized through the same event model. The dashboard and +Ollama correlate source IPs that occur across two or more streams, for example +FortiGate, AdGuard/DNS, Windows Security, Nginx, Squid, VPN, or Proxmox. + +The current MCP endpoint is `http://:9000/api/mcp`. Enable it in +Graylog under `System -> Configurations -> MCP` and use stream IDs internally; +the fgAI stream picker resolves titles in the UI. + +## Monitoring Export + +The dashboard also exposes Prometheus text metrics at: + +```text +http://127.0.0.1:8088/metrics +``` + +This endpoint is passive and has no Prometheus or Grafana dependency. It reports +low-cardinality event counts, anomaly severities, baseline readiness, and Graylog +MCP health. Use it later as a Prometheus scrape target or as input for a Checkmk +local check. Do not use source IPs, domains, or raw event IDs as metric labels. + Enable cached Ollama analyst notes in the dashboard: ```bash @@ -200,7 +236,7 @@ end - `FORTIGATE_VERIFY_TLS`: `true` or `false`, defaults to `true`. - `FGAI_ALLOWLIST`: comma-separated IPs/CIDRs never to block. - `OLLAMA_HOST`: defaults to `http://127.0.0.1:11434`. -- `OLLAMA_MODEL`: defaults to `llama3.3`. +- `OLLAMA_MODEL`: defaults to `llama3.1`. - `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`. diff --git a/src/fgai/dashboard.py b/src/fgai/dashboard.py index b564668..9f280a5 100644 --- a/src/fgai/dashboard.py +++ b/src/fgai/dashboard.py @@ -47,6 +47,14 @@ HTML = """ [data-view] { display: none; } [data-view].active { display: block; } .split { display: grid; grid-template-columns: minmax(0, 1.3fr) minmax(320px, 0.7fr); gap: 12px; } .table-wrap { overflow-x: auto; } + #settingsForm label:has(#fieldPicker) { grid-column: 1 / -1; } + #fieldPicker { margin-top: 8px; max-height: 460px; overflow: auto; border: 1px solid #d9e0e7; background: #fbfcfd; } + .field-header, .field-row { display: grid; grid-template-columns: minmax(190px, 1.2fr) 100px minmax(180px, 1fr) minmax(320px, 1.4fr); gap: 10px; align-items: center; padding: 8px 10px; } + .field-header { position: sticky; top: 0; background: #edf3f7; color: #536170; font-size: 12px; font-weight: 700; z-index: 1; } + .field-row { border-top: 1px solid #e4e9ee; font-size: 13px; } + .field-row code { width: fit-content; } + .field-controls { display: flex; flex-wrap: wrap; gap: 10px; } + .field-controls label { white-space: nowrap; } @media (max-width: 860px) { .hero, .split { grid-template-columns: 1fr; } .hero img { display: none; } } @@ -199,7 +207,8 @@ document.getElementById('loadFields').addEventListener('click', async () => { if (!selected) { document.getElementById('fieldPicker').textContent = 'Load streams, tick one stream, then load its fields.'; return; } const payload = await (await fetch(`/api/graylog/fields?stream_id=${encodeURIComponent(selected.dataset.id)}`)).json(); const profile = (window.streamProfiles || []).find(item => item.stream_id === selected.dataset.id) || {}; - document.getElementById('fieldPicker').innerHTML = (payload.fields || []).map(field => { const name=field.name||field.field, props=(field.type||{}).properties||[]; return `
${esc(name)} ${props.includes('enumerable')?``:''} ${props.includes('numeric')?``:''}
`; }).join('') || esc(payload.error || 'No fields found.'); + const rows = (payload.fields || []).map(field => { const name=field.name||field.field, type=(field.type||{}).type||'', props=(field.type||{}).properties||[]; return `
${esc(name)}${esc(type)}${esc(props.join(', '))}
${props.includes('enumerable')?``:''}${props.includes('numeric')?``:''}
`; }).join(''); + document.getElementById('fieldPicker').innerHTML = rows ? `
FieldTypeCapabilitiesUse In Profile
${rows}` : esc(payload.error || 'No fields found.'); window.activeProfileStream = selected.dataset.id; }); document.getElementById('settingsForm').addEventListener('submit', async event => {