diff --git a/CHANGELOG-Japanese.md b/CHANGELOG-Japanese.md index a083c39e..8679b1e7 100644 --- a/CHANGELOG-Japanese.md +++ b/CHANGELOG-Japanese.md @@ -10,10 +10,14 @@ - 結果概要に各レベルで検知した上位5つのルールを表示するようにした。 (#667) (@hitenkoku) - 結果概要を出力しないようにするために `--no-summary` オプションを追加した。 (#672) (@hitenkoku) +- 結果概要の表示を短縮させた。 (#675 #678) (@hitenkoku) +- channel_abbreviations.txtによるChannelフィールドのチェックを大文字小文字の区別をなくした。 (#685) (@hitenkoku) **バグ修正:** - ログオン情報の要約オプションを追加した場合に、Hayabusaがクラッシュしていたのを修正した。 (#674) (@hitenkoku) +- configオプションで指定したルールコンフィグの読み込みができていない問題を修正した。 (#681) (@hitenkoku) +- 結果概要のtotal eventsで読み込んだレコード数が出力されていたのを、検査対象にしているevtxファイルの実際のレコード数に修正した。 (#683) (@hitenkoku) ## v1.5.1 [2022/08/20] diff --git a/CHANGELOG.md b/CHANGELOG.md index 50893369..7386af87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,10 +10,14 @@ - Added top alerts to results summary. (#667) (@hitenkoku) - Added `--no-summary` option to not display the results summary. (#672) (@hitenkoku) +- Made the results summary more compact. (#675 #678) (@hitenkoku) +- Made Channel field in channel_abbreviations.txt case-insensitive. (#685) (@hitenkoku) **Bug Fixes:** - Hayabusa would crash with `-L` option (logon summary option). (#674) (@hitenkoku) +- Hayabusa would continue to scan without the correct config files but now will print and error and gracefully terminate. (#681) (@hitenkoku) +- Fixed total events from the number of scanned events to actual events in evtx. (#683) (@hitenkoku) ## v1.5.1 [2022/08/20] diff --git a/Cargo.lock b/Cargo.lock index fce05778..88be0569 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -254,6 +254,18 @@ dependencies = [ "os_str_bytes", ] +[[package]] +name = "comfy-table" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85914173c2f558d61613bfbbf1911f14e630895087a7ed2fafc0f5319e1536e7" +dependencies = [ + "crossterm", + "strum", + "strum_macros", + "unicode-width", +] + [[package]] name = "console" version = "0.15.1" @@ -334,6 +346,31 @@ dependencies = [ "once_cell", ] +[[package]] +name = "crossterm" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +dependencies = [ + "bitflags", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c" +dependencies = [ + "winapi", +] + [[package]] name = "csv" version = "1.1.6" @@ -533,7 +570,7 @@ dependencies = [ "indoc", "jemallocator", "log", - "quick-xml", + "quick-xml 0.23.0", "rayon", "rpmalloc", "serde", @@ -692,6 +729,7 @@ dependencies = [ "bytesize", "chrono", "clap 3.2.17", + "comfy-table", "crossbeam-utils", "csv", "dashmap", @@ -714,7 +752,7 @@ dependencies = [ "openssl", "pbr", "prettytable-rs", - "quick-xml", + "quick-xml 0.24.0", "rand", "regex", "serde", @@ -1034,9 +1072,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "lock_api" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +checksum = "9f80bf5aacaf25cbfc8210d1cfb718f2bf3b11c4c54e5afe36c236853a8ec390" dependencies = [ "autocfg", "scopeguard", @@ -1377,6 +1415,15 @@ name = "quick-xml" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9279fbdacaad3baf559d8cabe0acc3d06e30ea14931af31af79578ac0946decc" +dependencies = [ + "memchr", +] + +[[package]] +name = "quick-xml" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678404d55890514fa1c01fe98cf280b674db93944fdcb70310dd3be1d0d63be7" dependencies = [ "memchr", "serde", @@ -1579,18 +1626,18 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.143" +version = "1.0.144" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e8e5d5b70924f74ff5c6d64d9a5acd91422117c60f48c4e07855238a254553" +checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.143" +version = "1.0.144" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3d8e8de557aee63c26b85b947f5e59b690d0454c753f3adeb5cd7835ab88391" +checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00" dependencies = [ "proc-macro2", "quote", @@ -1599,9 +1646,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.83" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38dd04e3c8279e75b31ef29dbdceebfe5ad89f4d0937213c53f7d49d01b3d5a7" +checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" dependencies = [ "itoa 1.0.3", "ryu", @@ -1623,6 +1670,27 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" +[[package]] +name = "signal-hook" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a253b5e89e2698464fc26b545c9edceb338e18a89effeeecfea192c3025be29d" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.0" @@ -1666,9 +1734,9 @@ checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" [[package]] name = "socket2" -version = "0.4.4" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" +checksum = "10c98bba371b9b22a71a9414e420f92ddeb2369239af08200816169d5e2dd7aa" dependencies = [ "libc", "winapi", @@ -1750,6 +1818,25 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strum" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" + +[[package]] +name = "strum_macros" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "syn" version = "1.0.99" diff --git a/Cargo.toml b/Cargo.toml index 8e1b7674..bbe08346 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ hyper = "0.14.*" lock_api = "0.4.*" crossbeam-utils = "0.8.*" num-format = "*" +comfy-table = "6.*" [build-dependencies] static_vcruntime = "2.*" diff --git a/rules b/rules index c5110b27..ff5732fa 160000 --- a/rules +++ b/rules @@ -1 +1 @@ -Subproject commit c5110b27f6f9f788f17ac29fc2251d95e676688d +Subproject commit ff5732fa1788b1c2281fdc3ccaa0dd0301b030d8 diff --git a/screenshots/HayabusaResultsSummary.png b/screenshots/HayabusaResultsSummary.png index 1efd8ec9..0d91938b 100644 Binary files a/screenshots/HayabusaResultsSummary.png and b/screenshots/HayabusaResultsSummary.png differ diff --git a/src/afterfact.rs b/src/afterfact.rs index 00e3a65d..cb4e5aec 100644 --- a/src/afterfact.rs +++ b/src/afterfact.rs @@ -9,12 +9,15 @@ use bytesize::ByteSize; use chrono::{DateTime, Local, TimeZone, Utc}; use core::cmp::max; use csv::{QuoteStyle, WriterBuilder}; +use comfy_table::modifiers::UTF8_ROUND_CORNERS; +use comfy_table::presets::UTF8_FULL; use itertools::Itertools; use krapslog::{build_sparkline, build_time_markers}; use lazy_static::lazy_static; use linked_hash_map::LinkedHashMap; use std::str::FromStr; +use comfy_table::*; use hashbrown::{HashMap, HashSet}; use num_format::{Locale, ToFormattedString}; use std::cmp::min; @@ -31,17 +34,27 @@ use termcolor::{BufferWriter, Color, ColorChoice, ColorSpec, WriteColor}; use terminal_size::Width; lazy_static! { - pub static ref OUTPUT_COLOR: HashMap = set_output_color(); + pub static ref OUTPUT_COLOR: HashMap = set_output_color(); +} + +pub struct Colors { + pub output_color: termcolor::Color, + pub table_color: comfy_table::Color, } /// level_color.txtファイルを読み込み対応する文字色のマッピングを返却する関数 -pub fn set_output_color() -> HashMap { +pub fn set_output_color() -> HashMap { let read_result = utils::read_csv( - utils::check_setting_path(&CURRENT_EXE_PATH.to_path_buf(), "config/level_color.txt") - .to_str() - .unwrap(), + utils::check_setting_path( + &CURRENT_EXE_PATH.to_path_buf(), + "config/level_color.txt", + true, + ) + .unwrap() + .to_str() + .unwrap(), ); - let mut color_map: HashMap = HashMap::new(); + let mut color_map: HashMap = HashMap::new(); if configs::CONFIG.read().unwrap().args.no_color { return color_map; } @@ -71,16 +84,34 @@ pub fn set_output_color() -> HashMap { } color_map.insert( level.to_lowercase(), - Color::Rgb(color_code[0], color_code[1], color_code[2]), + Colors { + output_color: termcolor::Color::Rgb(color_code[0], color_code[1], color_code[2]), + table_color: comfy_table::Color::Rgb { + r: color_code[0], + g: color_code[1], + b: color_code[2], + }, + }, ); }); color_map } -fn _get_output_color(color_map: &HashMap, level: &str) -> Option { +fn _get_output_color(color_map: &HashMap, level: &str) -> Option { let mut color = None; if let Some(c) = color_map.get(&level.to_lowercase()) { - color = Some(c.to_owned()); + color = Some(c.output_color.to_owned()); + } + color +} + +fn _get_table_color( + color_map: &HashMap, + level: &str, +) -> Option { + let mut color = None; + if let Some(c) = color_map.get(&level.to_lowercase()) { + color = Some(c.table_color.to_owned()); } color } @@ -174,7 +205,7 @@ pub fn after_fact(all_record_cnt: usize) { fn emit_csv( writer: &mut W, displayflag: bool, - color_map: HashMap, + color_map: HashMap, all_record_cnt: u128, profile: LinkedHashMap, ) -> io::Result<()> { @@ -396,17 +427,9 @@ fn emit_csv( &disp_wtr, get_writable_color(None), &format!( - "Total events: {}", - all_record_cnt.to_formatted_string(&Locale::en) - ), - true, - ) - .ok(); - write_color_buffer( - &disp_wtr, - get_writable_color(None), - &format!( - "Data reduction: {} events ({:.2}%)", + "Saved alerts and events / Total events analyzed: {} / {} (Data reduction: {} events ({:.2}%))", + (all_record_cnt - reducted_record_cnt).to_formatted_string(&Locale::en), + all_record_cnt.to_formatted_string(&Locale::en), reducted_record_cnt.to_formatted_string(&Locale::en), reducted_percent ), @@ -417,15 +440,8 @@ fn emit_csv( _print_unique_results( total_detect_counts_by_level, - "Total".to_string(), - "detections".to_string(), - &color_map, - ); - println!(); - - _print_unique_results( unique_detect_counts_by_level, - "Unique".to_string(), + "Total | Unique".to_string(), "detections".to_string(), &color_map, ); @@ -433,11 +449,13 @@ fn emit_csv( _print_detection_summary_by_date(detect_counts_by_date_and_level, &color_map); println!(); + println!(); _print_detection_summary_by_computer(detect_counts_by_computer_and_level, &color_map); println!(); - _print_detection_summary_by_rule(detect_counts_by_rule_and_level, &color_map); + _print_detection_summary_tables(detect_counts_by_rule_and_level, &color_map); + println!(); } Ok(()) @@ -491,26 +509,30 @@ fn _format_cellpos(colval: &str, column: ColPos) -> String { } } -/// output info which unique detection count and all detection count information(devided by level and total) to stdout. +/// output info which unique detection count and all detection count information(separated by level and total) to stdout. fn _print_unique_results( mut counts_by_level: Vec, + mut unique_counts_by_level: Vec, head_word: String, tail_word: String, - color_map: &HashMap, + color_map: &HashMap, ) { // the order in which are registered and the order of levels to be displayed are reversed counts_by_level.reverse(); + unique_counts_by_level.reverse(); let total_count = counts_by_level.iter().sum::(); + let unique_total_count = unique_counts_by_level.iter().sum::(); // output total results write_color_buffer( &BufferWriter::stdout(ColorChoice::Always), None, &format!( - "{} {}: {}", + "{} {}: {} | {}", head_word, tail_word, total_count.to_formatted_string(&Locale::en), + unique_total_count.to_formatted_string(&Locale::en) ), true, ) @@ -525,13 +547,20 @@ fn _print_unique_results( } else { (counts_by_level[i] as f64) / (total_count as f64) * 100.0 }; + let unique_percent = if unique_total_count == 0 { + 0 as f64 + } else { + (unique_counts_by_level[i] as f64) / (unique_total_count as f64) * 100.0 + }; let output_raw_str = format!( - "{} {} {}: {} ({:.2}%)", + "{} {} {}: {} ({:.2}%) | {} ({:.2}%)", head_word, level_name, tail_word, counts_by_level[i].to_formatted_string(&Locale::en), - percent + percent, + unique_counts_by_level[i].to_formatted_string(&Locale::en), + unique_percent ); write_color_buffer( &BufferWriter::stdout(ColorChoice::Always), @@ -546,13 +575,15 @@ fn _print_unique_results( /// 各レベル毎で最も高い検知数を出した日付を出力する fn _print_detection_summary_by_date( detect_counts_by_date: HashMap>, - color_map: &HashMap, + color_map: &HashMap, ) { let buf_wtr = BufferWriter::stdout(ColorChoice::Always); let mut wtr = buf_wtr.buffer(); wtr.set_color(ColorSpec::new().set_fg(None)).ok(); - for level in LEVEL_ABBR.values() { + writeln!(wtr, "Dates with most total detections:").ok(); + + for (idx, level) in LEVEL_ABBR.values().enumerate() { // output_levelsはlevelsからundefinedを除外した配列であり、各要素は必ず初期化されているのでSomeであることが保証されているのでunwrapをそのまま実施 let detections_by_day = detect_counts_by_date.get(level).unwrap(); let mut max_detect_str = String::default(); @@ -573,13 +604,18 @@ fn _print_detection_summary_by_date( if !exist_max_data { max_detect_str = "n/a".to_string(); } - writeln!( + write!( wtr, - "Date with most total {} detections: {}", + "{}: {}", LEVEL_FULL.get(level.as_str()).unwrap(), &max_detect_str ) .ok(); + if idx != LEVEL_ABBR.len() - 1 { + wtr.set_color(ColorSpec::new().set_fg(None)).ok(); + + write!(wtr, ", ").ok(); + } } buf_wtr.print(&wtr).ok(); } @@ -587,12 +623,13 @@ fn _print_detection_summary_by_date( /// 各レベル毎で最も高い検知数を出した日付を出力する fn _print_detection_summary_by_computer( detect_counts_by_computer: HashMap>, - color_map: &HashMap, + color_map: &HashMap, ) { let buf_wtr = BufferWriter::stdout(ColorChoice::Always); let mut wtr = buf_wtr.buffer(); wtr.set_color(ColorSpec::new().set_fg(None)).ok(); + writeln!(wtr, "Top 5 computers with most unique detections:").ok(); for level in LEVEL_ABBR.values() { // output_levelsはlevelsからundefinedを除外した配列であり、各要素は必ず初期化されているのでSomeであることが保証されているのでunwrapをそのまま実施 let detections_by_computer = detect_counts_by_computer.get(level).unwrap(); @@ -625,7 +662,7 @@ fn _print_detection_summary_by_computer( .ok(); writeln!( wtr, - "Top 5 computers with most unique {} detections: {}", + "{}: {}", LEVEL_FULL.get(level.as_str()).unwrap(), &result_str ) @@ -634,53 +671,94 @@ fn _print_detection_summary_by_computer( buf_wtr.print(&wtr).ok(); } -/// 各レベルごとで検出数が多かったルールのタイトルを出力する関数 -fn _print_detection_summary_by_rule( +/// 各レベルごとで検出数が多かったルールを表形式で出力する関数 +fn _print_detection_summary_tables( detect_counts_by_rule_and_level: HashMap>, - color_map: &HashMap, + color_map: &HashMap, ) { let buf_wtr = BufferWriter::stdout(ColorChoice::Always); let mut wtr = buf_wtr.buffer(); wtr.set_color(ColorSpec::new().set_fg(None)).ok(); - let level_cnt = detect_counts_by_rule_and_level.len(); - for (idx, level) in LEVEL_ABBR.values().enumerate() { + let mut output = vec![]; + let mut col_color = vec![]; + for level in LEVEL_ABBR.values() { + let mut col_output: Vec = vec![]; + col_output.push(format!( + "Top {} alerts:", + LEVEL_FULL.get(level.as_str()).unwrap() + )); + + col_color.push(_get_table_color( + color_map, + LEVEL_FULL.get(level.as_str()).unwrap(), + )); + // output_levelsはlevelsからundefinedを除外した配列であり、各要素は必ず初期化されているのでSomeであることが保証されているのでunwrapをそのまま実施 let detections_by_computer = detect_counts_by_rule_and_level.get(level).unwrap(); - let mut result_vec: Vec = Vec::new(); let mut sorted_detections: Vec<(&String, &i128)> = detections_by_computer.iter().collect(); sorted_detections.sort_by(|a, b| (-a.1).cmp(&(-b.1))); - for x in sorted_detections.iter().take(5) { - result_vec.push(format!( + let take_cnt = + if LEVEL_FULL.get(level.as_str()).unwrap_or(&"-".to_string()) == "informational" { + 10 + } else { + 5 + }; + for x in sorted_detections.iter().take(take_cnt) { + col_output.push(format!( "{} ({})", x.0, x.1.to_formatted_string(&Locale::en) )); } - let result_str = if result_vec.is_empty() { - "None".to_string() + let na_cnt = if sorted_detections.len() > take_cnt { + 0 } else { - result_vec.join("\n") + take_cnt - sorted_detections.len() }; - - wtr.set_color(ColorSpec::new().set_fg(_get_output_color( - color_map, - LEVEL_FULL.get(level.as_str()).unwrap(), - ))) - .ok(); - writeln!( - wtr, - "Top {} alerts:\n{}", - LEVEL_FULL.get(level.as_str()).unwrap(), - &result_str - ) - .ok(); - if idx != level_cnt - 1 { - writeln!(wtr).ok(); + for _x in 0..na_cnt { + col_output.push("n/a".to_string()); } + output.push(col_output); } - buf_wtr.print(&wtr).ok(); + + let mut tb = Table::new(); + tb.load_preset(UTF8_FULL) + .apply_modifier(UTF8_ROUND_CORNERS) + .set_style(TableComponent::VerticalLines, ' '); + for x in 0..output.len() / 2 { + let hlch = tb.style(TableComponent::HorizontalLines).unwrap(); + let tbch = tb.style(TableComponent::TopBorder).unwrap(); + + tb.add_row(vec![ + Cell::new(&output[2 * x][0]).fg(col_color[2 * x].unwrap_or(comfy_table::Color::Reset)), + Cell::new(&output[2 * x + 1][0]) + .fg(col_color[2 * x + 1].unwrap_or(comfy_table::Color::Reset)), + ]) + .set_style(TableComponent::MiddleIntersections, hlch) + .set_style(TableComponent::TopBorderIntersections, tbch) + .set_style(TableComponent::BottomBorderIntersections, hlch); + + tb.add_row(vec![ + Cell::new(&output[2 * x][1..].join("\n")) + .fg(col_color[2 * x].unwrap_or(comfy_table::Color::Reset)), + Cell::new(&output[2 * x + 1][1..].join("\n")) + .fg(col_color[2 * x + 1].unwrap_or(comfy_table::Color::Reset)), + ]); + } + + let odd_row = &output[4][1..6]; + let even_row = &output[4][6..11]; + tb.add_row(vec![ + Cell::new(&output[4][0]).fg(col_color[4].unwrap_or(comfy_table::Color::Reset)), + Cell::new(""), + ]); + tb.add_row(vec![ + Cell::new(odd_row.join("\n")).fg(col_color[4].unwrap_or(comfy_table::Color::Reset)), + Cell::new(even_row.join("\n")).fg(col_color[4].unwrap_or(comfy_table::Color::Reset)), + ]); + println!("{tb}"); } /// get timestamp to input datetime. @@ -947,7 +1025,7 @@ mod tests { ( "%Channel%".to_owned(), mock_ch_filter - .get("Security") + .get(&"Security".to_ascii_lowercase()) .unwrap_or(&String::default()) .to_string(), ), diff --git a/src/detections/configs.rs b/src/detections/configs.rs index cbd7c412..73392bfc 100644 --- a/src/detections/configs.rs +++ b/src/detections/configs.rs @@ -23,10 +23,23 @@ lazy_static! { levelmap.insert("CRITICAL".to_owned(), 5); levelmap }; - pub static ref EVENTKEY_ALIAS: EventKeyAliasConfig = load_eventkey_alias(&format!( - "{}/eventkey_alias.txt", - CONFIG.read().unwrap().args.config.as_path().display() - )); + pub static ref EVENTKEY_ALIAS: EventKeyAliasConfig = load_eventkey_alias( + utils::check_setting_path( + &CONFIG.read().unwrap().args.config, + "eventkey_alias.txt", + false + ) + .unwrap_or_else(|| { + utils::check_setting_path( + &CURRENT_EXE_PATH.to_path_buf(), + "rules/config/eventkey_alias.txt", + true, + ) + .unwrap() + }) + .to_str() + .unwrap() + ); pub static ref IDS_REGEX: Regex = Regex::new(r"^[0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12}$").unwrap(); pub static ref TERM_SIZE: Option<(Width, Height)> = terminal_size(); @@ -52,7 +65,7 @@ impl Default for ConfigReader<'_> { } } -#[derive(Parser)] +#[derive(Parser, Clone)] #[clap( name = "Hayabusa", usage = "hayabusa.exe [OTHER-ACTIONS] [OPTIONS]", @@ -246,23 +259,33 @@ impl ConfigReader<'_> { .help_template("\n\nUSAGE:\n {usage}\n\nOPTIONS:\n{options}"); ConfigReader { app: build_cmd, - args: parse, + args: parse.clone(), headless_help: String::default(), event_timeline_config: load_eventcode_info( - utils::check_setting_path( - &CURRENT_EXE_PATH.to_path_buf(), - "rules/config/statistics_event_info.txt", - ) - .to_str() - .unwrap(), + utils::check_setting_path(&parse.config, "statistics_event_info.txt", false) + .unwrap_or_else(|| { + utils::check_setting_path( + &CURRENT_EXE_PATH.to_path_buf(), + "rules/config/statistics_event_info.txt", + true, + ) + .unwrap() + }) + .to_str() + .unwrap(), ), target_eventids: load_target_ids( - utils::check_setting_path( - &CURRENT_EXE_PATH.to_path_buf(), - "rules/config/target_event_IDs.txt", - ) - .to_str() - .unwrap(), + utils::check_setting_path(&parse.config, "target_event_IDs.txt", false) + .unwrap_or_else(|| { + utils::check_setting_path( + &CURRENT_EXE_PATH.to_path_buf(), + "rules/config/target_event_IDs.txt", + true, + ) + .unwrap() + }) + .to_str() + .unwrap(), ), } } diff --git a/src/detections/detection.rs b/src/detections/detection.rs index 2f4e6207..75e801de 100644 --- a/src/detections/detection.rs +++ b/src/detections/detection.rs @@ -264,7 +264,10 @@ impl Detection { "%Channel%" => { profile_converter.insert( "%Channel%".to_string(), - CH_CONFIG.get(ch_str).unwrap_or(ch_str).to_string(), + CH_CONFIG + .get(&ch_str.to_ascii_lowercase()) + .unwrap_or(ch_str) + .to_string(), ); } "%Level%" => { diff --git a/src/detections/message.rs b/src/detections/message.rs index 9aff48c5..fa374282 100644 --- a/src/detections/message.rs +++ b/src/detections/message.rs @@ -49,30 +49,32 @@ lazy_static! { pub static ref STATISTICS_FLAG: bool = configs::CONFIG.read().unwrap().args.statistics; pub static ref LOGONSUMMARY_FLAG: bool = configs::CONFIG.read().unwrap().args.logon_summary; pub static ref TAGS_CONFIG: HashMap = create_output_filter_config( - utils::check_setting_path(&CURRENT_EXE_PATH.to_path_buf(), "config/mitre_tactics.txt") - .to_str() + utils::check_setting_path(&CURRENT_EXE_PATH.to_path_buf(), "config/mitre_tactics.txt", true) + .unwrap().to_str() .unwrap(), ); pub static ref CH_CONFIG: HashMap = create_output_filter_config( - utils::check_setting_path( - &CURRENT_EXE_PATH.to_path_buf(), - "rules/config/channel_abbreviations.txt" - ) + utils::check_setting_path(&configs::CONFIG.read().unwrap().args.config, "channel_abbreviations.txt", false).unwrap_or_else(|| { + utils::check_setting_path( + &CURRENT_EXE_PATH.to_path_buf(), + "rules/config/channel_abbreviations.txt", true + ).unwrap() + }) .to_str() .unwrap(), ); pub static ref PIVOT_KEYWORD_LIST_FLAG: bool = configs::CONFIG.read().unwrap().args.pivot_keywords_list; - pub static ref DEFAULT_DETAILS: HashMap = get_default_details(&format!( - "{}/default_details.txt", - configs::CONFIG - .read() - .unwrap() - .args - .config - .as_path() - .display() - )); + pub static ref DEFAULT_DETAILS: HashMap = get_default_details( + utils::check_setting_path(&configs::CONFIG.read().unwrap().args.config, "default_details.txt", false).unwrap_or_else(|| { + utils::check_setting_path( + &CURRENT_EXE_PATH.to_path_buf(), + "rules/config/default_details.txt", true + ).unwrap() + }) + .to_str() + .unwrap() + ); pub static ref LEVEL_ABBR: LinkedHashMap = LinkedHashMap::from_iter([ ("critical".to_string(), "crit".to_string()), ("high".to_string(), "high".to_string()), @@ -103,10 +105,10 @@ pub fn create_output_filter_config(path: &str) -> HashMap { return; } - let tag_full_str = line[0].trim(); + let tag_full_str = line[0].trim().to_ascii_lowercase(); let tag_replace_str = line[1].trim(); - ret.insert(tag_full_str.to_owned(), tag_replace_str.to_owned()); + ret.insert(tag_full_str, tag_replace_str.to_owned()); }); ret } @@ -597,7 +599,7 @@ mod tests { let actual = create_output_filter_config("test_files/config/channel_abbreviations.txt"); let actual2 = create_output_filter_config("test_files/config/channel_abbreviations.txt"); let expected: HashMap = HashMap::from([ - ("Security".to_string(), "Sec".to_string()), + ("Security".to_ascii_lowercase(), "Sec".to_string()), ("xxx".to_string(), "yyy".to_string()), ]); _check_hashmap_element(&expected, actual); diff --git a/src/detections/utils.rs b/src/detections/utils.rs index d678bb07..f7ee3a14 100644 --- a/src/detections/utils.rs +++ b/src/detections/utils.rs @@ -73,7 +73,8 @@ pub fn value_to_string(value: &Value) -> Option { pub fn read_txt(filename: &str) -> Result, String> { let filepath = if filename.starts_with("./") { - check_setting_path(&CURRENT_EXE_PATH.to_path_buf(), filename) + check_setting_path(&CURRENT_EXE_PATH.to_path_buf(), filename, true) + .unwrap() .to_str() .unwrap() .to_string() @@ -380,14 +381,59 @@ pub fn make_ascii_titlecase(s: &mut str) -> String { } /// base_path/path が存在するかを確認し、存在しなければカレントディレクトリを参照するpathを返す関数 -pub fn check_setting_path(base_path: &Path, path: &str) -> PathBuf { +pub fn check_setting_path(base_path: &Path, path: &str, ignore_err: bool) -> Option { if base_path.join(path).exists() { - base_path.join(path) + Some(base_path.join(path)) + } else if ignore_err { + Some(Path::new(path).to_path_buf()) } else { - Path::new(path).to_path_buf() + None } } +/// rule configのファイルの所在を確認する関数。 +pub fn check_rule_config() -> Result<(), String> { + // rules/configのフォルダが存在するかを確認する + let exist_rule_config_folder = + if configs::CONFIG.read().unwrap().args.config == CURRENT_EXE_PATH.to_path_buf() { + check_setting_path( + &configs::CONFIG.read().unwrap().args.config, + "rules/config", + false, + ) + .is_some() + } else { + check_setting_path(&configs::CONFIG.read().unwrap().args.config, "", false).is_some() + }; + if !exist_rule_config_folder { + return Err("The required rules config files were not found. Please download them with --update-rules".to_string()); + } + + // 各種ファイルを確認する + let files = vec![ + "channel_abbreviations.txt", + "target_event_IDs.txt", + "default_details.txt", + "level_tuning.txt", + "statistics_event_info.txt", + "eventkey_alias.txt", + ]; + let mut not_exist_file = vec![]; + for file in &files { + if check_setting_path(&configs::CONFIG.read().unwrap().args.config, file, false).is_none() { + not_exist_file.push(*file); + } + } + + if !not_exist_file.is_empty() { + return Err(format!( + "Could not find the following config files: {}\nPlease specify a correct rules config directory.\n", + not_exist_file.join(", ") + )); + } + Ok(()) +} + ///タイムゾーンに合わせた情報を情報を取得する関数 pub fn format_time(time: &DateTime, date_only: bool) -> String { if configs::CONFIG.read().unwrap().args.utc { @@ -613,23 +659,31 @@ mod tests { let exist_path = Path::new("./test_files").to_path_buf(); let not_exist_path = Path::new("not_exist_path").to_path_buf(); assert_eq!( - check_setting_path(¬_exist_path, "rules") + check_setting_path(¬_exist_path, "rules", true) + .unwrap() .to_str() .unwrap(), "rules" ); assert_eq!( - check_setting_path(¬_exist_path, "fake") + check_setting_path(¬_exist_path, "fake", true) + .unwrap() .to_str() .unwrap(), "fake" ); assert_eq!( - check_setting_path(&exist_path, "rules").to_str().unwrap(), + check_setting_path(&exist_path, "rules", true) + .unwrap() + .to_str() + .unwrap(), exist_path.join("rules").to_str().unwrap() ); assert_eq!( - check_setting_path(&exist_path, "fake").to_str().unwrap(), + check_setting_path(&exist_path, "fake", true) + .unwrap() + .to_str() + .unwrap(), "fake" ); } diff --git a/src/main.rs b/src/main.rs index 539b8973..5da1b162 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,8 +7,8 @@ use bytesize::ByteSize; use chrono::{DateTime, Datelike, Local}; use evtx::{EvtxParser, ParserSettings}; use hashbrown::{HashMap, HashSet}; -use hayabusa::detections::configs::CURRENT_EXE_PATH; use hayabusa::detections::configs::{load_pivot_keywords, TargetEventTime, TARGET_EXTENSIONS}; +use hayabusa::detections::configs::{CONFIG, CURRENT_EXE_PATH}; use hayabusa::detections::detection::{self, EvtxRecordInfo}; use hayabusa::detections::message::{ AlertMessage, ERROR_LOG_PATH, ERROR_LOG_STACK, LOGONSUMMARY_FLAG, PIVOT_KEYWORD_LIST_FLAG, @@ -80,7 +80,9 @@ impl App { utils::check_setting_path( &CURRENT_EXE_PATH.to_path_buf(), "config/pivot_keywords.txt", + true, ) + .unwrap() .to_str() .unwrap(), ); @@ -148,13 +150,19 @@ impl App { // カレントディレクトリ以外からの実行の際にrules-configオプションの指定がないとエラーが発生することを防ぐための処理 if configs::CONFIG.read().unwrap().args.config == Path::new("./rules/config") { configs::CONFIG.write().unwrap().args.config = - utils::check_setting_path(&CURRENT_EXE_PATH.to_path_buf(), "./rules/config"); + utils::check_setting_path(&CURRENT_EXE_PATH.to_path_buf(), "rules/config", true) + .unwrap(); } // カレントディレクトリ以外からの実行の際にrulesオプションの指定がないとエラーが発生することを防ぐための処理 if configs::CONFIG.read().unwrap().args.rules == Path::new("./rules") { configs::CONFIG.write().unwrap().args.rules = - utils::check_setting_path(&CURRENT_EXE_PATH.to_path_buf(), "./rules"); + utils::check_setting_path(&CURRENT_EXE_PATH.to_path_buf(), "rules", true).unwrap(); + } + // rule configのフォルダ、ファイルを確認してエラーがあった場合は終了とする + if let Err(e) = utils::check_rule_config() { + AlertMessage::alert(&e).ok(); + return; } if let Some(csv_path) = &configs::CONFIG.read().unwrap().args.output { @@ -264,9 +272,18 @@ impl App { let level_tuning_config_path = match level_tuning_val { Some(path) => path.to_owned(), _ => utils::check_setting_path( - &CURRENT_EXE_PATH.to_path_buf(), - "./rules/config/level_tuning.txt", + &CONFIG.read().unwrap().args.config, + "level_tuning.txt", + false, ) + .unwrap_or_else(|| { + utils::check_setting_path( + &CURRENT_EXE_PATH.to_path_buf(), + "rules/config/level_tuning.txt", + true, + ) + .unwrap() + }) .display() .to_string(), }; @@ -469,10 +486,10 @@ impl App { } fn print_contributors(&self) { - match fs::read_to_string(utils::check_setting_path( - &CURRENT_EXE_PATH.to_path_buf(), - "contributors.txt", - )) { + match fs::read_to_string( + utils::check_setting_path(&CURRENT_EXE_PATH.to_path_buf(), "contributors.txt", true) + .unwrap(), + ) { Ok(contents) => { write_color_buffer( &BufferWriter::stdout(ColorChoice::Always), @@ -570,6 +587,7 @@ impl App { let mut tl = Timeline::new(); let mut parser = parser.unwrap(); let mut records = parser.records_json_value(); + loop { let mut records_per_detect = vec![]; while records_per_detect.len() < MAX_DETECT_RECORDS { @@ -578,6 +596,7 @@ impl App { if next_rec.is_none() { break; } + record_cnt += 1; let record_result = next_rec.unwrap(); if record_result.is_err() { @@ -619,8 +638,6 @@ impl App { break; } - record_cnt += records_per_detect.len(); - let records_per_detect = self.rt.block_on(App::create_rec_infos( records_per_detect, &path, @@ -721,7 +738,8 @@ impl App { /// output logo fn output_logo(&self) { - let fp = utils::check_setting_path(&CURRENT_EXE_PATH.to_path_buf(), "art/logo.txt"); + let fp = utils::check_setting_path(&CURRENT_EXE_PATH.to_path_buf(), "art/logo.txt", true) + .unwrap(); let content = fs::read_to_string(fp).unwrap_or_default(); let output_color = if configs::CONFIG.read().unwrap().args.no_color { None @@ -748,7 +766,8 @@ impl App { match eggs.get(exec_datestr) { None => {} Some(path) => { - let egg_path = utils::check_setting_path(&CURRENT_EXE_PATH.to_path_buf(), path); + let egg_path = + utils::check_setting_path(&CURRENT_EXE_PATH.to_path_buf(), path, true).unwrap(); let content = fs::read_to_string(egg_path).unwrap_or_default(); write_color_buffer( &BufferWriter::stdout(ColorChoice::Always), diff --git a/src/options/profile.rs b/src/options/profile.rs index 70e0e9cf..7483c26f 100644 --- a/src/options/profile.rs +++ b/src/options/profile.rs @@ -15,13 +15,20 @@ lazy_static! { pub static ref PROFILES: Option> = load_profile( check_setting_path( &CURRENT_EXE_PATH.to_path_buf(), - "config/default_profile.yaml" + "config/default_profile.yaml", + true ) + .unwrap() .to_str() .unwrap(), - check_setting_path(&CURRENT_EXE_PATH.to_path_buf(), "config/profiles.yaml") - .to_str() - .unwrap() + check_setting_path( + &CURRENT_EXE_PATH.to_path_buf(), + "config/profiles.yaml", + true + ) + .unwrap() + .to_str() + .unwrap() ); pub static ref LOAEDED_PROFILE_ALIAS: HashSet = HashSet::from_iter( PROFILES