Ir al contenido

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

manifest.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.ts

Compila:

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

Instala: 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 ​

CampoObligatorioTipoNotas
idsístringSolo letras minúsculas, dígitos y guiones. Máximo de 64 caracteres. Único entre todos los plugins instalados.
versionsístringSemver: MAJOR.MINOR.PATCH
namenostringNombre visible en la interfaz. El valor predeterminado es id.
descriptionnostringResumen de una línea.
authornoobject{ "name": "...", "email": "...", "url": "..." }
homepagenostringURL del repositorio del código fuente o de la documentación.
pluginssíarrayUna o varias entradas de componentes de plugin (consulta más abajo).
permissionsnoarrayLista 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" }
}
CampoObligatorioNotas
kindsí"backend" o "frontend"
idsíÚnico dentro del manifiesto. Minúsculas y guiones.
entrypointsíRuta relativa al archivo de entrada JS.
stylenoArchivo CSS cargado en el iframe del plugin.
assetsnoDirectorio de recursos estáticos que se sirven bajo /plugins/{id}/assets/.
backend.idnoVincula un componente de frontend con su componente de backend para RPC de sdk.backend.*.
runtimeno (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 plugin

sdk.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.sep

sdk.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: Response

sdk.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()       // > Date

Body:

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

Permisos ​

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:

PermisoQué permite
read_http_historysdk.requests.get, sdk.requests.search
read_findingssdk.findings.get, sdk.findings.list
read_scopesdk.scope.getActive
read_projectssdk.projects.getCurrent, sdk.projects.list
plugin_storagesdk.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:

PermisoQué permite
send_requestssdk.requests.send - enviar solicitudes HTTP salientes
write_findingssdk.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() ni import() 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. Sin fetch ni Buffer.
  • 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-origin en 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/frontend

Si 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 ​

  1. Abre Plugins en la barra lateral izquierda.
  2. Haz clic en Instalar (en la parte superior de la pestaña Instalados).
  3. 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.
  4. Haz clic en Validar para comprobar el manifiesto y el inventario de archivos.
  5. Haz clic en Instalar si la validación es correcta.
  6. Selecciona el plugin en la lista y haz clic en Activar.
  7. 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.

Software propietario. Todos los derechos reservados.