vamp-easm 1.1__py3-none-any.whl
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.
- easm/__init__.py +5 -0
- easm/alerter.py +207 -0
- easm/differ.py +374 -0
- easm/scanner.py +517 -0
- easm/storage.py +295 -0
- vamp_easm-1.1.dist-info/METADATA +210 -0
- vamp_easm-1.1.dist-info/RECORD +12 -0
- vamp_easm-1.1.dist-info/WHEEL +5 -0
- vamp_easm-1.1.dist-info/entry_points.txt +2 -0
- vamp_easm-1.1.dist-info/top_level.txt +3 -0
- vamp_easm.py +563 -0
- vampsec_report.py +959 -0
vamp_easm.py
ADDED
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# © VampSecure Studios — VampSecure Labs Security Research Division
|
|
3
|
+
"""
|
|
4
|
+
vamp_easm.py — Motor de Attack Surface Management Continuo
|
|
5
|
+
===========================================================
|
|
6
|
+
VampSecure Labs · VampSecure Studios
|
|
7
|
+
Para Uso Exclusivo en Pruebas de Penetración Autorizadas — v1.0
|
|
8
|
+
|
|
9
|
+
DESCRIPCIÓN GENERAL
|
|
10
|
+
-------------------
|
|
11
|
+
Motor de EASM (External Attack Surface Management) continuo que escanea
|
|
12
|
+
la superficie de ataque de un dominio objetivo, registra el historial en
|
|
13
|
+
SQLite y genera alertas cuando detecta cambios respecto al escaneo anterior.
|
|
14
|
+
|
|
15
|
+
El diferencial frente a escaneos puntuales es la **continuidad**: cada
|
|
16
|
+
ejecución mide deltas (diffs) contra el estado anterior, permitiendo
|
|
17
|
+
detectar nuevos activos, cambios de IP, puertos nuevos, certificados
|
|
18
|
+
expirados o cambiados.
|
|
19
|
+
|
|
20
|
+
ESCANEO EN TRES CAPAS
|
|
21
|
+
----------------------
|
|
22
|
+
1. DNS/Subdominios — crt.sh + HackerTarget (urllib stdlib)
|
|
23
|
+
2. Puertos/Servicios — asyncio TCP connect (o nmap con --nmap)
|
|
24
|
+
3. Certificados TLS — inspección con ssl stdlib (SANs, expiración, fingerprint)
|
|
25
|
+
|
|
26
|
+
HISTORIAL SQLite
|
|
27
|
+
----------------
|
|
28
|
+
vamp_easm.db (directorio de trabajo) — tablas: scans, assets, certs
|
|
29
|
+
|
|
30
|
+
DIFFS DETECTADOS
|
|
31
|
+
----------------
|
|
32
|
+
NUEVO_SUBDOMINIO HIGH CERT_EXPIRADO HIGH/CRITICAL
|
|
33
|
+
NUEVO_PUERTO MEDIUM CERT_CAMBIADO CRITICAL
|
|
34
|
+
SERVICIO_DESAPARECIDO LOW IP_CAMBIADA MEDIUM
|
|
35
|
+
|
|
36
|
+
USO
|
|
37
|
+
---
|
|
38
|
+
python vamp_easm.py scan --target ejemplo.com [--ports top100|22,80,443]
|
|
39
|
+
python vamp_easm.py history --target ejemplo.com [--limit 10]
|
|
40
|
+
python vamp_easm.py assets --target ejemplo.com
|
|
41
|
+
python vamp_easm.py export --target ejemplo.com [--json] [--html]
|
|
42
|
+
|
|
43
|
+
EXIT CODES
|
|
44
|
+
----------
|
|
45
|
+
0 — Sin diffs o diffs de severidad MEDIUM/LOW
|
|
46
|
+
1 — Hay diffs de severidad HIGH
|
|
47
|
+
2 — Hay diffs de severidad CRITICAL
|
|
48
|
+
|
|
49
|
+
AUTORÍA
|
|
50
|
+
-------
|
|
51
|
+
© VampSecure Studios — VampSecure Labs Security Research Division
|
|
52
|
+
Todos los derechos reservados. Uso exclusivo en entornos autorizados.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
from __future__ import annotations
|
|
56
|
+
|
|
57
|
+
import argparse
|
|
58
|
+
import json
|
|
59
|
+
import sys
|
|
60
|
+
from datetime import datetime, timezone
|
|
61
|
+
from typing import List, Optional
|
|
62
|
+
|
|
63
|
+
from rich.console import Console
|
|
64
|
+
from rich.table import Table
|
|
65
|
+
from rich.panel import Panel
|
|
66
|
+
from rich import box
|
|
67
|
+
|
|
68
|
+
from easm import scanner, storage, differ, alerter
|
|
69
|
+
from vampsec_report import (
|
|
70
|
+
Finding, ReportMeta, VampSecReport,
|
|
71
|
+
add_report_args, meta_from_args,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
# Constantes
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
VERSION = "1.1"
|
|
79
|
+
TOOL = "vamp-easm"
|
|
80
|
+
BRAND = "VampSecure Labs — EASM Continuo"
|
|
81
|
+
|
|
82
|
+
BANNER = (
|
|
83
|
+
"\n"
|
|
84
|
+
"__ ___ __ __ ___ ___ ___ ___ _ _ ___ ___ _ _ ___ ___ \n"
|
|
85
|
+
"\\ \\ / /_\\ | \\/ | _ \\/ __| __/ __| | | | _ \\ __| | /_\\ | _ ) __|\n"
|
|
86
|
+
" \\ V / _ \\| |\\/| | _/\\__ \\ _| (__| |_| | / _|| |__ / _ \\| _ \\__ \\\n"
|
|
87
|
+
" \\_/_/ \\_\\_| |_|_| |___/___\\___|\\___/|_|_\\___|____/_/ \\_\\___/___/\n"
|
|
88
|
+
' by Antonio Hernandez "Belky" — VampSecure Studios\n'
|
|
89
|
+
" vamp-easm v1.1 · External Attack Surface Management\n"
|
|
90
|
+
" ────────────────────────────────────────────────────────────────────────\n"
|
|
91
|
+
" USO EXCLUSIVO EN AUDITORÍAS AUTORIZADAS · El uso no autorizado es ilegal\n"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Mapa de colores de severidad para Rich
|
|
95
|
+
_COLORES = {
|
|
96
|
+
"CRITICAL": "bold red",
|
|
97
|
+
"HIGH": "bold orange3",
|
|
98
|
+
"MEDIUM": "yellow",
|
|
99
|
+
"LOW": "blue",
|
|
100
|
+
"INFO": "dim",
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
console = Console()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# ---------------------------------------------------------------------------
|
|
107
|
+
# Helpers de presentación
|
|
108
|
+
# ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
def _badge_sev(sev: str) -> str:
|
|
111
|
+
"""Devuelve el texto de severidad con estilo Rich."""
|
|
112
|
+
color = _COLORES.get(sev, "white")
|
|
113
|
+
return f"[{color}]{sev}[/{color}]"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _ts_ahora() -> str:
|
|
117
|
+
return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _tabla_diffs(diffs: List[differ.Diff]) -> Table:
|
|
121
|
+
"""Construye una tabla Rich con los diffs del escaneo."""
|
|
122
|
+
tabla = Table(
|
|
123
|
+
title="[bold]Diffs detectados[/bold]",
|
|
124
|
+
box=box.ROUNDED,
|
|
125
|
+
show_header=True,
|
|
126
|
+
header_style="bold dim",
|
|
127
|
+
)
|
|
128
|
+
tabla.add_column("Finding", style="bold dim", width=12)
|
|
129
|
+
tabla.add_column("Categoría", style="cyan", width=24)
|
|
130
|
+
tabla.add_column("Severidad", width=12)
|
|
131
|
+
tabla.add_column("Activo", style="white", no_wrap=False)
|
|
132
|
+
|
|
133
|
+
for d in diffs:
|
|
134
|
+
fid = d.finding.id if d.finding else "—"
|
|
135
|
+
tabla.add_row(
|
|
136
|
+
fid,
|
|
137
|
+
d.categoria,
|
|
138
|
+
_badge_sev(d.severidad),
|
|
139
|
+
d.activo,
|
|
140
|
+
)
|
|
141
|
+
return tabla
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _tabla_assets(rows) -> Table:
|
|
145
|
+
"""Construye una tabla Rich con los activos de un target."""
|
|
146
|
+
tabla = Table(
|
|
147
|
+
title="[bold]Activos conocidos[/bold]",
|
|
148
|
+
box=box.SIMPLE_HEAVY,
|
|
149
|
+
show_header=True,
|
|
150
|
+
header_style="bold dim",
|
|
151
|
+
)
|
|
152
|
+
tabla.add_column("Subdominio", style="cyan", no_wrap=False)
|
|
153
|
+
tabla.add_column("IP", style="white", width=18)
|
|
154
|
+
tabla.add_column("Puerto", style="yellow", width=8)
|
|
155
|
+
tabla.add_column("Protocolo", style="dim", width=8)
|
|
156
|
+
tabla.add_column("Servicio", style="green", width=14)
|
|
157
|
+
tabla.add_column("Primera vista", style="dim", width=20)
|
|
158
|
+
tabla.add_column("Última vista", style="dim", width=20)
|
|
159
|
+
|
|
160
|
+
for r in rows:
|
|
161
|
+
tabla.add_row(
|
|
162
|
+
r["subdominio"],
|
|
163
|
+
r["ip"] or "—",
|
|
164
|
+
str(r["puerto"]) if r["puerto"] else "—",
|
|
165
|
+
r["protocolo"] or "tcp",
|
|
166
|
+
r["servicio"] or "—",
|
|
167
|
+
r["primera_vista"][:16] if r["primera_vista"] else "—",
|
|
168
|
+
r["ultima_vista"][:16] if r["ultima_vista"] else "—",
|
|
169
|
+
)
|
|
170
|
+
return tabla
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _tabla_historial(rows) -> Table:
|
|
174
|
+
"""Construye una tabla Rich con el historial de escaneos."""
|
|
175
|
+
tabla = Table(
|
|
176
|
+
title="[bold]Historial de escaneos[/bold]",
|
|
177
|
+
box=box.SIMPLE_HEAVY,
|
|
178
|
+
show_header=True,
|
|
179
|
+
header_style="bold dim",
|
|
180
|
+
)
|
|
181
|
+
tabla.add_column("Inicio", style="cyan", width=20)
|
|
182
|
+
tabla.add_column("Fin", style="dim", width=20)
|
|
183
|
+
tabla.add_column("Activos", style="green", width=10)
|
|
184
|
+
tabla.add_column("Diffs", style="yellow",width=8)
|
|
185
|
+
tabla.add_column("Scan ID", style="dim", width=38)
|
|
186
|
+
|
|
187
|
+
for r in rows:
|
|
188
|
+
tabla.add_row(
|
|
189
|
+
r["ts_start"][:16] if r["ts_start"] else "—",
|
|
190
|
+
r["ts_end"][:16] if r["ts_end"] else "en curso",
|
|
191
|
+
str(r["assets_found"]),
|
|
192
|
+
str(r["diffs_found"]),
|
|
193
|
+
r["id"],
|
|
194
|
+
)
|
|
195
|
+
return tabla
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# ---------------------------------------------------------------------------
|
|
199
|
+
# Comando: scan
|
|
200
|
+
# ---------------------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
def cmd_scan(args: argparse.Namespace) -> int:
|
|
203
|
+
"""
|
|
204
|
+
Ejecuta un escaneo completo del target, guarda resultados en SQLite,
|
|
205
|
+
calcula diffs respecto al escaneo anterior y muestra los hallazgos.
|
|
206
|
+
|
|
207
|
+
Returns
|
|
208
|
+
-------
|
|
209
|
+
int : Código de salida (0 limpio, 1 HIGH, 2 CRITICAL)
|
|
210
|
+
"""
|
|
211
|
+
target = args.target
|
|
212
|
+
|
|
213
|
+
# Inicializar la base de datos
|
|
214
|
+
storage.inicializar_db()
|
|
215
|
+
|
|
216
|
+
console.print(Panel(
|
|
217
|
+
f"[bold cyan]vamp-easm v{VERSION}[/bold cyan] · {BRAND}\n"
|
|
218
|
+
f"Target: [bold]{target}[/bold] · {_ts_ahora()}",
|
|
219
|
+
border_style="dim",
|
|
220
|
+
))
|
|
221
|
+
|
|
222
|
+
# ── Registrar inicio del escaneo ──────────────────────────────────────
|
|
223
|
+
scan_id = storage.nuevo_scan(target)
|
|
224
|
+
|
|
225
|
+
# ── Determinar lista de puertos ───────────────────────────────────────
|
|
226
|
+
if args.ports and args.ports != "top100":
|
|
227
|
+
try:
|
|
228
|
+
puertos = [int(p.strip()) for p in args.ports.split(",") if p.strip()]
|
|
229
|
+
except ValueError:
|
|
230
|
+
console.print("[red]Error:[/red] --ports debe ser 'top100' o una lista de puertos (ej: 22,80,443)")
|
|
231
|
+
return 1
|
|
232
|
+
else:
|
|
233
|
+
puertos = scanner.TOP_100_PORTS
|
|
234
|
+
|
|
235
|
+
# ── Capa 1: subdominios ───────────────────────────────────────────────
|
|
236
|
+
console.print("\n[cyan]►[/cyan] [bold]Fase 1:[/bold] Enumeración de subdominios (crt.sh + HackerTarget)…")
|
|
237
|
+
resultado = scanner.ejecutar_escaneo(
|
|
238
|
+
target=target,
|
|
239
|
+
puertos=puertos,
|
|
240
|
+
usar_nmap=getattr(args, "nmap", False),
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
# Guardar subdominios en SQLite (sin puerto)
|
|
244
|
+
for sub in resultado.subdominios:
|
|
245
|
+
ip = sub.ips[0] if sub.ips else None
|
|
246
|
+
storage.upsert_asset(
|
|
247
|
+
target=target,
|
|
248
|
+
subdominio=sub.nombre,
|
|
249
|
+
scan_id=scan_id,
|
|
250
|
+
ip=ip,
|
|
251
|
+
)
|
|
252
|
+
console.print(f" {len(resultado.subdominios)} subdominios encontrados.")
|
|
253
|
+
|
|
254
|
+
# ── Capa 2: puertos ───────────────────────────────────────────────────
|
|
255
|
+
console.print("[cyan]►[/cyan] [bold]Fase 2:[/bold] Escaneo de puertos…")
|
|
256
|
+
for pa in resultado.puertos:
|
|
257
|
+
# Resolver IP del host si es posible
|
|
258
|
+
ip_pa = None
|
|
259
|
+
for sub in resultado.subdominios:
|
|
260
|
+
if sub.nombre == pa.host:
|
|
261
|
+
ip_pa = sub.ips[0] if sub.ips else None
|
|
262
|
+
break
|
|
263
|
+
storage.upsert_asset(
|
|
264
|
+
target=target,
|
|
265
|
+
subdominio=pa.host,
|
|
266
|
+
scan_id=scan_id,
|
|
267
|
+
ip=ip_pa,
|
|
268
|
+
puerto=pa.puerto,
|
|
269
|
+
protocolo=pa.protocolo,
|
|
270
|
+
servicio=pa.servicio,
|
|
271
|
+
)
|
|
272
|
+
console.print(f" {len(resultado.puertos)} puertos abiertos encontrados.")
|
|
273
|
+
|
|
274
|
+
# ── Capa 3: certificados ──────────────────────────────────────────────
|
|
275
|
+
console.print("[cyan]►[/cyan] [bold]Fase 3:[/bold] Inspección de certificados TLS…")
|
|
276
|
+
for cert in resultado.certs:
|
|
277
|
+
if cert.error:
|
|
278
|
+
continue
|
|
279
|
+
storage.insertar_cert(
|
|
280
|
+
target=target,
|
|
281
|
+
host=cert.host,
|
|
282
|
+
scan_id=scan_id,
|
|
283
|
+
issued_to=cert.issued_to,
|
|
284
|
+
issuer=cert.issuer,
|
|
285
|
+
not_after=cert.not_after,
|
|
286
|
+
sans=",".join(cert.sans) if cert.sans else None,
|
|
287
|
+
fingerprint=cert.fingerprint,
|
|
288
|
+
autofirmado=cert.autofirmado,
|
|
289
|
+
)
|
|
290
|
+
certs_ok = [c for c in resultado.certs if not c.error]
|
|
291
|
+
console.print(f" {len(certs_ok)} certificados inspeccionados.")
|
|
292
|
+
|
|
293
|
+
# ── Motor de diffs ────────────────────────────────────────────────────
|
|
294
|
+
console.print("\n[cyan]►[/cyan] [bold]Calculando diffs respecto al escaneo anterior…[/bold]")
|
|
295
|
+
scan_anterior = storage.ultimo_scan(target)
|
|
296
|
+
# El último scan completado puede ser el actual si ya acabó (no debería),
|
|
297
|
+
# así que buscamos el penúltimo para comparar
|
|
298
|
+
historial = storage.historial_scans(target, limit=5)
|
|
299
|
+
scan_id_prev: Optional[str] = None
|
|
300
|
+
for row in historial:
|
|
301
|
+
if row["id"] != scan_id:
|
|
302
|
+
scan_id_prev = row["id"]
|
|
303
|
+
break
|
|
304
|
+
|
|
305
|
+
motor = differ.MotorDiff(
|
|
306
|
+
target=target,
|
|
307
|
+
scan_id=scan_id,
|
|
308
|
+
scan_id_prev=scan_id_prev,
|
|
309
|
+
storage=storage,
|
|
310
|
+
)
|
|
311
|
+
diffs = motor.calcular()
|
|
312
|
+
|
|
313
|
+
# Contar totales de activos
|
|
314
|
+
assets_total = len(resultado.subdominios) + len(resultado.puertos)
|
|
315
|
+
|
|
316
|
+
# Cerrar escaneo en SQLite
|
|
317
|
+
storage.cerrar_scan(scan_id, assets_found=assets_total, diffs_found=len(diffs))
|
|
318
|
+
|
|
319
|
+
# ── Mostrar resultados ────────────────────────────────────────────────
|
|
320
|
+
if diffs:
|
|
321
|
+
console.print(_tabla_diffs(diffs))
|
|
322
|
+
else:
|
|
323
|
+
if scan_id_prev:
|
|
324
|
+
console.print("\n[green]✓ Sin diffs respecto al escaneo anterior. Superficie estable.[/green]")
|
|
325
|
+
else:
|
|
326
|
+
console.print("\n[blue]ℹ Primer escaneo del target — baseline registrado. No hay escaneo anterior para comparar.[/blue]")
|
|
327
|
+
|
|
328
|
+
# ── Alertas ───────────────────────────────────────────────────────────
|
|
329
|
+
webhook_url = alerter.resolver_webhook_url(getattr(args, "alert_webhook", None))
|
|
330
|
+
alerter.gestionar_alertas(webhook_url, target, diffs, scan_id)
|
|
331
|
+
|
|
332
|
+
# ── Exportar informes si se pidió ─────────────────────────────────────
|
|
333
|
+
findings = [d.finding for d in diffs if d.finding is not None]
|
|
334
|
+
if findings:
|
|
335
|
+
meta = meta_from_args(args, tool=TOOL, version=VERSION)
|
|
336
|
+
meta.scope = target
|
|
337
|
+
informe = VampSecReport(meta, findings)
|
|
338
|
+
|
|
339
|
+
if getattr(args, "json", False):
|
|
340
|
+
fname = f"easm_{target}_{scan_id[:8]}.json"
|
|
341
|
+
informe.to_json(fname)
|
|
342
|
+
console.print(f"[green]→ JSON:[/green] {fname}")
|
|
343
|
+
|
|
344
|
+
if getattr(args, "html", False):
|
|
345
|
+
fname = f"easm_{target}_{scan_id[:8]}.html"
|
|
346
|
+
informe.to_html_client(fname)
|
|
347
|
+
console.print(f"[green]→ HTML:[/green] {fname}")
|
|
348
|
+
|
|
349
|
+
# ── Código de salida ──────────────────────────────────────────────────
|
|
350
|
+
sevs = {d.severidad for d in diffs}
|
|
351
|
+
if "CRITICAL" in sevs:
|
|
352
|
+
return 2
|
|
353
|
+
if "HIGH" in sevs:
|
|
354
|
+
return 1
|
|
355
|
+
return 0
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
# ---------------------------------------------------------------------------
|
|
359
|
+
# Comando: history
|
|
360
|
+
# ---------------------------------------------------------------------------
|
|
361
|
+
|
|
362
|
+
def cmd_history(args: argparse.Namespace) -> int:
|
|
363
|
+
"""Muestra el historial de escaneos para el target especificado."""
|
|
364
|
+
storage.inicializar_db()
|
|
365
|
+
limit = getattr(args, "limit", 10)
|
|
366
|
+
rows = storage.historial_scans(args.target, limit=limit)
|
|
367
|
+
|
|
368
|
+
if not rows:
|
|
369
|
+
console.print(f"[dim]Sin historial para el target:[/dim] {args.target}")
|
|
370
|
+
return 0
|
|
371
|
+
|
|
372
|
+
console.print(_tabla_historial(rows))
|
|
373
|
+
return 0
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
# ---------------------------------------------------------------------------
|
|
377
|
+
# Comando: assets
|
|
378
|
+
# ---------------------------------------------------------------------------
|
|
379
|
+
|
|
380
|
+
def cmd_assets(args: argparse.Namespace) -> int:
|
|
381
|
+
"""Muestra todos los activos conocidos para el target especificado."""
|
|
382
|
+
storage.inicializar_db()
|
|
383
|
+
rows = storage.todos_los_assets(args.target)
|
|
384
|
+
|
|
385
|
+
if not rows:
|
|
386
|
+
console.print(f"[dim]Sin activos registrados para el target:[/dim] {args.target}")
|
|
387
|
+
return 0
|
|
388
|
+
|
|
389
|
+
console.print(_tabla_assets(rows))
|
|
390
|
+
return 0
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
# ---------------------------------------------------------------------------
|
|
394
|
+
# Comando: export
|
|
395
|
+
# ---------------------------------------------------------------------------
|
|
396
|
+
|
|
397
|
+
def cmd_export(args: argparse.Namespace) -> int:
|
|
398
|
+
"""
|
|
399
|
+
Exporta un snapshot del último escaneo en JSON y/o HTML.
|
|
400
|
+
Útil para integración con vamp-penreport.
|
|
401
|
+
"""
|
|
402
|
+
storage.inicializar_db()
|
|
403
|
+
|
|
404
|
+
# Buscar último escaneo
|
|
405
|
+
rows = storage.historial_scans(args.target, limit=1)
|
|
406
|
+
if not rows:
|
|
407
|
+
console.print(f"[red]Error:[/red] No hay escaneos registrados para {args.target}")
|
|
408
|
+
return 1
|
|
409
|
+
|
|
410
|
+
scan_id = rows[0]["id"]
|
|
411
|
+
assets = storage.assets_del_scan(args.target, scan_id)
|
|
412
|
+
certs = storage.certs_del_scan(args.target, scan_id)
|
|
413
|
+
|
|
414
|
+
# Construir findings de info a partir de los activos
|
|
415
|
+
findings: List[Finding] = []
|
|
416
|
+
n = 1
|
|
417
|
+
for a in assets:
|
|
418
|
+
if a["puerto"]:
|
|
419
|
+
findings.append(Finding(
|
|
420
|
+
id=f"EASM-{n:03d}",
|
|
421
|
+
title=f"Activo expuesto — {a['subdominio']}:{a['puerto']}",
|
|
422
|
+
severity="INFO",
|
|
423
|
+
description=f"Puerto {a['puerto']}/{a['protocolo']} abierto en {a['subdominio']}.",
|
|
424
|
+
evidence=f"Host: {a['subdominio']}\nIP: {a['ip'] or '—'}\nPuerto: {a['puerto']}/{a['protocolo']}\nServicio: {a['servicio'] or '—'}",
|
|
425
|
+
affected=f"{a['subdominio']}:{a['puerto']}",
|
|
426
|
+
remediation="Verificar que el servicio está correctamente asegurado y es intencionado.",
|
|
427
|
+
tags=["easm", "asset", "inventory"],
|
|
428
|
+
))
|
|
429
|
+
n += 1
|
|
430
|
+
|
|
431
|
+
meta = meta_from_args(args, tool=TOOL, version=VERSION)
|
|
432
|
+
meta.scope = args.target
|
|
433
|
+
informe = VampSecReport(meta, findings)
|
|
434
|
+
|
|
435
|
+
if not getattr(args, "json", False) and not getattr(args, "html", False):
|
|
436
|
+
# Por defecto exportar ambos
|
|
437
|
+
args.json = True
|
|
438
|
+
args.html = True
|
|
439
|
+
|
|
440
|
+
exportados = []
|
|
441
|
+
if getattr(args, "json", False):
|
|
442
|
+
fname = f"easm_export_{args.target}_{scan_id[:8]}.json"
|
|
443
|
+
informe.to_json(fname)
|
|
444
|
+
exportados.append(fname)
|
|
445
|
+
|
|
446
|
+
if getattr(args, "html", False):
|
|
447
|
+
fname = f"easm_export_{args.target}_{scan_id[:8]}.html"
|
|
448
|
+
informe.to_html_client(fname)
|
|
449
|
+
exportados.append(fname)
|
|
450
|
+
|
|
451
|
+
for f in exportados:
|
|
452
|
+
console.print(f"[green]→ Exportado:[/green] {f}")
|
|
453
|
+
|
|
454
|
+
return 0
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
# ---------------------------------------------------------------------------
|
|
458
|
+
# CLI — definición de argumentos
|
|
459
|
+
# ---------------------------------------------------------------------------
|
|
460
|
+
|
|
461
|
+
def construir_parser() -> argparse.ArgumentParser:
|
|
462
|
+
"""Construye el parser argparse con todos los subcomandos."""
|
|
463
|
+
parser = argparse.ArgumentParser(
|
|
464
|
+
prog="vamp-easm",
|
|
465
|
+
description=(
|
|
466
|
+
"vamp-easm — Motor de Attack Surface Management Continuo\n"
|
|
467
|
+
"VampSecure Labs · VampSecure Studios"
|
|
468
|
+
),
|
|
469
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
470
|
+
epilog=(
|
|
471
|
+
"Ejemplos:\n"
|
|
472
|
+
" python vamp_easm.py scan --target ejemplo.com\n"
|
|
473
|
+
" python vamp_easm.py scan --target ejemplo.com --ports 22,80,443 --html\n"
|
|
474
|
+
" python vamp_easm.py history --target ejemplo.com --limit 5\n"
|
|
475
|
+
" python vamp_easm.py assets --target ejemplo.com\n"
|
|
476
|
+
" python vamp_easm.py export --target ejemplo.com --json\n"
|
|
477
|
+
),
|
|
478
|
+
)
|
|
479
|
+
parser.add_argument(
|
|
480
|
+
"--version", action="version",
|
|
481
|
+
version=f"vamp-easm {VERSION} · VampSecure Labs",
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
subparsers = parser.add_subparsers(dest="comando", help="Subcomando a ejecutar")
|
|
485
|
+
|
|
486
|
+
# ── scan ─────────────────────────────────────────────────────────────
|
|
487
|
+
p_scan = subparsers.add_parser("scan", help="Escanear un target y calcular diffs")
|
|
488
|
+
p_scan.add_argument(
|
|
489
|
+
"--target", required=True, metavar="DOMINIO",
|
|
490
|
+
help="Dominio objetivo (ej: ejemplo.com)",
|
|
491
|
+
)
|
|
492
|
+
p_scan.add_argument(
|
|
493
|
+
"--ports", default="top100", metavar="PORTS",
|
|
494
|
+
help="Puertos a escanear: 'top100' (por defecto) o lista CSV (ej: 22,80,443)",
|
|
495
|
+
)
|
|
496
|
+
p_scan.add_argument(
|
|
497
|
+
"--nmap", action="store_true",
|
|
498
|
+
help="Usar nmap como backend de escaneo de puertos (requiere nmap en PATH)",
|
|
499
|
+
)
|
|
500
|
+
p_scan.add_argument(
|
|
501
|
+
"--json", action="store_true",
|
|
502
|
+
help="Exportar hallazgos en formato JSON (informe VSL estándar)",
|
|
503
|
+
)
|
|
504
|
+
p_scan.add_argument(
|
|
505
|
+
"--html", action="store_true",
|
|
506
|
+
help="Exportar hallazgos en formato HTML profesional (informe VSL)",
|
|
507
|
+
)
|
|
508
|
+
p_scan.add_argument(
|
|
509
|
+
"--alert-webhook", metavar="URL", dest="alert_webhook",
|
|
510
|
+
help="URL del webhook para alertas CRITICAL/HIGH (alternativa: env EASM_ALERT_WEBHOOK)",
|
|
511
|
+
)
|
|
512
|
+
add_report_args(p_scan)
|
|
513
|
+
|
|
514
|
+
# ── history ───────────────────────────────────────────────────────────
|
|
515
|
+
p_hist = subparsers.add_parser("history", help="Ver historial de escaneos de un target")
|
|
516
|
+
p_hist.add_argument("--target", required=True, metavar="DOMINIO", help="Dominio objetivo")
|
|
517
|
+
p_hist.add_argument("--limit", type=int, default=10, metavar="N", help="Número de entradas (por defecto: 10)")
|
|
518
|
+
|
|
519
|
+
# ── assets ────────────────────────────────────────────────────────────
|
|
520
|
+
p_assets = subparsers.add_parser("assets", help="Ver activos conocidos de un target")
|
|
521
|
+
p_assets.add_argument("--target", required=True, metavar="DOMINIO", help="Dominio objetivo")
|
|
522
|
+
|
|
523
|
+
# ── export ────────────────────────────────────────────────────────────
|
|
524
|
+
p_export = subparsers.add_parser("export", help="Exportar snapshot del último escaneo")
|
|
525
|
+
p_export.add_argument("--target", required=True, metavar="DOMINIO", help="Dominio objetivo")
|
|
526
|
+
p_export.add_argument("--json", action="store_true", help="Exportar como JSON")
|
|
527
|
+
p_export.add_argument("--html", action="store_true", help="Exportar como HTML")
|
|
528
|
+
add_report_args(p_export)
|
|
529
|
+
|
|
530
|
+
return parser
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
# ---------------------------------------------------------------------------
|
|
534
|
+
# Punto de entrada
|
|
535
|
+
# ---------------------------------------------------------------------------
|
|
536
|
+
|
|
537
|
+
def main() -> None:
|
|
538
|
+
"""Punto de entrada principal. Parsea la CLI y despacha al subcomando."""
|
|
539
|
+
parser = construir_parser()
|
|
540
|
+
args = parser.parse_args()
|
|
541
|
+
|
|
542
|
+
if not args.comando:
|
|
543
|
+
parser.print_help()
|
|
544
|
+
sys.exit(0)
|
|
545
|
+
|
|
546
|
+
_dispatch = {
|
|
547
|
+
"scan": cmd_scan,
|
|
548
|
+
"history": cmd_history,
|
|
549
|
+
"assets": cmd_assets,
|
|
550
|
+
"export": cmd_export,
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
handler = _dispatch.get(args.comando)
|
|
554
|
+
if handler is None:
|
|
555
|
+
parser.print_help()
|
|
556
|
+
sys.exit(1)
|
|
557
|
+
|
|
558
|
+
exit_code = handler(args)
|
|
559
|
+
sys.exit(exit_code)
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
if __name__ == "__main__":
|
|
563
|
+
main()
|