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
easm/scanner.py
ADDED
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
# © VampSecure Studios — VampSecure Labs Security Research Division
|
|
2
|
+
"""
|
|
3
|
+
scanner.py — Motor de escaneo en tres capas para vamp-easm
|
|
4
|
+
===========================================================
|
|
5
|
+
Realiza el descubrimiento de la superficie de ataque externa en tres fases:
|
|
6
|
+
|
|
7
|
+
Capa 1 · DNS/Subdominios
|
|
8
|
+
Consulta crt.sh (Certificate Transparency) y HackerTarget (hostsearch)
|
|
9
|
+
para enumerar subdominios conocidos del target. Solo usa urllib (stdlib).
|
|
10
|
+
|
|
11
|
+
Capa 2 · Puertos/Servicios
|
|
12
|
+
Escaneo TCP async sobre el top-100 de puertos (o lista personalizada).
|
|
13
|
+
Si nmap está en PATH y se activa --nmap, se usa como backend.
|
|
14
|
+
|
|
15
|
+
Capa 3 · Certificados TLS
|
|
16
|
+
Para cada subdominio con el puerto 443 abierto (o cualquier puerto HTTPS
|
|
17
|
+
detectado), inspecciona el certificado TLS: hostname, emisor, expiración,
|
|
18
|
+
SANs y si es autofirmado.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import asyncio
|
|
24
|
+
import ipaddress
|
|
25
|
+
import json
|
|
26
|
+
import shutil
|
|
27
|
+
import socket
|
|
28
|
+
import ssl
|
|
29
|
+
import subprocess
|
|
30
|
+
import urllib.request
|
|
31
|
+
import urllib.error
|
|
32
|
+
from dataclasses import dataclass, field
|
|
33
|
+
from datetime import datetime, timezone
|
|
34
|
+
from typing import Dict, List, Optional, Set, Tuple
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
# Top-100 puertos a escanear (fusión de servicios comunes + admin + web)
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
TOP_100_PORTS: List[int] = [
|
|
42
|
+
21, 22, 23, 25, 53, 80, 88, 110, 111, 119,
|
|
43
|
+
135, 139, 143, 161, 179, 389, 443, 445, 465, 500,
|
|
44
|
+
514, 515, 587, 631, 636, 873, 993, 995, 1080, 1194,
|
|
45
|
+
1433, 1521, 1723, 1883, 2049, 2181, 2375, 2376, 3000,
|
|
46
|
+
3001, 3306, 3389, 3478, 3632, 4443, 4444, 4848, 5000,
|
|
47
|
+
5001, 5432, 5601, 5672, 5900, 5985, 5986, 6379, 6443,
|
|
48
|
+
7001, 7002, 7070, 7443, 7474, 7777, 8000, 8008, 8080,
|
|
49
|
+
8081, 8082, 8088, 8089, 8161, 8443, 8500, 8530, 8888,
|
|
50
|
+
8983, 9000, 9001, 9042, 9090, 9091, 9092, 9200, 9300,
|
|
51
|
+
9418, 9443, 9999, 10000, 11211, 15672, 16379, 25565,
|
|
52
|
+
27017, 27018, 28017, 50000, 50070, 61616,
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
# Puertos que típicamente llevan TLS
|
|
56
|
+
_PUERTOS_TLS: Set[int] = {
|
|
57
|
+
443, 465, 636, 993, 995, 4443, 5601, 5986,
|
|
58
|
+
6443, 7443, 8443, 9200, 9300, 9443, 10000,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# Timeout de conexión TCP en segundos
|
|
62
|
+
_TCP_TIMEOUT = 2.0
|
|
63
|
+
# Timeout para llamadas HTTP a fuentes externas
|
|
64
|
+
_HTTP_TIMEOUT = 15
|
|
65
|
+
|
|
66
|
+
# Semáforo para limitar las conexiones simultáneas en el escaneo de puertos
|
|
67
|
+
_MAX_CONCURRENT = 200
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
# Estructuras de datos de resultado
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class Subdominio:
|
|
76
|
+
"""Subdominio descubierto con su resolución IP."""
|
|
77
|
+
nombre: str
|
|
78
|
+
ips: List[str] = field(default_factory=list)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class PuertoAbierto:
|
|
83
|
+
"""Puerto TCP abierto con información de servicio."""
|
|
84
|
+
host: str
|
|
85
|
+
puerto: int
|
|
86
|
+
protocolo: str = "tcp"
|
|
87
|
+
servicio: Optional[str] = None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass
|
|
91
|
+
class CertInfo:
|
|
92
|
+
"""Información de un certificado TLS inspeccionado."""
|
|
93
|
+
host: str
|
|
94
|
+
puerto: int
|
|
95
|
+
issued_to: Optional[str]
|
|
96
|
+
issuer: Optional[str]
|
|
97
|
+
not_after: Optional[str] # ISO-8601 UTC
|
|
98
|
+
sans: List[str] = field(default_factory=list)
|
|
99
|
+
fingerprint: Optional[str] = None
|
|
100
|
+
autofirmado: bool = False
|
|
101
|
+
error: Optional[str] = None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass
|
|
105
|
+
class ResultadoEscaneo:
|
|
106
|
+
"""Resultado completo de un escaneo para un target."""
|
|
107
|
+
target: str
|
|
108
|
+
subdominios: List[Subdominio] = field(default_factory=list)
|
|
109
|
+
puertos: List[PuertoAbierto] = field(default_factory=list)
|
|
110
|
+
certs: List[CertInfo] = field(default_factory=list)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ---------------------------------------------------------------------------
|
|
114
|
+
# Capa 1 — Enumeración de subdominios
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
def _http_get(url: str, timeout: int = _HTTP_TIMEOUT) -> str:
|
|
118
|
+
"""Realiza una petición HTTP GET con stdlib y devuelve el body como texto."""
|
|
119
|
+
req = urllib.request.Request(url, headers={"User-Agent": "vamp-easm/1.0"})
|
|
120
|
+
try:
|
|
121
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
122
|
+
return resp.read().decode("utf-8", errors="replace")
|
|
123
|
+
except Exception:
|
|
124
|
+
return ""
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _subdominios_crtsh(dominio: str) -> Set[str]:
|
|
128
|
+
"""
|
|
129
|
+
Consulta Certificate Transparency (crt.sh) para obtener subdominios
|
|
130
|
+
históricos del dominio. Devuelve un conjunto de nombres normalizados.
|
|
131
|
+
"""
|
|
132
|
+
url = f"https://crt.sh/?q=%.{dominio}&output=json"
|
|
133
|
+
raw = _http_get(url)
|
|
134
|
+
if not raw:
|
|
135
|
+
return set()
|
|
136
|
+
try:
|
|
137
|
+
datos = json.loads(raw)
|
|
138
|
+
except json.JSONDecodeError:
|
|
139
|
+
return set()
|
|
140
|
+
|
|
141
|
+
resultado: Set[str] = set()
|
|
142
|
+
for entrada in datos:
|
|
143
|
+
# name_value puede contener múltiples nombres separados por \n
|
|
144
|
+
for nombre in str(entrada.get("name_value", "")).splitlines():
|
|
145
|
+
nombre = nombre.strip().lstrip("*.")
|
|
146
|
+
if nombre and dominio in nombre:
|
|
147
|
+
resultado.add(nombre.lower())
|
|
148
|
+
return resultado
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _subdominios_hackertarget(dominio: str) -> Set[str]:
|
|
152
|
+
"""
|
|
153
|
+
Consulta HackerTarget hostsearch para obtener subdominios del dominio.
|
|
154
|
+
El endpoint devuelve líneas en formato 'host,ip'.
|
|
155
|
+
"""
|
|
156
|
+
url = f"https://api.hackertarget.com/hostsearch/?q={dominio}"
|
|
157
|
+
raw = _http_get(url)
|
|
158
|
+
resultado: Set[str] = set()
|
|
159
|
+
for linea in raw.splitlines():
|
|
160
|
+
partes = linea.strip().split(",")
|
|
161
|
+
if partes:
|
|
162
|
+
nombre = partes[0].strip().lower()
|
|
163
|
+
if nombre and dominio in nombre and not nombre.startswith("API"):
|
|
164
|
+
resultado.add(nombre)
|
|
165
|
+
return resultado
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _resolver_ip(nombre: str) -> List[str]:
|
|
169
|
+
"""Resuelve un nombre de host a sus IPs (IPv4 e IPv6). Devuelve lista vacía si falla."""
|
|
170
|
+
try:
|
|
171
|
+
info = socket.getaddrinfo(nombre, None)
|
|
172
|
+
return list({entry[4][0] for entry in info})
|
|
173
|
+
except Exception:
|
|
174
|
+
return []
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def enumerar_subdominios(target: str) -> List[Subdominio]:
|
|
178
|
+
"""
|
|
179
|
+
Enumera subdominios del target consultando crt.sh y HackerTarget.
|
|
180
|
+
Siempre incluye el propio target como subdominio raíz.
|
|
181
|
+
|
|
182
|
+
Parameters
|
|
183
|
+
----------
|
|
184
|
+
target : Dominio raíz a analizar (p.ej. 'ejemplo.com')
|
|
185
|
+
|
|
186
|
+
Returns
|
|
187
|
+
-------
|
|
188
|
+
list[Subdominio] : Subdominios con sus IPs resueltas
|
|
189
|
+
"""
|
|
190
|
+
nombres: Set[str] = set()
|
|
191
|
+
nombres.add(target) # el dominio raíz siempre
|
|
192
|
+
nombres.update(_subdominios_crtsh(target))
|
|
193
|
+
nombres.update(_subdominios_hackertarget(target))
|
|
194
|
+
|
|
195
|
+
resultado: List[Subdominio] = []
|
|
196
|
+
for nombre in sorted(nombres):
|
|
197
|
+
ips = _resolver_ip(nombre)
|
|
198
|
+
resultado.append(Subdominio(nombre=nombre, ips=ips))
|
|
199
|
+
return resultado
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# ---------------------------------------------------------------------------
|
|
203
|
+
# Capa 2 — Escaneo de puertos (asyncio TCP connect)
|
|
204
|
+
# ---------------------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
async def _tcp_connect(
|
|
207
|
+
sem: asyncio.Semaphore,
|
|
208
|
+
host: str,
|
|
209
|
+
puerto: int,
|
|
210
|
+
timeout: float = _TCP_TIMEOUT,
|
|
211
|
+
) -> Optional[PuertoAbierto]:
|
|
212
|
+
"""
|
|
213
|
+
Intenta una conexión TCP al host:puerto bajo el semáforo dado.
|
|
214
|
+
Devuelve PuertoAbierto si el puerto está abierto, None si está cerrado/filtrado.
|
|
215
|
+
"""
|
|
216
|
+
async with sem:
|
|
217
|
+
try:
|
|
218
|
+
conn = asyncio.open_connection(host, puerto)
|
|
219
|
+
reader, writer = await asyncio.wait_for(conn, timeout=timeout)
|
|
220
|
+
writer.close()
|
|
221
|
+
try:
|
|
222
|
+
await writer.wait_closed()
|
|
223
|
+
except Exception:
|
|
224
|
+
pass
|
|
225
|
+
servicio = _nombre_servicio(puerto)
|
|
226
|
+
return PuertoAbierto(host=host, puerto=puerto, servicio=servicio)
|
|
227
|
+
except (asyncio.TimeoutError, ConnectionRefusedError, OSError):
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _nombre_servicio(puerto: int) -> Optional[str]:
|
|
232
|
+
"""Intenta obtener el nombre de servicio estándar para un puerto."""
|
|
233
|
+
try:
|
|
234
|
+
return socket.getservbyport(puerto, "tcp")
|
|
235
|
+
except OSError:
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
async def _escanear_host_async(
|
|
240
|
+
host: str,
|
|
241
|
+
puertos: List[int],
|
|
242
|
+
sem: asyncio.Semaphore,
|
|
243
|
+
) -> List[PuertoAbierto]:
|
|
244
|
+
"""Escanea todos los puertos de un host de forma concurrente."""
|
|
245
|
+
tareas = [_tcp_connect(sem, host, p) for p in puertos]
|
|
246
|
+
resultados = await asyncio.gather(*tareas)
|
|
247
|
+
return [r for r in resultados if r is not None]
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def escanear_puertos_stdlib(
|
|
251
|
+
subdominios: List[Subdominio],
|
|
252
|
+
puertos: List[int],
|
|
253
|
+
) -> List[PuertoAbierto]:
|
|
254
|
+
"""
|
|
255
|
+
Escanea los puertos especificados en todos los subdominios usando asyncio TCP.
|
|
256
|
+
|
|
257
|
+
Parameters
|
|
258
|
+
----------
|
|
259
|
+
subdominios : Lista de subdominios a escanear
|
|
260
|
+
puertos : Lista de puertos a probar
|
|
261
|
+
|
|
262
|
+
Returns
|
|
263
|
+
-------
|
|
264
|
+
list[PuertoAbierto] : Puertos abiertos encontrados
|
|
265
|
+
"""
|
|
266
|
+
sem = asyncio.Semaphore(_MAX_CONCURRENT)
|
|
267
|
+
|
|
268
|
+
async def _main() -> List[PuertoAbierto]:
|
|
269
|
+
tareas = []
|
|
270
|
+
for sub in subdominios:
|
|
271
|
+
if sub.ips: # solo si resuelve
|
|
272
|
+
tareas.append(_escanear_host_async(sub.nombre, puertos, sem))
|
|
273
|
+
if not tareas:
|
|
274
|
+
return []
|
|
275
|
+
listas = await asyncio.gather(*tareas)
|
|
276
|
+
return [p for lista in listas for p in lista]
|
|
277
|
+
|
|
278
|
+
return asyncio.run(_main())
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def escanear_puertos_nmap(
|
|
282
|
+
subdominios: List[Subdominio],
|
|
283
|
+
puertos: List[int],
|
|
284
|
+
) -> List[PuertoAbierto]:
|
|
285
|
+
"""
|
|
286
|
+
Escanea puertos usando nmap como backend (requiere nmap en PATH).
|
|
287
|
+
Usa salida XML de nmap para parsear los puertos abiertos.
|
|
288
|
+
|
|
289
|
+
Parameters
|
|
290
|
+
----------
|
|
291
|
+
subdominios : Lista de subdominios a escanear
|
|
292
|
+
puertos : Lista de puertos a probar
|
|
293
|
+
|
|
294
|
+
Returns
|
|
295
|
+
-------
|
|
296
|
+
list[PuertoAbierto] : Puertos abiertos
|
|
297
|
+
"""
|
|
298
|
+
hosts_con_ip = [s.nombre for s in subdominios if s.ips]
|
|
299
|
+
if not hosts_con_ip:
|
|
300
|
+
return []
|
|
301
|
+
|
|
302
|
+
puertos_str = ",".join(str(p) for p in sorted(puertos))
|
|
303
|
+
cmd = [
|
|
304
|
+
"nmap", "-sT", "-Pn", "-T4",
|
|
305
|
+
f"-p{puertos_str}",
|
|
306
|
+
"--open",
|
|
307
|
+
"-oX", "-", # salida XML por stdout
|
|
308
|
+
] + hosts_con_ip
|
|
309
|
+
|
|
310
|
+
try:
|
|
311
|
+
resultado = subprocess.run(
|
|
312
|
+
cmd,
|
|
313
|
+
capture_output=True,
|
|
314
|
+
text=True,
|
|
315
|
+
timeout=300,
|
|
316
|
+
)
|
|
317
|
+
return _parsear_nmap_xml(resultado.stdout)
|
|
318
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, Exception):
|
|
319
|
+
return []
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _parsear_nmap_xml(xml: str) -> List[PuertoAbierto]:
|
|
323
|
+
"""Parsea la salida XML de nmap para extraer puertos abiertos."""
|
|
324
|
+
import xml.etree.ElementTree as ET
|
|
325
|
+
abiertos: List[PuertoAbierto] = []
|
|
326
|
+
if not xml.strip():
|
|
327
|
+
return abiertos
|
|
328
|
+
try:
|
|
329
|
+
root = ET.fromstring(xml)
|
|
330
|
+
except ET.ParseError:
|
|
331
|
+
return abiertos
|
|
332
|
+
|
|
333
|
+
for host in root.findall(".//host"):
|
|
334
|
+
hostname_el = host.find(".//hostname[@type='user']")
|
|
335
|
+
addr_el = host.find(".//address[@addrtype='ipv4']")
|
|
336
|
+
if hostname_el is not None:
|
|
337
|
+
host_str = hostname_el.get("name", "")
|
|
338
|
+
elif addr_el is not None:
|
|
339
|
+
host_str = addr_el.get("addr", "")
|
|
340
|
+
else:
|
|
341
|
+
continue
|
|
342
|
+
|
|
343
|
+
for port in host.findall(".//port"):
|
|
344
|
+
state = port.find("state")
|
|
345
|
+
if state is None or state.get("state") != "open":
|
|
346
|
+
continue
|
|
347
|
+
puerto_num = int(port.get("portid", 0))
|
|
348
|
+
servicio_el = port.find("service")
|
|
349
|
+
servicio = servicio_el.get("name") if servicio_el is not None else None
|
|
350
|
+
abiertos.append(PuertoAbierto(
|
|
351
|
+
host=host_str,
|
|
352
|
+
puerto=puerto_num,
|
|
353
|
+
servicio=servicio,
|
|
354
|
+
))
|
|
355
|
+
return abiertos
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
# ---------------------------------------------------------------------------
|
|
359
|
+
# Capa 3 — Inspección de certificados TLS
|
|
360
|
+
# ---------------------------------------------------------------------------
|
|
361
|
+
|
|
362
|
+
def inspeccionar_cert(host: str, puerto: int = 443, timeout: float = 5.0) -> CertInfo:
|
|
363
|
+
"""
|
|
364
|
+
Abre una conexión TLS al host:puerto y extrae la información del certificado.
|
|
365
|
+
|
|
366
|
+
Comprueba si el certificado es autofirmado (mismo CN en issuer y subject),
|
|
367
|
+
calcula la huella SHA-256 y extrae los SANs.
|
|
368
|
+
|
|
369
|
+
Parameters
|
|
370
|
+
----------
|
|
371
|
+
host : Hostname al que conectarse (se usa también para SNI)
|
|
372
|
+
puerto : Puerto TLS (por defecto 443)
|
|
373
|
+
timeout : Segundos de timeout de conexión
|
|
374
|
+
|
|
375
|
+
Returns
|
|
376
|
+
-------
|
|
377
|
+
CertInfo : Estructura con todos los metadatos del certificado
|
|
378
|
+
"""
|
|
379
|
+
ctx = ssl.create_default_context()
|
|
380
|
+
ctx.check_hostname = False
|
|
381
|
+
ctx.verify_mode = ssl.CERT_NONE # capturamos aunque sea inválido
|
|
382
|
+
|
|
383
|
+
try:
|
|
384
|
+
with socket.create_connection((host, puerto), timeout=timeout) as sock:
|
|
385
|
+
with ctx.wrap_socket(sock, server_hostname=host) as tls:
|
|
386
|
+
cert_der = tls.getpeercert(binary_form=True)
|
|
387
|
+
cert = tls.getpeercert()
|
|
388
|
+
except Exception as exc:
|
|
389
|
+
return CertInfo(host=host, puerto=puerto,
|
|
390
|
+
issued_to=None, issuer=None, not_after=None,
|
|
391
|
+
error=str(exc))
|
|
392
|
+
|
|
393
|
+
# Huella SHA-256
|
|
394
|
+
import hashlib
|
|
395
|
+
fingerprint = hashlib.sha256(cert_der).hexdigest() if cert_der else None
|
|
396
|
+
|
|
397
|
+
# Emisor y subject
|
|
398
|
+
subject = dict(x[0] for x in cert.get("subject", []))
|
|
399
|
+
issuer = dict(x[0] for x in cert.get("issuer", []))
|
|
400
|
+
issued_to = subject.get("commonName")
|
|
401
|
+
issuer_cn = issuer.get("commonName") or issuer.get("organizationName")
|
|
402
|
+
|
|
403
|
+
# Fecha de expiración → ISO-8601 UTC
|
|
404
|
+
not_after_raw = cert.get("notAfter", "")
|
|
405
|
+
not_after_iso: Optional[str] = None
|
|
406
|
+
if not_after_raw:
|
|
407
|
+
try:
|
|
408
|
+
dt = datetime.strptime(not_after_raw, "%b %d %H:%M:%S %Y %Z")
|
|
409
|
+
not_after_iso = dt.replace(tzinfo=timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
410
|
+
except ValueError:
|
|
411
|
+
not_after_iso = not_after_raw
|
|
412
|
+
|
|
413
|
+
# SANs (Subject Alternative Names)
|
|
414
|
+
sans: List[str] = []
|
|
415
|
+
for tipo, valor in cert.get("subjectAltName", []):
|
|
416
|
+
if tipo == "DNS":
|
|
417
|
+
sans.append(valor)
|
|
418
|
+
|
|
419
|
+
# Autofirmado: el CN del emisor coincide con el subject
|
|
420
|
+
autofirmado = bool(
|
|
421
|
+
issued_to and issuer_cn and issued_to == issuer_cn
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
return CertInfo(
|
|
425
|
+
host=host,
|
|
426
|
+
puerto=puerto,
|
|
427
|
+
issued_to=issued_to,
|
|
428
|
+
issuer=issuer_cn,
|
|
429
|
+
not_after=not_after_iso,
|
|
430
|
+
sans=sans,
|
|
431
|
+
fingerprint=fingerprint,
|
|
432
|
+
autofirmado=autofirmado,
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def inspeccionar_certs_batch(
|
|
437
|
+
subdominios: List[Subdominio],
|
|
438
|
+
puertos_abiertos: List[PuertoAbierto],
|
|
439
|
+
) -> List[CertInfo]:
|
|
440
|
+
"""
|
|
441
|
+
Inspecciona certificados TLS para todos los subdominios con puertos TLS abiertos.
|
|
442
|
+
Siempre intenta el puerto 443 si el subdominio resuelve, más cualquier
|
|
443
|
+
puerto de la lista _PUERTOS_TLS que esté abierto.
|
|
444
|
+
|
|
445
|
+
Parameters
|
|
446
|
+
----------
|
|
447
|
+
subdominios : Subdominios descubiertos en la capa 1
|
|
448
|
+
puertos_abiertos : Puertos abiertos detectados en la capa 2
|
|
449
|
+
|
|
450
|
+
Returns
|
|
451
|
+
-------
|
|
452
|
+
list[CertInfo] : Certificados inspeccionados
|
|
453
|
+
"""
|
|
454
|
+
# Construir conjunto de (host, puerto) a inspeccionar
|
|
455
|
+
candidatos: Set[Tuple[str, int]] = set()
|
|
456
|
+
|
|
457
|
+
# Intentar 443 para todos los subdominios que resuelven
|
|
458
|
+
for sub in subdominios:
|
|
459
|
+
if sub.ips:
|
|
460
|
+
candidatos.add((sub.nombre, 443))
|
|
461
|
+
|
|
462
|
+
# Añadir puertos TLS abiertos detectados
|
|
463
|
+
for pa in puertos_abiertos:
|
|
464
|
+
if pa.puerto in _PUERTOS_TLS:
|
|
465
|
+
candidatos.add((pa.host, pa.puerto))
|
|
466
|
+
|
|
467
|
+
certs: List[CertInfo] = []
|
|
468
|
+
for host, puerto in sorted(candidatos):
|
|
469
|
+
cert = inspeccionar_cert(host, puerto)
|
|
470
|
+
certs.append(cert)
|
|
471
|
+
return certs
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
# ---------------------------------------------------------------------------
|
|
475
|
+
# Función principal de escaneo
|
|
476
|
+
# ---------------------------------------------------------------------------
|
|
477
|
+
|
|
478
|
+
def ejecutar_escaneo(
|
|
479
|
+
target: str,
|
|
480
|
+
puertos: Optional[List[int]] = None,
|
|
481
|
+
usar_nmap: bool = False,
|
|
482
|
+
) -> ResultadoEscaneo:
|
|
483
|
+
"""
|
|
484
|
+
Ejecuta el escaneo completo en tres capas para el target dado.
|
|
485
|
+
|
|
486
|
+
Parameters
|
|
487
|
+
----------
|
|
488
|
+
target : Dominio raíz a escanear
|
|
489
|
+
puertos : Lista de puertos a probar (None → TOP_100_PORTS)
|
|
490
|
+
usar_nmap : Si True y nmap está en PATH, usarlo como backend de puertos
|
|
491
|
+
|
|
492
|
+
Returns
|
|
493
|
+
-------
|
|
494
|
+
ResultadoEscaneo : Estructura con subdominios, puertos abiertos y certs
|
|
495
|
+
"""
|
|
496
|
+
if puertos is None:
|
|
497
|
+
puertos = TOP_100_PORTS
|
|
498
|
+
|
|
499
|
+
# Capa 1 — subdominios
|
|
500
|
+
subdominios = enumerar_subdominios(target)
|
|
501
|
+
|
|
502
|
+
# Capa 2 — puertos
|
|
503
|
+
nmap_disponible = usar_nmap and shutil.which("nmap") is not None
|
|
504
|
+
if nmap_disponible:
|
|
505
|
+
puertos_abiertos = escanear_puertos_nmap(subdominios, puertos)
|
|
506
|
+
else:
|
|
507
|
+
puertos_abiertos = escanear_puertos_stdlib(subdominios, puertos)
|
|
508
|
+
|
|
509
|
+
# Capa 3 — certificados TLS
|
|
510
|
+
certs = inspeccionar_certs_batch(subdominios, puertos_abiertos)
|
|
511
|
+
|
|
512
|
+
return ResultadoEscaneo(
|
|
513
|
+
target=target,
|
|
514
|
+
subdominios=subdominios,
|
|
515
|
+
puertos=puertos_abiertos,
|
|
516
|
+
certs=certs,
|
|
517
|
+
)
|