מערכת התוספים של Ogma
תוספי Ogma מרחיבים את הכלי באמצעות לוגיקה מותאמת אישית בצד השרת, לוחות ממשק משתמש בצד הלקוח ושלבים בתהליכי עבודה. תוספים מותקנים מקומית מתיקייה בדיסק, מופעלים לכל פרויקט בנפרד ורצים בסביבה מבודדת.
מסמך זה הוא מדריך העזר הראשי למפתחי תוספים.
להתחלה המהירה ביותר, השתמשו במדריך ההתחלה המהירה עם תוספים.
התחלה מהירה
תוסף צד שרת מינימלי
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 | כן | מחרוזת | אותיות לטיניות קטנות, ספרות ומקפים בלבד. עד 64 תווים. ייחודי בין התוספים המותקנים. |
version | כן | מחרוזת | גרסאות סמנטיות: MAJOR.MINOR.PATCH |
name | לא | מחרוזת | שם התצוגה המופיע בממשק. ברירת המחדל היא id. |
description | לא | מחרוזת | תקציר בשורה אחת. |
author | לא | אובייקט | { "name": "...", "email": "...", "url": "..." } |
homepage | לא | מחרוזת | כתובת URL למאגר קוד המקור או לתיעוד. |
plugins | כן | מערך | רשומה אחת או יותר של רכיבי תוסף (ראו בהמשך). |
permissions | לא | מערך | רשימת שמות ההרשאות הנדרשות (ראו הרשאות). |
רשומת רכיב תוסף
כל אובייקט במערך 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 | לא | קובץ CSS הנטען ב-iframe של התוסף. |
assets | לא | תיקיית נכסים סטטיים המוגשים תחת /plugins/{id}/assets/. |
backend.id | לא | מקשר רכיב צד לקוח לרכיב צד השרת שלו עבור RPC דרך sdk.backend.*. |
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 מוגבל למזהה התוסף ונשמר בין הפעלות מחדש של התוסף.
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 אלה פועלים באופן סינכרוני בסביבת הרצת התוסף; הם אינם מודול fs המלא של Node.js. גישת תוסף לקבצים דורשת הרשאת 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
רשמו פונקציות RPC בצד השרת שצד הלקוח יכול לקרוא להן דרך 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" });פונקציית הטיפול מקבלת ארגומנטים שהועברו מצד הלקוח (לא מוזרק ארגומנט 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)
קוד תוסף צד הלקוח רץ ב-iframe מבודד הנטען מ-/plugins/{id}/ui. ה-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" });רושמת רשומה בסרגל הצד. כרגע היא מקומית ללוח ממשק התוסף - חיבור למיקום בסרגל הצד הכללי נמצא בתהליך.
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)(או להגדיר אותה כגלובלית). - תת-קבוצה של ES2020 הנתמכת ב-QuickJS:
async/await,Promise,Map,Set,Symbol,Proxy,Date,RegExp,JSON. ללאfetch, ללאBuffer. - ייבואים סטטיים נתמכים נפתרים בעיבוד המקדים של התוסף:
@ogma/sdk,crypto,fs,path. - גודל הקובץ המרבי: 256 KB.
דרישות סקריפט צד הלקוח
- רץ בתוך iframe מבודד.
connect-src: 'self'מותר כדי שהתוסף יוכל לשלוח POST אל/plugins/{id}/api/*ולתשאל במחזוריות את/plugins/{id}/events/poll. - מדיניות CSP:
default-src 'none'; script-src 'nonce-...'; style-src 'self'; img-src data: blob: 'self'; connect-src 'self'. - אין
allow-same-originבסביבת הבידוד של ה-iframe - התוסף אינו יכול לגשת ל-DOM של הורה Ogma או לעוגיות. - השתמשו ב-
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. ודאו שסקריפט צד הלקוח קורא ל-ogmaSDK.ready(cb) לפני גישה למתודת SDK כלשהי.
sdk.requests.send זורקת Permission denied: יש להצהיר על הרשאת send_requests במניפסט וגם לקבל את הענקתה בידי המשתמש בלשונית הרשאות.
לא ניתן לקרוא לפונקציות sdk.api.register מצד הלקוח: צד השרת חייב להיות מופעל (לא רק מותקן). שם הפונקציה חייב להתאים בדיוק (רגיש לאותיות גדולות וקטנות) למה שצד הלקוח מעביר אל sdk.backend.call.
אזהרות תאימות מוצגות כשגיאות: הן אינן חוסמות פעולה, אך מצביעות על פערים בכיסוי ה-API. ראו את טבלאות המיפוי של ה-SDK לעיל למידע על כיסוי ה-API.