"""Detect local network manipulation and man-in-the-middle indicators.""" from __future__ import annotations from collections import defaultdict from typing import Iterable from ..context import CaptureContext from ..findings import Finding, Category, Severity, Confidence from .base import BaseDetector, register @register class MitmDetector(BaseDetector): name = "MitmDetector" description = "Detects ARP spoofing, duplicate IP mappings, and ARP floods." def detect(self, ctx: CaptureContext) -> Iterable[Finding]: findings: list[Finding] = [] # Map each IP address to MAC addresses observed in ARP responses. ip_to_macs: dict[str, set[str]] = defaultdict(set) first_seen: dict[str, float] = {} last_seen: dict[str, float] = {} packets_for_ip: dict[str, list[int]] = defaultdict(list) for ev in ctx.arp_events: if ev.sender_ip and ev.sender_mac: ip_to_macs[ev.sender_ip].add(ev.sender_mac) if ev.timestamp is not None: first_seen[ev.sender_ip] = min(first_seen.get(ev.sender_ip, ev.timestamp), ev.timestamp) last_seen[ev.sender_ip] = max(last_seen.get(ev.sender_ip, ev.timestamp), ev.timestamp) if ev.packet_number: packets_for_ip[ev.sender_ip].append(ev.packet_number) for ip, host in ctx.hosts.items(): if host.macs: ip_to_macs[ip].update(host.macs) for ip, macs in ip_to_macs.items(): if len(macs) > 1: findings.append(Finding( title="Multiple MAC addresses associated with one IP", category=Category.MITM, severity=Severity.HIGH, confidence=Confidence.MEDIUM, source_ip=ip, protocol="arp", timestamp_first_seen=first_seen.get(ip), timestamp_last_seen=last_seen.get(ip), risk_description=( "One IP address was advertised by multiple MAC addresses, a common " "indicator of ARP spoofing or traffic interception." ), recommendation=( "Review ARP tables, enable Dynamic ARP Inspection and port security, " "and identify the conflicting host." ), evidence={"ip": ip, "macs": sorted(macs), "dedup_key": f"arpdup:{ip}"}, related_packets=sorted(packets_for_ip.get(ip, []))[:10], )) # An ARP reply flood can accompany active cache poisoning. grat = [e for e in ctx.arp_events if e.opcode == 2] if len(grat) >= 50: findings.append(Finding( title="Unusually high number of ARP replies", category=Category.MITM, severity=Severity.MEDIUM, confidence=Confidence.LOW, protocol="arp", risk_description=( "A large number of ARP replies may indicate active ARP cache poisoning." ), recommendation="Inspect the reply source and investigate possible interception.", evidence={"arp_reply_count": len(grat), "dedup_key": "arp_reply_flood"}, related_packets=sorted({e.packet_number for e in grat if e.packet_number})[:8], )) return self._tag(findings)