diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..afac14a --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "Bash(dotnet build:*)" + ], + "deny": [], + "ask": [] + } +} diff --git a/LD-SysInfo/LD_SysInfo.csproj b/LD-SysInfo/LD_SysInfo.csproj index 0d31a2e..84f02bd 100644 --- a/LD-SysInfo/LD_SysInfo.csproj +++ b/LD-SysInfo/LD_SysInfo.csproj @@ -12,6 +12,17 @@ C:\Users\Sonder\source\repos\psg-oversight-app\BuildDir\bin\Debug\net8.0-windows\ false + + 1.0.0.0 + 1.0.3.0 + 1.0.3 + + + true + true + https://gitea.psg.net.au/your-repo + true + - - - - - - + + + + + + + + + + - - - + + - + + diff --git a/LD-SysInfo/MainWindow.xaml.cs b/LD-SysInfo/MainWindow.xaml.cs index 024213e..69ea665 100644 --- a/LD-SysInfo/MainWindow.xaml.cs +++ b/LD-SysInfo/MainWindow.xaml.cs @@ -1,24 +1,33 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.IO; using System.Net.Http; +using System.Reflection; using System.Text; using System.Threading.Tasks; using System.Windows; -using Microsoft.Win32; -using System.IO; -using System.Windows.Threading; using System.Windows.Forms; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Animation; +using System.Windows.Threading; + using Hardcodet.Wpf.TaskbarNotification; + +using LD_SysInfo.Models; using LD_SysInfo.Services; + +using MaterialDesignColors; + +using MaterialDesignThemes.Wpf; + +using Microsoft.Win32; + using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using System.Windows.Media.Animation; -using LD_SysInfo.Models; -using MaterialDesignColors; -using MaterialDesignThemes.Wpf; -using System.Windows.Input; + using Application = System.Windows.Application; -using System.Windows.Media; @@ -34,6 +43,7 @@ namespace LD_SysInfo private readonly DispatcherTimer messageClearTimer; private readonly DispatcherTimer postTimer; private readonly DispatcherTimer keepAliveTimer; + private readonly DispatcherTimer tokenRefreshTimer; private readonly Uri LightThemeUri = new Uri("pack://application:,,,/Themes/LightTheme.xaml", UriKind.Absolute); private readonly Uri DarkThemeUri = new Uri("pack://application:,,,/Themes/DarkTheme.xaml", UriKind.Absolute); private bool isDarkTheme = false; @@ -47,6 +57,8 @@ namespace LD_SysInfo System.Net.ServicePointManager.ServerCertificateValidationCallback += (sender, cert, chain, sslPolicyErrors) => true; InitializeComponent(); + var fvi = FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location); + VersionTextBlock.Text = $"v{fvi.ProductVersion}"; LoadConfig(); DisplaySystemInfo(); AutoLogin(); @@ -74,7 +86,12 @@ namespace LD_SysInfo keepAliveTimer.Tick += KeepAliveTimer_Tick; keepAliveTimer.Start(); - + // 🔄 Initialize the Token Refresh timer to refresh token proactively before expiration + // Tokens expire in 60 minutes, so refresh at 50 minutes to be safe + tokenRefreshTimer = new DispatcherTimer(); + tokenRefreshTimer.Interval = TimeSpan.FromMinutes(50); + tokenRefreshTimer.Tick += TokenRefreshTimer_Tick; + tokenRefreshTimer.Start(); } @@ -252,6 +269,41 @@ namespace LD_SysInfo }); } + var formattedUserApplications = new List(); + foreach (var app in sysInfo.UserInstalledApplications) + { + formattedUserApplications.Add(new + { + app_name = app.Name, + app_version = app.Version, + publisher = app.Publisher + }); + } + + var formattedWindowsUpdates = new List(); + foreach (var update in sysInfo.WindowsUpdates) + { + formattedWindowsUpdates.Add(new + { + hotFixID = update.HotFixID, + description = update.Description, + installedOn = update.InstalledOn, + installedBy = update.InstalledBy + }); + } + + var formattedAppXPackages = new List(); + foreach (var pkg in sysInfo.AppXPackages) + { + formattedAppXPackages.Add(new + { + name = pkg.Name, + version = pkg.Version, + publisher = pkg.Publisher, + packageFullName = pkg.PackageFullName + }); + } + var payload = new { clientIdentifier = _config.ClientIdentifier, @@ -268,7 +320,10 @@ namespace LD_SysInfo ipAddresses = sysInfo.IpAddresses, lastBootTime = sysInfo.LastBootTime, drives = sysInfo.Drives, - installedApplications = formattedApplications + installedApplications = formattedApplications, + userInstalledApplications = formattedUserApplications, + windowsUpdates = formattedWindowsUpdates, + appXPackages = formattedAppXPackages }; @@ -519,6 +574,41 @@ namespace LD_SysInfo }); } + var formattedUserApplications = new List(); + foreach (var app in sysInfo.UserInstalledApplications) + { + formattedUserApplications.Add(new + { + app_name = app.Name, + app_version = app.Version, + publisher = app.Publisher + }); + } + + var formattedWindowsUpdates = new List(); + foreach (var update in sysInfo.WindowsUpdates) + { + formattedWindowsUpdates.Add(new + { + hotFixID = update.HotFixID, + description = update.Description, + installedOn = update.InstalledOn, + installedBy = update.InstalledBy + }); + } + + var formattedAppXPackages = new List(); + foreach (var pkg in sysInfo.AppXPackages) + { + formattedAppXPackages.Add(new + { + name = pkg.Name, + version = pkg.Version, + publisher = pkg.Publisher, + packageFullName = pkg.PackageFullName + }); + } + var payload = new { clientIdentifier = _config.ClientIdentifier, @@ -535,7 +625,10 @@ namespace LD_SysInfo ipAddresses = sysInfo.IpAddresses, lastBootTime = sysInfo.LastBootTime, drives = sysInfo.Drives, - installedApplications = formattedApplications + installedApplications = formattedApplications, + userInstalledApplications = formattedUserApplications, + windowsUpdates = formattedWindowsUpdates, + appXPackages = formattedAppXPackages }; @@ -590,6 +683,27 @@ namespace LD_SysInfo FadeOutStatusMessage(); // Trigger the fade-out effect } + private async void TokenRefreshTimer_Tick(object sender, EventArgs e) + { + if (string.IsNullOrEmpty(ApiClient.GetJwtToken())) + { + Console.WriteLine("⚠️ No token to refresh - skipping proactive refresh."); + return; + } + + var apiClient = new ApiClient(_config); + bool refreshed = await apiClient.RefreshTokenAsync(); + + if (refreshed) + { + Console.WriteLine("✅ Proactive token refresh successful."); + } + else + { + Console.WriteLine("⚠️ Proactive token refresh failed - will retry on next request."); + } + } + private void TrayIcon_DoubleClick(object sender, RoutedEventArgs e) { ShowWindow(); diff --git a/LD-SysInfo/Services/ApiClient.cs b/LD-SysInfo/Services/ApiClient.cs index addc93d..292f0ac 100644 --- a/LD-SysInfo/Services/ApiClient.cs +++ b/LD-SysInfo/Services/ApiClient.cs @@ -177,7 +177,54 @@ namespace LD_SysInfo.Services /// - /// Sends the collected data to the server. + /// Refreshes the current JWT token without requiring username/password. + /// + public async Task RefreshTokenAsync() + { + try + { + if (string.IsNullOrEmpty(jwtToken)) + { + Console.WriteLine("⚠️ No token to refresh."); + return false; + } + + HttpResponseMessage response = await httpClient.PostAsync(BuildUrl("/api/auth/refresh"), null); + string rawResponse = await response.Content.ReadAsStringAsync(); + + if (!response.IsSuccessStatusCode) + { + Console.WriteLine($"❌ Token refresh failed: {response.StatusCode} - {rawResponse}"); + return false; + } + + // Parse the response to get the new token if provided in body + // Note: Your server sets it via cookie, but may also return it in response + try + { + var refreshResponse = JsonConvert.DeserializeObject>(rawResponse); + if (refreshResponse?.ContainsKey("token") == true) + { + SetJwtToken(refreshResponse["token"]); + } + } + catch + { + // Token might only be in cookie, which is fine + } + + Console.WriteLine("✅ Token refreshed successfully."); + return true; + } + catch (Exception ex) + { + Console.WriteLine($"❌ Exception during token refresh: {ex.Message}"); + return false; + } + } + + /// + /// Sends the collected data to the server with automatic token refresh and re-auth. /// private async Task SendWithAutoReauthAsync(Func> requestFunc) { @@ -185,8 +232,24 @@ namespace LD_SysInfo.Services if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) { - Console.WriteLine("⚠️ Token expired or invalid. Attempting re-auth..."); + Console.WriteLine("⚠️ Token expired or invalid. Attempting token refresh..."); + // Try refreshing the token first (faster than full re-auth) + bool refreshed = await RefreshTokenAsync(); + + if (refreshed) + { + // Retry original request after refresh + response = await requestFunc(); + + if (response.IsSuccessStatusCode) + { + return response; + } + } + + // If refresh failed or didn't work, fall back to full re-authentication + Console.WriteLine("⚠️ Token refresh failed. Attempting full re-auth..."); var loginResponse = await AuthenticateAsync(_config.Auth.Username, _config.Auth.Password); if (loginResponse == null || string.IsNullOrEmpty(loginResponse.Token)) { diff --git a/LD-SysInfo/SystemInfo.cs b/LD-SysInfo/SystemInfo.cs index e6d1085..b5b8da4 100644 --- a/LD-SysInfo/SystemInfo.cs +++ b/LD-SysInfo/SystemInfo.cs @@ -68,6 +68,36 @@ namespace LD_SysInfo public string Publisher { get; set; } } + public class WindowsUpdate + { + [JsonProperty("hotFixID")] + public string HotFixID { get; set; } + + [JsonProperty("description")] + public string Description { get; set; } + + [JsonProperty("installedOn")] + public string InstalledOn { get; set; } + + [JsonProperty("installedBy")] + public string InstalledBy { get; set; } + } + + public class AppXPackage + { + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("version")] + public string Version { get; set; } + + [JsonProperty("publisher")] + public string Publisher { get; set; } + + [JsonProperty("packageFullName")] + public string PackageFullName { get; set; } + } + public class DriveInfoSummary { [JsonProperty("name")] @@ -134,9 +164,21 @@ namespace LD_SysInfo [JsonProperty("lastBootTime")] public string LastBootTime { get; set; } + [JsonProperty("collectedAt")] + public string CollectedAt { get; set; } + [JsonProperty("installedApplications")] public List InstalledApplications { get; set; } + [JsonProperty("userInstalledApplications")] + public List UserInstalledApplications { get; set; } = new(); + + [JsonProperty("windowsUpdates")] + public List WindowsUpdates { get; set; } = new(); + + [JsonProperty("appXPackages")] + public List AppXPackages { get; set; } = new(); + [JsonProperty("drives")] public List Drives { get; set; } = new(); @@ -149,6 +191,9 @@ namespace LD_SysInfo try { + // Capture collection timestamp with timezone info + sysInfo.CollectedAt = DateTimeOffset.Now.ToString("o"); + sysInfo.Hostname = Environment.MachineName; sysInfo.OSName = GetOSFriendlyName(); sysInfo.OSVersion = Environment.OSVersion.Version.ToString(); @@ -166,6 +211,11 @@ namespace LD_SysInfo sysInfo.LastBootTime = GetLastBootTime(); PopulateDriveInfo(sysInfo); + + // Populate new collections for comprehensive system information + sysInfo.UserInstalledApplications = GetUserInstalledApplications(); + sysInfo.WindowsUpdates = GetInstalledPatches(); + sysInfo.AppXPackages = GetAppXPackages(); } catch (Exception ex) { @@ -303,7 +353,11 @@ namespace LD_SysInfo foreach (var obj in searcher.Get()) { string lastBoot = obj["LastBootUpTime"]?.ToString(); - return ManagementDateTimeConverter.ToDateTime(lastBoot).ToString("o"); + // Convert to local time and ensure timezone info is preserved + DateTime localBootTime = ManagementDateTimeConverter.ToDateTime(lastBoot); + // Use DateTimeOffset to ensure timezone information is included + DateTimeOffset bootTimeWithZone = new DateTimeOffset(localBootTime, TimeZoneInfo.Local.GetUtcOffset(localBootTime)); + return bootTimeWithZone.ToString("o"); } } catch { } @@ -341,5 +395,138 @@ namespace LD_SysInfo return applications; } + + /// + /// Gets user-context installed applications from HKEY_CURRENT_USER registry. + /// + public static List GetUserInstalledApplications() + { + var applications = new List(); + string registryKey = @"Software\Microsoft\Windows\CurrentVersion\Uninstall"; + + try + { + using var regKey = Registry.CurrentUser.OpenSubKey(registryKey); + if (regKey == null) return applications; + + foreach (string subKeyName in regKey.GetSubKeyNames()) + { + using var subKey = regKey.OpenSubKey(subKeyName); + string name = subKey?.GetValue("DisplayName")?.ToString(); + if (!string.IsNullOrWhiteSpace(name)) + { + applications.Add(new InstalledApplication + { + Name = name, + Version = subKey?.GetValue("DisplayVersion")?.ToString(), + Publisher = subKey?.GetValue("Publisher")?.ToString() + }); + } + } + } + catch (Exception ex) + { + Console.WriteLine($"❌ Failed to gather user-installed applications: {ex.Message}"); + } + + return applications; + } + + /// + /// Gets Windows Updates and patches installed on the system using WMI. + /// + public static List GetInstalledPatches() + { + var patches = new List(); + + try + { + using var searcher = new ManagementObjectSearcher( + "SELECT HotFixID, Description, InstalledOn, InstalledBy FROM Win32_QuickFixEngineering"); + + foreach (var obj in searcher.Get()) + { + patches.Add(new WindowsUpdate + { + HotFixID = obj["HotFixID"]?.ToString(), + Description = obj["Description"]?.ToString(), + InstalledOn = obj["InstalledOn"]?.ToString(), + InstalledBy = obj["InstalledBy"]?.ToString() + }); + } + } + catch (Exception ex) + { + Console.WriteLine($"❌ Failed to gather Windows Updates: {ex.Message}"); + } + + return patches; + } + + /// + /// Gets Windows Store (AppX) packages installed on the system using WMI. + /// Note: This may require elevated permissions for complete results. + /// + public static List GetAppXPackages() + { + var packages = new List(); + + try + { + // Try using Win32_InstalledStoreProgram (Windows 10+) + using var searcher = new ManagementObjectSearcher( + "SELECT Name, Version, Publisher FROM Win32_InstalledStoreProgram"); + + foreach (var obj in searcher.Get()) + { + var name = obj["Name"]?.ToString(); + if (!string.IsNullOrWhiteSpace(name)) + { + packages.Add(new AppXPackage + { + Name = name, + Version = obj["Version"]?.ToString(), + Publisher = obj["Publisher"]?.ToString(), + PackageFullName = name // Win32_InstalledStoreProgram doesn't provide PackageFullName + }); + } + } + } + catch (Exception ex) + { + Console.WriteLine($"❌ Failed to gather AppX packages: {ex.Message}"); + } + + return packages; + } + + /// + /// Checks if a specific Windows Update patch is installed on the system. + /// + /// The KB number to check (e.g., "KB5034441") + /// True if the patch is installed, false otherwise + public static bool IsPatchInstalled(string kbNumber) + { + if (string.IsNullOrWhiteSpace(kbNumber)) + return false; + + try + { + // Normalize KB number format + string normalizedKB = kbNumber.ToUpper().Trim(); + if (!normalizedKB.StartsWith("KB")) + normalizedKB = "KB" + normalizedKB; + + using var searcher = new ManagementObjectSearcher( + $"SELECT HotFixID FROM Win32_QuickFixEngineering WHERE HotFixID = '{normalizedKB}'"); + + return searcher.Get().Count > 0; + } + catch (Exception ex) + { + Console.WriteLine($"❌ Failed to check patch {kbNumber}: {ex.Message}"); + return false; + } + } } } diff --git a/LD-SysInfo/config.json b/LD-SysInfo/config.json index 6cfe280..0d58f07 100644 --- a/LD-SysInfo/config.json +++ b/LD-SysInfo/config.json @@ -1,5 +1,5 @@ { - "ServerUrl": "https://sys.psg.net.au:8443", + "ServerUrl": "https://sys.psg.net.au:11443", "EnableLogging": true, "KeepAlivePeriod": 30, "SystemInfoInterval": 600, diff --git a/LD_SysInfo.sln b/LD_SysInfo.sln index 5547634..97b84d2 100644 --- a/LD_SysInfo.sln +++ b/LD_SysInfo.sln @@ -28,6 +28,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LD_SysInfo", "LD-SysInfo\LD EndProject Project("{B7DD6F7E-DEF8-4E67-B5B7-07EF123DB6F0}") = "OversightInstaller", "OversightInstaller\OversightInstaller.wixproj", "{99BFBED9-D563-375C-FBD6-E11D0770B009}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OversightService", "OversightService\OversightService.csproj", "{A8149609-CF69-4268-B985-B68444319344}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -72,6 +74,22 @@ Global {99BFBED9-D563-375C-FBD6-E11D0770B009}.Release|x64.Build.0 = Release|x64 {99BFBED9-D563-375C-FBD6-E11D0770B009}.Release|x86.ActiveCfg = Release|x86 {99BFBED9-D563-375C-FBD6-E11D0770B009}.Release|x86.Build.0 = Release|x86 + {A8149609-CF69-4268-B985-B68444319344}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A8149609-CF69-4268-B985-B68444319344}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A8149609-CF69-4268-B985-B68444319344}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {A8149609-CF69-4268-B985-B68444319344}.Debug|ARM64.Build.0 = Debug|Any CPU + {A8149609-CF69-4268-B985-B68444319344}.Debug|x64.ActiveCfg = Debug|Any CPU + {A8149609-CF69-4268-B985-B68444319344}.Debug|x64.Build.0 = Debug|Any CPU + {A8149609-CF69-4268-B985-B68444319344}.Debug|x86.ActiveCfg = Debug|Any CPU + {A8149609-CF69-4268-B985-B68444319344}.Debug|x86.Build.0 = Debug|Any CPU + {A8149609-CF69-4268-B985-B68444319344}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A8149609-CF69-4268-B985-B68444319344}.Release|Any CPU.Build.0 = Release|Any CPU + {A8149609-CF69-4268-B985-B68444319344}.Release|ARM64.ActiveCfg = Release|Any CPU + {A8149609-CF69-4268-B985-B68444319344}.Release|ARM64.Build.0 = Release|Any CPU + {A8149609-CF69-4268-B985-B68444319344}.Release|x64.ActiveCfg = Release|Any CPU + {A8149609-CF69-4268-B985-B68444319344}.Release|x64.Build.0 = Release|Any CPU + {A8149609-CF69-4268-B985-B68444319344}.Release|x86.ActiveCfg = Release|Any CPU + {A8149609-CF69-4268-B985-B68444319344}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/OversightService/OversightService.csproj b/OversightService/OversightService.csproj new file mode 100644 index 0000000..5387f3a --- /dev/null +++ b/OversightService/OversightService.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + dotnet-OversightService-9352272b-722c-4a12-acc2-8c9b146e5292 + + + + + + diff --git a/OversightService/Program.cs b/OversightService/Program.cs new file mode 100644 index 0000000..e04e76a --- /dev/null +++ b/OversightService/Program.cs @@ -0,0 +1,7 @@ +using OversightService; + +var builder = Host.CreateApplicationBuilder(args); +builder.Services.AddHostedService(); + +var host = builder.Build(); +host.Run(); diff --git a/OversightService/Properties/launchSettings.json b/OversightService/Properties/launchSettings.json new file mode 100644 index 0000000..d0dc0ef --- /dev/null +++ b/OversightService/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "OversightService": { + "commandName": "Project", + "dotnetRunMessages": true, + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} diff --git a/OversightService/Worker.cs b/OversightService/Worker.cs new file mode 100644 index 0000000..181340e --- /dev/null +++ b/OversightService/Worker.cs @@ -0,0 +1,24 @@ +namespace OversightService +{ + public class Worker : BackgroundService + { + private readonly ILogger _logger; + + public Worker(ILogger logger) + { + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + if (_logger.IsEnabled(LogLevel.Information)) + { + _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now); + } + await Task.Delay(1000, stoppingToken); + } + } + } +} diff --git a/OversightService/appsettings.Development.json b/OversightService/appsettings.Development.json new file mode 100644 index 0000000..b2dcdb6 --- /dev/null +++ b/OversightService/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/OversightService/appsettings.json b/OversightService/appsettings.json new file mode 100644 index 0000000..b2dcdb6 --- /dev/null +++ b/OversightService/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + } +}