Skip to main content

windows_erg/evt/
types.rs

1//! Event Log types with optimized string handling.
2
3use std::borrow::Cow;
4use std::collections::HashMap;
5use std::sync::OnceLock;
6use std::time::{Duration, SystemTime, UNIX_EPOCH};
7
8pub use crate::types::{ProcessId, ThreadId};
9
10/// Type-safe event ID.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
12pub struct EventId(u32);
13
14impl EventId {
15    /// Create a new event ID.
16    pub fn new(id: u32) -> Self {
17        EventId(id)
18    }
19
20    /// Get the raw event ID value.
21    pub fn as_u32(&self) -> u32 {
22        self.0
23    }
24}
25
26impl std::fmt::Display for EventId {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        write!(f, "{}", self.0)
29    }
30}
31
32/// Type-safe record ID.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
34pub struct RecordId(u64);
35
36impl RecordId {
37    /// Create a new record ID.
38    pub fn new(id: u64) -> Self {
39        RecordId(id)
40    }
41
42    /// Get the raw record ID value.
43    pub fn as_u64(&self) -> u64 {
44        self.0
45    }
46}
47
48impl std::fmt::Display for RecordId {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        write!(f, "{}", self.0)
51    }
52}
53
54/// Event severity level.
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
56pub enum EventLevel {
57    /// Audit successful
58    AuditSuccess = 0,
59    /// Audit failure
60    AuditFailure = 1,
61    /// Critical
62    Critical = 2,
63    /// Error
64    Error = 3,
65    /// Warning
66    Warning = 4,
67    /// Informational
68    #[default]
69    Informational = 5,
70    /// Verbose (Debug)
71    Verbose = 6,
72}
73
74impl EventLevel {
75    /// Create from Windows Event Log level code.
76    pub fn from_code(code: u8) -> Option<Self> {
77        match code {
78            0 => Some(EventLevel::AuditSuccess),
79            1 => Some(EventLevel::AuditFailure),
80            2 => Some(EventLevel::Critical),
81            3 => Some(EventLevel::Error),
82            4 => Some(EventLevel::Warning),
83            5 => Some(EventLevel::Informational),
84            6 => Some(EventLevel::Verbose),
85            _ => None,
86        }
87    }
88}
89
90impl std::fmt::Display for EventLevel {
91    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92        match self {
93            EventLevel::AuditSuccess => write!(f, "AuditSuccess"),
94            EventLevel::AuditFailure => write!(f, "AuditFailure"),
95            EventLevel::Critical => write!(f, "Critical"),
96            EventLevel::Error => write!(f, "Error"),
97            EventLevel::Warning => write!(f, "Warning"),
98            EventLevel::Informational => write!(f, "Informational"),
99            EventLevel::Verbose => write!(f, "Verbose"),
100        }
101    }
102}
103
104/// Static string cache for provider and channel names.
105///
106/// Caches commonly-used provider and channel names as static `Cow<'static, str>`
107/// to reduce allocations. All cached strings are compile-time constants.
108struct StringCache {
109    providers: HashMap<&'static str, Cow<'static, str>>,
110    channels: HashMap<&'static str, Cow<'static, str>>,
111}
112
113impl StringCache {
114    /// Initialize cache with common provider and channel names.
115    fn new() -> Self {
116        let mut cache = StringCache {
117            providers: HashMap::new(),
118            channels: HashMap::new(),
119        };
120
121        // Common providers - these are cached as static strings
122        const COMMON_PROVIDERS: &[&str] = &[
123            "Security",
124            "System",
125            "Application",
126            "Microsoft-Windows-Sysmon/Operational",
127            "Microsoft-Windows-PowerShell/Operational",
128            "Microsoft-Windows-WinRM/Operational",
129            "Microsoft-Windows-DNS-Client/Operational",
130            "Perflib",
131            "Windows Defender",
132            "Google Chrome",
133        ];
134
135        // Common channels
136        const COMMON_CHANNELS: &[&str] = &[
137            "Security",
138            "System",
139            "Application",
140            "Operational",
141            "Analytic",
142            "Debug",
143            "Setup",
144            "Forwarded Events",
145        ];
146
147        for provider in COMMON_PROVIDERS {
148            cache.providers.insert(provider, Cow::Borrowed(provider));
149        }
150
151        for channel in COMMON_CHANNELS {
152            cache.channels.insert(channel, Cow::Borrowed(channel));
153        }
154
155        cache
156    }
157}
158
159/// Static cache instance (lazy initialized).
160static STRING_CACHE: OnceLock<StringCache> = OnceLock::new();
161
162/// Get cached provider name.
163///
164/// Common provider names (Security, System, etc.) are cached as static strings.
165/// Unknown names are returned as owned strings.
166pub fn intern_provider(name: &str) -> Cow<'static, str> {
167    let cache = STRING_CACHE.get_or_init(StringCache::new);
168    if let Some(cached) = cache.providers.get(name) {
169        return cached.clone();
170    }
171    Cow::Owned(name.to_string())
172}
173
174/// Get cached channel name.
175///
176/// Common channel names (Security, Operational, etc.) are cached as static strings.
177/// Unknown names are returned as owned strings.
178pub fn intern_channel(name: &str) -> Cow<'static, str> {
179    let cache = STRING_CACHE.get_or_init(StringCache::new);
180    if let Some(cached) = cache.channels.get(name) {
181        return cached.clone();
182    }
183    Cow::Owned(name.to_string())
184}
185
186/// Intern common EventData field names to reduce allocations.
187///
188/// Common field names (Security events, Sysmon, PowerShell, etc.) are cached
189/// as static strings. Unknown field names are allocated.
190///
191/// Performance: ~2-3ns per lookup for cached names (10M+ lookups/sec).
192pub fn intern_field_name(name: &str) -> Cow<'static, str> {
193    match name {
194        // Security - Authentication
195        "SubjectUserName" => Cow::Borrowed("SubjectUserName"),
196        "SubjectDomainName" => Cow::Borrowed("SubjectDomainName"),
197        "SubjectUserSid" => Cow::Borrowed("SubjectUserSid"),
198        "SubjectLogonId" => Cow::Borrowed("SubjectLogonId"),
199        "TargetUserName" => Cow::Borrowed("TargetUserName"),
200        "TargetDomainName" => Cow::Borrowed("TargetDomainName"),
201        "TargetUserSid" => Cow::Borrowed("TargetUserSid"),
202        "TargetLogonId" => Cow::Borrowed("TargetLogonId"),
203        "LogonType" => Cow::Borrowed("LogonType"),
204        "IpAddress" => Cow::Borrowed("IpAddress"),
205        "IpPort" => Cow::Borrowed("IpPort"),
206        "WorkstationName" => Cow::Borrowed("WorkstationName"),
207        "AuthenticationPackageName" => Cow::Borrowed("AuthenticationPackageName"),
208
209        // Security - Process
210        "ProcessName" => Cow::Borrowed("ProcessName"),
211        "ProcessId" => Cow::Borrowed("ProcessId"),
212        "CommandLine" => Cow::Borrowed("CommandLine"),
213        "NewProcessName" => Cow::Borrowed("NewProcessName"),
214        "NewProcessId" => Cow::Borrowed("NewProcessId"),
215        "ParentProcessName" => Cow::Borrowed("ParentProcessName"),
216
217        // Security - Object Access
218        "ObjectName" => Cow::Borrowed("ObjectName"),
219        "AccessList" => Cow::Borrowed("AccessList"),
220        "PrivilegeList" => Cow::Borrowed("PrivilegeList"),
221
222        // Sysmon - Process
223        "Image" => Cow::Borrowed("Image"),
224        "ImageLoaded" => Cow::Borrowed("ImageLoaded"),
225        "ParentImage" => Cow::Borrowed("ParentImage"),
226        "ParentCommandLine" => Cow::Borrowed("ParentCommandLine"),
227        "ParentProcessId" => Cow::Borrowed("ParentProcessId"),
228        "Hashes" => Cow::Borrowed("Hashes"),
229        "User" => Cow::Borrowed("User"),
230        "IntegrityLevel" => Cow::Borrowed("IntegrityLevel"),
231
232        // Sysmon - Network
233        "SourceIp" => Cow::Borrowed("SourceIp"),
234        "SourcePort" => Cow::Borrowed("SourcePort"),
235        "SourceHostname" => Cow::Borrowed("SourceHostname"),
236        "DestinationIp" => Cow::Borrowed("DestinationIp"),
237        "DestinationPort" => Cow::Borrowed("DestinationPort"),
238        "DestinationHostname" => Cow::Borrowed("DestinationHostname"),
239        "Protocol" => Cow::Borrowed("Protocol"),
240
241        // Sysmon - File/Registry
242        "TargetFilename" => Cow::Borrowed("TargetFilename"),
243        "TargetObject" => Cow::Borrowed("TargetObject"),
244        "Details" => Cow::Borrowed("Details"),
245
246        // PowerShell
247        "ScriptBlockText" => Cow::Borrowed("ScriptBlockText"),
248        "Path" => Cow::Borrowed("Path"),
249        "MessageNumber" => Cow::Borrowed("MessageNumber"),
250        "MessageTotal" => Cow::Borrowed("MessageTotal"),
251
252        // Common
253        "EventID" => Cow::Borrowed("EventID"),
254        "Level" => Cow::Borrowed("Level"),
255        "Keywords" => Cow::Borrowed("Keywords"),
256        "Message" => Cow::Borrowed("Message"),
257        "Data" => Cow::Borrowed("Data"),
258
259        // Not in cache - allocate
260        _ => Cow::Owned(name.to_string()),
261    }
262}
263
264/// A complete Windows Event Log record.
265#[derive(Debug, Clone, Default)]
266pub struct Event {
267    /// Event ID (event type identifier)
268    pub id: EventId,
269
270    /// Event severity level
271    pub level: EventLevel,
272
273    /// Provider/source name (e.g., "Security", "System")
274    pub provider: Cow<'static, str>,
275
276    /// Channel name (e.g., "Security", "Operational")
277    pub channel: Cow<'static, str>,
278
279    /// Computer name where event occurred
280    pub computer: String,
281
282    /// Event timestamp (may be unavailable for corrupted events)
283    pub timestamp: Option<SystemTime>,
284
285    /// Event record ID (unique per log, may wrap)
286    pub record_id: Option<RecordId>,
287
288    /// Process ID that generated the event
289    pub process_id: Option<ProcessId>,
290
291    /// Thread ID that generated the event
292    pub thread_id: Option<ThreadId>,
293
294    /// User event data (key-value pairs with interned field names)
295    pub data: Option<std::collections::HashMap<Cow<'static, str>, String>>,
296
297    /// Formatted event message (available when parsed with `.with_message()`)
298    pub formatted_message: Option<String>,
299}
300
301impl Event {
302    /// Create an empty event with defaults.
303    pub fn new() -> Self {
304        Event {
305            id: EventId(0),
306            level: EventLevel::Informational,
307            provider: Cow::Borrowed(""),
308            channel: Cow::Borrowed(""),
309            computer: String::new(),
310            timestamp: None,
311            record_id: None,
312            process_id: None,
313            thread_id: None,
314            data: None,
315            formatted_message: None,
316        }
317    }
318}
319
320/// Rendering format for events.
321#[derive(Debug, Clone, Copy, PartialEq, Eq)]
322pub enum RenderFormat {
323    /// Render as individual property values (fastest, ~9 system fields)
324    Values,
325    /// Render as XML (complete, all fields)
326    Xml,
327}
328
329/// Corrupted or unparseable event.
330#[derive(Debug, Clone)]
331pub struct CorruptedEvent {
332    /// Record ID if available
333    pub record_id: Option<u64>,
334    /// Component that failed (e.g., "EvtRender_Xml")
335    pub component: Cow<'static, str>,
336    /// Error reason
337    pub reason: Cow<'static, str>,
338}
339
340/// Channel filter for listing event log channels.
341#[derive(Debug, Clone, Copy, PartialEq, Eq)]
342pub enum ChannelFilter {
343    /// All channels
344    All,
345    /// Operational channels only
346    Operational,
347    /// Admin and higher severity
348    AdminOrHigher,
349    /// Include analytic channels
350    IncludeAnalytic,
351}
352
353/// Result of an event query.
354#[derive(Debug, Clone, Default)]
355pub struct EventQueryResult {
356    /// Events returned by query
357    pub events: Vec<Event>,
358    /// Corrupted events encountered during query
359    pub corrupted: Vec<CorruptedEvent>,
360    /// Total events processed (including corrupted)
361    pub total_processed: usize,
362}
363
364// ============================================================================
365// Helper extraction functions for custom event parsing
366// ============================================================================
367
368use crate::error::{Error, Result};
369use std::ffi::c_void;
370use windows::Win32::System::EventLog::*;
371
372/// Extract event ID from raw EVT_HANDLE.
373///
374/// Useful for lightweight custom parsing without full Event struct allocation.
375pub fn extract_event_id(handle: EVT_HANDLE) -> Result<u32> {
376    extract_variant_field(handle, 0).and_then(|v| {
377        unsafe {
378            if v.Type == 8u32 {
379                // EvtVarTypeUInt32
380                Ok(v.Anonymous.UInt32Val)
381            } else {
382                Err(Error::Other(crate::error::OtherError::new(
383                    "EventID field is not UInt32",
384                )))
385            }
386        }
387    })
388}
389
390/// Extract provider name from raw EVT_HANDLE.
391pub fn extract_provider(handle: EVT_HANDLE) -> Result<String> {
392    extract_variant_field(handle, 2).and_then(|v| {
393        unsafe {
394            if v.Type == 21u32 {
395                // EvtVarTypeString
396                let pwstr = v.Anonymous.StringVal;
397                if !pwstr.is_null() {
398                    let len = (0..).take_while(|&i| *pwstr.0.offset(i) != 0).count();
399                    let slice = std::slice::from_raw_parts(pwstr.0, len);
400                    Ok(String::from_utf16_lossy(slice))
401                } else {
402                    Err(Error::Other(crate::error::OtherError::new(
403                        "Provider field is null",
404                    )))
405                }
406            } else {
407                Err(Error::Other(crate::error::OtherError::new(
408                    "Provider field is not string",
409                )))
410            }
411        }
412    })
413}
414
415/// Extract event level from raw EVT_HANDLE.
416pub fn extract_level(handle: EVT_HANDLE) -> Result<u8> {
417    extract_variant_field(handle, 1).and_then(|v| {
418        unsafe {
419            if v.Type == 2u32 {
420                // EvtVarTypeByte
421                Ok(v.Anonymous.ByteVal)
422            } else {
423                Err(Error::Other(crate::error::OtherError::new(
424                    "Level field is not byte",
425                )))
426            }
427        }
428    })
429}
430
431/// Extract timestamp from raw EVT_HANDLE.
432pub fn extract_timestamp(handle: EVT_HANDLE) -> Result<SystemTime> {
433    extract_variant_field(handle, 5).and_then(|v| {
434        unsafe {
435            if v.Type == 17u32 {
436                // EvtVarTypeFileTime
437                let filetime = v.Anonymous.FileTimeVal;
438                let intervals = filetime * 100;
439                let duration = Duration::from_nanos(intervals);
440                let epoch = UNIX_EPOCH - Duration::from_secs(11644473600); // Windows epoch
441                Ok(epoch + duration)
442            } else {
443                Err(Error::Other(crate::error::OtherError::new(
444                    "Timestamp field is not FILETIME",
445                )))
446            }
447        }
448    })
449}
450
451/// Extract record ID from raw EVT_HANDLE.
452pub fn extract_record_id(handle: EVT_HANDLE) -> Result<u64> {
453    extract_variant_field(handle, 6).and_then(|v| {
454        unsafe {
455            if v.Type == 10u32 {
456                // EvtVarTypeUInt64
457                Ok(v.Anonymous.UInt64Val)
458            } else {
459                Err(Error::Other(crate::error::OtherError::new(
460                    "RecordID field is not UInt64",
461                )))
462            }
463        }
464    })
465}
466
467/// Internal helper to extract a single variant field from an event handle.
468fn extract_variant_field(event_handle: EVT_HANDLE, field_index: usize) -> Result<EVT_VARIANT> {
469    let mut buffer = vec![0u8; 8192];
470    let mut buffer_used = 0u32;
471    let mut prop_count = 0u32;
472
473    let result = unsafe {
474        EvtRender(
475            EVT_HANDLE::default(),
476            event_handle,
477            EvtRenderEventValues.0,
478            buffer.len() as u32,
479            Some(buffer.as_mut_ptr() as *mut c_void),
480            &mut buffer_used,
481            &mut prop_count,
482        )
483    };
484
485    if result.is_err() {
486        return Err(Error::Other(crate::error::OtherError::new(
487            "Failed to render event values",
488        )));
489    }
490
491    let variant_ptr = buffer.as_ptr() as *const EVT_VARIANT;
492    let variants = unsafe { std::slice::from_raw_parts(variant_ptr, prop_count as usize) };
493
494    if field_index >= variants.len() {
495        return Err(Error::Other(crate::error::OtherError::new(Cow::Owned(
496            format!("Field index {} out of range", field_index),
497        ))));
498    }
499
500    Ok(variants[field_index])
501}