---
name: detect_web_tech
kind: function
lang: py
domain: cybersecurity
version: "1.0.0"
purity: pure
signature: "def detect_web_tech(headers: dict, html: str = '', cookies: list[str] | None = None, final_url: str = '') -> dict"
description: "Detector de tecnologia web estilo Wappalyzer: identifica el stack tecnologico de un sitio (web fingerprint) matcheando una tabla de firmas regex embebida contra las cabeceras HTTP, el HTML, los nombres de cookies y la URL final. Detecta servidor (nginx, Apache, IIS, LiteSpeed, Caddy), lenguaje (PHP, ASP.NET, Java, Python, Ruby, Node.js), CMS (WordPress, Drupal, Joomla, Shopify, Wix, Squarespace, Ghost), frameworks JS (React, Vue, Angular, Svelte, Next.js, Nuxt), librerias (jQuery, Bootstrap, Lodash, Modernizr), analytics/tag (Google Analytics, GTM, Facebook Pixel, Hotjar, Matomo), CDN (Cloudflare, Fastly, Akamai, CloudFront, jsDelivr, unpkg), ecommerce (WooCommerce, Magento, PrestaShop, Shopify) y WAF/seguridad (Cloudflare, Sucuri, Imperva Incapsula). Pieza pura del detector: no toca la red, recibe las senales ya recogidas por fetch_http_fingerprint."
tags: [recon, cybersecurity, web-recon, wappalyzer, fingerprint, tech-detection, cms, stack]
params:
  - name: headers
    desc: "dict de cabeceras de respuesta HTTP con claves en minusculas (tal como las devuelve fetch_http_fingerprint en su campo headers). Valores string. Si las claves vienen en mayusculas se normalizan internamente."
  - name: html
    desc: "HTML de la pagina como string. Default '' para detectar solo por cabeceras y cookies. De aqui se extraen meta generator y src de los <script>."
  - name: cookies
    desc: "lista de NOMBRES de cookies (no valores). Default None -> []. Ej: ['PHPSESSID', 'wordpress_logged_in']."
  - name: final_url
    desc: "URL final tras redirects, para firmas basadas en host/path. Opcional, default ''."
output: "dict con technologies (lista de {name, category, version, confidence, evidence} ordenada deterministicamente por categoria y nombre), by_category (dict categoria -> lista de nombres) y count (entero). confidence es 'high' para match directo de header/meta/cookie/url y 'medium' para HTML generico, script src o tecnologia implicada. version es best-effort (a menudo ''). Para entrada vacia devuelve technologies [], by_category {}, count 0. Nunca lanza."
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
tested: true
tests: ["test_nginx_por_header_con_version", "test_wordpress_por_html_y_meta_implica_php", "test_php_por_cookie", "test_cloudflare_por_header", "test_entrada_vacia", "test_entrada_vacia_explicita_headers_y_html", "test_determinismo", "test_count_y_by_category_consistentes", "test_headers_claves_mayusculas_se_normalizan", "test_jquery_por_script_src_es_medium"]
test_file_path: "python/functions/cybersecurity/detect_web_tech_test.py"
file_path: "python/functions/cybersecurity/detect_web_tech.py"
---

## Ejemplo

```python
from cybersecurity import detect_web_tech

# Senales fake de un sitio WordPress sobre nginx.
headers = {
    "server": "nginx/1.24.0",
    "x-powered-by": "PHP/8.2",
}
html = (
    '<html><head>'
    '<meta name="generator" content="WordPress 6.4">'
    '</head><body><link href="/wp-content/themes/x/style.css"></body></html>'
)
cookies = ["PHPSESSID", "wordpress_logged_in_abc"]

result = detect_web_tech(headers, html=html, cookies=cookies)
# result["count"] == 3
# result["by_category"] == {
#     "cms": ["WordPress"],
#     "programming-language": ["PHP"],
#     "web-server": ["nginx"],
# }
# nginx -> version "1.24.0", confidence "high", evidence "header server: nginx/1.24.0"
# WordPress -> version "6.4", confidence "high", evidence "meta generator: WordPress 6.4"
# PHP -> version "8.2", confidence "high", evidence "header x-powered-by: PHP/8.2"
```

Flujo real componiendo con la capa impura hermana (recoleccion -> matching):

```python
from cybersecurity import fetch_http_fingerprint, detect_web_tech

# Capa impura: recoge las senales con un GET real (red).
fp = fetch_http_fingerprint("https://example.com")
# fp = {"headers": {...lowercase...}, "html": "...", "cookies": [...], "final_url": "..."}

# Capa pura: identifica el stack sobre las senales recogidas (sin red).
tech = detect_web_tech(
    fp["headers"],
    html=fp.get("html", ""),
    cookies=fp.get("cookies"),
    final_url=fp.get("final_url", ""),
)
for t in tech["technologies"]:
    print(t["category"], t["name"], t["version"], t["confidence"])
```

## Cuando usarla

Cuando ya tienes los headers + html (+ cookies/URL) de una URL — recogidos por
`fetch_http_fingerprint_py_cybersecurity` — y quieres saber el stack tecnologico
del sitio: servidor, lenguaje, CMS, frameworks JS, librerias, analytics, CDN,
ecommerce y WAF. Usala como pieza de matching pura y testeable. Para el flujo
one-shot `url -> tecnologias` (recoger + detectar en una llamada) usa el pipeline
`fingerprint_web_stack`.

## Gotchas

- La tabla `SIGNATURES` es un **subconjunto curado** de lo que cubre Wappalyzer
  (~50 tecnologias), no es exhaustiva. Para ampliarla, anade entradas nuevas a la
  constante `SIGNATURES` del modulo siguiendo el formato documentado en el codigo
  (matchers `headers`/`html`/`meta_generator`/`cookies`/`script_src`/`url`,
  opcionales `version_group` e `implies`).
- La deteccion por HTML generico puede dar **falsos positivos**: un sitio que
  mencione "wordpress" o "woocommerce" en su texto/blog puede matchear sin usarlo
  realmente. Por eso esos matches tienen `confidence: "medium"` mientras que
  header/meta/cookie directos son `"high"`.
- Las **SPAs cargan los frameworks por JS en runtime**. Un fetch estatico (sin
  ejecutar JavaScript) ve el HTML inicial, que en muchas SPAs esta casi vacio
  (`<div id="root"></div>`). React/Vue/Angular pueden NO detectarse si el HTML
  servido no contiene aun sus marcadores. Para esos casos hace falta renderizar
  con un navegador headless, fuera del alcance de esta funcion pura.
- Las **versiones son best-effort**: solo se extraen cuando el regex que disparo
  tiene un group de version y este matcheo. A menudo quedan en `""`.
- Es PURA y determinista: misma entrada -> misma salida. Para entrada vacia
  (`headers={}, html=""`) devuelve `technologies: [], count: 0` y NUNCA lanza ni
  reporta status/error.
