vamp-easm 1.1__tar.gz → 1.2__tar.gz
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.
- vamp_easm-1.2/LICENSE +21 -0
- {vamp_easm-1.1 → vamp_easm-1.2}/PKG-INFO +3 -1
- {vamp_easm-1.1 → vamp_easm-1.2}/pyproject.toml +1 -1
- {vamp_easm-1.1 → vamp_easm-1.2}/vamp_easm.egg-info/PKG-INFO +3 -1
- {vamp_easm-1.1 → vamp_easm-1.2}/vamp_easm.egg-info/SOURCES.txt +1 -0
- {vamp_easm-1.1 → vamp_easm-1.2}/vamp_easm.py +294 -6
- {vamp_easm-1.1 → vamp_easm-1.2}/README.md +0 -0
- {vamp_easm-1.1 → vamp_easm-1.2}/easm/__init__.py +0 -0
- {vamp_easm-1.1 → vamp_easm-1.2}/easm/alerter.py +0 -0
- {vamp_easm-1.1 → vamp_easm-1.2}/easm/differ.py +0 -0
- {vamp_easm-1.1 → vamp_easm-1.2}/easm/scanner.py +0 -0
- {vamp_easm-1.1 → vamp_easm-1.2}/easm/storage.py +0 -0
- {vamp_easm-1.1 → vamp_easm-1.2}/setup.cfg +0 -0
- {vamp_easm-1.1 → vamp_easm-1.2}/vamp_easm.egg-info/dependency_links.txt +0 -0
- {vamp_easm-1.1 → vamp_easm-1.2}/vamp_easm.egg-info/entry_points.txt +0 -0
- {vamp_easm-1.1 → vamp_easm-1.2}/vamp_easm.egg-info/requires.txt +0 -0
- {vamp_easm-1.1 → vamp_easm-1.2}/vamp_easm.egg-info/top_level.txt +0 -0
- {vamp_easm-1.1 → vamp_easm-1.2}/vampsec_report.py +0 -0
vamp_easm-1.2/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 VampSecure Studios
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vamp-easm
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2
|
|
4
4
|
Summary: External Attack Surface Management (EASM) scanner for authorized audits
|
|
5
5
|
Author-email: VampSecure Studios <contact@vampsecurestudios.com>
|
|
6
6
|
License: MIT
|
|
@@ -16,7 +16,9 @@ Classifier: Programming Language :: Python :: 3
|
|
|
16
16
|
Classifier: Topic :: Security
|
|
17
17
|
Requires-Python: >=3.9
|
|
18
18
|
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
19
20
|
Requires-Dist: rich>=13.7.0
|
|
21
|
+
Dynamic: license-file
|
|
20
22
|
|
|
21
23
|
# vamp-easm
|
|
22
24
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vamp-easm
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2
|
|
4
4
|
Summary: External Attack Surface Management (EASM) scanner for authorized audits
|
|
5
5
|
Author-email: VampSecure Studios <contact@vampsecurestudios.com>
|
|
6
6
|
License: MIT
|
|
@@ -16,7 +16,9 @@ Classifier: Programming Language :: Python :: 3
|
|
|
16
16
|
Classifier: Topic :: Security
|
|
17
17
|
Requires-Python: >=3.9
|
|
18
18
|
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
19
20
|
Requires-Dist: rich>=13.7.0
|
|
21
|
+
Dynamic: license-file
|
|
20
22
|
|
|
21
23
|
# vamp-easm
|
|
22
24
|
|
|
@@ -57,6 +57,7 @@ from __future__ import annotations
|
|
|
57
57
|
import argparse
|
|
58
58
|
import json
|
|
59
59
|
import sys
|
|
60
|
+
import time
|
|
60
61
|
from datetime import datetime, timezone
|
|
61
62
|
from typing import List, Optional
|
|
62
63
|
|
|
@@ -75,7 +76,7 @@ from vampsec_report import (
|
|
|
75
76
|
# Constantes
|
|
76
77
|
# ---------------------------------------------------------------------------
|
|
77
78
|
|
|
78
|
-
VERSION = "1.
|
|
79
|
+
VERSION = "1.2"
|
|
79
80
|
TOOL = "vamp-easm"
|
|
80
81
|
BRAND = "VampSecure Labs — EASM Continuo"
|
|
81
82
|
|
|
@@ -86,7 +87,7 @@ BANNER = (
|
|
|
86
87
|
" \\ V / _ \\| |\\/| | _/\\__ \\ _| (__| |_| | / _|| |__ / _ \\| _ \\__ \\\n"
|
|
87
88
|
" \\_/_/ \\_\\_| |_|_| |___/___\\___|\\___/|_|_\\___|____/_/ \\_\\___/___/\n"
|
|
88
89
|
' by Antonio Hernandez "Belky" — VampSecure Studios\n'
|
|
89
|
-
" vamp-easm v1.
|
|
90
|
+
" vamp-easm v1.2 · External Attack Surface Management\n"
|
|
90
91
|
" ────────────────────────────────────────────────────────────────────────\n"
|
|
91
92
|
" USO EXCLUSIVO EN AUDITORÍAS AUTORIZADAS · El uso no autorizado es ilegal\n"
|
|
92
93
|
)
|
|
@@ -454,6 +455,243 @@ def cmd_export(args: argparse.Namespace) -> int:
|
|
|
454
455
|
return 0
|
|
455
456
|
|
|
456
457
|
|
|
458
|
+
# ---------------------------------------------------------------------------
|
|
459
|
+
# Comando: watch
|
|
460
|
+
# ---------------------------------------------------------------------------
|
|
461
|
+
|
|
462
|
+
def _escanear_target(
|
|
463
|
+
target: str,
|
|
464
|
+
puertos: List[int],
|
|
465
|
+
usar_nmap: bool,
|
|
466
|
+
alert_webhook: Optional[str],
|
|
467
|
+
) -> List[differ.Diff]:
|
|
468
|
+
"""
|
|
469
|
+
Ejecuta un escaneo completo del target, persiste en SQLite y devuelve
|
|
470
|
+
los diffs calculados frente al escaneo inmediatamente anterior.
|
|
471
|
+
|
|
472
|
+
Función interna compartida por cmd_scan (indirectamente) y cmd_watch.
|
|
473
|
+
"""
|
|
474
|
+
storage.inicializar_db()
|
|
475
|
+
scan_id = storage.nuevo_scan(target)
|
|
476
|
+
|
|
477
|
+
resultado = scanner.ejecutar_escaneo(
|
|
478
|
+
target=target,
|
|
479
|
+
puertos=puertos,
|
|
480
|
+
usar_nmap=usar_nmap,
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
# Persistir subdominios
|
|
484
|
+
for sub in resultado.subdominios:
|
|
485
|
+
ip = sub.ips[0] if sub.ips else None
|
|
486
|
+
storage.upsert_asset(target=target, subdominio=sub.nombre,
|
|
487
|
+
scan_id=scan_id, ip=ip)
|
|
488
|
+
|
|
489
|
+
# Persistir puertos
|
|
490
|
+
for pa in resultado.puertos:
|
|
491
|
+
ip_pa = None
|
|
492
|
+
for sub in resultado.subdominios:
|
|
493
|
+
if sub.nombre == pa.host:
|
|
494
|
+
ip_pa = sub.ips[0] if sub.ips else None
|
|
495
|
+
break
|
|
496
|
+
storage.upsert_asset(target=target, subdominio=pa.host,
|
|
497
|
+
scan_id=scan_id, ip=ip_pa,
|
|
498
|
+
puerto=pa.puerto, protocolo=pa.protocolo,
|
|
499
|
+
servicio=pa.servicio)
|
|
500
|
+
|
|
501
|
+
# Persistir certificados
|
|
502
|
+
for cert in resultado.certs:
|
|
503
|
+
if cert.error:
|
|
504
|
+
continue
|
|
505
|
+
storage.insertar_cert(target=target, host=cert.host,
|
|
506
|
+
scan_id=scan_id, issued_to=cert.issued_to,
|
|
507
|
+
issuer=cert.issuer, not_after=cert.not_after,
|
|
508
|
+
sans=",".join(cert.sans) if cert.sans else None,
|
|
509
|
+
fingerprint=cert.fingerprint,
|
|
510
|
+
autofirmado=cert.autofirmado)
|
|
511
|
+
|
|
512
|
+
# Calcular diffs frente al escaneo anterior
|
|
513
|
+
historial = storage.historial_scans(target, limit=5)
|
|
514
|
+
scan_id_prev: Optional[str] = None
|
|
515
|
+
for row in historial:
|
|
516
|
+
if row["id"] != scan_id:
|
|
517
|
+
scan_id_prev = row["id"]
|
|
518
|
+
break
|
|
519
|
+
|
|
520
|
+
motor = differ.MotorDiff(target=target, scan_id=scan_id,
|
|
521
|
+
scan_id_prev=scan_id_prev, storage=storage)
|
|
522
|
+
diffs = motor.calcular()
|
|
523
|
+
|
|
524
|
+
assets_total = len(resultado.subdominios) + len(resultado.puertos)
|
|
525
|
+
storage.cerrar_scan(scan_id, assets_found=assets_total,
|
|
526
|
+
diffs_found=len(diffs))
|
|
527
|
+
|
|
528
|
+
# Gestionar alertas solo si hay cambios
|
|
529
|
+
if diffs:
|
|
530
|
+
webhook_url = alerter.resolver_webhook_url(alert_webhook)
|
|
531
|
+
alerter.gestionar_alertas(webhook_url, target, diffs, scan_id)
|
|
532
|
+
|
|
533
|
+
return diffs
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def cmd_watch(args: argparse.Namespace) -> int:
|
|
537
|
+
"""
|
|
538
|
+
Modo watch continuo: ejecuta escaneos periódicos del target, compara
|
|
539
|
+
con el resultado anterior almacenado en SQLite e imprime solo los
|
|
540
|
+
cambios detectados (nuevos puertos, subdominios, certs, etc.).
|
|
541
|
+
|
|
542
|
+
El webhook (--alert-webhook) solo se dispara cuando hay diffs.
|
|
543
|
+
El bucle continúa hasta interrupción manual (Ctrl+C).
|
|
544
|
+
|
|
545
|
+
Returns
|
|
546
|
+
-------
|
|
547
|
+
int : Siempre 0 al salir por interrupción.
|
|
548
|
+
"""
|
|
549
|
+
target = args.target
|
|
550
|
+
interval = getattr(args, "interval", 3600)
|
|
551
|
+
webhook = getattr(args, "alert_webhook", None)
|
|
552
|
+
|
|
553
|
+
# Determinar lista de puertos
|
|
554
|
+
if getattr(args, "ports", None) and args.ports != "top100":
|
|
555
|
+
try:
|
|
556
|
+
puertos = [int(p.strip()) for p in args.ports.split(",") if p.strip()]
|
|
557
|
+
except ValueError:
|
|
558
|
+
console.print("[red]Error:[/red] --ports debe ser 'top100' o lista CSV (ej: 22,80,443)")
|
|
559
|
+
return 1
|
|
560
|
+
else:
|
|
561
|
+
puertos = scanner.TOP_100_PORTS
|
|
562
|
+
|
|
563
|
+
console.print(Panel(
|
|
564
|
+
f"[bold cyan]vamp-easm v{VERSION}[/bold cyan] · {BRAND}\n"
|
|
565
|
+
f"Modo: [bold]WATCH[/bold] · Target: [bold]{target}[/bold] · "
|
|
566
|
+
f"Intervalo: {interval}s · {_ts_ahora()}",
|
|
567
|
+
border_style="yellow",
|
|
568
|
+
))
|
|
569
|
+
|
|
570
|
+
iteracion = 0
|
|
571
|
+
try:
|
|
572
|
+
while True:
|
|
573
|
+
iteracion += 1
|
|
574
|
+
ts_scan = _ts_ahora()
|
|
575
|
+
console.print(
|
|
576
|
+
f"\n[yellow]►[/yellow] [bold]Escaneo #{iteracion}[/bold] — {ts_scan}"
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
diffs = _escanear_target(
|
|
580
|
+
target=target,
|
|
581
|
+
puertos=puertos,
|
|
582
|
+
usar_nmap=getattr(args, "nmap", False),
|
|
583
|
+
alert_webhook=webhook,
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
if diffs:
|
|
587
|
+
console.print(_tabla_diffs(diffs))
|
|
588
|
+
else:
|
|
589
|
+
console.print(
|
|
590
|
+
"[green]✓ Sin cambios respecto al escaneo anterior.[/green]"
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
console.print(
|
|
594
|
+
f"[dim]Próximo escaneo en {interval}s "
|
|
595
|
+
f"(Ctrl+C para detener)[/dim]"
|
|
596
|
+
)
|
|
597
|
+
time.sleep(interval)
|
|
598
|
+
|
|
599
|
+
except KeyboardInterrupt:
|
|
600
|
+
console.print("\n[yellow]Watch detenido por el usuario.[/yellow]")
|
|
601
|
+
|
|
602
|
+
return 0
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
# ---------------------------------------------------------------------------
|
|
606
|
+
# Comando: diff
|
|
607
|
+
# ---------------------------------------------------------------------------
|
|
608
|
+
|
|
609
|
+
def cmd_diff(args: argparse.Namespace) -> int:
|
|
610
|
+
"""
|
|
611
|
+
Muestra todos los cambios detectados entre dos snapshots almacenados.
|
|
612
|
+
|
|
613
|
+
--from y --to aceptan fechas en formato YYYY-MM-DD o YYYY-MM-DDTHH:MM.
|
|
614
|
+
Se busca el escaneo cuyo ts_start sea el más cercano (posterior) a cada
|
|
615
|
+
fecha indicada.
|
|
616
|
+
|
|
617
|
+
Returns
|
|
618
|
+
-------
|
|
619
|
+
int : 0 sin diffs, 1 con diffs HIGH, 2 con diffs CRITICAL.
|
|
620
|
+
"""
|
|
621
|
+
storage.inicializar_db()
|
|
622
|
+
|
|
623
|
+
target = args.target
|
|
624
|
+
fecha_from = getattr(args, "from_date", None) or ""
|
|
625
|
+
fecha_to = getattr(args, "to_date", None) or ""
|
|
626
|
+
|
|
627
|
+
# Obtener historial completo del target (máx 1000 para cubrir rango amplio)
|
|
628
|
+
historial = storage.historial_scans(target, limit=1000)
|
|
629
|
+
if not historial:
|
|
630
|
+
console.print(f"[red]Error:[/red] Sin historial para {target}")
|
|
631
|
+
return 1
|
|
632
|
+
|
|
633
|
+
def _scan_id_por_fecha(fecha_prefix: str, historial_rows) -> Optional[str]:
|
|
634
|
+
"""
|
|
635
|
+
Devuelve el scan_id cuyo ts_start comienza con fecha_prefix.
|
|
636
|
+
Si no hay match exacto, devuelve el más antiguo posterior a ese prefijo.
|
|
637
|
+
"""
|
|
638
|
+
if not fecha_prefix:
|
|
639
|
+
return None
|
|
640
|
+
for row in reversed(historial_rows): # orden cronológico ascendente
|
|
641
|
+
if row["ts_start"] and row["ts_start"] >= fecha_prefix:
|
|
642
|
+
return row["id"]
|
|
643
|
+
return None
|
|
644
|
+
|
|
645
|
+
scan_id_from = _scan_id_por_fecha(fecha_from, historial)
|
|
646
|
+
scan_id_to = _scan_id_por_fecha(fecha_to, historial)
|
|
647
|
+
|
|
648
|
+
if not scan_id_from:
|
|
649
|
+
console.print(
|
|
650
|
+
f"[red]Error:[/red] No se encontró escaneo en o posterior a "
|
|
651
|
+
f"[bold]{fecha_from}[/bold] para {target}"
|
|
652
|
+
)
|
|
653
|
+
return 1
|
|
654
|
+
|
|
655
|
+
if not scan_id_to:
|
|
656
|
+
console.print(
|
|
657
|
+
f"[red]Error:[/red] No se encontró escaneo en o posterior a "
|
|
658
|
+
f"[bold]{fecha_to}[/bold] para {target}"
|
|
659
|
+
)
|
|
660
|
+
return 1
|
|
661
|
+
|
|
662
|
+
if scan_id_from == scan_id_to:
|
|
663
|
+
console.print("[yellow]⚠ Los dos snapshots apuntan al mismo escaneo — sin diff.[/yellow]")
|
|
664
|
+
return 0
|
|
665
|
+
|
|
666
|
+
console.print(Panel(
|
|
667
|
+
f"[bold cyan]vamp-easm v{VERSION}[/bold cyan] · {BRAND}\n"
|
|
668
|
+
f"Target: [bold]{target}[/bold] · Diff acumulado\n"
|
|
669
|
+
f"Desde: [dim]{fecha_from}[/dim] (scan {scan_id_from[:8]})\n"
|
|
670
|
+
f"Hasta: [dim]{fecha_to}[/dim] (scan {scan_id_to[:8]})",
|
|
671
|
+
border_style="dim",
|
|
672
|
+
))
|
|
673
|
+
|
|
674
|
+
motor = differ.MotorDiff(
|
|
675
|
+
target=target,
|
|
676
|
+
scan_id=scan_id_to,
|
|
677
|
+
scan_id_prev=scan_id_from,
|
|
678
|
+
storage=storage,
|
|
679
|
+
)
|
|
680
|
+
diffs = motor.calcular()
|
|
681
|
+
|
|
682
|
+
if diffs:
|
|
683
|
+
console.print(_tabla_diffs(diffs))
|
|
684
|
+
else:
|
|
685
|
+
console.print("[green]✓ Sin cambios entre los dos snapshots indicados.[/green]")
|
|
686
|
+
|
|
687
|
+
sevs = {d.severidad for d in diffs}
|
|
688
|
+
if "CRITICAL" in sevs:
|
|
689
|
+
return 2
|
|
690
|
+
if "HIGH" in sevs:
|
|
691
|
+
return 1
|
|
692
|
+
return 0
|
|
693
|
+
|
|
694
|
+
|
|
457
695
|
# ---------------------------------------------------------------------------
|
|
458
696
|
# CLI — definición de argumentos
|
|
459
697
|
# ---------------------------------------------------------------------------
|
|
@@ -469,11 +707,13 @@ def construir_parser() -> argparse.ArgumentParser:
|
|
|
469
707
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
470
708
|
epilog=(
|
|
471
709
|
"Ejemplos:\n"
|
|
472
|
-
" python vamp_easm.py scan
|
|
473
|
-
" python vamp_easm.py scan
|
|
710
|
+
" python vamp_easm.py scan --target ejemplo.com\n"
|
|
711
|
+
" python vamp_easm.py scan --target ejemplo.com --ports 22,80,443 --html\n"
|
|
474
712
|
" python vamp_easm.py history --target ejemplo.com --limit 5\n"
|
|
475
|
-
" python vamp_easm.py assets
|
|
476
|
-
" python vamp_easm.py export
|
|
713
|
+
" python vamp_easm.py assets --target ejemplo.com\n"
|
|
714
|
+
" python vamp_easm.py export --target ejemplo.com --json\n"
|
|
715
|
+
" python vamp_easm.py watch --target ejemplo.com --interval 1800\n"
|
|
716
|
+
" python vamp_easm.py diff --target ejemplo.com --from 2026-09-01 --to 2026-09-17\n"
|
|
477
717
|
),
|
|
478
718
|
)
|
|
479
719
|
parser.add_argument(
|
|
@@ -483,6 +723,7 @@ def construir_parser() -> argparse.ArgumentParser:
|
|
|
483
723
|
|
|
484
724
|
subparsers = parser.add_subparsers(dest="comando", help="Subcomando a ejecutar")
|
|
485
725
|
|
|
726
|
+
|
|
486
727
|
# ── scan ─────────────────────────────────────────────────────────────
|
|
487
728
|
p_scan = subparsers.add_parser("scan", help="Escanear un target y calcular diffs")
|
|
488
729
|
p_scan.add_argument(
|
|
@@ -527,6 +768,51 @@ def construir_parser() -> argparse.ArgumentParser:
|
|
|
527
768
|
p_export.add_argument("--html", action="store_true", help="Exportar como HTML")
|
|
528
769
|
add_report_args(p_export)
|
|
529
770
|
|
|
771
|
+
# ── watch ─────────────────────────────────────────────────────────────
|
|
772
|
+
p_watch = subparsers.add_parser(
|
|
773
|
+
"watch",
|
|
774
|
+
help="Modo watch continuo: escanea periódicamente y notifica solo cambios",
|
|
775
|
+
)
|
|
776
|
+
p_watch.add_argument(
|
|
777
|
+
"--target", required=True, metavar="DOMINIO",
|
|
778
|
+
help="Dominio objetivo a monitorizar",
|
|
779
|
+
)
|
|
780
|
+
p_watch.add_argument(
|
|
781
|
+
"--interval", type=int, default=3600, metavar="SEGUNDOS",
|
|
782
|
+
help="Segundos entre escaneos (default: 3600)",
|
|
783
|
+
)
|
|
784
|
+
p_watch.add_argument(
|
|
785
|
+
"--alert-webhook", metavar="URL", dest="alert_webhook",
|
|
786
|
+
help="URL del webhook para alertas cuando se detecten cambios "
|
|
787
|
+
"(alternativa: env EASM_ALERT_WEBHOOK)",
|
|
788
|
+
)
|
|
789
|
+
p_watch.add_argument(
|
|
790
|
+
"--ports", default="top100", metavar="PORTS",
|
|
791
|
+
help="Puertos a escanear: 'top100' (default) o lista CSV (ej: 22,80,443)",
|
|
792
|
+
)
|
|
793
|
+
p_watch.add_argument(
|
|
794
|
+
"--nmap", action="store_true",
|
|
795
|
+
help="Usar nmap como backend de escaneo de puertos",
|
|
796
|
+
)
|
|
797
|
+
|
|
798
|
+
# ── diff ──────────────────────────────────────────────────────────────
|
|
799
|
+
p_diff = subparsers.add_parser(
|
|
800
|
+
"diff",
|
|
801
|
+
help="Mostrar cambios acumulados entre dos snapshots almacenados",
|
|
802
|
+
)
|
|
803
|
+
p_diff.add_argument(
|
|
804
|
+
"--target", required=True, metavar="DOMINIO",
|
|
805
|
+
help="Dominio objetivo",
|
|
806
|
+
)
|
|
807
|
+
p_diff.add_argument(
|
|
808
|
+
"--from", required=True, dest="from_date", metavar="FECHA",
|
|
809
|
+
help="Fecha de inicio del rango (YYYY-MM-DD o YYYY-MM-DDTHH:MM)",
|
|
810
|
+
)
|
|
811
|
+
p_diff.add_argument(
|
|
812
|
+
"--to", required=True, dest="to_date", metavar="FECHA",
|
|
813
|
+
help="Fecha de fin del rango (YYYY-MM-DD o YYYY-MM-DDTHH:MM)",
|
|
814
|
+
)
|
|
815
|
+
|
|
530
816
|
return parser
|
|
531
817
|
|
|
532
818
|
|
|
@@ -548,6 +834,8 @@ def main() -> None:
|
|
|
548
834
|
"history": cmd_history,
|
|
549
835
|
"assets": cmd_assets,
|
|
550
836
|
"export": cmd_export,
|
|
837
|
+
"watch": cmd_watch,
|
|
838
|
+
"diff": cmd_diff,
|
|
551
839
|
}
|
|
552
840
|
|
|
553
841
|
handler = _dispatch.get(args.comando)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|