Skip to content

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.