Sistema di plugin Ogma
I plugin Ogma estendono lo strumento con logica backend personalizzata, pannelli dell’interfaccia frontend e passaggi dei flussi di lavoro. I plugin vengono installati localmente da una directory su disco, abilitati per progetto ed eseguiti in un ambiente con isolamento sandbox.
Questo documento è il riferimento principale per gli autori di plugin.
Per iniziare il più rapidamente possibile, usa la Guida rapida ai plugin.
Avvio rapido
Plugin backend minimo
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; puoi usare le importazioni se utilizzi un 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());
}
});
}Questo è sufficiente per avere un plugin solo backend funzionante. Mantienilo come singolo file in backend/script.js e fai puntare manifest.json direttamente a quel file.
Processo di compilazione in un minuto (codice sorgente TypeScript)
Se sviluppi in TypeScript, usa questa struttura:
text
my-plugin/
manifest.json
backend/
src/index.tsCompila:
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.jsInstalla da Plugin > Installa, seleziona la directory my-plugin/. Poi abilita il plugin.
Riferimento del manifesto
manifest.json si trova nella directory radice del pacchetto. Tutti i campi distinguono maiuscole e minuscole.
Campi di livello superiore
| Campo | Obbligatorio | Tipo | Note |
|---|---|---|---|
id | sì | string | Solo lettere minuscole, cifre e trattini. Massimo 64 caratteri. Univoco tra tutti i plugin installati. |
version | sì | string | Semver: MAJOR.MINOR.PATCH |
name | no | string | Nome visualizzato nell’interfaccia. Il valore predefinito è id. |
description | no | string | Riepilogo di una riga. |
author | no | object | { "name": "...", "email": "...", "url": "..." } |
homepage | no | string | URL del repository del codice sorgente o della documentazione. |
plugins | sì | array | Una o più voci di componenti del plugin (consulta sotto). |
permissions | no | array | Elenco dei nomi delle autorizzazioni richieste (consulta Autorizzazioni). |
Voce di componente del plugin
Ogni oggetto nell’array plugins descrive un componente.
Componente backend:
json
{
"kind": "backend",
"id": "my-plugin-backend",
"entrypoint": "backend/script.js",
"runtime": "javascript",
"assets": "backend/assets"
}Componente frontend:
json
{
"kind": "frontend",
"id": "my-plugin-frontend",
"entrypoint": "frontend/script.js",
"style": "frontend/style.css",
"assets": "frontend/assets",
"backend": { "id": "my-plugin-backend" }
}| Campo | Obbligatorio | Note |
|---|---|---|
kind | sì | "backend" o "frontend" |
id | sì | Univoco all’interno del manifesto. Minuscole e trattini. |
entrypoint | sì | Percorso relativo del file di ingresso JS. |
style | no | File CSS caricato nell’iframe del plugin. |
assets | no | Directory delle risorse statiche servite sotto /plugins/{id}/assets/. |
backend.id | no | Collega un componente frontend al suo componente backend per RPC di sdk.backend.*. |
runtime | no (solo backend) | "javascript" (valore predefinito e unico supportato). |
API dei plugin backend (sdk)
L’oggetto sdk del backend viene passato alla tua funzione init(sdk). Tutti i metodi sono sincroni, salvo quelli contrassegnati come async.
sdk.console
js
sdk.console.log("message")
sdk.console.warn("message")
sdk.console.error("message")Scrive nel buffer dei log del plugin (visibile nella scheda Registri). Vengono conservate al massimo 500 voci. Ogni messaggio viene troncato a 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() punta alla directory privata dei dati del plugin con accesso in scrittura, ad esempio ~/.local/share/ogma/plugins/my-plugin/data. La directory viene creata automaticamente e può essere usata per conservare i dati del plugin tra i riavvii.
Sono disponibili anche sdk.storage, sdk.path e sdk.fs come utilità per stato e 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 è limitato all’ID del plugin e persiste tra i riavvii del 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 è limitato ai file sotto sdk.meta.path().
read e write restano alias di compatibilità di readFile e writeFile. Usa exists o existsSync prima di creare un file che non vuoi sovrascrivere. Queste API operano in modo sincrono nell’ambiente di esecuzione del plugin; non costituiscono il modulo fs completo di Node.js. L’accesso ai file del plugin richiede l’autorizzazione plugin_storage e resta all’interno della directory privata dei dati del plugin. Il JavaScript dei flussi di lavoro ha un contesto di file system diverso; consulta Accesso ai file dei flussi di lavoro.
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
Registra callback per gli eventi Ogma. Tutti i callback vengono chiamati in modo sincrono all’interno della 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 richiede che l’autorizzazione send_requests sia dichiarata nel manifesto e concessa dall’utente. Consulta Autorizzazioni.
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");Limiti di frequenza di sdk.findings.create: 10 al minuto, 500 per sessione del plugin e 3 per callback di evento.
sdk.api
Registra funzioni RPC backend che il frontend possa chiamare tramite 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" });Il gestore riceve gli argomenti passati dal frontend (non viene iniettato alcun argomento sdk aggiuntivo). I valori restituiti vengono serializzati come JSON e inviati a chi ha effettuato la chiamata.
Il frontend effettua queste chiamate tramite sdk.backend.getScans(scanId); consulta API dei plugin frontend.
sdk.api.send inserisce eventi in una coda per plugin (massimo 200 voci). I frontend interrogano periodicamente questa coda tramite sdk.backend.onEvent.
sdk.replay, sdk.projects, sdk.scope, sdk.workflows, sdk.matchReplace
Questi sono namespace di query di sola lettura. Consulta il riferimento dell’SDK backend per le firme complete dei metodi.
Classi delle richieste
RequestSpecRaw rappresenta una richiesta intercettata. La ricevi in 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 è una richiesta acquisita di sola lettura (da 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 è una risposta acquisita di sola lettura.
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 dei plugin frontend (sdk)
Il codice frontend del plugin viene eseguito in un iframe con isolamento sandbox caricato da /plugins/{id}/ui. L’iframe usa postMessage per comunicare con l’applicazione principale Ogma, che funge da intermediaria per le chiamate al backend.
L’SDK è disponibile tramite window.ogmaSDK. Chiama ogmaSDK.ready(cb) per ricevere l’SDK attivo una volta stabilito il bridge con l’applicazione principale:
js
ogmaSDK.ready(function(sdk) {
// sdk is the live SDK - safe to call any method here
sdk.log.info("frontend ready");
});Tutti i metodi dell’SDK restituiscono promesse.
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" });Richiede l’autorizzazione read_http_history (concessa automaticamente; non serve l’approvazione dell’utente).
sdk.findings
js
var page = await sdk.findings.list({ limit: 20, offset: 0 });Richiede l’autorizzazione read_findings (concessa automaticamente).
sdk.scope
js
var scope = await sdk.scope.getActive();sdk.projects
js
var project = await sdk.projects.getCurrent();sdk.backend - RPC backend
Chiama le funzioni registrate con sdk.api.register nel 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 usa internamente un ciclo di interrogazione ogni 2 secondi. Interrompi l’ascolto chiamando la funzione di annullamento dell’iscrizione restituita:
js
var unsub = sdk.backend.onEvent("scan:complete", handler);
// later:
unsub();sdk.navigation
js
await sdk.navigation.addPage("/my-plugin", { title: "My Plugin" });Registra una pagina di navigazione. Attualmente l’applicazione principale conferma la ricezione. L’integrazione completa con il router è in corso.
sdk.sidebar
js
await sdk.sidebar.registerItem("My Plugin", "/my-plugin", { icon: "puzzle" });Registra una voce della barra laterale. Attualmente è locale al pannello dell’interfaccia del plugin; il collegamento agli slot della barra laterale globale è in corso.
sdk.commands
js
await sdk.commands.register("my-plugin:scan", {
name: "Scan with My Plugin",
handler: function(context) { /* ... */ }
});L’applicazione principale conferma la ricezione. L’integrazione con la tavolozza dei comandi è in corso.
sdk.menu
js
await sdk.menu.registerItem({
type: "Request",
commandId: "my-plugin:scan",
leadingIcon: "shield"
});L’applicazione principale conferma la ricezione. L’inserimento nel menu contestuale è in corso.
sdk.window
js
sdk.window.showToast("Scan complete", { variant: "success", duration: 3000 });Mostra una notifica temporanea nel pannello del plugin. Varianti: 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.registerItemAutorizzazioni
Dichiara le autorizzazioni in manifest.json:
json
{
"permissions": ["send_requests", "write_findings"]
}Autorizzazioni concesse automaticamente (senza approvazione dell’utente)
Queste autorizzazioni vengono sempre concesse a qualsiasi plugin installato:
| Autorizzazione | Cosa consente |
|---|---|
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 |
Autorizzazioni protette (richiedono l’approvazione dell’utente)
Devono essere dichiarate nel manifesto e concesse esplicitamente dall’utente dalla scheda Autorizzazioni:
| Autorizzazione | Cosa consente |
|---|---|
send_requests | sdk.requests.send - effettuare richieste HTTP in uscita |
write_findings | sdk.findings.create, sdk.findings.update |
L’utente vede una richiesta di autorizzazione quando abilita un plugin che dichiara autorizzazioni protette. Può anche concedere o revocare le autorizzazioni in qualsiasi momento dalla scheda Autorizzazioni.
Struttura del pacchetto del plugin
Un pacchetto del plugin può essere installato da una directory locale e, nei flussi del browser, anche da esportazioni .zip.
my-plugin/
manifest.json - obbligatorio
backend/
script.js - JS backend in bundle (ES2020)
frontend/
script.js - JS frontend in bundle
style.css - CSS opzionale
assets/ - risorse statiche (immagini, font, ecc.)Requisiti dello script backend
- Deve essere un singolo file JS autonomo.
require()eimport()dinamico non sono supportati.- Deve esportare una funzione
init(sdk)(o definirla come globale). - Sottoinsieme ES2020 supportato da QuickJS:
async/await,Promise,Map,Set,Symbol,Proxy,Date,RegExp,JSON. Senzafetche senzaBuffer. - Le importazioni statiche supportate vengono risolte dal pre-elaboratore del plugin:
@ogma/sdk,crypto,fs,path. - Dimensione massima del file: 256 KB.
Requisiti dello script frontend
- Viene eseguito in un iframe con isolamento sandbox. È consentito
connect-src: 'self'affinché il plugin possa inviare POST a/plugins/{id}/api/*e interrogare periodicamente/plugins/{id}/events/poll. - CSP:
default-src 'none'; script-src 'nonce-...'; style-src 'self'; img-src data: blob: 'self'; connect-src 'self'. - Senza
allow-same-originnella sandbox dell’iframe: il plugin non può accedere al DOM della pagina principale Ogma né ai suoi cookie. - Usa
ogmaSDK.ready(cb)per accedere all’SDK; non chiamare i metodi dell’SDK prima che il callback venga eseguito.
Compilazione di un plugin per Ogma
Poiché il backend deve essere un singolo file JS in bundle, devi creare il bundle del tuo codice sorgente TypeScript o dei moduli ES prima dell’installazione.
Catena di strumenti consigliata:
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/frontendSe usi la catena di strumenti di sviluppo Caido (@caido-community/dev), esegui caido-dev build e poi copia l’output in una struttura compatibile con Ogma con manifest.json nella radice.
Installazione di un plugin
- Apri Plugin nella barra laterale sinistra.
- Fai clic su Installa (in alto nella scheda Installato).
- Nell’applicazione desktop, fai clic su Sfoglia per aprire un selettore di cartelle nativo. Nel browser, digita il percorso completo della directory del plugin sul server.
- Fai clic su Valida per controllare il manifesto e l’inventario dei file.
- Fai clic su Installa se la convalida ha esito positivo.
- Seleziona il plugin nell’elenco e fai clic su Attiva.
- Se il plugin dichiara autorizzazioni protette, esaminale e concedile dalla scheda Autorizzazioni prima di abilitarlo.
Risoluzione dei problemi
L’inizializzazione del plugin fallisce senza mostrare errori: Controlla la scheda Registri. Le cause più comuni sono:
- È stato chiamato
sdk.meta.path(), ma non è stato possibile creare la directory dei dati. - Un’eccezione non gestita in
init(). - Una chiamata
sdk.*mancante o scritta in modo errato.
Il frontend appare vuoto: Controlla la console del browser per individuare violazioni CSP. Assicurati che il tuo script frontend chiami ogmaSDK.ready(cb) prima di accedere a qualsiasi metodo dell’SDK.
sdk.requests.send genera Permission denied: L’autorizzazione send_requests deve essere dichiarata nel manifesto E concessa dall’utente nella scheda Autorizzazioni.
Le funzioni di sdk.api.register non sono richiamabili dal frontend: Il backend deve essere abilitato (non solo installato). Il nome della funzione deve corrispondere esattamente, distinguendo maiuscole e minuscole, a quello che il frontend passa a sdk.backend.call.
Gli avvisi di compatibilità appaiono come errori: Non bloccano il funzionamento, ma indicano lacune nell’interfaccia dell’API. Consulta le tabelle di corrispondenza dell’SDK sopra per la copertura dell’API.