vamp-easm 1.3__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.3 → vamp_easm-1.5}/PKG-INFO +14 -1
- {vamp_easm-1.3 → vamp_easm-1.5}/README.md +13 -0
- {vamp_easm-1.3 → vamp_easm-1.5}/pyproject.toml +1 -1
- {vamp_easm-1.3 → vamp_easm-1.5}/vamp_easm.egg-info/PKG-INFO +14 -1
- {vamp_easm-1.3 → vamp_easm-1.5}/vamp_easm.py +790 -2
- {vamp_easm-1.3 → vamp_easm-1.5}/LICENSE +0 -0
- {vamp_easm-1.3 → vamp_easm-1.5}/easm/__init__.py +0 -0
- {vamp_easm-1.3 → vamp_easm-1.5}/easm/alerter.py +0 -0
- {vamp_easm-1.3 → vamp_easm-1.5}/easm/differ.py +0 -0
- {vamp_easm-1.3 → vamp_easm-1.5}/easm/scanner.py +0 -0
- {vamp_easm-1.3 → vamp_easm-1.5}/easm/storage.py +0 -0
- {vamp_easm-1.3 → vamp_easm-1.5}/setup.cfg +0 -0
- {vamp_easm-1.3 → vamp_easm-1.5}/vamp_easm.egg-info/SOURCES.txt +0 -0
- {vamp_easm-1.3 → vamp_easm-1.5}/vamp_easm.egg-info/dependency_links.txt +0 -0
- {vamp_easm-1.3 → vamp_easm-1.5}/vamp_easm.egg-info/entry_points.txt +0 -0
- {vamp_easm-1.3 → vamp_easm-1.5}/vamp_easm.egg-info/requires.txt +0 -0
- {vamp_easm-1.3 → vamp_easm-1.5}/vamp_easm.egg-info/top_level.txt +0 -0
- {vamp_easm-1.3 → vamp_easm-1.5}/vampsec_report.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vamp-easm
|
|
3
|
-
Version: 1.
|
|
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
|
|
@@ -20,6 +20,7 @@ License-File: LICENSE
|
|
|
20
20
|
Requires-Dist: rich>=13.7.0
|
|
21
21
|
Dynamic: license-file
|
|
22
22
|
|
|
23
|
+
<!-- © VampSecure Studios — VampSecure Labs Security Research Division -->
|
|
23
24
|
# vamp-easm
|
|
24
25
|
|
|
25
26
|
**Continuous External Attack Surface Management with daily diff tracking**
|
|
@@ -59,6 +60,13 @@ Part of the [VampSecure Labs](https://github.com/Vampsecure-Labs) security toolk
|
|
|
59
60
|
|
|
60
61
|
## Installation
|
|
61
62
|
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pip install vamp-easm
|
|
66
|
+
# o con Homebrew:
|
|
67
|
+
brew install vampsecure-labs/labs/vamp-easm
|
|
68
|
+
```
|
|
69
|
+
|
|
62
70
|
```bash
|
|
63
71
|
# 1. Clone and enter the directory
|
|
64
72
|
git clone https://github.com/Vampsecure-Labs/vamp-easm.git
|
|
@@ -210,3 +218,8 @@ MIT License — see [LICENSE](LICENSE) for details.
|
|
|
210
218
|
|
|
211
219
|
© VampSecure Studios — VampSecure Labs Security Research Division
|
|
212
220
|
For authorized penetration testing use only.
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## Versión
|
|
225
|
+
v1.3 — VampSecure Labs Security Research Division
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
<!-- © VampSecure Studios — VampSecure Labs Security Research Division -->
|
|
1
2
|
# vamp-easm
|
|
2
3
|
|
|
3
4
|
**Continuous External Attack Surface Management with daily diff tracking**
|
|
@@ -37,6 +38,13 @@ Part of the [VampSecure Labs](https://github.com/Vampsecure-Labs) security toolk
|
|
|
37
38
|
|
|
38
39
|
## Installation
|
|
39
40
|
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install vamp-easm
|
|
44
|
+
# o con Homebrew:
|
|
45
|
+
brew install vampsecure-labs/labs/vamp-easm
|
|
46
|
+
```
|
|
47
|
+
|
|
40
48
|
```bash
|
|
41
49
|
# 1. Clone and enter the directory
|
|
42
50
|
git clone https://github.com/Vampsecure-Labs/vamp-easm.git
|
|
@@ -188,3 +196,8 @@ MIT License — see [LICENSE](LICENSE) for details.
|
|
|
188
196
|
|
|
189
197
|
© VampSecure Studios — VampSecure Labs Security Research Division
|
|
190
198
|
For authorized penetration testing use only.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Versión
|
|
203
|
+
v1.3 — VampSecure Labs Security Research Division
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vamp-easm
|
|
3
|
-
Version: 1.
|
|
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
|
|
@@ -20,6 +20,7 @@ License-File: LICENSE
|
|
|
20
20
|
Requires-Dist: rich>=13.7.0
|
|
21
21
|
Dynamic: license-file
|
|
22
22
|
|
|
23
|
+
<!-- © VampSecure Studios — VampSecure Labs Security Research Division -->
|
|
23
24
|
# vamp-easm
|
|
24
25
|
|
|
25
26
|
**Continuous External Attack Surface Management with daily diff tracking**
|
|
@@ -59,6 +60,13 @@ Part of the [VampSecure Labs](https://github.com/Vampsecure-Labs) security toolk
|
|
|
59
60
|
|
|
60
61
|
## Installation
|
|
61
62
|
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pip install vamp-easm
|
|
66
|
+
# o con Homebrew:
|
|
67
|
+
brew install vampsecure-labs/labs/vamp-easm
|
|
68
|
+
```
|
|
69
|
+
|
|
62
70
|
```bash
|
|
63
71
|
# 1. Clone and enter the directory
|
|
64
72
|
git clone https://github.com/Vampsecure-Labs/vamp-easm.git
|
|
@@ -210,3 +218,8 @@ MIT License — see [LICENSE](LICENSE) for details.
|
|
|
210
218
|
|
|
211
219
|
© VampSecure Studios — VampSecure Labs Security Research Division
|
|
212
220
|
For authorized penetration testing use only.
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## Versión
|
|
225
|
+
v1.3 — VampSecure Labs Security Research Division
|
|
@@ -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,527 @@ 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
|
+
|
|
710
|
+
# ---------------------------------------------------------------------------
|
|
711
|
+
# Integración Censys (v1.4)
|
|
712
|
+
# ---------------------------------------------------------------------------
|
|
713
|
+
|
|
714
|
+
# URL base de la API Censys v2
|
|
715
|
+
_CENSYS_SEARCH_URL = "https://search.censys.io/api/v2/hosts/search"
|
|
716
|
+
|
|
717
|
+
# Puertos considerados sensibles para enriquecer hallazgos Censys
|
|
718
|
+
_CENSYS_PUERTOS_SENSIBLES = {21, 22, 23, 25, 80, 443, 445, 3389, 1433, 3306, 5432, 5900, 6379, 8080, 8443, 27017, 9200}
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
class CensysEASMEnricher:
|
|
722
|
+
"""
|
|
723
|
+
Enriquecimiento EASM con la API Censys v2.
|
|
724
|
+
|
|
725
|
+
Consulta el endpoint de búsqueda de hosts Censys usando la query
|
|
726
|
+
``parsed.names:<dominio>`` para detectar hosts indexados por Censys
|
|
727
|
+
que no fueron descubiertos por crt.sh/HackerTarget.
|
|
728
|
+
|
|
729
|
+
Hallazgos emitidos:
|
|
730
|
+
- CENSYS_HOST_EXPOSED: host encontrado por Censys pero no en el
|
|
731
|
+
escaneo activo (superficie no visible para la organización).
|
|
732
|
+
- Puerto sensible abierto según Censys: MEDIUM.
|
|
733
|
+
|
|
734
|
+
Límite: 1 petición por segundo (rate limit de la API gratuita).
|
|
735
|
+
Resultados por página: 100 hosts (máximo permitido por la API).
|
|
736
|
+
"""
|
|
737
|
+
|
|
738
|
+
_MAX_PAGINAS = 3 # máximo de páginas (300 hosts) por invocación
|
|
739
|
+
|
|
740
|
+
def __init__(self, censys_id: str, censys_secret: str) -> None:
|
|
741
|
+
self._id = censys_id
|
|
742
|
+
self._secret = censys_secret
|
|
743
|
+
|
|
744
|
+
def _auth_header(self) -> str:
|
|
745
|
+
"""Devuelve el valor de la cabecera Authorization (Basic Auth)."""
|
|
746
|
+
import base64 as _b64
|
|
747
|
+
creds = f"{self._id}:{self._secret}".encode()
|
|
748
|
+
return "Basic " + _b64.b64encode(creds).decode()
|
|
749
|
+
|
|
750
|
+
def enriquecer(
|
|
751
|
+
self,
|
|
752
|
+
dominio: str,
|
|
753
|
+
ips_conocidas: set,
|
|
754
|
+
findings: list,
|
|
755
|
+
) -> None:
|
|
756
|
+
"""
|
|
757
|
+
Consulta Censys por el dominio y añade hallazgos a la lista recibida.
|
|
758
|
+
|
|
759
|
+
Parámetros
|
|
760
|
+
----------
|
|
761
|
+
dominio : str — Dominio objetivo
|
|
762
|
+
ips_conocidas : set[str] — IPs ya descubiertas por el escaneo
|
|
763
|
+
findings : list — Lista de Finding a la que se añaden hallazgos
|
|
764
|
+
"""
|
|
765
|
+
import urllib.request as _ureq
|
|
766
|
+
import urllib.error as _uerr
|
|
767
|
+
import json as _json
|
|
768
|
+
import time as _time
|
|
769
|
+
|
|
770
|
+
auth = self._auth_header()
|
|
771
|
+
idx_base = max(
|
|
772
|
+
(int(f.id.split("-")[-1]) for f in findings if "-" in f.id),
|
|
773
|
+
default=0,
|
|
774
|
+
)
|
|
775
|
+
|
|
776
|
+
cursor: str | None = None
|
|
777
|
+
pagina = 0
|
|
778
|
+
hosts_censys: list[dict] = []
|
|
779
|
+
|
|
780
|
+
while pagina < self._MAX_PAGINAS:
|
|
781
|
+
# Construir URL con paginación
|
|
782
|
+
params = f"q=parsed.names%3A{_ureq.quote(dominio)}&per_page=100"
|
|
783
|
+
if cursor:
|
|
784
|
+
params += f"&cursor={_ureq.quote(cursor)}"
|
|
785
|
+
url = f"{_CENSYS_SEARCH_URL}?{params}"
|
|
786
|
+
|
|
787
|
+
try:
|
|
788
|
+
req = _ureq.Request(
|
|
789
|
+
url,
|
|
790
|
+
headers={
|
|
791
|
+
"Authorization": auth,
|
|
792
|
+
"User-Agent": f"vamp-easm/{VERSION}",
|
|
793
|
+
"Accept": "application/json",
|
|
794
|
+
},
|
|
795
|
+
)
|
|
796
|
+
with _ureq.urlopen(req, timeout=20) as resp:
|
|
797
|
+
if resp.status != 200:
|
|
798
|
+
break
|
|
799
|
+
data = _json.loads(resp.read())
|
|
800
|
+
except _uerr.HTTPError as e:
|
|
801
|
+
if e.code in (401, 403):
|
|
802
|
+
findings.append(Finding(
|
|
803
|
+
id = f"EASM-CNS-{idx_base + 1:03d}",
|
|
804
|
+
title = "Credenciales Censys inválidas o sin permisos",
|
|
805
|
+
severity = "INFO",
|
|
806
|
+
description = (
|
|
807
|
+
f"La API Censys devolvió {e.code}. "
|
|
808
|
+
"Verificar --censys-id y --censys-secret."
|
|
809
|
+
),
|
|
810
|
+
evidence = f"URL: {_CENSYS_SEARCH_URL}",
|
|
811
|
+
affected = dominio,
|
|
812
|
+
remediation = "Comprobar credenciales en https://search.censys.io/account",
|
|
813
|
+
tags = ["censys", "easm"],
|
|
814
|
+
))
|
|
815
|
+
break
|
|
816
|
+
except Exception:
|
|
817
|
+
break
|
|
818
|
+
|
|
819
|
+
lote = data.get("result", {}).get("hits", [])
|
|
820
|
+
hosts_censys.extend(lote)
|
|
821
|
+
cursor = data.get("result", {}).get("links", {}).get("next")
|
|
822
|
+
pagina += 1
|
|
823
|
+
|
|
824
|
+
if not cursor or not lote:
|
|
825
|
+
break
|
|
826
|
+
|
|
827
|
+
# Respetar el límite de 1 req/seg de la API gratuita
|
|
828
|
+
_time.sleep(1.0)
|
|
829
|
+
|
|
830
|
+
# Procesar cada host devuelto por Censys
|
|
831
|
+
for host in hosts_censys:
|
|
832
|
+
ip = host.get("ip", "")
|
|
833
|
+
nombres = host.get("names", []) or []
|
|
834
|
+
servicios = host.get("services", []) or []
|
|
835
|
+
puertos = [s.get("port") for s in servicios if s.get("port")]
|
|
836
|
+
|
|
837
|
+
# Si la IP no fue encontrada por crt.sh/HackerTarget → hallazgo
|
|
838
|
+
if ip and ip not in ips_conocidas:
|
|
839
|
+
idx_base += 1
|
|
840
|
+
nombres_str = ", ".join(nombres[:5]) or "—"
|
|
841
|
+
puertos_str = ", ".join(str(p) for p in puertos[:10]) or "—"
|
|
842
|
+
findings.append(Finding(
|
|
843
|
+
id = f"EASM-CNS-{idx_base:03d}",
|
|
844
|
+
title = f"Host expuesto detectado por Censys (no en escaneo activo) — {ip}",
|
|
845
|
+
severity = "HIGH",
|
|
846
|
+
description = (
|
|
847
|
+
f"Censys indexa el host {ip} asociado al dominio '{dominio}' "
|
|
848
|
+
"pero no fue descubierto por crt.sh ni HackerTarget. Puede "
|
|
849
|
+
"tratarse de infraestructura shadow, activos olvidados o "
|
|
850
|
+
"exposición no intencionada."
|
|
851
|
+
),
|
|
852
|
+
evidence = (
|
|
853
|
+
f"IP: {ip}\n"
|
|
854
|
+
f"Nombres DNS (Censys): {nombres_str}\n"
|
|
855
|
+
f"Puertos indexados: {puertos_str}\n"
|
|
856
|
+
f"Dominio consultado: {dominio}\n"
|
|
857
|
+
"Fuente: Censys API v2 (datos históricos — verificar estado actual)"
|
|
858
|
+
),
|
|
859
|
+
affected = ip,
|
|
860
|
+
remediation = (
|
|
861
|
+
"Verificar si el activo pertenece a la organización. "
|
|
862
|
+
"Si es legítimo, incluirlo en el inventario de superficie de ataque. "
|
|
863
|
+
"Si no, investigar posible shadow IT o activo olvidado."
|
|
864
|
+
),
|
|
865
|
+
tags = ["censys", "easm", "CENSYS_HOST_EXPOSED"],
|
|
866
|
+
))
|
|
867
|
+
|
|
868
|
+
# Puertos sensibles abiertos según Censys (para IPs conocidas y nuevas)
|
|
869
|
+
sensibles = [p for p in puertos if p in _CENSYS_PUERTOS_SENSIBLES]
|
|
870
|
+
for puerto in sensibles:
|
|
871
|
+
# Evitar duplicados con hallazgos Shodan
|
|
872
|
+
ya_reportado = any(
|
|
873
|
+
f"{ip}:{puerto}" in (getattr(f, "affected", "") or "")
|
|
874
|
+
for f in findings
|
|
875
|
+
)
|
|
876
|
+
if ya_reportado:
|
|
877
|
+
continue
|
|
878
|
+
idx_base += 1
|
|
879
|
+
findings.append(Finding(
|
|
880
|
+
id = f"EASM-CNS-{idx_base:03d}",
|
|
881
|
+
title = f"Puerto sensible {puerto} abierto según Censys — {ip}",
|
|
882
|
+
severity = "MEDIUM",
|
|
883
|
+
description = (
|
|
884
|
+
f"Censys ha indexado el puerto {puerto} como abierto en "
|
|
885
|
+
f"la IP {ip}. Este puerto corresponde a un servicio de "
|
|
886
|
+
"alto riesgo si está expuesto sin control de acceso."
|
|
887
|
+
),
|
|
888
|
+
evidence = (
|
|
889
|
+
f"IP: {ip} Puerto: {puerto}\n"
|
|
890
|
+
f"Servicios Censys: {', '.join(s.get('service_name','?') for s in servicios if s.get('port') == puerto) or '—'}\n"
|
|
891
|
+
"Fuente: Censys API v2"
|
|
892
|
+
),
|
|
893
|
+
affected = f"{ip}:{puerto}",
|
|
894
|
+
remediation = (
|
|
895
|
+
f"Comprobar si el puerto {puerto} debe estar expuesto a internet. "
|
|
896
|
+
"Si no es necesario, restringir acceso con firewall."
|
|
897
|
+
),
|
|
898
|
+
tags = ["censys", "easm", "port"],
|
|
899
|
+
))
|
|
900
|
+
|
|
901
|
+
|
|
375
902
|
# ---------------------------------------------------------------------------
|
|
376
903
|
# Comando: scan
|
|
377
904
|
# ---------------------------------------------------------------------------
|
|
@@ -477,6 +1004,14 @@ def cmd_scan(args: argparse.Namespace) -> int:
|
|
|
477
1004
|
"[dim] ℹ Shodan no activo — usa --shodan-key para enriquecer con datos Shodan[/dim]"
|
|
478
1005
|
)
|
|
479
1006
|
|
|
1007
|
+
# Leer claves Censys (opcional, v1.4)
|
|
1008
|
+
censys_id = getattr(args, "censys_id", None) or __import__("os").environ.get("CENSYS_API_ID", "")
|
|
1009
|
+
censys_secret = getattr(args, "censys_secret", None) or __import__("os").environ.get("CENSYS_API_SECRET", "")
|
|
1010
|
+
if not (censys_id and censys_secret):
|
|
1011
|
+
console.print(
|
|
1012
|
+
"[dim] ℹ Censys no activo — usa --censys-id y --censys-secret para enriquecer con datos Censys[/dim]"
|
|
1013
|
+
)
|
|
1014
|
+
|
|
480
1015
|
# ── Motor de diffs ────────────────────────────────────────────────────
|
|
481
1016
|
console.print("\n[cyan]►[/cyan] [bold]Calculando diffs respecto al escaneo anterior…[/bold]")
|
|
482
1017
|
scan_anterior = storage.ultimo_scan(target)
|
|
@@ -521,6 +1056,40 @@ def cmd_scan(args: argparse.Namespace) -> int:
|
|
|
521
1056
|
else:
|
|
522
1057
|
console.print(" [dim]Sin IPs resueltas para consultar Shodan.[/dim]")
|
|
523
1058
|
|
|
1059
|
+
# ── Enriquecimiento Censys (v1.4, opcional) ───────────────────────────
|
|
1060
|
+
findings_censys: List[Finding] = []
|
|
1061
|
+
if censys_id and censys_secret:
|
|
1062
|
+
console.print("\n[cyan]►[/cyan] [bold]Enriquecimiento Censys (por dominio)…[/bold]")
|
|
1063
|
+
# Recopilar IPs ya conocidas del escaneo activo
|
|
1064
|
+
ips_conocidas: set = set()
|
|
1065
|
+
for sub in resultado.subdominios:
|
|
1066
|
+
if sub.ips:
|
|
1067
|
+
ips_conocidas.update(sub.ips)
|
|
1068
|
+
|
|
1069
|
+
censys_enricher = CensysEASMEnricher(censys_id, censys_secret)
|
|
1070
|
+
censys_enricher.enriquecer(target, ips_conocidas, findings_censys)
|
|
1071
|
+
nuevos = len([f for f in findings_censys if "CENSYS_HOST_EXPOSED" in (getattr(f, "tags", None) or [])])
|
|
1072
|
+
console.print(
|
|
1073
|
+
f" {nuevos} host(s) nuevos (CENSYS_HOST_EXPOSED) · "
|
|
1074
|
+
f"{len(findings_censys)} hallazgos totales Censys"
|
|
1075
|
+
)
|
|
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
|
+
|
|
524
1093
|
# ── Mostrar resultados ────────────────────────────────────────────────
|
|
525
1094
|
if diffs:
|
|
526
1095
|
console.print(_tabla_diffs(diffs))
|
|
@@ -538,6 +1107,10 @@ def cmd_scan(args: argparse.Namespace) -> int:
|
|
|
538
1107
|
findings = [d.finding for d in diffs if d.finding is not None]
|
|
539
1108
|
# Incluir hallazgos Shodan en el informe si los hay
|
|
540
1109
|
findings.extend(findings_shodan)
|
|
1110
|
+
# Incluir hallazgos Censys en el informe si los hay (v1.4)
|
|
1111
|
+
findings.extend(findings_censys)
|
|
1112
|
+
# Incluir hallazgos Shodan Monitor en el informe si los hay (v1.5)
|
|
1113
|
+
findings.extend(findings_monitor)
|
|
541
1114
|
if findings:
|
|
542
1115
|
meta = meta_from_args(args, tool=TOOL, version=VERSION)
|
|
543
1116
|
meta.scope = target
|
|
@@ -898,6 +1471,170 @@ def cmd_diff(args: argparse.Namespace) -> int:
|
|
|
898
1471
|
return 0
|
|
899
1472
|
|
|
900
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
|
+
|
|
901
1638
|
# ---------------------------------------------------------------------------
|
|
902
1639
|
# CLI — definición de argumentos
|
|
903
1640
|
# ---------------------------------------------------------------------------
|
|
@@ -915,11 +1652,15 @@ def construir_parser() -> argparse.ArgumentParser:
|
|
|
915
1652
|
"Ejemplos:\n"
|
|
916
1653
|
" python vamp_easm.py scan --target ejemplo.com\n"
|
|
917
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"
|
|
918
1656
|
" python vamp_easm.py history --target ejemplo.com --limit 5\n"
|
|
919
1657
|
" python vamp_easm.py assets --target ejemplo.com\n"
|
|
920
1658
|
" python vamp_easm.py export --target ejemplo.com --json\n"
|
|
921
1659
|
" python vamp_easm.py watch --target ejemplo.com --interval 1800\n"
|
|
922
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"
|
|
923
1664
|
),
|
|
924
1665
|
)
|
|
925
1666
|
parser.add_argument(
|
|
@@ -961,6 +1702,22 @@ def construir_parser() -> argparse.ArgumentParser:
|
|
|
961
1702
|
help="Clave API Shodan (o var SHODAN_API_KEY) — enriquece IPs descubiertas "
|
|
962
1703
|
"con puertos, servicios y vulnerabilidades indexadas por Shodan",
|
|
963
1704
|
)
|
|
1705
|
+
p_scan.add_argument(
|
|
1706
|
+
"--censys-id", metavar="ID", dest="censys_id",
|
|
1707
|
+
help="ID de la API Censys v2 (o var CENSYS_API_ID) — enriquece el escaneo "
|
|
1708
|
+
"con hosts detectados por Censys no visibles en crt.sh/HackerTarget (v1.4)",
|
|
1709
|
+
)
|
|
1710
|
+
p_scan.add_argument(
|
|
1711
|
+
"--censys-secret", metavar="SECRET", dest="censys_secret",
|
|
1712
|
+
help="Secret de la API Censys v2 (o var CENSYS_API_SECRET) — requerido junto "
|
|
1713
|
+
"con --censys-id para activar el enriquecimiento Censys (v1.4)",
|
|
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
|
+
)
|
|
964
1721
|
add_report_args(p_scan)
|
|
965
1722
|
|
|
966
1723
|
# ── history ───────────────────────────────────────────────────────────
|
|
@@ -1029,6 +1786,36 @@ def construir_parser() -> argparse.ArgumentParser:
|
|
|
1029
1786
|
help="Fecha de fin del rango (YYYY-MM-DD o YYYY-MM-DDTHH:MM)",
|
|
1030
1787
|
)
|
|
1031
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
|
+
|
|
1032
1819
|
return parser
|
|
1033
1820
|
|
|
1034
1821
|
|
|
@@ -1052,6 +1839,7 @@ def main() -> None:
|
|
|
1052
1839
|
"export": cmd_export,
|
|
1053
1840
|
"watch": cmd_watch,
|
|
1054
1841
|
"diff": cmd_diff,
|
|
1842
|
+
"monitor": cmd_monitor,
|
|
1055
1843
|
}
|
|
1056
1844
|
|
|
1057
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
|