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

feat: add support for multiple key grouping in count condition #1341

Merged
merged 4 commits into from
May 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
**改善:**

- `-d, --directory`オプションで複数のフォルダを指定できるようにした。 (#1335) (@hitenkoku)
- `count`で複数のグループを指定できるようにした。例: `count() by IpAddress,SubStatus,LogonType >= 2`。また、出力される結果を更新した。例: `[condition] count(TargetUserName) by IpAddress > 3 in timeframe [result] count: 4 TargetUserName:tanaka/Administrator/adsyncadmin/suzuki IpAddress:- timeframe:5m` -> `Count: 4 ¦ TargetUserName: tanaka/Administrator/adsyncadmin/suzuki ¦ IpAddress: -` (#1339) (@fukusuket)

## 2.15.0 [2024/04/20] "Sonic Release"

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

- You can now specify multiple directories with the `-d, --directory` option. (#1335) (@hitenkoku)
- You can now specify multiple groups with `count`. Ex: `count() by IpAddress,SubStatus,LogonType >= 2` Also, the output has been updated. Ex: `[condition] count(TargetUserName) by IpAddress > 3 in timeframe [result] count: 4 TargetUserName:tanaka/Administrator/adsyncadmin/suzuki IpAddress:- timeframe:5m` -> `Count: 4 ¦ TargetUserName: tanaka/Administrator/adsyncadmin/suzuki ¦ IpAddress: -` (#1339) (@fukusuket)

## 2.15.0 [2024/04/20] "Sonic Release"

Expand Down
77 changes: 32 additions & 45 deletions src/detections/detection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -973,7 +973,7 @@ impl Detection {

let detect_info = message::create_message(
&Value::default(),
CompactString::new(rule.yaml["details"].as_str().unwrap_or("-")),
CompactString::from(detect_info.detail.as_str()),
detect_info,
&profile_converter,
(true, is_json_timeline),
Expand Down Expand Up @@ -1001,58 +1001,47 @@ impl Detection {

///aggregation conditionのcount部分の検知出力文の文字列を返す関数
fn create_count_output(rule: &RuleNode, agg_result: &AggResult) -> CompactString {
// 条件式部分の出力
let mut ret: String = "[condition] ".to_string();
// この関数が呼び出されている段階で既にaggregation conditionは存在する前提なのでunwrap前の確認は行わない
let agg_condition = rule.get_agg_condition().unwrap();
let exist_timeframe = rule.yaml["detection"]["timeframe"].as_str().unwrap_or("") != "";
let mut ret: String = "".to_string();
// この関数が呼び出されている段階で既にaggregation conditionは存在する前提なのでagg_conditionの配列の長さは2となる
ret.push_str(
rule.yaml["detection"]["condition"]
.as_str()
.unwrap()
.split('|')
.nth(1)
.unwrap_or_default()
.trim(),
);
if exist_timeframe {
ret.push_str(" in timeframe");
}

write!(ret, " [result] count:{}", agg_result.data).ok();
let agg_condition = rule.get_agg_condition().unwrap();
write!(ret, "Count:{}", agg_result.data).ok();
if agg_condition._field_name.is_some() {
write!(
ret,
" {}:{}",
" ¦ {}:{}",
agg_condition._field_name.as_ref().unwrap(),
agg_result.field_values.join("/")
)
.ok();
}

if agg_condition._by_field_name.is_some() {
write!(
ret,
" {}:{}",
agg_condition._by_field_name.as_ref().unwrap(),
agg_result.key
)
.ok();
}

if exist_timeframe {
write!(
ret,
" timeframe:{}",
rule.yaml["detection"]["timeframe"].as_str().unwrap()
)
.ok();
let field_name = agg_condition._by_field_name.as_ref().unwrap();
if field_name.contains(',') {
write!(
ret,
" ¦ {}",
Self::zip_and_concat_strings(field_name, &agg_result.key)
)
.ok();
} else {
write!(ret, " ¦ {}:{}", field_name, agg_result.key).ok();
}
}

CompactString::from(ret)
}

fn zip_and_concat_strings(s1: &str, s2: &str) -> String {
let v1: Vec<&str> = s1.split(',').collect();
let v2: Vec<&str> = s2.split(',').collect();
v1.into_iter()
.zip(v2)
.map(|(s1, s2)| format!("{}:{}", s1, s2))
.collect::<Vec<String>>()
.join(" ¦ ")
}

pub fn print_rule_load_info(
rc: &HashMap<CompactString, u128>,
ld_rc: &HashMap<CompactString, u128>,
Expand Down Expand Up @@ -1337,7 +1326,7 @@ mod tests {
let test = rule_yaml.next().unwrap();
let mut rule_node = create_rule("testpath".to_string(), test);
rule_node.init(&create_dummy_stored_static()).ok();
let expected_output = "[condition] count() >= 1 [result] count:2";
let expected_output = "Count:2";
assert_eq!(
Detection::create_count_output(&rule_node, &agg_result),
expected_output
Expand All @@ -1364,7 +1353,7 @@ mod tests {
let test = rule_yaml.next().unwrap();
let mut rule_node = create_rule("testpath".to_string(), test);
rule_node.init(&create_dummy_stored_static()).ok();
let expected_output = "[condition] count() >= 1 [result] count:2";
let expected_output = "Count:2";
assert_eq!(
Detection::create_count_output(&rule_node, &agg_result),
expected_output
Expand Down Expand Up @@ -1392,8 +1381,7 @@ mod tests {
let test = rule_yaml.next().unwrap();
let mut rule_node = create_rule("testpath".to_string(), test);
rule_node.init(&create_dummy_stored_static()).ok();
let expected_output =
"[condition] count() >= 1 in timeframe [result] count:2 timeframe:15m";
let expected_output = "Count:2";
assert_eq!(
Detection::create_count_output(&rule_node, &agg_result),
expected_output
Expand Down Expand Up @@ -1423,7 +1411,7 @@ mod tests {
let test = rule_yaml.next().unwrap();
let mut rule_node = create_rule("testpath".to_string(), test);
rule_node.init(&create_dummy_stored_static()).ok();
let expected_output = "[condition] count(EventID) >= 1 [result] count:2 EventID:7040/9999";
let expected_output = "Count:2 ¦ EventID:7040/9999";
assert_eq!(
Detection::create_count_output(&rule_node, &agg_result),
expected_output
Expand Down Expand Up @@ -1453,7 +1441,7 @@ mod tests {
let test = rule_yaml.next().unwrap();
let mut rule_node = create_rule("testpath".to_string(), test);
rule_node.init(&create_dummy_stored_static()).ok();
let expected_output = "[condition] count(EventID) by process >= 1 [result] count:2 EventID:0000/1111 process:lsass.exe";
let expected_output = "Count:2 ¦ EventID:0000/1111 ¦ process:lsass.exe";
assert_eq!(
Detection::create_count_output(&rule_node, &agg_result),
expected_output
Expand Down Expand Up @@ -1482,8 +1470,7 @@ mod tests {
let test = rule_yaml.next().unwrap();
let mut rule_node = create_rule("testpath".to_string(), test);
rule_node.init(&create_dummy_stored_static()).ok();
let expected_output =
"[condition] count() by process >= 1 [result] count:2 process:lsass.exe";
let expected_output = "Count:2 ¦ process:lsass.exe";
assert_eq!(
Detection::create_count_output(&rule_node, &agg_result),
expected_output
Expand Down
30 changes: 29 additions & 1 deletion src/detections/rule/aggregation_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ lazy_static! {
Regex::new(r"^>=").unwrap(),
Regex::new(r"^<").unwrap(),
Regex::new(r"^>").unwrap(),
Regex::new(r"^\w+").unwrap(),
Regex::new(r"^(\s*\w+\s*,)+\s*\w+|^\w+").unwrap(),
];
pub static ref RE_PIPE: Regex = Regex::new(r"\|.*").unwrap();
}
Expand Down Expand Up @@ -309,6 +309,34 @@ mod tests {
assert!(matches!(result._cmp_op, AggregationConditionToken::GT));
}

#[test]
fn test_aggegation_condition_compiler_count_by_multiple_fieilds() {
let compiler = AggegationConditionCompiler::new();
let result = compiler.compile("select1 or select2 | count() by iiibbb,aaabbb > 27");
assert!(result.is_ok());
let result = result.unwrap();
assert!(result.is_some());
let result = result.unwrap();
assert_eq!("iiibbb,aaabbb".to_string(), result._by_field_name.unwrap());
assert!(result._field_name.is_none());
assert_eq!(27, result._cmp_num);
assert!(matches!(result._cmp_op, AggregationConditionToken::GT));
}

#[test]
fn test_aggegation_condition_compiler_count_by_multiple_fieilds_with_space() {
let compiler = AggegationConditionCompiler::new();
let result = compiler.compile("select1 or select2 | count() by iiibbb, aaabbb > 27");
assert!(result.is_ok());
let result = result.unwrap();
assert!(result.is_some());
let result = result.unwrap();
assert_eq!("iiibbb, aaabbb".to_string(), result._by_field_name.unwrap());
assert!(result._field_name.is_none());
assert_eq!(27, result._cmp_num);
assert!(matches!(result._cmp_op, AggregationConditionToken::GT));
}

#[test]
fn test_aggegation_condition_compiler_count_field() {
let compiler = AggegationConditionCompiler::new();
Expand Down
43 changes: 32 additions & 11 deletions src/detections/rule/count.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ pub fn count(
quiet_errors_flag: bool,
json_input_flag: bool,
) {
let key = create_count_key(
let key: String = create_count_key(
rule,
record,
verbose_flag,
Expand Down Expand Up @@ -138,16 +138,37 @@ pub fn create_count_key(
let agg_condition = rule.get_agg_condition().unwrap();
if agg_condition._by_field_name.is_some() {
let by_field_key = agg_condition._by_field_name.as_ref().unwrap();
get_alias_value_in_record(
rule,
by_field_key,
record,
true,
verbose_flag,
quiet_errors_flag,
eventkey_alias,
)
.unwrap_or_else(|| "_".to_string())
if by_field_key.contains(',') {
let mut res = String::default();
for key in by_field_key.split(',') {
res.push_str(
&get_alias_value_in_record(
rule,
key.trim(),
record,
true,
verbose_flag,
quiet_errors_flag,
eventkey_alias,
)
.unwrap_or_else(|| "_".to_string()),
);
res.push(',');
}
res.pop();
res
} else {
get_alias_value_in_record(
rule,
by_field_key,
record,
true,
verbose_flag,
quiet_errors_flag,
eventkey_alias,
)
.unwrap_or_else(|| "_".to_string())
}
} else {
"_".to_string()
}
Expand Down
Loading