Umbryx Module Developer Guide¶
This guide covers building extension modules for the Umbryx platform.
Quick Start¶
Create a directory under /modules/ with a manifest.yaml:
The module is auto-discovered on backend startup. No registration code needed.
Manifest Reference¶
The manifest.yaml is the single source of truth for a module's capabilities.
Required Fields¶
id: my-module # lowercase, hyphens, [a-z0-9-]{2,63}
name: My Module # display name
version: 1.0.0 # semantic versioning
Metadata¶
description: >
What this module does.
author: Your Name
license: MIT
icon: server # icon name from the Icon component
Dependencies¶
depends_on:
- topology # other module IDs that must be present
conflicts_with:
- old-module # modules that cannot coexist
Permissions¶
Declare what data your module needs to access:
permissions:
- read:hosts # read host data
- write:findings # create/update findings
- exec:jobs # execute jobs
- write:object # write to universal object model
Valid modes: read, write, exec
Valid scopes: hosts, creds, findings, scopes, loot, jobs, notes, networks, tasks, object, settings
Object Types¶
Define typed objects stored in the universal core_objects table:
objects:
- type: vulnerability # unique type identifier
name: Vulnerability # display name
icon: bug
list_columns: [title, severity, status] # columns in auto-generated list view
fields:
- name: title
type: string
label: Title
required: true
- name: severity
type: enum
label: Severity
options: [low, medium, high, critical]
default: medium
- name: cvss_score
type: float
label: CVSS Score
- name: description
type: text
label: Description
- name: is_exploitable
type: boolean
label: Exploitable
default: false
- name: remediation
type: text
label: Remediation
- name: related_finding
type: reference
label: Related Finding
reference_type: finding # references another object type
- name: metadata
type: json
label: Metadata
Field types: string, text, integer, float, boolean, enum, datetime, json, reference
Objects are accessible via the universal CRUD API:
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/modules/objects?type=vulnerability |
List objects |
| POST | /api/modules/objects |
Create object |
| GET | /api/modules/objects/{id} |
Get object |
| PATCH | /api/modules/objects/{id} |
Update object |
| DELETE | /api/modules/objects/{id} |
Delete object |
Extension Points¶
Register UI extensions:
extensions:
sidebar.nav:
- id: my-view
label: My View
icon: bolt
view: myView
project.tab:
- id: project-items
label: Items
icon: list
dashboard.widget:
- id: stats-widget
label: Statistics
component: StatsWidget
host.tab:
- id: host-details
label: My Tab
settings.section:
- id: my-settings
label: Module Settings
report.section:
- id: my-report
label: Custom Section
actions.host:
- id: scan-host
label: Scan
icon: target
importer:
- id: csv-import
label: Import CSV
accept: .csv
search.provider:
- id: search-items
label: Search Items
Known extension points: sidebar.nav, project.tab, host.tab, network.tab, dashboard.widget, settings.section, report.section, actions.host, actions.finding, actions.cred, actions.object, importer, search.provider
Navigation Placement (Required for sidebar.nav)¶
The sidebar uses a 2-level hierarchy of semantic groups, each
containing related sections. The groups are defined centrally in
frontend/src/constants.js::TAB_GROUPS — check that file for the
current authoritative list:
| Group | Purpose | Current sections |
|---|---|---|
| (Overview) | Project selection and dashboard | Projects, Overview |
| Recon | Discovery and scanning | Hosts, Scope, ROE, Watch, Network, Attack Path, Attack Graph |
| Exploit | Exploitation evidence and access | Creds, Findings, Loot, Notes |
| Automate | Automation and execution | Jobs, Playbooks, Automations, Cheatsheet |
| Infra | Infrastructure management | Platform Ops, Workstations, Connections |
| Output | Reporting and tracking | Access, Objectives, Checklist, KB, Timeline, Report, Purple |
Rules:
-
Prefer embedding over new nav items. Before adding a
sidebar.naventry, consider whether the functionality can be delivered as ahost.tab,dashboard.widget,actions.host, or inline button in an existing section. A button on the relevant object card is almost always better than a new top-level page. -
Match the user's mental model. Choose the group where a user would look for this feature; if the item does not map to a registered group, it will not render.
-
New groups require justification. Adding a new top-level group is a significant UX change. Only justified when:
-
the module introduces a fundamentally new workflow phase (not covered by existing groups)
- there are 3+ related sections that don't fit any existing group
-
the module lead + UX reviewer approve
-
One item per module. A module should contribute at most one
sidebar.naventry. Additional functionality should be embedded via other extension points (host.tab,dashboard.widget, etc.). -
Prefer contextual integration. For AI/ML features: use
actions.*extension points to add "Ask AI" buttons on object cards rather than creating a separate AI section. Only create a dedicated section if the module provides a full workspace (chat interface, agent orchestration, etc.).
Visual Consistency¶
All module UI must use the shared color and status glossary from src/constants.js:
| Import | Purpose |
|---|---|
NODE_STATUS |
Host status colors (unknown/alive/scanned/access/pwned/owned) |
SEVERITY |
Finding severity colors (critical/high/medium/low/info) |
FINDING_STATUS |
Finding lifecycle status colors |
OBJECTIVE_STATUS |
Objective status colors |
ACTIVITY_TYPES / ACTIVITY_STATUS |
From utils/hosts.js — activity log colors |
Never hardcode color hex values. Import from constants.js or utils/hosts.js. If you need a new status/severity level, add it to the canonical source first, then import.
Icon names must match keys in src/components/Icon.jsx. Use icon: in manifest YAML to specify the icon for nav items and object types.
Navigation Review Process¶
The sidebar structure (TAB_GROUPS in src/constants.js) should be reviewed:
- After every 3 new modules added
- Or once per quarter (whichever comes first)
Review checklist: 1. Does any group have more than 5 items? Consider splitting. 2. Are there items that users struggle to find? Check support tickets/anecdotal feedback. 3. Are there groups with only 1 item? Consider merging into a related group. 4. Has a new workflow phase emerged that doesn't fit any existing group?
Settings¶
Declare configurable settings:
settings:
- key: max_items
label: Maximum Items
type: integer
default: 100
description: Maximum number of items per project
- key: api_key
label: API Key
type: string
placeholder: "Enter your API key"
- key: enabled_features
label: Features
type: enum
options: [basic, advanced, enterprise]
default: basic
Backend Routes¶
Add custom API endpoints:
Create backend/routes.py:
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app.database import get_db
from app.models.core_object import CoreObject
router = APIRouter(prefix="/api/ext/my-module", tags=["my-module"])
@router.get("/custom-endpoint")
def my_endpoint(db: Session = Depends(get_db)):
return {"status": "ok"}
# The loader looks for this attribute
FASTAPI_ROUTER = router
The router is automatically mounted on the FastAPI app at startup.
Universal Object Model¶
The core_objects table provides schema-driven storage without creating custom tables:
from app.models.core_object import CoreObject
from app.core.modules import obj_type_registry
# Validate data against schema
errors = obj_type_registry.validate_data("vulnerability", data)
# Create object
obj = CoreObject(
id="vuln_001",
type="vulnerability",
module_id="my-module",
name=data.get("title"),
data=data,
)
db.add(obj)
db.commit()
Relations¶
Objects can have typed relationships:
# Add relation
obj.relations["affects"] = [{"id": "host_123", "weight": "high"}]
db.commit()
# Query via API
GET /api/modules/objects/{id}/relations/affects
Frontend Integration¶
ExtensionSlot Component¶
Render extensions registered by modules:
import ExtensionPoint from '../modules/ExtensionPoint.jsx';
// Render all dashboard widgets from modules
<ExtensionPoint point="dashboard.widget" projectId={pid} />
ExtensionPoint looks up the registered entries for the extension
point and renders each module's component, wrapped in an error
boundary.
Error Boundaries¶
Module components are wrapped in error boundaries automatically by ExtensionPoint.
To manually isolate a module component:
import ModuleErrorBoundary from '../modules/ModuleErrorBoundary.jsx';
<ModuleErrorBoundary moduleName="my-module">
<MyComponent />
</ModuleErrorBoundary>
API Reference¶
Module Management¶
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /api/modules |
any | List all modules |
| POST | /api/modules/{id}/enable |
admin | Enable module |
| POST | /api/modules/{id}/disable |
admin | Disable module |
| GET | /api/modules/{id}/health |
any | Health check |
Object Types¶
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/modules/object-types |
List all type schemas |
| GET | /api/modules/object-types/{type} |
Get schema for a type |
Extensions¶
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/modules/extensions |
All extension entries |
| GET | /api/modules/extensions/{point} |
Entries for a specific point |
Object CRUD¶
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/modules/objects |
List (filter by type, project_id, status) |
| POST | /api/modules/objects |
Create |
| GET | /api/modules/objects/{id} |
Get |
| PATCH | /api/modules/objects/{id} |
Update |
| DELETE | /api/modules/objects/{id} |
Delete |
| POST | /api/modules/objects/{id}/relations |
Add relation |
| GET | /api/modules/objects/{id}/relations/{type} |
Get relations |
Core vs Extension Boundary¶
Core (not extractable)¶
- Authentication & authorization (
auth,core/deps,core/security) - User & organization management (
users,organizations,projects) - Database & session management (
database.py,alembic) - Event system (
core/events,ws) - Settings & configuration (
core/config,core/enums) - Logging (
core/logging_setup) - Error handling (
core/errors) - Module framework itself (
core/modules)
Extension candidates¶
- Infrastructure provisioning (
core/infra/*) - AI assistant (
routers/ai) - Attack graph & topology (
routers/attack_graph*,routers/topology) - DNS management (
routers/dns) - Scan importers (
routers/import_scanners) - C2 integration (
routers/c2) - Playbooks (
routers/playbooks) - Reports (
routers/report) - Marketplace (
routers/marketplace)
Module Discovery Flow¶
- Backend scans
/modules/*/manifest.yamlon startup - Each manifest is parsed and validated
- Dependencies and conflicts are checked
- Object types registered in
obj_type_registry - Extension points registered in
ext_registry - Backend entry points imported (FASTAPI_ROUTER mounted)
- Frontend fetches
/api/modules/extensionson auth ExtensionSlotcomponents render registered extensions
Lifecycle¶
Modules can be enabled/disabled at runtime via the admin API:
# Disable
curl -X POST -b "rt_auth=TOKEN" http://localhost:3000/api/modules/my-module/disable
# Enable
curl -X POST -b "rt_auth=TOKEN" http://localhost:3000/api/modules/my-module/enable
# Health check
curl -b "rt_auth=TOKEN" http://localhost:3000/api/modules/my-module/health
Disabling unregisters all object types and extension entries for that module.
Existing data in core_objects is preserved.
Real-World Migration Examples¶
Pattern 1: Settings-only module (AI Assistant)¶
For features that already have backend code and just need formal registration:
# modules/ai-assistant/manifest.yaml
id: ai-assistant
name: AI Assistant
version: 1.0.0
description: AI-powered security assistant with multi-provider LLM support.
permissions:
- read:hosts
- read:findings
extensions:
settings.section:
- id: ai-providers
label: AI Providers
component: AISection
settings:
- key: provider
label: Default Provider
type: enum
options: [openai, anthropic, ollama]
default: anthropic
What happened: The backend code (routers/ai.py, core/ai_manager.py) was already wired in main.py. The manifest only declares metadata, permissions, settings, and extension points. No code was moved. The module appears in System → Modules and its settings are discoverable.
Pattern 2: Object type declaration (DNS Management)¶
For features with structured data that fits the universal object model:
# modules/dns-management/manifest.yaml
objects:
- type: dns_record
name: DNS Record
list_columns: [hostname, record_type, value, provider]
fields:
- name: hostname
type: string
required: true
- name: record_type
type: enum
options: [A, AAAA, CNAME, MX, TXT, NS, SRV, CAA, PTR]
default: A
- name: value
type: string
required: true
- name: provider
type: enum
options: [manual, cloudflare]
default: manual
What happened: DNS records previously had a custom table (dns_records). The manifest declares a dns_record object type. New records can be created via the universal CRUD API (POST /api/modules/objects) and rendered with GenericObjectView. The existing table stays for backward compatibility during migration.
Pattern 3: Extension point wiring (ROE)¶
For features that provide a project-scoped tab:
# modules/roe/manifest.yaml
id: roe
name: Rules of Engagement
version: 1.0.0
extensions:
project.tab:
- id: roe
label: ROE
icon: scroll
What happened: ROE already had a backend router and frontend view (RoeView.jsx). The manifest declares the project.tab extension point so the module system knows this module provides a project-level tab. The tab wiring in AppTabRouter.jsx stays as-is during the transition period.
Migration Checklist¶
When migrating an existing feature to a module:
- [ ] Create
modules/<name>/manifest.yamlwith metadata, permissions, objects, extensions, settings - [ ] Verify module appears in System → Modules panel
- [ ] Verify object types appear at
GET /api/modules/object-types - [ ] Verify extension points appear at
GET /api/modules/extensions - [ ] Run module validator:
python3 -c "from app.core.modules import *; from app.core.modules.manifest import Manifest; m = Manifest.from_file(Path('modules/<name>/manifest.yaml')); from app.core.modules.validator import validate_module; print(validate_module(m))" - [ ] If the module has custom API routes, create
backend/routes.pyexportingFASTAPI_ROUTER - [ ] Test enable/disable lifecycle:
POST /api/modules/<name>/disablethen/enable - [ ] Update this guide with the migration pattern used