mirror of
https://github.com/smittix/intercept.git
synced 2026-06-17 18:09:45 -07:00
v2.26.0: fix SSE fanout crash and branded logo FOUC
- Fix SSE fanout thread AttributeError when source queue is None during interpreter shutdown by snapshotting to local variable with null guard - Fix branded "i" logo rendering oversized on first page load (FOUC) by adding inline width/height to SVG elements across 10 templates - Bump version to 2.26.0 in config.py, pyproject.toml, and CHANGELOG.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+51
-55
@@ -8,25 +8,18 @@ Provides unified WiFi scanning with dual-mode architecture:
|
||||
Also includes channel analysis, hidden SSID correlation, and network aggregation.
|
||||
"""
|
||||
|
||||
from .models import (
|
||||
WiFiObservation,
|
||||
WiFiAccessPoint,
|
||||
WiFiClient,
|
||||
WiFiProbeRequest,
|
||||
WiFiScanResult,
|
||||
WiFiScanStatus,
|
||||
WiFiCapabilities,
|
||||
ChannelStats,
|
||||
ChannelRecommendation,
|
||||
from .channel_analyzer import (
|
||||
ChannelAnalyzer,
|
||||
analyze_channels,
|
||||
)
|
||||
|
||||
from .scanner import (
|
||||
UnifiedWiFiScanner,
|
||||
get_wifi_scanner,
|
||||
reset_wifi_scanner,
|
||||
)
|
||||
|
||||
from .constants import (
|
||||
AUTH_EAP,
|
||||
# Auth
|
||||
AUTH_OPEN,
|
||||
AUTH_OWE,
|
||||
AUTH_PSK,
|
||||
AUTH_SAE,
|
||||
AUTH_UNKNOWN,
|
||||
# Bands
|
||||
BAND_2_4_GHZ,
|
||||
BAND_5_GHZ,
|
||||
@@ -36,64 +29,67 @@ from .constants import (
|
||||
CHANNELS_2_4_GHZ,
|
||||
CHANNELS_5_GHZ,
|
||||
CHANNELS_6_GHZ,
|
||||
NON_OVERLAPPING_2_4_GHZ,
|
||||
NON_OVERLAPPING_5_GHZ,
|
||||
# Security
|
||||
SECURITY_OPEN,
|
||||
SECURITY_WEP,
|
||||
SECURITY_WPA,
|
||||
SECURITY_WPA2,
|
||||
SECURITY_WPA3,
|
||||
SECURITY_WPA_WPA2,
|
||||
SECURITY_WPA2_WPA3,
|
||||
SECURITY_ENTERPRISE,
|
||||
SECURITY_UNKNOWN,
|
||||
# Cipher
|
||||
CIPHER_NONE,
|
||||
CIPHER_WEP,
|
||||
CIPHER_TKIP,
|
||||
CIPHER_CCMP,
|
||||
CIPHER_GCMP,
|
||||
# Cipher
|
||||
CIPHER_NONE,
|
||||
CIPHER_TKIP,
|
||||
CIPHER_UNKNOWN,
|
||||
# Auth
|
||||
AUTH_OPEN,
|
||||
AUTH_PSK,
|
||||
AUTH_SAE,
|
||||
AUTH_EAP,
|
||||
AUTH_OWE,
|
||||
AUTH_UNKNOWN,
|
||||
# Signal bands
|
||||
SIGNAL_STRONG,
|
||||
SIGNAL_MEDIUM,
|
||||
SIGNAL_WEAK,
|
||||
SIGNAL_VERY_WEAK,
|
||||
SIGNAL_UNKNOWN,
|
||||
CIPHER_WEP,
|
||||
NON_OVERLAPPING_2_4_GHZ,
|
||||
NON_OVERLAPPING_5_GHZ,
|
||||
PROXIMITY_FAR,
|
||||
# Proximity bands (consistent with Bluetooth)
|
||||
PROXIMITY_IMMEDIATE,
|
||||
PROXIMITY_NEAR,
|
||||
PROXIMITY_FAR,
|
||||
PROXIMITY_UNKNOWN,
|
||||
SCAN_MODE_DEEP,
|
||||
# Scan modes
|
||||
SCAN_MODE_QUICK,
|
||||
SCAN_MODE_DEEP,
|
||||
SECURITY_ENTERPRISE,
|
||||
# Security
|
||||
SECURITY_OPEN,
|
||||
SECURITY_UNKNOWN,
|
||||
SECURITY_WEP,
|
||||
SECURITY_WPA,
|
||||
SECURITY_WPA2,
|
||||
SECURITY_WPA2_WPA3,
|
||||
SECURITY_WPA3,
|
||||
SECURITY_WPA_WPA2,
|
||||
SIGNAL_MEDIUM,
|
||||
# Signal bands
|
||||
SIGNAL_STRONG,
|
||||
SIGNAL_UNKNOWN,
|
||||
SIGNAL_VERY_WEAK,
|
||||
SIGNAL_WEAK,
|
||||
# Helper functions
|
||||
get_band_from_channel,
|
||||
get_band_from_frequency,
|
||||
get_channel_from_frequency,
|
||||
get_signal_band,
|
||||
get_proximity_band,
|
||||
get_signal_band,
|
||||
get_vendor_from_mac,
|
||||
)
|
||||
|
||||
from .channel_analyzer import (
|
||||
ChannelAnalyzer,
|
||||
analyze_channels,
|
||||
)
|
||||
|
||||
from .hidden_ssid import (
|
||||
HiddenSSIDCorrelator,
|
||||
get_hidden_correlator,
|
||||
)
|
||||
from .models import (
|
||||
ChannelRecommendation,
|
||||
ChannelStats,
|
||||
WiFiAccessPoint,
|
||||
WiFiCapabilities,
|
||||
WiFiClient,
|
||||
WiFiObservation,
|
||||
WiFiProbeRequest,
|
||||
WiFiScanResult,
|
||||
WiFiScanStatus,
|
||||
)
|
||||
from .scanner import (
|
||||
UnifiedWiFiScanner,
|
||||
get_wifi_scanner,
|
||||
reset_wifi_scanner,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# Main scanner
|
||||
|
||||
@@ -11,26 +11,20 @@ Analyzes channel congestion based on:
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .constants import (
|
||||
BAND_2_4_GHZ,
|
||||
BAND_5_GHZ,
|
||||
BAND_6_GHZ,
|
||||
CHANNELS_2_4_GHZ,
|
||||
CHANNELS_5_GHZ,
|
||||
CHANNELS_6_GHZ,
|
||||
NON_OVERLAPPING_2_4_GHZ,
|
||||
NON_OVERLAPPING_5_GHZ,
|
||||
CHANNEL_FREQUENCIES,
|
||||
CHANNEL_RSSI_INTERFERENCE_FACTOR,
|
||||
CHANNEL_WEIGHT_AP_COUNT,
|
||||
CHANNEL_WEIGHT_CLIENT_COUNT,
|
||||
CHANNEL_RSSI_INTERFERENCE_FACTOR,
|
||||
NON_OVERLAPPING_2_4_GHZ,
|
||||
NON_OVERLAPPING_5_GHZ,
|
||||
get_band_from_channel,
|
||||
)
|
||||
from .models import WiFiAccessPoint, ChannelStats, ChannelRecommendation
|
||||
from .models import ChannelRecommendation, ChannelStats, WiFiAccessPoint
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -255,7 +249,7 @@ class ChannelAnalyzer:
|
||||
if ap_count == 0:
|
||||
reason = "No APs detected - clear channel"
|
||||
elif ap_count == 1:
|
||||
reason = f"1 AP on channel"
|
||||
reason = "1 AP on channel"
|
||||
else:
|
||||
reason = f"{ap_count} APs on channel"
|
||||
|
||||
|
||||
@@ -7,18 +7,18 @@ frames, detecting potential deauth flood attacks.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Callable, Optional, Any
|
||||
from typing import Any, Callable
|
||||
|
||||
from utils.constants import (
|
||||
DEAUTH_DETECTION_WINDOW,
|
||||
DEAUTH_ALERT_THRESHOLD,
|
||||
DEAUTH_CRITICAL_THRESHOLD,
|
||||
DEAUTH_DETECTION_WINDOW,
|
||||
DEAUTH_SNIFF_TIMEOUT,
|
||||
)
|
||||
|
||||
@@ -63,7 +63,7 @@ class DeauthPacketInfo:
|
||||
dst_mac: str
|
||||
bssid: str
|
||||
reason_code: int
|
||||
signal_dbm: Optional[int] = None
|
||||
signal_dbm: int | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -106,20 +106,20 @@ class DeauthAlert:
|
||||
|
||||
# Attacker info
|
||||
attacker_mac: str
|
||||
attacker_vendor: Optional[str]
|
||||
attacker_signal_dbm: Optional[int]
|
||||
attacker_vendor: str | None
|
||||
attacker_signal_dbm: int | None
|
||||
is_spoofed_ap: bool
|
||||
|
||||
# Target info
|
||||
target_mac: str
|
||||
target_vendor: Optional[str]
|
||||
target_vendor: str | None
|
||||
target_type: str # 'client', 'broadcast', 'ap'
|
||||
target_known_from_scan: bool
|
||||
|
||||
# Access point info
|
||||
ap_bssid: str
|
||||
ap_essid: Optional[str]
|
||||
ap_channel: Optional[int]
|
||||
ap_essid: str | None
|
||||
ap_channel: int | None
|
||||
|
||||
# Attack info
|
||||
frame_type: str
|
||||
@@ -184,8 +184,8 @@ class DeauthDetector:
|
||||
self,
|
||||
interface: str,
|
||||
event_callback: Callable[[dict], None],
|
||||
get_networks: Optional[Callable[[], dict[str, Any]]] = None,
|
||||
get_clients: Optional[Callable[[], dict[str, Any]]] = None,
|
||||
get_networks: Callable[[], dict[str, Any]] | None = None,
|
||||
get_clients: Callable[[], dict[str, Any]] | None = None,
|
||||
):
|
||||
"""
|
||||
Initialize the deauth detector.
|
||||
@@ -202,7 +202,7 @@ class DeauthDetector:
|
||||
self.get_clients = get_clients
|
||||
|
||||
self._stop_event = threading.Event()
|
||||
self._thread: Optional[threading.Thread] = None
|
||||
self._thread: threading.Thread | None = None
|
||||
self._lock = threading.Lock()
|
||||
|
||||
# Track deauth packets by (src, dst, bssid) tuple
|
||||
@@ -215,7 +215,7 @@ class DeauthDetector:
|
||||
# Stats
|
||||
self._packets_captured = 0
|
||||
self._alerts_generated = 0
|
||||
self._started_at: Optional[float] = None
|
||||
self._started_at: float | None = None
|
||||
|
||||
@property
|
||||
def is_running(self) -> bool:
|
||||
@@ -296,7 +296,7 @@ class DeauthDetector:
|
||||
def _sniff_loop(self):
|
||||
"""Main sniffing loop using scapy."""
|
||||
try:
|
||||
from scapy.all import sniff, Dot11, Dot11Deauth, Dot11Disas
|
||||
from scapy.all import Dot11, Dot11Deauth, Dot11Disas, sniff
|
||||
except ImportError:
|
||||
logger.error("scapy not installed. Install with: pip install scapy")
|
||||
self.event_callback({
|
||||
@@ -388,10 +388,8 @@ class DeauthDetector:
|
||||
# Extract signal strength from RadioTap if available
|
||||
signal_dbm = None
|
||||
if pkt.haslayer(RadioTap):
|
||||
try:
|
||||
with contextlib.suppress(AttributeError):
|
||||
signal_dbm = pkt[RadioTap].dBm_AntSignal
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
# Create packet info
|
||||
pkt_info = DeauthPacketInfo(
|
||||
@@ -579,7 +577,7 @@ class DeauthDetector:
|
||||
|
||||
try:
|
||||
networks = self.get_networks()
|
||||
return {bssid.upper() for bssid in networks.keys()}
|
||||
return {bssid.upper() for bssid in networks}
|
||||
except Exception:
|
||||
return set()
|
||||
|
||||
@@ -587,7 +585,7 @@ class DeauthDetector:
|
||||
"""Check if source MAC matches a known AP (spoofing indicator)."""
|
||||
return src_mac.upper() in self._get_known_aps()
|
||||
|
||||
def _get_vendor(self, mac: str) -> Optional[str]:
|
||||
def _get_vendor(self, mac: str) -> str | None:
|
||||
"""Get vendor from MAC OUI."""
|
||||
try:
|
||||
from data.oui import get_manufacturer
|
||||
|
||||
@@ -17,9 +17,9 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import threading
|
||||
from dataclasses import dataclass, field
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Callable, Optional
|
||||
from typing import Callable
|
||||
|
||||
from .constants import (
|
||||
HIDDEN_CORRELATION_WINDOW_SECONDS,
|
||||
@@ -29,7 +29,7 @@ from .constants import (
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Global correlator instance
|
||||
_correlator_instance: Optional['HiddenSSIDCorrelator'] = None
|
||||
_correlator_instance: HiddenSSIDCorrelator | None = None
|
||||
_correlator_lock = threading.Lock()
|
||||
|
||||
|
||||
@@ -92,9 +92,9 @@ class HiddenSSIDCorrelator:
|
||||
self._revealed: dict[str, CorrelationResult] = {} # BSSID -> result
|
||||
|
||||
# Callbacks
|
||||
self._on_ssid_revealed: Optional[Callable[[CorrelationResult], None]] = None
|
||||
self._on_ssid_revealed: Callable[[CorrelationResult], None] | None = None
|
||||
|
||||
def record_probe(self, client_mac: str, probed_ssid: str, timestamp: Optional[datetime] = None):
|
||||
def record_probe(self, client_mac: str, probed_ssid: str, timestamp: datetime | None = None):
|
||||
"""
|
||||
Record a probe request.
|
||||
|
||||
@@ -122,7 +122,7 @@ class HiddenSSIDCorrelator:
|
||||
# Check for correlations with known hidden APs
|
||||
self._check_correlations()
|
||||
|
||||
def record_association(self, client_mac: str, bssid: str, timestamp: Optional[datetime] = None):
|
||||
def record_association(self, client_mac: str, bssid: str, timestamp: datetime | None = None):
|
||||
"""
|
||||
Record a client association with an AP.
|
||||
|
||||
@@ -151,7 +151,7 @@ class HiddenSSIDCorrelator:
|
||||
# Check for correlations
|
||||
self._check_correlations()
|
||||
|
||||
def record_hidden_ap(self, bssid: str, timestamp: Optional[datetime] = None):
|
||||
def record_hidden_ap(self, bssid: str, timestamp: datetime | None = None):
|
||||
"""
|
||||
Record a hidden access point (empty SSID).
|
||||
|
||||
@@ -171,7 +171,7 @@ class HiddenSSIDCorrelator:
|
||||
# Check for correlations
|
||||
self._check_correlations()
|
||||
|
||||
def get_revealed_ssid(self, bssid: str) -> Optional[str]:
|
||||
def get_revealed_ssid(self, bssid: str) -> str | None:
|
||||
"""
|
||||
Get the revealed SSID for a hidden AP, if known.
|
||||
|
||||
@@ -185,7 +185,7 @@ class HiddenSSIDCorrelator:
|
||||
result = self._revealed.get(bssid.upper())
|
||||
return result.revealed_ssid if result else None
|
||||
|
||||
def get_correlation(self, bssid: str) -> Optional[CorrelationResult]:
|
||||
def get_correlation(self, bssid: str) -> CorrelationResult | None:
|
||||
"""
|
||||
Get the full correlation result for a hidden AP.
|
||||
|
||||
|
||||
+51
-54
@@ -6,20 +6,17 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from .constants import (
|
||||
BAND_UNKNOWN,
|
||||
SECURITY_UNKNOWN,
|
||||
CIPHER_UNKNOWN,
|
||||
AUTH_UNKNOWN,
|
||||
WIDTH_UNKNOWN,
|
||||
SIGNAL_UNKNOWN,
|
||||
BAND_UNKNOWN,
|
||||
CIPHER_UNKNOWN,
|
||||
PROXIMITY_UNKNOWN,
|
||||
SCAN_MODE_QUICK,
|
||||
SECURITY_UNKNOWN,
|
||||
SIGNAL_UNKNOWN,
|
||||
WIDTH_UNKNOWN,
|
||||
get_band_from_channel,
|
||||
get_signal_band,
|
||||
get_proximity_band,
|
||||
get_vendor_from_mac,
|
||||
)
|
||||
|
||||
@@ -30,10 +27,10 @@ class WiFiObservation:
|
||||
|
||||
timestamp: datetime
|
||||
bssid: str
|
||||
essid: Optional[str] = None
|
||||
channel: Optional[int] = None
|
||||
frequency_mhz: Optional[int] = None
|
||||
rssi: Optional[int] = None
|
||||
essid: str | None = None
|
||||
channel: int | None = None
|
||||
frequency_mhz: int | None = None
|
||||
rssi: int | None = None
|
||||
|
||||
# Security
|
||||
security: str = SECURITY_UNKNOWN
|
||||
@@ -58,7 +55,7 @@ class WiFiObservation:
|
||||
return BAND_UNKNOWN
|
||||
|
||||
@property
|
||||
def vendor(self) -> Optional[str]:
|
||||
def vendor(self) -> str | None:
|
||||
"""Get vendor name from BSSID."""
|
||||
return get_vendor_from_mac(self.bssid)
|
||||
|
||||
@@ -89,29 +86,29 @@ class WiFiAccessPoint:
|
||||
|
||||
# Identity
|
||||
bssid: str
|
||||
essid: Optional[str] = None
|
||||
essid: str | None = None
|
||||
is_hidden: bool = False
|
||||
revealed_essid: Optional[str] = None # Revealed through correlation
|
||||
revealed_essid: str | None = None # Revealed through correlation
|
||||
|
||||
# Radio info
|
||||
channel: Optional[int] = None
|
||||
frequency_mhz: Optional[int] = None
|
||||
channel: int | None = None
|
||||
frequency_mhz: int | None = None
|
||||
band: str = BAND_UNKNOWN
|
||||
width: str = WIDTH_UNKNOWN
|
||||
|
||||
# Signal aggregation
|
||||
rssi_samples: list[tuple[datetime, int]] = field(default_factory=list)
|
||||
rssi_current: Optional[int] = None
|
||||
rssi_median: Optional[float] = None
|
||||
rssi_min: Optional[int] = None
|
||||
rssi_max: Optional[int] = None
|
||||
rssi_variance: Optional[float] = None
|
||||
rssi_ema: Optional[float] = None
|
||||
rssi_current: int | None = None
|
||||
rssi_median: float | None = None
|
||||
rssi_min: int | None = None
|
||||
rssi_max: int | None = None
|
||||
rssi_variance: float | None = None
|
||||
rssi_ema: float | None = None
|
||||
|
||||
# Proximity/signal bands
|
||||
signal_band: str = SIGNAL_UNKNOWN
|
||||
proximity_band: str = PROXIMITY_UNKNOWN
|
||||
estimated_distance_m: Optional[float] = None
|
||||
estimated_distance_m: float | None = None
|
||||
distance_confidence: float = 0.0
|
||||
|
||||
# Security
|
||||
@@ -131,7 +128,7 @@ class WiFiAccessPoint:
|
||||
client_count: int = 0
|
||||
|
||||
# Metadata
|
||||
vendor: Optional[str] = None
|
||||
vendor: str | None = None
|
||||
|
||||
# Heuristic flags
|
||||
heuristic_flags: list[str] = field(default_factory=list)
|
||||
@@ -141,7 +138,7 @@ class WiFiAccessPoint:
|
||||
|
||||
# Baseline tracking
|
||||
in_baseline: bool = False
|
||||
baseline_id: Optional[int] = None
|
||||
baseline_id: int | None = None
|
||||
|
||||
@property
|
||||
def display_name(self) -> str:
|
||||
@@ -281,23 +278,23 @@ class WiFiClient:
|
||||
|
||||
# Identity
|
||||
mac: str
|
||||
vendor: Optional[str] = None
|
||||
vendor: str | None = None
|
||||
|
||||
# Signal
|
||||
rssi_samples: list[tuple[datetime, int]] = field(default_factory=list)
|
||||
rssi_current: Optional[int] = None
|
||||
rssi_median: Optional[float] = None
|
||||
rssi_min: Optional[int] = None
|
||||
rssi_max: Optional[int] = None
|
||||
rssi_ema: Optional[float] = None
|
||||
rssi_current: int | None = None
|
||||
rssi_median: float | None = None
|
||||
rssi_min: int | None = None
|
||||
rssi_max: int | None = None
|
||||
rssi_ema: float | None = None
|
||||
|
||||
# Proximity
|
||||
signal_band: str = SIGNAL_UNKNOWN
|
||||
proximity_band: str = PROXIMITY_UNKNOWN
|
||||
estimated_distance_m: Optional[float] = None
|
||||
estimated_distance_m: float | None = None
|
||||
|
||||
# Association
|
||||
associated_bssid: Optional[str] = None
|
||||
associated_bssid: str | None = None
|
||||
is_associated: bool = False
|
||||
|
||||
# Probes
|
||||
@@ -380,8 +377,8 @@ class WiFiProbeRequest:
|
||||
timestamp: datetime
|
||||
client_mac: str
|
||||
probed_ssid: str
|
||||
rssi: Optional[int] = None
|
||||
client_vendor: Optional[str] = None
|
||||
rssi: int | None = None
|
||||
client_vendor: str | None = None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""Convert to dictionary for JSON serialization."""
|
||||
@@ -400,22 +397,22 @@ class ChannelStats:
|
||||
|
||||
channel: int
|
||||
band: str = BAND_UNKNOWN
|
||||
frequency_mhz: Optional[int] = None
|
||||
frequency_mhz: int | None = None
|
||||
|
||||
# Counts
|
||||
ap_count: int = 0
|
||||
client_count: int = 0
|
||||
|
||||
# Signal stats
|
||||
rssi_avg: Optional[float] = None
|
||||
rssi_min: Optional[int] = None
|
||||
rssi_max: Optional[int] = None
|
||||
rssi_avg: float | None = None
|
||||
rssi_min: int | None = None
|
||||
rssi_max: int | None = None
|
||||
|
||||
# Utilization score (0.0-1.0, lower is better)
|
||||
utilization_score: float = 0.0
|
||||
|
||||
# Recommendation rank (1 = best)
|
||||
recommendation_rank: Optional[int] = None
|
||||
recommendation_rank: int | None = None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""Convert to dictionary for JSON serialization."""
|
||||
@@ -442,7 +439,7 @@ class ChannelRecommendation:
|
||||
score: float # Lower is better
|
||||
reason: str
|
||||
is_dfs: bool = False
|
||||
recommendation_rank: Optional[int] = None
|
||||
recommendation_rank: int | None = None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""Convert to dictionary for JSON serialization."""
|
||||
@@ -471,14 +468,14 @@ class WiFiScanResult:
|
||||
|
||||
# Scan metadata
|
||||
scan_mode: str = SCAN_MODE_QUICK
|
||||
interface: Optional[str] = None
|
||||
started_at: Optional[datetime] = None
|
||||
completed_at: Optional[datetime] = None
|
||||
duration_seconds: Optional[float] = None
|
||||
interface: str | None = None
|
||||
started_at: datetime | None = None
|
||||
completed_at: datetime | None = None
|
||||
duration_seconds: float | None = None
|
||||
|
||||
# Status
|
||||
is_complete: bool = False
|
||||
error: Optional[str] = None
|
||||
error: str | None = None
|
||||
warnings: list[str] = field(default_factory=list)
|
||||
|
||||
@property
|
||||
@@ -545,14 +542,14 @@ class WiFiScanStatus:
|
||||
|
||||
is_scanning: bool = False
|
||||
scan_mode: str = SCAN_MODE_QUICK
|
||||
interface: Optional[str] = None
|
||||
started_at: Optional[datetime] = None
|
||||
interface: str | None = None
|
||||
started_at: datetime | None = None
|
||||
networks_found: int = 0
|
||||
clients_found: int = 0
|
||||
error: Optional[str] = None
|
||||
error: str | None = None
|
||||
|
||||
@property
|
||||
def elapsed_seconds(self) -> Optional[float]:
|
||||
def elapsed_seconds(self) -> float | None:
|
||||
"""Seconds since scan started."""
|
||||
if self.started_at:
|
||||
return (datetime.now() - self.started_at).total_seconds()
|
||||
@@ -582,20 +579,20 @@ class WiFiCapabilities:
|
||||
|
||||
# Interfaces
|
||||
interfaces: list[dict] = field(default_factory=list)
|
||||
default_interface: Optional[str] = None
|
||||
default_interface: str | None = None
|
||||
|
||||
# Quick scan tools
|
||||
has_nmcli: bool = False
|
||||
has_iw: bool = False
|
||||
has_iwlist: bool = False
|
||||
has_airport: bool = False
|
||||
preferred_quick_tool: Optional[str] = None
|
||||
preferred_quick_tool: str | None = None
|
||||
|
||||
# Deep scan tools
|
||||
has_airmon_ng: bool = False
|
||||
has_airodump_ng: bool = False
|
||||
has_monitor_capable_interface: bool = False
|
||||
monitor_interface: Optional[str] = None
|
||||
monitor_interface: str | None = None
|
||||
|
||||
# Issues
|
||||
issues: list[str] = field(default_factory=list)
|
||||
|
||||
@@ -4,11 +4,11 @@ WiFi scan output parsers.
|
||||
Each parser converts tool-specific output into WiFiObservation objects.
|
||||
"""
|
||||
|
||||
from .airodump import parse_airodump_csv
|
||||
from .airport import parse_airport_scan
|
||||
from .nmcli import parse_nmcli_scan
|
||||
from .iw import parse_iw_scan
|
||||
from .iwlist import parse_iwlist_scan
|
||||
from .airodump import parse_airodump_csv
|
||||
from .nmcli import parse_nmcli_scan
|
||||
|
||||
__all__ = [
|
||||
'parse_airport_scan',
|
||||
|
||||
@@ -22,29 +22,28 @@ import io
|
||||
import logging
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from ..models import WiFiObservation
|
||||
from ..constants import (
|
||||
AUTH_EAP,
|
||||
AUTH_OPEN,
|
||||
AUTH_OWE,
|
||||
AUTH_PSK,
|
||||
AUTH_SAE,
|
||||
AUTH_UNKNOWN,
|
||||
CHANNEL_FREQUENCIES,
|
||||
CIPHER_CCMP,
|
||||
CIPHER_TKIP,
|
||||
CIPHER_UNKNOWN,
|
||||
CIPHER_WEP,
|
||||
SECURITY_OPEN,
|
||||
SECURITY_UNKNOWN,
|
||||
SECURITY_WEP,
|
||||
SECURITY_WPA,
|
||||
SECURITY_WPA2,
|
||||
SECURITY_WPA3,
|
||||
SECURITY_WPA_WPA2,
|
||||
SECURITY_UNKNOWN,
|
||||
CIPHER_CCMP,
|
||||
CIPHER_TKIP,
|
||||
CIPHER_WEP,
|
||||
CIPHER_UNKNOWN,
|
||||
AUTH_PSK,
|
||||
AUTH_SAE,
|
||||
AUTH_EAP,
|
||||
AUTH_OWE,
|
||||
AUTH_OPEN,
|
||||
AUTH_UNKNOWN,
|
||||
CHANNEL_FREQUENCIES,
|
||||
)
|
||||
from ..models import WiFiObservation
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -63,7 +62,7 @@ def parse_airodump_csv(filepath: str) -> tuple[list[WiFiObservation], list[dict]
|
||||
clients = []
|
||||
|
||||
try:
|
||||
with open(filepath, 'r', encoding='utf-8', errors='replace') as f:
|
||||
with open(filepath, encoding='utf-8', errors='replace') as f:
|
||||
content = f.read()
|
||||
|
||||
# airodump-ng separates sections with blank lines
|
||||
|
||||
@@ -12,33 +12,31 @@ from __future__ import annotations
|
||||
import logging
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from ..models import WiFiObservation
|
||||
from ..constants import (
|
||||
AUTH_EAP,
|
||||
AUTH_OPEN,
|
||||
AUTH_PSK,
|
||||
AUTH_SAE,
|
||||
AUTH_UNKNOWN,
|
||||
CHANNEL_FREQUENCIES,
|
||||
CIPHER_CCMP,
|
||||
CIPHER_NONE,
|
||||
CIPHER_TKIP,
|
||||
CIPHER_UNKNOWN,
|
||||
CIPHER_WEP,
|
||||
SECURITY_OPEN,
|
||||
SECURITY_UNKNOWN,
|
||||
SECURITY_WEP,
|
||||
SECURITY_WPA,
|
||||
SECURITY_WPA2,
|
||||
SECURITY_WPA2_WPA3,
|
||||
SECURITY_WPA3,
|
||||
SECURITY_WPA_WPA2,
|
||||
SECURITY_WPA2_WPA3,
|
||||
SECURITY_UNKNOWN,
|
||||
CIPHER_CCMP,
|
||||
CIPHER_TKIP,
|
||||
CIPHER_WEP,
|
||||
CIPHER_NONE,
|
||||
CIPHER_UNKNOWN,
|
||||
AUTH_PSK,
|
||||
AUTH_SAE,
|
||||
AUTH_EAP,
|
||||
AUTH_OPEN,
|
||||
AUTH_UNKNOWN,
|
||||
WIDTH_20_MHZ,
|
||||
WIDTH_40_MHZ,
|
||||
get_band_from_channel,
|
||||
CHANNEL_FREQUENCIES,
|
||||
)
|
||||
from ..models import WiFiObservation
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -68,7 +66,7 @@ def parse_airport_scan(output: str) -> list[WiFiObservation]:
|
||||
return observations
|
||||
|
||||
|
||||
def _parse_airport_line(line: str) -> Optional[WiFiObservation]:
|
||||
def _parse_airport_line(line: str) -> WiFiObservation | None:
|
||||
"""Parse a single line of airport output."""
|
||||
# airport output is space-aligned, need careful parsing
|
||||
# Format: SSID (variable width) BSSID RSSI CHANNEL HT CC SECURITY
|
||||
@@ -95,10 +93,8 @@ def _parse_airport_line(line: str) -> Optional[WiFiObservation]:
|
||||
ssid = line[:bssid_pos].strip()
|
||||
|
||||
# Handle hidden network indicator
|
||||
is_hidden = False
|
||||
if ssid == '--' or not ssid:
|
||||
ssid = None
|
||||
is_hidden = True
|
||||
|
||||
# Parse remainder after BSSID
|
||||
remainder = line[bssid_match.end():].strip()
|
||||
|
||||
+13
-16
@@ -24,35 +24,32 @@ from __future__ import annotations
|
||||
import logging
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from ..models import WiFiObservation
|
||||
from ..constants import (
|
||||
AUTH_EAP,
|
||||
AUTH_OPEN,
|
||||
AUTH_OWE,
|
||||
AUTH_PSK,
|
||||
AUTH_SAE,
|
||||
AUTH_UNKNOWN,
|
||||
CIPHER_CCMP,
|
||||
CIPHER_GCMP,
|
||||
CIPHER_TKIP,
|
||||
CIPHER_UNKNOWN,
|
||||
CIPHER_WEP,
|
||||
SECURITY_OPEN,
|
||||
SECURITY_WEP,
|
||||
SECURITY_WPA,
|
||||
SECURITY_WPA2,
|
||||
SECURITY_WPA3,
|
||||
SECURITY_WPA_WPA2,
|
||||
SECURITY_WPA2_WPA3,
|
||||
SECURITY_UNKNOWN,
|
||||
CIPHER_CCMP,
|
||||
CIPHER_TKIP,
|
||||
CIPHER_GCMP,
|
||||
CIPHER_WEP,
|
||||
CIPHER_UNKNOWN,
|
||||
AUTH_PSK,
|
||||
AUTH_SAE,
|
||||
AUTH_EAP,
|
||||
AUTH_OWE,
|
||||
AUTH_OPEN,
|
||||
AUTH_UNKNOWN,
|
||||
WIDTH_20_MHZ,
|
||||
WIDTH_40_MHZ,
|
||||
WIDTH_80_MHZ,
|
||||
WIDTH_160_MHZ,
|
||||
get_channel_from_frequency,
|
||||
)
|
||||
from ..models import WiFiObservation
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -90,7 +87,7 @@ def parse_iw_scan(output: str) -> list[WiFiObservation]:
|
||||
return observations
|
||||
|
||||
|
||||
def _parse_iw_block(lines: list[str]) -> Optional[WiFiObservation]:
|
||||
def _parse_iw_block(lines: list[str]) -> WiFiObservation | None:
|
||||
"""Parse a single BSS block from iw output."""
|
||||
try:
|
||||
# First line: BSS 00:11:22:33:44:55(on wlan0) -- associated
|
||||
|
||||
@@ -25,27 +25,25 @@ from __future__ import annotations
|
||||
import logging
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from ..models import WiFiObservation
|
||||
from ..constants import (
|
||||
AUTH_EAP,
|
||||
AUTH_OPEN,
|
||||
AUTH_PSK,
|
||||
AUTH_UNKNOWN,
|
||||
CHANNEL_FREQUENCIES,
|
||||
CIPHER_CCMP,
|
||||
CIPHER_TKIP,
|
||||
CIPHER_UNKNOWN,
|
||||
CIPHER_WEP,
|
||||
SECURITY_OPEN,
|
||||
SECURITY_WEP,
|
||||
SECURITY_WPA,
|
||||
SECURITY_WPA2,
|
||||
SECURITY_WPA_WPA2,
|
||||
SECURITY_UNKNOWN,
|
||||
CIPHER_CCMP,
|
||||
CIPHER_TKIP,
|
||||
CIPHER_WEP,
|
||||
CIPHER_UNKNOWN,
|
||||
AUTH_PSK,
|
||||
AUTH_EAP,
|
||||
AUTH_OPEN,
|
||||
AUTH_UNKNOWN,
|
||||
get_channel_from_frequency,
|
||||
CHANNEL_FREQUENCIES,
|
||||
)
|
||||
from ..models import WiFiObservation
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -83,7 +81,7 @@ def parse_iwlist_scan(output: str) -> list[WiFiObservation]:
|
||||
return observations
|
||||
|
||||
|
||||
def _parse_iwlist_block(lines: list[str]) -> Optional[WiFiObservation]:
|
||||
def _parse_iwlist_block(lines: list[str]) -> WiFiObservation | None:
|
||||
"""Parse a single Cell block from iwlist output."""
|
||||
try:
|
||||
# Extract BSSID from first line
|
||||
|
||||
+17
-19
@@ -11,30 +11,28 @@ from __future__ import annotations
|
||||
import logging
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from ..models import WiFiObservation
|
||||
from ..constants import (
|
||||
SECURITY_OPEN,
|
||||
SECURITY_WEP,
|
||||
SECURITY_WPA,
|
||||
SECURITY_WPA2,
|
||||
SECURITY_WPA3,
|
||||
SECURITY_WPA_WPA2,
|
||||
SECURITY_WPA2_WPA3,
|
||||
SECURITY_ENTERPRISE,
|
||||
SECURITY_UNKNOWN,
|
||||
AUTH_EAP,
|
||||
AUTH_OPEN,
|
||||
AUTH_PSK,
|
||||
AUTH_SAE,
|
||||
AUTH_UNKNOWN,
|
||||
CIPHER_CCMP,
|
||||
CIPHER_TKIP,
|
||||
CIPHER_UNKNOWN,
|
||||
AUTH_PSK,
|
||||
AUTH_SAE,
|
||||
AUTH_EAP,
|
||||
AUTH_OPEN,
|
||||
AUTH_UNKNOWN,
|
||||
SECURITY_ENTERPRISE,
|
||||
SECURITY_OPEN,
|
||||
SECURITY_UNKNOWN,
|
||||
SECURITY_WEP,
|
||||
SECURITY_WPA,
|
||||
SECURITY_WPA2,
|
||||
SECURITY_WPA2_WPA3,
|
||||
SECURITY_WPA3,
|
||||
SECURITY_WPA_WPA2,
|
||||
get_channel_from_frequency,
|
||||
get_band_from_frequency,
|
||||
)
|
||||
from ..models import WiFiObservation
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -62,7 +60,7 @@ def parse_nmcli_scan(output: str) -> list[WiFiObservation]:
|
||||
return observations
|
||||
|
||||
|
||||
def _parse_nmcli_line(line: str) -> Optional[WiFiObservation]:
|
||||
def _parse_nmcli_line(line: str) -> WiFiObservation | None:
|
||||
"""Parse a single line of nmcli terse output."""
|
||||
try:
|
||||
# nmcli terse format uses : as delimiter but escapes colons in values with \:
|
||||
@@ -188,7 +186,7 @@ def _parse_nmcli_security(security_str: str) -> tuple[str, str, str]:
|
||||
cipher = CIPHER_UNKNOWN
|
||||
if security in (SECURITY_WPA2, SECURITY_WPA3, SECURITY_WPA2_WPA3, SECURITY_ENTERPRISE):
|
||||
cipher = CIPHER_CCMP
|
||||
elif security == SECURITY_WPA or security == SECURITY_WPA_WPA2:
|
||||
elif security in (SECURITY_WPA, SECURITY_WPA_WPA2):
|
||||
cipher = CIPHER_TKIP # Often TKIP for mixed mode
|
||||
|
||||
# Determine auth
|
||||
|
||||
+44
-53
@@ -17,44 +17,43 @@ import shutil
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
from collections.abc import Generator
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Callable, Generator, Optional, TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Callable
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .deauth_detector import DeauthDetector
|
||||
|
||||
import contextlib
|
||||
|
||||
from .constants import (
|
||||
DEFAULT_QUICK_SCAN_TIMEOUT,
|
||||
SCAN_MODE_QUICK,
|
||||
SCAN_MODE_DEEP,
|
||||
QUICK_SCAN_TOOLS_LINUX,
|
||||
QUICK_SCAN_TOOLS_DARWIN,
|
||||
TOOL_TIMEOUT_QUICK,
|
||||
TOOL_TIMEOUT_DETECT,
|
||||
NETWORK_STALE_TIMEOUT,
|
||||
MAX_RSSI_SAMPLES,
|
||||
SCAN_MODE_DEEP,
|
||||
SCAN_MODE_QUICK,
|
||||
TOOL_TIMEOUT_DETECT,
|
||||
WIFI_EMA_ALPHA,
|
||||
get_signal_band,
|
||||
get_proximity_band,
|
||||
get_signal_band,
|
||||
get_vendor_from_mac,
|
||||
)
|
||||
from .models import (
|
||||
ChannelRecommendation,
|
||||
ChannelStats,
|
||||
WiFiAccessPoint,
|
||||
WiFiCapabilities,
|
||||
WiFiClient,
|
||||
WiFiObservation,
|
||||
WiFiProbeRequest,
|
||||
WiFiScanResult,
|
||||
WiFiScanStatus,
|
||||
WiFiCapabilities,
|
||||
WiFiObservation,
|
||||
ChannelStats,
|
||||
ChannelRecommendation,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Global scanner instance
|
||||
_scanner_instance: Optional['UnifiedWiFiScanner'] = None
|
||||
_scanner_instance: UnifiedWiFiScanner | None = None
|
||||
_scanner_lock = threading.Lock()
|
||||
|
||||
|
||||
@@ -66,7 +65,7 @@ class UnifiedWiFiScanner:
|
||||
Deep Scan: Continuous monitoring with airodump-ng
|
||||
"""
|
||||
|
||||
def __init__(self, interface: Optional[str] = None):
|
||||
def __init__(self, interface: str | None = None):
|
||||
"""
|
||||
Initialize WiFi scanner.
|
||||
|
||||
@@ -78,7 +77,7 @@ class UnifiedWiFiScanner:
|
||||
|
||||
# State
|
||||
self._status = WiFiScanStatus()
|
||||
self._capabilities: Optional[WiFiCapabilities] = None
|
||||
self._capabilities: WiFiCapabilities | None = None
|
||||
|
||||
# Discovered entities
|
||||
self._access_points: dict[str, WiFiAccessPoint] = {} # bssid -> AP
|
||||
@@ -86,24 +85,24 @@ class UnifiedWiFiScanner:
|
||||
self._probe_requests: list[WiFiProbeRequest] = []
|
||||
|
||||
# Deep scan process
|
||||
self._deep_scan_process: Optional[subprocess.Popen] = None
|
||||
self._deep_scan_thread: Optional[threading.Thread] = None
|
||||
self._deep_scan_process: subprocess.Popen | None = None
|
||||
self._deep_scan_thread: threading.Thread | None = None
|
||||
self._deep_scan_stop_event = threading.Event()
|
||||
|
||||
# Deauth detector
|
||||
self._deauth_detector: Optional['DeauthDetector'] = None
|
||||
self._deauth_detector: DeauthDetector | None = None
|
||||
|
||||
# Event queue for SSE streaming
|
||||
self._event_queue: queue.Queue = queue.Queue(maxsize=1000)
|
||||
|
||||
# Callbacks
|
||||
self._on_network_updated: Optional[Callable[[WiFiAccessPoint], None]] = None
|
||||
self._on_client_updated: Optional[Callable[[WiFiClient], None]] = None
|
||||
self._on_probe_request: Optional[Callable[[WiFiProbeRequest], None]] = None
|
||||
self._on_network_updated: Callable[[WiFiAccessPoint], None] | None = None
|
||||
self._on_client_updated: Callable[[WiFiClient], None] | None = None
|
||||
self._on_probe_request: Callable[[WiFiProbeRequest], None] | None = None
|
||||
|
||||
# Baseline tracking
|
||||
self._baseline_networks: set[str] = set() # BSSIDs in baseline
|
||||
self._baseline_set_at: Optional[datetime] = None
|
||||
self._baseline_set_at: datetime | None = None
|
||||
|
||||
# =========================================================================
|
||||
# Properties
|
||||
@@ -374,7 +373,7 @@ class UnifiedWiFiScanner:
|
||||
|
||||
def quick_scan(
|
||||
self,
|
||||
interface: Optional[str] = None,
|
||||
interface: str | None = None,
|
||||
timeout: float = DEFAULT_QUICK_SCAN_TIMEOUT,
|
||||
) -> WiFiScanResult:
|
||||
"""
|
||||
@@ -664,10 +663,10 @@ class UnifiedWiFiScanner:
|
||||
|
||||
def start_deep_scan(
|
||||
self,
|
||||
interface: Optional[str] = None,
|
||||
interface: str | None = None,
|
||||
band: str = 'all',
|
||||
channel: Optional[int] = None,
|
||||
channels: Optional[list[int]] = None,
|
||||
channel: int | None = None,
|
||||
channels: list[int] | None = None,
|
||||
) -> bool:
|
||||
"""
|
||||
Start continuous deep scan with airodump-ng.
|
||||
@@ -733,8 +732,8 @@ class UnifiedWiFiScanner:
|
||||
Returns:
|
||||
True if scan was stopped.
|
||||
"""
|
||||
cleanup_process: Optional[subprocess.Popen] = None
|
||||
cleanup_thread: Optional[threading.Thread] = None
|
||||
cleanup_process: subprocess.Popen | None = None
|
||||
cleanup_thread: threading.Thread | None = None
|
||||
cleanup_detector = None
|
||||
|
||||
with self._lock:
|
||||
@@ -760,8 +759,8 @@ class UnifiedWiFiScanner:
|
||||
cleanup_start = time.perf_counter()
|
||||
|
||||
def _finalize_stop(
|
||||
process: Optional[subprocess.Popen],
|
||||
scan_thread: Optional[threading.Thread],
|
||||
process: subprocess.Popen | None,
|
||||
scan_thread: threading.Thread | None,
|
||||
detector,
|
||||
) -> None:
|
||||
if detector:
|
||||
@@ -777,10 +776,8 @@ class UnifiedWiFiScanner:
|
||||
process.terminate()
|
||||
process.wait(timeout=1.5)
|
||||
except Exception:
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
process.kill()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if scan_thread and scan_thread.is_alive():
|
||||
scan_thread.join(timeout=1.5)
|
||||
@@ -801,14 +798,14 @@ class UnifiedWiFiScanner:
|
||||
self,
|
||||
interface: str,
|
||||
band: str,
|
||||
channel: Optional[int],
|
||||
channels: Optional[list[int]],
|
||||
channel: int | None,
|
||||
channels: list[int] | None,
|
||||
):
|
||||
"""Background thread for running airodump-ng."""
|
||||
from .parsers.airodump import parse_airodump_csv
|
||||
|
||||
import tempfile
|
||||
|
||||
from .parsers.airodump import parse_airodump_csv
|
||||
|
||||
# Create temp directory for output files
|
||||
with tempfile.TemporaryDirectory(prefix='wifi_scan_') as tmpdir:
|
||||
output_prefix = os.path.join(tmpdir, 'scan')
|
||||
@@ -829,7 +826,7 @@ class UnifiedWiFiScanner:
|
||||
|
||||
logger.info(f"Starting airodump-ng: {' '.join(cmd)}")
|
||||
|
||||
process: Optional[subprocess.Popen] = None
|
||||
process: subprocess.Popen | None = None
|
||||
try:
|
||||
process = subprocess.Popen(
|
||||
cmd,
|
||||
@@ -848,10 +845,8 @@ class UnifiedWiFiScanner:
|
||||
process.terminate()
|
||||
process.wait(timeout=1.0)
|
||||
except Exception:
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
process.kill()
|
||||
except Exception:
|
||||
pass
|
||||
return
|
||||
|
||||
csv_file = f"{output_prefix}-01.csv"
|
||||
@@ -1129,8 +1124,6 @@ class UnifiedWiFiScanner:
|
||||
def _calculate_channel_stats(self) -> list[ChannelStats]:
|
||||
"""Calculate statistics for each channel."""
|
||||
from .constants import (
|
||||
CHANNELS_2_4_GHZ,
|
||||
CHANNELS_5_GHZ,
|
||||
CHANNEL_FREQUENCIES,
|
||||
get_band_from_channel,
|
||||
)
|
||||
@@ -1175,10 +1168,10 @@ class UnifiedWiFiScanner:
|
||||
def _generate_recommendations(self, stats: list[ChannelStats]) -> list[ChannelRecommendation]:
|
||||
"""Generate channel recommendations."""
|
||||
from .constants import (
|
||||
NON_OVERLAPPING_2_4_GHZ,
|
||||
NON_OVERLAPPING_5_GHZ,
|
||||
BAND_2_4_GHZ,
|
||||
BAND_5_GHZ,
|
||||
NON_OVERLAPPING_2_4_GHZ,
|
||||
NON_OVERLAPPING_5_GHZ,
|
||||
)
|
||||
|
||||
recommendations = []
|
||||
@@ -1273,12 +1266,12 @@ class UnifiedWiFiScanner:
|
||||
# Data Access
|
||||
# =========================================================================
|
||||
|
||||
def get_network(self, bssid: str) -> Optional[WiFiAccessPoint]:
|
||||
def get_network(self, bssid: str) -> WiFiAccessPoint | None:
|
||||
"""Get a specific network by BSSID."""
|
||||
with self._lock:
|
||||
return self._access_points.get(bssid.upper())
|
||||
|
||||
def get_client(self, mac: str) -> Optional[WiFiClient]:
|
||||
def get_client(self, mac: str) -> WiFiClient | None:
|
||||
"""Get a specific client by MAC."""
|
||||
with self._lock:
|
||||
return self._clients.get(mac.upper())
|
||||
@@ -1336,10 +1329,8 @@ class UnifiedWiFiScanner:
|
||||
alert_id = event.get('id', str(time.time()))
|
||||
app_module.deauth_alerts[alert_id] = event
|
||||
if hasattr(app_module, 'deauth_detector_queue'):
|
||||
try:
|
||||
with contextlib.suppress(queue.Full):
|
||||
app_module.deauth_detector_queue.put_nowait(event)
|
||||
except queue.Full:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.debug(f"Error storing deauth alert: {e}")
|
||||
|
||||
@@ -1389,7 +1380,7 @@ class UnifiedWiFiScanner:
|
||||
self._deauth_detector = None
|
||||
|
||||
@property
|
||||
def deauth_detector(self) -> Optional['DeauthDetector']:
|
||||
def deauth_detector(self) -> DeauthDetector | None:
|
||||
"""Get the deauth detector instance."""
|
||||
return self._deauth_detector
|
||||
|
||||
@@ -1416,7 +1407,7 @@ class UnifiedWiFiScanner:
|
||||
# Module-level functions
|
||||
# =============================================================================
|
||||
|
||||
def get_wifi_scanner(interface: Optional[str] = None) -> UnifiedWiFiScanner:
|
||||
def get_wifi_scanner(interface: str | None = None) -> UnifiedWiFiScanner:
|
||||
"""
|
||||
Get or create the global WiFi scanner instance.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user