157 lines
7.0 KiB
Markdown
157 lines
7.0 KiB
Markdown
---
|
|
name: osint_db
|
|
lang: py
|
|
domain: osint
|
|
version: 0.1.0
|
|
description: "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."
|
|
tags: [service, osint, duckdb, obsidian, dav, vault]
|
|
uses_functions:
|
|
- 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_collection_ctag_py_infra
|
|
- pass_get_secret_py_infra
|
|
- duckdb_query_readonly_py_infra
|
|
- render_markdown_table_py_core
|
|
- upsert_sentinel_block_py_core
|
|
uses_types: []
|
|
framework: "fastapi"
|
|
entry_point: "server/main.py"
|
|
dir_path: "projects/osint/apps/osint_db"
|
|
repo_url: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/dataforge/osint_db"
|
|
service:
|
|
port: 8771
|
|
health_endpoint: /api/health
|
|
health_timeout_s: 3
|
|
systemd_unit: null
|
|
systemd_scope: null
|
|
restart_policy: none
|
|
runtime: manual
|
|
pc_targets:
|
|
- lucas-linux
|
|
is_local_only: true
|
|
e2e_checks:
|
|
- id: tests
|
|
cmd: ".venv/bin/python -m pytest tests -q"
|
|
timeout_s: 120
|
|
- id: vault_missing
|
|
cmd: ".venv/bin/python server/main.py --vault /no/existe --port 0"
|
|
expect_exit: 2
|
|
timeout_s: 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.
|
|
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.
|
|
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
|
|
|
|
```bash
|
|
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 y reconstruye notes + entidades + derivadas |
|
|
| POST | `/api/ingest/dav` | baja Xandikos (CardDAV + 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) |
|
|
|
|
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
|
|
|
|
```bash
|
|
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.
|