Graylog query details
This commit is contained in:
@@ -13,6 +13,7 @@ from .models import LogEvent
|
||||
from .entities import profile_entities
|
||||
from .detectors import DETECTOR_MINIMUMS, event_detector_categories
|
||||
from .normalization import canonical_value
|
||||
from .query_details import event_query_details
|
||||
|
||||
|
||||
def _number(value: str | None) -> int:
|
||||
@@ -63,7 +64,8 @@ def _weighted_score(base: int, profile: object | None, field: str, detector: str
|
||||
return min(100, max(0, int(round(base * multiplier)))), round(multiplier, 2)
|
||||
|
||||
|
||||
def _sample_event(event: LogEvent, value: str = "") -> dict[str, str]:
|
||||
def _sample_event(event: LogEvent, value: str = "") -> dict[str, object]:
|
||||
query_details = event_query_details(event)
|
||||
return {
|
||||
"timestamp": event.fields.get("eventtime", event.fields.get("timestamp", "")),
|
||||
"source": event.src_ip or event.fields.get("source", ""),
|
||||
@@ -73,6 +75,8 @@ def _sample_event(event: LogEvent, value: str = "") -> dict[str, str]:
|
||||
"service": canonical_value(event.fields, "service"),
|
||||
"value": value,
|
||||
"message": canonical_value(event.fields, "context")[:240],
|
||||
"query_details": query_details,
|
||||
"graylog_query": query_details["query"],
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -179,7 +179,7 @@ async function refresh() {
|
||||
{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.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:'Timeline', render:r => { const rows=(r.timeline||[]).map(item => `${esc(`${item.timestamp || ''} | ${item.stream || ''} | ${item.action || ''} | ${item.destination || ''} | ${item.context || item.message || ''}`)}${item.graylog_query ? `<br><code>${esc(item.graylog_query)}</code>` : ''}`).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 () => {
|
||||
@@ -213,7 +213,7 @@ async function refresh() {
|
||||
document.getElementById('relatedActivity').innerHTML = table(relatedRows, [
|
||||
{label:'Entity', key:'entity', render:r => esc(r.entity || r.source_ip)}, {label:'Stream', key:'stream'}, {label:'Time', key:'timestamp'},
|
||||
{label:'Type', key:'type'}, {label:'Action', key:'action'}, {label:'Severity', key:'severity'},
|
||||
{label:'Destination', key:'destination'}, {label:'Service', key:'service'}, {label:'Context', key:'context'}
|
||||
{label:'Destination', key:'destination'}, {label:'Service', key:'service'}, {label:'Context', key:'context'}, {label:'Graylog Query', render:r => r.graylog_query ? `<code>${esc(r.graylog_query)}</code>` : '-'}
|
||||
], 'related-activity');
|
||||
document.getElementById('reputation').innerHTML = table(reputationRows, [
|
||||
{label:'IP', key:'ip'},
|
||||
|
||||
@@ -5,6 +5,7 @@ from collections.abc import Iterable
|
||||
|
||||
from .models import LogEvent
|
||||
from .normalization import canonical_value
|
||||
from .query_details import event_query_details
|
||||
|
||||
|
||||
ENTITY_FIELDS: dict[str, tuple[str, ...]] = {
|
||||
@@ -52,19 +53,23 @@ def profile_entities(event: LogEvent, profile: object) -> tuple[str, ...]:
|
||||
return tuple(values)
|
||||
|
||||
|
||||
def sample_timeline(events: Iterable[LogEvent], *, limit: int = 20) -> list[dict[str, str]]:
|
||||
samples = [
|
||||
{
|
||||
"stream": event.fields.get("fgai_stream", "local_syslog"),
|
||||
"timestamp": event.fields.get("eventtime", event.fields.get("timestamp", "")),
|
||||
"type": canonical_value(event.fields, "type"),
|
||||
"subtype": event.subtype,
|
||||
"action": event.action,
|
||||
"severity": event.severity,
|
||||
"destination": event.dst_ip or canonical_value(event.fields, "context"),
|
||||
"service": canonical_value(event.fields, "service"),
|
||||
"context": canonical_value(event.fields, "context")[:240],
|
||||
}
|
||||
for event in events
|
||||
]
|
||||
def _timeline_sample(event: LogEvent) -> dict[str, object]:
|
||||
query_details = event_query_details(event)
|
||||
return {
|
||||
"stream": event.fields.get("fgai_stream", "local_syslog"),
|
||||
"timestamp": event.fields.get("eventtime", event.fields.get("timestamp", "")),
|
||||
"type": canonical_value(event.fields, "type"),
|
||||
"subtype": event.subtype,
|
||||
"action": event.action,
|
||||
"severity": event.severity,
|
||||
"destination": event.dst_ip or canonical_value(event.fields, "context"),
|
||||
"service": canonical_value(event.fields, "service"),
|
||||
"context": canonical_value(event.fields, "context")[:240],
|
||||
"query_details": query_details,
|
||||
"graylog_query": query_details["query"],
|
||||
}
|
||||
|
||||
|
||||
def sample_timeline(events: Iterable[LogEvent], *, limit: int = 20) -> list[dict[str, object]]:
|
||||
samples = [_timeline_sample(event) for event in events]
|
||||
return sorted(samples, key=lambda item: item["timestamp"])[-limit:]
|
||||
|
||||
37
src/fgai/query_details.py
Normal file
37
src/fgai/query_details.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from .models import LogEvent
|
||||
from .normalization import FIELD_ALIASES, canonical_value
|
||||
|
||||
|
||||
def _quote(value: str) -> str:
|
||||
escaped = value.replace("\\", "\\\\").replace('"', '\\"')
|
||||
return f'"{escaped}"'
|
||||
|
||||
|
||||
def _field_group(canonical: str, value: str, *, limit: int = 6) -> str:
|
||||
aliases = FIELD_ALIASES.get(canonical, (canonical,))[:limit]
|
||||
return "(" + " OR ".join(f"{field}:{_quote(value)}" for field in aliases) + ")"
|
||||
|
||||
|
||||
def event_query_details(event: LogEvent) -> dict[str, str]:
|
||||
"""Build a copyable Graylog query for an event without assuming one vendor schema."""
|
||||
terms: list[str] = []
|
||||
for canonical in ("srcip", "dstip", "action", "dns_query"):
|
||||
value = canonical_value(event.fields, canonical)
|
||||
if value:
|
||||
terms.append(_field_group(canonical, value))
|
||||
timestamp = event.fields.get("eventtime", event.fields.get("timestamp", ""))
|
||||
if not terms:
|
||||
context = canonical_value(event.fields, "context")
|
||||
if context:
|
||||
terms.append(_field_group("context", context, limit=4))
|
||||
return {
|
||||
"stream": event.fields.get("fgai_stream", ""),
|
||||
"stream_id": event.fields.get("fgai_stream_id", ""),
|
||||
"timestamp": timestamp,
|
||||
"query": " AND ".join(terms) if terms else "*",
|
||||
"raw": event.raw if event.raw.startswith("{") else json.dumps(event.fields, sort_keys=True),
|
||||
}
|
||||
@@ -8,6 +8,7 @@ from .entities import event_entities
|
||||
from .logs import THREAT_ACTIONS
|
||||
from .models import LogEvent
|
||||
from .normalization import canonical_value
|
||||
from .query_details import event_query_details
|
||||
|
||||
|
||||
DEFAULT_SEQUENCE_PATTERNS = {
|
||||
@@ -46,7 +47,8 @@ def _is_network_event(event: LogEvent) -> bool:
|
||||
return event.action in {"accept", "pass", "allowed", "allow", "close", "client-rst", "server-rst"} | THREAT_ACTIONS
|
||||
|
||||
|
||||
def _sample(event: LogEvent, value: str) -> dict[str, str]:
|
||||
def _sample(event: LogEvent, value: str) -> dict[str, object]:
|
||||
query_details = event_query_details(event)
|
||||
return {
|
||||
"timestamp": event.fields.get("eventtime", event.fields.get("timestamp", "")),
|
||||
"stream": event.fields.get("fgai_stream", event.fields.get("fgai_stream_id", "")),
|
||||
@@ -57,6 +59,8 @@ def _sample(event: LogEvent, value: str) -> dict[str, str]:
|
||||
"service": canonical_value(event.fields, "service"),
|
||||
"value": value,
|
||||
"message": canonical_value(event.fields, "context")[:240],
|
||||
"query_details": query_details,
|
||||
"graylog_query": query_details["query"],
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user