Перейти до вмісту

Система плагінів Ogma ​

Плагіни розширюють Ogma власною логікою бекенду, панелями інтерфейсу фронтенду та кроками робочих процесів. Плагіни встановлюються локально з каталогу на диску, вмикаються для окремих проєктів і працюють в ізольованому середовищі.

Цей документ — основний довідник для авторів плагінів.

Для найшвидшого початку використовуйте короткий посібник зі створення плагіна.


Швидкий початок ​

Мінімальний плагін бекенду ​

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; імпорти дозволені за використання пакувальника):

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такstringSemver: MAJOR.MINOR.PATCH
nameніstringНазва в інтерфейсі. За замовчуванням — id.
descriptionніstringОднорядковий опис.
authorніobject{ "name": "...", "email": "...", "url": "..." }
homepageніstringURL репозиторію вихідного коду або документації.
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 plugin

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

sdk.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()       // > 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 плагіна фронтенду (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_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

Захищені дозволи (потребують схвалення користувача) ​

Їх потрібно оголосити в маніфесті, а користувач має явно надати їх на вкладці «Дозволи»:

ДозвілЩо дозволяє
send_requestssdk.requests.send — вихідні HTTP-запити
write_findingssdk.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 у корені.


Установлення плагіна ​

  1. Відкрийте Плагіни на лівій бічній панелі.
  2. Натисніть Установити (угорі вкладки «Установлено»).
  3. У настільній програмі натисніть Огляд, щоб відкрити системний вибір папки. У браузері введіть повний шлях до каталогу плагіна на сервері.
  4. Натисніть Перевірити, щоб перевірити маніфест і склад файлів.
  5. Натисніть Установити, якщо перевірка успішна.
  6. Виберіть плагін у списку й натисніть Увімкнути.
  7. Якщо плагін оголошує захищені дозволи, перегляньте й надайте їх на вкладці Дозволи перед увімкненням.

Усунення проблем ​

Ініціалізація плагіна завершується невдачею без повідомлення: перевірте вкладку Журнали. Найпоширеніші причини:

  • Викликано 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 вище.

Пропрієтарне програмне забезпечення. Усі права захищено.