Система плагинов Ogma
Плагины Ogma расширяют инструмент своей серверной логикой, панелями интерфейса и шагами рабочих процессов. Они устанавливаются локально из каталога на диске, включаются для проекта и работают в изолированной среде.
Этот документ — основной справочник разработчика плагинов.
Для самого быстрого начала используйте Первый плагин.
Быстрый старт
Минимальный серверный плагин
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; импорты допустимы при использовании сборщика):
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());
}
});
}Этого достаточно для рабочего серверного плагина. Оставьте один файл backend/script.js и укажите его непосредственно в manifest.json.
Сборка за минуту (исходники TypeScript)
Для разработки на TypeScript используйте структуру:
text
my-plugin/
manifest.json
backend/
src/index.tsСборка:
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Установка: Плагины > Установить, выберите каталог my-plugin/. Затем включите.
Справочник манифеста
manifest.json находится в корне пакета. Все поля чувствительны к регистру.
Поля верхнего уровня
| Поле | Обязательно | Тип | Примечания |
|---|---|---|---|
id | да | строка | Только строчные буквы, цифры и дефисы. До 64 символов. Уникален среди установленных плагинов. |
version | да | строка | Semver: MAJOR.MINOR.PATCH |
name | нет | строка | Имя в интерфейсе. По умолчанию id. |
description | нет | строка | Краткое описание в одну строку. |
author | нет | объект | { "name": "...", "email": "...", "url": "..." } |
homepage | нет | строка | URL репозитория или документации. |
plugins | да | массив | Одна или несколько записей компонентов (см. ниже). |
permissions | нет | массив | Имена нужных разрешений (см. Разрешения). |
Запись компонента плагина
Каждый объект массива plugins описывает компонент.
Серверный компонент:
json
{
"kind": "backend",
"id": "my-plugin-backend",
"entrypoint": "backend/script.js",
"runtime": "javascript",
"assets": "backend/assets"
}Интерфейсный компонент:
json
{
"kind": "frontend",
"id": "my-plugin-frontend",
"entrypoint": "frontend/script.js",
"style": "frontend/style.css",
"assets": "frontend/assets",
"backend": { "id": "my-plugin-backend" }
}| Поле | Обязательно | Примечания |
|---|---|---|
kind | да | "backend" или "frontend" |
id | да | Уникален в манифесте. Строчные буквы, дефисы. |
entrypoint | да | Относительный путь к входному JS-файлу. |
style | нет | CSS, загружаемый в iframe плагина. |
assets | нет | Каталог статических ресурсов, доступных по /plugins/{id}/assets/. |
backend.id | нет | Связывает интерфейсный компонент с серверным для RPC sdk.backend.*. |
runtime | нет (только сервер) | "javascript" (по умолчанию и единственное поддерживаемое значение). |
API серверного плагина (sdk)
Серверный объект sdk передаётся в init(sdk). Все методы синхронны, кроме помеченных async.
sdk.console
js
sdk.console.log("message")
sdk.console.warn("message")
sdk.console.error("message")Пишет в буфер журнала (вкладка журналов). Сохраняются максимум 500 записей. Сообщения обрезаются до 1 КБ.
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() указывает на доступный для записи частный каталог данных, например ~/.local/share/ogma/plugins/my-plugin/data. Он создаётся автоматически и сохраняет данные между перезапусками.
Для состояния и файлов также доступны sdk.storage, sdk.path и sdk.fs.
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 ограничен ID плагина и сохраняется между его перезапусками.
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 ограничен файлами внутри sdk.meta.path().
read и write остаются псевдонимами совместимости для readFile и writeFile. Используйте exists или existsSync перед созданием файла, который не хотите перезаписывать. API синхронен в среде плагина и не является полным модулем Node.js fs. Доступ требует plugin_storage и остаётся внутри частного каталога. У JavaScript рабочих процессов другой файловый контекст; см. Доступ рабочих процессов к файлам.
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
Регистрирует обработчики событий Ogma. Все вызываются синхронно внутри изолированной среды 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 разрешение send_requests должно быть объявлено в манифесте и выдано пользователем. См. Разрешения.
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");Ограничения sdk.findings.create: 10 в минуту, 500 за сессию плагина, 3 за вызов обработчика события.
sdk.api
Регистрирует серверные RPC-функции, вызываемые интерфейсом через 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" });Обработчик получает аргументы интерфейса (дополнительный sdk не вставляется). Возвращаемые значения сериализуются в JSON и отправляются вызывающему.
Интерфейс вызывает их через sdk.backend.getScans(scanId) — см. API интерфейсного плагина.
sdk.api.send добавляет события в очередь плагина (максимум 200). Интерфейсы опрашивают её через sdk.backend.onEvent.
sdk.replay, sdk.projects, sdk.scope, sdk.workflows, sdk.matchReplace
Это пространства имён для запросов только на чтение. Полные сигнатуры см. в справочнике серверного SDK.
Классы запросов
RequestSpecRaw — перехваченный запрос. Вы получаете его в 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 — записанный запрос только для чтения (из 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 — записанный ответ только для чтения.
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 интерфейсного плагина (sdk)
Код интерфейса работает в изолированном iframe, загруженном с /plugins/{id}/ui. Он использует postMessage для связи с хостом Ogma, который передаёт вызовы серверу.
SDK доступен через window.ogmaSDK. Вызовите ogmaSDK.ready(cb), чтобы получить готовый к работе SDK после установления связи с хостом:
js
ogmaSDK.ready(function(sdk) {
// sdk is the live SDK - safe to call any method here
sdk.log.info("frontend ready");
});Все методы SDK возвращают Promise.
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" });Требует read_http_history (выдаётся автоматически без одобрения пользователя).
sdk.findings
js
var page = await sdk.findings.list({ limit: 20, offset: 0 });Требует read_findings (выдаётся автоматически).
sdk.scope
js
var scope = await sdk.scope.getActive();sdk.projects
js
var project = await sdk.projects.getCurrent();sdk.backend — серверный RPC
Вызывайте функции, зарегистрированные сервером через sdk.api.register:
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 использует внутренний опрос каждые 2 секунды. Остановите прослушивание возвращённой функцией отписки:
js
var unsub = sdk.backend.onEvent("scan:complete", handler);
// later:
unsub();sdk.navigation
js
await sdk.navigation.addPage("/my-plugin", { title: "My Plugin" });Регистрирует страницу навигации. Сейчас подтверждается хостом. Полная интеграция маршрутизатора в разработке.
sdk.sidebar
js
await sdk.sidebar.registerItem("My Plugin", "/my-plugin", { icon: "puzzle" });Регистрирует элемент боковой панели. Пока локален панели плагина; подключение глобальных слотов в разработке.
sdk.commands
js
await sdk.commands.register("my-plugin:scan", {
name: "Scan with My Plugin",
handler: function(context) { /* ... */ }
});Подтверждается хостом. Интеграция палитры команд в разработке.
sdk.menu
js
await sdk.menu.registerItem({
type: "Request",
commandId: "my-plugin:scan",
leadingIcon: "shield"
});Подтверждается хостом. Вставка контекстного меню в разработке.
sdk.window
js
sdk.window.showToast("Scan complete", { variant: "success", duration: 3000 });Показывает уведомление в панели плагина. Варианты: 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Разрешения
Объявляйте разрешения в manifest.json:
json
{
"permissions": ["send_requests", "write_findings"]
}Автоматические разрешения (без одобрения пользователя)
Они всегда выдаются любому установленному плагину:
| Разрешение | Что позволяет |
|---|---|
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 |
Защищённые разрешения (нужно одобрение пользователя)
Они должны быть объявлены в манифесте и явно выданы пользователем на вкладке разрешений:
| Разрешение | Что позволяет |
|---|---|
send_requests | sdk.requests.send — исходящие HTTP-запросы |
write_findings | sdk.findings.create, sdk.findings.update |
При включении плагина с защищёнными разрешениями пользователь видит запрос. Он также может в любой момент выдать или отозвать разрешения на соответствующей вкладке.
Структура пакета плагина
Пакет устанавливается из локального каталога, а при работе через браузер также из экспорта .zip.
my-plugin/
manifest.json - обязателен
backend/
script.js - собранный серверный JS (ES2020)
frontend/
script.js - собранный интерфейсный JS
style.css - необязательный CSS
assets/ - статические ресурсы (изображения, шрифты и т. д.)Требования к серверному скрипту
- Один самодостаточный JS-файл.
require()и динамическийimport()не поддерживаются.- Нужно экспортировать
init(sdk)или определить её глобально. - Подмножество ES2020 QuickJS:
async/await,Promise,Map,Set,Symbol,Proxy,Date,RegExp,JSON. НетfetchиBuffer. - Поддерживаемые статические импорты разрешаются предобработкой:
@ogma/sdk,crypto,fs,path. - Максимальный размер файла: 256 КБ.
Требования к интерфейсному скрипту
- Работает в изолированном iframe. Разрешён
connect-src: 'self'для POST в/plugins/{id}/api/*и опроса/plugins/{id}/events/poll. - CSP:
default-src 'none'; script-src 'nonce-...'; style-src 'self'; img-src data: blob: 'self'; connect-src 'self'. - В изоляции нет
allow-same-origin— плагин не получает родительский DOM или cookie Ogma. - Получайте SDK через
ogmaSDK.ready(cb); не вызывайте методы до обработчика.
Сборка плагина для Ogma
Сервер должен быть единым JS-файлом, поэтому перед установкой соберите исходники TypeScript/ES-модулей.
Рекомендуемые инструменты:
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Если используете инструменты Caido (@caido-community/dev), выполните caido-dev build, затем скопируйте вывод в структуру, совместимую с Ogma, с manifest.json в корне.
Установка плагина
- Откройте Плагины слева.
- Нажмите Установить над вкладкой установленных.
- В десктопном приложении нажмите Обзор для нативного выбора папки. В браузере введите полный серверный путь каталога.
- Нажмите Проверить для проверки манифеста и состава файлов.
- При успехе нажмите Установить.
- Выберите плагин и нажмите Включить.
- При защищённых разрешениях проверьте и выдайте их на вкладке Разрешения перед включением.
Решение проблем
Инициализация молча завершается неудачей: проверьте Журналы. Частые причины:
- Вызван
sdk.meta.path(), но каталог данных не удалось создать. - Необработанное исключение в
init(). - Отсутствующий или ошибочно написанный вызов
sdk.*.
Пустой интерфейс: проверьте консоль браузера на нарушения CSP. Скрипт должен вызвать ogmaSDK.ready(cb) до обращения к методам SDK.
sdk.requests.send выдаёт Permission denied: разрешение send_requests должно быть объявлено в манифесте И выдано пользователем на вкладке разрешений.
Функции sdk.api.register не вызываются из интерфейса: серверный компонент должен быть включён, а не только установлен. Имя функции должно точно совпадать с переданным в sdk.backend.call, с учётом регистра.
Предупреждения совместимости выглядят как ошибки: они не блокируют работу, но показывают пробелы API. Покрытие смотрите в таблицах соответствия SDK выше.