--- id: 0034 title: Port de los 5 enrichers de sistema a Go status: pending priority: medium created: 2026-05-02 depends_on: [0033] --- ## Contexto Tras 0033 el dispatcher acepta `lang: go` y los binarios se distribuyen junto al `.exe` Windows. Los 5 enrichers de sistema (`extract_domain`, `fetch_webpage`, `extract_links`, `extract_text_entities`, `web_search`) son hoy Python — funcionan, estan testeados y son el flujo OSINT canonico de la app. Portarlos a Go nos da: - Cero dependencia de Python para el flujo base. El runtime embebido pasa a ser **opcional** (solo apps con enrichers custom Python lo necesitan). - ~10x menos arranque por job (Python cold ~120 ms, Go ~10 ms). - Reuso directo de funciones Go testeadas del registry (extract_iocs, extract_urls, normalize_url, html_to_markdown). Esto cierra el bucle del registry: lo que se testea en Go se ejecuta en Go. - Distribucion: el `.exe` + binarios `enrichers//run.exe` son ~25 MB totales; sin runtime Python si no hace falta. Los tests pytest existentes **no cambian** — testean el wire protocol (stdin JSON / stdout JSON / exit code), no la implementacion. Cada port intercambia el binario pero deja el contrato intacto. ## Funciones del registry necesarias Los 5 enrichers comparten una decena de funciones del dominio `cybersecurity` y `core`. Verificar que existen en Go con `fn search` antes de portar; si falta alguna, anadirla al registry primero. | Funcion Python actual | Equivalente Go que hace falta | Estado | |---|---|---| | `cybersecurity.normalize_url` | `normalize_url_go_cybersecurity` | revisar | | `core.html_to_markdown` | `html_to_markdown_go_core` | crear si no esta | | `cybersecurity.extract_urls` | `extract_urls_go_cybersecurity` | revisar | | `cybersecurity.extract_iocs` | `extract_iocs_go_cybersecurity` | crear si no esta | `fn check params` + `fn list -d cybersecurity --lang go` muestra que hay y que falta. Las que falten se crean primero (issue separada por funcion si la complejidad lo justifica, ej. `html_to_markdown` necesita un parser HTML — `golang.org/x/net/html` resuelve). ## Layout por enricher ``` enrichers// manifest.yaml # actualizado: lang: go main.go # entry point con main() go.mod # dependencias propias run # binario Linux (gitignored, generado) run.exe # binario Windows (gitignored, generado) build.sh # cross-compile recipe run.py # ELIMINADO tras port + tests verde ``` Ejemplo de `build.sh`: ```bash #!/usr/bin/env bash set -euo pipefail cd "$(dirname "$0")" GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w" -o run . GOOS=windows GOARCH=amd64 go build -trimpath -ldflags="-s -w" -o run.exe . ``` Hook al build de la app: `cpp/CMakeLists.txt` (o `compile.sh`) corre `build.sh` de cada enricher Go antes de copiar la app a Desktop. Idem en linux build. ## Estructura del `main.go` (template) ```go package main import ( "encoding/json" "fmt" "io" "os" // Funciones del registry — paths relativos al repo root. "fn_registry/cybersecurity/extractiocs" "fn_registry/cybersecurity/extracturls" ) type ctxIn struct { NodeID string `json:"node_id"` NodeName string `json:"node_name"` NodeType string `json:"node_type"` Metadata map[string]any `json:"metadata"` OpsDBPath string `json:"ops_db_path"` AppDir string `json:"app_dir"` CacheDir string `json:"cache_dir"` RegistryRoot string `json:"registry_root"` Params map[string]any `json:"params"` } type ctxOut struct { EntitiesAdded int `json:"entities_added"` RelationsAdded int `json:"relations_added"` Error string `json:"error,omitempty"` // ... campos especificos del enricher } func progress(p float64, stage string) { fmt.Fprintf(os.Stderr, "PROGRESS:%.2f %s\n", p, stage) } func main() { raw, _ := io.ReadAll(os.Stdin) var in ctxIn if err := json.Unmarshal(raw, &in); err != nil { fmt.Fprintln(os.Stderr, "stdin not valid JSON:", err) os.Exit(2) } out, code := run(in) enc, _ := json.Marshal(out) fmt.Println(string(enc)) os.Exit(code) } func run(in ctxIn) (ctxOut, int) { // Logica especifica del enricher, devolviendo (resumen, exit code). } ``` ## Plan de port (orden y por que) ### Fase 1 — `extract_domain` (1 sesion) El mas simple: regex puro, sin red, sin parser HTML. Sirve de **referencia canonica** para los demas. Confirma toda la cadena de build + dispatcher + tests pytest sin tocar dependencias externas. ### Fase 2 — `web_search` (1-2 sesiones) Recien escrito en Python, todavia caliente. Portar ahora minimiza el trabajo de mantenerlo en dos sitios. Dependencias: `net/http` (stdlib), parser HTML con `golang.org/x/net/html`. ### Fase 3 — `extract_links` (1 sesion) Lee markdown del cache, extract_urls. Si `extract_urls_go_cybersecurity` existe, port directo; si no, escribir la funcion antes (regex de URLs). ### Fase 4 — `extract_text_entities` (1-2 sesiones) Si `extract_iocs_go_cybersecurity` no existe, crear primero como funcion del registry — es el plato fuerte del port. ### Fase 5 — `fetch_webpage` (2 sesiones) El mas complejo: HTTP con redirects, decode de encoding, parse HTML a markdown, escritura de cache. Lo dejamos para el final cuando ya tenemos las primitivas Go probadas. ## Tests **Cero cambios necesarios** en los tests pytest si el dispatcher detecta correctamente el binario Go. El stub `tests/_stubs/requests.py` deja de aplicar para los enrichers Go — en su lugar se inyecta un servidor HTTP local con `httptest.Server` desde un test Go nativo **adicional** (no sustituto): ```go // enrichers/web_search/main_test.go func TestWebSearchParsesDDGFixture(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w, r) { body, _ := os.ReadFile("../../tests/fixtures/ddg_results.html") w.Write(body) })) defer srv.Close() // ... ctx con DDGEndpoint sobreescrito a srv.URL ... } ``` Los tests pytest se mantienen como **integration tests del wire protocol** y los tests Go cubren la logica unitaria. Total post-port: - 16 tests pytest (sin cambios) — wire protocol. - ~20 tests Go nuevos — logica de cada enricher. ## Limpieza tras port Por enricher, una vez los 16+N tests estan verde: 1. Borrar `run.py`. 2. Actualizar `manifest.yaml`: `lang: go`. 3. Actualizar `app.md` `python_runtime_deps` quitando lo que ya no se use (si extract_text_entities ya no necesita `requests`, fuera). Cuando los 5 esten portados, decidir: - Si `python_runtime: true` sigue siendo util → mantener (custom enrichers, prototipado). - Si nadie escribe Python custom → marcar `python_runtime: false` y el `.exe` deja de embeber Python por completo. Re-habilitable con un solo flag. ## Definicion de hecho - 5 enrichers con `lang: go` y binarios precompilados para Linux + Windows en el build pipeline. - 16 tests pytest pasan contra los binarios Go (mismo wire protocol). - Tests Go nativos cubren parsing/regex/IO de cada enricher. - `graph_explorer.exe` distribuido a Desktop sin runtime Python ejecuta el flujo OSINT completo (search → fetch → extract). - `python_runtime: true` queda como flag opcional, no obligatorio. ## Riesgos y mitigaciones | Riesgo | Mitigacion | |---|---| | Falta `extract_iocs` o `html_to_markdown` en Go | Issue dependiente que las crea primero. Marcar este 0034 como bloqueado hasta que existan. | | Diferencia de comportamiento Python vs Go (regex, normalizacion HTML) | Tests pytest comparten fixtures de input — si Go produce salida distinta a Python, falla. Iteramos hasta paridad. | | Cross-compile de Go con cgo | Los enrichers no necesitan cgo. Build estatico simple `CGO_ENABLED=0`. | | Mantener dos implementaciones durante el port | NO se mantienen dos. Cada enricher se porta en una rama corta, tests verde, merge, eliminacion del `run.py`. Nada de toggles. | | Echo escribiendo enrichers nuevos durante el port | Echo escribe Python custom — eso vive feliz junto a los Go portados (gracias al dispatcher de 0033). Sin conflicto. | ## Fuera de alcance - Port de enrichers de fase 2 (browser session, screenshot, login). Esos viven en `lang: python` con Playwright porque la libreria Go equivalente (`chromedp`) duplica esfuerzo sin ganancia clara. Justamente el caso de uso ideal del runtime embebido. - Reescribir el sistema de jobs en Go (issue futura si el panel C++ se queda corto). - Ports en otros lenguajes (Rust, Zig) — no aporta ahora.