Aller au contenu

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

Compilation :

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

Installation : 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 ​

ChampObligatoireTypeRemarques
idouichaîneLettres minuscules, chiffres et traits d'union uniquement. 64 caractères maximum. Unique parmi les plugins installés.
versionouichaîneVersion sémantique : MAJOR.MINOR.PATCH
namenonchaîneNom affiché dans l'interface. Utilise id par défaut.
descriptionnonchaîneRésumé sur une ligne.
authornonobjet{ "name": "...", "email": "...", "url": "..." }
homepagenonchaîneURL du dépôt de sources ou de la documentation.
pluginsouitableauUne ou plusieurs entrées de composants de plugin (voir ci-dessous).
permissionsnontableauListe 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" }
}
ChampObligatoireRemarques
kindoui"backend" ou "frontend"
idouiUnique dans le manifeste. Minuscules et traits d'union.
entrypointouiChemin relatif du fichier JS du point d'entrée.
stylenonFichier CSS chargé dans l'iframe du plugin.
assetsnonRépertoire des ressources statiques servies sous /plugins/{id}/assets/.
backend.idnonAssocie un composant frontend à son composant backend pour les appels RPC sdk.backend.*.
runtimenon (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 plugin

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

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

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

Permissions ​

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é :

PermissionCe qu'elle autorise
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

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 :

PermissionCe qu'elle autorise
send_requestssdk.requests.send : effectuer des requêtes HTTP sortantes
write_findingssdk.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 dynamiques import() 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 de fetch ni de Buffer.
  • 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-origin dans 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/frontend

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

  1. Ouvrez Plugins dans la barre latérale gauche.
  2. Cliquez sur Installer, en haut de l'onglet Installé.
  3. 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.
  4. Cliquez sur Valider pour vérifier le manifeste et l'inventaire des fichiers.
  5. Cliquez sur Installer si la validation réussit.
  6. Sélectionnez le plugin dans la liste et cliquez sur Activer.
  7. 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.

Logiciel propriétaire. Tous droits réservés.