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,116 @@
|
||||
---
|
||||
name: scan_port_services
|
||||
kind: pipeline
|
||||
lang: py
|
||||
domain: pipelines
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def scan_port_services(host: str, ports: str | list[int] = 'common', timeout_s: float = 1.0, workers: int = 100, grab_banners: bool = True, banner_timeout_s: float = 3.0, save: bool = True) -> dict"
|
||||
description: "One-shot que escanea los servicios de los puertos de un host: hace un connect-scan TCP y, por cada puerto abierto, devuelve el servicio esperado por convencion IANA (identify_port_service) y el servicio/version REAL leido del banner en vivo (grab_service_banner). Reemplaza el patron scan_tcp_ports -> identify -> grab repetido (1 scan + 2*K por puerto abierto) por una sola llamada. Opcionalmente archiva la evidencia (tabla PORT/EXPECTED/ACTUAL/BANNER) en OSINT. No requiere nmap. Util para fingerprint de servicios, auditoria de superficie de ataque y reconocimiento de puertos de un host."
|
||||
tags: [recon, pipelines, cybersecurity, port-scan, service-detection, banner, sink]
|
||||
uses_functions:
|
||||
- scan_tcp_ports_py_cybersecurity
|
||||
- identify_port_service_py_cybersecurity
|
||||
- grab_service_banner_py_cybersecurity
|
||||
- save_scan_to_osint_py_cybersecurity
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_py_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: host
|
||||
desc: "Hostname o IP objetivo del escaneo (ej. 127.0.0.1, scanme.nmap.org, 10.0.0.5)."
|
||||
- name: ports
|
||||
desc: "Especificacion de puertos, se pasa tal cual a scan_tcp_ports. Acepta lista de ints ([22,80,443]), preset 'common' (~30 puertos comunes, default), rango '1-1024' o CSV '22,80,443' (con rangos mezclados '22,80,8000-8010')."
|
||||
- name: timeout_s
|
||||
desc: "Timeout por conexion TCP del connect-scan, en segundos. Default 1.0. Bajo en redes lentas puede marcar abiertos como filtered."
|
||||
- name: workers
|
||||
desc: "Numero de hilos concurrentes del escaneo de puertos. Default 100. Se acota al numero de puertos a escanear."
|
||||
- name: grab_banners
|
||||
desc: "Si True (default) llama grab_service_banner por cada puerto abierto para identificar el servicio/version real; si False solo usa identify_port_service (servicio esperado por convencion) sin tocar el servicio en vivo: mas rapido y mas sigiloso (sin segunda ronda de conexiones)."
|
||||
- name: banner_timeout_s
|
||||
desc: "Timeout del grab de banner por puerto, en segundos. Default 3.0. Solo aplica si grab_banners=True."
|
||||
- name: save
|
||||
desc: "Si True (default) archiva la evidencia en OSINT via save_scan_to_osint con scan_type='port_services'; si False solo ejecuta el escaneo y no toca el vault ni el service osint_db. Politica recon: todo scan se archiva. Si el sink falla, el resultado degrada sin romper (saved.status='error')."
|
||||
output: "dict con status ('ok'|'error'), host, ip (resuelta), open_ports (lista de ints), services (lista de dicts con port, expected_service, expected_desc, actual_service, product, version, banner, match), saved (dict de save_scan_to_osint con note_path/registered/scan_id, o None si save=False) y raw (tabla legible PORT/EXPECTED/ACTUAL/BANNER). Si el escaneo de puertos falla (host no resuelve, spec invalida) -> {status:error, stage:scan, scan:<dict>}. Cuando grab_banners=False, actual_service/product/version/banner quedan None/'' y match=None. Nunca lanza."
|
||||
tested: true
|
||||
tests: ["test_golden_scan_localhost_con_banner_real", "test_grab_banners_false_solo_servicio_esperado", "test_scan_fallido_propaga_error_sin_red", "test_save_false_no_archiva_osint"]
|
||||
test_file_path: "python/functions/pipelines/scan_port_services_test.py"
|
||||
file_path: "python/functions/pipelines/scan_port_services.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
from pipelines.scan_port_services import scan_port_services
|
||||
|
||||
# Escaneo + fingerprint de servicios de un host, archivado en OSINT (1 paso).
|
||||
r = scan_port_services("127.0.0.1", ports="common")
|
||||
print(r["status"]) # "ok"
|
||||
print(r["open_ports"]) # [22, 5432, 6379]
|
||||
for s in r["services"]:
|
||||
print(s["port"], s["expected_service"], "->", s["actual_service"], s["version"])
|
||||
# 22 ssh -> ssh 9.6p1
|
||||
print(r["saved"]["note_path"]) # ruta de la nota creada en el vault osint
|
||||
```
|
||||
|
||||
```python
|
||||
from pipelines.scan_port_services import scan_port_services
|
||||
|
||||
# Solo servicio esperado por convencion (sin tocar el servicio en vivo), sin archivar.
|
||||
r = scan_port_services("10.0.0.5", ports=[22, 80, 443, 3306], grab_banners=False, save=False)
|
||||
print(r["raw"]) # tabla PORT/EXPECTED/ACTUAL/BANNER (ACTUAL vacio)
|
||||
```
|
||||
|
||||
```bash
|
||||
# Por CLI: escanea los puertos comunes de un host.
|
||||
./fn run scan_port_services 127.0.0.1 common
|
||||
# Flags: --no-banners (solo servicio esperado), --no-save (no archiva OSINT).
|
||||
./fn run scan_port_services scanme.nmap.org 22,80,443 --no-save
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando quieras en UN solo paso saber que puertos estan abiertos en un host Y
|
||||
que servicio/version corre en cada uno, sin nmap. Reemplaza el patron repetido
|
||||
`scan_tcp_ports` -> `identify_port_service` -> `grab_service_banner` (una ronda
|
||||
de scan + dos llamadas por cada puerto abierto). Tipico para: fingerprint de
|
||||
servicios de un objetivo, auditoria de superficie de ataque, validar que un
|
||||
puerto abierto corre el servicio esperado (campo `match`), o reconocimiento
|
||||
inicial de un host autorizado.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **Ruidoso/detectable**: es un connect-scan (handshake TCP completo) seguido,
|
||||
si `grab_banners=True`, de una segunda conexion por puerto para leer el
|
||||
banner. Deja rastro en logs del objetivo. Usa `grab_banners=False` para un
|
||||
paso menos invasivo (sin segunda ronda).
|
||||
- **Servicios sobre TLS no dan banner plano**: puertos como 443/993/995/8443
|
||||
hablan TLS, no emiten un banner texto al conectar, asi que `actual_service`
|
||||
quedara `"unknown"` ahi (no hay handshake TLS en `grab_service_banner`). El
|
||||
`expected_service` (https/imaps/...) si lo identifica por convencion.
|
||||
- **match es heuristico**: `match=True` solo cuando expected y actual son ambos
|
||||
concretos (no "unknown") y coinciden. Un `match=False` puede significar
|
||||
"no coinciden" o "no se pudo determinar el real"; mira `actual_service`.
|
||||
- **save=True escribe en el vault OSINT** (`~/Obsidian/osint`) y hace POST al
|
||||
service `osint_db` (`http://127.0.0.1:8771`). Si el service esta caido,
|
||||
`save_scan_to_osint` degrada a solo-nota (`saved.registered=False` con
|
||||
`register_warning`); el pipeline no falla por eso.
|
||||
- **Autorizacion legal**: escanear puertos y leer banners de hosts ajenos sin
|
||||
permiso puede ser ilegal. Solo objetivos propios o con autorizacion explicita.
|
||||
- **Pipeline impuro**: hace red (scan + banners) y FS/HTTP (vault + service).
|
||||
No es determinista entre ejecuciones.
|
||||
- Si el escaneo de puertos falla (`status != "ok"`: host no resuelve, spec
|
||||
invalida), el pipeline devuelve `{"status":"error","stage":"scan",...}` y
|
||||
**no** intenta identificar servicios ni archivar nada.
|
||||
|
||||
## Notas
|
||||
|
||||
Pipeline que compone 4 funciones atomicas del dominio `cybersecurity`. No
|
||||
reimplementa logica de escaneo, identificacion ni persistencia: solo orquesta
|
||||
`scan_tcp_ports` (puertos abiertos) + `identify_port_service` (servicio esperado,
|
||||
puro) + `grab_service_banner` (servicio real, por puerto abierto) y delega el
|
||||
guardado en `save_scan_to_osint`. El grab de banners es secuencial por KISS (los
|
||||
puertos abiertos suelen ser pocos y cada grab ya tiene timeout acotado). Nunca
|
||||
lanza excepciones: todo fallo se refleja en la clave `status` del dict devuelto.
|
||||
Reference in New Issue
Block a user