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

8.6 KiB

id, title, status, priority, created, depends_on
id title status priority created depends_on
0034 Port de los 5 enrichers de sistema a Go pending medium 2026-05-02
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:

#!/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)

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.

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):

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