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>
245 lines
10 KiB
C#
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 { }
|
|
}
|
|
}
|