moriarty-project 0.1.24__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,25 +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
10
  from datetime import datetime
12
- from typing import Dict, List, Optional, Tuple, Any
13
-
14
- # Importa a classe ServiceInfo para uso no código
15
-
16
- import aiohttp
17
- import dns.resolver
11
+ from typing import Dict, List, Optional, Any, Set
12
+ from pathlib import Path
18
13
  import dns.asyncresolver
19
- import OpenSSL.crypto
20
14
  import structlog
21
15
  from rich.console import Console
22
- from rich.json import JSON
23
- from rich.table import Table
16
+ from rich.progress import track
17
+ from rich.table import Table, box
24
18
 
25
19
  logger = structlog.get_logger(__name__)
26
20
  console = Console()
@@ -37,6 +31,34 @@ PROFILES = {
37
31
  "all": list(range(1, 65536)),
38
32
  }
39
33
 
34
+ # URL base para atualizações de serviços e vulnerabilidades
35
+ SERVICES_DB_URL = "https://raw.githubusercontent.com/nmap/nmap/master/nmap-services"
36
+ VULN_DB_URL = "https://cve.mitre.org/data/downloads/allitems.csv"
37
+
38
+ # Diretório para armazenar dados locais
39
+ DATA_DIR = Path.home() / ".moriarty" / "data"
40
+ SERVICES_DB = DATA_DIR / "services.yml"
41
+ VULN_DB = DATA_DIR / "vulnerabilities.yml"
42
+
43
+ # Garante que o diretório de dados existe
44
+ DATA_DIR.mkdir(parents=True, exist_ok=True)
45
+
46
+ # Estrutura para armazenar assinaturas de serviços
47
+ @dataclass
48
+ class ServiceSignature:
49
+ name: str
50
+ port: int
51
+ protocol: str = "tcp"
52
+ banner_patterns: List[str] = field(default_factory=list)
53
+ ssl_ports: Set[int] = field(default_factory=set)
54
+ version_pattern: Optional[str] = None
55
+ cpe: Optional[str] = None
56
+ vulns: List[Dict[str, str]] = field(default_factory=list)
57
+ last_updated: Optional[datetime] = None
58
+
59
+ # Dicionário para armazenar assinaturas de serviços
60
+ SERVICE_SIGNATURES: Dict[str, ServiceSignature] = {}
61
+
40
62
  # Mapeamento de portas para serviços comuns
41
63
  SERVICE_MAP = {
42
64
  21: "FTP",
@@ -117,9 +139,10 @@ SERVICE_MAP = {
117
139
  27018: "MongoDB",
118
140
  27019: "MongoDB",
119
141
  28017: "MongoDB",
120
- 32608: "Kubernetes",
142
+ 32608: "Kubernetes"
121
143
  }
122
144
 
145
+
123
146
  # Vulnerabilidades comuns por serviço
124
147
  VULNERABILITIES = {
125
148
  "SSH": ["CVE-2016-0777", "CVE-2016-0778", "CVE-2018-15473"],
@@ -147,15 +170,31 @@ class ServiceInfo:
147
170
  ssl: bool = False
148
171
  ssl_info: Optional[Dict[str, Any]] = None
149
172
  banner: Optional[str] = None
150
- vulns: List[str] = field(default_factory=list)
173
+ vulns: List[str] = field(default_factory=list) # Alterado para List[str]
151
174
  cpe: Optional[str] = None
152
175
  extra: Dict[str, Any] = field(default_factory=dict)
176
+ confidence: float = 0.0 # Nível de confiança na identificação (0.0 a 1.0)
177
+ last_checked: Optional[datetime] = None
178
+
179
+ def to_dict(self) -> Dict[str, Any]:
180
+ """Converte o objeto para dicionário."""
181
+ return {
182
+ "name": self.name,
183
+ "version": self.version,
184
+ "ssl": self.ssl,
185
+ "ssl_info": self.ssl_info,
186
+ "banner": self.banner,
187
+ "vulns": self.vulns,
188
+ "cpe": self.cpe,
189
+ "confidence": self.confidence,
190
+ "last_checked": self.last_checked.isoformat() if self.last_checked else None
191
+ }
153
192
 
154
193
  @dataclass
155
194
  class PortScanResult:
156
195
  port: int
157
196
  protocol: str = "tcp"
158
- status: str = "open"
197
+ status: str = "closed"
159
198
  target: Optional[str] = None
160
199
  service: Optional[ServiceInfo] = None
161
200
  banner: Optional[str] = None
@@ -218,152 +257,272 @@ class PortScanner:
218
257
  # Ajusta timeout baseado no nível de stealth
219
258
  self.timeout = timeout * (1 + (self.stealth_level * 0.5))
220
259
 
221
- # Cache para serviços já identificados
222
- self.service_cache: Dict[int, ServiceInfo] = {}
223
-
224
260
  # Resolvedor DNS assíncrono
225
261
  self.resolver = dns.asyncresolver.Resolver()
226
262
  self.resolver.timeout = 2.0
227
263
  self.resolver.lifetime = 2.0
228
264
 
229
265
  async def scan(self) -> List[PortScanResult]:
230
- """Executa a varredura de portas."""
231
- console.print(f"[bold]Iniciando varredura em {self.target}[/bold]")
232
- 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]")
233
271
 
234
- ports = PROFILES[self.profile]
235
272
  if self.stealth_level > 0:
236
- # Aleatoriza a ordem das portas para maior discrição
237
- random.shuffle(ports)
238
-
239
- sem = asyncio.Semaphore(self.concurrency)
240
- results: List[PortScanResult] = []
273
+ console.print(f"🔒 Modo furtivo: nível {self.stealth_level}")
241
274
 
242
- async def worker(port: int):
243
- async with sem:
244
- result = await self._probe_port(port)
245
- if result:
246
- results.append(result)
247
- self._print_result(result)
275
+ results: List[PortScanResult] = []
276
+ sem = asyncio.Semaphore(self.concurrency)
248
277
 
249
- # Executa os workers em paralelo
250
- tasks = [worker(port) for port in ports]
251
- 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})")
252
299
 
253
300
  # Ordena os resultados por número de porta
254
301
  results.sort(key=lambda r: r.port)
255
302
 
256
- return results
257
-
258
- def _print_result(self, result: PortScanResult):
259
- """Exibe o resultado formatado no console."""
260
- port_info = f"[bold blue]{result.port:>5}/tcp[/bold blue]"
261
- 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"]
262
307
 
263
- service_name = result.service.name if result.service else "unknown"
264
- service_info = f"[cyan]{service_name}[/cyan]"
308
+ # Exibe resultados
309
+ console.print("\n[bold]📊 Resultados da varredura:[/bold]")
265
310
 
266
- if result.service and result.service.version:
267
- 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")
268
318
 
269
- if result.service and result.service.ssl:
270
- 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]", "")
271
336
 
272
- if result.service and result.service.vulns:
273
- vuln_count = len(result.service.vulns)
274
- service_info += f" [red]({vuln_count} vulns)[/red]"
337
+ console.print("\n[bold]🚪 Portas abertas:[/bold]")
338
+ console.print(table)
339
+
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")
275
346
 
276
- console.print(f"{port_info} {status} {service_info}")
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}")
277
353
 
278
- if result.banner:
279
- console.print(f" [dim]Banner: {result.banner[:100]}{'...' if len(result.banner) > 100 else ''}[/dim]")
354
+ return results
280
355
 
281
- 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:
282
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
+
283
363
  # Atraso aleatório para evitar detecção
284
364
  if self.stealth_level > 0:
365
+ import random
285
366
  await asyncio.sleep(random.uniform(0.01, 0.5) * self.stealth_level)
286
367
 
287
- # 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
+
288
407
  try:
289
- # Usa um timeout menor para a conexão inicial
290
- 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
291
435
  reader, writer = await asyncio.wait_for(
292
436
  asyncio.open_connection(self.target, port),
293
- timeout=conn_timeout
437
+ timeout=self.timeout
294
438
  )
295
439
 
296
- # Se chegou aqui, a porta está aberta
297
- result = PortScanResult(port=port, status="open", target=self.target)
298
-
299
- # 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
300
442
  try:
301
- # Configura timeout para leitura
302
- read_timeout = max(0.5, self.timeout - 0.5)
443
+ # o banner inicial (até 1024 bytes)
444
+ banner_bytes = await asyncio.wait_for(
445
+ reader.read(1024),
446
+ timeout=self.timeout
447
+ )
303
448
 
304
- # o banner (se houver)
305
- writer.write(b"\r\n\r\n")
306
- await writer.drain()
449
+ # Se chegou aqui sem exceção, a porta está realmente aberta
450
+ result.status = "open"
307
451
 
308
- # Lê até 1024 bytes
309
- banner_bytes = await asyncio.wait_for(reader.read(1024), timeout=read_timeout)
310
452
  if banner_bytes:
311
453
  # Tenta decodificar como texto
312
454
  try:
313
455
  banner = banner_bytes.decode('utf-8', errors='replace').strip()
314
456
  result.banner = banner
315
457
 
316
- # Tenta identificar o serviço pelo banner
458
+ # Identifica o serviço baseado no banner
317
459
  service_info = await self._identify_service(port, banner)
318
460
  if service_info:
319
461
  result.service = service_info
462
+
320
463
  except UnicodeDecodeError:
321
- # Se não for texto, mostra como hexdump
322
- result.banner = banner_bytes.hex(' ', 1)
323
- except (asyncio.TimeoutError, ConnectionResetError, OSError):
324
- # Ignora erros de leitura do banner
325
- pass
326
-
327
- # Verifica se é um serviço SSL/TLS
328
- if port in [443, 465, 636, 993, 995, 8443] or (result.service and result.service.ssl):
329
- ssl_info = await self._check_ssl(port)
330
- if ssl_info:
331
- if not result.service:
332
- result.service = ServiceInfo(name="ssl")
333
- result.service.ssl = True
334
- result.service.ssl_info = ssl_info
335
-
336
- # Tenta identificar o serviço SSL
337
- if not result.service.name or result.service.name == "ssl":
338
- service_name = self._identify_ssl_service(port, ssl_info)
339
- result.service.name = service_name
340
-
341
- # Se não identificou o serviço, tenta pelo número da porta
342
- if not result.service and port in SERVICE_MAP:
343
- result.service = ServiceInfo(name=SERVICE_MAP[port])
464
+ # Se não for texto, exibe como hex
465
+ result.banner = f"[binary data] {banner_bytes.hex()[:100]}..."
344
466
 
345
- # Verifica vulnerabilidades conhecidas
346
- if self.check_vulns:
347
- result.service.vulns = self._check_known_vulns(port, result.service.name)
348
-
349
- 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
350
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
+
351
495
  except (asyncio.TimeoutError, ConnectionRefusedError, OSError):
352
496
  # Porta fechada ou inacessível
353
- return None
497
+ result.status = "closed"
354
498
 
355
499
  except Exception as e:
356
- logger.error(f"Erro ao verificar porta {port}: {str(e)}")
357
- return None
500
+ logger.error(f"Erro inesperado ao verificar porta {port}: {str(e)}")
501
+ result.status = "error"
358
502
 
359
- finally:
360
- # Fecha a conexão se ainda estiver aberta
361
- if 'writer' in locals():
362
- writer.close()
363
- try:
364
- await writer.wait_closed()
365
- except:
366
- 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
367
526
 
368
527
  async def _identify_service(self, port: int, banner: str) -> Optional[ServiceInfo]:
369
528
  """Tenta identificar o serviço rodando na porta com base no banner."""
@@ -752,6 +911,27 @@ class PortScanner:
752
911
  await writer.wait_closed()
753
912
  except:
754
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
+
755
935
 
756
936
  def _check_known_vulns(self, port: int, service_name: str) -> List[str]:
757
937
  """Verifica vulnerabilidades conhecidas para o serviço na porta."""
@@ -762,29 +942,35 @@ class PortScanner:
762
942
  if service.lower() in service_name.lower():
763
943
  vulns.extend(cves)
764
944
 
765
- # Verifica vulnerabilidades específicas da porta
945
+ # Verifica vulnerabilidades específicas da porta (apenas como informação, não como confirmação)
766
946
  if port == 22: # SSH
767
- 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)"])
768
948
  elif port == 445: # SMB
769
- vulns.extend(["EternalBlue", "SMBGhost", "EternalRomance", "SambaCry"])
949
+ vulns.extend(["Possível vulnerabilidade SMB - requer verificação de versão"])
770
950
  elif port == 3389: # RDP
771
- 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"])
772
952
  elif port == 27017: # MongoDB
773
- vulns.extend(["Unauthenticated Access", "CVE-2016-6494"])
953
+ vulns.extend(["Acesso não autenticado (se sem senha)"])
774
954
  elif port == 9200: # Elasticsearch
775
- vulns.extend(["CVE-2015-1427", "CVE-2015-3337", "CVE-2015-5531"])
955
+ vulns.extend(["Possível exposição indevida - verificar configurações"])
776
956
  elif port == 11211: # Memcached
777
- vulns.extend(["DRDoS Amplification", "CVE-2016-8704", "CVE-2016-8705"])
957
+ vulns.extend(["Possível amplificação DRDoS - verificar configuração"])
778
958
  elif port == 2375: # Docker
779
- vulns.extend(["CVE-2019-5736", "CVE-2019-13139", "CVE-2019-14271"])
959
+ vulns.extend(["API Docker exposta - verificar autenticação"])
780
960
  elif port == 10250: # Kubelet
781
- vulns.extend(["CVE-2018-1002105", "CVE-2019-11253", "CVE-2019-11255"])
961
+ vulns.extend(["Kubelet exposto - verificar autenticação"])
782
962
 
783
963
  return list(set(vulns)) # Remove duplicatas
784
964
 
785
965
 
786
- def format_scan_results(results: List[PortScanResult], output_format: str = "text") -> str:
787
- """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
+ """
788
974
  if output_format.lower() == "json":
789
975
  return json.dumps([r.to_dict() for r in results], indent=2)
790
976
 
@@ -793,7 +979,7 @@ def format_scan_results(results: List[PortScanResult], output_format: str = "tex
793
979
  output.append("")
794
980
  output.append(f"[bold]Resultado da varredura de portas[/bold]")
795
981
  output.append(f"Alvo: {results[0].target if results else 'N/A'}")
796
- 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'])}")
797
983
  output.append("-" * 80)
798
984
 
799
985
  # Cabeçalho da tabela
@@ -807,9 +993,13 @@ def format_scan_results(results: List[PortScanResult], output_format: str = "tex
807
993
  if result.status == "open":
808
994
  port = f"[green]{result.port}[/green]"
809
995
  status = "[green]ABERTA[/green]"
996
+ elif result.status == "closed":
997
+ port = f"[red]{result.port}[/red]"
998
+ status = "[red]FECHADA[/red]"
810
999
  else:
811
1000
  port = f"[yellow]{result.port}[/yellow]"
812
- status = "[yellow]FILTRADA[/yellow]"
1001
+ status = "[yellow]ERRO[/yellow]"
1002
+
813
1003
 
814
1004
  service = result.service.name if result.service else "desconhecido"
815
1005
  version = f" {result.service.version}" if result.service and result.service.version else ""