76 lines
3.0 KiB
Python
76 lines
3.0 KiB
Python
from __future__ import annotations
|
|
|
|
import ipaddress
|
|
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, ...]] = {
|
|
"ip": ("srcip", "src_ip", "source_ip", "client_ip", "remote_addr", "remote_ip", "ip", "ipaddress", "winlog_event_data_ipaddress", "event_data_ipaddress"),
|
|
"user": ("username", "user", "user_name", "account", "account_name", "targetusername", "subjectusername", "xauthuser", "winlog_event_data_targetusername", "winlog_event_data_subjectusername"),
|
|
"host": ("hostname", "host", "computer", "computer_name", "workstation", "device_name", "winlog_computer_name", "agent_name"),
|
|
}
|
|
|
|
|
|
def entity_type(value: str) -> str:
|
|
try:
|
|
ipaddress.ip_address(value)
|
|
return "ip"
|
|
except ValueError:
|
|
return "entity"
|
|
|
|
|
|
def event_entities(event: LogEvent) -> list[dict[str, str]]:
|
|
"""Return normalized identities shared across network, endpoint, DNS, and web logs."""
|
|
identities: list[dict[str, str]] = []
|
|
seen: set[tuple[str, str]] = set()
|
|
for kind, fields in ENTITY_FIELDS.items():
|
|
for field in fields:
|
|
value = str(event.fields.get(field, "")).strip()
|
|
if not value or value in {"-", "unknown", "n/a"}:
|
|
continue
|
|
key = (kind, value.lower() if kind != "ip" else value)
|
|
if key not in seen:
|
|
seen.add(key)
|
|
identities.append({"entity": value, "entity_type": kind, "field": field})
|
|
return identities
|
|
|
|
|
|
def profile_entity(event: LogEvent, field: str) -> str:
|
|
return str(event.fields.get(field.lower(), "")).strip()
|
|
|
|
|
|
def profile_entities(event: LogEvent, profile: object) -> tuple[str, ...]:
|
|
fields = tuple(str(field) for field in getattr(profile, "entity_fields", ()) if field) or (str(getattr(profile, "entity_field", "")),)
|
|
values = []
|
|
for field in fields:
|
|
value = profile_entity(event, field)
|
|
if value and value not in values:
|
|
values.append(value)
|
|
return tuple(values)
|
|
|
|
|
|
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:]
|