79 lines
3.4 KiB
Python
79 lines
3.4 KiB
Python
"""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)
|