diff --git a/rules/deep_blue_cli/powershell/4104.yml b/rules/deep_blue_cli/powershell/4104.yml index 78952873..9bef35b5 100644 --- a/rules/deep_blue_cli/powershell/4104.yml +++ b/rules/deep_blue_cli/powershell/4104.yml @@ -9,7 +9,7 @@ detection: Channel: Microsoft-Windows-PowerShell/Operational EventID: 4104 Path: null - ScriptBlockText: '.+' + ScriptBlockText|re: '.+' # condition: selection falsepositives: - unknown diff --git a/rules/deep_blue_cli/security/4674.yml b/rules/deep_blue_cli/security/4674.yml index ab5763d3..f8ea7145 100644 --- a/rules/deep_blue_cli/security/4674.yml +++ b/rules/deep_blue_cli/security/4674.yml @@ -8,7 +8,7 @@ detection: selection: Channel: Security EventID: 4674 - ProcessName: '(?i)C:\WINDOWS\SYSTEM32\SERVICE.EXE' # (?i) means case insesitive for Rust Regex + ProcessName|re: '(?i)C:\WINDOWS\SYSTEM32\SERVICE.EXE' # (?i) means case insesitive for Rust Regex AccessMask: '%%1539' # condition: selection falsepositives: diff --git a/rules/deep_blue_cli/security/4688.yml b/rules/deep_blue_cli/security/4688.yml index 628b17a7..1a4002cb 100644 --- a/rules/deep_blue_cli/security/4688.yml +++ b/rules/deep_blue_cli/security/4688.yml @@ -8,7 +8,7 @@ detection: selection: Channel: Security EventID: 4688 - CommandLine: '.+' + CommandLine|re: '.+' # condition: selection falsepositives: - unknown diff --git a/rules/deep_blue_cli/security/_4672.yml b/rules/deep_blue_cli/security/_4672.yml index 65fb8ce8..a195173f 100644 --- a/rules/deep_blue_cli/security/_4672.yml +++ b/rules/deep_blue_cli/security/_4672.yml @@ -8,8 +8,7 @@ detection: selection: Channel: Security EventID: 4672 - PrivilegeList: - contain: SeDebugPrivilege + PrivilegeList|contains: SeDebugPrivilege # condition: selection falsepositives: - unknown diff --git a/rules/deep_blue_cli/sysmon/1.yml b/rules/deep_blue_cli/sysmon/1.yml index 3f2b36c6..c3c83107 100644 --- a/rules/deep_blue_cli/sysmon/1.yml +++ b/rules/deep_blue_cli/sysmon/1.yml @@ -8,7 +8,7 @@ detection: selection: Channel: Sysmon EventID: 1 - CommandLine: '.+' + CommandLine|re: '.+' # condition: selection falsepositives: - unknown diff --git a/rules/powershell/downgrade_attack.yml b/rules/powershell/downgrade_attack.yml index 5cba122d..d59489e7 100644 --- a/rules/powershell/downgrade_attack.yml +++ b/rules/powershell/downgrade_attack.yml @@ -8,7 +8,7 @@ detection: selection: Channel: Windows PowerShell EventID: 400 - EventData: '[\s\S]*EngineVersion=2.0[\s\S]*' + EventData|re: '[\s\S]*EngineVersion=2\.0[\s\S]*' falsepositives: - unknown level: medium diff --git a/src/detections/rule.rs b/src/detections/rule.rs index cf62b496..e00906e1 100644 --- a/src/detections/rule.rs +++ b/src/detections/rule.rs @@ -1,6 +1,7 @@ extern crate regex; use mopa::mopafy; +use std::collections::VecDeque; use std::{collections::HashMap, sync::Arc, vec}; @@ -600,7 +601,6 @@ impl AggegationConditionCompiler { .replacen("|", "", 1); let tokens = self.tokenize(aggregation_str)?; - return self.parse(tokens); } @@ -1247,7 +1247,9 @@ impl LeafSelectionNode { return String::default(); } - return self.key_list[0].to_string(); + let topkey = self.key_list[0].to_string(); + let values: Vec<&str> = topkey.split("|").collect(); + return values[0].to_string(); } /// JSON形式のEventJSONから値を取得する関数 aliasも考慮されている。 @@ -1263,10 +1265,10 @@ impl LeafSelectionNode { /// 上から順番に調べて、一番始めに一致したMatcherが適用される fn get_matchers(&self) -> Vec> { return vec![ - Box::new(RegexMatcher::new()), Box::new(MinlengthMatcher::new()), Box::new(RegexesFileMatcher::new()), Box::new(WhitelistFileMatcher::new()), + Box::new(DefaultMatcher::new()), ]; } } @@ -1297,7 +1299,7 @@ impl SelectionNode for LeafSelectionNode { ] } */ - if self.key_list.len() > 0 && self.key_list[0].to_string() == "EventData" { + if self.get_key() == "EventData" { let values = utils::get_event_value(&"Event.EventData.Data".to_string(), event_record); if values.is_none() { return self.matcher.as_ref().unwrap().is_match(Option::None); @@ -1305,7 +1307,6 @@ impl SelectionNode for LeafSelectionNode { // 配列じゃなくて、文字列や数値等の場合は普通通りに比較する。 let eventdata_data = values.unwrap(); - if eventdata_data.is_boolean() || eventdata_data.is_i64() || eventdata_data.is_string() { return self @@ -1337,35 +1338,11 @@ impl SelectionNode for LeafSelectionNode { } fn init(&mut self) -> Result<(), Vec> { - let mut fixed_key_list = Vec::new(); // |xx を排除したkey_listを作成する - for key in &self.key_list { - if key.contains('|') { - let v: Vec<&str> = key.split('|').collect(); - self.matcher = match v[1] { - "startswith" => Some(Box::new(StartsWithMatcher::new())), - "endswith" => Some(Box::new(EndsWithMatcher::new())), - "contains" => Some(Box::new(ContainsMatcher::new())), - _ => { - return Result::Err(vec![format!( - "Found unknown key option. option: {}", - v[1] - )]) - } - }; - fixed_key_list.push(v[0].to_string()); - } else { - fixed_key_list.push(key.to_string()); - } - } - self.key_list = fixed_key_list; - let mut match_key_list = self.key_list.clone(); - match_key_list.remove(0); - if self.matcher.is_none() { - let matchers = self.get_matchers(); - self.matcher = matchers - .into_iter() - .find(|matcher| matcher.is_target_key(&match_key_list)); - } + let match_key_list = self.key_list.clone(); + let matchers = self.get_matchers(); + self.matcher = matchers + .into_iter() + .find(|matcher| matcher.is_target_key(&match_key_list)); // 一致するmatcherが見つからないエラー if self.matcher.is_none() { @@ -1404,105 +1381,20 @@ impl SelectionNode for LeafSelectionNode { // 新規にLeafMatcherを実装するクラスを作成した場合、 // LeafSelectionNodeのget_matchersクラスの戻り値の配列に新規作成したクラスのインスタンスを追加する。 trait LeafMatcher: mopa::Any { + /// 指定されたkey_listにマッチするLeafMatcherであるかどうか判定する。 fn is_target_key(&self, key_list: &Vec) -> bool; + /// 引数に指定されたJSON形式のデータがマッチするかどうか判定する。 + /// main.rsでWindows Event LogをJSON形式に変換していて、そのJSON形式のWindowsのイベントログデータがここには来る + /// 例えば正規表現でマッチするロジックなら、ここに正規表現でマッチさせる処理を書く。 fn is_match(&self, event_value: Option<&Value>) -> bool; + /// 初期化ロジックをここに記載します。 + /// ルールファイルの書き方が間違っている等の原因により、正しくルールファイルからパースできない場合、戻り値のResult型でエラーを返してください。 fn init(&mut self, key_list: &Vec, select_value: &Yaml) -> Result<(), Vec>; } mopafy!(LeafMatcher); -/// 正規表現で比較するロジックを表すクラス -struct RegexMatcher { - re: Option, -} - -impl RegexMatcher { - fn new() -> RegexMatcher { - return RegexMatcher { - re: Option::None, // empty - }; - } - fn is_regex_fullmatch(&self, re: &Regex, value: String) -> bool { - return re.find_iter(&value).any(|match_obj| { - return match_obj.as_str().to_string() == value; - }); - } -} - -impl LeafMatcher for RegexMatcher { - fn is_target_key(&self, key_list: &Vec) -> bool { - if key_list.is_empty() { - return true; - } - - if key_list.len() == 1 { - return key_list.get(0).unwrap_or(&"".to_string()) == &"regex".to_string(); - } else { - return false; - } - } - - fn init(&mut self, key_list: &Vec, select_value: &Yaml) -> Result<(), Vec> { - if select_value.is_null() { - self.re = Option::None; - return Result::Ok(()); - } - - // stringで比較する。 - let yaml_value = match select_value { - Yaml::Boolean(b) => Option::Some(b.to_string()), - Yaml::Integer(i) => Option::Some(i.to_string()), - Yaml::Real(r) => Option::Some(r.to_string()), - Yaml::String(s) => Option::Some(s.to_owned()), - _ => Option::None, - }; - // ここには来ないはず - if yaml_value.is_none() { - let errmsg = format!( - "unknown error occured. [key:{}]", - concat_selection_key(key_list) - ); - return Result::Err(vec![errmsg]); - } - - // 指定された正規表現が間違っていて、パースに失敗した場合 - let yaml_str = yaml_value.unwrap(); - let re_result = Regex::new(&yaml_str); - if re_result.is_err() { - let errmsg = format!( - "cannot parse regex. [regex:{}, key:{}]", - yaml_str, - concat_selection_key(key_list) - ); - return Result::Err(vec![errmsg]); - } - self.re = re_result.ok(); - - return Result::Ok(()); - } - - fn is_match(&self, event_value: Option<&Value>) -> bool { - // unwrap_orの引数に""ではなく" "を指定しているのは、 - // event_valueが文字列じゃない場合にis_event_value_nullの値がfalseになるように、len() == 0とならない値を指定している。 - let is_event_value_null = event_value.is_none() - || event_value.unwrap().is_null() - || event_value.unwrap().as_str().unwrap_or(" ").len() == 0; - - // yamlにnullが設定されていた場合 - if self.re.is_none() { - return is_event_value_null; - } - - return match event_value.unwrap_or(&Value::Null) { - Value::Bool(b) => self.is_regex_fullmatch(self.re.as_ref().unwrap(), b.to_string()), - Value::String(s) => self.is_regex_fullmatch(self.re.as_ref().unwrap(), s.to_owned()), - Value::Number(n) => self.is_regex_fullmatch(self.re.as_ref().unwrap(), n.to_string()), - _ => false, - }; - } -} - /// 指定された文字数以上であることをチェックするクラス。 struct MinlengthMatcher { min_len: i64, @@ -1516,11 +1408,11 @@ impl MinlengthMatcher { impl LeafMatcher for MinlengthMatcher { fn is_target_key(&self, key_list: &Vec) -> bool { - if key_list.len() != 1 { + if key_list.len() != 2 { return false; } - return key_list.get(0).unwrap() == "min_length"; + return key_list.get(1).unwrap() == "min_length"; } fn init(&mut self, key_list: &Vec, select_value: &Yaml) -> Result<(), Vec> { @@ -1562,11 +1454,11 @@ impl RegexesFileMatcher { impl LeafMatcher for RegexesFileMatcher { fn is_target_key(&self, key_list: &Vec) -> bool { - if key_list.len() != 1 { + if key_list.len() != 2 { return false; } - return key_list.get(0).unwrap() == "regexes"; + return key_list.get(1).unwrap() == "regexes"; } fn init(&mut self, key_list: &Vec, select_value: &Yaml) -> Result<(), Vec> { @@ -1598,6 +1490,7 @@ impl LeafMatcher for RegexesFileMatcher { } fn is_match(&self, event_value: Option<&Value>) -> bool { + //TODO Wildcardの場合、CaseInsensitiveなので、ToLowerする。 return match event_value.unwrap_or(&Value::Null) { Value::String(s) => !utils::check_regex(s, 0, &self.regexes_csv_content).is_empty(), Value::Number(n) => { @@ -1624,11 +1517,11 @@ impl WhitelistFileMatcher { impl LeafMatcher for WhitelistFileMatcher { fn is_target_key(&self, key_list: &Vec) -> bool { - if key_list.len() != 1 { + if key_list.len() != 2 { return false; } - return key_list.get(0).unwrap() == "whitelist"; + return key_list.get(1).unwrap() == "whitelist"; } fn init(&mut self, key_list: &Vec, select_value: &Yaml) -> Result<(), Vec> { @@ -1671,23 +1564,48 @@ impl LeafMatcher for WhitelistFileMatcher { } } -/// 指定された文字列で始まるか調べるクラス -struct StartsWithMatcher { - start_text: String, +/// デフォルトのマッチクラス +/// ワイルドカードの処理やパイプ +struct DefaultMatcher { + re: Option, + pipes: Vec, } -impl StartsWithMatcher { - fn new() -> StartsWithMatcher { - return StartsWithMatcher { - start_text: String::from(""), +impl DefaultMatcher { + fn new() -> DefaultMatcher { + return DefaultMatcher { + re: Option::None, + pipes: Vec::new(), }; } + + /// このmatcherの正規表現とマッチするかどうか判定します。 + /// 判定対象の文字列とこのmatcherが保持する正規表現が完全にマッチした場合のTRUEを返します。 + /// 例えば、判定対象文字列が"abc"で、正規表現が"ab"の場合、正規表現は判定対象文字列の一部分にしか一致していないので、この関数はfalseを返します。 + fn is_regex_fullmatch(&self, value: String) -> bool { + return self + .re + .as_ref() + .unwrap() + .find_iter(&value) + .any(|match_obj| { + return match_obj.as_str().to_string() == value; + }); + } + + /// YEAのルールファイルのフィールド名とそれに続いて指定されるパイプを、正規表現形式の文字列に変換します。 + /// ワイルドカードの文字列を正規表現にする処理もこのメソッドに実装されています。patternにワイルドカードの文字列を指定して、pipesにPipeElement::Wildcardを指定すればOK!! + fn from_pattern_to_regex_str(pattern: String, pipes: &Vec) -> String { + // パターンをPipeで処理する。 + return pipes.iter().fold(pattern, |acc, pipe| { + return pipe.pipe_pattern(acc); + }); + } } -impl LeafMatcher for StartsWithMatcher { - fn is_target_key(&self, _: &Vec) -> bool { - // ContextInfo|startswith のような場合にLeafをStartsWithMatcherにする。 - return false; +impl LeafMatcher for DefaultMatcher { + fn is_target_key(&self, key_list: &Vec) -> bool { + return key_list.len() == 1; } fn init(&mut self, key_list: &Vec, select_value: &Yaml) -> Result<(), Vec> { @@ -1695,7 +1613,7 @@ impl LeafMatcher for StartsWithMatcher { return Result::Ok(()); } - // stringに変換 + // patternをパースする let yaml_value = match select_value { Yaml::Boolean(b) => Option::Some(b.to_string()), Yaml::Integer(i) => Option::Some(i.to_string()), @@ -1710,135 +1628,247 @@ impl LeafMatcher for StartsWithMatcher { ); return Result::Err(vec![errmsg]); } + let pattern = yaml_value.unwrap(); - self.start_text = yaml_value.unwrap(); - return Result::Ok(()); - } + // Pipeが指定されていればパースする + let mut keys: VecDeque<&str> = key_list.get(0).unwrap().split("|").collect(); // key_listが空はあり得ない + keys.pop_front(); + while !keys.is_empty() { + let key = keys.pop_front().unwrap(); + let pipe_element = match key { + "startswith" => Option::Some(PipeElement::Startswith), + "endswith" => Option::Some(PipeElement::Endswith), + "contains" => Option::Some(PipeElement::Contains), + "re" => Option::Some(PipeElement::Re), + _ => Option::None, + }; + if pipe_element.is_none() { + let errmsg = format!( + "unknown pipe element was specified. key:{}", + concat_selection_key(key_list) + ); + return Result::Err(vec![errmsg]); + } - fn is_match(&self, event_value: Option<&Value>) -> bool { - // 調査する文字列がself.start_textで始まるならtrueを返す - return match event_value.unwrap_or(&Value::Null) { - Value::String(s) => s.starts_with(&self.start_text), - Value::Number(n) => n.to_string().starts_with(&self.start_text), - _ => false, - }; - } -} - -/// 指定された文字列で終わるか調べるクラス -struct EndsWithMatcher { - end_text: String, -} - -impl EndsWithMatcher { - fn new() -> EndsWithMatcher { - return EndsWithMatcher { - end_text: String::from(""), - }; - } -} - -impl LeafMatcher for EndsWithMatcher { - fn is_target_key(&self, _: &Vec) -> bool { - // ContextInfo|endswith のような場合にLeafをEndsWithMatcherにする。 - return false; - } - - fn init(&mut self, key_list: &Vec, select_value: &Yaml) -> Result<(), Vec> { - if select_value.is_null() { - return Result::Ok(()); + self.pipes.push(pipe_element.unwrap()); } - - // stringに変換 - let yaml_value = match select_value { - Yaml::Boolean(b) => Option::Some(b.to_string()), - Yaml::Integer(i) => Option::Some(i.to_string()), - Yaml::Real(r) => Option::Some(r.to_string()), - Yaml::String(s) => Option::Some(s.to_owned()), - _ => Option::None, - }; - if yaml_value.is_none() { + if self.pipes.len() >= 2 { + // 現状では複数のパイプは対応していない let errmsg = format!( - "unknown error occured. [key:{}]", + "multiple pipe element can't be used. key:{}", concat_selection_key(key_list) ); return Result::Err(vec![errmsg]); } - - self.end_text = yaml_value.unwrap(); - return Result::Ok(()); - } - - fn is_match(&self, event_value: Option<&Value>) -> bool { - // 調査する文字列がself.end_textで終わるならtrueを返す - return match event_value.unwrap_or(&Value::Null) { - Value::String(s) => s.ends_with(&self.end_text), - Value::Number(n) => n.to_string().ends_with(&self.end_text), - _ => false, - }; - } -} - -/// 指定された文字列が含まれるか調べるクラス -struct ContainsMatcher { - pattern: String, -} - -impl ContainsMatcher { - fn new() -> ContainsMatcher { - return ContainsMatcher { - pattern: String::from(""), - }; - } -} - -impl LeafMatcher for ContainsMatcher { - fn is_target_key(&self, _: &Vec) -> bool { - // ContextInfo|contains のような場合にLeafをContainsMatcherにする。 - return false; - } - - fn init(&mut self, key_list: &Vec, select_value: &Yaml) -> Result<(), Vec> { - if select_value.is_null() { - return Result::Ok(()); + let is_re = &self.pipes.iter().any(|pipe_element| { + return match pipe_element { + PipeElement::Re => true, + _ => false, + }; + }); + // 正規表現ではない場合、ワイルドカードであることを表す。 + // ワイルドカードは正規表現でマッチングするので、ワイルドカードを正規表現に変換するPipeを内部的に追加することにする。 + if !is_re { + self.pipes.push(PipeElement::Wildcard); } - // stringに変換 - let yaml_value = match select_value { - Yaml::Boolean(b) => Option::Some(b.to_string()), - Yaml::Integer(i) => Option::Some(i.to_string()), - Yaml::Real(r) => Option::Some(r.to_string()), - Yaml::String(s) => Option::Some(s.to_owned()), - _ => Option::None, - }; - if yaml_value.is_none() { + // パターンをPipeで処理する。 + let pattern = DefaultMatcher::from_pattern_to_regex_str(pattern, &self.pipes); + // Pipeで処理されたパターンを正規表現に変換 + let re_result = Regex::new(&pattern); + if re_result.is_err() { let errmsg = format!( - "unknown error occured. [key:{}]", + "cannot parse regex. [regex:{}, key:{}]", + pattern, concat_selection_key(key_list) ); return Result::Err(vec![errmsg]); } + self.re = re_result.ok(); - self.pattern = yaml_value.unwrap(); return Result::Ok(()); } fn is_match(&self, event_value: Option<&Value>) -> bool { - // 調査する文字列にself.patternが含まれるならtrueを返す - return match event_value.unwrap_or(&Value::Null) { - Value::String(s) => s.contains(&self.pattern), - Value::Number(n) => n.to_string().contains(&self.pattern), - _ => false, + // unwrap_orの引数に""ではなく" "を指定しているのは、 + // event_valueが文字列じゃない場合にis_event_value_nullの値がfalseになるように、len() == 0とならない値を指定している。 + let is_event_value_null = event_value.is_none() + || event_value.unwrap().is_null() + || event_value.unwrap().as_str().unwrap_or(" ").len() == 0; + + // yamlにnullが設定されていた場合 + if self.re.is_none() { + return is_event_value_null; + } + + // JSON形式のEventLogデータをstringに変換 + let event_value_str = match event_value.unwrap_or(&Value::Null) { + Value::Bool(b) => Option::Some(b.to_string()), + Value::String(s) => Option::Some(s.to_owned()), + Value::Number(n) => Option::Some(n.to_string()), + _ => Option::None, }; + if event_value_str.is_none() { + return false; + } + + // 変換したデータに対してパイプ処理を実行する。 + let event_value_str = (&self.pipes) + .iter() + .fold(event_value_str.unwrap(), |acc, pipe| { + return pipe.pipe_eventlog_data(acc); + }); + + return self.is_regex_fullmatch(event_value_str); + } +} + +/// パイプ(|)で指定される要素を表すクラス。 +enum PipeElement { + Startswith, + Endswith, + Contains, + Re, + Wildcard, +} + +impl PipeElement { + /// WindowsEventLogのJSONデータに対してパイプ処理します。 + fn pipe_eventlog_data(&self, pattern: String) -> String { + return match self { + // wildcardはcase sensetiveなので、全て小文字にして比較する。 + PipeElement::Wildcard => pattern.to_lowercase(), + _ => pattern, + }; + } + + /// patternをパイプ処理します + fn pipe_pattern(&self, pattern: String) -> String { + // enumでポリモーフィズムを実装すると、一つのメソッドに全部の型の実装をする感じになる。Java使い的にはキモイ感じがする。 + let fn_add_asterisk_end = |patt: String| { + if patt.ends_with("//*") { + return patt; + } else if patt.ends_with("/*") { + return patt + "*"; + } else if patt.ends_with("*") { + return patt; + } else { + return patt + "*"; + } + }; + let fn_add_asterisk_begin = |patt: String| { + if patt.starts_with("//*") { + return patt; + } else if patt.starts_with("/*") { + return "*".to_string() + &patt; + } else if patt.starts_with("*") { + return patt; + } else { + return "*".to_string() + &patt; + } + }; + + let val: String = match self { + // startswithの場合はpatternの最後にwildcardを足すことで対応する + PipeElement::Startswith => fn_add_asterisk_end(pattern), + // endswithの場合はpatternの最初にwildcardを足すことで対応する + PipeElement::Endswith => fn_add_asterisk_begin(pattern), + // containsの場合はpatternの前後にwildcardを足すことで対応する + PipeElement::Contains => fn_add_asterisk_end(fn_add_asterisk_begin(pattern)), + // 正規表現の場合は特に処理する必要無い + PipeElement::Re => pattern, + // WildCardは正規表現に変換する。 + PipeElement::Wildcard => PipeElement::pipe_pattern_wildcard(pattern), + }; + return val; + } + + /// PipeElement::Wildcardのパイプ処理です。 + /// pipe_pattern()に含めて良い処理ですが、複雑な処理になってしまったので別関数にしました。 + fn pipe_pattern_wildcard(pattern: String) -> String { + // wildcardはcase sensetiveなので、全て小文字にして比較する。 + let pattern = pattern.to_lowercase(); + let wildcards = vec!["*".to_string(), "?".to_string()]; + + // patternをwildcardでsplitした結果をpattern_splitsに入れる + // 以下のアルゴリズムの場合、pattern_splitsの偶数indexの要素はwildcardじゃない文字列となり、奇数indexの要素はwildcardが入る。 + let mut idx = 0; + let mut pattern_splits = vec![]; + let mut cur_str = String::default(); + while idx < pattern.len() { + let prev_idx = idx; + for wildcard in &wildcards { + let cur_pattern: String = pattern.chars().skip(idx).collect::(); + if cur_pattern.starts_with(&format!(r"\\{}", wildcard)) { + // wildcardの前にエスケープ文字が2つある場合 + cur_str = format!("{}{}", cur_str, r"\"); + pattern_splits.push(cur_str); + pattern_splits.push(wildcard.to_string()); + + cur_str = String::default(); + idx += 3; + break; + } else if cur_pattern.starts_with(&format!(r"\{}", wildcard)) { + // wildcardの前にエスケープ文字が1つある場合 + cur_str = format!("{}{}", cur_str, wildcard); + idx += 2; + break; + } else if cur_pattern.starts_with(wildcard) { + // wildcardの場合 + pattern_splits.push(cur_str); + pattern_splits.push(wildcard.to_string()); + + cur_str = String::default(); + idx += 1; + break; + } + } + // 上記のFor文でHitした場合はcontinue + if prev_idx != idx { + continue; + } + + cur_str = format!( + "{}{}", + cur_str, + pattern.chars().skip(idx).take(1).collect::() + ); + idx += 1; + } + // 最後の文字がwildcardじゃない場合は、cur_strに文字が入っているので、それをpattern_splitsに入れておく + if !cur_str.is_empty() { + pattern_splits.push(cur_str); + } + + // SIGMAルールのwildcard表記から正規表現の表記に変換します。 + return pattern_splits.iter().enumerate().fold( + String::default(), + |acc: String, (idx, pattern)| { + let regex_value = if idx % 2 == 0 { + // wildcardじゃない場合はescapeした文字列を返す + regex::escape(pattern) + } else { + // wildcardの場合、"*"は".*"という正規表現に変換し、"?"は"."に変換する。 + let wildcard_regex_value = if pattern.to_string() == "*" { + ".*" + } else { + "." + }; + wildcard_regex_value.to_string() + }; + + return format!("{}{}", acc, regex_value); + }, + ); } } #[cfg(test)] mod tests { use crate::detections::rule::{ - create_rule, AggregationConditionToken, AndSelectionNode, LeafSelectionNode, - MinlengthMatcher, OrSelectionNode, RegexMatcher, RegexesFileMatcher, SelectionNode, - WhitelistFileMatcher, + create_rule, AggregationConditionToken, AndSelectionNode, DefaultMatcher, + LeafSelectionNode, MinlengthMatcher, OrSelectionNode, PipeElement, RegexesFileMatcher, + SelectionNode, WhitelistFileMatcher, }; use yaml_rust::YamlLoader; @@ -1869,7 +1899,7 @@ mod tests { description: hogehoge enabled: true author: Yea - logsource: + logsource: product: windows detection: selection: @@ -1909,14 +1939,14 @@ mod tests { 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.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" + r"Microsoft\-Windows\-PowerShell/Operational".to_lowercase() ); } @@ -1933,8 +1963,8 @@ mod tests { 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.is::(), true); + let matcher = matcher.downcast_ref::().unwrap(); assert_eq!(matcher.re.is_some(), true); let re = matcher.re.as_ref(); @@ -1959,11 +1989,11 @@ mod tests { 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.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"); + assert_eq!(re.unwrap().as_str(), "Host Application".to_lowercase()); // LeafSelectionNodeである、ホスト アプリケーションノードが正しいことを確認 let hostapp_jp_node = ancestors[1].as_ref() as &dyn SelectionNode; @@ -1973,8 +2003,8 @@ mod tests { 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.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(), "ホスト アプリケーション"); @@ -2072,21 +2102,6 @@ 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_notdetect_regex_eventid() { // 完全一致なので、前方一致で検知しないことを確認 @@ -2229,6 +2244,63 @@ mod tests { } } } + + #[test] + fn test_detect_wildcard_multibyte() { + // multi byteの確認 + let rule_str = r#" + enabled: true + detection: + selection: + Channel: ホストアプリケーション + output: 'command=%CommandLine%' + "#; + + let record_json_str = r#" + { + "Event": {"System": {"EventID": 4103, "Channel": "ホストアプリケーション"}}, + "Event_attributes": {"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"} + }"#; + + let rule_node = parse_rule_from_str(rule_str); + match serde_json::from_str(record_json_str) { + Ok(record) => { + assert_eq!(rule_node.select(&record), true); + } + Err(_) => { + assert!(false, "failed to parse json record."); + } + } + } + + #[test] + fn test_detect_wildcard_multibyte_notdetect() { + // multi byteの確認 + let rule_str = r#" + enabled: true + detection: + selection: + Channel: ホスとアプリケーション + output: 'command=%CommandLine%' + "#; + + let record_json_str = r#" + { + "Event": {"System": {"EventID": 4103, "Channel": "ホストアプリケーション"}}, + "Event_attributes": {"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"} + }"#; + + let rule_node = parse_rule_from_str(rule_str); + match serde_json::from_str(record_json_str) { + Ok(record) => { + assert_eq!(rule_node.select(&record), false); + } + Err(_) => { + assert!(false, "failed to parse json record."); + } + } + } + #[test] fn test_detect_regex_str() { // 文字列っぽいデータでも完全一致することを確認 @@ -2520,8 +2592,8 @@ mod tests { } #[test] - fn test_notdetect_casesensetive() { - // OR条件が正しく検知できることを確認 + fn test_wildcard_case_insensitive() { + // wildcardは大文字小文字関係なくマッチする。 let rule_str = r#" enabled: true detection: @@ -2539,7 +2611,7 @@ mod tests { let rule_node = parse_rule_from_str(rule_str); match serde_json::from_str(record_json_str) { Ok(record) => { - assert_eq!(rule_node.select(&record), false); + assert_eq!(rule_node.select(&record), true); } Err(_) => { assert!(false, "failed to parse json record."); @@ -2642,7 +2714,6 @@ mod tests { detection: selection: Channel: - regex: Security10 min_length: 10 output: 'command=%CommandLine%' "#; @@ -2672,7 +2743,6 @@ mod tests { detection: selection: Channel: - regex: Security10 min_length: 11 output: 'command=%CommandLine%' "#; @@ -2701,7 +2771,7 @@ mod tests { enabled: true detection: selection: - Channel: ^Program$ + Channel|re: ^Program$ output: 'command=%CommandLine%' "#; @@ -3114,7 +3184,7 @@ mod tests { detection: selection: EventID: 403 - EventData: '[\s\S]*EngineVersion=2.0[\s\S]*' + EventData|re: '[\s\S]*EngineVersion=2\.0[\s\S]*' output: 'command=%CommandLine%' "#; @@ -3538,7 +3608,10 @@ mod tests { assert_eq!( rule_node.init(), - Err(vec!["Found unknown key option. option: failed".to_string()]) + Err(vec![ + "unknown pipe element was specified. key:detection -> selection -> Channel|failed" + .to_string() + ]) ); } @@ -4613,6 +4686,64 @@ mod tests { ); } + #[test] + fn test_pipe_pattern_wildcard_asterisk() { + let value = PipeElement::pipe_pattern_wildcard(r"*ho*ge*".to_string()); + assert_eq!(".*ho.*ge.*", value); + } + + #[test] + fn test_pipe_pattern_wildcard_asterisk2() { + let value = PipeElement::pipe_pattern_wildcard(r"\*ho\*\*ge\*".to_string()); + // wildcardの「\*」は文字列としての「*」を表す。 + // 正規表現で「*」はエスケープする必要があるので、\*が正解 + assert_eq!(r"\*ho\*\*ge\*", value); + } + + #[test] + fn test_pipe_pattern_wildcard_asterisk3() { + // wildcardの「\\*」は文字列としての「\」と正規表現の「.*」を表す。 + // 文字列としての「\」はエスケープされるので、「\\.*」が正解 + let value = PipeElement::pipe_pattern_wildcard(r"\\*ho\\*ge\\*".to_string()); + assert_eq!(r"\\.*ho\\.*ge\\.*", value); + } + + #[test] + fn test_pipe_pattern_wildcard_question() { + let value = PipeElement::pipe_pattern_wildcard(r"?ho?ge?".to_string()); + assert_eq!(r".ho.ge.", value); + } + + #[test] + fn test_pipe_pattern_wildcard_question2() { + let value = PipeElement::pipe_pattern_wildcard(r"\?ho\?ge\?".to_string()); + assert_eq!(r"\?ho\?ge\?", value); + } + + #[test] + fn test_pipe_pattern_wildcard_question3() { + let value = PipeElement::pipe_pattern_wildcard(r"\\?ho\\?ge\\?".to_string()); + assert_eq!(r"\\.ho\\.ge\\.", value); + } + + #[test] + fn test_pipe_pattern_wildcard_backshash() { + let value = PipeElement::pipe_pattern_wildcard(r"\\ho\\ge\\".to_string()); + assert_eq!(r"\\\\ho\\\\ge\\\\", value); + } + + #[test] + fn test_pipe_pattern_wildcard_mixed() { + let value = PipeElement::pipe_pattern_wildcard(r"\\*\****\*\\*".to_string()); + assert_eq!(r"\\.*\*.*.*.*\*\\.*", value); + } + + #[test] + fn test_pipe_pattern_wildcard_many_backshashs() { + let value = PipeElement::pipe_pattern_wildcard(r"\\\*ho\\\*ge\\\".to_string()); + assert_eq!(r"\\\\.*ho\\\\.*ge\\\\\\", value); + } + fn check_aggregation_condition_ope(expr: String, cmp_num: i32) -> AggregationConditionToken { let compiler = AggegationConditionCompiler::new(); let result = compiler.compile(expr);