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