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:
2026-06-14 15:12:07 +02:00
parent d89da1292d
commit 935008ec3f
49 changed files with 6659 additions and 302 deletions
@@ -0,0 +1,87 @@
---
name: save_scan_to_osint
kind: function
lang: py
domain: cybersecurity
version: "1.0.0"
purity: impure
signature: "def save_scan_to_osint(target: str, scan_type: str, raw: str, summary: dict | None = None, vault_dir: str = '~/Obsidian/osint', service_url: str = 'http://127.0.0.1:8771', tool: str | None = None) -> dict"
description: "Sink comun OSINT: persiste el resultado de cualquier escaneo de red (whois|rdap|dns|nmap|traceroute|ping) en el ecosistema OSINT del repo. Dos capas: (1) capa nota SIEMPRE (fuente de verdad) que escribe una nota Markdown tipada en el vault Obsidian bajo dominios/<slug>/recon/<scan_type>-<ts>.md con el raw en bloque de codigo, componiendo create_obsidian_note; (2) capa registro estructurado best-effort que hace POST al service osint_db (DuckDB single-writer) en /api/scan. Si el service esta caido o el endpoint no existe (404), degrada a solo-nota con register_warning sin fallar. No lanza: devuelve dict de estado."
tags: [recon, osint, cybersecurity, obsidian, sink]
uses_functions: [create_obsidian_note_py_obsidian]
uses_types: []
returns: []
returns_optional: false
error_type: "error_py_core"
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/cybersecurity/save_scan_to_osint.py"
params:
- name: target
desc: "Objetivo del scan (dominio, host o IP). Define el slug de la carpeta en el vault."
- name: scan_type
desc: "Tipo de scan (whois|rdap|dns|nmap|traceroute|ping). Texto libre que se sanea a slug seguro para nombre de archivo y tags."
- name: raw
desc: "Salida cruda del scan (texto). Se embebe en un bloque de codigo en la nota; si supera ~200KB se trunca dejando una marca."
- name: summary
desc: "dict opcional con campos resumidos del scan (registrar, ips, puertos, rtt...). Se anade al frontmatter de la nota y se envia al registro estructurado. None -> {}."
- name: vault_dir
desc: "Raiz del vault OSINT. Se expande ~. Default ~/Obsidian/osint."
- name: service_url
desc: "Base del service osint_db (FastAPI + DuckDB). Default http://127.0.0.1:8771. Se le concatena /api/scan."
- name: tool
desc: "Nombre de la herramienta usada (nmap, dig, whois...). Si None usa el scan_type saneado."
output: "dict de estado. Caso ok: {status:'ok', target, slug, scan_type, note_path (rel al vault), note_abs (ruta absoluta), registered (bool: si el POST a osint_db tuvo exito), register_warning (str|None: motivo si el registro DuckDB fallo), scan_id (str|None: id devuelto por el service)}. Caso error (solo si falla la escritura critica de la nota): {status:'error', error: str}."
---
## Ejemplo
```python
import sys, os
sys.path.insert(0, os.path.join("python", "functions"))
from cybersecurity.save_scan_to_osint import save_scan_to_osint
res = save_scan_to_osint(
"example.com",
"whois",
"Domain: EXAMPLE.COM\nRegistrar: X",
summary={"registrar": "X"},
)
print(res["note_path"]) # dominios/example.com/recon/whois-YYYYMMDD-HHMM.md
print(res["registered"]) # True si osint_db esta vivo y expone POST /api/scan, False si degrado
```
## Cuando usarla
Tras cualquier escaneo de red (whois/rdap/dns/nmap/traceroute/ping), para que el
resultado quede archivado y navegable en el vault OSINT. Llamar SIEMPRE despues de
un scan. Es el sink comun del ecosistema: cualquier funcion de scan del registry
(whois_lookup, dns_records, scan_port_tcp, etc.) deberia volcar aqui su salida.
## Gotchas
- **Impura**: escribe en disco (el vault Obsidian) y hace una request HTTP de red.
- **overwrite=True**: un re-scan del mismo target+tipo dentro del mismo minuto pisa la
nota anterior (el timestamp del nombre de archivo tiene granularidad de minuto,
`YYYYMMDD-HHMM`).
- **Registro DuckDB best-effort**: la capa 2 depende de que el service `osint_db` este
vivo y exponga `POST /api/scan`. Si esta caido (ConnectionError) o el endpoint no
existe todavia (404), la funcion NO falla: degrada a solo-nota y devuelve
`registered=False` + `register_warning` con el motivo. `status` sigue siendo `"ok"`
porque la capa critica (la nota) se guardo.
- **Single-writer DuckDB**: la DB esta abierta por el service `osint_db`. NUNCA abrir
`osint.duckdb` directo en paralelo; el registro estructurado pasa SIEMPRE por HTTP.
- **Solo `status:"error"`** si falla la escritura de la nota (capa critica). Un fallo de
red nunca produce error.
- **Contrato del endpoint** (lo crea el service osint_db): `POST /api/scan` con JSON
`{target, target_slug, scan_type, tool, note_path, summary, scan_ts}`; respuesta 2xx,
opcionalmente `{"id": "..."}` que se devuelve como `scan_id`.
## Notas
Compone `create_obsidian_note_py_obsidian` (del grupo obsidian) para la capa nota y usa
`urllib.request` de stdlib para la capa de registro (sin dependencias nuevas). El
timeout HTTP es de 5s. El raw se envuelve en un bloque de codigo fenced con backticks
suficientes para no colisionar con backticks internos del propio raw.