feat(recon): modo CDP en fingerprint_web_stack para detectar SPAs

Añade fetch_http_fingerprint_cdp_py_browser (domain browser): recoge el HTML
renderizado tras ejecutar JavaScript usando un Chrome remoto via CDP, componiendo
cdp_open_url_and_wait + cdp_eval. Devuelve la misma estructura que el fetch
estático para que detect_web_tech lo consuma sin cambios.

Integra use_cdp en el pipeline fingerprint_web_stack (v1.1.0): combina los headers
reales del fetch estático con el HTML post-JS del CDP. Detecta frameworks de SPA
(React/Vue/Angular/Next) que el fetch estático no ve porque montan el DOM en
runtime. Si no hay Chrome en cdp_port, degrada al fetch estático con un warning
(no rompe). cdp_port=9333 (Chrome aislado) recomendado para terceros, 9222 diario.

Verificado en vivo (Chrome 9333): sobre una SPA cuyo marcador de framework solo
aparece tras ejecutar JS, el estático detecta solo nginx; con use_cdp=True detecta
además Next.js, React y Node.js.

Tests: 48 verdes (error path sin Chrome + happy path mockeado + degradación).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-14 15:31:28 +02:00
parent 935008ec3f
commit 1430039688
7 changed files with 649 additions and 40 deletions
@@ -178,3 +178,44 @@ def test_fetch_fallido_propaga_error_sin_red():
assert result["fetch"]["status"] == "error", result
# No se intento archivar nada.
assert save_called["n"] == 0, save_called
# --- 4. use_cdp sin Chrome: DEGRADA a estatico con warning (no falla) ---------
def test_use_cdp_sin_chrome_degrada_a_estatico():
"""use_cdp=True sin Chrome (cdp_port=1) degrada al fetch estatico con warning.
Levanta el mismo HTTPServer WordPress/nginx/PHP local que el golden y pide
use_cdp con cdp_port=1 (donde no hay ningun Chrome escuchando). El fetch CDP
falla, el pipeline NO rompe: usa el HTML estatico, marca html_source=static,
rellena warnings y sigue detectando WordPress/nginx por el html/headers.
"""
httpd, port, thread = _start_wp_server()
try:
result = fingerprint_web_stack(
f"http://127.0.0.1:{port}/",
timeout_s=5.0,
save=False,
use_cdp=True,
cdp_port=1, # puerto sin Chrome: el fetch CDP falla -> degrada
wait_render_s=0.0,
)
finally:
httpd.shutdown()
httpd.server_close()
thread.join(timeout=2.0)
# Degrado, no fallo.
assert result["status"] == "ok", result
# Cayo al HTML estatico (CDP no disponible).
assert result["html_source"] == "static", result
assert result["rendered"] is False, result
# Hubo warning de degradacion.
assert result["warnings"], result
assert any("cdp no disponible" in w for w in result["warnings"]), result["warnings"]
# La deteccion estatica sigue funcionando.
names = {t["name"] for t in result["technologies"]}
assert "WordPress" in names, names
assert "nginx" in names, names
# No se archivo (save=False).
assert result["saved"] is None, result