diff --git a/CHANGELOG-Japanese.md b/CHANGELOG-Japanese.md index 5c7c4db5..7486dd45 100644 --- a/CHANGELOG-Japanese.md +++ b/CHANGELOG-Japanese.md @@ -5,6 +5,7 @@ **新機能:** - ルール内の`details`で複数の`Data`レコードから特定のデータを指定して出力できるようにした。 (#487) (@hitenkoku) +- 読み込んだルールのステータス情報の要約を追加した。 (#583) (@hitenkoku) **改善:** @@ -12,6 +13,7 @@ - 新たな時刻表示のオプションとして`--US-time`、`--US-military-time`、`--European-time`の3つを追加した (#574) (@hitenkoku) - `--rfc-3339` オプションの時刻表示形式を変更した。 (#574) (@hitenkoku) - `-R/ --display-record-id`オプションを`-R/ --hide-record-id`に変更。レコードIDはデフォルトで出力するようにして`-R`オプションを付けた際に表示しないように変更した。(#579) (@hitenkoku) +- ルール読み込み時のメッセージを追加した。 (#583) (@hitenkoku) **バグ修正:** diff --git a/CHANGELOG.md b/CHANGELOG.md index f4081df1..27f99e31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ **New Features:** - You can now specify specific fields when there are multiple fields with the same name (Ex: `Data`). In the `details` line in a rule, specify a placeholder like `%Data[1]%` to display the first `Data` field. (#487) (@hitenkoku) +- Added loaded rules status summary. (#583) (@hitenkoku) **Enhancements:** @@ -12,6 +13,7 @@ - Added new output time format options. (`--US-time`, `--US-military-time`, `--European-time`) (#574) (@hitenkoku) - Changed output time format when `--rfc-3339` option is enabled. (#574) (@hitenkoku) - Changed the `-R / --display-record-id` option to `-R / --hide-record-id` and now by default the event record ID is displayed. You can hide the record ID with `-R / --hide-record-id`. (#579) (@hitenkoku) +- Added rule loading message. (#583) (@hitenkoku) **Bug Fixes:** diff --git a/screenshots/Hayabusa-Startup.png b/screenshots/Hayabusa-Startup.png index 2a34db8f..72b5284b 100644 Binary files a/screenshots/Hayabusa-Startup.png and b/screenshots/Hayabusa-Startup.png differ diff --git a/src/detections/detection.rs b/src/detections/detection.rs index 04a2a236..23a38cd8 100644 --- a/src/detections/detection.rs +++ b/src/detections/detection.rs @@ -13,7 +13,7 @@ use crate::detections::print::{CH_CONFIG, IS_HIDE_RECORD_ID, TAGS_CONFIG}; use crate::detections::rule; use crate::detections::rule::AggResult; use crate::detections::rule::RuleNode; -use crate::detections::utils::get_serde_number_to_string; +use crate::detections::utils::{get_serde_number_to_string, make_ascii_titlecase}; use crate::filter; use crate::yaml::ParseYaml; use hashbrown; @@ -109,7 +109,7 @@ impl Detection { }); } parseerror_count += 1; - println!(); // 一行開けるためのprintln + println!(); }); Option::None }; @@ -126,12 +126,13 @@ impl Detection { .args .is_present("logon-summary") { + let _ = &rulefile_loader + .rule_load_cnt + .insert(String::from("rule parsing error"), parseerror_count); Detection::print_rule_load_info( &rulefile_loader.rulecounter, - &parseerror_count, - &rulefile_loader.exclude_rule_count, - &rulefile_loader.noisy_rule_count, - &rulefile_loader.deprecate_rule_count, + &rulefile_loader.rule_load_cnt, + &rulefile_loader.rule_status_cnt, ); } ret @@ -355,27 +356,49 @@ impl Detection { pub fn print_rule_load_info( rc: &HashMap, - parseerror_count: &u128, - exclude_count: &u128, - noisy_count: &u128, - deprecate_count: &u128, + ld_rc: &HashMap, + st_rc: &HashMap, ) { if *STATISTICS_FLAG { return; } - println!("Deprecated rules: {}", deprecate_count); - println!("Excluded rules: {}", exclude_count); - println!("Noisy rules: {}", noisy_count); - println!("Rule parsing errors: {}", parseerror_count); + let mut sorted_ld_rc: Vec<(&String, &u128)> = ld_rc.iter().collect(); + sorted_ld_rc.sort_by(|a, b| a.0.cmp(b.0)); + sorted_ld_rc.into_iter().for_each(|(key, value)| { + //タイトルに利用するものはascii文字であることを前提として1文字目を大文字にするように変更する + println!( + "{} rules: {}", + make_ascii_titlecase(key.clone().as_mut()), + value, + ); + }); println!(); + + let mut sorted_st_rc: Vec<(&String, &u128)> = st_rc.iter().collect(); + let total_loaded_rule_cnt: u128 = sorted_st_rc.iter().map(|(_, v)| v.to_owned()).sum(); + sorted_st_rc.sort_by(|a, b| a.0.cmp(b.0)); + sorted_st_rc.into_iter().for_each(|(key, value)| { + let rate = if value == &0_u128 { + 0 as f64 + } else { + (*value as f64) / (total_loaded_rule_cnt as f64) * 100.0 + }; + //タイトルに利用するものはascii文字であることを前提として1文字目を大文字にするように変更する + println!( + "{} rules: {} ({:.2}%)", + make_ascii_titlecase(key.clone().as_mut()), + value, + rate + ); + }); + println!(); + let mut sorted_rc: Vec<(&String, &u128)> = rc.iter().collect(); sorted_rc.sort_by(|a, b| a.0.cmp(b.0)); - let mut enable_total = 0; sorted_rc.into_iter().for_each(|(key, value)| { println!("{} rules: {}", key, value); - enable_total += value; }); - println!("Total enabled detection rules: {}", enable_total); + println!("Total enabled detection rules: {}", total_loaded_rule_cnt); println!(); } } diff --git a/src/detections/utils.rs b/src/detections/utils.rs index fa86b628..59f8f2d1 100644 --- a/src/detections/utils.rs +++ b/src/detections/utils.rs @@ -341,9 +341,26 @@ fn _collect_recordinfo<'a>( } } +/** + * 最初の文字を大文字にする関数 + */ +pub fn make_ascii_titlecase(s: &mut str) -> String { + let mut c = s.chars(); + match c.next() { + None => String::new(), + Some(f) => { + if !f.is_ascii() { + s.to_string() + } else { + f.to_uppercase().collect::() + c.as_str() + } + } + } +} + #[cfg(test)] mod tests { - use crate::detections::utils; + use crate::detections::utils::{self, make_ascii_titlecase}; use regex::Regex; use serde_json::Value; @@ -494,4 +511,15 @@ mod tests { assert!(utils::get_serde_number_to_string(&event_record["Event"]["EventData"]).is_none()); } + + #[test] + /// 文字列を与えてascii文字を大文字にするように対応する関数のテスト + fn test_make_ascii_titlecase() { + assert_eq!(make_ascii_titlecase("aaaa".to_string().as_mut()), "Aaaa"); + assert_eq!( + make_ascii_titlecase("i am Test".to_string().as_mut()), + "I am Test" + ); + assert_eq!(make_ascii_titlecase("β".to_string().as_mut()), "β"); + } } diff --git a/src/main.rs b/src/main.rs index 13c766cd..2ae395cc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -453,6 +453,8 @@ impl App { } println!("Total file size: {}", total_file_size.to_string_as(false)); println!(); + println!("Loading detections rules. Please wait."); + println!(); let rule_files = detection::Detection::parse_rule_files( level, diff --git a/src/yaml.rs b/src/yaml.rs index db853afb..97522faf 100644 --- a/src/yaml.rs +++ b/src/yaml.rs @@ -18,9 +18,8 @@ use yaml_rust::YamlLoader; pub struct ParseYaml { pub files: Vec<(String, yaml_rust::Yaml)>, pub rulecounter: HashMap, - pub exclude_rule_count: u128, - pub noisy_rule_count: u128, - pub deprecate_rule_count: u128, + pub rule_load_cnt: HashMap, + pub rule_status_cnt: HashMap, pub errorrule_count: u128, } @@ -35,9 +34,11 @@ impl ParseYaml { ParseYaml { files: Vec::new(), rulecounter: HashMap::new(), - exclude_rule_count: 0, - noisy_rule_count: 0, - deprecate_rule_count: 0, + rule_load_cnt: HashMap::from([ + ("excluded".to_string(), 0_u128), + ("noisy".to_string(), 0_u128), + ]), + rule_status_cnt: HashMap::from([("deprecated".to_string(), 0_u128)]), errorrule_count: 0, } } @@ -221,19 +222,18 @@ impl ParseYaml { //除外されたルールは無視する let rule_id = &yaml_doc["id"].as_str(); if rule_id.is_some() { - match exclude_ids + if let Some(v) = exclude_ids .no_use_rule - .get(&rule_id.unwrap_or("").to_string()) + .get(&rule_id.unwrap_or(&String::default()).to_string()) { - None => (), - Some(v) => { - if v.contains("exclude_rule") { - self.exclude_rule_count += 1; - } else { - self.noisy_rule_count += 1; - } - return Option::None; - } + let entry_key = if v.contains("exclude_rule") { + "excluded" + } else { + "noisy" + }; + let entry = self.rule_load_cnt.entry(entry_key.to_string()).or_insert(0); + *entry += 1; + return Option::None; } } @@ -245,6 +245,17 @@ impl ParseYaml { + 1, ); + let status_cnt = self + .rule_status_cnt + .entry( + yaml_doc["status"] + .as_str() + .unwrap_or("undefined") + .to_string(), + ) + .or_insert(0); + *status_cnt += 1; + if configs::CONFIG.read().unwrap().args.is_present("verbose") { println!("Loaded yml file path: {}", filepath); } @@ -269,7 +280,11 @@ impl ParseYaml { { let rule_status = &yaml_doc["status"].as_str().unwrap_or_default(); if *rule_status == "deprecated" { - self.deprecate_rule_count += 1; + let entry = self + .rule_status_cnt + .entry(rule_status.to_string()) + .or_insert(0); + *entry += 1; return Option::None; } } @@ -400,7 +415,7 @@ mod tests { let mut yaml = yaml::ParseYaml::new(); let path = Path::new("test_files/rules/yaml"); yaml.read_dir(path, "", &filter::exclude_ids()).unwrap(); - assert_eq!(yaml.exclude_rule_count, 5); + assert_eq!(yaml.rule_load_cnt.get("excluded").unwrap().to_owned(), 5); } #[test] fn test_all_noisy_rules_file() { @@ -409,7 +424,7 @@ mod tests { let mut yaml = yaml::ParseYaml::new(); let path = Path::new("test_files/rules/yaml"); yaml.read_dir(path, "", &filter::exclude_ids()).unwrap(); - assert_eq!(yaml.noisy_rule_count, 5); + assert_eq!(yaml.rule_load_cnt.get("noisy").unwrap().to_owned(), 5); } #[test] fn test_none_exclude_rules_file() { @@ -419,7 +434,7 @@ mod tests { let path = Path::new("test_files/rules/yaml"); let exclude_ids = RuleExclude::default(); yaml.read_dir(path, "", &exclude_ids).unwrap(); - assert_eq!(yaml.exclude_rule_count, 0); + assert_eq!(yaml.rule_load_cnt.get("excluded").unwrap().to_owned(), 0); } #[test] fn test_exclude_deprecated_rules_file() { @@ -427,6 +442,9 @@ mod tests { let path = Path::new("test_files/rules/deprecated"); let exclude_ids = RuleExclude::default(); yaml.read_dir(path, "", &exclude_ids).unwrap(); - assert_eq!(yaml.deprecate_rule_count, 1); + assert_eq!( + yaml.rule_status_cnt.get("deprecated").unwrap().to_owned(), + 2 + ); } }