From 7913fbfb95f976312cb3481d71c92c9b66dab57a Mon Sep 17 00:00:00 2001 From: HajimeTakai Date: Sun, 9 May 2021 17:26:17 +0900 Subject: [PATCH 01/13] 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 02/13] 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 03/13] 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 04/13] 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 05/13] 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 06/13] 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 07/13] 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 08/13] 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 テストケースかかなきゃ... From 99b640adaa3d594d9a5a7a0675aeb6c57ef84def Mon Sep 17 00:00:00 2001 From: Alan Smithee Date: Thu, 13 May 2021 22:52:15 +0900 Subject: [PATCH 09/13] Add rule of Kerberoasting and AS-REP Roasting #91 (#101) * Feature/call error message struct#66 (#69) * change way to use write trait #66 * change call error message struct #66 * erase finished TODO #66 * erase comment in error message format test #66 * resolve conflict #66 * Feature/call error message struct#66 (#71) * change ERROR writeln struct #66 * add Kerberoasting & AS-REP Roasting Rule #91 * fix rule and add alias #91 --- config/eventkey_alias.txt | 4 +++- rules/kerberoast/as-rep-roasting.yml | 18 ++++++++++++++++++ rules/kerberoast/kerberoasting.yml | 18 ++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 rules/kerberoast/as-rep-roasting.yml create mode 100644 rules/kerberoast/kerberoasting.yml diff --git a/config/eventkey_alias.txt b/config/eventkey_alias.txt index 4c6b4c97..c39542dd 100644 --- a/config/eventkey_alias.txt +++ b/config/eventkey_alias.txt @@ -21,4 +21,6 @@ LogFileCleared,Event.UserData.LogFileCleared.SubjectUserName LogFileClearedSubjectUserName,Event.UserData.SubjectUserName SubjectUserName,Event.EventData.SubjectUserName SubjectUserSid,Event.EventData.SubjectUserSid -DomainName,Event.EventData.SubjectDomainName \ No newline at end of file +DomainName,Event.EventData.SubjectDomainName +TicketEncryptionType,Event.EventData.TicketEncryptionType +PreAuthType,Event.EventData.PreAuthType \ No newline at end of file diff --git a/rules/kerberoast/as-rep-roasting.yml b/rules/kerberoast/as-rep-roasting.yml new file mode 100644 index 00000000..9585fa33 --- /dev/null +++ b/rules/kerberoast/as-rep-roasting.yml @@ -0,0 +1,18 @@ +title: AS-REP Roasting +description: For each account found without preauthentication, an adversary may send an AS-REQ message without the encrypted timestamp and receive an AS-REP message with TGT data which may be encrypted with an insecure algorithm such as RC4. +enabled: true +author: Yea +logsource: + product: windows +detection: + selection: + Channel: Security + EventID: 4768 + TicketEncryptionType: '0x17' + PreAuthType: 0 +falsepositives: + - unknown +level: medium +output: 'Detected AS-REP Roasting Risk Actvity.' +creation_date: 2021/4/31 +updated_date: 2021/4/31 diff --git a/rules/kerberoast/kerberoasting.yml b/rules/kerberoast/kerberoasting.yml new file mode 100644 index 00000000..4d829045 --- /dev/null +++ b/rules/kerberoast/kerberoasting.yml @@ -0,0 +1,18 @@ +title: Kerberoasting +description: Adversaries may abuse a valid Kerberos ticket-granting ticket (TGT) or sniff network traffic to obtain a ticket-granting service (TGS) ticket that may be vulnerable to Brute Force. +enabled: true +author: Yea +logsource: + product: windows +detection: + selection: + Channel: Security + EventID: 4768 + TicketEncryptionType: '0x17' + PreAuthType: 2 +falsepositives: + - unknown +level: medium +output: 'Detected Kerberoasting Risk Activity.' +creation_date: 2021/4/31 +updated_date: 2021/4/31 From 0064ccb4f84e682b11187cfba365e726c37b98b5 Mon Sep 17 00:00:00 2001 From: HajimeTakai Date: Fri, 14 May 2021 09:09:37 +0900 Subject: [PATCH 10/13] under constructing --- src/detections/configs.rs | 1 + src/detections/detection.rs | 170 +++++------------------------------- src/detections/mod.rs | 2 +- src/detections/rule.rs | 92 +++++++++---------- src/lib.rs | 1 + src/main.rs | 108 ++++++++++++++++++++++- src/timeline/mod.rs | 2 + src/timeline/statistics.rs | 1 + src/timeline/timeline.rs | 1 + 9 files changed, 182 insertions(+), 196 deletions(-) create mode 100644 src/timeline/mod.rs create mode 100644 src/timeline/statistics.rs create mode 100644 src/timeline/timeline.rs diff --git a/src/detections/configs.rs b/src/detections/configs.rs index a05dee2a..98502f00 100644 --- a/src/detections/configs.rs +++ b/src/detections/configs.rs @@ -51,6 +51,7 @@ fn build_app<'a>() -> ArgMatches<'a> { .arg(Arg::from_usage("-d --directory=[DIRECTORY] 'event log files directory'")) .arg(Arg::from_usage("-s --statistics 'event statistics'")) .arg(Arg::from_usage("-t --threadnum=[NUM] 'thread number'")) + .arg(Arg::from_usage("-tl --timeline 'show event log timeline'")) .arg(Arg::from_usage("--credits 'Zachary Mathis, Akira Nishikawa'")) .get_matches() } diff --git a/src/detections/detection.rs b/src/detections/detection.rs index 8be81e26..7228ffbb 100644 --- a/src/detections/detection.rs +++ b/src/detections/detection.rs @@ -1,54 +1,50 @@ extern crate csv; +use serde_json::Value; +use tokio::{spawn, task::JoinHandle}; + use crate::detections::print::MESSAGES; use crate::detections::rule; use crate::detections::rule::RuleNode; use crate::detections::{print::AlertMessage, utils}; use crate::yaml::ParseYaml; -use evtx::err; -use evtx::{EvtxParser, ParserSettings, SerializedEvtxRecord}; -use serde_json::Value; -use tokio::{spawn, task::JoinHandle}; - -use std::{collections::HashSet, path::PathBuf}; -use std::{fs::File, sync::Arc}; +use std::{sync::Arc}; const DIRPATH_RULES: &str = "rules"; // イベントファイルの1レコード分の情報を保持する構造体 #[derive(Clone, Debug)] pub struct EvtxRecordInfo { - evtx_filepath: String, // イベントファイルのファイルパス ログで出力するときに使う - record: Value, // 1レコード分のデータをJSON形式にシリアライズしたもの + pub evtx_filepath: String, // イベントファイルのファイルパス ログで出力するときに使う + pub record: Value, // 1レコード分のデータをJSON形式にシリアライズしたもの +} + +impl EvtxRecordInfo { + pub fn new(evtx_filepath:String, record: Value) -> EvtxRecordInfo{ + return EvtxRecordInfo{ + evtx_filepath: evtx_filepath, + record: record + }; + } } // TODO テストケースかかなきゃ... #[derive(Debug)] pub struct Detection { - parseinfos: Vec, } impl Detection { pub fn new() -> Detection { - let initializer: Vec = Vec::new(); - Detection { - parseinfos: initializer, - } + return Detection {}; } - pub fn start(&mut self, evtx_files: Vec) { - if evtx_files.is_empty() { - return; - } - + pub fn start(&mut self, records: Vec ) { let rules = self.parse_rule_files(); if rules.is_empty() { return; } - let records = self.evtx_to_jsons(&evtx_files, &rules); - let tokio_rt = utils::create_tokio_runtime(); tokio_rt.block_on(self.execute_rule(rules, records)); tokio_rt.shutdown_background(); @@ -99,124 +95,6 @@ impl Detection { .collect(); } - // evtxファイルをjsonに変換します。 - fn evtx_to_jsons( - &mut self, - evtx_files: &Vec, - rules: &Vec, - ) -> Vec { - // EvtxParserを生成する。 - let evtx_parsers: Vec> = evtx_files - .clone() - .into_iter() - .filter_map(|evtx_file| { - // convert to evtx parser - // println!("PathBuf:{}", evtx_file.display()); - match EvtxParser::from_path(evtx_file) { - Ok(parser) => Option::Some(parser), - Err(e) => { - eprintln!("{}", e); - return Option::None; - } - } - }) - .collect(); - - let tokio_rt = utils::create_tokio_runtime(); - let ret = tokio_rt.block_on(self.evtx_to_json(evtx_parsers, &evtx_files, rules)); - tokio_rt.shutdown_background(); - - return ret; - } - - // evtxファイルからEvtxRecordInfoを生成する。 - // 戻り値は「どのイベントファイルから生成されたXMLかを示すindex」と「変換されたXML」のタプルです。 - // タプルのindexは、引数で指定されるevtx_filesのindexに対応しています。 - async fn evtx_to_json( - &mut self, - evtx_parsers: Vec>, - evtx_files: &Vec, - 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()); - - 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![]; - for (parser_idx, handle) in handles.into_iter().enumerate() { - let future_result = handle.await; - if future_result.is_err() { - let evtx_filepath = &evtx_files[parser_idx].display(); - let errmsg = format!( - "Failed to parse event file. EventFile:{} Error:{}", - evtx_filepath, - future_result.unwrap_err() - ); - AlertMessage::alert(&mut std::io::stdout().lock(), errmsg).ok(); - continue; - } - - future_result.unwrap().into_iter().for_each(|parse_result| { - ret.push((parser_idx, parse_result)); - }); - } - - 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!( - "Failed to parse event file. EventFile:{} Error:{}", - evtx_filepath, - parse_result.unwrap_err() - ); - AlertMessage::alert(&mut std::io::stdout().lock(), errmsg).ok(); - return Option::None; - } - - // ルールファイルに記載されていない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; - } - - 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(); - } - // 検知ロジックを実行します。 async fn execute_rule(&mut self, rules: Vec, records: Vec) { // 複数スレッドで所有権を共有するため、recordsをArcでwwap @@ -270,13 +148,13 @@ impl Detection { } } - fn get_event_ids(rules: &Vec) -> HashSet { - return rules - .iter() - .map(|rule| rule.get_event_ids()) - .flatten() - .collect(); - } + // fn get_event_ids(rules: &Vec) -> HashSet { + // return rules + // .iter() + // .map(|rule| rule.get_event_ids()) + // .flatten() + // .collect(); + // } // 配列を指定したサイズで分割する。Vector.chunksと同じ動作をするが、Vectorの関数だとinto的なことができないので自作 fn chunks(ary: Vec, size: usize) -> Vec> { diff --git a/src/detections/mod.rs b/src/detections/mod.rs index 48034d62..3bfab408 100644 --- a/src/detections/mod.rs +++ b/src/detections/mod.rs @@ -2,4 +2,4 @@ pub mod configs; pub mod detection; pub mod print; mod rule; -mod utils; +pub mod utils; diff --git a/src/detections/rule.rs b/src/detections/rule.rs index 047d50d4..53837060 100644 --- a/src/detections/rule.rs +++ b/src/detections/rule.rs @@ -126,40 +126,40 @@ impl RuleNode { return selection.unwrap().select(event_record); } - pub fn get_event_ids(&self) -> Vec { - let selection = self - .detection - .as_ref() - .and_then(|detection| detection.selection.as_ref()); - if selection.is_none() { - return vec![]; - } + // pub fn get_event_ids(&self) -> Vec { + // let selection = self + // .detection + // .as_ref() + // .and_then(|detection| detection.selection.as_ref()); + // if selection.is_none() { + // return vec![]; + // } - return selection - .unwrap() - .get_descendants() - .iter() - .filter_map(|node| return node.downcast_ref::()) // mopaというライブラリを使うと簡単にダウンキャストできるらしいです。https://crates.io/crates/mopa - .filter(|node| { - // キーがEventIDのノードである - let key = utils::get_event_id_key(); - if node.get_key() == key { - return true; - } + // return selection + // .unwrap() + // .get_descendants() + // .iter() + // .filter_map(|node| return node.downcast_ref::()) // mopaというライブラリを使うと簡単にダウンキャストできるらしいです。https://crates.io/crates/mopa + // .filter(|node| { + // // キーがEventIDのノードである + // let key = utils::get_event_id_key(); + // if node.get_key() == key { + // return true; + // } - // EventIDのAliasに一致しているかどうか - let alias = utils::get_alias(&key); - if alias.is_none() { - return false; - } else { - return node.get_key() == alias.unwrap(); - } - }) - .filter_map(|node| { - return node.select_value.as_i64(); - }) - .collect(); - } + // // EventIDのAliasに一致しているかどうか + // let alias = utils::get_alias(&key); + // if alias.is_none() { + // return false; + // } else { + // return node.get_key() == alias.unwrap(); + // } + // }) + // .filter_map(|node| { + // return node.select_value.as_i64(); + // }) + // .collect(); + // } } // Ruleファイルのdetectionを表すノード @@ -980,20 +980,20 @@ mod tests { } } - #[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_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() { diff --git a/src/lib.rs b/src/lib.rs index 06371c5a..623298ad 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub mod afterfact; pub mod detections; pub mod omikuji; +pub mod timeline; pub mod yaml; diff --git a/src/main.rs b/src/main.rs index 79f62399..1d09a0d1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,18 @@ extern crate serde; extern crate serde_derive; -use std::{fs, path::PathBuf}; -use yamato_event_analyzer::afterfact::after_fact; +use evtx::{err, EvtxParser, ParserSettings, SerializedEvtxRecord}; +use std::{ + fs::{self, File}, + path::PathBuf, +}; +use tokio::{spawn, task::JoinHandle}; use yamato_event_analyzer::detections::configs; use yamato_event_analyzer::detections::detection; +use yamato_event_analyzer::detections::detection::EvtxRecordInfo; use yamato_event_analyzer::detections::print::AlertMessage; use yamato_event_analyzer::omikuji::Omikuji; +use yamato_event_analyzer::{afterfact::after_fact, detections::utils}; fn main() { if let Some(filepath) = configs::CONFIG.read().unwrap().args.value_of("filepath") { @@ -64,12 +70,108 @@ fn print_credits() { } fn detect_files(evtx_files: Vec) { + let evnt_records = evtx_to_jsons(&evtx_files); + let mut detection = detection::Detection::new(); - &detection.start(evtx_files); + &detection.start(evnt_records); after_fact(); } +// evtxファイルをjsonに変換します。 +fn evtx_to_jsons(evtx_files: &Vec) -> Vec { + // EvtxParserを生成する。 + let evtx_parsers: Vec> = evtx_files + .clone() + .into_iter() + .filter_map(|evtx_file| { + // convert to evtx parser + // println!("PathBuf:{}", evtx_file.display()); + match EvtxParser::from_path(evtx_file) { + Ok(parser) => Option::Some(parser), + Err(e) => { + eprintln!("{}", e); + return Option::None; + } + } + }) + .collect(); + + let tokio_rt = utils::create_tokio_runtime(); + let ret = tokio_rt.block_on(evtx_to_json(evtx_parsers, &evtx_files)); + tokio_rt.shutdown_background(); + + return ret; +} + +// evtxファイルからEvtxRecordInfoを生成する。 +// 戻り値は「どのイベントファイルから生成されたXMLかを示すindex」と「変換されたXML」のタプルです。 +// タプルのindexは、引数で指定されるevtx_filesのindexに対応しています。 +async fn evtx_to_json( + evtx_parsers: Vec>, + evtx_files: &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()); + + 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![]; + for (parser_idx, handle) in handles.into_iter().enumerate() { + let future_result = handle.await; + if future_result.is_err() { + let evtx_filepath = &evtx_files[parser_idx].display(); + let errmsg = format!( + "Failed to parse event file. EventFile:{} Error:{}", + evtx_filepath, + future_result.unwrap_err() + ); + AlertMessage::alert(&mut std::io::stdout().lock(), errmsg).ok(); + continue; + } + + future_result.unwrap().into_iter().for_each(|parse_result| { + ret.push((parser_idx, parse_result)); + }); + } + + 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!( + "Failed to parse event file. EventFile:{} Error:{}", + evtx_filepath, + parse_result.unwrap_err() + ); + AlertMessage::alert(&mut std::io::stdout().lock(), errmsg).ok(); + return Option::None; + } + + let record_info = EvtxRecordInfo::new( + evtx_files[parser_idx].display().to_string(), + parse_result.unwrap().data, + ); + return Option::Some(record_info); + }) + .collect(); +} + fn _output_with_omikuji(omikuji: Omikuji) { let fp = &format!("art/omikuji/{}", omikuji); let content = fs::read_to_string(fp).unwrap(); diff --git a/src/timeline/mod.rs b/src/timeline/mod.rs new file mode 100644 index 00000000..5c4b7d84 --- /dev/null +++ b/src/timeline/mod.rs @@ -0,0 +1,2 @@ +pub mod statistics; +pub mod timeline; diff --git a/src/timeline/statistics.rs b/src/timeline/statistics.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/src/timeline/statistics.rs @@ -0,0 +1 @@ + diff --git a/src/timeline/timeline.rs b/src/timeline/timeline.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/src/timeline/timeline.rs @@ -0,0 +1 @@ + From bf1e57945b554bdb5225de846b99a89b2dc1e7cd Mon Sep 17 00:00:00 2001 From: HajimeTakai Date: Sat, 15 May 2021 01:12:36 +0900 Subject: [PATCH 11/13] add statistics template --- src/detections/configs.rs | 1 - src/main.rs | 5 ++++- src/timeline/statistics.rs | 27 +++++++++++++++++++++++++++ src/timeline/timeline.rs | 18 ++++++++++++++++++ 4 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/detections/configs.rs b/src/detections/configs.rs index 98502f00..a05dee2a 100644 --- a/src/detections/configs.rs +++ b/src/detections/configs.rs @@ -51,7 +51,6 @@ fn build_app<'a>() -> ArgMatches<'a> { .arg(Arg::from_usage("-d --directory=[DIRECTORY] 'event log files directory'")) .arg(Arg::from_usage("-s --statistics 'event statistics'")) .arg(Arg::from_usage("-t --threadnum=[NUM] 'thread number'")) - .arg(Arg::from_usage("-tl --timeline 'show event log timeline'")) .arg(Arg::from_usage("--credits 'Zachary Mathis, Akira Nishikawa'")) .get_matches() } diff --git a/src/main.rs b/src/main.rs index 1d09a0d1..93126867 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,7 @@ use std::{ path::PathBuf, }; use tokio::{spawn, task::JoinHandle}; -use yamato_event_analyzer::detections::configs; +use yamato_event_analyzer::{detections::configs, timeline::timeline::Timeline}; use yamato_event_analyzer::detections::detection; use yamato_event_analyzer::detections::detection::EvtxRecordInfo; use yamato_event_analyzer::detections::print::AlertMessage; @@ -72,6 +72,9 @@ fn print_credits() { fn detect_files(evtx_files: Vec) { let evnt_records = evtx_to_jsons(&evtx_files); + let mut tl = Timeline::new(); + tl.start(&evnt_records); + let mut detection = detection::Detection::new(); &detection.start(evnt_records); diff --git a/src/timeline/statistics.rs b/src/timeline/statistics.rs index 8b137891..08a80586 100644 --- a/src/timeline/statistics.rs +++ b/src/timeline/statistics.rs @@ -1 +1,28 @@ +use crate::detections::{configs, detection::EvtxRecordInfo}; +#[derive(Debug)] +pub struct EventStatistics { +} +/** +* Windows Event Logの統計情報を出力する +*/ +impl EventStatistics { + pub fn new() -> EventStatistics { + return EventStatistics {}; + } + + // この関数の戻り値として、コンソールに出力する内容をStringの可変配列(Vec)として返却してください。 + // 可変配列にしているのは改行を表すためで、可変配列にコンソールに出力する内容を1行ずつ追加してください。 + + // 現状では、この関数の戻り値として返すVecを表示するコードは実装していません。 + pub fn start(&mut self, _records: &Vec ) -> Vec { + // 引数でstatisticsオプションが指定されている時だけ、 + if configs::CONFIG.read().unwrap().args.value_of("statistics").is_none() { + return vec![]; + } + + // TODO ここから下を書いて欲しいです!! + + return vec![]; + } +} \ No newline at end of file diff --git a/src/timeline/timeline.rs b/src/timeline/timeline.rs index 8b137891..47294d1f 100644 --- a/src/timeline/timeline.rs +++ b/src/timeline/timeline.rs @@ -1 +1,19 @@ +use crate::detections::detection::EvtxRecordInfo; +use super::statistics::EventStatistics; + + +#[derive(Debug)] +pub struct Timeline { +} + +impl Timeline { + pub fn new() -> Timeline { + return Timeline {}; + } + + pub fn start(&mut self, records: &Vec ) { + let mut statistic = EventStatistics::new(); + statistic.start(records); + } +} \ No newline at end of file From ff2bbcc1b8c59bfa7989cd1510cf00eb048dd644 Mon Sep 17 00:00:00 2001 From: ichiichi11 Date: Sat, 15 May 2021 01:28:47 +0900 Subject: [PATCH 12/13] fix --- src/timeline/statistics.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/timeline/statistics.rs b/src/timeline/statistics.rs index 08a80586..c73051fc 100644 --- a/src/timeline/statistics.rs +++ b/src/timeline/statistics.rs @@ -16,12 +16,12 @@ impl EventStatistics { // 現状では、この関数の戻り値として返すVecを表示するコードは実装していません。 pub fn start(&mut self, _records: &Vec ) -> Vec { - // 引数でstatisticsオプションが指定されている時だけ、 - if configs::CONFIG.read().unwrap().args.value_of("statistics").is_none() { + // 引数でstatisticsオプションが指定されている時だけ、統計情報を出力する。 + if !configs::CONFIG.read().unwrap().args.is_present("statistics") { return vec![]; } - // TODO ここから下を書いて欲しいです!! + // TODO ここから下を書いて欲しいです。 return vec![]; } From 6e1e414e181ecf54b9c184c8bf231ffd4d92cbe7 Mon Sep 17 00:00:00 2001 From: ichiichi11 Date: Sat, 15 May 2021 01:39:43 +0900 Subject: [PATCH 13/13] add comment --- src/detections/detection.rs | 13 ++++++------- src/main.rs | 2 +- src/timeline/statistics.rs | 18 +++++++++++++----- src/timeline/timeline.rs | 8 +++----- 4 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/detections/detection.rs b/src/detections/detection.rs index 7228ffbb..d657c22c 100644 --- a/src/detections/detection.rs +++ b/src/detections/detection.rs @@ -9,7 +9,7 @@ use crate::detections::rule::RuleNode; use crate::detections::{print::AlertMessage, utils}; use crate::yaml::ParseYaml; -use std::{sync::Arc}; +use std::sync::Arc; const DIRPATH_RULES: &str = "rules"; @@ -21,25 +21,24 @@ pub struct EvtxRecordInfo { } impl EvtxRecordInfo { - pub fn new(evtx_filepath:String, record: Value) -> EvtxRecordInfo{ - return EvtxRecordInfo{ + pub fn new(evtx_filepath: String, record: Value) -> EvtxRecordInfo { + return EvtxRecordInfo { evtx_filepath: evtx_filepath, - record: record + record: record, }; } } // TODO テストケースかかなきゃ... #[derive(Debug)] -pub struct Detection { -} +pub struct Detection {} impl Detection { pub fn new() -> Detection { return Detection {}; } - pub fn start(&mut self, records: Vec ) { + pub fn start(&mut self, records: Vec) { let rules = self.parse_rule_files(); if rules.is_empty() { return; diff --git a/src/main.rs b/src/main.rs index 93126867..ade1ad82 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,12 +7,12 @@ use std::{ path::PathBuf, }; use tokio::{spawn, task::JoinHandle}; -use yamato_event_analyzer::{detections::configs, timeline::timeline::Timeline}; use yamato_event_analyzer::detections::detection; use yamato_event_analyzer::detections::detection::EvtxRecordInfo; use yamato_event_analyzer::detections::print::AlertMessage; use yamato_event_analyzer::omikuji::Omikuji; use yamato_event_analyzer::{afterfact::after_fact, detections::utils}; +use yamato_event_analyzer::{detections::configs, timeline::timeline::Timeline}; fn main() { if let Some(filepath) = configs::CONFIG.read().unwrap().args.value_of("filepath") { diff --git a/src/timeline/statistics.rs b/src/timeline/statistics.rs index c73051fc..f0cfe1c1 100644 --- a/src/timeline/statistics.rs +++ b/src/timeline/statistics.rs @@ -1,8 +1,7 @@ use crate::detections::{configs, detection::EvtxRecordInfo}; #[derive(Debug)] -pub struct EventStatistics { -} +pub struct EventStatistics {} /** * Windows Event Logの統計情報を出力する */ @@ -13,11 +12,20 @@ impl EventStatistics { // この関数の戻り値として、コンソールに出力する内容をStringの可変配列(Vec)として返却してください。 // 可変配列にしているのは改行を表すためで、可変配列にコンソールに出力する内容を1行ずつ追加してください。 + // 引数の_recordsが読み込んだWindowsイベントログのを表す、EvtxRecordInfo構造体の配列になっています。 + // EvtxRecordInfo構造体の pub record: Value というメンバーがいて、それがWindowsイベントログの1レコード分を表していますので、 + // EvtxRecordInfo構造体のrecordから、EventIDとか統計情報を取得するようにしてください。 + // recordからEventIDを取得するには、detection::utils::get_event_value()という関数があるので、それを使うと便利かもしれません。 // 現状では、この関数の戻り値として返すVecを表示するコードは実装していません。 - pub fn start(&mut self, _records: &Vec ) -> Vec { + pub fn start(&mut self, _records: &Vec) -> Vec { // 引数でstatisticsオプションが指定されている時だけ、統計情報を出力する。 - if !configs::CONFIG.read().unwrap().args.is_present("statistics") { + if !configs::CONFIG + .read() + .unwrap() + .args + .is_present("statistics") + { return vec![]; } @@ -25,4 +33,4 @@ impl EventStatistics { return vec![]; } -} \ No newline at end of file +} diff --git a/src/timeline/timeline.rs b/src/timeline/timeline.rs index 47294d1f..1ac008d6 100644 --- a/src/timeline/timeline.rs +++ b/src/timeline/timeline.rs @@ -2,18 +2,16 @@ use crate::detections::detection::EvtxRecordInfo; use super::statistics::EventStatistics; - #[derive(Debug)] -pub struct Timeline { -} +pub struct Timeline {} impl Timeline { pub fn new() -> Timeline { return Timeline {}; } - pub fn start(&mut self, records: &Vec ) { + pub fn start(&mut self, records: &Vec) { let mut statistic = EventStatistics::new(); statistic.start(records); } -} \ No newline at end of file +}