- WebSocket Shell/RDP: Rollenprüfung statt nur JWT-Gültigkeit (war: jeder eingeloggte User konnte fremde Agents per Shell/RDP übernehmen) - JWT_SECRET: Server bricht ab statt mit unsicherem Default weiterzulaufen - Auth: Token läuft jetzt über httpOnly-Cookie statt localStorage (XSS-Schutz gegen Session-Diebstahl) - WS-Auth: Token nicht mehr als URL-Query-Param (landete in nginx-Logs), sondern als erste Message bzw. automatisch via Cookie - Frontend: toter Rollen-Check (isSuperAdmin/isAdmin ohne Funktionsaufruf) in AgentDetailPage gefixt - XSS: DOMPurify-Sanitizing für alle marked.parse()-Renderstellen (KI-Antworten, Kommentare, Knowledge Base) - E-Mail: HTML-Escaping für alle ticket-gesteuerten Felder (auch über öffentliche Ticket-Route erreichbar) - SSRF-Schutz beim Knowledge-Base-URL-Import (blockt private/Loopback-Adressen) - TV-Dashboard: Shared-Key statt komplett offenem Endpoint - Striktes Rate-Limit auf /login, must_change_password serverseitig erzwungen - Agent (C#) v2.7.0: RDP-Consent/Disconnect/Capture verlangen jetzt ein Pro-Session-Secret (war: jeder lokale Prozess konnte Consent vortäuschen), DataDir-ACL für agent.log/status.json - FIDO-PINs AES-256-GCM-verschlüsselt statt Klartext, Retention-Job für alte patch_commands/audit_log Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
233 lines
9.5 KiB
C#
233 lines
9.5 KiB
C#
using ITNexusAgent.Models;
|
|
using ITNexusAgent.Services;
|
|
using Newtonsoft.Json;
|
|
|
|
namespace ITNexusAgent;
|
|
|
|
public class AgentWorker
|
|
{
|
|
private const string Version = "2.7.0";
|
|
private const string DataDir = @"C:\ProgramData\IT Nexus Agent";
|
|
private const string ConfigPath = @"C:\ProgramData\IT Nexus Agent\config.json";
|
|
private const string StatusPath = @"C:\ProgramData\IT Nexus Agent\status.json";
|
|
private const string LogPath = @"C:\ProgramData\IT Nexus Agent\agent.log";
|
|
|
|
private readonly CancellationToken _ct;
|
|
private AgentConfig? _config;
|
|
private ApiService? _api;
|
|
private NotificationService? _notifier;
|
|
private string _exePath = "";
|
|
|
|
public AgentWorker(CancellationToken ct) => _ct = ct;
|
|
|
|
public async Task RunAsync()
|
|
{
|
|
_exePath = System.Diagnostics.Process.GetCurrentProcess().MainModule!.FileName;
|
|
Directory.CreateDirectory(DataDir);
|
|
SecureDataDir(DataDir);
|
|
|
|
if (!File.Exists(ConfigPath))
|
|
{
|
|
Log("ERROR: config.json nicht gefunden");
|
|
return;
|
|
}
|
|
|
|
SecureConfigFile(ConfigPath);
|
|
|
|
_config = AgentConfig.Load(ConfigPath);
|
|
_api = new ApiService(_config.ServerUrl, _config.AgentKey);
|
|
_notifier = new NotificationService(_exePath, DataDir);
|
|
|
|
Log($"Agent v{Version} gestartet");
|
|
|
|
// WebSocket Shell-Service im Hintergrund starten
|
|
var shellService = new ShellService(_config.ServerUrl, _config.AgentKey, SystemInfoService.GetHostname());
|
|
_ = shellService.RunAsync(_ct);
|
|
|
|
// WebRTC Remote Desktop Service im Hintergrund starten
|
|
var rtcService = new RtcService(_config.ServerUrl, _config.AgentKey, SystemInfoService.GetHostname());
|
|
_ = rtcService.RunAsync(_ct);
|
|
|
|
while (!_ct.IsCancellationRequested)
|
|
{
|
|
await RunCycleAsync();
|
|
await Task.Delay(TimeSpan.FromMinutes(1), _ct).ContinueWith(_ => { });
|
|
}
|
|
}
|
|
|
|
private async Task RunCycleAsync()
|
|
{
|
|
try
|
|
{
|
|
Log("Sammle Systemdaten...");
|
|
var payload = CollectData();
|
|
var response = await _api!.CheckinAsync(payload);
|
|
|
|
Log($"OK: Checkin erfolgreich (Hostname: {payload.Hostname})");
|
|
|
|
// Status-Cache für Dashboard schreiben
|
|
WriteStatusCache(payload, response, true);
|
|
|
|
// Auto-Update prüfen
|
|
if (!string.IsNullOrEmpty(response.AgentVersion) &&
|
|
System.Version.TryParse(response.AgentVersion, out var serverVer) &&
|
|
System.Version.TryParse(Version, out var localVer) &&
|
|
serverVer > localVer)
|
|
{
|
|
Log($"UPDATE: Neue Agent-Version verfügbar: {response.AgentVersion} (aktuell: {Version})");
|
|
await new CommandExecutor(_api, payload.Hostname, DataDir, _exePath)
|
|
.ExecuteAsync(new PatchCommand { Id = 0, Command = "update_agent" });
|
|
return;
|
|
}
|
|
|
|
// Patch-Commands ausführen
|
|
var executor = new CommandExecutor(_api, payload.Hostname, DataDir, _exePath);
|
|
foreach (var cmd in response.Commands)
|
|
{
|
|
Log($"PATCH: Command empfangen: {cmd.Command}");
|
|
await executor.ExecuteAsync(cmd);
|
|
}
|
|
|
|
// Ankündigungen anzeigen + sofort ACKen damit Server sie nicht mehr schickt
|
|
if (response.Announcements.Count > 0)
|
|
{
|
|
_notifier!.ShowAnnouncements(response.Announcements);
|
|
foreach (var ann in response.Announcements)
|
|
{
|
|
Log($"ANNOUNCEMENT: Dialog für {SystemInfoService.GetLastUser()} gestartet - ID {ann.Id}");
|
|
try { await _api!.AckAnnouncementAsync(ann.Id, payload.Hostname); }
|
|
catch (Exception ex) { Log($"ANNOUNCEMENT ACK Fehler: {ex.Message}"); }
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log($"ERROR: {ex.Message}");
|
|
WriteStatusCache(null, null, false);
|
|
}
|
|
}
|
|
|
|
private CheckinPayload CollectData()
|
|
{
|
|
var (cpuModel, cpuCores) = SystemInfoService.GetCpuInfo();
|
|
var (ramTotal, ramUsed) = SystemInfoService.GetRamInfo();
|
|
var (diskTotal, diskFree) = SystemInfoService.GetDiskInfo();
|
|
var (tpmPresent, tpmVersion, tpmV2) = SystemInfoService.GetTpmInfo();
|
|
var (defEnabled, defSigAge, _) = SecurityInfoService.GetDefenderStatus();
|
|
|
|
return new CheckinPayload
|
|
{
|
|
Hostname = SystemInfoService.GetHostname(),
|
|
AgentVersion = Version,
|
|
Os = SystemInfoService.GetOs(),
|
|
OsVersion = SystemInfoService.GetOsVersion(),
|
|
Domain = SystemInfoService.GetDomain(),
|
|
IpAddress = SystemInfoService.GetIpAddress(),
|
|
MacAddress = SystemInfoService.GetMacAddress(),
|
|
LastUser = SystemInfoService.GetLastUser(),
|
|
CpuModel = cpuModel,
|
|
CpuCores = cpuCores,
|
|
CpuUsage = SystemInfoService.GetCpuUsage(),
|
|
RamTotal = ramTotal,
|
|
RamUsed = ramUsed,
|
|
DiskTotal = diskTotal,
|
|
DiskFree = diskFree,
|
|
UptimeHours = SystemInfoService.GetUptimeHours(),
|
|
PendingUpdates = SystemInfoService.GetPendingUpdates(),
|
|
TpmPresent = tpmPresent,
|
|
TpmVersion = tpmVersion,
|
|
TpmV2 = tpmV2,
|
|
SecureBoot = SystemInfoService.GetSecureBoot(),
|
|
Win11Ready = SystemInfoService.GetWin11Readiness(tpmV2, SystemInfoService.GetSecureBoot(), cpuCores, ramTotal),
|
|
BitlockerStatus = SecurityInfoService.GetBitlockerStatus(),
|
|
DefenderEnabled = defEnabled,
|
|
DefenderSignaturesAge = defSigAge >= 0 ? defSigAge : null,
|
|
HardwareSerial = SystemInfoService.GetHardwareSerial(),
|
|
InstalledSoftware = SystemInfoService.GetInstalledSoftware(),
|
|
};
|
|
}
|
|
|
|
private void WriteStatusCache(CheckinPayload? p, CheckinResponse? r, bool online)
|
|
{
|
|
try
|
|
{
|
|
var status = new StatusCache
|
|
{
|
|
Hostname = p?.Hostname ?? SystemInfoService.GetHostname(),
|
|
LastCheckin = DateTime.Now,
|
|
AgentVersion = Version,
|
|
ServerUrl = _config?.ServerUrl ?? "",
|
|
Online = online,
|
|
CpuUsage = p?.CpuUsage,
|
|
RamTotal = p?.RamTotal,
|
|
RamUsed = p?.RamUsed,
|
|
DiskTotal = p?.DiskTotal,
|
|
DiskFree = p?.DiskFree,
|
|
PendingUpdates = p?.PendingUpdates ?? 0,
|
|
BitlockerStatus = p?.BitlockerStatus ?? "",
|
|
DefenderEnabled = p?.DefenderEnabled ?? false,
|
|
DefenderSignaturesAge = p?.DefenderSignaturesAge,
|
|
HardwareSerial = p?.HardwareSerial ?? "",
|
|
Os = p?.Os ?? "",
|
|
LastUser = p?.LastUser ?? "",
|
|
InstalledSoftware = p?.InstalledSoftware ?? [],
|
|
};
|
|
File.WriteAllText(StatusPath, JsonConvert.SerializeObject(status, Formatting.Indented));
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
// Verzeichnis-ACL: nur SYSTEM/Administratoren — verhindert dass normale lokale User
|
|
// agent.log/status.json lesen (Hostname, letzter User, RDP-/Patch-Aktivität) oder manipulieren.
|
|
private static void SecureDataDir(string dir)
|
|
{
|
|
try
|
|
{
|
|
var di = new System.IO.DirectoryInfo(dir);
|
|
var acl = di.GetAccessControl();
|
|
acl.SetAccessRuleProtection(true, false);
|
|
acl.AddAccessRule(new System.Security.AccessControl.FileSystemAccessRule(
|
|
"SYSTEM", System.Security.AccessControl.FileSystemRights.FullControl,
|
|
System.Security.AccessControl.InheritanceFlags.ContainerInherit | System.Security.AccessControl.InheritanceFlags.ObjectInherit,
|
|
System.Security.AccessControl.PropagationFlags.None,
|
|
System.Security.AccessControl.AccessControlType.Allow));
|
|
acl.AddAccessRule(new System.Security.AccessControl.FileSystemAccessRule(
|
|
"Administrators", System.Security.AccessControl.FileSystemRights.FullControl,
|
|
System.Security.AccessControl.InheritanceFlags.ContainerInherit | System.Security.AccessControl.InheritanceFlags.ObjectInherit,
|
|
System.Security.AccessControl.PropagationFlags.None,
|
|
System.Security.AccessControl.AccessControlType.Allow));
|
|
di.SetAccessControl(acl);
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
private static void SecureConfigFile(string path)
|
|
{
|
|
try
|
|
{
|
|
var fi = new System.IO.FileInfo(path);
|
|
var acl = fi.GetAccessControl();
|
|
acl.SetAccessRuleProtection(true, false); // Vererbung entfernen
|
|
// Nur SYSTEM und Administratoren
|
|
acl.AddAccessRule(new System.Security.AccessControl.FileSystemAccessRule(
|
|
"SYSTEM", System.Security.AccessControl.FileSystemRights.FullControl,
|
|
System.Security.AccessControl.AccessControlType.Allow));
|
|
acl.AddAccessRule(new System.Security.AccessControl.FileSystemAccessRule(
|
|
"Administrators", System.Security.AccessControl.FileSystemRights.FullControl,
|
|
System.Security.AccessControl.AccessControlType.Allow));
|
|
fi.SetAccessControl(acl);
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
public static void Log(string msg)
|
|
{
|
|
try
|
|
{
|
|
var line = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} {msg}";
|
|
File.AppendAllText(LogPath, line + Environment.NewLine, System.Text.Encoding.UTF8);
|
|
}
|
|
catch { }
|
|
}
|
|
}
|