evtx 0.12.2

A Fast (and safe) parser for the Windows XML Event Log (EVTX) format
Documentation
use crate::EvtxChunk;
use crate::binxml::ir::RecordContent;
use crate::err::{DeserializationError, DeserializationResult, EvtxError, Result};
use crate::utils::ByteCursor;
use crate::utils::bytes;
use crate::utils::windows::filetime_to_timestamp;

pub use jiff::Timestamp;
#[allow(unused)]
pub use jiff::tz::Offset;

pub type RecordId = u64;

pub(crate) const EVTX_RECORD_HEADER_SIZE: usize = 24;

#[derive(Debug, Clone)]
pub struct EvtxRecord<'a> {
    pub chunk: &'a EvtxChunk<'a>,
    pub event_record_id: RecordId,
    pub timestamp: Timestamp,
    pub(crate) content: RecordContent<'a>,
    pub binxml_offset: u64,
    pub binxml_size: u32,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EvtxRecordHeader {
    pub data_size: u32,
    pub event_record_id: RecordId,
    pub timestamp: Timestamp,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SerializedEvtxRecord<T> {
    pub event_record_id: RecordId,
    pub timestamp: Timestamp,
    pub data: T,
}

impl SerializedEvtxRecord<Vec<u8>> {
    /// Validate the rendered bytes as UTF-8 and convert into a `String` record.
    pub fn into_string_record(self) -> Result<SerializedEvtxRecord<String>> {
        Ok(SerializedEvtxRecord {
            event_record_id: self.event_record_id,
            timestamp: self.timestamp,
            data: String::from_utf8(self.data).map_err(crate::err::SerializationError::from)?,
        })
    }
}

impl EvtxRecordHeader {
    pub fn from_bytes_at(buf: &[u8], offset: usize) -> DeserializationResult<EvtxRecordHeader> {
        let _ = bytes::slice_r(buf, offset, EVTX_RECORD_HEADER_SIZE, "EVTX record header")?;

        let magic = bytes::read_array_r::<4>(buf, offset, "record header magic")?;
        if &magic != b"\x2a\x2a\x00\x00" {
            return Err(DeserializationError::InvalidEvtxRecordHeaderMagic { magic });
        }

        let size = bytes::read_u32_le_r(buf, offset + 4, "record.data_size")?;
        let record_id = bytes::read_u64_le_r(buf, offset + 8, "record.event_record_id")?;
        let filetime = bytes::read_u64_le_r(buf, offset + 16, "record.filetime")?;

        let timestamp = filetime_to_timestamp(filetime)?;

        Ok(EvtxRecordHeader {
            data_size: size,
            event_record_id: record_id,
            timestamp,
        })
    }

    pub fn from_bytes(buf: &[u8]) -> DeserializationResult<EvtxRecordHeader> {
        Self::from_bytes_at(buf, 0)
    }

    pub fn record_data_size(&self) -> Result<u32> {
        // 24 - record header size
        // 4 - copy of size record size
        let decal = EVTX_RECORD_HEADER_SIZE as u32 + 4;
        if self.data_size < decal {
            return Err(EvtxError::InvalidDataSize {
                length: self.data_size,
                expected: decal,
            });
        }
        Ok(self.data_size - decal)
    }
}

impl<'a> EvtxRecord<'a> {
    /// Consumes the record and returns the rendered JSON as a `serde_json::Value`.
    pub fn into_json_value(self) -> Result<SerializedEvtxRecord<serde_json::Value>> {
        let event_record_id = self.event_record_id;
        let timestamp = self.timestamp;
        let record_with_json = self.into_json()?;

        Ok(SerializedEvtxRecord {
            event_record_id,
            timestamp,
            data: serde_json::from_str(&record_with_json.data)
                .map_err(crate::err::SerializationError::from)?,
        })
    }

    /// Render the record through the compiled-template machinery: cached
    /// program when the record's templates compile (the common case), the
    /// same materialized walker otherwise.
    fn render_into(&self, json: bool, data: &mut Vec<u8>) -> Result<()> {
        use crate::binxml::compiled::{
            render_tree_json, render_tree_xml, try_render_json_compiled, try_render_xml_compiled,
        };
        use crate::binxml::ir::{IrTemplateCache, build_tree_from_binxml_bytes_direct};
        use crate::binxml::value_render::ValueRenderer;

        let chunk = self.chunk;
        let start = self.binxml_offset as usize;
        let bytes = &chunk.data[start..start + self.binxml_size as usize];
        let caches = &mut *chunk.render_caches.borrow_mut();
        // Program-cache misses re-parse the template through this transient
        // cache (once per template per chunk); hits skip it entirely.
        let mut ir_cache = IrTemplateCache::with_capacity(0, &chunk.arena);
        let mut vr = ValueRenderer::new();
        let done = if json {
            try_render_json_compiled(
                bytes,
                chunk,
                &mut ir_cache,
                &mut caches.json,
                &mut caches.pf_json,
                &chunk.settings,
                &mut vr,
                data,
            )
        } else {
            try_render_xml_compiled(
                bytes,
                chunk,
                &mut ir_cache,
                &mut caches.xml,
                &mut caches.pf_xml,
                &chunk.settings,
                &mut vr,
                data,
            )
        };
        if done {
            return Ok(());
        }
        // Slow lane: walk a fully materialized tree (reusing the one built at
        // iteration time when available).
        let render = |tree: &crate::model::ir::IrTree<'_>, data: &mut Vec<u8>| {
            if json {
                render_tree_json(tree, &chunk.settings, data)
            } else {
                render_tree_xml(tree, &chunk.settings, data)
            }
        };
        match &self.content {
            RecordContent::Tree(tree) => render(tree, data),
            RecordContent::Template => {
                let tree = build_tree_from_binxml_bytes_direct(bytes, chunk, &mut ir_cache)?;
                render(&tree, data)
            }
        }
    }

    /// Consumes the record and renders it as compact JSON bytes (no UTF-8 validation pass).
    ///
    /// The renderers only emit valid UTF-8; use this in write-to-sink paths where the
    /// `String` type is not needed.
    pub fn into_json_bytes(self) -> Result<SerializedEvtxRecord<Vec<u8>>> {
        // Estimate buffer size based on BinXML size
        let mut data = Vec::with_capacity(self.binxml_size as usize * 2);
        self.render_into(true, &mut data)
            .map_err(|e| EvtxError::FailedToParseRecord {
                record_id: self.event_record_id,
                source: Box::new(e),
            })?;
        Ok(SerializedEvtxRecord {
            event_record_id: self.event_record_id,
            timestamp: self.timestamp,
            data,
        })
    }

    /// Consumes the record and renders it as compact JSON (streaming IR renderer).
    pub fn into_json(self) -> Result<SerializedEvtxRecord<String>> {
        self.into_json_bytes()?.into_string_record()
    }

    /// Consumes the record and renders it as XML bytes (no UTF-8 validation pass).
    pub fn into_xml_bytes(self) -> Result<SerializedEvtxRecord<Vec<u8>>> {
        let mut data = Vec::with_capacity(self.binxml_size as usize * 2);
        self.render_into(false, &mut data)
            .map_err(|e| EvtxError::FailedToParseRecord {
                record_id: self.event_record_id,
                source: Box::new(e),
            })?;
        Ok(SerializedEvtxRecord {
            event_record_id: self.event_record_id,
            timestamp: self.timestamp,
            data,
        })
    }

    /// Consumes the record and parse it, producing an XML serialized record.
    pub fn into_xml(self) -> Result<SerializedEvtxRecord<String>> {
        self.into_xml_bytes()?.into_string_record()
    }

    /// Parse all `TemplateInstance` substitution arrays from this record.
    ///
    /// This is a lightweight scan over the record's BinXML stream that extracts typed substitution
    /// values without building a legacy token vector.
    pub fn template_instances(&self) -> Result<Vec<crate::binxml::BinXmlTemplateValues<'a>>> {
        use crate::binxml::name::BinXmlNameEncoding;
        use crate::binxml::tokens::{
            read_attribute_cursor, read_entity_ref_cursor, read_fragment_header_cursor,
            read_open_start_element_cursor, read_processing_instruction_data_cursor,
            read_processing_instruction_target_cursor, read_substitution_descriptor_cursor,
            read_template_values_cursor,
        };

        let ansi_codec = self.chunk.settings.get_ansi_codec();
        let mut out: Vec<crate::binxml::BinXmlTemplateValues<'a>> = Vec::new();

        let mut cursor = ByteCursor::with_pos(self.chunk.data, self.binxml_offset as usize)?;
        let mut data_read: u32 = 0;
        let data_size = self.binxml_size;
        let mut eof = false;

        while !eof && data_read < data_size {
            let start = cursor.position();
            let token_byte = cursor.u8()?;

            match token_byte {
                0x00 => {
                    eof = true;
                }
                0x0c => {
                    let template = read_template_values_cursor(
                        &mut cursor,
                        Some(self.chunk),
                        ansi_codec,
                        &self.chunk.arena,
                    )?;
                    out.push(template);
                }
                0x01 => {
                    let _ = read_open_start_element_cursor(
                        &mut cursor,
                        false,
                        false,
                        BinXmlNameEncoding::Offset,
                    )?;
                }
                0x41 => {
                    let _ = read_open_start_element_cursor(
                        &mut cursor,
                        true,
                        false,
                        BinXmlNameEncoding::Offset,
                    )?;
                }
                0x02..=0x04 => {
                    // Structural tokens; no payload.
                }
                0x05 | 0x45 => {
                    let _ = crate::binxml::value_variant::BinXmlValue::from_binxml_cursor_in(
                        &mut cursor,
                        Some(self.chunk),
                        None,
                        ansi_codec,
                        &self.chunk.arena,
                    )?;
                }
                0x06 | 0x46 => {
                    let _ = read_attribute_cursor(&mut cursor, BinXmlNameEncoding::Offset)?;
                }
                0x09 | 0x49 => {
                    let _ = read_entity_ref_cursor(&mut cursor, BinXmlNameEncoding::Offset)?;
                }
                0x0a => {
                    let _ = read_processing_instruction_target_cursor(
                        &mut cursor,
                        BinXmlNameEncoding::Offset,
                    )?;
                }
                0x0b => {
                    let _ = read_processing_instruction_data_cursor(&mut cursor)?;
                }
                0x0d => {
                    let _ = read_substitution_descriptor_cursor(&mut cursor, false)?;
                }
                0x0e => {
                    let _ = read_substitution_descriptor_cursor(&mut cursor, true)?;
                }
                0x0f => {
                    let _ = read_fragment_header_cursor(&mut cursor)?;
                }
                0x07 | 0x47 => {
                    return Err(DeserializationError::UnimplementedToken {
                        name: "CDataSection",
                        offset: cursor.position(),
                    }
                    .into());
                }
                0x08 | 0x48 => {
                    return Err(DeserializationError::UnimplementedToken {
                        name: "CharReference",
                        offset: cursor.position(),
                    }
                    .into());
                }
                _ => {
                    return Err(DeserializationError::InvalidToken {
                        value: token_byte,
                        offset: cursor.position(),
                    }
                    .into());
                }
            }

            let total_read = cursor.position() - start;
            data_read = data_read.saturating_add(total_read as u32);
        }

        Ok(out)
    }
}