Feature/sigmarule wildcard regex caseinsensitive#119 (#123)

* under constructing

* underconstructing

* fix rule file for SIGMA rule.

* wildcard case insensetive.

* refactor

* Update src/detections/rule.rs

add test triple backshash

Co-authored-by: itiB <is0312vx@ed.ritsumei.ac.jp>

* remove unnecessary if statement

Co-authored-by: itiB <is0312vx@ed.ritsumei.ac.jp>
This commit is contained in:
James
2021-07-02 20:19:53 +09:00
committed by GitHub
parent ea9f6b96c7
commit c13e6da932
7 changed files with 416 additions and 286 deletions

View File

@@ -9,7 +9,7 @@ detection:
Channel: Microsoft-Windows-PowerShell/Operational
EventID: 4104
Path: null
ScriptBlockText: '.+'
ScriptBlockText|re: '.+'
# condition: selection
falsepositives:
- unknown

View File

@@ -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:

View File

@@ -8,7 +8,7 @@ detection:
selection:
Channel: Security
EventID: 4688
CommandLine: '.+'
CommandLine|re: '.+'
# condition: selection
falsepositives:
- unknown

View File

@@ -8,8 +8,7 @@ detection:
selection:
Channel: Security
EventID: 4672
PrivilegeList:
contain: SeDebugPrivilege
PrivilegeList|contains: SeDebugPrivilege
# condition: selection
falsepositives:
- unknown

View File

@@ -8,7 +8,7 @@ detection:
selection:
Channel: Sysmon
EventID: 1
CommandLine: '.+'
CommandLine|re: '.+'
# condition: selection
falsepositives:
- unknown

View File

@@ -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

View File

@@ -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<Box<dyn LeafMatcher>> {
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<String>> {
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 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<String>) -> 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<String>, select_value: &Yaml) -> Result<(), Vec<String>>;
}
mopafy!(LeafMatcher);
/// 正規表現で比較するロジックを表すクラス
struct RegexMatcher {
re: Option<Regex>,
}
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<String>) -> 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<String>, select_value: &Yaml) -> Result<(), Vec<String>> {
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<String>) -> 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<String>, select_value: &Yaml) -> Result<(), Vec<String>> {
@@ -1562,11 +1454,11 @@ impl RegexesFileMatcher {
impl LeafMatcher for RegexesFileMatcher {
fn is_target_key(&self, key_list: &Vec<String>) -> 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<String>, select_value: &Yaml) -> Result<(), Vec<String>> {
@@ -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<String>) -> 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<String>, select_value: &Yaml) -> Result<(), Vec<String>> {
@@ -1671,23 +1564,48 @@ impl LeafMatcher for WhitelistFileMatcher {
}
}
/// 指定された文字列で始まるか調べるクラス
struct StartsWithMatcher {
start_text: String,
/// デフォルトのマッチクラス
/// ワイルドカードの処理やパイプ
struct DefaultMatcher {
re: Option<Regex>,
pipes: Vec<PipeElement>,
}
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;
});
}
impl LeafMatcher for StartsWithMatcher {
fn is_target_key(&self, _: &Vec<String>) -> bool {
// ContextInfo|startswith のような場合にLeafをStartsWithMatcherにする。
return false;
/// YEAのルールファイルのフィールド名とそれに続いて指定されるパイプを、正規表現形式の文字列に変換します。
/// ワイルドカードの文字列を正規表現にする処理もこのメソッドに実装されています。patternにワイルドカードの文字列を指定して、pipesにPipeElement::Wildcardを指定すればOK!!
fn from_pattern_to_regex_str(pattern: String, pipes: &Vec<PipeElement>) -> String {
// パターンをPipeで処理する。
return pipes.iter().fold(pattern, |acc, pipe| {
return pipe.pipe_pattern(acc);
});
}
}
impl LeafMatcher for DefaultMatcher {
fn is_target_key(&self, key_list: &Vec<String>) -> bool {
return key_list.len() == 1;
}
fn init(&mut self, key_list: &Vec<String>, select_value: &Yaml) -> Result<(), Vec<String>> {
@@ -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(());
}
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<String>) -> bool {
// ContextInfo|endswith のような場合にLeafをEndsWithMatcherにする。
return false;
}
fn init(&mut self, key_list: &Vec<String>, select_value: &Yaml) -> Result<(), Vec<String>> {
if select_value.is_null() {
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()),
// 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 yaml_value.is_none() {
if pipe_element.is_none() {
let errmsg = format!(
"unknown error occured. [key:{}]",
"unknown pipe element was specified. key:{}",
concat_selection_key(key_list)
);
return Result::Err(vec![errmsg]);
}
self.end_text = yaml_value.unwrap();
return Result::Ok(());
self.pipes.push(pipe_element.unwrap());
}
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<String>) -> bool {
// ContextInfo|contains のような場合にLeafをContainsMatcherにする。
return false;
}
fn init(&mut self, key_list: &Vec<String>, select_value: &Yaml) -> Result<(), Vec<String>> {
if select_value.is_null() {
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() {
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]);
}
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);
}
// パターンを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!(
"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::<String>();
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::<String>()
);
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;
@@ -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::<RegexMatcher>(), true);
let matcher = matcher.downcast_ref::<RegexMatcher>().unwrap();
assert_eq!(matcher.is::<DefaultMatcher>(), true);
let matcher = matcher.downcast_ref::<DefaultMatcher>().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::<RegexMatcher>(), true);
let matcher = matcher.downcast_ref::<RegexMatcher>().unwrap();
assert_eq!(matcher.is::<DefaultMatcher>(), true);
let matcher = matcher.downcast_ref::<DefaultMatcher>().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::<RegexMatcher>(), true);
let hostapp_en_matcher = hostapp_en_matcher.downcast_ref::<RegexMatcher>().unwrap();
assert_eq!(hostapp_en_matcher.is::<DefaultMatcher>(), true);
let hostapp_en_matcher = hostapp_en_matcher.downcast_ref::<DefaultMatcher>().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::<RegexMatcher>(), true);
let hostapp_jp_matcher = hostapp_jp_matcher.downcast_ref::<RegexMatcher>().unwrap();
assert_eq!(hostapp_jp_matcher.is::<DefaultMatcher>(), true);
let hostapp_jp_matcher = hostapp_jp_matcher.downcast_ref::<DefaultMatcher>().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);