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,99 @@
---
name: scan_tcp_ports
kind: function
lang: py
domain: cybersecurity
version: "1.0.0"
purity: impure
signature: "def scan_tcp_ports(host: str, ports: str | list[int] = 'common', timeout_s: float = 1.0, workers: int = 100) -> dict"
description: "Connect-scan TCP concurrente de un host sobre una lista o rango de puertos usando SOLO stdlib (socket + ThreadPoolExecutor). NO requiere nmap ni sudo: es un connect-scan simple (full handshake) que clasifica cada puerto en open/closed/filtered y los corre en paralelo con threads. Complementa a nmap_scan para escaneo rapido en Python puro; NO detecta version de servicio. Acepta ports como lista de ints, preset 'common', rango '1-1024' o CSV '22,80,443'. NO lanza: devuelve dict status ok/error con campo raw legible para evidencia OSINT."
tags: [recon, cybersecurity, port-scan, tcp, network]
params:
- name: host
desc: "Hostname o IP objetivo a escanear (ej. 'scanme.nmap.org', '127.0.0.1', '192.168.1.10'). Se resuelve a IP con socket.gethostbyname; si no resuelve devuelve status error. Vacio devuelve status error."
- name: ports
desc: "Especificacion de puertos. Cuatro formas: lista de ints [22,80,443]; string preset 'common' (~30 puertos comunes: 21,22,23,25,53,80,110,135,139,143,443,445,993,995,3306,3389,5432,5900,6379,8080,8443,27017... default); string rango '1-1024'; string CSV '22,80,443' (admite rangos mezclados '22,80,8000-8010'). Se normaliza a lista ordenada de ints unicos en 1..65535. Spec invalida devuelve status error."
- name: timeout_s
desc: "Timeout por conexion TCP en segundos (float). Default 1.0. Valor bajo en redes lentas puede marcar puertos realmente abiertos como filtered."
- name: workers
desc: "Numero de hilos concurrentes del ThreadPoolExecutor. Default 100. Se acota internamente a >=1 y al numero de puertos a escanear. Valores muy altos pueden saturar descriptores de archivo o la red local."
output: "dict de estado. ok: {status:'ok', host, ip (resuelta), ports_scanned:int, open:[int] (ordenada), closed_count:int, filtered_count:int, results:[{port:int, state:'open'|'closed'|'filtered'}] (ordenado por puerto), raw:str (bloque PORT/STATE legible con open+filtered, omite closed)}. error (host no resuelve, spec invalida, host vacio): {status:'error', error:str, host}. Nunca lanza excepciones."
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_py_core"
imports: []
tested: true
tests: ["test_parse_ports_common", "test_parse_ports_rango", "test_parse_ports_csv", "test_parse_ports_lista", "test_scan_localhost_puerto_abierto", "test_scan_host_no_resuelve_error", "test_scan_host_vacio_error", "test_scan_spec_invalida_error"]
test_file_path: "python/functions/cybersecurity/scan_tcp_ports_test.py"
file_path: "python/functions/cybersecurity/scan_tcp_ports.py"
---
## Ejemplo
```python
import sys, os
sys.path.insert(0, os.path.join("python", "functions"))
from cybersecurity import scan_tcp_ports
# 1) Puertos comunes contra el host oficial de pruebas de nmap (legal escanear).
res = scan_tcp_ports("scanme.nmap.org", ports="common", timeout_s=1.0)
if res["status"] == "ok":
print(res["ip"], "abiertos:", res["open"]) # ej. 45.33.32.156 abiertos: [22, 80]
print(res["raw"]) # bloque PORT/STATE para el vault
else:
print("error:", res["error"])
# 2) Rango de puertos concreto en localhost.
res = scan_tcp_ports("127.0.0.1", ports="1-1024", timeout_s=0.3, workers=200)
print(res["open"])
# 3) Lista explicita de puertos.
res = scan_tcp_ports("192.168.1.10", ports=[22, 80, 443, 8080])
```
Invocacion directa por el registry:
```bash
# Via MCP (preferido):
# mcp__registry__fn_run id="scan_tcp_ports_py_cybersecurity" args=["scanme.nmap.org"]
# Via CLI:
./fn run scan_tcp_ports scanme.nmap.org
```
## Cuando usarla
Usala cuando quieras saber rapidamente que puertos TCP estan abiertos en UN host
sin depender de nmap ni de sudo: escaneo en Python puro, scriptable y headless.
Ideal en entornos donde no puedes instalar nmap o quieres un sondeo ligero de la
superficie expuesta (un puñado de puertos o un rango pequeño) antes de pasar a
herramientas mas pesadas.
A diferencia de `nmap_scan_py_cybersecurity`: este NO da version ni nombre del
servicio, solo el estado del puerto (open/closed/filtered). Si necesitas
deteccion de version (-sV), scripts NSE, OS detection, UDP o barrido de subred,
usa `nmap_scan`. Para un check rapido "que puertos responden" en 1 host, esta es
mas directa.
## Gotchas
- Funcion impura: abre conexiones TCP reales (full three-way handshake). Es un
connect-scan, por lo que NO es sigiloso: queda en los logs del objetivo y es
facilmente detectable por IDS/firewalls. Sin sudo no hace SYN-scan (half-open).
- LEGAL: escanear puertos de hosts de terceros sin autorizacion puede ser delito.
Escanea solo objetivos propios o con permiso explicito. `scanme.nmap.org` es el
host oficial de pruebas de nmap (legal escanear).
- `timeout_s` bajo en redes lentas o con alta latencia puede marcar puertos
realmente ABIERTOS como `filtered` (la conexion no completa a tiempo). Sube
`timeout_s` si dudas de los resultados; bajalo para escanear rangos grandes mas
rapido a costa de falsos filtered.
- `workers` muy alto (miles) puede agotar descriptores de archivo del proceso o
saturar la red local / el objetivo. Se acota internamente al numero de puertos,
pero un rango grande con workers altos sigue siendo agresivo.
- Distincion de estados: `open` = connect exito; `closed` = RST / connection
refused (host vivo, puerto cerrado); `filtered` = timeout / inalcanzable
(probable firewall que descarta el paquete). Un host detras de firewall
drop-all puede devolver TODO filtered aunque tenga servicios.
- Solo IPv4: usa `socket.gethostbyname` (resuelve a A record). Para IPv6 usar otra
ruta. No lanza: revisa siempre `res["status"]` antes de leer `open`/`results`.