vamp-easm 1.2__tar.gz → 1.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {vamp_easm-1.2 → vamp_easm-1.3}/PKG-INFO +1 -1
- {vamp_easm-1.2 → vamp_easm-1.3}/pyproject.toml +1 -1
- {vamp_easm-1.2 → vamp_easm-1.3}/vamp_easm.egg-info/PKG-INFO +1 -1
- {vamp_easm-1.2 → vamp_easm-1.3}/vamp_easm.py +218 -2
- {vamp_easm-1.2 → vamp_easm-1.3}/LICENSE +0 -0
- {vamp_easm-1.2 → vamp_easm-1.3}/README.md +0 -0
- {vamp_easm-1.2 → vamp_easm-1.3}/easm/__init__.py +0 -0
- {vamp_easm-1.2 → vamp_easm-1.3}/easm/alerter.py +0 -0
- {vamp_easm-1.2 → vamp_easm-1.3}/easm/differ.py +0 -0
- {vamp_easm-1.2 → vamp_easm-1.3}/easm/scanner.py +0 -0
- {vamp_easm-1.2 → vamp_easm-1.3}/easm/storage.py +0 -0
- {vamp_easm-1.2 → vamp_easm-1.3}/setup.cfg +0 -0
- {vamp_easm-1.2 → vamp_easm-1.3}/vamp_easm.egg-info/SOURCES.txt +0 -0
- {vamp_easm-1.2 → vamp_easm-1.3}/vamp_easm.egg-info/dependency_links.txt +0 -0
- {vamp_easm-1.2 → vamp_easm-1.3}/vamp_easm.egg-info/entry_points.txt +0 -0
- {vamp_easm-1.2 → vamp_easm-1.3}/vamp_easm.egg-info/requires.txt +0 -0
- {vamp_easm-1.2 → vamp_easm-1.3}/vamp_easm.egg-info/top_level.txt +0 -0
- {vamp_easm-1.2 → vamp_easm-1.3}/vampsec_report.py +0 -0
|
@@ -76,7 +76,7 @@ from vampsec_report import (
|
|
|
76
76
|
# Constantes
|
|
77
77
|
# ---------------------------------------------------------------------------
|
|
78
78
|
|
|
79
|
-
VERSION = "1.
|
|
79
|
+
VERSION = "1.3"
|
|
80
80
|
TOOL = "vamp-easm"
|
|
81
81
|
BRAND = "VampSecure Labs — EASM Continuo"
|
|
82
82
|
|
|
@@ -87,7 +87,7 @@ BANNER = (
|
|
|
87
87
|
" \\ V / _ \\| |\\/| | _/\\__ \\ _| (__| |_| | / _|| |__ / _ \\| _ \\__ \\\n"
|
|
88
88
|
" \\_/_/ \\_\\_| |_|_| |___/___\\___|\\___/|_|_\\___|____/_/ \\_\\___/___/\n"
|
|
89
89
|
' by Antonio Hernandez "Belky" — VampSecure Studios\n'
|
|
90
|
-
" vamp-easm v1.
|
|
90
|
+
" vamp-easm v1.3 · External Attack Surface Management\n"
|
|
91
91
|
" ────────────────────────────────────────────────────────────────────────\n"
|
|
92
92
|
" USO EXCLUSIVO EN AUDITORÍAS AUTORIZADAS · El uso no autorizado es ilegal\n"
|
|
93
93
|
)
|
|
@@ -196,6 +196,182 @@ def _tabla_historial(rows) -> Table:
|
|
|
196
196
|
return tabla
|
|
197
197
|
|
|
198
198
|
|
|
199
|
+
# ---------------------------------------------------------------------------
|
|
200
|
+
# Integración Shodan (v1.3)
|
|
201
|
+
# ---------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
# Puertos considerados sensibles para emitir hallazgo MEDIUM
|
|
204
|
+
_SHODAN_PUERTOS_SENSIBLES = {21, 23, 445, 3389, 1433, 3306, 5432, 5900, 6379, 27017, 9200}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class ShodanEASMEnricher:
|
|
208
|
+
"""
|
|
209
|
+
Enriquecimiento EASM con la API REST de Shodan por IP.
|
|
210
|
+
|
|
211
|
+
Para cada IP descubierta en el escaneo consulta
|
|
212
|
+
https://api.shodan.io/shodan/host/<ip>?key=<api_key>
|
|
213
|
+
y añade hallazgos al listado de findings del escaneo:
|
|
214
|
+
|
|
215
|
+
- Puerto sensible abierto → MEDIUM
|
|
216
|
+
- Vulnerabilidades en campo 'vulns' → HIGH
|
|
217
|
+
- Hostname alternativo → INFO
|
|
218
|
+
|
|
219
|
+
Límite: 10 IPs máximo por invocación (para no agotar créditos API).
|
|
220
|
+
"""
|
|
221
|
+
|
|
222
|
+
_HOST_URL = "https://api.shodan.io/shodan/host/{ip}?key={key}"
|
|
223
|
+
_MAX_IPS = 10
|
|
224
|
+
|
|
225
|
+
def __init__(self, api_key: str) -> None:
|
|
226
|
+
self._key = api_key
|
|
227
|
+
|
|
228
|
+
def _enrich_with_shodan(
|
|
229
|
+
self,
|
|
230
|
+
ips_or_hosts: list,
|
|
231
|
+
api_key: str,
|
|
232
|
+
findings: list,
|
|
233
|
+
) -> None:
|
|
234
|
+
"""
|
|
235
|
+
Enriquece la lista de findings con datos Shodan para cada IP.
|
|
236
|
+
|
|
237
|
+
Modifica findings in-place añadiendo los hallazgos relevantes.
|
|
238
|
+
|
|
239
|
+
Parámetros
|
|
240
|
+
----------
|
|
241
|
+
ips_or_hosts : list[str] — IPs o nombres de host a consultar
|
|
242
|
+
api_key : str — Clave API de Shodan
|
|
243
|
+
findings : list — Lista de Finding a la que se añaden los hallazgos
|
|
244
|
+
"""
|
|
245
|
+
import urllib.request as _ureq
|
|
246
|
+
import json as _json
|
|
247
|
+
|
|
248
|
+
# Filtrar solo IPs (IPv4 básico) y descartar repeticiones
|
|
249
|
+
ips_validas = []
|
|
250
|
+
vistas: set = set()
|
|
251
|
+
for h in ips_or_hosts:
|
|
252
|
+
h = h.strip()
|
|
253
|
+
partes = h.split(".")
|
|
254
|
+
if len(partes) == 4 and all(p.isdigit() for p in partes) and h not in vistas:
|
|
255
|
+
ips_validas.append(h)
|
|
256
|
+
vistas.add(h)
|
|
257
|
+
|
|
258
|
+
ips_validas = ips_validas[: self._MAX_IPS]
|
|
259
|
+
|
|
260
|
+
idx_base = max((int(f.id.split("-")[-1]) for f in findings if "-" in f.id), default=0)
|
|
261
|
+
|
|
262
|
+
for i, ip in enumerate(ips_validas, start=1):
|
|
263
|
+
url = self._HOST_URL.format(ip=ip, key=api_key)
|
|
264
|
+
try:
|
|
265
|
+
req = _ureq.Request(
|
|
266
|
+
url,
|
|
267
|
+
headers={"User-Agent": f"vamp-easm/{VERSION}"},
|
|
268
|
+
)
|
|
269
|
+
with _ureq.urlopen(req, timeout=15) as resp:
|
|
270
|
+
codigo = resp.status
|
|
271
|
+
if codigo == 401:
|
|
272
|
+
findings.append(Finding(
|
|
273
|
+
id = f"EASM-SHD-{idx_base + i:03d}",
|
|
274
|
+
title = "API key Shodan inválida o sin créditos",
|
|
275
|
+
severity = "INFO",
|
|
276
|
+
description = "La clave API de Shodan devolvió 401. Verificar validez y cuota.",
|
|
277
|
+
evidence = f"URL: {self._HOST_URL.format(ip=ip, key='***')}",
|
|
278
|
+
affected = ip,
|
|
279
|
+
remediation = "Comprobar la clave API en https://account.shodan.io/",
|
|
280
|
+
tags = ["shodan", "easm"],
|
|
281
|
+
))
|
|
282
|
+
return # Si 401, todas las demás peticiones fallarán también
|
|
283
|
+
if codigo == 404:
|
|
284
|
+
# IP no indexada en Shodan: INFO silencioso
|
|
285
|
+
continue
|
|
286
|
+
if codigo != 200:
|
|
287
|
+
continue
|
|
288
|
+
data = _json.loads(resp.read())
|
|
289
|
+
except Exception:
|
|
290
|
+
# Error de red o timeout: continuar con la siguiente IP
|
|
291
|
+
continue
|
|
292
|
+
|
|
293
|
+
puertos: list = data.get("ports", []) or []
|
|
294
|
+
vulns_raw: dict = data.get("vulns", {}) or {}
|
|
295
|
+
hostnames: list = data.get("hostnames", []) or []
|
|
296
|
+
org: str = data.get("org", "") or ""
|
|
297
|
+
sistema: str = data.get("os", "") or ""
|
|
298
|
+
|
|
299
|
+
# Hallazgo: puertos sensibles abiertos
|
|
300
|
+
sensibles = [p for p in puertos if p in _SHODAN_PUERTOS_SENSIBLES]
|
|
301
|
+
for puerto in sensibles:
|
|
302
|
+
idx_base += 1
|
|
303
|
+
findings.append(Finding(
|
|
304
|
+
id = f"EASM-SHD-{idx_base:03d}",
|
|
305
|
+
title = f"Puerto sensible {puerto} abierto según Shodan — {ip}",
|
|
306
|
+
severity = "MEDIUM",
|
|
307
|
+
description = (
|
|
308
|
+
f"Shodan ha indexado el puerto {puerto} como abierto en la IP {ip}. "
|
|
309
|
+
"Este puerto corresponde a un servicio de alto riesgo si está expuesto "
|
|
310
|
+
"sin control de acceso."
|
|
311
|
+
),
|
|
312
|
+
evidence = (
|
|
313
|
+
f"IP: {ip}\n"
|
|
314
|
+
f"Puerto: {puerto}\n"
|
|
315
|
+
f"Org: {org or '—'}\n"
|
|
316
|
+
f"OS: {sistema or '—'}\n"
|
|
317
|
+
f"Fuente: Shodan (datos históricos — verificar estado actual)"
|
|
318
|
+
),
|
|
319
|
+
affected = f"{ip}:{puerto}",
|
|
320
|
+
remediation = (
|
|
321
|
+
f"Verificar si el puerto {puerto} en {ip} debe estar accesible desde Internet. "
|
|
322
|
+
"Aplicar reglas de firewall para restringir el acceso por IP de origen. "
|
|
323
|
+
"Actualizar el servicio a la última versión segura."
|
|
324
|
+
),
|
|
325
|
+
tags = ["shodan", "easm", "exposed-port"],
|
|
326
|
+
))
|
|
327
|
+
|
|
328
|
+
# Hallazgo: vulnerabilidades reportadas por Shodan (campo 'vulns')
|
|
329
|
+
if vulns_raw:
|
|
330
|
+
cves_str = ", ".join(list(vulns_raw.keys())[:10])
|
|
331
|
+
idx_base += 1
|
|
332
|
+
findings.append(Finding(
|
|
333
|
+
id = f"EASM-SHD-{idx_base:03d}",
|
|
334
|
+
title = f"Vulnerabilidades Shodan en {ip}: {cves_str[:60]}",
|
|
335
|
+
severity = "HIGH",
|
|
336
|
+
description = (
|
|
337
|
+
f"Shodan reporta {len(vulns_raw)} vulnerabilidad(es) conocida(s) en la IP {ip}. "
|
|
338
|
+
"Estas vulnerabilidades pueden estar presentes en los servicios actualmente "
|
|
339
|
+
"expuestos según el índice de Shodan."
|
|
340
|
+
),
|
|
341
|
+
evidence = (
|
|
342
|
+
f"IP: {ip}\n"
|
|
343
|
+
f"Vulnerabilidades Shodan: {cves_str}\n"
|
|
344
|
+
f"Org: {org or '—'}\n"
|
|
345
|
+
f"Fuente: Shodan — los datos pueden tener latencia de semanas"
|
|
346
|
+
),
|
|
347
|
+
affected = ip,
|
|
348
|
+
remediation = (
|
|
349
|
+
"Verificar el estado actual de las vulnerabilidades listadas en los servicios "
|
|
350
|
+
"del host. Consultar el NVD para detalles de parches y aplicarlos. "
|
|
351
|
+
"Referencia: https://nvd.nist.gov/"
|
|
352
|
+
),
|
|
353
|
+
cve = list(vulns_raw.keys())[0] if vulns_raw else None,
|
|
354
|
+
tags = ["shodan", "easm", "cve", "vulnerability"],
|
|
355
|
+
))
|
|
356
|
+
|
|
357
|
+
# Hallazgo INFO: hostname alternativo reportado por Shodan
|
|
358
|
+
for hostname in hostnames[:3]:
|
|
359
|
+
idx_base += 1
|
|
360
|
+
findings.append(Finding(
|
|
361
|
+
id = f"EASM-SHD-{idx_base:03d}",
|
|
362
|
+
title = f"Shodan reporta hostname alternativo: {hostname}",
|
|
363
|
+
severity = "INFO",
|
|
364
|
+
description = (
|
|
365
|
+
f"Shodan indexa el hostname '{hostname}' para la IP {ip}. "
|
|
366
|
+
"Puede indicar servicios adicionales o dominios compartiendo esta IP."
|
|
367
|
+
),
|
|
368
|
+
evidence = f"IP: {ip}\nHostname Shodan: {hostname}",
|
|
369
|
+
affected = ip,
|
|
370
|
+
remediation = "Verificar que el hostname corresponde a infraestructura autorizada.",
|
|
371
|
+
tags = ["shodan", "easm", "hostname"],
|
|
372
|
+
))
|
|
373
|
+
|
|
374
|
+
|
|
199
375
|
# ---------------------------------------------------------------------------
|
|
200
376
|
# Comando: scan
|
|
201
377
|
# ---------------------------------------------------------------------------
|
|
@@ -291,6 +467,16 @@ def cmd_scan(args: argparse.Namespace) -> int:
|
|
|
291
467
|
certs_ok = [c for c in resultado.certs if not c.error]
|
|
292
468
|
console.print(f" {len(certs_ok)} certificados inspeccionados.")
|
|
293
469
|
|
|
470
|
+
# ── Nota Shodan al inicio (si no se proporcionó clave) ───────────────────
|
|
471
|
+
shodan_key_easm = (
|
|
472
|
+
getattr(args, "shodan_key", None)
|
|
473
|
+
or __import__("os").environ.get("SHODAN_API_KEY", "")
|
|
474
|
+
)
|
|
475
|
+
if not shodan_key_easm:
|
|
476
|
+
console.print(
|
|
477
|
+
"[dim] ℹ Shodan no activo — usa --shodan-key para enriquecer con datos Shodan[/dim]"
|
|
478
|
+
)
|
|
479
|
+
|
|
294
480
|
# ── Motor de diffs ────────────────────────────────────────────────────
|
|
295
481
|
console.print("\n[cyan]►[/cyan] [bold]Calculando diffs respecto al escaneo anterior…[/bold]")
|
|
296
482
|
scan_anterior = storage.ultimo_scan(target)
|
|
@@ -317,6 +503,24 @@ def cmd_scan(args: argparse.Namespace) -> int:
|
|
|
317
503
|
# Cerrar escaneo en SQLite
|
|
318
504
|
storage.cerrar_scan(scan_id, assets_found=assets_total, diffs_found=len(diffs))
|
|
319
505
|
|
|
506
|
+
# ── Enriquecimiento Shodan (opcional) ────────────────────────────────────
|
|
507
|
+
findings_shodan: List[Finding] = []
|
|
508
|
+
if shodan_key_easm:
|
|
509
|
+
console.print("\n[cyan]►[/cyan] [bold]Enriquecimiento Shodan (por IP)…[/bold]")
|
|
510
|
+
ips_descubiertas = []
|
|
511
|
+
for sub in resultado.subdominios:
|
|
512
|
+
if sub.ips:
|
|
513
|
+
ips_descubiertas.extend(sub.ips)
|
|
514
|
+
if ips_descubiertas:
|
|
515
|
+
enricher = ShodanEASMEnricher(shodan_key_easm)
|
|
516
|
+
enricher._enrich_with_shodan(ips_descubiertas, shodan_key_easm, findings_shodan)
|
|
517
|
+
console.print(
|
|
518
|
+
f" {len([f for f in findings_shodan if f.severity in ('HIGH','MEDIUM')])} "
|
|
519
|
+
f"hallazgos Shodan (MEDIUM+HIGH) · {len(findings_shodan)} totales"
|
|
520
|
+
)
|
|
521
|
+
else:
|
|
522
|
+
console.print(" [dim]Sin IPs resueltas para consultar Shodan.[/dim]")
|
|
523
|
+
|
|
320
524
|
# ── Mostrar resultados ────────────────────────────────────────────────
|
|
321
525
|
if diffs:
|
|
322
526
|
console.print(_tabla_diffs(diffs))
|
|
@@ -332,6 +536,8 @@ def cmd_scan(args: argparse.Namespace) -> int:
|
|
|
332
536
|
|
|
333
537
|
# ── Exportar informes si se pidió ─────────────────────────────────────
|
|
334
538
|
findings = [d.finding for d in diffs if d.finding is not None]
|
|
539
|
+
# Incluir hallazgos Shodan en el informe si los hay
|
|
540
|
+
findings.extend(findings_shodan)
|
|
335
541
|
if findings:
|
|
336
542
|
meta = meta_from_args(args, tool=TOOL, version=VERSION)
|
|
337
543
|
meta.scope = target
|
|
@@ -750,6 +956,11 @@ def construir_parser() -> argparse.ArgumentParser:
|
|
|
750
956
|
"--alert-webhook", metavar="URL", dest="alert_webhook",
|
|
751
957
|
help="URL del webhook para alertas CRITICAL/HIGH (alternativa: env EASM_ALERT_WEBHOOK)",
|
|
752
958
|
)
|
|
959
|
+
p_scan.add_argument(
|
|
960
|
+
"--shodan-key", metavar="API_KEY", dest="shodan_key",
|
|
961
|
+
help="Clave API Shodan (o var SHODAN_API_KEY) — enriquece IPs descubiertas "
|
|
962
|
+
"con puertos, servicios y vulnerabilidades indexadas por Shodan",
|
|
963
|
+
)
|
|
753
964
|
add_report_args(p_scan)
|
|
754
965
|
|
|
755
966
|
# ── history ───────────────────────────────────────────────────────────
|
|
@@ -794,6 +1005,11 @@ def construir_parser() -> argparse.ArgumentParser:
|
|
|
794
1005
|
"--nmap", action="store_true",
|
|
795
1006
|
help="Usar nmap como backend de escaneo de puertos",
|
|
796
1007
|
)
|
|
1008
|
+
p_watch.add_argument(
|
|
1009
|
+
"--shodan-key", metavar="API_KEY", dest="shodan_key",
|
|
1010
|
+
help="Clave API Shodan (o var SHODAN_API_KEY) — enriquece IPs descubiertas "
|
|
1011
|
+
"con datos Shodan en cada ciclo de watch",
|
|
1012
|
+
)
|
|
797
1013
|
|
|
798
1014
|
# ── diff ──────────────────────────────────────────────────────────────
|
|
799
1015
|
p_diff = subparsers.add_parser(
|
|
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
|