Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
d577a7c
Hash event templates by render-equivalent field tuples
jschick04 Jun 25, 2026
8894f39
Route modern provider loading through a shared ProviderDetails assembler
jschick04 Jun 25, 2026
8307e60
Remove redundant ProviderMetadata getters superseded by the assembler
jschick04 Jun 25, 2026
f888ce0
Add offline WEVT_TEMPLATE provider reader feeding the shared assembler
jschick04 Jun 26, 2026
143cc49
Reproduce EvtFormatMessage escaping for offline provider descriptions
jschick04 Jun 26, 2026
efc832f
Populate offline provider messages and parameters to match the live path
jschick04 Jun 26, 2026
a8d34f2
Extract a shared message-table open loop in the legacy message source
jschick04 Jun 26, 2026
feb5d48
Synthesize struct-template binary XML in the offline WEVT reader
jschick04 Jun 26, 2026
2220115
Broaden the offline WEVT fixed-length gate to all length-bearing types
jschick04 Jun 26, 2026
c5788ab
Strip the first numbered insert's format spec in offline messages
jschick04 Jun 27, 2026
f8fe557
Relabel offline classic-provider events with their native qualified id
jschick04 Jun 27, 2026
9e0716a
Restrict the offline first-insert format-spec strip to string specs
jschick04 Jun 27, 2026
b86d565
Escape the apostrophe in offline template attribute values
jschick04 Jun 27, 2026
1b18783
Resolve offline %%N parameter references in event descriptions
jschick04 Jun 27, 2026
4915700
Guard WEVT read-helper bounds checks against integer overflow
jschick04 Jun 27, 2026
472069e
Addressed unchecked casts, attribute case and map escaping
jschick04 Jun 27, 2026
3d765fe
Fail closed when a template data element has no signature value
jschick04 Jun 27, 2026
2db3f7f
Compare template signatures without allocating encode buffers
jschick04 Jun 27, 2026
03a9104
Add offline foreign-image provider extraction core
jschick04 Jun 27, 2026
fbd362e
Add offline image provider source facade with full enumeration
jschick04 Jun 28, 2026
75aca07
Route create-database to the offline image provider source
jschick04 Jun 28, 2026
a5f6509
Recover dirty image registry hives via RegLoadKey fallback
jschick04 Jun 28, 2026
ac4ab90
Build offline provider databases from WIM and ESD images
jschick04 Jun 28, 2026
959863d
Resolve offline legacy provider parameter message files
jschick04 Jun 28, 2026
a6a83ab
Limit the offline image-kind error and isolate the hive native API
jschick04 Jun 28, 2026
03fd770
Harden offline image path containment and resolve ProgramFiles providers
jschick04 Jun 28, 2026
f950d53
Infer offline image kind from the path and make --image-kind optional
jschick04 Jun 28, 2026
c48f7af
Reject iso at parse and clarify the orphan wim-index error
jschick04 Jun 28, 2026
04d7ae9
Make WIM size estimate reparse-safe and re-arm the orphan-mount sweep…
jschick04 Jun 29, 2026
ba1756e
Fail closed when the recovery ownership beacon cannot be published
jschick04 Jun 29, 2026
fcdc276
Name the Mutex name argument
jschick04 Jun 29, 2026
b91ee76
Skip already-seen names before building modern offline providers
jschick04 Jun 29, 2026
fa49437
Fail the WIM free-space preflight on negative required bytes
jschick04 Jun 29, 2026
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 @@ -108,6 +108,30 @@ protected static async IAsyncEnumerable<ProviderDetails> LoadLocalProvidersAsync
}
}

/// <summary>
/// Streams providers extracted from a mounted or extracted foreign Windows image, fully offline. Mirrors
/// <see cref="LoadLocalProvidersAsync" />: offline extraction is synchronous (registry-hive + DLL reads), so this
/// wrapper exposes it as an <see cref="IAsyncEnumerable{T}" /> for the shared <c>await foreach</c> consume loop; the
/// await keeps it a valid async iterator without scheduling an extra continuation. The facade applies the name filter
/// and exclude set itself (so this does not re-filter) and stamps each provider with the IMAGE's OS provenance.
/// </summary>
protected static async IAsyncEnumerable<ProviderDetails> LoadOfflineImageProvidersAsync(
string offlineImagePath,
ITraceLogger logger,
Regex? regex,
IReadOnlySet<string>? excludeProviderNames = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
await Task.CompletedTask;

foreach (var details in OfflineImageProviderSource.LoadProviders(offlineImagePath, logger, regex, excludeProviderNames))
{
cancellationToken.ThrowIfCancellationRequested();

yield return details;
}
}

/// <summary>
/// Emits the provider-details column header sized to the longest provider name. Updates the instance format
/// string so subsequent <see cref="LogProviderDetails" /> calls align to the same width.
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,37 @@
namespace EventLogExpert.DatabaseTools.CreateDatabase;

/// <summary>
/// Creates a new provider database from local providers or from a source. When <see cref="SourcePath" /> is null,
/// local providers on this machine are used (no fallback). When supplied, ONLY the source is used.
/// Creates a new provider database from local providers, a file source, or an offline Windows image. When both
/// <see cref="SourcePath" /> and <see cref="OfflineImagePath" /> are null, local providers on this machine are used
/// (no fallback). When <see cref="SourcePath" /> is supplied, ONLY that source is used. When
/// <see cref="OfflineImagePath" /> is supplied, ONLY that image is used, fully offline; it is mutually exclusive with
/// <see cref="SourcePath" />.
/// </summary>
/// <param name="TargetPath">Target .db file path. Must not already exist; must have .db extension.</param>
/// <param name="SourcePath">Optional source: .db, .evtx, or folder. Null = local providers.</param>
/// <param name="SourcePath">
/// Optional source: .db, .evtx, or folder. Null = local providers (unless an offline image is
/// given).
/// </param>
/// <param name="FilterRegex">Optional regex applied to provider names; null = no filter.</param>
/// <param name="SkipProvidersInFile">Optional source whose provider names are excluded from the new database.</param>
public sealed record CreateDatabaseRequest(string TargetPath, string? SourcePath, Regex? FilterRegex, string? SkipProvidersInFile);
/// <param name="OfflineImagePath">
/// Optional offline Windows image to extract providers from, fully offline (no host
/// registry or host files). Null = not an offline build. Mutually exclusive with <paramref name="SourcePath" />.
/// </param>
/// <param name="ImageKind">
/// How <paramref name="OfflineImagePath" /> is accessed: a mounted volume/extracted folder (
/// <see cref="OfflineImageKind.Directory" />) or a <c>.wim</c>/<c>.esd</c> file (<see cref="OfflineImageKind.Wim" />,
/// which extracts <paramref name="WimIndex" /> first). Null = auto-detect from the path (directory vs .wim/.esd/.iso).
/// </param>
/// <param name="WimIndex">
/// The 1-based image index to extract from a <c>.wim</c>/<c>.esd</c>, for
/// <see cref="OfflineImageKind.Wim" />. Null otherwise.
/// </param>
public sealed record CreateDatabaseRequest(
string TargetPath,
string? SourcePath,
Regex? FilterRegex,
string? SkipProvidersInFile,
string? OfflineImagePath = null,
OfflineImageKind? ImageKind = null,
int? WimIndex = null);
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// // Copyright (c) Microsoft Corporation.
// // Licensed under the MIT License.

namespace EventLogExpert.DatabaseTools.CreateDatabase;

/// <summary>
/// How the offline Windows image named by <see cref="CreateDatabaseRequest.OfflineImagePath" /> is accessed when
/// building a provider database from it. <see cref="Directory" /> reads a mounted volume or extracted image folder;
/// <see cref="Wim" /> extracts an image (by <see cref="CreateDatabaseRequest.WimIndex" />) from a <c>.wim</c>/
/// <c>.esd</c> file first. <see cref="Iso" /> is reserved for a later phase.
/// </summary>
public enum OfflineImageKind
{
Directory,
Wim,
Iso
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,29 @@ public static Command GetCommand()
"would be saved in the new database."
};

Option<string> offlineImageOption = new("--offline-image")
{
Description =
"Build the database from a Windows image, fully offline (no host registry or host files are " +
"read): a mounted volume root (e.g. an attached VHDX), an extracted image folder, or a .wim/.esd " +
"file (use --wim-index N to pick the image). The kind is auto-detected from the path; override with " +
"--image-kind. Mutually exclusive with the source argument."
};

Option<string> imageKindOption = new("--image-kind")
{
Description =
"Override how --offline-image is read: 'directory' (a mounted volume or extracted folder) or 'wim' " +
"(a .wim/.esd file). Omit to auto-detect from the path (a directory, or a .wim/.esd file)."
};

Option<int?> wimIndexOption = new("--wim-index")
{
Description =
"The 1-based image index to extract from a .wim/.esd, for --image-kind wim. Omit to list the " +
"available images."
};

Option<bool> verboseOption = new("--verbose")
{
Description = "Enable verbose logging. May be useful for troubleshooting."
Expand All @@ -54,6 +77,9 @@ public static Command GetCommand()
createDatabaseCommand.Arguments.Add(sourceArgument);
createDatabaseCommand.Options.Add(filterOption);
createDatabaseCommand.Options.Add(skipProvidersInFileOption);
createDatabaseCommand.Options.Add(offlineImageOption);
createDatabaseCommand.Options.Add(imageKindOption);
createDatabaseCommand.Options.Add(wimIndexOption);
createDatabaseCommand.Options.Add(verboseOption);

createDatabaseCommand.SetAction(async result =>
Expand All @@ -70,11 +96,31 @@ public static Command GetCommand()
return;
}

var imageKindValue = result.GetValue(imageKindOption);
OfflineImageKind? imageKind = null;

if (!string.IsNullOrWhiteSpace(imageKindValue))
{
if (!Enum.TryParse(imageKindValue, ignoreCase: true, out OfflineImageKind parsed) ||
!Enum.IsDefined(parsed) ||
parsed == OfflineImageKind.Iso)
{
logger.Error($"Invalid --image-kind '{imageKindValue}'. Valid values: directory, wim.");

return;
}

imageKind = parsed;
}

var request = new CreateDatabaseRequest(
result.GetRequiredValue(fileArgument),
result.GetValue(sourceArgument),
regex,
result.GetValue(skipProvidersInFileOption));
result.GetValue(skipProvidersInFileOption),
OfflineImagePath: result.GetValue(offlineImageOption),
ImageKind: imageKind,
WimIndex: result.GetValue(wimIndexOption));

var factory = sp.GetRequiredService<IDatabaseToolsOperationFactory>();

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

using System.Runtime.InteropServices;

namespace EventLogExpert.Eventing.Interop;

internal static partial class NativeMethods
{
/// <summary>Read access mask (STANDARD_RIGHTS_READ | KEY_QUERY_VALUE | KEY_ENUMERATE_SUB_KEYS | KEY_NOTIFY).</summary>
internal const int KEY_READ = 0x20019;
internal const int SE_PRIVILEGE_ENABLED = 0x00000002;
internal const int TOKEN_ADJUST_PRIVILEGES = 0x00000020;
internal const int TOKEN_QUERY = 0x00000008;

private const string Advapi32Api = "advapi32.dll";

/// <summary>Predefined handle for HKEY_LOCAL_MACHINE, under which recovery-loaded image hives are mounted.</summary>
internal static readonly IntPtr HKEY_LOCAL_MACHINE = unchecked((int)0x80000002);

/// <summary>
/// Enables or disables the single privilege in <paramref name="newState" /> on <paramref name="tokenHandle" />,
/// writing the prior state to <paramref name="previousState" /> so the caller can restore it exactly. The BOOL return
/// is <see langword="true" /> even when the privilege is not held by the token; callers MUST also check
/// <c>GetLastError() == ERROR_SUCCESS</c> (a not-held privilege reports <c>ERROR_NOT_ALL_ASSIGNED</c>).
/// </summary>
[LibraryImport(Advapi32Api, EntryPoint = "AdjustTokenPrivileges", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool AdjustTokenPrivileges(
IntPtr tokenHandle,
[MarshalAs(UnmanagedType.Bool)] bool disableAllPrivileges,
ref TOKEN_PRIVILEGES newState,
int bufferLength,
ref TOKEN_PRIVILEGES previousState,
out int returnLength);

[LibraryImport(Kernel32Api, EntryPoint = "CloseHandle", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool CloseHandle(IntPtr handle);

[LibraryImport(Kernel32Api, EntryPoint = "GetCurrentProcess")]
internal static partial IntPtr GetCurrentProcess();

[LibraryImport(Advapi32Api, EntryPoint = "LookupPrivilegeValueW", StringMarshalling = StringMarshalling.Utf16, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool LookupPrivilegeValue(string? lpSystemName, string lpName, out long lpLuid);

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

/// <summary>
/// Loads the registry hive in <paramref name="lpFile" /> into a private, process-local application subtree and
/// returns the root key handle. Unlike <c>RegLoadKey</c> it needs no backup/restore privilege (so it runs unelevated),
/// and the hive auto-unloads once every returned key is closed. The handle is returned as a raw <see cref="IntPtr" />
/// because the source-generated marshaller cannot construct a <c>SafeRegistryHandle</c> for an <c>out</c> parameter;
/// the caller wraps it. Returns a non-zero Win32 error code on failure (it does not set last error). It cannot replay
/// the dual-log sidecars a dirty image hive carries - that recovery requires <see cref="RegLoadKey" />.
/// </summary>
[LibraryImport(Advapi32Api, EntryPoint = "RegLoadAppKeyW", StringMarshalling = StringMarshalling.Utf16)]
internal static partial int RegLoadAppKey(string lpFile, out IntPtr phkResult, int samDesired, int dwOptions, int reserved);

/// <summary>
/// Mounts the hive in <paramref name="lpFile" /> under <paramref name="hKey" />\<paramref name="lpSubKey" />,
/// performing log recovery (replaying the dual <c>.LOG1</c>/<c>.LOG2</c> sidecars a dirty image hive carries) that
/// <see cref="RegLoadAppKey" /> cannot. Requires <c>SeBackupPrivilege</c> + <c>SeRestorePrivilege</c>; returns a
/// non-zero Win32 error code on failure. The named subtree persists until <see cref="RegUnLoadKey" />.
/// </summary>
[LibraryImport(Advapi32Api, EntryPoint = "RegLoadKeyW", StringMarshalling = StringMarshalling.Utf16)]
internal static partial int RegLoadKey(IntPtr hKey, string lpSubKey, string lpFile);

/// <summary>
/// Unmounts the hive previously mounted at <paramref name="hKey" />\<paramref name="lpSubKey" /> by
/// <see cref="RegLoadKey" />. Requires <c>SeRestorePrivilege</c>; fails while any key under the subtree is still open.
/// Returns a non-zero Win32 error code on failure.
/// </summary>
[LibraryImport(Advapi32Api, EntryPoint = "RegUnLoadKeyW", StringMarshalling = StringMarshalling.Utf16)]
internal static partial int RegUnLoadKey(IntPtr hKey, string lpSubKey);

// TOKEN_PRIVILEGES with exactly one LUID_AND_ATTRIBUTES. Pack=4 is REQUIRED: the native layout is
// DWORD PrivilegeCount; LUID(8 bytes, 4-aligned); DWORD Attributes. Default 8-byte packing would 8-align the
// long Luid (offset 8 instead of 4), so AdjustTokenPrivileges reads a misaligned LUID and silently enables nothing
// (returns TRUE with ERROR_NOT_ALL_ASSIGNED) - the exact wrong-Pack trap the lease unit test guards against.
[StructLayout(LayoutKind.Sequential, Pack = 4)]
internal struct TOKEN_PRIVILEGES
{
internal int PrivilegeCount;
internal long Luid;
internal int Attributes;
}
}
113 changes: 113 additions & 0 deletions src/EventLogExpert.Eventing/Interop/NativeMethods.Wim.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// // Copyright (c) Microsoft Corporation.
// // Licensed under the MIT License.

using System.Runtime.InteropServices;

namespace EventLogExpert.Eventing.Interop;

internal static partial class NativeMethods
{
internal const uint WIM_COMPRESS_NONE = 0;

/// <summary>
/// <see cref="WIMApplyImage" /> flags that skip restoring the captured directory/file security descriptors, so
/// the extracted tree is owned by the (elevated) caller and is readable + deletable. Without these the default apply
/// restores TrustedInstaller-owned ACLs, which would block staging the hives and the recursive temp cleanup.
/// </summary>
internal const uint WIM_FLAG_NO_DIRACL = 0x00000010;

internal const uint WIM_FLAG_NO_FILEACL = 0x00000020;
/// <summary>Read access for <see cref="WIMCreateFile" /> (GENERIC_READ).</summary>
internal const uint WIM_GENERIC_READ = 0x80000000;

/// <summary><see cref="WIMRegisterMessageCallback" /> sentinel returned when registration fails.</summary>
internal const uint WIM_INVALID_CALLBACK_VALUE = 0xFFFFFFFF;

/// <summary>Message callback return: abort the in-progress apply (WIMApplyImage then fails with ERROR_REQUEST_ABORTED).</summary>
internal const uint WIM_MSG_ABORT_IMAGE = 0xFFFFFFFF;

/// <summary>Message callback return: continue the operation.</summary>
internal const uint WIM_MSG_SUCCESS = 0;

/// <summary>Open-existing disposition for <see cref="WIMCreateFile" /> (OPEN_EXISTING).</summary>
internal const uint WIM_OPEN_EXISTING = 3;

private const string WimgapiApi = "wimgapi.dll";

/// <summary>
/// Native WIM message callback (<c>fpMessageProc</c>). Returns <see cref="WIM_MSG_SUCCESS" /> to continue or
/// <see cref="WIM_MSG_ABORT_IMAGE" /> to abort the current apply.
/// </summary>
internal delegate uint WimMessageCallback(uint messageId, IntPtr wParam, IntPtr lParam, IntPtr userData);

/// <summary>
/// Frees a buffer allocated by the system with <c>LocalAlloc</c> (e.g. the XML from
/// <see cref="WIMGetImageInformation" />).
/// </summary>
[LibraryImport(Kernel32Api, EntryPoint = "LocalFree", SetLastError = true)]
internal static partial IntPtr LocalFree(IntPtr mem);

/// <summary>
/// Applies (extracts) the loaded <paramref name="image" /> to <paramref name="path" /> with
/// <paramref name="applyFlags" />. Requires administrator privileges. Returns <see langword="false" /> on failure
/// (sets last error); a callback-driven abort surfaces as <c>ERROR_REQUEST_ABORTED</c>.
/// </summary>
[LibraryImport(WimgapiApi, EntryPoint = "WIMApplyImage", StringMarshalling = StringMarshalling.Utf16, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool WIMApplyImage(WimImageSafeHandle image, string path, uint applyFlags);

/// <summary>Closes a WIM file or image handle. Used by the <c>SafeHandle</c> wrappers' release.</summary>
[LibraryImport(WimgapiApi, EntryPoint = "WIMCloseHandle", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool WIMCloseHandle(IntPtr handle);

/// <summary>
/// Opens a <c>.wim</c>/<c>.esd</c> image file. Returns an invalid handle on failure (sets last error);
/// <paramref name="creationResult" /> receives whether the file was created or opened.
/// </summary>
[LibraryImport(WimgapiApi, EntryPoint = "WIMCreateFile", StringMarshalling = StringMarshalling.Utf16, SetLastError = true)]
internal static partial WimFileSafeHandle WIMCreateFile(
string wimPath,
uint desiredAccess,
uint creationDisposition,
uint flagsAndAttributes,
uint compressionType,
out uint creationResult);

/// <summary>Returns the number of images in <paramref name="wim" /> (0 on failure, sets last error).</summary>
[LibraryImport(WimgapiApi, EntryPoint = "WIMGetImageCount", SetLastError = true)]
internal static partial uint WIMGetImageCount(WimFileSafeHandle wim);

/// <summary>
/// Returns the WIM's <c>&lt;WIM&gt;</c> XML metadata (UTF-16) describing every image, in a system-allocated
/// buffer the caller MUST release with <see cref="LocalFree" />. <paramref name="imageInfoBytes" /> is the byte
/// length.
/// </summary>
[LibraryImport(WimgapiApi, EntryPoint = "WIMGetImageInformation", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool WIMGetImageInformation(WimFileSafeHandle wim, out IntPtr imageInfo, out uint imageInfoBytes);

/// <summary>
/// Loads the 1-based image <paramref name="imageIndex" /> from <paramref name="wim" />. Invalid handle on
/// failure.
/// </summary>
[LibraryImport(WimgapiApi, EntryPoint = "WIMLoadImage", SetLastError = true)]
internal static partial WimImageSafeHandle WIMLoadImage(WimFileSafeHandle wim, uint imageIndex);

/// <summary>
/// Registers a per-WIM message callback (a raw function pointer, so the caller MUST keep the delegate rooted for
/// the call's duration). Returns the callback index, or <see cref="WIM_INVALID_CALLBACK_VALUE" /> on failure.
/// </summary>
[LibraryImport(WimgapiApi, EntryPoint = "WIMRegisterMessageCallback", SetLastError = true)]
internal static partial uint WIMRegisterMessageCallback(WimFileSafeHandle wim, IntPtr messageProc, IntPtr userData);

/// <summary>Sets the scratch directory WIMGAPI uses while loading/applying images on <paramref name="wim" />.</summary>
[LibraryImport(WimgapiApi, EntryPoint = "WIMSetTemporaryPath", StringMarshalling = StringMarshalling.Utf16, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool WIMSetTemporaryPath(WimFileSafeHandle wim, string path);

/// <summary>Unregisters the callback previously registered for <paramref name="wim" />.</summary>
[LibraryImport(WimgapiApi, EntryPoint = "WIMUnregisterMessageCallback", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool WIMUnregisterMessageCallback(WimFileSafeHandle wim, IntPtr messageProc);
}
Loading
Loading