multi-entity stream profiles.
This commit is contained in:
@@ -8,6 +8,7 @@ from .config import ConfigStore
|
||||
from .graylog_mcp import GraylogMcpClient
|
||||
from .metrics import prometheus_metrics
|
||||
from .feedback import FeedbackStore
|
||||
from .incidents import IncidentStore
|
||||
|
||||
|
||||
HTML = """<!doctype html>
|
||||
@@ -175,10 +176,18 @@ async function refresh() {
|
||||
{label:'Entity', key:'entity', render:r => esc(`${r.entity} (${r.entity_type || 'entity'})`)},
|
||||
{label:'Score', key:'score'},
|
||||
{label:'Severity', render:r => `<span class="sev-${esc(r.severity)}">${esc(r.severity)}</span>`},
|
||||
{label:'State', render:r => esc(r.lifecycle_status || 'open')},
|
||||
{label:'Streams', render:r => esc((r.correlated_streams || []).join(', ') || 'single stream')},
|
||||
{label:'Evidence', render:r => esc((r.evidence || []).join('; '))},
|
||||
{label:'Timeline', render:r => { const rows=(r.timeline||[]).map(item => esc(`${item.timestamp || ''} | ${item.stream || ''} | ${item.action || ''} | ${item.destination || ''} | ${item.context || item.message || ''}`)).join('<br>'); const id=`incident:${r.entity}:${r.first_seen || ''}`; return rows ? `<details data-detail-id="${esc(id)}"><summary>${esc(`${r.first_seen || '-'} to ${r.last_seen || '-'}`)}</summary><p>${rows}</p></details>` : '-'; }}
|
||||
{label:'Timeline', render:r => { const rows=(r.timeline||[]).map(item => esc(`${item.timestamp || ''} | ${item.stream || ''} | ${item.action || ''} | ${item.destination || ''} | ${item.context || item.message || ''}`)).join('<br>'); const id=`incident:${r.id || r.entity}:${r.first_seen || ''}`; return rows ? `<details data-detail-id="${esc(id)}"><summary>${esc(`${r.first_seen || '-'} to ${r.last_seen || '-'}`)}</summary><p>${rows}</p></details>` : '-'; }},
|
||||
{label:'Action', render:r => `<div class="review-actions"><button class="incident-action" data-id="${esc(r.id)}" data-status="acknowledged">Ack</button><button class="incident-action" data-id="${esc(r.id)}" data-status="resolved">Resolve</button><button class="incident-action" data-id="${esc(r.id)}" data-status="open">Reopen</button></div>${r.note ? `<div class="muted">${esc(r.note)}</div>` : ''}`}
|
||||
], 'incidents');
|
||||
document.querySelectorAll('.incident-action').forEach(button => button.addEventListener('click', async () => {
|
||||
const note = prompt('Incident note (optional):') || '';
|
||||
const response = await fetch('/api/incidents', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({id:button.dataset.id, status:button.dataset.status, note})});
|
||||
document.getElementById('feedbackNotice').textContent = response.ok ? 'Incident state saved.' : 'Could not save incident state.';
|
||||
refresh();
|
||||
}));
|
||||
document.getElementById('blocks').innerHTML = table(data.block_candidates || [], [
|
||||
{label:'Source', key:'src_ip'},
|
||||
{label:'Score', key:'score'},
|
||||
@@ -330,6 +339,9 @@ def serve_dashboard(host: str, port: int, status_file: str, *, image_dir: str |
|
||||
if self.path == "/api/feedback":
|
||||
self._send(200, "application/json", json.dumps(FeedbackStore().entries()).encode("utf-8"))
|
||||
return
|
||||
if self.path == "/api/incidents":
|
||||
self._send(200, "application/json", json.dumps(IncidentStore().entries()).encode("utf-8"))
|
||||
return
|
||||
if self.path == "/api/graylog/streams":
|
||||
config = config_store.read()
|
||||
try:
|
||||
@@ -399,6 +411,13 @@ def serve_dashboard(host: str, port: int, status_file: str, *, image_dir: str |
|
||||
except (ValueError, json.JSONDecodeError) as exc:
|
||||
self._send(400, "application/json", json.dumps({"error": str(exc)}).encode("utf-8"))
|
||||
return
|
||||
if self.path == "/api/incidents" and self._is_loopback_client():
|
||||
try:
|
||||
payload = json.loads(self.rfile.read(min(int(self.headers.get("Content-Length", "0")), 16_384)).decode("utf-8"))
|
||||
self._send(200, "application/json", json.dumps(IncidentStore().update(str(payload.get("id", "")), str(payload.get("status", "")), str(payload.get("note", "")))).encode("utf-8"))
|
||||
except (ValueError, json.JSONDecodeError) as exc:
|
||||
self._send(400, "application/json", json.dumps({"error": str(exc)}).encode("utf-8"))
|
||||
return
|
||||
if self.path != "/api/config" or not self._is_loopback_client():
|
||||
self._send(403, "application/json", b'{"error":"configuration is local-only"}')
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user