Sistema de plugins de Ogma
Los plugins de Ogma amplían la herramienta con lógica de backend personalizada, paneles de interfaz de frontend y pasos de flujos de trabajo. Los plugins se instalan localmente desde un directorio del disco, se habilitan por proyecto y se ejecutan en un entorno con aislamiento de sandbox.
Este documento es la referencia principal para los autores de plugins.
Para comenzar lo más rápido posible, consulta el Inicio rápido de plugins.
Inicio rápido
Plugin mínimo de backend
my-plugin/
manifest.json
backend/script.jsmanifest.json:
json
{
"id": "my-plugin",
"name": "My Plugin",
"version": "1.0.0",
"plugins": [
{
"kind": "backend",
"id": "my-plugin-backend",
"entrypoint": "backend/script.js"
}
]
}backend/script.js (ES2020; se pueden utilizar importaciones si se usa un empaquetador):
js
async function init(sdk) {
sdk.console.log("my-plugin started");
sdk.events.onInterceptResponse(function(req, res) {
if (res.getCode() === 403) {
sdk.console.warn("403 on " + req.getUrl());
}
});
}Esto basta para tener un plugin solo de backend en funcionamiento. Mantenlo en un único archivo backend/script.js y haz que manifest.json apunte directamente a él.
Proceso de compilación en un minuto (código fuente TypeScript)
Si desarrollas en TypeScript, utiliza esta estructura:
text
my-plugin/
manifest.json
backend/
src/index.tsCompila:
bash
pnpm add -D @ogmabox/ogma-sdk esbuild typescript
pnpm exec esbuild backend/src/index.ts --bundle --format=iife --platform=neutral --external:@ogma/sdk --external:@ogmabox/ogma-sdk --outfile=backend/script.jsInstala: Plugins > Instalar, selecciona el directorio my-plugin/. Después habilítalo.
Referencia del manifiesto
manifest.json se encuentra en el directorio raíz del paquete. Todos los campos distinguen mayúsculas y minúsculas.
Campos de nivel superior
| Campo | Obligatorio | Tipo | Notas |
|---|---|---|---|
id | sí | string | Solo letras minúsculas, dígitos y guiones. Máximo de 64 caracteres. Único entre todos los plugins instalados. |
version | sí | string | Semver: MAJOR.MINOR.PATCH |
name | no | string | Nombre visible en la interfaz. El valor predeterminado es id. |
description | no | string | Resumen de una línea. |
author | no | object | { "name": "...", "email": "...", "url": "..." } |
homepage | no | string | URL del repositorio del código fuente o de la documentación. |
plugins | sí | array | Una o varias entradas de componentes de plugin (consulta más abajo). |
permissions | no | array | Lista de nombres de los permisos necesarios (consulta Permisos). |
Entrada de componente de plugin
Cada objeto del array plugins describe un componente.
Componente de backend:
json
{
"kind": "backend",
"id": "my-plugin-backend",
"entrypoint": "backend/script.js",
"runtime": "javascript",
"assets": "backend/assets"
}Componente de frontend:
json
{
"kind": "frontend",
"id": "my-plugin-frontend",
"entrypoint": "frontend/script.js",
"style": "frontend/style.css",
"assets": "frontend/assets",
"backend": { "id": "my-plugin-backend" }
}| Campo | Obligatorio | Notas |
|---|---|---|
kind | sí | "backend" o "frontend" |
id | sí | Único dentro del manifiesto. Minúsculas y guiones. |
entrypoint | sí | Ruta relativa al archivo de entrada JS. |
style | no | Archivo CSS cargado en el iframe del plugin. |
assets | no | Directorio de recursos estáticos que se sirven bajo /plugins/{id}/assets/. |
backend.id | no | Vincula un componente de frontend con su componente de backend para RPC de sdk.backend.*. |
runtime | no (solo backend) | "javascript" (valor predeterminado y único compatible). |
API de plugins de backend (sdk)
El objeto sdk del backend se pasa a la función init(sdk). Todos los métodos son síncronos salvo los marcados como async.
sdk.console
js
sdk.console.log("message")
sdk.console.warn("message")
sdk.console.error("message")Escribe en el búfer de registros del plugin (visible en la pestaña Registros). Se conservan como máximo 500 entradas. Cada mensaje se trunca a 1 KB.
sdk.meta
js
sdk.meta.id() // > string: plugin id (e.g. "my-plugin")
sdk.meta.packageId() // > string: same as id
sdk.meta.version() // > string: semver (e.g. "1.0.0")
sdk.meta.path() // > string: writable data directory for this pluginsdk.meta.path() apunta al directorio privado de datos del plugin con permiso de escritura, por ejemplo, ~/.local/share/ogma/plugins/my-plugin/data. El directorio se crea automáticamente y permite conservar los datos del plugin entre reinicios.
También están disponibles sdk.storage, sdk.path y sdk.fs como utilidades de estado y archivos.
sdk.storage
js
sdk.storage.get("key") // > string | null
sdk.storage.set("key", "value")
sdk.storage.delete("key")
sdk.storage.clear()
sdk.storage.keys() // > string[]sdk.storage está limitado al ID del plugin y persiste entre reinicios del plugin.
sdk.fs
js
sdk.fs.readFile("relative/file.txt") // > string
sdk.fs.writeFile("relative/file.txt", "text")
sdk.fs.appendFile("relative/file.txt", "more")
sdk.fs.exists("relative/file.txt") // > boolean
sdk.fs.existsSync("relative/file.txt") // > boolean
sdk.fs.list("relative/dir") // > string[]
sdk.fs.mkdir("relative/dir")sdk.fs está limitado a los archivos bajo sdk.meta.path().
read y write siguen siendo alias de compatibilidad de readFile y writeFile. Utiliza exists o existsSync antes de crear un archivo que no quieras sobrescribir. Estas API funcionan de forma síncrona en el entorno de ejecución del plugin; no constituyen el módulo fs completo de Node.js. El acceso a archivos del plugin requiere el permiso plugin_storage y se limita a su directorio privado de datos. El JavaScript de los flujos de trabajo tiene un contexto de sistema de archivos distinto; consulta Acceso a archivos de los flujos de trabajo.
sdk.path
js
sdk.path.join("a", "b", "c")
sdk.path.basename("/tmp/file.txt")
sdk.path.dirname("/tmp/file.txt")
sdk.path.extname("file.txt")
sdk.path.resolve("/a", "b")
sdk.path.isAbsolute("/tmp/file.txt")
sdk.path.sepsdk.events
Registra callbacks para eventos de Ogma. Todos los callbacks se invocan de forma síncrona dentro del sandbox de QuickJS.
js
sdk.events.onInterceptRequest(function(req) {
// req: RequestSpecRaw
// Return a modified RequestSpecRaw to mutate the request.
// Return null/undefined to pass through unchanged.
});
sdk.events.onInterceptResponse(function(req, res) {
// req: Request (read-only), res: Response (read-only)
// Return value is ignored.
});
sdk.events.onProjectChange(function() {
// no callback args
});
sdk.events.onFindingCreated(function(finding) {
// finding: { id, title, reporter }
});sdk.requests
js
// Get a single HTTP entry by id
var entry = sdk.requests.get("entry-id");
// entry: { id, method, host, path, query, tls, ... } or null
// Search HTTP history
var results = sdk.requests.search({ limit: 20, offset: 0 });
// results: { entries: [...], total: N }
// Send an HTTP request (requires send_requests permission)
var response = await sdk.requests.send(spec);
// spec: RequestSpecRaw (see below)
// response: Responsesdk.requests.send requiere que el permiso send_requests esté declarado en el manifiesto y lo haya concedido el usuario. Consulta Permisos.
sdk.findings
js
// Create a finding (requires write_findings permission)
sdk.findings.create({
title: "SSRF via redirect",
reporter: "my-plugin",
dedupeKey: "ssrf-" + request.getId(),
request: { id: request.getId() }
});
// Check if a finding already exists (dedup check)
var exists = sdk.findings.exists({ dedupeKey: "ssrf-abc" });
// List findings
var page = sdk.findings.list({ limit: 20, offset: 0 });
// Get a single finding
var finding = sdk.findings.get("finding-id");Límites de frecuencia de sdk.findings.create: 10 por minuto, 500 por sesión de plugin y 3 por callback de evento.
sdk.api
Registra funciones RPC del backend que el frontend pueda invocar mediante sdk.backend.*:
js
// In backend init:
sdk.api.register("getScans", function(scanId) {
return { scans: [] };
});
// Emit an event to connected frontends:
sdk.api.send("scan:complete", { scanId: 1, status: "ok" });El manejador recibe los argumentos enviados desde el frontend (no se inyecta ningún argumento sdk adicional). Los valores devueltos se serializan como JSON y se envían al llamante.
El frontend los invoca mediante sdk.backend.getScans(scanId); consulta API de plugins de frontend.
sdk.api.send inserta eventos en una cola por plugin (máximo de 200 entradas). Los frontends consultan periódicamente esta cola mediante sdk.backend.onEvent.
sdk.replay, sdk.projects, sdk.scope, sdk.workflows, sdk.matchReplace
Estos son espacios de nombres de consultas de solo lectura. Consulta la referencia del SDK de backend para ver las firmas completas de los métodos.
Clases de solicitudes
RequestSpecRaw representa una solicitud interceptada. Se recibe en onInterceptRequest.
js
spec.getMethod() // > string
spec.setMethod("POST")
spec.getHost() // > string
spec.setHost("example.com")
spec.getPort() // > number
spec.getPath() // > string
spec.setPath("/new/path")
spec.getQuery() // > string
spec.getTls() // > boolean
spec.getHeaders() // > Record<string, string[]>
spec.setHeader("X-Foo", "bar")
spec.getBody() // > Body | null
spec.setBody("new body")
spec.getRaw() // > Uint8Array (raw bytes) or []
spec.setRaw(bytes) // set raw bytes
// Create a new spec from a URL string:
var spec = new RequestSpecRaw("https://example.com/path?q=1");Request es una solicitud capturada de solo lectura (de sdk.requests.get).
js
req.getId()
req.getMethod()
req.getHost()
req.getPort()
req.getTls()
req.getPath()
req.getQuery()
req.getUrl() // > full URL string
req.getHeaders() // > Record<string, string>
req.getHeader("name")
req.getBody() // > Body | null
req.getCreatedAt() // > Date
req.toSpec() // > RequestSpec (mutable copy)Response es una respuesta capturada de solo lectura.
js
res.getCode() // > number (HTTP status)
res.getHeaders() // > Record<string, string>
res.getHeader("name")
res.getBody() // > Body | null
res.getRoundtripTime() // > number (ms)
res.getCreatedAt() // > DateBody:
js
body.toText() // > string
body.toJson() // > parsed object or null
body.toRaw() // > Uint8Array
body.length // > number (original size, may differ from toText() if truncated)API de plugins de frontend (sdk)
El código de frontend del plugin se ejecuta en un iframe con aislamiento de sandbox cargado desde /plugins/{id}/ui. El iframe utiliza postMessage para comunicarse con la aplicación principal de Ogma, que actúa como intermediaria de las llamadas al backend.
El SDK está disponible mediante window.ogmaSDK. Llama a ogmaSDK.ready(cb) para recibir el SDK activo una vez establecido el puente con la aplicación principal:
js
ogmaSDK.ready(function(sdk) {
// sdk is the live SDK - safe to call any method here
sdk.log.info("frontend ready");
});Todos los métodos del SDK devuelven promesas.
sdk.log
js
sdk.log.info("message")
sdk.log.warn("message")
sdk.log.error("message")sdk.meta
js
var meta = await sdk.meta.get();
// { pluginId, packageId, name, version, ogmaVersion }sdk.requests
js
var entry = await sdk.requests.get({ id: "entry-id" });
var result = await sdk.requests.search({ limit: 20, offset: 0, query: "host:example.com" });Requiere el permiso read_http_history (concedido automáticamente; no necesita aprobación del usuario).
sdk.findings
js
var page = await sdk.findings.list({ limit: 20, offset: 0 });Requiere el permiso read_findings (concedido automáticamente).
sdk.scope
js
var scope = await sdk.scope.getActive();sdk.projects
js
var project = await sdk.projects.getCurrent();sdk.backend - RPC de backend
Llama a las funciones registradas con sdk.api.register en el backend:
js
// Call a named backend function
var result = await sdk.backend.call("getScans", [scanId]);
// Poll for backend-emitted events (sdk.api.send on the backend side)
var { events, next_since } = await sdk.backend.poll(since);
// events: [{ event: "scan:complete", args: [...] }]
// Register an event listener (uses polling internally)
sdk.backend.onEvent("scan:complete", function(data) {
console.log("scan done", data);
});sdk.backend.onEvent utiliza internamente un bucle de consulta cada 2 segundos. Deja de escuchar llamando a la función de cancelación de suscripción devuelta:
js
var unsub = sdk.backend.onEvent("scan:complete", handler);
// later:
unsub();sdk.navigation
js
await sdk.navigation.addPage("/my-plugin", { title: "My Plugin" });Registra una página de navegación. Actualmente la aplicación principal confirma su recepción. La integración completa con el enrutador está en desarrollo.
sdk.sidebar
js
await sdk.sidebar.registerItem("My Plugin", "/my-plugin", { icon: "puzzle" });Registra una entrada en la barra lateral. Actualmente es local al panel de interfaz del plugin; la conexión con los espacios de la barra lateral global está en desarrollo.
sdk.commands
js
await sdk.commands.register("my-plugin:scan", {
name: "Scan with My Plugin",
handler: function(context) { /* ... */ }
});La aplicación principal confirma la recepción. La integración con la paleta de comandos está en desarrollo.
sdk.menu
js
await sdk.menu.registerItem({
type: "Request",
commandId: "my-plugin:scan",
leadingIcon: "shield"
});La aplicación principal confirma la recepción. La inserción en el menú contextual está en desarrollo.
sdk.window
js
sdk.window.showToast("Scan complete", { variant: "success", duration: 3000 });Muestra una notificación emergente en el panel del plugin. Variantes: info, success, warning, error.
sdk.ui
js
sdk.ui.resize(600); // request iframe height change
sdk.ui.sidebar.registerItem("name", "/path"); // alias for sdk.sidebar.registerItemPermisos
Declara los permisos en manifest.json:
json
{
"permissions": ["send_requests", "write_findings"]
}Permisos concedidos automáticamente (sin aprobación del usuario)
Estos permisos siempre se conceden a cualquier plugin instalado:
| Permiso | Qué permite |
|---|---|
read_http_history | sdk.requests.get, sdk.requests.search |
read_findings | sdk.findings.get, sdk.findings.list |
read_scope | sdk.scope.getActive |
read_projects | sdk.projects.getCurrent, sdk.projects.list |
plugin_storage | sdk.storage, sdk.fs, sdk.path |
Permisos protegidos (requieren aprobación del usuario)
Deben declararse en el manifiesto y el usuario debe concederlos explícitamente desde la pestaña Permisos:
| Permiso | Qué permite |
|---|---|
send_requests | sdk.requests.send - enviar solicitudes HTTP salientes |
write_findings | sdk.findings.create, sdk.findings.update |
El usuario ve una solicitud de autorización al habilitar un plugin que declara permisos protegidos. También puede conceder o revocar permisos en cualquier momento desde la pestaña Permisos.
Estructura del paquete de plugin
Un paquete de plugin se puede instalar desde un directorio local y, en los flujos del navegador, también desde exportaciones .zip.
my-plugin/
manifest.json - obligatorio
backend/
script.js - JS de backend empaquetado (ES2020)
frontend/
script.js - JS de frontend empaquetado
style.css - CSS opcional
assets/ - recursos estáticos (imágenes, fuentes, etc.)Requisitos del script de backend
- Debe ser un único archivo JS autónomo.
- No se admiten
require()niimport()dinámico. - Debe exportar una función
init(sdk)(o definirla como global). - Subconjunto de ES2020 compatible con QuickJS:
async/await,Promise,Map,Set,Symbol,Proxy,Date,RegExp,JSON. SinfetchniBuffer. - El preprocesamiento del plugin resuelve las importaciones estáticas compatibles:
@ogma/sdk,crypto,fs,path. - Tamaño máximo del archivo: 256 KB.
Requisitos del script de frontend
- Se ejecuta dentro de un iframe con aislamiento de sandbox. Se permite
connect-src: 'self'para que el plugin pueda enviar POST a/plugins/{id}/api/*y consultar periódicamente/plugins/{id}/events/poll. - CSP:
default-src 'none'; script-src 'nonce-...'; style-src 'self'; img-src data: blob: 'self'; connect-src 'self'. - Sin
allow-same-originen el sandbox del iframe: el plugin no puede acceder al DOM de la página principal de Ogma ni a sus cookies. - Utiliza
ogmaSDK.ready(cb)para acceder al SDK; no llames a sus métodos antes de que se ejecute el callback.
Compilación de un plugin para Ogma
Como el backend debe ser un único archivo JS empaquetado, debes empaquetar el código fuente de TypeScript o módulos ES antes de instalarlo.
Cadena de herramientas recomendada:
bash
# Install dependencies
pnpm install
# Bundle backend (outputs a single CJS/IIFE file):
esbuild packages/backend/src/index.ts \
--bundle \
--platform=neutral \
--format=iife \
--global-name=_plugin \
--outfile=dist/backend/script.js \
--external:@ogma/sdk --external:@ogmabox/ogma-sdk
# Bundle frontend:
vite build packages/frontend --outDir ../../dist/frontendSi utilizas la cadena de herramientas de desarrollo de Caido (@caido-community/dev), ejecuta caido-dev build y después copia la salida a una estructura compatible con Ogma con manifest.json en la raíz.
Instalación de un plugin
- Abre Plugins en la barra lateral izquierda.
- Haz clic en Instalar (en la parte superior de la pestaña Instalados).
- En la aplicación de escritorio, haz clic en Explorar para abrir un selector de carpetas nativo. En el navegador, escribe la ruta completa del directorio del plugin en el servidor.
- Haz clic en Validar para comprobar el manifiesto y el inventario de archivos.
- Haz clic en Instalar si la validación es correcta.
- Selecciona el plugin en la lista y haz clic en Activar.
- Si el plugin declara permisos protegidos, revísalos y concédelos desde la pestaña Permisos antes de habilitarlo.
Resolución de problemas
La inicialización del plugin falla sin mostrar errores: Revisa la pestaña Registros. Las causas más frecuentes son:
- Se llamó a
sdk.meta.path(), pero no se pudo crear el directorio de datos. - Una excepción no controlada en
init(). - Una llamada
sdk.*inexistente o mal escrita.
El frontend aparece en blanco: Revisa la consola del navegador para detectar infracciones de CSP. Asegúrate de que el script de frontend llama a ogmaSDK.ready(cb) antes de acceder a cualquier método del SDK.
sdk.requests.send lanza Permission denied: El permiso send_requests debe declararse en el manifiesto Y el usuario debe concederlo en la pestaña Permisos.
No se pueden invocar las funciones de sdk.api.register desde el frontend: El backend debe estar habilitado (no basta con instalarlo). El nombre de la función debe coincidir exactamente, distinguiendo mayúsculas y minúsculas, con el que el frontend pasa a sdk.backend.call.
Las advertencias de compatibilidad aparecen como errores: No bloquean el funcionamiento, pero indican carencias en la interfaz de la API. Consulta las tablas de correspondencia del SDK anteriores para conocer la cobertura de la API.