نظام افزونههای 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 | بله | رشته | نسخهبندی معنایی: 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 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() به پوشهٔ خصوصی و قابل نوشتن دادههای افزونه اشاره میکند؛ مثلاً ~/.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 برای ارتباط با میزبان Ogma از postMessage استفاده میکند و میزبان فراخوانیها را به بکاند منتقل میکند.
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 - 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 KB.
الزامات اسکریپت فرانتاند
- در یک iframe ایزوله اجرا میشود.
connect-src: 'self'مجاز است تا افزونه بتواند به/plugins/{id}/api/*درخواست POST بفرستد و/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 یا کوکیها دسترسی داشته باشد. - برای دسترسی به SDK از
ogmaSDK.ready(cb)استفاده کنید؛ پیش از اجرای تابع فراخوانی برگشتی، متدهای 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 بررسی کنید. مطمئن شوید اسکریپت فرانتاند پیش از دسترسی به هر متد SDK، ogmaSDK.ready(cb) را فراخوانی میکند.
sdk.requests.send خطای Permission denied میدهد: مجوز send_requests باید هم در مانیفست اعلام شود و هم توسط کاربر در زبانهٔ مجوزها اعطا شود.
توابع sdk.api.register از فرانتاند قابل فراخوانی نیستند: بکاند باید فعال باشد (نه صرفاً نصبشده). نام تابع باید دقیقاً با آنچه فرانتاند به sdk.backend.call میدهد مطابقت داشته باشد (حساس به بزرگی و کوچکی حروف).
هشدارهای سازگاری بهصورت خطا نمایش داده میشوند: این هشدارها مانع اجرا نمیشوند، اما نشاندهندهٔ کمبود در پوشش API هستند. برای پوشش API به جدولهای نگاشت SDK در بالا مراجعه کنید.