Sistema de plugins do Ogma
Os plugins do Ogma ampliam a ferramenta com lógica de backend personalizada, painéis de interface de frontend e etapas de fluxos de trabalho. Os plugins são instalados localmente a partir de um diretório no disco, habilitados por projeto e executados em um ambiente com isolamento de sandbox.
Este documento é a referência principal para autores de plugins.
Para começar o mais rápido possível, use o Início rápido de plugins.
Início 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; você pode usar importações ao utilizar um empacotador):
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());
}
});
}Isso basta para ter um plugin somente de backend funcionando. Mantenha-o como um único arquivo em backend/script.js e faça manifest.json apontar diretamente para ele.
Processo de compilação em um minuto (código-fonte TypeScript)
Se você desenvolve em TypeScript, use esta estrutura:
text
my-plugin/
manifest.json
backend/
src/index.tsCompile:
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.jsInstale em Plugins > Instalar, selecione o diretório my-plugin/. Depois, habilite o plugin.
Referência do manifesto
manifest.json fica no diretório raiz do pacote. Todos os campos diferenciam maiúsculas e minúsculas.
Campos de nível superior
| Campo | Obrigatório | Tipo | Observações |
|---|---|---|---|
id | sim | string | Somente letras minúsculas, dígitos e hífens. Máximo de 64 caracteres. Único entre os plugins instalados. |
version | sim | string | Semver: MAJOR.MINOR.PATCH |
name | não | string | Nome exibido na interface. O padrão é id. |
description | não | string | Resumo de uma linha. |
author | não | object | { "name": "...", "email": "...", "url": "..." } |
homepage | não | string | URL do repositório do código-fonte ou da documentação. |
plugins | sim | array | Uma ou mais entradas de componentes de plugin (consulte abaixo). |
permissions | não | array | Lista dos nomes das permissões exigidas (consulte Permissões). |
Entrada de componente de plugin
Cada objeto no array plugins descreve um 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 | Obrigatório | Observações |
|---|---|---|
kind | sim | "backend" ou "frontend" |
id | sim | Único dentro do manifesto. Minúsculas e hífens. |
entrypoint | sim | Caminho relativo para o arquivo de entrada JS. |
style | não | Arquivo CSS carregado no iframe do plugin. |
assets | não | Diretório de recursos estáticos servidos em /plugins/{id}/assets/. |
backend.id | não | Vincula um componente de frontend ao seu componente de backend para RPC de sdk.backend.*. |
runtime | não (somente backend) | "javascript" (valor padrão e único compatível). |
API de plugins de backend (sdk)
O objeto sdk do backend é passado à sua função init(sdk). Todos os métodos são síncronos, exceto os marcados como async.
sdk.console
js
sdk.console.log("message")
sdk.console.warn("message")
sdk.console.error("message")Grava no buffer de logs do plugin (visível na aba Logs). No máximo 500 entradas são mantidas. Cada mensagem é truncada em 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() aponta para o diretório privado de dados do plugin com permissão de escrita, por exemplo, ~/.local/share/ogma/plugins/my-plugin/data. O diretório é criado automaticamente e pode ser usado para manter os dados do plugin entre reinicializações.
sdk.storage, sdk.path e sdk.fs também estão disponíveis como utilitários de estado e arquivos.
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 é restrito ao ID do plugin e persiste entre reinicializações do 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 é limitado aos arquivos em sdk.meta.path().
read e write continuam sendo aliases de compatibilidade para readFile e writeFile. Use exists ou existsSync antes de criar um arquivo que você não queira sobrescrever. Essas APIs operam de forma síncrona no ambiente de execução do plugin; elas não correspondem ao módulo fs completo do Node.js. O acesso a arquivos do plugin exige a permissão plugin_storage e fica restrito ao diretório privado de dados do plugin. O JavaScript dos fluxos de trabalho tem um contexto de sistema de arquivos diferente; consulte Acesso a arquivos dos fluxos de trabalho.
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
Registre callbacks para eventos do Ogma. Todos os callbacks são chamados de forma síncrona dentro do sandbox do 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 exige que a permissão send_requests seja declarada no manifesto e concedida pelo usuário. Consulte Permissões.
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");Limites de frequência de sdk.findings.create: 10 por minuto, 500 por sessão de plugin e 3 por callback de evento.
sdk.api
Registre funções RPC do backend que o frontend possa chamar por 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" });O handler recebe os argumentos passados pelo frontend (nenhum argumento sdk adicional é injetado). Os valores retornados são serializados como JSON e enviados de volta a quem fez a chamada.
O frontend faz essas chamadas por sdk.backend.getScans(scanId); consulte API de plugins de frontend.
sdk.api.send insere eventos em uma fila por plugin (máximo de 200 entradas). Os frontends consultam essa fila periodicamente por sdk.backend.onEvent.
sdk.replay, sdk.projects, sdk.scope, sdk.workflows, sdk.matchReplace
Esses são namespaces de consulta somente leitura. Consulte a referência do SDK de backend para ver as assinaturas completas dos métodos.
Classes de requisições
RequestSpecRaw representa uma requisição interceptada. Você a recebe em 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 é uma requisição capturada somente leitura (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 é uma resposta capturada somente leitura.
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)
O código de frontend do plugin é executado em um iframe com isolamento de sandbox carregado de /plugins/{id}/ui. O iframe usa postMessage para se comunicar com o aplicativo principal do Ogma, que intermedeia as chamadas ao backend.
O SDK está disponível por window.ogmaSDK. Chame ogmaSDK.ready(cb) para receber o SDK ativo quando a ponte com o aplicativo principal estiver estabelecida:
js
ogmaSDK.ready(function(sdk) {
// sdk is the live SDK - safe to call any method here
sdk.log.info("frontend ready");
});Todos os métodos do SDK retornam promessas.
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" });Exige a permissão read_http_history (concedida automaticamente; não é necessária aprovação do usuário).
sdk.findings
js
var page = await sdk.findings.list({ limit: 20, offset: 0 });Exige a permissão read_findings (concedida automaticamente).
sdk.scope
js
var scope = await sdk.scope.getActive();sdk.projects
js
var project = await sdk.projects.getCurrent();sdk.backend - RPC de backend
Chame as funções registradas com sdk.api.register no 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 usa internamente um loop de consulta a cada 2 segundos. Pare de escutar chamando a função de cancelamento de inscrição retornada:
js
var unsub = sdk.backend.onEvent("scan:complete", handler);
// later:
unsub();sdk.navigation
js
await sdk.navigation.addPage("/my-plugin", { title: "My Plugin" });Registra uma página de navegação. No momento, o aplicativo principal confirma o recebimento. A integração completa com o roteador está em desenvolvimento.
sdk.sidebar
js
await sdk.sidebar.registerItem("My Plugin", "/my-plugin", { icon: "puzzle" });Registra uma entrada na barra lateral. No momento, ela é local ao painel de interface do plugin; a conexão com os slots da barra lateral global está em desenvolvimento.
sdk.commands
js
await sdk.commands.register("my-plugin:scan", {
name: "Scan with My Plugin",
handler: function(context) { /* ... */ }
});O aplicativo principal confirma o recebimento. A integração com a paleta de comandos está em desenvolvimento.
sdk.menu
js
await sdk.menu.registerItem({
type: "Request",
commandId: "my-plugin:scan",
leadingIcon: "shield"
});O aplicativo principal confirma o recebimento. A inserção no menu de contexto está em desenvolvimento.
sdk.window
js
sdk.window.showToast("Scan complete", { variant: "success", duration: 3000 });Exibe uma notificação temporária no painel do 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.registerItemPermissões
Declare as permissões em manifest.json:
json
{
"permissions": ["send_requests", "write_findings"]
}Permissões concedidas automaticamente (sem aprovação do usuário)
Essas permissões são sempre concedidas a qualquer plugin instalado:
| Permissão | O que 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 |
Permissões protegidas (exigem aprovação do usuário)
Elas devem ser declaradas no manifesto e concedidas explicitamente pelo usuário na aba Permissões:
| Permissão | O que permite |
|---|---|
send_requests | sdk.requests.send - fazer requisições HTTP de saída |
write_findings | sdk.findings.create, sdk.findings.update |
O usuário vê uma solicitação de autorização ao habilitar um plugin que declara permissões protegidas. Ele também pode conceder ou revogar permissões a qualquer momento na aba Permissões.
Estrutura do pacote de plugin
Um pacote de plugin pode ser instalado a partir de um diretório local e, nos fluxos do navegador, também de exportações .zip.
my-plugin/
manifest.json - obrigatório
backend/
script.js - JS de backend empacotado (ES2020)
frontend/
script.js - JS de frontend empacotado
style.css - CSS opcional
assets/ - recursos estáticos (imagens, fontes etc.)Requisitos do script de backend
- Deve ser um único arquivo JS autossuficiente.
- Não há suporte a
require()nem aimport()dinâmico. - Deve exportar uma função
init(sdk)(ou defini-la como global). - Subconjunto de ES2020 compatível com QuickJS:
async/await,Promise,Map,Set,Symbol,Proxy,Date,RegExp,JSON. Semfetche semBuffer. - As importações estáticas compatíveis são resolvidas pelo pré-processamento do plugin:
@ogma/sdk,crypto,fs,path. - Tamanho máximo do arquivo: 256 KB.
Requisitos do script de frontend
- É executado em um iframe com isolamento de sandbox.
connect-src: 'self'é permitido para que o plugin possa enviar POST para/plugins/{id}/api/*e consultar periodicamente/plugins/{id}/events/poll. - CSP:
default-src 'none'; script-src 'nonce-...'; style-src 'self'; img-src data: blob: 'self'; connect-src 'self'. - Sem
allow-same-originno sandbox do iframe: o plugin não pode acessar o DOM da página principal do Ogma nem seus cookies. - Use
ogmaSDK.ready(cb)para acessar o SDK; não chame métodos do SDK antes de o callback ser executado.
Compilação de um plugin para o Ogma
Como o backend deve ser um único arquivo JS empacotado, você deve empacotar seu código-fonte TypeScript ou de módulos ES antes de instalar.
Conjunto de ferramentas recomendado:
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/frontendSe você usa o conjunto de ferramentas de desenvolvimento do Caido (@caido-community/dev), execute caido-dev build e copie a saída para uma estrutura compatível com o Ogma, com manifest.json na raiz.
Instalação de um plugin
- Abra Plugins na barra lateral esquerda.
- Clique em Instalar (na parte superior da aba Instalados).
- No aplicativo desktop, clique em Procurar para abrir um seletor de pastas nativo. No navegador, digite o caminho completo do diretório do plugin no servidor.
- Clique em Validar para verificar o manifesto e o inventário de arquivos.
- Clique em Instalar se a validação for bem-sucedida.
- Selecione o plugin na lista e clique em Ativar.
- Se o plugin declara permissões protegidas, revise-as e conceda-as na aba Permissões antes de habilitá-lo.
Solução de problemas
A inicialização do plugin falha silenciosamente: Verifique a aba Logs. As causas mais comuns são:
sdk.meta.path()foi chamado, mas o diretório de dados não pôde ser criado.- Uma exceção não tratada em
init(). - Uma chamada
sdk.*inexistente ou escrita incorretamente.
O frontend aparece em branco: Verifique o console do navegador para identificar violações de CSP. Certifique-se de que seu script de frontend chama ogmaSDK.ready(cb) antes de acessar qualquer método do SDK.
sdk.requests.send lança Permission denied: A permissão send_requests deve ser declarada no manifesto E concedida pelo usuário na aba Permissões.
As funções de sdk.api.register não podem ser chamadas pelo frontend: O backend deve estar habilitado (não basta estar instalado). O nome da função deve corresponder exatamente, diferenciando maiúsculas e minúsculas, ao que o frontend passa para sdk.backend.call.
Avisos de compatibilidade aparecem como erros: Eles não bloqueiam o funcionamento, mas indicam lacunas na interface da API. Consulte as tabelas de mapeamento do SDK acima para verificar a cobertura da API.