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