From 7913fbfb95f976312cb3481d71c92c9b66dab57a Mon Sep 17 00:00:00 2001 From: HajimeTakai Date: Sun, 9 May 2021 17:26:17 +0900 Subject: [PATCH 1/8] refactoring --- Cargo.lock | 7 +++++ Cargo.toml | 1 + src/detections/rule.rs | 63 ++++++++++++++++++++++++++++++------------ 3 files changed, 54 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dd0f2dad..6105b6a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -632,6 +632,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "mopa" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a785740271256c230f57462d3b83e52f998433a7062fc18f96d5999474a9f915" + [[package]] name = "ntapi" version = "0.3.6" @@ -1358,6 +1364,7 @@ dependencies = [ "flate2", "lazy_static", "linked-hash-map", + "mopa", "num_cpus", "quick-xml 0.17.2", "regex", diff --git a/Cargo.toml b/Cargo.toml index 54dce47f..a38917e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ yaml-rust = "0.4" linked-hash-map = "0.5.3" tokio = { version = "1", features = ["full"] } num_cpus = "1.13.0" +mopa = "0.2.2" [target.x86_64-pc-windows-gnu] linker = "x86_64-w64-mingw32-gcc" diff --git a/src/detections/rule.rs b/src/detections/rule.rs index 92461e8c..945dfbf3 100644 --- a/src/detections/rule.rs +++ b/src/detections/rule.rs @@ -1,5 +1,7 @@ extern crate regex; +use mopa::mopafy; + use std::vec; use crate::detections::utils; @@ -135,16 +137,17 @@ impl RuleNode { return selection .unwrap() - .get_leaf_nodes() + .get_descendants() .iter() + .filter_map(|node| return node.downcast_ref::()) // mopaというライブラリを使うと簡単にダウンキャストできるらしいです。https://crates.io/crates/mopa .filter(|node| { - // alias.txtのevent_keyに一致するかどうか + // キーがEventIDのノードである let key = utils::get_event_id_key(); if node.get_key() == key { return true; } - // alias.txtのaliasに一致するかどうか + // EventIDのAliasに一致しているかどうか let alias = utils::get_alias(&key); if alias.is_none() { return false; @@ -175,11 +178,13 @@ impl DetectionNode { } // Ruleファイルの detection- selection配下のノードはこのtraitを実装する。 -trait SelectionNode { +trait SelectionNode: mopa::Any { fn select(&self, event_record: &Value) -> bool; fn init(&mut self) -> Result<(), Vec>; - fn get_leaf_nodes(&self) -> Vec<&LeafSelectionNode>; + fn get_childs(&self) -> Vec<&Box>; + fn get_descendants(&self) -> Vec<&Box>; } +mopafy!(SelectionNode); // detection - selection配下でAND条件を表すノード struct AndSelectionNode { @@ -230,17 +235,26 @@ impl SelectionNode for AndSelectionNode { } } - fn get_leaf_nodes(&self) -> Vec<&LeafSelectionNode> { + fn get_childs(&self) -> Vec<&Box> { let mut ret = vec![]; + self.child_nodes.iter().for_each(|child_node| { + ret.push(child_node); + }); + + return ret; + } + + fn get_descendants(&self) -> Vec<&Box> { + let mut ret = self.get_childs(); self.child_nodes .iter() - .map(|child| { - return child.get_leaf_nodes(); + .map(|child_node| { + return child_node.get_descendants(); }) .flatten() - .for_each(|descendant| { - ret.push(descendant); + .for_each(|descendant_node| { + ret.push(descendant_node); }); return ret; @@ -296,17 +310,26 @@ impl SelectionNode for OrSelectionNode { } } - fn get_leaf_nodes(&self) -> Vec<&LeafSelectionNode> { + fn get_childs(&self) -> Vec<&Box> { let mut ret = vec![]; + self.child_nodes.iter().for_each(|child_node| { + ret.push(child_node); + }); + + return ret; + } + + fn get_descendants(&self) -> Vec<&Box> { + let mut ret = self.get_childs(); self.child_nodes .iter() - .map(|child| { - return child.get_leaf_nodes(); + .map(|child_node| { + return child_node.get_descendants(); }) .flatten() - .for_each(|descendant| { - ret.push(descendant); + .for_each(|descendant_node| { + ret.push(descendant_node); }); return ret; @@ -453,8 +476,12 @@ impl SelectionNode for LeafSelectionNode { .init(&match_key_list, &self.select_value); } - fn get_leaf_nodes(&self) -> Vec<&LeafSelectionNode> { - return vec![&self]; + fn get_childs(&self) -> Vec<&Box> { + return vec![]; + } + + fn get_descendants(&self) -> Vec<&Box> { + return vec![]; } } @@ -727,3 +754,5 @@ impl LeafMatcher for WhitelistFileMatcher { }; } } + + From 61ae299e4b7f25dce4a369f0deeb5aed353c2ae8 Mon Sep 17 00:00:00 2001 From: HajimeTakai Date: Mon, 10 May 2021 00:14:50 +0900 Subject: [PATCH 2/8] underconstructing --- src/detections/rule.rs | 159 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 158 insertions(+), 1 deletion(-) diff --git a/src/detections/rule.rs b/src/detections/rule.rs index 945dfbf3..c43ba4b2 100644 --- a/src/detections/rule.rs +++ b/src/detections/rule.rs @@ -490,13 +490,14 @@ impl SelectionNode for LeafSelectionNode { // // 新規にLeafMatcherを実装するクラスを作成した場合、 // LeafSelectionNodeのget_matchersクラスの戻り値の配列に新規作成したクラスのインスタンスを追加する。 -trait LeafMatcher { +trait LeafMatcher: mopa::Any { fn is_target_key(&self, key_list: &Vec) -> bool; fn is_match(&self, event_value: Option<&Value>) -> bool; fn init(&mut self, key_list: &Vec, select_value: &Yaml) -> Result<(), Vec>; } +mopafy!(LeafMatcher); // 正規表現で比較するロジックを表すクラス struct RegexMatcher { @@ -755,4 +756,160 @@ impl LeafMatcher for WhitelistFileMatcher { } } +#[cfg(test)] +mod tests { + use yaml_rust::YamlLoader; + use crate::detections::rule::{ + parse_rule, AndSelectionNode, LeafSelectionNode, MinlengthMatcher, OrSelectionNode, + RegexMatcher, SelectionNode, + }; + + #[test] + fn test_rule_parse() { + // ルールファイルをYAML形式で読み込み + let rule_str = r#" + title: PowerShell Execution Pipeline + description: hogehoge + enabled: true + author: Yea + logsource: + product: windows + detection: + selection: + Channel: Microsoft-Windows-PowerShell/Operational + EventID: 4103 + ContextInfo: + - Host Application + - ホスト アプリケーション + ImagePath: + min_length: 1234321 + falsepositives: + - unknown + level: medium + output: 'command=%CommandLine%' + creation_date: 2020/11/8 + updated_date: 2020/11/8 + "#; + let rule_yaml = YamlLoader::load_from_str(rule_str); + assert_eq!(rule_yaml.is_ok(), true); + let rule_yamls = rule_yaml.unwrap(); + + let mut rule_yaml = rule_yamls.into_iter(); + let mut rule_node = parse_rule(rule_yaml.next().unwrap()); + assert_eq!(rule_node.init().is_ok(), true); + let selection_node = rule_node.detection.unwrap().selection.unwrap(); + + // Root + let detection_childs = selection_node.get_childs(); + assert_eq!(detection_childs.len(), 4); + + // Channel + { + // LeafSelectionNodeが正しく読み込めることを確認 + let child_node = detection_childs[0]; + assert_eq!(child_node.is::(), true); + let child_node = child_node.downcast_ref::().unwrap(); + assert_eq!(child_node.get_key(), "Channel"); + assert_eq!(child_node.get_childs().len(), 0); + + // 比較する正規表現が正しいことを確認 + let matcher = &child_node.matcher; + assert_eq!(matcher.is_some(), true); + let matcher = child_node.matcher.as_ref().unwrap(); + assert_eq!(matcher.is::(), true); + let matcher = matcher.downcast_ref::().unwrap(); + + assert_eq!(matcher.re.is_some(), true); + let re = matcher.re.as_ref(); + assert_eq!( + re.unwrap().as_str(), + "Microsoft-Windows-PowerShell/Operational" + ); + } + + // EventID + { + // LeafSelectionNodeが正しく読み込めることを確認 + let child_node = detection_childs[1]; + assert_eq!(child_node.is::(), true); + let child_node = child_node.downcast_ref::().unwrap(); + assert_eq!(child_node.get_key(), "EventID"); + assert_eq!(child_node.get_childs().len(), 0); + + // 比較する正規表現が正しいことを確認 + let matcher = &child_node.matcher; + assert_eq!(matcher.is_some(), true); + let matcher = child_node.matcher.as_ref().unwrap(); + assert_eq!(matcher.is::(), true); + let matcher = matcher.downcast_ref::().unwrap(); + + assert_eq!(matcher.re.is_some(), true); + let re = matcher.re.as_ref(); + assert_eq!(re.unwrap().as_str(), "4103"); + } + + // ContextInfo + { + // OrSelectionNodeを正しく読み込めることを確認 + let child_node = detection_childs[2]; + assert_eq!(child_node.is::(), true); + let child_node = child_node.downcast_ref::().unwrap(); + let ancestors = child_node.get_childs(); + assert_eq!(ancestors.len(), 2); + + // OrSelectionNodeの下にLeafSelectionNodeがあるパターンをテスト + // LeafSelectionNodeである、Host Applicationノードが正しいことを確認 + let hostapp_en_node = ancestors[0]; + assert_eq!(hostapp_en_node.is::(), true); + let hostapp_en_node = hostapp_en_node.downcast_ref::().unwrap(); + + let hostapp_en_matcher = &hostapp_en_node.matcher; + assert_eq!(hostapp_en_matcher.is_some(), true); + let hostapp_en_matcher = hostapp_en_matcher.as_ref().unwrap(); + assert_eq!(hostapp_en_matcher.is::(), true); + 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"); + + // LeafSelectionNodeである、ホスト アプリケーションノードが正しいことを確認 + let hostapp_jp_node = ancestors[1]; + assert_eq!(hostapp_jp_node.is::(), true); + let hostapp_jp_node = hostapp_jp_node.downcast_ref::().unwrap(); + + let hostapp_jp_matcher = &hostapp_jp_node.matcher; + assert_eq!(hostapp_jp_matcher.is_some(), true); + let hostapp_jp_matcher = hostapp_jp_matcher.as_ref().unwrap(); + assert_eq!(hostapp_jp_matcher.is::(), true); + 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(), "ホスト アプリケーション"); + } + + // ImagePath + { + // AndSelectionNodeを正しく読み込めることを確認 + let child_node = detection_childs[3]; + assert_eq!(child_node.is::(), true); + let child_node = child_node.downcast_ref::().unwrap(); + let ancestors = child_node.get_childs(); + assert_eq!(ancestors.len(), 1); + + // min-lenが正しく読み込めることを確認 + { + let ancestor_node = ancestors[0]; + assert_eq!(ancestor_node.is::(), true); + let ancestor_node = ancestor_node.downcast_ref::().unwrap(); + + let ancestor_node = &ancestor_node.matcher; + assert_eq!(ancestor_node.is_some(), true); + let ancestor_matcher = ancestor_node.as_ref().unwrap(); + assert_eq!(ancestor_matcher.is::(), true); + let ancestor_matcher = ancestor_matcher.downcast_ref::().unwrap(); + assert_eq!(ancestor_matcher.min_len, 1234321); + } + } + } +} From b9752e567db547dfab3c9d5b52603e2bd53031d0 Mon Sep 17 00:00:00 2001 From: HajimeTakai Date: Mon, 10 May 2021 00:41:20 +0900 Subject: [PATCH 3/8] underconstructing --- src/detections/rule.rs | 46 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/src/detections/rule.rs b/src/detections/rule.rs index c43ba4b2..1ba8dce7 100644 --- a/src/detections/rule.rs +++ b/src/detections/rule.rs @@ -762,7 +762,7 @@ mod tests { use crate::detections::rule::{ parse_rule, AndSelectionNode, LeafSelectionNode, MinlengthMatcher, OrSelectionNode, - RegexMatcher, SelectionNode, + RegexMatcher, RegexesFileMatcher, SelectionNode, }; #[test] @@ -784,6 +784,8 @@ mod tests { - ホスト アプリケーション ImagePath: min_length: 1234321 + regexes: ./regexes.txt + whitelist: ./whitelist.txt falsepositives: - unknown level: medium @@ -895,7 +897,7 @@ mod tests { assert_eq!(child_node.is::(), true); let child_node = child_node.downcast_ref::().unwrap(); let ancestors = child_node.get_childs(); - assert_eq!(ancestors.len(), 1); + assert_eq!(ancestors.len(), 3); // min-lenが正しく読み込めることを確認 { @@ -910,6 +912,46 @@ mod tests { let ancestor_matcher = ancestor_matcher.downcast_ref::().unwrap(); assert_eq!(ancestor_matcher.min_len, 1234321); } + + // regexesが正しく読み込めることを確認 + { + let ancestor_node = ancestors[1]; + assert_eq!(ancestor_node.is::(), true); + let ancestor_node = ancestor_node.downcast_ref::().unwrap(); + + let ancestor_node = &ancestor_node.matcher; + assert_eq!(ancestor_node.is_some(), true); + let ancestor_matcher = ancestor_node.as_ref().unwrap(); + assert_eq!(ancestor_matcher.is::(), true); + let ancestor_matcher = ancestor_matcher + .downcast_ref::() + .unwrap(); + + // regexes.txtの中身と一致していることを確認 + let csvcontent = &ancestor_matcher.regexes_csv_content; + assert_eq!(csvcontent.len(), 14); + + let firstcontent = &csvcontent[0]; + assert_eq!(firstcontent.len(), 3); + assert_eq!(firstcontent[0], "0"); + assert_eq!( + firstcontent[1], + r"^cmd.exe /c echo [a-z]{6} > \\\\.\\pipe\\[a-z]{6}$" + ); + assert_eq!( + firstcontent[2], + r"Metasploit-style cmd with pipe (possible use of Meterpreter 'getsystem')" + ); + + let lastcontent = &csvcontent[13]; + assert_eq!(lastcontent.len(), 3); + assert_eq!(lastcontent[0], "0"); + assert_eq!( + lastcontent[1], + r"\\cvtres\.exe.*\\AppData\\Local\\Temp\\[A-Z0-9]{7}\.tmp" + ); + assert_eq!(lastcontent[2], r"PSAttack-style command via cvtres.exe"); + } } } } From 4e68e75cb2d2fa691998a192ebc38eff8e1c0e77 Mon Sep 17 00:00:00 2001 From: HajimeTakai Date: Wed, 12 May 2021 22:45:38 +0900 Subject: [PATCH 4/8] add testcase --- src/detections/rule.rs | 1278 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 1266 insertions(+), 12 deletions(-) diff --git a/src/detections/rule.rs b/src/detections/rule.rs index 1ba8dce7..047d50d4 100644 --- a/src/detections/rule.rs +++ b/src/detections/rule.rs @@ -748,10 +748,12 @@ impl LeafMatcher for WhitelistFileMatcher { fn is_match(&self, event_value: Option<&Value>) -> bool { return match event_value.unwrap_or(&Value::Null) { - Value::String(s) => utils::check_whitelist(s, &self.whitelist_csv_content), - Value::Number(n) => utils::check_whitelist(&n.to_string(), &self.whitelist_csv_content), - Value::Bool(b) => utils::check_whitelist(&b.to_string(), &self.whitelist_csv_content), - _ => false, + Value::String(s) => !utils::check_whitelist(s, &self.whitelist_csv_content), + Value::Number(n) => { + !utils::check_whitelist(&n.to_string(), &self.whitelist_csv_content) + } + Value::Bool(b) => !utils::check_whitelist(&b.to_string(), &self.whitelist_csv_content), + _ => true, }; } } @@ -762,9 +764,11 @@ mod tests { use crate::detections::rule::{ parse_rule, AndSelectionNode, LeafSelectionNode, MinlengthMatcher, OrSelectionNode, - RegexMatcher, RegexesFileMatcher, SelectionNode, + RegexMatcher, RegexesFileMatcher, SelectionNode, WhitelistFileMatcher, }; + use super::RuleNode; + #[test] fn test_rule_parse() { // ルールファイルをYAML形式で読み込み @@ -793,13 +797,7 @@ mod tests { creation_date: 2020/11/8 updated_date: 2020/11/8 "#; - let rule_yaml = YamlLoader::load_from_str(rule_str); - assert_eq!(rule_yaml.is_ok(), true); - let rule_yamls = rule_yaml.unwrap(); - - let mut rule_yaml = rule_yamls.into_iter(); - let mut rule_node = parse_rule(rule_yaml.next().unwrap()); - assert_eq!(rule_node.init().is_ok(), true); + let rule_node = parse_rule_from_str(rule_str); let selection_node = rule_node.detection.unwrap().selection.unwrap(); // Root @@ -952,6 +950,1262 @@ mod tests { ); assert_eq!(lastcontent[2], r"PSAttack-style command via cvtres.exe"); } + + // whitelist.txtが読み込めることを確認 + { + let ancestor_node = ancestors[2]; + assert_eq!(ancestor_node.is::(), true); + let ancestor_node = ancestor_node.downcast_ref::().unwrap(); + + let ancestor_node = &ancestor_node.matcher; + assert_eq!(ancestor_node.is_some(), true); + let ancestor_matcher = ancestor_node.as_ref().unwrap(); + assert_eq!(ancestor_matcher.is::(), true); + let ancestor_matcher = ancestor_matcher + .downcast_ref::() + .unwrap(); + + let csvcontent = &ancestor_matcher.whitelist_csv_content; + assert_eq!(csvcontent.len(), 2); + + assert_eq!( + csvcontent[0][0], + r#"^"C:\\Program Files\\Google\\Chrome\\Application\\chrome\.exe""#.to_string() + ); + assert_eq!( + csvcontent[1][0], + r#"^"C:\\Program Files\\Google\\Update\\GoogleUpdate\.exe""#.to_string() + ); + } } } + + #[test] + fn test_get_event_ids() { + let rule_str = r#" + enabled: true + detection: + selection: + EventID: 1234 + output: 'command=%CommandLine%' + "#; + let rule_node = parse_rule_from_str(rule_str); + let event_ids = rule_node.get_event_ids(); + assert_eq!(event_ids.len(), 1); + assert_eq!(event_ids[0], 1234); + } + + #[test] + fn test_notdetect_regex_eventid() { + // 完全一致なので、前方一致で検知しないことを確認 + let rule_str = r#" + enabled: true + detection: + selection: + EventID: 4103 + output: 'command=%CommandLine%' + "#; + + let record_json_str = r#" + { + "Event": {"System": {"EventID": 410}}, + "Event_attributes": {"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"} + }"#; + + let rule_node = parse_rule_from_str(rule_str); + let selection_node = rule_node.detection.unwrap().selection.unwrap(); + + match serde_json::from_str(record_json_str) { + Ok(record) => { + assert_eq!(selection_node.select(&record), false); + } + Err(_) => { + assert!(false, "failed to parse json record."); + } + } + } + + #[test] + fn test_notdetect_regex_eventid2() { + // 完全一致なので、後方一致で検知しないことを確認 + let rule_str = r#" + enabled: true + detection: + selection: + EventID: 4103 + output: 'command=%CommandLine%' + "#; + + let record_json_str = r#" + { + "Event": {"System": {"EventID": 103}}, + "Event_attributes": {"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"} + }"#; + + let rule_node = parse_rule_from_str(rule_str); + let selection_node = rule_node.detection.unwrap().selection.unwrap(); + + match serde_json::from_str(record_json_str) { + Ok(record) => { + assert_eq!(selection_node.select(&record), false); + } + Err(_) => { + assert!(false, "failed to parse json record."); + } + } + } + + #[test] + fn test_detect_regex_eventid() { + // これはEventID=4103で検知するはず + let rule_str = r#" + enabled: true + detection: + selection: + EventID: 4103 + output: 'command=%CommandLine%' + "#; + + let record_json_str = r#" + { + "Event": {"System": {"EventID": 4103}}, + "Event_attributes": {"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"} + }"#; + + let rule_node = parse_rule_from_str(rule_str); + let selection_node = rule_node.detection.unwrap().selection.unwrap(); + + match serde_json::from_str(record_json_str) { + Ok(record) => { + assert_eq!(selection_node.select(&record), true); + } + Err(_) => { + assert!(false, "failed to parse json record."); + } + } + } + + #[test] + fn test_notdetect_regex_str() { + // 文字列っぽいデータでも確認 + // 完全一致なので、前方一致しないことを確認 + let rule_str = r#" + enabled: true + detection: + selection: + Channel: Security + output: 'command=%CommandLine%' + "#; + + let record_json_str = r#" + { + "Event": {"System": {"EventID": 4103, "Channel": "Securit"}}, + "Event_attributes": {"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"} + }"#; + + let rule_node = parse_rule_from_str(rule_str); + let selection_node = rule_node.detection.unwrap().selection.unwrap(); + + match serde_json::from_str(record_json_str) { + Ok(record) => { + assert_eq!(selection_node.select(&record), false); + } + Err(_) => { + assert!(false, "failed to parse json record."); + } + } + } + + #[test] + fn test_notdetect_regex_str2() { + // 文字列っぽいデータでも確認 + // 完全一致なので、後方一致しないことを確認 + let rule_str = r#" + enabled: true + detection: + selection: + Channel: Security + output: 'command=%CommandLine%' + "#; + + let record_json_str = r#" + { + "Event": {"System": {"EventID": 4103, "Channel": "ecurity"}}, + "Event_attributes": {"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"} + }"#; + + let rule_node = parse_rule_from_str(rule_str); + let selection_node = rule_node.detection.unwrap().selection.unwrap(); + + match serde_json::from_str(record_json_str) { + Ok(record) => { + assert_eq!(selection_node.select(&record), false); + } + Err(_) => { + assert!(false, "failed to parse json record."); + } + } + } + #[test] + fn test_detect_regex_str() { + // 文字列っぽいデータでも完全一致することを確認 + let rule_str = r#" + enabled: true + detection: + selection: + Channel: Security + output: 'command=%CommandLine%' + "#; + + let record_json_str = r#" + { + "Event": {"System": {"EventID": 4103, "Channel": "Security"}}, + "Event_attributes": {"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"} + }"#; + + let rule_node = parse_rule_from_str(rule_str); + let selection_node = rule_node.detection.unwrap().selection.unwrap(); + + match serde_json::from_str(record_json_str) { + Ok(record) => { + assert_eq!(selection_node.select(&record), true); + } + Err(_) => { + assert!(false, "failed to parse json record."); + } + } + } + + #[test] + fn test_notdetect_regex_emptystr() { + // 文字列っぽいデータでも完全一致することを確認 + let rule_str = r#" + enabled: true + detection: + selection: + Channel: Security + output: 'command=%CommandLine%' + "#; + + let record_json_str = r#" + { + "Event": {"System": {"Channel": ""}}, + "Event_attributes": {"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"} + }"#; + + let rule_node = parse_rule_from_str(rule_str); + let selection_node = rule_node.detection.unwrap().selection.unwrap(); + + match serde_json::from_str(record_json_str) { + Ok(record) => { + assert_eq!(selection_node.select(&record), false); + } + Err(_) => { + assert!(false, "failed to parse json record."); + } + } + } + + #[test] + fn test_detect_mutiple_regex_and() { + // AND条件が正しく検知することを確認する。 + let rule_str = r#" + enabled: true + detection: + selection: + Channel: Security + EventID: 4103 + output: 'command=%CommandLine%' + "#; + + let record_json_str = r#" + { + "Event": {"System": {"EventID": 4103, "Channel": "Security"}}, + "Event_attributes": {"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"} + }"#; + + let rule_node = parse_rule_from_str(rule_str); + let selection_node = rule_node.detection.unwrap().selection.unwrap(); + + match serde_json::from_str(record_json_str) { + Ok(record) => { + assert_eq!(selection_node.select(&record), true); + } + Err(_) => { + assert!(false, "failed to parse json record."); + } + } + } + + #[test] + fn test_notdetect_mutiple_regex_and() { + // AND条件で一つでも条件に一致しないと、検知しないことを確認 + // この例ではComputerの値が異なっている。 + let rule_str = r#" + enabled: true + detection: + selection: + Channel: Security + EventID: 4103 + Computer: DESKTOP-ICHIICHIN + output: 'command=%CommandLine%' + "#; + + let record_json_str = r#" + { + "Event": {"System": {"EventID": 4103, "Channel": "Security", "Computer":"DESKTOP-ICHIICHI"}}, + "Event_attributes": {"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"} + }"#; + + let rule_node = parse_rule_from_str(rule_str); + let selection_node = rule_node.detection.unwrap().selection.unwrap(); + + match serde_json::from_str(record_json_str) { + Ok(record) => { + assert_eq!(selection_node.select(&record), false); + } + Err(_) => { + assert!(false, "failed to parse json record."); + } + } + } + + #[test] + fn test_detect_dotkey() { + // aliasじゃなくて、.区切りでつなげるケースが正しく検知できる。 + let rule_str = r#" + enabled: true + detection: + selection: + Event.System.Computer: DESKTOP-ICHIICHI + output: 'command=%CommandLine%' + "#; + + let record_json_str = r#" + { + "Event": {"System": {"EventID": 4103, "Channel": "Security", "Computer":"DESKTOP-ICHIICHI"}}, + "Event_attributes": {"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"} + }"#; + + let rule_node = parse_rule_from_str(rule_str); + let selection_node = rule_node.detection.unwrap().selection.unwrap(); + + match serde_json::from_str(record_json_str) { + Ok(record) => { + assert_eq!(selection_node.select(&record), true); + } + Err(_) => { + assert!(false, "failed to parse json record."); + } + } + } + + #[test] + fn test_notdetect_dotkey() { + // aliasじゃなくて、.区切りでつなげるケースで、検知しないはずのケースで検知しないことを確かめる。 + let rule_str = r#" + enabled: true + detection: + selection: + Event.System.Computer: DESKTOP-ICHIICHIN + output: 'command=%CommandLine%' + "#; + + let record_json_str = r#" + { + "Event": {"System": {"EventID": 4103, "Channel": "Security", "Computer":"DESKTOP-ICHIICHI"}}, + "Event_attributes": {"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"} + }"#; + + let rule_node = parse_rule_from_str(rule_str); + let selection_node = rule_node.detection.unwrap().selection.unwrap(); + + match serde_json::from_str(record_json_str) { + Ok(record) => { + assert_eq!(selection_node.select(&record), false); + } + Err(_) => { + assert!(false, "failed to parse json record."); + } + } + } + + #[test] + fn test_notdetect_differentkey() { + // aliasじゃなくて、.区切りでつなげるケースで、検知しないはずのケースで検知しないことを確かめる。 + let rule_str = r#" + enabled: true + detection: + selection: + Channel: NOTDETECT + output: 'command=%CommandLine%' + "#; + + let record_json_str = r#" + { + "Event": {"System": {"EventID": 4103, "Channel": "Security", "Computer":"DESKTOP-ICHIICHI"}}, + "Event_attributes": {"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"} + }"#; + + let rule_node = parse_rule_from_str(rule_str); + let selection_node = rule_node.detection.unwrap().selection.unwrap(); + + match serde_json::from_str(record_json_str) { + Ok(record) => { + assert_eq!(selection_node.select(&record), false); + } + Err(_) => { + assert!(false, "failed to parse json record."); + } + } + } + + #[test] + fn test_detect_or() { + // OR条件が正しく検知できることを確認 + let rule_str = r#" + enabled: true + detection: + selection: + Channel: + - PowerShell + - Security + output: 'command=%CommandLine%' + "#; + + let record_json_str = r#" + { + "Event": {"System": {"EventID": 4103, "Channel": "Security", "Computer":"DESKTOP-ICHIICHI"}}, + "Event_attributes": {"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"} + }"#; + + let rule_node = parse_rule_from_str(rule_str); + let selection_node = rule_node.detection.unwrap().selection.unwrap(); + + match serde_json::from_str(record_json_str) { + Ok(record) => { + assert_eq!(selection_node.select(&record), true); + } + Err(_) => { + assert!(false, "failed to parse json record."); + } + } + } + + #[test] + fn test_detect_or2() { + // OR条件が正しく検知できることを確認 + let rule_str = r#" + enabled: true + detection: + selection: + Channel: + - PowerShell + - Security + output: 'command=%CommandLine%' + "#; + + let record_json_str = r#" + { + "Event": {"System": {"EventID": 4103, "Channel": "PowerShell", "Computer":"DESKTOP-ICHIICHI"}}, + "Event_attributes": {"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"} + }"#; + + let rule_node = parse_rule_from_str(rule_str); + let selection_node = rule_node.detection.unwrap().selection.unwrap(); + + match serde_json::from_str(record_json_str) { + Ok(record) => { + assert_eq!(selection_node.select(&record), true); + } + Err(_) => { + assert!(false, "failed to parse json record."); + } + } + } + + #[test] + fn test_notdetect_or() { + // OR条件が正しく検知できることを確認 + let rule_str = r#" + enabled: true + detection: + selection: + Channel: + - PowerShell + - Security + output: 'command=%CommandLine%' + "#; + + let record_json_str = r#" + { + "Event": {"System": {"EventID": 4103, "Channel": "not detect", "Computer":"DESKTOP-ICHIICHI"}}, + "Event_attributes": {"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"} + }"#; + + let rule_node = parse_rule_from_str(rule_str); + let selection_node = rule_node.detection.unwrap().selection.unwrap(); + + match serde_json::from_str(record_json_str) { + Ok(record) => { + assert_eq!(selection_node.select(&record), false); + } + Err(_) => { + assert!(false, "failed to parse json record."); + } + } + } + + #[test] + fn test_notdetect_casesensetive() { + // OR条件が正しく検知できることを確認 + let rule_str = r#" + enabled: true + detection: + selection: + Channel: Security + output: 'command=%CommandLine%' + "#; + + let record_json_str = r#" + { + "Event": {"System": {"EventID": 4103, "Channel": "security", "Computer":"DESKTOP-ICHIICHI"}}, + "Event_attributes": {"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"} + }"#; + + let rule_node = parse_rule_from_str(rule_str); + let selection_node = rule_node.detection.unwrap().selection.unwrap(); + + match serde_json::from_str(record_json_str) { + Ok(record) => { + assert_eq!(selection_node.select(&record), false); + } + Err(_) => { + assert!(false, "failed to parse json record."); + } + } + } + + #[test] + fn test_notdetect_minlen() { + // minlenが正しく検知できることを確認 + let rule_str = r#" + enabled: true + detection: + selection: + Channel: + min_length: 10 + output: 'command=%CommandLine%' + "#; + + let record_json_str = r#" + { + "Event": {"System": {"EventID": 4103, "Channel": "Security9", "Computer":"DESKTOP-ICHIICHI"}}, + "Event_attributes": {"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"} + }"#; + + let rule_node = parse_rule_from_str(rule_str); + let selection_node = rule_node.detection.unwrap().selection.unwrap(); + + match serde_json::from_str(record_json_str) { + Ok(record) => { + assert_eq!(selection_node.select(&record), false); + } + Err(_) => { + assert!(false, "failed to parse json record."); + } + } + } + + #[test] + fn test_detect_minlen() { + // minlenが正しく検知できることを確認 + let rule_str = r#" + enabled: true + detection: + selection: + Channel: + min_length: 10 + output: 'command=%CommandLine%' + "#; + + let record_json_str = r#" + { + "Event": {"System": {"EventID": 4103, "Channel": "Security10", "Computer":"DESKTOP-ICHIICHI"}}, + "Event_attributes": {"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"} + }"#; + + let rule_node = parse_rule_from_str(rule_str); + let selection_node = rule_node.detection.unwrap().selection.unwrap(); + + match serde_json::from_str(record_json_str) { + Ok(record) => { + assert_eq!(selection_node.select(&record), true); + } + Err(_) => { + assert!(false, "failed to parse json record."); + } + } + } + + #[test] + fn test_detect_minlen2() { + // minlenが正しく検知できることを確認 + let rule_str = r#" + enabled: true + detection: + selection: + Channel: + min_length: 10 + output: 'command=%CommandLine%' + "#; + + let record_json_str = r#" + { + "Event": {"System": {"EventID": 4103, "Channel": "Security.11", "Computer":"DESKTOP-ICHIICHI"}}, + "Event_attributes": {"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"} + }"#; + + let rule_node = parse_rule_from_str(rule_str); + let selection_node = rule_node.detection.unwrap().selection.unwrap(); + + match serde_json::from_str(record_json_str) { + Ok(record) => { + assert_eq!(selection_node.select(&record), true); + } + Err(_) => { + assert!(false, "failed to parse json record."); + } + } + } + + #[test] + fn test_detect_minlen_and() { + // minlenが正しく検知できることを確認 + let rule_str = r#" + enabled: true + detection: + selection: + Channel: + regex: Security10 + min_length: 10 + output: 'command=%CommandLine%' + "#; + + let record_json_str = r#" + { + "Event": {"System": {"EventID": 4103, "Channel": "Security10", "Computer":"DESKTOP-ICHIICHI"}}, + "Event_attributes": {"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"} + }"#; + + let rule_node = parse_rule_from_str(rule_str); + let selection_node = rule_node.detection.unwrap().selection.unwrap(); + + match serde_json::from_str(record_json_str) { + Ok(record) => { + assert_eq!(selection_node.select(&record), true); + } + Err(_) => { + assert!(false, "failed to parse json record."); + } + } + } + + #[test] + fn test_notdetect_minlen_and() { + // minlenが正しく検知できることを確認 + let rule_str = r#" + enabled: true + detection: + selection: + Channel: + regex: Security10 + min_length: 11 + output: 'command=%CommandLine%' + "#; + + let record_json_str = r#" + { + "Event": {"System": {"EventID": 4103, "Channel": "Security10", "Computer":"DESKTOP-ICHIICHI"}}, + "Event_attributes": {"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"} + }"#; + + let rule_node = parse_rule_from_str(rule_str); + let selection_node = rule_node.detection.unwrap().selection.unwrap(); + + match serde_json::from_str(record_json_str) { + Ok(record) => { + assert_eq!(selection_node.select(&record), false); + } + Err(_) => { + assert!(false, "failed to parse json record."); + } + } + } + + #[test] + fn test_detect_regex() { + // 正規表現が使えることを確認 + let rule_str = r#" + enabled: true + detection: + selection: + Channel: ^Program$ + output: 'command=%CommandLine%' + "#; + + let record_json_str = r#" + { + "Event": {"System": {"EventID": 4103, "Channel": "Program", "Computer":"DESKTOP-ICHIICHI"}}, + "Event_attributes": {"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"} + }"#; + + let rule_node = parse_rule_from_str(rule_str); + let selection_node = rule_node.detection.unwrap().selection.unwrap(); + + match serde_json::from_str(record_json_str) { + Ok(record) => { + assert_eq!(selection_node.select(&record), true); + } + Err(_) => { + assert!(false, "failed to parse json record."); + } + } + } + + #[test] + fn test_detect_regexes() { + // regexes.txtが正しく検知できることを確認 + // この場合ではEventIDが一致しているが、whitelistに一致するので検知しないはず。 + let rule_str = r#" + enabled: true + detection: + selection: + EventID: 4103 + Channel: + - whitelist: whitelist.txt + output: 'command=%CommandLine%' + "#; + + // JSONで値としてダブルクオートを使う場合、\でエスケープが必要なのに注意 + let record_json_str = r#" + { + "Event": {"System": {"EventID": 4103, "Channel": "\"C:\\Program Files\\Google\\Update\\GoogleUpdate.exe\"", "Computer":"DESKTOP-ICHIICHI"}}, + "Event_attributes": {"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"} + }"#; + + let rule_node = parse_rule_from_str(rule_str); + let selection_node = rule_node.detection.unwrap().selection.unwrap(); + + match serde_json::from_str(record_json_str) { + Ok(record) => { + assert_eq!(selection_node.select(&record), false); + } + Err(_) => { + assert!(false, "failed to parse json record."); + } + } + } + + #[test] + fn test_detect_whitelist() { + // whitelistが正しく検知できることを確認 + // この場合ではEventIDが一致しているが、whitelistに一致するので検知しないはず。 + let rule_str = r#" + enabled: true + detection: + selection: + EventID: 4103 + Channel: + - whitelist: whitelist.txt + output: 'command=%CommandLine%' + "#; + + // JSONで値としてダブルクオートを使う場合、\でエスケープが必要なのに注意 + let record_json_str = r#" + { + "Event": {"System": {"EventID": 4103, "Channel": "\"C:\\Program Files\\Google\\Update\\GoogleUpdate.exe\"", "Computer":"DESKTOP-ICHIICHI"}}, + "Event_attributes": {"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"} + }"#; + + let rule_node = parse_rule_from_str(rule_str); + let selection_node = rule_node.detection.unwrap().selection.unwrap(); + + match serde_json::from_str(record_json_str) { + Ok(record) => { + assert_eq!(selection_node.select(&record), false); + } + Err(_) => { + assert!(false, "failed to parse json record."); + } + } + } + + #[test] + fn test_detect_whitelist2() { + // whitelistが正しく検知できることを確認 + // この場合ではEventIDが一致しているが、whitelistに一致するので検知しないはず。 + let rule_str = r#" + enabled: true + detection: + selection: + EventID: 4103 + Channel: + - whitelist: whitelist.txt + output: 'command=%CommandLine%' + "#; + + let record_json_str = r#" + { + "Event": {"System": {"EventID": 4103, "Channel": "\"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe\"", "Computer":"DESKTOP-ICHIICHI"}}, + "Event_attributes": {"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"} + }"#; + + let rule_node = parse_rule_from_str(rule_str); + let selection_node = rule_node.detection.unwrap().selection.unwrap(); + + match serde_json::from_str(record_json_str) { + Ok(record) => { + assert_eq!(selection_node.select(&record), false); + } + Err(_) => { + assert!(false, "failed to parse json record."); + } + } + } + + #[test] + fn test_detect_attribute() { + // XMLのタグのattributionの部分に値がある場合、JSONが特殊な感じでパースされるのでそのテスト + // 元のXMLは下記のような感じで、Providerタグの部分のNameとかGuidを検知するテスト + /* - + - + + 4672 + 0 + 0 + 12548 + 0 + 0x8020000000000000 + + 244666 + + + Security + + + - + SYSTEM + NT AUTHORITY + SeAssignPrimaryTokenPrivilege SeTcbPrivilege SeSecurityPrivilege SeTakeOwnershipPrivilege SeLoadDriverPrivilege SeBackupPrivilege SeRestorePrivilege SeDebugPrivilege SeAuditPrivilege SeSystemEnvironmentPrivilege SeImpersonatePrivilege SeDelegateSessionUserImpersonatePrivilege + + */ + + let rule_str = r#" + enabled: true + detection: + selection: + EventID: 4797 + Event.System.Provider_attributes.Guid: 54849625-5478-4994-A5BA-3E3B0328C30D + output: 'command=%CommandLine%' + "#; + + let record_json_str = r#" + { + "Event": { + "System": { + "Channel": "Security", + "Correlation_attributes": { + "ActivityID": "0188DD7A-447D-000C-82DD-88017D44D701" + }, + "EventID": 4797, + "EventRecordID": 239219, + "Execution_attributes": { + "ProcessID": 1172, + "ThreadID": 23236 + }, + "Keywords": "0x8020000000000000", + "Level": 0, + "Opcode": 0, + "Provider_attributes": { + "Guid": "54849625-5478-4994-A5BA-3E3B0328C30D", + "Name": "Microsoft-Windows-Security-Auditing" + }, + "Security": null, + "Task": 13824, + "TimeCreated_attributes": { + "SystemTime": "2021-05-12T09:39:19.828403Z" + }, + "Version": 0 + } + }, + "Event_attributes": { + "xmlns": "http://schemas.microsoft.com/win/2004/08/events/event" + } + }"#; + + let rule_node = parse_rule_from_str(rule_str); + let selection_node = rule_node.detection.unwrap().selection.unwrap(); + + match serde_json::from_str(record_json_str) { + Ok(record) => { + assert_eq!(selection_node.select(&record), true); + } + Err(_) => { + assert!(false, "failed to parse json record."); + } + } + } + + #[test] + fn test_notdetect_attribute() { + // XMLのタグのattributionの検知しないケースを確認 + let rule_str = r#" + enabled: true + detection: + selection: + EventID: 4797 + Event.System.Provider_attributes.Guid: 54849625-5478-4994-A5BA-3E3B0328C30DSS + output: 'command=%CommandLine%' + "#; + + let record_json_str = r#" + { + "Event": { + "System": { + "Channel": "Security", + "Correlation_attributes": { + "ActivityID": "0188DD7A-447D-000C-82DD-88017D44D701" + }, + "EventID": 4797, + "EventRecordID": 239219, + "Execution_attributes": { + "ProcessID": 1172, + "ThreadID": 23236 + }, + "Keywords": "0x8020000000000000", + "Level": 0, + "Opcode": 0, + "Provider_attributes": { + "Guid": "54849625-5478-4994-A5BA-3E3B0328C30D", + "Name": "Microsoft-Windows-Security-Auditing" + }, + "Security": null, + "Task": 13824, + "TimeCreated_attributes": { + "SystemTime": "2021-05-12T09:39:19.828403Z" + }, + "Version": 0 + } + }, + "Event_attributes": { + "xmlns": "http://schemas.microsoft.com/win/2004/08/events/event" + } + }"#; + + let rule_node = parse_rule_from_str(rule_str); + let selection_node = rule_node.detection.unwrap().selection.unwrap(); + + match serde_json::from_str(record_json_str) { + Ok(record) => { + assert_eq!(selection_node.select(&record), false); + } + Err(_) => { + assert!(false, "failed to parse json record."); + } + } + } + + #[test] + fn test_detect_eventdata() { + // XML形式の特殊なパターンでEventDataというタグあって、Name=の部分にキー的なものが来る。 + /* - + S-1-5-21-2673273881-979819022-3746999991-1001 + takai + DESKTOP-ICHIICH + 0x312cd + DESKTOP-ICHIICH + Administrator + DESKTOP-ICHIICH + */ + + // その場合、イベントパーサーのJSONは下記のような感じになるので、それで正しく検知出来ることをテスト。 + /* { + "Event": { + "EventData": { + "TargetDomainName": "TEST-DOMAIN", + "Workstation": "TEST WorkStation" + "TargetUserName": "ichiichi11", + }, + } + } */ + + let rule_str = r#" + enabled: true + detection: + selection: + Event.EventData.Workstation: 'TEST WorkStation' + Event.EventData.TargetUserName: ichiichi11 + output: 'command=%CommandLine%' + "#; + + let record_json_str = r#" + { + "Event": { + "EventData": { + "Workstation": "TEST WorkStation", + "TargetUserName": "ichiichi11" + }, + "System": { + "Channel": "Security", + "EventID": 4103, + "EventRecordID": 239219, + "Security": null + } + }, + "Event_attributes": { + "xmlns": "http://schemas.microsoft.com/win/2004/08/events/event" + } + } + "#; + + let rule_node = parse_rule_from_str(rule_str); + let selection_node = rule_node.detection.unwrap().selection.unwrap(); + + match serde_json::from_str(record_json_str) { + Ok(record) => { + assert_eq!(selection_node.select(&record), true); + } + Err(_) => { + assert!(false, "failed to parse json record."); + } + } + } + + #[test] + fn test_detect_eventdata2() { + let rule_str = r#" + enabled: true + detection: + selection: + EventID: 4103 + TargetUserName: ichiichi11 + output: 'command=%CommandLine%' + "#; + + let record_json_str = r#" + { + "Event": { + "EventData": { + "Workstation": "TEST WorkStation", + "TargetUserName": "ichiichi11" + }, + "System": { + "Channel": "Security", + "EventID": 4103, + "EventRecordID": 239219, + "Security": null + } + }, + "Event_attributes": { + "xmlns": "http://schemas.microsoft.com/win/2004/08/events/event" + } + } + "#; + + let rule_node = parse_rule_from_str(rule_str); + let selection_node = rule_node.detection.unwrap().selection.unwrap(); + + match serde_json::from_str(record_json_str) { + Ok(record) => { + assert_eq!(selection_node.select(&record), true); + } + Err(_) => { + assert!(false, "failed to parse json record."); + } + } + } + + #[test] + fn test_notdetect_eventdata() { + // EventDataの検知しないパターン + let rule_str = r#" + enabled: true + detection: + selection: + EventID: 4103 + TargetUserName: ichiichi12 + output: 'command=%CommandLine%' + "#; + + let record_json_str = r#" + { + "Event": { + "EventData": { + "Workstation": "TEST WorkStation", + "TargetUserName": "ichiichi11" + }, + "System": { + "Channel": "Security", + "EventID": 4103, + "EventRecordID": 239219, + "Security": null + } + }, + "Event_attributes": { + "xmlns": "http://schemas.microsoft.com/win/2004/08/events/event" + } + } + "#; + + let rule_node = parse_rule_from_str(rule_str); + let selection_node = rule_node.detection.unwrap().selection.unwrap(); + + match serde_json::from_str(record_json_str) { + Ok(record) => { + assert_eq!(selection_node.select(&record), false); + } + Err(_) => { + assert!(false, "failed to parse json record."); + } + } + } + + #[test] + fn test_detect_special_eventdata() { + // 上記テストケースのEventDataの更に特殊ケースで下記のようにDataタグの中にNameキーがないケースがある。 + // そのためにruleファイルでEventDataというキーだけ特別対応している。 + // 現状、downgrade_attack.ymlというルールの場合だけで確認出来ているケース + let rule_str = r#" + enabled: true + detection: + selection: + EventID: 403 + EventData: '[\s\S]*EngineVersion=2.0[\s\S]*' + output: 'command=%CommandLine%' + "#; + + let record_json_str = r#" + { + "Event": { + "EventData": { + "Binary": null, + "Data": [ + "Stopped", + "Available", + "\tNewEngineState=Stopped\n\tPreviousEngineState=Available\n\n\tSequenceNumber=10\n\n\tHostName=ConsoleHost\n\tHostVersion=2.0\n\tHostId=5cbb33bf-acf7-47cc-9242-141cd0ba9f0c\n\tEngineVersion=2.0\n\tRunspaceId=c6e94dca-0daf-418c-860a-f751a9f2cbe1\n\tPipelineId=\n\tCommandName=\n\tCommandType=\n\tScriptName=\n\tCommandPath=\n\tCommandLine=" + ] + }, + "System": { + "Channel": "Windows PowerShell", + "Computer": "DESKTOP-ST69BPO", + "EventID": 403, + "EventID_attributes": { + "Qualifiers": 0 + }, + "EventRecordID": 730, + "Keywords": "0x80000000000000", + "Level": 4, + "Provider_attributes": { + "Name": "PowerShell" + }, + "Security": null, + "Task": 4, + "TimeCreated_attributes": { + "SystemTime": "2021-01-28T10:40:54.946866Z" + } + } + }, + "Event_attributes": { + "xmlns": "http://schemas.microsoft.com/win/2004/08/events/event" + } + } + "#; + + let rule_node = parse_rule_from_str(rule_str); + let selection_node = rule_node.detection.unwrap().selection.unwrap(); + + match serde_json::from_str(record_json_str) { + Ok(record) => { + assert_eq!(selection_node.select(&record), true); + } + Err(_) => { + assert!(false, "failed to parse json record."); + } + } + } + + #[test] + fn test_notdetect_special_eventdata() { + // 上記テストケースのEventDataの更に特殊ケースで下記のようにDataタグの中にNameキーがないケースがある。 + // そのためにruleファイルでEventDataというキーだけ特別対応している。 + // 現状、downgrade_attack.ymlというルールの場合だけで確認出来ているケース + let rule_str = r#" + enabled: true + detection: + selection: + EventID: 403 + EventData: '[\s\S]*EngineVersion=3.0[\s\S]*' + output: 'command=%CommandLine%' + "#; + + let record_json_str = r#" + { + "Event": { + "EventData": { + "Binary": null, + "Data": [ + "Stopped", + "Available", + "\tNewEngineState=Stopped\n\tPreviousEngineState=Available\n\n\tSequenceNumber=10\n\n\tHostName=ConsoleHost\n\tHostVersion=2.0\n\tHostId=5cbb33bf-acf7-47cc-9242-141cd0ba9f0c\n\tEngineVersion=2.0\n\tRunspaceId=c6e94dca-0daf-418c-860a-f751a9f2cbe1\n\tPipelineId=\n\tCommandName=\n\tCommandType=\n\tScriptName=\n\tCommandPath=\n\tCommandLine=" + ] + }, + "System": { + "Channel": "Windows PowerShell", + "Computer": "DESKTOP-ST69BPO", + "EventID": 403, + "EventID_attributes": { + "Qualifiers": 0 + }, + "EventRecordID": 730, + "Keywords": "0x80000000000000", + "Level": 4, + "Provider_attributes": { + "Name": "PowerShell" + }, + "Security": null, + "Task": 4, + "TimeCreated_attributes": { + "SystemTime": "2021-01-28T10:40:54.946866Z" + } + } + }, + "Event_attributes": { + "xmlns": "http://schemas.microsoft.com/win/2004/08/events/event" + } + } + "#; + + let rule_node = parse_rule_from_str(rule_str); + let selection_node = rule_node.detection.unwrap().selection.unwrap(); + + match serde_json::from_str(record_json_str) { + Ok(record) => { + assert_eq!(selection_node.select(&record), false); + } + Err(_) => { + assert!(false, "failed to parse json record."); + } + } + } + + fn parse_rule_from_str(rule_str: &str) -> RuleNode { + let rule_yaml = YamlLoader::load_from_str(rule_str); + assert_eq!(rule_yaml.is_ok(), true); + let rule_yamls = rule_yaml.unwrap(); + let mut rule_yaml = rule_yamls.into_iter(); + let mut rule_node = parse_rule(rule_yaml.next().unwrap()); + assert_eq!(rule_node.init().is_ok(), true); + return rule_node; + } } From e504a36d0a245aa2ab8b23eef7f58d46154fc028 Mon Sep 17 00:00:00 2001 From: HajimeTakai Date: Wed, 12 May 2021 23:16:11 +0900 Subject: [PATCH 5/8] refactoring --- src/detections/detection.rs | 125 ++++++++---------------------------- 1 file changed, 25 insertions(+), 100 deletions(-) diff --git a/src/detections/detection.rs b/src/detections/detection.rs index e13237cd..9c43efec 100644 --- a/src/detections/detection.rs +++ b/src/detections/detection.rs @@ -122,33 +122,23 @@ impl Detection { .collect(); let tokio_rt = utils::create_tokio_runtime(); - let xml_records = tokio_rt.block_on(self.evtx_to_xml(evtx_parsers, &evtx_files)); - - let json_records = tokio_rt.block_on(self.xml_to_json(xml_records, &evtx_files, &rules)); + let ret = tokio_rt.block_on(self.evtx_to_json(evtx_parsers, &evtx_files, rules)); tokio_rt.shutdown_background(); - return json_records - .into_iter() - .map(|(parser_idx, json_record)| { - let evtx_filepath = evtx_files[parser_idx].display().to_string(); - return EvtxRecordInfo { - evtx_filepath: String::from(&evtx_filepath), - record: json_record, - }; - }) - .collect(); + return ret; } // evtxファイルからxmlを生成する。 // 戻り値は「どのイベントファイルから生成されたXMLかを示すindex」と「変換されたXML」のタプルです。 // タプルのindexは、引数で指定されるevtx_filesのindexに対応しています。 - async fn evtx_to_xml( + async fn evtx_to_json( &mut self, evtx_parsers: Vec>, evtx_files: &Vec, - ) -> Vec<(usize, SerializedEvtxRecord)> { + rules: &Vec, + ) -> Vec { // evtx_parser.records_json()でevtxをxmlに変換するJobを作成 - let handles: Vec>>>> = evtx_parsers + let handles: Vec>>>> = evtx_parsers .into_iter() .map(|mut evtx_parser| { return spawn(async move { @@ -157,7 +147,7 @@ impl Detection { parse_config = parse_config.num_threads(utils::get_thread_num()); evtx_parser = evtx_parser.with_configuration(parse_config); - let values = evtx_parser.records_json().collect(); + let values = evtx_parser.records_json_value().collect(); return values; }); }) @@ -183,9 +173,11 @@ impl Detection { }); } + let event_id_set = Detection::get_event_ids(rules); return ret .into_iter() .filter_map(|(parser_idx, parse_result)| { + // パースに失敗している場合、エラーメッセージを出力 if parse_result.is_err() { let evtx_filepath = &evtx_files[parser_idx].display(); let errmsg = format!( @@ -196,95 +188,28 @@ impl Detection { AlertMessage::alert(&mut std::io::stdout().lock(), errmsg).ok(); return Option::None; } - return Option::Some((parser_idx, parse_result.unwrap())); - }) - .collect(); - } - // xmlからjsonに変換します。 - // 戻り値は「どのイベントファイルから生成されたXMLかを示すindex」と「変換されたJSON」のタプルです。 - // タプルのindexは、引数で指定されるevtx_filesのindexに対応しています。 - async fn xml_to_json( - &mut self, - xml_records: Vec<(usize, SerializedEvtxRecord)>, - evtx_files: &Vec, - rules: &Vec, - ) -> Vec<(usize, Value)> { - // TODO スレッド作り過ぎなので、数を減らす - - // 非同期で実行される無名関数を定義 - let async_job = |pair: (usize, SerializedEvtxRecord), - event_id_set: Arc>, - evtx_files: Arc>| { - let parser_idx = pair.0; - let handle = spawn(async move { - let parse_result = serde_json::from_str(&pair.1.data); - // パースに失敗した場合はエラー出力しておく。 - if parse_result.is_err() { - let evtx_filepath = &evtx_files[parser_idx].display(); - let errmsg = format!( - "Failed to serialize from event xml to json. EventFile:{} Error:{}", - evtx_filepath, - parse_result.unwrap_err() - ); - AlertMessage::alert(&mut std::io::stdout().lock(), errmsg).ok(); + // ルールファイルに記載されていないEventIDのレコードは絶対に検知しないので無視する。 + let record_json = parse_result.unwrap().data; + let event_id_opt = utils::get_event_value(&utils::get_event_id_key(), &record_json); + let is_exit_eventid = event_id_opt + .and_then(|event_id| event_id.as_i64()) + .and_then(|event_id| { + if event_id_set.contains(&event_id) { + return Option::Some(&record_json); + } else { + return Option::None; + } + }); + if is_exit_eventid.is_none() { return Option::None; } - // ルールファイルで検知しようとしているEventIDでないレコードはここで捨てる。 - let parsed_json: Value = parse_result.unwrap(); - let event_id_opt = utils::get_event_value(&utils::get_event_id_key(), &parsed_json); - return event_id_opt - .and_then(|event_id| event_id.as_i64()) - .and_then(|event_id| { - if event_id_set.contains(&event_id) { - return Option::Some(parsed_json); - } else { - return Option::None; - } - }); - }); - - return (parser_idx, handle); - }; - // 非同期で実行するスレッドを生成し、実行する。 - let event_id_set_arc = Arc::new(Detection::get_event_ids(rules)); - let evtx_files_arc = Arc::new(evtx_files.clone()); - let handles: Vec<(usize, JoinHandle>)> = xml_records - .into_iter() - .map(|xml_record_pair| { - let event_id_set_clone = Arc::clone(&event_id_set_arc); - let evtx_files_clone = Arc::clone(&evtx_files_arc); - return async_job(xml_record_pair, event_id_set_clone, evtx_files_clone); + let evtx_filepath = evtx_files[parser_idx].display().to_string(); + let record_info = EvtxRecordInfo{ evtx_filepath: evtx_filepath, record: record_json}; + return Option::Some(record_info); }) .collect(); - - // スレッドの終了待ちをしている。 - let mut ret = vec![]; - for (parser_idx, handle) in handles { - let future = handle.await; - // スレッドが正常に完了しなかった場合はエラーメッセージを出力する。 - if future.is_err() { - let evtx_filepath = &evtx_files[parser_idx].display(); - let errmsg = format!( - "Failed to serialize from event xml to json. EventFile:{} Error:{}", - evtx_filepath, - future.unwrap_err() - ); - AlertMessage::alert(&mut std::io::stdout().lock(), errmsg).ok(); - continue; - } - - // パース失敗やルールファイルで検知しようとしていないEventIDの場合等はis_none()==trueになる。 - let parse_result = future.unwrap(); - if parse_result.is_none() { - continue; - } - - ret.push((parser_idx, parse_result.unwrap())); - } - - return ret; } // 検知ロジックを実行します。 From 7cd06917644b5d9bda4788fdc6dca5eecd5c1236 Mon Sep 17 00:00:00 2001 From: HajimeTakai Date: Wed, 12 May 2021 23:19:03 +0900 Subject: [PATCH 6/8] cargo fmt --all --- src/detections/detection.rs | 48 ++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/src/detections/detection.rs b/src/detections/detection.rs index 9c43efec..a5d2a797 100644 --- a/src/detections/detection.rs +++ b/src/detections/detection.rs @@ -138,20 +138,21 @@ impl Detection { rules: &Vec, ) -> Vec { // evtx_parser.records_json()でevtxをxmlに変換するJobを作成 - let handles: Vec>>>> = evtx_parsers - .into_iter() - .map(|mut evtx_parser| { - return spawn(async move { - let mut parse_config = ParserSettings::default(); - parse_config = parse_config.separate_json_attributes(true); - parse_config = parse_config.num_threads(utils::get_thread_num()); + let handles: Vec>>>> = + evtx_parsers + .into_iter() + .map(|mut evtx_parser| { + return spawn(async move { + let mut parse_config = ParserSettings::default(); + parse_config = parse_config.separate_json_attributes(true); + parse_config = parse_config.num_threads(utils::get_thread_num()); - evtx_parser = evtx_parser.with_configuration(parse_config); - let values = evtx_parser.records_json_value().collect(); - return values; - }); - }) - .collect(); + evtx_parser = evtx_parser.with_configuration(parse_config); + let values = evtx_parser.records_json_value().collect(); + return values; + }); + }) + .collect(); // 作成したjobを実行し(handle.awaitの部分)、スレッドの実行時にエラーが発生した場合、標準エラー出力に出しておく let mut ret = vec![]; @@ -193,20 +194,23 @@ impl Detection { let record_json = parse_result.unwrap().data; let event_id_opt = utils::get_event_value(&utils::get_event_id_key(), &record_json); let is_exit_eventid = event_id_opt - .and_then(|event_id| event_id.as_i64()) - .and_then(|event_id| { - if event_id_set.contains(&event_id) { - return Option::Some(&record_json); - } else { - return Option::None; - } - }); + .and_then(|event_id| event_id.as_i64()) + .and_then(|event_id| { + if event_id_set.contains(&event_id) { + return Option::Some(&record_json); + } else { + return Option::None; + } + }); if is_exit_eventid.is_none() { return Option::None; } let evtx_filepath = evtx_files[parser_idx].display().to_string(); - let record_info = EvtxRecordInfo{ evtx_filepath: evtx_filepath, record: record_json}; + let record_info = EvtxRecordInfo { + evtx_filepath: evtx_filepath, + record: record_json, + }; return Option::Some(record_info); }) .collect(); From e960586ede1d207559f0872d9fa67e6e1ace70da Mon Sep 17 00:00:00 2001 From: ichiichi11 Date: Thu, 13 May 2021 22:05:49 +0900 Subject: [PATCH 7/8] fix comment --- src/detections/detection.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/detections/detection.rs b/src/detections/detection.rs index a5d2a797..284d8e0d 100644 --- a/src/detections/detection.rs +++ b/src/detections/detection.rs @@ -16,10 +16,11 @@ use std::{fs::File, sync::Arc}; const DIRPATH_RULES: &str = "rules"; +// イベントファイルの1レコード分の情報を保持する構造体 #[derive(Clone, Debug)] pub struct EvtxRecordInfo { - evtx_filepath: String, - record: Value, + evtx_filepath: String,// イベントファイルのファイルパス ログで出力するときに使う + record: Value, // 1レコード分のデータをJSON形式にシリアライズしたもの } // TODO テストケースかかなきゃ... @@ -128,7 +129,7 @@ impl Detection { return ret; } - // evtxファイルからxmlを生成する。 + // evtxファイルからEvtxRecordInfoを生成する。 // 戻り値は「どのイベントファイルから生成されたXMLかを示すindex」と「変換されたXML」のタプルです。 // タプルのindexは、引数で指定されるevtx_filesのindexに対応しています。 async fn evtx_to_json( From ee23fc9a66cf03801eed54539a679beb4157a651 Mon Sep 17 00:00:00 2001 From: ichiichi11 Date: Thu, 13 May 2021 22:07:41 +0900 Subject: [PATCH 8/8] cargo fmt --all --- src/detections/detection.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/detections/detection.rs b/src/detections/detection.rs index 284d8e0d..8be81e26 100644 --- a/src/detections/detection.rs +++ b/src/detections/detection.rs @@ -19,8 +19,8 @@ const DIRPATH_RULES: &str = "rules"; // イベントファイルの1レコード分の情報を保持する構造体 #[derive(Clone, Debug)] pub struct EvtxRecordInfo { - evtx_filepath: String,// イベントファイルのファイルパス ログで出力するときに使う - record: Value, // 1レコード分のデータをJSON形式にシリアライズしたもの + evtx_filepath: String, // イベントファイルのファイルパス ログで出力するときに使う + record: Value, // 1レコード分のデータをJSON形式にシリアライズしたもの } // TODO テストケースかかなきゃ...