ap_scanner/models.py (3364B)
1 """Data models for AP Scanner."""
2
3 from __future__ import annotations
4
5 from dataclasses import dataclass, field
6 from datetime import datetime
7
8
9 @dataclass
10 class ActiveScanClient:
11 """A client discovered during active ARP scanning."""
12
13 ip: str
14 mac: str
15 vendor: str = ""
16
17
18 @dataclass
19 class ActiveScanResult:
20 """Results from an active scan of an open AP."""
21
22 bssid: str
23 ssid: str
24 scan_time: datetime = field(default_factory=datetime.now)
25 connected: bool = False
26 assigned_ip: str = ""
27 subnet_mask: str = ""
28 gateway_ip: str = ""
29 gateway_mac: str = ""
30 dns_servers: list[str] = field(default_factory=list)
31 dhcp_server: str = ""
32 clients: list[ActiveScanClient] = field(default_factory=list)
33 gateway_open_ports: list[int] = field(default_factory=list)
34 duration_secs: float = 0.0
35 error: str = ""
36
37 @property
38 def client_count(self) -> int:
39 return len(self.clients)
40
41 @property
42 def summary(self) -> str:
43 if self.error:
44 return f"Failed: {self.error}"
45 parts = []
46 if self.assigned_ip:
47 parts.append(f"IP: {self.assigned_ip}")
48 if self.gateway_ip:
49 parts.append(f"GW: {self.gateway_ip}")
50 if self.clients:
51 parts.append(f"Clients: {self.client_count}")
52 if self.gateway_open_ports:
53 parts.append(f"Ports: {','.join(str(p) for p in self.gateway_open_ports)}")
54 return " | ".join(parts) if parts else "No data collected"
55
56
57 @dataclass
58 class AccessPoint:
59 """Represents a discovered wireless access point."""
60
61 bssid: str
62 ssid: str # empty string for hidden networks
63 channel: int # primary channel
64 rssi: int | None # dBm (e.g., -42)
65 encryption: str # OPN, WEP, WPA, WPA2, WPA3, WPA2/WPA3
66 cipher: str # CCMP, TKIP, CCMP/TKIP, --
67 auth: str # PSK, SAE, MGT, PSK/SAE, --
68 band: str # "2.4GHz" or "5GHz"
69 wifi_standard: str # "b, g, n" or "a, n, ac, ax" (comma-separated, all supported)
70 width: int = 20 # Channel width: 20, 40, 80, 160 MHz
71 channel_desc: str = "" # Bonded channels: "36+40+44+48"
72 vendor: str = "" # OUI vendor name
73 wps: str = "" # WPS version: "1.0", "2.0", or ""
74 wep: str = "" # WEP security detail (auth-cipher) or ""
75 wpa: str = "" # WPA security detail (auth-cipher) or ""
76 wpa2: str = "" # WPA2 security detail (auth-cipher) or ""
77 wpa3: str = "" # WPA3 security detail (auth-cipher) or ""
78 first_seen: datetime = field(default_factory=datetime.now)
79 last_seen: datetime = field(default_factory=datetime.now)
80 beacon_count: int = 0
81 frequency: int = 0 # MHz
82 active_scan_result: ActiveScanResult | None = None
83
84 @property
85 def is_hidden(self) -> bool:
86 return len(self.ssid.strip("\x00").strip()) == 0
87
88 @property
89 def display_ssid(self) -> str:
90 return "[Hidden]" if self.is_hidden else self.ssid
91
92 @property
93 def rssi_display(self) -> str:
94 return str(self.rssi) if self.rssi is not None else "N/A"
95
96 @property
97 def standard_display(self) -> str:
98 return self.wifi_standard if self.wifi_standard else "--"
99
100 @property
101 def channel_display(self) -> str:
102 """Show bonded channels if available, otherwise primary channel."""
103 return self.channel_desc if self.channel_desc else str(self.channel)