Files
graph_explorer/issues/0034-port-system-enrichers-to-go.md
egutierrez 35ace544d9 docs(issues): roadmap fase 2 navegador + ports Go + runtime embebido
Anade siete issues que definen el camino para hacer graph_explorer
distribuible como binario Windows autocontenido (sin WSL):

- 0032 — browser_session enrichers via Playwright (login interactivo,
  cookies persistentes, fetch_webpage_browser, web_search_browser).
- 0033 — dispatcher multi-lenguaje (lang: go|python|bash en manifest)
  + runtime Python embebido en <app>/runtime/. 3 fases (A=dispatcher,
  B=runtime, C=UI badges).
- 0033b — vendoring de funciones Python por enricher (_vendored/ +
  .vendor.lock) para que los enrichers no dependan de registry_root
  en runtime.
- 0033c — fn check vendored: drift detection con --fix.
- 0033d — fn index lee python_runtime / python_runtime_deps de app.md.
- 0033e — /compile orquesta freeze + vendor + go builds.
- 0034 — port de los 5 enrichers de sistema a Go. Reusa funciones
  Go del registry directamente (no copias). Tests pytest existentes
  pasan sin cambios.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 16:10:35 +02:00

237 lines
8.6 KiB
Markdown

---
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/<id>/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/<id>/
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.