from __future__ import annotations import json import shlex from collections import Counter from collections.abc import Iterable from pathlib import Path from .models import LogEvent from .normalization import canonical_value, normalize_fields UTM_SUBTYPES = { "ips", "virus", "av", "anomaly", "ddos", "dos", "webfilter", "app-ctrl", "application", "waf", "dns", } THREAT_ACTIONS = { "blocked", "block", "dropped", "drop", "reset", "reset-client", "reset-server", "detected", "deny", "quarantine", } SEVERITY_SCORE = { "emergency": 6, "alert": 6, "critical": 5, "high": 4, "warning": 3, "medium": 3, "notice": 2, "low": 1, "information": 1, "info": 1, } def parse_log_line(line: str) -> LogEvent: stripped = line.strip() if not stripped: return LogEvent(raw=line, fields={}) if stripped.startswith("{"): try: data = json.loads(stripped) except json.JSONDecodeError: data = {} else: return LogEvent(raw=line, fields=normalize_fields({str(k).lower(): str(v) for k, v in data.items()})) fields: dict[str, str] = {} try: parts = shlex.split(stripped) except ValueError: parts = stripped.split() for part in parts: if "=" not in part: continue key, value = part.split("=", 1) fields[key.strip().lower()] = value.strip().strip('"') return LogEvent(raw=line, fields=normalize_fields(fields)) def read_events(path: str | Path) -> list[LogEvent]: with Path(path).open("r", encoding="utf-8", errors="replace") as handle: return [event for line in handle if (event := parse_log_line(line)).fields] def is_utm_event(event: LogEvent) -> bool: subtype = event.subtype event_type = event.fields.get("type", "").lower() return event_type == "utm" or subtype in UTM_SUBTYPES def event_score(event: LogEvent) -> int: score = SEVERITY_SCORE.get(event.severity, 0) if is_utm_event(event): score += 2 if event.action in THREAT_ACTIONS: score += 2 if event.fields.get("attack") or event.fields.get("attackid") or event.fields.get("signature"): score += 1 return score def summarize_events(events: Iterable[LogEvent]) -> dict[str, int]: summary = { "total": 0, "utm": 0, "threat_actions": 0, "with_src_ip": 0, "critical_or_high": 0, } for event in events: summary["total"] += 1 if is_utm_event(event): summary["utm"] += 1 if event.action in THREAT_ACTIONS: summary["threat_actions"] += 1 if event.src_ip: summary["with_src_ip"] += 1 if event.severity in {"critical", "high", "alert", "emergency"}: summary["critical_or_high"] += 1 return summary def top_field_values(events: Iterable[LogEvent], field: str, *, limit: int = 10) -> list[dict[str, int | str]]: counter: Counter[str] = Counter() for event in events: value = event.fields.get(field) if value: counter[value] += 1 return [{"value": value, "count": count} for value, count in counter.most_common(limit)] def local_in_failures(events: Iterable[LogEvent], *, limit: int = 10) -> list[dict[str, int | str]]: counter: Counter[tuple[str, str, str]] = Counter() for event in events: policy_type = event.fields.get("policytype", event.fields.get("type", "")).lower() msg = event.fields.get("msg", "").lower() if not (policy_type.startswith("local-in") or "local-in" in policy_type or msg == "connection failed"): continue src_ip = event.src_ip or "unknown" service = canonical_value(event.fields, "service") or "unknown" policy_id = event.fields.get("policyid", event.fields.get("poluuid", "unknown")) counter[(src_ip, service, policy_id)] += 1 return [ {"src_ip": src_ip, "service": service, "policy": policy_id, "count": count} for (src_ip, service, policy_id), count in counter.most_common(limit) ]