WebSocket Protocol¶
Stentor uses WebSocket connections for real-time bidirectional communication between system components. Two independent WebSocket hubs handle different concerns:
- CockpitHub streams events to operator UI clients (beacon updates, task output, console logs)
- RelayHub manages relay agent connections (commands, heartbeats, C2 events)
Both hubs use JSON message framing over standard WebSocket connections (RFC 6455). This page documents the connection procedures, message formats, event types, and recovery mechanisms for each hub.
CockpitHub (Operator WebSocket)¶
CockpitHub is the real-time event stream between the Stentor server and operator browser clients. Every operator UI session opens a WebSocket connection to receive beacon updates, task progress, console output, and system events as they happen.
Connection¶
Endpoint: GET /api/v1/cockpit/ws
CockpitHub supports two authentication methods. Ticket-based auth is the recommended approach -- it avoids exposing long-lived JWTs in WebSocket URLs (which may appear in server logs and proxy caches).
First, obtain a short-lived ticket by calling the REST API with your access token:
# Step 1: Get a WebSocket ticket (valid for 30 seconds, single-use)
TICKET=$(curl -s -X POST https://stentor.app/api/v1/auth/ws-ticket \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq -r '.ticket')
# Step 2: Connect with the ticket
wscat -c "wss://stentor.app/api/v1/cockpit/ws?ticket=$TICKET"
// JavaScript: Ticket-based WebSocket connection
async function connectCockpit(apiUrl, accessToken) {
// 1. Fetch a single-use ticket
const res = await fetch(`${apiUrl}/v1/auth/ws-ticket`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${accessToken}` },
});
const { ticket } = await res.json();
// 2. Connect using the ticket (not the JWT)
const wsUrl = apiUrl.replace(/^http/, 'ws');
const ws = new WebSocket(`${wsUrl}/v1/cockpit/ws?ticket=${ticket}`);
ws.onopen = () => console.log('Connected to CockpitHub');
ws.onmessage = (e) => {
const event = JSON.parse(e.data);
console.log(`[seq=${event.seq}] ${event.type}:`, event.payload);
};
return ws;
}
Ticket Properties
- Single-use: Each ticket can only be used once (consumed on validation)
- 30-second TTL: Tickets expire 30 seconds after generation
- Opaque: Tickets are 64-character hex strings (32 random bytes)
- Background cleanup runs every 60 seconds to purge expired tickets
Error Responses¶
If authentication fails, the server responds with a JSON error before the WebSocket upgrade:
| Code | Error | Description |
|---|---|---|
TICKET_EXPIRED | ticket expired | Ticket TTL exceeded (30s) |
TICKET_NOT_FOUND | ticket not found | Ticket already used or never existed |
TICKET_INVALID | invalid ticket | Ticket validation error |
TOKEN_EXPIRED | token expired | JWT access token expired |
TOKEN_INVALID_SIGNATURE | invalid token signature | JWT signature verification failed |
TOKEN_WRONG_TYPE | invalid token type | Token is not an access token |
TOKEN_INVALID | invalid token | General JWT validation failure |
AUTH_MISSING | ticket or token required | No authentication provided |
Message Format¶
All messages from CockpitHub are SequencedEvents -- each event is wrapped with a monotonically increasing sequence number:
{
"seq": 42,
"type": "beacon_update",
"payload": {
"beacon_id": "a1b2c3d4-...",
"hostname": "WORKSTATION-01",
"username": "CORP\\jsmith",
"status": "active"
}
}
| Field | Type | Description |
|---|---|---|
seq | uint64 | Monotonically increasing sequence number (starts at 1) |
type | string | Event type identifier (see Event Types below) |
payload | object | Type-specific payload (varies per event type) |
The seq field enables clients to detect dropped messages. If your client receives seq=40 followed by seq=43, sequences 41 and 42 were dropped (see Drop Recovery).
Event Types¶
CockpitHub broadcasts the following event types to all connected operator clients.
beacon_update¶
A beacon's state has changed (new beacon registered, checkin, or status change).
{
"seq": 1,
"type": "beacon_update",
"payload": {
"beacon_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"machine_ip": "10.0.0.20",
"hostname": "WORKSTATION-01",
"username": "CORP\\jsmith",
"pid": 4832,
"last_seen": "2026-02-19T10:00:00Z",
"status": "active"
}
}
Payload Schema
| Field | Type | Description |
|---|---|---|
beacon_id | string | Beacon UUID |
machine_ip | string | Target machine IP address |
hostname | string | Target hostname |
username | string | User context (DOMAIN\user format) |
pid | int | Process ID of the beacon |
last_seen | string | ISO 8601 timestamp of last check-in |
status | string | "active" or "lost" |
console_log¶
A log entry for the operator console (command input/output, info messages, errors).
{
"seq": 2,
"type": "console_log",
"payload": {
"tab_id": "beacon-a1b2c3d4",
"timestamp": "2026-02-19T10:00:05Z",
"type": "output",
"text": "Directory listing of C:\\Users\\jsmith\\Desktop..."
}
}
Payload Schema
| Field | Type | Description |
|---|---|---|
tab_id | string | Target console tab: "event-log" for global log, "beacon-{id}" for beacon-specific |
timestamp | string | ISO 8601 timestamp |
type | string | Entry type: "input", "output", "info", "success", "error" |
text | string | Log message content |
task_started¶
A task has been dispatched to a beacon for execution.
{
"seq": 3,
"type": "task_started",
"payload": {
"task_id": "f1e2d3c4-...",
"beacon_id": "a1b2c3d4-...",
"technique": "shell",
"status": "started"
}
}
Payload Schema
| Field | Type | Description |
|---|---|---|
task_id | string | Task UUID |
beacon_id | string | Target beacon UUID |
technique | string | Command/technique name (e.g., "shell", "ls", "hashdump") |
status | string | Always "started" for this event |
task_completed¶
A task has finished execution on a beacon.
{
"seq": 4,
"type": "task_completed",
"payload": {
"task_id": "f1e2d3c4-...",
"beacon_id": "a1b2c3d4-...",
"technique": "shell",
"status": "completed",
"output": "Volume in drive C has no label..."
}
}
Payload Schema
| Field | Type | Description |
|---|---|---|
task_id | string | Task UUID |
beacon_id | string | Target beacon UUID |
technique | string | Command/technique name |
status | string | "completed" or "failed" |
output | string | Command output (empty if no output) |
pivot_link¶
A lateral movement link has been established between two machines (rendered as edges in the pivot graph).
{
"seq": 5,
"type": "pivot_link",
"payload": {
"from": "machine-001",
"to": "machine-002",
"type": "pivot",
"method": "smb"
}
}
Payload Schema
| Field | Type | Description |
|---|---|---|
from | string | Source node ID |
to | string | Target node ID |
type | string | Link type: "initial" (C2 link) or "pivot" (lateral movement) |
method | string | Movement method: "smb", "wmi", "winrm", "dcom" (empty for initial links) |
lateral_movement_completed¶
A lateral movement operation has started, completed, or failed (triggers edge animations in the topology view).
{
"seq": 6,
"type": "lateral_movement_completed",
"payload": {
"task_id": "f1e2d3c4-...",
"source_beacon_id": "a1b2c3d4-...",
"source_machine_id": "machine-001",
"target_ip": "10.10.10.50",
"method": "psexec",
"status": "completed"
}
}
Payload Schema
| Field | Type | Description |
|---|---|---|
task_id | string | Task UUID |
source_beacon_id | string | Beacon initiating the movement |
source_machine_id | string | Source machine identifier |
target_ip | string | Target machine IP |
method | string | Lateral movement method (e.g., "psexec", "wmi", "winrm", "dcom") |
status | string | "started", "completed", or "failed" |
shell_output¶
Real-time output from an interactive shell session (routes to the correct xterm.js terminal tab).
{
"seq": 7,
"type": "shell_output",
"payload": {
"beacon_id": "a1b2c3d4-...",
"output": "C:\\Users\\jsmith> ",
"is_error": false,
"timestamp": "2026-02-19T10:01:00Z"
}
}
Payload Schema
| Field | Type | Description |
|---|---|---|
beacon_id | string | Beacon UUID (routes output to correct terminal tab) |
output | string | Shell output text |
is_error | bool | Whether this is stderr output |
timestamp | string | ISO 8601 timestamp |
file_transfer_progress¶
Progress update for an ongoing file upload or download operation.
{
"seq": 8,
"type": "file_transfer_progress",
"payload": {
"transfer_id": "t1r2a3n4-...",
"beacon_id": "a1b2c3d4-...",
"direction": "download",
"filename": "secrets.docx",
"progress": 0.65,
"bytes_done": 6553600,
"bytes_total": 10085376,
"status": "in_progress",
"sha256": ""
}
}
Payload Schema
| Field | Type | Description |
|---|---|---|
transfer_id | string | Unique transfer identifier (correlates progress events) |
beacon_id | string | Beacon UUID |
direction | string | "download" (beacon to server) or "upload" (server to beacon) |
filename | string | Original filename |
progress | float | Completion ratio (0.0 to 1.0) |
bytes_done | int64 | Bytes transferred so far |
bytes_total | int64 | Total file size in bytes |
status | string | "in_progress", "completed", or "failed" |
sha256 | string | File hash (populated on completion) |
beacon_highlight_update¶
An operator has changed a beacon's highlight color (visual marker for collaboration).
{
"seq": 9,
"type": "beacon_highlight_update",
"payload": {
"beacon_id": "a1b2c3d4-...",
"color": "#ef4444"
}
}
Payload Schema
| Field | Type | Description |
|---|---|---|
beacon_id | string | Beacon UUID |
color | string | CSS color value (hex), or empty string when highlight is removed |
dialog_request¶
A CNA script is requesting operator input via a dialog prompt (prompt_text, prompt_confirm, prompt_file, dialog_show).
{
"seq": 10,
"type": "dialog_request",
"payload": {
"dialog_type": "prompt_text",
"title": "Enter target username",
"message": "Provide the username for lateral movement:",
"default_value": "administrator"
}
}
navigate¶
A CNA script is requesting navigation to a specific page (openBeaconConsole, openPayloadDialog, etc.).
{
"seq": 11,
"type": "navigate",
"payload": {
"target": "beacon_console",
"beacon_id": "a1b2c3d4-..."
}
}
cna_tab¶
A CNA script is creating a custom visualization tab (addTab, showVisualization).
{
"seq": 12,
"type": "cna_tab",
"payload": {
"tab_id": "custom-viz-1",
"title": "Attack Graph",
"content_type": "html",
"content": "<div>...</div>"
}
}
siem_alert¶
A SIEM alert forwarded to operator clients for real-time awareness.
{
"seq": 13,
"type": "siem_alert",
"payload": {
"rule": "Suspicious PowerShell execution",
"severity": "high",
"host": "WORKSTATION-01",
"timestamp": "2026-02-19T10:02:00Z"
}
}
dropped_messages¶
Sent when the server detects that messages were dropped for this client (see Drop Recovery).
{
"seq": 50,
"type": "dropped_messages",
"payload": {
"last_sent_seq": 42,
"current_seq": 50,
"dropped_count": 8
}
}
Payload Schema
| Field | Type | Description |
|---|---|---|
last_sent_seq | uint64 | Last sequence number successfully sent to this client |
current_seq | uint64 | Current server sequence number |
dropped_count | uint64 | Number of messages that were dropped |
Drop Recovery¶
CockpitHub uses a bounded send buffer (1024 messages per client). When a client's buffer is full (e.g., a slow connection or heavy event volume), the server cannot deliver new events. Instead of silently discarding messages, the server:
- Sends a
dropped_messagesevent with the gap range (last_sent_seq through current_seq) - If even the drop notification cannot be delivered, the client is disconnected
When your client receives a dropped_messages event, the recommended recovery strategy is:
ws.onmessage = (e) => {
const event = JSON.parse(e.data);
if (event.type === 'dropped_messages') {
console.warn(
`Dropped ${event.payload.dropped_count} messages ` +
`(seq ${event.payload.last_sent_seq + 1} - ${event.payload.current_seq})`
);
// Full data refresh -- re-fetch all mutable state from REST API
refreshBeacons();
refreshTasks();
refreshCredentials();
return;
}
// Normal event processing
handleEvent(event);
};
Sequence Number Tracking
Track the last received seq value. On reconnection, compare it with the server's current sequence (available via the REST API) to determine if a refresh is needed.
Client-to-Server Messages¶
CockpitHub also accepts messages from connected clients. The ReadPump processes incoming JSON messages and routes them by type.
tunnel_local_response¶
Tunnel frames sent back from operator browsers for rportfwd_local (reverse port forward through the operator's network).
{
"type": "tunnel_local_response",
"beacon_id": "a1b2c3d4-...",
"bind_port": 8080,
"frames": [
{
"conn_id": 1,
"type": 2,
"data": "<base64>"
}
]
}
| Field | Type | Description |
|---|---|---|
type | string | Always "tunnel_local_response" |
beacon_id | string | Beacon UUID that owns the reverse port forward |
bind_port | uint16 | Local bind port on the operator's machine |
frames | array | Tunnel frames (connection data flowing back to the beacon) |
RelayHub (Relay WebSocket)¶
RelayHub manages persistent WebSocket connections between the Stentor server and relay agents. Relays are Linux processes (typically running on Kali) that host C2 listeners, process beacon check-ins, and execute payload generation.
Connection¶
Endpoint: GET /ws/relay
Relays authenticate using a shared secret and their registered UUID. Both can be provided via headers (preferred) or query parameters.
Prerequisites¶
- Relay must exist in the database -- The server validates the relay UUID against the
relaystable. If the relay is not registered, the connection returns HTTP 404. - Shared secret must match -- The
X-Relay-Secretheader (orsecretquery param) must match the server's configuredRELAY_SECRETenvironment variable.
Error Responses¶
| Status | Error | Description |
|---|---|---|
| 400 | relay_id required | No relay ID provided |
| 400 | invalid relay_id format | Relay ID is not a valid UUID |
| 401 | invalid relay secret | Secret does not match server configuration |
| 404 | relay not found | Relay UUID not registered in database |
| 500 | internal error | Database query failure |
Database Registration Required
After a database rebuild, relay records are lost. You must re-insert the relay into the relays table before the relay can reconnect:
Message Format¶
All RelayHub messages use the RelayMessage envelope:
{
"type": "command",
"id": "550e8400-e29b-41d4-a716-446655440000",
"correlation_id": null,
"timestamp": "2026-02-19T10:00:00Z",
"payload": { ... }
}
| Field | Type | Description |
|---|---|---|
type | string | Message type: "command", "response", "heartbeat", "event", "ack" |
id | uuid | Unique message identifier |
correlation_id | uuid? | For responses: the id of the original command (enables request/response matching) |
timestamp | string | ISO 8601 creation timestamp |
payload | object | Type-specific payload (see sections below) |
Message Types¶
command (Backend to Relay)¶
Commands are sent from the server to a relay to initiate actions (start listeners, queue tasks, generate payloads).
{
"type": "command",
"id": "cmd-uuid",
"timestamp": "2026-02-19T10:00:00Z",
"payload": {
"command": "start_listener",
"args": {
"listener_id": "6ea88162-...",
"name": "HTTPS Relay",
"type": "https",
"bind_address": "0.0.0.0",
"port": 8443
}
}
}
Payload Schema (RelayCommandPayload):
| Field | Type | Description |
|---|---|---|
command | string | Command name (see Command Reference) |
args | object | Command-specific arguments (JSON object) |
response (Relay to Backend)¶
Responses carry the result of a previously received command. The correlation_id links the response to the original command's id.
{
"type": "response",
"id": "resp-uuid",
"correlation_id": "cmd-uuid",
"timestamp": "2026-02-19T10:00:01Z",
"payload": {
"success": true,
"error": "",
"data": { "listener_id": "6ea88162-...", "status": "running" }
}
}
Payload Schema (RelayResponsePayload):
| Field | Type | Description |
|---|---|---|
success | bool | Whether the command succeeded |
error | string | Error message (empty on success) |
data | object? | Response data (command-specific, present on success) |
heartbeat (Bidirectional)¶
Health monitoring messages sent in both directions to maintain the connection and report relay status.
{
"type": "heartbeat",
"id": "hb-uuid",
"timestamp": "2026-02-19T10:00:30Z",
"payload": {
"relay_id": "aaaaaaaa-...",
"status": "idle"
}
}
Payload Schema (RelayHeartbeatPayload):
| Field | Type | Description |
|---|---|---|
relay_id | string | Relay instance UUID |
status | string | Current relay state: "idle", "busy", or "error" |
event (Relay to Backend)¶
Asynchronous events from the relay to the server (beacon registrations, task completions, phishing activity).
{
"type": "event",
"id": "evt-uuid",
"timestamp": "2026-02-19T10:01:00Z",
"payload": {
"event": "beacon_new",
"data": {
"beacon_id": "a1b2c3d4-...",
"hostname": "WORKSTATION-01",
"username": "jsmith",
"ip": "10.0.0.20",
"os": "Windows 10",
"arch": "x64",
"pid": 4832
}
}
}
Payload Schema (RelayEventPayload):
| Field | Type | Description |
|---|---|---|
event | string | Event name (see Event Reference) |
data | object | Event-specific data |
ack (Relay to Backend)¶
Acknowledgment sent by the relay immediately upon receiving a command, before execution begins. This confirms receipt while the actual result arrives later as a response.
{
"type": "ack",
"id": "ack-uuid",
"timestamp": "2026-02-19T10:00:00Z",
"payload": {
"command_id": "cmd-uuid",
"received": "2026-02-19T10:00:00Z"
}
}
Payload Schema (RelayAckPayload):
| Field | Type | Description |
|---|---|---|
command_id | uuid | ID of the command being acknowledged |
received | string | ISO 8601 timestamp when command was received |
Command Reference¶
Commands sent from the Stentor server to relay agents. Each command is wrapped in a RelayCommandPayload with the command name and arguments.
Listener Management¶
| Command | Description | Key Arguments |
|---|---|---|
start_listener | Start a C2 listener on the relay | listener_id, name, type, bind_address, port, profile_name, tls_cert, tls_key, dns_domain, smb_pipe_name |
stop_listener | Stop a running listener | listener_id |
host_file | Host a file at a custom URI on a listener | listener_id, uri, data, content_type, filename |
unhost_file | Remove a hosted file from a listener | listener_id, uri |
start_listener Arguments (StartListenerArgs)
| Field | Type | Description |
|---|---|---|
listener_id | string | Listener UUID |
name | string | Display name |
type | string | Protocol: "http", "https", "dns", "smb_pipe" |
bind_address | string | Listen address (e.g., "0.0.0.0") |
port | int | Listen port |
profile_name | string? | Malleable C2 profile name |
profile_config | object? | Profile configuration JSON |
tls_cert | string? | TLS certificate PEM (for HTTPS) |
tls_key | string? | TLS private key PEM (for HTTPS) |
dns_domain | string? | DNS domain (for DNS listeners) |
dns_ttl | int? | DNS record TTL |
dns_resolver | string? | Custom DNS resolver |
smb_pipe_name | string? | Named pipe name (for SMB listeners) |
beacon_port | int? | Port override for beacon callback |
guardrails | object? | Execution guardrail rules |
wg_private_key | string? | WireGuard private key (for VPN) |
wg_peer_public_key | string? | WireGuard peer public key |
wg_tunnel_ip | string? | WireGuard tunnel IP |
wg_peer_tunnel_ip | string? | WireGuard peer tunnel IP |
wg_c2_port | int? | WireGuard C2 port |
Task Operations¶
| Command | Description | Key Arguments |
|---|---|---|
queue_task | Queue a task for beacon delivery on next check-in | beacon_id, task_type, task_data |
tunnel | Send SOCKS tunnel frames to a beacon | beacon_id, frames[] |
covertvpn | Manage covert VPN TAP interface | (VPN-specific args) |
queue_task Arguments (QueueTaskPayload)
| Field | Type | Description |
|---|---|---|
task_id | uuid? | Optional: Backend-provided task ID for correlation |
beacon_id | uuid | Target beacon UUID |
task_type | string | Task type (e.g., "shell", "upload", "inject") |
task_data | object | Task-specific arguments |
Payload Generation¶
| Command | Description | Key Arguments |
|---|---|---|
generate_payload | Generate a payload binary (EXE, DLL, shellcode, etc.) | type, c2_url, obfuscate, profile, evasion_options, architecture |
generate_clr_shellcode | Generate CLR loader shellcode for PowerShell | assembly, script, arch |
generate_assembly_shellcode | Generate shellcode for .NET assembly execution | assembly, args, arch |
generate_browser_pivot_shellcode | Generate WinINet proxy shellcode | pipe_name, arch |
sign_payload | Authenticode-sign a payload binary | payload, cert_pfx, password |
generate_payload Arguments (GeneratePayloadArgs)
| Field | Type | Description |
|---|---|---|
type | string | Payload type: "exe", "dll", "shellcode", "lnk", "docm", "xlsm", "iso", "hta", "html_smuggling", "stager", "service", "msi" |
c2_url | string | C2 callback URL |
obfuscate | bool | Use garble for unique binary hash per build |
profile | string | Malleable C2 profile name |
architecture | string? | Target arch: "x64", "x86" |
sleep | int? | Beacon sleep interval (seconds) |
jitter | int? | Jitter percentage (0-100) |
evasion_options | object? | AMSI bypass, ETW patch, sleep obfuscation toggles |
guardrail_config | object? | Execution restrictions (IP, hostname, domain, username patterns) |
kill_date | string? | RFC 3339 payload expiration date |
exit_func | string? | Exit function: "process" or "thread" |
Additional type-specific configs: iso_config, hta_config, html_smuggling_config, stager_config, dll_config, service_config, shellcode_config, msi_config, udrl_config, drip_config, proxy_config, host_rotation_config, sleep_mask_config, udc2_config.
Phishing Operations¶
| Command | Description | Key Arguments |
|---|---|---|
send_email | Send a phishing email via the relay | template_id, from_email, to_email, tracking_id, tracking_base_url |
preview_email | Preview a rendered email template | template_id, target_email |
host_payload | Host a payload on the tracking server | payload_id, data, content_type |
Event Reference¶
Events sent from relay agents to the Stentor server. Each event is wrapped in a RelayEventPayload.
Beacon Lifecycle Events¶
| Event | Description | Key Data Fields |
|---|---|---|
beacon_new | New beacon registered from a target | beacon_id, hostname, username, ip, os, arch, pid, listener_id |
beacon_checkin | Existing beacon checked in | beacon_id, hostname, username, ip, os, arch, pid |
beacon_dead | Beacon missed check-ins and is marked dead | beacon_id |
beacon_new / beacon_checkin Payload (BeaconEventPayload)
| Field | Type | Description |
|---|---|---|
beacon_id | uuid | Beacon UUID |
hostname | string | Target hostname |
username | string | User context |
ip | string | Target IP address |
os | string | Operating system (e.g., "Windows 10") |
arch | string | Architecture ("x64", "x86") |
pid | int | Process ID |
listener_id | string? | Originating listener UUID |
Task Events¶
| Event | Description | Key Data Fields |
|---|---|---|
task_complete | Task execution finished on a beacon | task_id, beacon_id, success, output, error, artifacts[] |
task_complete Payload (TaskCompletePayload)
| Field | Type | Description |
|---|---|---|
task_id | uuid | Task UUID |
beacon_id | uuid | Beacon that executed the task |
task_type | string? | Task type (optional for backward compat) |
success | bool | Whether execution succeeded |
output | string | Command output |
error | string? | Error message (on failure) |
artifacts | array? | Forensic artifacts produced (files, registry keys) |
Listener Lifecycle Events¶
| Event | Description | Key Data Fields |
|---|---|---|
listener_started | Listener successfully started | listener_id, status, port |
listener_stopped | Listener stopped | listener_id, status |
listener_error | Listener encountered an error | listener_id, status, error |
Listener Status Payload (ListenerStatusPayload)
| Field | Type | Description |
|---|---|---|
listener_id | string | Listener UUID |
status | string | "running", "stopped", or "error" |
error | string? | Error description (for listener_error) |
port | int? | Actual bound port (for listener_started) |
Tunnel Events¶
| Event | Description | Key Data Fields |
|---|---|---|
tunnel_response | SOCKS tunnel frames from a beacon | beacon_id, frames[] |
Tunnel Event Payload (TunnelEventPayload)
| Field | Type | Description |
|---|---|---|
beacon_id | uuid | Beacon UUID |
frames | array | Array of TunnelFrame objects |
TunnelFrame:
| Field | Type | Description |
|---|---|---|
conn_id | uint32 | Connection multiplexer ID |
type | uint8 | Frame type: 1=connect, 2=data, 3=close, 4=error |
data | bytes | Payload (address for connect, raw bytes for data) |
Guardrail Events¶
| Event | Description | Key Data Fields |
|---|---|---|
guardrail_violation | A guardrail rule was triggered on the relay | Rule-specific violation details |
Phishing Events¶
| Event | Description | Key Data Fields |
|---|---|---|
email_sent | Phishing email sent successfully | tracking_id, to_email, timestamp, success |
email_opened | Tracking pixel loaded by target | tracking_id, timestamp, user_agent, ip |
link_clicked | Phishing link clicked by target | tracking_id, timestamp, user_agent, ip |
creds_captured | Credentials captured from landing page | tracking_id, username, password, timestamp |
payload_downloaded | Payload file downloaded by target | payload_id, timestamp, user_agent, ip |
keystroke_captured | Keystrokes captured from cloned site | tracking_id, keystrokes, timestamp |
CNA Events¶
| Event | Description | Key Data Fields |
|---|---|---|
profiler_hit | System Profiler visit collected | external_ip, internal_ip, user_agent, platform, applications, token |
web_hit | HTTP request served by web server | method, uri, remote_addr, user_agent, response_code, response_size, handler |
Synchronous Commands¶
Some operations require waiting for a relay's response before proceeding (e.g., payload generation must return the binary data). The SendCommandSync mechanism implements synchronous request/response over the asynchronous WebSocket channel.
sequenceDiagram
participant Server as Stentor Server
participant Relay as Relay Agent
Server->>Server: Create command with unique ID
Server->>Server: Register pending request (ID -> response channel)
Server->>Relay: Send command message
Relay->>Server: Send ack (command_id)
Note over Relay: Execute command...
Relay->>Server: Send response (correlation_id = command ID)
Server->>Server: Match correlation_id to pending request
Server->>Server: Deliver response via channel
Note over Server: Returns data to caller Timeout handling: If the relay does not respond within the specified timeout (typically 30-60 seconds for payload generation), the request returns an ErrRelayTimeout error. The pending request is cleaned up regardless of outcome.
Correlation matching: The relay includes the original command's id as the correlation_id in its response. The server's handleMessage function checks incoming responses against the pendingRequests map -- if a match is found, the response is delivered to the waiting goroutine and the pending entry is removed.
Connection Lifecycle¶
CockpitHub Lifecycle¶
sequenceDiagram
participant Client as Operator Browser
participant API as REST API
participant Hub as CockpitHub
Client->>API: POST /v1/auth/login
API-->>Client: access_token
Client->>API: POST /v1/auth/ws-ticket
Note over Client: Authorization: Bearer <token>
API-->>Client: { ticket: "a3f2..." }
Client->>Hub: GET /v1/cockpit/ws?ticket=a3f2...
Hub->>Hub: Validate ticket (single-use, 30s TTL)
Hub-->>Client: 101 Switching Protocols
Hub->>Hub: Register client, fire event_join CNA event
loop Real-time Events
Hub->>Client: SequencedEvent { seq, type, payload }
end
Hub->>Client: Ping (every 54s)
Client->>Hub: Pong (must respond within 60s)
alt Client Send Buffer Full
Hub->>Client: dropped_messages event
Note over Client: Re-fetch all data from REST API
end
alt Disconnection
Hub->>Hub: Unregister client, fire event_quit CNA event
Hub->>Hub: Clean up operator tunnel forwards
end Reconnection strategy: When the WebSocket connection drops, clients should:
- Wait briefly (exponential backoff with jitter: initial 1s, max 30s)
- Request a new ticket via
POST /v1/auth/ws-ticket - Reconnect with the new ticket
- Compare the last received
seqwith the server's current sequence - If there is a gap, perform a full data refresh
RelayHub Lifecycle¶
sequenceDiagram
participant DB as PostgreSQL
participant Relay as Relay Agent
participant Hub as RelayHub
Note over DB: Relay must be registered in relays table
Relay->>Hub: GET /ws/relay
Note over Relay: X-Relay-ID + X-Relay-Secret headers
Hub->>Hub: Validate secret
Hub->>DB: GetByID(relay_uuid)
DB-->>Hub: Relay record (or 404)
Hub-->>Relay: 101 Switching Protocols
Hub->>DB: UpdateStatus(relay_id, "connected")
Hub->>Hub: Register client (replaces existing if same ID)
loop Heartbeat Loop
Relay->>Hub: heartbeat { relay_id, status: "idle" }
end
loop Commands & Events
Hub->>Relay: command { command: "start_listener", args: {...} }
Relay->>Hub: ack { command_id: "..." }
Relay->>Hub: response { correlation_id: "...", success: true }
Relay->>Hub: event { event: "beacon_new", data: {...} }
end
alt Disconnection
Hub->>Hub: Unregister client
Hub->>DB: UpdateStatus(relay_id, "disconnected")
end Relay reconnection: If the relay process restarts or the WebSocket connection drops:
- The relay attempts to reconnect with the same UUID and secret
- If a client with the same ID is already registered, the old connection is replaced
- The relay status is updated to "connected" in the database
- Active listeners must be re-started after reconnection (state is not preserved)