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
@@ -0,0 +1,557 @@
|
|
1
|
+
"""Port scanning avançado com detecção de serviços usando Nmap."""
|
2
|
+
from __future__ import annotations
|
3
|
+
|
4
|
+
import asyncio
|
5
|
+
import json
|
6
|
+
import re
|
7
|
+
import nmap3
|
8
|
+
from dataclasses import dataclass, field
|
9
|
+
from datetime import datetime
|
10
|
+
from typing import Dict, List, Optional, Any, Set, Union
|
11
|
+
from pathlib import Path
|
12
|
+
import structlog
|
13
|
+
from rich.console import Console
|
14
|
+
from rich.table import Table, box
|
15
|
+
from rich.live import Live
|
16
|
+
from rich.spinner import Spinner
|
17
|
+
|
18
|
+
logger = structlog.get_logger(__name__)
|
19
|
+
console = Console()
|
20
|
+
|
21
|
+
# Perfis de varredura
|
22
|
+
PROFILES = {
|
23
|
+
"quick": "21-23,25,53,80,110,111,135,139,143,389,443,445,465,587,993,995,1433,1521,2049,3306,3389,5432,5900,6379,8080,8443,9000,10000,27017",
|
24
|
+
"web": "80,443,8080,8443,8000,8888,10443,4443",
|
25
|
+
"db": "1433,1521,27017-27019,28017,3306,5000,5432,5984,6379,8081",
|
26
|
+
"full": "1-1024",
|
27
|
+
"all": "1-65535",
|
28
|
+
}
|
29
|
+
|
30
|
+
@dataclass
|
31
|
+
class ServiceInfo:
|
32
|
+
"""Informações detalhadas sobre um serviço."""
|
33
|
+
name: str = "unknown"
|
34
|
+
version: Optional[str] = None
|
35
|
+
ssl: bool = False
|
36
|
+
ssl_info: Optional[Dict[str, Any]] = None
|
37
|
+
banner: Optional[str] = None
|
38
|
+
vulns: List[str] = field(default_factory=list)
|
39
|
+
cpe: Optional[str] = None
|
40
|
+
extra: Dict[str, Any] = field(default_factory=dict)
|
41
|
+
confidence: float = 0.0
|
42
|
+
last_checked: Optional[datetime] = field(default_factory=datetime.utcnow)
|
43
|
+
|
44
|
+
def to_dict(self) -> Dict[str, Any]:
|
45
|
+
"""Converte o objeto para dicionário."""
|
46
|
+
return {
|
47
|
+
"name": self.name,
|
48
|
+
"version": self.version,
|
49
|
+
"ssl": self.ssl,
|
50
|
+
"ssl_info": self.ssl_info,
|
51
|
+
"banner": self.banner,
|
52
|
+
"vulns": self.vulns,
|
53
|
+
"cpe": self.cpe,
|
54
|
+
"extra": self.extra,
|
55
|
+
"confidence": self.confidence,
|
56
|
+
"last_checked": self.last_checked.isoformat() if self.last_checked else None,
|
57
|
+
}
|
58
|
+
|
59
|
+
@dataclass
|
60
|
+
class PortScanResult:
|
61
|
+
"""Resultado da varredura de uma porta."""
|
62
|
+
port: int
|
63
|
+
protocol: str = "tcp"
|
64
|
+
status: str = "closed"
|
65
|
+
target: Optional[str] = None
|
66
|
+
service: Optional[ServiceInfo] = None
|
67
|
+
banner: Optional[str] = None
|
68
|
+
timestamp: str = field(default_factory=lambda: datetime.utcnow().isoformat())
|
69
|
+
|
70
|
+
def to_dict(self) -> Dict[str, Any]:
|
71
|
+
"""Converte o resultado para dicionário."""
|
72
|
+
return {
|
73
|
+
"port": self.port,
|
74
|
+
"protocol": self.protocol,
|
75
|
+
"status": self.status,
|
76
|
+
"target": self.target,
|
77
|
+
"service": self.service.to_dict() if self.service else None,
|
78
|
+
"banner": self.banner,
|
79
|
+
"timestamp": self.timestamp,
|
80
|
+
}
|
81
|
+
|
82
|
+
def to_json(self) -> str:
|
83
|
+
"""Retorna uma representação JSON do resultado."""
|
84
|
+
return json.dumps(self.to_dict(), indent=2)
|
85
|
+
|
86
|
+
class PortScanner:
|
87
|
+
"""Execução de port scanning com detecção avançada de serviços usando Nmap."""
|
88
|
+
|
89
|
+
def __init__(
|
90
|
+
self,
|
91
|
+
target: str,
|
92
|
+
ports: Union[str, List[int], None] = None,
|
93
|
+
scan_type: str = "tcp", # CORREÇÃO: Padrão mudado para TCP (não requer root)
|
94
|
+
stealth_level: int = 0,
|
95
|
+
resolve_services: bool = True,
|
96
|
+
check_vulns: bool = True,
|
97
|
+
debug: bool = False, # NOVO: Opção de debug
|
98
|
+
):
|
99
|
+
self.target = target
|
100
|
+
# CORREÇÃO: Armazena o range real, não o apelido
|
101
|
+
self.ports = self._parse_ports(ports) if ports else PROFILES["all"]
|
102
|
+
# CORREÇÃO: Default para TCP se não especificado
|
103
|
+
self.scan_type = scan_type if scan_type in ["syn", "tcp", "udp", "sS", "sT", "sU"] else "sT"
|
104
|
+
self.stealth_level = max(0, min(stealth_level, 5))
|
105
|
+
self.resolve_services = resolve_services
|
106
|
+
self.check_vulns = check_vulns
|
107
|
+
self.debug = debug # NOVO
|
108
|
+
self.stealth_level = max(0, min(stealth_level, 5))
|
109
|
+
self.resolve_services = resolve_services
|
110
|
+
self.check_vulns = check_vulns
|
111
|
+
|
112
|
+
self.nm = nmap3.Nmap()
|
113
|
+
self.scan_tech = nmap3.NmapScanTechniques()
|
114
|
+
|
115
|
+
# CORREÇÃO: Remove -sS dos args base (será adicionado pela técnica)
|
116
|
+
self.scan_arguments = self._get_scan_arguments()
|
117
|
+
|
118
|
+
def _parse_ports(self, ports: Union[str, List[int]]) -> str:
|
119
|
+
"""Converte diferentes formatos de portas para o formato do Nmap."""
|
120
|
+
if isinstance(ports, list):
|
121
|
+
return ",".join(str(p) for p in ports)
|
122
|
+
if isinstance(ports, str):
|
123
|
+
# CORREÇÃO: Sempre retorna o range real, não o apelido
|
124
|
+
if ports in PROFILES:
|
125
|
+
return PROFILES[ports]
|
126
|
+
return ports
|
127
|
+
return PROFILES["all"]
|
128
|
+
|
129
|
+
def _get_scan_arguments(self) -> str:
|
130
|
+
"""Gera os argumentos do Nmap baseados nas configurações."""
|
131
|
+
# CORREÇÃO: Remove -sV e -sC daqui pois já são adicionados pela técnica de scan
|
132
|
+
args = "-Pn -T4 --open"
|
133
|
+
|
134
|
+
if self.stealth_level > 0:
|
135
|
+
args += f" --max-rtt-timeout {1000 - (self.stealth_level * 100)}ms"
|
136
|
+
args += f" --scan-delay {self.stealth_level * 2}s"
|
137
|
+
|
138
|
+
return args.strip()
|
139
|
+
|
140
|
+
@staticmethod
|
141
|
+
def render_pipe_summary(results: List[PortScanResult]) -> str:
|
142
|
+
"""Renderiza tabela em formato pipe (ASCII) com larguras automáticas."""
|
143
|
+
if not results:
|
144
|
+
return "Nenhuma porta aberta encontrada."
|
145
|
+
|
146
|
+
# Prepara dados
|
147
|
+
headers = [" PORTA", "STATUS", "SERVICO", "PROTOCOLO", "VERSAO", "SSL"]
|
148
|
+
rows = []
|
149
|
+
|
150
|
+
for r in sorted(results, key=lambda x: (x.protocol, x.port)):
|
151
|
+
service_name = r.service.name if r.service else "unknown"
|
152
|
+
version = (r.service.version or "").strip() if r.service else ""
|
153
|
+
ssl = "✅" if (r.service and r.service.ssl) else ""
|
154
|
+
|
155
|
+
rows.append([
|
156
|
+
str(r.port),
|
157
|
+
r.status,
|
158
|
+
service_name or "unknown",
|
159
|
+
r.protocol.upper(),
|
160
|
+
version or "-",
|
161
|
+
ssl
|
162
|
+
])
|
163
|
+
|
164
|
+
# Calcula larguras automáticas
|
165
|
+
col_widths = [len(h) for h in headers]
|
166
|
+
for row in rows:
|
167
|
+
for i, cell in enumerate(row):
|
168
|
+
col_widths[i] = max(col_widths[i], len(str(cell)))
|
169
|
+
|
170
|
+
# Formata linhas
|
171
|
+
def fmt_line(cells):
|
172
|
+
return " | ".join(str(cell).ljust(col_widths[i]) for i, cell in enumerate(cells))
|
173
|
+
|
174
|
+
separator = "-+-".join("-" * w for w in col_widths)
|
175
|
+
|
176
|
+
output = [
|
177
|
+
fmt_line(headers),
|
178
|
+
separator
|
179
|
+
]
|
180
|
+
output.extend(fmt_line(row) for row in rows)
|
181
|
+
|
182
|
+
return "\n".join(output)
|
183
|
+
|
184
|
+
async def scan(self) -> List[PortScanResult]:
|
185
|
+
"""Executa a varredura de portas usando Nmap (python3-nmap)."""
|
186
|
+
# CORREÇÃO: Exibe o range real
|
187
|
+
logger.info(f"Iniciando varredura Nmap", target=self.target, ports=self.ports, arguments=self.scan_arguments)
|
188
|
+
console.print(f"[bold] 🔍 Iniciando varredura Nmap em {self.target}[/bold]")
|
189
|
+
console.print(f" 📊 Portas: [bold]{self.ports}[/bold]")
|
190
|
+
console.print(f" ⚙️ Argumentos: [bold]{self.scan_arguments}[/bold]")
|
191
|
+
|
192
|
+
if self.stealth_level > 0:
|
193
|
+
console.print(f" 🔒 Modo furtivo: nível {self.stealth_level}")
|
194
|
+
|
195
|
+
# Cria spinner animado
|
196
|
+
spinner = Spinner("dots", text="[cyan]Executando scan Nmap...[/cyan]", style="cyan")
|
197
|
+
|
198
|
+
try:
|
199
|
+
# CORREÇÃO: self.ports já é o range correto
|
200
|
+
ports_str = self.ports
|
201
|
+
args = f"{self.scan_arguments} -p {ports_str}"
|
202
|
+
logger.debug("Argumentos completos do Nmap", nmap_args=args)
|
203
|
+
|
204
|
+
st = self.scan_type
|
205
|
+
logger.info(f"Técnica de varredura selecionada: {st}")
|
206
|
+
|
207
|
+
# Inicia o spinner em Live context
|
208
|
+
with Live(spinner, console=console, refresh_per_second=10):
|
209
|
+
try:
|
210
|
+
# Detecta se precisa de root e ajusta automaticamente
|
211
|
+
needs_root = st in ("syn", "sS")
|
212
|
+
if needs_root:
|
213
|
+
spinner.update(text="[yellow]⚠️ Alternando para TCP Connect (-sT)...[/yellow]")
|
214
|
+
await asyncio.sleep(0.5) # Pequena pausa para o usuário ver a mensagem
|
215
|
+
st = "sT"
|
216
|
+
self.scan_type = "sT"
|
217
|
+
|
218
|
+
if self.debug:
|
219
|
+
console.print(f"[dim]🐛 Executando: nmap -{st} {args} {self.target}[/dim]")
|
220
|
+
|
221
|
+
spinner.update(text=f"[cyan]Scaneando portas {self.ports}...[/cyan]")
|
222
|
+
|
223
|
+
# Executa o scan baseado na técnica
|
224
|
+
if st in ("syn", "sS"):
|
225
|
+
logger.debug("Executando varredura SYN (sS)")
|
226
|
+
scan_results = await asyncio.to_thread(
|
227
|
+
self.scan_tech.nmap_syn_scan, self.target, args=args
|
228
|
+
)
|
229
|
+
elif st in ("tcp", "sT"):
|
230
|
+
logger.debug("Executando varredura TCP (sT)")
|
231
|
+
scan_results = await asyncio.to_thread(
|
232
|
+
self.scan_tech.nmap_tcp_scan, self.target, args=args
|
233
|
+
)
|
234
|
+
elif st in ("udp", "sU"):
|
235
|
+
if hasattr(self.scan_tech, "nmap_udp_scan"):
|
236
|
+
logger.debug("Executando varredura UDP (sU)")
|
237
|
+
scan_results = await asyncio.to_thread(
|
238
|
+
self.scan_tech.nmap_udp_scan, self.target, args=args
|
239
|
+
)
|
240
|
+
else:
|
241
|
+
logger.debug("Executando varredura UDP via fallback")
|
242
|
+
scan_results = await asyncio.to_thread(
|
243
|
+
self.nm.nmap_version_detection, self.target, args=f"-sU {args}"
|
244
|
+
)
|
245
|
+
else:
|
246
|
+
logger.debug(f"Técnica não reconhecida '{st}', usando fallback")
|
247
|
+
scan_results = await asyncio.to_thread(
|
248
|
+
self.nm.nmap_version_detection, self.target, args=args
|
249
|
+
)
|
250
|
+
|
251
|
+
spinner.update(text="[cyan]Processando resultados...[/cyan]")
|
252
|
+
|
253
|
+
if self.debug:
|
254
|
+
console.print(f"[dim]🐛 Tipo de resultado: {type(scan_results)}[/dim]")
|
255
|
+
|
256
|
+
# Detecta erro de permissão e tenta novamente com TCP
|
257
|
+
if isinstance(scan_results, dict) and scan_results.get("error") and "root" in str(scan_results.get("msg", "")).lower():
|
258
|
+
spinner.update(text="[yellow]⚠️ Erro de permissão. Tentando TCP...[/yellow]")
|
259
|
+
await asyncio.sleep(0.5)
|
260
|
+
st = "sT"
|
261
|
+
self.scan_type = "sT"
|
262
|
+
scan_results = await asyncio.to_thread(
|
263
|
+
self.scan_tech.nmap_tcp_scan, self.target, args=args
|
264
|
+
)
|
265
|
+
if self.debug:
|
266
|
+
console.print(f"[dim]🐛 Tipo de resultado (2ª tentativa): {type(scan_results)}[/dim]")
|
267
|
+
|
268
|
+
# Se ports está vazio, tenta scan com subprocess direto
|
269
|
+
if isinstance(scan_results, dict):
|
270
|
+
has_ports = False
|
271
|
+
for key, val in scan_results.items():
|
272
|
+
if isinstance(val, dict) and isinstance(val.get("ports"), list) and val.get("ports"):
|
273
|
+
has_ports = True
|
274
|
+
break
|
275
|
+
|
276
|
+
if not has_ports and self.debug:
|
277
|
+
spinner.update(text="[yellow]⚠️ Tentando método alternativo...[/yellow]")
|
278
|
+
await asyncio.sleep(0.3)
|
279
|
+
try:
|
280
|
+
clean_args = args.replace("-sV", "").replace("-sC", "").strip()
|
281
|
+
scan_results = await asyncio.to_thread(
|
282
|
+
self.scan_tech.nmap_tcp_scan,
|
283
|
+
self.target,
|
284
|
+
args=f"{clean_args} -p {ports_str}"
|
285
|
+
)
|
286
|
+
console.print(f"[dim]🐛 Tipo de resultado (scan limpo): {type(scan_results)}[/dim]")
|
287
|
+
except Exception as e:
|
288
|
+
console.print(f"[red]❌ Erro no scan alternativo: {e}[/red]")
|
289
|
+
|
290
|
+
if self.debug:
|
291
|
+
console.print(f"[dim]🐛 Resultado RAW:[/dim]")
|
292
|
+
try:
|
293
|
+
import pprint
|
294
|
+
console.print(f"[dim]{pprint.pformat(scan_results, width=120)}[/dim]")
|
295
|
+
except:
|
296
|
+
console.print(f"[dim]{str(scan_results)[:2000]}[/dim]")
|
297
|
+
|
298
|
+
logger.debug("Varredura Nmap concluída", scan_results=scan_results)
|
299
|
+
|
300
|
+
except Exception as e:
|
301
|
+
logger.error("Erro durante a execução do Nmap", error=str(e), exc_info=True)
|
302
|
+
raise
|
303
|
+
|
304
|
+
# CORREÇÃO: Parser melhorado para lidar com diferentes formatos
|
305
|
+
logger.debug("Iniciando processamento dos resultados do Nmap")
|
306
|
+
|
307
|
+
results = []
|
308
|
+
|
309
|
+
if self.debug:
|
310
|
+
if isinstance(scan_results, dict):
|
311
|
+
console.print(f"[dim]🐛 Chaves do resultado: {list(scan_results.keys())}[/dim]")
|
312
|
+
|
313
|
+
# CORREÇÃO: Trata múltiplos formatos de retorno
|
314
|
+
if isinstance(scan_results, dict):
|
315
|
+
for host_key, host_data in scan_results.items():
|
316
|
+
# Ignora chaves de metadados
|
317
|
+
if host_key in ("stats", "runtime", "task_results"):
|
318
|
+
if self.debug:
|
319
|
+
console.print(f"[dim]🐛 Ignorando chave de metadados: {host_key}[/dim]")
|
320
|
+
continue
|
321
|
+
|
322
|
+
if self.debug:
|
323
|
+
console.print(f"[dim]🐛 Processando host: {host_key}[/dim]")
|
324
|
+
console.print(f"[dim]🐛 Tipo de host_data: {type(host_data)}[/dim]")
|
325
|
+
|
326
|
+
# Formato: { "host": { "ports": [...] } }
|
327
|
+
if isinstance(host_data, dict):
|
328
|
+
if self.debug:
|
329
|
+
console.print(f"[dim]🐛 Chaves de host_data: {list(host_data.keys())}[/dim]")
|
330
|
+
|
331
|
+
ports_list = host_data.get("ports", [])
|
332
|
+
|
333
|
+
if self.debug:
|
334
|
+
console.print(f"[dim]🐛 Portas no formato 'ports': {len(ports_list)}[/dim]")
|
335
|
+
|
336
|
+
# CORREÇÃO: Também processa ports no formato alternativo
|
337
|
+
if not ports_list and "tcp" in host_data:
|
338
|
+
if self.debug:
|
339
|
+
console.print(f"[dim]🐛 Tentando formato alternativo 'tcp'...[/dim]")
|
340
|
+
tcp_ports = host_data.get("tcp", {})
|
341
|
+
if self.debug:
|
342
|
+
console.print(f"[dim]🐛 Portas TCP encontradas: {list(tcp_ports.keys())}[/dim]")
|
343
|
+
|
344
|
+
for port_num, port_data in tcp_ports.items():
|
345
|
+
ports_list.append({
|
346
|
+
"portid": str(port_num),
|
347
|
+
"protocol": "tcp",
|
348
|
+
"state": port_data,
|
349
|
+
"service": port_data.get("service", {}) if isinstance(port_data, dict) else {}
|
350
|
+
})
|
351
|
+
|
352
|
+
# CORREÇÃO: Também processa formato direto de portas
|
353
|
+
if not ports_list and self.debug:
|
354
|
+
console.print(f"[dim]🐛 Tentando outros formatos de porta...[/dim]")
|
355
|
+
for key, value in host_data.items():
|
356
|
+
if key.isdigit() or (isinstance(value, dict) and "portid" in value):
|
357
|
+
if self.debug:
|
358
|
+
console.print(f"[dim]🐛 Encontrada porta: {key}[/dim]")
|
359
|
+
ports_list.append(value if isinstance(value, dict) else {"portid": key, "state": value})
|
360
|
+
|
361
|
+
if self.debug and ports_list:
|
362
|
+
console.print(f"[dim]🐛 Total de portas a processar: {len(ports_list)}[/dim]")
|
363
|
+
|
364
|
+
for i, port_info in enumerate(ports_list):
|
365
|
+
if self.debug:
|
366
|
+
console.print(f"[dim]🐛 Processando porta {i+1}/{len(ports_list)}: {port_info}[/dim]")
|
367
|
+
result = self._process_port_info(port_info, host_key)
|
368
|
+
if result:
|
369
|
+
if self.debug:
|
370
|
+
console.print(f"[dim]✅ Porta {result.port} processada com sucesso![/dim]")
|
371
|
+
results.append(result)
|
372
|
+
elif self.debug:
|
373
|
+
console.print(f"[dim]❌ Porta não passou na validação[/dim]")
|
374
|
+
|
375
|
+
elif isinstance(scan_results, list):
|
376
|
+
if self.debug:
|
377
|
+
console.print(f"[dim]🐛 Resultado é uma lista com {len(scan_results)} itens[/dim]")
|
378
|
+
for port_info in scan_results:
|
379
|
+
result = self._process_port_info(port_info, self.target)
|
380
|
+
if result:
|
381
|
+
results.append(result)
|
382
|
+
else:
|
383
|
+
if self.debug:
|
384
|
+
console.print(f"[red]⚠️ Formato de resultado desconhecido: {type(scan_results)}[/red]")
|
385
|
+
|
386
|
+
results.sort(key=lambda x: x.port)
|
387
|
+
|
388
|
+
console.print("\n[bold green] ✅ Varredura concluída![/]")
|
389
|
+
|
390
|
+
if not results:
|
391
|
+
logger.warning(
|
392
|
+
"Nenhuma porta aberta encontrada",
|
393
|
+
target=self.target,
|
394
|
+
ports=self.ports,
|
395
|
+
arguments=args
|
396
|
+
)
|
397
|
+
console.print("[yellow] ℹ️ Nenhuma porta aberta encontrada.[/]")
|
398
|
+
else:
|
399
|
+
logger.info(
|
400
|
+
"Portas abertas encontradas",
|
401
|
+
count=len(results),
|
402
|
+
ports=[r.port for r in results]
|
403
|
+
)
|
404
|
+
|
405
|
+
# Exibe tabela formatada
|
406
|
+
console.print("\n[bold cyan] 🚪 Portas Abertas:[/bold cyan]")
|
407
|
+
table_output = self.render_pipe_summary(results)
|
408
|
+
console.print(table_output)
|
409
|
+
console.print(f"\n[bold] Total: {len(results)} porta(s) aberta(s)[/bold]")
|
410
|
+
|
411
|
+
return results
|
412
|
+
|
413
|
+
except Exception as e:
|
414
|
+
error_msg = f"Erro durante a varredura Nmap: {str(e)}"
|
415
|
+
console.print(f"[bold red]❌ {error_msg}[/]")
|
416
|
+
logger.error(
|
417
|
+
error_msg,
|
418
|
+
target=self.target,
|
419
|
+
ports=self.ports,
|
420
|
+
error_type=type(e).__name__,
|
421
|
+
exc_info=True
|
422
|
+
)
|
423
|
+
raise
|
424
|
+
|
425
|
+
def _process_port_info(self, port_info: Dict, host: str) -> Optional[PortScanResult]:
|
426
|
+
"""Processa informações de uma porta individual."""
|
427
|
+
if not isinstance(port_info, dict):
|
428
|
+
if self.debug:
|
429
|
+
console.print(f"[dim]❌ port_info não é dict: {type(port_info)}[/dim]")
|
430
|
+
return None
|
431
|
+
|
432
|
+
try:
|
433
|
+
if self.debug:
|
434
|
+
console.print(f"[dim]🐛 Processando: {port_info}[/dim]")
|
435
|
+
|
436
|
+
# CORREÇÃO: Aceita diferentes formatos de estado
|
437
|
+
port_state = port_info.get("state", {})
|
438
|
+
|
439
|
+
if self.debug:
|
440
|
+
console.print(f"[dim]🐛 port_state: {port_state} (tipo: {type(port_state)})[/dim]")
|
441
|
+
|
442
|
+
# Formato 1: { "state": { "state": "open" } }
|
443
|
+
if isinstance(port_state, dict):
|
444
|
+
state_value = port_state.get("state", "unknown")
|
445
|
+
# Formato 2: { "state": "open" }
|
446
|
+
elif isinstance(port_state, str):
|
447
|
+
state_value = port_state
|
448
|
+
else:
|
449
|
+
state_value = "unknown"
|
450
|
+
|
451
|
+
if self.debug:
|
452
|
+
console.print(f"[dim]🐛 state_value: {state_value}[/dim]")
|
453
|
+
|
454
|
+
# CORREÇÃO: Aceita portas abertas E filtered (importantes)
|
455
|
+
if state_value not in ("open", "filtered"):
|
456
|
+
if self.debug:
|
457
|
+
console.print(f"[dim]❌ Estado '{state_value}' não aceito[/dim]")
|
458
|
+
return None
|
459
|
+
|
460
|
+
port_num = int(port_info.get("portid", 0))
|
461
|
+
if port_num == 0:
|
462
|
+
if self.debug:
|
463
|
+
console.print(f"[dim]❌ Porta número 0 inválida[/dim]")
|
464
|
+
return None
|
465
|
+
|
466
|
+
if self.debug:
|
467
|
+
console.print(f"[dim] ✅ Porta {port_num} válida, estado: {state_value}[/dim]")
|
468
|
+
|
469
|
+
# Extrai informações do serviço
|
470
|
+
service_data = port_info.get("service", {})
|
471
|
+
service_info = None
|
472
|
+
|
473
|
+
if isinstance(service_data, dict) and service_data:
|
474
|
+
service_info = ServiceInfo(
|
475
|
+
name=service_data.get("name", "unknown"),
|
476
|
+
version=service_data.get("version", ""),
|
477
|
+
banner=service_data.get("product", ""),
|
478
|
+
)
|
479
|
+
|
480
|
+
result = PortScanResult(
|
481
|
+
port=port_num,
|
482
|
+
protocol=port_info.get("protocol", "tcp"),
|
483
|
+
status=state_value,
|
484
|
+
target=host,
|
485
|
+
service=service_info,
|
486
|
+
banner=service_data.get("product", "") if isinstance(service_data, dict) else "",
|
487
|
+
)
|
488
|
+
|
489
|
+
logger.debug(
|
490
|
+
"Porta processada",
|
491
|
+
port=port_num,
|
492
|
+
status=state_value,
|
493
|
+
service=service_info.name if service_info else "unknown",
|
494
|
+
protocol=port_info.get("protocol")
|
495
|
+
)
|
496
|
+
|
497
|
+
return result
|
498
|
+
|
499
|
+
except (TypeError, ValueError, KeyError) as e:
|
500
|
+
if self.debug:
|
501
|
+
console.print(f"[red]❌ Erro ao processar porta: {e}[/red]")
|
502
|
+
logger.warning("Erro ao processar porta",
|
503
|
+
port_info=port_info,
|
504
|
+
error=str(e),
|
505
|
+
error_type=type(e).__name__)
|
506
|
+
return None
|
507
|
+
|
508
|
+
def format_scan_results(results: List[PortScanResult], output_format: str = "text", total_ports: Optional[int] = None) -> str:
|
509
|
+
"""Formata os resultados da varredura no formato solicitado."""
|
510
|
+
if output_format == "json":
|
511
|
+
return json.dumps([r.to_dict() for r in results], indent=2)
|
512
|
+
|
513
|
+
if output_format == "pipe":
|
514
|
+
base = PortScanner.render_pipe_summary(results)
|
515
|
+
suffix = f"\n\n 🔍 Total de portas abertas: {len(results)}"
|
516
|
+
if total_ports:
|
517
|
+
suffix = f"\n\n 🔍 Resumo: {len(results)} portas abertas de {total_ports} verificadas"
|
518
|
+
return base + suffix
|
519
|
+
|
520
|
+
# Formato de texto
|
521
|
+
if not results:
|
522
|
+
return "Nenhuma porta aberta encontrada."
|
523
|
+
|
524
|
+
# Cria tabela
|
525
|
+
table = Table(show_header=True, header_style="bold magenta", box=box.ROUNDED)
|
526
|
+
table.add_column("Porta", style="cyan", width=10)
|
527
|
+
table.add_column("Protocolo", style="blue")
|
528
|
+
table.add_column("Status", style="yellow")
|
529
|
+
table.add_column("Serviço", style="green")
|
530
|
+
table.add_column("Versão", style="yellow")
|
531
|
+
table.add_column("SSL/TLS", style="magenta")
|
532
|
+
|
533
|
+
for result in results:
|
534
|
+
service = result.service.name if result.service and hasattr(result.service, 'name') else "desconhecido"
|
535
|
+
version = result.service.version if result.service and hasattr(result.service, 'version') else ""
|
536
|
+
ssl = "✅" if result.service and result.service.ssl else ""
|
537
|
+
|
538
|
+
table.add_row(
|
539
|
+
f"{result.port}",
|
540
|
+
result.protocol.upper(),
|
541
|
+
result.status,
|
542
|
+
service,
|
543
|
+
version or "-",
|
544
|
+
ssl
|
545
|
+
)
|
546
|
+
|
547
|
+
output = [str(table)]
|
548
|
+
|
549
|
+
# Adiciona resumo
|
550
|
+
if total_ports:
|
551
|
+
output.append(f"\n 🔍 [bold]Resumo:[/bold] {len(results)} portas abertas de {total_ports} verificadas")
|
552
|
+
else:
|
553
|
+
output.append(f"\n 🔍 [bold]Total de portas:[/bold] {len(results)}")
|
554
|
+
|
555
|
+
return "\n".join(output)
|
556
|
+
|
557
|
+
__all__ = ["PortScanner", "PortScanResult", "format_scan_results"]
|