diff --git a/src/bin/evtxview/app.rs b/src/bin/evtxview/app.rs index bb97d6b..744acfe 100644 --- a/src/bin/evtxview/app.rs +++ b/src/bin/evtxview/app.rs @@ -11,7 +11,8 @@ use ratatui::{ }; const INFO_TEXT: &str = - "(Esc) quit | (↑) move up | (↓) move down | (→) next color | (←) previous color"; + r#"(Esc) quit | (↑) move up | (↓) move down | (→) next color | (←) previous color | + (x) eXclude by event id" | (i) Include by event id | (R) Reset filter"#; pub struct App { evtx_table: EvtxTable, @@ -145,6 +146,9 @@ impl App { KeyCode::Up => self.previous(1), KeyCode::PageDown => self.next((self.table_view_port.height / 2).into()), KeyCode::PageUp => self.previous((self.table_view_port.height / 2).into()), + KeyCode::Char('x') => self.exclude_event_id(), + KeyCode::Char('i') => self.include_event_id(), + KeyCode::Char('R') => self.reset_filter(), _ => {} } } @@ -152,6 +156,26 @@ impl App { self.exit = true; } + fn exclude_event_id(&mut self) { + if !self.evtx_table.is_empty() { + if let Some(i) = self.state.selected() { + self.evtx_table.exclude_event_id(i) + } + } + } + + fn include_event_id(&mut self) { + if !self.evtx_table.is_empty() { + if let Some(i) = self.state.selected() { + self.evtx_table.include_event_id(i) + } + } + } + + fn reset_filter(&mut self) { + self.evtx_table.reset_filter(); + } + fn set_selected(&mut self, idx: usize) { self.state.select(Some(idx)); self.table_scroll_state = self.table_scroll_state.position(idx); diff --git a/src/bin/evtxview/event.rs b/src/bin/evtxview/event.rs index e880be8..4fdeaa6 100644 --- a/src/bin/evtxview/event.rs +++ b/src/bin/evtxview/event.rs @@ -18,7 +18,7 @@ pub struct Event { #[allow(unused,non_snake_case)] pub struct System { //provider: Provider, - EventID: String, + EventID: u32, version: String, level: EventLevel, task: String, diff --git a/src/bin/evtxview/tui/evtx_table.rs b/src/bin/evtxview/tui/evtx_table.rs index 5220cf4..3216467 100644 --- a/src/bin/evtxview/tui/evtx_table.rs +++ b/src/bin/evtxview/tui/evtx_table.rs @@ -1,3 +1,4 @@ +use std::collections::HashSet; use std::{fs::File, path::Path}; use chrono::{DateTime, Utc}; @@ -6,7 +7,7 @@ use evtx::{EvtxParser, SerializedEvtxRecord}; use ouroboros::self_referencing; use quick_xml::de::from_str; use ratatui::layout::Constraint; -use ratatui::style::Stylize; +use ratatui::style::{Color, Stylize}; use ratatui::widgets::HighlightSpacing; use ratatui::{ style::{Modifier, Style}, @@ -18,11 +19,28 @@ use crate::event::Event; use super::color_scheme::{ColorScheme, PALETTES}; +#[derive(Eq, PartialEq, Hash)] +pub enum EventFilter { + ExcludeByEventId(u32), + IncludeByEventId(u32), +} + +impl EventFilter { + pub fn filter(&self, rc: &RowContents) -> bool { + match self { + EventFilter::ExcludeByEventId(event_id) => rc.event.system().EventID() != event_id, + EventFilter::IncludeByEventId(event_id) => rc.event.system().EventID() == event_id, + } + } +} + pub struct EvtxTable { rows: Vec, sparkline_data: Vec, colors: ColorScheme, timestamp_width: u16, + event_filters: HashSet, + filtered_rows_count: usize, } impl TryFrom<&Path> for EvtxTable { @@ -42,8 +60,11 @@ impl TryFrom<&Path> for EvtxTable { first_ts /= step_size; for row in rows.iter() { - let ts = usize::try_from((row.record_timestamp.timestamp() / step_size) - first_ts)?; - while ts + 1 > sparkline_data.len() {sparkline_data.push(0)} + let ts = + usize::try_from((row.record_timestamp.timestamp() / step_size) - first_ts)?; + while ts + 1 > sparkline_data.len() { + sparkline_data.push(0) + } sparkline_data[ts] += 1; } } @@ -53,11 +74,14 @@ impl TryFrom<&Path> for EvtxTable { .to_string() .len(), )?; + let filtered_rows_count = rows.len(); Ok(EvtxTable { rows, colors: ColorScheme::new(&PALETTES[0]), timestamp_width, - sparkline_data + sparkline_data, + event_filters: HashSet::new(), + filtered_rows_count, }) } } @@ -82,10 +106,12 @@ impl EvtxTable { let bar = " █ "; + let rows: Vec<_> = self.filtered_rows().collect(); + let table = Table::new( - &self.rows, + rows, vec![ - Constraint::Length(4), + Constraint::Length(2), Constraint::Length(self.timestamp_width), Constraint::Length(column_headers[1].len() as u16), Constraint::Length(column_headers[1].len() as u16), @@ -107,12 +133,23 @@ impl EvtxTable { table } + fn filtered_rows(&self) -> impl Iterator { + self.rows.iter().filter(|rc| self.filter_row(rc)) + } + + fn filter_row(&self, rc: &RowContents) -> bool { + self.event_filters + .iter() + .map(|filter| filter.filter(rc)) + .fold(true, |a, b| a & b) + } + pub fn len(&self) -> usize { - self.rows.len() + self.filtered_rows_count } pub fn is_empty(&self) -> bool { - self.rows.is_empty() + self.filtered_rows_count == 0 } pub fn content(&self, idx: usize) -> Option<&String> { @@ -122,6 +159,31 @@ impl EvtxTable { pub fn sparkline_data(&self) -> &Vec { &self.sparkline_data } + + pub fn event_id_in_row(&self, filtered_row_id: usize) -> Option { + self.filtered_rows() + .nth(filtered_row_id) + .map(|r| *r.event.system().EventID()) + } + + pub fn exclude_event_id(&mut self, filtered_row_id: usize) { + if let Some(event_id) = self.event_id_in_row(filtered_row_id) { + self.event_filters + .insert(EventFilter::ExcludeByEventId(event_id)); + } + self.filtered_rows_count = self.filtered_rows().count(); + } + pub fn include_event_id(&mut self, filtered_row_id: usize) { + if let Some(event_id) = self.event_id_in_row(filtered_row_id) { + self.event_filters + .insert(EventFilter::IncludeByEventId(event_id)); + } + self.filtered_rows_count = self.filtered_rows().count(); + } + pub fn reset_filter(&mut self) { + self.event_filters.clear(); + self.filtered_rows_count = self.filtered_rows().count(); + } } #[self_referencing] @@ -218,7 +280,7 @@ impl<'r> TryFrom<&'r SerializedEvtxRecord> for RowContents { level: event.system().level().to_string(), timestamp: FormattableDatetime::from(record.timestamp).to_string(), record_id: record.event_record_id.to_string(), - event_id: event.system().EventID().clone(), + event_id: event.system().EventID().to_string(), raw_value: record.data.clone(), user_id, event, @@ -238,11 +300,11 @@ impl<'r> From<&'r RowContents> for Row<'r> { &contents.event_data[..], ]); - if !contents.user_id.is_empty() && ! contents.user_id.contains('-') { + if !contents.user_id.is_empty() && !contents.user_id.contains('-') { if contents.user_id == "500" { row = row.bold().red() } else { - row = row.on_light_red() + row = row.fg(Color::Red) } }