Hệ thống plugin Ogma
Plugin Ogma mở rộng công cụ bằng logic backend tùy chỉnh, bảng giao diện frontend và các bước quy trình. Plugin được cài đặt cục bộ từ một thư mục trên đĩa, bật theo từng dự án và chạy trong môi trường cách ly bằng sandbox.
Tài liệu này là nguồn tham khảo chính cho người viết plugin.
Để bắt đầu nhanh nhất có thể, xem Bắt đầu nhanh với plugin.
Bắt đầu nhanh
Plugin backend tối giản
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; có thể dùng import khi dùng công cụ tạo bundle):
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());
}
});
}Như vậy là đủ để có một plugin chỉ có backend hoạt động. Giữ plugin trong một tệp duy nhất tại backend/script.js và cho manifest.json trỏ trực tiếp đến tệp đó.
Quy trình biên dịch trong một phút (mã nguồn TypeScript)
Nếu bạn viết TypeScript, dùng cấu trúc sau:
text
my-plugin/
manifest.json
backend/
src/index.tsBiên dịch:
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.jsCài đặt: Plugin > Cài đặt, chọn thư mục my-plugin/. Sau đó bật plugin.
Tài liệu tham khảo manifest
manifest.json nằm trong thư mục gốc của gói. Tất cả các trường đều phân biệt chữ hoa và chữ thường.
Các trường cấp cao nhất
| Trường | Bắt buộc | Kiểu | Ghi chú |
|---|---|---|---|
id | có | string | Chỉ gồm chữ thường, chữ số và dấu gạch nối. Tối đa 64 ký tự. Duy nhất trong số các plugin đã cài đặt. |
version | có | string | Semver: MAJOR.MINOR.PATCH |
name | không | string | Tên hiển thị trong giao diện. Mặc định là id. |
description | không | string | Tóm tắt một dòng. |
author | không | object | { "name": "...", "email": "...", "url": "..." } |
homepage | không | string | URL đến kho mã nguồn hoặc tài liệu. |
plugins | có | array | Một hoặc nhiều mục thành phần plugin (xem bên dưới). |
permissions | không | array | Danh sách tên các quyền cần thiết (xem Quyền). |
Mục thành phần plugin
Mỗi đối tượng trong mảng plugins mô tả một thành phần.
Thành phần backend:
json
{
"kind": "backend",
"id": "my-plugin-backend",
"entrypoint": "backend/script.js",
"runtime": "javascript",
"assets": "backend/assets"
}Thành phần frontend:
json
{
"kind": "frontend",
"id": "my-plugin-frontend",
"entrypoint": "frontend/script.js",
"style": "frontend/style.css",
"assets": "frontend/assets",
"backend": { "id": "my-plugin-backend" }
}| Trường | Bắt buộc | Ghi chú |
|---|---|---|
kind | có | "backend" hoặc "frontend" |
id | có | Duy nhất trong manifest. Chữ thường và dấu gạch nối. |
entrypoint | có | Đường dẫn tương đối đến tệp JS tại điểm vào. |
style | không | Tệp CSS được tải trong iframe của plugin. |
assets | không | Thư mục tài nguyên tĩnh được phục vụ dưới /plugins/{id}/assets/. |
backend.id | không | Liên kết thành phần frontend với thành phần backend của nó để gọi RPC sdk.backend.*. |
runtime | không (chỉ backend) | "javascript" (giá trị mặc định và duy nhất được hỗ trợ). |
API plugin backend (sdk)
Đối tượng sdk backend được truyền vào hàm init(sdk) của bạn. Tất cả phương thức đều đồng bộ, trừ khi được đánh dấu async.
sdk.console
js
sdk.console.log("message")
sdk.console.warn("message")
sdk.console.error("message")Ghi vào bộ đệm nhật ký của plugin (hiển thị trong thẻ Nhật ký). Giữ lại tối đa 500 mục. Mỗi thông điệp bị cắt ngắn ở 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() trỏ đến thư mục dữ liệu riêng có thể ghi của plugin, ví dụ ~/.local/share/ogma/plugins/my-plugin/data. Thư mục được tạo tự động và có thể dùng để lưu bền vững dữ liệu plugin qua các lần khởi động lại.
sdk.storage, sdk.path và sdk.fs cũng có sẵn để hỗ trợ xử lý trạng thái và tệp.
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 có phạm vi theo ID plugin và được lưu bền vững qua các lần khởi động lại plugin.
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 chỉ được thao tác với tệp nằm dưới sdk.meta.path().
read và write vẫn là bí danh tương thích của readFile và writeFile. Dùng exists hoặc existsSync trước khi tạo tệp mà bạn không muốn ghi đè. Các API này hoạt động đồng bộ trong môi trường chạy plugin; chúng không phải mô-đun fs đầy đủ của Node.js. Truy cập tệp của plugin cần quyền plugin_storage và chỉ nằm trong thư mục dữ liệu riêng của plugin. JavaScript của quy trình có ngữ cảnh hệ thống tệp khác; xem Truy cập tệp trong quy trình.
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
Đăng ký callback cho các sự kiện Ogma. Tất cả callback được gọi đồng bộ trong sandbox 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 yêu cầu quyền send_requests được khai báo trong manifest và được người dùng cấp. Xem Quyền.
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");Giới hạn tần suất của sdk.findings.create: 10 mỗi phút, 500 mỗi phiên plugin và 3 mỗi callback sự kiện.
sdk.api
Đăng ký các hàm RPC backend mà frontend có thể gọi qua 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" });Hàm xử lý nhận các đối số được truyền từ frontend (không chèn thêm đối số sdk). Giá trị trả về được tuần tự hóa thành JSON và gửi lại cho bên gọi.
Frontend gọi các hàm này qua sdk.backend.getScans(scanId); xem API plugin frontend.
sdk.api.send đưa sự kiện vào hàng đợi riêng cho mỗi plugin (tối đa 200 mục). Frontend truy vấn định kỳ hàng đợi này qua sdk.backend.onEvent.
sdk.replay, sdk.projects, sdk.scope, sdk.workflows, sdk.matchReplace
Đây là các namespace truy vấn chỉ đọc. Xem tài liệu tham khảo SDK backend để biết chữ ký phương thức đầy đủ.
Lớp yêu cầu
RequestSpecRaw đại diện cho yêu cầu đã bị chặn bắt. Bạn nhận đối tượng này trong 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 là yêu cầu đã thu thập, chỉ đọc (từ 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 là phản hồi đã thu thập, chỉ đọc.
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 plugin frontend (sdk)
Mã frontend của plugin chạy trong iframe được cách ly bằng sandbox, được tải từ /plugins/{id}/ui. Iframe dùng postMessage để giao tiếp với ứng dụng chủ Ogma, ứng dụng này làm trung gian cho các lệnh gọi backend.
SDK có sẵn qua window.ogmaSDK. Gọi ogmaSDK.ready(cb) để nhận SDK đang hoạt động sau khi cầu nối với ứng dụng chủ được thiết lập:
js
ogmaSDK.ready(function(sdk) {
// sdk is the live SDK - safe to call any method here
sdk.log.info("frontend ready");
});Tất cả phương thức SDK trả về 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" });Yêu cầu quyền read_http_history (được cấp tự động; không cần người dùng phê duyệt).
sdk.findings
js
var page = await sdk.findings.list({ limit: 20, offset: 0 });Yêu cầu quyền read_findings (được cấp tự động).
sdk.scope
js
var scope = await sdk.scope.getActive();sdk.projects
js
var project = await sdk.projects.getCurrent();sdk.backend - RPC backend
Gọi các hàm đã đăng ký bằng sdk.api.register ở backend:
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 dùng vòng lặp truy vấn định kỳ mỗi 2 giây ở bên trong. Dừng lắng nghe bằng cách gọi hàm hủy đăng ký được trả về:
js
var unsub = sdk.backend.onEvent("scan:complete", handler);
// later:
unsub();sdk.navigation
js
await sdk.navigation.addPage("/my-plugin", { title: "My Plugin" });Đăng ký trang điều hướng. Hiện ứng dụng chủ xác nhận đã nhận. Việc tích hợp đầy đủ với bộ định tuyến đang được thực hiện.
sdk.sidebar
js
await sdk.sidebar.registerItem("My Plugin", "/my-plugin", { icon: "puzzle" });Đăng ký mục trên thanh bên. Hiện chỉ có trong bảng giao diện của plugin; việc kết nối với các slot của thanh bên toàn cục đang được thực hiện.
sdk.commands
js
await sdk.commands.register("my-plugin:scan", {
name: "Scan with My Plugin",
handler: function(context) { /* ... */ }
});Ứng dụng chủ xác nhận đã nhận. Việc tích hợp với bảng lệnh đang được thực hiện.
sdk.menu
js
await sdk.menu.registerItem({
type: "Request",
commandId: "my-plugin:scan",
leadingIcon: "shield"
});Ứng dụng chủ xác nhận đã nhận. Việc chèn vào menu ngữ cảnh đang được thực hiện.
sdk.window
js
sdk.window.showToast("Scan complete", { variant: "success", duration: 3000 });Hiển thị thông báo tạm thời trong bảng plugin. Các kiểu: 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.registerItemQuyền
Khai báo quyền trong manifest.json:
json
{
"permissions": ["send_requests", "write_findings"]
}Quyền được cấp tự động (không cần người dùng phê duyệt)
Các quyền này luôn được cấp cho mọi plugin đã cài đặt:
| Quyền | Cho phép |
|---|---|
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 |
Quyền được bảo vệ (cần người dùng phê duyệt)
Các quyền này phải được khai báo trong manifest và được người dùng cấp rõ ràng từ thẻ Quyền:
| Quyền | Cho phép |
|---|---|
send_requests | sdk.requests.send - gửi yêu cầu HTTP ra ngoài |
write_findings | sdk.findings.create, sdk.findings.update |
Người dùng thấy lời nhắc khi bật plugin có khai báo quyền được bảo vệ. Họ cũng có thể cấp hoặc thu hồi quyền bất cứ lúc nào từ thẻ Quyền.
Cấu trúc gói plugin
Có thể cài đặt gói plugin từ thư mục cục bộ và, trong quy trình trên trình duyệt, cả từ bản xuất .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.)Yêu cầu đối với tập lệnh backend
- Phải là một tệp JS duy nhất, tự chứa đầy đủ mã cần thiết.
- Không hỗ trợ
require()vàimport()động. - Phải xuất hàm
init(sdk)(hoặc định nghĩa hàm đó ở phạm vi toàn cục). - Tập con ES2020 được QuickJS hỗ trợ:
async/await,Promise,Map,Set,Symbol,Proxy,Date,RegExp,JSON. Không cófetchvà không cóBuffer. - Các import tĩnh được hỗ trợ được xử lý trong bước tiền xử lý plugin:
@ogma/sdk,crypto,fs,path. - Kích thước tệp tối đa: 256 KB.
Yêu cầu đối với tập lệnh frontend
- Chạy trong iframe được cách ly bằng sandbox. Cho phép
connect-src: 'self'để plugin có thể gửi POST đến/plugins/{id}/api/*và truy vấn định kỳ/plugins/{id}/events/poll. - CSP:
default-src 'none'; script-src 'nonce-...'; style-src 'self'; img-src data: blob: 'self'; connect-src 'self'. - Không có
allow-same-origintrong sandbox của iframe; plugin không thể truy cập DOM của trang cha Ogma hoặc cookie của Ogma. - Dùng
ogmaSDK.ready(cb)để truy cập SDK; không gọi phương thức SDK trước khi callback được chạy.
Biên dịch plugin cho Ogma
Vì backend phải là một tệp JS bundle duy nhất, bạn phải tạo bundle từ mã nguồn TypeScript hoặc mô-đun ES trước khi cài đặt.
Bộ công cụ được khuyến nghị:
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/frontendNếu bạn dùng bộ công cụ phát triển Caido (@caido-community/dev), chạy caido-dev build, sau đó sao chép đầu ra vào cấu trúc tương thích Ogma với manifest.json ở thư mục gốc.
Cài đặt plugin
- Mở Plugin trên thanh bên trái.
- Nhấp Cài đặt (ở đầu thẻ Đã cài đặt).
- Trong ứng dụng máy tính, nhấp Duyệt để mở bộ chọn thư mục của hệ điều hành. Trong trình duyệt, nhập đường dẫn đầy đủ phía máy chủ đến thư mục plugin.
- Nhấp Kiểm tra tính hợp lệ để kiểm tra manifest và danh sách tệp.
- Nhấp Cài đặt nếu kiểm tra tính hợp lệ thành công.
- Chọn plugin trong danh sách và nhấp Bật.
- Nếu plugin khai báo quyền được bảo vệ, xem xét và cấp quyền từ thẻ Quyền trước khi bật.
Khắc phục sự cố
Khởi tạo plugin thất bại mà không báo lỗi: Kiểm tra thẻ Nhật ký. Các nguyên nhân thường gặp nhất:
- Đã gọi
sdk.meta.path()nhưng không tạo được thư mục dữ liệu. - Ngoại lệ chưa được xử lý trong
init(). - Lệnh gọi
sdk.*bị thiếu hoặc viết sai.
Frontend hiển thị trống: Kiểm tra console trình duyệt để tìm vi phạm CSP. Đảm bảo tập lệnh frontend của bạn gọi ogmaSDK.ready(cb) trước khi truy cập bất kỳ phương thức SDK nào.
sdk.requests.send phát sinh Permission denied: Quyền send_requests phải được khai báo trong manifest VÀ được người dùng cấp trong thẻ Quyền.
Không gọi được các hàm sdk.api.register từ frontend: Backend phải được bật (không chỉ được cài đặt). Tên hàm phải khớp chính xác, có phân biệt chữ hoa và chữ thường, với tên frontend truyền vào sdk.backend.call.
Cảnh báo tương thích hiển thị như lỗi: Chúng không ngăn hoạt động nhưng cho biết các phần API còn thiếu. Xem các bảng ánh xạ SDK ở trên để biết mức độ bao phủ API.