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: 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
注册前端可通过 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 宿主通信,宿主将调用转发到后端。
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" });注册侧边栏条目。目前仅作用于插件 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 映射表。