Add REST API reference (Unit 13)

2026-04-18 06:06:15 -04:00
parent f511c01b32
commit 45013a8167

456
REST-API-Reference.md Normal file

@@ -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 <token>`) 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 `<number><unit>`
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"
```