---
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"`.
