diff --git a/Data/Agent/Roles/ReverseTunnel/tunnel_Powershell.py b/Data/Agent/Roles/ReverseTunnel/tunnel_Powershell.py
index 528ed0fc..97cbd4d0 100644
--- a/Data/Agent/Roles/ReverseTunnel/tunnel_Powershell.py
+++ b/Data/Agent/Roles/ReverseTunnel/tunnel_Powershell.py
@@ -2,7 +2,6 @@
from __future__ import annotations
import asyncio
-import os
import sys
import subprocess
from typing import Any, Dict, Optional
@@ -30,7 +29,6 @@ class PowershellChannel:
self._reader_task = None
self._writer_task = None
self._stdin_queue: asyncio.Queue = asyncio.Queue()
- self._pty = None
self._proc: Optional[asyncio.subprocess.Process] = None
self._exit_code: Optional[int] = None
self._frame_cls = getattr(role, "_frame_cls", None)
@@ -70,13 +68,6 @@ class PowershellChannel:
# Keep the process alive and read commands from stdin; -Command - tells PS to consume stdin.
return [shell, "-NoLogo", "-NoProfile", "-NoExit", "-Command", "-"]
- def _initial_size(self) -> tuple:
- cols = int(self.metadata.get("cols") or self.metadata.get("columns") or 120) if isinstance(self.metadata, dict) else 120
- rows = int(self.metadata.get("rows") or 32) if isinstance(self.metadata, dict) else 32
- cols = max(20, min(cols, 300))
- rows = max(10, min(rows, 200))
- return cols, rows
-
# ------------------------------------------------------------------ Lifecycle
async def start(self) -> None:
if sys.platform.lower().startswith("win") is False:
@@ -85,25 +76,9 @@ class PowershellChannel:
return
argv = self._powershell_argv()
- cols, rows = self._initial_size()
- self.role._log(f"reverse_tunnel ps start channel={self.channel_id} argv={' '.join(argv)} cols={cols} rows={rows}")
+ self.role._log(f"reverse_tunnel ps start channel={self.channel_id} argv={' '.join(argv)} mode=pipes")
- # Preferred: ConPTY via pywinpty.
- try:
- import pywinpty # type: ignore
-
- self._pty = pywinpty.Process(
- spawn_cmd=" ".join(argv[:-1]) if argv[-1] == "-" else " ".join(argv),
- dimensions=(cols, rows),
- )
- self._reader_task = self.loop.create_task(self._pump_pty_stdout())
- self._writer_task = self.loop.create_task(self._pump_pty_stdin())
- self.role._log(f"reverse_tunnel ps channel started (pty) argv={' '.join(argv)} cols={cols} rows={rows}")
- return
- except Exception as exc:
- self.role._log(f"reverse_tunnel ps channel pywinpty unavailable, falling back to pipes: {exc}", error=True)
-
- # Fallback: subprocess pipes (no PTY).
+ # Pipes (no PTY).
try:
self._proc = await asyncio.create_subprocess_exec(
*argv,
@@ -119,7 +94,7 @@ class PowershellChannel:
self._reader_task = self.loop.create_task(self._pump_proc_stdout())
self._writer_task = self.loop.create_task(self._pump_proc_stdin())
- self.role._log(f"reverse_tunnel ps channel started (pipes) argv={' '.join(argv)} cols={cols} rows={rows}")
+ self.role._log(f"reverse_tunnel ps channel started (pipes) argv={' '.join(argv)}")
async def on_frame(self, frame) -> None:
if self._closed:
@@ -139,80 +114,8 @@ class PowershellChannel:
return
async def _handle_control(self, payload: bytes) -> None:
- try:
- import json
-
- data = json.loads(payload.decode("utf-8"))
- except Exception:
- return
- cols = data.get("cols") or data.get("columns")
- rows = data.get("rows")
- if cols is None and rows is None:
- return
- try:
- cols_int = int(cols) if cols is not None else None
- rows_int = int(rows) if rows is not None else None
- except Exception:
- return
- await self._resize(cols_int, rows_int)
-
- async def _resize(self, cols: Optional[int], rows: Optional[int]) -> None:
- # Resize only applies to PTY sessions; pipe mode ignores.
- if self._pty is None:
- return
- try:
- cur_cols, cur_rows = self._initial_size()
- if cols is None:
- cols = cur_cols
- if rows is None:
- rows = cur_rows
- cols = max(20, min(int(cols), 300))
- rows = max(10, min(int(rows), 200))
- self._pty.set_size(cols, rows)
- self.role._log(f"reverse_tunnel ps channel resized cols={cols} rows={rows}")
- except Exception:
- self.role._log("reverse_tunnel ps channel resize failed", error=True)
-
- async def _pump_pty_stdout(self) -> None:
- loop = asyncio.get_event_loop()
- try:
- while not self._closed and self._pty:
- chunk = await loop.run_in_executor(None, self._pty.read, 4096)
- if chunk is None:
- break
- data = chunk.encode("utf-8", errors="replace") if isinstance(chunk, str) else bytes(chunk)
- if not data:
- break
- frame = self._make_frame(MSG_DATA, payload=data)
- await self._send_frame(frame)
- except asyncio.CancelledError:
- pass
- except Exception:
- self.role._log("reverse_tunnel ps pty stdout pump error", error=True)
- finally:
- await self.stop(reason="stdout_closed")
-
- async def _pump_pty_stdin(self) -> None:
- loop = asyncio.get_event_loop()
- try:
- while not self._closed and self._pty:
- try:
- data = await self._stdin_queue.get()
- except asyncio.CancelledError:
- break
- if data is None:
- break
- text = data.decode("utf-8", errors="replace") if isinstance(data, (bytes, bytearray)) else str(data)
- try:
- await loop.run_in_executor(None, self._pty.write, text)
- except Exception:
- break
- except asyncio.CancelledError:
- pass
- except Exception:
- self.role._log("reverse_tunnel ps pty stdin pump error", error=True)
- finally:
- await self.stop(reason="stdin_closed")
+ # No-op for pipe mode; resize is not supported here.
+ return
# -------------------- Pipe fallback pumps --------------------
async def _pump_proc_stdout(self) -> None:
@@ -258,11 +161,6 @@ class PowershellChannel:
if self._closed:
return
self._closed = True
- if self._pty is not None:
- try:
- self._pty.terminate()
- except Exception:
- pass
if self._proc is not None:
try:
self._proc.terminate()
diff --git a/Data/Agent/agent-requirements.txt b/Data/Agent/agent-requirements.txt
index 8a152489..4a37c88f 100644
--- a/Data/Agent/agent-requirements.txt
+++ b/Data/Agent/agent-requirements.txt
@@ -27,7 +27,6 @@ pywinauto # Windows-based Macro Automation Library
sounddevice
numpy
pywin32; platform_system == "Windows"
-pywinpty; platform_system == "Windows" # ConPTY bridge for reverse tunnel PowerShell sessions
# Ansible Libraries
ansible-core
diff --git a/Data/Engine/web-interface/src/Devices/ReverseTunnel/Powershell.jsx b/Data/Engine/web-interface/src/Devices/ReverseTunnel/Powershell.jsx
index ec75dc15..c798c7f7 100644
--- a/Data/Engine/web-interface/src/Devices/ReverseTunnel/Powershell.jsx
+++ b/Data/Engine/web-interface/src/Devices/ReverseTunnel/Powershell.jsx
@@ -9,7 +9,6 @@ import {
MenuItem,
IconButton,
Tooltip,
- Alert,
LinearProgress,
} from "@mui/material";
import {
@@ -102,8 +101,8 @@ export default function ReverseTunnelPowershell({ device }) {
const [connectionType, setConnectionType] = useState("ps");
const [tunnel, setTunnel] = useState(null);
const [sessionState, setSessionState] = useState("idle");
- const [statusMessage, setStatusMessage] = useState("");
- const [statusSeverity, setStatusSeverity] = useState("info");
+ const [, setStatusMessage] = useState("");
+ const [, setStatusSeverity] = useState("info");
const [output, setOutput] = useState("");
const [input, setInput] = useState("");
const [copyFlash, setCopyFlash] = useState(false);
@@ -242,14 +241,8 @@ export default function ReverseTunnelPowershell({ device }) {
setPolling(true);
pollTimerRef.current = setTimeout(async () => {
const resp = await emitAsync(socket, "ps_poll", {});
- if (resp?.error) {
- if (resp.error === "ps_unsupported") {
- setStatusSeverity("info");
- setStatusMessage("PowerShell channel warming up...");
- } else {
- setStatusSeverity("warning");
- setStatusMessage(resp.error);
- }
+ if (resp?.error) {
+ // Suppress warming/errors in UI; rely on session chips.
}
if (Array.isArray(resp?.output) && resp.output.length) {
appendOutput(resp.output.join(""));
@@ -375,11 +368,9 @@ export default function ReverseTunnelPowershell({ device }) {
const dims = measureTerminal();
const openResp = await emitAsync(socket, "ps_open", dims);
if (openResp?.error && openResp.error === "ps_unsupported") {
- setStatusSeverity("info");
- setStatusMessage("PowerShell channel warming up...");
+ // Suppress warming message; channel will settle once agent attaches.
}
appendOutput("");
- setStatusMessage("Attached — waiting for agent to acknowledge...");
setSessionState("waiting_agent");
pollLoop(socket, lease.tunnel_id);
handleResize();
@@ -391,7 +382,7 @@ export default function ReverseTunnelPowershell({ device }) {
const requestTunnel = useCallback(async () => {
if (tunnel && sessionState !== "closed" && sessionState !== "idle") {
setStatusSeverity("info");
- setStatusMessage("Re-attaching to existing tunnel...");
+ setStatusMessage("");
connectSocket(tunnel);
return;
}
@@ -407,8 +398,8 @@ export default function ReverseTunnelPowershell({ device }) {
}
resetState();
setSessionState("requesting");
- setStatusSeverity("info");
- setStatusMessage("Requesting tunnel lease...");
+ setStatusSeverity("info");
+ setStatusMessage("");
try {
const resp = await fetch("/api/tunnel/request", {
method: "POST",
@@ -420,21 +411,17 @@ export default function ReverseTunnelPowershell({ device }) {
const err = data?.error || `HTTP ${resp.status}`;
setSessionState("error");
setStatusSeverity(err === "domain_limit" ? "warning" : "error");
- setStatusMessage(
- err === "domain_limit"
- ? "PowerShell session already active for this agent. Try again after it closes."
- : err
- );
+ setStatusMessage("");
return;
}
setTunnel(data);
- setStatusMessage("Lease issued. Waiting for agent to connect...");
+ setStatusMessage("");
setSessionState("lease_issued");
connectSocket(data);
} catch (e) {
setSessionState("error");
setStatusSeverity("error");
- setStatusMessage(e?.message || "Failed to request tunnel");
+ setStatusMessage("");
}
}, [agentId, connectSocket, connectionType, resetState]);
@@ -448,7 +435,7 @@ export default function ReverseTunnelPowershell({ device }) {
const resp = await emitAsync(socket, "ps_send", { data: payload });
if (resp?.error) {
setStatusSeverity("warning");
- setStatusMessage(resp.error);
+ setStatusMessage("");
}
},
[appendOutput, emitAsync]
@@ -583,20 +570,6 @@ export default function ReverseTunnelPowershell({ device }) {
- {statusMessage ? (
-
- {statusMessage}
-
- ) : null}
-