HexProxy Plugin API v2

Extiende HexProxy con workspaces, hooks, exporters, analyzers y settings propios.

El sistema de plugins de HexProxy está pensado para equipos que necesitan adaptar el proxy a su operación real. Los plugins son módulos Python cargados dentro del proceso y pueden observar, transformar, enriquecer y exportar tráfico, además de inyectar componentes visuales y acciones dentro de la TUI.

Trusted Python modules register(api) / register() / PLUGIN / contribute(api) HookContext y PluginRenderContext Metadata persistente por flow

Arranque recomendado

HexProxy carga plugins Python desde la carpeta plugins/ y desde cada directorio adicional pasado con --plugin-dir. La recomendación práctica es comenzar con un plugin pequeño, validar que el runtime lo carga y recién después sumar panels, exporters o analyzers más complejos.

pip install hexproxy
hexproxy --listen-port 8080 --plugin-dir plugins
Los plugins son módulos Python de confianza cargados in-process. No hay sandbox, unload ni hot reload.

Loading model y entrypoints soportados

HexProxy recorre archivos *.py, ignora los que empiezan con guion bajo y acepta cuatro formas de entrada: register(api), register(), PLUGIN y contribute(api). Si un módulo expone register(api) o register() y devuelve None, el runtime conserva el propio módulo como instancia del plugin. Además, si existen tanto contribute(api) a nivel módulo como en la instancia, ambas contribuciones pueden ejecutarse.

def register(api):
    return MyPlugin()
def contribute(api):
    api.add_workspace(
        "demo_workspace",
        "Demo",
        "Workspace del módulo"
    )

Clases y contextos principales

La API v2 gira en torno a dos contextos. HookContext vive dentro del pipeline de tráfico y permite etiquetar, persistir metadata, anexar findings y acceder a estado global o de proyecto. PluginRenderContext vive en renderers, exporters, analyzers, metadata providers, keybindings y callbacks de settings, y ofrece acceso al entry seleccionado, request, response, store, TUI y helpers de estado.

ClaseCampos establesHelpers relevantes
HookContext entry_id, client_addr, store, tags, metadata, findings set_metadata, add_finding, global_state, set_global_value, project_state, set_project_value
PluginRenderContext plugin_id, plugin_manager, store, workspace_id, panel_id set_status, open_workspace, global_state, set_global_value, project_state, set_project_value, theme_manager
context.set_metadata("jwt_inspector", "summary", json.dumps(summary))
context.add_finding("jwt_inspector", "JWT observado en request")
def render_panel(context):
    entry = context.entry
    if entry is None:
        return ["No flow selected."]
    return [f"id={entry.id}"]

Hooks del runtime

Los hooks opcionales del plugin son on_loaded(), before_request_forward(...), on_response_received(...) y on_error(...). El hook de request corre después de la interceptación y antes del Match/Replace de request. El hook de response corre después de la respuesta upstream y antes del Match/Replace de response. Si necesitas alterar la respuesta, debes mutar el objeto en sitio; devolver otro response no tiene efecto contractual en el runtime actual.

HookCuándo correRetornoUso típico
on_loaded()Al terminar la carga del pluginIgnoradoInicialización simple
before_request_forward(context, request)Antes del upstream requestParsedRequest o NoneModificar headers, body o tags
on_response_received(context, request, response)Después de recibir la respuestaIgnoradoPersistir metadata, findings o mutar response in-place
on_error(context, error)Cuando HexProxy captura una excepción del flowIgnoradoDiagnóstico y findings
class AddHeaderPlugin:
    plugin_id = "add_header"

    def before_request_forward(self, context, request):
        request.headers.append(("X-HexProxy-Plugin", "active"))
        return request

    def on_response_received(self, context, request, response):
        context.set_metadata(self.plugin_id, "status", response.status_code)

Funciones de PluginAPI

Cuando el plugin usa register(api), recibe una instancia de PluginAPI. Desde ahí puede registrar workspaces, panels, exporters, keybindings, analyzers, metadata providers y campos dentro de Settings. En la práctica, esa es la superficie principal para extender la UI y el flujo operativo.

add_workspace(workspace_id, label, description="", order=100, shortcut="")

Crea un workspace superior nuevo. El workspace_id debe ser único y no puede colisionar con workspaces built-in como overview, intercept, repeater, http o settings.

add_panel(workspace_id, panel_id, title, description="", order=100, render_lines=...)

Registra panels dentro de workspaces propios o sobre targets internos como overview_detail, http_request, http_response, sitemap_request, sitemap_response, repeater_request y repeater_response.

add_exporter(exporter_id, label, description, render=..., order=100, style_kind=None)

Suma formatos al workspace Export. El renderer devuelve un string y puede usar style_kind como http, python, shell, javascript, php, go o rust.

add_keybinding(action, key, description, handler=..., section="Plugin Actions")

Registra acciones configurables por teclado. Las keys deben tener uno o dos caracteres visibles. El runtime rechaza whitespace, duplicados y prefijos ambiguos como d coexistiendo con dw.

add_analyzer(...) y add_metadata(...)

Permiten producir findings y colecciones estructuradas visibles dentro de la TUI. Son ideales para plugins que inspeccionan JWT, headers de seguridad, tokens o patrones repetitivos.

add_setting_field(field_id, section, label, description, kind=..., scope=...)

Agrega campos a Settings. Los kind soportados son toggle, choice, text y action. El scope puede ser global o project.

Código de ejemplo visible en la página

Aquí tienes ejemplos completos y listos para copiar. El primero muestra un plugin mínimo que crea un workspace y un panel. El segundo muestra una inspección básica que persiste metadata JSON-safe y findings por flow.

class DemoPlugin:
    plugin_id = "demo"
    name = "demo-plugin"


def register(api):
    api.add_workspace(
        "demo_workspace",
        "Demo Workspace",
        "plugin workspace",
        shortcut="dw",
    )
    api.add_panel(
        "demo_workspace",
        "demo_panel",
        "Demo Panel",
        render_lines=lambda context: ["hello", context.plugin_id],
    )
    return DemoPlugin()
from __future__ import annotations
import json

class ExampleInspector:
    plugin_id = "example_inspector"

    def before_request_forward(self, context, request):
        summary = {
            "method": request.method,
            "target": request.target,
        }
        context.set_metadata(
            self.plugin_id,
            "summary",
            json.dumps(summary),
        )
        context.add_finding(
            self.plugin_id,
            f"Observed request to {request.target}",
        )
        return request

    def on_response_received(self, context, request, response):
        context.set_metadata(self.plugin_id, "status", response.status_code)


def register(api):
    api.add_exporter(
        "example_json",
        "Example Inspector JSON",
        "Exporta metadata persistida como JSON",
        render=lambda context: json.dumps(
            context.entry.plugin_metadata.get("example_inspector", {}),
            indent=2,
            ensure_ascii=False,
        ),
        style_kind="javascript",
    )
    return ExampleInspector()
def render_export(context):
    entry = context.entry
    if entry is None:
        export_source = getattr(context, "export_source", None)
        entry_id = getattr(export_source, "entry_id", None)
        if entry_id is not None:
            entry = context.store.get(entry_id)
    if entry is None:
        return "No flow available."
    bucket = entry.plugin_metadata.get("example_inspector", {})
    return json.dumps(bucket, indent=2, ensure_ascii=False)

Pitfalls y comportamiento real del runtime

La trampa principal del sistema actual es que HookContext.metadata persiste todo como string. Si guardas dicts o listas sin serializarlos, luego los panels fallarán al intentar tratarlos como estructuras reales. El patrón seguro es json.dumps(...) al escribir y json.loads(...) al leer. También conviene tratar export_source como un objeto dinámico y usar getattr(...) de forma defensiva.

Escritura segura de metadata

context.set_metadata(
    self.plugin_id,
    "details",
    json.dumps(details),
)

Lectura segura de metadata

bucket = entry.plugin_metadata.get("my_plugin", {})
details = json.loads(bucket.get("details", "[]"))
ProblemaCausa frecuenteSolución
Plugin render error: 'str' object has no attribute 'get'Metadata estructurada guardada sin JSONSerializa con json.dumps y lee con json.loads
Exporter vacío o rotoSe asumió context.export_source.entryPrefiere context.entry y resuelve entry_id de forma defensiva
Panel registrado pero invisibleSe apuntó a un target built-in que el TUI no renderizaUsa targets documentados y valida el workspace real
Keybinding rechazadoSecuencia duplicada o prefijo ambiguoElige combinaciones únicas de uno o dos caracteres