Files
IT-Nexus/agent-cs/AgentWorker.cs
Simon Grüssing f3bd8d0910 Security: Pro-Geräte Agent-Keys statt geteiltem AGENT_API_KEY (v2.8.0)
Agent v2.8.0 tauscht beim Start automatisch den geteilten Bootstrap-Key
gegen einen individuellen Per-Device-Key (POST /api/monitoring/enroll,
idempotent). Checkin/Announcements-Poll/Setup-Download/WS-Agent-Verbindungen
validieren den Key jetzt gegen den jeweiligen Hostname — ein gestohlener
Key kann sich nicht mehr als anderer Agent ausgeben (manuell verifiziert).

Alte Agents mit dem geteilten Key funktionieren während der Übergangsphase
weiter (validateAgentKey() akzeptiert beides), damit der Rollout die Fleet
nicht abrupt bricht — Migration läuft über den bestehenden Staged-Rollout
(Test → Pilot → Produktion).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 13:50:02 +02:00

245 lines
10 KiB
C#

using ITNexusAgent.Models;
using ITNexusAgent.Services;
using Newtonsoft.Json;
namespace ITNexusAgent;
public class AgentWorker
{
private const string Version = "2.8.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");
// Security-Migration: geteilten Bootstrap-Key gegen individuellen Per-Device-Key tauschen.
// Idempotent (Server liefert bestehenden Key erneut) — daher bei jedem Start sicher aufrufbar.
var hostname = SystemInfoService.GetHostname();
var enrolledKey = await _api.EnrollAsync(hostname);
if (!string.IsNullOrEmpty(enrolledKey) && enrolledKey != _config.AgentKey)
{
_config.AgentKey = enrolledKey;
_config.Save(ConfigPath);
_api.UpdateKey(enrolledKey);
Log("ENROLL: Per-Device-Key erhalten und gespeichert");
}
// WebSocket Shell-Service im Hintergrund starten
var shellService = new ShellService(_config.ServerUrl, _config.AgentKey, hostname);
_ = shellService.RunAsync(_ct);
// WebRTC Remote Desktop Service im Hintergrund starten
var rtcService = new RtcService(_config.ServerUrl, _config.AgentKey, hostname);
_ = 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 { }
}
}