1
REST API Reference
anti edited this page 2026-04-18 06:06:15 -04:00

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, 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):

{ "username": "admin", "password": "hunter2" }

Response (Token):

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "bearer",
  "must_change_password": false
}

Errors: 401 bad credentials, 422 validation.

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):

{ "old_password": "hunter2", "new_password": "newpass123" }

Response: {"message": "Password updated successfully"}

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):

{
  "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\"}"
    }
  ]
}
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}].

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.

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):

{ "total_logs": 14231, "unique_attackers": 47, "active_deckies": 5, "deployed_deckies": 5 }
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":[{...}]}
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):

{
  "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.

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, ...}].

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.

Request body (DeployIniRequest):

{ "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.

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.

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):

{ "mutate_interval": "5d" }

Errors: 404 no active deployment, 422 invalid duration.

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.

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.

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}.

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, ...]}.

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):

{ "role": "viewer", "deployment_limit": 10, "global_mutation_interval": "30m" }

Response (admin adds):

{ "users": [ { "uuid": "...", "username": "admin", "role": "admin", "must_change_password": false } ] }
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):

{ "username": "alice", "password": "initialpass1", "role": "viewer" }

Response (UserResponse): {uuid, username, role, must_change_password}. Errors: 409 duplicate username.

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}}.

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.

curl -s -OJ -H "Authorization: Bearer $TOKEN" \
  "http://localhost:8000/api/v1/artifacts/decky-03/2026-04-18T02:22:56Z_abcdef012345_payload.sh"