moriarty-project 0.1.25__py3-none-any.whl → 0.1.26__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- moriarty/__init__.py +1 -1
- moriarty/cli/domain_cmd.py +629 -119
- moriarty/modules/domain_scanner.py +182 -59
- moriarty/modules/port_scanner.py +266 -158
- moriarty/modules/port_scanner_nmap.py +290 -0
- moriarty/modules/web_crawler.py +774 -152
- {moriarty_project-0.1.25.dist-info → moriarty_project-0.1.26.dist-info}/METADATA +5 -3
- {moriarty_project-0.1.25.dist-info → moriarty_project-0.1.26.dist-info}/RECORD +10 -9
- {moriarty_project-0.1.25.dist-info → moriarty_project-0.1.26.dist-info}/WHEEL +0 -0
- {moriarty_project-0.1.25.dist-info → moriarty_project-0.1.26.dist-info}/entry_points.txt +0 -0
moriarty/cli/domain_cmd.py
CHANGED
@@ -5,14 +5,15 @@ import json
|
|
5
5
|
from typing import Optional, Dict, List
|
6
6
|
|
7
7
|
import typer
|
8
|
+
from rich.console import Console
|
9
|
+
from rich.panel import Panel
|
10
|
+
from rich.table import Table, box
|
8
11
|
|
9
12
|
from moriarty.modules.web_crawler import WebCrawler
|
10
|
-
|
11
13
|
from moriarty.modules.port_scanner import PortScanner, PROFILES
|
12
14
|
from moriarty.modules.passive_recon import PassiveRecon
|
13
|
-
from rich.console import Console
|
14
15
|
|
15
|
-
app = typer.Typer(
|
16
|
+
app = typer.Typer(help="🔍 Ferramentas avançadas de reconhecimento de domínios")
|
16
17
|
console = Console()
|
17
18
|
|
18
19
|
|
@@ -61,7 +62,7 @@ def scan_full(
|
|
61
62
|
|
62
63
|
if output:
|
63
64
|
scanner.export(output)
|
64
|
-
console.print(f"[green]✅[/green] Results saved to: [
|
65
|
+
console.print(f"[green]✅[/green] Results saved to: [red]{output}[/red]\n")
|
65
66
|
|
66
67
|
|
67
68
|
@app.command("subdiscover")
|
@@ -101,7 +102,7 @@ def subdomain_discovery(
|
|
101
102
|
from moriarty.modules.subdomain_discovery import SubdomainDiscovery
|
102
103
|
import asyncio
|
103
104
|
|
104
|
-
console.print(f"[bold
|
105
|
+
console.print(f"[bold red]🔍 Descobrindo subdomínios de:[/bold red] {domain}\n")
|
105
106
|
|
106
107
|
discovery = SubdomainDiscovery(
|
107
108
|
domain=domain,
|
@@ -141,7 +142,7 @@ def wayback_urls(
|
|
141
142
|
from moriarty.modules.wayback_discovery import WaybackDiscovery
|
142
143
|
import asyncio
|
143
144
|
|
144
|
-
console.print(f"[bold
|
145
|
+
console.print(f"[bold red]🕰️ Buscando URLs históricas de:[/bold red] {domain}\n")
|
145
146
|
|
146
147
|
wayback = WaybackDiscovery(
|
147
148
|
domain=domain,
|
@@ -181,7 +182,7 @@ def template_scan(
|
|
181
182
|
|
182
183
|
from moriarty.modules.template_scanner import TemplateScanner
|
183
184
|
|
184
|
-
console.print(f"[bold
|
185
|
+
console.print(f"[bold red]📝 Template scan em:[/bold red] {target}\n")
|
185
186
|
|
186
187
|
scanner = TemplateScanner(
|
187
188
|
target=target,
|
@@ -227,7 +228,7 @@ def run_pipeline(
|
|
227
228
|
from moriarty.modules.pipeline_orchestrator import PipelineOrchestrator
|
228
229
|
import asyncio
|
229
230
|
|
230
|
-
console.print(f"[bold
|
231
|
+
console.print(f"[bold red]🔄 Executando pipeline:[/bold red] {pipeline_file}\n")
|
231
232
|
|
232
233
|
orchestrator = PipelineOrchestrator(
|
233
234
|
pipeline_file=pipeline_file,
|
@@ -283,41 +284,7 @@ def stealth_command(
|
|
283
284
|
console.print(f"[red]❌ Ação inválida: {action}[/red]")
|
284
285
|
|
285
286
|
|
286
|
-
|
287
|
-
def port_scan(
|
288
|
-
target: str = typer.Argument(..., help="IP ou domínio"),
|
289
|
-
ports: str = typer.Option("common", "--ports", "-p", help="Portas: common, all, 1-1000, 80,443"),
|
290
|
-
scan_type: str = typer.Option("syn", "--type", "-t", help="Tipo: syn, tcp, udp"),
|
291
|
-
stealth: int = typer.Option(0, "--stealth", "-s", help="Stealth level (0-4)"),
|
292
|
-
output: str = typer.Option(None, "--output", "-o", help="Arquivo de saída"),
|
293
|
-
verbose: bool = typer.Option(False, "--verbose", help="Ativar saída detalhada"),
|
294
|
-
):
|
295
|
-
"""
|
296
|
-
🔌 Scan avançado de portas.
|
297
|
-
|
298
|
-
Exemplos:
|
299
|
-
moriarty domain ports 8.8.8.8
|
300
|
-
moriarty domain ports target.com --ports 1-10000
|
301
|
-
moriarty domain ports target.com --type syn --stealth 3
|
302
|
-
"""
|
303
|
-
from moriarty.modules.port_scanner import PortScanner
|
304
|
-
import asyncio
|
305
|
-
|
306
|
-
console.print(f"[bold cyan]🔌 Port scan em:[/bold cyan] {target}\n")
|
307
|
-
|
308
|
-
scanner = PortScanner(
|
309
|
-
target=target,
|
310
|
-
ports=ports,
|
311
|
-
scan_type=scan_type,
|
312
|
-
stealth_level=stealth,
|
313
|
-
)
|
314
|
-
|
315
|
-
results = asyncio.run(scanner.scan())
|
316
|
-
|
317
|
-
console.print(f"\n[green]✅ {len(results)} portas abertas encontradas[/green]")
|
318
|
-
|
319
|
-
if output:
|
320
|
-
scanner.export(results, output)
|
287
|
+
# A função port_scan foi movida para a implementação mais completa abaixo
|
321
288
|
|
322
289
|
|
323
290
|
@app.command("waf-detect")
|
@@ -342,7 +309,7 @@ def waf_detection(
|
|
342
309
|
from moriarty.modules.waf_detector import WAFDetector
|
343
310
|
import asyncio
|
344
311
|
|
345
|
-
console.print(f"[bold
|
312
|
+
console.print(f"[bold red]🛡️ Detectando WAF em:[/bold red] {target}\n")
|
346
313
|
|
347
314
|
detector = WAFDetector(target=target)
|
348
315
|
waf_info = asyncio.run(detector.detect())
|
@@ -352,7 +319,7 @@ def waf_detection(
|
|
352
319
|
console.print(f"[dim]Confidence: {waf_info['confidence']}%[/dim]")
|
353
320
|
|
354
321
|
if bypass:
|
355
|
-
console.print("\n[
|
322
|
+
console.print("\n[red]🔓 Tentando bypass...[/red]")
|
356
323
|
bypass_methods = asyncio.run(detector.attempt_bypass())
|
357
324
|
|
358
325
|
for method in bypass_methods:
|
@@ -393,12 +360,12 @@ def passive_recon(
|
|
393
360
|
console = Console()
|
394
361
|
|
395
362
|
# Cabeçalho
|
396
|
-
console.print(f"\n[bold
|
363
|
+
console.print(f"\n[bold red]🌐 Moriarty Passive Recon[/bold red]")
|
397
364
|
console.print(f"[dim]Alvo:[/dim] {domain}")
|
398
365
|
console.print(f"[dim]Data:[/dim] {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
|
399
366
|
|
400
367
|
async def _run():
|
401
|
-
with console.status(f"[bold green]Coletando informações sobre [
|
368
|
+
with console.status(f"[bold green]Coletando informações sobre [red]{domain}[/red]..."):
|
402
369
|
recon = PassiveRecon(domain)
|
403
370
|
try:
|
404
371
|
return await recon.collect()
|
@@ -412,15 +379,15 @@ def passive_recon(
|
|
412
379
|
if status_only:
|
413
380
|
sub_count = sum(len(v) for v in payload["subdomains"].values())
|
414
381
|
console.print(f"[bold]🔍 Resumo do Reconhecimento[/bold]")
|
415
|
-
console.print(f" • [
|
416
|
-
console.print(f" • [
|
417
|
-
console.print(f" • [
|
382
|
+
console.print(f" • [red]Subdomínios:[/red] {sub_count} encontrados")
|
383
|
+
console.print(f" • [red]Fontes:[/red] {', '.join(payload['subdomains'].keys()) or 'nenhuma'}")
|
384
|
+
console.print(f" • [red]Credenciais vazadas:[/red] {len(payload['leaks'])}")
|
418
385
|
return
|
419
386
|
|
420
387
|
# Seção de Subdomínios
|
421
388
|
if payload.get("subdomains"):
|
422
389
|
table = Table(title="[bold]🌐 Subdomínios Encontrados[/bold]", box=box.ROUNDED)
|
423
|
-
table.add_column("Subdomínio", style="
|
390
|
+
table.add_column("Subdomínio", style="red")
|
424
391
|
table.add_column("Fonte", style="green")
|
425
392
|
|
426
393
|
for source, subdomains in payload["subdomains"].items():
|
@@ -432,7 +399,7 @@ def passive_recon(
|
|
432
399
|
# Seção de Tecnologias
|
433
400
|
if payload.get("technologies", {}).get("detections"):
|
434
401
|
tech_table = Table(title="[bold]🛠️ Tecnologias Detectadas[/bold]", box=box.ROUNDED)
|
435
|
-
tech_table.add_column("Tecnologia", style="
|
402
|
+
tech_table.add_column("Tecnologia", style="red")
|
436
403
|
tech_table.add_column("Confiança", style="green")
|
437
404
|
tech_table.add_column("Categoria", style="magenta")
|
438
405
|
|
@@ -447,7 +414,7 @@ def passive_recon(
|
|
447
414
|
|
448
415
|
# Seção de Segurança
|
449
416
|
security_table = Table(title="[bold]🔒 Análise de Segurança[/bold]", box=box.ROUNDED)
|
450
|
-
security_table.add_column("Item", style="
|
417
|
+
security_table.add_column("Item", style="red")
|
451
418
|
security_table.add_column("Status", style="green")
|
452
419
|
|
453
420
|
# Verifica HSTS
|
@@ -468,7 +435,7 @@ def passive_recon(
|
|
468
435
|
# Seção de Reputação
|
469
436
|
if payload.get("reputation"):
|
470
437
|
rep_table = Table(title="[bold]📊 Reputação do Domínio[/bold]", box=box.ROUNDED)
|
471
|
-
rep_table.add_column("Fonte", style="
|
438
|
+
rep_table.add_column("Fonte", style="red")
|
472
439
|
rep_table.add_column("Status", style="green")
|
473
440
|
|
474
441
|
for source, data in payload["reputation"].items():
|
@@ -491,7 +458,7 @@ def passive_recon(
|
|
491
458
|
if output:
|
492
459
|
with open(output, "w", encoding="utf-8") as handle:
|
493
460
|
json.dump(payload, handle, indent=2, ensure_ascii=False)
|
494
|
-
console.print(f"\n[green]✓[/green] Resultados salvos em: [
|
461
|
+
console.print(f"\n[green]✓[/green] Resultados salvos em: [red]{output}[/red]")
|
495
462
|
|
496
463
|
except Exception as e:
|
497
464
|
console.print(f"[red]✗ Erro durante o reconhecimento:[/red] {str(e)}")
|
@@ -501,6 +468,384 @@ def passive_recon(
|
|
501
468
|
console.print(f"[yellow]⚠ Log de erro salvo em: {output}[/yellow]")
|
502
469
|
|
503
470
|
|
471
|
+
import asyncio
|
472
|
+
|
473
|
+
async def _port_scan_async(
|
474
|
+
target: str,
|
475
|
+
profile: str,
|
476
|
+
stealth: int,
|
477
|
+
resolve_services: bool,
|
478
|
+
check_vulns: bool,
|
479
|
+
format: str,
|
480
|
+
output: Optional[str]
|
481
|
+
):
|
482
|
+
"""Função assíncrona que realiza a varredura de portas."""
|
483
|
+
# Código da função port_scan aqui...
|
484
|
+
console = Console()
|
485
|
+
|
486
|
+
# Valida os parâmetros
|
487
|
+
def validate_parameters():
|
488
|
+
if stealth < 0 or stealth > 5:
|
489
|
+
console.print("[red]Erro:[/red] O nível de stealth deve estar entre 0 e 5")
|
490
|
+
raise typer.Exit(1)
|
491
|
+
|
492
|
+
if profile not in PROFILES:
|
493
|
+
console.print(f"[red]Erro:[/red] Perfil inválido. Use um dos seguintes: {', '.join(PROFILES.keys())}")
|
494
|
+
raise typer.Exit(1)
|
495
|
+
|
496
|
+
# Configura o console do Rich
|
497
|
+
console = Console()
|
498
|
+
|
499
|
+
# Valida os parâmetros
|
500
|
+
validate_parameters()
|
501
|
+
|
502
|
+
# Usa o perfil especificado ou o padrão 'quick'
|
503
|
+
ports = PROFILES.get(profile.lower(), PROFILES["quick"])
|
504
|
+
|
505
|
+
# Configura o arquivo de saída
|
506
|
+
output_path = Path(output) if output else None
|
507
|
+
if output_path:
|
508
|
+
if output_path.suffix not in (".json", ".txt", ".md"):
|
509
|
+
console.print("[yellow]Aviso:[/yellow] Extensão de arquivo não suportada. Usando .json")
|
510
|
+
output_path = output_path.with_suffix(".json")
|
511
|
+
|
512
|
+
# Configura a barra de progresso
|
513
|
+
progress_columns = [
|
514
|
+
SpinnerColumn(),
|
515
|
+
TextColumn("[progress.description]{task.description}"),
|
516
|
+
BarColumn(bar_width=None),
|
517
|
+
TaskProgressColumn(),
|
518
|
+
TimeElapsedColumn(),
|
519
|
+
]
|
520
|
+
|
521
|
+
# Cabeçalho da varredura
|
522
|
+
console.rule(f"🔍 [bold red]Varredura de Portas[/bold red]")
|
523
|
+
|
524
|
+
# Tabela de informações
|
525
|
+
info_table = Table.grid(padding=(0, 1))
|
526
|
+
info_table.add_row("🌐 Alvo:", f"[bold]{target}")
|
527
|
+
info_table.add_row("🔢 Portas:", f"[red]{ports}")
|
528
|
+
info_table.add_row("🛡️ Nível de stealth:", f"[yellow]{stealth}")
|
529
|
+
info_table.add_row("🔍 Detecção de serviços:", "[green]Ativada[/green]" if resolve_services else "[red]Desativada[/red]")
|
530
|
+
info_table.add_row("⚠️ Verificação de vulnerabilidades:", "[green]Ativada[/green]" if check_vulns else "[red]Desativada[/red]")
|
531
|
+
|
532
|
+
console.print(Panel(info_table, title="[bold]Configuração da Varredura[/bold]", border_style="red"))
|
533
|
+
console.print()
|
534
|
+
|
535
|
+
async def run_scan() -> List[PortScanResult]:
|
536
|
+
"""Executa a varredura de portas."""
|
537
|
+
# Usa sempre TCP Connect que não requer privilégios de root
|
538
|
+
console.print("[yellow]Aviso:[/yellow] Usando varredura TCP Connect (não requer privilégios de root).")
|
539
|
+
scan_type = "tcp"
|
540
|
+
|
541
|
+
# Cria o scanner Nmap
|
542
|
+
scanner = PortScanner(
|
543
|
+
target=target,
|
544
|
+
ports=ports,
|
545
|
+
scan_type=scan_type,
|
546
|
+
stealth_level=stealth,
|
547
|
+
resolve_services=resolve_services,
|
548
|
+
check_vulns=check_vulns,
|
549
|
+
)
|
550
|
+
|
551
|
+
# Executa o scan e retorna os resultados
|
552
|
+
return await scanner.scan()
|
553
|
+
|
554
|
+
console.print("🚀 Iniciando varredura de portas...\n")
|
555
|
+
|
556
|
+
# Executa a varredura com barra de progresso
|
557
|
+
with Progress(*progress_columns) as progress:
|
558
|
+
task = progress.add_task("[red]Escaneando portas...", total=100)
|
559
|
+
|
560
|
+
try:
|
561
|
+
# Executa o scan de forma assíncrona
|
562
|
+
results = await run_scan()
|
563
|
+
progress.update(task, completed=100)
|
564
|
+
|
565
|
+
if not results:
|
566
|
+
console.print("\n[yellow]⚠️ Nenhuma porta aberta detectada[/yellow]")
|
567
|
+
return
|
568
|
+
|
569
|
+
output_format = format.lower()
|
570
|
+
|
571
|
+
if output_format == "json":
|
572
|
+
output_text = json.dumps([r.to_dict() for r in results], indent=2, ensure_ascii=False)
|
573
|
+
if not output_path:
|
574
|
+
console.print_json(data=json.loads(output_text))
|
575
|
+
elif output_format == "markdown":
|
576
|
+
# Formata a saída em Markdown
|
577
|
+
output_lines = [
|
578
|
+
f"# Resultados da varredura de portas em {target}\n",
|
579
|
+
"| Porta | Protocolo | Status | Serviço | Versão | Vulnerabilidades |",
|
580
|
+
"|-------|-----------|--------|---------|---------|------------------|"
|
581
|
+
]
|
582
|
+
|
583
|
+
for result in results:
|
584
|
+
service = getattr(result, 'service', None)
|
585
|
+
status = "🟢 ABERTA" if getattr(result, 'status', '') == "open" else "🔴 FECHADA"
|
586
|
+
service_name = getattr(service, 'name', 'desconhecido') if service else "desconhecido"
|
587
|
+
version = getattr(service, 'version', '') if service else ""
|
588
|
+
vulns = ", ".join(service.vulns) if service and hasattr(service, 'vulns') and service.vulns else "-"
|
589
|
+
|
590
|
+
output_lines.append(
|
591
|
+
f"| {getattr(result, 'port', '')} | "
|
592
|
+
f"{getattr(result, 'protocol', 'tcp').upper()} | "
|
593
|
+
f"{status} | "
|
594
|
+
f"{service_name} | "
|
595
|
+
f"{version} | "
|
596
|
+
f"{vulns if vulns and vulns != '[]' else '-'} |"
|
597
|
+
)
|
598
|
+
|
599
|
+
# Adiciona resumo
|
600
|
+
open_ports = [r for r in results if getattr(r, 'status', '') == "open"]
|
601
|
+
output_text = "\n".join(output_lines)
|
602
|
+
output_text += f"\n\n✅ **Varredura concluída!** {len(open_ports)} porta(s) aberta(s) encontrada(s)."
|
603
|
+
|
604
|
+
if not output_path:
|
605
|
+
console.print(output_text)
|
606
|
+
else: # Formato de texto simples
|
607
|
+
# Cria tabela de resultados
|
608
|
+
table = Table(
|
609
|
+
title=f"🔍 Resultados da varredura em {target}",
|
610
|
+
box=box.ROUNDED,
|
611
|
+
header_style="bold magenta",
|
612
|
+
expand=True
|
613
|
+
)
|
614
|
+
|
615
|
+
# Adiciona colunas
|
616
|
+
table.add_column("Porta", style="red", justify="right")
|
617
|
+
table.add_column("Protocolo", style="magenta")
|
618
|
+
table.add_column("Status", style="green")
|
619
|
+
table.add_column("Serviço", style="yellow")
|
620
|
+
table.add_column("Versão", style="blue")
|
621
|
+
table.add_column("Vulnerabilidades", style="red")
|
622
|
+
|
623
|
+
# Adiciona linhas com os resultados
|
624
|
+
for result in results:
|
625
|
+
service = getattr(result, 'service', None)
|
626
|
+
status = "[green]ABERTA[/green]" if getattr(result, 'status', '') == "open" else "[red]FECHADA[/red]"
|
627
|
+
service_name = getattr(service, 'name', 'desconhecido') if service else "desconhecido"
|
628
|
+
version = getattr(service, 'version', '') if service else ""
|
629
|
+
vulns = ", ".join(service.vulns) if service and hasattr(service, 'vulns') and service.vulns else "-"
|
630
|
+
|
631
|
+
table.add_row(
|
632
|
+
str(getattr(result, 'port', '')),
|
633
|
+
getattr(result, 'protocol', 'tcp').upper(),
|
634
|
+
status,
|
635
|
+
service_name,
|
636
|
+
version,
|
637
|
+
vulns if vulns and vulns != "[]" else "-"
|
638
|
+
)
|
639
|
+
|
640
|
+
# Exibe a tabela
|
641
|
+
console.print(table)
|
642
|
+
|
643
|
+
# Resumo
|
644
|
+
open_ports = [r for r in results if getattr(r, 'status', '') == "open"]
|
645
|
+
console.print(f"\n✅ [bold green]Varredura concluída![/bold green] {len(open_ports)} porta(s) aberta(s) encontrada(s).")
|
646
|
+
|
647
|
+
# Prepara o texto para salvar em arquivo
|
648
|
+
output_text = str(table)
|
649
|
+
|
650
|
+
# Salva em arquivo se especificado
|
651
|
+
if output_path:
|
652
|
+
try:
|
653
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
654
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
655
|
+
f.write(output_text)
|
656
|
+
console.print(f"\n💾 [bold]Resultados salvos em:[/bold] [red]{output_path.absolute()}[/red]")
|
657
|
+
except Exception as e:
|
658
|
+
console.print(f"\n❌ [bold red]Erro ao salvar arquivo:[/bold red] {str(e)}")
|
659
|
+
|
660
|
+
except KeyboardInterrupt:
|
661
|
+
console.print("\n❌ [bold red]Varredura cancelada pelo usuário[/bold red]")
|
662
|
+
raise typer.Exit(1)
|
663
|
+
except Exception as e:
|
664
|
+
console.print(f"\n❌ [bold red]Erro durante a varredura:[/bold red] {str(e)}")
|
665
|
+
if "Errno 8" in str(e) or "Name or service not known" in str(e):
|
666
|
+
console.print("💡 [yellow]Dica:[/yellow] Não foi possível resolver o nome do host. Verifique a conexão com a internet ou o nome do domínio.")
|
667
|
+
if "No module named 'nmap'" in str(e):
|
668
|
+
console.print("💡 [yellow]Dica:[/yellow] O módulo 'python-nmap' não está instalado. Instale com: [red]pip install python-nmap[/red]")
|
669
|
+
raise typer.Exit(1)
|
670
|
+
|
671
|
+
async def run_port_scan(
|
672
|
+
target: str,
|
673
|
+
profile: str,
|
674
|
+
stealth: int,
|
675
|
+
concurrency: int,
|
676
|
+
timeout: float,
|
677
|
+
output: Optional[str],
|
678
|
+
format: str,
|
679
|
+
):
|
680
|
+
"""Função assíncrona que executa a varredura de portas."""
|
681
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn, TimeElapsedColumn
|
682
|
+
from rich.panel import Panel
|
683
|
+
from rich.table import Table
|
684
|
+
from pathlib import Path
|
685
|
+
import json
|
686
|
+
|
687
|
+
# Valida os parâmetros
|
688
|
+
if stealth < 0 or stealth > 5:
|
689
|
+
console.print("[red]Erro:[/red] O nível de stealth deve estar entre 0 e 5")
|
690
|
+
raise typer.Exit(1)
|
691
|
+
|
692
|
+
if profile not in PROFILES:
|
693
|
+
console.print(f"[red]Erro:[/red] Perfil inválido. Use um dos seguintes: {', '.join(PROFILES.keys())}")
|
694
|
+
raise typer.Exit(1)
|
695
|
+
|
696
|
+
# Usa o perfil especificado ou o padrão 'quick'
|
697
|
+
ports = PROFILES.get(profile.lower(), PROFILES["quick"])
|
698
|
+
|
699
|
+
# Configura o arquivo de saída
|
700
|
+
output_path = Path(output) if output else None
|
701
|
+
if output_path:
|
702
|
+
if output_path.suffix not in (".json", ".txt", ".md"):
|
703
|
+
console.print("[yellow]Aviso:[/yellow] Extensão de arquivo não suportada. Usando .json")
|
704
|
+
output_path = output_path.with_suffix(".json")
|
705
|
+
|
706
|
+
# Configura a barra de progresso
|
707
|
+
progress_columns = [
|
708
|
+
SpinnerColumn(),
|
709
|
+
TextColumn("[progress.description]{task.description}"),
|
710
|
+
BarColumn(bar_width=None),
|
711
|
+
TaskProgressColumn(),
|
712
|
+
TimeElapsedColumn(),
|
713
|
+
]
|
714
|
+
|
715
|
+
# Cabeçalho da varredura
|
716
|
+
console.rule(f"🔍 [bold red]Varredura de Portas[/bold red]")
|
717
|
+
|
718
|
+
# Tabela de informações
|
719
|
+
info_table = Table.grid(padding=(0, 1))
|
720
|
+
info_table.add_row("🌐 Alvo:", f"[bold]{target}")
|
721
|
+
info_table.add_row("🔢 Portas:", f"[red]{ports}")
|
722
|
+
info_table.add_row("🛡️ Nível de stealth:", f"[yellow]{stealth}")
|
723
|
+
info_table.add_row("🔍 Detecção de serviços:", "[green]Ativada[/green]")
|
724
|
+
info_table.add_row("⚠️ Verificação de vulnerabilidades:", "[green]Ativada[/green]")
|
725
|
+
|
726
|
+
console.print(Panel(info_table, title="[bold]Configuração da Varredura[/bold]", border_style="red"))
|
727
|
+
console.print()
|
728
|
+
|
729
|
+
# Executa a varredura com barra de progresso
|
730
|
+
with Progress(*progress_columns) as progress:
|
731
|
+
task = progress.add_task("[red]Escaneando portas...", total=100)
|
732
|
+
|
733
|
+
try:
|
734
|
+
# Cria o scanner Nmap
|
735
|
+
scanner = PortScanner(
|
736
|
+
target=target,
|
737
|
+
ports=ports,
|
738
|
+
scan_type="syn" if stealth < 2 else "tcp",
|
739
|
+
stealth_level=stealth,
|
740
|
+
resolve_services=True,
|
741
|
+
check_vulns=True,
|
742
|
+
)
|
743
|
+
|
744
|
+
# Executa o scan de forma assíncrona
|
745
|
+
results = await scanner.scan()
|
746
|
+
progress.update(task, completed=100)
|
747
|
+
|
748
|
+
if not results:
|
749
|
+
console.print("\n[yellow]⚠️ Nenhuma porta aberta detectada[/yellow]")
|
750
|
+
return
|
751
|
+
|
752
|
+
# Formata a saída
|
753
|
+
output_format = format.lower()
|
754
|
+
|
755
|
+
if output_format == "json":
|
756
|
+
output_text = json.dumps([r.to_dict() for r in results], indent=2, ensure_ascii=False)
|
757
|
+
if not output_path:
|
758
|
+
console.print_json(data=json.loads(output_text))
|
759
|
+
elif output_format == "markdown":
|
760
|
+
output_lines = [
|
761
|
+
f"# Resultados da varredura de portas em {target}\n",
|
762
|
+
"| Porta | Protocolo | Status | Serviço | Versão |",
|
763
|
+
"|-------|-----------|--------|---------|---------|"
|
764
|
+
]
|
765
|
+
|
766
|
+
for result in results:
|
767
|
+
service = getattr(result, 'service', None)
|
768
|
+
status = "🟢 ABERTA" if getattr(result, 'status', '') == "open" else "🔴 FECHADA"
|
769
|
+
service_name = getattr(service, 'name', 'desconhecido') if service else "desconhecido"
|
770
|
+
version = getattr(service, 'version', '') if service else ""
|
771
|
+
|
772
|
+
output_lines.append(
|
773
|
+
f"| {getattr(result, 'port', '')} | "
|
774
|
+
f"{getattr(result, 'protocol', 'tcp').upper()} | "
|
775
|
+
f"{status} | "
|
776
|
+
f"{service_name} | "
|
777
|
+
f"{version} |"
|
778
|
+
)
|
779
|
+
|
780
|
+
# Adiciona resumo
|
781
|
+
open_ports = [r for r in results if getattr(r, 'status', '') == "open"]
|
782
|
+
output_text = "\n".join(output_lines)
|
783
|
+
output_text += f"\n\n✅ **Varredura concluída!** {len(open_ports)} porta(s) aberta(s) encontrada(s)."
|
784
|
+
|
785
|
+
if not output_path:
|
786
|
+
console.print(output_text)
|
787
|
+
else: # Formato de texto simples
|
788
|
+
# Cria tabela de resultados
|
789
|
+
table = Table(
|
790
|
+
title=f"🔍 Resultados da varredura em {target}",
|
791
|
+
box=box.ROUNDED,
|
792
|
+
header_style="bold magenta",
|
793
|
+
expand=True
|
794
|
+
)
|
795
|
+
|
796
|
+
# Adiciona colunas
|
797
|
+
table.add_column("Porta", style="red", justify="right")
|
798
|
+
table.add_column("Protocolo", style="magenta")
|
799
|
+
table.add_column("Status", style="green")
|
800
|
+
table.add_column("Serviço", style="yellow")
|
801
|
+
table.add_column("Versão", style="blue")
|
802
|
+
|
803
|
+
# Adiciona linhas com os resultados
|
804
|
+
for result in results:
|
805
|
+
service = getattr(result, 'service', None)
|
806
|
+
status = "[green]ABERTA[/green]" if getattr(result, 'status', '') == "open" else "[red]FECHADA[/red]"
|
807
|
+
service_name = getattr(service, 'name', 'desconhecido') if service else "desconhecido"
|
808
|
+
version = getattr(service, 'version', '') if service else ""
|
809
|
+
|
810
|
+
table.add_row(
|
811
|
+
str(getattr(result, 'port', '')),
|
812
|
+
getattr(result, 'protocol', 'tcp').upper(),
|
813
|
+
status,
|
814
|
+
service_name,
|
815
|
+
version,
|
816
|
+
)
|
817
|
+
|
818
|
+
# Exibe a tabela
|
819
|
+
console.print(table)
|
820
|
+
|
821
|
+
# Resumo
|
822
|
+
open_ports = [r for r in results if getattr(r, 'status', '') == "open"]
|
823
|
+
console.print(f"\n✅ [bold green]Varredura concluída![/bold green] {len(open_ports)} porta(s) aberta(s) encontrada(s).")
|
824
|
+
|
825
|
+
# Prepara o texto para salvar em arquivo
|
826
|
+
output_text = str(table)
|
827
|
+
|
828
|
+
# Salva em arquivo se especificado
|
829
|
+
if output_path:
|
830
|
+
try:
|
831
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
832
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
833
|
+
f.write(output_text)
|
834
|
+
console.print(f"\n💾 [bold]Resultados salvos em:[/bold] [red]{output_path.absolute()}[/red]")
|
835
|
+
except Exception as e:
|
836
|
+
console.print(f"\n❌ [bold red]Erro ao salvar arquivo:[/bold red] {str(e)}")
|
837
|
+
|
838
|
+
except KeyboardInterrupt:
|
839
|
+
console.print("\n❌ [bold red]Varredura cancelada pelo usuário[/bold red]")
|
840
|
+
raise typer.Exit(1)
|
841
|
+
except Exception as e:
|
842
|
+
console.print(f"\n❌ [bold red]Erro durante a varredura:[/bold red] {str(e)}")
|
843
|
+
if "Errno 8" in str(e) or "Name or service not known" in str(e):
|
844
|
+
console.print("💡 [yellow]Dica:[/yellow] Não foi possível resolver o nome do host. Verifique a conexão com a internet ou o nome do domínio.")
|
845
|
+
if "No module named 'nmap'" in str(e):
|
846
|
+
console.print("💡 [yellow]Dica:[/yellow] O módulo 'python-nmap' não está instalado. Instale com: [red]pip install python-nmap[/red]")
|
847
|
+
raise typer.Exit(1)
|
848
|
+
|
504
849
|
@app.command("ports")
|
505
850
|
def port_scan(
|
506
851
|
target: str = typer.Argument(..., help="Domínio ou IP para escanear"),
|
@@ -558,7 +903,7 @@ def port_scan(
|
|
558
903
|
),
|
559
904
|
):
|
560
905
|
"""
|
561
|
-
🔍 Varredura avançada de portas com detecção de serviços e vulnerabilidades.
|
906
|
+
🔍 Varredura avançada de portas com detecção de serviços e vulnerabilidades usando Nmap.
|
562
907
|
|
563
908
|
Exemplos:
|
564
909
|
moriarty domain ports example.com
|
@@ -567,84 +912,218 @@ def port_scan(
|
|
567
912
|
"""
|
568
913
|
import asyncio
|
569
914
|
import json
|
915
|
+
import os
|
916
|
+
import sys
|
570
917
|
from pathlib import Path
|
571
|
-
from
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
918
|
+
from typing import List, Dict, Any, Optional
|
919
|
+
|
920
|
+
from rich.progress import (
|
921
|
+
Progress,
|
922
|
+
SpinnerColumn,
|
923
|
+
BarColumn,
|
924
|
+
TextColumn,
|
925
|
+
TimeElapsedColumn,
|
926
|
+
TaskProgressColumn,
|
927
|
+
)
|
928
|
+
from rich.panel import Panel
|
929
|
+
from rich.table import Table, box
|
930
|
+
from rich.console import Console
|
931
|
+
|
932
|
+
from moriarty.modules.port_scanner_nmap import PortScanner, PortScanResult, ServiceInfo
|
933
|
+
|
934
|
+
# Mapeia os perfis para o formato do Nmap
|
935
|
+
port_profiles = {
|
936
|
+
"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",
|
937
|
+
"web": "80,443,8080,8443,8000,8888,10443,4443",
|
938
|
+
"mail": "25,110,143,465,587,993,995",
|
939
|
+
"db": "1433,1521,27017-27019,28017,3306,5000,5432,5984,6379,8081",
|
940
|
+
"full": "1-1024",
|
941
|
+
"all": "1-65535",
|
942
|
+
}
|
943
|
+
|
944
|
+
# Verifica se o Nmap está instalado
|
945
|
+
def check_nmap_installed() -> bool:
|
946
|
+
"""Verifica se o Nmap está instalado no sistema."""
|
947
|
+
try:
|
948
|
+
import nmap
|
949
|
+
return True
|
950
|
+
except ImportError:
|
951
|
+
return False
|
952
|
+
|
953
|
+
# Valida os parâmetros de entrada
|
954
|
+
def validate_parameters() -> None:
|
955
|
+
"""Valida os parâmetros de entrada."""
|
956
|
+
if not check_nmap_installed():
|
957
|
+
console.print("❌ [bold red]Erro:[/bold red] O módulo 'python-nmap' não está instalado.")
|
958
|
+
console.print("💡 [yellow]Dica:[/yellow] Instale com: pip install python-nmap")
|
959
|
+
raise typer.Exit(1)
|
960
|
+
|
961
|
+
if stealth < 0 or stealth > 5:
|
962
|
+
console.print("❌ [bold red]Erro:[/bold red] O nível de stealth deve estar entre 0 e 5.")
|
963
|
+
raise typer.Exit(1)
|
964
|
+
|
965
|
+
# Inicializa o console do Rich
|
966
|
+
console = Console()
|
967
|
+
|
968
|
+
# Valida os parâmetros
|
969
|
+
validate_parameters()
|
970
|
+
|
971
|
+
# Usa o perfil especificado ou o padrão 'quick'
|
972
|
+
ports = port_profiles.get(profile.lower(), port_profiles["quick"])
|
973
|
+
|
974
|
+
# Configura o arquivo de saída
|
975
|
+
output_path = Path(output) if output else None
|
976
|
+
if output_path:
|
977
|
+
if output_path.suffix not in (".json", ".txt", ".md"):
|
581
978
|
console.print("[yellow]Aviso:[/yellow] Extensão de arquivo não suportada. Usando .json")
|
582
|
-
|
583
|
-
|
584
|
-
|
979
|
+
output_path = output_path.with_suffix(".json")
|
980
|
+
|
981
|
+
# Configura a barra de progresso
|
982
|
+
progress_columns = [
|
983
|
+
SpinnerColumn(),
|
984
|
+
TextColumn("[progress.description]{task.description}"),
|
985
|
+
BarColumn(bar_width=None),
|
986
|
+
TaskProgressColumn(),
|
987
|
+
TimeElapsedColumn(),
|
988
|
+
]
|
989
|
+
|
990
|
+
# Cabeçalho da varredura
|
991
|
+
console.rule(f"🔍 [bold red]Varredura de Portas[/bold red]")
|
992
|
+
|
993
|
+
# Tabela de informações
|
994
|
+
info_table = Table.grid(padding=(0, 1))
|
995
|
+
info_table.add_row("🌐 Alvo:", f"[bold]{target}")
|
996
|
+
info_table.add_row("🔢 Portas:", f"[red]{ports}")
|
997
|
+
info_table.add_row("🛡️ Nível de stealth:", f"[yellow]{stealth}")
|
998
|
+
info_table.add_row("🔍 Detecção de serviços:", "[green]Ativada[/green]" if resolve_services else "[red]Desativada[/red]")
|
999
|
+
info_table.add_row("⚠️ Verificação de vulnerabilidades:", "[green]Ativada[/green]" if check_vulns else "[red]Desativada[/red]")
|
1000
|
+
|
1001
|
+
console.print(Panel(info_table, title="[bold]Configuração da Varredura[/bold]", border_style="red"))
|
1002
|
+
console.print()
|
1003
|
+
|
1004
|
+
async def run_scan() -> List[PortScanResult]:
|
1005
|
+
"""Executa a varredura de portas."""
|
1006
|
+
# Usa sempre TCP Connect que não requer privilégios de root
|
1007
|
+
console.print("[yellow]Aviso:[/yellow] Usando varredura TCP Connect (não requer privilégios de root).")
|
1008
|
+
scan_type = "tcp"
|
1009
|
+
|
1010
|
+
# Cria o scanner Nmap
|
585
1011
|
scanner = PortScanner(
|
586
1012
|
target=target,
|
587
|
-
|
588
|
-
|
589
|
-
timeout=timeout,
|
1013
|
+
ports=ports,
|
1014
|
+
scan_type=scan_type,
|
590
1015
|
stealth_level=stealth,
|
591
1016
|
resolve_services=resolve_services,
|
592
1017
|
check_vulns=check_vulns,
|
593
1018
|
)
|
1019
|
+
|
1020
|
+
# Executa o scan e retorna os resultados
|
594
1021
|
return await scanner.scan()
|
595
1022
|
|
596
|
-
|
597
|
-
console.print(f"[bold]🔍 Iniciando varredura em:[/bold] {target}")
|
598
|
-
console.print(f"📊 Perfil: {profile} (portas: {len(PROFILES[profile])})")
|
599
|
-
console.print(f"🕵️ Nível de stealth: {stealth}")
|
1023
|
+
console.print("🚀 Iniciando varredura de portas...\n")
|
600
1024
|
|
601
|
-
|
602
|
-
|
1025
|
+
# Função para salvar resultados em diferentes formatos
|
1026
|
+
def save_results(results, output_path, format):
|
1027
|
+
output_format = format.lower()
|
603
1028
|
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
# Formata a saída
|
609
|
-
if format.lower() == "json":
|
610
|
-
output_text = json.dumps([r.to_dict() for r in results], indent=2)
|
611
|
-
if not output:
|
612
|
-
console.print_json(data=json.loads(output_text))
|
613
|
-
else:
|
614
|
-
output_text = format_scan_results(results, output_format=format)
|
615
|
-
if not output:
|
616
|
-
console.print(output_text)
|
617
|
-
|
618
|
-
# Salva em arquivo se especificado
|
619
|
-
if output:
|
620
|
-
output.parent.mkdir(parents=True, exist_ok=True)
|
621
|
-
with open(output, "w", encoding="utf-8") as f:
|
622
|
-
if output.suffix == ".json":
|
623
|
-
json.dump([r.to_dict() for r in results], f, indent=2, ensure_ascii=False)
|
624
|
-
else:
|
1029
|
+
try:
|
1030
|
+
if output_format == "json":
|
1031
|
+
output_text = json.dumps([r.to_dict() for r in results], indent=2, ensure_ascii=False)
|
1032
|
+
with open(output_path, 'w', encoding='utf-8') as f:
|
625
1033
|
f.write(output_text)
|
626
|
-
|
1034
|
+
elif output_format == "markdown":
|
1035
|
+
with open(output_path, 'w', encoding='utf-8') as f:
|
1036
|
+
f.write("# Resultados da Varredura de Portas\n\n")
|
1037
|
+
f.write("| Porta | Estado | Serviço | Versão |\n")
|
1038
|
+
f.write("|-------|--------|---------|---------|\n")
|
1039
|
+
for result in results:
|
1040
|
+
if result.state == 'open':
|
1041
|
+
service = result.service or "desconhecido"
|
1042
|
+
version = result.version or ""
|
1043
|
+
f.write(f"| {result.port} | ABERTA | {service} | {version} |\n")
|
1044
|
+
else: # text
|
1045
|
+
with open(output_path, 'w', encoding='utf-8') as f:
|
1046
|
+
for result in results:
|
1047
|
+
if result.state == 'open':
|
1048
|
+
service = result.service or "desconhecido"
|
1049
|
+
version = f" ({result.version})" if result.version else ""
|
1050
|
+
f.write(f"Porta {result.port}/tcp: ABERTA - {service}{version}\n")
|
1051
|
+
except Exception as e:
|
1052
|
+
console.print(f"❌ [bold red]Erro ao salvar o arquivo:[/bold red] {str(e)}")
|
1053
|
+
raise typer.Exit(1)
|
627
1054
|
|
628
|
-
|
629
|
-
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
|
1055
|
+
# Executa o scan de forma assíncrona
|
1056
|
+
with Progress(*progress_columns) as progress:
|
1057
|
+
task = progress.add_task("[red]Escaneando portas...", total=100)
|
1058
|
+
|
1059
|
+
try:
|
1060
|
+
# Executa a função assíncrona
|
1061
|
+
results = asyncio.run(run_scan())
|
1062
|
+
progress.update(task, completed=100)
|
1063
|
+
|
1064
|
+
# Exibe os resultados
|
1065
|
+
if results:
|
1066
|
+
# Cria uma tabela para exibir os resultados
|
1067
|
+
table = Table(title="[bold]Resultados da Varredura[/bold]")
|
1068
|
+
table.add_column("Porta", style="red", no_wrap=True)
|
1069
|
+
table.add_column("Estado", style="green")
|
1070
|
+
table.add_column("Serviço", style="yellow")
|
1071
|
+
table.add_column("Versão", style="magenta")
|
1072
|
+
|
1073
|
+
for result in results:
|
1074
|
+
if result.state == 'open':
|
1075
|
+
service = result.service or "desconhecido"
|
1076
|
+
version = result.version or ""
|
1077
|
+
table.add_row(
|
1078
|
+
str(result.port),
|
1079
|
+
"[green]ABERTA[/green]",
|
1080
|
+
service,
|
1081
|
+
version
|
1082
|
+
)
|
1083
|
+
|
1084
|
+
console.print()
|
1085
|
+
console.print(table)
|
1086
|
+
|
1087
|
+
# Salva os resultados em um arquivo, se solicitado
|
1088
|
+
if output_path:
|
1089
|
+
save_results(results, output_path, format)
|
1090
|
+
console.print(f"\n✅ Resultados salvos em: [bold red]{output_path}[/bold red]")
|
1091
|
+
|
1092
|
+
# Exibe saída JSON no console se não houver arquivo de saída e o formato for JSON
|
1093
|
+
if format.lower() == "json" and not output_path:
|
1094
|
+
output_text = json.dumps([r.to_dict() for r in results], indent=2, ensure_ascii=False)
|
1095
|
+
console.print_json(data=json.loads(output_text))
|
1096
|
+
|
1097
|
+
else:
|
1098
|
+
console.print("\n❌ Nenhuma porta aberta encontrada ou ocorreu um erro durante a varredura.")
|
1099
|
+
|
1100
|
+
except Exception as e:
|
1101
|
+
progress.update(task, completed=100)
|
1102
|
+
console.print(f"\n❌ [bold red]Erro durante a varredura:[/bold red] {str(e)}")
|
1103
|
+
if "Errno 8" in str(e) or "Name or service not known" in str(e):
|
1104
|
+
console.print("💡 [yellow]Dica:[/yellow] Não foi possível resolver o nome do host. Verifique a conexão com a internet ou o nome do domínio.")
|
1105
|
+
raise typer.Exit(1)
|
634
1106
|
|
635
1107
|
|
636
1108
|
@app.command("crawl")
|
637
1109
|
def crawl(
|
638
1110
|
domain: str = typer.Argument(..., help="Alvo base (ex: https://example.com)"),
|
639
|
-
max_pages: int = typer.Option(100, "--max-pages", help="Máximo de páginas"),
|
640
1111
|
max_depth: int = typer.Option(2, "--max-depth", help="Profundidade máxima"),
|
1112
|
+
max_pages: int = typer.Option(100, "--max-pages", help="Número máximo de páginas a serem rastreadas"),
|
641
1113
|
concurrency: int = typer.Option(10, "--concurrency", help="Workers paralelos"),
|
642
1114
|
follow_subdomains: bool = typer.Option(False, "--follow-subdomains", help="Seguir subdomínios"),
|
643
1115
|
output: Optional[str] = typer.Option(None, "--output", "-o", help="Arquivo JSON de saída"),
|
644
1116
|
):
|
645
|
-
"""Crawler leve para mapear formulários e rotas.
|
1117
|
+
"""Crawler leve para mapear formulários e rotas.
|
1118
|
+
|
1119
|
+
Exemplos:
|
1120
|
+
moriarty domain crawl https://example.com
|
1121
|
+
moriarty domain crawl https://example.com --max-depth 3 --max-pages 50
|
1122
|
+
"""
|
646
1123
|
import asyncio
|
647
1124
|
import json
|
1125
|
+
from typing import Dict, Any
|
1126
|
+
from pathlib import Path
|
648
1127
|
|
649
1128
|
async def _run():
|
650
1129
|
crawler = WebCrawler(
|
@@ -659,15 +1138,46 @@ def crawl(
|
|
659
1138
|
finally:
|
660
1139
|
await crawler.close()
|
661
1140
|
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
1141
|
+
try:
|
1142
|
+
console.print(f"🚀 Iniciando crawler em [red]{domain}[/red]...")
|
1143
|
+
pages = asyncio.run(_run())
|
1144
|
+
|
1145
|
+
# Cria um dicionário seguro para serialização
|
1146
|
+
safe_pages = {}
|
1147
|
+
for url, page in pages.items():
|
1148
|
+
safe_pages[url] = {
|
1149
|
+
'url': url,
|
1150
|
+
'title': getattr(page, 'title', ''),
|
1151
|
+
'forms': [form.__dict__ for form in getattr(page, 'forms', [])],
|
1152
|
+
'links': getattr(page, 'links', []),
|
1153
|
+
'status_code': getattr(page, 'status_code', 0)
|
1154
|
+
}
|
1155
|
+
|
1156
|
+
# Exibe resumo
|
1157
|
+
summary = {
|
1158
|
+
"total_pages": len(pages),
|
1159
|
+
"forms": sum(len(page.get('forms', [])) for page in safe_pages.values()),
|
1160
|
+
"links_unicos": len({link for page in safe_pages.values() for link in page.get('links', [])})
|
1161
|
+
}
|
1162
|
+
|
1163
|
+
console.print("\n📊 [bold]Resumo do Crawling:[/bold]")
|
1164
|
+
console.print(f"• Páginas processadas: {summary['total_pages']}")
|
1165
|
+
console.print(f"• Formulários encontrados: {summary['forms']}")
|
1166
|
+
console.print(f"• Links únicos: {summary['links_unicos']}")
|
668
1167
|
|
669
|
-
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
|
1168
|
+
# Salva em arquivo se especificado
|
1169
|
+
if output:
|
1170
|
+
output_path = Path(output)
|
1171
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
1172
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
1173
|
+
json.dump({
|
1174
|
+
"summary": summary,
|
1175
|
+
"pages": safe_pages
|
1176
|
+
}, f, indent=2, ensure_ascii=False)
|
1177
|
+
console.print(f"\n💾 [bold]Resultados salvos em:[/bold] [red]{output_path.absolute()}[/red]")
|
1178
|
+
|
1179
|
+
except Exception as e:
|
1180
|
+
console.print(f"\n❌ [bold red]Erro durante o crawling:[/bold red] {str(e)}")
|
1181
|
+
if "Failed to establish a new connection" in str(e):
|
1182
|
+
console.print("💡 [yellow]Dica:[/yellow] Verifique sua conexão com a internet ou a URL fornecida.")
|
1183
|
+
raise typer.Exit(1)
|