moriarty-project 0.1.26__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.
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
  import asyncio
5
5
  import json
6
6
  import re
7
- import nmap
7
+ import nmap3
8
8
  from dataclasses import dataclass, field
9
9
  from datetime import datetime
10
10
  from typing import Dict, List, Optional, Any, Set, Union
@@ -12,6 +12,8 @@ from pathlib import Path
12
12
  import structlog
13
13
  from rich.console import Console
14
14
  from rich.table import Table, box
15
+ from rich.live import Live
16
+ from rich.spinner import Spinner
15
17
 
16
18
  logger = structlog.get_logger(__name__)
17
19
  console = Console()
@@ -88,171 +90,434 @@ class PortScanner:
88
90
  self,
89
91
  target: str,
90
92
  ports: Union[str, List[int], None] = None,
91
- scan_type: str = "syn",
93
+ scan_type: str = "tcp", # CORREÇÃO: Padrão mudado para TCP (não requer root)
92
94
  stealth_level: int = 0,
93
95
  resolve_services: bool = True,
94
96
  check_vulns: bool = True,
97
+ debug: bool = False, # NOVO: Opção de debug
95
98
  ):
96
99
  self.target = target
100
+ # CORREÇÃO: Armazena o range real, não o apelido
97
101
  self.ports = self._parse_ports(ports) if ports else PROFILES["all"]
98
- self.scan_type = scan_type if scan_type in ["syn", "tcp", "udp", "sS", "sT", "sU"] else "sS"
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"
99
104
  self.stealth_level = max(0, min(stealth_level, 5))
100
105
  self.resolve_services = resolve_services
101
106
  self.check_vulns = check_vulns
102
- self.nm = nmap.PortScanner()
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()
103
114
 
104
- # Configurações baseadas no nível de stealth
115
+ # CORREÇÃO: Remove -sS dos args base (será adicionado pela técnica)
105
116
  self.scan_arguments = self._get_scan_arguments()
106
117
 
107
118
  def _parse_ports(self, ports: Union[str, List[int]]) -> str:
108
119
  """Converte diferentes formatos de portas para o formato do Nmap."""
109
120
  if isinstance(ports, list):
110
121
  return ",".join(str(p) for p in ports)
111
- elif ports in PROFILES:
112
- return PROFILES[ports]
113
- return 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"]
114
128
 
115
129
  def _get_scan_arguments(self) -> str:
116
130
  """Gera os argumentos do Nmap baseados nas configurações."""
117
- # Argumentos base - removendo -O (OS detection) e usando -T4 para velocidade
118
- args = "-T4 -sT" # Usando TCP Connect por padrão sem necessidade de root
119
-
120
- # Adiciona detecção de versão se necessário
121
- if self.resolve_services:
122
- args += " -sV"
131
+ # CORREÇÃO: Remove -sV e -sC daqui pois são adicionados pela técnica de scan
132
+ args = "-Pn -T4 --open"
123
133
 
124
- # Adiciona scripts padrão se não for muito furtivo
125
- if self.stealth_level < 3:
126
- args += " -sC"
127
-
128
- # Adiciona argumentos de furtividade baseado no nível
129
134
  if self.stealth_level > 0:
130
135
  args += f" --max-rtt-timeout {1000 - (self.stealth_level * 100)}ms"
131
136
  args += f" --scan-delay {self.stealth_level * 2}s"
132
-
137
+
133
138
  return args.strip()
134
139
 
135
140
  @staticmethod
136
141
  def render_pipe_summary(results: List[PortScanResult]) -> str:
142
+ """Renderiza tabela em formato pipe (ASCII) com larguras automáticas."""
137
143
  if not results:
138
144
  return "Nenhuma porta aberta encontrada."
139
- headers = ["PORTA", "STATUS", "SERVICO", "PROTOCOLO", "VERSAO", "SSL"]
145
+
146
+ # Prepara dados
147
+ headers = [" PORTA", "STATUS", "SERVICO", "PROTOCOLO", "VERSAO", "SSL"]
140
148
  rows = []
149
+
141
150
  for r in sorted(results, key=lambda x: (x.protocol, x.port)):
142
151
  service_name = r.service.name if r.service else "unknown"
143
152
  version = (r.service.version or "").strip() if r.service else ""
144
153
  ssl = "✅" if (r.service and r.service.ssl) else ""
154
+
145
155
  rows.append([
146
- str(r.port), r.status, service_name or "unknown",
147
- r.protocol.upper(), version, ssl
156
+ str(r.port),
157
+ r.status,
158
+ service_name or "unknown",
159
+ r.protocol.upper(),
160
+ version or "-",
161
+ ssl
148
162
  ])
149
-
163
+
164
+ # Calcula larguras automáticas
150
165
  col_widths = [len(h) for h in headers]
151
166
  for row in rows:
152
167
  for i, cell in enumerate(row):
153
- col_widths[i] = max(col_widths[i], len(cell))
154
-
168
+ col_widths[i] = max(col_widths[i], len(str(cell)))
169
+
170
+ # Formata linhas
155
171
  def fmt_line(cells):
156
- return " | ".join(cell.ljust(col_widths[i]) for i, cell in enumerate(cells))
157
-
158
- out = [fmt_line(headers), " | ".join("-" * w for w in col_widths)]
159
- out.extend(fmt_line(row) for row in rows)
160
- return "\n".join(out)
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)
161
183
 
162
184
  async def scan(self) -> List[PortScanResult]:
163
- """Executa a varredura de portas usando Nmap."""
164
- console.print(f"[bold]🔍 Iniciando varredura Nmap em {self.target}[/bold]")
165
- console.print(f"📊 Portas: [bold]{self.ports}[/bold]")
166
- console.print(f"⚙️ Argumentos: [bold]{self.scan_arguments}[/bold]")
167
-
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
+
168
192
  if self.stealth_level > 0:
169
- console.print(f"🔒 Modo furtivo: nível {self.stealth_level}")
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")
170
197
 
171
198
  try:
172
- # Executa o scan Nmap
173
- self.nm.scan(
174
- hosts=self.target,
175
- ports=self.ports,
176
- arguments=self.scan_arguments,
177
- sudo=False # Necessário para SYN scan
178
- )
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")
179
306
 
180
- # Processa os resultados
181
307
  results = []
182
- for host in self.nm.all_hosts():
183
- for proto in self.nm[host].all_protocols():
184
- ports = self.nm[host][proto].keys()
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
185
321
 
186
- for port in ports:
187
- port_info = self.nm[host][proto][port]
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]")
188
330
 
189
- # Cria o objeto ServiceInfo
190
- service_info = ServiceInfo(
191
- name=port_info.get('name', 'unknown'),
192
- version=port_info.get('version', ''),
193
- banner=port_info.get('product', ''),
194
- ssl=port_info.get('tunnel') == 'ssl' or 'ssl' in port_info.get('name', '').lower()
195
- )
331
+ ports_list = host_data.get("ports", [])
196
332
 
197
- # Adiciona CPE se disponível
198
- if 'cpe' in port_info:
199
- service_info.cpe = port_info['cpe']
333
+ if self.debug:
334
+ console.print(f"[dim]🐛 Portas no formato 'ports': {len(ports_list)}[/dim]")
200
335
 
201
- state = port_info.get('state', 'closed')
202
- if state != 'open':
203
- continue
204
-
205
- # Cria o resultado da porta
206
- result = PortScanResult(
207
- port=port,
208
- protocol=proto,
209
- status=state,
210
- target=host,
211
- service=service_info,
212
- banner=port_info.get('product', '') + ' ' + port_info.get('version', '')
213
- )
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})
214
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:
215
381
  results.append(result)
216
-
217
- # Ordena os resultados por número de porta
382
+ else:
383
+ if self.debug:
384
+ console.print(f"[red]⚠️ Formato de resultado desconhecido: {type(scan_results)}[/red]")
385
+
218
386
  results.sort(key=lambda x: x.port)
219
387
 
220
- # Exibe resumo
221
- open_ports = len(results)
222
- console.print(f"\n✅ [bold green]Varredura concluída![/bold green] {open_ports} portas abertas encontradas.")
223
-
224
- # Mostra a visão tipo "PORTA | STATUS | SERVICO | ..."
225
- console.print("\n[bold]Resumo das portas abertas[/bold]")
226
- console.print(self.render_pipe_summary(results))
227
-
228
- return results
388
+ console.print("\n[bold green] ✅ Varredura concluída![/]")
229
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
+
230
413
  except Exception as e:
231
- console.print(f"[bold red]❌ Erro ao executar Nmap: {str(e)}[/bold red]")
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
+ )
232
423
  raise
233
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
+
234
508
  def format_scan_results(results: List[PortScanResult], output_format: str = "text", total_ports: Optional[int] = None) -> str:
235
- """Formata os resultados da varredura no formato solicitado.
236
-
237
- Args:
238
- results: Lista de resultados da varredura
239
- output_format: Formato de saída ('text' ou 'json')
240
- total_ports: Número total de portas verificadas (opcional)
241
- """
509
+ """Formata os resultados da varredura no formato solicitado."""
242
510
  if output_format == "json":
243
511
  return json.dumps([r.to_dict() for r in results], indent=2)
244
512
 
245
513
  if output_format == "pipe":
246
514
  base = PortScanner.render_pipe_summary(results)
247
- suffix = f"\n\n🔍 Total de portas abertas: {len(results)}"
515
+ suffix = f"\n\n 🔍 Total de portas abertas: {len(results)}"
248
516
  if total_ports:
249
- suffix = f"\n\n🔍 Resumo: {len(results)} portas abertas de {total_ports} verificadas"
517
+ suffix = f"\n\n 🔍 Resumo: {len(results)} portas abertas de {total_ports} verificadas"
250
518
  return base + suffix
251
-
252
519
 
253
520
  # Formato de texto
254
- output = []
255
-
256
521
  if not results:
257
522
  return "Nenhuma porta aberta encontrada."
258
523
 
@@ -260,6 +525,7 @@ def format_scan_results(results: List[PortScanResult], output_format: str = "tex
260
525
  table = Table(show_header=True, header_style="bold magenta", box=box.ROUNDED)
261
526
  table.add_column("Porta", style="cyan", width=10)
262
527
  table.add_column("Protocolo", style="blue")
528
+ table.add_column("Status", style="yellow")
263
529
  table.add_column("Serviço", style="green")
264
530
  table.add_column("Versão", style="yellow")
265
531
  table.add_column("SSL/TLS", style="magenta")
@@ -272,19 +538,20 @@ def format_scan_results(results: List[PortScanResult], output_format: str = "tex
272
538
  table.add_row(
273
539
  f"{result.port}",
274
540
  result.protocol.upper(),
541
+ result.status,
275
542
  service,
276
- version,
543
+ version or "-",
277
544
  ssl
278
545
  )
279
546
 
280
- output.append(str(table))
547
+ output = [str(table)]
281
548
 
282
549
  # Adiciona resumo
283
550
  if total_ports:
284
- output.append(f"\n🔍 [bold]Resumo:[/bold] {len(results)} portas abertas de {total_ports} verificadas")
551
+ output.append(f"\n 🔍 [bold]Resumo:[/bold] {len(results)} portas abertas de {total_ports} verificadas")
285
552
  else:
286
- output.append(f"\n🔍 [bold]Total de portas abertas:[/bold] {len(results)}")
553
+ output.append(f"\n 🔍 [bold]Total de portas:[/bold] {len(results)}")
287
554
 
288
555
  return "\n".join(output)
289
556
 
290
- __all__ = ["PortScanner", "PortScanResult", "format_scan_results"]
557
+ __all__ = ["PortScanner", "PortScanResult", "format_scan_results"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: moriarty-project
3
- Version: 0.1.26
3
+ Version: 0.1.27
4
4
  Summary: Client-side OSINT toolkit with forensic-grade evidence handling.
5
5
  Project-URL: Homepage, https://github.com/DonatoReis/moriarty
6
6
  Project-URL: Documentation, https://github.com/DonatoReis/moriarty#readme
@@ -30,7 +30,7 @@ Requires-Dist: httpx[http2]>=0.27.0
30
30
  Requires-Dist: idna>=3.6
31
31
  Requires-Dist: jsonpath-ng>=1.6.0
32
32
  Requires-Dist: lxml>=5.3.0
33
- Requires-Dist: netifaces>=0.11.0
33
+ Requires-Dist: netifaces-plus>=0.12.4
34
34
  Requires-Dist: networkx>=3.2.0
35
35
  Requires-Dist: orjson>=3.9.0
36
36
  Requires-Dist: packaging>=25.0
@@ -40,7 +40,7 @@ Requires-Dist: psutil>=5.9.0
40
40
  Requires-Dist: pycryptodomex>=3.23.0
41
41
  Requires-Dist: pydantic>=2.7.0
42
42
  Requires-Dist: pyopenssl>=25.0.0
43
- Requires-Dist: python-nmap>=0.7.1
43
+ Requires-Dist: python3-nmap>=1.9.1
44
44
  Requires-Dist: pyyaml>=6.0
45
45
  Requires-Dist: rapidfuzz>=3.0.0
46
46
  Requires-Dist: requests>=2.32.0
@@ -100,7 +100,7 @@ Description-Content-Type: text/markdown
100
100
  <!-- Badges -->
101
101
  <p align="center">
102
102
  <a href="https://pypi.org/project/moriarty-project/">
103
- <img src="https://img.shields.io/badge/version-0.1.26-blue" alt="Version 0.1.26">
103
+ <img src="https://img.shields.io/badge/version-0.1.27-blue" alt="Version 0.1.27">
104
104
  </a>
105
105
  <a href="https://www.python.org/downloads/">
106
106
  <img src="https://img.shields.io/pypi/pyversions/moriarty-project?color=blue" alt="Python Versions">
@@ -154,7 +154,7 @@ Description-Content-Type: text/markdown
154
154
  pipx install moriarty-project
155
155
 
156
156
  # OU para instalar uma versão específica
157
- # pipx install moriarty-project==0.1.26
157
+ # pipx install moriarty-project==0.1.27
158
158
 
159
159
  # Verificar a instalação
160
160
  moriarty --help