From d42dc562a3bf6e079ad28fb613f5d95825e8e36c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Gr=C3=BCssing?= Date: Thu, 11 Jun 2026 10:19:43 +0200 Subject: [PATCH] Add RDP user consent popup before screen sharing When rdp_start arrives, agent first spawns a WPF consent dialog in the user session (via schtasks). User has 30s to accept or deny. On deny, agent sends rdp_denied to browser which shows "Zugriff abgelehnt". On accept, screen capture starts as before. Co-Authored-By: Claude Sonnet 4.6 --- agent-cs/ConsentModeRunner.cs | 21 +++ agent-cs/Program.cs | 4 + agent-cs/Services/RtcService.cs | 98 +++++++++++- agent-cs/UI/RdpConsentWindow.xaml | 142 ++++++++++++++++++ agent-cs/UI/RdpConsentWindow.xaml.cs | 61 ++++++++ .../components/common/RemoteDesktopPanel.jsx | 4 + 6 files changed, 328 insertions(+), 2 deletions(-) create mode 100644 agent-cs/ConsentModeRunner.cs create mode 100644 agent-cs/UI/RdpConsentWindow.xaml create mode 100644 agent-cs/UI/RdpConsentWindow.xaml.cs diff --git a/agent-cs/ConsentModeRunner.cs b/agent-cs/ConsentModeRunner.cs new file mode 100644 index 0000000..0c034b6 --- /dev/null +++ b/agent-cs/ConsentModeRunner.cs @@ -0,0 +1,21 @@ +using ITNexusAgent.UI; + +namespace ITNexusAgent; + +// Läuft als User-Prozess (via schtasks), zeigt Consent-Dialog und sendet Antwort via TCP +public static class ConsentModeRunner +{ + public static void Run(string portStr) + { + if (!int.TryParse(portStr, out var port) || port <= 0) return; + + var app = new System.Windows.Application(); + app.ShutdownMode = System.Windows.ShutdownMode.OnMainWindowClose; + + var win = new RdpConsentWindow(port); + win.Topmost = true; + win.Show(); + win.Activate(); + app.Run(); + } +} diff --git a/agent-cs/Program.cs b/agent-cs/Program.cs index bbe3d78..1a16e6a 100644 --- a/agent-cs/Program.cs +++ b/agent-cs/Program.cs @@ -19,6 +19,10 @@ internal class Program RunNotification(args.Length > 1 ? args[1] : ""); return; + case "--rdp-consent": + ConsentModeRunner.Run(args.Length > 1 ? args[1] : ""); + return; + case "--rdp-capture": CaptureModeRunner.Run( args.Length > 1 ? args[1] : "", diff --git a/agent-cs/Services/RtcService.cs b/agent-cs/Services/RtcService.cs index e83d5f5..c10e85a 100644 --- a/agent-cs/Services/RtcService.cs +++ b/agent-cs/Services/RtcService.cs @@ -67,8 +67,8 @@ public class RtcService { var screenIdx = obj["screen"]?.ToObject() ?? 0; captureCts = CancellationTokenSource.CreateLinkedTokenSource(ct); - _ = CapturePipeLoopAsync(ws, screenIdx, captureCts.Token); - AgentWorker.Log($"RDP: Screen-Capture gestartet (screen={screenIdx})"); + _ = ConsentAndCaptureAsync(ws, screenIdx, captureCts.Token); + AgentWorker.Log($"RDP: Consent angefordert (screen={screenIdx})"); } else if (type == "rdp_stop" && captureCts != null) { @@ -82,6 +82,100 @@ public class RtcService AgentWorker.Log("RDP: Getrennt"); } + private async Task ConsentAndCaptureAsync(ClientWebSocket ws, int screenIdx, CancellationToken ct) + { + var exePath = System.Diagnostics.Process.GetCurrentProcess().MainModule!.FileName; + + var consentListener = new TcpListener(IPAddress.Loopback, 0); + consentListener.Start(); + var consentPort = ((IPEndPoint)consentListener.LocalEndpoint).Port; + + if (!SpawnConsentHelper(exePath, consentPort.ToString())) + { + consentListener.Stop(); + AgentWorker.Log("RDP: Consent-Helper fehlgeschlagen, starte ohne Consent"); + await CapturePipeLoopAsync(ws, screenIdx, ct); + return; + } + + bool accepted = false; + try + { + using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(35)); + using var linked = CancellationTokenSource.CreateLinkedTokenSource(ct, timeoutCts.Token); + using var tcp = await consentListener.AcceptTcpClientAsync(linked.Token); + var b = tcp.GetStream().ReadByte(); + accepted = b == 1; + } + catch { accepted = false; } + finally { consentListener.Stop(); } + + if (!accepted) + { + AgentWorker.Log("RDP: User hat Zugriff abgelehnt"); + var denied = Encoding.UTF8.GetBytes("{\"type\":\"rdp_denied\"}"); + if (ws.State == WebSocketState.Open) + await ws.SendAsync(new ArraySegment(denied), WebSocketMessageType.Text, true, CancellationToken.None); + return; + } + + AgentWorker.Log("RDP: User hat Zugriff erlaubt, starte Capture"); + await CapturePipeLoopAsync(ws, screenIdx, ct); + } + + private static bool SpawnConsentHelper(string exePath, string portStr) + { + try + { + var fullUser = NotificationService.GetLoggedOnUser(); + if (string.IsNullOrEmpty(fullUser)) + { + AgentWorker.Log("RDP: Kein eingeloggter User für Consent"); + return false; + } + + var taskName = $"ITNexus-RDPConsent-{portStr}"; + Process.Start(new ProcessStartInfo("schtasks.exe", $"/delete /tn \"{taskName}\" /f") + { CreateNoWindow = true })?.WaitForExit(); + + var triggerTime = DateTime.Now.AddMinutes(60).ToString("HH:mm:ss"); + var ruArg = fullUser.StartsWith("AzureAD\\", StringComparison.OrdinalIgnoreCase) + ? "/ru \"INTERACTIVE\"" + : $"/ru \"{fullUser}\""; + + var args = $"/create /tn \"{taskName}\" /tr \"\\\"{exePath}\\\" --rdp-consent {portStr}\" " + + $"/sc ONCE /st {triggerTime} {ruArg} /it /f"; + + var p = Process.Start(new ProcessStartInfo("schtasks.exe", args) + { CreateNoWindow = true, RedirectStandardError = true, UseShellExecute = false }); + p?.WaitForExit(); + + if (p?.ExitCode != 0) + { + AgentWorker.Log($"RDP: schtasks consent fehlgeschlagen (ExitCode={p?.ExitCode})"); + return false; + } + + Process.Start(new ProcessStartInfo("schtasks.exe", $"/run /tn \"{taskName}\"") + { CreateNoWindow = true })?.WaitForExit(); + + _ = Task.Run(async () => + { + await Task.Delay(40000); + Process.Start(new ProcessStartInfo("schtasks.exe", $"/delete /tn \"{taskName}\" /f") + { CreateNoWindow = true })?.WaitForExit(); + }); + + AgentWorker.Log($"RDP: Consent-Helper gestartet als '{fullUser}'"); + return true; + } + catch (Exception ex) + { + AgentWorker.Log($"RDP: SpawnConsentHelper Fehler: {ex.Message}"); + return false; + } + } + private async Task CapturePipeLoopAsync(ClientWebSocket ws, int screenIdx, CancellationToken ct) { var exePath = System.Diagnostics.Process.GetCurrentProcess().MainModule!.FileName; diff --git a/agent-cs/UI/RdpConsentWindow.xaml b/agent-cs/UI/RdpConsentWindow.xaml new file mode 100644 index 0000000..b61689a --- /dev/null +++ b/agent-cs/UI/RdpConsentWindow.xaml @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Ein IT-Administrator möchte sich mit Ihrem Bildschirm verbinden. + Ihr Bildschirminhalt wird dabei übertragen und ist für die IT sichtbar. + + + Lehnen Sie ab, falls Sie diese Anfrage nicht erwartet haben oder + gerade sensible Daten auf dem Bildschirm haben. + + + + + + + + + + + + + + + + + + + + + + diff --git a/agent-cs/UI/RdpConsentWindow.xaml.cs b/agent-cs/UI/RdpConsentWindow.xaml.cs new file mode 100644 index 0000000..6b4c5a7 --- /dev/null +++ b/agent-cs/UI/RdpConsentWindow.xaml.cs @@ -0,0 +1,61 @@ +using System.Net.Sockets; +using System.Windows; + +namespace ITNexusAgent.UI; + +public partial class RdpConsentWindow : Window +{ + private readonly int _port; + private bool _answered = false; + private System.Threading.CancellationTokenSource _countdownCts = new(); + + public RdpConsentWindow(int port) + { + InitializeComponent(); + _port = port; + _ = RunCountdownAsync(_countdownCts.Token); + } + + private async Task RunCountdownAsync(System.Threading.CancellationToken ct) + { + for (int i = 30; i > 0; i--) + { + if (ct.IsCancellationRequested) return; + Dispatcher.Invoke(() => CountdownText.Text = $"Automatische Ablehnung in {i} Sekunden"); + await Task.Delay(1000, ct).ContinueWith(_ => { }); + } + if (!_answered) + { + SendResponse(false); + Dispatcher.Invoke(Close); + } + } + + private void AcceptButton_Click(object sender, RoutedEventArgs e) + { + _countdownCts.Cancel(); + SendResponse(true); + Close(); + } + + private void DenyButton_Click(object sender, RoutedEventArgs e) + { + _countdownCts.Cancel(); + SendResponse(false); + Close(); + } + + private void SendResponse(bool accepted) + { + if (_answered) return; + _answered = true; + try + { + using var tcp = new TcpClient(); + tcp.Connect("127.0.0.1", _port); + tcp.GetStream().WriteByte((byte)(accepted ? 1 : 0)); + tcp.GetStream().Flush(); + } + catch { } + } +} diff --git a/frontend/src/components/common/RemoteDesktopPanel.jsx b/frontend/src/components/common/RemoteDesktopPanel.jsx index f2a1385..2958553 100644 --- a/frontend/src/components/common/RemoteDesktopPanel.jsx +++ b/frontend/src/components/common/RemoteDesktopPanel.jsx @@ -55,6 +55,10 @@ export default function RemoteDesktopPanel({ agentId, agentHostname, autoConnect } }; img.src = 'data:image/jpeg;base64,' + msg.data; + } else if (msg.type === 'rdp_denied') { + setStatus('error'); + setError('Zugriff wurde vom Benutzer abgelehnt'); + ws.close(); } else if (msg.type === 'rdp_disconnected') { setStatus('idle'); setError('Agent hat die Verbindung getrennt');