rule.rsを分割する (#121)
* Refact: split code for matcher from rule.rs * Reafact: combine multiple declared functions * Refact: split code for SelectionNode from rule.rs * Refact: mv test code for SelectionNode from rule.rs * Refact: mv condition's code from rule.rs * Refact: mv aggregation's code from condition_parser.rs * Refact: use relationships * cargo fmt --all * remove unnecessary matcher Co-authored-by: HajimeTakai <takai.wa.hajime@gmail.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
473
src/detections/rule/aggregation_parser.rs
Normal file
473
src/detections/rule/aggregation_parser.rs
Normal file
@@ -0,0 +1,473 @@
|
||||
use regex::Regex;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AggregationParseInfo {
|
||||
_field_name: Option<String>, // countの括弧に囲まれた部分の文字
|
||||
_by_field_name: Option<String>, // count() by の後に指定される文字列
|
||||
_cmp_op: AggregationConditionToken, // (必須)<とか>とか何が指定されたのか
|
||||
_cmp_num: i32, // (必須)<とか>とかの後にある数値
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AggregationConditionToken {
|
||||
COUNT(String), // count
|
||||
SPACE, // 空白
|
||||
BY, // by
|
||||
EQ, // ..と等しい
|
||||
LE, // ..以下
|
||||
LT, // ..未満
|
||||
GE, // ..以上
|
||||
GT, // .よりおおきい
|
||||
KEYWORD(String), // BYのフィールド名
|
||||
}
|
||||
|
||||
/// SIGMAルールでいうAggregationConditionを解析する。
|
||||
/// AggregationConditionはconditionに指定された式のパイプ以降の部分を指してます。
|
||||
#[derive(Debug)]
|
||||
pub struct AggegationConditionCompiler {
|
||||
regex_patterns: Vec<Regex>,
|
||||
}
|
||||
|
||||
impl AggegationConditionCompiler {
|
||||
pub fn new() -> Self {
|
||||
// ここで字句解析するときに使う正規表現の一覧を定義する。
|
||||
// ここはSigmaのGithubレポジトリにある、toos/sigma/parser/condition.pyのSigmaConditionTokenizerのtokendefsを参考にしています。
|
||||
let mut regex_patterns = vec![];
|
||||
regex_patterns.push(Regex::new(r"^count\( *\w* *\)").unwrap()); // countの式
|
||||
regex_patterns.push(Regex::new(r"^ ").unwrap());
|
||||
regex_patterns.push(Regex::new(r"^by").unwrap());
|
||||
regex_patterns.push(Regex::new(r"^==").unwrap());
|
||||
regex_patterns.push(Regex::new(r"^<=").unwrap());
|
||||
regex_patterns.push(Regex::new(r"^>=").unwrap());
|
||||
regex_patterns.push(Regex::new(r"^<").unwrap());
|
||||
regex_patterns.push(Regex::new(r"^>").unwrap());
|
||||
regex_patterns.push(Regex::new(r"^\w+").unwrap());
|
||||
|
||||
return AggegationConditionCompiler {
|
||||
regex_patterns: regex_patterns,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn compile(&self, condition_str: String) -> Result<Option<AggregationParseInfo>, String> {
|
||||
let result = self.compile_body(condition_str);
|
||||
if let Result::Err(msg) = result {
|
||||
return Result::Err(format!(
|
||||
"aggregation condition parse error has occurred. {}",
|
||||
msg
|
||||
));
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compile_body(
|
||||
&self,
|
||||
condition_str: String,
|
||||
) -> Result<Option<AggregationParseInfo>, String> {
|
||||
// パイプの部分だけを取り出す
|
||||
let re_pipe = Regex::new(r"\|.*").unwrap();
|
||||
let captured = re_pipe.captures(&condition_str);
|
||||
if captured.is_none() {
|
||||
// パイプが無いので終了
|
||||
return Result::Ok(Option::None);
|
||||
}
|
||||
// ハイプ自体は削除してからパースする。
|
||||
let aggregation_str = captured
|
||||
.unwrap()
|
||||
.get(0)
|
||||
.unwrap()
|
||||
.as_str()
|
||||
.to_string()
|
||||
.replacen("|", "", 1);
|
||||
|
||||
let tokens = self.tokenize(aggregation_str)?;
|
||||
|
||||
return self.parse(tokens);
|
||||
}
|
||||
|
||||
/// 字句解析します。
|
||||
pub fn tokenize(
|
||||
&self,
|
||||
condition_str: String,
|
||||
) -> Result<Vec<AggregationConditionToken>, String> {
|
||||
let mut cur_condition_str = condition_str.clone();
|
||||
|
||||
let mut tokens = Vec::new();
|
||||
while cur_condition_str.len() != 0 {
|
||||
let captured = self.regex_patterns.iter().find_map(|regex| {
|
||||
return regex.captures(cur_condition_str.as_str());
|
||||
});
|
||||
if captured.is_none() {
|
||||
// トークンにマッチしないのはありえないという方針でパースしています。
|
||||
return Result::Err("An unusable character was found.".to_string());
|
||||
}
|
||||
|
||||
let mached_str = captured.unwrap().get(0).unwrap().as_str();
|
||||
let token = self.to_enum(mached_str.to_string());
|
||||
|
||||
if let AggregationConditionToken::SPACE = token {
|
||||
// 空白は特に意味ないので、読み飛ばす。
|
||||
cur_condition_str = cur_condition_str.replacen(mached_str, "", 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
tokens.push(token);
|
||||
cur_condition_str = cur_condition_str.replacen(mached_str, "", 1);
|
||||
}
|
||||
|
||||
return Result::Ok(tokens);
|
||||
}
|
||||
|
||||
/// 比較演算子かどうか判定します。
|
||||
fn is_cmp_op(&self, token: &AggregationConditionToken) -> bool {
|
||||
return match token {
|
||||
AggregationConditionToken::EQ => true,
|
||||
AggregationConditionToken::LE => true,
|
||||
AggregationConditionToken::LT => true,
|
||||
AggregationConditionToken::GE => true,
|
||||
AggregationConditionToken::GT => true,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
/// 構文解析します。
|
||||
fn parse(
|
||||
&self,
|
||||
tokens: Vec<AggregationConditionToken>,
|
||||
) -> Result<Option<AggregationParseInfo>, String> {
|
||||
if tokens.is_empty() {
|
||||
// パイプしか無いのはおかしいのでエラー
|
||||
return Result::Err("There are no strings after pipe(|).".to_string());
|
||||
}
|
||||
|
||||
let mut token_ite = tokens.into_iter();
|
||||
let token = token_ite.next().unwrap();
|
||||
|
||||
let mut count_field_name: Option<String> = Option::None;
|
||||
if let AggregationConditionToken::COUNT(field_name) = token {
|
||||
if !field_name.is_empty() {
|
||||
count_field_name = Option::Some(field_name);
|
||||
}
|
||||
} else {
|
||||
// いろんなパターンがあるので難しいが、countというキーワードしか使えないことを説明しておく。
|
||||
return Result::Err("aggregation condition can use count only.".to_string());
|
||||
}
|
||||
|
||||
let token = token_ite.next();
|
||||
if token.is_none() {
|
||||
// 論理演算子がないのはだめ
|
||||
return Result::Err(
|
||||
"count keyword needs compare operator and number like '> 3'".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
// BYはオプションでつけなくても良い
|
||||
let mut by_field_name = Option::None;
|
||||
let token = token.unwrap();
|
||||
let token = if let AggregationConditionToken::BY = token {
|
||||
let after_by = token_ite.next();
|
||||
if after_by.is_none() {
|
||||
// BYの後に何もないのはだめ
|
||||
return Result::Err("by keyword needs field name like 'by EventID'".to_string());
|
||||
}
|
||||
|
||||
if let AggregationConditionToken::KEYWORD(keyword) = after_by.unwrap() {
|
||||
by_field_name = Option::Some(keyword);
|
||||
token_ite.next()
|
||||
} else {
|
||||
return Result::Err("by keyword needs field name like 'by EventID'".to_string());
|
||||
}
|
||||
} else {
|
||||
Option::Some(token)
|
||||
};
|
||||
|
||||
// 比較演算子と数値をパース
|
||||
if token.is_none() {
|
||||
// 論理演算子がないのはだめ
|
||||
return Result::Err(
|
||||
"count keyword needs compare operator and number like '> 3'".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let cmp_token = token.unwrap();
|
||||
if !self.is_cmp_op(&cmp_token) {
|
||||
return Result::Err(
|
||||
"count keyword needs compare operator and number like '> 3'".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let token = token_ite.next().unwrap_or(AggregationConditionToken::SPACE);
|
||||
let cmp_number = if let AggregationConditionToken::KEYWORD(number) = token {
|
||||
let number: Result<i32, _> = number.parse();
|
||||
if number.is_err() {
|
||||
// 比較演算子の後に数値が無い。
|
||||
return Result::Err("compare operator needs a number like '> 3'.".to_string());
|
||||
} else {
|
||||
number.unwrap()
|
||||
}
|
||||
} else {
|
||||
// 比較演算子の後に数値が無い。
|
||||
return Result::Err("compare operator needs a number like '> 3'.".to_string());
|
||||
};
|
||||
|
||||
if token_ite.next().is_some() {
|
||||
return Result::Err("unnecessary word was found.".to_string());
|
||||
}
|
||||
|
||||
let info = AggregationParseInfo {
|
||||
_field_name: count_field_name,
|
||||
_by_field_name: by_field_name,
|
||||
_cmp_op: cmp_token,
|
||||
_cmp_num: cmp_number,
|
||||
};
|
||||
return Result::Ok(Option::Some(info));
|
||||
}
|
||||
|
||||
/// 文字列をConditionTokenに変換する。
|
||||
fn to_enum(&self, token: String) -> AggregationConditionToken {
|
||||
if token.starts_with("count(") {
|
||||
let count_field = token
|
||||
.replacen("count(", "", 1)
|
||||
.replacen(")", "", 1)
|
||||
.replace(" ", "");
|
||||
return AggregationConditionToken::COUNT(count_field);
|
||||
} else if token == " " {
|
||||
return AggregationConditionToken::SPACE;
|
||||
} else if token == "by" {
|
||||
return AggregationConditionToken::BY;
|
||||
} else if token == "==" {
|
||||
return AggregationConditionToken::EQ;
|
||||
} else if token == "<=" {
|
||||
return AggregationConditionToken::LE;
|
||||
} else if token == ">=" {
|
||||
return AggregationConditionToken::GE;
|
||||
} else if token == "<" {
|
||||
return AggregationConditionToken::LT;
|
||||
} else if token == ">" {
|
||||
return AggregationConditionToken::GT;
|
||||
} else {
|
||||
return AggregationConditionToken::KEYWORD(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::super::aggregation_parser::{
|
||||
AggegationConditionCompiler, AggregationConditionToken,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_aggegation_condition_compiler_no_count() {
|
||||
// countが無いパターン
|
||||
let compiler = AggegationConditionCompiler::new();
|
||||
let result = compiler.compile("select1 and select2".to_string());
|
||||
assert_eq!(true, result.is_ok());
|
||||
let result = result.unwrap();
|
||||
assert_eq!(true, result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_aggegation_condition_compiler_count_ope() {
|
||||
// 正常系 countの中身にフィールドが無い 各種演算子を試す
|
||||
let token =
|
||||
check_aggregation_condition_ope("select1 and select2|count() > 32".to_string(), 32);
|
||||
let is_gt = match token {
|
||||
AggregationConditionToken::GT => true,
|
||||
_ => false,
|
||||
};
|
||||
assert_eq!(is_gt, true);
|
||||
|
||||
let token =
|
||||
check_aggregation_condition_ope("select1 and select2|count() >= 43".to_string(), 43);
|
||||
let is_gt = match token {
|
||||
AggregationConditionToken::GE => true,
|
||||
_ => false,
|
||||
};
|
||||
assert_eq!(is_gt, true);
|
||||
|
||||
let token =
|
||||
check_aggregation_condition_ope("select1 and select2|count() < 59".to_string(), 59);
|
||||
let is_gt = match token {
|
||||
AggregationConditionToken::LT => true,
|
||||
_ => false,
|
||||
};
|
||||
assert_eq!(is_gt, true);
|
||||
|
||||
let token =
|
||||
check_aggregation_condition_ope("select1 and select2|count() <= 12".to_string(), 12);
|
||||
let is_gt = match token {
|
||||
AggregationConditionToken::LE => true,
|
||||
_ => false,
|
||||
};
|
||||
assert_eq!(is_gt, true);
|
||||
|
||||
let token =
|
||||
check_aggregation_condition_ope("select1 and select2|count() == 28".to_string(), 28);
|
||||
let is_gt = match token {
|
||||
AggregationConditionToken::EQ => true,
|
||||
_ => false,
|
||||
};
|
||||
assert_eq!(is_gt, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_aggegation_condition_compiler_count_by() {
|
||||
let compiler = AggegationConditionCompiler::new();
|
||||
let result = compiler.compile("select1 or select2 | count() by iiibbb > 27".to_string());
|
||||
|
||||
assert_eq!(true, result.is_ok());
|
||||
let result = result.unwrap();
|
||||
assert_eq!(true, result.is_some());
|
||||
|
||||
let result = result.unwrap();
|
||||
assert_eq!("iiibbb".to_string(), result._by_field_name.unwrap());
|
||||
assert_eq!(true, result._field_name.is_none());
|
||||
assert_eq!(27, result._cmp_num);
|
||||
let is_ok = match result._cmp_op {
|
||||
AggregationConditionToken::GT => true,
|
||||
_ => false,
|
||||
};
|
||||
assert_eq!(true, is_ok);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_aggegation_condition_compiler_count_field() {
|
||||
let compiler = AggegationConditionCompiler::new();
|
||||
let result = compiler.compile("select1 or select2 | count( hogehoge ) > 3".to_string());
|
||||
|
||||
assert_eq!(true, result.is_ok());
|
||||
let result = result.unwrap();
|
||||
assert_eq!(true, result.is_some());
|
||||
|
||||
let result = result.unwrap();
|
||||
assert_eq!(true, result._by_field_name.is_none());
|
||||
assert_eq!("hogehoge", result._field_name.unwrap());
|
||||
assert_eq!(3, result._cmp_num);
|
||||
let is_ok = match result._cmp_op {
|
||||
AggregationConditionToken::GT => true,
|
||||
_ => false,
|
||||
};
|
||||
assert_eq!(true, is_ok);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_aggegation_condition_compiler_count_all_field() {
|
||||
let compiler = AggegationConditionCompiler::new();
|
||||
let result =
|
||||
compiler.compile("select1 or select2 | count( hogehoge) by snsn > 3".to_string());
|
||||
|
||||
assert_eq!(true, result.is_ok());
|
||||
let result = result.unwrap();
|
||||
assert_eq!(true, result.is_some());
|
||||
|
||||
let result = result.unwrap();
|
||||
assert_eq!("snsn".to_string(), result._by_field_name.unwrap());
|
||||
assert_eq!("hogehoge", result._field_name.unwrap());
|
||||
assert_eq!(3, result._cmp_num);
|
||||
let is_ok = match result._cmp_op {
|
||||
AggregationConditionToken::GT => true,
|
||||
_ => false,
|
||||
};
|
||||
assert_eq!(true, is_ok);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_aggegation_condition_compiler_only_pipe() {
|
||||
let compiler = AggegationConditionCompiler::new();
|
||||
let result = compiler.compile("select1 or select2 |".to_string());
|
||||
|
||||
assert_eq!(true, result.is_err());
|
||||
assert_eq!(
|
||||
"aggregation condition parse error has occurred. There are no strings after pipe(|)."
|
||||
.to_string(),
|
||||
result.unwrap_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_aggegation_condition_compiler_unused_character() {
|
||||
let compiler = AggegationConditionCompiler::new();
|
||||
let result =
|
||||
compiler.compile("select1 or select2 | count( hogeess ) by ii-i > 33".to_string());
|
||||
|
||||
assert_eq!(true, result.is_err());
|
||||
assert_eq!(
|
||||
"aggregation condition parse error has occurred. An unusable character was found."
|
||||
.to_string(),
|
||||
result.unwrap_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_aggegation_condition_compiler_not_count() {
|
||||
// countじゃないものが先頭に来ている。
|
||||
let compiler = AggegationConditionCompiler::new();
|
||||
let result =
|
||||
compiler.compile("select1 or select2 | by count( hogehoge) by snsn > 3".to_string());
|
||||
|
||||
assert_eq!(true, result.is_err());
|
||||
assert_eq!("aggregation condition parse error has occurred. aggregation condition can use count only.".to_string(),result.unwrap_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_aggegation_condition_compiler_no_ope() {
|
||||
// 比較演算子がない
|
||||
let compiler = AggegationConditionCompiler::new();
|
||||
let result = compiler.compile("select1 or select2 | count( hogehoge) 3".to_string());
|
||||
|
||||
assert_eq!(true, result.is_err());
|
||||
assert_eq!("aggregation condition parse error has occurred. count keyword needs compare operator and number like '> 3'".to_string(),result.unwrap_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_aggegation_condition_compiler_by() {
|
||||
// byの後に何もない
|
||||
let compiler = AggegationConditionCompiler::new();
|
||||
let result = compiler.compile("select1 or select2 | count( hogehoge) by".to_string());
|
||||
|
||||
assert_eq!(true, result.is_err());
|
||||
assert_eq!("aggregation condition parse error has occurred. by keyword needs field name like 'by EventID'".to_string(),result.unwrap_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_aggegation_condition_compiler_no_ope_afterby() {
|
||||
// byの後に何もない
|
||||
let compiler = AggegationConditionCompiler::new();
|
||||
let result =
|
||||
compiler.compile("select1 or select2 | count( hogehoge ) by hoe >".to_string());
|
||||
|
||||
assert_eq!(true, result.is_err());
|
||||
assert_eq!("aggregation condition parse error has occurred. compare operator needs a number like '> 3'.".to_string(),result.unwrap_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_aggegation_condition_compiler_unneccesary_word() {
|
||||
// byの後に何もない
|
||||
let compiler = AggegationConditionCompiler::new();
|
||||
let result =
|
||||
compiler.compile("select1 or select2 | count( hogehoge ) by hoe > 3 33".to_string());
|
||||
|
||||
assert_eq!(true, result.is_err());
|
||||
assert_eq!(
|
||||
"aggregation condition parse error has occurred. unnecessary word was found."
|
||||
.to_string(),
|
||||
result.unwrap_err()
|
||||
);
|
||||
}
|
||||
|
||||
fn check_aggregation_condition_ope(expr: String, cmp_num: i32) -> AggregationConditionToken {
|
||||
let compiler = AggegationConditionCompiler::new();
|
||||
let result = compiler.compile(expr);
|
||||
|
||||
assert_eq!(true, result.is_ok());
|
||||
let result = result.unwrap();
|
||||
assert_eq!(true, result.is_some());
|
||||
|
||||
let result = result.unwrap();
|
||||
assert_eq!(true, result._by_field_name.is_none());
|
||||
assert_eq!(true, result._field_name.is_none());
|
||||
assert_eq!(cmp_num, result._cmp_num);
|
||||
return result._cmp_op;
|
||||
}
|
||||
}
|
||||
1402
src/detections/rule/condition_parser.rs
Normal file
1402
src/detections/rule/condition_parser.rs
Normal file
File diff suppressed because it is too large
Load Diff
1566
src/detections/rule/matchers.rs
Normal file
1566
src/detections/rule/matchers.rs
Normal file
File diff suppressed because it is too large
Load Diff
827
src/detections/rule/mod.rs
Normal file
827
src/detections/rule/mod.rs
Normal file
@@ -0,0 +1,827 @@
|
||||
extern crate regex;
|
||||
use std::{collections::HashMap, sync::Arc, vec};
|
||||
|
||||
use serde_json::Value;
|
||||
use yaml_rust::Yaml;
|
||||
|
||||
mod matchers;
|
||||
mod selectionnodes;
|
||||
use self::selectionnodes::SelectionNode;
|
||||
mod aggregation_parser;
|
||||
mod condition_parser;
|
||||
|
||||
pub fn create_rule(yaml: Yaml) -> RuleNode {
|
||||
return RuleNode::new(yaml);
|
||||
}
|
||||
|
||||
/// Ruleファイルを表すノード
|
||||
pub struct RuleNode {
|
||||
pub yaml: Yaml,
|
||||
detection: Option<DetectionNode>,
|
||||
}
|
||||
|
||||
unsafe impl Sync for RuleNode {}
|
||||
|
||||
impl RuleNode {
|
||||
pub fn new(yaml: Yaml) -> RuleNode {
|
||||
return RuleNode {
|
||||
yaml: yaml,
|
||||
detection: Option::None,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn init(&mut self) -> Result<(), Vec<String>> {
|
||||
let mut errmsgs: Vec<String> = vec![];
|
||||
|
||||
// SIGMAルールを受け入れるため、outputがなくてもOKにする。
|
||||
// if self.yaml["output"].as_str().unwrap_or("").is_empty() {
|
||||
// errmsgs.push("Cannot find required key. key:output".to_string());
|
||||
// }
|
||||
|
||||
// detection node initialization
|
||||
let mut detection = DetectionNode::new();
|
||||
let detection_result = detection.init(&self.yaml["detection"]);
|
||||
if detection_result.is_err() {
|
||||
errmsgs.extend(detection_result.unwrap_err());
|
||||
}
|
||||
self.detection = Option::Some(detection);
|
||||
|
||||
if errmsgs.is_empty() {
|
||||
return Result::Ok(());
|
||||
} else {
|
||||
return Result::Err(errmsgs);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select(&self, event_record: &Value) -> bool {
|
||||
if self.detection.is_none() {
|
||||
return false;
|
||||
}
|
||||
|
||||
return self.detection.as_ref().unwrap().select(event_record);
|
||||
}
|
||||
}
|
||||
|
||||
/// Ruleファイルのdetectionを表すノード
|
||||
struct DetectionNode {
|
||||
pub name_to_selection: HashMap<String, Arc<Box<dyn SelectionNode + Send + Sync>>>,
|
||||
pub condition: Option<Box<dyn SelectionNode + Send + Sync>>,
|
||||
pub aggregation_condition: Option<aggregation_parser::AggregationParseInfo>,
|
||||
}
|
||||
|
||||
impl DetectionNode {
|
||||
fn new() -> DetectionNode {
|
||||
return DetectionNode {
|
||||
name_to_selection: HashMap::new(),
|
||||
condition: Option::None,
|
||||
aggregation_condition: Option::None,
|
||||
};
|
||||
}
|
||||
|
||||
fn init(&mut self, detection_yaml: &Yaml) -> Result<(), Vec<String>> {
|
||||
// selection nodeの初期化
|
||||
self.parse_name_to_selection(detection_yaml)?;
|
||||
|
||||
// conditionに指定されている式を取得
|
||||
let condition = &detection_yaml["condition"].as_str();
|
||||
let condition_str = if let Some(cond_str) = condition {
|
||||
cond_str.to_string()
|
||||
} else {
|
||||
// conditionが指定されていない場合、selectionが一つだけならそのselectionを採用することにする。
|
||||
let mut keys = self.name_to_selection.keys().clone();
|
||||
if keys.len() >= 2 {
|
||||
return Result::Err(vec![
|
||||
"There are no condition node under detection.".to_string()
|
||||
]);
|
||||
}
|
||||
|
||||
keys.nth(0).unwrap().to_string()
|
||||
};
|
||||
|
||||
// conditionをパースして、SelectionNodeに変換する
|
||||
let mut err_msgs = vec![];
|
||||
let compiler = condition_parser::ConditionCompiler::new();
|
||||
let compile_result =
|
||||
compiler.compile_condition(condition_str.clone(), &self.name_to_selection);
|
||||
if let Result::Err(err_msg) = compile_result {
|
||||
err_msgs.extend(vec![err_msg]);
|
||||
} else {
|
||||
self.condition = Option::Some(compile_result.unwrap());
|
||||
}
|
||||
|
||||
// aggregation condition(conditionのパイプ以降の部分)をパース
|
||||
let agg_compiler = aggregation_parser::AggegationConditionCompiler::new();
|
||||
let compile_result = agg_compiler.compile(condition_str);
|
||||
if let Result::Err(err_msg) = compile_result {
|
||||
err_msgs.push(err_msg);
|
||||
} else if let Result::Ok(info) = compile_result {
|
||||
self.aggregation_condition = info;
|
||||
}
|
||||
|
||||
if err_msgs.is_empty() {
|
||||
return Result::Ok(());
|
||||
} else {
|
||||
return Result::Err(err_msgs);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select(&self, event_record: &Value) -> bool {
|
||||
if self.condition.is_none() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let condition = &self.condition.as_ref().unwrap();
|
||||
return condition.select(event_record);
|
||||
}
|
||||
|
||||
/// selectionノードをパースします。
|
||||
fn parse_name_to_selection(&mut self, detection_yaml: &Yaml) -> Result<(), Vec<String>> {
|
||||
let detection_hash = detection_yaml.as_hash();
|
||||
if detection_hash.is_none() {
|
||||
return Result::Err(vec!["not found detection node".to_string()]);
|
||||
}
|
||||
|
||||
// selectionをパースする。
|
||||
let detection_hash = detection_hash.unwrap();
|
||||
let keys = detection_hash.keys();
|
||||
let mut err_msgs = vec![];
|
||||
for key in keys {
|
||||
let name = key.as_str().unwrap_or("");
|
||||
if name.len() == 0 {
|
||||
continue;
|
||||
}
|
||||
// condition等、特殊なキーワードを無視する。
|
||||
if name == "condition" {
|
||||
continue;
|
||||
}
|
||||
|
||||
// パースして、エラーメッセージがあれば配列にためて、戻り値で返す。
|
||||
let selection_node = self.parse_selection(&detection_hash[key]);
|
||||
if selection_node.is_some() {
|
||||
let mut selection_node = selection_node.unwrap();
|
||||
let init_result = selection_node.init();
|
||||
if init_result.is_err() {
|
||||
err_msgs.extend(init_result.unwrap_err());
|
||||
} else {
|
||||
let rc_selection = Arc::new(selection_node);
|
||||
self.name_to_selection
|
||||
.insert(name.to_string(), rc_selection);
|
||||
}
|
||||
}
|
||||
}
|
||||
if !err_msgs.is_empty() {
|
||||
return Result::Err(err_msgs);
|
||||
}
|
||||
|
||||
// selectionノードが無いのはエラー
|
||||
if self.name_to_selection.len() == 0 {
|
||||
return Result::Err(vec![
|
||||
"There are no selection node under detection.".to_string()
|
||||
]);
|
||||
}
|
||||
|
||||
return Result::Ok(());
|
||||
}
|
||||
|
||||
/// selectionをパースします。
|
||||
fn parse_selection(
|
||||
&self,
|
||||
selection_yaml: &Yaml,
|
||||
) -> Option<Box<dyn SelectionNode + Send + Sync>> {
|
||||
return Option::Some(self.parse_selection_recursively(vec![], selection_yaml));
|
||||
}
|
||||
|
||||
/// selectionをパースします。
|
||||
fn parse_selection_recursively(
|
||||
&self,
|
||||
key_list: Vec<String>,
|
||||
yaml: &Yaml,
|
||||
) -> Box<dyn SelectionNode + Send + Sync> {
|
||||
if yaml.as_hash().is_some() {
|
||||
// 連想配列はAND条件と解釈する
|
||||
let yaml_hash = yaml.as_hash().unwrap();
|
||||
let mut and_node = selectionnodes::AndSelectionNode::new();
|
||||
|
||||
yaml_hash.keys().for_each(|hash_key| {
|
||||
let child_yaml = yaml_hash.get(hash_key).unwrap();
|
||||
let mut child_key_list = key_list.clone();
|
||||
child_key_list.push(hash_key.as_str().unwrap().to_string());
|
||||
let child_node = self.parse_selection_recursively(child_key_list, child_yaml);
|
||||
and_node.child_nodes.push(child_node);
|
||||
});
|
||||
return Box::new(and_node);
|
||||
} else if yaml.as_vec().is_some() {
|
||||
// 配列はOR条件と解釈する。
|
||||
let mut or_node = selectionnodes::OrSelectionNode::new();
|
||||
yaml.as_vec().unwrap().iter().for_each(|child_yaml| {
|
||||
let child_node = self.parse_selection_recursively(key_list.clone(), child_yaml);
|
||||
or_node.child_nodes.push(child_node);
|
||||
});
|
||||
|
||||
return Box::new(or_node);
|
||||
} else {
|
||||
// 連想配列と配列以外は末端ノード
|
||||
return Box::new(selectionnodes::LeafSelectionNode::new(
|
||||
key_list,
|
||||
yaml.clone(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::detections::rule::create_rule;
|
||||
use yaml_rust::YamlLoader;
|
||||
|
||||
use super::RuleNode;
|
||||
|
||||
pub fn parse_rule_from_str(rule_str: &str) -> RuleNode {
|
||||
let rule_yaml = YamlLoader::load_from_str(rule_str);
|
||||
assert_eq!(rule_yaml.is_ok(), true);
|
||||
let rule_yamls = rule_yaml.unwrap();
|
||||
let mut rule_yaml = rule_yamls.into_iter();
|
||||
let mut rule_node = create_rule(rule_yaml.next().unwrap());
|
||||
assert_eq!(rule_node.init().is_ok(), true);
|
||||
return rule_node;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_dotkey() {
|
||||
// aliasじゃなくて、.区切りでつなげるケースが正しく検知できる。
|
||||
let rule_str = r#"
|
||||
enabled: true
|
||||
detection:
|
||||
selection:
|
||||
Event.System.Computer: DESKTOP-ICHIICHI
|
||||
output: 'command=%CommandLine%'
|
||||
"#;
|
||||
|
||||
let record_json_str = r#"
|
||||
{
|
||||
"Event": {"System": {"EventID": 4103, "Channel": "Security", "Computer":"DESKTOP-ICHIICHI"}},
|
||||
"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_notdetect_dotkey() {
|
||||
// aliasじゃなくて、.区切りでつなげるケースで、検知しないはずのケースで検知しないことを確かめる。
|
||||
let rule_str = r#"
|
||||
enabled: true
|
||||
detection:
|
||||
selection:
|
||||
Event.System.Computer: DESKTOP-ICHIICHIN
|
||||
output: 'command=%CommandLine%'
|
||||
"#;
|
||||
|
||||
let record_json_str = r#"
|
||||
{
|
||||
"Event": {"System": {"EventID": 4103, "Channel": "Security", "Computer":"DESKTOP-ICHIICHI"}},
|
||||
"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_notdetect_differentkey() {
|
||||
// aliasじゃなくて、.区切りでつなげるケースで、検知しないはずのケースで検知しないことを確かめる。
|
||||
let rule_str = r#"
|
||||
enabled: true
|
||||
detection:
|
||||
selection:
|
||||
Channel: NOTDETECT
|
||||
output: 'command=%CommandLine%'
|
||||
"#;
|
||||
|
||||
let record_json_str = r#"
|
||||
{
|
||||
"Event": {"System": {"EventID": 4103, "Channel": "Security", "Computer":"DESKTOP-ICHIICHI"}},
|
||||
"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_attribute() {
|
||||
// XMLのタグのattributionの部分に値がある場合、JSONが特殊な感じでパースされるのでそのテスト
|
||||
// 元のXMLは下記のような感じで、Providerタグの部分のNameとかGuidを検知するテスト
|
||||
/* - <Event xmlns="http://schemas.microsoft.com/win/2004/08/events/event">
|
||||
- <System>
|
||||
<Provider Name="Microsoft-Windows-Security-Auditing" Guid="{54849625-5478-4994-a5ba-3e3b0328c30d}" />
|
||||
<EventID>4672</EventID>
|
||||
<Version>0</Version>
|
||||
<Level>0</Level>
|
||||
<Task>12548</Task>
|
||||
<Opcode>0</Opcode>
|
||||
<Keywords>0x8020000000000000</Keywords>
|
||||
<TimeCreated SystemTime="2021-05-12T13:33:08.0144343Z" />
|
||||
<EventRecordID>244666</EventRecordID>
|
||||
<Correlation ActivityID="{0188dd7a-447d-000c-82dd-88017d44d701}" />
|
||||
<Execution ProcessID="1172" ThreadID="22352" />
|
||||
<Channel>Security</Channel>
|
||||
<Security />
|
||||
</System>
|
||||
- <EventData>
|
||||
<Data Name="SubjectUserName">SYSTEM</Data>
|
||||
<Data Name="SubjectDomainName">NT AUTHORITY</Data>
|
||||
<Data Name="PrivilegeList">SeAssignPrimaryTokenPrivilege SeTcbPrivilege SeSecurityPrivilege SeTakeOwnershipPrivilege SeLoadDriverPrivilege SeBackupPrivilege SeRestorePrivilege SeDebugPrivilege SeAuditPrivilege SeSystemEnvironmentPrivilege SeImpersonatePrivilege SeDelegateSessionUserImpersonatePrivilege</Data>
|
||||
</EventData>
|
||||
</Event> */
|
||||
|
||||
let rule_str = r#"
|
||||
enabled: true
|
||||
detection:
|
||||
selection:
|
||||
EventID: 4797
|
||||
Event.System.Provider_attributes.Guid: 54849625-5478-4994-A5BA-3E3B0328C30D
|
||||
output: 'command=%CommandLine%'
|
||||
"#;
|
||||
|
||||
let record_json_str = r#"
|
||||
{
|
||||
"Event": {
|
||||
"System": {
|
||||
"Channel": "Security",
|
||||
"Correlation_attributes": {
|
||||
"ActivityID": "0188DD7A-447D-000C-82DD-88017D44D701"
|
||||
},
|
||||
"EventID": 4797,
|
||||
"EventRecordID": 239219,
|
||||
"Execution_attributes": {
|
||||
"ProcessID": 1172,
|
||||
"ThreadID": 23236
|
||||
},
|
||||
"Keywords": "0x8020000000000000",
|
||||
"Level": 0,
|
||||
"Opcode": 0,
|
||||
"Provider_attributes": {
|
||||
"Guid": "54849625-5478-4994-A5BA-3E3B0328C30D",
|
||||
"Name": "Microsoft-Windows-Security-Auditing"
|
||||
},
|
||||
"Security": null,
|
||||
"Task": 13824,
|
||||
"TimeCreated_attributes": {
|
||||
"SystemTime": "2021-05-12T09:39:19.828403Z"
|
||||
},
|
||||
"Version": 0
|
||||
}
|
||||
},
|
||||
"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_notdetect_attribute() {
|
||||
// XMLのタグのattributionの検知しないケースを確認
|
||||
let rule_str = r#"
|
||||
enabled: true
|
||||
detection:
|
||||
selection:
|
||||
EventID: 4797
|
||||
Event.System.Provider_attributes.Guid: 54849625-5478-4994-A5BA-3E3B0328C30DSS
|
||||
output: 'command=%CommandLine%'
|
||||
"#;
|
||||
|
||||
let record_json_str = r#"
|
||||
{
|
||||
"Event": {
|
||||
"System": {
|
||||
"Channel": "Security",
|
||||
"Correlation_attributes": {
|
||||
"ActivityID": "0188DD7A-447D-000C-82DD-88017D44D701"
|
||||
},
|
||||
"EventID": 4797,
|
||||
"EventRecordID": 239219,
|
||||
"Execution_attributes": {
|
||||
"ProcessID": 1172,
|
||||
"ThreadID": 23236
|
||||
},
|
||||
"Keywords": "0x8020000000000000",
|
||||
"Level": 0,
|
||||
"Opcode": 0,
|
||||
"Provider_attributes": {
|
||||
"Guid": "54849625-5478-4994-A5BA-3E3B0328C30D",
|
||||
"Name": "Microsoft-Windows-Security-Auditing"
|
||||
},
|
||||
"Security": null,
|
||||
"Task": 13824,
|
||||
"TimeCreated_attributes": {
|
||||
"SystemTime": "2021-05-12T09:39:19.828403Z"
|
||||
},
|
||||
"Version": 0
|
||||
}
|
||||
},
|
||||
"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_eventdata() {
|
||||
// XML形式の特殊なパターンでEventDataというタグあって、Name=の部分にキー的なものが来る。
|
||||
/* - <EventData>
|
||||
<Data Name="SubjectUserSid">S-1-5-21-2673273881-979819022-3746999991-1001</Data>
|
||||
<Data Name="SubjectUserName">takai</Data>
|
||||
<Data Name="SubjectDomainName">DESKTOP-ICHIICH</Data>
|
||||
<Data Name="SubjectLogonId">0x312cd</Data>
|
||||
<Data Name="Workstation">DESKTOP-ICHIICH</Data>
|
||||
<Data Name="TargetUserName">Administrator</Data>
|
||||
<Data Name="TargetDomainName">DESKTOP-ICHIICH</Data>
|
||||
</EventData> */
|
||||
|
||||
// その場合、イベントパーサーのJSONは下記のような感じになるので、それで正しく検知出来ることをテスト。
|
||||
/* {
|
||||
"Event": {
|
||||
"EventData": {
|
||||
"TargetDomainName": "TEST-DOMAIN",
|
||||
"Workstation": "TEST WorkStation"
|
||||
"TargetUserName": "ichiichi11",
|
||||
},
|
||||
}
|
||||
} */
|
||||
|
||||
let rule_str = r#"
|
||||
enabled: true
|
||||
detection:
|
||||
selection:
|
||||
Event.EventData.Workstation: 'TEST WorkStation'
|
||||
Event.EventData.TargetUserName: ichiichi11
|
||||
output: 'command=%CommandLine%'
|
||||
"#;
|
||||
|
||||
let record_json_str = r#"
|
||||
{
|
||||
"Event": {
|
||||
"EventData": {
|
||||
"Workstation": "TEST WorkStation",
|
||||
"TargetUserName": "ichiichi11"
|
||||
},
|
||||
"System": {
|
||||
"Channel": "Security",
|
||||
"EventID": 4103,
|
||||
"EventRecordID": 239219,
|
||||
"Security": null
|
||||
}
|
||||
},
|
||||
"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_eventdata2() {
|
||||
let rule_str = r#"
|
||||
enabled: true
|
||||
detection:
|
||||
selection:
|
||||
EventID: 4103
|
||||
TargetUserName: ichiichi11
|
||||
output: 'command=%CommandLine%'
|
||||
"#;
|
||||
|
||||
let record_json_str = r#"
|
||||
{
|
||||
"Event": {
|
||||
"EventData": {
|
||||
"Workstation": "TEST WorkStation",
|
||||
"TargetUserName": "ichiichi11"
|
||||
},
|
||||
"System": {
|
||||
"Channel": "Security",
|
||||
"EventID": 4103,
|
||||
"EventRecordID": 239219,
|
||||
"Security": null
|
||||
}
|
||||
},
|
||||
"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_notdetect_eventdata() {
|
||||
// EventDataの検知しないパターン
|
||||
let rule_str = r#"
|
||||
enabled: true
|
||||
detection:
|
||||
selection:
|
||||
EventID: 4103
|
||||
TargetUserName: ichiichi12
|
||||
output: 'command=%CommandLine%'
|
||||
"#;
|
||||
|
||||
let record_json_str = r#"
|
||||
{
|
||||
"Event": {
|
||||
"EventData": {
|
||||
"Workstation": "TEST WorkStation",
|
||||
"TargetUserName": "ichiichi11"
|
||||
},
|
||||
"System": {
|
||||
"Channel": "Security",
|
||||
"EventID": 4103,
|
||||
"EventRecordID": 239219,
|
||||
"Security": null
|
||||
}
|
||||
},
|
||||
"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_special_eventdata() {
|
||||
// 上記テストケースのEventDataの更に特殊ケースで下記のようにDataタグの中にNameキーがないケースがある。
|
||||
// そのためにruleファイルでEventDataというキーだけ特別対応している。
|
||||
// 現状、downgrade_attack.ymlというルールの場合だけで確認出来ているケース
|
||||
let rule_str = r#"
|
||||
enabled: true
|
||||
detection:
|
||||
selection:
|
||||
EventID: 403
|
||||
EventData|re: '[\s\S]*EngineVersion=2\.0[\s\S]*'
|
||||
output: 'command=%CommandLine%'
|
||||
"#;
|
||||
|
||||
let record_json_str = r#"
|
||||
{
|
||||
"Event": {
|
||||
"EventData": {
|
||||
"Binary": null,
|
||||
"Data": [
|
||||
"Stopped",
|
||||
"Available",
|
||||
"\tNewEngineState=Stopped\n\tPreviousEngineState=Available\n\n\tSequenceNumber=10\n\n\tHostName=ConsoleHost\n\tHostVersion=2.0\n\tHostId=5cbb33bf-acf7-47cc-9242-141cd0ba9f0c\n\tEngineVersion=2.0\n\tRunspaceId=c6e94dca-0daf-418c-860a-f751a9f2cbe1\n\tPipelineId=\n\tCommandName=\n\tCommandType=\n\tScriptName=\n\tCommandPath=\n\tCommandLine="
|
||||
]
|
||||
},
|
||||
"System": {
|
||||
"Channel": "Windows PowerShell",
|
||||
"Computer": "DESKTOP-ST69BPO",
|
||||
"EventID": 403,
|
||||
"EventID_attributes": {
|
||||
"Qualifiers": 0
|
||||
},
|
||||
"EventRecordID": 730,
|
||||
"Keywords": "0x80000000000000",
|
||||
"Level": 4,
|
||||
"Provider_attributes": {
|
||||
"Name": "PowerShell"
|
||||
},
|
||||
"Security": null,
|
||||
"Task": 4,
|
||||
"TimeCreated_attributes": {
|
||||
"SystemTime": "2021-01-28T10:40:54.946866Z"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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_notdetect_special_eventdata() {
|
||||
// 上記テストケースのEventDataの更に特殊ケースで下記のようにDataタグの中にNameキーがないケースがある。
|
||||
// そのためにruleファイルでEventDataというキーだけ特別対応している。
|
||||
// 現状、downgrade_attack.ymlというルールの場合だけで確認出来ているケース
|
||||
let rule_str = r#"
|
||||
enabled: true
|
||||
detection:
|
||||
selection:
|
||||
EventID: 403
|
||||
EventData: '[\s\S]*EngineVersion=3.0[\s\S]*'
|
||||
output: 'command=%CommandLine%'
|
||||
"#;
|
||||
|
||||
let record_json_str = r#"
|
||||
{
|
||||
"Event": {
|
||||
"EventData": {
|
||||
"Binary": null,
|
||||
"Data": [
|
||||
"Stopped",
|
||||
"Available",
|
||||
"\tNewEngineState=Stopped\n\tPreviousEngineState=Available\n\n\tSequenceNumber=10\n\n\tHostName=ConsoleHost\n\tHostVersion=2.0\n\tHostId=5cbb33bf-acf7-47cc-9242-141cd0ba9f0c\n\tEngineVersion=2.0\n\tRunspaceId=c6e94dca-0daf-418c-860a-f751a9f2cbe1\n\tPipelineId=\n\tCommandName=\n\tCommandType=\n\tScriptName=\n\tCommandPath=\n\tCommandLine="
|
||||
]
|
||||
},
|
||||
"System": {
|
||||
"Channel": "Windows PowerShell",
|
||||
"Computer": "DESKTOP-ST69BPO",
|
||||
"EventID": 403,
|
||||
"EventID_attributes": {
|
||||
"Qualifiers": 0
|
||||
},
|
||||
"EventRecordID": 730,
|
||||
"Keywords": "0x80000000000000",
|
||||
"Level": 4,
|
||||
"Provider_attributes": {
|
||||
"Name": "PowerShell"
|
||||
},
|
||||
"Security": null,
|
||||
"Task": 4,
|
||||
"TimeCreated_attributes": {
|
||||
"SystemTime": "2021-01-28T10:40:54.946866Z"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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_use_strfeature_in_or_node() {
|
||||
// orNodeの中でもstartswithが使えるかのテスト
|
||||
let rule_str = r#"
|
||||
enabled: true
|
||||
detection:
|
||||
selection:
|
||||
Channel: 'System'
|
||||
EventID: 7040
|
||||
param1: 'Windows Event Log'
|
||||
param2|startswith:
|
||||
- "disa"
|
||||
- "aut"
|
||||
output: 'Service name : %param1%¥nMessage : Event Log Service Stopped¥nResults: Selective event log manipulation may follow this event.'
|
||||
"#;
|
||||
|
||||
let record_json_str = r#"
|
||||
{
|
||||
"Event": {
|
||||
"System": {
|
||||
"EventID": 7040,
|
||||
"Channel": "System"
|
||||
},
|
||||
"EventData": {
|
||||
"param1": "Windows Event Log",
|
||||
"param2": "auto start"
|
||||
}
|
||||
},
|
||||
"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(_rec) => {
|
||||
assert!(false, "failed to parse json record.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_undefined_rule_option() {
|
||||
// 不明な文字列オプションがルールに書かれていたら警告するテスト
|
||||
let rule_str = r#"
|
||||
enabled: true
|
||||
detection:
|
||||
selection:
|
||||
Channel|failed: Security
|
||||
EventID: 0
|
||||
output: 'Rule parse test'
|
||||
"#;
|
||||
let mut rule_yaml = YamlLoader::load_from_str(rule_str).unwrap().into_iter();
|
||||
let mut rule_node = create_rule(rule_yaml.next().unwrap());
|
||||
|
||||
assert_eq!(
|
||||
rule_node.init(),
|
||||
Err(vec![
|
||||
"unknown pipe element was specified. key:detection -> selection -> Channel|failed"
|
||||
.to_string()
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_not_defined_selection() {
|
||||
// 不明な文字列オプションがルールに書かれていたら警告するテスト
|
||||
let rule_str = r#"
|
||||
enabled: true
|
||||
detection:
|
||||
output: 'Rule parse test'
|
||||
"#;
|
||||
let mut rule_yaml = YamlLoader::load_from_str(rule_str).unwrap().into_iter();
|
||||
let mut rule_node = create_rule(rule_yaml.next().unwrap());
|
||||
|
||||
assert_eq!(
|
||||
rule_node.init(),
|
||||
Err(vec!["not found detection node".to_string()])
|
||||
);
|
||||
}
|
||||
}
|
||||
555
src/detections/rule/selectionnodes.rs
Normal file
555
src/detections/rule/selectionnodes.rs
Normal file
@@ -0,0 +1,555 @@
|
||||
use crate::detections::utils;
|
||||
use mopa::mopafy;
|
||||
use serde_json::Value;
|
||||
use std::{sync::Arc, vec};
|
||||
use yaml_rust::Yaml;
|
||||
|
||||
use super::matchers;
|
||||
|
||||
// Ruleファイルの detection- selection配下のノードはこのtraitを実装する。
|
||||
pub trait SelectionNode: mopa::Any {
|
||||
// 引数で指定されるイベントログのレコードが、条件に一致するかどうかを判定する
|
||||
// このトレイトを実装する構造体毎に適切な判定処理を書く必要がある。
|
||||
fn select(&self, event_record: &Value) -> bool;
|
||||
|
||||
// 初期化処理を行う
|
||||
// 戻り値としてエラーを返却できるようになっているので、Ruleファイルが間違っていて、SelectionNodeを構成出来ない時はここでエラーを出す
|
||||
// AndSelectionNode等ではinit()関数とは別にnew()関数を実装しているが、new()関数はただインスタンスを作るだけにして、あまり長い処理を書かないようにしている。
|
||||
// これはRuleファイルのパースのエラー処理をinit()関数にまとめるためにこうしている。
|
||||
fn init(&mut self) -> Result<(), Vec<String>>;
|
||||
|
||||
// 子ノードを取得する(グラフ理論のchildと同じ意味)
|
||||
fn get_childs(&self) -> Vec<&Box<dyn SelectionNode + Send + Sync>>;
|
||||
|
||||
// 子孫ノードを取得する(グラフ理論のdescendantと同じ意味)
|
||||
fn get_descendants(&self) -> Vec<&Box<dyn SelectionNode + Send + Sync>>;
|
||||
}
|
||||
mopafy!(SelectionNode);
|
||||
|
||||
/// detection - selection配下でAND条件を表すノード
|
||||
pub struct AndSelectionNode {
|
||||
pub child_nodes: Vec<Box<dyn SelectionNode + Send + Sync>>,
|
||||
}
|
||||
|
||||
unsafe impl Send for AndSelectionNode {}
|
||||
unsafe impl Sync for AndSelectionNode {}
|
||||
|
||||
impl AndSelectionNode {
|
||||
pub fn new() -> AndSelectionNode {
|
||||
return AndSelectionNode {
|
||||
child_nodes: vec![],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectionNode for AndSelectionNode {
|
||||
fn select(&self, event_record: &Value) -> bool {
|
||||
return self.child_nodes.iter().all(|child_node| {
|
||||
return child_node.select(event_record);
|
||||
});
|
||||
}
|
||||
|
||||
fn init(&mut self) -> Result<(), Vec<String>> {
|
||||
let err_msgs = self
|
||||
.child_nodes
|
||||
.iter_mut()
|
||||
.map(|child_node| {
|
||||
let res = child_node.init();
|
||||
if res.is_err() {
|
||||
return res.unwrap_err();
|
||||
} else {
|
||||
return vec![];
|
||||
}
|
||||
})
|
||||
.fold(
|
||||
vec![],
|
||||
|mut acc: Vec<String>, cur: Vec<String>| -> Vec<String> {
|
||||
acc.extend(cur.into_iter());
|
||||
return acc;
|
||||
},
|
||||
);
|
||||
|
||||
if err_msgs.is_empty() {
|
||||
return Result::Ok(());
|
||||
} else {
|
||||
return Result::Err(err_msgs);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_childs(&self) -> Vec<&Box<dyn SelectionNode + Send + Sync>> {
|
||||
let mut ret = vec![];
|
||||
self.child_nodes.iter().for_each(|child_node| {
|
||||
ret.push(child_node);
|
||||
});
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
fn get_descendants(&self) -> Vec<&Box<dyn SelectionNode + Send + Sync>> {
|
||||
let mut ret = self.get_childs();
|
||||
|
||||
self.child_nodes
|
||||
.iter()
|
||||
.map(|child_node| {
|
||||
return child_node.get_descendants();
|
||||
})
|
||||
.flatten()
|
||||
.for_each(|descendant_node| {
|
||||
ret.push(descendant_node);
|
||||
});
|
||||
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
/// detection - selection配下でOr条件を表すノード
|
||||
pub struct OrSelectionNode {
|
||||
pub child_nodes: Vec<Box<dyn SelectionNode + Send + Sync>>,
|
||||
}
|
||||
|
||||
unsafe impl Send for OrSelectionNode {}
|
||||
unsafe impl Sync for OrSelectionNode {}
|
||||
|
||||
impl OrSelectionNode {
|
||||
pub fn new() -> OrSelectionNode {
|
||||
return OrSelectionNode {
|
||||
child_nodes: vec![],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectionNode for OrSelectionNode {
|
||||
fn select(&self, event_record: &Value) -> bool {
|
||||
return self.child_nodes.iter().any(|child_node| {
|
||||
return child_node.select(event_record);
|
||||
});
|
||||
}
|
||||
|
||||
fn init(&mut self) -> Result<(), Vec<String>> {
|
||||
let err_msgs = self
|
||||
.child_nodes
|
||||
.iter_mut()
|
||||
.map(|child_node| {
|
||||
let res = child_node.init();
|
||||
if res.is_err() {
|
||||
return res.unwrap_err();
|
||||
} else {
|
||||
return vec![];
|
||||
}
|
||||
})
|
||||
.fold(
|
||||
vec![],
|
||||
|mut acc: Vec<String>, cur: Vec<String>| -> Vec<String> {
|
||||
acc.extend(cur.into_iter());
|
||||
return acc;
|
||||
},
|
||||
);
|
||||
|
||||
if err_msgs.is_empty() {
|
||||
return Result::Ok(());
|
||||
} else {
|
||||
return Result::Err(err_msgs);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_childs(&self) -> Vec<&Box<dyn SelectionNode + Send + Sync>> {
|
||||
let mut ret = vec![];
|
||||
self.child_nodes.iter().for_each(|child_node| {
|
||||
ret.push(child_node);
|
||||
});
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
fn get_descendants(&self) -> Vec<&Box<dyn SelectionNode + Send + Sync>> {
|
||||
let mut ret = self.get_childs();
|
||||
|
||||
self.child_nodes
|
||||
.iter()
|
||||
.map(|child_node| {
|
||||
return child_node.get_descendants();
|
||||
})
|
||||
.flatten()
|
||||
.for_each(|descendant_node| {
|
||||
ret.push(descendant_node);
|
||||
});
|
||||
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
/// conditionでNotを表すノード
|
||||
pub struct NotSelectionNode {
|
||||
node: Box<dyn SelectionNode + Send + Sync>,
|
||||
}
|
||||
|
||||
unsafe impl Send for NotSelectionNode {}
|
||||
unsafe impl Sync for NotSelectionNode {}
|
||||
|
||||
impl NotSelectionNode {
|
||||
pub fn new(node: Box<dyn SelectionNode + Send + Sync>) -> NotSelectionNode {
|
||||
return NotSelectionNode { node: node };
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectionNode for NotSelectionNode {
|
||||
fn select(&self, event_record: &Value) -> bool {
|
||||
return !self.node.select(event_record);
|
||||
}
|
||||
|
||||
fn init(&mut self) -> Result<(), Vec<String>> {
|
||||
return Result::Ok(());
|
||||
}
|
||||
|
||||
fn get_childs(&self) -> Vec<&Box<dyn SelectionNode + Send + Sync>> {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
fn get_descendants(&self) -> Vec<&Box<dyn SelectionNode + Send + Sync>> {
|
||||
return self.get_childs();
|
||||
}
|
||||
}
|
||||
|
||||
/// detectionで定義した条件をconditionで参照するためのもの
|
||||
pub struct RefSelectionNode {
|
||||
// selection_nodeはDetectionNodeのname_2_nodeが所有権を持っていて、RefSelectionNodeのselection_nodeに所有権を持たせることができない。
|
||||
// そこでArcを使って、DetectionNodeのname_2_nodeとRefSelectionNodeのselection_nodeで所有権を共有する。
|
||||
// RcじゃなくてArcなのはマルチスレッド対応のため
|
||||
selection_node: Arc<Box<dyn SelectionNode + Send + Sync>>,
|
||||
}
|
||||
|
||||
unsafe impl Send for RefSelectionNode {}
|
||||
unsafe impl Sync for RefSelectionNode {}
|
||||
|
||||
impl RefSelectionNode {
|
||||
pub fn new(selection_node: Arc<Box<dyn SelectionNode + Send + Sync>>) -> RefSelectionNode {
|
||||
return RefSelectionNode {
|
||||
selection_node: selection_node,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectionNode for RefSelectionNode {
|
||||
fn select(&self, event_record: &Value) -> bool {
|
||||
return self.selection_node.select(event_record);
|
||||
}
|
||||
|
||||
fn init(&mut self) -> Result<(), Vec<String>> {
|
||||
return Result::Ok(());
|
||||
}
|
||||
|
||||
fn get_childs(&self) -> Vec<&Box<dyn SelectionNode + Send + Sync>> {
|
||||
return vec![&self.selection_node];
|
||||
}
|
||||
|
||||
fn get_descendants(&self) -> Vec<&Box<dyn SelectionNode + Send + Sync>> {
|
||||
return self.get_childs();
|
||||
}
|
||||
}
|
||||
|
||||
/// detection - selection配下の末端ノード
|
||||
pub struct LeafSelectionNode {
|
||||
key_list: Vec<String>,
|
||||
select_value: Yaml,
|
||||
pub matcher: Option<Box<dyn matchers::LeafMatcher>>,
|
||||
}
|
||||
|
||||
unsafe impl Send for LeafSelectionNode {}
|
||||
unsafe impl Sync for LeafSelectionNode {}
|
||||
|
||||
impl LeafSelectionNode {
|
||||
pub fn new(key_list: Vec<String>, value_yaml: Yaml) -> LeafSelectionNode {
|
||||
return LeafSelectionNode {
|
||||
key_list: key_list,
|
||||
select_value: value_yaml,
|
||||
matcher: Option::None,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_key(&self) -> String {
|
||||
if self.key_list.is_empty() {
|
||||
return String::default();
|
||||
}
|
||||
|
||||
let topkey = self.key_list[0].to_string();
|
||||
let values: Vec<&str> = topkey.split("|").collect();
|
||||
return values[0].to_string();
|
||||
}
|
||||
|
||||
/// JSON形式のEventJSONから値を取得する関数 aliasも考慮されている。
|
||||
fn get_event_value<'a>(&self, event_value: &'a Value) -> Option<&'a Value> {
|
||||
if self.key_list.is_empty() {
|
||||
return Option::None;
|
||||
}
|
||||
|
||||
return utils::get_event_value(&self.get_key(), event_value);
|
||||
}
|
||||
|
||||
/// matchers::LeafMatcherの一覧を取得する。
|
||||
/// 上から順番に調べて、一番始めに一致したMatcherが適用される
|
||||
fn get_matchers(&self) -> Vec<Box<dyn matchers::LeafMatcher>> {
|
||||
return vec![
|
||||
Box::new(matchers::MinlengthMatcher::new()),
|
||||
Box::new(matchers::RegexesFileMatcher::new()),
|
||||
Box::new(matchers::WhitelistFileMatcher::new()),
|
||||
Box::new(matchers::DefaultMatcher::new()),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectionNode for LeafSelectionNode {
|
||||
fn select(&self, event_record: &Value) -> bool {
|
||||
if self.matcher.is_none() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// EventDataはXMLが特殊な形式になっているので特別対応。
|
||||
//// 元のXMLは下記のような形式
|
||||
/*
|
||||
<EventData>
|
||||
<Data>Available</Data>
|
||||
<Data>None</Data>
|
||||
<Data>NewEngineState=Available PreviousEngineState=None SequenceNumber=9 HostName=ConsoleHost HostVersion=2.0 HostId=5cbb33bf-acf7-47cc-9242-141cd0ba9f0c EngineVersion=2.0 RunspaceId=c6e94dca-0daf-418c-860a-f751a9f2cbe1 PipelineId= CommandName= CommandType= ScriptName= CommandPath= CommandLine=</Data>
|
||||
</EventData>
|
||||
*/
|
||||
//// XMLをJSONにパースすると、下記のような形式になっていた。
|
||||
//// JSONが配列になってしまうようなルールは現状では書けない。
|
||||
/* "EventData": {
|
||||
"Binary": null,
|
||||
"Data": [
|
||||
"",
|
||||
"\tDetailSequence=1\r\n\tDetailTotal=1\r\n\r\n\tSequenceNumber=15\r\n\r\n\tUserId=DESKTOP-ST69BPO\\user01\r\n\tHostName=ConsoleHost\r\n\tHostVersion=5.1.18362.145\r\n\tHostId=64821494-0737-4ce9-ad67-3ac0e50a81b8\r\n\tHostApplication=powershell calc\r\n\tEngineVersion=5.1.18362.145\r\n\tRunspaceId=74ae21ca-7fa9-40cc-a265-7a41fdb168a6\r\n\tPipelineId=1\r\n\tScriptName=\r\n\tCommandLine=",
|
||||
"CommandInvocation(Out-Default): \"Out-Default\"\r\n"
|
||||
]
|
||||
}
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
// 配列じゃなくて、文字列や数値等の場合は普通通りに比較する。
|
||||
let eventdata_data = values.unwrap();
|
||||
if eventdata_data.is_boolean() || eventdata_data.is_i64() || eventdata_data.is_string()
|
||||
{
|
||||
return self
|
||||
.matcher
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.is_match(Option::Some(eventdata_data));
|
||||
}
|
||||
// 配列の場合は配列の要素のどれか一つでもルールに合致すれば条件に一致したことにする。
|
||||
if eventdata_data.is_array() {
|
||||
return eventdata_data
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|ary_element| {
|
||||
return self
|
||||
.matcher
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.is_match(Option::Some(ary_element));
|
||||
});
|
||||
} else {
|
||||
return self.matcher.as_ref().unwrap().is_match(Option::None);
|
||||
}
|
||||
}
|
||||
|
||||
let event_value = self.get_event_value(event_record);
|
||||
return self.matcher.as_ref().unwrap().is_match(event_value);
|
||||
}
|
||||
|
||||
fn init(&mut self) -> Result<(), Vec<String>> {
|
||||
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() {
|
||||
return Result::Err(vec![format!(
|
||||
"Found unknown key. key:{}",
|
||||
utils::concat_selection_key(&match_key_list)
|
||||
)]);
|
||||
}
|
||||
|
||||
if self.select_value.is_badvalue() {
|
||||
return Result::Err(vec![format!(
|
||||
"Cannot parse yaml file. key:{}",
|
||||
utils::concat_selection_key(&match_key_list)
|
||||
)]);
|
||||
}
|
||||
|
||||
return self
|
||||
.matcher
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.init(&match_key_list, &self.select_value);
|
||||
}
|
||||
|
||||
fn get_childs(&self) -> Vec<&Box<dyn SelectionNode + Send + Sync>> {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
fn get_descendants(&self) -> Vec<&Box<dyn SelectionNode + Send + Sync>> {
|
||||
return vec![];
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::detections::rule::tests::parse_rule_from_str;
|
||||
|
||||
#[test]
|
||||
fn test_detect_mutiple_regex_and() {
|
||||
// AND条件が正しく検知することを確認する。
|
||||
let rule_str = r#"
|
||||
enabled: true
|
||||
detection:
|
||||
selection:
|
||||
Channel: Security
|
||||
EventID: 4103
|
||||
output: 'command=%CommandLine%'
|
||||
"#;
|
||||
|
||||
let record_json_str = r#"
|
||||
{
|
||||
"Event": {"System": {"EventID": 4103, "Channel": "Security"}},
|
||||
"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_notdetect_mutiple_regex_and() {
|
||||
// AND条件で一つでも条件に一致しないと、検知しないことを確認
|
||||
// この例ではComputerの値が異なっている。
|
||||
let rule_str = r#"
|
||||
enabled: true
|
||||
detection:
|
||||
selection:
|
||||
Channel: Security
|
||||
EventID: 4103
|
||||
Computer: DESKTOP-ICHIICHIN
|
||||
output: 'command=%CommandLine%'
|
||||
"#;
|
||||
|
||||
let record_json_str = r#"
|
||||
{
|
||||
"Event": {"System": {"EventID": 4103, "Channel": "Security", "Computer":"DESKTOP-ICHIICHI"}},
|
||||
"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_or() {
|
||||
// OR条件が正しく検知できることを確認
|
||||
let rule_str = r#"
|
||||
enabled: true
|
||||
detection:
|
||||
selection:
|
||||
Channel:
|
||||
- PowerShell
|
||||
- Security
|
||||
output: 'command=%CommandLine%'
|
||||
"#;
|
||||
|
||||
let record_json_str = r#"
|
||||
{
|
||||
"Event": {"System": {"EventID": 4103, "Channel": "Security", "Computer":"DESKTOP-ICHIICHI"}},
|
||||
"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_or2() {
|
||||
// OR条件が正しく検知できることを確認
|
||||
let rule_str = r#"
|
||||
enabled: true
|
||||
detection:
|
||||
selection:
|
||||
Channel:
|
||||
- PowerShell
|
||||
- Security
|
||||
output: 'command=%CommandLine%'
|
||||
"#;
|
||||
|
||||
let record_json_str = r#"
|
||||
{
|
||||
"Event": {"System": {"EventID": 4103, "Channel": "PowerShell", "Computer":"DESKTOP-ICHIICHI"}},
|
||||
"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_notdetect_or() {
|
||||
// OR条件が正しく検知できることを確認
|
||||
let rule_str = r#"
|
||||
enabled: true
|
||||
detection:
|
||||
selection:
|
||||
Channel:
|
||||
- PowerShell
|
||||
- Security
|
||||
output: 'command=%CommandLine%'
|
||||
"#;
|
||||
|
||||
let record_json_str = r#"
|
||||
{
|
||||
"Event": {"System": {"EventID": 4103, "Channel": "not detect", "Computer":"DESKTOP-ICHIICHI"}},
|
||||
"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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,15 @@ use std::io::prelude::*;
|
||||
use std::str;
|
||||
use std::string::String;
|
||||
|
||||
pub fn concat_selection_key(key_list: &Vec<String>) -> String {
|
||||
return key_list
|
||||
.iter()
|
||||
.fold("detection -> selection".to_string(), |mut acc, cur| {
|
||||
acc = acc + " -> " + cur;
|
||||
return acc;
|
||||
});
|
||||
}
|
||||
|
||||
pub fn check_regex(
|
||||
string: &str,
|
||||
r#type: usize,
|
||||
|
||||
Reference in New Issue
Block a user