Vai al contenuto

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

Compila:

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

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

CampoObbligatorioTipoNote
idsìstringSolo lettere minuscole, cifre e trattini. Massimo 64 caratteri. Univoco tra tutti i plugin installati.
versionsìstringSemver: MAJOR.MINOR.PATCH
namenostringNome visualizzato nell’interfaccia. Il valore predefinito è id.
descriptionnostringRiepilogo di una riga.
authornoobject{ "name": "...", "email": "...", "url": "..." }
homepagenostringURL del repository del codice sorgente o della documentazione.
pluginssìarrayUna o più voci di componenti del plugin (consulta sotto).
permissionsnoarrayElenco 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" }
}
CampoObbligatorioNote
kindsì"backend" o "frontend"
idsìUnivoco all’interno del manifesto. Minuscole e trattini.
entrypointsìPercorso relativo del file di ingresso JS.
stylenoFile CSS caricato nell’iframe del plugin.
assetsnoDirectory delle risorse statiche servite sotto /plugins/{id}/assets/.
backend.idnoCollega un componente frontend al suo componente backend per RPC di sdk.backend.*.
runtimeno (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 plugin

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

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

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

Autorizzazioni ​

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:

AutorizzazioneCosa consente
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

Autorizzazioni protette (richiedono l’approvazione dell’utente) ​

Devono essere dichiarate nel manifesto e concesse esplicitamente dall’utente dalla scheda Autorizzazioni:

AutorizzazioneCosa consente
send_requestssdk.requests.send - effettuare richieste HTTP in uscita
write_findingssdk.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() e import() 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. Senza fetch e senza Buffer.
  • 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-origin nella 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/frontend

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

  1. Apri Plugin nella barra laterale sinistra.
  2. Fai clic su Installa (in alto nella scheda Installato).
  3. 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.
  4. Fai clic su Valida per controllare il manifesto e l’inventario dei file.
  5. Fai clic su Installa se la convalida ha esito positivo.
  6. Seleziona il plugin nell’elenco e fai clic su Attiva.
  7. 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.

Software proprietario. Tutti i diritti riservati.