본문으로 이동

Ogma 플러그인 시스템 ​

Ogma 플러그인은 사용자 지정 백엔드 로직, 프런트엔드 UI 패널 및 워크플로 단계를 통해 도구를 확장합니다. 플러그인은 디스크의 디렉터리에서 로컬로 설치되며, 프로젝트별로 활성화되고 샌드박스 환경에서 실행됩니다.

이 문서는 플러그인 개발자를 위한 기본 참조입니다.

가장 빠르게 시작하려면 플러그인 빠른 시작을 확인하세요.


빠른 시작 ​

최소한의 백엔드 플러그인 ​

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, 번들러를 사용하는 경우 import 사용 가능):

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예string소문자, 숫자, 하이픈만 허용합니다. 최대 64자이며 설치된 플러그인 간에 고유해야 합니다.
version예stringSemver: MAJOR.MINOR.PATCH
name아니요stringUI에 표시되는 이름입니다. 기본값은 id입니다.
description아니요string한 줄 요약입니다.
author아니요object{ "name": "...", "email": "...", "url": "..." }
homepage아니요string소스 저장소 또는 문서의 URL입니다.
plugins예array하나 이상의 플러그인 구성 요소 항목입니다(아래 참조).
permissions아니요array필요한 권한 이름 목록입니다(권한 참조).

플러그인 구성 요소 항목 ​

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아니요플러그인 iframe에서 로드되는 CSS 파일입니다.
assets아니요/plugins/{id}/assets/ 아래에서 제공되는 정적 에셋 디렉터리입니다.
backend.id아니요sdk.backend.* RPC를 위해 프런트엔드 구성 요소를 백엔드 구성 요소에 연결합니다.
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 plugin

sdk.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는 플러그인 id별로 분리되며 플러그인을 재시작해도 유지됩니다.

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는 플러그인 런타임에서 동기식으로 동작하며 전체 Node.js fs 모듈이 아닙니다. 플러그인의 파일 접근에는 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.sep

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

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

프런트엔드에서 sdk.backend.*를 통해 호출할 수 있는 백엔드 RPC 함수를 등록합니다.

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

프런트엔드 플러그인 코드는 /plugins/{id}/ui에서 로드되는 샌드박스 iframe에서 실행됩니다. iframe은 postMessage로 Ogma 호스트와 통신하며, 호스트는 호출을 백엔드로 중계합니다.

SDK는 window.ogmaSDK를 통해 사용할 수 있습니다. 호스트 브리지가 연결된 후 실제 SDK를 받으려면 ogmaSDK.ready(cb)를 호출하세요.

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" });

사이드바 항목을 등록합니다. 현재 플러그인 UI 패널 내부에만 적용되며, 전역 사이드바 슬롯 연결은 개발 중입니다.

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_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

보호된 권한(사용자 승인 필요) ​

매니페스트에 선언해야 하며 사용자가 권한 탭에서 명시적으로 부여해야 합니다.

권한허용되는 기능
send_requestssdk.requests.send - 외부로 HTTP 요청 전송
write_findingssdk.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) 함수를 내보내거나 전역 함수로 정의해야 합니다.
  • QuickJS에서 지원하는 ES2020 하위 집합: async/await, Promise, Map, Set, Symbol, Proxy, Date, RegExp, JSON. fetch와 Buffer는 사용할 수 없습니다.
  • 지원되는 정적 import는 플러그인 전처리로 해석됩니다: @ogma/sdk, crypto, fs, path.
  • 최대 파일 크기: 256 KB.

프런트엔드 스크립트 요구 사항 ​

  • 샌드박스 iframe 안에서 실행됩니다. connect-src: 'self'가 허용되므로 플러그인은 /plugins/{id}/api/*에 POST 요청을 보내고 /plugins/{id}/events/poll을 폴링할 수 있습니다.
  • CSP: default-src 'none'; script-src 'nonce-...'; style-src 'self'; img-src data: blob: 'self'; connect-src 'self'.
  • iframe 샌드박스에는 allow-same-origin이 없으므로 플러그인은 Ogma의 부모 DOM이나 쿠키에 접근할 수 없습니다.
  • SDK 접근에는 ogmaSDK.ready(cb)를 사용하세요. 콜백이 실행되기 전에 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를 실행한 다음, 루트에 manifest.json이 있는 Ogma 호환 구조로 결과물을 복사하세요.


플러그인 설치 ​

  1. 왼쪽 사이드바에서 플러그인을 엽니다.
  2. 설치를 클릭합니다(설치됨 탭 상단).
  3. 데스크톱 앱에서는 찾아보기를 클릭해 운영체제의 폴더 선택 창을 엽니다. 브라우저에서는 서버 측 플러그인 디렉터리의 전체 경로를 입력합니다.
  4. 유효성 검사를 클릭해 매니페스트와 파일 목록을 확인합니다.
  5. 검사를 통과하면 설치를 클릭합니다.
  6. 목록에서 플러그인을 선택하고 활성화를 클릭합니다.
  7. 플러그인이 보호된 권한을 선언한 경우 활성화하기 전에 권한 탭에서 검토하고 부여합니다.

문제 해결 ​

플러그인 초기화가 오류 표시 없이 실패함: 로그 탭을 확인하세요. 주요 원인은 다음과 같습니다.

  • sdk.meta.path()를 호출했지만 데이터 디렉터리를 만들지 못함.
  • init()에서 처리되지 않은 예외 발생.
  • sdk.* 호출이 누락되었거나 철자가 잘못됨.

프런트엔드가 빈 화면으로 표시됨: 브라우저 콘솔에서 CSP 위반을 확인하세요. 프런트엔드 스크립트가 SDK 메서드에 접근하기 전에 ogmaSDK.ready(cb)를 호출하는지 확인하세요.

sdk.requests.send에서 Permission denied 발생: 매니페스트에 send_requests 권한을 선언하고 사용자가 권한 탭에서 부여해야 합니다.

프런트엔드에서 sdk.api.register로 등록한 함수를 호출할 수 없음: 백엔드는 설치만 된 상태가 아니라 활성화되어 있어야 합니다. 함수 이름은 프런트엔드가 sdk.backend.call에 전달하는 이름과 대소문자까지 정확히 일치해야 합니다.

호환성 경고가 오류로 표시됨: 실행을 막지는 않지만 API 기능에 부족한 부분이 있음을 나타냅니다. API 지원 범위는 위 SDK 매핑 표를 확인하세요.

독점 소프트웨어입니다. 모든 권리는 저작권자에게 있습니다.