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
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
<ItemGroup>
<InternalsVisibleTo Include="EventLogExpert.Eventing.Tests" />
<InternalsVisibleTo Include="EventLogExpert.Eventing.IntegrationTests" />
<InternalsVisibleTo Include="EventLogExpert.Eventing.TestUtils" />
</ItemGroup>

</Project>
74 changes: 60 additions & 14 deletions src/EventLogExpert.Eventing/Interop/NativeMethods.Evt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,17 +71,7 @@ internal static partial class NativeMethods
case (int)EvtVariantType.FileTime:
return DateTime.FromFileTimeUtc((long)variant.FileTimeVal);
case (int)EvtVariantType.SysTime:
var sysTime = Marshal.PtrToStructure<SystemTime>(variant.SysTimeVal);

return new DateTime(
sysTime.Year,
sysTime.Month,
sysTime.Day,
sysTime.Hour,
sysTime.Minute,
sysTime.Second,
sysTime.Milliseconds,
DateTimeKind.Utc);
return ReadSysTime(variant);
case (int)EvtVariantType.Sid:
return variant.SidVal == IntPtr.Zero ? null : new SecurityIdentifier(variant.SidVal);
case (int)EvtVariantType.HexInt32:
Expand Down Expand Up @@ -129,6 +119,47 @@ internal static partial class NativeMethods
}
}

internal static EventProperty ConvertVariantToProperty(EvtVariant variant)
{
switch (variant.Type)
{
case (int)EvtVariantType.SByte:
return variant.SByteVal;
case (int)EvtVariantType.Byte:
return variant.ByteVal;
case (int)EvtVariantType.Int16:
return variant.Int16Val;
case (int)EvtVariantType.UInt16:
return variant.UInt16Val;
case (int)EvtVariantType.Int32:
case (int)EvtVariantType.HexInt32:
return variant.Int32Val;
case (int)EvtVariantType.UInt32:
return variant.UInt32Val;
case (int)EvtVariantType.Int64:
return variant.Int64Val;
case (int)EvtVariantType.UInt64:
case (int)EvtVariantType.HexInt64:
return variant.UInt64Val;
case (int)EvtVariantType.Single:
return variant.SingleVal;
case (int)EvtVariantType.Double:
return variant.DoubleVal;
case (int)EvtVariantType.Boolean:
return variant.BooleanVal != 0;
case (int)EvtVariantType.SizeT:
return variant.SizeTVal;
case (int)EvtVariantType.FileTime:
return DateTime.FromFileTimeUtc((long)variant.FileTimeVal);
case (int)EvtVariantType.SysTime:
return ReadSysTime(variant);
default:
// Reference shapes reuse the boxing converter (reference types add no allocation; the rare boxed
// Guid is acceptable). Null / unsupported types throw, preserving the boxed path's ?? throw contract.
return EventProperty.FromReference(ConvertVariant(variant) ?? throw new InvalidDataException());
}
}

/// <summary>Closes an open handle</summary>
[LibraryImport(EventLogApi, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
Expand Down Expand Up @@ -621,7 +652,7 @@ internal static EventRecord RenderEvent(EvtHandle eventHandle)
}
}

internal static IReadOnlyList<object> RenderEventProperties(EvtHandle eventHandle)
internal static IReadOnlyList<EventProperty> RenderEventProperties(EvtHandle eventHandle)
{
bool success = EvtRender(
EventLogSession.GlobalSession.UserRenderContext,
Expand Down Expand Up @@ -671,7 +702,7 @@ internal static IReadOnlyList<object> RenderEventProperties(EvtHandle eventHandl

if (propertyCount <= 0) { return []; }

var properties = new object[propertyCount];
var properties = new EventProperty[propertyCount];

unsafe
{
Expand All @@ -681,7 +712,7 @@ internal static IReadOnlyList<object> RenderEventProperties(EvtHandle eventHandl

for (int i = 0; i < propertyCount; i++)
{
properties[i] = ConvertVariant(variants[i]) ?? throw new InvalidDataException();
properties[i] = ConvertVariantToProperty(variants[i]);
}
}
}
Expand Down Expand Up @@ -870,4 +901,19 @@ private static unsafe T[] ReadBlittableArray<T>(IntPtr reference, uint count, Ev
throw new InvalidDataException($"Expected EVT_VARIANT type UInt16, got {variant.Type}.") :
variant.UInt16Val;
}

private static DateTime ReadSysTime(EvtVariant variant)
{
var sysTime = Marshal.PtrToStructure<SystemTime>(variant.SysTimeVal);

return new DateTime(
sysTime.Year,
sysTime.Month,
sysTime.Day,
sysTime.Hour,
sysTime.Minute,
sysTime.Second,
sysTime.Milliseconds,
DateTimeKind.Utc);
}
}
8 changes: 8 additions & 0 deletions src/EventLogExpert.Eventing/Interop/NativeMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ internal static partial IntPtr FindResourceExA(
int lpName,
ushort wLanguage = 0);

// String-typed resource lookup (FindResourceExA only handles integer-typed resources). Used to locate the
// "WEVT_TEMPLATE" resource by name. Returns an HRSRC, a non-owning pointer; see FindResourceExA above.
[LibraryImport(Kernel32Api, StringMarshalling = StringMarshalling.Utf16, SetLastError = true)]
internal static partial IntPtr FindResourceW(
LibraryHandle hModule,
string lpName,
string lpType);
Comment thread
jschick04 marked this conversation as resolved.

[LibraryImport(Kernel32Api, StringMarshalling = StringMarshalling.Utf16, SetLastError = true)]
internal static partial int FormatMessageW(
uint dwFlags,
Expand Down
157 changes: 146 additions & 11 deletions src/EventLogExpert.Eventing/PublisherMetadata/EventMessageProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,43 @@ public sealed class EventMessageProvider(

public ProviderDetails LoadProviderDetails() => LoadProviderDetailsCore(null);

internal static string InjectMapAttribute(string template, string fieldName, string mapName)
{
string nameAttribute = $"name=\"{fieldName}\"";
int searchStart = 0;

while (true)
{
int dataIndex = template.IndexOf("<data", searchStart, StringComparison.OrdinalIgnoreCase);

if (dataIndex < 0) { return template; }

int afterTag = dataIndex + "<data".Length;
char delimiter = afterTag < template.Length ? template[afterTag] : '\0';

// "<data" prefixes "<dataSource"; only an element whose tag ends here is a real <data> node.
if (delimiter is not (' ' or '\t' or '\r' or '\n' or '>' or '/'))
{
searchStart = afterTag;

continue;
}

int elementEnd = template.IndexOf('>', afterTag);

if (elementEnd < 0) { return template; }

int nameIndex = template.IndexOf(nameAttribute, dataIndex, StringComparison.OrdinalIgnoreCase);

if (nameIndex >= 0 && nameIndex < elementEnd)
{
return template.Insert(nameIndex + nameAttribute.Length, $" map=\"{mapName}\"");
}

searchStart = elementEnd + 1;
}
}

internal static List<MessageModel> LoadMessagesFromFiles(
IEnumerable<string> legacyProviderFiles,
string providerName,
Expand All @@ -48,6 +85,38 @@ internal static List<MessageModel> LoadMessagesFromFiles(
return messages;
}

private static void InjectMapAttributes(
IReadOnlyList<EventModel> events,
IReadOnlyDictionary<WevtEventKey, IReadOnlyDictionary<string, string>> eventFieldMaps,
IReadOnlyDictionary<string, ValueMapDefinition> decodedMaps)
{
if (eventFieldMaps.Count == 0) { return; }

foreach (EventModel model in events)
{
if (string.IsNullOrEmpty(model.Template)) { continue; }

if (!eventFieldMaps.TryGetValue(
new WevtEventKey((uint)model.Id, model.Version),
out IReadOnlyDictionary<string, string>? fieldMaps))
{
continue;
}

string template = model.Template;

foreach ((string fieldName, string mapName) in fieldMaps)
{
if (decodedMaps.ContainsKey(mapName))
{
template = InjectMapAttribute(template, fieldName, mapName);
}
}

model.Template = template;
}
}

private LegacyMessageFileSource? BuildLazySource(IReadOnlyList<string> files)
{
if (files.Count == 0) { return null; }
Expand Down Expand Up @@ -75,10 +144,6 @@ internal static List<MessageModel> LoadMessagesFromFiles(
return total > 0 ? new LegacyMessageFileSource(walkable, _providerName, total, _logger) : null;
}

/// <summary>
/// Loads the messages for a modern provider. This info is stored at
/// Computer\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\WINEVT
/// </summary>
private ProviderDetails LoadMessagesFromModernProvider(ProviderMetadata providerMetadata)
{
_logger?.Debug($"{nameof(LoadMessagesFromModernProvider)} called for provider {_providerName}");
Expand Down Expand Up @@ -139,6 +204,8 @@ private ProviderDetails LoadMessagesFromModernProvider(ProviderMetadata provider
_logger?.Debug($"Failed to load Tasks for modern provider: {_providerName}. Exception:\n{ex}");
}

PopulateValueMaps(provider, providerMetadata);

_logger?.Debug($"Returning {provider.Events.Count} events for provider {_providerName}");

return provider;
Expand Down Expand Up @@ -192,13 +259,80 @@ private ProviderDetails LoadProviderDetailsCore(HashSet<string>? visited)
return provider;
}

/// <summary>
/// Final fallback when neither modern publisher metadata nor a legacy registry entry exists for the configured
/// provider name. Some events (notably modern channel-named providers like
/// "Microsoft-Windows-AppXDeploymentServer/Operational") carry a channel path in the ProviderName slot; the real
/// publisher must be looked up through the channel config's OwningPublisher property and resolved separately. Produces
/// no result on failure.
/// </summary>
private void PopulateValueMaps(ProviderDetails provider, ProviderMetadata providerMetadata)
{
try
{
Guid publisherGuid = providerMetadata.PublisherGuid;

if (publisherGuid == Guid.Empty) { return; }

string resourceFilePath = providerMetadata.ResourceFilePath;

if (string.IsNullOrEmpty(resourceFilePath)) { return; }

WevtTemplateData? templateData = WevtTemplateReader.TryRead(resourceFilePath, publisherGuid, _logger);

if (templateData is null || templateData.Maps.Count == 0) { return; }

Dictionary<string, ValueMapDefinition> decodedMaps = new(StringComparer.Ordinal);

foreach ((string mapName, WevtRawMap rawMap) in templateData.Maps)
{
ValueMapDefinition? definition = ResolveMap(rawMap, providerMetadata);

if (definition is not null)
{
decodedMaps[mapName] = definition;
}
}

if (decodedMaps.Count == 0) { return; }

provider.Maps = decodedMaps;

InjectMapAttributes(provider.Events, templateData.EventFieldMaps, decodedMaps);
}
catch (Exception ex) when (ex is not OutOfMemoryException
and not StackOverflowException
and not AccessViolationException)
{
_logger?.Debug($"Failed to populate value maps for modern provider: {_providerName}. Exception:\n{ex}");
}
}

private ValueMapDefinition? ResolveMap(WevtRawMap rawMap, ProviderMetadata providerMetadata)
{
List<ValueMapEntry> entries = new(rawMap.Entries.Count);

foreach (WevtRawMapEntry entry in rawMap.Entries)
{
if (entry.MessageId == uint.MaxValue) { continue; }

string name;

try
{
name = providerMetadata.FormatMessageById(entry.MessageId);
}
catch (Exception ex) when (ex is not OutOfMemoryException
and not StackOverflowException
and not AccessViolationException)
{
_logger?.Debug(
$"Failed to resolve map message {entry.MessageId} for provider {_providerName}: {ex.Message}");

continue;
}

if (string.IsNullOrEmpty(name)) { continue; }

entries.Add(new ValueMapEntry(entry.Value, name.TrimEnd('\0', '\r', '\n', '\t', ' ')));
}

return entries.Count > 0 ? new ValueMapDefinition(rawMap.IsBitMap, entries) : null;
}

private void TryFallbackToOwningPublisher(ProviderDetails target, HashSet<string>? visited)
{
// Bound the fallback in case channel/publisher misconfiguration creates a chain.
Expand Down Expand Up @@ -253,6 +387,7 @@ private void TryFallbackToOwningPublisher(ProviderDetails target, HashSet<string
target.Keywords = ownerDetails.Keywords;
target.Opcodes = ownerDetails.Opcodes;
target.Tasks = ownerDetails.Tasks;
target.Maps = ownerDetails.Maps;
target.ResolvedFromOwningPublisher = owningPublisher;
}

Expand Down
Loading
Loading