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:
@@ -0,0 +1,93 @@
|
||||
---
|
||||
name: grab_service_banner
|
||||
kind: function
|
||||
lang: py
|
||||
domain: cybersecurity
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def grab_service_banner(host: str, port: int, timeout_s: float = 3.0, send_probe: bool = True) -> dict"
|
||||
description: "Captura el banner de un servicio TCP y lo identifica heuristicamente sin nmap -sV. Abre un socket TCP a host:port, opcionalmente envia un probe (HEAD / HTTP/1.0 para puertos web), lee hasta ~4096 bytes con timeout y reconoce el servicio (ssh, ftp, smtp, http, mysql/mariadb, redis, pop3, imap, telnet, ...) por heuristica sobre el banner, extrayendo producto y version best-effort. Complementa a un port scan: el scan dice si el puerto esta abierto, esta funcion dice QUE servicio y version hablan detras. Solo stdlib (socket, re, struct). NO lanza: devuelve dict status ok/error con campo raw (repr del banner crudo)."
|
||||
tags: [recon, cybersecurity, banner-grab, service-detection, network]
|
||||
params:
|
||||
- name: host
|
||||
desc: "Hostname o IP del objetivo (ej. 'scanme.nmap.org', '127.0.0.1'). Vacio devuelve status error."
|
||||
- name: port
|
||||
desc: "Puerto TCP a sondear (ej. 22 ssh, 80 http, 3306 mysql, 6379 redis). Fuera del rango 1..65535 o no convertible a int devuelve status error."
|
||||
- name: timeout_s
|
||||
desc: "Timeout en segundos tanto de conexion como de lectura del socket. Default 3.0. Subirlo para hosts lentos; bajarlo para barridos rapidos de muchos puertos."
|
||||
- name: send_probe
|
||||
desc: "Si True (default) y el puerto esta en el mapa interno de probes (puertos HTTP tipicos: 80/8080/8000/8888/8081/8008), envia 'HEAD / HTTP/1.0\\r\\n\\r\\n' para provocar respuesta de servicios web que no emiten banner pasivo. Para el resto de puertos no envia nada e intenta leer el banner pasivo (SSH/FTP/SMTP/POP3/IMAP emiten banner solo con conectar). False nunca envia probe (captura siempre pasiva)."
|
||||
output: "dict de estado. ok: {status:'ok', host, port:int, service:str (ssh|ftp|smtp|http|mysql|redis|pop3|imap|telnet|ftp-or-smtp|unknown), product:str (best-effort, p.ej. OpenSSH/nginx/Postfix/MySQL; '' si no se extrae), version:str (best-effort, p.ej. '8.9p1'; '' si no se extrae), banner:str (banner decodificado y .strip()), raw:str (repr() del banner crudo en bytes, seguro para guardar)}. error: {status:'error', error:str, host, port}. Nunca lanza excepciones."
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_py_core"
|
||||
imports: []
|
||||
tested: true
|
||||
tests: ["test_identifica_ssh_de_banner_local", "test_host_vacio_devuelve_error", "test_port_fuera_de_rango_devuelve_error"]
|
||||
test_file_path: "python/functions/cybersecurity/grab_service_banner_test.py"
|
||||
file_path: "python/functions/cybersecurity/grab_service_banner.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join("python", "functions"))
|
||||
from cybersecurity import grab_service_banner
|
||||
|
||||
# 1) Identificar el servicio SSH del host oficial de pruebas de nmap (legal).
|
||||
res = grab_service_banner("scanme.nmap.org", 22, timeout_s=5)
|
||||
if res["status"] == "ok":
|
||||
print(res["service"]) # "ssh"
|
||||
print(res["product"]) # "OpenSSH"
|
||||
print(res["version"]) # "8.9p1" (o similar)
|
||||
print(res["banner"]) # "SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.1"
|
||||
else:
|
||||
print("error:", res["error"])
|
||||
|
||||
# 2) Identificar un servidor web: el probe HTTP provoca respuesta con Server:.
|
||||
res = grab_service_banner("scanme.nmap.org", 80, timeout_s=5)
|
||||
print(res["service"], res["product"], res["version"]) # http nginx 1.18.0
|
||||
```
|
||||
|
||||
Tambien via `fn run` tras indexar:
|
||||
|
||||
```bash
|
||||
./fn run grab_service_banner_py_cybersecurity
|
||||
```
|
||||
|
||||
(El smoke del modulo sondea scanme.nmap.org:22 y tolera fallos de red.)
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Usala cuando YA sabes que un puerto esta abierto (p.ej. tras un escaneo de
|
||||
puertos) y quieres identificar el servicio y su version de forma rapida para un
|
||||
puerto concreto, sin levantar `nmap -sV`. Encaja como segundo paso de recon:
|
||||
primero localizas los puertos abiertos de un host, luego compones esta funcion
|
||||
sobre cada puerto interesante para etiquetar QUE habla detras (ssh, http, mysql,
|
||||
redis, ...) y guardar el banner como evidencia en la nota OSINT.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Funcion impura: abre una conexion TCP real al objetivo. Solo sondea hosts
|
||||
propios o con autorizacion explicita; conectar a servicios de terceros sin
|
||||
permiso puede ser ilegal.
|
||||
- TLS/HTTPS implicito (443, 993, 995, 465, ...): el servicio espera un handshake
|
||||
TLS antes de hablar, asi que el banner plano que captura esta funcion NO
|
||||
funciona ahi — devolvera bytes binarios ilegibles o timeout, con
|
||||
`service:"unknown"`. Para esos puertos hay que envolver el socket en TLS
|
||||
(ssl.SSLSocket) primero; esta funcion no lo hace.
|
||||
- Banner pasivo no garantizado: algunos servicios no emiten nada hasta completar
|
||||
un handshake especifico del protocolo. Para esos casos `banner` puede venir
|
||||
vacio y `service:"unknown"` aunque el puerto este abierto. El probe HTTP solo
|
||||
cubre los puertos web listados en el mapa interno; otros protocolos quedarian
|
||||
sin probe activo.
|
||||
- Decodificacion best-effort: el banner se decodifica utf-8 y cae a latin-1, lo
|
||||
que puede dar mojibake en bytes no textuales (handshakes binarios como MySQL).
|
||||
Por eso `raw` guarda el `repr()` de los bytes crudos como fuente fiable.
|
||||
- La identificacion es heuristica (regex/substring): puede equivocarse o quedar
|
||||
como `service:"unknown"`. `product`/`version` son best-effort y pueden ser "".
|
||||
- Nunca lanza: revisa siempre `res["status"]` antes de leer `service`/`banner`.
|
||||
Puerto cerrado/filtrado/inalcanzable devuelve `status:"error"`.
|
||||
Reference in New Issue
Block a user