From 4d44291a6e7bc8a29e15b6c872f607286ebc862a Mon Sep 17 00:00:00 2001 From: enzok <7831008+enzok@users.noreply.github.com> Date: Thu, 4 Jun 2026 12:06:05 -0400 Subject: [PATCH 01/26] add direct vnc access to vm --- web/guac/consumers.py | 142 ++++++++++++++++++++--------- web/guac/routing.py | 2 +- web/guac/templates/guac/index.html | 18 ++++ web/guac/templates/guac/wait.html | 10 ++ web/guac/urls.py | 5 +- web/guac/views.py | 112 +++++++++++++++++++++++ 6 files changed, 243 insertions(+), 46 deletions(-) diff --git a/web/guac/consumers.py b/web/guac/consumers.py index 99944ea9562..635b3a3a3b2 100644 --- a/web/guac/consumers.py +++ b/web/guac/consumers.py @@ -35,6 +35,7 @@ def _get_vnc_port(vm_label): """Look up VNC port for a VM from libvirt. Must be called from sync context.""" if not LIBVIRT_AVAILABLE: return None + conn = None try: conn = libvirt.open(machinery_dsn) @@ -139,24 +140,47 @@ async def connect(self): self.guac_task_id = session_data["task_id"] vm_label = session_data["vm_label"] - # 3. Verify task can still host an interactive session - task = await sync_to_async(db.view_task)(self.guac_task_id) - if not task or task.status not in ACTIVE_GUAC_TASK_STATUSES: - logger.warning( - "WebSocket rejected: task %s is not active for guac", self.guac_task_id - ) - await self._delete_guac_session() - await self.close() - return - - # 4. Look up VNC port server-side from libvirt - vnc_port = await sync_to_async(_get_vnc_port)(vm_label) - if not vnc_port: - logger.warning( - "WebSocket rejected: no VNC port for VM %s", vm_label - ) - await self.close() - return + vnc_port = None + if self.guac_task_id > 0: + # 3. Verify task can still host an interactive session + task = await sync_to_async(db.view_task)(self.guac_task_id) + if not task or task.status not in ACTIVE_GUAC_TASK_STATUSES: + logger.warning( + "WebSocket rejected: task %s is not active for guac", self.guac_task_id + ) + await self._delete_guac_session() + await self.close() + return + + # 4. Look up VNC port server-side from libvirt + vnc_port = await sync_to_async(_get_vnc_port)(vm_label) + if not vnc_port: + logger.warning( + "WebSocket rejected: no VNC port for VM %s", vm_label + ) + await self.close() + return + else: + # Direct VNC connection + guest_ip = session_data.get("guest_ip") + if not guest_ip: + # Autodiscover port given just the VM name + vnc_port = await sync_to_async(_get_vnc_port)(vm_label) + if not vnc_port: + logger.warning( + "WebSocket rejected: could not autodiscover VNC port for VM %s", vm_label + ) + await self.close() + return + else: + try: + vnc_port = int(vm_label) + except ValueError: + logger.warning( + "WebSocket rejected: invalid direct VNC port %s", vm_label + ) + await self.close() + return # 5. Parse config guacd_hostname = web_cfg.guacamole.guacd_host or "localhost" @@ -174,22 +198,38 @@ async def connect(self): raw_recording = params.get("recording_name", ["task-recording"])[0] guacd_recording_name = re.sub(r"[^a-zA-Z0-9_-]", "", raw_recording) - if "rdp" in guest_protocol: - guest_host = session_data.get("guest_ip", vm_label) - if not guest_host: - guest_host = vm_label - guest_port = int(web_cfg.guacamole.guest_rdp_port) or 3389 - ignore_cert = ( - "true" - if web_cfg.guacamole.ignore_rdp_cert is True - else "false" - ) - extra_args = { - "disable-wallpaper": "true", - "disable-theming": "true", - } + if self.guac_task_id > 0: + if "rdp" in guest_protocol: + guest_host = session_data.get("guest_ip", vm_label) + if not guest_host: + guest_host = vm_label + guest_port = int(web_cfg.guacamole.guest_rdp_port) or 3389 + ignore_cert = ( + "true" + if web_cfg.guacamole.ignore_rdp_cert is True + else "false" + ) + extra_args = { + "disable-wallpaper": "true", + "disable-theming": "true", + } + else: + guest_host = web_cfg.guacamole.vnc_host or "localhost" + guest_port = vnc_port + ignore_cert = "false" + vnc_color_depth = str( + getattr(web_cfg.guacamole, "vnc_color_depth", 16) + ) + vnc_cursor = getattr(web_cfg.guacamole, "vnc_cursor", "local") + extra_args = { + "color-depth": vnc_color_depth, + "cursor": vnc_cursor, + } else: - guest_host = web_cfg.guacamole.vnc_host or "localhost" + # Direct VNC connection + guest_protocol = "vnc" + guest_ip = session_data.get("guest_ip") + guest_host = guest_ip or web_cfg.guacamole.vnc_host or "localhost" guest_port = vnc_port ignore_cert = "false" vnc_color_depth = str( @@ -204,6 +244,16 @@ async def connect(self): # 6. Connect to guacd self.client = GuacamoleClient(guacd_hostname, guacd_port) + logger.info( + "Guacamole connecting to guacd at %s:%s. Handshake: protocol=%s, host=%s, port=%s, recording_name=%s", + guacd_hostname, + guacd_port, + guest_protocol, + guest_host, + guest_port, + guacd_recording_name, + ) + await sync_to_async(self.client.handshake)( protocol=guest_protocol, width=guest_width, @@ -227,21 +277,25 @@ async def connect(self): ) # 7. Initialize timeout handling - try: - vm_ip = session_data.get("guest_ip") or guest_host - self.timeout_manager = SessionTimeoutManager( - vm_ip=vm_ip, - user="unknown_user", - session_id=self.guac_token, - task_id=str(self.guac_task_id), - ) - except Exception as e: - logger.error("Failed to initialize timeout manager: %s", e) + if self.guac_task_id > 0: + try: + vm_ip = session_data.get("guest_ip") or guest_host + self.timeout_manager = SessionTimeoutManager( + vm_ip=vm_ip, + user="unknown_user", + session_id=self.guac_token, + task_id=str(self.guac_task_id), + ) + except Exception as e: + logger.error("Failed to initialize timeout manager: %s", e) + self.timeout_manager = None + else: self.timeout_manager = None # 8. Start background tasks self.task = asyncio.create_task(self.read_guacd()) - self.monitor_task = asyncio.create_task(self.monitor_task_status()) + if self.guac_task_id > 0: + self.monitor_task = asyncio.create_task(self.monitor_task_status()) if self.timeout_manager and self.timeout_manager.idle_timeout_seconds > 0: self.timeout_task = asyncio.create_task(self.monitor_timeout()) else: diff --git a/web/guac/routing.py b/web/guac/routing.py index 6ea31022a96..b9a97c0effb 100644 --- a/web/guac/routing.py +++ b/web/guac/routing.py @@ -4,7 +4,7 @@ websocket_urlpatterns = [ re_path( - r"^guac/websocket-tunnel/(?P\w+)/?$", + r"^guac/websocket-tunnel/(?P[\w-]+)/?$", GuacamoleWebSocketConsumer.as_asgi(), ), ] diff --git a/web/guac/templates/guac/index.html b/web/guac/templates/guac/index.html index 87ba55825e6..53d4e6c58d6 100644 --- a/web/guac/templates/guac/index.html +++ b/web/guac/templates/guac/index.html @@ -23,11 +23,19 @@ CAPE Sandbox | + {% if task_id and task_id != 0 and task_id != '0' %} Task #{{ task_id }} + {% else %} + Direct VNC + {{ vnc_host }}:{{ vnc_port }} + + {% endif %}
@@ -35,15 +43,25 @@
Session Ended

+ {% if task_id and task_id != 0 and task_id != '0' %} View Task + {% else %} + Go Home + {% endif %}
diff --git a/web/guac/templates/guac/wait.html b/web/guac/templates/guac/wait.html index 508bc80609d..ac6a20c6234 100644 --- a/web/guac/templates/guac/wait.html +++ b/web/guac/templates/guac/wait.html @@ -29,9 +29,15 @@

Hang on...

+ {% if task_id and task_id != 0 and task_id != '0' %} + {% else %} + + {% endif %} @@ -40,7 +46,11 @@

Hang on...

diff --git a/web/guac/urls.py b/web/guac/urls.py index dbf41e6f560..21664bfe0d7 100644 --- a/web/guac/urls.py +++ b/web/guac/urls.py @@ -1,7 +1,10 @@ -from django.urls import re_path +from django.urls import path, re_path from guac import views urlpatterns = [ re_path(r"^(?P\d+)/(?P[\w=]+)/$", views.index, name="index"), + path("direct/vnc///", views.direct_vnc_host_port, name="direct_vnc_host_port"), + path("direct/vnc//", views.direct_vnc_vm, name="direct_vnc_vm"), ] + diff --git a/web/guac/views.py b/web/guac/views.py index 7b12c44f215..b59d7c9920d 100644 --- a/web/guac/views.py +++ b/web/guac/views.py @@ -104,3 +104,115 @@ def index(request, task_id, session_data): conn.close() except Exception: pass + + +@conditional_login_required(login_required, settings.WEB_AUTHENTICATION) +def direct_vnc_host_port(request, host, port): + token = uuid.uuid4() + try: + guac_session = db.create_guac_session( + token=token, + task_id=0, + vm_label=str(port), + guest_ip=host, + ) + except Exception as e: + return _error(request, 0, f"Failed to create Guacamole session: {e}") + + clean_host = "".join(c for c in host if c.isalnum() or c in ".-_") + recording_name = f"direct_{clean_host}_{port}_{str(token)[:8]}" + + response = render(request, "guac/index.html", { + "session_id": str(token), + "task_id": 0, + "recording_name": recording_name, + "vnc_host": host, + "vnc_port": port, + }) + + response.set_cookie( + "guac_session", + str(guac_session.token), + httponly=True, + secure=request.is_secure(), + samesite="Lax", + path="/guac/", + ) + + return response + + +@conditional_login_required(login_required, settings.WEB_AUTHENTICATION) +def direct_vnc_vm(request, vm_name): + if not LIBVIRT_AVAILABLE: + return _error(request, 0, "Libvirt not available") + + if machinery not in machinery_available: + return _error(request, 0, f"Machinery type '{machinery}' is not supported") + + is_running = False + vm_exists = False + error_msg = "" + + conn = None + try: + conn = libvirt.open(machinery_dsn) + if conn: + try: + dom = conn.lookupByName(vm_name) + if dom: + vm_exists = True + state = dom.state(flags=0) + is_running = state and state[0] == 1 + except Exception as e: + error_msg = str(e) + except Exception as e: + error_msg = str(e) + finally: + if conn: + try: + conn.close() + except Exception: + pass + + if error_msg and not vm_exists: + return _error(request, 0, error_msg) + + if not vm_exists: + return _error(request, 0, f"VM {vm_name} not found") + + if not is_running: + return render(request, "guac/wait.html", {"task_id": 0}) + + token = uuid.uuid4() + try: + guac_session = db.create_guac_session( + token=token, + task_id=0, + vm_label=vm_name, + guest_ip="", + ) + except Exception as e: + return _error(request, 0, f"Failed to create Guacamole session: {e}") + + recording_name = f"direct_{vm_name}_{str(token)[:8]}" + + response = render(request, "guac/index.html", { + "session_id": str(token), + "task_id": 0, + "recording_name": recording_name, + "vnc_host": vm_name, + "vnc_port": "auto", + }) + + response.set_cookie( + "guac_session", + str(guac_session.token), + httponly=True, + secure=request.is_secure(), + samesite="Lax", + path="/guac/", + ) + + return response + From e9f4a1773e6444396ac8183ce9f83f59b9280921 Mon Sep 17 00:00:00 2001 From: enzok <7831008+enzok@users.noreply.github.com> Date: Tue, 9 Jun 2026 12:50:56 -0400 Subject: [PATCH 02/26] web: Hide redundant jQuery UI titlebar in Guacamole console --- web/static/css/guac-main.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/web/static/css/guac-main.css b/web/static/css/guac-main.css index 9ef5fb19041..9e8a624e4a1 100644 --- a/web/static/css/guac-main.css +++ b/web/static/css/guac-main.css @@ -42,3 +42,8 @@ html, body { box-shadow: 0 0 30px rgba(220, 53, 69, 0.3); color: #fff; } + +.no-close .ui-dialog-titlebar { + display: none !important; +} + From 60e27bec881a1140358a649fb5fc251dc7949a52 Mon Sep 17 00:00:00 2001 From: enzok <7831008+enzok@users.noreply.github.com> Date: Tue, 9 Jun 2026 13:10:19 -0400 Subject: [PATCH 03/26] web: Add VNC Console dropdown option in header navigation bar --- conf/default/web.conf.default | 4 ++++ web/guac/context_processors.py | 21 +++++++++++++++++++++ web/templates/header.html | 14 ++++++++++++++ web/web/settings.py | 1 + 4 files changed, 40 insertions(+) create mode 100644 web/guac/context_processors.py diff --git a/conf/default/web.conf.default b/conf/default/web.conf.default index 5cb2648e239..68198224357 100644 --- a/conf/default/web.conf.default +++ b/conf/default/web.conf.default @@ -237,6 +237,10 @@ enabled = yes enabled = no [guacamole] +# Show a VNC Console dropdown of VM guests in the navigation bar +vnc_console_enabled = no +# Open VNC console sessions in a new tab +vnc_console_new_tab = yes enabled = no mode = vnc username = diff --git a/web/guac/context_processors.py b/web/guac/context_processors.py new file mode 100644 index 00000000000..2aabd0df8c8 --- /dev/null +++ b/web/guac/context_processors.py @@ -0,0 +1,21 @@ +from lib.cuckoo.common.config import Config +from lib.cuckoo.core.database import Database + +web_cfg = Config("web") + + +def guac_vnc_console(request): + """Context processor that exposes VNC Console settings and guests to templates.""" + enabled = web_cfg.guacamole.get("vnc_console_enabled", False) + if not enabled: + return {"vnc_console_enabled": False} + + db = Database() + machines = [machine.label for machine in db.list_machines(include_reserved=True)] + new_tab = web_cfg.guacamole.get("vnc_console_new_tab", True) + + return { + "vnc_console_enabled": True, + "vnc_console_machines": machines, + "vnc_console_new_tab": new_tab, + } diff --git a/web/templates/header.html b/web/templates/header.html index 11e7cd48e72..b7630a78b15 100644 --- a/web/templates/header.html +++ b/web/templates/header.html @@ -42,6 +42,20 @@ {% endif %} + {% if vnc_console_enabled %} + + {% endif %}