from __future__ import annotations import ipaddress from collections import Counter, defaultdict from .entities import entity_display from .logs import THREAT_ACTIONS, is_utm_event from .models import LogEvent from .normalization import canonical_value def _entity(event: LogEvent) -> str: return event.src_ip or event.fields.get("user") or event.fields.get("username") or event.fields.get("hostname") or event.fields.get("source") or "unknown" def _display_label(entity: str, events: list[LogEvent]) -> dict[str, str]: try: ipaddress.ip_address(entity) except ValueError: return {"entity_display": entity, "entity_label": entity, "entity_detail": ""} return entity_display("ip", entity, events) def build_event_context(events: list[LogEvent], *, source_limit: int = 30, sample_limit: int = 15) -> dict[str, object]: grouped: dict[str, list[LogEvent]] = defaultdict(list) field_presence: Counter[str] = Counter() for event in events: grouped[_entity(event)].append(event) field_presence.update(event.fields.keys()) source_profiles = [] for entity, source_events in grouped.items(): actions = Counter(event.action or "unknown" for event in source_events) destinations = {event.dst_ip for event in source_events if event.dst_ip} source_profiles.append({ "entity": entity, "events": len(source_events), "utm_events": sum(is_utm_event(event) for event in source_events), **_display_label(entity, source_events), "deny_or_threat_actions": sum(event.action in THREAT_ACTIONS for event in source_events), "distinct_destinations": len(destinations), "top_actions": [action for action, _ in actions.most_common(3)], }) source_profiles.sort(key=lambda item: (int(item["utm_events"]) + int(item["deny_or_threat_actions"]), int(item["events"])), reverse=True) suspicious = [event for event in events if is_utm_event(event) or event.action in THREAT_ACTIONS or event.severity in {"critical", "high", "alert", "emergency"}] samples = [{ "entity": _entity(event), **_display_label(_entity(event), [event]), "type": canonical_value(event.fields, "type"), "subtype": event.subtype, "action": event.action, "severity": event.severity, "dst": event.dst_ip or canonical_value(event.fields, "context"), "service": canonical_value(event.fields, "service"), "policyid": event.fields.get("policyid", ""), "timestamp": event.fields.get("eventtime", event.fields.get("timestamp", "")), } for event in suspicious[:sample_limit]] return { "entities_total": len(grouped), "source_profiles": source_profiles[:source_limit], "field_coverage": [{"field": field, "events": count} for field, count in field_presence.most_common(30)], "security_event_samples": samples, }