diff --git a/Cargo.lock b/Cargo.lock index b1299b9f..cab6a4bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,17 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e" +[[package]] +name = "ahash" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43bb833f0bf979d8475d38fbf09ed3b8a55e1885fe93ad3f93239fc6a4f17b98" +dependencies = [ + "getrandom 0.2.3", + "once_cell", + "version_check", +] + [[package]] name = "aho-corasick" version = "0.7.18" @@ -698,6 +709,17 @@ dependencies = [ "wasi 0.9.0+wasi-snapshot-preview1", ] +[[package]] +name = "getrandom" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", +] + [[package]] name = "gimli" version = "0.22.0" @@ -733,6 +755,9 @@ name = "hashbrown" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash", +] [[package]] name = "hayabusa" @@ -745,6 +770,7 @@ dependencies = [ "dotenv", "evtx", "flate2", + "hashbrown", "hhmmss", "lazy_static", "linked-hash-map", @@ -1457,7 +1483,7 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" dependencies = [ - "getrandom", + "getrandom 0.1.15", "libc", "rand_chacha 0.2.2", "rand_core 0.5.1", @@ -1505,7 +1531,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" dependencies = [ - "getrandom", + "getrandom 0.1.15", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 7ced827a..af29763b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "hayabusa" version = "1.0.0" -authors = ["akiranishikawa "] -edition = "2018" +authors = ["Yamato Security @yamatosecurity"] +edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -28,9 +28,13 @@ slack-hook = "0.8" dotenv = "0.15.0" hhmmss = "*" pbr = "*" +hashbrown = "0.11.2" [target.x86_64-pc-windows-gnu] linker = "x86_64-w64-mingw32-gcc" [target.i686-pc-windows-gnu] linker = "i686-w64-mingw32-gcc" + +[profile.release] +lto = true \ No newline at end of file diff --git a/src/afterfact.rs b/src/afterfact.rs index 2be77936..72d2aabb 100644 --- a/src/afterfact.rs +++ b/src/afterfact.rs @@ -113,7 +113,7 @@ fn emit_csv(writer: &mut W, displayflag: bool) -> io::Result< wtr.flush()?; println!(""); - println!("Total events detected: {:?}", detect_count); + println!("Total events: {:?}", detect_count); Ok(()) } diff --git a/src/detections/configs.rs b/src/detections/configs.rs index c19079f5..ceabc05a 100644 --- a/src/detections/configs.rs +++ b/src/detections/configs.rs @@ -2,8 +2,9 @@ use crate::detections::print::AlertMessage; use crate::detections::utils; use chrono::{DateTime, Utc}; use clap::{App, AppSettings, ArgMatches}; +use hashbrown::HashMap; +use hashbrown::HashSet; use lazy_static::lazy_static; -use std::collections::{HashMap, HashSet}; use std::sync::RwLock; lazy_static! { pub static ref CONFIG: RwLock = RwLock::new(ConfigReader::new()); @@ -16,12 +17,13 @@ lazy_static! { levelmap.insert("CRITICAL".to_owned(), 5); return levelmap; }; + pub static ref EVENTKEY_ALIAS: EventKeyAliasConfig = + load_eventkey_alias("config/eventkey_alias.txt"); } #[derive(Clone)] pub struct ConfigReader { pub args: ArgMatches<'static>, - pub event_key_alias_config: EventKeyAliasConfig, pub event_timeline_config: EventInfoConfig, pub target_eventids: TargetEventIds, } @@ -30,7 +32,6 @@ impl ConfigReader { pub fn new() -> Self { ConfigReader { args: build_app(), - event_key_alias_config: load_eventkey_alias("config/eventkey_alias.txt"), event_timeline_config: load_eventcode_info("config/timeline_event_info.txt"), target_eventids: load_target_ids("config/target_eventids.txt"), } @@ -60,12 +61,13 @@ fn build_app<'a>() -> ArgMatches<'a> { --endtimeline=[ENDTIMELINE]'End time of the event to load from event file. Example: '2018/11/28 12:00:00 +09:00'' -q 'Quiet mode. Do not display the launch banner' -r --rules=[RULEDIRECTORY] 'Rule file directory (default: ./rules)' - -L --level=[LEVEL] 'Minimum level for rules (default: INFORMATIONAL)' + -m --min-level=[LEVEL] 'Minimum level for rules (default: informational)' -u --utc 'Output time in UTC format (default: local time)' -d --directory=[DIRECTORY] 'Directory of multiple .evtx files' -s --statistics 'Prints statistics of event IDs' -n --show-noisyalerts 'do not exclude noisy rules' -t --threadnum=[NUM] 'Thread number (default: optimal number for performance)' + --show-deprecated 'do not exclude rules with YAML's status deprecated' --contributors 'Prints the list of contributors'"; App::new(&program) .about("Hayabusa: Aiming to be the world's greatest Windows event log analysis tool!") @@ -198,21 +200,23 @@ impl TargetEventTime { #[derive(Debug, Clone)] pub struct EventKeyAliasConfig { key_to_eventkey: HashMap, + key_to_split_eventkey: HashMap>, } impl EventKeyAliasConfig { pub fn new() -> EventKeyAliasConfig { return EventKeyAliasConfig { key_to_eventkey: HashMap::new(), + key_to_split_eventkey: HashMap::new(), }; } - pub fn get_event_key(&self, alias: String) -> Option<&String> { - return self.key_to_eventkey.get(&alias); + pub fn get_event_key(&self, alias: &String) -> Option<&String> { + return self.key_to_eventkey.get(alias); } - pub fn get_event_key_values(&self) -> Vec<(&String, &String)> { - return self.key_to_eventkey.iter().map(|e| e).collect(); + pub fn get_event_key_split(&self, alias: &String) -> Option<&Vec> { + return self.key_to_split_eventkey.get(alias); } } @@ -236,7 +240,12 @@ fn load_eventkey_alias(path: &str) -> EventKeyAliasConfig { config .key_to_eventkey .insert(alias.to_owned(), event_key.to_owned()); + let splits = event_key.split(".").map(|s| s.len()).collect(); + config + .key_to_split_eventkey + .insert(alias.to_owned(), splits); }); + config.key_to_eventkey.shrink_to_fit(); return config; } @@ -273,14 +282,6 @@ impl EventInfoConfig { pub fn get_event_id(&self, eventid: &String) -> Option<&EventInfo> { return self.eventinfo.get(eventid); } - - pub fn get_event_info(&self) -> Vec<(&String, &EventInfo)> { - return self.eventinfo.iter().map(|e| e).collect(); - } - - // pub fn get_event_key_values(&self) -> Vec<(&String, &String)> { - // return self.timeline_eventcode_info.iter().map(|e| e).collect(); - // } } fn load_eventcode_info(path: &str) -> EventInfoConfig { @@ -312,27 +313,26 @@ fn load_eventcode_info(path: &str) -> EventInfoConfig { #[cfg(test)] mod tests { - use crate::detections::configs; use chrono::{DateTime, Utc}; - #[test] - #[ignore] - fn singleton_read_and_write() { - let message = - "EventKeyAliasConfig { key_to_eventkey: {\"EventID\": \"Event.System.EventID\"} }"; - configs::CONFIG.write().unwrap().event_key_alias_config = - configs::load_eventkey_alias("test_files/config/eventkey_alias.txt"); - - let display = format!( - "{}", - format_args!( - "{:?}", - configs::CONFIG.write().unwrap().event_key_alias_config - ) - ); - assert_eq!(message, display); - } +// #[test] +// #[ignore] +// fn singleton_read_and_write() { +// let message = +// "EventKeyAliasConfig { key_to_eventkey: {\"EventID\": \"Event.System.EventID\"} }"; +// configs::EVENT_KEY_ALIAS_CONFIG = +// configs::load_eventkey_alias("test_files/config/eventkey_alias.txt"); +// let display = format!( +// "{}", +// format_args!( +// "{:?}", +// configs::CONFIG.write().unwrap().event_key_alias_config +// ) +// ); +// assert_eq!(message, display); +// } +// } #[test] fn target_event_time_filter() { @@ -358,4 +358,4 @@ mod tests { assert_eq!(time_filter.is_target(&start_time), true); assert_eq!(time_filter.is_target(&end_time), true); } -} +} \ No newline at end of file diff --git a/src/detections/detection.rs b/src/detections/detection.rs index 10a1542a..7f16a3b9 100644 --- a/src/detections/detection.rs +++ b/src/detections/detection.rs @@ -31,12 +31,11 @@ impl EvtxRecordInfo { return EvtxRecordInfo { evtx_filepath: evtx_filepath, record: record, - data_string, + data_string: data_string, }; } } -// TODO テストケースかかなきゃ... #[derive(Debug)] pub struct Detection { rules: Vec, @@ -181,7 +180,7 @@ impl Detection { println!("{} alerts: {}", levellabel[i], value); total_unique += value; } - println!("Unique events detected: {}", total_unique); + println!("Unique alerts detected: {}", total_unique); } // 複数のイベントレコードに対して、ルールを1個実行します。 diff --git a/src/detections/print.rs b/src/detections/print.rs index 26e05046..594aa504 100644 --- a/src/detections/print.rs +++ b/src/detections/print.rs @@ -112,12 +112,7 @@ impl Message { .take(target_length) .collect::(); - if let Some(array_str) = configs::CONFIG - .read() - .unwrap() - .event_key_alias_config - .get_event_key(target_str.to_string()) - { + if let Some(array_str) = configs::EVENTKEY_ALIAS.get_event_key(&target_str) { let split: Vec<&str> = array_str.split(".").collect(); let mut is_exist_event_key = false; let mut tmp_event_record: &Value = event_record.into(); @@ -166,7 +161,7 @@ impl Message { detect_count += detect_infos.len(); } println!(""); - println!("Total Events Detected:{:?}", detect_count); + println!("Total events:{:?}", detect_count); } pub fn iter(&self) -> &BTreeMap, Vec> { diff --git a/src/detections/rule/condition_parser.rs b/src/detections/rule/condition_parser.rs index 349f558f..8bc90d71 100644 --- a/src/detections/rule/condition_parser.rs +++ b/src/detections/rule/condition_parser.rs @@ -114,8 +114,8 @@ impl ConditionCompiler { pub fn compile_condition( &self, condition_str: String, - name_2_node: &HashMap>>, - ) -> Result, String> { + name_2_node: &HashMap>>, + ) -> Result, String> { // パイプはここでは処理しない let captured = self::RE_PIPE.captures(&condition_str); let condition_str = if captured.is_some() { @@ -137,8 +137,8 @@ impl ConditionCompiler { fn compile_condition_body( &self, condition_str: String, - name_2_node: &HashMap>>, - ) -> Result, String> { + name_2_node: &HashMap>>, + ) -> Result, String> { let tokens = self.tokenize(&condition_str)?; let parsed = self.parse(tokens)?; @@ -410,8 +410,8 @@ impl ConditionCompiler { fn to_selectnode( &self, token: ConditionToken, - name_2_node: &HashMap>>, - ) -> Result, String> { + name_2_node: &HashMap>>, + ) -> Result, String> { // RefSelectionNodeに変換 if let ConditionToken::SelectionReference(selection_name) = token { let selection_node = name_2_node.get(&selection_name); @@ -540,7 +540,7 @@ mod tests { let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: record, - data_string: String::default(), + data_string: record_str.to_string(), }; assert_eq!( rule_node.select(&"testpath".to_owned(), &recinfo), @@ -589,7 +589,7 @@ mod tests { let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: record, - data_string: String::default(), + data_string: record_json_str.to_string(), }; assert_eq!(rule_node.select(&"testpath".to_owned(), &recinfo), true); } @@ -636,7 +636,7 @@ mod tests { let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: record, - data_string: String::default(), + data_string: record_json_str.to_string(), }; assert_eq!(rule_node.select(&"testpath".to_owned(), &recinfo), false); } diff --git a/src/detections/rule/count.rs b/src/detections/rule/count.rs index 5a65de2c..9ad83e2b 100644 --- a/src/detections/rule/count.rs +++ b/src/detections/rule/count.rs @@ -43,23 +43,10 @@ pub fn countup( /// countでgroupbyなどの情報を区分するためのハッシュマップのキーを作成する関数 pub fn create_count_key(rule: &RuleNode, record: &Value) -> String { - if rule - .detection - .as_ref() - .unwrap() - .aggregation_condition - .as_ref() - .is_none() - { + if rule.detection.aggregation_condition.as_ref().is_none() { return "_".to_string(); } - let aggcondition = rule - .detection - .as_ref() - .unwrap() - .aggregation_condition - .as_ref() - .unwrap(); + let aggcondition = rule.detection.aggregation_condition.as_ref().unwrap(); // recordでaliasが登録されている前提とする let mut key = "".to_string(); if aggcondition._field_name.is_some() { @@ -115,13 +102,7 @@ pub fn aggregation_condition_select(rule: &RuleNode, filepath: &String) -> Vec String { //この関数はaggregation ruleのパースが正常終了した後に呼ばれる想定のためOptionの判定は行わない - let agg_condition = rule - .detection - .as_ref() - .unwrap() - .aggregation_condition - .as_ref() - .unwrap(); + let agg_condition = rule.detection.aggregation_condition.as_ref().unwrap(); let mut ret: String = "".to_owned(); match agg_condition._cmp_op { AggregationConditionToken::EQ => { @@ -268,18 +249,12 @@ pub fn judge_timeframe( let mut ret: Vec = Vec::new(); let mut time_data = time_datas.clone(); time_data.sort(); - let aggcondition = rule - .detection - .as_ref() - .unwrap() - .aggregation_condition - .as_ref() - .unwrap(); + let aggcondition = rule.detection.aggregation_condition.as_ref().unwrap(); let mut start_point = 0; // 最初はcountの条件として記載されている分のレコードを取得するためのindex指定 let mut check_point = start_point + aggcondition._cmp_num - 1; // timeframeで指定された基準の値を秒数として保持 - let judge_sec_frame = get_sec_timeframe(&rule.detection.as_ref().unwrap().timeframe); + let judge_sec_frame = get_sec_timeframe(&rule.detection.timeframe); loop { // 基準となるレコードもしくはcountを最低限満たす対象のレコードのindexが配列の領域を超えていた場合 if start_point as usize >= time_data.len() || check_point as usize >= time_data.len() { @@ -670,7 +645,7 @@ mod tests { let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: rec, - data_string: String::default(), + data_string: record.to_string(), }; let _result = rule_node.select(&"testpath".to_string(), &recinfo); } @@ -763,7 +738,7 @@ mod tests { let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: record, - data_string: String::default(), + data_string: record_str.to_string(), }; let result = &rule_node.select(&"testpath".to_owned(), &recinfo); assert_eq!(result, &true); diff --git a/src/detections/rule/matchers.rs b/src/detections/rule/matchers.rs index fe778e53..42c69614 100644 --- a/src/detections/rule/matchers.rs +++ b/src/detections/rule/matchers.rs @@ -263,7 +263,7 @@ impl LeafMatcher for DefaultMatcher { // Pipeが指定されていればパースする let emp = String::default(); let mut keys: VecDeque<&str> = key_list.get(0).unwrap_or(&emp).split("|").collect(); // key_listが空はあり得ない - keys.pop_front(); + keys.pop_front(); // 一つ目はただのキーで、2つめ以降がpipe while !keys.is_empty() { let key = keys.pop_front().unwrap(); let pipe_element = match key { @@ -333,14 +333,27 @@ impl LeafMatcher for DefaultMatcher { return is_event_value_null; } + // JSON形式のEventLogデータをstringに変換するための前処理 + // 以前のコードはstringに変換に変換する必ずto_string()がするような処理になっていた。 + // そうすると、凄く遅くなるので、そうならないように回避 + let mut b_str = String::default(); + let mut n_str = String::default(); + match event_value.unwrap_or(&Value::Null) { + Value::Bool(b) => b_str = b.to_string(), + Value::Number(n) => { + n_str = n.to_string(); + } + _ => (), + }; + // JSON形式のEventLogデータをstringに変換 - let event_value_str: Option = if self.key_list.is_empty() { - Option::Some(recinfo.data_string.to_string()) + let event_value_str: Option<&String> = if self.key_list.is_empty() { + Option::Some(&recinfo.data_string) } else { let value = match event_value.unwrap_or(&Value::Null) { - Value::Bool(b) => Option::Some(b.to_string()), - Value::String(s) => Option::Some(s.to_string()), - Value::Number(n) => Option::Some(n.to_string()), + Value::Bool(_) => Option::Some(&b_str), + Value::String(s) => Option::Some(s), + Value::Number(_) => Option::Some(&n_str), _ => Option::None, }; value @@ -350,12 +363,7 @@ impl LeafMatcher for DefaultMatcher { } // 変換したデータに対してパイプ処理を実行する。 - let event_value_str = (&self.pipes) - .iter() - .fold(event_value_str.unwrap(), |acc, pipe| { - return pipe.pipe_eventlog_data(acc); - }); - + let event_value_str = event_value_str.unwrap(); if self.key_list.is_empty() { // この場合ただのgrep検索なので、ただ正規表現に一致するかどうか調べればよいだけ return self.re.as_ref().unwrap().is_match(&event_value_str); @@ -376,15 +384,6 @@ enum PipeElement { } impl PipeElement { - /// WindowsEventLogのJSONデータに対してパイプ処理します。 - fn pipe_eventlog_data(&self, pattern: String) -> String { - return match self { - // wildcardはcase insensetiveなので、全て小文字にして比較する。 - PipeElement::Wildcard => pattern.to_lowercase(), - _ => pattern, - }; - } - /// patternをパイプ処理します fn pipe_pattern(&self, pattern: String) -> String { // enumでポリモーフィズムを実装すると、一つのメソッドに全部の型の実装をする感じになる。Java使い的にはキモイ感じがする。 @@ -429,8 +428,6 @@ impl PipeElement { /// PipeElement::Wildcardのパイプ処理です。 /// pipe_pattern()に含めて良い処理ですが、複雑な処理になってしまったので別関数にしました。 fn pipe_pattern_wildcard(pattern: String) -> String { - // wildcardはcase sensetiveなので、全て小文字にして比較する。 - let pattern = pattern.to_lowercase(); let wildcards = vec!["*".to_string(), "?".to_string()]; // patternをwildcardでsplitした結果をpattern_splitsに入れる @@ -484,7 +481,7 @@ impl PipeElement { } // SIGMAルールのwildcard表記から正規表現の表記に変換します。 - return pattern_splits.iter().enumerate().fold( + let ret = pattern_splits.iter().enumerate().fold( String::default(), |acc: String, (idx, pattern)| { let regex_value = if idx % 2 == 0 { @@ -503,6 +500,10 @@ impl PipeElement { return format!("{}{}", acc, regex_value); }, ); + + // sigmaのwildcardはcase insensitive + // なので、正規表現の先頭にcase insensitiveであることを表す記号を付与 + return "(?i)".to_string() + &ret; } } @@ -547,7 +548,7 @@ mod tests { updated_date: 2020/11/8 "#; let rule_node = parse_rule_from_str(rule_str); - let selection_node = &rule_node.detection.unwrap().name_to_selection["selection"]; + let selection_node = &rule_node.detection.name_to_selection["selection"]; // Root let detection_childs = selection_node.get_childs(); @@ -573,7 +574,7 @@ mod tests { let re = matcher.re.as_ref(); assert_eq!( re.unwrap().as_str(), - r"Microsoft\-Windows\-PowerShell/Operational".to_lowercase() + r"(?i)Microsoft\-Windows\-PowerShell/Operational" ); } @@ -595,7 +596,7 @@ mod tests { assert_eq!(matcher.re.is_some(), true); let re = matcher.re.as_ref(); - assert_eq!(re.unwrap().as_str(), "4103"); + assert_eq!(re.unwrap().as_str(), "(?i)4103"); } // ContextInfo @@ -620,7 +621,7 @@ mod tests { let hostapp_en_matcher = hostapp_en_matcher.downcast_ref::().unwrap(); assert_eq!(hostapp_en_matcher.re.is_some(), true); let re = hostapp_en_matcher.re.as_ref(); - assert_eq!(re.unwrap().as_str(), "Host Application".to_lowercase()); + assert_eq!(re.unwrap().as_str(), "(?i)Host Application"); // LeafSelectionNodeである、ホスト アプリケーションノードが正しいことを確認 let hostapp_jp_node = ancestors[1].as_ref() as &dyn SelectionNode; @@ -634,7 +635,7 @@ mod tests { let hostapp_jp_matcher = hostapp_jp_matcher.downcast_ref::().unwrap(); assert_eq!(hostapp_jp_matcher.re.is_some(), true); let re = hostapp_jp_matcher.re.as_ref(); - assert_eq!(re.unwrap().as_str(), "ホスト アプリケーション"); + assert_eq!(re.unwrap().as_str(), "(?i)ホスト アプリケーション"); } // ImagePath @@ -741,7 +742,7 @@ mod tests { let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: record, - data_string: String::default(), + data_string: record_json_str.to_string(), }; assert_eq!(rule_node.select(&"testpath".to_owned(), &recinfo), false); } @@ -774,7 +775,7 @@ mod tests { let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: record, - data_string: String::default(), + data_string: record_json_str.to_string(), }; assert_eq!(rule_node.select(&"testpath".to_owned(), &recinfo), false); } @@ -807,7 +808,7 @@ mod tests { let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: record, - data_string: String::default(), + data_string: record_json_str.to_string(), }; assert_eq!(rule_node.select(&"testpath".to_owned(), &recinfo), true); } @@ -841,7 +842,7 @@ mod tests { let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: record, - data_string: String::default(), + data_string: record_json_str.to_string(), }; assert_eq!(rule_node.select(&"testpath".to_owned(), &recinfo), false); } @@ -875,7 +876,7 @@ mod tests { let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: record, - data_string: String::default(), + data_string: record_json_str.to_string(), }; assert_eq!(rule_node.select(&"testpath".to_owned(), &recinfo), false); } @@ -908,7 +909,7 @@ mod tests { let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: record, - data_string: String::default(), + data_string: record_json_str.to_string(), }; assert_eq!(rule_node.select(&"testpath".to_owned(), &recinfo), true); } @@ -941,7 +942,7 @@ mod tests { let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: record, - data_string: String::default(), + data_string: record_json_str.to_string(), }; assert_eq!(rule_node.select(&"testpath".to_owned(), &recinfo), false); } @@ -975,7 +976,7 @@ mod tests { let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: record, - data_string: String::default(), + data_string: record_json_str.to_string(), }; assert_eq!(rule_node.select(&"testpath".to_owned(), &recinfo), false); } @@ -1009,7 +1010,7 @@ mod tests { let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: record, - data_string: String::default(), + data_string: record_json_str.to_string(), }; assert_eq!(rule_node.select(&"testpath".to_owned(), &recinfo), true); } @@ -1043,7 +1044,7 @@ mod tests { let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: record, - data_string: String::default(), + data_string: record_json_str.to_string(), }; assert_eq!(rule_node.select(&"testpath".to_owned(), &recinfo), true); } @@ -1077,7 +1078,7 @@ mod tests { let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: record, - data_string: String::default(), + data_string: record_json_str.to_string(), }; assert_eq!(rule_node.select(&"testpath".to_owned(), &recinfo), true); } @@ -1111,7 +1112,7 @@ mod tests { let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: record, - data_string: String::default(), + data_string: record_json_str.to_string(), }; assert_eq!(rule_node.select(&"testpath".to_owned(), &recinfo), false); } @@ -1144,7 +1145,7 @@ mod tests { let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: record, - data_string: String::default(), + data_string: record_json_str.to_string(), }; assert_eq!(rule_node.select(&"testpath".to_owned(), &recinfo), true); } @@ -1181,7 +1182,7 @@ mod tests { let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: record, - data_string: String::default(), + data_string: record_json_str.to_string(), }; assert_eq!(rule_node.select(&"testpath".to_owned(), &recinfo), false); } @@ -1218,7 +1219,7 @@ mod tests { let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: record, - data_string: String::default(), + data_string: record_json_str.to_string(), }; assert_eq!(rule_node.select(&"testpath".to_owned(), &recinfo), false); } @@ -1254,7 +1255,7 @@ mod tests { let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: record, - data_string: String::default(), + data_string: record_json_str.to_string(), }; assert_eq!(rule_node.select(&"testpath".to_owned(), &recinfo), false); } @@ -1299,7 +1300,7 @@ mod tests { let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: record, - data_string: String::default(), + data_string: record_json_str.to_string(), }; assert_eq!(rule_node.select(&"testpath".to_owned(), &recinfo), true); } @@ -1344,7 +1345,7 @@ mod tests { let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: record, - data_string: String::default(), + data_string: record_json_str.to_string(), }; assert_eq!(rule_node.select(&"testpath".to_owned(), &recinfo), false); } @@ -1389,7 +1390,7 @@ mod tests { let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: record, - data_string: String::default(), + data_string: record_json_str.to_string(), }; assert_eq!(rule_node.select(&"testpath".to_owned(), &recinfo), true); } @@ -1434,7 +1435,7 @@ mod tests { let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: record, - data_string: String::default(), + data_string: record_json_str.to_string(), }; assert_eq!(rule_node.select(&"testpath".to_owned(), &recinfo), false); } @@ -1479,7 +1480,7 @@ mod tests { let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: record, - data_string: String::default(), + data_string: record_json_str.to_string(), }; assert_eq!(rule_node.select(&"testpath".to_owned(), &recinfo), true); } @@ -1524,7 +1525,7 @@ mod tests { let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: record, - data_string: String::default(), + data_string: record_json_str.to_string(), }; assert_eq!(rule_node.select(&"testpath".to_owned(), &recinfo), false); } @@ -1557,7 +1558,7 @@ mod tests { let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: record, - data_string: String::default(), + data_string: record_json_str.to_string(), }; assert_eq!(rule_node.select(&"testpath".to_owned(), &recinfo), true); } @@ -1590,7 +1591,7 @@ mod tests { let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: record, - data_string: String::default(), + data_string: record_json_str.to_string(), }; assert_eq!(rule_node.select(&"testpath".to_owned(), &recinfo), false); } @@ -1623,7 +1624,7 @@ mod tests { let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: record, - data_string: String::default(), + data_string: record_json_str.to_string(), }; assert_eq!(rule_node.select(&"testpath".to_owned(), &recinfo), true); } @@ -1636,7 +1637,7 @@ mod tests { #[test] fn test_pipe_pattern_wildcard_asterisk() { let value = PipeElement::pipe_pattern_wildcard(r"*ho*ge*".to_string()); - assert_eq!(".*ho.*ge.*", value); + assert_eq!("(?i).*ho.*ge.*", value); } #[test] @@ -1644,7 +1645,7 @@ mod tests { let value = PipeElement::pipe_pattern_wildcard(r"\*ho\*\*ge\*".to_string()); // wildcardの「\*」は文字列としての「*」を表す。 // 正規表現で「*」はエスケープする必要があるので、\*が正解 - assert_eq!(r"\*ho\*\*ge\*", value); + assert_eq!(r"(?i)\*ho\*\*ge\*", value); } #[test] @@ -1652,43 +1653,43 @@ mod tests { // wildcardの「\\*」は文字列としての「\」と正規表現の「.*」を表す。 // 文字列としての「\」はエスケープされるので、「\\.*」が正解 let value = PipeElement::pipe_pattern_wildcard(r"\\*ho\\*ge\\*".to_string()); - assert_eq!(r"\\.*ho\\.*ge\\.*", value); + assert_eq!(r"(?i)\\.*ho\\.*ge\\.*", value); } #[test] fn test_pipe_pattern_wildcard_question() { let value = PipeElement::pipe_pattern_wildcard(r"?ho?ge?".to_string()); - assert_eq!(r".ho.ge.", value); + assert_eq!(r"(?i).ho.ge.", value); } #[test] fn test_pipe_pattern_wildcard_question2() { let value = PipeElement::pipe_pattern_wildcard(r"\?ho\?ge\?".to_string()); - assert_eq!(r"\?ho\?ge\?", value); + assert_eq!(r"(?i)\?ho\?ge\?", value); } #[test] fn test_pipe_pattern_wildcard_question3() { let value = PipeElement::pipe_pattern_wildcard(r"\\?ho\\?ge\\?".to_string()); - assert_eq!(r"\\.ho\\.ge\\.", value); + assert_eq!(r"(?i)\\.ho\\.ge\\.", value); } #[test] fn test_pipe_pattern_wildcard_backshash() { let value = PipeElement::pipe_pattern_wildcard(r"\\ho\\ge\\".to_string()); - assert_eq!(r"\\\\ho\\\\ge\\\\", value); + assert_eq!(r"(?i)\\\\ho\\\\ge\\\\", value); } #[test] fn test_pipe_pattern_wildcard_mixed() { let value = PipeElement::pipe_pattern_wildcard(r"\\*\****\*\\*".to_string()); - assert_eq!(r"\\.*\*.*.*.*\*\\.*", value); + assert_eq!(r"(?i)\\.*\*.*.*.*\*\\.*", value); } #[test] fn test_pipe_pattern_wildcard_many_backshashs() { let value = PipeElement::pipe_pattern_wildcard(r"\\\*ho\\\*ge\\\".to_string()); - assert_eq!(r"\\\\.*ho\\\\.*ge\\\\\\", value); + assert_eq!(r"(?i)\\\\.*ho\\\\.*ge\\\\\\", value); } #[test] @@ -1712,11 +1713,10 @@ mod tests { match serde_json::from_str(record_json_str) { Ok(rec) => { let rec: Value = rec; - let recstr = rec.to_string(); let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: rec, - data_string: recstr, + data_string: record_json_str.to_string(), }; assert_eq!(rule_node.select(&"testpath".to_owned(), &recinfo), true); } @@ -1747,11 +1747,10 @@ mod tests { match serde_json::from_str(record_json_str) { Ok(record) => { let rec: Value = record; - let recstr = rec.to_string(); let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: rec, - data_string: recstr, + data_string: record_json_str.to_string(), }; assert_eq!(rule_node.select(&"testpath".to_owned(), &recinfo), false); } @@ -1786,7 +1785,7 @@ mod tests { let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: record, - data_string: String::default(), + data_string: record_json_str.to_string(), }; assert_eq!(rule_node.select(&"testpath".to_owned(), &recinfo), true); } @@ -1821,7 +1820,7 @@ mod tests { let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: record, - data_string: String::default(), + data_string: record_json_str.to_string(), }; assert_eq!(rule_node.select(&"testpath".to_owned(), &recinfo), false); } diff --git a/src/detections/rule/mod.rs b/src/detections/rule/mod.rs index c4d3150d..c5a1f779 100644 --- a/src/detections/rule/mod.rs +++ b/src/detections/rule/mod.rs @@ -27,7 +27,7 @@ pub fn create_rule(rulepath: String, yaml: Yaml) -> RuleNode { pub struct RuleNode { pub rulepath: String, pub yaml: Yaml, - detection: Option, + detection: DetectionNode, countdata: HashMap>>>, } @@ -38,13 +38,14 @@ impl Debug for RuleNode { } unsafe impl Sync for RuleNode {} +unsafe impl Send for RuleNode {} impl RuleNode { pub fn new(rulepath: String, yaml: Yaml) -> RuleNode { return RuleNode { rulepath: rulepath, yaml: yaml, - detection: Option::None, + detection: DetectionNode::new(), countdata: HashMap::new(), }; } @@ -52,18 +53,11 @@ impl RuleNode { pub fn init(&mut self) -> Result<(), Vec> { let mut errmsgs: Vec = vec![]; - // SIGMAルールを受け入れるため、outputがなくてもOKにする。 - // if self.yaml["output"].as_str().unwrap_or("").is_empty() { - // errmsgs.push("Cannot find required key. key:output".to_string()); - // } - // detection node initialization - let mut detection = DetectionNode::new(); - let detection_result = detection.init(&self.yaml["detection"]); + let detection_result = self.detection.init(&self.yaml["detection"]); if detection_result.is_err() { errmsgs.extend(detection_result.unwrap_err()); } - self.detection = Option::Some(detection); if errmsgs.is_empty() { return Result::Ok(()); @@ -73,10 +67,7 @@ impl RuleNode { } pub fn select(&mut self, filepath: &String, event_record: &EvtxRecordInfo) -> bool { - if self.detection.is_none() { - return false; - } - let result = self.detection.as_ref().unwrap().select(event_record); + let result = self.detection.select(event_record); if result { count::count(self, filepath, &event_record.record); } @@ -84,12 +75,7 @@ impl RuleNode { } /// aggregation conditionが存在するかを返す関数 pub fn has_agg_condition(&self) -> bool { - return self - .detection - .as_ref() - .unwrap() - .aggregation_condition - .is_some(); + return self.detection.aggregation_condition.is_some(); } /// Aggregation Conditionの結果を配列で返却する関数 pub fn judge_satisfy_aggcondition(&self) -> Vec { @@ -109,8 +95,8 @@ impl RuleNode { /// Ruleファイルのdetectionを表すノード struct DetectionNode { - pub name_to_selection: HashMap>>, - pub condition: Option>, + pub name_to_selection: HashMap>>, + pub condition: Option>, pub aggregation_condition: Option, pub timeframe: Option, } @@ -237,10 +223,7 @@ impl DetectionNode { } /// selectionをパースします。 - fn parse_selection( - &self, - selection_yaml: &Yaml, - ) -> Option> { + fn parse_selection(&self, selection_yaml: &Yaml) -> Option> { return Option::Some(self.parse_selection_recursively(vec![], selection_yaml)); } @@ -249,7 +232,7 @@ impl DetectionNode { &self, key_list: Vec, yaml: &Yaml, - ) -> Box { + ) -> Box { if yaml.as_hash().is_some() { // 連想配列はAND条件と解釈する let yaml_hash = yaml.as_hash().unwrap(); @@ -355,7 +338,7 @@ mod tests { let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: record, - data_string: String::default(), + data_string: record_json_str.to_string(), }; assert_eq!(rule_node.select(&"testpath".to_owned(), &recinfo), true); } @@ -388,7 +371,7 @@ mod tests { let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: record, - data_string: String::default(), + data_string: record_json_str.to_string(), }; assert_eq!(rule_node.select(&"testpath".to_owned(), &recinfo), false); } @@ -421,7 +404,7 @@ mod tests { let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: record, - data_string: String::default(), + data_string: record_json_str.to_string(), }; assert_eq!(rule_node.select(&"testpath".to_owned(), &recinfo), false); } @@ -507,7 +490,7 @@ mod tests { let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: record, - data_string: String::default(), + data_string: record_json_str.to_string(), }; assert_eq!(rule_node.select(&"testpath".to_owned(), &recinfo), true); } @@ -569,7 +552,7 @@ mod tests { let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: record, - data_string: String::default(), + data_string: record_json_str.to_string(), }; assert_eq!(rule_node.select(&"testpath".to_owned(), &recinfo), false); } @@ -638,7 +621,7 @@ mod tests { let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: record, - data_string: String::default(), + data_string: record_json_str.to_string(), }; assert_eq!(rule_node.select(&"testpath".to_owned(), &recinfo), true); } @@ -685,7 +668,7 @@ mod tests { let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: record, - data_string: String::default(), + data_string: record_json_str.to_string(), }; assert_eq!(rule_node.select(&"testpath".to_owned(), &recinfo), true); } @@ -733,7 +716,7 @@ mod tests { let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: record, - data_string: String::default(), + data_string: record_json_str.to_string(), }; assert_eq!(rule_node.select(&"testpath".to_owned(), &recinfo), false); } @@ -800,7 +783,7 @@ mod tests { let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: record, - data_string: String::default(), + data_string: record_json_str.to_string(), }; assert_eq!(rule_node.select(&"testpath".to_owned(), &recinfo), true); } @@ -867,7 +850,7 @@ mod tests { let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: record, - data_string: String::default(), + data_string: record_json_str.to_string(), }; assert_eq!(rule_node.select(&"testpath".to_owned(), &recinfo), false); } @@ -916,7 +899,7 @@ mod tests { let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: record, - data_string: String::default(), + data_string: record_json_str.to_string(), }; assert_eq!(rule_node.select(&"testpath".to_owned(), &recinfo), true); } @@ -977,13 +960,10 @@ mod tests { let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: record, - data_string: String::default(), + data_string: record_str.to_string(), }; let result = rule_node.select(&"testpath".to_string(), &recinfo); - assert_eq!( - rule_node.detection.unwrap().aggregation_condition.is_some(), - true - ); + assert_eq!(rule_node.detection.aggregation_condition.is_some(), true); assert_eq!(result, true); assert_eq!( *&rule_node diff --git a/src/detections/rule/selectionnodes.rs b/src/detections/rule/selectionnodes.rs index 030d832c..f7446866 100644 --- a/src/detections/rule/selectionnodes.rs +++ b/src/detections/rule/selectionnodes.rs @@ -19,21 +19,18 @@ pub trait SelectionNode: mopa::Any { fn init(&mut self) -> Result<(), Vec>; // 子ノードを取得する(グラフ理論のchildと同じ意味) - fn get_childs(&self) -> Vec<&Box>; + fn get_childs(&self) -> Vec<&Box>; // 子孫ノードを取得する(グラフ理論のdescendantと同じ意味) - fn get_descendants(&self) -> Vec<&Box>; + fn get_descendants(&self) -> Vec<&Box>; } mopafy!(SelectionNode); /// detection - selection配下でAND条件を表すノード pub struct AndSelectionNode { - pub child_nodes: Vec>, + pub child_nodes: Vec>, } -unsafe impl Send for AndSelectionNode {} -unsafe impl Sync for AndSelectionNode {} - impl AndSelectionNode { pub fn new() -> AndSelectionNode { return AndSelectionNode { @@ -76,7 +73,7 @@ impl SelectionNode for AndSelectionNode { } } - fn get_childs(&self) -> Vec<&Box> { + fn get_childs(&self) -> Vec<&Box> { let mut ret = vec![]; self.child_nodes.iter().for_each(|child_node| { ret.push(child_node); @@ -85,7 +82,7 @@ impl SelectionNode for AndSelectionNode { return ret; } - fn get_descendants(&self) -> Vec<&Box> { + fn get_descendants(&self) -> Vec<&Box> { let mut ret = self.get_childs(); self.child_nodes @@ -104,12 +101,9 @@ impl SelectionNode for AndSelectionNode { /// detection - selection配下でOr条件を表すノード pub struct OrSelectionNode { - pub child_nodes: Vec>, + pub child_nodes: Vec>, } -unsafe impl Send for OrSelectionNode {} -unsafe impl Sync for OrSelectionNode {} - impl OrSelectionNode { pub fn new() -> OrSelectionNode { return OrSelectionNode { @@ -152,7 +146,7 @@ impl SelectionNode for OrSelectionNode { } } - fn get_childs(&self) -> Vec<&Box> { + fn get_childs(&self) -> Vec<&Box> { let mut ret = vec![]; self.child_nodes.iter().for_each(|child_node| { ret.push(child_node); @@ -161,7 +155,7 @@ impl SelectionNode for OrSelectionNode { return ret; } - fn get_descendants(&self) -> Vec<&Box> { + fn get_descendants(&self) -> Vec<&Box> { let mut ret = self.get_childs(); self.child_nodes @@ -180,14 +174,11 @@ impl SelectionNode for OrSelectionNode { /// conditionでNotを表すノード pub struct NotSelectionNode { - node: Box, + node: Box, } -unsafe impl Send for NotSelectionNode {} -unsafe impl Sync for NotSelectionNode {} - impl NotSelectionNode { - pub fn new(node: Box) -> NotSelectionNode { + pub fn new(node: Box) -> NotSelectionNode { return NotSelectionNode { node: node }; } } @@ -201,11 +192,11 @@ impl SelectionNode for NotSelectionNode { return Result::Ok(()); } - fn get_childs(&self) -> Vec<&Box> { + fn get_childs(&self) -> Vec<&Box> { return vec![]; } - fn get_descendants(&self) -> Vec<&Box> { + fn get_descendants(&self) -> Vec<&Box> { return self.get_childs(); } } @@ -215,14 +206,11 @@ pub struct RefSelectionNode { // selection_nodeはDetectionNodeのname_2_nodeが所有権を持っていて、RefSelectionNodeのselection_nodeに所有権を持たせることができない。 // そこでArcを使って、DetectionNodeのname_2_nodeとRefSelectionNodeのselection_nodeで所有権を共有する。 // RcじゃなくてArcなのはマルチスレッド対応のため - selection_node: Arc>, + selection_node: Arc>, } -unsafe impl Send for RefSelectionNode {} -unsafe impl Sync for RefSelectionNode {} - impl RefSelectionNode { - pub fn new(selection_node: Arc>) -> RefSelectionNode { + pub fn new(selection_node: Arc>) -> RefSelectionNode { return RefSelectionNode { selection_node: selection_node, }; @@ -238,35 +226,38 @@ impl SelectionNode for RefSelectionNode { return Result::Ok(()); } - fn get_childs(&self) -> Vec<&Box> { + fn get_childs(&self) -> Vec<&Box> { return vec![&self.selection_node]; } - fn get_descendants(&self) -> Vec<&Box> { + fn get_descendants(&self) -> Vec<&Box> { return self.get_childs(); } } /// detection - selection配下の末端ノード pub struct LeafSelectionNode { + key: String, key_list: Vec, select_value: Yaml, pub matcher: Option>, } -unsafe impl Send for LeafSelectionNode {} -unsafe impl Sync for LeafSelectionNode {} - impl LeafSelectionNode { pub fn new(key_list: Vec, value_yaml: Yaml) -> LeafSelectionNode { return LeafSelectionNode { + key: String::default(), key_list: key_list, select_value: value_yaml, matcher: Option::None, }; } - pub fn get_key(&self) -> String { + pub fn get_key(&self) -> &String { + return &self.key; + } + + fn _create_key(&self) -> String { if self.key_list.is_empty() { return String::default(); } @@ -397,6 +388,7 @@ impl SelectionNode for LeafSelectionNode { )]); } + self.key = self._create_key(); return self .matcher .as_mut() @@ -404,11 +396,11 @@ impl SelectionNode for LeafSelectionNode { .init(&match_key_list, &self.select_value); } - fn get_childs(&self) -> Vec<&Box> { + fn get_childs(&self) -> Vec<&Box> { return vec![]; } - fn get_descendants(&self) -> Vec<&Box> { + fn get_descendants(&self) -> Vec<&Box> { return vec![]; } } @@ -441,7 +433,7 @@ mod tests { let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: record, - data_string: String::default(), + data_string: record_json_str.to_string(), }; assert_eq!(rule_node.select(&"testpath".to_owned(), &recinfo), true); } @@ -477,7 +469,7 @@ mod tests { let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: record, - data_string: String::default(), + data_string: record_json_str.to_string(), }; assert_eq!(rule_node.select(&"testpath".to_owned(), &recinfo), false); } @@ -512,7 +504,7 @@ mod tests { let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: record, - data_string: String::default(), + data_string: record_json_str.to_string(), }; assert_eq!(rule_node.select(&"testpath".to_owned(), &recinfo), true); } @@ -547,7 +539,7 @@ mod tests { let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: record, - data_string: String::default(), + data_string: record_json_str.to_string(), }; assert_eq!(rule_node.select(&"testpath".to_owned(), &recinfo), true); } @@ -582,7 +574,7 @@ mod tests { let recinfo = EvtxRecordInfo { evtx_filepath: "testpath".to_owned(), record: record, - data_string: String::default(), + data_string: record_json_str.to_string(), }; assert_eq!(rule_node.select(&"testpath".to_owned(), &recinfo), false); } diff --git a/src/detections/utils.rs b/src/detections/utils.rs index 5d0d52ff..72d6448c 100644 --- a/src/detections/utils.rs +++ b/src/detections/utils.rs @@ -129,43 +129,41 @@ pub fn get_serde_number_to_string(value: &serde_json::Value) -> Option { } } -// alias.txtについて、指定されたevent_keyに対応するaliasを取得します。 -pub fn get_alias(event_key: &String) -> Option { - let conf = configs::CONFIG.read().unwrap(); - let keyvalues = &conf.event_key_alias_config.get_event_key_values(); - let value = keyvalues - .iter() - .find(|(_, cur_event_key)| &event_key == cur_event_key); - - if value.is_none() { - return Option::None; - } else { - return Option::Some(value.unwrap().0.clone()); - } -} - pub fn get_event_value<'a>(key: &String, event_value: &'a Value) -> Option<&'a Value> { if key.len() == 0 { return Option::None; } - let singleton = configs::CONFIG.read().unwrap(); - let event_key = match singleton - .event_key_alias_config - .get_event_key(key.to_string()) - { - Some(alias_event_key) => alias_event_key, - None => key, - }; - let mut ret: &Value = event_value; - for key in event_key.split(".") { - if ret.is_object() == false { - return Option::None; + let event_key = configs::EVENTKEY_ALIAS.get_event_key(key); + if let Some(event_key) = event_key { + let mut ret: &Value = event_value; + // get_event_keyが取得できてget_event_key_splitが取得できないことはない + let splits = configs::EVENTKEY_ALIAS.get_event_key_split(key); + let mut start_idx = 0; + for key in splits.unwrap() { + if ret.is_object() == false { + return Option::None; + } + + let val = &event_key[start_idx..(*key + start_idx)]; + ret = &ret[val]; + start_idx = *key + start_idx; + start_idx += 1; } - ret = &ret[key]; - } - return Option::Some(ret); + return Option::Some(ret); + } else { + let mut ret: &Value = event_value; + let event_key = key; + for key in event_key.split(".") { + if ret.is_object() == false { + return Option::None; + } + ret = &ret[key]; + } + + return Option::Some(ret); + } } pub fn get_thread_num() -> usize { diff --git a/src/main.rs b/src/main.rs index db64be91..7bce8cc9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,6 +15,7 @@ use hhmmss::Hhmmss; use pbr::ProgressBar; use serde_json::Value; use std::collections::HashMap; +use std::fmt::Display; use std::{ fs::{self, File}, path::PathBuf, @@ -125,7 +126,7 @@ fn analysis_files(evtx_files: Vec) { .read() .unwrap() .args - .value_of("level") + .value_of("min-level") .unwrap_or("informational") .to_uppercase(); println!("Analyzing event files: {:?}", evtx_files.len()); @@ -187,19 +188,10 @@ fn analysis_file( continue; } - let data = record_result.unwrap().data; - // target_eventids.txtでフィルタする。 - let eventid = utils::get_event_value(&utils::get_event_id_key(), &data); - if eventid.is_some() { - let is_target = match eventid.unwrap() { - Value::String(s) => utils::is_target_event_id(s), - Value::Number(n) => utils::is_target_event_id(&n.to_string()), - _ => true, // レコードからEventIdが取得できない場合は、特にフィルタしない - }; - if !is_target { - continue; - } + let data = record_result.unwrap().data; + if _is_target_event_id(&data) == false { + continue; } let eventtime = utils::get_event_value(&utils::get_event_time(), &data); @@ -211,9 +203,7 @@ fn analysis_file( } // EvtxRecordInfo構造体に変更 - let data_string = data.to_string(); - let record_info = EvtxRecordInfo::new((&filepath_disp).to_string(), data, data_string); - records_per_detect.push(record_info); + records_per_detect.push(_create_rec_info(data, &filepath_disp)); } if records_per_detect.len() == 0 { break; @@ -233,6 +223,51 @@ fn analysis_file( return detection; } +// target_eventids.txtの設定を元にフィルタする。 +fn _is_target_event_id(data: &Value) -> bool { + let eventid = utils::get_event_value(&utils::get_event_id_key(), data); + if eventid.is_none() { + return true; + } + + return match eventid.unwrap() { + Value::String(s) => utils::is_target_event_id(s), + Value::Number(n) => utils::is_target_event_id(&n.to_string()), + _ => true, // レコードからEventIdが取得できない場合は、特にフィルタしない + }; +} + +// EvtxRecordInfoを作成します。 +fn _create_rec_info(mut data: Value, path: &dyn Display) -> EvtxRecordInfo { + // 高速化のための処理 + // RuleNodeでワイルドカードや正規表現のマッチング処理をする際には、 + // Value(JSON)がstring型以外の場合はstringに変換して比較している。 + // RuleNodeでマッチングする毎にstring変換していると、 + // 1回の処理はそこまででもないが相当回数呼び出されれるとボトルネックになりうる。 + + // なので、よく使われるstring型ではない値を事前に変換しておくことで、 + // string変換する回数を減らせる。 + // 本当はやりたくないが... + match &data["Event"]["System"]["EventID"] { + Value::Number(n) => data["Event"]["System"]["EventID"] = Value::String(n.to_string()), + _ => (), + }; + match &data["Event"]["EventData"]["LogonType"] { + Value::Number(n) => data["Event"]["EventData"]["LogonType"] = Value::String(n.to_string()), + _ => (), + } + match &data["Event"]["EventData"]["DestinationPort"] { + Value::Number(n) => { + data["Event"]["EventData"]["DestinationPort"] = Value::String(n.to_string()) + } + _ => (), + } + + // EvtxRecordInfoを作る + let data_str = data.to_string(); + return EvtxRecordInfo::new(path.to_string(), data, data_str); +} + fn evtx_to_jsons(evtx_filepath: PathBuf) -> Option> { match EvtxParser::from_path(evtx_filepath) { Ok(evtx_parser) => { diff --git a/src/yaml.rs b/src/yaml.rs index 33b50a6a..53a6a0af 100644 --- a/src/yaml.rs +++ b/src/yaml.rs @@ -152,6 +152,19 @@ impl ParseYaml { } } + if !configs::CONFIG + .read() + .unwrap() + .args + .is_present("show-deprecated") + { + let rule_status = &yaml_doc["status"].as_str(); + if rule_status.is_some() && rule_status.unwrap() == "deprecated" { + self.ignorerule_count += 1; + return Option::None; + } + } + return Option::Some((filepath, yaml_doc)); }) .collect(); @@ -279,4 +292,15 @@ mod tests { .unwrap(); assert_eq!(yaml.ignorerule_count, 0); } + #[test] + fn test_exclude_deprecated_rules_file() { + let mut yaml = yaml::ParseYaml::new(); + let path = Path::new("test_files/rules/deprecated"); + let exclude_ids = RuleExclude { + no_use_rule: HashSet::new(), + }; + yaml.read_dir(path.to_path_buf(), &"", &exclude_ids) + .unwrap(); + assert_eq!(yaml.ignorerule_count, 1); + } } diff --git a/test_files/rules/deprecated/1.yml b/test_files/rules/deprecated/1.yml new file mode 100644 index 00000000..621c2c3c --- /dev/null +++ b/test_files/rules/deprecated/1.yml @@ -0,0 +1,30 @@ +title: CreateMiniDump Hacktool +author: Florian Roth +date: 2019/12/22 +description: Detects the use of CreateMiniDump hack tool used to dump the LSASS process + memory for credential extraction on the attacker's machine +detection: + SELECTION_1: + EventID: 11 + SELECTION_2: + TargetFilename: '*\lsass.dmp' + condition: (SELECTION_1 and SELECTION_2) +falsepositives: +- Unknown +id: db2110f3-479d-42a6-94fb-d35bc1e46492 +level: high +logsource: + category: file_event + product: windows +modified: 2021/09/19 +references: +- https://ired.team/offensive-security/credential-access-and-credential-dumping/dumping-lsass-passwords-without-mimikatz-minidumpwritedump-av-signature-bypass +related: +- id: 36d88494-1d43-4dc0-b3fa-35c8fea0ca9d + type: derived +status: deprecated +tags: +- attack.credential_access +- attack.t1003.001 +- attack.t1003 +ruletype: SIGMA