moriarty-project 0.1.7__py3-none-any.whl → 0.1.9__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.
@@ -1,129 +1,856 @@
1
- """Port scanning assíncrono com fingerprints básicos."""
1
+ """Port scanning avançado com detecção de serviços e vulnerabilidades."""
2
2
  from __future__ import annotations
3
3
 
4
4
  import asyncio
5
5
  import contextlib
6
- from dataclasses import dataclass
7
- from typing import Dict, List, Optional
8
-
9
- import random
6
+ import json
7
+ import re
8
+ import socket
9
+ import ssl
10
+ from dataclasses import dataclass, field
11
+ from datetime import datetime
12
+ from typing import Dict, List, Optional, Tuple, Any
10
13
 
14
+ import aiohttp
15
+ import dns.resolver
16
+ import OpenSSL.crypto
11
17
  import structlog
18
+ from rich.console import Console
19
+ from rich.json import JSON
20
+ from rich.table import Table
12
21
 
13
22
  logger = structlog.get_logger(__name__)
23
+ console = Console()
14
24
 
15
-
25
+ # Perfis de varredura
16
26
  PROFILES = {
17
- "quick": [22, 25, 53, 80, 110, 143, 443, 465, 587, 993, 995, 3306, 3389, 8080, 8443],
27
+ "quick": [21, 22, 23, 25, 53, 80, 110, 111, 135, 139, 143, 389, 443, 445,
28
+ 465, 587, 993, 995, 1433, 1521, 2049, 3306, 3389, 5432, 5900, 6379,
29
+ 8080, 8443, 9000, 10000, 27017],
30
+ "web": [80, 443, 8080, 8443, 8000, 8888, 10443, 4443],
31
+ "mail": [25, 110, 143, 465, 587, 993, 995],
32
+ "db": [1433, 1521, 27017, 27018, 27019, 28017, 3306, 5000, 5432, 5984, 6379, 8081],
18
33
  "full": list(range(1, 1025)),
19
- "extended": [
20
- 21,
21
- 22,
22
- 23,
23
- 25,
24
- 53,
25
- 80,
26
- 110,
27
- 111,
28
- 135,
29
- 139,
30
- 143,
31
- 161,
32
- 389,
33
- 443,
34
- 445,
35
- 465,
36
- 587,
37
- 631,
38
- 993,
39
- 995,
40
- 1023,
41
- 1433,
42
- 1521,
43
- 2049,
44
- 3128,
45
- 3306,
46
- 3389,
47
- 5432,
48
- 5900,
49
- 6379,
50
- 8080,
51
- 8443,
52
- 9000,
53
- ],
34
+ "all": list(range(1, 65536)),
35
+ }
36
+
37
+ # Mapeamento de portas para serviços comuns
38
+ SERVICE_MAP = {
39
+ 21: "FTP",
40
+ 22: "SSH",
41
+ 23: "Telnet",
42
+ 25: "SMTP",
43
+ 53: "DNS",
44
+ 80: "HTTP",
45
+ 110: "POP3",
46
+ 111: "RPCbind",
47
+ 135: "MSRPC",
48
+ 139: "NetBIOS",
49
+ 143: "IMAP",
50
+ 389: "LDAP",
51
+ 443: "HTTPS",
52
+ 445: "SMB",
53
+ 465: "SMTPS",
54
+ 500: "IKE",
55
+ 515: "LPD",
56
+ 554: "RTSP",
57
+ 587: "SMTP (Submission)",
58
+ 631: "IPP",
59
+ 636: "LDAPS",
60
+ 993: "IMAPS",
61
+ 995: "POP3S",
62
+ 1080: "SOCKS",
63
+ 1194: "OpenVPN",
64
+ 1433: "MSSQL",
65
+ 1521: "Oracle",
66
+ 2049: "NFS",
67
+ 2375: "Docker",
68
+ 2376: "Docker TLS",
69
+ 3000: "Node.js",
70
+ 3306: "MySQL",
71
+ 3389: "RDP",
72
+ 5000: "UPnP",
73
+ 5432: "PostgreSQL",
74
+ 5601: "Kibana",
75
+ 5672: "AMQP",
76
+ 5900: "VNC",
77
+ 5984: "CouchDB",
78
+ 6379: "Redis",
79
+ 8000: "HTTP-Alt",
80
+ 8008: "HTTP-Alt",
81
+ 8080: "HTTP-Proxy",
82
+ 8081: "HTTP-Alt",
83
+ 8088: "HTTP-Alt",
84
+ 8089: "Splunk",
85
+ 8090: "HTTP-Alt",
86
+ 8091: "Couchbase",
87
+ 8096: "Plex",
88
+ 8125: "StatsD",
89
+ 8140: "Puppet",
90
+ 8200: "Vault",
91
+ 8300: "Consul",
92
+ 8333: "Bitcoin",
93
+ 8443: "HTTPS-Alt",
94
+ 8500: "Consul",
95
+ 8545: "Ethereum",
96
+ 8765: "Grafana",
97
+ 8888: "Jupyter",
98
+ 9000: "SonarQube",
99
+ 9001: "Tor",
100
+ 9042: "Cassandra",
101
+ 9090: "Prometheus",
102
+ 9092: "Kafka",
103
+ 9100: "Node-Exporter",
104
+ 9200: "Elasticsearch",
105
+ 9300: "Elasticsearch",
106
+ 9418: "Git",
107
+ 9999: "JIRA",
108
+ 10000: "Webmin",
109
+ 10250: "Kubelet",
110
+ 11211: "Memcached",
111
+ 15672: "RabbitMQ",
112
+ 16379: "Redis",
113
+ 27017: "MongoDB",
114
+ 27018: "MongoDB",
115
+ 27019: "MongoDB",
116
+ 28017: "MongoDB",
117
+ 32608: "Kubernetes",
54
118
  }
55
119
 
120
+ # Vulnerabilidades comuns por serviço
121
+ VULNERABILITIES = {
122
+ "SSH": ["CVE-2016-0777", "CVE-2016-0778", "CVE-2018-15473"],
123
+ "SMB": ["EternalBlue", "SMBGhost", "EternalRomance", "SambaCry"],
124
+ "RDP": ["BlueKeep", "CVE-2019-0708", "CVE-2019-1181", "CVE-2019-1182"],
125
+ "Redis": ["Unauthenticated Access", "CVE-2015-4335", "CVE-2016-8339"],
126
+ "MongoDB": ["Unauthenticated Access", "CVE-2016-6494"],
127
+ "Elasticsearch": ["CVE-2015-1427", "CVE-2015-3337", "CVE-2015-5531"],
128
+ "Memcached": ["DRDoS Amplification", "CVE-2016-8704", "CVE-2016-8705"],
129
+ "Docker": ["CVE-2019-5736", "CVE-2019-13139", "CVE-2019-14271"],
130
+ "Kubernetes": ["CVE-2018-1002105", "CVE-2019-11253", "CVE-2019-11255"],
131
+ "VNC": ["CVE-2006-2369", "CVE-2015-5239", "CVE-2018-20019"],
132
+ "Jenkins": ["CVE-2017-1000353", "CVE-2018-1000861", "CVE-2019-1003000"],
133
+ "MySQL": ["CVE-2016-6662", "CVE-2016-6663", "CVE-2016-6664"],
134
+ "PostgreSQL": ["CVE-2019-9193", "CVE-2018-1058", "CVE-2016-5423"],
135
+ "Oracle": ["CVE-2012-1675", "CVE-2012-3137", "CVE-2018-3110"],
136
+ "MSSQL": ["CVE-2019-1068", "CVE-2018-8273", "CVE-2018-8271"],
137
+ }
138
+
139
+ @dataclass
140
+ class ServiceInfo:
141
+ """Informações detalhadas sobre um serviço."""
142
+ name: str
143
+ version: Optional[str] = None
144
+ ssl: bool = False
145
+ ssl_info: Optional[Dict[str, Any]] = None
146
+ banner: Optional[str] = None
147
+ vulns: List[str] = field(default_factory=list)
148
+ cpe: Optional[str] = None
149
+ extra: Dict[str, Any] = field(default_factory=dict)
56
150
 
57
151
  @dataclass
58
152
  class PortScanResult:
59
153
  port: int
60
- status: str
154
+ protocol: str = "tcp"
155
+ status: str = "open"
156
+ service: Optional[ServiceInfo] = None
61
157
  banner: Optional[str] = None
158
+ timestamp: str = field(default_factory=lambda: datetime.utcnow().isoformat())
159
+
160
+ def to_dict(self) -> Dict[str, Any]:
161
+ """Converte o resultado para dicionário."""
162
+ result = {
163
+ "port": self.port,
164
+ "protocol": self.protocol,
165
+ "status": self.status,
166
+ "banner": self.banner,
167
+ "timestamp": self.timestamp,
168
+ }
169
+
170
+ if self.service:
171
+ service_info = {
172
+ "name": self.service.name,
173
+ "version": self.service.version,
174
+ "ssl": self.service.ssl,
175
+ "vulnerabilities": self.service.vulns,
176
+ "cpe": self.service.cpe,
177
+ "extra": self.service.extra,
178
+ }
179
+ if self.service.ssl_info:
180
+ service_info["ssl_info"] = self.service.ssl_info
181
+
182
+ result["service"] = service_info
183
+
184
+ return result
62
185
 
186
+ def to_json(self) -> str:
187
+ """Retorna uma representação JSON do resultado."""
188
+ return json.dumps(self.to_dict(), indent=2)
63
189
 
64
190
  class PortScanner:
65
- """Execução assíncrona de port scanning com banners e fingerprint simples."""
191
+ """Execução assíncrona de port scanning com detecção avançada de serviços."""
66
192
 
67
193
  def __init__(
68
194
  self,
69
195
  target: str,
70
196
  profile: str = "quick",
71
197
  concurrency: int = 200,
72
- timeout: float = 1.5,
198
+ timeout: float = 2.0,
73
199
  stealth_level: int = 0,
200
+ resolve_services: bool = True,
201
+ check_vulns: bool = True,
74
202
  ):
75
203
  self.target = target
76
204
  self.profile = profile if profile in PROFILES else "quick"
77
- self.stealth_level = max(0, stealth_level)
78
- adjusted_concurrency = concurrency
79
- if self.stealth_level >= 3:
80
- adjusted_concurrency = min(concurrency, 80)
81
- elif self.stealth_level == 2:
82
- adjusted_concurrency = min(concurrency, 120)
83
- self.concurrency = max(10, adjusted_concurrency)
84
- self.timeout = timeout
205
+ self.stealth_level = max(0, min(stealth_level, 5))
206
+ self.resolve_services = resolve_services
207
+ self.check_vulns = check_vulns
208
+
209
+ # Ajusta concorrência baseado no nível de stealth
210
+ stealth_factors = [1.0, 0.8, 0.6, 0.4, 0.2, 0.1]
211
+ adjusted_concurrency = int(concurrency * stealth_factors[self.stealth_level])
212
+ self.concurrency = max(10, min(adjusted_concurrency, 500))
213
+
214
+ # Ajusta timeout baseado no nível de stealth
215
+ self.timeout = timeout * (1 + (self.stealth_level * 0.5))
216
+
217
+ # Cache para serviços já identificados
218
+ self.service_cache: Dict[int, ServiceInfo] = {}
219
+
220
+ # Resolvedor DNS assíncrono
221
+ self.resolver = dns.asyncresolver.Resolver()
222
+ self.resolver.timeout = 2.0
223
+ self.resolver.lifetime = 2.0
85
224
 
86
225
  async def scan(self) -> List[PortScanResult]:
87
- sem = asyncio.Semaphore(self.concurrency)
226
+ """Executa a varredura de portas."""
227
+ console.print(f"[bold]Iniciando varredura em {self.target}[/bold]")
228
+ console.print(f"Perfil: {self.profile}, Portas: {len(PROFILES[self.profile])}")
229
+
88
230
  ports = PROFILES[self.profile]
231
+ if self.stealth_level > 0:
232
+ # Aleatoriza a ordem das portas para maior discrição
233
+ random.shuffle(ports)
234
+
235
+ sem = asyncio.Semaphore(self.concurrency)
89
236
  results: List[PortScanResult] = []
90
-
237
+
91
238
  async def worker(port: int):
92
239
  async with sem:
93
- res = await self._probe(port)
94
- if res:
95
- results.append(res)
96
-
97
- await asyncio.gather(*(worker(p) for p in ports))
240
+ result = await self._probe_port(port)
241
+ if result:
242
+ results.append(result)
243
+ self._print_result(result)
244
+
245
+ # Executa os workers em paralelo
246
+ tasks = [worker(port) for port in ports]
247
+ await asyncio.gather(*tasks)
248
+
249
+ # Ordena os resultados por número de porta
98
250
  results.sort(key=lambda r: r.port)
251
+
99
252
  return results
100
-
101
- async def _probe(self, port: int) -> Optional[PortScanResult]:
253
+
254
+ def _print_result(self, result: PortScanResult):
255
+ """Exibe o resultado formatado no console."""
256
+ port_info = f"[bold blue]{result.port:>5}/tcp[/bold blue]"
257
+ status = "[green]open[/green]" if result.status == "open" else "[yellow]filtered[/yellow]"
258
+
259
+ service_name = result.service.name if result.service else "unknown"
260
+ service_info = f"[cyan]{service_name}[/cyan]"
261
+
262
+ if result.service and result.service.version:
263
+ service_info += f" [yellow]{result.service.version}[/yellow]"
264
+
265
+ if result.service and result.service.ssl:
266
+ service_info += " [green]🔒[/green]"
267
+
268
+ if result.service and result.service.vulns:
269
+ vuln_count = len(result.service.vulns)
270
+ service_info += f" [red]({vuln_count} vulns)[/red]"
271
+
272
+ console.print(f"{port_info} {status} {service_info}")
273
+
274
+ if result.banner:
275
+ console.print(f" [dim]Banner: {result.banner[:100]}{'...' if len(result.banner) > 100 else ''}[/dim]")
276
+
277
+ async def _probe_port(self, port: int) -> Optional[PortScanResult]:
278
+ """Verifica se uma porta está aberta e coleta informações do serviço."""
279
+ # Atraso aleatório para evitar detecção
280
+ if self.stealth_level > 0:
281
+ await asyncio.sleep(random.uniform(0.01, 0.5) * self.stealth_level)
282
+
283
+ # Tenta conexão TCP
102
284
  try:
103
- if self.stealth_level:
104
- await asyncio.sleep(random.uniform(0.01, 0.2) * self.stealth_level)
285
+ # Usa um timeout menor para a conexão inicial
286
+ conn_timeout = min(1.0, self.timeout)
105
287
  reader, writer = await asyncio.wait_for(
106
288
  asyncio.open_connection(self.target, port),
107
- timeout=self.timeout,
289
+ timeout=conn_timeout
108
290
  )
109
- except Exception:
291
+
292
+ # Se chegou aqui, a porta está aberta
293
+ result = PortScanResult(port=port, status="open")
294
+
295
+ # Tenta obter o banner do serviço
296
+ try:
297
+ # Configura timeout para leitura
298
+ read_timeout = max(0.5, self.timeout - 0.5)
299
+
300
+ # Lê o banner (se houver)
301
+ writer.write(b"\r\n\r\n")
302
+ await writer.drain()
303
+
304
+ # Lê até 1024 bytes
305
+ banner_bytes = await asyncio.wait_for(reader.read(1024), timeout=read_timeout)
306
+ if banner_bytes:
307
+ # Tenta decodificar como texto
308
+ try:
309
+ banner = banner_bytes.decode('utf-8', errors='replace').strip()
310
+ result.banner = banner
311
+
312
+ # Tenta identificar o serviço pelo banner
313
+ service_info = await self._identify_service(port, banner)
314
+ if service_info:
315
+ result.service = service_info
316
+ except UnicodeDecodeError:
317
+ # Se não for texto, mostra como hexdump
318
+ result.banner = banner_bytes.hex(' ', 1)
319
+ except (asyncio.TimeoutError, ConnectionResetError, OSError):
320
+ # Ignora erros de leitura do banner
321
+ pass
322
+
323
+ # Verifica se é um serviço SSL/TLS
324
+ if port in [443, 465, 636, 993, 995, 8443] or (result.service and result.service.ssl):
325
+ ssl_info = await self._check_ssl(port)
326
+ if ssl_info:
327
+ if not result.service:
328
+ result.service = ServiceInfo(name="ssl")
329
+ result.service.ssl = True
330
+ result.service.ssl_info = ssl_info
331
+
332
+ # Tenta identificar o serviço SSL
333
+ if not result.service.name or result.service.name == "ssl":
334
+ service_name = self._identify_ssl_service(port, ssl_info)
335
+ result.service.name = service_name
336
+
337
+ # Se não identificou o serviço, tenta pelo número da porta
338
+ if not result.service and port in SERVICE_MAP:
339
+ result.service = ServiceInfo(name=SERVICE_MAP[port])
340
+
341
+ # Verifica vulnerabilidades conhecidas
342
+ if self.check_vulns:
343
+ result.service.vulns = self._check_known_vulns(port, result.service.name)
344
+
345
+ return result
346
+
347
+ except (asyncio.TimeoutError, ConnectionRefusedError, OSError):
348
+ # Porta fechada ou inacessível
110
349
  return None
111
-
112
- banner = None
350
+
351
+ except Exception as e:
352
+ logger.error(f"Erro ao verificar porta {port}: {str(e)}")
353
+ return None
354
+
355
+ finally:
356
+ # Fecha a conexão se ainda estiver aberta
357
+ if 'writer' in locals():
358
+ writer.close()
359
+ try:
360
+ await writer.wait_closed()
361
+ except:
362
+ pass
363
+
364
+ async def _identify_service(self, port: int, banner: str) -> Optional[ServiceInfo]:
365
+ """Tenta identificar o serviço rodando na porta com base no banner."""
366
+ if not banner:
367
+ return None
368
+
369
+ banner_lower = banner.lower()
370
+ service = ServiceInfo(name="unknown")
371
+
372
+ # Verifica por padrões comuns de banners
373
+ if "apache" in banner_lower or "httpd" in banner_lower:
374
+ service.name = "Apache HTTP Server"
375
+ if match := re.search(r'Apache[/\s]([0-9.]+)', banner, re.IGNORECASE):
376
+ service.version = match.group(1)
377
+ service.cpe = f"cpe:/a:apache:http_server:{service.version}"
378
+
379
+ elif "nginx" in banner_lower:
380
+ service.name = "Nginx"
381
+ if match := re.search(r'nginx[/\s]([0-9.]+)', banner, re.IGNORECASE):
382
+ service.version = match.group(1)
383
+ service.cpe = f"cpe:/a:nginx:nginx:{service.version}"
384
+
385
+ elif "microsoft-iis" in banner_lower or "microsoft httpapi" in banner_lower:
386
+ service.name = "Microsoft IIS"
387
+ if match := re.search(r'Microsoft-IIS/([0-9.]+)', banner, re.IGNORECASE):
388
+ service.version = match.group(1)
389
+ service.cpe = f"cpe:/a:microsoft:iis:{service.version}"
390
+
391
+ elif "openbsd openssh" in banner_lower or "openssh" in banner_lower:
392
+ service.name = "OpenSSH"
393
+ if match := re.search(r'openssh[_-]?([0-9.]+[a-z]*)', banner, re.IGNORECASE):
394
+ service.version = match.group(1)
395
+ service.cpe = f"cpe:/a:openbsd:openssh:{service.version}"
396
+
397
+ elif "postfix" in banner_lower:
398
+ service.name = "Postfix"
399
+ if match := re.search(r'postfix[\s(]([0-9.]+)', banner, re.IGNORECASE):
400
+ service.version = match.group(1)
401
+ service.cpe = f"cpe:/a:postfix:postfix:{service.version}"
402
+
403
+ elif "exim" in banner_lower:
404
+ service.name = "Exim"
405
+ if match := re.search(r'exim[\s(]([0-9.]+)', banner, re.IGNORECASE):
406
+ service.version = match.group(1)
407
+ service.cpe = f"cpe:/a:exim:exim:{service.version}"
408
+
409
+ elif "dovecot" in banner_lower:
410
+ service.name = "Dovecot"
411
+ if match := re.search(r'dovecot[\s(]([0-9.]+)', banner, re.IGNORECASE):
412
+ service.version = match.group(1)
413
+ service.cpe = f"cpe:/a:dovecot:dovecot:{service.version}"
414
+
415
+ elif "proftpd" in banner_lower:
416
+ service.name = "ProFTPD"
417
+ if match := re.search(r'proftpd[\s(]([0-9.]+)', banner, re.IGNORECASE):
418
+ service.version = match.group(1)
419
+ service.cpe = f"cpe:/a:proftpd:proftpd:{service.version}"
420
+
421
+ elif "vsftpd" in banner_lower:
422
+ service.name = "vsFTPd"
423
+ if match := re.search(r'vsftpd[\s(]([0-9.]+)', banner, re.IGNORECASE):
424
+ service.version = match.group(1)
425
+ service.cpe = f"cpe:/a:vsftpd:vsftpd:{service.version}"
426
+
427
+ elif "mysql" in banner_lower:
428
+ service.name = "MySQL"
429
+ if match := re.search(r'([0-9.]+)[- ]*mysql', banner, re.IGNORECASE):
430
+ service.version = match.group(1)
431
+ service.cpe = f"cpe:/a:mysql:mysql:{service.version}"
432
+
433
+ elif "postgresql" in banner_lower or 'postgres' in banner_lower:
434
+ service.name = "PostgreSQL"
435
+ if match := re.search(r'postgresql[\s(]([0-9.]+)', banner, re.IGNORECASE):
436
+ service.version = match.group(1)
437
+ service.cpe = f"cpe:/a:postgresql:postgresql:{service.version}"
438
+
439
+ elif "redis" in banner_lower:
440
+ service.name = "Redis"
441
+ if match := re.search(r'redis[\s:]([0-9.]+)', banner, re.IGNORECASE):
442
+ service.version = match.group(1)
443
+ service.cpe = f"cpe:/a:redis:redis:{service.version}"
444
+
445
+ elif "mongodb" in banner_lower:
446
+ service.name = "MongoDB"
447
+ if match := re.search(r'mongod?b[\s(]([0-9.]+)', banner, re.IGNORECASE):
448
+ service.version = match.group(1)
449
+ service.cpe = f"cpe:/a:mongodb:mongodb:{service.version}"
450
+
451
+ elif "microsoft sql server" in banner_lower or "sql server" in banner_lower:
452
+ service.name = "Microsoft SQL Server"
453
+ if match := re.search(r'sql server[\s(]([0-9.]+)', banner, re.IGNORECASE):
454
+ service.version = match.group(1)
455
+ service.cpe = f"cpe:/a:microsoft:sql_server:{service.version}"
456
+
457
+ elif "oracle" in banner_lower and "database" in banner_lower:
458
+ service.name = "Oracle Database"
459
+ if match := re.search(r'oracle[\s(]([0-9.]+)', banner, re.IGNORECASE):
460
+ service.version = match.group(1)
461
+ service.cpe = f"cpe:/a:oracle:database:{service.version}"
462
+
463
+ # Se não identificou pelo banner, tenta pela porta
464
+ if service.name == "unknown" and port in SERVICE_MAP:
465
+ service.name = SERVICE_MAP[port]
466
+
467
+ # Verifica vulnerabilidades conhecidas
468
+ if self.check_vulns:
469
+ service.vulns = self._check_known_vulns(port, service.name)
470
+
471
+ return service if service.name != "unknown" else None
472
+
473
+ def _identify_ssl_service(self, port: int, ssl_info: Dict[str, Any]) -> str:
474
+ """Tenta identificar o serviço baseado na porta e informações SSL."""
475
+ # Mapeia portas comuns para serviços SSL
476
+ ssl_services = {
477
+ 443: "HTTPS",
478
+ 465: "SMTPS",
479
+ 563: "NNTPS",
480
+ 636: "LDAPS",
481
+ 853: "DNS-over-TLS",
482
+ 989: "FTPS (data)",
483
+ 990: "FTPS (control)",
484
+ 992: "Telnet over TLS/SSL",
485
+ 993: "IMAPS",
486
+ 994: "IRC over SSL",
487
+ 995: "POP3S",
488
+ 1443: "HTTPS (alt)",
489
+ 2376: "Docker TLS",
490
+ 2377: "Docker Swarm",
491
+ 3001: "HTTPS (Node.js)",
492
+ 3306: "MySQL over SSL",
493
+ 3389: "RDP over TLS",
494
+ 4000: "HTTPS (alt)",
495
+ 4001: "HTTPS (alt)",
496
+ 4002: "HTTPS (alt)",
497
+ 4003: "HTTPS (alt)",
498
+ 4004: "HTTPS (alt)",
499
+ 4005: "HTTPS (alt)",
500
+ 4006: "HTTPS (alt)",
501
+ 4007: "HTTPS (alt)",
502
+ 4008: "HTTPS (alt)",
503
+ 4009: "HTTPS (alt)",
504
+ 4433: "HTTPS (alt)",
505
+ 4443: "HTTPS (alt)",
506
+ 5000: "HTTPS (alt)",
507
+ 5001: "HTTPS (alt)",
508
+ 5002: "HTTPS (alt)",
509
+ 5003: "HTTPS (alt)",
510
+ 5004: "HTTPS (alt)",
511
+ 5005: "HTTPS (alt)",
512
+ 5006: "HTTPS (alt)",
513
+ 5007: "HTTPS (alt)",
514
+ 5008: "HTTPS (alt)",
515
+ 5009: "HTTPS (alt)",
516
+ 5432: "PostgreSQL over SSL",
517
+ 5671: "AMQPS",
518
+ 5800: "VNC over TLS",
519
+ 5901: "VNC over TLS (alt)",
520
+ 6001: "HTTPS (alt)",
521
+ 6002: "HTTPS (alt)",
522
+ 6003: "HTTPS (alt)",
523
+ 6004: "HTTPS (alt)",
524
+ 6005: "HTTPS (alt)",
525
+ 6006: "HTTPS (alt)",
526
+ 6007: "HTTPS (alt)",
527
+ 6008: "HTTPS (alt)",
528
+ 6009: "HTTPS (alt)",
529
+ 7000: "HTTPS (alt)",
530
+ 7001: "HTTPS (alt)",
531
+ 7002: "HTTPS (alt)",
532
+ 7003: "HTTPS (alt)",
533
+ 7004: "HTTPS (alt)",
534
+ 7005: "HTTPS (alt)",
535
+ 7006: "HTTPS (alt)",
536
+ 7007: "HTTPS (alt)",
537
+ 7008: "HTTPS (alt)",
538
+ 7009: "HTTPS (alt)",
539
+ 8000: "HTTPS (alt)",
540
+ 8001: "HTTPS (alt)",
541
+ 8002: "HTTPS (alt)",
542
+ 8003: "HTTPS (alt)",
543
+ 8004: "HTTPS (alt)",
544
+ 8005: "HTTPS (alt)",
545
+ 8006: "HTTPS (alt)",
546
+ 8007: "HTTPS (alt)",
547
+ 8008: "HTTPS (alt)",
548
+ 8009: "HTTPS (alt)",
549
+ 8080: "HTTPS (alt)",
550
+ 8081: "HTTPS (alt)",
551
+ 8082: "HTTPS (alt)",
552
+ 8083: "HTTPS (alt)",
553
+ 8084: "HTTPS (alt)",
554
+ 8085: "HTTPS (alt)",
555
+ 8086: "HTTPS (alt)",
556
+ 8087: "HTTPS (alt)",
557
+ 8088: "HTTPS (alt)",
558
+ 8089: "HTTPS (alt)",
559
+ 8090: "HTTPS (alt)",
560
+ 8091: "HTTPS (alt)",
561
+ 8443: "HTTPS (alt)",
562
+ 8444: "HTTPS (alt)",
563
+ 8445: "HTTPS (alt)",
564
+ 8446: "HTTPS (alt)",
565
+ 8447: "HTTPS (alt)",
566
+ 8448: "HTTPS (alt)",
567
+ 8449: "HTTPS (alt)",
568
+ 9000: "HTTPS (alt)",
569
+ 9001: "HTTPS (alt)",
570
+ 9002: "HTTPS (alt)",
571
+ 9003: "HTTPS (alt)",
572
+ 9004: "HTTPS (alt)",
573
+ 9005: "HTTPS (alt)",
574
+ 9006: "HTTPS (alt)",
575
+ 9007: "HTTPS (alt)",
576
+ 9008: "HTTPS (alt)",
577
+ 9009: "HTTPS (alt)",
578
+ 9010: "HTTPS (alt)",
579
+ 9443: "HTTPS (alt)",
580
+ 10000: "HTTPS (alt)",
581
+ 10443: "HTTPS (alt)",
582
+ 18080: "HTTPS (alt)",
583
+ 18081: "HTTPS (alt)",
584
+ 18082: "HTTPS (alt)",
585
+ 18083: "HTTPS (alt)",
586
+ 18084: "HTTPS (alt)",
587
+ 18085: "HTTPS (alt)",
588
+ 18086: "HTTPS (alt)",
589
+ 18087: "HTTPS (alt)",
590
+ 18088: "HTTPS (alt)",
591
+ 18089: "HTTPS (alt)",
592
+ 20000: "HTTPS (alt)",
593
+ 27017: "MongoDB over SSL",
594
+ 27018: "MongoDB over SSL (alt)",
595
+ 27019: "MongoDB over SSL (alt)",
596
+ 28017: "MongoDB over SSL (alt)",
597
+ 30000: "HTTPS (alt)",
598
+ 30001: "HTTPS (alt)",
599
+ 30002: "HTTPS (alt)",
600
+ 30003: "HTTPS (alt)",
601
+ 30004: "HTTPS (alt)",
602
+ 30005: "HTTPS (alt)",
603
+ 30006: "HTTPS (alt)",
604
+ 30007: "HTTPS (alt)",
605
+ 30008: "HTTPS (alt)",
606
+ 30009: "HTTPS (alt)",
607
+ 30010: "HTTPS (alt)",
608
+ 40000: "HTTPS (alt)",
609
+ 40001: "HTTPS (alt)",
610
+ 40002: "HTTPS (alt)",
611
+ 40003: "HTTPS (alt)",
612
+ 40004: "HTTPS (alt)",
613
+ 40005: "HTTPS (alt)",
614
+ 40006: "HTTPS (alt)",
615
+ 40007: "HTTPS (alt)",
616
+ 40008: "HTTPS (alt)",
617
+ 40009: "HTTPS (alt)",
618
+ 40010: "HTTPS (alt)",
619
+ 50000: "HTTPS (alt)",
620
+ 50001: "HTTPS (alt)",
621
+ 50002: "HTTPS (alt)",
622
+ 50003: "HTTPS (alt)",
623
+ 50004: "HTTPS (alt)",
624
+ 50005: "HTTPS (alt)",
625
+ 50006: "HTTPS (alt)",
626
+ 50007: "HTTPS (alt)",
627
+ 50008: "HTTPS (alt)",
628
+ 50009: "HTTPS (alt)",
629
+ 50010: "HTTPS (alt)",
630
+ 60000: "HTTPS (alt)",
631
+ 60001: "HTTPS (alt)",
632
+ 60002: "HTTPS (alt)",
633
+ 60003: "HTTPS (alt)",
634
+ 60004: "HTTPS (alt)",
635
+ 60005: "HTTPS (alt)",
636
+ 60006: "HTTPS (alt)",
637
+ 60007: "HTTPS (alt)",
638
+ 60008: "HTTPS (alt)",
639
+ 60009: "HTTPS (alt)",
640
+ 60010: "HTTPS (alt)",
641
+ }
642
+
643
+ return ssl_services.get(port, "SSL Service")
644
+
645
+ async def _check_ssl(self, port: int) -> Optional[Dict[str, Any]]:
646
+ """Verifica informações SSL/TLS da porta."""
647
+ ssl_info = {}
648
+
113
649
  try:
114
- await writer.drain()
115
- reader._transport.set_read_buffer_limits(1024)
116
- banner_bytes = await asyncio.wait_for(reader.read(256), timeout=0.5)
117
- if banner_bytes:
118
- banner = banner_bytes.decode(errors="ignore").strip()
119
- except Exception:
120
- banner = None
650
+ # Cria um contexto SSL
651
+ ssl_context = ssl.create_default_context()
652
+ ssl_context.check_hostname = False
653
+ ssl_context.verify_mode = ssl.CERT_NONE
654
+
655
+ # Tenta conectar com SSL/TLS
656
+ reader, writer = await asyncio.wait_for(
657
+ asyncio.open_connection(
658
+ self.target,
659
+ port,
660
+ ssl=ssl_context,
661
+ server_hostname=self.target
662
+ ),
663
+ timeout=self.timeout
664
+ )
665
+
666
+ # Obtém o certificado
667
+ ssl_object = writer.get_extra_info('ssl_object')
668
+ if ssl_object and hasattr(ssl_object, 'getpeercert'):
669
+ cert = ssl_object.getpeercert()
670
+
671
+ # Extrai informações do certificado
672
+ if cert:
673
+ ssl_info = {
674
+ 'version': ssl_object.version(),
675
+ 'cipher': ssl_object.cipher(),
676
+ 'compression': ssl_object.compression(),
677
+ 'issuer': dict(x[0] for x in cert.get('issuer', [])),
678
+ 'subject': dict(x[0] for x in cert.get('subject', [])),
679
+ 'not_before': cert.get('notBefore'),
680
+ 'not_after': cert.get('notAfter'),
681
+ 'serial_number': cert.get('serialNumber'),
682
+ 'subject_alt_name': [
683
+ name[1] for name in cert.get('subjectAltName', [])
684
+ if name[0] == 'DNS'
685
+ ],
686
+ 'ocsp': cert.get('OCSP', []),
687
+ 'ca_issuers': cert.get('caIssuers', []),
688
+ 'crl_distribution_points': cert.get('crlDistributionPoints', []),
689
+ }
690
+
691
+ # Verifica se o certificado está expirado
692
+ from datetime import datetime
693
+ now = datetime.utcnow()
694
+ not_after = datetime.strptime(ssl_info['not_after'], '%b %d %H:%M:%S %Y %Z')
695
+ ssl_info['expired'] = now > not_after
696
+
697
+ # Verifica se o certificado é auto-assinado
698
+ ssl_info['self_signed'] = (
699
+ ssl_info['issuer'] == ssl_info['subject']
700
+ and ssl_info['issuer'].get('organizationName', '').lower() != 'let\'s encrypt'
701
+ )
702
+
703
+ # Verifica se o certificado é válido para o domínio
704
+ import idna
705
+ from socket import gethostbyname
706
+
707
+ try:
708
+ hostname = idna.encode(self.target).decode('ascii')
709
+ ip = gethostbyname(hostname)
710
+
711
+ # Verifica se o IP está nos subjectAltNames
712
+ alt_names = []
713
+ for name in ssl_info.get('subject_alt_name', []):
714
+ if name.startswith('*'):
715
+ # Lida com wildcards básicos
716
+ domain = name[2:] # Remove o *.
717
+ if hostname.endswith(domain):
718
+ alt_names.append(hostname)
719
+ else:
720
+ alt_names.append(name)
721
+
722
+ ssl_info['valid_hostname'] = (
723
+ hostname in alt_names or
724
+ f'*.{hostname.split(".", 1)[1]}' in alt_names
725
+ )
726
+
727
+ # Verifica se o IP está nos subjectAltNames
728
+ ssl_info['valid_ip'] = ip in alt_names
729
+
730
+ except (UnicodeError, IndexError, OSError):
731
+ ssl_info['valid_hostname'] = False
732
+ ssl_info['valid_ip'] = False
733
+
734
+ return ssl_info
735
+
736
+ except (ssl.SSLError, asyncio.TimeoutError, ConnectionRefusedError, OSError) as e:
737
+ logger.debug(f"Erro ao verificar SSL na porta {port}: {str(e)}")
738
+ return None
739
+
740
+ except Exception as e:
741
+ logger.error(f"Erro inesperado ao verificar SSL na porta {port}: {str(e)}", exc_info=True)
742
+ return None
743
+
121
744
  finally:
122
- writer.close()
123
- with contextlib.suppress(Exception):
124
- await writer.wait_closed()
745
+ if 'writer' in locals():
746
+ writer.close()
747
+ try:
748
+ await writer.wait_closed()
749
+ except:
750
+ pass
751
+
752
+ def _check_known_vulns(self, port: int, service_name: str) -> List[str]:
753
+ """Verifica vulnerabilidades conhecidas para o serviço na porta."""
754
+ vulns = []
755
+
756
+ # Verifica vulnerabilidades específicas do serviço
757
+ for service, cves in VULNERABILITIES.items():
758
+ if service.lower() in service_name.lower():
759
+ vulns.extend(cves)
760
+
761
+ # Verifica vulnerabilidades específicas da porta
762
+ if port == 22: # SSH
763
+ vulns.extend(["CVE-2016-0777", "CVE-2016-0778", "CVE-2018-15473"])
764
+ elif port == 445: # SMB
765
+ vulns.extend(["EternalBlue", "SMBGhost", "EternalRomance", "SambaCry"])
766
+ elif port == 3389: # RDP
767
+ vulns.extend(["BlueKeep", "CVE-2019-0708", "CVE-2019-1181", "CVE-2019-1182"])
768
+ elif port == 27017: # MongoDB
769
+ vulns.extend(["Unauthenticated Access", "CVE-2016-6494"])
770
+ elif port == 9200: # Elasticsearch
771
+ vulns.extend(["CVE-2015-1427", "CVE-2015-3337", "CVE-2015-5531"])
772
+ elif port == 11211: # Memcached
773
+ vulns.extend(["DRDoS Amplification", "CVE-2016-8704", "CVE-2016-8705"])
774
+ elif port == 2375: # Docker
775
+ vulns.extend(["CVE-2019-5736", "CVE-2019-13139", "CVE-2019-14271"])
776
+ elif port == 10250: # Kubelet
777
+ vulns.extend(["CVE-2018-1002105", "CVE-2019-11253", "CVE-2019-11255"])
778
+
779
+ return list(set(vulns)) # Remove duplicatas
780
+
125
781
 
126
- return PortScanResult(port=port, status="open", banner=banner)
782
+ def format_scan_results(results: List[PortScanResult], output_format: str = "text") -> str:
783
+ """Formata os resultados da varredura no formato solicitado."""
784
+ if output_format.lower() == "json":
785
+ return json.dumps([r.to_dict() for r in results], indent=2)
786
+
787
+ # Formato de texto para saída no console
788
+ output = []
789
+ output.append("")
790
+ output.append(f"[bold]Resultado da varredura de portas[/bold]")
791
+ output.append(f"Alvo: {results[0].target if results else 'N/A'}")
792
+ output.append(f"Portas verificadas: {len(results)}")
793
+ output.append("-" * 80)
794
+
795
+ # Cabeçalho da tabela
796
+ output.append(
797
+ f"{'PORTA':<8} {'PROTOCOLO':<10} {'STATUS':<10} {'SERVIÇO':<25} {'VULNERABILIDADES'}"
798
+ )
799
+ output.append("-" * 80)
800
+
801
+ # Linhas da tabela
802
+ for result in results:
803
+ if result.status == "open":
804
+ port = f"[green]{result.port}[/green]"
805
+ status = "[green]ABERTA[/green]"
806
+ else:
807
+ port = f"[yellow]{result.port}[/yellow]"
808
+ status = "[yellow]FILTRADA[/yellow]"
809
+
810
+ service = result.service.name if result.service else "desconhecido"
811
+ version = f" {result.service.version}" if result.service and result.service.version else ""
812
+ service_info = f"{service}{version}"
813
+
814
+ if result.service and result.service.ssl:
815
+ service_info += " 🔒"
816
+
817
+ vulns = ", ".join(result.service.vulns) if result.service and result.service.vulns else "-"
818
+
819
+ output.append(
820
+ f"{port:<8} {'tcp':<10} {status:<10} {service_info:<25} {vulns}"
821
+ )
822
+
823
+ # Resumo
824
+ open_ports = [r for r in results if r.status == "open"]
825
+ output.append("-" * 80)
826
+ output.append(f"Total de portas abertas: {len(open_ports)}")
827
+
828
+ # Conta serviços por tipo
829
+ services = {}
830
+ for result in open_ports:
831
+ if result.service:
832
+ service_name = result.service.name
833
+ services[service_name] = services.get(service_name, 0) + 1
834
+
835
+ if services:
836
+ output.append("\nServiços identificados:")
837
+ for service, count in sorted(services.items()):
838
+ output.append(f" - {service}: {count} porta{'s' if count > 1 else ''}")
839
+
840
+ # Verifica vulnerabilidades críticas
841
+ critical_vulns = []
842
+ for result in open_ports:
843
+ if result.service and result.service.vulns:
844
+ for vuln in result.service.vulns:
845
+ if any(cve in vuln.upper() for cve in ["CVE", "MS"]):
846
+ critical_vulns.append((result.port, vuln))
847
+
848
+ if critical_vulns:
849
+ output.append("\n[bold red]VULNERABILIDADES CRÍTICAS ENCONTRADAS:[/bold red]")
850
+ for port, vuln in critical_vulns:
851
+ output.append(f" - Porta {port}: {vuln}")
852
+
853
+ return "\n".join(output)
127
854
 
128
855
 
129
- __all__ = ["PortScanner", "PortScanResult", "PROFILES"]
856
+ __all__ = ["PortScanner", "PortScanResult", "format_scan_results"]