moriarty-project 0.1.25__py3-none-any.whl → 0.1.26__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.
@@ -2,31 +2,19 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  import asyncio
5
- import contextlib
6
5
  import json
7
6
  import re
8
- import socket
9
7
  import ssl
8
+ import contextlib
10
9
  from dataclasses import dataclass, field
11
- from datetime import datetime, timedelta
10
+ from datetime import datetime
11
+ from typing import Dict, List, Optional, Any, Set
12
12
  from pathlib import Path
13
- from typing import Dict, List, Optional, Tuple, Any, Set, Union
14
- from urllib.parse import urlparse
15
- import aiofiles
16
- import yaml
17
- import requests
18
- from packaging import version
19
-
20
- # Importa a classe ServiceInfo para uso no código
21
-
22
- import aiohttp
23
- import dns.resolver
24
13
  import dns.asyncresolver
25
- import OpenSSL.crypto
26
14
  import structlog
27
15
  from rich.console import Console
28
- from rich.json import JSON
29
- from rich.table import Table
16
+ from rich.progress import track
17
+ from rich.table import Table, box
30
18
 
31
19
  logger = structlog.get_logger(__name__)
32
20
  console = Console()
@@ -182,7 +170,7 @@ class ServiceInfo:
182
170
  ssl: bool = False
183
171
  ssl_info: Optional[Dict[str, Any]] = None
184
172
  banner: Optional[str] = None
185
- vulns: List[Dict[str, str]] = field(default_factory=list)
173
+ vulns: List[str] = field(default_factory=list) # Alterado para List[str]
186
174
  cpe: Optional[str] = None
187
175
  extra: Dict[str, Any] = field(default_factory=dict)
188
176
  confidence: float = 0.0 # Nível de confiança na identificação (0.0 a 1.0)
@@ -206,7 +194,7 @@ class ServiceInfo:
206
194
  class PortScanResult:
207
195
  port: int
208
196
  protocol: str = "tcp"
209
- status: str = "open"
197
+ status: str = "closed"
210
198
  target: Optional[str] = None
211
199
  service: Optional[ServiceInfo] = None
212
200
  banner: Optional[str] = None
@@ -269,112 +257,197 @@ class PortScanner:
269
257
  # Ajusta timeout baseado no nível de stealth
270
258
  self.timeout = timeout * (1 + (self.stealth_level * 0.5))
271
259
 
272
- # Cache para serviços já identificados
273
- self.service_cache: Dict[int, ServiceInfo] = {}
274
-
275
260
  # Resolvedor DNS assíncrono
276
261
  self.resolver = dns.asyncresolver.Resolver()
277
262
  self.resolver.timeout = 2.0
278
263
  self.resolver.lifetime = 2.0
279
264
 
280
265
  async def scan(self) -> List[PortScanResult]:
281
- """Executa a varredura de portas."""
282
- console.print(f"[bold]Iniciando varredura em {self.target}[/bold]")
283
- console.print(f"Perfil: {self.profile}, Portas: {len(PROFILES[self.profile])}")
266
+ """Executa a varredura de portas de forma assíncrona."""
267
+ console.print(f"[bold]🔍 Iniciando varredura em {self.target}[/bold]")
268
+ ports = sorted(PROFILES[self.profile])
269
+ total_ports = len(ports)
270
+ console.print(f"📊 Perfil: [bold]{self.profile}[/bold], Portas a verificar: [bold]{total_ports}[/bold]")
284
271
 
285
- ports = PROFILES[self.profile]
286
272
  if self.stealth_level > 0:
287
- # Aleatoriza a ordem das portas para maior discrição
288
- random.shuffle(ports)
289
-
290
- sem = asyncio.Semaphore(self.concurrency)
291
- results: List[PortScanResult] = []
273
+ console.print(f"🔒 Modo furtivo: nível {self.stealth_level}")
292
274
 
293
- async def worker(port: int):
294
- async with sem:
295
- result = await self._probe_port(port)
296
- if result:
297
- results.append(result)
298
- self._print_result(result)
275
+ results: List[PortScanResult] = []
276
+ sem = asyncio.Semaphore(self.concurrency)
299
277
 
300
- # Executa os workers em paralelo
301
- tasks = [worker(port) for port in ports]
302
- await asyncio.gather(*tasks)
278
+ # Barra de progresso
279
+ with console.status("[bold]Verificando portas...") as status:
280
+ async def scan_port(port: int) -> PortScanResult:
281
+ async with sem:
282
+ try:
283
+ return await self._probe_port(port)
284
+ except Exception as e:
285
+ logger.debug(f"Erro ao escanear porta {port}: {str(e)}")
286
+ return PortScanResult(port=port, status="error", target=self.target)
287
+
288
+ # Executa as tarefas em lote para evitar sobrecarga de memória
289
+ batch_size = min(100, max(10, self.concurrency * 2))
290
+ for i in range(0, len(ports), batch_size):
291
+ batch = ports[i:i + batch_size]
292
+ tasks = [scan_port(port) for port in batch]
293
+ batch_results = await asyncio.gather(*tasks)
294
+ results.extend(batch_results)
295
+
296
+ # Atualiza status
297
+ open_ports = len([r for r in results if r.status == "open"])
298
+ status.update(f"[bold]Verificando portas... {min(i + len(batch), total_ports)}/{total_ports} (abertas: {open_ports})")
303
299
 
304
300
  # Ordena os resultados por número de porta
305
301
  results.sort(key=lambda r: r.port)
306
302
 
307
- return results
308
-
309
- def _print_result(self, result: PortScanResult):
310
- """Exibe o resultado formatado no console."""
311
- port_info = f"[bold blue]{result.port:>5}/tcp[/bold blue]"
312
- status = "[green]open[/green]" if result.status == "open" else "[yellow]filtered[/yellow]"
303
+ # Filtra e conta portas por status
304
+ open_ports = [r for r in results if r.status == "open"]
305
+ closed_ports = [r for r in results if r.status == "closed"]
306
+ error_ports = [r for r in results if r.status == "error"]
313
307
 
314
- service_name = result.service.name if result.service else "unknown"
315
- service_info = f"[cyan]{service_name}[/cyan]"
308
+ # Exibe resultados
309
+ console.print("\n[bold]📊 Resultados da varredura:[/bold]")
316
310
 
317
- if result.service and result.service.version:
318
- service_info += f" [yellow]{result.service.version}[/yellow]"
311
+ # Tabela de portas abertas
312
+ if open_ports:
313
+ table = Table(show_header=True, header_style="bold magenta", box=box.ROUNDED)
314
+ table.add_column("Porta", style="red", width=10)
315
+ table.add_column("Status", style="green")
316
+ table.add_column("Serviço", style="blue")
317
+ table.add_column("Detalhes", style="yellow")
319
318
 
320
- if result.service and result.service.ssl:
321
- service_info += " [green]🔒[/green]"
319
+ for result in open_ports:
320
+ service = result.service.name if result.service else "desconhecido"
321
+ version = f" {result.service.version}" if result.service and result.service.version else ""
322
+ ssl = "🔒" if result.service and result.service.ssl else ""
323
+ vulns = f" [red]({len(result.service.vulns)} vulns)" if result.service and result.service.vulns else ""
324
+
325
+ table.add_row(
326
+ f"{result.port}/tcp",
327
+ "[green]ABERTA[/green]",
328
+ f"[red]{service}{version}[/red] {ssl}",
329
+ vulns
330
+ )
331
+
332
+ # Adiciona banner se disponível
333
+ if result.banner:
334
+ banner = result.banner.split('\n')[0][:80] # Pega apenas a primeira linha e limita o tamanho
335
+ table.add_row("", "", f"[dim]↳ {banner}...[/dim]", "")
322
336
 
323
- if result.service and result.service.vulns:
324
- vuln_count = len(result.service.vulns)
325
- service_info += f" [red]({vuln_count} vulns)[/red]"
337
+ console.print("\n[bold]🚪 Portas abertas:[/bold]")
338
+ console.print(table)
326
339
 
327
- console.print(f"{port_info} {status} {service_info}")
340
+ # Resumo
341
+ console.print("\n[bold]📋 Resumo:[/bold]")
342
+ console.print(f" • [green]Portas abertas:[/green] {len(open_ports)}")
343
+ console.print(f" • [red]Portas fechadas:[/red] {len(closed_ports)}")
344
+ if error_ports:
345
+ console.print(f" • [yellow]Erros:[/yellow] {len(error_ports)} porta(s) com erro")
328
346
 
329
- if result.banner:
330
- console.print(f" [dim]Banner: {result.banner[:100]}{'...' if len(result.banner) > 100 else ''}[/dim]")
347
+ # Lista de portas abertas resumida
348
+ if open_ports:
349
+ open_ports_str = ", ".join(str(r.port) for r in open_ports)
350
+ if len(open_ports_str) > 80:
351
+ open_ports_str = open_ports_str[:77] + "..."
352
+ console.print(f"\n🔍 Portas abertas: {open_ports_str}")
353
+
354
+ return results
331
355
 
332
- async def _probe_port(self, port: int) -> Optional[PortScanResult]:
356
+ def _print_result(self, result: PortScanResult):
357
+ """Método mantido para compatibilidade, mas não é mais usado internamente."""
358
+ pass
359
+ async def _probe_port(self, port: int) -> PortScanResult:
333
360
  """Verifica se uma porta está aberta e coleta informações do serviço."""
361
+ result = PortScanResult(port=port, status="closed", target=self.target)
362
+
334
363
  # Atraso aleatório para evitar detecção
335
364
  if self.stealth_level > 0:
365
+ import random
336
366
  await asyncio.sleep(random.uniform(0.01, 0.5) * self.stealth_level)
337
367
 
338
- # Tenta conexão TCP
368
+ # Portas que devem ser verificadas com TLS primeiro
369
+ ssl_ports = {
370
+ # HTTPS
371
+ 443, # HTTPS padrão
372
+ 8443, # HTTPS alternativo
373
+ 10443, # HTTPS alternativo
374
+ 4443, # HTTPS alternativo
375
+ 1443, # HTTPS alternativo
376
+ 9443, # Nginx Admin / HTTPS alternativo
377
+ # Email seguro
378
+ 465, # SMTPS
379
+ 993, # IMAPS
380
+ 995, # POP3S
381
+ # Outros serviços seguros
382
+ 636, # LDAPS
383
+ 853, # DNS sobre TLS
384
+ 989, # FTPS Data
385
+ 990, # FTPS Control
386
+ 992, # Telnet sobre TLS
387
+ 994, # IRCS
388
+ # Adicionais comuns
389
+ 5061, # SIP-TLS
390
+ 5062, # SIP-TLS
391
+ 5989, # CIM XML sobre HTTPS
392
+ 832, # NETCONF sobre TLS
393
+ 6514, # Syslog sobre TLS
394
+ 5684, # CoAP sobre DTLS
395
+ 8883, # MQTT sobre SSL
396
+ 8884, # MQTT sobre WebSocket
397
+ 8888, # HTTP alternativo com SSL
398
+ 10000, # Webmin
399
+ 10001, # Webmin alternativo
400
+ 20000 # Usermin (Webmin para usuários)
401
+ }
402
+
403
+ # Portas que devem tentar obter banner via TLS
404
+ http_ports_with_tls = {443, 8443, 10443, 4443, 9443, 10000, 10001, 20000}
405
+
406
+
339
407
  try:
340
- # Usa um timeout menor para a conexão inicial
341
- conn_timeout = min(1.0, self.timeout)
408
+ # Para portas SSL conhecidas, tenta TLS primeiro
409
+ if port in ssl_ports:
410
+ ssl_info = await self._check_ssl(port)
411
+ if ssl_info:
412
+ result.status = "open"
413
+ service_name = self._identify_ssl_service(port, ssl_info)
414
+ result.service = ServiceInfo(name=service_name, ssl=True, ssl_info=ssl_info)
415
+
416
+ # Tenta obter banner apenas para portas HTTP/HTTPS conhecidas
417
+ if port in http_ports_with_tls:
418
+ try:
419
+ banner = await self._get_ssl_banner(port)
420
+ if banner:
421
+ result.banner = banner
422
+ # Atualiza informações do serviço com base no banner
423
+ service_info = await self._identify_service(port, banner)
424
+ if service_info:
425
+ result.service.name = service_info.name
426
+ result.service.version = service_info.version
427
+ result.service.cpe = service_info.cpe
428
+ except Exception as e:
429
+ logger.debug(f"Erro ao obter banner SSL da porta {port}: {str(e)}")
430
+
431
+ return result
432
+
433
+
434
+ # Se não for porta SSL ou falhar, tenta conexão TCP normal
342
435
  reader, writer = await asyncio.wait_for(
343
436
  asyncio.open_connection(self.target, port),
344
- timeout=conn_timeout
437
+ timeout=self.timeout
345
438
  )
346
439
 
347
- # Se chegou aqui, a porta está aberta
348
- result = PortScanResult(port=port, status="open", target=self.target)
349
-
350
- # Tenta obter o banner do serviço
440
+ # Se chegou aqui, a conexão TCP foi estabelecida, mas ainda não sabemos se a porta está realmente aberta
441
+ # Vamos tentar ler o banner para confirmar
351
442
  try:
352
- # Configura timeout para leitura
353
- read_timeout = max(1.0, self.timeout - 1.0)
443
+ # o banner inicial (até 1024 bytes)
444
+ banner_bytes = await asyncio.wait_for(
445
+ reader.read(1024),
446
+ timeout=self.timeout
447
+ )
354
448
 
355
- # Envia uma requisição específica baseada no serviço comum da porta
356
- banner_bytes = b""
357
- if port in [80, 8080, 8000, 8008, 8081, 8443, 443]:
358
- # HTTP/HTTPS - envia um HEAD / HTTP/1.0
359
- writer.write(b"HEAD / HTTP/1.0\r\nHost: " + self.target.encode() + b"\r\n\r\n")
360
- elif port == 21:
361
- # FTP - envia um comando USER anonymous
362
- writer.write(b"USER anonymous\r\n")
363
- elif port == 22:
364
- # SSH - apenas lê a versão do servidor
365
- pass
366
- elif port == 25 or port == 587 or port == 465:
367
- # SMTP - envia EHLO
368
- writer.write(b"EHLO moriarty-scanner\r\n")
369
- elif port == 53:
370
- # DNS - envia uma query DNS padrão
371
- query = b'\x00\x01\x01\x00\x00\x01\x00\x00\x00\x00\x00\x01\x07version\x04bind\x00\x00\x10\x00\x03'
372
- writer.write(query)
373
-
374
- await writer.drain()
375
-
376
- # Lê até 1024 bytes
377
- banner_bytes = await asyncio.wait_for(reader.read(1024), timeout=read_timeout)
449
+ # Se chegou aqui sem exceção, a porta está realmente aberta
450
+ result.status = "open"
378
451
 
379
452
  if banner_bytes:
380
453
  # Tenta decodificar como texto
@@ -382,70 +455,74 @@ class PortScanner:
382
455
  banner = banner_bytes.decode('utf-8', errors='replace').strip()
383
456
  result.banner = banner
384
457
 
385
- # Tenta identificar o serviço pelo banner
458
+ # Identifica o serviço baseado no banner
386
459
  service_info = await self._identify_service(port, banner)
387
460
  if service_info:
388
461
  result.service = service_info
462
+
389
463
  except UnicodeDecodeError:
390
- # Se não for texto, mostra como hexdump
391
- result.banner = banner_bytes.hex(' ', 1)
392
- else:
393
- # Se não recebeu banner, verifica se a porta é conhecida
394
- if port in SERVICE_MAP:
395
- result.service = ServiceInfo(name=SERVICE_MAP[port])
396
- else:
397
- # Se não recebeu resposta, marca como filtrada
398
- result.status = "filtered"
399
- return result
400
- except (asyncio.TimeoutError, ConnectionResetError, OSError):
401
- # Ignora erros de leitura do banner
402
- pass
403
-
404
- # Verifica se é um serviço SSL/TLS
405
- if port in [443, 465, 636, 993, 995, 8443] or (result.service and result.service.ssl):
406
- ssl_info = await self._check_ssl(port)
407
- if ssl_info:
408
- if not result.service:
409
- result.service = ServiceInfo(name="ssl")
410
- result.service.ssl = True
411
- result.service.ssl_info = ssl_info
412
-
413
- # Tenta identificar o serviço SSL
414
- if not result.service.name or result.service.name == "ssl":
415
- service_name = self._identify_ssl_service(port, ssl_info)
416
- result.service.name = service_name
417
-
418
- # Se não identificou o serviço, tenta pelo número da porta
419
- if not result.service and port in SERVICE_MAP:
420
- result.service = ServiceInfo(name=SERVICE_MAP[port])
421
-
422
- # Se ainda não identificou o serviço e não recebeu banner, marca como filtrada
423
- if not result.service and not result.banner:
424
- result.status = "filtered"
425
- return result
464
+ # Se não for texto, exibe como hex
465
+ result.banner = f"[binary data] {banner_bytes.hex()[:100]}..."
426
466
 
427
- # Verifica vulnerabilidades conhecidas para portas abertas
428
- if result.status == "open" and self.check_vulns and result.service:
429
- result.service.vulns = self._check_known_vulns(port, result.service.name)
430
-
431
- return result
467
+ # Se a porta está aberta, verifica SSL/TLS em portas comuns (apenas se ainda não verificou)
468
+ if result.status == "open" and port in ssl_ports and (result.service is None or not result.service.ssl):
469
+ try:
470
+ ssl_info = await self._check_ssl(port)
471
+ if ssl_info:
472
+ if result.service is None:
473
+ result.service = ServiceInfo(name=SERVICE_MAP.get(port, "unknown"))
474
+ result.service.ssl = True
475
+ result.service.ssl_info = ssl_info
476
+ # Atualiza o nome do serviço com base nas informações SSL
477
+ ssl_service = self._identify_ssl_service(port, ssl_info)
478
+ if ssl_service:
479
+ result.service.name = ssl_service
480
+ except Exception as e:
481
+ logger.debug(f"Erro ao verificar SSL na porta {port}: {str(e)}")
482
+ # Se falhar ao verificar SSL, mantém a porta como aberta mas sem informações SSL
432
483
 
484
+ except (asyncio.TimeoutError, ConnectionResetError, OSError) as e:
485
+ logger.debug(f"Erro ao ler banner da porta {port}: {str(e)}")
486
+
487
+ finally:
488
+ # Fecha a conexão
489
+ writer.close()
490
+ try:
491
+ await writer.wait_closed()
492
+ except Exception as e:
493
+ logger.debug(f"Erro ao fechar conexão: {str(e)}")
494
+
433
495
  except (asyncio.TimeoutError, ConnectionRefusedError, OSError):
434
496
  # Porta fechada ou inacessível
435
- return None
497
+ result.status = "closed"
436
498
 
437
499
  except Exception as e:
438
- logger.error(f"Erro ao verificar porta {port}: {str(e)}")
439
- return None
500
+ logger.error(f"Erro inesperado ao verificar porta {port}: {str(e)}")
501
+ result.status = "error"
440
502
 
441
- finally:
442
- # Fecha a conexão se ainda estiver aberta
443
- if 'writer' in locals():
444
- writer.close()
445
- try:
446
- await writer.wait_closed()
447
- except:
448
- pass
503
+ # Se a porta está aberta, tenta identificar o serviço se ainda não identificado
504
+ if result.status == "open":
505
+ try:
506
+ # Se tem serviço SSL mas não foi identificado corretamente
507
+ if result.service and (not result.service.name or result.service.name == "ssl"):
508
+ if 'ssl_info' in locals() and ssl_info:
509
+ service_name = self._identify_ssl_service(port, ssl_info)
510
+ if service_name:
511
+ result.service.name = service_name
512
+
513
+ # Se ainda não identificou o serviço, tenta pelo número da porta
514
+ if not result.service and port in SERVICE_MAP:
515
+ result.service = ServiceInfo(name=SERVICE_MAP[port])
516
+
517
+ # Verifica vulnerabilidades conhecidas para portas abertas
518
+ if self.check_vulns and result.service:
519
+ result.service.vulns = self._check_known_vulns(port, result.service.name)
520
+ except Exception as e:
521
+ logger.error(f"Erro ao processar informações da porta {port}: {str(e)}")
522
+ # Em caso de erro, mantém a porta como aberta mas sem informações adicionais
523
+
524
+
525
+ return result
449
526
 
450
527
  async def _identify_service(self, port: int, banner: str) -> Optional[ServiceInfo]:
451
528
  """Tenta identificar o serviço rodando na porta com base no banner."""
@@ -834,6 +911,27 @@ class PortScanner:
834
911
  await writer.wait_closed()
835
912
  except:
836
913
  pass
914
+
915
+ async def _get_ssl_banner(self, port: int) -> Optional[str]:
916
+ try:
917
+ ssl_context = ssl.create_default_context()
918
+ ssl_context.check_hostname = False
919
+ ssl_context.verify_mode = ssl.CERT_NONE
920
+ reader, writer = await asyncio.wait_for(
921
+ asyncio.open_connection(self.target, port, ssl=ssl_context, server_hostname=self.target),
922
+ timeout=self.timeout
923
+ )
924
+ req = f"HEAD / HTTP/1.0\r\nHost: {self.target}\r\nUser-Agent: moriarty/1.0\r\nConnection: close\r\n\r\n"
925
+ writer.write(req.encode("ascii"))
926
+ await writer.drain()
927
+ data = await asyncio.wait_for(reader.read(2048), timeout=self.timeout)
928
+ writer.close()
929
+ with contextlib.suppress(Exception):
930
+ await writer.wait_closed()
931
+ return data.decode("utf-8", errors="replace").strip()
932
+ except Exception:
933
+ return None
934
+
837
935
 
838
936
  def _check_known_vulns(self, port: int, service_name: str) -> List[str]:
839
937
  """Verifica vulnerabilidades conhecidas para o serviço na porta."""
@@ -844,29 +942,35 @@ class PortScanner:
844
942
  if service.lower() in service_name.lower():
845
943
  vulns.extend(cves)
846
944
 
847
- # Verifica vulnerabilidades específicas da porta
945
+ # Verifica vulnerabilidades específicas da porta (apenas como informação, não como confirmação)
848
946
  if port == 22: # SSH
849
- vulns.extend(["CVE-2016-0777", "CVE-2016-0778", "CVE-2018-15473"])
947
+ vulns.extend(["CVE-2016-0777 (verificar versão)", "CVE-2016-0778 (verificar versão)", "CVE-2018-15473 (verificar versão)"])
850
948
  elif port == 445: # SMB
851
- vulns.extend(["EternalBlue", "SMBGhost", "EternalRomance", "SambaCry"])
949
+ vulns.extend(["Possível vulnerabilidade SMB - requer verificação de versão"])
852
950
  elif port == 3389: # RDP
853
- vulns.extend(["BlueKeep", "CVE-2019-0708", "CVE-2019-1181", "CVE-2019-1182"])
951
+ vulns.extend(["Possível vulnerabilidade RDP - requer verificação de versão"])
854
952
  elif port == 27017: # MongoDB
855
- vulns.extend(["Unauthenticated Access", "CVE-2016-6494"])
953
+ vulns.extend(["Acesso não autenticado (se sem senha)"])
856
954
  elif port == 9200: # Elasticsearch
857
- vulns.extend(["CVE-2015-1427", "CVE-2015-3337", "CVE-2015-5531"])
955
+ vulns.extend(["Possível exposição indevida - verificar configurações"])
858
956
  elif port == 11211: # Memcached
859
- vulns.extend(["DRDoS Amplification", "CVE-2016-8704", "CVE-2016-8705"])
957
+ vulns.extend(["Possível amplificação DRDoS - verificar configuração"])
860
958
  elif port == 2375: # Docker
861
- vulns.extend(["CVE-2019-5736", "CVE-2019-13139", "CVE-2019-14271"])
959
+ vulns.extend(["API Docker exposta - verificar autenticação"])
862
960
  elif port == 10250: # Kubelet
863
- vulns.extend(["CVE-2018-1002105", "CVE-2019-11253", "CVE-2019-11255"])
961
+ vulns.extend(["Kubelet exposto - verificar autenticação"])
864
962
 
865
963
  return list(set(vulns)) # Remove duplicatas
866
964
 
867
965
 
868
- def format_scan_results(results: List[PortScanResult], output_format: str = "text") -> str:
869
- """Formata os resultados da varredura no formato solicitado."""
966
+ def format_scan_results(results: List[PortScanResult], output_format: str = "text", total_ports: Optional[int] = None) -> str:
967
+ """Formata os resultados da varredura no formato solicitado.
968
+
969
+ Args:
970
+ results: Lista de resultados da varredura
971
+ output_format: Formato de saída ('text' ou 'json')
972
+ total_ports: Número total de portas verificadas (opcional)
973
+ """
870
974
  if output_format.lower() == "json":
871
975
  return json.dumps([r.to_dict() for r in results], indent=2)
872
976
 
@@ -875,7 +979,7 @@ def format_scan_results(results: List[PortScanResult], output_format: str = "tex
875
979
  output.append("")
876
980
  output.append(f"[bold]Resultado da varredura de portas[/bold]")
877
981
  output.append(f"Alvo: {results[0].target if results else 'N/A'}")
878
- output.append(f"Portas verificadas: {len(results)}")
982
+ output.append(f"Portas verificadas: {total_ports if total_ports is not None else len([r for r in results if r.status != 'error'])}")
879
983
  output.append("-" * 80)
880
984
 
881
985
  # Cabeçalho da tabela
@@ -889,9 +993,13 @@ def format_scan_results(results: List[PortScanResult], output_format: str = "tex
889
993
  if result.status == "open":
890
994
  port = f"[green]{result.port}[/green]"
891
995
  status = "[green]ABERTA[/green]"
996
+ elif result.status == "closed":
997
+ port = f"[red]{result.port}[/red]"
998
+ status = "[red]FECHADA[/red]"
892
999
  else:
893
1000
  port = f"[yellow]{result.port}[/yellow]"
894
- status = "[yellow]FILTRADA[/yellow]"
1001
+ status = "[yellow]ERRO[/yellow]"
1002
+
895
1003
 
896
1004
  service = result.service.name if result.service else "desconhecido"
897
1005
  version = f" {result.service.version}" if result.service and result.service.version else ""