feat(recon): grupo de reconocimiento de red + servicios + fingerprint web

Añade el capability group `recon` (dominio cybersecurity + pipelines, Python),
con la política de archivado OSINT y página madre docs/capabilities/recon.md.

Lookups y sondeo (wrappers de CLI):
- whois_lookup, rdap_lookup, dns_records, ping_host, traceroute_host, nmap_scan
- save_scan_to_osint (sink común) + recon_osint (pipeline one-shot scan+archivado)

Escaneo de puertos/servicios nativo (stdlib, sin nmap ni sudo):
- scan_tcp_ports: connect-scan TCP concurrente (open/closed/filtered)
- grab_service_banner: banner grab + identificación de servicio/versión real
- identify_port_service: puro, puerto -> servicio IANA esperado (~120 puertos)
- scan_port_services: pipeline one-shot (scan -> identify + banner por puerto abierto)

Fingerprint de tecnología web (estilo Wappalyzer), patrón pura/impura:
- fetch_http_fingerprint: GET stdlib, recoge headers/html/cookies (solo nombres)
- detect_web_tech: puro, matchea ~50 firmas regex -> tecnologías por categoría
- fingerprint_web_stack: pipeline one-shot url -> tecnologías

Todas devuelven dict {status} sin lanzar. Tests: 43 verdes, sin red externa.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-14 15:12:07 +02:00
parent d89da1292d
commit 935008ec3f
49 changed files with 6659 additions and 302 deletions
@@ -0,0 +1,111 @@
---
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.