From a6bc3cd384203a94d2a7834645d21ed3a05d2bf9 Mon Sep 17 00:00:00 2001 From: jschick04 Date: Thu, 30 Apr 2026 22:05:46 -0500 Subject: [PATCH 1/2] Gate Add/Close-All on open logs and Security/State on admin elevation --- .../LogNameMethodsTests.cs | 36 ++++++++++ src/EventLogExpert.UI/LogNameMethods.cs | 15 ++++ .../Services/MauiMenuActionService.cs | 1 + .../Shared/Components/Menu/MenuBar.razor.cs | 71 ++++++++++++------- 4 files changed, 97 insertions(+), 26 deletions(-) diff --git a/src/EventLogExpert.UI.Tests/LogNameMethodsTests.cs b/src/EventLogExpert.UI.Tests/LogNameMethodsTests.cs index d480e087..9acffafe 100644 --- a/src/EventLogExpert.UI.Tests/LogNameMethodsTests.cs +++ b/src/EventLogExpert.UI.Tests/LogNameMethodsTests.cs @@ -7,6 +7,24 @@ namespace EventLogExpert.UI.Tests; public sealed class LogNameMethodsTests { + [Fact] + public void AdminOnlyLiveLogNames_ContainsExpectedNames() + { + Assert.Contains("Security", LogNameMethods.AdminOnlyLiveLogNames); + Assert.Contains("State", LogNameMethods.AdminOnlyLiveLogNames); + Assert.Equal(2, LogNameMethods.AdminOnlyLiveLogNames.Count); + } + + [Theory] + [InlineData("security")] + [InlineData("SECURITY")] + [InlineData("state")] + [InlineData("STATE")] + public void AdminOnlyLiveLogNames_ShouldMatchCaseInsensitively(string input) + { + Assert.Contains(input, LogNameMethods.AdminOnlyLiveLogNames); + } + [Fact] public void GetMenuPath_WhenChannelHasEmptySegments_ShouldIgnoreEmpties() { @@ -119,4 +137,22 @@ public void GetMenuPath_WhenSimpleRootLog_ShouldReturnSingleSegment() Assert.Equal(["Application"], path); } + + [Fact] + public void HardCodedLiveLogNames_ContainsExpectedNames() + { + Assert.Contains("Application", LogNameMethods.HardCodedLiveLogNames); + Assert.Contains("System", LogNameMethods.HardCodedLiveLogNames); + Assert.Contains("Security", LogNameMethods.HardCodedLiveLogNames); + Assert.Equal(3, LogNameMethods.HardCodedLiveLogNames.Count); + } + + [Theory] + [InlineData("application")] + [InlineData("APPLICATION")] + [InlineData("Application")] + public void HardCodedLiveLogNames_ShouldMatchCaseInsensitively(string input) + { + Assert.Contains(input, LogNameMethods.HardCodedLiveLogNames); + } } diff --git a/src/EventLogExpert.UI/LogNameMethods.cs b/src/EventLogExpert.UI/LogNameMethods.cs index 1c57ef93..1326d621 100644 --- a/src/EventLogExpert.UI/LogNameMethods.cs +++ b/src/EventLogExpert.UI/LogNameMethods.cs @@ -7,6 +7,21 @@ public static class LogNameMethods { private const string MicrosoftWindowsPrefix = "Microsoft-Windows-"; + /// Live event log names that require process elevation to read. + public static IReadOnlySet AdminOnlyLiveLogNames { get; } = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "Security", + "State", + }; + + /// Live event log names hard-coded in MenuBar's File menu — filter these from dynamic log enumeration to avoid duplicates. + public static IReadOnlySet HardCodedLiveLogNames { get; } = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "Application", + "System", + "Security", + }; + public static IReadOnlyList GetMenuPath(string logName) { if (string.IsNullOrWhiteSpace(logName)) diff --git a/src/EventLogExpert/Services/MauiMenuActionService.cs b/src/EventLogExpert/Services/MauiMenuActionService.cs index 18162b7c..d6b4d1a0 100644 --- a/src/EventLogExpert/Services/MauiMenuActionService.cs +++ b/src/EventLogExpert/Services/MauiMenuActionService.cs @@ -114,6 +114,7 @@ public async Task> GetOtherLogNamesAsync() _cachedLogNames = await Task.Run>(() => EventLogSession.GlobalSession.GetLogNames() + .Where(name => !LogNameMethods.HardCodedLiveLogNames.Contains(name)) .OrderBy(name => name, StringComparer.OrdinalIgnoreCase) .ToList()); diff --git a/src/EventLogExpert/Shared/Components/Menu/MenuBar.razor.cs b/src/EventLogExpert/Shared/Components/Menu/MenuBar.razor.cs index e5eeb1ca..75355f47 100644 --- a/src/EventLogExpert/Shared/Components/Menu/MenuBar.razor.cs +++ b/src/EventLogExpert/Shared/Components/Menu/MenuBar.razor.cs @@ -3,6 +3,7 @@ using EventLogExpert.UI; using EventLogExpert.UI.Interfaces; +using EventLogExpert.UI.Services; using EventLogExpert.UI.Store.EventLog; using EventLogExpert.UI.Store.FilterPane; using Fluxor; @@ -16,6 +17,7 @@ namespace EventLogExpert.Shared.Components.Menu; public sealed partial class MenuBar : IDisposable { private readonly List _bars = []; + private ElementReference[] _barElements = []; private int _focusedBarIndex; private long _openRequestId; @@ -27,9 +29,14 @@ public sealed partial class MenuBar : IDisposable [Inject] private IStateSelection ContinuouslyUpdate { get; init; } = null!; + [Inject] private ICurrentVersionProvider CurrentVersionProvider { get; init; } = null!; + [Inject] private IStateSelection FilterPaneIsEnabled { get; init; } = null!; + [Inject] + private IStateSelection HasActiveLogs { get; init; } = null!; + [Inject] private IJSRuntime JSRuntime { get; init; } = null!; [Inject] private IMenuService MenuService { get; init; } = null!; @@ -49,6 +56,7 @@ protected override void OnInitialized() { ContinuouslyUpdate.Select(state => state.ContinuouslyUpdate); FilterPaneIsEnabled.Select(state => state.IsEnabled); + HasActiveLogs.Select(state => !state.ActiveLogs.IsEmpty); Settings.CopyTypeChanged += OnSettingsChanged; MenuService.StateChanged += OnMenuServiceStateChanged; @@ -86,14 +94,19 @@ private IReadOnlyList BuildEdit() ]; } - private IReadOnlyList BuildFile() => - [ - MenuItem.SubMenu("Open", BuildOpenSubMenu(false)), - MenuItem.SubMenu("Add Another Log To This View", BuildOpenSubMenu(true)), - MenuItem.Separator(), - MenuItem.Item("Close All Open Logs", () => Actions.CloseAllLogsAsync()), - MenuItem.Item("Exit", Actions.Exit), - ]; + private IReadOnlyList BuildFile() + { + bool hasActiveLogs = HasActiveLogs.Value; + + return + [ + MenuItem.SubMenu("Open", BuildOpenSubMenu(false)), + MenuItem.SubMenu("Combine", BuildOpenSubMenu(true), isEnabled: hasActiveLogs), + MenuItem.Separator(), + MenuItem.Item("Close All", () => Actions.CloseAllLogsAsync(), isEnabled: hasActiveLogs), + MenuItem.Item("Exit", Actions.Exit), + ]; + } private IReadOnlyList BuildHelp() => [ @@ -104,22 +117,27 @@ private IReadOnlyList BuildHelp() => MenuItem.Item("View Logs", () => Actions.ShowDebugLogsAsync()), ]; - private IReadOnlyList BuildOpenSubMenu(bool addLog) => - [ - MenuItem.Item("File", () => Actions.OpenFileAsync(addLog), addLog ? null : "Ctrl+O"), - MenuItem.Item("Folder", () => Actions.OpenFolderAsync(addLog)), - MenuItem.SubMenu("Live Event Log", + private IReadOnlyList BuildOpenSubMenu(bool combineLog) + { + bool isAdmin = CurrentVersionProvider.IsAdmin; + + return [ - MenuItem.Item("Application", () => Actions.OpenLiveLogAsync("Application", addLog)), - MenuItem.Item("System", () => Actions.OpenLiveLogAsync("System", addLog)), - MenuItem.Item("Security", () => Actions.OpenLiveLogAsync("Security", addLog)), - MenuItem.AsyncSubMenu( - "Other Logs", - async () => BuildOtherLogsTree(await Actions.GetOtherLogNamesAsync(), addLog)), - ]), - ]; + MenuItem.Item("File", () => Actions.OpenFileAsync(combineLog), combineLog ? null : "Ctrl+O"), + MenuItem.Item("Folder", () => Actions.OpenFolderAsync(combineLog)), + MenuItem.SubMenu("Live", + [ + MenuItem.Item("Application", () => Actions.OpenLiveLogAsync("Application", combineLog)), + MenuItem.Item("System", () => Actions.OpenLiveLogAsync("System", combineLog)), + MenuItem.Item("Security", () => Actions.OpenLiveLogAsync("Security", combineLog), isEnabled: isAdmin), + MenuItem.AsyncSubMenu( + "Other Logs", + async () => BuildOtherLogsTree(await Actions.GetOtherLogNamesAsync(), combineLog, isAdmin)), + ]), + ]; + } - private IReadOnlyList BuildOtherLogsTree(IReadOnlyList logNames, bool addLog) + private IReadOnlyList BuildOtherLogsTree(IReadOnlyList logNames, bool addLog, bool isAdmin) { var rootChildren = new List(); var folderMap = new Dictionary>(StringComparer.OrdinalIgnoreCase); @@ -130,12 +148,13 @@ private IReadOnlyList BuildOtherLogsTree(IReadOnlyList logName if (path.Count == 0) { continue; } - var leafLabel = path[^1]; - var leaf = MenuItem.Item(leafLabel, () => Actions.OpenLiveLogAsync(logName, addLog)); + var log = path[^1]; + var logIsEnabled = isAdmin || !LogNameMethods.AdminOnlyLiveLogNames.Contains(logName); + var logMenuItem = MenuItem.Item(log, () => Actions.OpenLiveLogAsync(logName, addLog), isEnabled: logIsEnabled); if (path.Count == 1) { - rootChildren.Add(leaf); + rootChildren.Add(logMenuItem); continue; } @@ -161,7 +180,7 @@ private IReadOnlyList BuildOtherLogsTree(IReadOnlyList logName children = newChildren; } - children.Add(leaf); + children.Add(logMenuItem); } return rootChildren; From 74f5a909e9afa400a68922d20db189012e29380b Mon Sep 17 00:00:00 2001 From: jschick04 Date: Thu, 30 Apr 2026 22:32:15 -0500 Subject: [PATCH 2/2] Move log-name constants to Eventing.Helpers and rename addLog parameter to combineLog --- .../Helpers/LogNamesTests.cs | 36 +++++++++++++++++++ .../Helpers/LogNames.cs | 19 ++++++++++ .../Providers/RegistryProvider.cs | 2 +- .../LogNameMethodsTests.cs | 18 ---------- .../Interfaces/IMenuActionService.cs | 6 ++-- src/EventLogExpert.UI/LogNameMethods.cs | 15 +++----- .../Services/MauiMenuActionService.cs | 16 ++++----- .../Shared/Components/Menu/MenuBar.razor.cs | 13 +++---- 8 files changed, 79 insertions(+), 46 deletions(-) create mode 100644 src/EventLogExpert.Eventing.Tests/Helpers/LogNamesTests.cs create mode 100644 src/EventLogExpert.Eventing/Helpers/LogNames.cs diff --git a/src/EventLogExpert.Eventing.Tests/Helpers/LogNamesTests.cs b/src/EventLogExpert.Eventing.Tests/Helpers/LogNamesTests.cs new file mode 100644 index 00000000..ff058a3e --- /dev/null +++ b/src/EventLogExpert.Eventing.Tests/Helpers/LogNamesTests.cs @@ -0,0 +1,36 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.Eventing.Helpers; + +namespace EventLogExpert.Eventing.Tests.Helpers; + +public sealed class LogNamesTests +{ + [Fact] + public void AdminOnlyLiveLogNames_ContainsExpectedNames() + { + Assert.Contains(LogNames.SecurityLog, LogNames.AdminOnlyLiveLogNames); + Assert.Contains(LogNames.StateLog, LogNames.AdminOnlyLiveLogNames); + Assert.Equal(2, LogNames.AdminOnlyLiveLogNames.Count); + } + + [Theory] + [InlineData("security")] + [InlineData("SECURITY")] + [InlineData("state")] + [InlineData("STATE")] + public void AdminOnlyLiveLogNames_ShouldMatchCaseInsensitively(string input) + { + Assert.Contains(input, LogNames.AdminOnlyLiveLogNames); + } + + [Fact] + public void Constants_HaveExpectedValues() + { + Assert.Equal("Application", LogNames.ApplicationLog); + Assert.Equal("Security", LogNames.SecurityLog); + Assert.Equal("State", LogNames.StateLog); + Assert.Equal("System", LogNames.SystemLog); + } +} diff --git a/src/EventLogExpert.Eventing/Helpers/LogNames.cs b/src/EventLogExpert.Eventing/Helpers/LogNames.cs new file mode 100644 index 00000000..74996b29 --- /dev/null +++ b/src/EventLogExpert.Eventing/Helpers/LogNames.cs @@ -0,0 +1,19 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.Eventing.Helpers; + +public static class LogNames +{ + public const string ApplicationLog = "Application"; + public const string SecurityLog = "Security"; + public const string StateLog = "State"; + public const string SystemLog = "System"; + + /// Live event log names that require process elevation to read. + public static IReadOnlySet AdminOnlyLiveLogNames { get; } = new HashSet(StringComparer.OrdinalIgnoreCase) + { + SecurityLog, + StateLog, + }; +} diff --git a/src/EventLogExpert.Eventing/Providers/RegistryProvider.cs b/src/EventLogExpert.Eventing/Providers/RegistryProvider.cs index 21cc0d6d..bebc81e4 100644 --- a/src/EventLogExpert.Eventing/Providers/RegistryProvider.cs +++ b/src/EventLogExpert.Eventing/Providers/RegistryProvider.cs @@ -34,7 +34,7 @@ public IEnumerable GetMessageFilesForLegacyProvider(string providerName) foreach (var logSubKeyName in eventLogKey.GetSubKeyNames()) { // Skip Security and State since it requires elevation - if (logSubKeyName is "Security" or "State") + if (LogNames.AdminOnlyLiveLogNames.Contains(logSubKeyName)) { continue; } diff --git a/src/EventLogExpert.UI.Tests/LogNameMethodsTests.cs b/src/EventLogExpert.UI.Tests/LogNameMethodsTests.cs index 9acffafe..b46eaf0f 100644 --- a/src/EventLogExpert.UI.Tests/LogNameMethodsTests.cs +++ b/src/EventLogExpert.UI.Tests/LogNameMethodsTests.cs @@ -7,24 +7,6 @@ namespace EventLogExpert.UI.Tests; public sealed class LogNameMethodsTests { - [Fact] - public void AdminOnlyLiveLogNames_ContainsExpectedNames() - { - Assert.Contains("Security", LogNameMethods.AdminOnlyLiveLogNames); - Assert.Contains("State", LogNameMethods.AdminOnlyLiveLogNames); - Assert.Equal(2, LogNameMethods.AdminOnlyLiveLogNames.Count); - } - - [Theory] - [InlineData("security")] - [InlineData("SECURITY")] - [InlineData("state")] - [InlineData("STATE")] - public void AdminOnlyLiveLogNames_ShouldMatchCaseInsensitively(string input) - { - Assert.Contains(input, LogNameMethods.AdminOnlyLiveLogNames); - } - [Fact] public void GetMenuPath_WhenChannelHasEmptySegments_ShouldIgnoreEmpties() { diff --git a/src/EventLogExpert.UI/Interfaces/IMenuActionService.cs b/src/EventLogExpert.UI/Interfaces/IMenuActionService.cs index ee1ad2ce..9c35162a 100644 --- a/src/EventLogExpert.UI/Interfaces/IMenuActionService.cs +++ b/src/EventLogExpert.UI/Interfaces/IMenuActionService.cs @@ -30,13 +30,13 @@ public interface IMenuActionService Task OpenDocsAsync(); - Task OpenFileAsync(bool addLog); + Task OpenFileAsync(bool combineLog); - Task OpenFolderAsync(bool addLog); + Task OpenFolderAsync(bool combineLog); Task OpenIssueAsync(); - Task OpenLiveLogAsync(string logName, bool addLog); + Task OpenLiveLogAsync(string logName, bool combineLog); Task OpenSettingsAsync(); diff --git a/src/EventLogExpert.UI/LogNameMethods.cs b/src/EventLogExpert.UI/LogNameMethods.cs index 1326d621..02e6524c 100644 --- a/src/EventLogExpert.UI/LogNameMethods.cs +++ b/src/EventLogExpert.UI/LogNameMethods.cs @@ -1,25 +1,20 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. +using EventLogExpert.Eventing.Helpers; + namespace EventLogExpert.UI; public static class LogNameMethods { private const string MicrosoftWindowsPrefix = "Microsoft-Windows-"; - /// Live event log names that require process elevation to read. - public static IReadOnlySet AdminOnlyLiveLogNames { get; } = new HashSet(StringComparer.OrdinalIgnoreCase) - { - "Security", - "State", - }; - /// Live event log names hard-coded in MenuBar's File menu — filter these from dynamic log enumeration to avoid duplicates. public static IReadOnlySet HardCodedLiveLogNames { get; } = new HashSet(StringComparer.OrdinalIgnoreCase) { - "Application", - "System", - "Security", + LogNames.ApplicationLog, + LogNames.SystemLog, + LogNames.SecurityLog, }; public static IReadOnlyList GetMenuPath(string logName) diff --git a/src/EventLogExpert/Services/MauiMenuActionService.cs b/src/EventLogExpert/Services/MauiMenuActionService.cs index d6b4d1a0..e000e329 100644 --- a/src/EventLogExpert/Services/MauiMenuActionService.cs +++ b/src/EventLogExpert/Services/MauiMenuActionService.cs @@ -131,7 +131,7 @@ public async Task> GetOtherLogNamesAsync() public Task OpenDocsAsync() => OpenBrowserAsync("https://github.com/microsoft/EventLogExpert/blob/main/docs/Home.md"); - public async Task OpenFileAsync(bool addLog) + public async Task OpenFileAsync(bool combineLog) { var options = new PickOptions { @@ -143,7 +143,7 @@ public async Task OpenFileAsync(bool addLog) if (!files.Any()) { return; } - if (!addLog) + if (!combineLog) { await CloseAllLogsAsync(); } @@ -156,7 +156,7 @@ public async Task OpenFileAsync(bool addLog) } } - public async Task OpenFolderAsync(bool addLog) + public async Task OpenFolderAsync(bool combineLog) { string? folderPath = await FolderPickerHelper.PickFolderAsync(); @@ -166,7 +166,7 @@ public async Task OpenFolderAsync(bool addLog) if (files.Count == 0) { return; } - if (!addLog) + if (!combineLog) { await CloseAllLogsAsync(); } @@ -179,13 +179,13 @@ public async Task OpenFolderAsync(bool addLog) public Task OpenIssueAsync() => OpenBrowserAsync("https://github.com/microsoft/EventLogExpert/issues/new"); - public Task OpenLiveLogAsync(string logName, bool addLog) => OpenLogAsync(logName, PathType.LogName, addLog); + public Task OpenLiveLogAsync(string logName, bool combineLog) => OpenLogAsync(logName, PathType.LogName, combineLog); - public async Task OpenLogAsync(string logPath, PathType pathType, bool shouldAddLog = false) + public async Task OpenLogAsync(string logPath, PathType pathType, bool combineLog = false) { if (string.IsNullOrWhiteSpace(logPath)) { return; } - if (shouldAddLog && _eventLogState.Value.ActiveLogs.ContainsKey(logPath)) { return; } + if (combineLog && _eventLogState.Value.ActiveLogs.ContainsKey(logPath)) { return; } EventLogInformation? eventLogInformation; @@ -215,7 +215,7 @@ await _dialogService.ShowAlert( return; } - if (!shouldAddLog) + if (!combineLog) { await _cancellationTokenSource.CancelAsync(); _dispatcher.Dispatch(new EventLogAction.CloseAll()); diff --git a/src/EventLogExpert/Shared/Components/Menu/MenuBar.razor.cs b/src/EventLogExpert/Shared/Components/Menu/MenuBar.razor.cs index 75355f47..fffb5835 100644 --- a/src/EventLogExpert/Shared/Components/Menu/MenuBar.razor.cs +++ b/src/EventLogExpert/Shared/Components/Menu/MenuBar.razor.cs @@ -1,6 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. +using EventLogExpert.Eventing.Helpers; using EventLogExpert.UI; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Services; @@ -127,9 +128,9 @@ private IReadOnlyList BuildOpenSubMenu(bool combineLog) MenuItem.Item("Folder", () => Actions.OpenFolderAsync(combineLog)), MenuItem.SubMenu("Live", [ - MenuItem.Item("Application", () => Actions.OpenLiveLogAsync("Application", combineLog)), - MenuItem.Item("System", () => Actions.OpenLiveLogAsync("System", combineLog)), - MenuItem.Item("Security", () => Actions.OpenLiveLogAsync("Security", combineLog), isEnabled: isAdmin), + MenuItem.Item(LogNames.ApplicationLog, () => Actions.OpenLiveLogAsync(LogNames.ApplicationLog, combineLog)), + MenuItem.Item(LogNames.SystemLog, () => Actions.OpenLiveLogAsync(LogNames.SystemLog, combineLog)), + MenuItem.Item(LogNames.SecurityLog, () => Actions.OpenLiveLogAsync(LogNames.SecurityLog, combineLog), isEnabled: isAdmin), MenuItem.AsyncSubMenu( "Other Logs", async () => BuildOtherLogsTree(await Actions.GetOtherLogNamesAsync(), combineLog, isAdmin)), @@ -137,7 +138,7 @@ private IReadOnlyList BuildOpenSubMenu(bool combineLog) ]; } - private IReadOnlyList BuildOtherLogsTree(IReadOnlyList logNames, bool addLog, bool isAdmin) + private IReadOnlyList BuildOtherLogsTree(IReadOnlyList logNames, bool combineLog, bool isAdmin) { var rootChildren = new List(); var folderMap = new Dictionary>(StringComparer.OrdinalIgnoreCase); @@ -149,8 +150,8 @@ private IReadOnlyList BuildOtherLogsTree(IReadOnlyList logName if (path.Count == 0) { continue; } var log = path[^1]; - var logIsEnabled = isAdmin || !LogNameMethods.AdminOnlyLiveLogNames.Contains(logName); - var logMenuItem = MenuItem.Item(log, () => Actions.OpenLiveLogAsync(logName, addLog), isEnabled: logIsEnabled); + var logIsEnabled = isAdmin || !LogNames.AdminOnlyLiveLogNames.Contains(logName); + var logMenuItem = MenuItem.Item(log, () => Actions.OpenLiveLogAsync(logName, combineLog), isEnabled: logIsEnabled); if (path.Count == 1) {