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.
- {vamp_easm-1.4 → vamp_easm-1.5}/PKG-INFO +1 -1
- {vamp_easm-1.4 → vamp_easm-1.5}/pyproject.toml +1 -1
- {vamp_easm-1.4 → vamp_easm-1.5}/vamp_easm.egg-info/PKG-INFO +1 -1
- {vamp_easm-1.4 → vamp_easm-1.5}/vamp_easm.py +560 -2
- {vamp_easm-1.4 → vamp_easm-1.5}/LICENSE +0 -0
- {vamp_easm-1.4 → vamp_easm-1.5}/README.md +0 -0
- {vamp_easm-1.4 → vamp_easm-1.5}/easm/__init__.py +0 -0
- {vamp_easm-1.4 → vamp_easm-1.5}/easm/alerter.py +0 -0
- {vamp_easm-1.4 → vamp_easm-1.5}/easm/differ.py +0 -0
- {vamp_easm-1.4 → vamp_easm-1.5}/easm/scanner.py +0 -0
- {vamp_easm-1.4 → vamp_easm-1.5}/easm/storage.py +0 -0
- {vamp_easm-1.4 → vamp_easm-1.5}/setup.cfg +0 -0
- {vamp_easm-1.4 → vamp_easm-1.5}/vamp_easm.egg-info/SOURCES.txt +0 -0
- {vamp_easm-1.4 → vamp_easm-1.5}/vamp_easm.egg-info/dependency_links.txt +0 -0
- {vamp_easm-1.4 → vamp_easm-1.5}/vamp_easm.egg-info/entry_points.txt +0 -0
- {vamp_easm-1.4 → vamp_easm-1.5}/vamp_easm.egg-info/requires.txt +0 -0
- {vamp_easm-1.4 → vamp_easm-1.5}/vamp_easm.egg-info/top_level.txt +0 -0
- {vamp_easm-1.4 → vamp_easm-1.5}/vampsec_report.py +0 -0
|
@@ -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.
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|