151 lines
4.2 KiB
Python
151 lines
4.2 KiB
Python
"""
|
|
©AngelaMos | 2026
|
|
normalizer.py
|
|
"""
|
|
|
|
from datetime import datetime, UTC
|
|
from typing import Any
|
|
from collections.abc import Callable
|
|
|
|
from app.models.LogEvent import SourceType
|
|
|
|
NormalizerFn = Callable[[dict[str, Any]], dict[str, Any]]
|
|
|
|
NORMALIZERS: dict[str, NormalizerFn] = {}
|
|
|
|
|
|
def _register(source_type: SourceType) -> Callable[[NormalizerFn], NormalizerFn]:
|
|
"""
|
|
Register a normalizer function for a source type
|
|
"""
|
|
def decorator(fn: NormalizerFn) -> NormalizerFn:
|
|
NORMALIZERS[source_type.value] = fn
|
|
return fn
|
|
|
|
return decorator
|
|
|
|
|
|
def normalize(raw: dict[str, Any]) -> dict[str, Any]:
|
|
"""
|
|
Dispatch to the appropriate format normalizer and return enriched data
|
|
"""
|
|
source_type = raw.get("source_type", SourceType.GENERIC)
|
|
normalizer_fn = NORMALIZERS.get(source_type, _normalize_generic)
|
|
base = _extract_common(raw)
|
|
specific = normalizer_fn(raw)
|
|
base["normalized"] = {**base.get("normalized", {}), **specific}
|
|
return base
|
|
|
|
|
|
def _extract_common(raw: dict[str, Any]) -> dict[str, Any]:
|
|
"""
|
|
Pull fields shared across all log formats into a base dict
|
|
"""
|
|
return {
|
|
"timestamp": raw.get("timestamp",
|
|
datetime.now(UTC)),
|
|
"source_type": raw.get("source_type",
|
|
SourceType.GENERIC),
|
|
"source_ip": raw.get("source_ip"),
|
|
"dest_ip": raw.get("dest_ip"),
|
|
"source_port": raw.get("source_port"),
|
|
"dest_port": raw.get("dest_port"),
|
|
"event_type": raw.get("event_type"),
|
|
"hostname": raw.get("hostname"),
|
|
"username": raw.get("username"),
|
|
"raw": raw,
|
|
"normalized": {},
|
|
}
|
|
|
|
|
|
@_register(SourceType.FIREWALL)
|
|
def _normalize_firewall(raw: dict[str, Any]) -> dict[str, Any]:
|
|
"""
|
|
Extract firewall-specific fields like action and protocol
|
|
"""
|
|
return {
|
|
"action": raw.get("action"),
|
|
"protocol": raw.get("protocol"),
|
|
"bytes_sent": raw.get("bytes_sent"),
|
|
"bytes_received": raw.get("bytes_received"),
|
|
"rule_name": raw.get("rule_name"),
|
|
}
|
|
|
|
|
|
@_register(SourceType.IDS)
|
|
def _normalize_ids(raw: dict[str, Any]) -> dict[str, Any]:
|
|
"""
|
|
Extract IDS-specific fields like signature and severity
|
|
"""
|
|
return {
|
|
"signature_id": raw.get("signature_id"),
|
|
"signature_name": raw.get("signature_name"),
|
|
"classification": raw.get("classification"),
|
|
"priority": raw.get("priority"),
|
|
}
|
|
|
|
|
|
@_register(SourceType.AUTH)
|
|
def _normalize_auth(raw: dict[str, Any]) -> dict[str, Any]:
|
|
"""
|
|
Extract authentication-specific fields like method and result
|
|
"""
|
|
return {
|
|
"auth_method": raw.get("auth_method"),
|
|
"result": raw.get("result"),
|
|
"failure_reason": raw.get("failure_reason"),
|
|
"service": raw.get("service"),
|
|
}
|
|
|
|
|
|
@_register(SourceType.ENDPOINT)
|
|
def _normalize_endpoint(raw: dict[str, Any]) -> dict[str, Any]:
|
|
"""
|
|
Extract endpoint-specific fields like process and command line
|
|
"""
|
|
return {
|
|
"process_name": raw.get("process_name"),
|
|
"process_id": raw.get("process_id"),
|
|
"parent_process": raw.get("parent_process"),
|
|
"command_line": raw.get("command_line"),
|
|
"file_path": raw.get("file_path"),
|
|
}
|
|
|
|
|
|
@_register(SourceType.DNS)
|
|
def _normalize_dns(raw: dict[str, Any]) -> dict[str, Any]:
|
|
"""
|
|
Extract DNS-specific fields like query and record type
|
|
"""
|
|
return {
|
|
"query": raw.get("query"),
|
|
"query_type": raw.get("query_type"),
|
|
"response": raw.get("response"),
|
|
"response_code": raw.get("response_code"),
|
|
}
|
|
|
|
|
|
@_register(SourceType.PROXY)
|
|
def _normalize_proxy(raw: dict[str, Any]) -> dict[str, Any]:
|
|
"""
|
|
Extract proxy-specific fields like URL and user agent
|
|
"""
|
|
return {
|
|
"url": raw.get("url"),
|
|
"method": raw.get("method"),
|
|
"status_code": raw.get("status_code"),
|
|
"user_agent": raw.get("user_agent"),
|
|
"content_type": raw.get("content_type"),
|
|
"bytes_transferred": raw.get("bytes_transferred"),
|
|
}
|
|
|
|
|
|
@_register(SourceType.GENERIC)
|
|
def _normalize_generic(raw: dict[str, Any]) -> dict[str, Any]:
|
|
"""
|
|
Fallback normalizer for unrecognized source types
|
|
"""
|
|
return {
|
|
"message": raw.get("message"),
|
|
}
|