--- 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//recon/-.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.