Initial commit: IT Nexus Web-App

This commit is contained in:
2026-06-01 20:49:07 +02:00
commit 8023765e6c
387 changed files with 106900 additions and 0 deletions

View File

@@ -0,0 +1,89 @@
using ITNexusAgent.Models;
using Newtonsoft.Json;
using System.Net.Http;
using System.Text;
namespace ITNexusAgent.Services;
public class ApiService(string serverUrl, string agentKey)
{
private readonly HttpClient _http = new() { Timeout = TimeSpan.FromSeconds(30) };
private readonly string _baseUrl = serverUrl.TrimEnd('/');
private readonly string _agentKey = agentKey;
private HttpRequestMessage BuildRequest(HttpMethod method, string path, object? body = null)
{
var req = new HttpRequestMessage(method, $"{_baseUrl}{path}");
req.Headers.Add("X-Agent-Key", _agentKey);
if (body != null)
{
var json = JsonConvert.SerializeObject(body);
req.Content = new StringContent(json, Encoding.UTF8, "application/json");
}
return req;
}
public async Task<CheckinResponse> CheckinAsync(CheckinPayload payload)
{
var req = BuildRequest(HttpMethod.Post, "/api/monitoring/checkin", payload);
var resp = await _http.SendAsync(req);
resp.EnsureSuccessStatusCode();
var body = await resp.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<CheckinResponse>(body) ?? new CheckinResponse();
}
public async Task<List<Announcement>> PollAnnouncementsAsync(string hostname)
{
var req = BuildRequest(HttpMethod.Post, "/api/monitoring/announcements-poll",
new { hostname });
var resp = await _http.SendAsync(req);
resp.EnsureSuccessStatusCode();
var body = await resp.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeAnonymousType(body, new { announcements = new List<Announcement>() });
return result?.announcements ?? [];
}
public async Task AckAnnouncementAsync(int id, string hostname)
{
var req = BuildRequest(HttpMethod.Post, $"/api/announcements/{id}/ack-agent",
new { hostname });
var resp = await _http.SendAsync(req);
resp.EnsureSuccessStatusCode();
}
public async Task ReportCommandResultAsync(CommandResult result)
{
var req = BuildRequest(HttpMethod.Post, "/api/patch/commands/result", result);
var resp = await _http.SendAsync(req);
resp.EnsureSuccessStatusCode();
}
public async Task<byte[]> DownloadAgentAsync()
{
var req = BuildRequest(HttpMethod.Get, "/api/monitoring/agent-script");
var resp = await _http.SendAsync(req);
return await resp.Content.ReadAsByteArrayAsync();
}
public async Task<byte[]> DownloadSetupAsync()
{
var req = BuildRequest(HttpMethod.Get, "/api/monitoring/agent-setup");
var resp = await _http.SendAsync(req);
resp.EnsureSuccessStatusCode();
return await resp.Content.ReadAsByteArrayAsync();
}
public async Task SendMessageAsync(string hostname, string message)
{
var req = BuildRequest(HttpMethod.Post, "/api/monitoring/message",
new { hostname, message });
await _http.SendAsync(req);
}
public async Task SendRebootRequestAsync(string hostname)
{
var req = BuildRequest(HttpMethod.Post, "/api/monitoring/reboot-request",
new { hostname });
await _http.SendAsync(req);
}
}

View File

@@ -0,0 +1,123 @@
using ITNexusAgent.Models;
using System.Diagnostics;
using System.Management;
using System.Net.Http;
namespace ITNexusAgent.Services;
public class CommandExecutor(ApiService api, string hostname, string dataDir, string exePath)
{
private readonly ApiService _api = api;
private readonly string _hostname = hostname;
private readonly string _dataDir = dataDir;
private readonly string _exePath = exePath;
private static void RunSchtasks(string args) =>
Process.Start(new ProcessStartInfo("schtasks.exe", args) { CreateNoWindow = true })?.WaitForExit();
public async Task ExecuteAsync(PatchCommand cmd)
{
string result;
try
{
result = cmd.Command switch
{
"check_updates" => await CheckUpdates(),
"install_updates" => await InstallUpdates(),
"reboot" => await Reboot(cmd.Id),
"upgrade_win11" => await UpgradeWin11(),
"update_agent" => await UpdateAgent(),
_ => $"Unbekannter Command: {cmd.Command}"
};
}
catch (Exception ex)
{
result = $"Fehler: {ex.Message}";
}
if (cmd.Command != "reboot")
{
await _api.ReportCommandResultAsync(new CommandResult
{
CommandId = cmd.Id,
Status = "done",
Result = result
});
}
}
private Task<string> CheckUpdates()
{
Process.Start(new ProcessStartInfo("UsoClient.exe", "StartScan") { CreateNoWindow = true });
Thread.Sleep(5000);
return Task.FromResult("Update-Scan gestartet");
}
private Task<string> InstallUpdates()
{
Process.Start(new ProcessStartInfo("UsoClient.exe", "StartDownload") { CreateNoWindow = true });
Thread.Sleep(3000);
Process.Start(new ProcessStartInfo("UsoClient.exe", "StartInstall") { CreateNoWindow = true });
return Task.FromResult("Update-Installation gestartet");
}
private async Task<string> Reboot(int cmdId)
{
await _api.ReportCommandResultAsync(new CommandResult
{
CommandId = cmdId,
Status = "done",
Result = "Neustart wird in 60 Sekunden durchgeführt"
});
Process.Start("shutdown.exe", "/r /t 60 /c \"IT Nexus Patch Management - Geplanter Neustart\"");
return "";
}
private Task<string> UpgradeWin11()
{
var exePath = Path.Combine(_dataDir, "Win11Upgrade.exe");
using var client = new HttpClient();
var bytes = client.GetByteArrayAsync("https://go.microsoft.com/fwlink/?linkid=2171764").Result;
if (bytes.Length < 1024 * 1024) return Task.FromResult("Download fehlgeschlagen");
File.WriteAllBytes(exePath, bytes);
var fullUser = NotificationService.GetLoggedOnUser();
if (!string.IsNullOrEmpty(fullUser))
{
RunSchtasks("/delete /tn \"IT Nexus Win11 Upgrade\" /f");
RunSchtasks($"/create /tn \"IT Nexus Win11 Upgrade\" /tr \"\\\"{exePath}\\\" /skipeula /auto upgrade\" /sc ONCE /st {DateTime.Now.AddSeconds(20):HH:mm} /ru \"{fullUser}\" /it /rl HIGHEST /f");
}
else
{
Process.Start(new ProcessStartInfo(exePath, "/quietinstall /skipeula /auto upgrade")
{ CreateNoWindow = true });
}
return Task.FromResult("running");
}
private async Task<string> UpdateAgent()
{
var bytes = await _api.DownloadSetupAsync();
if (bytes.Length < 512 * 1024) return "Download fehlgeschlagen - Installer zu klein";
var setupPath = Path.Combine(_dataDir, "IT-Nexus-Agent-Setup-Update.exe");
var batchPath = Path.Combine(_dataDir, "update.cmd");
await File.WriteAllBytesAsync(setupPath, bytes);
// Batch: wartet kurz, führt Installer silent aus, löscht sich selbst
var batch = $"""
@echo off
timeout /t 5 /nobreak > nul
"{setupPath}" /VERYSILENT /SUPPRESSMSGBOXES /NORESTART
del "{setupPath}" > nul
del "%~f0"
""";
await File.WriteAllTextAsync(batchPath, batch, System.Text.Encoding.ASCII);
Process.Start(new ProcessStartInfo("cmd.exe", $"/c \"{batchPath}\"")
{ CreateNoWindow = true, WindowStyle = ProcessWindowStyle.Hidden });
return "Update-Installer gestartet";
}
}

View File

@@ -0,0 +1,110 @@
using ITNexusAgent.Models;
using Newtonsoft.Json;
using System.Diagnostics;
using System.Management;
using System.Text;
namespace ITNexusAgent.Services;
public class NotificationService
{
private readonly string _exePath;
private readonly string _dataDir;
private readonly string _shownIdsPath;
private readonly HashSet<int> _shownIds;
public NotificationService(string exePath, string dataDir)
{
_exePath = exePath;
_dataDir = dataDir;
_shownIdsPath = Path.Combine(dataDir, "shown_announcements.json");
_shownIds = LoadShownIds();
}
private HashSet<int> LoadShownIds()
{
try
{
if (File.Exists(_shownIdsPath))
{
var ids = JsonConvert.DeserializeObject<List<int>>(File.ReadAllText(_shownIdsPath));
return ids != null ? new HashSet<int>(ids) : [];
}
}
catch { }
return [];
}
private void SaveShownIds()
{
try { File.WriteAllText(_shownIdsPath, JsonConvert.SerializeObject(_shownIds.ToList())); }
catch { }
}
public void ShowAnnouncements(List<Announcement> announcements)
{
foreach (var ann in announcements)
{
if (_shownIds.Contains(ann.Id)) continue;
_shownIds.Add(ann.Id);
SaveShownIds();
ShowNotification(ann);
}
}
// Vollständigen eingeloggten User aus WMI holen (z.B. "WINKEL\gruessing" oder "AzureAD\gruessing")
public static string GetLoggedOnUser()
{
try
{
using var searcher = new ManagementObjectSearcher("SELECT UserName FROM Win32_ComputerSystem");
foreach (ManagementObject obj in searcher.Get())
{
var user = obj["UserName"]?.ToString();
if (!string.IsNullOrEmpty(user)) return user;
}
}
catch { }
return "";
}
private void ShowNotification(Announcement ann)
{
try
{
var fullUser = GetLoggedOnUser();
if (string.IsNullOrEmpty(fullUser)) return;
var json = Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(ann)));
var taskName = $"ITNexus-Ann-{ann.Id}";
// Alten Task entfernen
Process.Start(new ProcessStartInfo("schtasks.exe", $"/delete /tn \"{taskName}\" /f")
{ CreateNoWindow = true })?.WaitForExit();
// Task mit Trigger weit in der Zukunft erstellen (damit er nicht abläuft vor /run)
var triggerTime = DateTime.Now.AddMinutes(60).ToString("HH:mm:ss");
var args = $"/create /tn \"{taskName}\" /tr \"\\\"{_exePath}\\\" --notify {json}\" " +
$"/sc ONCE /st {triggerTime} /ru \"{fullUser}\" /it /f";
var p = Process.Start(new ProcessStartInfo("schtasks.exe", args)
{ CreateNoWindow = true, RedirectStandardError = true, UseShellExecute = false });
p?.WaitForExit();
// Sofort ausführen
var run = Process.Start(new ProcessStartInfo("schtasks.exe", $"/run /tn \"{taskName}\"")
{ CreateNoWindow = true });
run?.WaitForExit();
// Task löschen damit der +60min Trigger nicht nochmal feuert
Thread.Sleep(3000);
Process.Start(new ProcessStartInfo("schtasks.exe", $"/delete /tn \"{taskName}\" /f")
{ CreateNoWindow = true })?.WaitForExit();
AgentWorker.Log($"NOTIFY: Task '{taskName}' für User '{fullUser}' gestartet (ExitCode={p?.ExitCode})");
}
catch (Exception ex)
{
AgentWorker.Log($"NOTIFY ERROR: {ex.Message}");
}
}
}

View File

@@ -0,0 +1,64 @@
using System.Management;
namespace ITNexusAgent.Services;
public static class SecurityInfoService
{
public static string GetBitlockerStatus()
{
try
{
var scope = new ManagementScope(@"\\.\root\cimv2\Security\MicrosoftVolumeEncryption");
scope.Connect();
using var searcher = new ManagementObjectSearcher(scope,
new System.Management.ObjectQuery("SELECT * FROM Win32_EncryptableVolume WHERE DriveLetter='C:'"));
foreach (ManagementObject obj in searcher.Get())
{
var protection = Convert.ToInt32(obj["ProtectionStatus"]);
return protection switch
{
1 => "encrypted",
2 => "unknown",
_ => "off"
};
}
}
catch { }
return "unknown";
}
public static (bool Enabled, int SignaturesAgeDays, DateTime? LastScan) GetDefenderStatus()
{
try
{
var scope = new ManagementScope(@"\\.\root\Microsoft\Windows\Defender");
scope.Connect();
using var searcher = new ManagementObjectSearcher(scope,
new System.Management.ObjectQuery("SELECT * FROM MSFT_MpComputerStatus"));
foreach (ManagementObject obj in searcher.Get())
{
var enabled = Convert.ToBoolean(obj["AMServiceEnabled"] ?? false);
var sigDate = obj["AntivirusSignatureLastUpdated"]?.ToString();
var lastScanAge = Convert.ToInt32(obj["QuickScanAge"] ?? 999);
int sigAge = 0;
if (!string.IsNullOrEmpty(sigDate) && sigDate.Length >= 8)
{
if (DateTime.TryParseExact(sigDate[..14], "yyyyMMddHHmmss",
null, System.Globalization.DateTimeStyles.None, out var sigDateTime))
{
sigAge = (int)(DateTime.Now - sigDateTime).TotalDays;
}
}
DateTime? lastScan = lastScanAge < 999
? DateTime.Now.AddDays(-lastScanAge)
: null;
return (enabled, sigAge, lastScan);
}
}
catch { }
return (false, -1, null);
}
}

View File

@@ -0,0 +1,267 @@
using ITNexusAgent.Models;
using System.Diagnostics;
using System.Management;
using System.Net;
using System.Net.NetworkInformation;
using System.Runtime.InteropServices;
using System.Security.Principal;
using Microsoft.Win32;
namespace ITNexusAgent.Services;
public static class SystemInfoService
{
public static string GetHostname() => Environment.MachineName;
public static string GetDomain() =>
Environment.UserDomainName != Environment.MachineName
? Environment.UserDomainName
: "";
public static string GetOs()
{
try
{
using var key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion");
var name = key?.GetValue("ProductName")?.ToString() ?? "Windows";
var build = key?.GetValue("CurrentBuildNumber")?.ToString();
var ubr = key?.GetValue("UBR")?.ToString();
// Build >= 22000 = Windows 11 — Registry ProductName kann noch "Windows 10" sagen nach Upgrade
if (build != null && int.TryParse(build, out var buildNum) && buildNum >= 22000)
name = name.Replace("Windows 10", "Windows 11");
return build != null ? $"{name} (Build {build}.{ubr})" : name;
}
catch { return RuntimeInformation.OSDescription; }
}
public static string GetOsVersion()
{
try
{
using var key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion");
return key?.GetValue("CurrentBuildNumber")?.ToString() ?? Environment.OSVersion.Version.ToString();
}
catch { return Environment.OSVersion.Version.ToString(); }
}
public static string GetIpAddress()
{
try
{
foreach (var iface in NetworkInterface.GetAllNetworkInterfaces())
{
if (iface.OperationalStatus != OperationalStatus.Up) continue;
if (iface.NetworkInterfaceType == NetworkInterfaceType.Loopback) continue;
var name = iface.Name.ToLower();
if (name.Contains("virtual") || name.Contains("wsl") || name.Contains("vmware")) continue;
foreach (var addr in iface.GetIPProperties().UnicastAddresses)
{
if (addr.Address.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork) continue;
var ip = addr.Address.ToString();
if (ip.StartsWith("169.254") || ip.StartsWith("172.")) continue;
return ip;
}
}
}
catch { }
return "";
}
public static string GetMacAddress()
{
try
{
foreach (var iface in NetworkInterface.GetAllNetworkInterfaces())
{
if (iface.OperationalStatus != OperationalStatus.Up) continue;
if (iface.NetworkInterfaceType == NetworkInterfaceType.Loopback) continue;
var mac = iface.GetPhysicalAddress().ToString();
if (mac.Length == 12)
return string.Join(":", Enumerable.Range(0, 6).Select(i => mac.Substring(i * 2, 2)));
}
}
catch { }
return "";
}
public static string GetLastUser()
{
try
{
using var searcher = new ManagementObjectSearcher("SELECT UserName FROM Win32_ComputerSystem");
foreach (ManagementObject obj in searcher.Get())
{
var user = obj["UserName"]?.ToString();
if (!string.IsNullOrEmpty(user))
return user.Contains('\\') ? user.Split('\\')[1] : user;
}
// Fallback: letztes Profil
using var key = Registry.LocalMachine.OpenSubKey(
@"SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\LogonUI");
return key?.GetValue("LastLoggedOnUser")?.ToString()?.Split('\\').LastOrDefault() ?? "";
}
catch { return ""; }
}
public static (string Model, int Cores) GetCpuInfo()
{
try
{
using var searcher = new ManagementObjectSearcher("SELECT Name, NumberOfCores FROM Win32_Processor");
foreach (ManagementObject obj in searcher.Get())
return (obj["Name"]?.ToString()?.Trim() ?? "", Convert.ToInt32(obj["NumberOfCores"]));
}
catch { }
return ("", 0);
}
public static double GetCpuUsage()
{
try
{
using var counter = new PerformanceCounter("Processor", "% Processor Time", "_Total");
counter.NextValue();
Thread.Sleep(800);
return Math.Round(counter.NextValue(), 1);
}
catch { return 0; }
}
public static (double Total, double Used) GetRamInfo()
{
try
{
using var searcher = new ManagementObjectSearcher("SELECT TotalVisibleMemorySize, FreePhysicalMemory FROM Win32_OperatingSystem");
foreach (ManagementObject obj in searcher.Get())
{
var total = Math.Round(Convert.ToDouble(obj["TotalVisibleMemorySize"]) / (1024 * 1024), 2);
var free = Math.Round(Convert.ToDouble(obj["FreePhysicalMemory"]) / (1024 * 1024), 2);
return (total, Math.Round(total - free, 2));
}
}
catch { }
return (0, 0);
}
public static (double Total, double Free) GetDiskInfo()
{
try
{
var drive = new DriveInfo("C");
return (
Math.Round(drive.TotalSize / (double)(1024 * 1024 * 1024), 2),
Math.Round(drive.AvailableFreeSpace / (double)(1024 * 1024 * 1024), 2)
);
}
catch { return (0, 0); }
}
public static double GetUptimeHours()
{
try
{
var uptime = TimeSpan.FromMilliseconds(Environment.TickCount64);
return Math.Round(uptime.TotalHours, 1);
}
catch { return 0; }
}
public static int GetPendingUpdates()
{
try
{
var type = Type.GetTypeFromProgID("Microsoft.Update.Session");
if (type == null) return 0;
dynamic session = Activator.CreateInstance(type)!;
var searcher = session.CreateUpdateSearcher();
var result = searcher.Search("IsInstalled=0 and Type='Software'");
return result.Updates.Count;
}
catch { return 0; }
}
public static (bool Present, string Version, bool V2) GetTpmInfo()
{
try
{
using var searcher = new ManagementObjectSearcher(@"root\CIMv2\Security\MicrosoftTpm",
"SELECT * FROM Win32_Tpm");
foreach (ManagementObject obj in searcher.Get())
{
var version = obj["SpecVersion"]?.ToString() ?? "";
return (true, version, version.StartsWith("2.0"));
}
}
catch { }
return (false, "", false);
}
public static bool GetSecureBoot()
{
try
{
using var key = Registry.LocalMachine.OpenSubKey(
@"SYSTEM\CurrentControlSet\Control\SecureBoot\State");
return Convert.ToInt32(key?.GetValue("UEFISecureBootEnabled") ?? 0) == 1;
}
catch { return false; }
}
public static bool GetWin11Readiness(bool tpmV2, bool secureBoot, int cpuCores, double ramGb)
{
// Wenn bereits Win 11 läuft → immer true
try
{
using var key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion");
var build = key?.GetValue("CurrentBuildNumber")?.ToString();
if (build != null && int.TryParse(build, out var b) && b >= 22000) return true;
}
catch { }
return tpmV2 && secureBoot && cpuCores >= 2 && ramGb >= 4;
}
public static string GetHardwareSerial()
{
try
{
using var searcher = new ManagementObjectSearcher("SELECT SerialNumber FROM Win32_BIOS");
foreach (ManagementObject obj in searcher.Get())
return obj["SerialNumber"]?.ToString()?.Trim() ?? "";
}
catch { }
return "";
}
public static List<string> GetInstalledSoftware()
{
var apps = new HashSet<string>();
var paths = new[]
{
@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall",
@"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall"
};
foreach (var path in paths)
{
try
{
using var key = Registry.LocalMachine.OpenSubKey(path);
if (key == null) continue;
foreach (var subName in key.GetSubKeyNames())
{
using var sub = key.OpenSubKey(subName);
var name = sub?.GetValue("DisplayName")?.ToString();
if (string.IsNullOrWhiteSpace(name)) continue;
if (System.Text.RegularExpressions.Regex.IsMatch(name, @"^KB\d+")) continue;
apps.Add(name.Trim());
}
}
catch { }
}
return [.. apps.OrderBy(x => x).Take(100)];
}
}