REST API Reference
Base URL: http://localhost:8080/api/v1
All protected endpoints require Authorization: Bearer <token>.
JWT access tokens and API keys use the same header format:
Authorization: Bearer <access_token_or_easm_api_key>
Auth
| Method | Path | Description |
|---|---|---|
| POST | /auth/register |
Create account |
| POST | /auth/login |
Login, returns token pair |
| POST | /auth/refresh |
Refresh access token |
| GET | /me/me |
Current user |
| GET | /me/users |
List users (admin) |
| GET | /me/api-keys |
List own API keys |
| POST | /me/api-keys |
Create API key |
| DELETE | /me/api-keys/{id} |
Revoke API key |
POST /auth/login
{ "email": "user@example.com", "password": "secret" }
→ { "tokens": { "access_token": "…", "refresh_token": "…" }, "user": { … } }
POST /me/api-keys
Create an API key:
{
"name": "Assets read key",
"permissions": ["read:assets"]
}
Response:
{
"key": "easm_...",
"api_key": {
"id": "...",
"name": "Assets read key",
"key_prefix": "easm_...",
"permissions": ["read:assets"]
}
}
The raw key is only returned once when created.
Organizations
| Method | Path | Description |
|---|---|---|
| GET | /organizations |
List orgs (admin: all; others: assigned) |
| POST | /organizations |
Create org |
| GET | /organizations/{id} |
Get org |
| PATCH | /organizations/{id} |
Update org |
| DELETE | /organizations/{id} |
Delete org |
| GET | /organizations/{id}/members |
List members |
| POST | /organizations/{id}/members |
Add member |
Scopes
| Method | Path | Description |
|---|---|---|
| GET | /organizations/{id}/scopes |
List scopes |
| POST | /organizations/{id}/scopes |
Create scope (→ pending) |
| PATCH | /scopes/{scope_id} |
Update scope |
| POST | /scopes/{scope_id}/approve |
Approve (admin) |
| POST | /scopes/{scope_id}/reject |
Reject (admin) |
POST /organizations/{id}/scopes
{ "type": "domain", "value": "example.com", "description": "Main domain" }
Scans
| Method | Path | Description |
|---|---|---|
| GET | /organizations/{id}/scans |
List scans |
| POST | /organizations/{id}/scans |
Create & queue scan |
| GET | /scans/{scan_id} |
Get scan |
| POST | /scans/{scan_id}/cancel |
Cancel scan |
| GET | /scans/{scan_id}/jobs |
List scan jobs |
POST /organizations/{id}/scans
{ "profile": "default", "scope_ids": ["<scope-uuid>"] }
Profiles are returned by GET /api/v1/scan-profiles and are defined in backend/configs/config.yaml.
Assets
| Method | Path | Description |
|---|---|---|
| GET | /organizations/{id}/assets |
List assets (paginated) |
| GET | /organizations/{id}/assets/graph |
Get asset graph |
| GET | /organizations/{id}/assets/stats |
Count by type |
| GET | /assets/{asset_id} |
Get single asset |
| PATCH | /assets/{asset_id}/criticality |
Manually update asset criticality (admin/hacker) |
| POST | /assets/{asset_id}/run-plugin |
Run one compatible plugin against one asset (admin/hacker) |
| GET | /plugins/manual-capabilities |
List registry-defined manual plugin capabilities (admin/hacker) |
Query params for list: type, criticality, page, page_size
Asset Criticality
Asset responses include criticality, criticality_source, criticality_updated_at, and criticality_updated_by. Values are unknown, low, medium, high, and critical; new assets default to unknown.
Criticality is business importance, not vulnerability severity. A low-severity issue on a critical VPN may be more important than a medium issue on a test host.
For this MVP, criticality is manual-only and criticality_source is always manual. hxEASM does not infer it from hostnames, banners, technologies, vulnerabilities, exposure changes, LLMs, agents, or heuristics. Suggested criticality and future risk scoring are planned future work.
PATCH /api/v1/assets/{asset_id}/criticality
Allowed roles: admin, hacker. Clients can read criticality but cannot modify it. The endpoint uses the same organization-scoped asset access checks as other asset endpoints.
Request:
{ "criticality": "high" }
Response: the updated asset.
POST /assets/{asset_id}/run-plugin
Launch a single plugin against a selected asset. This creates a normal queued scan with internal profile manual_scan and exactly one scan job. Allowed roles: admin, hacker. Clients are denied.
Request:
{ "plugin": "nuclei" }
Response:
{
"scan_id": "...",
"scan_job_id": "...",
"status": "queued"
}
If the plugin does not support the selected asset type, the API returns 400:
{ "error": "plugin katana does not support asset type ip" }
Manual scans appear in GET /organizations/{id}/scans like any other scan. Their scan metadata includes manual, asset_id, asset_type, asset_value, and plugin.
GET /plugins/manual-capabilities
Returns enabled plugins that declare manual_scan in supported_execution_modes. The frontend uses this endpoint to build the Run Plugin menu dynamically; there is no frontend plugin allowlist.
[
{
"id": "httpx",
"name": "httpx",
"type": "http_probe",
"supported_asset_types": ["domain", "subdomain", "ip", "service"],
"supported_execution_modes": ["profile_scan", "manual_scan", "retry"],
"manual_execution": true
}
]
Disabled plugins and plugins without manual_scan are omitted.
API key example: read:assets
Use an API key as a Bearer token. For example, with:
easm_b3d070d84cf7c121927d0bcbed23fcc287aaa29575f8d5dffe54919588458e0b
List assets for an organization:
curl "http://localhost:8080/api/v1/organizations/<organization_id>/assets?page=1&page_size=20" \
-H "Authorization: Bearer easm_b3d070d84cf7c121927d0bcbed23fcc287aaa29575f8d5dffe54919588458e0b"
Filter by asset type:
curl "http://localhost:8080/api/v1/organizations/<organization_id>/assets?type=domain&page=1&page_size=20" \
-H "Authorization: Bearer easm_b3d070d84cf7c121927d0bcbed23fcc287aaa29575f8d5dffe54919588458e0b"
Get the asset graph:
curl "http://localhost:8080/api/v1/organizations/<organization_id>/assets/graph" \
-H "Authorization: Bearer easm_b3d070d84cf7c121927d0bcbed23fcc287aaa29575f8d5dffe54919588458e0b"
Replace <organization_id> with the UUID of the organization to query.
Vulnerabilities
| Method | Path | Description |
|---|---|---|
| GET | /organizations/{id}/vulnerabilities |
List vulns (paginated) |
| GET | /organizations/{id}/vulnerabilities/stats |
Count by severity |
| GET | /vulnerabilities/{id} |
Get single vuln |
| PATCH | /vulnerabilities/{id}/status |
Update status |
| POST | /vulnerabilities/{id}/retest |
Request retest |
PATCH /vulnerabilities/{id}/status
{ "status": "confirmed", "note": "Verified manually" }
Valid status transitions:
- new → confirmed | false_positive
- confirmed → fixed | accepted_risk | retest_required
- fixed → reopened | retest_required
- false_positive / accepted_risk → reopened
Reports
| Method | Path | Description |
|---|---|---|
| GET | /organizations/{id}/reports |
List generated reports |
| POST | /organizations/{id}/reports |
Create report generation request |
| GET | /organizations/{id}/reports/{report_id} |
Get report metadata |
| GET | /organizations/{id}/reports/{report_id}/download |
Download generated report file |
Supported formats: json, csv, pdf, html.
Example request:
{
"format": "pdf",
"filters": {
"severity_min": "medium",
"scan_id": "<scan-uuid>"
}
}
PDF reports use the styled report renderer. HTML reports use the embedded template at backend/internal/reports/templates/easm_report.html.
Health check
GET /health
→ { "status": "ok", "time": "2026-04-29T…" }
Error format
All errors return JSON:
{ "error": "description" }
Exposure Changes
| Method | Path | Description |
|---|---|---|
| GET | /organizations/{id}/changes |
List exposure change timeline events |
| GET | /organizations/{id}/changes/summary |
Summary counters for recent changes |
| GET | /assets/{asset_id}/changes |
History for a single asset |
List query params: change_type, entity_type, severity, asset_id, vulnerability_id, scan_id, date_from, date_to, limit, offset.
Summary query params: days defaults to 7.
Example summary response:
{
"days": 7,
"total": 21,
"asset_discovered": 10,
"service_discovered": 4,
"url_discovered": 5,
"path_discovered": 0,
"certificate_discovered": 0,
"vulnerability_discovered": 2,
"file_collected": 0,
"critical": 1,
"high": 3
}
Exposure changes are read-only through the public API. They are generated internally when new assets, vulnerabilities, vulnerability status transitions, and file artifacts are persisted.