Ogma 外掛系統
Ogma 外掛可透過自訂後端邏輯、前端 UI 面板和工作流程步驟,擴充工具的功能。外掛從磁碟上的目錄安裝至本機,依專案啟用,並在沙箱環境中執行。
本文是外掛開發者的主要參考文件。
若要以最快的方式開始,請參閱外掛快速入門。
快速入門
最小後端外掛
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 | 語意化版本:MAJOR.MINOR.PATCH |
name | 否 | string | UI 中顯示的名稱,預設為 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 | 否 | 在外掛 iframe 中載入的 CSS 檔案。 |
assets | 否 | 透過 /plugins/{id}/assets/ 提供的靜態資源目錄。 |
backend.id | 否 | 將前端元件連結至其後端元件,以使用 sdk.backend.* RPC。 |
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 的作用範圍限定於外掛 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
註冊可由前端透過 sdk.backend.* 呼叫的後端 RPC 函式:
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)
前端外掛程式碼在從 /plugins/{id}/ui 載入的沙箱 iframe 中執行。iframe 透過 postMessage 與 Ogma 主應用程式通訊,再由主應用程式將呼叫轉送至後端。
可透過 window.ogmaSDK 使用 SDK。呼叫 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" });註冊側邊欄項目。目前僅限於外掛 UI 面板;全域側邊欄插槽的串接仍在開發中。
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)函式(或將它定義為全域函式)。 - 支援 QuickJS 所實作的 ES2020 子集:
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,因此外掛無法存取 Ogma 的父層 DOM 或 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 違規。確認前端指令碼在存取任何 SDK 方法前,已呼叫 ogmaSDK.ready(cb)。
sdk.requests.send 擲回 Permission denied: 必須在資訊清單中宣告 send_requests 權限,並由使用者在權限分頁授予該權限,兩者缺一不可。
前端無法呼叫 sdk.api.register 註冊的函式: 後端必須已啟用(而不只是安裝)。函式名稱必須與前端傳入 sdk.backend.call 的名稱完全相符(區分大小寫)。
相容性警告顯示為錯誤: 這些訊息不會阻止運作,但代表 API 功能仍有缺口。API 支援範圍請參閱上方的 SDK 對照表。