Umbryx — Module Development Guide¶
Overview¶
Umbryx supports drop-in backend and frontend modules. Drop a Python file in
backend/app/plugins/modules/ and a JS module that calls moduleRegistry.register()
on the frontend side. Backend modules are auto-discovered at startup; frontend
modules must be imported by the frontend bundle.
Backend module¶
Minimal example¶
Create backend/app/plugins/modules/my_plugin.py:
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app.plugins.types import BackendModule
from app.core.deps import get_current_user
from app.database import get_db
router = APIRouter(prefix="/api/my-plugin", tags=["my-plugin"])
@router.get("/hello")
def hello(user=Depends(get_current_user)):
return {"hello": user.username}
MODULE = BackendModule(
name="my_plugin",
version="1.0.0",
description="My custom plugin",
router=router,
)
The loader discovers MODULE automatically and registers its router with the app.
Scan parser example¶
To add a new scan format (e.g. Masscan JSON):
import json
def parse_masscan_json(content: str) -> list[dict]:
data = json.loads(content)
hosts = []
for entry in data:
hosts.append({
"ip": entry["ip"],
"hostname": "",
"os": "Unknown",
"ports": [
{"port": p["port"], "proto": p["proto"], "state": "open", "service": p.get("reason", "")}
for p in entry.get("ports", [])
],
})
return hosts
MODULE = BackendModule(
name="masscan_parser",
version="1.0.0",
description="Masscan JSON scan parser",
scan_parsers={"masscan": parse_masscan_json},
)
Registered scan_parsers are exposed through GET /api/modules
(usable by frontend tooling and importers). Note that the topology
preview endpoint currently accepts nmap as its source_type — a
module that wants its parser used by the built-in topology builder
should integrate with the topology router package as well.
BackendModule contract¶
@dataclass
class BackendModule:
name: str
version: str
description: str = ""
enabled: bool = True
router: APIRouter | None = None
scan_parsers: dict[str, Callable] = field(default_factory=dict)
export_contributors: list[Callable] = field(default_factory=list)
report_contributors: list[Callable] = field(default_factory=list)
search_providers: list[Callable] = field(default_factory=list)
startup_hooks: list[Callable] = field(default_factory=list)
shutdown_hooks: list[Callable] = field(default_factory=list)
Checking registered modules¶
{
"modules": [
{
"name": "my_plugin",
"version": "1.0.0",
"description": "My custom plugin",
"enabled": true,
"has_router": true,
"scan_parsers": []
}
]
}
Frontend module¶
Minimal example — adds a sidebar item and a host tab¶
import { moduleRegistry } from './features/plugins/registry.js';
import MyHostTab from './components/MyHostTab.jsx';
moduleRegistry.register({
id: 'my-plugin',
title: 'My Plugin',
version: '1.0.0',
description: 'Adds a custom host tab',
enabled: true,
menuItems: [{
id: 'my-menu',
label: 'My Plugin',
icon: 'target',
tab: 'my-plugin',
}],
hostTabs: [{
id: 'my-tab',
label: 'My Tab',
component: MyHostTab,
}],
});
All extension points¶
| Field | Type | Where it appears |
|---|---|---|
menuItems |
[{ id, label, icon, tab }] |
Sidebar navigation |
routes |
[{ path, component }] |
React Router routes |
projectTabs |
[{ id, label, component }] |
Tabs on the project page |
hostTabs |
[{ id, label, component }] |
Tabs inside the host detail card |
networkTabs |
[{ id, label, component }] |
Tabs in the network node panel |
reportSections |
[{ id, label, component }] |
Extra sections in report generation |
importers |
[{ id, label, accept, component }] |
Import UI entries |
dashboardWidgets |
[{ id, component }] |
Dashboard widget slots |
actions.hosts |
[{ id, label, handler }] |
Context actions on host rows |
actions.findings |
[{ id, label, handler }] |
Context actions on finding rows |
actions.creds |
[{ id, label, handler }] |
Context actions on credential rows |
actions.networkNodes |
[{ id, label, handler }] |
Context actions on topology nodes |
Reading aggregated extension points¶
import { moduleRegistry } from './features/plugins/registry.js';
const hostTabs = moduleRegistry.getHostTabs();
const menuItems = moduleRegistry.getMenuItems();
const importers = moduleRegistry.getImporters();
File placement¶
backend/app/plugins/modules/ ← drop .py files here (auto-loaded)
frontend/src/features/plugins/ ← registry + extension-point helpers
frontend/src/... ← import your module file from the active frontend bundle
Backend modules are discovered via pkgutil.iter_modules on startup.
Frontend modules must be explicitly imported (no browser-side auto-discovery).