This commit is contained in:
Mateusz Gruszczyński
2026-08-17 10:07:19 +02:00
parent 074d17be89
commit cc3c446c8e
29 changed files with 627 additions and 105 deletions
+24 -8
View File
@@ -21,6 +21,7 @@ SUPPORTED_EVENT_TYPES = {
"ssh", "rdp", "smb", "quic", "dhcp", "arp", "ike", "mqtt", "ftp", "ftp_data", "smtp",
"websocket", "nfs", "tftp", "dcerpc", "krb5", "snmp", "rfb", "sip", "ldap", "pop3",
}
MAX_ANALYTICS_DIMENSION_KEYS = 4096
def _utc_now() -> str:
@@ -52,6 +53,20 @@ def _text(value: Any, max_len: int = 512) -> str:
return str(value)[:max_len]
def _bounded_counter_add(
counter: collections.Counter[str],
key: str,
amount: int = 1,
*,
max_keys: int = MAX_ANALYTICS_DIMENSION_KEYS,
) -> None:
"""Update a dashboard Counter without retaining unbounded unique values."""
if not key:
return
if key in counter or len(counter) < max_keys:
counter[key] += amount
def _parse_networks(value: str) -> list[ipaddress._BaseNetwork]:
result: list[ipaddress._BaseNetwork] = []
for raw in (value or "").split(","):
@@ -1215,7 +1230,8 @@ class TrafficHistory:
def _trim_if_due(self, redis: RedisConnection) -> None:
now = time.monotonic()
if now - self._last_trim > 15:
interval = 1.0 if self.max_events > 0 else 15.0
if now - self._last_trim > interval:
self._trim(redis)
self._last_trim = now
@@ -1437,7 +1453,7 @@ class _AnalyticsAccumulator:
self.alerts += 1
signature = _text(item.get("signature"), 160)
if signature:
self.signatures[signature] += 1
_bounded_counter_add(self.signatures, signature)
severity = item.get("severity")
if severity not in (None, ""):
self.severities[f"S{severity}"] += 1
@@ -1451,26 +1467,26 @@ class _AnalyticsAccumulator:
self.files += 1
filename = _text(item.get("filename"), 180) or "unnamed file"
digest = _text(item.get("file_sha256") or item.get("file_sha1") or item.get("file_md5"), 32)
self.file_activity[f"{filename}{' · ' + digest if digest else ''}"] += 1
_bounded_counter_add(self.file_activity, f"{filename}{' · ' + digest if digest else ''}")
direction = _text(item.get("direction"), 24) or "unknown"
src_ip = _text(item.get("src_ip"), 64)
dest_ip = _text(item.get("dest_ip"), 64)
ether_src = _text(item.get("ether_src"), 32)
ether_dest = _text(item.get("ether_dest"), 32)
if direction in {"outbound", "internal"} and src_ip and ether_src:
self.assets[f"{src_ip} · {ether_src}"] += 1
_bounded_counter_add(self.assets, f"{src_ip} · {ether_src}")
if direction in {"inbound", "internal"} and dest_ip and ether_dest:
self.assets[f"{dest_ip} · {ether_dest}"] += 1
_bounded_counter_add(self.assets, f"{dest_ip} · {ether_dest}")
if item.get("type") == "dhcp":
asset_ip = _text(item.get("dhcp_assigned_ip") or item.get("src_ip"), 64)
identity = _text(item.get("dhcp_hostname") or item.get("dhcp_client_mac"), 160)
if asset_ip or identity:
self.assets[f"{asset_ip}{' · ' if asset_ip and identity else ''}{identity}"] += 1
_bounded_counter_add(self.assets, f"{asset_ip}{' · ' if asset_ip and identity else ''}{identity}")
elif item.get("type") == "arp":
asset_ip = _text(item.get("arp_src_ip") or item.get("src_ip"), 64)
mac = _text(item.get("arp_src_mac"), 32)
if asset_ip or mac:
self.assets[f"{asset_ip}{' · ' if asset_ip and mac else ''}{mac}"] += 1
_bounded_counter_add(self.assets, f"{asset_ip}{' · ' if asset_ip and mac else ''}{mac}")
app_proto = _valid_app_proto(item.get("app_proto"))
if item.get("type") in {"tls", "quic", "ssh"} or app_proto in {"tls", "quic", "ssh"}:
self.encrypted += 1
@@ -1484,7 +1500,7 @@ class _AnalyticsAccumulator:
):
value = _text(item.get(key), 160)
if value:
self.fingerprints[f"{label} {value}"] += 1
_bounded_counter_add(self.fingerprints, f"{label} {value}")
if item.get("type") in {"http", "ftp", "smtp"} or app_proto in {"http", "ftp", "smtp", "telnet"}:
self.cleartext += 1
if is_flow: