Files
hayabusa/src/detections/rule.rs
James 9b8bed70f8 Feature/timeline template #104 (#106)
* under constructing

* add statistics template

* fix

* add comment

* change for statistics
2021-05-16 01:34:48 +09:00

2212 lines
75 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
extern crate regex;
use mopa::mopafy;
use std::vec;
use crate::detections::utils;
use regex::Regex;
use serde_json::Value;
use yaml_rust::Yaml;
// TODO テストケースかかなきゃ...
pub fn parse_rule(yaml: Yaml) -> RuleNode {
let detection = parse_detection(&yaml);
return RuleNode {
yaml: yaml,
detection: detection,
};
}
fn parse_detection(yaml: &Yaml) -> Option<DetectionNode> {
if yaml["detection"].is_badvalue() {
return Option::None;
} else {
let node = DetectionNode {
selection: parse_selection(&yaml),
};
return Option::Some(node);
}
}
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;
});
}
fn parse_selection(yaml: &Yaml) -> Option<Box<dyn SelectionNode + Send>> {
// TODO detection-selectionが存在しない場合のチェック
let selection_yaml = &yaml["detection"]["selection"];
if selection_yaml.is_badvalue() {
return Option::None;
}
return Option::Some(parse_selection_recursively(vec![], &selection_yaml));
}
fn parse_selection_recursively(
key_list: Vec<String>,
yaml: &Yaml,
) -> Box<dyn SelectionNode + Send> {
if yaml.as_hash().is_some() {
// 連想配列はAND条件と解釈する
let yaml_hash = yaml.as_hash().unwrap();
let mut and_node = 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 = 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 = OrSelectionNode::new();
yaml.as_vec().unwrap().iter().for_each(|child_yaml| {
let child_node = parse_selection_recursively(key_list.clone(), child_yaml);
or_node.child_nodes.push(child_node);
});
return Box::new(or_node);
} else {
// 連想配列と配列以外は末端ノード
return Box::new(LeafSelectionNode::new(key_list, yaml.clone()));
}
}
// Ruleファイルを表すード
pub struct RuleNode {
pub yaml: Yaml,
detection: Option<DetectionNode>,
}
unsafe impl Sync for RuleNode {}
impl RuleNode {
pub fn init(&mut self) -> Result<(), Vec<String>> {
let mut errmsgs: Vec<String> = vec![];
// field check
if self.yaml["output"].as_str().unwrap_or("").is_empty() {
errmsgs.push("Cannot find required key. key:output".to_string());
}
// detection node initialization
self.detection.as_mut().and_then(|detection| {
let detection_result = detection.init();
if detection_result.is_err() {
errmsgs.extend(detection_result.unwrap_err());
}
return Option::Some(detection);
});
if errmsgs.is_empty() {
return Result::Ok(());
} else {
return Result::Err(errmsgs);
}
}
pub fn select(&self, event_record: &Value) -> bool {
let selection = self
.detection
.as_ref()
.and_then(|detect_node| detect_node.selection.as_ref());
if selection.is_none() {
return false;
}
return selection.unwrap().select(event_record);
}
// pub fn get_event_ids(&self) -> Vec<i64> {
// let selection = self
// .detection
// .as_ref()
// .and_then(|detection| detection.selection.as_ref());
// if selection.is_none() {
// return vec![];
// }
// return selection
// .unwrap()
// .get_descendants()
// .iter()
// .filter_map(|node| return node.downcast_ref::<LeafSelectionNode>()) // mopaというライブラリを使うと簡単にダウンキャストできるらしいです。https://crates.io/crates/mopa
// .filter(|node| {
// // キーがEventIDのードである
// let key = utils::get_event_id_key();
// if node.get_key() == key {
// return true;
// }
// // EventIDのAliasに一致しているかどうか
// let alias = utils::get_alias(&key);
// if alias.is_none() {
// return false;
// } else {
// return node.get_key() == alias.unwrap();
// }
// })
// .filter_map(|node| {
// return node.select_value.as_i64();
// })
// .collect();
// }
}
// Ruleファイルのdetectionを表すード
struct DetectionNode {
pub selection: Option<Box<dyn SelectionNode + Send>>,
}
impl DetectionNode {
fn init(&mut self) -> Result<(), Vec<String>> {
if self.selection.is_none() {
return Result::Ok(());
}
return self.selection.as_mut().unwrap().init();
}
}
// Ruleファイルの detection- selection配下のードはこのtraitを実装する。
trait SelectionNode: mopa::Any {
fn select(&self, event_record: &Value) -> bool;
fn init(&mut self) -> Result<(), Vec<String>>;
fn get_childs(&self) -> Vec<&Box<dyn SelectionNode>>;
fn get_descendants(&self) -> Vec<&Box<dyn SelectionNode>>;
}
mopafy!(SelectionNode);
// detection - selection配下でAND条件を表すード
struct AndSelectionNode {
pub child_nodes: Vec<Box<dyn SelectionNode>>,
}
unsafe impl Send 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>> {
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>> {
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条件を表すード
struct OrSelectionNode {
pub child_nodes: Vec<Box<dyn SelectionNode>>,
}
unsafe impl Send 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>> {
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>> {
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配下の末端ード
struct LeafSelectionNode {
key_list: Vec<String>,
select_value: Yaml,
matcher: Option<Box<dyn LeafMatcher>>,
}
unsafe impl Send for LeafSelectionNode {}
impl LeafSelectionNode {
fn new(key_list: Vec<String>, value_yaml: Yaml) -> LeafSelectionNode {
return LeafSelectionNode {
key_list: key_list,
select_value: value_yaml,
matcher: Option::None,
};
}
fn get_key(&self) -> String {
if self.key_list.is_empty() {
return String::default();
}
return self.key_list[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);
}
// LeafMatcherの一覧を取得する。
fn get_matchers(&self) -> Vec<Box<dyn LeafMatcher>> {
return vec![
Box::new(RegexMatcher::new()),
Box::new(MinlengthMatcher::new()),
Box::new(RegexesFileMatcher::new()),
Box::new(WhitelistFileMatcher::new()),
];
}
}
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.key_list.len() > 0 && self.key_list[0].to_string() == "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 matchers = self.get_matchers();
let mut match_key_list = self.key_list.clone();
match_key_list.remove(0);
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:{}",
concat_selection_key(&match_key_list)
)]);
}
if self.select_value.is_badvalue() {
return Result::Err(vec![format!(
"Cannot parse yaml file. key:{}",
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>> {
return vec![];
}
fn get_descendants(&self) -> Vec<&Box<dyn SelectionNode>> {
return vec![];
}
}
// 末端ードがEventLogの値を比較するロジックを表す。
// 正規条件のマッチや文字数制限など、比較ロジック毎にこのtraitを実装したクラスが存在する。
//
// 新規にLeafMatcherを実装するクラスを作成した場合、
// LeafSelectionNodeのget_matchersクラスの戻り値の配列に新規作成したクラスのインスタンスを追加する。
trait LeafMatcher: mopa::Any {
fn is_target_key(&self, key_list: &Vec<String>) -> bool;
fn is_match(&self, event_value: Option<&Value>) -> bool;
fn init(&mut self, key_list: &Vec<String>, select_value: &Yaml) -> Result<(), Vec<String>>;
}
mopafy!(LeafMatcher);
// 正規表現で比較するロジックを表すクラス
struct RegexMatcher {
re: Option<Regex>,
}
impl RegexMatcher {
fn new() -> RegexMatcher {
return RegexMatcher {
re: Option::None, // empty
};
}
fn is_regex_fullmatch(&self, re: &Regex, value: String) -> bool {
return re.find_iter(&value).any(|match_obj| {
return match_obj.as_str().to_string() == value;
});
}
}
impl LeafMatcher for RegexMatcher {
fn is_target_key(&self, key_list: &Vec<String>) -> bool {
if key_list.is_empty() {
return true;
}
if key_list.len() == 1 {
return key_list.get(0).unwrap_or(&"".to_string()) == &"regex".to_string();
} else {
return false;
}
}
fn init(&mut self, key_list: &Vec<String>, select_value: &Yaml) -> Result<(), Vec<String>> {
if select_value.is_null() {
self.re = Option::None;
return Result::Ok(());
}
// stringで比較する。
let yaml_value = match select_value {
Yaml::Boolean(b) => Option::Some(b.to_string()),
Yaml::Integer(i) => Option::Some(i.to_string()),
Yaml::Real(r) => Option::Some(r.to_string()),
Yaml::String(s) => Option::Some(s.to_owned()),
_ => Option::None,
};
// ここには来ないはず
if yaml_value.is_none() {
let errmsg = format!(
"unknown error occured. [key:{}]",
concat_selection_key(key_list)
);
return Result::Err(vec![errmsg]);
}
// 指定された正規表現が間違っていて、パースに失敗した場合
let yaml_str = yaml_value.unwrap();
let re_result = Regex::new(&yaml_str);
if re_result.is_err() {
let errmsg = format!(
"cannot parse regex. [regex:{}, key:{}]",
yaml_str,
concat_selection_key(key_list)
);
return Result::Err(vec![errmsg]);
}
self.re = re_result.ok();
return Result::Ok(());
}
fn is_match(&self, event_value: Option<&Value>) -> bool {
// unwrap_orの引数に""ではなく" "を指定しているのは、
// event_valueが文字列じゃない場合にis_event_value_nullの値がfalseになるように、len() == 0とならない値を指定している。
let is_event_value_null = event_value.is_none()
|| event_value.unwrap().is_null()
|| event_value.unwrap().as_str().unwrap_or(" ").len() == 0;
// yamlにnullが設定されていた場合
if self.re.is_none() {
return is_event_value_null;
}
return match event_value.unwrap_or(&Value::Null) {
Value::Bool(b) => self.is_regex_fullmatch(self.re.as_ref().unwrap(), b.to_string()),
Value::String(s) => self.is_regex_fullmatch(self.re.as_ref().unwrap(), s.to_owned()),
Value::Number(n) => self.is_regex_fullmatch(self.re.as_ref().unwrap(), n.to_string()),
_ => false,
};
}
}
// 指定された文字数以上であることをチェックするクラス。
struct MinlengthMatcher {
min_len: i64,
}
impl MinlengthMatcher {
fn new() -> MinlengthMatcher {
return MinlengthMatcher { min_len: 0 };
}
}
impl LeafMatcher for MinlengthMatcher {
fn is_target_key(&self, key_list: &Vec<String>) -> bool {
if key_list.len() != 1 {
return false;
}
return key_list.get(0).unwrap() == "min_length";
}
fn init(&mut self, key_list: &Vec<String>, select_value: &Yaml) -> Result<(), Vec<String>> {
let min_length = select_value.as_i64();
if min_length.is_none() {
let errmsg = format!(
"min_length value should be Integer. [key:{}]",
concat_selection_key(key_list)
);
return Result::Err(vec![errmsg]);
}
self.min_len = min_length.unwrap();
return Result::Ok(());
}
fn is_match(&self, event_value: Option<&Value>) -> bool {
return match event_value.unwrap_or(&Value::Null) {
Value::String(s) => s.len() as i64 >= self.min_len,
Value::Number(n) => n.to_string().len() as i64 >= self.min_len,
_ => false,
};
}
}
// 正規表現のリストが記載されたファイルを読み取って、比較するロジックを表すクラス
// DeepBlueCLIのcheck_cmdメソッドの一部に同様の処理が実装されていた。
struct RegexesFileMatcher {
regexes_csv_content: Vec<Vec<String>>,
}
impl RegexesFileMatcher {
fn new() -> RegexesFileMatcher {
return RegexesFileMatcher {
regexes_csv_content: vec![],
};
}
}
impl LeafMatcher for RegexesFileMatcher {
fn is_target_key(&self, key_list: &Vec<String>) -> bool {
if key_list.len() != 1 {
return false;
}
return key_list.get(0).unwrap() == "regexes";
}
fn init(&mut self, key_list: &Vec<String>, select_value: &Yaml) -> Result<(), Vec<String>> {
let value = match select_value {
Yaml::String(s) => Option::Some(s.to_owned()),
Yaml::Integer(i) => Option::Some(i.to_string()),
Yaml::Real(r) => Option::Some(r.to_owned()),
_ => Option::None,
};
if value.is_none() {
let errmsg = format!(
"regexes value should be String. [key:{}]",
concat_selection_key(key_list)
);
return Result::Err(vec![errmsg]);
}
let csv_content = utils::read_csv(&value.unwrap());
if csv_content.is_err() {
let errmsg = format!(
"cannot read regexes file. [key:{}]",
concat_selection_key(key_list)
);
return Result::Err(vec![errmsg]);
}
self.regexes_csv_content = csv_content.unwrap();
return Result::Ok(());
}
fn is_match(&self, event_value: Option<&Value>) -> bool {
return match event_value.unwrap_or(&Value::Null) {
Value::String(s) => !utils::check_regex(s, 0, &self.regexes_csv_content).is_empty(),
Value::Number(n) => {
!utils::check_regex(&n.to_string(), 0, &self.regexes_csv_content).is_empty()
}
_ => false,
};
}
}
// ファイルに列挙された文字列に一致する場合に検知するロジックを表す
// DeepBlueCLIのcheck_cmdメソッドの一部に同様の処理が実装されていた。
struct WhitelistFileMatcher {
whitelist_csv_content: Vec<Vec<String>>,
}
impl WhitelistFileMatcher {
fn new() -> WhitelistFileMatcher {
return WhitelistFileMatcher {
whitelist_csv_content: vec![],
};
}
}
impl LeafMatcher for WhitelistFileMatcher {
fn is_target_key(&self, key_list: &Vec<String>) -> bool {
if key_list.len() != 1 {
return false;
}
return key_list.get(0).unwrap() == "whitelist";
}
fn init(&mut self, key_list: &Vec<String>, select_value: &Yaml) -> Result<(), Vec<String>> {
let value = match select_value {
Yaml::String(s) => Option::Some(s.to_owned()),
Yaml::Integer(i) => Option::Some(i.to_string()),
Yaml::Real(r) => Option::Some(r.to_owned()),
_ => Option::None,
};
if value.is_none() {
let errmsg = format!(
"whitelist value should be String. [key:{}]",
concat_selection_key(key_list)
);
return Result::Err(vec![errmsg]);
}
let csv_content = utils::read_csv(&value.unwrap());
if csv_content.is_err() {
let errmsg = format!(
"cannot read whitelist file. [key:{}]",
concat_selection_key(key_list)
);
return Result::Err(vec![errmsg]);
}
self.whitelist_csv_content = csv_content.unwrap();
return Result::Ok(());
}
fn is_match(&self, event_value: Option<&Value>) -> bool {
return match event_value.unwrap_or(&Value::Null) {
Value::String(s) => !utils::check_whitelist(s, &self.whitelist_csv_content),
Value::Number(n) => {
!utils::check_whitelist(&n.to_string(), &self.whitelist_csv_content)
}
Value::Bool(b) => !utils::check_whitelist(&b.to_string(), &self.whitelist_csv_content),
_ => true,
};
}
}
#[cfg(test)]
mod tests {
use yaml_rust::YamlLoader;
use crate::detections::rule::{
parse_rule, AndSelectionNode, LeafSelectionNode, MinlengthMatcher, OrSelectionNode,
RegexMatcher, RegexesFileMatcher, SelectionNode, WhitelistFileMatcher,
};
use super::RuleNode;
#[test]
fn test_rule_parse() {
// ルールファイルをYAML形式で読み込み
let rule_str = r#"
title: PowerShell Execution Pipeline
description: hogehoge
enabled: true
author: Yea
logsource:
product: windows
detection:
selection:
Channel: Microsoft-Windows-PowerShell/Operational
EventID: 4103
ContextInfo:
- Host Application
- ホスト アプリケーション
ImagePath:
min_length: 1234321
regexes: ./regexes.txt
whitelist: ./whitelist.txt
falsepositives:
- unknown
level: medium
output: 'command=%CommandLine%'
creation_date: 2020/11/8
updated_date: 2020/11/8
"#;
let rule_node = parse_rule_from_str(rule_str);
let selection_node = rule_node.detection.unwrap().selection.unwrap();
// Root
let detection_childs = selection_node.get_childs();
assert_eq!(detection_childs.len(), 4);
// Channel
{
// LeafSelectionNodeが正しく読み込めることを確認
let child_node = detection_childs[0];
assert_eq!(child_node.is::<LeafSelectionNode>(), true);
let child_node = child_node.downcast_ref::<LeafSelectionNode>().unwrap();
assert_eq!(child_node.get_key(), "Channel");
assert_eq!(child_node.get_childs().len(), 0);
// 比較する正規表現が正しいことを確認
let matcher = &child_node.matcher;
assert_eq!(matcher.is_some(), true);
let matcher = child_node.matcher.as_ref().unwrap();
assert_eq!(matcher.is::<RegexMatcher>(), true);
let matcher = matcher.downcast_ref::<RegexMatcher>().unwrap();
assert_eq!(matcher.re.is_some(), true);
let re = matcher.re.as_ref();
assert_eq!(
re.unwrap().as_str(),
"Microsoft-Windows-PowerShell/Operational"
);
}
// EventID
{
// LeafSelectionNodeが正しく読み込めることを確認
let child_node = detection_childs[1];
assert_eq!(child_node.is::<LeafSelectionNode>(), true);
let child_node = child_node.downcast_ref::<LeafSelectionNode>().unwrap();
assert_eq!(child_node.get_key(), "EventID");
assert_eq!(child_node.get_childs().len(), 0);
// 比較する正規表現が正しいことを確認
let matcher = &child_node.matcher;
assert_eq!(matcher.is_some(), true);
let matcher = child_node.matcher.as_ref().unwrap();
assert_eq!(matcher.is::<RegexMatcher>(), true);
let matcher = matcher.downcast_ref::<RegexMatcher>().unwrap();
assert_eq!(matcher.re.is_some(), true);
let re = matcher.re.as_ref();
assert_eq!(re.unwrap().as_str(), "4103");
}
// ContextInfo
{
// OrSelectionNodeを正しく読み込めることを確認
let child_node = detection_childs[2];
assert_eq!(child_node.is::<OrSelectionNode>(), true);
let child_node = child_node.downcast_ref::<OrSelectionNode>().unwrap();
let ancestors = child_node.get_childs();
assert_eq!(ancestors.len(), 2);
// OrSelectionNodeの下にLeafSelectionNodeがあるパターンをテスト
// LeafSelectionNodeである、Host Applicationードが正しいことを確認
let hostapp_en_node = ancestors[0];
assert_eq!(hostapp_en_node.is::<LeafSelectionNode>(), true);
let hostapp_en_node = hostapp_en_node.downcast_ref::<LeafSelectionNode>().unwrap();
let hostapp_en_matcher = &hostapp_en_node.matcher;
assert_eq!(hostapp_en_matcher.is_some(), true);
let hostapp_en_matcher = hostapp_en_matcher.as_ref().unwrap();
assert_eq!(hostapp_en_matcher.is::<RegexMatcher>(), true);
let hostapp_en_matcher = hostapp_en_matcher.downcast_ref::<RegexMatcher>().unwrap();
assert_eq!(hostapp_en_matcher.re.is_some(), true);
let re = hostapp_en_matcher.re.as_ref();
assert_eq!(re.unwrap().as_str(), "Host Application");
// LeafSelectionNodeである、ホスト アプリケーションノードが正しいことを確認
let hostapp_jp_node = ancestors[1];
assert_eq!(hostapp_jp_node.is::<LeafSelectionNode>(), true);
let hostapp_jp_node = hostapp_jp_node.downcast_ref::<LeafSelectionNode>().unwrap();
let hostapp_jp_matcher = &hostapp_jp_node.matcher;
assert_eq!(hostapp_jp_matcher.is_some(), true);
let hostapp_jp_matcher = hostapp_jp_matcher.as_ref().unwrap();
assert_eq!(hostapp_jp_matcher.is::<RegexMatcher>(), true);
let hostapp_jp_matcher = hostapp_jp_matcher.downcast_ref::<RegexMatcher>().unwrap();
assert_eq!(hostapp_jp_matcher.re.is_some(), true);
let re = hostapp_jp_matcher.re.as_ref();
assert_eq!(re.unwrap().as_str(), "ホスト アプリケーション");
}
// ImagePath
{
// AndSelectionNodeを正しく読み込めることを確認
let child_node = detection_childs[3];
assert_eq!(child_node.is::<AndSelectionNode>(), true);
let child_node = child_node.downcast_ref::<AndSelectionNode>().unwrap();
let ancestors = child_node.get_childs();
assert_eq!(ancestors.len(), 3);
// min-lenが正しく読み込めることを確認
{
let ancestor_node = ancestors[0];
assert_eq!(ancestor_node.is::<LeafSelectionNode>(), true);
let ancestor_node = ancestor_node.downcast_ref::<LeafSelectionNode>().unwrap();
let ancestor_node = &ancestor_node.matcher;
assert_eq!(ancestor_node.is_some(), true);
let ancestor_matcher = ancestor_node.as_ref().unwrap();
assert_eq!(ancestor_matcher.is::<MinlengthMatcher>(), true);
let ancestor_matcher = ancestor_matcher.downcast_ref::<MinlengthMatcher>().unwrap();
assert_eq!(ancestor_matcher.min_len, 1234321);
}
// regexesが正しく読み込めることを確認
{
let ancestor_node = ancestors[1];
assert_eq!(ancestor_node.is::<LeafSelectionNode>(), true);
let ancestor_node = ancestor_node.downcast_ref::<LeafSelectionNode>().unwrap();
let ancestor_node = &ancestor_node.matcher;
assert_eq!(ancestor_node.is_some(), true);
let ancestor_matcher = ancestor_node.as_ref().unwrap();
assert_eq!(ancestor_matcher.is::<RegexesFileMatcher>(), true);
let ancestor_matcher = ancestor_matcher
.downcast_ref::<RegexesFileMatcher>()
.unwrap();
// regexes.txtの中身と一致していることを確認
let csvcontent = &ancestor_matcher.regexes_csv_content;
assert_eq!(csvcontent.len(), 14);
let firstcontent = &csvcontent[0];
assert_eq!(firstcontent.len(), 3);
assert_eq!(firstcontent[0], "0");
assert_eq!(
firstcontent[1],
r"^cmd.exe /c echo [a-z]{6} > \\\\.\\pipe\\[a-z]{6}$"
);
assert_eq!(
firstcontent[2],
r"Metasploit-style cmd with pipe (possible use of Meterpreter 'getsystem')"
);
let lastcontent = &csvcontent[13];
assert_eq!(lastcontent.len(), 3);
assert_eq!(lastcontent[0], "0");
assert_eq!(
lastcontent[1],
r"\\cvtres\.exe.*\\AppData\\Local\\Temp\\[A-Z0-9]{7}\.tmp"
);
assert_eq!(lastcontent[2], r"PSAttack-style command via cvtres.exe");
}
// whitelist.txtが読み込めることを確認
{
let ancestor_node = ancestors[2];
assert_eq!(ancestor_node.is::<LeafSelectionNode>(), true);
let ancestor_node = ancestor_node.downcast_ref::<LeafSelectionNode>().unwrap();
let ancestor_node = &ancestor_node.matcher;
assert_eq!(ancestor_node.is_some(), true);
let ancestor_matcher = ancestor_node.as_ref().unwrap();
assert_eq!(ancestor_matcher.is::<WhitelistFileMatcher>(), true);
let ancestor_matcher = ancestor_matcher
.downcast_ref::<WhitelistFileMatcher>()
.unwrap();
let csvcontent = &ancestor_matcher.whitelist_csv_content;
assert_eq!(csvcontent.len(), 2);
assert_eq!(
csvcontent[0][0],
r#"^"C:\\Program Files\\Google\\Chrome\\Application\\chrome\.exe""#.to_string()
);
assert_eq!(
csvcontent[1][0],
r#"^"C:\\Program Files\\Google\\Update\\GoogleUpdate\.exe""#.to_string()
);
}
}
}
// #[test]
// fn test_get_event_ids() {
// let rule_str = r#"
// enabled: true
// detection:
// selection:
// EventID: 1234
// output: 'command=%CommandLine%'
// "#;
// let rule_node = parse_rule_from_str(rule_str);
// let event_ids = rule_node.get_event_ids();
// assert_eq!(event_ids.len(), 1);
// assert_eq!(event_ids[0], 1234);
// }
#[test]
fn test_notdetect_regex_eventid() {
// 完全一致なので、前方一致で検知しないことを確認
let rule_str = r#"
enabled: true
detection:
selection:
EventID: 4103
output: 'command=%CommandLine%'
"#;
let record_json_str = r#"
{
"Event": {"System": {"EventID": 410}},
"Event_attributes": {"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"}
}"#;
let rule_node = parse_rule_from_str(rule_str);
let selection_node = rule_node.detection.unwrap().selection.unwrap();
match serde_json::from_str(record_json_str) {
Ok(record) => {
assert_eq!(selection_node.select(&record), false);
}
Err(_) => {
assert!(false, "failed to parse json record.");
}
}
}
#[test]
fn test_notdetect_regex_eventid2() {
// 完全一致なので、後方一致で検知しないことを確認
let rule_str = r#"
enabled: true
detection:
selection:
EventID: 4103
output: 'command=%CommandLine%'
"#;
let record_json_str = r#"
{
"Event": {"System": {"EventID": 103}},
"Event_attributes": {"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"}
}"#;
let rule_node = parse_rule_from_str(rule_str);
let selection_node = rule_node.detection.unwrap().selection.unwrap();
match serde_json::from_str(record_json_str) {
Ok(record) => {
assert_eq!(selection_node.select(&record), false);
}
Err(_) => {
assert!(false, "failed to parse json record.");
}
}
}
#[test]
fn test_detect_regex_eventid() {
// これはEventID=4103で検知するはず
let rule_str = r#"
enabled: true
detection:
selection:
EventID: 4103
output: 'command=%CommandLine%'
"#;
let record_json_str = r#"
{
"Event": {"System": {"EventID": 4103}},
"Event_attributes": {"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"}
}"#;
let rule_node = parse_rule_from_str(rule_str);
let selection_node = rule_node.detection.unwrap().selection.unwrap();
match serde_json::from_str(record_json_str) {
Ok(record) => {
assert_eq!(selection_node.select(&record), true);
}
Err(_) => {
assert!(false, "failed to parse json record.");
}
}
}
#[test]
fn test_notdetect_regex_str() {
// 文字列っぽいデータでも確認
// 完全一致なので、前方一致しないことを確認
let rule_str = r#"
enabled: true
detection:
selection:
Channel: Security
output: 'command=%CommandLine%'
"#;
let record_json_str = r#"
{
"Event": {"System": {"EventID": 4103, "Channel": "Securit"}},
"Event_attributes": {"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"}
}"#;
let rule_node = parse_rule_from_str(rule_str);
let selection_node = rule_node.detection.unwrap().selection.unwrap();
match serde_json::from_str(record_json_str) {
Ok(record) => {
assert_eq!(selection_node.select(&record), false);
}
Err(_) => {
assert!(false, "failed to parse json record.");
}
}
}
#[test]
fn test_notdetect_regex_str2() {
// 文字列っぽいデータでも確認
// 完全一致なので、後方一致しないことを確認
let rule_str = r#"
enabled: true
detection:
selection:
Channel: Security
output: 'command=%CommandLine%'
"#;
let record_json_str = r#"
{
"Event": {"System": {"EventID": 4103, "Channel": "ecurity"}},
"Event_attributes": {"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"}
}"#;
let rule_node = parse_rule_from_str(rule_str);
let selection_node = rule_node.detection.unwrap().selection.unwrap();
match serde_json::from_str(record_json_str) {
Ok(record) => {
assert_eq!(selection_node.select(&record), false);
}
Err(_) => {
assert!(false, "failed to parse json record.");
}
}
}
#[test]
fn test_detect_regex_str() {
// 文字列っぽいデータでも完全一致することを確認
let rule_str = r#"
enabled: true
detection:
selection:
Channel: Security
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);
let selection_node = rule_node.detection.unwrap().selection.unwrap();
match serde_json::from_str(record_json_str) {
Ok(record) => {
assert_eq!(selection_node.select(&record), true);
}
Err(_) => {
assert!(false, "failed to parse json record.");
}
}
}
#[test]
fn test_notdetect_regex_emptystr() {
// 文字列っぽいデータでも完全一致することを確認
let rule_str = r#"
enabled: true
detection:
selection:
Channel: Security
output: 'command=%CommandLine%'
"#;
let record_json_str = r#"
{
"Event": {"System": {"Channel": ""}},
"Event_attributes": {"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"}
}"#;
let rule_node = parse_rule_from_str(rule_str);
let selection_node = rule_node.detection.unwrap().selection.unwrap();
match serde_json::from_str(record_json_str) {
Ok(record) => {
assert_eq!(selection_node.select(&record), false);
}
Err(_) => {
assert!(false, "failed to parse json record.");
}
}
}
#[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);
let selection_node = rule_node.detection.unwrap().selection.unwrap();
match serde_json::from_str(record_json_str) {
Ok(record) => {
assert_eq!(selection_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);
let selection_node = rule_node.detection.unwrap().selection.unwrap();
match serde_json::from_str(record_json_str) {
Ok(record) => {
assert_eq!(selection_node.select(&record), false);
}
Err(_) => {
assert!(false, "failed to parse json record.");
}
}
}
#[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);
let selection_node = rule_node.detection.unwrap().selection.unwrap();
match serde_json::from_str(record_json_str) {
Ok(record) => {
assert_eq!(selection_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);
let selection_node = rule_node.detection.unwrap().selection.unwrap();
match serde_json::from_str(record_json_str) {
Ok(record) => {
assert_eq!(selection_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);
let selection_node = rule_node.detection.unwrap().selection.unwrap();
match serde_json::from_str(record_json_str) {
Ok(record) => {
assert_eq!(selection_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);
let selection_node = rule_node.detection.unwrap().selection.unwrap();
match serde_json::from_str(record_json_str) {
Ok(record) => {
assert_eq!(selection_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);
let selection_node = rule_node.detection.unwrap().selection.unwrap();
match serde_json::from_str(record_json_str) {
Ok(record) => {
assert_eq!(selection_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);
let selection_node = rule_node.detection.unwrap().selection.unwrap();
match serde_json::from_str(record_json_str) {
Ok(record) => {
assert_eq!(selection_node.select(&record), false);
}
Err(_) => {
assert!(false, "failed to parse json record.");
}
}
}
#[test]
fn test_notdetect_casesensetive() {
// OR条件が正しく検知できることを確認
let rule_str = r#"
enabled: true
detection:
selection:
Channel: 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);
let selection_node = rule_node.detection.unwrap().selection.unwrap();
match serde_json::from_str(record_json_str) {
Ok(record) => {
assert_eq!(selection_node.select(&record), false);
}
Err(_) => {
assert!(false, "failed to parse json record.");
}
}
}
#[test]
fn test_notdetect_minlen() {
// minlenが正しく検知できることを確認
let rule_str = r#"
enabled: true
detection:
selection:
Channel:
min_length: 10
output: 'command=%CommandLine%'
"#;
let record_json_str = r#"
{
"Event": {"System": {"EventID": 4103, "Channel": "Security9", "Computer":"DESKTOP-ICHIICHI"}},
"Event_attributes": {"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"}
}"#;
let rule_node = parse_rule_from_str(rule_str);
let selection_node = rule_node.detection.unwrap().selection.unwrap();
match serde_json::from_str(record_json_str) {
Ok(record) => {
assert_eq!(selection_node.select(&record), false);
}
Err(_) => {
assert!(false, "failed to parse json record.");
}
}
}
#[test]
fn test_detect_minlen() {
// minlenが正しく検知できることを確認
let rule_str = r#"
enabled: true
detection:
selection:
Channel:
min_length: 10
output: 'command=%CommandLine%'
"#;
let record_json_str = r#"
{
"Event": {"System": {"EventID": 4103, "Channel": "Security10", "Computer":"DESKTOP-ICHIICHI"}},
"Event_attributes": {"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"}
}"#;
let rule_node = parse_rule_from_str(rule_str);
let selection_node = rule_node.detection.unwrap().selection.unwrap();
match serde_json::from_str(record_json_str) {
Ok(record) => {
assert_eq!(selection_node.select(&record), true);
}
Err(_) => {
assert!(false, "failed to parse json record.");
}
}
}
#[test]
fn test_detect_minlen2() {
// minlenが正しく検知できることを確認
let rule_str = r#"
enabled: true
detection:
selection:
Channel:
min_length: 10
output: 'command=%CommandLine%'
"#;
let record_json_str = r#"
{
"Event": {"System": {"EventID": 4103, "Channel": "Security.11", "Computer":"DESKTOP-ICHIICHI"}},
"Event_attributes": {"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"}
}"#;
let rule_node = parse_rule_from_str(rule_str);
let selection_node = rule_node.detection.unwrap().selection.unwrap();
match serde_json::from_str(record_json_str) {
Ok(record) => {
assert_eq!(selection_node.select(&record), true);
}
Err(_) => {
assert!(false, "failed to parse json record.");
}
}
}
#[test]
fn test_detect_minlen_and() {
// minlenが正しく検知できることを確認
let rule_str = r#"
enabled: true
detection:
selection:
Channel:
regex: Security10
min_length: 10
output: 'command=%CommandLine%'
"#;
let record_json_str = r#"
{
"Event": {"System": {"EventID": 4103, "Channel": "Security10", "Computer":"DESKTOP-ICHIICHI"}},
"Event_attributes": {"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"}
}"#;
let rule_node = parse_rule_from_str(rule_str);
let selection_node = rule_node.detection.unwrap().selection.unwrap();
match serde_json::from_str(record_json_str) {
Ok(record) => {
assert_eq!(selection_node.select(&record), true);
}
Err(_) => {
assert!(false, "failed to parse json record.");
}
}
}
#[test]
fn test_notdetect_minlen_and() {
// minlenが正しく検知できることを確認
let rule_str = r#"
enabled: true
detection:
selection:
Channel:
regex: Security10
min_length: 11
output: 'command=%CommandLine%'
"#;
let record_json_str = r#"
{
"Event": {"System": {"EventID": 4103, "Channel": "Security10", "Computer":"DESKTOP-ICHIICHI"}},
"Event_attributes": {"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"}
}"#;
let rule_node = parse_rule_from_str(rule_str);
let selection_node = rule_node.detection.unwrap().selection.unwrap();
match serde_json::from_str(record_json_str) {
Ok(record) => {
assert_eq!(selection_node.select(&record), false);
}
Err(_) => {
assert!(false, "failed to parse json record.");
}
}
}
#[test]
fn test_detect_regex() {
// 正規表現が使えることを確認
let rule_str = r#"
enabled: true
detection:
selection:
Channel: ^Program$
output: 'command=%CommandLine%'
"#;
let record_json_str = r#"
{
"Event": {"System": {"EventID": 4103, "Channel": "Program", "Computer":"DESKTOP-ICHIICHI"}},
"Event_attributes": {"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"}
}"#;
let rule_node = parse_rule_from_str(rule_str);
let selection_node = rule_node.detection.unwrap().selection.unwrap();
match serde_json::from_str(record_json_str) {
Ok(record) => {
assert_eq!(selection_node.select(&record), true);
}
Err(_) => {
assert!(false, "failed to parse json record.");
}
}
}
#[test]
fn test_detect_regexes() {
// regexes.txtが正しく検知できることを確認
// この場合ではEventIDが一致しているが、whitelistに一致するので検知しないはず。
let rule_str = r#"
enabled: true
detection:
selection:
EventID: 4103
Channel:
- whitelist: whitelist.txt
output: 'command=%CommandLine%'
"#;
// JSONで値としてダブルクオートを使う場合、\でエスケープが必要なのに注意
let record_json_str = r#"
{
"Event": {"System": {"EventID": 4103, "Channel": "\"C:\\Program Files\\Google\\Update\\GoogleUpdate.exe\"", "Computer":"DESKTOP-ICHIICHI"}},
"Event_attributes": {"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"}
}"#;
let rule_node = parse_rule_from_str(rule_str);
let selection_node = rule_node.detection.unwrap().selection.unwrap();
match serde_json::from_str(record_json_str) {
Ok(record) => {
assert_eq!(selection_node.select(&record), false);
}
Err(_) => {
assert!(false, "failed to parse json record.");
}
}
}
#[test]
fn test_detect_whitelist() {
// whitelistが正しく検知できることを確認
// この場合ではEventIDが一致しているが、whitelistに一致するので検知しないはず。
let rule_str = r#"
enabled: true
detection:
selection:
EventID: 4103
Channel:
- whitelist: whitelist.txt
output: 'command=%CommandLine%'
"#;
// JSONで値としてダブルクオートを使う場合、\でエスケープが必要なのに注意
let record_json_str = r#"
{
"Event": {"System": {"EventID": 4103, "Channel": "\"C:\\Program Files\\Google\\Update\\GoogleUpdate.exe\"", "Computer":"DESKTOP-ICHIICHI"}},
"Event_attributes": {"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"}
}"#;
let rule_node = parse_rule_from_str(rule_str);
let selection_node = rule_node.detection.unwrap().selection.unwrap();
match serde_json::from_str(record_json_str) {
Ok(record) => {
assert_eq!(selection_node.select(&record), false);
}
Err(_) => {
assert!(false, "failed to parse json record.");
}
}
}
#[test]
fn test_detect_whitelist2() {
// whitelistが正しく検知できることを確認
// この場合ではEventIDが一致しているが、whitelistに一致するので検知しないはず。
let rule_str = r#"
enabled: true
detection:
selection:
EventID: 4103
Channel:
- whitelist: whitelist.txt
output: 'command=%CommandLine%'
"#;
let record_json_str = r#"
{
"Event": {"System": {"EventID": 4103, "Channel": "\"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe\"", "Computer":"DESKTOP-ICHIICHI"}},
"Event_attributes": {"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"}
}"#;
let rule_node = parse_rule_from_str(rule_str);
let selection_node = rule_node.detection.unwrap().selection.unwrap();
match serde_json::from_str(record_json_str) {
Ok(record) => {
assert_eq!(selection_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);
let selection_node = rule_node.detection.unwrap().selection.unwrap();
match serde_json::from_str(record_json_str) {
Ok(record) => {
assert_eq!(selection_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);
let selection_node = rule_node.detection.unwrap().selection.unwrap();
match serde_json::from_str(record_json_str) {
Ok(record) => {
assert_eq!(selection_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);
let selection_node = rule_node.detection.unwrap().selection.unwrap();
match serde_json::from_str(record_json_str) {
Ok(record) => {
assert_eq!(selection_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);
let selection_node = rule_node.detection.unwrap().selection.unwrap();
match serde_json::from_str(record_json_str) {
Ok(record) => {
assert_eq!(selection_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);
let selection_node = rule_node.detection.unwrap().selection.unwrap();
match serde_json::from_str(record_json_str) {
Ok(record) => {
assert_eq!(selection_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: '[\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);
let selection_node = rule_node.detection.unwrap().selection.unwrap();
match serde_json::from_str(record_json_str) {
Ok(record) => {
assert_eq!(selection_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);
let selection_node = rule_node.detection.unwrap().selection.unwrap();
match serde_json::from_str(record_json_str) {
Ok(record) => {
assert_eq!(selection_node.select(&record), false);
}
Err(_) => {
assert!(false, "failed to parse json record.");
}
}
}
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 = parse_rule(rule_yaml.next().unwrap());
assert_eq!(rule_node.init().is_ok(), true);
return rule_node;
}
}