35ace544d9
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>
237 lines
8.6 KiB
Markdown
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.
|