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