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>
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. |
|
|
fastapi | server/main.py | projects/osint/apps/osint_db | https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/dataforge/osint_db |
|
|
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)
- 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 llevanote_path: el path relativo de la nota dentro del vault.personses dueña de sus campos estructurados (multi-valortelefonos/emails/direccionesJSON + singulares de compattelefono/email/direccion): la API los edita y los materializa a la nota. El ingest del vault es selectivo parapersons— una ficha que ya existe en la DB solo refrescanote_path+extra_fm(el frontmatter no-owned), conservando los campos OWNED; una ficha nueva se inserta completa (bootstrap desde el frontmatter).addressbooks(schemamain) registra las libretas CardDAV: el ingest DAV las recorre todas (no solo la fija). - Maestras DAV (schema
main):contactsyeventsimportados de Xandikos — fuente de verdad del lado agenda/calendario.contacts.note_pathse enlaza contrapersonsmatcheando por UIDosint-<slug>, por eldav_uidextraído del campofuentede la ficha, por teléfono normalizado o por email.network_scans(schemamain) registra escaneos de red (whois/rdap/dns/nmap/traceroute/ping) que otras herramientas guardan víaPOST /api/scan; llevanote_path(la notadominios/<slug>/recon/<...>.mddonde se documenta cada escaneo) y NO se reconstruye en ningún ingest — la pueblan los escaneos y nunca se trunca. - Derivadas (schema
derived): SOLO datos computados. Regla dura: ninguna tabla dederivedlleva columna que referencie notas (note_pathprohibido ahí; hay un test que lo verifica víainformation_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) yderived.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.1yruntime: manual(no se despliega a VPS,is_local_only: true). /api/queryes estrictamente read-only (conexiónread_onlyde DuckDB)./api/render/notevalida 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_onlyconcurrente puede devolver{"status":"error"}por conflicto de lock. El cliente reintenta. /api/ingest/davrequierepassdesbloqueado (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 ennotespero NO cuentan como entidades.