Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Switch to LocalProviderEventResolver #8

Merged
merged 1 commit into from
Oct 24, 2022
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 @@ -3,6 +3,8 @@
using EventLogExpert.Library.Providers;
using System.Diagnostics;
using System.Diagnostics.Eventing.Reader;
using System.Text;
using System.Text.RegularExpressions;

namespace EventLogExpert.Library.EventResolvers;

Expand All @@ -12,8 +14,9 @@ namespace EventLogExpert.Library.EventResolvers;
/// </summary>
public class LocalProviderEventResolver : IEventResolver
{
private Dictionary<string, EventMessageProvider> _messageProviders = new();
private Dictionary<string, ProviderDetails> _providerDetails = new();
private Action<string> _tracer;
private Regex _formatRegex = new("%+[0-9]+");

public LocalProviderEventResolver()
{
Expand All @@ -27,16 +30,15 @@ public LocalProviderEventResolver(Action<string> tracer)

public DisplayEventModel Resolve(EventRecord eventRecord)
{
_messageProviders.TryGetValue(eventRecord.ProviderName, out EventMessageProvider provider);

if (provider == null)
_providerDetails.TryGetValue(eventRecord.ProviderName, out ProviderDetails providerDetails);
if (providerDetails == null)
{
provider = new EventMessageProvider(eventRecord.ProviderName, _tracer);
provider.LoadProviderDetails();
_messageProviders.Add(eventRecord.ProviderName, provider);
var provider = new EventMessageProvider(eventRecord.ProviderName, _tracer);
providerDetails = provider.LoadProviderDetails();
_providerDetails.Add(eventRecord.ProviderName, providerDetails);
}

if (provider == null)
if (providerDetails == null)
{
return new DisplayEventModel(
eventRecord.RecordId,
Expand All @@ -49,7 +51,149 @@ public DisplayEventModel Resolve(EventRecord eventRecord)
"Description not found. No provider available.");
}

// TODO: Implement this
return null;
string description = null;
string taskName = null;

if (eventRecord.Version != null && eventRecord.LogName != null)
{
var modernEvents = providerDetails.Events?.Where(e => e.Id == eventRecord.Id && e.Version == eventRecord.Version && e.LogName == eventRecord.LogName).ToList();
if (modernEvents != null && modernEvents.Count == 0)
{
// Try again forcing the long to a short and with no log name. This is needed for providers such as Microsoft-Windows-Complus
modernEvents = providerDetails.Events?.Where(e => (short)e.Id == eventRecord.Id && e.Version == eventRecord.Version).ToList();
}

if (modernEvents != null && modernEvents.Any())
{
if (modernEvents.Count > 1)
{
_tracer("Ambiguous modern event found:");
foreach (var modernEvent in modernEvents)
{
_tracer($" Version: {modernEvent.Version} Id: {modernEvent.Id} LogName: {modernEvent.LogName} Description: {modernEvent.Description}");
}
}

var e = modernEvents[0];

taskName = providerDetails.Tasks.FirstOrDefault(t => t.Value == e.Task)?.Name;

// If we don't have a description template
if (string.IsNullOrEmpty(e.Description))
{
// And we have exactly one property
if (eventRecord.Properties.Count == 1)
{
// Return that property as the description. This is what certain EventRecords look like
// when the entire description is a string literal, and there is no provider DLL needed.
description = eventRecord.Properties[0].ToString();
}
else
{
description = "This event record is missing a template. The following information was included with the event:\n\n" +
string.Join("\n", eventRecord.Properties);
}
}
else
{
description = FormatDescription(eventRecord, e.Description);
}
}
}

if (description == null)
{
var potentialTaskNames = providerDetails.Messages?.Where(m => m.ShortId == eventRecord.Task && m.LogLink != null && m.LogLink == eventRecord.LogName).ToList();
if (potentialTaskNames != null && potentialTaskNames.Any())
{
taskName = potentialTaskNames[0].Text;

if (potentialTaskNames.Count > 1)
{
_tracer("More than one matching task ID was found.");
_tracer($" eventRecord.Task: {eventRecord.Task}");
_tracer(" Potential matches:");
potentialTaskNames.ForEach(t => _tracer($" {t.LogLink} {t.Text}"));
}
}
else
{
taskName = eventRecord.Task == null | eventRecord.Task == 0 ? "None" : $"({eventRecord.Task}";
}

description = providerDetails.Messages?.FirstOrDefault(m => m.ShortId == eventRecord.Id)?.Text;
description = FormatDescription(eventRecord, description);
}

if (description == null)
{
description = "";
}

return new DisplayEventModel(
eventRecord.RecordId,
eventRecord.TimeCreated,
eventRecord.Id,
eventRecord.MachineName,
(SeverityLevel?)eventRecord.Level,
eventRecord.ProviderName,
taskName,
description);
}

private string FormatDescription(EventRecord eventRecord, string descriptionTemplate)
{
if (descriptionTemplate == null)
{
return null;
}

string description = descriptionTemplate
.Replace("\r\n%n", " \r\n")
.Replace("%n\r\n", "\r\n ")
.Replace("%n", "\r\n");
var matches = _formatRegex.Matches(description);
if (matches.Count > 0)
{
var sb = new StringBuilder();
var lastIndex = 0;
for (var i = 0; i < matches.Count; i++)
{
if (matches[i].Value.StartsWith("%%"))
{
// The % is escaped, so skip it.
continue;
}

sb.Append(description.AsSpan(lastIndex, matches[i].Index - lastIndex));
var propIndex = int.Parse(matches[i].Value.Trim(new[] { '{', '}', '%' }));
var propValue = eventRecord.Properties[propIndex - 1].Value;
if (propValue is DateTime)
{
// Exactly match the format produced by EventRecord.FormatMessage(). I have no idea why it includes Unicode LRM marks, but it does.
sb.Append(((DateTime)propValue).ToUniversalTime().ToString("\u200Eyyyy\u200E-\u200EMM\u200E-\u200EddTHH:mm:ss.fffffff00K"));
}
else
{
sb.Append(propValue);
}

lastIndex = matches[i].Index + matches[i].Length;
}

if (lastIndex < description.Length)
{
sb.Append(description.Substring(lastIndex));
}

description = sb.ToString();
}

while (description.EndsWith("\r\n"))
{
description = description.Remove(description.Length - "\r\n".Length);
}

return description;
}
}
25 changes: 25 additions & 0 deletions src/EventLogExpert.Library/Helpers/NativeMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,37 @@ public class NativeMethods
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr LoadLibrary(string fileName);

[Flags]
public enum LoadLibraryFlags : uint
{
None = 0,
DONT_RESOLVE_DLL_REFERENCES = 0x00000001,
LOAD_IGNORE_CODE_AUTHZ_LEVEL = 0x00000010,
LOAD_LIBRARY_AS_DATAFILE = 0x00000002,
LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE = 0x00000040,
LOAD_LIBRARY_AS_IMAGE_RESOURCE = 0x00000020,
LOAD_LIBRARY_SEARCH_APPLICATION_DIR = 0x00000200,
LOAD_LIBRARY_SEARCH_DEFAULT_DIRS = 0x00001000,
LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR = 0x00000100,
LOAD_LIBRARY_SEARCH_SYSTEM32 = 0x00000800,
LOAD_LIBRARY_SEARCH_USER_DIRS = 0x00000400,
LOAD_WITH_ALTERED_SEARCH_PATH = 0x00000008
}

[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr LoadLibraryEx(string lpFileName, IntPtr hReservedNull, LoadLibraryFlags dwFlags);

[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool FreeLibrary(IntPtr hModule);

[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr FindResource(IntPtr hModule, int lpID, int lpType);

public delegate bool EnumResTypeProc(IntPtr hModule, string lpszType, IntPtr lParam);

[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool EnumResourceTypes(IntPtr hModule, EnumResTypeProc lpEnumFunc, IntPtr lParam);

[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr LoadResource(IntPtr hModule, IntPtr hResInfo);

Expand Down
51 changes: 47 additions & 4 deletions src/EventLogExpert.Library/Providers/EventMessageProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,31 @@ private IEnumerable<MessageModel> LoadMessagesFromLegacyProvider()

try
{
// https://stackoverflow.com/questions/33498244/marshaling-a-message-table-resource
hModule = NativeMethods.LoadLibrary(file);
/*
* https://stackoverflow.com/questions/33498244/marshaling-a-message-table-resource
*
* The approach documented there has some issues, so we deviate a bit.
*
* RT_MESSAGETABLE is not found unless LoadLibraryEx is called with LOAD_LIBRARY_AS_DATAFILE.
* So we must use LoadLibraryEx below rather than LoadLibrary. Msedgeupdate.dll exposes this
* issue.
*/

hModule = NativeMethods.LoadLibraryEx(file, IntPtr.Zero, NativeMethods.LoadLibraryFlags.LOAD_LIBRARY_AS_DATAFILE);

// TODO: Evaulate if there's any need to EnumResourceTypes.
// This is an alternative approach to FindResource. Leaving it here until we're sure FindResource is good enough.
var result = NativeMethods.EnumResourceTypes(hModule, GetMessagesFromOneResource, IntPtr.Zero);

var msgTableInfo =
NativeMethods.FindResource(hModule, 1, NativeMethods.RT_MESSAGETABLE);

if (msgTableInfo == IntPtr.Zero)
{
_traceAction($"No message table found. Returning 0 messages from file: {file}");
continue;
}

var msgTable = NativeMethods.LoadResource(hModule, msgTableInfo);
var memTable = NativeMethods.LockResource(msgTable);

Expand Down Expand Up @@ -91,6 +110,13 @@ private IEnumerable<MessageModel> LoadMessagesFromLegacyProvider()
{
text = Marshal.PtrToStringUni(textPtr);
}
else if (flags == 2)
{
// All the ESE messages are a single-byte character set
// but have flags of 2, which is not defined. So just
// treat it as ANSI I guess?
text = Marshal.PtrToStringAnsi(textPtr);
}
else
{
text = "Error: Bad flags. Could not get text.";
Expand Down Expand Up @@ -133,6 +159,12 @@ private IEnumerable<MessageModel> LoadMessagesFromLegacyProvider()
return messages;
}

private bool GetMessagesFromOneResource(IntPtr hModule, string lpszType, IntPtr lParam)
{
// No need to implement this as long as we can use FindResource instead.
return true;
}

/// <summary>
/// Loads the messages for a modern provider. This info is stored at
/// Computer\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\WINEVT
Expand All @@ -144,11 +176,22 @@ private ProviderDetails LoadMessagesFromModernProvider()

var provider = new ProviderDetails { ProviderName = _providerName };

var providerMetadata = new ProviderMetadata(_providerName);
ProviderMetadata providerMetadata;
try
{
providerMetadata = new ProviderMetadata(_providerName);
}
catch (Exception ex)
{
_traceAction($"Couldn't get metadata for rovider {_providerName}. Exception: {ex}. Returning empty Events list.");
provider.Events = new List<EventModel>();
return provider;
}

if (providerMetadata.Id == Guid.Empty)
{
_traceAction($"Provider {_providerName} has no provider GUID. Returning empty provider.");
_traceAction($"Provider {_providerName} has no provider GUID. Returning empty Events list.");
provider.Events = new List<EventModel>();
return provider;
}

Expand Down
28 changes: 28 additions & 0 deletions src/EventLogExpert.Test/EventLogExpert.Test.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.1.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\EventLogExpert.Library\EventLogExpert.Library.csproj" />
</ItemGroup>

</Project>
Loading