diff --git a/CHANGELOG-Japanese.md b/CHANGELOG-Japanese.md index 2e59ede0..28c1468c 100644 --- a/CHANGELOG-Japanese.md +++ b/CHANGELOG-Japanese.md @@ -11,6 +11,12 @@ - EventID解析のオプションをmetricsオプションに変更した。(旧: `-s, --statistics` -> 新: `-M, --metrics`) (#706) (@hitenkoku) - ルール更新オプション(`-u`)を利用したときにHayabusaの新バージョンがないかを確認し、表示するようにした。 (#710) (@hitenkoku) - HTMLレポート内にロゴを追加した。 (#714) (@hitenkoku) +- メトリクスオプション(`-M --metrics`)もしくはログオン情報(`-L --logon-summary`)と`-d`オプションを利用した場合に1つのテーブルで表示されるように修正した。 (#707) (@hitenkoku) +- メトリクスオプションの結果出力にチャンネル列を追加した。 (#707) (@hitenkoku) +- メトリクスオプション(`-M --metrics`)もしくはログオン情報(`-L --logon-summary`)と`-d`オプションを利用した場合に「First Timestamp」と「Last Timestamp」の出力を行わないように修正した。 (#707) (@hitenkoku) +- メトリクスオプションとログオン情報オプションに対してcsv出力機能(`-o --output`)を追加した。 (#707) (@hitenkoku) +- メトリクスオプションの出力を検出回数と全体の割合が1つのセルで表示されていた箇所を2つの列に分けた。 (#707) (@hitenkoku) +- メトリクスオプションとログオン情報の画面出力に利用していたprettytable-rsクレートをcomfy_tableクレートに修正した. (#707) (@hitenkoku) ## v1.6.0 [2022/09/16] diff --git a/CHANGELOG.md b/CHANGELOG.md index dd6703b3..97716bf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,12 @@ (Note: `statistics_event_info.txt` was changed to `event_id_info.txt`.) - Display new version of Hayabusa link when updating if there is a newer version. (#710) (@hitenkoku) - Added logo in HTML summary output. (#714) (@hitenkoku) +- Unified output one table of -M or -L option with -d option. (#707) (@hitenkoku) +- Added Channel column to metrics output. (#707) (@hitenkoku) +- Removed First Timestamp and Last Timestamp of -M and -L option with -d option. (#707) (@hitenkoku) +- Added csv output option(`-o --output`) when -M and -L option is used. (#707) (@hitenkoku) +- Separated Count and Percent columns in metric output. (#707) (@hitenkoku) +- Changed output table format of metric option and logon information crate from prettytable-rs to comfy_table. (#707) (@hitenkoku) ## v1.6.0 [2022/09/16] diff --git a/Cargo.lock b/Cargo.lock index 5b6b3458..ccbf9f91 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -248,7 +248,7 @@ version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89eab4d20ce20cea182308bca13088fecea9c05f6776cf287205d41a0ed3c847" dependencies = [ - "encode_unicode 0.3.6", + "encode_unicode", "libc", "once_cell", "terminal_size", @@ -403,27 +403,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "dirs-next" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" -dependencies = [ - "cfg-if", - "dirs-sys-next", -] - -[[package]] -name = "dirs-sys-next" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" -dependencies = [ - "libc", - "redox_users", - "winapi", -] - [[package]] name = "discard" version = "1.0.4" @@ -448,12 +427,6 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" -[[package]] -name = "encode_unicode" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" - [[package]] name = "encoding" version = "0.2.33" @@ -781,7 +754,6 @@ dependencies = [ "num_cpus", "openssl", "pbr", - "prettytable-rs", "pulldown-cmark", "quick-xml", "rand", @@ -1399,20 +1371,6 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" -[[package]] -name = "prettytable-rs" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f375cb74c23b51d23937ffdeb48b1fbf5b6409d4b9979c1418c1de58bc8f801" -dependencies = [ - "atty", - "csv", - "encode_unicode 1.0.0", - "lazy_static", - "term", - "unicode-width", -] - [[package]] name = "proc-macro-error" version = "1.0.4" @@ -1551,17 +1509,6 @@ dependencies = [ "bitflags", ] -[[package]] -name = "redox_users" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" -dependencies = [ - "getrandom", - "redox_syscall", - "thiserror", -] - [[package]] name = "regex" version = "1.6.0" @@ -1997,17 +1944,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "term" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" -dependencies = [ - "dirs-next", - "rustversion", - "winapi", -] - [[package]] name = "termcolor" version = "1.1.3" diff --git a/Cargo.toml b/Cargo.toml index f1435d16..48db1b77 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,6 @@ hashbrown = "0.12.*" hex = "0.4.*" git2 = "0.*" termcolor = "*" -prettytable-rs = "0.*" krapslog = "*" terminal_size = "*" bytesize = "1.*" diff --git a/src/lib.rs b/src/lib.rs index db666270..655018ce 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,4 @@ pub mod options; pub mod timeline; pub mod yaml; #[macro_use] -extern crate prettytable; -#[macro_use] extern crate horrorshow; diff --git a/src/main.rs b/src/main.rs index b08f2659..fa78a0cd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -656,15 +656,23 @@ impl App { self.rule_keys = self.get_all_keys(&rule_files); let mut detection = detection::Detection::new(rule_files); let mut total_records: usize = 0; + let mut tl = Timeline::new(); for evtx_file in evtx_files { if configs::CONFIG.read().unwrap().args.verbose { println!("Checking target evtx FilePath: {:?}", &evtx_file); } let cnt_tmp: usize; - (detection, cnt_tmp) = self.analysis_file(evtx_file, detection, time_filter); + (detection, cnt_tmp, tl) = + self.analysis_file(evtx_file, detection, time_filter, tl.clone()); total_records += cnt_tmp; pb.inc(); } + if *METRICS_FLAG { + tl.tm_stats_dsp_msg(); + } + if *LOGONSUMMARY_FLAG { + tl.tm_logon_stats_dsp_msg(); + } if configs::CONFIG.read().unwrap().args.output.is_some() { println!(); println!(); @@ -683,15 +691,15 @@ impl App { evtx_filepath: PathBuf, mut detection: detection::Detection, time_filter: &TargetEventTime, - ) -> (detection::Detection, usize) { + mut tl: Timeline, + ) -> (detection::Detection, usize, Timeline) { let path = evtx_filepath.display(); let parser = self.evtx_to_jsons(evtx_filepath.clone()); let mut record_cnt = 0; if parser.is_none() { - return (detection, record_cnt); + return (detection, record_cnt, tl); } - let mut tl = Timeline::new(); let mut parser = parser.unwrap(); let mut records = parser.records_json_value(); @@ -760,10 +768,7 @@ impl App { } } - tl.tm_stats_dsp_msg(); - tl.tm_logon_stats_dsp_msg(); - - (detection, record_cnt) + (detection, record_cnt, tl) } async fn create_rec_infos( diff --git a/src/timeline/metrics.rs b/src/timeline/metrics.rs index d04e2ddd..1126e88f 100644 --- a/src/timeline/metrics.rs +++ b/src/timeline/metrics.rs @@ -2,13 +2,13 @@ use crate::detections::message::{LOGONSUMMARY_FLAG, METRICS_FLAG}; use crate::detections::{detection::EvtxRecordInfo, utils}; use hashbrown::HashMap; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct EventMetrics { pub total: usize, pub filepath: String, pub start_time: String, pub end_time: String, - pub stats_list: HashMap, + pub stats_list: HashMap<(String, String), usize>, pub stats_login_list: HashMap, } /** @@ -20,7 +20,7 @@ impl EventMetrics { filepath: String, start_time: String, end_time: String, - stats_list: HashMap, + stats_list: HashMap<(String, String), usize>, stats_login_list: HashMap, ) -> EventMetrics { EventMetrics { @@ -66,79 +66,71 @@ impl EventMetrics { self.filepath = records[0].evtx_filepath.as_str().to_owned(); // sortしなくてもイベントログのTimeframeを取得できるように修正しました。 // sortしないことにより計算量が改善されています。 - // もうちょっと感じに書けるといえば書けます。 for record in records.iter() { - let evttime = utils::get_event_value( + if let Some(evttime) = utils::get_event_value( "Event.System.TimeCreated_attributes.SystemTime", &record.record, ) - .map(|evt_value| evt_value.to_string()); - if evttime.is_none() { - continue; - } - - let evttime = evttime.unwrap(); - if self.start_time.is_empty() || evttime < self.start_time { - self.start_time = evttime.to_string(); - } - if self.end_time.is_empty() || evttime > self.end_time { - self.end_time = evttime; - } + .map(|evt_value| evt_value.to_string()) + { + if self.start_time.is_empty() || evttime < self.start_time { + self.start_time = evttime.to_string(); + } + if self.end_time.is_empty() || evttime > self.end_time { + self.end_time = evttime; + } + }; } self.total += records.len(); } - // EventIDで集計 + /// EventID`で集計 fn stats_eventid(&mut self, records: &[EvtxRecordInfo]) { // let mut evtstat_map = HashMap::new(); for record in records.iter() { - let evtid = utils::get_event_value("EventID", &record.record); - if evtid.is_none() { - continue; - } - - let idnum = evtid.unwrap(); - let count: &mut usize = self.stats_list.entry(idnum.to_string()).or_insert(0); - *count += 1; + let channel = if let Some(ch) = utils::get_event_value("Channel", &record.record) { + ch.to_string() + } else { + "-".to_string() + }; + if let Some(idnum) = utils::get_event_value("EventID", &record.record) { + let count: &mut usize = self + .stats_list + .entry((idnum.to_string(), channel)) + .or_insert(0); + *count += 1; + }; } - // return evtstat_map; } // Login event fn stats_login_eventid(&mut self, records: &[EvtxRecordInfo]) { for record in records.iter() { - let evtid = utils::get_event_value("EventID", &record.record); - if evtid.is_none() { - continue; - } - let idnum: i64 = if evtid.unwrap().is_number() { - evtid.unwrap().as_i64().unwrap() - } else { - evtid - .unwrap() - .as_str() - .unwrap() - .parse::() - .unwrap_or_default() - }; - if !(idnum == 4624 || idnum == 4625) { - continue; - } + if let Some(evtid) = utils::get_event_value("EventID", &record.record) { + let idnum: i64 = if evtid.is_number() { + evtid.as_i64().unwrap() + } else { + evtid.as_str().unwrap().parse::().unwrap_or_default() + }; + if !(idnum == 4624 || idnum == 4625) { + continue; + } - let username = utils::get_event_value("TargetUserName", &record.record); - let countlist: [usize; 2] = [0, 0]; - if idnum == 4624 { - let count: &mut [usize; 2] = self - .stats_login_list - .entry(username.unwrap().to_string()) - .or_insert(countlist); - count[0] += 1; - } else if idnum == 4625 { - let count: &mut [usize; 2] = self - .stats_login_list - .entry(username.unwrap().to_string()) - .or_insert(countlist); - count[1] += 1; - } + let username = utils::get_event_value("TargetUserName", &record.record); + let countlist: [usize; 2] = [0, 0]; + if idnum == 4624 { + let count: &mut [usize; 2] = self + .stats_login_list + .entry(username.unwrap().to_string()) + .or_insert(countlist); + count[0] += 1; + } else if idnum == 4625 { + let count: &mut [usize; 2] = self + .stats_login_list + .entry(username.unwrap().to_string()) + .or_insert(countlist); + count[1] += 1; + } + }; } } } diff --git a/src/timeline/timelines.rs b/src/timeline/timelines.rs index 751643cd..86ac113f 100644 --- a/src/timeline/timelines.rs +++ b/src/timeline/timelines.rs @@ -1,11 +1,18 @@ -use crate::detections::message::{LOGONSUMMARY_FLAG, METRICS_FLAG}; +use std::fs::File; +use std::io::BufWriter; + +use crate::detections::message::{AlertMessage, CH_CONFIG, LOGONSUMMARY_FLAG, METRICS_FLAG}; use crate::detections::{configs::CONFIG, detection::EvtxRecordInfo}; -use prettytable::{Cell, Row, Table}; +use comfy_table::modifiers::UTF8_ROUND_CORNERS; +use comfy_table::presets::UTF8_FULL; +use comfy_table::*; +use csv::WriterBuilder; +use downcast_rs::__std::process; use super::metrics::EventMetrics; use hashbrown::HashMap; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Timeline { pub stats: EventMetrics, } @@ -41,27 +48,62 @@ impl Timeline { } // 出力メッセージ作成 let mut sammsges: Vec = Vec::new(); - sammsges.push("---------------------------------------".to_string()); - sammsges.push(format!("Evtx File Path: {}", self.stats.filepath)); - sammsges.push(format!("Total Event Records: {}\n", self.stats.total)); - sammsges.push(format!("First Timestamp: {}", self.stats.start_time)); - sammsges.push(format!("Last Timestamp: {}\n", self.stats.end_time)); - sammsges.push("Count (Percent)\tID\tEvent\t".to_string()); - sammsges.push("--------------- ------- ---------------".to_string()); + 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); + let stats_msges: Vec> = self.tm_stats_set_msg(mapsorted); for msgprint in sammsges.iter() { println!("{}", msgprint); } - for msgprint in stats_msges.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) { @@ -70,12 +112,15 @@ impl Timeline { } // 出力メッセージ作成 let mut sammsges: Vec = Vec::new(); - sammsges.push("---------------------------------------".to_string()); - sammsges.push(format!("Evtx File Path: {}", self.stats.filepath)); - sammsges.push(format!("Total Event Records: {}\n", self.stats.total)); - sammsges.push(format!("First Timestamp: {}", self.stats.start_time)); - sammsges.push(format!("Last Timestamp: {}\n", self.stats.end_time)); - sammsges.push("---------------------------------------".to_string()); + 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); } @@ -84,10 +129,13 @@ impl Timeline { } // イベントID毎の出力メッセージ生成 - fn tm_stats_set_msg(&self, mapsorted: Vec<(&std::string::String, &usize)>) -> Vec { - let mut msges: Vec = Vec::new(); + 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, event_cnt) in mapsorted.iter() { + for ((event_id, channel), event_cnt) in mapsorted.iter() { // 件数の割合を算出 let rate: f32 = **event_cnt as f32 / self.stats.total as f32; @@ -96,40 +144,44 @@ impl Timeline { .read() .unwrap() .event_timeline_config - .get_event_id(*event_id) + .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 { - // 出力メッセージ1行作成 - msges.push(format!( - "{0} ({1:.1}%)\t{2}\t{3}", - event_cnt, - (rate * 1000.0).round() / 10.0, - event_id, - &CONFIG + 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) + .get_event_id(event_id) .unwrap() - .evttitle, - )); + .evttitle + .to_string(), + ]); } else { - // 出力メッセージ1行作成 - msges.push(format!( - "{0} ({1:.1}%)\t{2}\t{3}", - event_cnt, - (rate * 1000.0).round() / 10.0, - event_id, - "Unknown", - )); + msges.push(vec![ + event_cnt.to_string(), + format!("{:.1}%", (rate * 1000.0).round() / 10.0), + ch, + event_id.replace('\"', ""), + "Unknown".to_string(), + ]); } } - - msges.push("---------------------------------------".to_string()); msges } - // ユーザ毎のログイン統計情報出力メッセージ生成 + + /// ユーザ毎のログイン統計情報出力メッセージ生成 fn tm_loginstats_tb_set_msg(&self) { println!("Logon Summary"); if self.stats.stats_login_list.is_empty() { @@ -141,25 +193,47 @@ impl Timeline { 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.set_titles(row!["User", "Failed", "Successful"]); + 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(); - //key.to_string().retain(|c| c != '\"'); - //key.to_string().pop(); username.pop(); username.remove(0); - logins_stats_tb.add_row(Row::new(vec![ - Cell::new(&username), - Cell::new(&values[1].to_string()), - Cell::new(&values[0].to_string()), - ])); + 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); } - logins_stats_tb.printstd(); + println!("{logins_stats_tb}"); println!(); } }