diff --git a/.github/codeql-config.yml b/.github/codeql-config.yml index d4e0d9cc..8b2ecd02 100644 --- a/.github/codeql-config.yml +++ b/.github/codeql-config.yml @@ -3,3 +3,11 @@ name: "SysManager CodeQL Configuration" paths-ignore: - '**/obj/**' - '**/bin/**' + +query-filters: + # Intentional use of CreateFromSignedFile for Authenticode verification — + # no modern .NET replacement exists without P/Invoke. Already suppressed + # with #pragma warning disable SYSLIB0057. + - exclude: + id: cs/call-to-obsolete-method + path: SysManager/SysManager/Services/UpdateService.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 28b364c5..02d7ae2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,27 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [0.48.33] - 2026-05-18 -### Changed +### Fixed +- **CodeQL** — resolved 5 remaining source code alerts: + - Replaced generic `catch (Exception)` in `ViewModelBase.InitializeAsync` + with specific exception types (`InvalidOperationException`, + `UnauthorizedAccessException`, `IOException`, `HttpRequestException`, + `TimeoutException`). + - Converted `UninstallerService.IsUnderTrustedDirectory` foreach+if to + LINQ `.Any()` (cs/linq/missed-where). + - Converted `WindowsFeaturesService.ParseFeatureList` foreach loop to + LINQ `.Select().Where()` pipeline (cs/linq/missed-select). + - Extracted `ProcessManagerViewModel.MatchesFilter` complex condition into + three focused helper methods (cs/complex-condition). + - Replaced `Path.Combine` with `Path.Join` in `UpdateService.DownloadAsync` + (cs/path-combine). +- **CodeQL workflow** — added query filter to suppress `cs/call-to-obsolete-method` + for `UpdateService.VerifyAuthenticode` (intentional use of `CreateFromSignedFile` + — no modern .NET replacement exists without P/Invoke). + +## [0.48.32] - 2026-05-18 + +### Fixed - **ConsoleViewModel** — buffer trimming now uses clear-and-rebuild when removing more than 25% of lines, reducing worst-case from O(n×excess) to O(n) (CQ-LOW: ConsoleViewModel O(n²)). @@ -16,7 +36,7 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). in batches of 50 instead of one-at-a-time, reducing dispatcher overhead by ~98% when loading large event logs (CQ-LOW: LogsViewModel batch dispatch). -## [0.48.32] - 2026-05-18 +## [0.48.31] - 2026-05-18 ### Fixed - **FormatSize duplication** — extracted shared `FormatHelper.FormatSize` method; @@ -26,8 +46,6 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). `SystemHealthViewModel` (chkdsk) now use `PowerShellRunner.OemEncoding` instead of duplicating the encoding resolution logic inline. -## [0.48.31] - 2026-05-18 - ### Changed - **Test parallelism** — enabled `parallelizeTestCollections` in xunit.runner.json so pure-logic unit tests run concurrently, reducing CI test time. Tests that diff --git a/SysManager/SysManager/Services/UninstallerService.cs b/SysManager/SysManager/Services/UninstallerService.cs index febd32f3..d8ca12ec 100644 --- a/SysManager/SysManager/Services/UninstallerService.cs +++ b/SysManager/SysManager/Services/UninstallerService.cs @@ -402,11 +402,8 @@ private static bool IsUnderTrustedDirectory(string fullPath) Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) }; - foreach (var dir in trustedDirs) - { - if (!string.IsNullOrEmpty(dir) && fullPath.StartsWith(dir, StringComparison.OrdinalIgnoreCase)) - return true; - } - return false; + return trustedDirs.Any(dir => + !string.IsNullOrEmpty(dir) && + fullPath.StartsWith(dir, StringComparison.OrdinalIgnoreCase)); } } diff --git a/SysManager/SysManager/Services/UpdateService.cs b/SysManager/SysManager/Services/UpdateService.cs index 9e2521eb..9775a4e6 100644 --- a/SysManager/SysManager/Services/UpdateService.cs +++ b/SysManager/SysManager/Services/UpdateService.cs @@ -141,7 +141,7 @@ public async Task> GetRecentAsync(int count = 10, Can Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "SysManager", "updates"); Directory.CreateDirectory(dir); - var target = Path.Combine(dir, $"SysManager-{rel.Version}.exe"); + var target = Path.Join(dir, $"SysManager-{rel.Version}.exe"); // SEC-M2: Skip re-download only if we have a cached hash that matches. // File size alone is insufficient — an attacker could replace the binary diff --git a/SysManager/SysManager/Services/WindowsFeaturesService.cs b/SysManager/SysManager/Services/WindowsFeaturesService.cs index bafaa168..c89fd9dc 100644 --- a/SysManager/SysManager/Services/WindowsFeaturesService.cs +++ b/SysManager/SysManager/Services/WindowsFeaturesService.cs @@ -114,32 +114,32 @@ void Collect(PowerShellLine l) /// Parses the pipe-delimited feature list output. internal static List ParseFeatureList(List lines) { - var features = new List(); - - foreach (var line in lines.Where(l => !string.IsNullOrWhiteSpace(l))) - { - var parts = line.Split('|', 2); - if (parts.Length < 2) continue; - - var name = parts[0].Trim(); - var state = parts[1].Trim(); - - if (string.IsNullOrWhiteSpace(name)) continue; - - var isEnabled = state.Equals("Enabled", StringComparison.OrdinalIgnoreCase); - - features.Add(new WindowsFeature + return lines + .Where(l => !string.IsNullOrWhiteSpace(l)) + .Select(line => { - Name = name, - DisplayName = HumanizeName(name), - IsEnabled = isEnabled, - Category = WindowsFeature.CategorizeFeature(name) - }); - } - - return features.OrderBy(f => f.Category) - .ThenBy(f => f.DisplayName) - .ToList(); + var parts = line.Split('|', 2); + if (parts.Length < 2) return null; + + var name = parts[0].Trim(); + var state = parts[1].Trim(); + + if (string.IsNullOrWhiteSpace(name)) return null; + + var isEnabled = state.Equals("Enabled", StringComparison.OrdinalIgnoreCase); + + return new WindowsFeature + { + Name = name, + DisplayName = HumanizeName(name), + IsEnabled = isEnabled, + Category = WindowsFeature.CategorizeFeature(name) + }; + }) + .Where(f => f is not null) + .OrderBy(f => f!.Category) + .ThenBy(f => f!.DisplayName) + .ToList()!; } /// diff --git a/SysManager/SysManager/ViewModels/ProcessManagerViewModel.cs b/SysManager/SysManager/ViewModels/ProcessManagerViewModel.cs index 2351d199..8e0d5774 100644 --- a/SysManager/SysManager/ViewModels/ProcessManagerViewModel.cs +++ b/SysManager/SysManager/ViewModels/ProcessManagerViewModel.cs @@ -160,9 +160,16 @@ private void ApplyFilter() private static string FormatSize(long bytes) => Helpers.FormatHelper.FormatSize(bytes); private static bool MatchesFilter(ProcessEntry p, string filter) => - p.Name.Contains(filter, StringComparison.OrdinalIgnoreCase) || + MatchesName(p, filter) || MatchesDescription(p, filter) || MatchesPid(p, filter); + + private static bool MatchesName(ProcessEntry p, string filter) => + p.Name.Contains(filter, StringComparison.OrdinalIgnoreCase); + + private static bool MatchesDescription(ProcessEntry p, string filter) => (p.Description?.Contains(filter, StringComparison.OrdinalIgnoreCase) ?? false) || (p.PlainDescription?.Contains(filter, StringComparison.OrdinalIgnoreCase) ?? false) || - (p.Category?.Contains(filter, StringComparison.OrdinalIgnoreCase) ?? false) || + (p.Category?.Contains(filter, StringComparison.OrdinalIgnoreCase) ?? false); + + private static bool MatchesPid(ProcessEntry p, string filter) => p.Pid.ToString().Contains(filter); } diff --git a/SysManager/SysManager/ViewModels/ViewModelBase.cs b/SysManager/SysManager/ViewModels/ViewModelBase.cs index c6f356e8..8704578c 100644 --- a/SysManager/SysManager/ViewModels/ViewModelBase.cs +++ b/SysManager/SysManager/ViewModels/ViewModelBase.cs @@ -31,9 +31,25 @@ protected static async void InitializeAsync(Func asyncAction, [System.Runt { // Expected during shutdown — no action needed. } - catch (Exception ex) + catch (InvalidOperationException ex) { - Log.Error(ex, "Unhandled exception in async initialization of {Caller}", callerName); + Log.Error(ex, "Invalid operation in async initialization of {Caller}", callerName); + } + catch (UnauthorizedAccessException ex) + { + Log.Error(ex, "Access denied in async initialization of {Caller}", callerName); + } + catch (System.IO.IOException ex) + { + Log.Error(ex, "I/O error in async initialization of {Caller}", callerName); + } + catch (System.Net.Http.HttpRequestException ex) + { + Log.Error(ex, "Network error in async initialization of {Caller}", callerName); + } + catch (TimeoutException ex) + { + Log.Error(ex, "Timeout in async initialization of {Caller}", callerName); } }