use std::io::BufWriter; use std::fs::File; use csv::WriterBuilder; use downcast_rs::__std::process; use crate::detections::message::{LOGONSUMMARY_FLAG, METRICS_FLAG, CH_CONFIG, AlertMessage}; use crate::detections::{configs::CONFIG, detection::EvtxRecordInfo}; use comfy_table::*; use comfy_table::modifiers::UTF8_ROUND_CORNERS; use comfy_table::presets::UTF8_FULL; use super::metrics::EventMetrics; use hashbrown::HashMap; #[derive(Debug, Clone)] pub struct Timeline { pub stats: EventMetrics, } impl Default for Timeline { fn default() -> Self { Self::new() } } impl Timeline { pub fn new() -> Timeline { let totalcnt = 0; let filepath = String::default(); let starttm = String::default(); let endtm = String::default(); let statslst = HashMap::new(); let statsloginlst = HashMap::new(); let statistic = EventMetrics::new(totalcnt, filepath, starttm, endtm, statslst, statsloginlst); Timeline { stats: statistic } } pub fn start(&mut self, records: &[EvtxRecordInfo]) { self.stats.evt_stats_start(records); self.stats.logon_stats_start(records); } pub fn tm_stats_dsp_msg(&mut self) { if !*METRICS_FLAG { return; } // 出力メッセージ作成 let mut sammsges: Vec = Vec::new(); let total_event_record = format!("\nTotal Event Records: {}\n", self.stats.total); if CONFIG.read().unwrap().args.filepath.is_some() { sammsges.push(format!("Evtx File Path: {}", self.stats.filepath)); sammsges.push(total_event_record); sammsges.push(format!("First Timestamp: {}", self.stats.start_time)); sammsges.push(format!("Last Timestamp: {}\n", self.stats.end_time)); } else { sammsges.push(total_event_record); } let header = vec!["Count", "Percent", "Channel", "ID", "Event"]; let target; let mut wtr= if let Some(csv_path) = &CONFIG.read().unwrap().args.output { // output to file match File::create(csv_path) { Ok(file) => { target = Box::new(BufWriter::new(file)); Some(WriterBuilder::new().from_writer(target)) }, Err(err) => { AlertMessage::alert(&format!("Failed to open file. {}", err)).ok(); process::exit(1); } } } else { None }; if let Some(ref mut w) = wtr { w.write_record(&header).ok(); } let mut stats_tb = Table::new(); stats_tb.load_preset(UTF8_FULL).apply_modifier(UTF8_ROUND_CORNERS); stats_tb.set_header(header); // 集計件数でソート let mut mapsorted: Vec<_> = self.stats.stats_list.iter().collect(); mapsorted.sort_by(|x, y| y.1.cmp(x.1)); // イベントID毎の出力メッセージ生成 let stats_msges: Vec> = self.tm_stats_set_msg(mapsorted); for msgprint in sammsges.iter() { println!("{}", msgprint); } if CONFIG.read().unwrap().args.output.is_some() { for msg in stats_msges.iter(){ if let Some(ref mut w) = wtr { w.write_record(msg).ok(); } } } stats_tb.add_rows(stats_msges); println!("{stats_tb}"); } pub fn tm_logon_stats_dsp_msg(&mut self) { if !*LOGONSUMMARY_FLAG { return; } // 出力メッセージ作成 let mut sammsges: Vec = Vec::new(); let total_event_record = format!("\nTotal Event Records: {}\n", self.stats.total); if CONFIG.read().unwrap().args.filepath.is_some() { sammsges.push(format!("Evtx File Path: {}", self.stats.filepath)); sammsges.push(total_event_record); sammsges.push(format!("First Timestamp: {}", self.stats.start_time)); sammsges.push(format!("Last Timestamp: {}\n", self.stats.end_time)); } else { sammsges.push(total_event_record); } for msgprint in sammsges.iter() { println!("{}", msgprint); } self.tm_loginstats_tb_set_msg(); } // イベントID毎の出力メッセージ生成 fn tm_stats_set_msg(&self, mapsorted: Vec<(&(std::string::String, std::string::String), &usize)>) -> Vec> { let mut msges: Vec> = Vec::new(); for ((event_id, channel), event_cnt) in mapsorted.iter() { // 件数の割合を算出 let rate: f32 = **event_cnt as f32 / self.stats.total as f32; // イベント情報取得(eventtitleなど) let conf = CONFIG .read() .unwrap() .event_timeline_config .get_event_id(event_id) .is_some(); // event_id_info.txtに登録あるものは情報設定 // 出力メッセージ1行作成 let fmted_channel = channel.replace('\"', ""); let ch = CH_CONFIG.get(fmted_channel.to_lowercase().as_str()).unwrap_or(&fmted_channel).to_string(); if conf { msges.push(vec! [event_cnt.to_string(), format!("{:.1}%", (rate * 1000.0).round() / 10.0), ch, event_id.to_string(), CONFIG .read() .unwrap() .event_timeline_config .get_event_id(event_id) .unwrap() .evttitle.to_string(), ] ); } else { msges.push( vec![ event_cnt.to_string(), format!("{:.1}%", (rate * 1000.0).round() / 10.0), ch, event_id.replace('\"', ""), "Unknown".to_string(), ]); } } msges } /// ユーザ毎のログイン統計情報出力メッセージ生成 fn tm_loginstats_tb_set_msg(&self) { println!("Logon Summary"); if self.stats.stats_login_list.is_empty() { let mut loginmsges: Vec = Vec::new(); loginmsges.push("-----------------------------------------".to_string()); loginmsges.push("| No logon events were detected. |".to_string()); loginmsges.push("-----------------------------------------\n".to_string()); for msgprint in loginmsges.iter() { println!("{}", msgprint); } } else { let header = vec!["User", "Failed", "Successful"]; let target; let mut wtr= if let Some(csv_path) = &CONFIG.read().unwrap().args.output { // output to file match File::create(csv_path) { Ok(file) => { target = Box::new(BufWriter::new(file)); Some(WriterBuilder::new().from_writer(target)) }, Err(err) => { AlertMessage::alert(&format!("Failed to open file. {}", err)).ok(); process::exit(1); } } } else { None }; if let Some(ref mut w) = wtr { w.write_record(&header).ok(); } let mut logins_stats_tb = Table::new(); logins_stats_tb.load_preset(UTF8_FULL).apply_modifier(UTF8_ROUND_CORNERS); logins_stats_tb.set_header(&header); // 集計件数でソート let mut mapsorted: Vec<_> = self.stats.stats_login_list.iter().collect(); mapsorted.sort_by(|x, y| x.0.cmp(y.0)); for (key, values) in &mapsorted { let mut username: String = key.to_string(); username.pop(); username.remove(0); let record_data = vec![ username, values[1].to_string(), values[0].to_string(), ]; if let Some(ref mut w) = wtr { w.write_record(&record_data).ok(); } logins_stats_tb.add_row(record_data); } println!("{logins_stats_tb}"); println!(); } } }