Files
osint_db/app.md
T
egutierrez 3063d3c44f feat(scans): persistencia de escaneos de red + POST /api/scan
Tabla network_scans (migración 005, schema main, lleva note_path) que otras
herramientas pueblan vía HTTP con escaneos de reconocimiento (whois/rdap/dns/
nmap/traceroute/ping). Endpoint POST /api/scan: id determinista
<target_slug>:<scan_type>:<YYYYMMDD-HHMM> derivado de scan_ts, idempotente por
id (duckdb_upsert ON CONFLICT DO UPDATE) bajo el lock single-writer del service.
summary (dict) se serializa a JSON.

network_scans no se deriva de notas: ni ingest_vault ni ingest_dav la tocan, así
que un re-ingest del vault no la trunca (test lo verifica).

Tests: inserción + id derivado, idempotencia mismo-minuto, validación de campos
requeridos (422), y no-truncado por ingest del vault.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 13:13:36 +02:00

12 KiB

name, lang, domain, version, description, tags, uses_functions, uses_types, framework, entry_point, dir_path, repo_url, service, e2e_checks
name lang domain version description tags uses_functions uses_types framework entry_point dir_path repo_url service e2e_checks
osint_db py osint 0.1.0 Service FastAPI local (solo 127.0.0.1, puerto 8771) dueño único de la base DuckDB data/osint.duckdb: fuente de verdad estructurada del ecosistema OSINT. Indexa el vault de Obsidian osint (notes + persons/organizations/domains/cases/places con note_path), importa las maestras DAV de Xandikos (contacts, events), computa derivadas sin referencias a notas y renderiza tableros Markdown con bloques sentinel idempotentes dentro del vault.
service
osint
duckdb
obsidian
dav
vault
list_obsidian_notes_py_obsidian
read_obsidian_note_py_obsidian
update_obsidian_note_py_obsidian
create_obsidian_note_py_obsidian
slugify_obsidian_name_py_obsidian
dav_get_collection_py_infra
dav_list_calendars_py_infra
dav_list_addressbooks_py_infra
dav_collection_ctag_py_infra
dav_list_resources_py_infra
dav_get_resource_py_infra
carddav_put_vcard_py_infra
caldav_put_event_py_infra
dav_delete_resource_py_infra
dav_make_addressbook_py_infra
dav_make_calendar_py_infra
pass_get_secret_py_infra
duckdb_query_readonly_py_infra
duckdb_execute_py_infra
duckdb_upsert_py_infra
build_vcard_py_core
render_markdown_table_py_core
upsert_sentinel_block_py_core
fastapi server/main.py projects/osint/apps/osint_db https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/dataforge/osint_db
port health_endpoint health_timeout_s systemd_unit systemd_scope restart_policy runtime pc_targets is_local_only
8771 /api/health 3 null null none manual
lucas-linux
true
id cmd timeout_s
tests .venv/bin/python -m pytest tests -q 120
id cmd expect_exit timeout_s
vault_missing .venv/bin/python server/main.py --vault /no/existe --port 0 2 30

Qué es

Service del project osint que posee en exclusiva la base DuckDB data/osint.duckdb, la fuente de verdad estructurada del ecosistema OSINT (vault de Obsidian ~/Obsidian/osint + servidor Xandikos CardDAV/CalDAV). Un plugin de Obsidian se construye en paralelo contra el contrato de esta API, por eso los endpoints de datos responden SIEMPRE HTTP 200 con status: ok|error en el body (el plugin parsea el body, no el código HTTP).

Arquitectura de datos (3 categorías)

  1. Maestras con referencia a notas (schema main): notes (índice completo del vault) + persons, organizations, domains, cases, places (fichas de nivel-1 de cada carpeta de entidades, excluyendo las notas con prefijo _). Cada una lleva note_path: el path relativo de la nota dentro del vault. persons es dueña de sus campos estructurados (multi-valor telefonos/emails/direcciones JSON + singulares de compat telefono/email/direccion): la API los edita y los materializa a la nota. El ingest del vault es selectivo para persons — una ficha que ya existe en la DB solo refresca note_path + extra_fm (el frontmatter no-owned), conservando los campos OWNED; una ficha nueva se inserta completa (bootstrap desde el frontmatter). addressbooks (schema main) registra las libretas CardDAV: el ingest DAV las recorre todas (no solo la fija).
  2. Maestras DAV (schema main): contacts y events importados de Xandikos — fuente de verdad del lado agenda/calendario. contacts.note_path se enlaza contra persons matcheando por UID osint-<slug>, por el dav_uid extraído del campo fuente de la ficha, por teléfono normalizado o por email. network_scans (schema main) registra escaneos de red (whois/rdap/dns/nmap/traceroute/ping) que otras herramientas guardan vía POST /api/scan; lleva note_path (la nota dominios/<slug>/recon/<...>.md donde se documenta cada escaneo) y NO se reconstruye en ningún ingest — la pueblan los escaneos y nunca se trunca.
  3. Derivadas (schema derived): SOLO datos computados. Regla dura: ninguna tabla de derived lleva columna que referencie notas (note_path prohibido ahí; hay un test que lo verifica vía information_schema). Se reconstruyen completas (DROP + CREATE) en cada ingest: derived.person_stats (agregados por contexto/país/tag), derived.event_monthly (eventos por calendario y mes) y derived.contact_link_quality (contactos enlazados vs no, solo números).

Single-writer: SOLO este service escribe la DuckDB. La conexión de escritura se abre bajo demanda (migraciones, ingest, render) serializada con un lock de proceso y se cierra al terminar; las lecturas de /api/query abren su propia conexión read_only vía duckdb_query_readonly.

Migraciones

migrations/NNN_*.sql numeradas, aditivas e idempotentes, aplicadas en orden al arrancar. La tabla _migrations registra las aplicadas (regla db_migrations adaptada a DuckDB).

Arrancar

cd projects/osint/apps/osint_db
uv sync                                   # primera vez: crea .venv
.venv/bin/python server/main.py           # defaults: vault ~/Obsidian/osint, puerto 8771
.venv/bin/python server/main.py --vault ~/Obsidian/osint --db data/osint.duckdb --port 8771

Health check: curl http://127.0.0.1:8771/api/health.

Endpoints

Método Ruta Qué hace
GET /api/health {"status":"ok","db_path":"...","tables":N}
GET /api/tables inventario: schema, name, kind master/derived, row_count, columnas
POST /api/query {sql, params, max_rows} → respuesta exacta de duckdb_query_readonly (solo lectura)
GET /api/queries catálogo de queries con nombre (server/named_queries.py)
POST /api/query/named {name, max_rows} → misma shape que /api/query
POST /api/ingest/vault escanea el vault completo; notes y entidades de espejo puro se reemplazan, persons se ingesta SELECTIVO (existentes solo note_path+extra_fm, nuevas bootstrap completo)
POST /api/ingest/dav baja TODAS las libretas registradas en addressbooks + cada calendario CalDAV, reconstruye contacts/events, enlaza y reconstruye derivadas
POST /api/render/note {note_path, block_id, sql|query, title?} → tabla Markdown upsertada como bloque sentinel osintdb en la nota (la crea si no existe)
POST/PUT/DELETE /api/person[/{slug}] CRUD de personas multi-valor (telefonos/emails/direcciones listas). Tras escribir la DB, materializa la ficha DB→nota (singulares = lista[0]) sin tocar la prosa
POST /api/person/{slug}/render re-materializa la ficha DB→nota (frontmatter OWNED + merge extra_fm, preserva el body)
POST/PUT/DELETE /api/contact[/{uid}] CRUD de contactos CardDAV (tels/emails listas). Tras la DB, push DB→Xandikos (build_vcard+carddav_put_vcard, o dav_delete_resource en delete) fuera de la transacción
POST/PUT/DELETE /api/event[/{uid}] CRUD de eventos CalDAV. Push caldav_put_event/dav_delete_resource
POST /api/addressbook {slug, display_name?, description?, color?}dav_make_addressbook + INSERT en addressbooks
POST /api/calendar {slug, display_name?, color?}dav_make_calendar (paridad)
POST /api/scan {target, target_slug, scan_type, note_path, tool?, summary?, scan_ts?} → registra un escaneo de red en network_scans. id = <target_slug>:<scan_type>:<YYYYMMDD-HHMM> derivado de scan_ts (default: ahora); idempotente por id (upsert). Responde {"status":"ok","id":<id>}
POST /api/push/dav reconcilia en bloque por HTTP: recorre contacts y events de la DB y los empuja a Xandikos (1 PUT + 1 PROPFIND + 1 commit git por recurso, sin borrar). Fallback cuando no hay SSH al host de Xandikos
POST /api/push/dav-bulk vía RÁPIDA del push de contactos por DISCO: genera todos los .vcf en un tmpdir local y hace 1 rsync + 1 commit + 1 PROPFIND contra el working tree git que Xandikos sirve. Reconcilia ~1000 contactos en <1s en vez de ~6 min. Requiere SSH por clave

/api/push/dav (HTTP) vs /api/push/dav-bulk (disco)

Ambos vuelcan los contactos de la DB a Xandikos, pero por mecanismos distintos:

/api/push/dav (HTTP) /api/push/dav-bulk (disco)
Mecanismo N PUT WebDAV (uno por contacto) 1 rsync de todos los .vcf a la vez
PROPFIND 1 por contacto (lee el etag tras cada PUT) 1 al final, lee todos los etags de golpe
Commits git en el remoto N (Xandikos commitea cada PUT) 1 (git add -A && commit único)
Coste para ~1000 contactos ~6 min (≥1 PROPFIND completo/contacto) <1s (dominado por 2-3 round-trips SSH)
Eventos CalDAV sí (también empuja events) no (solo contacts)
Borra huérfanos remotos no sí — rsync --delete deja la colección .vcf == DB
Requisitos red HTTPS a Xandikos + pass SSH por clave al host + rsync ambos lados

Usa /api/push/dav-bulk para reconciliar en bloque (tras una migración o un ingest masivo) cuando hay SSH al host de Xandikos: es el camino normal por rapidez. Usa /api/push/dav como fallback cuando no hay SSH, o cuando también necesitas empujar eventos CalDAV.

Cómo lo ve Xandikos: sirve cada colección desde el working tree de un repo git y calcula el ctag desde el HEAD. El push por disco escribe los .vcf directamente en ese working tree y hace un único commit; Xandikos ve el nuevo HEAD en el siguiente request (nuevo ctag → DAVx5 detecta el cambio). El rsync sincroniza SOLO los *.vcf (--include='*.vcf' --exclude='*'), preservando .git/, .xandikos (tipo de colección) y push-subscriptions.json (suscripciones WebDAV-Push). Config del host/working tree: DAV_BULK_SSH_HOST / DAV_BULK_REMOTE_DIR en server/config.py.

Queries con nombre incluidas: personas_por_contexto, personas_recientes, eventos_proximos, contactos_sin_nota, stats_personas, calidad_enlace_contactos, eventos_por_mes.

Configuración

server/config.py: vault (~/Obsidian/osint), db (data/osint.duckdb relativa a la app), DAV base/colecciones de Xandikos (los mismos valores que projects/osint/tools/sync_dav_to_osint.py) y puerto 8771. Overrides por CLI: --vault, --db, --port. La credencial DAV se resuelve SIEMPRE con pass_get_secret("dav/xandikos-enmanuel"), nunca hardcodeada.

Seguridad

  • El vault y la base contienen datos personales sensibles: el server escucha solo en 127.0.0.1 y runtime: manual (no se despliega a VPS, is_local_only: true).
  • /api/query es estrictamente read-only (conexión read_only de DuckDB).
  • /api/render/note valida que el path destino no escapa del vault (realpath bajo el realpath del vault).
  • Vault inexistente al arrancar → error claro en stderr + exit 2.

Tests

cd projects/osint/apps/osint_db
.venv/bin/python -m pytest tests -q

Vault temporal + DuckDB temporal, sin red: migraciones idempotentes, ingest del vault con fixture (conteos, exclusión de sub-notas y _), extracción de dav_uid desde fuente, /api/query ok/error/solo-lectura (siempre HTTP 200), catálogo y queries con nombre, inventario de tablas, regla dura derivadas sin note_path, enlace contacto→ficha por teléfono y render de nota con bloque sentinel idempotente + validación de inputs y path traversal.

Gotchas

  • DuckDB bloquea el archivo a un escritor exclusivo: mientras un ingest está escribiendo, una lectura read_only concurrente puede devolver {"status":"error"} por conflicto de lock. El cliente reintenta.
  • /api/ingest/dav requiere pass desbloqueado (gpg-agent); si gpg está bloqueado devuelve {"status":"error"} con el detalle, sin crash.
  • Las notas con prefijo _ (_indice.md, _plantilla.md) y las sub-notas de documento (personas/<slug>/<doc>.md) entran en notes pero NO cuentan como entidades.