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