add equalsfield pipe (#467)
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
use regex::Regex;
|
||||
use std::collections::VecDeque;
|
||||
use std::{cmp::Ordering, collections::VecDeque};
|
||||
use yaml_rust::Yaml;
|
||||
|
||||
use crate::detections::{detection::EvtxRecordInfo, utils};
|
||||
@@ -192,6 +192,7 @@ pub struct DefaultMatcher {
|
||||
re: Option<Regex>,
|
||||
pipes: Vec<PipeElement>,
|
||||
key_list: Vec<String>,
|
||||
eqfield_key: Option<String>,
|
||||
}
|
||||
|
||||
impl DefaultMatcher {
|
||||
@@ -200,9 +201,14 @@ impl DefaultMatcher {
|
||||
re: Option::None,
|
||||
pipes: Vec::new(),
|
||||
key_list: Vec::new(),
|
||||
eqfield_key: Option::None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_eqfield_key(&self) -> Option<&String> {
|
||||
self.eqfield_key.as_ref()
|
||||
}
|
||||
|
||||
/// このmatcherの正規表現とマッチするかどうか判定します。
|
||||
/// 判定対象の文字列とこのmatcherが保持する正規表現が完全にマッチした場合のTRUEを返します。
|
||||
/// 例えば、判定対象文字列が"abc"で、正規表現が"ab"の場合、正規表現は判定対象文字列の一部分にしか一致していないので、この関数はfalseを返します。
|
||||
@@ -265,6 +271,7 @@ impl LeafMatcher for DefaultMatcher {
|
||||
"endswith" => Option::Some(PipeElement::Endswith),
|
||||
"contains" => Option::Some(PipeElement::Contains),
|
||||
"re" => Option::Some(PipeElement::Re),
|
||||
"equalsfield" => Option::Some(PipeElement::EqualsField),
|
||||
_ => Option::None,
|
||||
};
|
||||
if pipe_element.is_none() {
|
||||
@@ -285,34 +292,54 @@ impl LeafMatcher for DefaultMatcher {
|
||||
);
|
||||
return Result::Err(vec![errmsg]);
|
||||
}
|
||||
let is_re = &self
|
||||
|
||||
let is_eqfield = self
|
||||
.pipes
|
||||
.iter()
|
||||
.any(|pipe_element| matches!(pipe_element, PipeElement::Re));
|
||||
// 正規表現ではない場合、ワイルドカードであることを表す。
|
||||
// ワイルドカードは正規表現でマッチングするので、ワイルドカードを正規表現に変換するPipeを内部的に追加することにする。
|
||||
if !is_re {
|
||||
self.pipes.push(PipeElement::Wildcard);
|
||||
}
|
||||
.any(|pipe_element| matches!(pipe_element, PipeElement::EqualsField));
|
||||
if is_eqfield {
|
||||
// PipeElement::EqualsFieldは特別
|
||||
self.eqfield_key = Option::Some(pattern);
|
||||
} else {
|
||||
// 正規表現ではない場合、ワイルドカードであることを表す。
|
||||
// ワイルドカードは正規表現でマッチングするので、ワイルドカードを正規表現に変換するPipeを内部的に追加することにする。
|
||||
let is_re = self
|
||||
.pipes
|
||||
.iter()
|
||||
.any(|pipe_element| matches!(pipe_element, PipeElement::Re));
|
||||
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,
|
||||
utils::concat_selection_key(key_list)
|
||||
);
|
||||
return Result::Err(vec![errmsg]);
|
||||
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,
|
||||
utils::concat_selection_key(key_list)
|
||||
);
|
||||
return Result::Err(vec![errmsg]);
|
||||
}
|
||||
self.re = re_result.ok();
|
||||
}
|
||||
self.re = re_result.ok();
|
||||
|
||||
Result::Ok(())
|
||||
}
|
||||
|
||||
fn is_match(&self, event_value: Option<&String>, _recinfo: &EvtxRecordInfo) -> bool {
|
||||
fn is_match(&self, event_value: Option<&String>, recinfo: &EvtxRecordInfo) -> bool {
|
||||
// PipeElement::EqualsFieldが設定されていた場合
|
||||
if let Some(eqfield_key) = &self.eqfield_key {
|
||||
let another_value = recinfo.get_value(eqfield_key);
|
||||
// Evtxのレコードに存在しないeventkeyを指定された場合はfalseにする
|
||||
if event_value.is_none() || another_value.is_none() {
|
||||
return false;
|
||||
}
|
||||
|
||||
return another_value.unwrap().cmp(event_value.unwrap()) == Ordering::Equal;
|
||||
}
|
||||
|
||||
// yamlにnullが設定されていた場合
|
||||
// keylistが空(==JSONのgrep検索)の場合、無視する。
|
||||
if self.key_list.is_empty() && self.re.is_none() {
|
||||
@@ -341,6 +368,7 @@ enum PipeElement {
|
||||
Contains,
|
||||
Re,
|
||||
Wildcard,
|
||||
EqualsField,
|
||||
}
|
||||
|
||||
impl PipeElement {
|
||||
@@ -377,10 +405,9 @@ impl PipeElement {
|
||||
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),
|
||||
_ => pattern,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1707,4 +1734,128 @@ mod tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_eq_field() {
|
||||
// equalsfieldsで正しく検知できることを確認
|
||||
let rule_str = r#"
|
||||
detection:
|
||||
selection:
|
||||
Channel|equalsfield: Computer
|
||||
details: 'command=%CommandLine%'
|
||||
"#;
|
||||
|
||||
let record_json_str = r#"
|
||||
{
|
||||
"Event": {"System": {"EventID": 4103, "Channel": "Security", "Computer": "Security" }},
|
||||
"Event_attributes": {"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"}
|
||||
}"#;
|
||||
|
||||
let mut rule_node = parse_rule_from_str(rule_str);
|
||||
match serde_json::from_str(record_json_str) {
|
||||
Ok(record) => {
|
||||
let keys = detections::rule::get_detection_keys(&rule_node);
|
||||
let recinfo = utils::create_rec_info(record, "testpath".to_owned(), &keys);
|
||||
assert!(rule_node.select(&recinfo));
|
||||
}
|
||||
Err(_) => {
|
||||
panic!("Failed to parse json record.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_eq_field_notdetect() {
|
||||
// equalsfieldsの検知できないパターン
|
||||
// equalsfieldsで正しく検知できることを確認
|
||||
let rule_str = r#"
|
||||
detection:
|
||||
selection:
|
||||
Channel|equalsfield: Computer
|
||||
details: 'command=%CommandLine%'
|
||||
"#;
|
||||
|
||||
let record_json_str = r#"
|
||||
{
|
||||
"Event": {"System": {"EventID": 4103, "Channel": "Security", "Computer": "Powershell" }},
|
||||
"Event_attributes": {"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"}
|
||||
}"#;
|
||||
|
||||
let mut rule_node = parse_rule_from_str(rule_str);
|
||||
match serde_json::from_str(record_json_str) {
|
||||
Ok(record) => {
|
||||
let keys = detections::rule::get_detection_keys(&rule_node);
|
||||
let recinfo = utils::create_rec_info(record, "testpath".to_owned(), &keys);
|
||||
assert!(!rule_node.select(&recinfo));
|
||||
}
|
||||
Err(_) => {
|
||||
panic!("Failed to parse json record.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_eq_field_emptyfield() {
|
||||
// 存在しないフィールドを指定した場合は検知しない
|
||||
let rule_str = r#"
|
||||
detection:
|
||||
selection:
|
||||
Channel|equalsfield: NoField
|
||||
details: 'command=%CommandLine%'
|
||||
"#;
|
||||
|
||||
let record_json_str = r#"
|
||||
{
|
||||
"Event": {"System": {"EventID": 4103, "Channel": "Security", "Computer": "Securiti" }},
|
||||
"Event_attributes": {"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"}
|
||||
}"#;
|
||||
|
||||
let mut rule_node = parse_rule_from_str(rule_str);
|
||||
match serde_json::from_str(record_json_str) {
|
||||
Ok(record) => {
|
||||
let keys = detections::rule::get_detection_keys(&rule_node);
|
||||
let recinfo = utils::create_rec_info(record, "testpath".to_owned(), &keys);
|
||||
assert!(!rule_node.select(&recinfo));
|
||||
}
|
||||
Err(_) => {
|
||||
panic!("Failed to parse json record.");
|
||||
}
|
||||
}
|
||||
|
||||
let rule_str = r#"
|
||||
detection:
|
||||
selection:
|
||||
NoField|equalsfield: Channel
|
||||
details: 'command=%CommandLine%'
|
||||
"#;
|
||||
let mut rule_node = parse_rule_from_str(rule_str);
|
||||
match serde_json::from_str(record_json_str) {
|
||||
Ok(record) => {
|
||||
let keys = detections::rule::get_detection_keys(&rule_node);
|
||||
let recinfo = utils::create_rec_info(record, "testpath".to_owned(), &keys);
|
||||
assert!(!rule_node.select(&recinfo));
|
||||
}
|
||||
Err(_) => {
|
||||
panic!("Failed to parse json record.");
|
||||
}
|
||||
}
|
||||
|
||||
let rule_str = r#"
|
||||
detection:
|
||||
selection:
|
||||
NoField|equalsfield: NoField1
|
||||
details: 'command=%CommandLine%'
|
||||
"#;
|
||||
let mut rule_node = parse_rule_from_str(rule_str);
|
||||
match serde_json::from_str(record_json_str) {
|
||||
Ok(record) => {
|
||||
let keys = detections::rule::get_detection_keys(&rule_node);
|
||||
let recinfo = utils::create_rec_info(record, "testpath".to_owned(), &keys);
|
||||
assert!(!rule_node.select(&recinfo));
|
||||
}
|
||||
Err(_) => {
|
||||
panic!("Failed to parse json record.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,19 +106,21 @@ pub fn get_detection_keys(node: &RuleNode) -> Vec<String> {
|
||||
for key in detection.name_to_selection.keys() {
|
||||
let selection = &detection.name_to_selection[key];
|
||||
let desc = selection.get_descendants();
|
||||
let keys = desc.iter().filter_map(|node| {
|
||||
desc.iter().for_each(|node| {
|
||||
if !node.is::<LeafSelectionNode>() {
|
||||
return Option::None;
|
||||
return;
|
||||
}
|
||||
|
||||
let node = node.downcast_ref::<LeafSelectionNode>().unwrap();
|
||||
let key = node.get_key();
|
||||
if key.is_empty() {
|
||||
return Option::None;
|
||||
}
|
||||
Option::Some(key.to_string())
|
||||
let keys = node.get_keys();
|
||||
let keys = keys.iter().filter_map(|key| {
|
||||
if key.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(key.to_string())
|
||||
});
|
||||
ret.extend(keys);
|
||||
});
|
||||
ret.extend(keys);
|
||||
}
|
||||
|
||||
ret
|
||||
|
||||
@@ -3,7 +3,7 @@ use downcast_rs::Downcast;
|
||||
use std::{sync::Arc, vec};
|
||||
use yaml_rust::Yaml;
|
||||
|
||||
use super::matchers;
|
||||
use super::matchers::{self, DefaultMatcher};
|
||||
|
||||
// Ruleファイルの detection- selection配下のノードはこのtraitを実装する。
|
||||
pub trait SelectionNode: Downcast {
|
||||
@@ -250,6 +250,24 @@ impl LeafSelectionNode {
|
||||
&self.key
|
||||
}
|
||||
|
||||
pub fn get_keys(&self) -> Vec<&String> {
|
||||
let mut keys = vec![];
|
||||
if !self.key.is_empty() {
|
||||
keys.push(&self.key);
|
||||
}
|
||||
|
||||
if let Some(matcher) = &self.matcher {
|
||||
let matcher = matcher.downcast_ref::<DefaultMatcher>();
|
||||
if let Some(matcher) = matcher {
|
||||
if let Some(eq_key) = matcher.get_eqfield_key() {
|
||||
keys.push(eq_key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
keys
|
||||
}
|
||||
|
||||
fn _create_key(&self) -> String {
|
||||
if self.key_list.is_empty() {
|
||||
return String::default();
|
||||
|
||||
Reference in New Issue
Block a user