Skip to main content

windows_erg/etw/
types.rs

1//! ETW types and structures.
2
3use super::decode::{DecodedEvent, EventField, decode_trace_event};
4use crate::types::{ProcessId, ThreadId};
5use std::time::SystemTime;
6use windows::core::GUID;
7
8/// System-level event sources for kernel tracing.
9///
10/// Each variant represents a category of events emitted by the Windows kernel.
11/// Because these providers operate inside the kernel, they capture activity
12/// from **all processes** on the machine without any instrumentation in the
13/// target application.
14///
15/// # Privileges
16///
17/// Enabling any `SystemProvider` requires **Administrator** privileges. The
18/// underlying trace session uses the `NT Kernel Logger` — a Windows-reserved
19/// name for kernel providers — so only one kernel session can be active at a time.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum SystemProvider {
22    /// Process and thread creation/termination events.
23    ///
24    /// Emits an event whenever any process or thread starts or stops system-wide.
25    Process,
26
27    /// Registry key and value operations.
28    ///
29    /// Emits an event for every registry read, write, create, and delete
30    /// across all processes. Can be high-volume on busy systems.
31    Registry,
32
33    /// TCP/IP network connections and data transfer.
34    ///
35    /// Emits events for TCP connections, UDP sends/receives, and connection
36    /// failures. Covers both IPv4 and IPv6.
37    Network,
38
39    /// File I/O operations (create, read, write, delete).
40    ///
41    /// Emits an event for every file system operation. Very high volume —
42    /// consider using `next_batch_with_filter` to focus on relevant paths.
43    FileIo,
44
45    /// DLL and EXE image load/unload events.
46    ///
47    /// Emits an event whenever any executable or library is mapped into or
48    /// unmapped from a process. Useful for detecting code injection.
49    ImageLoad,
50}
51
52impl SystemProvider {
53    /// Get the `EVENT_TRACE_FLAG` bitmask for this provider.
54    pub(crate) fn trace_flags(self) -> u32 {
55        use windows::Win32::System::Diagnostics::Etw::*;
56        match self {
57            SystemProvider::Process => (EVENT_TRACE_FLAG_PROCESS | EVENT_TRACE_FLAG_THREAD).0,
58            SystemProvider::Registry => EVENT_TRACE_FLAG_REGISTRY.0,
59            SystemProvider::Network => EVENT_TRACE_FLAG_NETWORK_TCPIP.0,
60            SystemProvider::FileIo => (EVENT_TRACE_FLAG_FILE_IO | EVENT_TRACE_FLAG_FILE_IO_INIT).0,
61            SystemProvider::ImageLoad => EVENT_TRACE_FLAG_IMAGE_LOAD.0,
62        }
63    }
64}
65
66/// Verbosity level for a trace session.
67#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
68#[repr(u8)]
69pub enum TraceLevel {
70    /// Critical events only.
71    Critical = 1,
72    /// Error events.
73    Error = 2,
74    /// Warning events.
75    Warning = 3,
76    /// Informational events.
77    Info = 4,
78    /// Verbose / debug events.
79    Verbose = 5,
80}
81
82/// Optional per-event thread metadata enrichment.
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub struct ThreadContext {
85    /// Process ID associated with the event's thread.
86    pub process_id: ProcessId,
87    /// Thread ID that emitted the event.
88    pub thread_id: ThreadId,
89}
90
91impl ThreadContext {
92    /// Create a new thread context value.
93    pub fn new(process_id: ProcessId, thread_id: ThreadId) -> Self {
94        Self {
95            process_id,
96            thread_id,
97        }
98    }
99}
100
101/// Optional per-event stack trace enrichment.
102#[derive(Debug, Clone, PartialEq, Eq)]
103pub struct StackTrace {
104    /// Correlation identifier for matching stack begin/end context.
105    pub match_id: u64,
106    /// Captured instruction pointer frames (normalized as 64-bit addresses).
107    pub frames: Vec<u64>,
108}
109
110impl StackTrace {
111    /// Create a new stack trace enrichment payload.
112    pub fn new(match_id: u64, frames: Vec<u64>) -> Self {
113        Self { match_id, frames }
114    }
115}
116
117/// Optional per-event CPU sampling enrichment.
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119pub struct CpuSample {
120    /// Logical processor number that emitted the event.
121    pub processor_number: u8,
122}
123
124impl CpuSample {
125    /// Create a new CPU sample enrichment payload.
126    pub fn new(processor_number: u8) -> Self {
127        Self { processor_number }
128    }
129}
130
131/// A single event captured from a kernel trace session.
132///
133/// Each `TraceEvent` is emitted by one of the active [`SystemProvider`]s.
134/// The `id` and `opcode` fields identify the specific operation; the `data`
135/// field contains the raw binary payload whose layout depends on the provider
136/// and event ID.
137#[derive(Debug, Clone)]
138pub struct TraceEvent {
139    /// Event ID — identifies the event type within the provider.
140    pub id: u16,
141
142    /// Event version.
143    pub version: u8,
144
145    /// Opcode — identifies the operation phase (start, stop, info, etc.).
146    pub opcode: u8,
147
148    /// Severity level of this event.
149    pub level: u8,
150
151    /// GUID of the provider that emitted this event.
152    pub provider_guid: GUID,
153
154    /// ID of the process that triggered the event.
155    pub process_id: ProcessId,
156
157    /// ID of the thread that triggered the event.
158    pub thread_id: ThreadId,
159
160    /// When the event was recorded.
161    pub timestamp: SystemTime,
162
163    /// Raw binary payload. Layout depends on the provider and event ID.
164    pub data: Vec<u8>,
165
166    /// Optional thread context enrichment (enabled by `with_thread_context`).
167    pub thread_context: Option<ThreadContext>,
168
169    /// Optional stack trace enrichment (enabled by `with_stack_traces`).
170    pub stack_trace: Option<StackTrace>,
171
172    /// Optional CPU sampling enrichment (enabled by `with_cpu_samples`).
173    pub cpu_sample: Option<CpuSample>,
174
175    /// Optional schema-parsed fields (available when detailed event parsing is enabled).
176    fields: Option<Vec<EventField>>,
177}
178
179impl TraceEvent {
180    /// Build a `TraceEvent` from a raw `EVENT_RECORD`.
181    ///
182    /// Used by the ProcessTrace callback pipeline in `session.rs`.
183    pub fn from_event_record(
184        record: &windows::Win32::System::Diagnostics::Etw::EVENT_RECORD,
185    ) -> Self {
186        Self::from_event_record_with_fields(record, None)
187    }
188
189    /// Build a `TraceEvent` from a raw `EVENT_RECORD` with optional pre-parsed fields.
190    pub(crate) fn from_event_record_with_fields(
191        record: &windows::Win32::System::Diagnostics::Etw::EVENT_RECORD,
192        fields: Option<Vec<EventField>>,
193    ) -> Self {
194        let header = &record.EventHeader;
195        let desc = &header.EventDescriptor;
196
197        let timestamp = filetime_to_systemtime(header.TimeStamp);
198
199        let data = if record.UserDataLength > 0 && !record.UserData.is_null() {
200            unsafe {
201                std::slice::from_raw_parts(
202                    record.UserData as *const u8,
203                    record.UserDataLength as usize,
204                )
205                .to_vec()
206            }
207        } else {
208            Vec::new()
209        };
210
211        TraceEvent {
212            id: desc.Id,
213            version: desc.Version,
214            opcode: desc.Opcode,
215            level: desc.Level,
216            provider_guid: header.ProviderId,
217            process_id: ProcessId::new(header.ProcessId),
218            thread_id: ThreadId::new(header.ThreadId),
219            timestamp,
220            data,
221            thread_context: None,
222            stack_trace: None,
223            cpu_sample: None,
224            fields,
225        }
226    }
227
228    /// Decode this event into a typed representation when a known kernel
229    /// layout is available.
230    ///
231    /// Returns [`DecodedEvent::Unknown`] when no direct decoder matches the
232    /// provider, version, and opcode combination.
233    pub fn decode(&self) -> DecodedEvent {
234        decode_trace_event(self)
235    }
236
237    /// Returns schema-parsed fields when detailed event parsing is enabled.
238    pub fn fields(&self) -> Option<&[EventField]> {
239        self.fields.as_deref()
240    }
241}
242
243/// Convert a Windows FILETIME (100-ns intervals since 1601-01-01) to `SystemTime`.
244fn filetime_to_systemtime(filetime: i64) -> SystemTime {
245    // Number of 100-ns intervals between 1601-01-01 and 1970-01-01.
246    const FILETIME_TO_UNIX_EPOCH: i64 = 116_444_736_000_000_000;
247    const NANOS_PER_100NS: u32 = 100;
248
249    let intervals_since_unix = filetime.saturating_sub(FILETIME_TO_UNIX_EPOCH);
250    let seconds = (intervals_since_unix / 10_000_000) as u64;
251    let nanos = ((intervals_since_unix % 10_000_000) * NANOS_PER_100NS as i64) as u32;
252
253    SystemTime::UNIX_EPOCH + std::time::Duration::new(seconds, nanos)
254}
255
256#[cfg(test)]
257mod tests {
258    use super::{DecodedEvent, TraceEvent};
259    use crate::etw::{
260        EventField, EventFieldValue, FileIoOperation, RegistryOperation, TcpOperation,
261    };
262    use crate::types::{ProcessId, ThreadId};
263    use std::time::SystemTime;
264    use windows::Win32::System::Diagnostics::Etw::{
265        FileIoGuid, ImageLoadGuid, ProcessGuid, RegistryGuid, TcpIpGuid,
266    };
267
268    #[test]
269    fn decode_process_v0_start_event() {
270        let event = TraceEvent {
271            id: 0,
272            version: 0,
273            opcode: 1,
274            level: 0,
275            provider_guid: ProcessGuid,
276            process_id: ProcessId::new(672),
277            thread_id: ThreadId::new(0),
278            timestamp: SystemTime::UNIX_EPOCH,
279            data: vec![
280                160, 2, 0, 0, 220, 7, 0, 0, 0, 0, 0, 0, 96, 140, 79, 210, 6, 180, 255, 255, 0, 0,
281                0, 0, 0, 0, 0, 0, 1, 5, 0, 0, 0, 0, 0, 5, 21, 0, 0, 0, 54, 245, 194, 143, 120, 21,
282                213, 94, 151, 105, 93, 135, 89, 4, 0, 0, 99, 109, 100, 46, 101, 120, 101, 0, 0, 0,
283                0, 0,
284            ],
285            thread_context: None,
286            stack_trace: None,
287            cpu_sample: None,
288            fields: None,
289        };
290
291        match event.decode() {
292            DecodedEvent::ProcessStart(decoded) => {
293                assert_eq!(decoded.process_id, ProcessId::new(672));
294                assert_eq!(decoded.parent_process_id, ProcessId::new(2012));
295                assert_eq!(decoded.image_file_name, "cmd.exe");
296            }
297            other => panic!("unexpected decode result: {other:?}"),
298        }
299    }
300
301    #[test]
302    fn decode_image_v2_v3_v4_load_event() {
303        let event = TraceEvent {
304            id: 0,
305            version: 3,
306            opcode: 10,
307            level: 0,
308            provider_guid: ImageLoadGuid,
309            process_id: ProcessId::new(3996),
310            thread_id: ThreadId::new(0),
311            timestamp: SystemTime::UNIX_EPOCH,
312            data: vec![
313                0, 0, 8, 72, 251, 127, 0, 0, 0, 32, 8, 0, 0, 0, 0, 0, 156, 15, 0, 0, 70, 110, 8, 0,
314                255, 233, 215, 246, 12, 1, 0, 0, 0, 0, 8, 72, 251, 127, 0, 0, 0, 0, 0, 0, 0, 0, 0,
315                0, 0, 0, 0, 0, 0, 0, 0, 0, 92, 0, 68, 0, 101, 0, 118, 0, 105, 0, 99, 0, 101, 0, 92,
316                0, 72, 0, 97, 0, 114, 0, 100, 0, 100, 0, 105, 0, 115, 0, 107, 0, 86, 0, 111, 0,
317                108, 0, 117, 0, 109, 0, 101, 0, 50, 0, 92, 0, 87, 0, 105, 0, 110, 0, 100, 0, 111,
318                0, 119, 0, 115, 0, 92, 0, 83, 0, 121, 0, 115, 0, 116, 0, 101, 0, 109, 0, 51, 0, 50,
319                0, 92, 0, 98, 0, 99, 0, 114, 0, 121, 0, 112, 0, 116, 0, 112, 0, 114, 0, 105, 0,
320                109, 0, 105, 0, 116, 0, 105, 0, 118, 0, 101, 0, 115, 0, 46, 0, 100, 0, 108, 0, 108,
321                0, 0, 0,
322            ],
323            thread_context: None,
324            stack_trace: None,
325            cpu_sample: None,
326            fields: None,
327        };
328
329        for version in [2u8, 3u8, 4u8] {
330            let mut candidate = event.clone();
331            candidate.version = version;
332
333            match candidate.decode() {
334                DecodedEvent::ImageLoad(decoded) => {
335                    assert_eq!(decoded.process_id, ProcessId::new(3996));
336                    assert!(decoded.file_name.ends_with("bcryptprimitives.dll"));
337                    assert_eq!(decoded.version, version);
338                }
339                other => panic!("unexpected decode result for version {version}: {other:?}"),
340            }
341        }
342    }
343
344    #[test]
345    fn decode_generic_from_preparsed_fields() {
346        let event = TraceEvent {
347            id: 999,
348            version: 1,
349            opcode: 42,
350            level: 4,
351            provider_guid: windows::core::GUID::from_u128(0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa),
352            process_id: ProcessId::new(1),
353            thread_id: ThreadId::new(1),
354            timestamp: SystemTime::UNIX_EPOCH,
355            data: Vec::new(),
356            thread_context: None,
357            stack_trace: None,
358            cpu_sample: None,
359            fields: Some(vec![EventField {
360                name: "Example".to_string(),
361                value: EventFieldValue::U32(7),
362            }]),
363        };
364
365        match event.decode() {
366            DecodedEvent::Generic(fields) => {
367                assert_eq!(fields.len(), 1);
368                assert_eq!(fields[0].name, "Example");
369            }
370            other => panic!("unexpected decode result: {other:?}"),
371        }
372    }
373
374    #[test]
375    fn decode_typed_tcp_from_preparsed_fields() {
376        let event = TraceEvent {
377            id: 10,
378            version: 2,
379            opcode: 10,
380            level: 4,
381            provider_guid: TcpIpGuid,
382            process_id: ProcessId::new(1234),
383            thread_id: ThreadId::new(1),
384            timestamp: SystemTime::UNIX_EPOCH,
385            data: Vec::new(),
386            thread_context: None,
387            stack_trace: None,
388            cpu_sample: None,
389            fields: Some(vec![
390                EventField {
391                    name: "PID".to_string(),
392                    value: EventFieldValue::U32(1234),
393                },
394                EventField {
395                    name: "saddr".to_string(),
396                    value: EventFieldValue::String("10.0.0.10".to_string()),
397                },
398                EventField {
399                    name: "sport".to_string(),
400                    value: EventFieldValue::U16(5050),
401                },
402                EventField {
403                    name: "daddr".to_string(),
404                    value: EventFieldValue::String("1.1.1.1".to_string()),
405                },
406                EventField {
407                    name: "dport".to_string(),
408                    value: EventFieldValue::U16(443),
409                },
410            ]),
411        };
412
413        match event.decode() {
414            DecodedEvent::Tcp(tcp) => {
415                assert_eq!(tcp.operation, TcpOperation::Send);
416                assert_eq!(tcp.process_id, Some(ProcessId::new(1234)));
417                assert_eq!(tcp.destination_port, Some(443));
418            }
419            other => panic!("unexpected decode result: {other:?}"),
420        }
421    }
422
423    #[test]
424    fn decode_typed_registry_from_preparsed_fields() {
425        let event = TraceEvent {
426            id: 14,
427            version: 1,
428            opcode: 14,
429            level: 4,
430            provider_guid: RegistryGuid,
431            process_id: ProcessId::new(5678),
432            thread_id: ThreadId::new(1),
433            timestamp: SystemTime::UNIX_EPOCH,
434            data: Vec::new(),
435            thread_context: None,
436            stack_trace: None,
437            cpu_sample: None,
438            fields: Some(vec![
439                EventField {
440                    name: "ProcessId".to_string(),
441                    value: EventFieldValue::U32(5678),
442                },
443                EventField {
444                    name: "KeyName".to_string(),
445                    value: EventFieldValue::String("\\Registry\\Machine\\SOFTWARE".to_string()),
446                },
447                EventField {
448                    name: "ValueName".to_string(),
449                    value: EventFieldValue::String("TestValue".to_string()),
450                },
451                EventField {
452                    name: "Status".to_string(),
453                    value: EventFieldValue::U32(0),
454                },
455            ]),
456        };
457
458        match event.decode() {
459            DecodedEvent::Registry(reg) => {
460                assert_eq!(reg.operation, RegistryOperation::SetValue);
461                assert_eq!(reg.process_id, Some(ProcessId::new(5678)));
462                assert_eq!(reg.value_name.as_deref(), Some("TestValue"));
463            }
464            other => panic!("unexpected decode result: {other:?}"),
465        }
466    }
467
468    #[test]
469    fn decode_typed_fileio_from_preparsed_fields() {
470        let event = TraceEvent {
471            id: 32,
472            version: 1,
473            opcode: 32,
474            level: 4,
475            provider_guid: FileIoGuid,
476            process_id: ProcessId::new(2222),
477            thread_id: ThreadId::new(1),
478            timestamp: SystemTime::UNIX_EPOCH,
479            data: Vec::new(),
480            thread_context: None,
481            stack_trace: None,
482            cpu_sample: None,
483            fields: Some(vec![
484                EventField {
485                    name: "ProcessId".to_string(),
486                    value: EventFieldValue::U32(2222),
487                },
488                EventField {
489                    name: "OpenPath".to_string(),
490                    value: EventFieldValue::String("C:\\Temp\\test.txt".to_string()),
491                },
492                EventField {
493                    name: "FileObject".to_string(),
494                    value: EventFieldValue::U64(0x1000),
495                },
496                EventField {
497                    name: "IrpPtr".to_string(),
498                    value: EventFieldValue::Pointer(0x2000),
499                },
500                EventField {
501                    name: "CreateOptions".to_string(),
502                    value: EventFieldValue::U32(0x20),
503                },
504            ]),
505        };
506
507        match event.decode() {
508            DecodedEvent::FileIo(file) => {
509                assert_eq!(file.operation, FileIoOperation::Create);
510                assert_eq!(file.process_id, Some(ProcessId::new(2222)));
511                assert_eq!(file.open_path.as_deref(), Some("C:\\Temp\\test.txt"));
512                assert_eq!(file.file_object, Some(0x1000));
513                assert_eq!(file.irp_ptr, Some(0x2000));
514            }
515            other => panic!("unexpected decode result: {other:?}"),
516        }
517    }
518}