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>
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 |
|
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+ binariosenrichers/<id>/run.exeson ~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.
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):
// 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:
- Borrar
run.py. - Actualizar
manifest.yaml:lang: go. - Actualizar
app.mdpython_runtime_depsquitando lo que ya no se use (si extract_text_entities ya no necesitarequests, fuera).
Cuando los 5 esten portados, decidir:
- Si
python_runtime: truesigue siendo util → mantener (custom enrichers, prototipado). - Si nadie escribe Python custom → marcar
python_runtime: falsey el.exedeja de embeber Python por completo. Re-habilitable con un solo flag.
Definicion de hecho
- 5 enrichers con
lang: goy 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.exedistribuido a Desktop sin runtime Python ejecuta el flujo OSINT completo (search → fetch → extract).python_runtime: truequeda 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: pythoncon 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.