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
14 changes: 8 additions & 6 deletions src/EventLogExpert.Eventing/Common/Channels/LogChannelNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ public static class LogChannelNames
public const string StateLog = "State";
public const string SystemLog = "System";

/// <summary>Live event log channels that require process elevation to read.</summary>
public static IReadOnlySet<string> AdminOnlyLiveChannels { get; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
SecurityLog,
StateLog,
};
private static readonly string[] s_securityScopedChannels = [SecurityLog, StateLog];

/// <summary>Legacy protected live channels retained for catalog validation and export contracts.</summary>
public static IReadOnlySet<string> AdminOnlyLiveChannels { get; } =
new HashSet<string>(s_securityScopedChannels, StringComparer.OrdinalIgnoreCase);

public static IReadOnlySet<string> RegistrySkipChannels { get; } =
new HashSet<string>(s_securityScopedChannels, StringComparer.OrdinalIgnoreCase);
}
10 changes: 10 additions & 0 deletions src/EventLogExpert.Eventing/Interop/EvtEnums.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,16 @@ internal enum EvtChannelConfigPropertyId
EvtChannelConfigIsolation = 1,
EvtChannelConfigType = 2,
EvtChannelConfigOwningPublisher = 3,
EvtChannelConfigClassicEventlog = 4,
EvtChannelConfigAccess = 5,
}

public enum EvtChannelType
{
Admin = 0,
Operational = 1,
Analytic = 2,
Debug = 3,
}

internal enum EvtVariantType
Expand Down
117 changes: 117 additions & 0 deletions src/EventLogExpert.Eventing/Interop/NativeMethods.Access.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// // Copyright (c) Microsoft Corporation.
// // Licensed under the MIT License.

using Microsoft.Win32.SafeHandles;
using System.Runtime.InteropServices;

namespace EventLogExpert.Eventing.Interop;

internal static partial class NativeMethods
{
private const string Advapi32Api = "advapi32.dll";

[LibraryImport(Advapi32Api, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool AccessCheck(
IntPtr securityDescriptor,
SafeAccessTokenHandle clientToken,
uint desiredAccess,
ref GenericMapping genericMapping,
IntPtr privilegeSet,
ref uint privilegeSetLength,
out uint grantedAccess,
[MarshalAs(UnmanagedType.Bool)] out bool accessStatus);

[LibraryImport(Advapi32Api, EntryPoint = "ConvertStringSecurityDescriptorToSecurityDescriptorW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool ConvertStringSecurityDescriptorToSecurityDescriptor(
string stringSecurityDescriptor,
uint stringSecurityDescriptorRevision,
out IntPtr securityDescriptor,
out uint securityDescriptorSize);

[LibraryImport(Advapi32Api, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool DuplicateTokenEx(
SafeAccessTokenHandle existingToken,
uint desiredAccess,
IntPtr tokenAttributes,
SecurityImpersonationLevel impersonationLevel,
TokenType tokenType,
out SafeAccessTokenHandle newToken);

[LibraryImport(Kernel32Api, SetLastError = true)]
internal static partial IntPtr GetCurrentProcess();

[LibraryImport(Kernel32Api, SetLastError = true)]
internal static partial IntPtr LocalFree(IntPtr memory);

[LibraryImport(Advapi32Api, EntryPoint = "LookupPrivilegeValueW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool LookupPrivilegeValue(
string? systemName,
string name,
out Luid luid);

[LibraryImport(Advapi32Api)]
internal static partial void MapGenericMask(ref uint accessMask, ref GenericMapping genericMapping);

[LibraryImport(Advapi32Api, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool OpenProcessToken(
IntPtr processHandle,
uint desiredAccess,
out SafeAccessTokenHandle tokenHandle);

[LibraryImport(Advapi32Api, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool PrivilegeCheck(
SafeAccessTokenHandle clientToken,
ref PrivilegeSet requiredPrivileges,
[MarshalAs(UnmanagedType.Bool)] out bool result);
}

[StructLayout(LayoutKind.Sequential)]
internal struct GenericMapping
{
internal uint GenericRead;
internal uint GenericWrite;
internal uint GenericExecute;
internal uint GenericAll;
}

internal enum SecurityImpersonationLevel
{
SecurityAnonymous = 0,
SecurityIdentification = 1,
SecurityImpersonation = 2,
SecurityDelegation = 3
}

internal enum TokenType
{
TokenPrimary = 1,
TokenImpersonation = 2
}

[StructLayout(LayoutKind.Sequential)]
internal struct Luid
{
internal uint LowPart;
internal int HighPart;
}

[StructLayout(LayoutKind.Sequential)]
internal struct LuidAndAttributes
{
internal Luid Luid;
internal uint Attributes;
}

[StructLayout(LayoutKind.Sequential)]
internal struct PrivilegeSet
{
internal uint PrivilegeCount;
internal uint Control;
internal LuidAndAttributes Privilege;
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public IReadOnlyList<string> GetMessageFilesForLegacyProvider(string providerNam

foreach (var logSubKeyName in eventLogKey.GetSubKeyNames())
{
if (LogChannelNames.AdminOnlyLiveChannels.Contains(logSubKeyName))
if (LogChannelNames.RegistrySkipChannels.Contains(logSubKeyName))
{
continue;
}
Expand Down Expand Up @@ -83,7 +83,5 @@ public IReadOnlyList<string> GetMessageFilesForLegacyProvider(string providerNam
return [];
}

private class OpenEventLogRegistryKeyFailedException(string msg) : Exception(msg)
{
}
private class OpenEventLogRegistryKeyFailedException(string msg) : Exception(msg);
}
12 changes: 12 additions & 0 deletions src/EventLogExpert.Eventing/Readers/ChannelAccess.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// // Copyright (c) Microsoft Corporation.
// // Licensed under the MIT License.

namespace EventLogExpert.Eventing.Readers;

public enum ChannelAccess
{
Accessible,
RequiresElevation,
Unknown,
NotEvaluated
}
11 changes: 11 additions & 0 deletions src/EventLogExpert.Eventing/Readers/ChannelConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// // Copyright (c) Microsoft Corporation.
// // Licensed under the MIT License.

using EventLogExpert.Eventing.Interop;

namespace EventLogExpert.Eventing.Readers;

public sealed record ChannelConfig(bool? Enabled, ChannelAccess Access, EvtChannelType? Type)
{
public static ChannelConfig Unknown { get; } = new(null, ChannelAccess.Unknown, null);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// // Copyright (c) Microsoft Corporation.
// // Licensed under the MIT License.

using EventLogExpert.Eventing.Interop;

namespace EventLogExpert.Eventing.Readers;

internal sealed record ChannelConfigPropertySnapshot(bool? Enabled, string? AccessSddl, EvtChannelType? Type);
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// // Copyright (c) Microsoft Corporation.
// // Licensed under the MIT License.

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

namespace EventLogExpert.Eventing.Readers;

internal sealed class EventLogChannelConfigPropertyReader(ITraceLogger? logger = null) : IChannelConfigPropertyReader
{
private readonly ITraceLogger? _logger = logger;

public ChannelConfigPropertySnapshot ReadProperties(string channelName)
{
ArgumentException.ThrowIfNullOrWhiteSpace(channelName);

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

if (channelConfig.IsInvalid)
{
int openError = Marshal.GetLastWin32Error();

_logger?.Debug(
$"{nameof(ReadProperties)}: EvtOpenChannelConfig failed for {channelName}. Error: {openError} ({NativeMethods.FormatSystemMessage((uint)openError) ?? "unknown"})");

return new ChannelConfigPropertySnapshot(null, null, null);
}

var type = ReadChannelType(channelConfig, channelName);
var enabled = ReadEnabled(channelConfig, channelName);
var accessSddl = type is EvtChannelType.Analytic or EvtChannelType.Debug
? null
: ReadAccessSddl(channelConfig, channelName);

return new ChannelConfigPropertySnapshot(enabled, accessSddl, type);
}

// ConvertVariant boxes the channel type as uint; Enum.IsDefined(typeof(...), boxedUint) throws on this int-backed enum, so match the enum value via the generic overload.
internal static EvtChannelType? ConvertChannelType(object? value) =>
value switch
{
uint raw when Enum.IsDefined((EvtChannelType)raw) => (EvtChannelType)raw,
int raw when Enum.IsDefined((EvtChannelType)raw) => (EvtChannelType)raw,
_ => null
};

private string? ReadAccessSddl(EvtHandle channelConfig, string channelName) =>
ReadProperty(channelConfig, EvtChannelConfigPropertyId.EvtChannelConfigAccess, channelName) as string;

private EvtChannelType? ReadChannelType(EvtHandle channelConfig, string channelName) =>
ConvertChannelType(ReadProperty(channelConfig, EvtChannelConfigPropertyId.EvtChannelConfigType, channelName));

private bool? ReadEnabled(EvtHandle channelConfig, string channelName) =>
ReadProperty(channelConfig, EvtChannelConfigPropertyId.EvtChannelConfigEnabled, channelName) as bool?;
Comment thread
jschick04 marked this conversation as resolved.

private unsafe object? ReadProperty(
EvtHandle channelConfig,
EvtChannelConfigPropertyId propertyId,
string channelName)
{
IntPtr buffer = IntPtr.Zero;

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

int sizeError = Marshal.GetLastWin32Error();

if (!success && sizeError != Win32ErrorCodes.ERROR_INSUFFICIENT_BUFFER)
{
_logger?.Debug(
$"{nameof(ReadProperty)}: size probe failed for {channelName} {propertyId}. Error: {sizeError}");

return null;
}

buffer = Marshal.AllocHGlobal(bufferSize);

success = NativeMethods.EvtGetChannelConfigProperty(
channelConfig,
propertyId,
0,
bufferSize,
buffer,
out _);

if (!success)
{
int readError = Marshal.GetLastWin32Error();

_logger?.Debug(
$"{nameof(ReadProperty)}: read failed for {channelName} {propertyId}. Error: {readError}");

return null;
}

var variant = *(EvtVariant*)buffer;
return NativeMethods.ConvertVariant(variant);
}
catch (Exception ex) when (ex is not OutOfMemoryException
and not StackOverflowException
and not AccessViolationException)
{
_logger?.Debug(
$"{nameof(ReadProperty)}: unexpected failure for {channelName} {propertyId}.\n{ex}");

return null;
}
finally
{
if (buffer != IntPtr.Zero)
{
Marshal.FreeHGlobal(buffer);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// // Copyright (c) Microsoft Corporation.
// // Licensed under the MIT License.

using EventLogExpert.Eventing.Common.Channels;
using EventLogExpert.Eventing.Interop;
using EventLogExpert.Logging.Abstractions;

namespace EventLogExpert.Eventing.Readers;

public sealed class EventLogChannelConfigReader : IChannelConfigReader, IDisposable
{
private readonly IChannelAccessEvaluator _accessEvaluator;
private readonly IChannelAccessNative? _ownedAccessNative;
private readonly IChannelConfigPropertyReader _propertyReader;

public EventLogChannelConfigReader(ITraceLogger? logger = null)
{
_ownedAccessNative = new Win32ChannelAccessNative();
_accessEvaluator = new NativeChannelAccessEvaluator(_ownedAccessNative);
_propertyReader = new EventLogChannelConfigPropertyReader(logger);
}

internal EventLogChannelConfigReader(
IChannelConfigPropertyReader propertyReader,
IChannelAccessEvaluator accessEvaluator)
{
_propertyReader = propertyReader;
_accessEvaluator = accessEvaluator;
}

public void Dispose() => _ownedAccessNative?.Dispose();

public ChannelConfig ReadConfig(string channelName)
{
ArgumentException.ThrowIfNullOrWhiteSpace(channelName);

var properties = _propertyReader.ReadProperties(channelName);

if (properties.Type is not { } type)
{
return new ChannelConfig(properties.Enabled, ChannelAccess.Unknown, null);
}

if (type is EvtChannelType.Analytic or EvtChannelType.Debug)
{
return new ChannelConfig(properties.Enabled, ChannelAccess.NotEvaluated, type);
}

var access = _accessEvaluator.EvaluateAccess(
properties.AccessSddl,
string.Equals(channelName, LogChannelNames.SecurityLog, StringComparison.OrdinalIgnoreCase));

return new ChannelConfig(properties.Enabled, access, type);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// // Copyright (c) Microsoft Corporation.
// // Licensed under the MIT License.

namespace EventLogExpert.Eventing.Readers;

internal interface IChannelAccessEvaluator
{
ChannelAccess EvaluateAccess(string? sddl, bool isSecurityChannel);
}
Loading
Loading