vamp-easm 1.3__tar.gz → 1.4__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.3
3
+ Version: 1.4
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "vamp-easm"
7
- version = "1.3"
7
+ version = "1.4"
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.3
3
+ Version: 1.4
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
@@ -76,7 +76,7 @@ from vampsec_report import (
76
76
  # Constantes
77
77
  # ---------------------------------------------------------------------------
78
78
 
79
- VERSION = "1.3"
79
+ VERSION = "1.4"
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.3 · External Attack Surface Management\n"
90
+ " vamp-easm v1.4 · 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
  )
@@ -372,6 +372,198 @@ class ShodanEASMEnricher:
372
372
  ))
373
373
 
374
374
 
375
+ # ---------------------------------------------------------------------------
376
+ # Integración Censys (v1.4)
377
+ # ---------------------------------------------------------------------------
378
+
379
+ # URL base de la API Censys v2
380
+ _CENSYS_SEARCH_URL = "https://search.censys.io/api/v2/hosts/search"
381
+
382
+ # Puertos considerados sensibles para enriquecer hallazgos Censys
383
+ _CENSYS_PUERTOS_SENSIBLES = {21, 22, 23, 25, 80, 443, 445, 3389, 1433, 3306, 5432, 5900, 6379, 8080, 8443, 27017, 9200}
384
+
385
+
386
+ class CensysEASMEnricher:
387
+ """
388
+ Enriquecimiento EASM con la API Censys v2.
389
+
390
+ Consulta el endpoint de búsqueda de hosts Censys usando la query
391
+ ``parsed.names:<dominio>`` para detectar hosts indexados por Censys
392
+ que no fueron descubiertos por crt.sh/HackerTarget.
393
+
394
+ Hallazgos emitidos:
395
+ - CENSYS_HOST_EXPOSED: host encontrado por Censys pero no en el
396
+ escaneo activo (superficie no visible para la organización).
397
+ - Puerto sensible abierto según Censys: MEDIUM.
398
+
399
+ Límite: 1 petición por segundo (rate limit de la API gratuita).
400
+ Resultados por página: 100 hosts (máximo permitido por la API).
401
+ """
402
+
403
+ _MAX_PAGINAS = 3 # máximo de páginas (300 hosts) por invocación
404
+
405
+ def __init__(self, censys_id: str, censys_secret: str) -> None:
406
+ self._id = censys_id
407
+ self._secret = censys_secret
408
+
409
+ def _auth_header(self) -> str:
410
+ """Devuelve el valor de la cabecera Authorization (Basic Auth)."""
411
+ import base64 as _b64
412
+ creds = f"{self._id}:{self._secret}".encode()
413
+ return "Basic " + _b64.b64encode(creds).decode()
414
+
415
+ def enriquecer(
416
+ self,
417
+ dominio: str,
418
+ ips_conocidas: set,
419
+ findings: list,
420
+ ) -> None:
421
+ """
422
+ Consulta Censys por el dominio y añade hallazgos a la lista recibida.
423
+
424
+ Parámetros
425
+ ----------
426
+ dominio : str — Dominio objetivo
427
+ ips_conocidas : set[str] — IPs ya descubiertas por el escaneo
428
+ findings : list — Lista de Finding a la que se añaden hallazgos
429
+ """
430
+ import urllib.request as _ureq
431
+ import urllib.error as _uerr
432
+ import json as _json
433
+ import time as _time
434
+
435
+ auth = self._auth_header()
436
+ idx_base = max(
437
+ (int(f.id.split("-")[-1]) for f in findings if "-" in f.id),
438
+ default=0,
439
+ )
440
+
441
+ cursor: str | None = None
442
+ pagina = 0
443
+ hosts_censys: list[dict] = []
444
+
445
+ while pagina < self._MAX_PAGINAS:
446
+ # Construir URL con paginación
447
+ params = f"q=parsed.names%3A{_ureq.quote(dominio)}&per_page=100"
448
+ if cursor:
449
+ params += f"&cursor={_ureq.quote(cursor)}"
450
+ url = f"{_CENSYS_SEARCH_URL}?{params}"
451
+
452
+ try:
453
+ req = _ureq.Request(
454
+ url,
455
+ headers={
456
+ "Authorization": auth,
457
+ "User-Agent": f"vamp-easm/{VERSION}",
458
+ "Accept": "application/json",
459
+ },
460
+ )
461
+ with _ureq.urlopen(req, timeout=20) as resp:
462
+ if resp.status != 200:
463
+ break
464
+ data = _json.loads(resp.read())
465
+ except _uerr.HTTPError as e:
466
+ if e.code in (401, 403):
467
+ findings.append(Finding(
468
+ id = f"EASM-CNS-{idx_base + 1:03d}",
469
+ title = "Credenciales Censys inválidas o sin permisos",
470
+ severity = "INFO",
471
+ description = (
472
+ f"La API Censys devolvió {e.code}. "
473
+ "Verificar --censys-id y --censys-secret."
474
+ ),
475
+ evidence = f"URL: {_CENSYS_SEARCH_URL}",
476
+ affected = dominio,
477
+ remediation = "Comprobar credenciales en https://search.censys.io/account",
478
+ tags = ["censys", "easm"],
479
+ ))
480
+ break
481
+ except Exception:
482
+ break
483
+
484
+ lote = data.get("result", {}).get("hits", [])
485
+ hosts_censys.extend(lote)
486
+ cursor = data.get("result", {}).get("links", {}).get("next")
487
+ pagina += 1
488
+
489
+ if not cursor or not lote:
490
+ break
491
+
492
+ # Respetar el límite de 1 req/seg de la API gratuita
493
+ _time.sleep(1.0)
494
+
495
+ # Procesar cada host devuelto por Censys
496
+ for host in hosts_censys:
497
+ ip = host.get("ip", "")
498
+ nombres = host.get("names", []) or []
499
+ servicios = host.get("services", []) or []
500
+ puertos = [s.get("port") for s in servicios if s.get("port")]
501
+
502
+ # Si la IP no fue encontrada por crt.sh/HackerTarget → hallazgo
503
+ if ip and ip not in ips_conocidas:
504
+ idx_base += 1
505
+ nombres_str = ", ".join(nombres[:5]) or "—"
506
+ puertos_str = ", ".join(str(p) for p in puertos[:10]) or "—"
507
+ findings.append(Finding(
508
+ id = f"EASM-CNS-{idx_base:03d}",
509
+ title = f"Host expuesto detectado por Censys (no en escaneo activo) — {ip}",
510
+ severity = "HIGH",
511
+ description = (
512
+ f"Censys indexa el host {ip} asociado al dominio '{dominio}' "
513
+ "pero no fue descubierto por crt.sh ni HackerTarget. Puede "
514
+ "tratarse de infraestructura shadow, activos olvidados o "
515
+ "exposición no intencionada."
516
+ ),
517
+ evidence = (
518
+ f"IP: {ip}\n"
519
+ f"Nombres DNS (Censys): {nombres_str}\n"
520
+ f"Puertos indexados: {puertos_str}\n"
521
+ f"Dominio consultado: {dominio}\n"
522
+ "Fuente: Censys API v2 (datos históricos — verificar estado actual)"
523
+ ),
524
+ affected = ip,
525
+ remediation = (
526
+ "Verificar si el activo pertenece a la organización. "
527
+ "Si es legítimo, incluirlo en el inventario de superficie de ataque. "
528
+ "Si no, investigar posible shadow IT o activo olvidado."
529
+ ),
530
+ tags = ["censys", "easm", "CENSYS_HOST_EXPOSED"],
531
+ ))
532
+
533
+ # Puertos sensibles abiertos según Censys (para IPs conocidas y nuevas)
534
+ sensibles = [p for p in puertos if p in _CENSYS_PUERTOS_SENSIBLES]
535
+ for puerto in sensibles:
536
+ # Evitar duplicados con hallazgos Shodan
537
+ ya_reportado = any(
538
+ f"{ip}:{puerto}" in (getattr(f, "affected", "") or "")
539
+ for f in findings
540
+ )
541
+ if ya_reportado:
542
+ continue
543
+ idx_base += 1
544
+ findings.append(Finding(
545
+ id = f"EASM-CNS-{idx_base:03d}",
546
+ title = f"Puerto sensible {puerto} abierto según Censys — {ip}",
547
+ severity = "MEDIUM",
548
+ description = (
549
+ f"Censys ha indexado el puerto {puerto} como abierto en "
550
+ f"la IP {ip}. Este puerto corresponde a un servicio de "
551
+ "alto riesgo si está expuesto sin control de acceso."
552
+ ),
553
+ evidence = (
554
+ f"IP: {ip} Puerto: {puerto}\n"
555
+ f"Servicios Censys: {', '.join(s.get('service_name','?') for s in servicios if s.get('port') == puerto) or '—'}\n"
556
+ "Fuente: Censys API v2"
557
+ ),
558
+ affected = f"{ip}:{puerto}",
559
+ remediation = (
560
+ f"Comprobar si el puerto {puerto} debe estar expuesto a internet. "
561
+ "Si no es necesario, restringir acceso con firewall."
562
+ ),
563
+ tags = ["censys", "easm", "port"],
564
+ ))
565
+
566
+
375
567
  # ---------------------------------------------------------------------------
376
568
  # Comando: scan
377
569
  # ---------------------------------------------------------------------------
@@ -477,6 +669,14 @@ def cmd_scan(args: argparse.Namespace) -> int:
477
669
  "[dim] ℹ Shodan no activo — usa --shodan-key para enriquecer con datos Shodan[/dim]"
478
670
  )
479
671
 
672
+ # Leer claves Censys (opcional, v1.4)
673
+ censys_id = getattr(args, "censys_id", None) or __import__("os").environ.get("CENSYS_API_ID", "")
674
+ censys_secret = getattr(args, "censys_secret", None) or __import__("os").environ.get("CENSYS_API_SECRET", "")
675
+ if not (censys_id and censys_secret):
676
+ console.print(
677
+ "[dim] ℹ Censys no activo — usa --censys-id y --censys-secret para enriquecer con datos Censys[/dim]"
678
+ )
679
+
480
680
  # ── Motor de diffs ────────────────────────────────────────────────────
481
681
  console.print("\n[cyan]►[/cyan] [bold]Calculando diffs respecto al escaneo anterior…[/bold]")
482
682
  scan_anterior = storage.ultimo_scan(target)
@@ -521,6 +721,24 @@ def cmd_scan(args: argparse.Namespace) -> int:
521
721
  else:
522
722
  console.print(" [dim]Sin IPs resueltas para consultar Shodan.[/dim]")
523
723
 
724
+ # ── Enriquecimiento Censys (v1.4, opcional) ───────────────────────────
725
+ findings_censys: List[Finding] = []
726
+ if censys_id and censys_secret:
727
+ console.print("\n[cyan]►[/cyan] [bold]Enriquecimiento Censys (por dominio)…[/bold]")
728
+ # Recopilar IPs ya conocidas del escaneo activo
729
+ ips_conocidas: set = set()
730
+ for sub in resultado.subdominios:
731
+ if sub.ips:
732
+ ips_conocidas.update(sub.ips)
733
+
734
+ censys_enricher = CensysEASMEnricher(censys_id, censys_secret)
735
+ censys_enricher.enriquecer(target, ips_conocidas, findings_censys)
736
+ nuevos = len([f for f in findings_censys if "CENSYS_HOST_EXPOSED" in (getattr(f, "tags", None) or [])])
737
+ console.print(
738
+ f" {nuevos} host(s) nuevos (CENSYS_HOST_EXPOSED) · "
739
+ f"{len(findings_censys)} hallazgos totales Censys"
740
+ )
741
+
524
742
  # ── Mostrar resultados ────────────────────────────────────────────────
525
743
  if diffs:
526
744
  console.print(_tabla_diffs(diffs))
@@ -538,6 +756,8 @@ def cmd_scan(args: argparse.Namespace) -> int:
538
756
  findings = [d.finding for d in diffs if d.finding is not None]
539
757
  # Incluir hallazgos Shodan en el informe si los hay
540
758
  findings.extend(findings_shodan)
759
+ # Incluir hallazgos Censys en el informe si los hay (v1.4)
760
+ findings.extend(findings_censys)
541
761
  if findings:
542
762
  meta = meta_from_args(args, tool=TOOL, version=VERSION)
543
763
  meta.scope = target
@@ -961,6 +1181,16 @@ def construir_parser() -> argparse.ArgumentParser:
961
1181
  help="Clave API Shodan (o var SHODAN_API_KEY) — enriquece IPs descubiertas "
962
1182
  "con puertos, servicios y vulnerabilidades indexadas por Shodan",
963
1183
  )
1184
+ p_scan.add_argument(
1185
+ "--censys-id", metavar="ID", dest="censys_id",
1186
+ help="ID de la API Censys v2 (o var CENSYS_API_ID) — enriquece el escaneo "
1187
+ "con hosts detectados por Censys no visibles en crt.sh/HackerTarget (v1.4)",
1188
+ )
1189
+ p_scan.add_argument(
1190
+ "--censys-secret", metavar="SECRET", dest="censys_secret",
1191
+ help="Secret de la API Censys v2 (o var CENSYS_API_SECRET) — requerido junto "
1192
+ "con --censys-id para activar el enriquecimiento Censys (v1.4)",
1193
+ )
964
1194
  add_report_args(p_scan)
965
1195
 
966
1196
  # ── history ───────────────────────────────────────────────────────────
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes