Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reduce memory usage by removing unnecessary regex #894

Merged
merged 11 commits into from
Jan 26, 2023
1 change: 1 addition & 0 deletions CHANGELOG-Japanese.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- 正規表現使用前のフィルタリングにより、約14%の速度向上とメモリ使用量の削減を実現した。 (#883) (@fukusuket)
- 正規表現ではなく大文字小文字を区別しない比較により、約8%の速度向上とメモリ使用量の削減を実現した。 (#884) (@fukusuket)
- ワイルドカード表現における正規表現の使用量を削減することで、約5%の速度向上とメモリ使用量の削減を実現した。 (#890) (@fukusuket)
- 正規表現の使用を避けることで、さらなる高速化とメモリ使用量の削減を実現した。 (#894) (@fukusuket)
- JSON形式のイベントログファイルの入力に対応した。(#386) (@hitenkoku)

**バグ修正:**
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- Approximately 14% speed increase and memory usage reduction by filtering before regex usage. (#883) (@fukusuket)
- Approximately 8% speed increase and memory usage reduction by case-insensitive comparisons instead of regex usage. (#884) (@fukusuket)
- Approximately 5% speed increase and memory usage reduction by reducing regex usage in wildcard expressions. (#890) (@fukusuket)
- Further speed increase and memory usage reduction by removing unnecessary regex usage. (#894) (@fukusuket)
- Added support for JSON-formatted event logs as input. (#386) (@hitenkoku)

**Bug Fixes:**
Expand Down
10 changes: 5 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

235 changes: 158 additions & 77 deletions src/detections/rule/matchers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,13 +180,19 @@ impl LeafMatcher for AllowlistFileMatcher {
}
}

// 正規表現マッチは遅いため、できるだけ高速なstd::stringのlen/starts_with/ends_withでマッチ判定するためのenum
#[derive(PartialEq, Debug)]
enum FastMatch {
Exact(String),
StartsWith(String),
EndsWith(String),
}

/// デフォルトのマッチクラス
/// ワイルドカードの処理やパイプ
pub struct DefaultMatcher {
re: Option<Regex>,
match_str: Option<String>,
starts_with: Option<String>,
ends_with: Option<String>,
case_ignore_fast_match: Option<FastMatch>,
pipes: Vec<PipeElement>,
key_list: Nested<String>,
}
Expand All @@ -195,9 +201,7 @@ impl DefaultMatcher {
pub fn new() -> DefaultMatcher {
DefaultMatcher {
re: Option::None,
match_str: Option::None,
starts_with: Option::None,
ends_with: Option::None,
case_ignore_fast_match: Option::None,
pipes: Vec::new(),
key_list: Nested::<String>::new(),
}
Expand Down Expand Up @@ -225,6 +229,56 @@ impl DefaultMatcher {
.iter()
.fold(pattern, |acc, pipe| pipe.pipe_pattern(acc))
}

fn eq_ignore_case(event_value_str: &str, match_str: &str) -> bool {
if match_str.len() == event_value_str.len() {
return match_str.eq_ignore_ascii_case(event_value_str);
}
false
}

fn starts_with_ignore_case(event_value_str: &str, match_str: &str) -> Option<bool> {
let len = match_str.len();
if len > event_value_str.len() {
return Some(false);
}
// マルチバイト文字を含む場合は、index out of boundsになるため、asciiのみ
if event_value_str.is_ascii() {
let match_result = match_str.eq_ignore_ascii_case(&event_value_str[0..len]);
return Some(match_result);
}
None
}

fn ends_with_ignore_case(event_value_str: &str, match_str: &str) -> Option<bool> {
let len1 = match_str.len();
let len2 = event_value_str.len();
if len1 > len2 {
return Some(false);
}
// マルチバイト文字を含む場合は、index out of boundsになるため、asciiのみ
if event_value_str.is_ascii() {
let match_result = match_str.eq_ignore_ascii_case(&event_value_str[len2 - len1..]);
return Some(match_result);
}
None
}

// ワイルドカードマッチを高速なstd::stringのlen/starts_with/ends_withに変換するための関数
fn convert_to_fast_match(s: &str) -> Option<FastMatch> {
if s.contains('?') || s.chars().filter(|c| *c == '*').count() > 1 {
return None;
} else if s.starts_with('*') && s.is_ascii() {
let s = s.strip_prefix('*').unwrap();
return Some(FastMatch::EndsWith(s.replace(r"\\", r"\")));
} else if s.ends_with('*') && !s.ends_with(r"\\\*") && s.is_ascii() {
let s = s.strip_suffix('*').unwrap();
return Some(FastMatch::StartsWith(s.replace(r"\\", r"\")));
} else if s.contains('*') {
return None; // *が先頭・末尾以外にあるパターンは、starts_with/ends_withに変換できないため、正規表現マッチのみ
}
Some(FastMatch::Exact(s.replace(r"\\", r"\")))
}
}

impl LeafMatcher for DefaultMatcher {
Expand Down Expand Up @@ -260,7 +314,6 @@ impl LeafMatcher for DefaultMatcher {
return Result::Err(vec![errmsg]);
}
let pattern = yaml_value.unwrap();

// Pipeが指定されていればパースする
let emp = String::default();
// 一つ目はただのキーで、2つめ以降がpipe
Expand All @@ -282,26 +335,29 @@ impl LeafMatcher for DefaultMatcher {
}
let n = self.pipes.len();
if n == 0 {
// 正規表現マッチは遅いため、できるだけstd::stringのlen/stars_with/ends_with/containsでマッチ判定させるための処理
self.match_str = match select_value.as_str() {
None => None,
Some(s) if s.contains('?') => None, // ?を含む場合は、正規表現マッチのみ
Some(s) if s.chars().filter(|c| *c == '*').count() == 1 && s.is_ascii() => {
// ワイルドカード1文字だけ含む場合は、is_match()で、starts_with/ends_with相当のマッチに変換
if s.starts_with('*') {
if let Some(x) = s.strip_prefix('*') {
self.ends_with = Some(x.replace(r"\\", r"\"));
}
} else if s.ends_with('*') && !s.ends_with(r"\\\*") {
if let Some(x) = s.strip_suffix('*') {
self.starts_with = Some(x.replace(r"\\", r"\"));
}
}
None
}
Some(s) if s.contains('*') => None, //先頭・末尾のワイルドカード以外は、正規表現マッチのみ
Some(s) => Some(s.replace(r"\\", r"\")), //ワイルドカードを含まない場合は、is_match()で、文字列長マッチに変換
// パイプがないケース
if let Some(val) = select_value.as_str() {
self.case_ignore_fast_match = Self::convert_to_fast_match(val);
};
if matches!(&self.case_ignore_fast_match, Some(FastMatch::Exact(_)))
&& !self.key_list.is_empty()
{
// FastMatch::Exact検索に置き換えられたときは正規表現は不要
return Result::Ok(());
}
} else if n == 1 {
// パイプがあるケース
if let Some(val) = select_value.as_str() {
self.case_ignore_fast_match = match &self.pipes[0] {
PipeElement::Startswith => {
Self::convert_to_fast_match(format!("{}{}", val, '*').as_str())
}
PipeElement::Endswith => {
Self::convert_to_fast_match(format!("{}{}", '*', val).as_str())
}
_ => None,
};
}
} else if n >= 2 {
// 現状では複数のパイプは対応していない
let errmsg = format!(
Expand Down Expand Up @@ -352,12 +408,12 @@ impl LeafMatcher for DefaultMatcher {

// yamlにnullが設定されていた場合
// keylistが空(==JSONのgrep検索)の場合、無視する。
if self.key_list.is_empty() && self.re.is_none() {
if self.key_list.is_empty() && self.re.is_none() && self.case_ignore_fast_match.is_none() {
return false;
}

// yamlにnullが設定されていた場合
if self.re.is_none() {
if self.re.is_none() && self.case_ignore_fast_match.is_none() {
// レコード内に対象のフィールドが存在しなければ検知したものとして扱う
for v in self.key_list.iter() {
if recinfo.get_value(v).is_none() {
Expand All @@ -374,42 +430,19 @@ impl LeafMatcher for DefaultMatcher {
let event_value_str = event_value.unwrap();
if self.key_list.is_empty() {
// この場合ただのgrep検索なので、ただ正規表現に一致するかどうか調べればよいだけ
self.re.as_ref().unwrap().is_match(event_value_str)
} else {
// 通常の検索はこっち
if let Some(match_str) = &self.match_str {
//ワイルドカードを含まない場合はこの分岐。文字数を比較
if match_str.len() == event_value_str.len() {
return match_str.eq_ignore_ascii_case(event_value_str);
}
return false;
}
if let Some(match_str) = &self.starts_with {
// ワイルドカードを末尾に1つだけ含む場合はこの分岐。starts_with相当のマッチで比較
let len = match_str.len();
if len > event_value_str.len() {
return false;
}
if event_value_str.is_ascii() {
// マルチバイト文字を含む場合は、index out of boundsになるため、asciiのみ
return match_str.eq_ignore_ascii_case(&event_value_str[0..len]);
}
}
if let Some(match_str) = &self.ends_with {
// ワイルドカードを先頭に1つだけ含む場合はこの分岐。ends_with相当のマッチで比較
let len1 = match_str.len();
let len2 = event_value_str.len();
if len1 > len2 {
return false;
}
if event_value_str.is_ascii() {
// マルチバイト文字を含む場合は、index out of boundsになるため、asciiのみ
return match_str.eq_ignore_ascii_case(&event_value_str[len2 - len1..]);
}
return self.re.as_ref().unwrap().is_match(event_value_str);
} else if let Some(fast_matcher) = &self.case_ignore_fast_match {
let fast_match_result = match fast_matcher {
FastMatch::Exact(s) => Some(Self::eq_ignore_case(event_value_str, s)),
FastMatch::StartsWith(s) => Self::starts_with_ignore_case(event_value_str, s),
FastMatch::EndsWith(s) => Self::ends_with_ignore_case(event_value_str, s),
};
if let Some(is_match) = fast_match_result {
return is_match;
}
// 文字数/starts_with/ends_with検索に変換できなかった場合は、正規表現マッチで比較
self.is_regex_fullmatch(event_value_str)
}
// 文字数/starts_with/ends_with検索に変換できなかった場合は、正規表現マッチで比較
self.is_regex_fullmatch(event_value_str)
}
}

Expand Down Expand Up @@ -609,12 +642,14 @@ mod tests {
use super::super::matchers::{
AllowlistFileMatcher, DefaultMatcher, MinlengthMatcher, PipeElement, RegexesFileMatcher,
};

use super::super::selectionnodes::{
AndSelectionNode, LeafSelectionNode, OrSelectionNode, SelectionNode,
};
use crate::detections::configs::{
Action, Config, CsvOutputOption, InputOption, OutputOption, StoredStatic, STORED_EKEY_ALIAS,
};
use crate::detections::rule::matchers::FastMatch;
use crate::detections::rule::tests::parse_rule_from_str;
use crate::detections::{self, utils};

Expand Down Expand Up @@ -673,7 +708,7 @@ mod tests {
dummy_stored_static.verbose_flag,
dummy_stored_static.quiet_errors_flag,
dummy_stored_static.json_input_flag,
&dummy_stored_static.eventkey_alias
&dummy_stored_static.eventkey_alias,
),
expect_select
);
Expand Down Expand Up @@ -735,11 +770,11 @@ mod tests {
assert!(matcher.is::<DefaultMatcher>());
let matcher = matcher.downcast_ref::<DefaultMatcher>().unwrap();

assert!(matcher.re.is_some());
let re = matcher.re.as_ref();
assert!(matcher.case_ignore_fast_match.is_some());
let fast_match = matcher.case_ignore_fast_match.as_ref().unwrap();
assert_eq!(
re.unwrap().as_str(),
r"(?i)Microsoft\-Windows\-PowerShell/Operational"
*fast_match,
FastMatch::Exact("Microsoft-Windows-PowerShell/Operational".to_string())
);
}

Expand All @@ -758,10 +793,7 @@ mod tests {
let matcher = child_node.matcher.as_ref().unwrap();
assert!(matcher.is::<DefaultMatcher>());
let matcher = matcher.downcast_ref::<DefaultMatcher>().unwrap();

assert!(matcher.re.is_some());
let re = matcher.re.as_ref();
assert_eq!(re.unwrap().as_str(), "(?i)4103");
assert!(matcher.case_ignore_fast_match.is_none());
}

// ContextInfo
Expand All @@ -784,9 +816,12 @@ mod tests {
let hostapp_en_matcher = hostapp_en_matcher.as_ref().unwrap();
assert!(hostapp_en_matcher.is::<DefaultMatcher>());
let hostapp_en_matcher = hostapp_en_matcher.downcast_ref::<DefaultMatcher>().unwrap();
assert!(hostapp_en_matcher.re.is_some());
let re = hostapp_en_matcher.re.as_ref();
assert_eq!(re.unwrap().as_str(), "(?i)Host Application");
assert!(hostapp_en_matcher.case_ignore_fast_match.is_some());
let fast_match = hostapp_en_matcher.case_ignore_fast_match.as_ref().unwrap();
assert_eq!(
*fast_match,
FastMatch::Exact("Host Application".to_string())
);

// LeafSelectionNodeである、ホスト アプリケーションノードが正しいことを確認
let hostapp_jp_node = ancestors[1] as &dyn SelectionNode;
Expand All @@ -798,9 +833,12 @@ mod tests {
let hostapp_jp_matcher = hostapp_jp_matcher.as_ref().unwrap();
assert!(hostapp_jp_matcher.is::<DefaultMatcher>());
let hostapp_jp_matcher = hostapp_jp_matcher.downcast_ref::<DefaultMatcher>().unwrap();
assert!(hostapp_jp_matcher.re.is_some());
let re = hostapp_jp_matcher.re.as_ref();
assert_eq!(re.unwrap().as_str(), "(?i)ホスト アプリケーション");
assert!(hostapp_jp_matcher.case_ignore_fast_match.is_some());
let fast_match = hostapp_jp_matcher.case_ignore_fast_match.as_ref().unwrap();
assert_eq!(
*fast_match,
FastMatch::Exact("ホスト アプリケーション".to_string())
);
}

// ImagePath
Expand Down Expand Up @@ -2129,4 +2167,47 @@ mod tests {

check_select(rule_str, record_json_str, true);
}

#[test]
fn test_eq_ignore_case() {
assert!(DefaultMatcher::eq_ignore_case("abc", "abc"));
assert!(DefaultMatcher::eq_ignore_case("AbC", "abc"));
assert!(!DefaultMatcher::eq_ignore_case("abc", "ab"));
assert!(!DefaultMatcher::eq_ignore_case("ab", "abc"));
}

#[test]
fn test_starts_with_ignore_case() {
assert!(DefaultMatcher::starts_with_ignore_case("abc", "ab").unwrap(),);
assert!(DefaultMatcher::starts_with_ignore_case("AbC", "ab").unwrap(),);
assert!(!DefaultMatcher::starts_with_ignore_case("abc", "abcd").unwrap(),);
assert!(!DefaultMatcher::starts_with_ignore_case("aab", "ab").unwrap(),);
}

#[test]
fn test_ends_with_ignore_case() {
assert!(DefaultMatcher::ends_with_ignore_case("abc", "bc").unwrap());
assert!(DefaultMatcher::ends_with_ignore_case("AbC", "bc").unwrap());
assert!(!DefaultMatcher::ends_with_ignore_case("bc", "bcd").unwrap());
assert!(!DefaultMatcher::ends_with_ignore_case("bcd", "abc").unwrap());
}
#[test]
fn test_convert_to_fast_match() {
assert_eq!(DefaultMatcher::convert_to_fast_match("ab?"), None);
assert_eq!(DefaultMatcher::convert_to_fast_match("a*c"), None);
assert_eq!(DefaultMatcher::convert_to_fast_match("*a*b"), None);
assert_eq!(DefaultMatcher::convert_to_fast_match(r"a\\\*"), None);
assert_eq!(
DefaultMatcher::convert_to_fast_match("abc*").unwrap(),
FastMatch::StartsWith("abc".to_string())
);
assert_eq!(
DefaultMatcher::convert_to_fast_match("*abc").unwrap(),
FastMatch::EndsWith("abc".to_string())
);
assert_eq!(
DefaultMatcher::convert_to_fast_match("abc").unwrap(),
FastMatch::Exact("abc".to_string())
);
}
}