feat(infra): auto-commit con 88 cambios

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-11 00:16:46 +02:00
parent 6bc97df5c0
commit eb8dbf66a1
126 changed files with 10933 additions and 287 deletions
+4
View File
@@ -25,6 +25,7 @@ Indice de grupos de capacidades del registry. Cada grupo agrupa >=3 funciones qu
| [android](android.md) | 37 | Toolbelt Android desde WSL2: adb, emuladores AVD, APK build/install, Capacitor, logcat |
| [web-proxy](web-proxy.md) | 5 | Captura de trafico HTTP/HTTPS liviana (mitmproxy): proxy con rotacion, navegador proxeado, consulta de capturas, tee del SSE de claude. Alternativa ligera a ZAP/Burp |
| [flow-replay](flow-replay.md) | 3 | Guardar un flujo web (login, reiniciar server, formulario) como funcion reproducible: destila un HAR a call specs y lo reproduce sin navegador (HTTP puro), con fallback a chromium headless/visible. Consume las capturas de web-proxy |
| [hoppscotch](hoppscotch.md) | 7 | Operar Hoppscotch SELF-HOSTED (docker en selfhost/) via API GraphQL: login (magic link headless via mailpit), CRUD de requests (create/update/delete/list), set_environment (idempotente, resuelve secretos pass:). El agente crea/edita y el humano lo ve en vivo en su GUI (subscriptions). build es helper interno de serializacion. Modo .json local ELIMINADO |
| [metabase](metabase.md) | 106 | Operar Metabase via API REST: auth, cards, dashboards, collections, snippets, permissions |
| [doctor](doctor.md) | 11 | Diagnostico read-only del registry: artefactos, servicios, drift, funciones huerfanas |
| [notebook](notebook.md) | 5 | Operar Jupyter Lab colaborativo (discover/read/exec/write/kernel) |
@@ -49,6 +50,9 @@ Indice de grupos de capacidades del registry. Cada grupo agrupa >=3 funciones qu
| [mesh-3d](mesh-3d.md) | 3 | Carga y upload a GPU de meshes 3D (OBJ, GLB/glTF 2.0): loaders CPU + mesh_gpu_upload OpenGL |
| [terminal-capture](terminal-capture.md) | 6 | Automatizar y capturar el texto de una CLI/TUI interactiva via PTY headless: spawn+input scripteado (one-shot y streaming), render del layout 2D (emulador VT), strip ANSI, delta por prefijo, y parseo de la TUI de claude a datos |
| [claude-direct](claude-direct.md) | 3 | Hablar directamente con la API de Anthropic Messages usando el token OAuth de Claude Code (Claude Max): leer token, stream SSE, bucle agentico de tool-use |
| [obsidian](obsidian.md) | 14 | CRUD headless de vaults y notas Obsidian como Markdown plano (frontmatter YAML + wikilinks): parse/format, read/create/update/delete/list/search notas, list/create vaults, slugify/embeds/resolve. Sin app GUI |
| [osint-passive](osint-passive.md) | 8 | Recoleccion OSINT pasiva (fuentes publicas, no intrusiva): EXIF/PDF metadata, whois RDAP, DNS, subdominios crt.sh, guess emails, username enumeration, search dorks |
| [osint-enrich](osint-enrich.md) | 3 | Orquestadores de enriquecimiento OSINT: componen osint-passive para aumentar datapoints de personas (emails/usernames/dorks), orgs (whois+dns+subdominios) y metadatos de attachments |
## Como anadir grupo
+83
View File
@@ -0,0 +1,83 @@
# Capability group: `hoppscotch`
Operar una instancia **self-hosted de Hoppscotch** (consola de APIs, alternativa open-source a
Postman) desde el registry, vía su **API GraphQL**. El agente crea/edita requests, colecciones y
environments por la API; el humano los ve **en vivo** en su GUI (subscriptions = hot-reload real).
Las requests viven en la base de datos del self-host (Postgres), compartida entre el agente y la GUI.
Este es el **flujo canónico**. El antiguo modo "archivo `.json` local" (funciones
`parse_*` / `run_*` / `add_hoppscotch_request`) **fue eliminado**: escribía un `.json` en disco que
NO subía al workspace, así que el humano no lo veía en la GUI. No lo reintroduzcas.
## Stack self-host
Vive en `projects/web_scraping/hoppscotch/selfhost/` (docker compose: AIO + Postgres + mailpit).
| Servicio | URL | Para qué |
|---|---|---|
| App (cliente) | `http://localhost:3009` | la GUI donde el humano usa las colecciones (instalable como PWA) |
| Admin dashboard | `http://localhost:3100` | gestión (usuarios, config) |
| Backend GraphQL | `http://localhost:3170/graphql` | la API que usan las funciones |
| Mailpit | `http://localhost:8025` | captura el magic link del login (SMTP de pruebas, sin correo real) |
Levantar: `cd selfhost && docker compose up -d`. Team de trabajo: **"registry"**. Cuenta: `admin@example.com`.
## Funciones
| ID | Firma corta | Qué hace |
|---|---|---|
| `hoppscotch_login_py_infra` | `(email, *, backend_url, mailpit_url) -> {access_token,...}` | login por magic link headless (lee el link de mailpit) → JWT |
| `hoppscotch_create_request_py_infra` | `(collection_id, method, url, *, title, headers, body, body_type, team_id, access_token) -> dict` | crea una request en una colección de la team |
| `hoppscotch_update_request_py_infra` | `(request_id, method, url, *, title, headers, body, body_type, access_token) -> dict` | actualiza una request |
| `hoppscotch_delete_request_py_infra` | `(request_id, *, access_token) -> dict` | borra una request |
| `hoppscotch_list_requests_py_infra` | `(collection_id, *, access_token) -> {requests:[...]}` | lista las requests de una colección |
| `hoppscotch_set_environment_py_infra` | `(team_id, name, variables, *, access_token) -> dict` | crea/actualiza (idempotente) el environment de la team; resuelve secretos `pass:` |
| `build_hoppscotch_collection_py_infra` | `(calls, *, name, request_names) -> dict` | **helper interno** de create/update: serializa call specs al formato HoppRESTRequest. NO para escribir `.json` a mano |
| `pass_get_secret_py_infra` | `(path, *, line) -> {value}` | lee un secreto de `pass` (lo consume `set_environment` para no hardcodear keys) |
`access_token` se pasa como **cookie**, no header `Authorization`. Caduca a 24h → re-login con `hoppscotch_login`.
## Ejemplo canónico (end-to-end)
```python
import sys, os
sys.path.insert(0, os.path.join(os.path.expanduser("~/fn_registry"), "python", "functions"))
from infra.hoppscotch_login import hoppscotch_login
from infra.hoppscotch_create_request import hoppscotch_create_request
from infra.hoppscotch_set_environment import hoppscotch_set_environment
TEAM = "cmq8kn0v500030xls1nvminjy" # team "registry"
COLL = "cmq8knppc00040xlskt4ist27" # colección registry_api (de hoppscotch_list/DB)
tok = hoppscotch_login("admin@example.com")["access_token"]
# 1. Variables del workspace (secreto resuelto desde pass, no hardcodeado)
hoppscotch_set_environment(TEAM, "registry", [
{"key": "baseURL", "value": "https://registry.organic-machine.com", "secret": False},
{"key": "api_key", "value": "pass:apis/registry", "secret": True}, # pass: -> pass_get_secret
], access_token=tok)
# 2. Crear una request → aparece EN VIVO en la GUI del humano (subscriptions)
hoppscotch_create_request(
COLL, "GET", "<<baseURL>>/api/status",
title="status", headers={"Accept": "application/json"},
team_id=TEAM, access_token=tok,
)
```
## Fronteras (qué NO cubre)
- **No es modo archivo**: no escribe colecciones `.json` locales como fuente. Las requests viven en el
Postgres del self-host. (Los `.json` en `collections/` son solo respaldo/semilla importable.)
- **No automatiza la GUI**: opera por la API; la GUI la mira el humano.
- **No gestiona usuarios/teams del dashboard**: eso es el admin dashboard (`:3100`).
- **No ejecuta los scripts pre/post-request JS** de Hoppscotch.
## Gotchas
- `access_token` como **cookie** (`cookies={"access_token": tok}`), no `Authorization`. 24h de vida.
- `createRequestInCollection` de esta instancia **exige `team_id`** en el input (no solo el collectionID).
- Variables `<<var>>` se resuelven con el environment de la team (subscriptions las propagan a la GUI).
- Secretos: usa `value="pass:<ruta>"` en `set_environment` → se resuelve de `pass`, nunca se hardcodea
ni se logea en crudo.
- El secreto viaja en claro al backend local por GraphQL — es local (`127.0.0.1`), aceptable.
+80
View File
@@ -0,0 +1,80 @@
# Capability: obsidian
CRUD headless de vaults y notas de Obsidian, tratadas como Markdown plano con frontmatter YAML y wikilinks `[[...]]`. NO depende de la app GUI de Obsidian ni de su URI scheme — manipula los archivos `.md` directamente en disco. Scriptable, rapido, con telemetria del registry.
Los vaults de Obsidian del usuario viven en `/home/enmanuel/Obsidian/` y estan enlazados como vaults del registry en el project `obsidian` (`projects/obsidian/vaults/`). Ver `projects/obsidian/project.md`.
## Funciones
| ID | Firma | Que hace |
|---|---|---|
| `parse_obsidian_frontmatter_py_obsidian` | `parse_obsidian_frontmatter(content: str) -> {"frontmatter": dict, "body": str}` | **Pure.** Separa el frontmatter YAML (bloque `---` inicial) del cuerpo. Si no hay frontmatter valido devuelve `{}` + el contenido completo. |
| `extract_obsidian_wikilinks_py_obsidian` | `extract_obsidian_wikilinks(body: str) -> list` | **Pure.** Extrae los targets de los wikilinks `[[...]]` y embeds `![[...]]`. Normaliza `[[nota\|alias]]`, `[[nota#heading]]`, `[[nota#^block]]` -> `nota`. Dedup preservando orden. |
| `format_obsidian_note_py_obsidian` | `format_obsidian_note(frontmatter: dict, body: str) -> str` | **Pure.** Inversa de parse: serializa frontmatter (YAML entre `---`) + body a una nota `.md` completa. |
| `read_obsidian_note_py_obsidian` | `read_obsidian_note(path: str) -> dict` | Lee una nota: `{path, frontmatter, body, wikilinks, tags}`. Compone parse + extract. |
| `create_obsidian_note_py_obsidian` | `create_obsidian_note(vault_dir, rel_path, body="", frontmatter=None, overwrite=False) -> str` | Crea nota nueva (crea dirs padre, añade `.md`). Error si existe y `overwrite=False`. |
| `update_obsidian_note_py_obsidian` | `update_obsidian_note(path, body=None, set_frontmatter=None, append=None) -> str` | Edita nota existente: merge de frontmatter, reemplazo de body, o append al final. |
| `delete_obsidian_note_py_obsidian` | `delete_obsidian_note(path: str) -> bool` | Borra una nota (solo archivo, nunca directorio). Error si no existe. |
| `list_obsidian_notes_py_obsidian` | `list_obsidian_notes(vault_dir, subfolder="", tag="") -> list` | Lista paths de notas `.md` (recursivo). Excluye `.obsidian/` y `.trash/`. Filtro opcional por tag de frontmatter. |
| `search_obsidian_notes_py_obsidian` | `search_obsidian_notes(vault_dir, query, in_body=True, in_frontmatter=True) -> list` | Busca substring (case-insensitive) en las notas. Devuelve `[{path, matches:[{line, text}]}]`. |
| `list_obsidian_vaults_py_obsidian` | `list_obsidian_vaults(base_dir: str) -> list` | Lista los vaults (subdirs con `.obsidian/`) bajo `base_dir`. `[{name, path}]`. |
| `create_obsidian_vault_py_obsidian` | `create_obsidian_vault(parent_dir, name) -> str` | Crea un vault nuevo: carpeta + `.obsidian/app.json` minimo. Error si ya existe. |
## Ejemplo canonico
Componer varias funciones del grupo se hace por heredoc importando del registry (las funciones se importan, no se reescriben):
```bash
cd /home/enmanuel/fn_registry
python/.venv/bin/python3 - <<'PYEOF'
import sys
sys.path.insert(0, "python/functions")
from obsidian import (
list_obsidian_vaults, list_obsidian_notes, search_obsidian_notes,
create_obsidian_note, read_obsidian_note, update_obsidian_note, delete_obsidian_note,
)
# 1. Descubrir vaults del usuario
vaults = list_obsidian_vaults("/home/enmanuel/Obsidian")
print("vaults:", [v["name"] for v in vaults])
# 2. Listar y buscar notas en un vault
finanzas = "/home/enmanuel/Obsidian/Finanzas"
print("notas:", len(list_obsidian_notes(finanzas)))
print("hits:", [h["path"] for h in search_obsidian_notes(finanzas, "presupuesto")][:5])
# 3. CRUD de una nota (crear -> leer -> editar -> borrar)
p = create_obsidian_note(finanzas, "inbox/idea_x", body="Primera linea",
frontmatter={"tags": ["inbox"], "created": "2026-06-09"})
note = read_obsidian_note(p)
print("creada:", note["path"], note["frontmatter"], note["wikilinks"])
update_obsidian_note(p, set_frontmatter={"status": "done"}, append="Ver [[Otra Nota]]")
delete_obsidian_note(p)
PYEOF
```
Para una sola operacion con un id conocido, `fn run` tambien sirve:
```bash
./fn run list_obsidian_vaults /home/enmanuel/Obsidian
./fn run list_obsidian_notes /home/enmanuel/Obsidian/Finanzas
```
## Cuando usar el grupo
- Crear/editar/leer notas de cualquier vault de Obsidian desde un agente o script, sin abrir la app.
- Buscar o listar notas por contenido o tag (ingesta, migracion, reporting sobre el vault).
- Crear vaults nuevos o inventariar los existentes.
## Fronteras (que NO cubre)
- **No habla con la app GUI** (no usa el URI scheme `obsidian://`, no abre notas en la interfaz, no dispara plugins). Si la app esta abierta, escribir en disco puede chocar con sus locks/cache — cerrar la app o refrescar manualmente.
- **No resuelve wikilinks a paths** automaticamente (devuelve los targets crudos). Resolver `[[nota]]` -> archivo real es responsabilidad del caller (busqueda por nombre en el vault).
- **No renderiza Markdown** ni evalua Dataview/templating. Trata las notas como texto + frontmatter.
- **No indexa el grafo** de enlaces entre notas (solo extrae links por nota). Para grafo agregado, componer sobre `list_obsidian_notes` + `extract_obsidian_wikilinks`.
## Gotchas
- Vaults grandes son caros: `NotasDeObsidian` pesa ~554M. `list_obsidian_notes` / `search_obsidian_notes` recorren todo el arbol — filtra por `subfolder` cuando puedas.
- `delete_obsidian_note` borra de verdad (no manda a `.trash/`). Para acciones destructivas masivas, listar primero y confirmar.
- El frontmatter `tags` puede venir como lista o como CSV string; `read_obsidian_note` lo normaliza a lista.
+51
View File
@@ -0,0 +1,51 @@
# Capability: osint-enrich
Orquestadores de enriquecimiento OSINT: componen las funciones atómicas de
[osint-passive](osint-passive.md) para aumentar los datapoints de una entidad (persona u
organización) del vault `osint` a partir de fuentes públicas. No tocan al objetivo de forma
intrusiva. Mismo encuadre dual-use que `osint-passive`: solo investigación autorizada.
## Funciones
| ID | Firma | Qué hace |
|---|---|---|
| `scan_ficha_attachments_metadata_py_cybersecurity` | `scan_ficha_attachments_metadata(attachments_dir) -> dict` | Escanea los attachments de una ficha (imágenes + PDFs), extrae EXIF/PDF metadata y agrega GPS y fechas. |
| `enrich_person_passive_py_cybersecurity` | `enrich_person_passive(nombre, apellidos, dominios=None, usernames=None) -> dict` | Candidatos para una persona: emails (guess), username hits, dorks. No verifica ni ejecuta. |
| `enrich_org_passive_py_cybersecurity` | `enrich_org_passive(dominio) -> dict` | Perfil pasivo de una org: whois + dns + subdominios. Resiliente a fallo parcial (campo `errors`). |
## Ejemplo canónico
```bash
cd /home/enmanuel/fn_registry
python/.venv/bin/python3 - <<'PYEOF'
import sys; sys.path.insert(0, "python/functions")
from cybersecurity import (scan_ficha_attachments_metadata,
enrich_person_passive, enrich_org_passive)
# 1. Metadatos de los documentos ya guardados de una persona (datos propios)
m = scan_ficha_attachments_metadata(
"/home/enmanuel/Obsidian/osint/attachments/personas/enmanuel-gutierrez-perez")
print(m["summary"]) # {n_files, n_images, n_pdfs, n_gps_points, n_dates, errors}
# 2. Candidatos de enriquecimiento de una persona (no toca al objetivo)
p = enrich_person_passive("Enmanuel", "Gutierrez Perez",
dominios=["gmail.com"], usernames=["enmanuelgp"])
print(p["email_candidates"][:5], len(p["dorks"]))
# 3. Perfil pasivo de una organización por su dominio
o = enrich_org_passive("organic-machine.com")
print(o["whois"].get("registrar"), o["dns"].get("A"), len(o["subdomains"]), o["errors"])
PYEOF
```
## Fronteras
- Compone solo funciones `osint-passive`. Para activa (port scan, fingerprint) haría falta
`osint-active` (no construido).
- Devuelve candidatos/datos crudos; **decidir qué escribir en la ficha** (y verificar) es del
caller. Encaja con el reporte de `projects/osint/tools/person_datapoints.py`.
## Gotchas
- `enrich_org_passive` nunca peta por una fuente lenta (crt.sh): el fallo va a `errors`.
- `enrich_person_passive` puede tardar por `enumerate_username_sites` (un request por sitio).
+64
View File
@@ -0,0 +1,64 @@
# Capability: osint-passive
Recolección OSINT **pasiva**: obtener información sin interactuar de forma intrusiva con el
objetivo, usando solo fuentes públicas (DNS público, RDAP, Certificate Transparency, metadatos
de documentos propios, servicios de perfil públicos). Pensado para investigación autorizada,
due diligence, pentest con permiso y enriquecimiento de las fichas del vault `osint`.
**Encuadre:** dual-use. Úsese solo contra objetivos propios o con autorización. Las funciones
que tocan servicios públicos (`enumerate_username_sites`, `enum_subdomains_crtsh`) dejan una
huella mínima (un request a cada servicio); respeta sus rate limits.
## Funciones
| ID | Firma | Qué hace |
|---|---|---|
| `extract_exif_metadata_py_cybersecurity` | `extract_exif_metadata(image_path) -> dict` | EXIF de una imagen (fecha, cámara, software, GPS decimal) vía Pillow. |
| `extract_pdf_metadata_py_cybersecurity` | `extract_pdf_metadata(pdf_path) -> dict` | Document Info de un PDF (autor, fechas, software, páginas) vía pypdf. |
| `guess_email_formats_py_cybersecurity` | `guess_email_formats(nombre, apellidos, dominio) -> list` | **Pure.** Candidatos de email comunes a partir de nombre + dominio. |
| `enumerate_username_sites_py_cybersecurity` | `enumerate_username_sites(username, ...) -> list` | ¿Existe un username en ~12 redes públicas? (sherlock ligero, por código HTTP). |
| `build_search_dorks_py_cybersecurity` | `build_search_dorks(target, tipo, ...) -> list` | **Pure.** Genera dorks de motor de búsqueda (persona/email/dominio/usuario). |
| `dns_records_py_cybersecurity` | `dns_records(dominio, types=None) -> dict` | Registros DNS (A/AAAA/MX/TXT/NS/CNAME) vía `dig`. |
| `whois_lookup_py_cybersecurity` | `whois_lookup(dominio, ...) -> dict` | Datos de registro vía RDAP (WHOIS moderno HTTP/JSON, sin CLI). |
| `enum_subdomains_crtsh_py_cybersecurity` | `enum_subdomains_crtsh(dominio, ...) -> list` | Subdominios desde Certificate Transparency (crt.sh). |
Orquestadores (grupo [osint-enrich](osint-enrich.md)): `scan_ficha_attachments_metadata`,
`enrich_person_passive`, `enrich_org_passive`.
## Ejemplo canónico
```bash
cd /home/enmanuel/fn_registry
python/.venv/bin/python3 - <<'PYEOF'
import sys; sys.path.insert(0, "python/functions")
from cybersecurity import (dns_records, whois_lookup, enum_subdomains_crtsh,
guess_email_formats, build_search_dorks, extract_exif_metadata)
# Dominio (org)
print(whois_lookup("organic-machine.com")["registrar"]) # OVH sas
print(dns_records("organic-machine.com")["A"]) # ['135.125.201.30']
print(enum_subdomains_crtsh("organic-machine.com")[:5])
# Persona
print(guess_email_formats("Enmanuel", "Gutierrez Perez", "gmail.com")[:5])
print(build_search_dorks("Enmanuel Gutierrez Perez", "persona")[:3])
# Metadatos de un documento propio
print(extract_exif_metadata("/home/enmanuel/Obsidian/osint/attachments/personas/enmanuel-gutierrez-perez/dni-1.jpg"))
PYEOF
```
## Fronteras (qué NO es)
- **No es recolección activa**: no hace port scan, dns brute, ni sondea la infra del objetivo.
Eso sería el grupo `osint-active` (no construido todavía).
- **No verifica** los candidatos: `guess_email_formats` propone, no confirma que el email exista.
- **No ejecuta** los dorks: `build_search_dorks` los genera; ejecutarlos es otro paso (browser).
- **No incluye breach/leak lookup** (HIBP requiere API key de pago) — pendiente.
## Gotchas
- `crt.sh` va lento / rate-limitado y a veces responde 404; los orquestadores lo capturan en
`errors` y siguen.
- `enumerate_username_sites` da falsos positivos/negativos por anti-bot de algunos sitios.
- El GPS de EXIF revela ubicación — dato sensible; trátese como PII.