Система плагінів 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 | так | string | Лише малі літери, цифри й дефіси. До 64 символів. Унікальне серед установлених плагінів. |
version | так | string | Semver: MAJOR.MINOR.PATCH |
name | ні | string | Назва в інтерфейсі. За замовчуванням — id. |
description | ні | string | Однорядковий опис. |
author | ні | object | { "name": "...", "email": "...", "url": "..." } |
homepage | ні | string | URL репозиторію вихідного коду або документації. |
plugins | так | array | Один або кілька записів компонентів плагіна (див. нижче). |
permissions | ні | array | Список потрібних дозволів (див. Дозволи). |
Запис компонента плагіна
Кожен об’єкт у масиві 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 обмежене ідентифікатором плагіна й зберігається між його перезапусками.
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 працюють синхронно в середовищі виконання плагіна; це не повний модуль fs Node.js. Доступ плагіна до файлів потребує дозволу 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: Responsesdk.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. iframe використовує 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 повертають Promises.
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 - required
backend/
script.js - bundled backend JS (ES2020)
frontend/
script.js - bundled frontend JS
style.css - optional CSS
assets/ - static assets (images, fonts, etc.)Вимоги до сценарію бекенду
- Має бути одним самодостатнім файлом 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'. - У налаштуваннях ізоляції iframe немає
allow-same-origin— плагін не може отримати доступ до батьківського DOM Ogma чи cookie. - Використовуйте
ogmaSDK.ready(cb)для доступу до SDK; не викликайте методи SDK до спрацювання функції зворотного виклику.
Збирання плагіна для 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. Про охоплення API див. таблиці відповідності SDK вище.