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:]