vamp-easm 1.4__tar.gz → 1.5__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vamp-easm
3
- Version: 1.4
3
+ Version: 1.5
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "vamp-easm"
7
- version = "1.4"
7
+ version = "1.5"
8
8
  description = "External Attack Surface Management (EASM) scanner for authorized audits"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vamp-easm
3
- Version: 1.4
3
+ Version: 1.5
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
@@ -36,9 +36,14 @@ DIFFS DETECTADOS
36
36
  USO
37
37
  ---
38
38
  python vamp_easm.py scan --target ejemplo.com [--ports top100|22,80,443]
39
+ python vamp_easm.py scan --target ejemplo.com --shodan-key KEY --shodan-monitor
39
40
  python vamp_easm.py history --target ejemplo.com [--limit 10]
40
41
  python vamp_easm.py assets --target ejemplo.com
41
42
  python vamp_easm.py export --target ejemplo.com [--json] [--html]
43
+ python vamp_easm.py monitor --target ejemplo.com --shodan-key KEY --setup
44
+ python vamp_easm.py monitor --target ejemplo.com --shodan-key KEY --check
45
+ python vamp_easm.py monitor --target ejemplo.com --shodan-key KEY --remove
46
+ python vamp_easm.py monitor --shodan-key KEY --list
42
47
 
43
48
  EXIT CODES
44
49
  ----------
@@ -59,6 +64,7 @@ import json
59
64
  import sys
60
65
  import time
61
66
  from datetime import datetime, timezone
67
+ from pathlib import Path
62
68
  from typing import List, Optional
63
69
 
64
70
  from rich.console import Console
@@ -76,7 +82,7 @@ from vampsec_report import (
76
82
  # Constantes
77
83
  # ---------------------------------------------------------------------------
78
84
 
79
- VERSION = "1.4"
85
+ VERSION = "1.5"
80
86
  TOOL = "vamp-easm"
81
87
  BRAND = "VampSecure Labs — EASM Continuo"
82
88
 
@@ -87,7 +93,7 @@ BANNER = (
87
93
  " \\ V / _ \\| |\\/| | _/\\__ \\ _| (__| |_| | / _|| |__ / _ \\| _ \\__ \\\n"
88
94
  " \\_/_/ \\_\\_| |_|_| |___/___\\___|\\___/|_|_\\___|____/_/ \\_\\___/___/\n"
89
95
  ' by Antonio Hernandez "Belky" — VampSecure Studios\n'
90
- " vamp-easm v1.4 · External Attack Surface Management\n"
96
+ " vamp-easm v1.5 · External Attack Surface Management\n"
91
97
  " ────────────────────────────────────────────────────────────────────────\n"
92
98
  " USO EXCLUSIVO EN AUDITORÍAS AUTORIZADAS · El uso no autorizado es ilegal\n"
93
99
  )
@@ -372,6 +378,335 @@ class ShodanEASMEnricher:
372
378
  ))
373
379
 
374
380
 
381
+ # ---------------------------------------------------------------------------
382
+ # Integración Shodan Monitor (v1.5)
383
+ # ---------------------------------------------------------------------------
384
+
385
+ class ShodanMonitorManager:
386
+ """
387
+ Gestión de alertas Shodan Monitor para monitorización automática de IPs.
388
+
389
+ Shodan Monitor crea alertas que se activan cuando Shodan detecta nuevos
390
+ puertos abiertos, vulnerabilidades o cambios en las IPs monitorizadas.
391
+
392
+ Endpoints:
393
+ GET /shodan/alert/info?key= → lista todas las alertas activas
394
+ POST /shodan/alert?key= → crea una alerta nueva
395
+ GET /shodan/alert/{id}/info?key= → estado y matches de una alerta
396
+ DELETE /shodan/alert/{id}?key= → elimina una alerta
397
+ """
398
+
399
+ _BASE = "https://api.shodan.io/shodan/alert"
400
+ _ESTADO = Path.home() / ".config" / "vampsec" / "easm_shodan_monitor.json"
401
+
402
+ def __init__(self, api_key: str) -> None:
403
+ self._key = api_key
404
+
405
+ def _peticion(self, metodo: str, url: str, body: Optional[dict] = None) -> dict:
406
+ """Realiza una petición HTTP a la API de Shodan Monitor."""
407
+ import urllib.request as _ureq
408
+ import urllib.error as _uerr
409
+ import json as _json
410
+
411
+ data_bytes = None
412
+ if body is not None:
413
+ data_bytes = _json.dumps(body).encode()
414
+
415
+ req = _ureq.Request(
416
+ url,
417
+ data=data_bytes,
418
+ method=metodo,
419
+ headers={
420
+ "User-Agent": f"vamp-easm/{VERSION}",
421
+ "Accept": "application/json",
422
+ "Content-Type": "application/json",
423
+ },
424
+ )
425
+ try:
426
+ with _ureq.urlopen(req, timeout=20) as resp:
427
+ contenido = resp.read()
428
+ if not contenido:
429
+ return {}
430
+ return _json.loads(contenido)
431
+ except _uerr.HTTPError as e:
432
+ return {"__error__": e.code, "__msg__": e.reason}
433
+ except Exception as e:
434
+ return {"__error__": -1, "__msg__": str(e)}
435
+
436
+ def listar_alertas(self) -> list:
437
+ """Devuelve la lista de todas las alertas Shodan Monitor activas."""
438
+ resultado = self._peticion("GET", f"{self._BASE}/info?key={self._key}")
439
+ if "__error__" in resultado:
440
+ return []
441
+ if isinstance(resultado, list):
442
+ return resultado
443
+ return resultado.get("alerts", [])
444
+
445
+ def crear_alerta(self, nombre: str, ips: List[str]) -> Optional[dict]:
446
+ """
447
+ Crea una alerta Shodan Monitor para la lista de IPs.
448
+
449
+ Parámetros
450
+ ----------
451
+ nombre : str — Nombre identificativo de la alerta
452
+ ips : list[str] — Lista de IPs o rangos CIDR a monitorizar
453
+
454
+ Devuelve el dict de la alerta creada, o None si hay error.
455
+ """
456
+ ips_filtradas = [ip.strip() for ip in ips if ip.strip()]
457
+ if not ips_filtradas:
458
+ return None
459
+
460
+ body = {
461
+ "name": nombre,
462
+ "filters": {"ip": " ".join(ips_filtradas)},
463
+ }
464
+ resultado = self._peticion("POST", f"{self._BASE}?key={self._key}", body=body)
465
+ if "__error__" in resultado:
466
+ return None
467
+ return resultado
468
+
469
+ def obtener_alerta(self, alert_id: str) -> Optional[dict]:
470
+ """Obtiene el estado actual y los matches de una alerta por ID."""
471
+ resultado = self._peticion("GET", f"{self._BASE}/{alert_id}/info?key={self._key}")
472
+ if "__error__" in resultado:
473
+ return None
474
+ return resultado
475
+
476
+ def eliminar_alerta(self, alert_id: str) -> bool:
477
+ """Elimina una alerta Shodan Monitor. Devuelve True si tuvo éxito."""
478
+ resultado = self._peticion("DELETE", f"{self._BASE}/{alert_id}?key={self._key}")
479
+ return resultado.get("success", False) or "__error__" not in resultado
480
+
481
+ def _cargar_estado(self) -> dict:
482
+ """Carga el estado persistido en ~/.config/vampsec/easm_shodan_monitor.json."""
483
+ import json as _j
484
+ if self._ESTADO.exists():
485
+ try:
486
+ return _j.loads(self._ESTADO.read_text())
487
+ except Exception:
488
+ return {}
489
+ return {}
490
+
491
+ def _guardar_estado(self, estado: dict) -> None:
492
+ """Persiste el estado en ~/.config/vampsec/easm_shodan_monitor.json."""
493
+ import json as _j
494
+ self._ESTADO.parent.mkdir(parents=True, exist_ok=True)
495
+ self._ESTADO.write_text(_j.dumps(estado, indent=2, ensure_ascii=False))
496
+
497
+ def setup_para_target(self, target: str, ips: List[str]) -> Optional[str]:
498
+ """
499
+ Crea (o reutiliza) una alerta Shodan Monitor para el target.
500
+
501
+ Devuelve el ID de la alerta, o None si no se pudo crear.
502
+ """
503
+ estado = self._cargar_estado()
504
+
505
+ # Reutilizar alerta existente si sigue activa en Shodan
506
+ if target in estado:
507
+ alert_id = estado[target].get("alert_id")
508
+ if alert_id and self.obtener_alerta(alert_id):
509
+ console.print(
510
+ f" [dim]Alerta Shodan Monitor ya existente: "
511
+ f"ID [bold]{alert_id}[/bold][/dim]"
512
+ )
513
+ return alert_id
514
+
515
+ nombre = f"vamp-easm:{target}"
516
+ alerta = self.crear_alerta(nombre, ips)
517
+ if not alerta:
518
+ return None
519
+
520
+ alert_id = alerta.get("id") or alerta.get("alert_id")
521
+ if not alert_id:
522
+ return None
523
+
524
+ estado[target] = {
525
+ "alert_id": alert_id,
526
+ "alert_name": nombre,
527
+ "ips": ips,
528
+ "created_at": datetime.now(timezone.utc).isoformat(),
529
+ "last_checked": None,
530
+ "last_matches": {},
531
+ }
532
+ self._guardar_estado(estado)
533
+ return alert_id
534
+
535
+ def check_matches(self, target: str, findings: List) -> None:
536
+ """
537
+ Consulta la alerta activa del target y genera findings por cambios.
538
+
539
+ Compara los matches actuales con el estado previo persistido en disco
540
+ y emite hallazgos para nuevos puertos o CVEs detectados desde la
541
+ última comprobación. Modifica findings in-place.
542
+ """
543
+ estado = self._cargar_estado()
544
+ if target not in estado:
545
+ findings.append(Finding(
546
+ id = "EASM-MON-001",
547
+ title = "Shodan Monitor no configurado para este target",
548
+ severity = "INFO",
549
+ description = (
550
+ "No hay ninguna alerta Shodan Monitor activa para este target. "
551
+ "Ejecuta 'vamp-easm monitor --target ... --setup' para crearla."
552
+ ),
553
+ evidence = f"Target: {target}\nEstado: sin alerta",
554
+ affected = target,
555
+ remediation = (
556
+ "Ejecutar: vamp-easm monitor --target <dominio> "
557
+ "--shodan-key <KEY> --setup"
558
+ ),
559
+ tags = ["shodan-monitor", "easm"],
560
+ ))
561
+ return
562
+
563
+ info_target = estado[target]
564
+ alert_id = info_target.get("alert_id")
565
+ if not alert_id:
566
+ return
567
+
568
+ alerta = self.obtener_alerta(alert_id)
569
+ if alerta is None:
570
+ findings.append(Finding(
571
+ id = "EASM-MON-002",
572
+ title = f"Alerta Shodan Monitor {alert_id} no encontrada",
573
+ severity = "HIGH",
574
+ description = (
575
+ f"La alerta Shodan Monitor con ID '{alert_id}' no responde. "
576
+ "Puede haber sido eliminada o expirado. Recrear con '--setup'."
577
+ ),
578
+ evidence = f"Alert ID: {alert_id}\nTarget: {target}",
579
+ affected = target,
580
+ remediation = (
581
+ "Ejecutar: vamp-easm monitor --target <dominio> "
582
+ "--shodan-key <KEY> --setup"
583
+ ),
584
+ tags = ["shodan-monitor", "easm"],
585
+ ))
586
+ return
587
+
588
+ # Construir mapa {ip: {ports, vulns}} de los matches actuales
589
+ matches_actuales: dict = {}
590
+ for match in alerta.get("matches", []) or []:
591
+ ip = match.get("ip_str", "")
592
+ if not ip:
593
+ continue
594
+ puertos = match.get("port", [])
595
+ if isinstance(puertos, int):
596
+ puertos = [puertos]
597
+ cves = list((match.get("vulns", {}) or {}).keys())
598
+ if ip not in matches_actuales:
599
+ matches_actuales[ip] = {"ports": [], "vulns": []}
600
+ for p in puertos:
601
+ if p not in matches_actuales[ip]["ports"]:
602
+ matches_actuales[ip]["ports"].append(p)
603
+ for c in cves:
604
+ if c not in matches_actuales[ip]["vulns"]:
605
+ matches_actuales[ip]["vulns"].append(c)
606
+
607
+ # Índice base para IDs de hallazgos
608
+ matches_previos = info_target.get("last_matches", {}) or {}
609
+ idx_base = 10
610
+
611
+ for ip, datos in matches_actuales.items():
612
+ puertos_previos = set(matches_previos.get(ip, {}).get("ports", []))
613
+ vulns_previas = set(matches_previos.get(ip, {}).get("vulns", []))
614
+
615
+ # Nuevos puertos detectados
616
+ for puerto in datos["ports"]:
617
+ if puerto in puertos_previos:
618
+ continue
619
+ idx_base += 1
620
+ sev = "HIGH" if puerto in _SHODAN_PUERTOS_SENSIBLES else "MEDIUM"
621
+ findings.append(Finding(
622
+ id = f"EASM-MON-{idx_base:03d}",
623
+ title = f"Shodan Monitor: nuevo puerto {puerto} en {ip}",
624
+ severity = sev,
625
+ description = (
626
+ f"Shodan Monitor alertó de la apertura del puerto {puerto} "
627
+ f"en {ip}. No estaba presente en la última comprobación."
628
+ ),
629
+ evidence = (
630
+ f"IP: {ip}\n"
631
+ f"Puerto nuevo: {puerto}\n"
632
+ f"Puertos previos: "
633
+ f"{', '.join(str(p) for p in sorted(puertos_previos)) or '—'}\n"
634
+ f"Fuente: Shodan Monitor (alerta {alert_id})"
635
+ ),
636
+ affected = f"{ip}:{puerto}",
637
+ remediation = (
638
+ f"Verificar inmediatamente el puerto {puerto} en {ip}. "
639
+ "Si no es un servicio autorizado, aplicar regla de firewall."
640
+ ),
641
+ tags = ["shodan-monitor", "easm", "new-port"],
642
+ ))
643
+
644
+ # Nuevas CVEs detectadas
645
+ vulns_nuevas = [v for v in datos["vulns"] if v not in vulns_previas]
646
+ if vulns_nuevas:
647
+ cves_str = ", ".join(vulns_nuevas[:10])
648
+ idx_base += 1
649
+ sev = "CRITICAL" if len(vulns_nuevas) >= 3 else "HIGH"
650
+ findings.append(Finding(
651
+ id = f"EASM-MON-{idx_base:03d}",
652
+ title = (
653
+ f"Shodan Monitor: {len(vulns_nuevas)} CVE(s) "
654
+ f"nueva(s) en {ip}"
655
+ ),
656
+ severity = sev,
657
+ description = (
658
+ f"Shodan Monitor reporta {len(vulns_nuevas)} nueva(s) "
659
+ f"vulnerabilidad(es) en {ip}: {cves_str[:100]}."
660
+ ),
661
+ evidence = (
662
+ f"IP: {ip}\n"
663
+ f"CVEs nuevas: {cves_str}\n"
664
+ f"CVEs previas: "
665
+ f"{', '.join(sorted(vulns_previas)[:10]) or '—'}\n"
666
+ f"Fuente: Shodan Monitor (alerta {alert_id})"
667
+ ),
668
+ affected = ip,
669
+ remediation = (
670
+ "Consultar NVD para detalles y aplicar parches. "
671
+ "Referencia: https://nvd.nist.gov/"
672
+ ),
673
+ cve = vulns_nuevas[0],
674
+ tags = ["shodan-monitor", "easm", "cve", "new-vuln"],
675
+ ))
676
+
677
+ # IPs que desaparecen de matches (puertos posiblemente cerrados)
678
+ for ip in matches_previos:
679
+ if ip not in matches_actuales and matches_previos[ip].get("ports"):
680
+ idx_base += 1
681
+ findings.append(Finding(
682
+ id = f"EASM-MON-{idx_base:03d}",
683
+ title = f"Shodan Monitor: {ip} sin matches activos",
684
+ severity = "INFO",
685
+ description = (
686
+ f"La IP {ip} ya no aparece en los matches de Shodan Monitor. "
687
+ "Los puertos previamente detectados podrían haberse cerrado."
688
+ ),
689
+ evidence = (
690
+ f"IP: {ip}\n"
691
+ f"Puertos previos: "
692
+ f"{', '.join(str(p) for p in matches_previos[ip].get('ports', []))}\n"
693
+ "Fuente: Shodan Monitor"
694
+ ),
695
+ affected = ip,
696
+ remediation = (
697
+ "Verificar el estado actual del host. "
698
+ "Si es intencional, actualizar el inventario."
699
+ ),
700
+ tags = ["shodan-monitor", "easm", "port-closed"],
701
+ ))
702
+
703
+ # Persistir estado actualizado
704
+ info_target["last_checked"] = datetime.now(timezone.utc).isoformat()
705
+ info_target["last_matches"] = matches_actuales
706
+ estado[target] = info_target
707
+ self._guardar_estado(estado)
708
+
709
+
375
710
  # ---------------------------------------------------------------------------
376
711
  # Integración Censys (v1.4)
377
712
  # ---------------------------------------------------------------------------
@@ -739,6 +1074,22 @@ def cmd_scan(args: argparse.Namespace) -> int:
739
1074
  f"{len(findings_censys)} hallazgos totales Censys"
740
1075
  )
741
1076
 
1077
+ # ── Shodan Monitor: verificar alertas activas (v1.5) ─────────────────
1078
+ findings_monitor: List[Finding] = []
1079
+ if shodan_key_easm and getattr(args, "shodan_monitor", False):
1080
+ console.print("\n[cyan]►[/cyan] [bold]Shodan Monitor: verificando alertas activas…[/bold]")
1081
+ mon_mgr = ShodanMonitorManager(shodan_key_easm)
1082
+ mon_mgr.check_matches(target, findings_monitor)
1083
+ nuevos_mon = [
1084
+ f for f in findings_monitor
1085
+ if "new-port" in (getattr(f, "tags", None) or [])
1086
+ or "new-vuln" in (getattr(f, "tags", None) or [])
1087
+ ]
1088
+ console.print(
1089
+ f" {len(nuevos_mon)} nueva(s) amenaza(s) Shodan Monitor · "
1090
+ f"{len(findings_monitor)} hallazgos totales"
1091
+ )
1092
+
742
1093
  # ── Mostrar resultados ────────────────────────────────────────────────
743
1094
  if diffs:
744
1095
  console.print(_tabla_diffs(diffs))
@@ -758,6 +1109,8 @@ def cmd_scan(args: argparse.Namespace) -> int:
758
1109
  findings.extend(findings_shodan)
759
1110
  # Incluir hallazgos Censys en el informe si los hay (v1.4)
760
1111
  findings.extend(findings_censys)
1112
+ # Incluir hallazgos Shodan Monitor en el informe si los hay (v1.5)
1113
+ findings.extend(findings_monitor)
761
1114
  if findings:
762
1115
  meta = meta_from_args(args, tool=TOOL, version=VERSION)
763
1116
  meta.scope = target
@@ -1118,6 +1471,170 @@ def cmd_diff(args: argparse.Namespace) -> int:
1118
1471
  return 0
1119
1472
 
1120
1473
 
1474
+ # ---------------------------------------------------------------------------
1475
+ # Comando: monitor
1476
+ # ---------------------------------------------------------------------------
1477
+
1478
+ def cmd_monitor(args: argparse.Namespace) -> int:
1479
+ """
1480
+ Gestión de alertas Shodan Monitor para monitorización automática de IPs.
1481
+
1482
+ Acciones disponibles:
1483
+ --setup : crea una alerta Shodan Monitor para las IPs del target
1484
+ --check : consulta la alerta y muestra nuevas amenazas detectadas
1485
+ --remove : elimina la alerta del target
1486
+ --list : lista todas las alertas Shodan Monitor de la cuenta
1487
+ """
1488
+ shodan_key = (
1489
+ getattr(args, "shodan_key", None)
1490
+ or __import__("os").environ.get("SHODAN_API_KEY", "")
1491
+ )
1492
+ if not shodan_key:
1493
+ console.print("[red]Error:[/red] Se requiere --shodan-key o SHODAN_API_KEY")
1494
+ return 1
1495
+
1496
+ mgr = ShodanMonitorManager(shodan_key)
1497
+
1498
+ # ── Listar todas las alertas ──────────────────────────────────────────
1499
+ if getattr(args, "list_alerts", False):
1500
+ alertas = mgr.listar_alertas()
1501
+ if not alertas:
1502
+ console.print("[dim]No hay alertas Shodan Monitor activas.[/dim]")
1503
+ return 0
1504
+ tabla = Table(title="Alertas Shodan Monitor", box=box.SIMPLE_HEAVY)
1505
+ tabla.add_column("ID", style="cyan", no_wrap=True)
1506
+ tabla.add_column("Nombre", style="white")
1507
+ tabla.add_column("IPs", style="dim", max_width=40)
1508
+ tabla.add_column("Creada", style="dim")
1509
+ for a in alertas:
1510
+ tabla.add_row(
1511
+ str(a.get("id", "—")),
1512
+ str(a.get("name", "—")),
1513
+ str(a.get("filters", {}).get("ip", "—"))[:40],
1514
+ str(a.get("created", "—"))[:16],
1515
+ )
1516
+ console.print(tabla)
1517
+ return 0
1518
+
1519
+ # Las demás acciones requieren --target
1520
+ target = getattr(args, "target", None)
1521
+ if not target:
1522
+ console.print(
1523
+ "[red]Error:[/red] --target es obligatorio para "
1524
+ "--setup, --check y --remove"
1525
+ )
1526
+ return 1
1527
+
1528
+ # ── Setup: crear alerta Shodan Monitor ───────────────────────────────
1529
+ if getattr(args, "setup", False):
1530
+ storage.inicializar_db()
1531
+ assets = storage.todos_los_assets(target)
1532
+ ips_set: set = {a["ip"] for a in assets if a.get("ip")}
1533
+
1534
+ if not ips_set:
1535
+ # Fallback: resolución DNS directa del dominio
1536
+ import socket as _sock
1537
+ try:
1538
+ for item in _sock.getaddrinfo(target, None):
1539
+ ips_set.add(item[4][0])
1540
+ except Exception:
1541
+ pass
1542
+
1543
+ if not ips_set:
1544
+ console.print(
1545
+ f"[red]Error:[/red] Sin IPs para {target}. "
1546
+ "Ejecuta primero: vamp-easm scan --target " + target
1547
+ )
1548
+ return 1
1549
+
1550
+ ips = list(ips_set)
1551
+ ips_str = ", ".join(ips[:5]) + ("…" if len(ips) > 5 else "")
1552
+ console.print(
1553
+ f"[cyan]►[/cyan] Creando alerta Shodan Monitor para "
1554
+ f"[bold]{target}[/bold] · {len(ips)} IP(s): [dim]{ips_str}[/dim]"
1555
+ )
1556
+
1557
+ alert_id = mgr.setup_para_target(target, ips)
1558
+ if not alert_id:
1559
+ console.print(
1560
+ "[red]Error:[/red] No se pudo crear la alerta en Shodan Monitor.\n"
1561
+ "[dim]Verifica que la clave API tiene permisos de monitorización.[/dim]"
1562
+ )
1563
+ return 1
1564
+
1565
+ console.print(
1566
+ f"[green]✓ Alerta creada:[/green] ID [bold]{alert_id}[/bold]\n"
1567
+ f" Usa '--check' para consultar los matches."
1568
+ )
1569
+ return 0
1570
+
1571
+ # ── Check: consultar matches y generar findings ───────────────────────
1572
+ if getattr(args, "check", False):
1573
+ console.print(
1574
+ f"[cyan]►[/cyan] Consultando Shodan Monitor para [bold]{target}[/bold]…"
1575
+ )
1576
+ findings: List[Finding] = []
1577
+ mgr.check_matches(target, findings)
1578
+
1579
+ if not findings:
1580
+ console.print("[green]✓ Sin novedades en Shodan Monitor.[/green]")
1581
+ return 0
1582
+
1583
+ tabla = Table(title=f"Shodan Monitor — {target}", box=box.SIMPLE_HEAVY)
1584
+ tabla.add_column("ID", style="dim", no_wrap=True, max_width=16)
1585
+ tabla.add_column("Sev", no_wrap=True, max_width=10)
1586
+ tabla.add_column("Título", style="white", max_width=60)
1587
+ tabla.add_column("Afectado", style="cyan", max_width=25)
1588
+ for f in findings:
1589
+ tabla.add_row(
1590
+ f.id,
1591
+ _badge_sev(f.severity),
1592
+ f.title[:60],
1593
+ f.affected or "—",
1594
+ )
1595
+ console.print(tabla)
1596
+
1597
+ criticos = [f for f in findings if f.severity == "CRITICAL"]
1598
+ highs = [f for f in findings if f.severity == "HIGH"]
1599
+ console.print(
1600
+ f"\n[bold]Resumen:[/bold] {len(findings)} hallazgos — "
1601
+ f"{len(criticos)} CRITICAL · {len(highs)} HIGH"
1602
+ )
1603
+ if criticos:
1604
+ return 2
1605
+ if highs:
1606
+ return 1
1607
+ return 0
1608
+
1609
+ # ── Remove: eliminar alerta ───────────────────────────────────────────
1610
+ if getattr(args, "remove", False):
1611
+ estado = mgr._cargar_estado()
1612
+ if target not in estado:
1613
+ console.print(f"[yellow]⚠[/yellow] Sin alerta registrada para {target}")
1614
+ return 0
1615
+ alert_id = estado[target].get("alert_id")
1616
+ if not alert_id:
1617
+ console.print(f"[yellow]⚠[/yellow] Sin alert_id registrado para {target}")
1618
+ return 0
1619
+ console.print(
1620
+ f"[cyan]►[/cyan] Eliminando alerta Shodan Monitor "
1621
+ f"ID [bold]{alert_id}[/bold]…"
1622
+ )
1623
+ if mgr.eliminar_alerta(alert_id):
1624
+ del estado[target]
1625
+ mgr._guardar_estado(estado)
1626
+ console.print(f"[green]✓ Alerta {alert_id} eliminada.[/green]")
1627
+ return 0
1628
+ console.print(f"[red]Error:[/red] No se pudo eliminar la alerta {alert_id}")
1629
+ return 1
1630
+
1631
+ console.print(
1632
+ "[yellow]⚠[/yellow] Especifica una acción: "
1633
+ "--setup, --check, --remove o --list"
1634
+ )
1635
+ return 1
1636
+
1637
+
1121
1638
  # ---------------------------------------------------------------------------
1122
1639
  # CLI — definición de argumentos
1123
1640
  # ---------------------------------------------------------------------------
@@ -1135,11 +1652,15 @@ def construir_parser() -> argparse.ArgumentParser:
1135
1652
  "Ejemplos:\n"
1136
1653
  " python vamp_easm.py scan --target ejemplo.com\n"
1137
1654
  " python vamp_easm.py scan --target ejemplo.com --ports 22,80,443 --html\n"
1655
+ " python vamp_easm.py scan --target ejemplo.com --shodan-key KEY --shodan-monitor\n"
1138
1656
  " python vamp_easm.py history --target ejemplo.com --limit 5\n"
1139
1657
  " python vamp_easm.py assets --target ejemplo.com\n"
1140
1658
  " python vamp_easm.py export --target ejemplo.com --json\n"
1141
1659
  " python vamp_easm.py watch --target ejemplo.com --interval 1800\n"
1142
1660
  " python vamp_easm.py diff --target ejemplo.com --from 2026-09-01 --to 2026-09-17\n"
1661
+ " python vamp_easm.py monitor --target ejemplo.com --shodan-key KEY --setup\n"
1662
+ " python vamp_easm.py monitor --target ejemplo.com --shodan-key KEY --check\n"
1663
+ " python vamp_easm.py monitor --shodan-key KEY --list\n"
1143
1664
  ),
1144
1665
  )
1145
1666
  parser.add_argument(
@@ -1191,6 +1712,12 @@ def construir_parser() -> argparse.ArgumentParser:
1191
1712
  help="Secret de la API Censys v2 (o var CENSYS_API_SECRET) — requerido junto "
1192
1713
  "con --censys-id para activar el enriquecimiento Censys (v1.4)",
1193
1714
  )
1715
+ p_scan.add_argument(
1716
+ "--shodan-monitor", action="store_true", dest="shodan_monitor",
1717
+ help="Integrar con Shodan Monitor: verificar alertas activas durante el escaneo. "
1718
+ "Requiere --shodan-key y haber ejecutado previamente "
1719
+ "'vamp-easm monitor --setup' (v1.5)",
1720
+ )
1194
1721
  add_report_args(p_scan)
1195
1722
 
1196
1723
  # ── history ───────────────────────────────────────────────────────────
@@ -1259,6 +1786,36 @@ def construir_parser() -> argparse.ArgumentParser:
1259
1786
  help="Fecha de fin del rango (YYYY-MM-DD o YYYY-MM-DDTHH:MM)",
1260
1787
  )
1261
1788
 
1789
+ # ── monitor ───────────────────────────────────────────────────────────
1790
+ p_mon = subparsers.add_parser(
1791
+ "monitor",
1792
+ help="Gestión de alertas Shodan Monitor para monitorización automática de IPs",
1793
+ )
1794
+ p_mon.add_argument(
1795
+ "--target", metavar="DOMINIO",
1796
+ help="Dominio objetivo (requerido para --setup, --check, --remove)",
1797
+ )
1798
+ p_mon.add_argument(
1799
+ "--shodan-key", metavar="API_KEY", dest="shodan_key",
1800
+ help="Clave API Shodan (o var SHODAN_API_KEY)",
1801
+ )
1802
+ p_mon.add_argument(
1803
+ "--setup", action="store_true",
1804
+ help="Crear alerta Shodan Monitor para las IPs del target",
1805
+ )
1806
+ p_mon.add_argument(
1807
+ "--check", action="store_true",
1808
+ help="Consultar matches actuales y mostrar nuevas amenazas detectadas",
1809
+ )
1810
+ p_mon.add_argument(
1811
+ "--remove", action="store_true",
1812
+ help="Eliminar la alerta Shodan Monitor del target",
1813
+ )
1814
+ p_mon.add_argument(
1815
+ "--list", action="store_true", dest="list_alerts",
1816
+ help="Listar todas las alertas Shodan Monitor activas en la cuenta",
1817
+ )
1818
+
1262
1819
  return parser
1263
1820
 
1264
1821
 
@@ -1282,6 +1839,7 @@ def main() -> None:
1282
1839
  "export": cmd_export,
1283
1840
  "watch": cmd_watch,
1284
1841
  "diff": cmd_diff,
1842
+ "monitor": cmd_monitor,
1285
1843
  }
1286
1844
 
1287
1845
  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