diff --git a/src/bin/evtxcat/cli.rs b/src/bin/evtxcat/cli.rs new file mode 100644 index 0000000..65af151 --- /dev/null +++ b/src/bin/evtxcat/cli.rs @@ -0,0 +1,42 @@ +use clap::Parser; +use dfir_toolkit::common::HasVerboseFlag; +use log::LevelFilter; + +use crate::output_format::OutputFormat; + +/// Display one or more events from an evtx file +#[derive(Parser)] +#[clap(name=env!("CARGO_BIN_NAME"),author,version,about)] +pub (crate) struct Cli { + /// Name of the evtx file to read from + pub (crate) evtx_file: String, + + /// filter: minimal event record identifier + #[clap(long)] + pub (crate) min: Option, + + /// filter: maximal event record identifier + #[clap(long)] + pub (crate) max: Option, + + /// show only the one event with this record identifier + #[clap(short, long)] + pub (crate) id: Option, + + /// don't display the records in a table format + #[clap(short('T'), long("display-table"))] + pub (crate) show_table: bool, + + /// output format + #[clap(value_enum, short('F'), long("format"), default_value_t = OutputFormat::Xml)] + pub (crate) format: OutputFormat, + + #[clap(flatten)] + verbose: clap_verbosity_flag::Verbosity, +} + +impl HasVerboseFlag for Cli { + fn log_level_filter(&self)-> LevelFilter { + self.verbose.log_level_filter() + } +} \ No newline at end of file diff --git a/src/bin/evtxcat/output_format.rs b/src/bin/evtxcat/output_format.rs new file mode 100644 index 0000000..03acf20 --- /dev/null +++ b/src/bin/evtxcat/output_format.rs @@ -0,0 +1,6 @@ + +#[derive(clap::ValueEnum, Clone)] +pub (crate) enum OutputFormat { + Json, + Xml, +} \ No newline at end of file diff --git a/src/bin/evtxcat/record_filter.rs b/src/bin/evtxcat/record_filter.rs new file mode 100644 index 0000000..d45077e --- /dev/null +++ b/src/bin/evtxcat/record_filter.rs @@ -0,0 +1,75 @@ +use std::{io::{Read, Seek}, collections::HashMap, fs::File}; + +use evtx::{EvtxParser, SerializedEvtxRecord}; + +use crate::unfiltered::Unfiltered; + + +pub (crate) trait RecordFilter: Sized { + type ReaderType: Read + Seek; + + fn unfiltered(parser: &mut EvtxParser) -> Unfiltered; + + fn filter_by_id( + mut parser: EvtxParser, + filter_id: u64, + ) -> (Vec, HashMap>) { + let mut record_ids: Vec = Vec::new(); + let mut records: HashMap> = HashMap::new(); + if let Some(result) = Self::unfiltered(&mut parser).find(|record| match record { + Ok(evt) => evt.event_record_id == filter_id, + _ => false, + }) { + let evt = result.unwrap(); + record_ids.push(evt.event_record_id); + records.insert(evt.event_record_id, evt); + } + (record_ids, records) + } + + fn filter_by_range( + mut parser: EvtxParser, + min: u64, + max: u64, + ) -> (Vec, HashMap>) { + let mut record_ids: Vec = Vec::new(); + let mut records: HashMap> = HashMap::new(); + + for record in Self::unfiltered(&mut parser) { + match record { + Err(_) => (), + Ok(evt) => { + let id = evt.event_record_id; + + if id >= min && id <= max { + record_ids.push(id); + records.insert(id, evt); + } + } + } + } + + record_ids.sort_unstable(); + (record_ids, records) + } +} + +impl RecordFilter for serde_json::Value { + type ReaderType = File; + + fn unfiltered(parser: &mut EvtxParser) -> Unfiltered { + Unfiltered { + inner: Box::new(parser.records_json_value()), + } + } +} + +impl RecordFilter for String { + type ReaderType = File; + + fn unfiltered(parser: &mut EvtxParser) -> Unfiltered { + Unfiltered { + inner: Box::new(parser.records()), + } + } +} diff --git a/src/bin/evtxcat/record_list_formatter.rs b/src/bin/evtxcat/record_list_formatter.rs new file mode 100644 index 0000000..bc023e1 --- /dev/null +++ b/src/bin/evtxcat/record_list_formatter.rs @@ -0,0 +1,50 @@ +use std::collections::HashMap; + +use colored_json::to_colored_json_auto; +use evtx::SerializedEvtxRecord; +use term_table::{row::Row, table_cell::TableCell}; + +use crate::cli::Cli; + +pub (crate) trait RecordListFormatter: Sized { + fn format(record: &SerializedEvtxRecord) -> String; + + fn display_results( + record_ids: Vec, + records: HashMap>, + cli: &Cli, + ) { + if !cli.show_table { + for id in record_ids.into_iter() { + let record = &records[&id]; + println!("{}", Self::format(record)); + } + } else { + let mut table = term_table::Table::new(); + if let Some(size) = termsize::get() { + table.set_max_column_widths(vec![(0, 12), (1, (size.cols - 16).into())]) + } + + for id in record_ids.into_iter() { + let record = &records[&id]; + table.add_row(Row::new(vec![ + TableCell::new(id), + TableCell::new(Self::format(record)), + ])); + } + println!("{}", table.render()); + } + } +} + +impl RecordListFormatter for String { + fn format(record: &SerializedEvtxRecord) -> String { + record.data.clone() + } +} + +impl RecordListFormatter for serde_json::Value { + fn format(record: &SerializedEvtxRecord) -> String { + to_colored_json_auto(&record.data).unwrap() + } +} \ No newline at end of file diff --git a/src/bin/evtxcat/unfiltered.rs b/src/bin/evtxcat/unfiltered.rs new file mode 100644 index 0000000..e88b2e0 --- /dev/null +++ b/src/bin/evtxcat/unfiltered.rs @@ -0,0 +1,13 @@ +use evtx::SerializedEvtxRecord; + +pub (crate) struct Unfiltered<'a, V> { + pub (crate) inner: Box>> + 'a>, +} + +impl<'a, V> Iterator for Unfiltered<'a, V> { + type Item = evtx::err::Result>; + + fn next(&mut self) -> Option { + self.inner.next() + } +} \ No newline at end of file