evtxtools 1.12.1

tools for the analysis of evtx files
Documentation
use std::{
    io::{Read, Seek},
    path::PathBuf,
};

use anyhow::Result;
use colored::{control::SHOULD_COLORIZE, Colorize};
use data::EventId;
use evtx::{EvtxParser, ParserSettings, SerializedEvtxRecord};
mod data;

use serde_json::Value;

mod ls;
use clap::Parser;
use ls::{Cli, FilterBySystemField, HighlightedStringBuilder, SortOrder};

struct EvtxLs {
    cli: Cli,
    hs_builder: HighlightedStringBuilder,
}

impl EvtxLs {
    fn new() -> Self {
        let cli = Cli::parse();
        let hs_builder = HighlightedStringBuilder::new(cli.highlight.clone());

        Self { cli, hs_builder }
    }

    fn run(self) -> Result<()> {
        let mut records = Vec::new();

        for f_name in self.cli.evtx_files.iter() {
            let path = PathBuf::try_from(&f_name)?;

            let settings = ParserSettings::default().num_threads(0);
            let parser = EvtxParser::from_path(path)?.with_configuration(settings);

            records.extend(self.read_records(parser)?);
        }

        match self.cli.sort_order {
            SortOrder::Storage => assert!(records.is_empty()),
            SortOrder::RecordId => {
                records.sort_by(|a, b| a.event_record_id.cmp(&b.event_record_id))
            }
            SortOrder::Time => records.sort_by(|a, b| a.timestamp.cmp(&b.timestamp)),
        }

        if !records.is_empty() {
            for record in records.into_iter() {
                self.display_record(&record)?;
            }
        }

        Ok(())
    }

    fn read_records<T: Read + Seek>(&self, mut parser: EvtxParser<T>) -> Result<Vec<SerializedEvtxRecord<Value>>> {
        if self.cli.display_colors {
            SHOULD_COLORIZE.set_override(true);
        }

        let mut records = Vec::new();

        for result in parser.records_json_value() {
            match result {
                Err(_) => (),
                Ok(record) => {
                    if let Some(not_before) = self.cli.not_before.as_ref() {
                        if &record.timestamp < not_before {
                            continue;
                        }
                    }

                    if let Some(not_after) = self.cli.not_after.as_ref() {
                        if &record.timestamp < not_after {
                            continue;
                        }
                    }

                    if !self.cli.included_event_ids.is_empty() {
                        let event_id = EventId::try_from(&record)?.into();
                        if !self.cli.included_event_ids.contains(&event_id) {
                            continue;
                        }
                    }

                    if !self.cli.excluded_event_ids.is_empty() {
                        let event_id = EventId::try_from(&record)?.into();
                        if self.cli.excluded_event_ids.contains(&event_id) {
                            continue;
                        }
                    }

                    if matches!(self.cli.sort_order, SortOrder::Storage) {
                        self.display_record(&record)?
                    } else {
                        records.push(record);
                    }
                }
            }
        }

        Ok(records)
    }

    fn display_record(&self, record: &SerializedEvtxRecord<Value>) -> Result<()> {
        let system_fields = if self.cli.hide_base_fields {
            "".to_owned()
        } else {
            let system_fields = <SerializedEvtxRecord<Value> as FilterBySystemField>::filter_fields(
                record,
                self.cli.display_system_fields.as_ref()
            )?;

            let line_parts: Vec<String> = if self.cli.delimiter.is_none() {
                system_fields
                    .iter()
                    .map(|f| f.value_with_padding())
                    .collect()
            } else {
                system_fields.iter().map(|f| f.to_string()).collect()
            };
            if line_parts.is_empty() {
                "".to_owned()
            } else {
                format!(
                    "{}{}",
                    line_parts.join(&self.cli.delimiter.unwrap_or(' ').to_string()),
                    &self.cli.delimiter.unwrap_or(' ')
                )
            }
        };

        let event_data = self
            .format_custom_data(record, "UserData")
            .or_else(|| self.format_custom_data(record, "EventData"))
            .unwrap_or_else(|| "".to_owned())
            .replace("\\u001b", "\u{001b}");

        let output = match self.cli.delimiter {
            None => format!(
                "{} {system_fields}{event_data} {event_data}",
                record.timestamp.format("%FT%T%.3f")
            ),
            Some(d) => format!(
                "{}{d}{system_fields}{event_data}{d}{event_data}",
                record.timestamp.to_rfc3339()
            ),
        }
        .normal();

        println!("{output}");

        Ok(())
    }

    fn format_custom_data(
        &self,
        record: &SerializedEvtxRecord<Value>,
        tag_name: &str,
    ) -> Option<String> {
        // fail if the event has no "Event" content
        let event = record.data.get("Event").unwrap();

        match event.get(tag_name) {
            None => None,
            Some(custom_data) => match custom_data {
                Value::Null => Some("".to_owned()),
                v => Some(self.hs_builder.highlight_data(v).to_string())
            },
        }
    }
}

fn main() -> Result<()> {
    sigpipe::reset();
    EvtxLs::new().run()
}