Chuyển đến nội dung

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.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; 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.ts

Biê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.js

Cà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ườngBắt buộcKiểuGhi chú
idcóstringChỉ 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.
versioncóstringSemver: MAJOR.MINOR.PATCH
namekhôngstringTên hiển thị trong giao diện. Mặc định là id.
descriptionkhôngstringTóm tắt một dòng.
authorkhôngobject{ "name": "...", "email": "...", "url": "..." }
homepagekhôngstringURL đến kho mã nguồn hoặc tài liệu.
pluginscóarrayMột hoặc nhiều mục thành phần plugin (xem bên dưới).
permissionskhôngarrayDanh 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ườngBắt buộcGhi chú
kindcó"backend" hoặc "frontend"
idcóDuy nhất trong manifest. Chữ thường và dấu gạch nối.
entrypointcóĐường dẫn tương đối đến tệp JS tại điểm vào.
stylekhôngTệp CSS được tải trong iframe của plugin.
assetskhôngThư mục tài nguyên tĩnh được phục vụ dưới /plugins/{id}/assets/.
backend.idkhôngLiên kết thành phần frontend với thành phần backend của nó để gọi RPC sdk.backend.*.
runtimekhô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 plugin

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

sdk.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: Response

sdk.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()       // > 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 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.registerItem

Quyề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ềnCho phép
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

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ềnCho phép
send_requestssdk.requests.send - gửi yêu cầu HTTP ra ngoài
write_findingssdk.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ó fetch và 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-origin trong 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/frontend

Nế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 ​

  1. Mở Plugin trên thanh bên trái.
  2. Nhấp Cài đặt (ở đầu thẻ Đã cài đặt).
  3. 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.
  4. Nhấp Kiểm tra tính hợp lệ để kiểm tra manifest và danh sách tệp.
  5. Nhấp Cài đặt nếu kiểm tra tính hợp lệ thành công.
  6. Chọn plugin trong danh sách và nhấp Bật.
  7. 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.

Phần mềm độc quyền. Bảo lưu mọi quyền.