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