Multiplayer Red Team Mode
Multiplayer Red Team Mode
The following files were used as context for generating this wiki page:
- wshawk/team_engine.py
- wshawk/gui_bridge.py
- desktop/src/renderer.js
- desktop/src/index.html
- docs/V3.0.3_RELEASE_GUIDE.md
- CHANGELOG.md
Overview
Multiplayer Red Team Mode enables multiple WSHawk Desktop operators to join a shared assessment session tied to a single gui_bridge.py backend server. All scan events, vulnerability findings, and endpoint discoveries broadcast instantly to every connected operator. Shared notes are collaboratively edited with 300ms debounced synchronization. A live operator roster shows who is online, their assigned color, and which tool tab they are currently using.
This replaces the common pattern of red teamers sharing raw curl commands on Slack, independently running the same scans, and manually reconciling findings in a shared spreadsheet at the end of the assessment.
For information about the backend engine that powers the room system, see team_engine.py. For information about how findings are shared across all operators, see Activity Feed.
Use Case
In a typical enterprise assessment, two or three operators work against the same target simultaneously:
- Operator 1 runs endpoint discovery and feeds discovered WebSocket URLs to the team.
- Operator 2 runs the Payload Blaster against those endpoints and broadcasts findings as they are confirmed.
- Operator 3 monitors the interceptor, manually edits frames, and documents operational notes.
Without Multiplayer Mode, each operator works from a separate local tool state. With it, all three share a single activity feed, a synchronized endpoint list, and a single collaborative notes document — all in real-time, without any external service.
Architecture
graph TB
subgraph "Operator A — WSHawk Desktop"
A_UI["Desktop UI"]
A_SIO["Socket.IO Client"]
end
subgraph "Operator B — WSHawk Desktop"
B_UI["Desktop UI"]
B_SIO["Socket.IO Client"]
end
subgraph "Shared Backend (gui_bridge.py)"
SIO_Server["Socket.IO Server"]
TeamEngine["TeamEngine"]
Rooms["Room Registry\n{code -> TeamRoom}"]
SidMap["SID → Room Map"]
end
A_SIO <--> SIO_Server
B_SIO <--> SIO_Server
SIO_Server --> TeamEngine
TeamEngine --> Rooms
TeamEngine --> SidMap
Key design point: The TeamEngine class (in wshawk/team_engine.py) is completely framework-agnostic. It manages state and returns plain Python data. The transport layer in gui_bridge.py is responsible for emitting Socket.IO events and returning HTTP responses. This keeps the room logic testable independently of the web server.
Sources: team_engine.py: TeamEngine class, gui_bridge.py: team Socket.IO handlers
Team Engine Architecture
Room Lifecycle
stateDiagram-v2
[*] --> Active: create_room(creator_name, target)
Active --> Active: join_room(code, sid, name)
Active --> Active: leave_room(sid) — operators > 0
Active --> [*]: leave_room(sid) — last operator leaves
Active --> [*]: destroy_room(code) — forced cleanup
An empty room is automatically destroyed when the last operator disconnects. Room codes are 6-character uppercase alphanumeric strings generated from a charset that excludes visually confusable characters (O, 0, I, 1, L).
Room Code Generation:
charset = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789'
Collision detection ensures duplicate codes are regenerated before the room is registered.
TeamRoom State
| Field | Type | Description |
|-------|------|-------------|
| code | str | 6-character room identifier |
| created_at | str | ISO 8601 timestamp |
| created_by | str | Name of the creating operator |
| target | str | Assessment target (optional, for display purposes) |
| operators | Dict[str, Operator] | SID → Operator map |
| shared_notes | str | Current state of the collaborative notes |
| shared_endpoints | List[Dict] | Endpoints added by any operator |
| activity_log | List[ActivityEntry] | Full audit trail of team events |
Operator State
Each connected operator is represented by an Operator object:
| Field | Type | Description |
|-------|------|-------------|
| sid | str | Socket.IO session ID |
| name | str | Display name chosen at join time |
| color | str | Hex color assigned from the palette |
| joined_at | str | ISO 8601 join timestamp |
| cursor | Optional[Dict] | Current cursor position (for notes live cursors) |
| active_tab | str | Which Desktop tab the operator is on |
Color Palette:
Colors are assigned from a curated set optimized for high contrast on dark backgrounds. No two adjacent operators share a visually confusable color:
#3b82f6 blue
#ef4444 red
#22c55e green
#f59e0b amber
#8b5cf6 violet
#ec4899 pink
#06b6d4 cyan
#f97316 orange
#14b8a6 teal
#6366f1 indigo
Sources: team_engine.py: OPERATOR_COLORS, team_engine.py: TeamRoom class, team_engine.py: Operator class
Joining a Session
Creating a Room
Operator A starts WSHawk Desktop normally with a local or shared wshawk-bridge backend. In the Desktop UI, they open the Team tab and click Create Room. An optional target field accepts the WebSocket URL of the assessment target for display purposes.
The frontend calls the backend, which calls TeamEngine.create_room() and returns the 6-character room code. The code is displayed prominently for easy dictation to teammates.
Joining a Room
Operator B enters the room code in the Team tab's join field along with a display name. The frontend calls the backend join route, which calls TeamEngine.join_room(code, sid, name).
On successful join:
- The operator is added to the room with an assigned color.
- A
team_operator_joinedSocket.IO event is broadcast to all operators currently in the room. - The joining operator receives the full current room state: roster, activity log, shared notes, and shared endpoints.
Reconnect Behaviour
If an operator's Desktop loses its connection to the backend (network drop, backend restart), the Socket.IO client automatically reconnects. On reconnect, the operator re-joins their previous room if it still exists. The room state — notes, activity log, endpoints — is preserved in memory on the backend between brief reconnections.
Sources: team_engine.py: join_room(), team_engine.py: leave_room()
Activity Feed
The activity feed is the central broadcast channel for team events. Every scan action, vulnerability finding, and endpoint discovery by any operator is appended to the room's activity_log and broadcast as a Socket.IO event to all operators in the room.
Activity Entry Types
| Type | Triggered By | Broadcast Payload |
|------|-------------|------------------|
| join | Operator joins | Operator name, color, timestamp |
| leave | Operator disconnects | Operator name, color, timestamp |
| scan | Any tool run completes | Scan type, target, status, result count |
| finding | Vulnerability confirmed | Full finding object including CVSS score |
| endpoint | WebSocket endpoint discovered | URL, protocol, server software |
| note_update | Shared notes content changes | — (notes content synced separately) |
| cursor | Operator moves cursor in notes | Operator SID, line/column position |
Broadcasting from Tool Modules
Any tool in the Desktop can broadcast team events using the window.WSHawkTeam API exposed by the Team module in renderer.js:
// Broadcast a scan completion
window.WSHawkTeam.broadcastScanEvent({
scan_type: "DirectoryScanner",
target: "https://admin.corp.com",
status: "completed",
results_count: 14
});
// Broadcast a confirmed vulnerability
window.WSHawkTeam.broadcastFinding({
type: "Cross-Site WebSocket Hijacking",
severity: "HIGH",
cvss: 8.1,
url: "wss://corp.com/api/stream",
payload: "Origin: https://evil.com"
});
// Broadcast a discovered endpoint
window.WSHawkTeam.broadcastEndpoint({
url: "wss://corp.com/api/orders",
protocol: "json",
requires_auth: true
});
The Payload Blaster, Directory Scanner, Endpoint Mapper, and Subdomain Finder all call this API automatically when they produce results while a team session is active.
Sources: team_engine.py: log_scan_event(), team_engine.py: log_finding(), team_engine.py: add_endpoint()
Collaborative Notes
The Notes tab in the Desktop is a standard rich text editor. When a team session is active, it becomes a collaborative document shared across all operators in the room.
Sync Mechanism
- Debounce: The frontend sends a
team_notes_updateSocket.IO event 300ms after the operator stops typing. This prevents flooding the backend on every keystroke. - Broadcast: The backend updates
room.shared_notesand emits the new content to all other operators in the room. - Apply: Each receiving operator's Notes editor applies the incoming content without triggering its own update event, preventing echo loops.
Live Cursors
When an operator moves their cursor within the Notes editor, a team_cursor event is emitted (also debounced, at 100ms). Other operators see a colored cursor indicator at the same position, labeled with the operator's display name and styled with their assigned color.
sequenceDiagram
participant A as Operator A (alice)
participant Backend as gui_bridge.py
participant B as Operator B (bob)
A->>Backend: team_notes_update {content: "Testing CSWSH..."}
Backend->>Backend: room.shared_notes = content
Backend->>B: team_notes_broadcast {content, operator: "alice"}
B->>B: Apply content to Notes editor
A->>Backend: team_cursor {position: {line: 4, col: 12}, tab: "notes"}
Backend->>B: team_cursor_broadcast {sid: alice_sid, position, color: "#3b82f6"}
B->>B: Render alice's cursor at line 4, col 12
Sources: team_engine.py: update_notes(), team_engine.py: update_cursor()
Shared Endpoint List
When any operator runs the Endpoint Mapper, Subdomain Finder, or manually adds an endpoint via the Endpoint Map tab, the discovered endpoints are added to the room's shared_endpoints list and broadcast to all operators.
The shared endpoint list is visible in the Team tab as a unified table. Each row shows the endpoint URL, the operator who discovered it (with their color indicator), the discovery timestamp, and a quick-launch button that opens the endpoint directly in the Payload Blaster or Interceptor.
REST API and Socket.IO Events
HTTP Routes
| Method | Route | Description |
|--------|-------|-------------|
| POST | /team/create | Create a room. Body: {name, target} |
| POST | /team/join | Join a room. Body: {code, name} |
| POST | /team/leave | Leave current room. Body: {code, name} |
| GET | /team/rooms | List active rooms (admin view) |
| GET | /team/stats | Room count and total operator count |
Socket.IO Events (Server → Client)
| Event | Payload | Description |
|-------|---------|-------------|
| team_operator_joined | {operator: {...}, roster: [...]} | New operator joined the room |
| team_operator_left | {operator: {...}, roster: [...]} | Operator disconnected |
| team_activity | ActivityEntry | Any activity log entry |
| team_finding | Finding dict | Vulnerability confirmed by any operator |
| team_endpoint | Endpoint dict | Endpoint discovered by any operator |
| team_notes_broadcast | {content: str, operator: str} | Shared notes updated |
| team_cursor_broadcast | {sid, position, color, name} | Operator cursor moved in notes |
Socket.IO Events (Client → Server)
| Event | Payload | Description |
|-------|---------|-------------|
| team_notes_update | {content: str} | Operator updated notes content |
| team_cursor | {position: {...}, tab: str} | Cursor position update |
Security Considerations
Room codes are not secrets. They are 6-character codes with approximately 36^6 combinations — sufficient to prevent accidental collision but not designed to prevent a targeted attacker from guessing them. Room sessions should only be run on trusted local networks or over a VPN. Do not expose gui_bridge.py to the public internet during a team session.
No authentication on room join. Any operator who knows the room code and has access to the backend can join. Operators on a shared assessment should use a VPN or private network to restrict backend access.
Activity log is in-memory only. The team activity log is not written to disk or the WSHawk SQLite database. If the backend process is restarted, the room and all its history are lost. Export the activity log to the Report Generator before ending a session.
Operator Presence Display
The Team tab in the Desktop UI shows the live operator roster as a horizontal strip at the top of the tab. Each operator is represented by:
- A colored circle avatar generated from their display name initials.
- Their display name.
- A status indicator: which tool tab they are currently viewing.
- An online/offline pulse indicator.
The roster updates in real-time as operators join, leave, or switch tabs. Tab change events are emitted automatically by the Desktop's navigation handler whenever an operator clicks a different sidebar tab — no manual action required.
Summary
Multiplayer Red Team Mode eliminates the coordination overhead that slows down team engagements. Every discovery made by any operator is immediately visible to the entire team. The shared activity feed, collaborative notes, and synchronized endpoint list provide a level of situational awareness previously only available in expensive commercial platforms. The window.WSHawkTeam API makes the system extensible — any existing or future WSHawk tool can broadcast its findings to the team with a single function call.
Sources: team_engine.py, gui_bridge.py: team routes, docs/V3.0.3_RELEASE_GUIDE.md