Langsung ke konten

Sistem Plugin Ogma ​

Plugin Ogma memperluas alat dengan logika backend kustom, panel UI frontend, dan langkah alur kerja. Plugin diinstal secara lokal dari direktori pada disk, diaktifkan per proyek, dan berjalan dalam lingkungan sandbox.

Dokumen ini adalah referensi utama bagi pembuat plugin.

Jika Anda menginginkan cara paling cepat untuk memulai, gunakan Panduan Awal Plugin.


Mulai cepat ​

Plugin backend minimal ​

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; impor dapat digunakan dengan bundler):

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());
    }
  });
}

Ini sudah cukup untuk plugin yang hanya memiliki backend dan berfungsi. Simpan sebagai satu file di backend/script.js dan arahkan manifest.json langsung ke file tersebut.

Alur build satu menit (sumber TypeScript) ​

Jika Anda menulis TypeScript, gunakan struktur ini:

text
my-plugin/
  manifest.json
  backend/
    src/index.ts

Build:

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

Instal: Plugin > Pasang, pilih direktori my-plugin/. Lalu aktifkan.


Referensi manifes ​

manifest.json berada di direktori root paket. Semua kolom membedakan huruf besar/kecil.

Kolom tingkat teratas ​

KolomWajibTipeCatatan
idyastringHanya huruf kecil, angka, dan tanda hubung. Maksimum 64 karakter. Unik di antara plugin terinstal.
versionyastringSemver: MAJOR.MINOR.PATCH
nametidakstringNama tampilan di UI. Default-nya adalah id.
descriptiontidakstringRingkasan satu baris.
authortidakobjek{ "name": "...", "email": "...", "url": "..." }
homepagetidakstringURL repositori sumber atau dokumentasi.
pluginsyaarraySatu atau beberapa entri komponen plugin (lihat di bawah).
permissionstidakarrayDaftar nama izin yang diperlukan (lihat Izin).

Entri komponen plugin ​

Setiap objek dalam array plugins menjelaskan satu komponen.

Komponen backend:

json
{
  "kind": "backend",
  "id": "my-plugin-backend",
  "entrypoint": "backend/script.js",
  "runtime": "javascript",
  "assets": "backend/assets"
}

Komponen frontend:

json
{
  "kind": "frontend",
  "id": "my-plugin-frontend",
  "entrypoint": "frontend/script.js",
  "style": "frontend/style.css",
  "assets": "frontend/assets",
  "backend": { "id": "my-plugin-backend" }
}
KolomWajibCatatan
kindya"backend" atau "frontend"
idyaUnik di dalam manifes. Huruf kecil, tanda hubung.
entrypointyaPath relatif ke file titik masuk JS.
styletidakFile CSS yang dimuat dalam iframe plugin.
assetstidakDirektori aset statis yang disajikan di bawah /plugins/{id}/assets/.
backend.idtidakMenautkan komponen frontend ke komponen backend-nya untuk RPC sdk.backend.*.
runtimetidak (hanya backend)"javascript" (nilai default dan satu-satunya yang didukung).

API plugin backend (sdk) ​

Objek sdk backend diberikan ke fungsi init(sdk) Anda. Semua metode bersifat sinkron kecuali ditandai async.

sdk.console ​

js
sdk.console.log("message")
sdk.console.warn("message")
sdk.console.error("message")

Menulis ke buffer log plugin (terlihat di tab Log). Maksimum 500 entri dipertahankan. Setiap pesan dipotong pada 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() mengarah ke direktori data privat plugin yang dapat ditulis, misalnya ~/.local/share/ogma/plugins/my-plugin/data. Direktori dibuat secara otomatis dan dapat digunakan untuk menyimpan data plugin secara persisten setelah mulai ulang.

sdk.storage, sdk.path, dan sdk.fs juga tersedia sebagai helper status dan file.

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 dibatasi pada ID plugin dan dipertahankan setelah plugin dimulai ulang.

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 dibatasi pada file di bawah sdk.meta.path().

read dan write tetap menjadi alias kompatibilitas untuk readFile dan writeFile. Gunakan exists atau existsSync sebelum membuat file yang tidak ingin Anda timpa. API ini bekerja secara sinkron dalam runtime plugin; API ini bukan modul fs Node.js lengkap. Akses file plugin memerlukan izin plugin_storage dan tetap berada di dalam direktori data privat plugin. JavaScript alur kerja memiliki konteks sistem file berbeda; lihat Akses file alur kerja.

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 ​

Daftarkan callback untuk peristiwa Ogma. Semua callback dipanggil secara sinkron di dalam 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 memerlukan izin send_requests yang dideklarasikan dalam manifes dan diberikan oleh pengguna. Lihat Izin.

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");

Batas laju pada sdk.findings.create: 10 per menit, 500 per sesi plugin, 3 per callback peristiwa.

sdk.api ​

Daftarkan fungsi RPC backend yang dapat dipanggil frontend melalui 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" });

Handler menerima argumen yang diberikan dari frontend (tidak ada argumen sdk tambahan yang disisipkan). Nilai kembalian diserialisasi sebagai JSON dan dikirim kembali kepada pemanggil.

Frontend memanggilnya melalui sdk.backend.getScans(scanId) - lihat API plugin frontend.

sdk.api.send mendorong peristiwa ke antrean per plugin (maksimum 200 entri). Frontend melakukan polling antrean ini melalui sdk.backend.onEvent.

sdk.replay, sdk.projects, sdk.scope, sdk.workflows, sdk.matchReplace ​

Ini adalah namespace kueri baca saja. Lihat Referensi SDK backend untuk signature metode lengkap.

Kelas permintaan ​

RequestSpecRaw - mewakili permintaan yang diintersepsi. Anda menerimanya dalam 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 - permintaan rekaman baca saja (dari 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 - respons rekaman baca saja.

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) ​

Kode plugin frontend berjalan dalam iframe sandbox yang dimuat dari /plugins/{id}/ui. Iframe menggunakan postMessage untuk berkomunikasi dengan host Ogma, yang meneruskan pemanggilan ke backend.

SDK tersedia melalui window.ogmaSDK. Panggil ogmaSDK.ready(cb) untuk menerima SDK aktif setelah bridge host terbentuk:

js
ogmaSDK.ready(function(sdk) {
  // sdk is the live SDK - safe to call any method here
  sdk.log.info("frontend ready");
});

Semua metode SDK mengembalikan 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" });

Memerlukan izin read_http_history (diberikan otomatis; tidak memerlukan persetujuan pengguna).

sdk.findings ​

js
var page = await sdk.findings.list({ limit: 20, offset: 0 });

Memerlukan izin read_findings (diberikan otomatis).

sdk.scope ​

js
var scope = await sdk.scope.getActive();

sdk.projects ​

js
var project = await sdk.projects.getCurrent();

sdk.backend - RPC backend ​

Panggil fungsi yang didaftarkan dengan sdk.api.register pada 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 menggunakan siklus polling 2 detik secara internal. Berhenti mendengarkan dengan memanggil fungsi berhenti berlangganan yang dikembalikan:

js
var unsub = sdk.backend.onEvent("scan:complete", handler);
// later:
unsub();

sdk.navigation ​

js
await sdk.navigation.addPage("/my-plugin", { title: "My Plugin" });

Mendaftarkan halaman navigasi. Saat ini dikonfirmasi oleh host. Integrasi router penuh sedang dikembangkan.

sdk.sidebar ​

js
await sdk.sidebar.registerItem("My Plugin", "/my-plugin", { icon: "puzzle" });

Mendaftarkan entri bilah sisi. Saat ini terbatas pada panel UI plugin - penghubungan slot bilah sisi global sedang dikembangkan.

sdk.commands ​

js
await sdk.commands.register("my-plugin:scan", {
  name: "Scan with My Plugin",
  handler: function(context) { /* ... */ }
});

Dikonfirmasi oleh host. Integrasi palet perintah sedang dikembangkan.

sdk.menu ​

js
await sdk.menu.registerItem({
  type: "Request",
  commandId: "my-plugin:scan",
  leadingIcon: "shield"
});

Dikonfirmasi oleh host. Penyisipan menu konteks sedang dikembangkan.

sdk.window ​

js
sdk.window.showToast("Scan complete", { variant: "success", duration: 3000 });

Menampilkan notifikasi toast dalam panel plugin. Varian: 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

Izin ​

Deklarasikan izin dalam manifest.json:

json
{
  "permissions": ["send_requests", "write_findings"]
}

Izin yang diberikan otomatis (tidak memerlukan persetujuan pengguna) ​

Izin berikut selalu diberikan kepada setiap plugin yang terinstal:

IzinYang diizinkan
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

Izin yang dilindungi (memerlukan persetujuan pengguna) ​

Izin berikut harus dideklarasikan dalam manifes dan diberikan secara eksplisit oleh pengguna dari tab Izin:

IzinYang diizinkan
send_requestssdk.requests.send - membuat permintaan HTTP keluar
write_findingssdk.findings.create, sdk.findings.update

Pengguna melihat prompt saat mengaktifkan plugin yang mendeklarasikan izin dilindungi. Pengguna juga dapat memberikan/mencabut izin kapan saja dari tab Izin.


Struktur paket plugin ​

Paket plugin dapat diinstal dari direktori lokal dan, dalam alur peramban, juga dari ekspor .zip.

my-plugin/
  manifest.json           - wajib
  backend/
    script.js             - JS backend yang dibundel (ES2020)
  frontend/
    script.js             - JS frontend yang dibundel
    style.css             - CSS opsional
    assets/               - aset statis (gambar, font, dan sebagainya)

Persyaratan skrip backend ​

  • Harus berupa satu file JS mandiri.
  • require() dan import() dinamis tidak didukung.
  • Harus mengekspor fungsi init(sdk) (atau mendefinisikannya sebagai global).
  • Subset ES2020 yang didukung QuickJS: async/await, Promise, Map, Set, Symbol, Proxy, Date, RegExp, JSON. Tanpa fetch, tanpa Buffer.
  • Impor statis yang didukung diselesaikan oleh prapemrosesan plugin: @ogma/sdk, crypto, fs, path.
  • Ukuran file maksimum: 256 KB.

Persyaratan skrip frontend ​

  • Berjalan di dalam iframe sandbox. connect-src: 'self' diizinkan agar plugin dapat melakukan POST ke /plugins/{id}/api/* dan polling /plugins/{id}/events/poll.
  • CSP: default-src 'none'; script-src 'nonce-...'; style-src 'self'; img-src data: blob: 'self'; connect-src 'self'.
  • Tanpa allow-same-origin dalam sandbox iframe - plugin tidak dapat mengakses DOM induk atau cookie Ogma.
  • Gunakan ogmaSDK.ready(cb) untuk mengakses SDK; jangan panggil metode SDK sebelum callback terpicu.

Melakukan build plugin untuk Ogma ​

Karena backend harus berupa satu file JS yang dibundel, Anda harus membundel sumber modul TypeScript/ES sebelum menginstal.

Rangkaian alat yang direkomendasikan:

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

Jika Anda menggunakan rangkaian alat pengembangan Caido (@caido-community/dev), jalankan caido-dev build, lalu salin keluaran ke tata letak yang kompatibel dengan Ogma dengan manifest.json pada root.


Menginstal plugin ​

  1. Buka Plugin di bilah sisi kiri.
  2. Klik Pasang (di bagian atas tab Terpasang).
  3. Dalam aplikasi desktop, klik Telusuri untuk membuka pemilih folder native. Dalam peramban, ketik path lengkap direktori plugin di sisi server.
  4. Klik Validasi untuk memeriksa manifes dan inventaris file.
  5. Klik Pasang jika validasi lulus.
  6. Pilih plugin dalam daftar dan klik Aktifkan.
  7. Jika plugin mendeklarasikan izin dilindungi, tinjau dan berikan izin dari tab Izin sebelum mengaktifkan.

Pemecahan masalah ​

Inisialisasi plugin gagal tanpa pemberitahuan: Periksa tab Log. Penyebab paling umum:

  • sdk.meta.path() dipanggil, tetapi direktori data tidak dapat dibuat.
  • Pengecualian yang tidak ditangani dalam init().
  • Pemanggilan sdk.* yang tidak ada atau salah eja.

Frontend kosong: Periksa konsol peramban untuk pelanggaran CSP. Pastikan skrip frontend memanggil ogmaSDK.ready(cb) sebelum mengakses metode SDK apa pun.

sdk.requests.send melempar Permission denied: Izin send_requests harus dideklarasikan dalam manifes DAN diberikan oleh pengguna di tab Izin.

Fungsi sdk.api.register tidak dapat dipanggil dari frontend: Backend harus diaktifkan (bukan hanya diinstal). Nama fungsi harus sama persis (membedakan huruf besar/kecil) dengan yang diberikan frontend ke sdk.backend.call.

Peringatan kompatibilitas muncul sebagai kesalahan: Peringatan ini tidak memblokir operasi, tetapi menunjukkan celah cakupan API. Lihat tabel pemetaan SDK di atas untuk cakupan API.

Perangkat lunak proprietari. Seluruh hak cipta dilindungi.