Add REST API reference (Unit 13)
456
REST-API-Reference.md
Normal file
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"
|
||||
```
|
||||
Reference in New Issue
Block a user