Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/PullRequest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ jobs:
shell: pwsh
env:
EVENTLOG_CONTAINER: "1"
EVENTLOG_ALLOW_CHANNEL_MUTATION: "1"
run: |
$projects = @(Get-ChildItem tests/Integration -Filter *.csproj -Recurse)
if ($projects.Count -eq 0) { throw 'No integration test projects found under tests/Integration.' }
Expand Down
1 change: 1 addition & 0 deletions compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ x-base: &base
environment:
NUGET_PACKAGES: C:/nuget-packages
EVENTLOG_CONTAINER: "1"
EVENTLOG_ALLOW_CHANNEL_MUTATION: "1"
volumes:
- type: bind
source: .
Expand Down
9 changes: 9 additions & 0 deletions src/EventLogExpert.Eventing/Interop/EvtVariant.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,13 @@ internal readonly record struct EvtVariant
[FieldOffset(0)] internal readonly nint XmlValArr;
[FieldOffset(8)] internal readonly uint Count;
[FieldOffset(12)] internal readonly uint Type;

// Zero the whole 16-byte union (including the padding between BooleanVal and Count) before setting the
// Boolean, so no indeterminate bytes reach the native EvtSetChannelConfigProperty call.
internal EvtVariant(bool booleanValue)
{
this = default;
BooleanVal = booleanValue ? 1 : 0;
Type = (uint)EvtVariantType.Boolean;
}
}
14 changes: 14 additions & 0 deletions src/EventLogExpert.Eventing/Interop/NativeMethods.Evt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,20 @@ internal static partial bool EvtRender(
out int bufferUsed,
out int propertyCount);

/// <summary>Persists the channel configuration changes made with EvtSetChannelConfigProperty (requires elevation)</summary>
[LibraryImport(EventLogApi, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool EvtSaveChannelConfig(EvtHandle channelConfig, int flags);

/// <summary>Sets the specified channel configuration property on the in-memory config handle</summary>
[LibraryImport(EventLogApi, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool EvtSetChannelConfigProperty(
EvtHandle channelConfig,
EvtChannelConfigPropertyId propertyId,
int flags,
in EvtVariant propertyValue);

/// <summary>
/// Creates a subscription that will receive current and future events from a channel or log file that match the
/// specified query criteria
Expand Down
158 changes: 158 additions & 0 deletions src/EventLogExpert.Eventing/Writers/ChannelConfigWriter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// // Copyright (c) Microsoft Corporation.
// // Licensed under the MIT License.

using EventLogExpert.Eventing.Interop;
using EventLogExpert.Eventing.Readers;
using EventLogExpert.Logging.Abstractions;
using System.Runtime.InteropServices;

namespace EventLogExpert.Eventing.Writers;

public sealed class ChannelConfigWriter(ITraceLogger? logger = null) : IChannelConfigWriter
{
private readonly ITraceLogger? _logger = logger;

public ChannelEnableResult EnableChannel(string channelName)
{
ArgumentException.ThrowIfNullOrWhiteSpace(channelName);

using var channelConfig = NativeMethods.EvtOpenChannelConfig(
EventLogSession.GlobalSession.Handle,
channelName,
0);

if (channelConfig.IsInvalid)
{
return FromNativeError(Marshal.GetLastWin32Error(), channelName, nameof(NativeMethods.EvtOpenChannelConfig));
}

// Read-first, fail-closed: a failed or non-Boolean read must never be treated as "disabled" (that would drive a
// machine write on an unreadable state). Only a successful Boolean read that is already true short-circuits.
if (!TryReadEnabled(channelConfig, channelName, out bool alreadyEnabled, out ChannelEnableResult readFailure))
{
return readFailure;
}

if (alreadyEnabled)
{
return new ChannelEnableResult(ChannelEnableOutcome.AlreadyEnabled, Win32ErrorCodes.ERROR_SUCCESS);
}

var enabledValue = new EvtVariant(true);

if (!NativeMethods.EvtSetChannelConfigProperty(
channelConfig,
EvtChannelConfigPropertyId.EvtChannelConfigEnabled,
0,
in enabledValue))
{
return FromNativeError(
Marshal.GetLastWin32Error(),
channelName,
nameof(NativeMethods.EvtSetChannelConfigProperty));
}

return !NativeMethods.EvtSaveChannelConfig(channelConfig, 0) ?
FromNativeError(Marshal.GetLastWin32Error(),
channelName,
nameof(NativeMethods.EvtSaveChannelConfig)) :
// EvtSaveChannelConfig returning true is the persistence guarantee, so the change is committed here.
new ChannelEnableResult(ChannelEnableOutcome.Enabled, Win32ErrorCodes.ERROR_SUCCESS);
}

private ChannelEnableResult FromNativeError(int win32Error, string channelName, string stage)
{
_logger?.Debug($"{nameof(EnableChannel)}: {stage} failed for {channelName}. Error: {win32Error}");

var outcome = win32Error switch
{
Win32ErrorCodes.ERROR_ACCESS_DENIED => ChannelEnableOutcome.AccessDenied,
Win32ErrorCodes.ERROR_EVT_CHANNEL_NOT_FOUND => ChannelEnableOutcome.NotFound,
_ => ChannelEnableOutcome.Failed
};

return new ChannelEnableResult(outcome, win32Error);
}

private unsafe bool TryReadEnabled(
EvtHandle channelConfig,
string channelName,
out bool enabled,
out ChannelEnableResult failure)
{
enabled = false;
failure = null!;
IntPtr buffer = IntPtr.Zero;

try
{
bool success = NativeMethods.EvtGetChannelConfigProperty(
channelConfig,
EvtChannelConfigPropertyId.EvtChannelConfigEnabled,
0,
0,
IntPtr.Zero,
out int bufferSize);

int sizeError = Marshal.GetLastWin32Error();

if (!success && sizeError != Win32ErrorCodes.ERROR_INSUFFICIENT_BUFFER)
{
failure = FromNativeError(sizeError, channelName, nameof(NativeMethods.EvtGetChannelConfigProperty));

return false;
}

buffer = Marshal.AllocHGlobal(bufferSize);

if (!NativeMethods.EvtGetChannelConfigProperty(
channelConfig,
EvtChannelConfigPropertyId.EvtChannelConfigEnabled,
0,
bufferSize,
buffer,
out _))
{
failure = FromNativeError(
Marshal.GetLastWin32Error(),
channelName,
nameof(NativeMethods.EvtGetChannelConfigProperty));

return false;
}

var variant = *(EvtVariant*)buffer;

if (variant.Type != (uint)EvtVariantType.Boolean)
{
// The read succeeded but the value is not Boolean, so the last Win32 error is not meaningful; surface a
// non-zero sentinel (ERROR_INVALID_DATA) rather than 0 so downstream diagnostics stay actionable.
_logger?.Debug(
$"{nameof(EnableChannel)}: {channelName} enabled property was not Boolean (type {variant.Type}).");

failure = new ChannelEnableResult(ChannelEnableOutcome.Failed, Win32ErrorCodes.ERROR_INVALID_DATA);

return false;
}
Comment thread
jschick04 marked this conversation as resolved.

enabled = variant.BooleanVal != 0;
return true;
}
catch (Exception ex) when (ex is not OutOfMemoryException
and not StackOverflowException
and not AccessViolationException)
{
_logger?.Debug($"{nameof(EnableChannel)}: unexpected failure reading enabled state for {channelName}.\n{ex}");
failure = new ChannelEnableResult(ChannelEnableOutcome.Failed, Win32ErrorCodes.ERROR_INVALID_DATA);

return false;
}
Comment thread
jschick04 marked this conversation as resolved.
finally
{
if (buffer != IntPtr.Zero)
{
Marshal.FreeHGlobal(buffer);
}
}
}
}
25 changes: 25 additions & 0 deletions src/EventLogExpert.Eventing/Writers/ChannelEnableOutcome.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// // Copyright (c) Microsoft Corporation.
// // Licensed under the MIT License.

namespace EventLogExpert.Eventing.Writers;

public enum ChannelEnableOutcome
{
/// <summary>The channel was disabled and has now been enabled (the save committed).</summary>
Enabled,

/// <summary>The channel was already enabled, so no write was performed.</summary>
AlreadyEnabled,

/// <summary>The caller is not elevated; the enable was not attempted (set by the runtime service).</summary>
NotElevated,

/// <summary>The channel configuration could not be opened or saved because access was denied.</summary>
AccessDenied,

/// <summary>No channel with the requested name is registered on the computer.</summary>
NotFound,

/// <summary>The enable failed for any other reason; see the Win32 error code.</summary>
Failed
}
6 changes: 6 additions & 0 deletions src/EventLogExpert.Eventing/Writers/ChannelEnableResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// // Copyright (c) Microsoft Corporation.
// // Licensed under the MIT License.

namespace EventLogExpert.Eventing.Writers;

public sealed record ChannelEnableResult(ChannelEnableOutcome Outcome, int Win32Error);
13 changes: 13 additions & 0 deletions src/EventLogExpert.Eventing/Writers/IChannelConfigWriter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// // Copyright (c) Microsoft Corporation.
// // Licensed under the MIT License.

namespace EventLogExpert.Eventing.Writers;

public interface IChannelConfigWriter
{
/// <summary>
/// Enables a disabled event log channel by persisting <c>EvtChannelConfigEnabled = true</c>. Requires an elevated
/// process; without elevation a native call fails and the result is <see cref="ChannelEnableOutcome.AccessDenied" />.
/// </summary>
ChannelEnableResult EnableChannel(string channelName);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using EventLogExpert.DatabaseTools.DependencyInjection;
using EventLogExpert.Eventing.Readers;
using EventLogExpert.Eventing.Writers;
using EventLogExpert.Logging.Abstractions;
using EventLogExpert.Logging.Configuration;
using EventLogExpert.Logging.Routing;
Expand Down Expand Up @@ -276,9 +277,12 @@ public IServiceCollection AddEventLogRuntime()
services.AddSingleton<BuiltInScenarioRegistry>();
services.AddSingleton<IChannelConfigReader>(static sp =>
new EventLogChannelConfigReader(CategoryLogger(sp, LogCategories.EventLog)));
services.AddSingleton<IChannelConfigWriter>(static sp =>
new ChannelConfigWriter(CategoryLogger(sp, LogCategories.EventLog)));
services.AddSingleton<ChannelPresenceProbe>();
services.AddSingleton<IChannelPresenceProbe>(static sp => sp.GetRequiredService<ChannelPresenceProbe>());
services.AddSingleton<IChannelReadinessService>(static sp => sp.GetRequiredService<ChannelPresenceProbe>());
services.AddSingleton<IChannelEnableService, ChannelEnableService>();
services.AddSingleton<IEvtxChannelReader, EvtxChannelReader>();
services.AddSingleton<IScenarioQueryService, ScenarioQueryService>();
services.AddSingleton<IScenarioLaunchService, ScenarioLaunchService>();
Expand Down
44 changes: 44 additions & 0 deletions src/EventLogExpert.Runtime/Scenarios/ChannelEnableService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// // Copyright (c) Microsoft Corporation.
// // Licensed under the MIT License.

using EventLogExpert.Eventing.Writers;
using EventLogExpert.Runtime.Common.Identity;

namespace EventLogExpert.Runtime.Scenarios;

internal sealed class ChannelEnableService(
IChannelConfigWriter channelConfigWriter,
IChannelReadinessService readinessService,
IWindowsIdentityProvider identityProvider) : IChannelEnableService
{
private readonly Lazy<bool> _canEnable = new(identityProvider.IsUserInAdministratorRole);

public bool CanEnable => _canEnable.Value;

public async Task<ChannelEnableResult> EnableAsync(string channel, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(channel);

// Do not attempt a write we know will be denied; a non-elevated caller never reaches the native layer.
if (!CanEnable)
{
return new ChannelEnableResult(ChannelEnableOutcome.NotElevated, 0);
}

// Cancellation is honored only up to the moment the native write begins: the token guards both the
// synchronous entry and the Task.Run scheduling boundary, so a cancel that arrives before the writer runs
// prevents the write. Once EnableChannel starts it runs to completion (Task.Run stops consulting the token
// once the delegate is executing), so a committed machine change is never reported as cancelled.
cancellationToken.ThrowIfCancellationRequested();

var result = await Task.Run(() => channelConfigWriter.EnableChannel(channel), cancellationToken)
.ConfigureAwait(false);

if (result.Outcome is ChannelEnableOutcome.Enabled or ChannelEnableOutcome.AlreadyEnabled)
{
readinessService.Invalidate();
}

return result;
}
}
17 changes: 17 additions & 0 deletions src/EventLogExpert.Runtime/Scenarios/IChannelEnableService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// // Copyright (c) Microsoft Corporation.
// // Licensed under the MIT License.

using EventLogExpert.Eventing.Writers;

namespace EventLogExpert.Runtime.Scenarios;

public interface IChannelEnableService
{
/// <summary>
/// Whether an enable can be ATTEMPTED (the process is elevated). A true value is not a guarantee that the write
/// will succeed - a channel-specific ACL can still deny an elevated caller.
/// </summary>
bool CanEnable { get; }

Task<ChannelEnableResult> EnableAsync(string channel, CancellationToken cancellationToken = default);
}
26 changes: 26 additions & 0 deletions src/EventLogExpert.UI/Common/EnableChannelConfirmation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// // Copyright (c) Microsoft Corporation.
// // Licensed under the MIT License.

using EventLogExpert.Runtime.Alerts;

namespace EventLogExpert.UI.Common;

internal static class EnableChannelConfirmation
{
public static Task<bool> ConfirmAsync(IAlertDialogService dialog, string channelName, bool isAnalyticOrDebug)
{
var message =
$"Enable the \"{channelName}\" log?\n\n" +
"This changes Windows event logging for the whole computer and stays in effect until it is disabled again. " +
"Only events generated after enabling are collected - events that occurred while the log was disabled cannot be recovered.";

if (isAnalyticOrDebug)
{
message +=
"\n\nThis is an analytic or debug log: enabling it can clear the records it already holds, and Windows may " +
"keep collecting to it while refusing to open it for live viewing until it is disabled again.";
}

return dialog.ShowAlert("Enable log", message, "Enable", "Cancel");
}
}
Loading
Loading