evtx 0.11.2

A Fast (and safe) parser for the Windows XML Event Log (EVTX) format
Documentation
use anyhow::{Context, Result};
use clap::{Arg, ArgMatches, Command};
use indoc::indoc;

pub fn command() -> Command {
    Command::new("dump-template-instances")
        .about("Dump BinXML TemplateInstance substitution arrays from EVTX records (JSONL)")
        .long_about(indoc!(
            r#"
            Dump BinXML TemplateInstance substitution arrays from EVTX records as JSONL.

            This is useful for offline rendering workflows where you have a template cache
            (from `extract-wevt-templates`) and need the record's substitution values array.
        "#
        ))
        .arg(
            Arg::new("input")
                .long("input")
                .short('i')
                .required(true)
                .value_name("EVTX")
                .help("Input EVTX file path."),
        )
        .arg(
            Arg::new("record-id")
                .long("record-id")
                .value_parser(clap::value_parser!(u64).range(0..))
                .value_name("ID")
                .help("Only dump template instances for the specified event record id."),
        )
        .arg(
            Arg::new("template-instance-index")
                .long("template-instance-index")
                .value_parser(clap::value_parser!(usize))
                .default_value("0")
                .value_name("N")
                .help("When a record contains multiple TemplateInstance tokens, select which one to dump (default: 0)."),
        )
}

pub fn run(matches: &ArgMatches) -> Result<()> {
    #[cfg(feature = "wevt_templates")]
    {
        run_impl(matches)
    }

    #[cfg(not(feature = "wevt_templates"))]
    {
        let _ = matches;
        anyhow::bail!(
            "This subcommand requires building `evtx_dump` with template support enabled.\n\
             Example:\n\
              cargo run --bin evtx_dump -- dump-template-instances ..."
        );
    }
}

#[cfg(feature = "wevt_templates")]
mod imp {
    use super::*;
    use evtx::{EvtxParser, ParserSettings};
    use serde::Serialize;
    use std::path::PathBuf;

    #[derive(Debug, Serialize)]
    struct DumpTemplateInstanceOutputLine {
        source: String,
        record_id: u64,
        timestamp: String,
        template_instance_index: usize,
        template_id: u32,
        template_def_offset: u32,
        template_guid: Option<String>,
        substitutions: Vec<String>,
    }

    fn binxml_value_to_string_lossy(
        value: &evtx::binxml::value_variant::BinXmlValue<'_>,
    ) -> String {
        value.to_string()
    }

    pub(super) fn run_impl(matches: &ArgMatches) -> Result<()> {
        let input = PathBuf::from(matches.get_one::<String>("input").expect("required"));
        let record_id_filter = matches.get_one::<u64>("record-id").copied();
        let template_instance_index: usize = *matches
            .get_one::<usize>("template-instance-index")
            .expect("has default");

        let settings = ParserSettings::default();
        let mut parser = EvtxParser::from_path(&input)
            .with_context(|| format!("Failed to open evtx file at: {}", input.display()))?
            .with_configuration(settings.clone());

        let source = input.to_string_lossy().to_string();

        for chunk_res in parser.chunks() {
            let mut chunk_data = match chunk_res {
                Ok(c) => c,
                Err(e) => {
                    eprintln!("{e}");
                    continue;
                }
            };

            let mut chunk = match chunk_data.parse(std::sync::Arc::new(settings.clone())) {
                Ok(c) => c,
                Err(e) => {
                    eprintln!("{e}");
                    continue;
                }
            };

            for record_res in chunk.iter() {
                let record = match record_res {
                    Ok(r) => r,
                    Err(e) => {
                        eprintln!("{e}");
                        continue;
                    }
                };

                if record_id_filter.is_some_and(|want| record.event_record_id != want) {
                    continue;
                }

                let instances = record.template_instances()?;
                let Some(tpl) = instances.get(template_instance_index) else {
                    continue;
                };

                let substitutions = tpl
                    .values
                    .iter()
                    .map(binxml_value_to_string_lossy)
                    .collect::<Vec<_>>();

                let line = DumpTemplateInstanceOutputLine {
                    source: source.clone(),
                    record_id: record.event_record_id,
                    timestamp: record.timestamp.to_string(),
                    template_instance_index,
                    template_id: tpl.template_id,
                    template_def_offset: tpl.template_def_offset,
                    template_guid: tpl.template_guid.as_ref().map(|g| g.to_string()),
                    substitutions,
                };

                println!("{}", serde_json::to_string(&line)?);

                if record_id_filter.is_some() {
                    // Found the record we wanted; keep going anyway in case there are duplicates? No.
                    return Ok(());
                }
            }
        }

        Ok(())
    }
}

#[cfg(feature = "wevt_templates")]
use imp::run_impl;