Skip to main content

evtx/
evtx_record.rs

1use crate::binxml::ir_json::render_json_record;
2use crate::binxml::ir_xml::render_xml_record;
3use crate::err::{DeserializationError, DeserializationResult, EvtxError, Result};
4use crate::model::ir::IrTree;
5use crate::utils::ByteCursor;
6use crate::utils::bytes;
7use crate::utils::windows::filetime_to_timestamp;
8use crate::{EvtxChunk, ParserSettings};
9
10pub use jiff::Timestamp;
11#[allow(unused)]
12pub use jiff::tz::Offset;
13use std::io::Cursor;
14use std::sync::Arc;
15
16pub type RecordId = u64;
17
18pub(crate) const EVTX_RECORD_HEADER_SIZE: usize = 24;
19
20#[derive(Debug, Clone)]
21pub struct EvtxRecord<'a> {
22    pub chunk: &'a EvtxChunk<'a>,
23    pub event_record_id: RecordId,
24    pub timestamp: Timestamp,
25    pub tree: IrTree<'a>,
26    pub binxml_offset: u64,
27    pub binxml_size: u32,
28    pub settings: Arc<ParserSettings>,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct EvtxRecordHeader {
33    pub data_size: u32,
34    pub event_record_id: RecordId,
35    pub timestamp: Timestamp,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct SerializedEvtxRecord<T> {
40    pub event_record_id: RecordId,
41    pub timestamp: Timestamp,
42    pub data: T,
43}
44
45impl EvtxRecordHeader {
46    pub fn from_bytes_at(buf: &[u8], offset: usize) -> DeserializationResult<EvtxRecordHeader> {
47        let _ = bytes::slice_r(buf, offset, EVTX_RECORD_HEADER_SIZE, "EVTX record header")?;
48
49        let magic = bytes::read_array_r::<4>(buf, offset, "record header magic")?;
50        if &magic != b"\x2a\x2a\x00\x00" {
51            return Err(DeserializationError::InvalidEvtxRecordHeaderMagic { magic });
52        }
53
54        let size = bytes::read_u32_le_r(buf, offset + 4, "record.data_size")?;
55        let record_id = bytes::read_u64_le_r(buf, offset + 8, "record.event_record_id")?;
56        let filetime = bytes::read_u64_le_r(buf, offset + 16, "record.filetime")?;
57
58        let timestamp = filetime_to_timestamp(filetime)?;
59
60        Ok(EvtxRecordHeader {
61            data_size: size,
62            event_record_id: record_id,
63            timestamp,
64        })
65    }
66
67    pub fn from_bytes(buf: &[u8]) -> DeserializationResult<EvtxRecordHeader> {
68        Self::from_bytes_at(buf, 0)
69    }
70
71    pub fn from_reader(input: &mut Cursor<&[u8]>) -> DeserializationResult<EvtxRecordHeader> {
72        let start = input.position() as usize;
73        let buf = input.get_ref();
74        let header = Self::from_bytes_at(buf, start)?;
75        input.set_position((start + EVTX_RECORD_HEADER_SIZE) as u64);
76        Ok(header)
77    }
78
79    pub fn record_data_size(&self) -> Result<u32> {
80        // 24 - record header size
81        // 4 - copy of size record size
82        let decal = EVTX_RECORD_HEADER_SIZE as u32 + 4;
83        if self.data_size < decal {
84            return Err(EvtxError::InvalidDataSize {
85                length: self.data_size,
86                expected: decal,
87            });
88        }
89        Ok(self.data_size - decal)
90    }
91}
92
93impl<'a> EvtxRecord<'a> {
94    /// Consumes the record and returns the rendered JSON as a `serde_json::Value`.
95    pub fn into_json_value(self) -> Result<SerializedEvtxRecord<serde_json::Value>> {
96        let event_record_id = self.event_record_id;
97        let timestamp = self.timestamp;
98        let record_with_json = self.into_json()?;
99
100        Ok(SerializedEvtxRecord {
101            event_record_id,
102            timestamp,
103            data: serde_json::from_str(&record_with_json.data)
104                .map_err(crate::err::SerializationError::from)?,
105        })
106    }
107
108    /// Consumes the record and renders it as compact JSON (streaming IR renderer).
109    pub fn into_json(self) -> Result<SerializedEvtxRecord<String>> {
110        // Estimate buffer size based on BinXML size
111        let capacity_hint = self.binxml_size as usize * 2;
112        let buf = Vec::with_capacity(capacity_hint);
113
114        let event_record_id = self.event_record_id;
115        let timestamp = self.timestamp;
116
117        let mut writer = buf;
118        render_json_record(&self.tree, &self.settings, &mut writer).map_err(|e| {
119            EvtxError::FailedToParseRecord {
120                record_id: event_record_id,
121                source: Box::new(e),
122            }
123        })?;
124        let data = String::from_utf8(writer).map_err(crate::err::SerializationError::from)?;
125
126        Ok(SerializedEvtxRecord {
127            event_record_id,
128            timestamp,
129            data,
130        })
131    }
132
133    /// Consumes the record and parse it, producing an XML serialized record.
134    pub fn into_xml(self) -> Result<SerializedEvtxRecord<String>> {
135        let capacity_hint = self.binxml_size as usize * 2;
136        let buf = Vec::with_capacity(capacity_hint);
137
138        let event_record_id = self.event_record_id;
139        let timestamp = self.timestamp;
140
141        let mut writer = buf;
142        render_xml_record(&self.tree, &self.settings, &mut writer).map_err(|e| {
143            EvtxError::FailedToParseRecord {
144                record_id: event_record_id,
145                source: Box::new(e),
146            }
147        })?;
148
149        let data = String::from_utf8(writer).map_err(crate::err::SerializationError::from)?;
150
151        Ok(SerializedEvtxRecord {
152            event_record_id,
153            timestamp,
154            data,
155        })
156    }
157
158    /// Parse all `TemplateInstance` substitution arrays from this record.
159    ///
160    /// This is a lightweight scan over the record's BinXML stream that extracts typed substitution
161    /// values without building a legacy token vector.
162    pub fn template_instances(&self) -> Result<Vec<crate::binxml::BinXmlTemplateValues<'a>>> {
163        use crate::binxml::name::BinXmlNameEncoding;
164        use crate::binxml::tokens::{
165            read_attribute_cursor, read_entity_ref_cursor, read_fragment_header_cursor,
166            read_open_start_element_cursor, read_processing_instruction_data_cursor,
167            read_processing_instruction_target_cursor, read_substitution_descriptor_cursor,
168            read_template_values_cursor,
169        };
170
171        let ansi_codec = self.settings.get_ansi_codec();
172        let mut out: Vec<crate::binxml::BinXmlTemplateValues<'a>> = Vec::new();
173
174        let mut cursor = ByteCursor::with_pos(self.chunk.data, self.binxml_offset as usize)?;
175        let mut data_read: u32 = 0;
176        let data_size = self.binxml_size;
177        let mut eof = false;
178
179        while !eof && data_read < data_size {
180            let start = cursor.position();
181            let token_byte = cursor.u8()?;
182
183            match token_byte {
184                0x00 => {
185                    eof = true;
186                }
187                0x0c => {
188                    let template = read_template_values_cursor(
189                        &mut cursor,
190                        Some(self.chunk),
191                        ansi_codec,
192                        &self.chunk.arena,
193                    )?;
194                    out.push(template);
195                }
196                0x01 => {
197                    let _ = read_open_start_element_cursor(
198                        &mut cursor,
199                        false,
200                        false,
201                        BinXmlNameEncoding::Offset,
202                    )?;
203                }
204                0x41 => {
205                    let _ = read_open_start_element_cursor(
206                        &mut cursor,
207                        true,
208                        false,
209                        BinXmlNameEncoding::Offset,
210                    )?;
211                }
212                0x02..=0x04 => {
213                    // Structural tokens; no payload.
214                }
215                0x05 | 0x45 => {
216                    let _ = crate::binxml::value_variant::BinXmlValue::from_binxml_cursor_in(
217                        &mut cursor,
218                        Some(self.chunk),
219                        None,
220                        ansi_codec,
221                        &self.chunk.arena,
222                    )?;
223                }
224                0x06 | 0x46 => {
225                    let _ = read_attribute_cursor(&mut cursor, BinXmlNameEncoding::Offset)?;
226                }
227                0x09 | 0x49 => {
228                    let _ = read_entity_ref_cursor(&mut cursor, BinXmlNameEncoding::Offset)?;
229                }
230                0x0a => {
231                    let _ = read_processing_instruction_target_cursor(
232                        &mut cursor,
233                        BinXmlNameEncoding::Offset,
234                    )?;
235                }
236                0x0b => {
237                    let _ = read_processing_instruction_data_cursor(&mut cursor)?;
238                }
239                0x0d => {
240                    let _ = read_substitution_descriptor_cursor(&mut cursor, false)?;
241                }
242                0x0e => {
243                    let _ = read_substitution_descriptor_cursor(&mut cursor, true)?;
244                }
245                0x0f => {
246                    let _ = read_fragment_header_cursor(&mut cursor)?;
247                }
248                0x07 | 0x47 => {
249                    return Err(DeserializationError::UnimplementedToken {
250                        name: "CDataSection",
251                        offset: cursor.position(),
252                    }
253                    .into());
254                }
255                0x08 | 0x48 => {
256                    return Err(DeserializationError::UnimplementedToken {
257                        name: "CharReference",
258                        offset: cursor.position(),
259                    }
260                    .into());
261                }
262                _ => {
263                    return Err(DeserializationError::InvalidToken {
264                        value: token_byte,
265                        offset: cursor.position(),
266                    }
267                    .into());
268                }
269            }
270
271            let total_read = cursor.position() - start;
272            data_read = data_read.saturating_add(total_read as u32);
273        }
274
275        Ok(out)
276    }
277}