mirror of
https://github.com/Yamato-Security/WELA.git
synced 2026-10-02 20:44:42 +02:00
* Add bounded local delivery measurement and exact EVTX samples * Link delivery measurement changelog to PR 430 * Reject evidence aliases before Windows path normalization * Use PowerShell 5.1-compatible record IDs and bound fixture cleanup * Revalidate the native EVTX artifact before recording final evidence * Require exact observed local computer identities for sampled events * Clarify provider scope within shared built-in event channels * Preserve mixed XML payload ordering in EVTX sample verification * Bound ordered event XML comparisons for nested UserData * Dispose observer wait handle when bookmark creation fails
124 lines
7.8 KiB
C#
124 lines
7.8 KiB
C#
// Native local future-event delivery observer. No log, policy or service writes.
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Runtime.InteropServices;
|
|
using System.Threading;
|
|
|
|
namespace Wela.EventMeasurementV1 {
|
|
public sealed class Delivery {
|
|
public string Xml;
|
|
public string BookmarkXml;
|
|
public double ElapsedSeconds;
|
|
}
|
|
public sealed class Capture {
|
|
public string Status;
|
|
public int NativeError;
|
|
public string Diagnostic;
|
|
public string StartedUtc;
|
|
public string CompletedUtc;
|
|
public double RegistrationSeconds;
|
|
public double ElapsedSeconds;
|
|
public int OutsideWindowCallbacks;
|
|
public int BeforeWindowCallbacks;
|
|
public long XmlUtf8Bytes;
|
|
public Delivery[] Events;
|
|
}
|
|
public sealed class Observer : IDisposable {
|
|
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
|
private delegate uint Callback(uint action, IntPtr context, IntPtr evt);
|
|
[DllImport("wevtapi.dll", CharSet=CharSet.Unicode, SetLastError=true)]
|
|
private static extern IntPtr EvtSubscribe(IntPtr session, IntPtr signal, string channel, string query, IntPtr bookmark, IntPtr context, Callback callback, uint flags);
|
|
[DllImport("wevtapi.dll", CharSet=CharSet.Unicode, SetLastError=true)]
|
|
private static extern IntPtr EvtCreateBookmark(string xml);
|
|
[DllImport("wevtapi.dll", SetLastError=true)]
|
|
[return: MarshalAs(UnmanagedType.Bool)]
|
|
private static extern bool EvtUpdateBookmark(IntPtr bookmark, IntPtr evt);
|
|
[DllImport("wevtapi.dll", SetLastError=true)]
|
|
[return: MarshalAs(UnmanagedType.Bool)]
|
|
private static extern bool EvtRender(IntPtr context, IntPtr evt, uint flags, int size, IntPtr buffer, out int used, out int count);
|
|
[DllImport("wevtapi.dll", SetLastError=true)]
|
|
[return: MarshalAs(UnmanagedType.Bool)]
|
|
private static extern bool EvtClose(IntPtr handle);
|
|
private readonly object gate = new object();
|
|
private readonly ManualResetEvent finished = new ManualResetEvent(false);
|
|
private readonly Stopwatch timer = new Stopwatch();
|
|
private readonly List<Delivery> events = new List<Delivery>();
|
|
private readonly Callback callback;
|
|
private readonly int seconds, maximum;
|
|
private IntPtr subscription, bookmark;
|
|
private bool armed, disposed, completed;
|
|
private string status = "WindowComplete", diagnostic = "", started;
|
|
private int nativeError, outsideWindow, beforeWindow;
|
|
private long xmlBytes;
|
|
private double registration, elapsed;
|
|
public string StartedUtc { get { return started; } }
|
|
public double RegistrationSeconds { get { return registration; } }
|
|
public Observer(string channel, int seconds, int maximum) {
|
|
if (seconds < 1 || seconds > 60 || maximum < 1 || maximum > 1024) throw new ArgumentOutOfRangeException();
|
|
this.seconds=seconds; this.maximum=maximum; callback=OnEvent;
|
|
bookmark=EvtCreateBookmark(null);
|
|
if (bookmark==IntPtr.Zero) {
|
|
int bookmarkError=Marshal.GetLastWin32Error();
|
|
finished.Dispose();
|
|
throw new System.ComponentModel.Win32Exception(bookmarkError, "EvtCreateBookmark failed");
|
|
}
|
|
Stopwatch opening=Stopwatch.StartNew();
|
|
subscription=EvtSubscribe(IntPtr.Zero, IntPtr.Zero, channel, "*", IntPtr.Zero, IntPtr.Zero, callback, 0x10001);
|
|
int error=Marshal.GetLastWin32Error();
|
|
registration=opening.Elapsed.TotalSeconds;
|
|
if(subscription==IntPtr.Zero) { EvtClose(bookmark); bookmark=IntPtr.Zero; finished.Dispose(); throw new System.ComponentModel.Win32Exception(error,"EvtSubscribe failed"); }
|
|
lock(gate) { started=DateTime.UtcNow.ToString("o"); timer.Start(); armed=true; }
|
|
}
|
|
private static string Render(IntPtr handle, uint flags, int maximumBytes) {
|
|
int used, count;
|
|
bool ok=EvtRender(IntPtr.Zero,handle,flags,0,IntPtr.Zero,out used,out count);
|
|
int error=Marshal.GetLastWin32Error();
|
|
if(ok || error!=122 || used<2 || used>maximumBytes) throw new InvalidOperationException("EvtRender size/error outside bounds: "+error+"/"+used);
|
|
IntPtr buffer=Marshal.AllocHGlobal(used);
|
|
try {
|
|
if(!EvtRender(IntPtr.Zero,handle,flags,used,buffer,out used,out count)) throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error(),"EvtRender failed");
|
|
return Marshal.PtrToStringUni(buffer);
|
|
} finally { Marshal.FreeHGlobal(buffer); }
|
|
}
|
|
private uint OnEvent(uint action, IntPtr context, IntPtr evt) {
|
|
// The service owns callback event handles. Never close them here.
|
|
lock(gate) {
|
|
if(disposed || completed) return 0;
|
|
if(action==0) { nativeError=unchecked((int)evt.ToInt64()); status="NativeError"; diagnostic="Native subscription error (including stale/missing records when reported): "+nativeError; finished.Set(); return 0; }
|
|
if(action!=1) { status="NativeError"; diagnostic="Unknown subscription callback action"; finished.Set(); return 0; }
|
|
if(!armed) { beforeWindow++; return 0; }
|
|
double observed=timer.Elapsed.TotalSeconds;
|
|
if(observed>=seconds) { outsideWindow++; finished.Set(); return 0; }
|
|
if(status!="WindowComplete") return 0;
|
|
if(events.Count>=maximum) { status="EventCapExceeded"; diagnostic="At least one additional delivery exceeded the selected event cap."; elapsed=observed; finished.Set(); return 0; }
|
|
try {
|
|
string xml=Render(evt,1,2097152);
|
|
int bytes=System.Text.Encoding.UTF8.GetByteCount(xml);
|
|
if(bytes>1048576 || xmlBytes+bytes>16777216) { status="XmlCapExceeded"; diagnostic="Rendered XML exceeds the per-event 1 MiB or batch 16 MiB cap."; elapsed=observed; finished.Set(); return 0; }
|
|
if(!EvtUpdateBookmark(bookmark,evt)) throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error(),"EvtUpdateBookmark failed");
|
|
events.Add(new Delivery { Xml=xml, BookmarkXml=Render(bookmark,2,65536), ElapsedSeconds=observed });
|
|
xmlBytes+=bytes;
|
|
} catch(Exception ex) { status="NativeError"; diagnostic=ex.Message; elapsed=observed; finished.Set(); }
|
|
}
|
|
return 0;
|
|
}
|
|
public Capture Complete() {
|
|
if(disposed || completed) throw new InvalidOperationException("Observer is already closed");
|
|
double remaining=seconds-timer.Elapsed.TotalSeconds;
|
|
if(remaining>0) finished.WaitOne((int)Math.Ceiling(remaining*1000));
|
|
lock(gate) { completed=true; if(status=="WindowComplete") elapsed=seconds; else if(elapsed==0) elapsed=Math.Min(timer.Elapsed.TotalSeconds,seconds); }
|
|
// EvtClose outside the callback lock avoids a shutdown/callback deadlock.
|
|
if(subscription!=IntPtr.Zero) { EvtClose(subscription); subscription=IntPtr.Zero; }
|
|
return new Capture { Status=status, NativeError=nativeError, Diagnostic=diagnostic, StartedUtc=started, CompletedUtc=DateTime.UtcNow.ToString("o"), RegistrationSeconds=registration, ElapsedSeconds=elapsed, OutsideWindowCallbacks=outsideWindow, BeforeWindowCallbacks=beforeWindow, XmlUtf8Bytes=xmlBytes, Events=events.ToArray() };
|
|
}
|
|
public void Dispose() {
|
|
lock(gate) { if(disposed) return; disposed=true; }
|
|
if(subscription!=IntPtr.Zero) { EvtClose(subscription); subscription=IntPtr.Zero; }
|
|
if(bookmark!=IntPtr.Zero) { EvtClose(bookmark); bookmark=IntPtr.Zero; }
|
|
finished.Dispose();
|
|
GC.KeepAlive(callback);
|
|
}
|
|
}
|
|
}
|