Système de plugins Ogma
Les plugins Ogma étendent l'outil avec une logique backend personnalisée, des panneaux d'interface frontend et des étapes de workflow. Ils sont installés localement depuis un répertoire sur disque, activés par projet et exécutés dans un environnement isolé.
Ce document constitue la référence principale pour les auteurs de plugins.
Pour démarrer le plus rapidement possible, consultez le démarrage rapide des plugins.
Démarrage rapide
Plugin backend minimal
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 ; les imports conviennent si vous utilisez un outil de regroupement de code) :
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());
}
});
}Cela suffit pour obtenir un plugin fonctionnel avec un backend uniquement. Conservez tout le code dans un seul fichier backend/script.js et faites pointer manifest.json directement vers ce fichier.
Compilation en une minute (sources TypeScript)
Si vous développez en TypeScript, utilisez cette structure :
text
my-plugin/
manifest.json
backend/
src/index.tsCompilation :
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.jsInstallation : Plugins > Installer, puis sélectionnez le répertoire my-plugin/. Activez ensuite le plugin.
Référence du manifeste
manifest.json se trouve à la racine du paquet. Tous les champs sont sensibles à la casse.
Champs de premier niveau
| Champ | Obligatoire | Type | Remarques |
|---|---|---|---|
id | oui | chaîne | Lettres minuscules, chiffres et traits d'union uniquement. 64 caractères maximum. Unique parmi les plugins installés. |
version | oui | chaîne | Version sémantique : MAJOR.MINOR.PATCH |
name | non | chaîne | Nom affiché dans l'interface. Utilise id par défaut. |
description | non | chaîne | Résumé sur une ligne. |
author | non | objet | { "name": "...", "email": "...", "url": "..." } |
homepage | non | chaîne | URL du dépôt de sources ou de la documentation. |
plugins | oui | tableau | Une ou plusieurs entrées de composants de plugin (voir ci-dessous). |
permissions | non | tableau | Liste des noms des permissions requises (voir Permissions). |
Entrée de composant de plugin
Chaque objet du tableau plugins décrit un composant.
Composant backend :
json
{
"kind": "backend",
"id": "my-plugin-backend",
"entrypoint": "backend/script.js",
"runtime": "javascript",
"assets": "backend/assets"
}Composant frontend :
json
{
"kind": "frontend",
"id": "my-plugin-frontend",
"entrypoint": "frontend/script.js",
"style": "frontend/style.css",
"assets": "frontend/assets",
"backend": { "id": "my-plugin-backend" }
}| Champ | Obligatoire | Remarques |
|---|---|---|
kind | oui | "backend" ou "frontend" |
id | oui | Unique dans le manifeste. Minuscules et traits d'union. |
entrypoint | oui | Chemin relatif du fichier JS du point d'entrée. |
style | non | Fichier CSS chargé dans l'iframe du plugin. |
assets | non | Répertoire des ressources statiques servies sous /plugins/{id}/assets/. |
backend.id | non | Associe un composant frontend à son composant backend pour les appels RPC sdk.backend.*. |
runtime | non (backend uniquement) | "javascript" (valeur par défaut et seule valeur prise en charge). |
API des plugins backend (sdk)
L'objet backend sdk est transmis à votre fonction init(sdk). Toutes les méthodes sont synchrones sauf si elles sont marquées async.
sdk.console
js
sdk.console.log("message")
sdk.console.warn("message")
sdk.console.error("message")Écrit dans le tampon des journaux du plugin (visible dans l'onglet Journaux). Un maximum de 500 entrées est conservé. Chaque message est tronqué à 1 Ko.
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() pointe vers le répertoire de données privé du plugin, accessible en écriture, par exemple ~/.local/share/ogma/plugins/my-plugin/data. Ce répertoire est créé automatiquement et peut servir à conserver les données du plugin entre les redémarrages.
sdk.storage, sdk.path et sdk.fs sont également disponibles pour la gestion de l'état et les opérations utilitaires sur les fichiers.
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 est propre à l'identifiant du plugin et ses données persistent entre les redémarrages du 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 est limité aux fichiers situés sous sdk.meta.path().
read et write restent des alias de compatibilité pour readFile et writeFile. Utilisez exists ou existsSync avant de créer un fichier que vous ne souhaitez pas écraser. Ces API fonctionnent de manière synchrone dans l'environnement d'exécution du plugin ; elles ne constituent pas le module Node.js fs complet. L'accès aux fichiers du plugin nécessite la permission plugin_storage et reste confiné au répertoire de données privé du plugin. Le JavaScript des workflows dispose d'un contexte de système de fichiers différent ; voir Accès aux fichiers dans les workflows.
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
Enregistrez des fonctions de rappel pour les événements Ogma. Toutes sont appelées de manière synchrone dans le bac à sable 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 nécessite que la permission send_requests soit déclarée dans le manifeste et accordée par l'utilisateur. Voir Permissions.
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");Limites de fréquence de sdk.findings.create : 10 par minute, 500 par session de plugin, 3 par fonction de rappel d'événement.
sdk.api
Enregistrez des fonctions RPC backend que le frontend peut appeler via 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" });Le gestionnaire reçoit les arguments transmis par le frontend (aucun argument sdk supplémentaire n'est injecté). Les valeurs renvoyées sont sérialisées en JSON et retournées à l'appelant.
Le frontend appelle ces fonctions via sdk.backend.getScans(scanId) ; voir API des plugins frontend.
sdk.api.send place les événements dans une file propre à chaque plugin (200 entrées maximum). Les frontends interrogent cette file périodiquement via sdk.backend.onEvent.
sdk.replay, sdk.projects, sdk.scope, sdk.workflows, sdk.matchReplace
Ces espaces de noms permettent uniquement des consultations en lecture seule. Consultez la référence du SDK backend pour les signatures complètes des méthodes.
Classes de requêtes
RequestSpecRaw : représente une requête interceptée. Vous recevez cet objet dans 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 : une requête capturée en lecture seule (issue de 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 : une réponse capturée en lecture seule.
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 des plugins frontend (sdk)
Le code des plugins frontend s'exécute dans une iframe isolée chargée depuis /plugins/{id}/ui. L'iframe utilise postMessage pour communiquer avec l'application hôte Ogma, qui relaie les appels au backend.
Le SDK est disponible via window.ogmaSDK. Appelez ogmaSDK.ready(cb) pour recevoir le SDK opérationnel une fois la passerelle avec l'hôte établie :
js
ogmaSDK.ready(function(sdk) {
// sdk is the live SDK - safe to call any method here
sdk.log.info("frontend ready");
});Toutes les méthodes du SDK renvoient des promesses.
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" });Nécessite la permission read_http_history (accordée automatiquement ; aucune approbation de l'utilisateur n'est nécessaire).
sdk.findings
js
var page = await sdk.findings.list({ limit: 20, offset: 0 });Nécessite la permission read_findings (accordée automatiquement).
sdk.scope
js
var scope = await sdk.scope.getActive();sdk.projects
js
var project = await sdk.projects.getCurrent();sdk.backend : RPC backend
Appelez les fonctions enregistrées avec sdk.api.register sur le 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 utilise en interne une boucle d'interrogation toutes les 2 secondes. Arrêtez l'écoute en appelant la fonction de désabonnement renvoyée :
js
var unsub = sdk.backend.onEvent("scan:complete", handler);
// later:
unsub();sdk.navigation
js
await sdk.navigation.addPage("/my-plugin", { title: "My Plugin" });Enregistre une page de navigation. L'hôte en accuse actuellement réception. L'intégration complète avec le routeur est en cours.
sdk.sidebar
js
await sdk.sidebar.registerItem("My Plugin", "/my-plugin", { icon: "puzzle" });Enregistre une entrée dans la barre latérale. Elle est actuellement limitée au panneau d'interface du plugin ; le raccordement aux emplacements de la barre latérale globale est en cours.
sdk.commands
js
await sdk.commands.register("my-plugin:scan", {
name: "Scan with My Plugin",
handler: function(context) { /* ... */ }
});L'hôte accuse réception. L'intégration à la palette de commandes est en cours.
sdk.menu
js
await sdk.menu.registerItem({
type: "Request",
commandId: "my-plugin:scan",
leadingIcon: "shield"
});L'hôte accuse réception. L'injection dans les menus contextuels est en cours.
sdk.window
js
sdk.window.showToast("Scan complete", { variant: "success", duration: 3000 });Affiche une notification temporaire dans le panneau du plugin. Variantes : 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.registerItemPermissions
Déclarez les permissions dans manifest.json :
json
{
"permissions": ["send_requests", "write_findings"]
}Permissions accordées automatiquement (sans approbation de l'utilisateur)
Ces permissions sont toujours accordées à tout plugin installé :
| Permission | Ce qu'elle autorise |
|---|---|
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 |
Permissions protégées (nécessitent l'approbation de l'utilisateur)
Elles doivent être déclarées dans le manifeste et explicitement accordées par l'utilisateur depuis l'onglet Autorisations :
| Permission | Ce qu'elle autorise |
|---|---|
send_requests | sdk.requests.send : effectuer des requêtes HTTP sortantes |
write_findings | sdk.findings.create, sdk.findings.update |
L'utilisateur voit une demande d'autorisation lorsqu'il active un plugin qui déclare des permissions protégées. Il peut également accorder ou révoquer des permissions à tout moment depuis l'onglet Autorisations.
Structure d'un paquet de plugin
Un paquet de plugin peut être installé depuis un répertoire local et, dans les parcours via le navigateur, également depuis des exports .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.)Exigences du script backend
- Doit être un fichier JS unique et autonome.
require()et les imports dynamiquesimport()ne sont pas pris en charge.- Doit exporter une fonction
init(sdk)(ou la définir comme fonction globale). - Sous-ensemble ES2020 pris en charge par QuickJS :
async/await,Promise,Map,Set,Symbol,Proxy,Date,RegExp,JSON. Pas defetchni deBuffer. - Les imports statiques pris en charge sont résolus lors du prétraitement du plugin :
@ogma/sdk,crypto,fs,path. - Taille maximale du fichier : 256 Ko.
Exigences du script frontend
- S'exécute dans une iframe isolée.
connect-src: 'self'est autorisé pour que le plugin puisse effectuer des requêtes POST vers/plugins/{id}/api/*et interroger périodiquement/plugins/{id}/events/poll. - CSP :
default-src 'none'; script-src 'nonce-...'; style-src 'self'; img-src data: blob: 'self'; connect-src 'self'. - Pas de
allow-same-origindans le bac à sable de l'iframe : le plugin ne peut pas accéder au DOM parent d'Ogma ni à ses cookies. - Utilisez
ogmaSDK.ready(cb)pour accéder au SDK ; n'appelez pas les méthodes du SDK avant le déclenchement de la fonction de rappel.
Compiler un plugin pour Ogma
Le backend devant être un fichier JS unique regroupant tout le code, vous devez regrouper vos sources TypeScript ou modules ES avant l'installation.
Chaîne d'outils recommandée :
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/frontendSi vous utilisez la chaîne d'outils de développement Caido (@caido-community/dev), exécutez caido-dev build, puis copiez la sortie dans une structure compatible avec Ogma, avec manifest.json à la racine.
Installer un plugin
- Ouvrez Plugins dans la barre latérale gauche.
- Cliquez sur Installer, en haut de l'onglet Installé.
- Dans l'application de bureau, cliquez sur Parcourir pour ouvrir le sélecteur de dossiers natif. Dans le navigateur, saisissez le chemin complet, côté serveur, du répertoire du plugin.
- Cliquez sur Valider pour vérifier le manifeste et l'inventaire des fichiers.
- Cliquez sur Installer si la validation réussit.
- Sélectionnez le plugin dans la liste et cliquez sur Activer.
- Si le plugin déclare des permissions protégées, examinez-les et accordez-les depuis l'onglet Autorisations avant de l'activer.
Dépannage
L'initialisation du plugin échoue sans message : consultez l'onglet Journaux. Les causes les plus fréquentes sont :
- L'appel à
sdk.meta.path()alors que le répertoire de données n'a pas pu être créé. - Une exception non gérée dans
init(). - Un appel
sdk.*manquant ou mal orthographié.
Le frontend reste vide : consultez la console du navigateur pour détecter les violations de la CSP. Assurez-vous que le script frontend appelle ogmaSDK.ready(cb) avant d'accéder à toute méthode du SDK.
sdk.requests.send lève l'erreur Permission denied : la permission send_requests doit être déclarée dans le manifeste ET accordée par l'utilisateur dans l'onglet Autorisations.
Les fonctions sdk.api.register ne sont pas appelables depuis le frontend : le backend doit être activé (et pas seulement installé). Le nom de la fonction doit correspondre exactement, casse comprise, à celui transmis par le frontend à sdk.backend.call.
Les avertissements de compatibilité apparaissent comme des erreurs : ils ne bloquent pas le fonctionnement, mais signalent des lacunes dans les API disponibles. Consultez les tableaux de correspondance du SDK ci-dessus pour connaître la couverture des API.