diff --git a/CHANGELOG-Japanese.md b/CHANGELOG-Japanese.md index 5c7c4db5..ad08d59c 100644 --- a/CHANGELOG-Japanese.md +++ b/CHANGELOG-Japanese.md @@ -1,21 +1,20 @@ # 変更点 -## v1.3.1 [2022/xx/xx] +## v1.3.1 [2022/06/13] **新機能:** - ルール内の`details`で複数の`Data`レコードから特定のデータを指定して出力できるようにした。 (#487) (@hitenkoku) +- 読み込んだルールのステータス情報の要約を追加した。 (#583) (@hitenkoku) **改善:** - LinuxとmacOSのバイナリサイズをより小さくするために、デバッグシンボルをストリップします。(#568) (@YamatoSecurity) +- Crateパッケージの更新 (@YamatoSecurity) - 新たな時刻表示のオプションとして`--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) - -**バグ修正:** - -- xxx +- ルール読み込み時のメッセージを追加した。 (#583) (@hitenkoku) **バグ修正:** diff --git a/CHANGELOG.md b/CHANGELOG.md index f4081df1..e982f99e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,25 +1,24 @@ # Changes -## v1.3.1 [2022/xx/xx] +## v1.3.1 [2022/06/13] **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:** -- Strip debug symbols by default for smaller Linux and macOS binaries. (#568) (@YamatoSecurity) +- Debug symbols are stripped by default for smaller Linux and macOS binaries. (#568) (@YamatoSecurity) +- Updated crate packages (@YamatoSecurity) - 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 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:** -- xxx - -**Bug Fixes:** - -- fixed bug that RecordID and RecordInformation column is showed when options is not enabled. (#577) (@hitenkoku) +- The RecordID and RecordInformation column headers would be shown even if those options were not enabled. (#577) (@hitenkoku) ## v1.3.0 [2022/06/06] diff --git a/Cargo.lock b/Cargo.lock index a37bde5c..95fbe630 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -175,7 +175,7 @@ checksum = "4acbb09d9ee8e23699b9634375c72795d095bf268439da88562cf9b501f181fa" dependencies = [ "camino", "cargo-platform", - "semver 1.0.9", + "semver 1.0.10", "serde", "serde_json", ] @@ -692,7 +692,7 @@ dependencies = [ [[package]] name = "hayabusa" -version = "1.3.1-dev" +version = "1.3.1" dependencies = [ "base64", "bytesize", @@ -1474,9 +1474,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cb243bdfdb5936c8dc3c45762a19d12ab4550cdc753bc247637d4ec35a040fd" +checksum = "a41d061efea015927ac527063765e73601444cdc344ba855bc7bd44578b25e1c" dependencies = [ "serde", ] diff --git a/Cargo.toml b/Cargo.toml index 29f01160..79bd0669 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hayabusa" -version = "1.3.1-dev" +version = "1.3.1" authors = ["Yamato Security @SecurityYamato"] edition = "2021" 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/configs.rs b/src/detections/configs.rs index 15b2e973..ed5c49e3 100644 --- a/src/detections/configs.rs +++ b/src/detections/configs.rs @@ -102,7 +102,7 @@ fn build_app<'a>() -> ArgMatches<'a> { --contributors 'Prints the list of contributors.'"; App::new(&program) .about("Hayabusa: Aiming to be the world's greatest Windows event log analysis tool!") - .version("1.3.1-dev") + .version("1.3.1") .author("Yamato Security (https://github.com/Yamato-Security/hayabusa) @SecurityYamato") .setting(AppSettings::VersionlessSubcommands) .arg( 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 ccb5d192..2202630c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -455,6 +455,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 + ); } }