diff --git a/REST-API-Reference.md b/REST-API-Reference.md new file mode 100644 index 0000000..09fd48e --- /dev/null +++ b/REST-API-Reference.md @@ -0,0 +1,456 @@ +# REST API Reference + +The DECNET web API is a FastAPI application mounted under the `/api/v1` prefix. +All JSON endpoints return `application/json` and use `ORJSONResponse` internally. +Authentication is JWT bearer (`Authorization: Bearer `) except where noted. + +Related pages: [Web dashboard](Web-Dashboard), [Environment variables](Environment-Variables). + +## Roles + +- `viewer` — read-only access to logs, attackers, stats, stream, health, bounty, deckies, config (non-admin fields). +- `admin` — everything `viewer` can do, plus mutating endpoints (deploy, mutate, user CRUD, config writes, artifact download). + +`require_viewer` admits both roles. `require_admin` is admin-only. `require_stream_viewer` +is the SSE variant of viewer (accepts the token via query string or header). + +## Endpoint summary + +| Method | Path | Auth | Summary | +|--------|------|------|---------| +| POST | `/auth/login` | public | Obtain a JWT access token | +| POST | `/auth/change-password` | JWT | Change current user's password | +| GET | `/logs` | viewer | Paginated log feed with filters | +| GET | `/logs/histogram` | viewer | Time-bucketed log counts | +| GET | `/bounty` | viewer | Paginated bounty vault | +| GET | `/stats` | viewer | Aggregate telemetry | +| GET | `/stream` | viewer | SSE live event stream | +| GET | `/deckies` | viewer | Full fleet inventory | +| POST | `/deckies/deploy` | admin | Deploy fleet from INI payload | +| POST | `/deckies/{decky_name}/mutate` | admin | Force immediate mutation | +| PUT | `/deckies/{decky_name}/mutate-interval` | admin | Set per-decky mutation interval | +| GET | `/attackers` | viewer | Paginated attacker profiles | +| GET | `/attackers/{uuid}` | viewer | Single attacker with behavior block | +| GET | `/attackers/{uuid}/commands` | viewer | Paginated per-attacker commands | +| GET | `/attackers/{uuid}/artifacts` | viewer | Captured file-drop log rows | +| GET | `/config` | viewer | Config (users list admin-only) | +| PUT | `/config/deployment-limit` | admin | Cap concurrent deckies | +| PUT | `/config/global-mutation-interval` | admin | Fleet-wide mutation cadence | +| POST | `/config/users` | admin | Create user | +| DELETE | `/config/users/{user_uuid}` | admin | Delete user | +| PUT | `/config/users/{user_uuid}/role` | admin | Change user role | +| PUT | `/config/users/{user_uuid}/reset-password` | admin | Force password reset | +| DELETE | `/config/reinit` | admin+dev | Purge logs and bounties (developer mode only) | +| GET | `/artifacts/{decky}/{stored_as}` | admin | Download captured SSH artifact bytes | +| GET | `/health` | viewer | Component health; returns 503 when critical fails | + +--- + +## Authentication + +### POST /auth/login + +Exchange username/password for a JWT. + +Request body (`LoginRequest`): + +```json +{ "username": "admin", "password": "hunter2" } +``` + +Response (`Token`): + +```json +{ + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "token_type": "bearer", + "must_change_password": false +} +``` + +Errors: `401` bad credentials, `422` validation. + +```bash +curl -s -X POST http://localhost:8000/api/v1/auth/login \ + -H 'Content-Type: application/json' \ + -d '{"username":"admin","password":"hunter2"}' +``` + +### POST /auth/change-password + +Rotate the calling user's own password. Clears `must_change_password`. + +Request body (`ChangePasswordRequest`): + +```json +{ "old_password": "hunter2", "new_password": "newpass123" } +``` + +Response: `{"message": "Password updated successfully"}` + +```bash +curl -s -X POST http://localhost:8000/api/v1/auth/change-password \ + -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ + -d '{"old_password":"hunter2","new_password":"newpass123"}' +``` + +--- + +## Logs + +### GET /logs + +Paginated log feed. Unfiltered total count is cached for 2 s. + +Query params: + +- `limit` (int, 1..1000, default 50) +- `offset` (int, 0.., default 0) +- `search` (str, optional) +- `start_time`, `end_time` (ISO-8601, optional) + +Response (`LogsResponse`): + +```json +{ + "total": 1423, + "limit": 50, + "offset": 0, + "data": [ + { + "id": 1423, + "timestamp": "2026-04-18T02:22:56Z", + "decky": "decky-03", + "service": "ssh", + "event_type": "auth_fail", + "attacker_ip": "203.0.113.9", + "msg": "Failed password for root", + "fields": "{\"user\":\"root\"}" + } + ] +} +``` + +```bash +curl -s -H "Authorization: Bearer $TOKEN" \ + 'http://localhost:8000/api/v1/logs?limit=50&search=ssh' +``` + +### GET /logs/histogram + +Time-bucketed event counts. Default unfiltered response is cached for 5 s. + +Query params: `search`, `start_time`, `end_time`, `interval_minutes` (int ge 1, default 15). + +Response: `list[{bucket, count}]`. + +```bash +curl -s -H "Authorization: Bearer $TOKEN" \ + 'http://localhost:8000/api/v1/logs/histogram?interval_minutes=5' +``` + +--- + +## Bounty + +### GET /bounty + +Paginated bounty vault (harvested credentials, payloads, etc.). +Default unfiltered page is cached for 5 s. + +Query params: `limit` (1..1000), `offset`, `bounty_type`, `search`. + +Response (`BountyResponse`): same shape as `/logs`. + +```bash +curl -s -H "Authorization: Bearer $TOKEN" \ + 'http://localhost:8000/api/v1/bounty?bounty_type=credential' +``` + +--- + +## Observability + +### GET /stats + +Aggregate telemetry. Cached for 5 s. + +Response (`StatsResponse`): + +```json +{ "total_logs": 14231, "unique_attackers": 47, "active_deckies": 5, "deployed_deckies": 5 } +``` + +```bash +curl -s -H "Authorization: Bearer $TOKEN" http://localhost:8000/api/v1/stats +``` + +### GET /stream + +Server-Sent Events stream. Emits `stats`, `histogram`, and `logs` messages. + +Query params: `lastEventId` (int), `search`, `start_time`, `end_time`, +`maxOutput` (developer mode only — terminates after N chunks). + +Each event is a JSON payload inside an SSE `data:` line, e.g.: + +``` +event: message +data: {"type":"logs","data":[{...}]} +``` + +```bash +curl -N -H "Authorization: Bearer $TOKEN" \ + 'http://localhost:8000/api/v1/stream?lastEventId=0' +``` + +### GET /health + +Component health report. Returns HTTP `503` when a critical component +(`database`, `docker`, `ingestion_worker`) is failing. + +Response (`HealthResponse`): + +```json +{ + "status": "healthy", + "components": { + "database": {"status": "ok"}, + "docker": {"status": "ok"}, + "ingestion_worker": {"status": "ok"}, + "collector_worker": {"status": "ok"}, + "attacker_worker": {"status": "ok"}, + "sniffer_worker": {"status": "ok"} + } +} +``` + +Overall tiers: `healthy` | `degraded` (non-critical failure) | `unhealthy`. + +```bash +curl -s -H "Authorization: Bearer $TOKEN" http://localhost:8000/api/v1/health +``` + +--- + +## Fleet + +### GET /deckies + +Full fleet inventory (cached 5 s). + +Response: `list[{name, ip, mac, services, status, mutate_interval, ...}]`. + +```bash +curl -s -H "Authorization: Bearer $TOKEN" http://localhost:8000/api/v1/deckies +``` + +### POST /deckies/deploy + +Deploy or extend the fleet from an INI payload. See [INI config format](INI-Config-Format). + +Request body (`DeployIniRequest`): + +```json +{ "ini_content": "[general]\ninterface=eth0\n\n[decky-01]\nservices=ssh,smb\n" } +``` + +Response: `{"message": "..."}` + +Errors: `409` network/INI conflict or deployment-limit exceeded, `422` +schema error, `500` deploy failure. + +```bash +curl -s -X POST -H "Authorization: Bearer $TOKEN" \ + -H 'Content-Type: application/json' \ + -d @deploy.json http://localhost:8000/api/v1/deckies/deploy +``` + +### POST /deckies/{decky_name}/mutate + +Force an immediate mutation of a single decky. Name must match `^[a-z0-9\-]{1,64}$`. + +Response: `{"message": "Successfully mutated decky-03"}` or `404`. + +```bash +curl -s -X POST -H "Authorization: Bearer $TOKEN" \ + http://localhost:8000/api/v1/deckies/decky-03/mutate +``` + +### PUT /deckies/{decky_name}/mutate-interval + +Set per-decky mutation cadence. `mutate_interval` is `` +where unit is `m` (minutes), `d` (days), `M` (months), `y`/`Y` (years). +Pass `null` to clear. + +Request body (`MutateIntervalRequest`): + +```json +{ "mutate_interval": "5d" } +``` + +Errors: `404` no active deployment, `422` invalid duration. + +```bash +curl -s -X PUT -H "Authorization: Bearer $TOKEN" \ + -H 'Content-Type: application/json' -d '{"mutate_interval":"5d"}' \ + http://localhost:8000/api/v1/deckies/decky-03/mutate-interval +``` + +--- + +## Attackers + +### GET /attackers + +Paginated attacker profiles, bulk-joined to their behavior block. + +Query params: `limit` (1..1000), `offset`, `search`, `service`, +`sort_by` (`recent` | `active` | `traversals`, default `recent`). + +Response (`AttackersResponse`): paginated envelope with +`data[i].behavior` populated for each IP. + +```bash +curl -s -H "Authorization: Bearer $TOKEN" \ + 'http://localhost:8000/api/v1/attackers?sort_by=active' +``` + +### GET /attackers/{uuid} + +Single attacker with `behavior` block attached. `404` when not found. + +```bash +curl -s -H "Authorization: Bearer $TOKEN" \ + http://localhost:8000/api/v1/attackers/$UUID +``` + +### GET /attackers/{uuid}/commands + +Paginated commands issued by an attacker. + +Query params: `limit` (1..200), `offset`, `service`. + +Response: `{total, limit, offset, data}`. + +```bash +curl -s -H "Authorization: Bearer $TOKEN" \ + "http://localhost:8000/api/v1/attackers/$UUID/commands?service=ssh" +``` + +### GET /attackers/{uuid}/artifacts + +List captured `file_captured` log rows for an attacker (newest first). + +Response: `{total, data: [log_row, ...]}`. + +```bash +curl -s -H "Authorization: Bearer $TOKEN" \ + "http://localhost:8000/api/v1/attackers/$UUID/artifacts" +``` + +--- + +## Configuration + +### GET /config + +Get current configuration. Admins additionally receive the full users list +and (in developer mode) a `developer_mode: true` flag. + +Response (viewer): + +```json +{ "role": "viewer", "deployment_limit": 10, "global_mutation_interval": "30m" } +``` + +Response (admin adds): + +```json +{ "users": [ { "uuid": "...", "username": "admin", "role": "admin", "must_change_password": false } ] } +``` + +```bash +curl -s -H "Authorization: Bearer $TOKEN" http://localhost:8000/api/v1/config +``` + +### PUT /config/deployment-limit + +Body (`DeploymentLimitRequest`): `{"deployment_limit": 25}` (int, 1..500). + +### PUT /config/global-mutation-interval + +Body (`GlobalMutationIntervalRequest`): `{"global_mutation_interval": "1d"}` +(pattern `^[1-9]\d*[mdMyY]$`). + +### POST /config/users + +Create a user. New users always land with `must_change_password=true`. + +Body (`CreateUserRequest`): + +```json +{ "username": "alice", "password": "initialpass1", "role": "viewer" } +``` + +Response (`UserResponse`): `{uuid, username, role, must_change_password}`. +Errors: `409` duplicate username. + +```bash +curl -s -X POST -H "Authorization: Bearer $TOKEN" \ + -H 'Content-Type: application/json' \ + -d '{"username":"alice","password":"initialpass1","role":"viewer"}' \ + http://localhost:8000/api/v1/config/users +``` + +### DELETE /config/users/{user_uuid} + +Delete a user. You cannot delete yourself (`403`). + +### PUT /config/users/{user_uuid}/role + +Body (`UpdateUserRoleRequest`): `{"role": "admin"}`. You cannot change +your own role (`403`). + +### PUT /config/users/{user_uuid}/reset-password + +Body (`ResetUserPasswordRequest`): `{"new_password": "newpass1234"}`. +Target user is forced into `must_change_password=true`. + +### DELETE /config/reinit + +Purge all logs and bounties. Requires admin **and** `DECNET_DEVELOPER=true`; +otherwise `403`. + +Response: `{"message": "Data purged", "deleted": {"logs": N, "bounties": M}}`. + +```bash +curl -s -X DELETE -H "Authorization: Bearer $TOKEN" \ + http://localhost:8000/api/v1/config/reinit +``` + +--- + +## Artifacts + +### GET /artifacts/{decky}/{stored_as} + +Download the raw bytes of a captured SSH drop. Admin-only because the +payloads are attacker-controlled content. + +Path parameters: + +- `decky` — `^[a-z0-9][a-z0-9-]{0,62}$` +- `stored_as` — `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z_[a-f0-9]{12}_[A-Za-z0-9._-]{1,255}$` + +The path is resolved under `DECNET_ARTIFACTS_ROOT` +(default `/var/lib/decnet/artifacts`) and confined via `resolve()` +to prevent traversal. + +Returns a `FileResponse` with `application/octet-stream`. +Errors: `400` invalid name, `404` missing file. + +```bash +curl -s -OJ -H "Authorization: Bearer $TOKEN" \ + "http://localhost:8000/api/v1/artifacts/decky-03/2026-04-18T02:22:56Z_abcdef012345_payload.sh" +```