diff --git a/Cargo.lock b/Cargo.lock index dd0f2dad..6105b6a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -632,6 +632,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "mopa" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a785740271256c230f57462d3b83e52f998433a7062fc18f96d5999474a9f915" + [[package]] name = "ntapi" version = "0.3.6" @@ -1358,6 +1364,7 @@ dependencies = [ "flate2", "lazy_static", "linked-hash-map", + "mopa", "num_cpus", "quick-xml 0.17.2", "regex", diff --git a/Cargo.toml b/Cargo.toml index 54dce47f..a38917e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ yaml-rust = "0.4" linked-hash-map = "0.5.3" tokio = { version = "1", features = ["full"] } num_cpus = "1.13.0" +mopa = "0.2.2" [target.x86_64-pc-windows-gnu] linker = "x86_64-w64-mingw32-gcc" diff --git a/config/eventkey_alias.txt b/config/eventkey_alias.txt index 4c6b4c97..c39542dd 100644 --- a/config/eventkey_alias.txt +++ b/config/eventkey_alias.txt @@ -21,4 +21,6 @@ LogFileCleared,Event.UserData.LogFileCleared.SubjectUserName LogFileClearedSubjectUserName,Event.UserData.SubjectUserName SubjectUserName,Event.EventData.SubjectUserName SubjectUserSid,Event.EventData.SubjectUserSid -DomainName,Event.EventData.SubjectDomainName \ No newline at end of file +DomainName,Event.EventData.SubjectDomainName +TicketEncryptionType,Event.EventData.TicketEncryptionType +PreAuthType,Event.EventData.PreAuthType \ No newline at end of file diff --git a/rules/kerberoast/as-rep-roasting.yml b/rules/kerberoast/as-rep-roasting.yml new file mode 100644 index 00000000..9585fa33 --- /dev/null +++ b/rules/kerberoast/as-rep-roasting.yml @@ -0,0 +1,18 @@ +title: AS-REP Roasting +description: For each account found without preauthentication, an adversary may send an AS-REQ message without the encrypted timestamp and receive an AS-REP message with TGT data which may be encrypted with an insecure algorithm such as RC4. +enabled: true +author: Yea +logsource: + product: windows +detection: + selection: + Channel: Security + EventID: 4768 + TicketEncryptionType: '0x17' + PreAuthType: 0 +falsepositives: + - unknown +level: medium +output: 'Detected AS-REP Roasting Risk Actvity.' +creation_date: 2021/4/31 +updated_date: 2021/4/31 diff --git a/rules/kerberoast/kerberoasting.yml b/rules/kerberoast/kerberoasting.yml new file mode 100644 index 00000000..4d829045 --- /dev/null +++ b/rules/kerberoast/kerberoasting.yml @@ -0,0 +1,18 @@ +title: Kerberoasting +description: Adversaries may abuse a valid Kerberos ticket-granting ticket (TGT) or sniff network traffic to obtain a ticket-granting service (TGS) ticket that may be vulnerable to Brute Force. +enabled: true +author: Yea +logsource: + product: windows +detection: + selection: + Channel: Security + EventID: 4768 + TicketEncryptionType: '0x17' + PreAuthType: 2 +falsepositives: + - unknown +level: medium +output: 'Detected Kerberoasting Risk Activity.' +creation_date: 2021/4/31 +updated_date: 2021/4/31 diff --git a/src/detections/detection.rs b/src/detections/detection.rs index e13237cd..d657c22c 100644 --- a/src/detections/detection.rs +++ b/src/detections/detection.rs @@ -1,53 +1,49 @@ extern crate csv; +use serde_json::Value; +use tokio::{spawn, task::JoinHandle}; + use crate::detections::print::MESSAGES; use crate::detections::rule; use crate::detections::rule::RuleNode; use crate::detections::{print::AlertMessage, utils}; use crate::yaml::ParseYaml; -use evtx::err; -use evtx::{EvtxParser, ParserSettings, SerializedEvtxRecord}; -use serde_json::Value; -use tokio::{spawn, task::JoinHandle}; - -use std::{collections::HashSet, path::PathBuf}; -use std::{fs::File, sync::Arc}; +use std::sync::Arc; const DIRPATH_RULES: &str = "rules"; +// イベントファイルの1レコード分の情報を保持する構造体 #[derive(Clone, Debug)] pub struct EvtxRecordInfo { - evtx_filepath: String, - record: Value, + pub evtx_filepath: String, // イベントファイルのファイルパス ログで出力するときに使う + pub record: Value, // 1レコード分のデータをJSON形式にシリアライズしたもの +} + +impl EvtxRecordInfo { + pub fn new(evtx_filepath: String, record: Value) -> EvtxRecordInfo { + return EvtxRecordInfo { + evtx_filepath: evtx_filepath, + record: record, + }; + } } // TODO テストケースかかなきゃ... #[derive(Debug)] -pub struct Detection { - parseinfos: Vec, -} +pub struct Detection {} impl Detection { pub fn new() -> Detection { - let initializer: Vec = Vec::new(); - Detection { - parseinfos: initializer, - } + return Detection {}; } - pub fn start(&mut self, evtx_files: Vec) { - if evtx_files.is_empty() { - return; - } - + pub fn start(&mut self, records: Vec) { let rules = self.parse_rule_files(); if rules.is_empty() { return; } - let records = self.evtx_to_jsons(&evtx_files, &rules); - let tokio_rt = utils::create_tokio_runtime(); tokio_rt.block_on(self.execute_rule(rules, records)); tokio_rt.shutdown_background(); @@ -98,195 +94,6 @@ impl Detection { .collect(); } - // evtxファイルをjsonに変換します。 - fn evtx_to_jsons( - &mut self, - evtx_files: &Vec, - rules: &Vec, - ) -> Vec { - // EvtxParserを生成する。 - let evtx_parsers: Vec> = evtx_files - .clone() - .into_iter() - .filter_map(|evtx_file| { - // convert to evtx parser - // println!("PathBuf:{}", evtx_file.display()); - match EvtxParser::from_path(evtx_file) { - Ok(parser) => Option::Some(parser), - Err(e) => { - eprintln!("{}", e); - return Option::None; - } - } - }) - .collect(); - - let tokio_rt = utils::create_tokio_runtime(); - let xml_records = tokio_rt.block_on(self.evtx_to_xml(evtx_parsers, &evtx_files)); - - let json_records = tokio_rt.block_on(self.xml_to_json(xml_records, &evtx_files, &rules)); - tokio_rt.shutdown_background(); - - return json_records - .into_iter() - .map(|(parser_idx, json_record)| { - let evtx_filepath = evtx_files[parser_idx].display().to_string(); - return EvtxRecordInfo { - evtx_filepath: String::from(&evtx_filepath), - record: json_record, - }; - }) - .collect(); - } - - // evtxファイルからxmlを生成する。 - // 戻り値は「どのイベントファイルから生成されたXMLかを示すindex」と「変換されたXML」のタプルです。 - // タプルのindexは、引数で指定されるevtx_filesのindexに対応しています。 - async fn evtx_to_xml( - &mut self, - evtx_parsers: Vec>, - evtx_files: &Vec, - ) -> Vec<(usize, SerializedEvtxRecord)> { - // evtx_parser.records_json()でevtxをxmlに変換するJobを作成 - let handles: Vec>>>> = evtx_parsers - .into_iter() - .map(|mut evtx_parser| { - return spawn(async move { - let mut parse_config = ParserSettings::default(); - parse_config = parse_config.separate_json_attributes(true); - parse_config = parse_config.num_threads(utils::get_thread_num()); - - evtx_parser = evtx_parser.with_configuration(parse_config); - let values = evtx_parser.records_json().collect(); - return values; - }); - }) - .collect(); - - // 作成したjobを実行し(handle.awaitの部分)、スレッドの実行時にエラーが発生した場合、標準エラー出力に出しておく - let mut ret = vec![]; - for (parser_idx, handle) in handles.into_iter().enumerate() { - let future_result = handle.await; - if future_result.is_err() { - let evtx_filepath = &evtx_files[parser_idx].display(); - let errmsg = format!( - "Failed to parse event file. EventFile:{} Error:{}", - evtx_filepath, - future_result.unwrap_err() - ); - AlertMessage::alert(&mut std::io::stdout().lock(), errmsg).ok(); - continue; - } - - future_result.unwrap().into_iter().for_each(|parse_result| { - ret.push((parser_idx, parse_result)); - }); - } - - return ret - .into_iter() - .filter_map(|(parser_idx, parse_result)| { - if parse_result.is_err() { - let evtx_filepath = &evtx_files[parser_idx].display(); - let errmsg = format!( - "Failed to parse event file. EventFile:{} Error:{}", - evtx_filepath, - parse_result.unwrap_err() - ); - AlertMessage::alert(&mut std::io::stdout().lock(), errmsg).ok(); - return Option::None; - } - return Option::Some((parser_idx, parse_result.unwrap())); - }) - .collect(); - } - - // xmlからjsonに変換します。 - // 戻り値は「どのイベントファイルから生成されたXMLかを示すindex」と「変換されたJSON」のタプルです。 - // タプルのindexは、引数で指定されるevtx_filesのindexに対応しています。 - async fn xml_to_json( - &mut self, - xml_records: Vec<(usize, SerializedEvtxRecord)>, - evtx_files: &Vec, - rules: &Vec, - ) -> Vec<(usize, Value)> { - // TODO スレッド作り過ぎなので、数を減らす - - // 非同期で実行される無名関数を定義 - let async_job = |pair: (usize, SerializedEvtxRecord), - event_id_set: Arc>, - evtx_files: Arc>| { - let parser_idx = pair.0; - let handle = spawn(async move { - let parse_result = serde_json::from_str(&pair.1.data); - // パースに失敗した場合はエラー出力しておく。 - if parse_result.is_err() { - let evtx_filepath = &evtx_files[parser_idx].display(); - let errmsg = format!( - "Failed to serialize from event xml to json. EventFile:{} Error:{}", - evtx_filepath, - parse_result.unwrap_err() - ); - AlertMessage::alert(&mut std::io::stdout().lock(), errmsg).ok(); - return Option::None; - } - - // ルールファイルで検知しようとしているEventIDでないレコードはここで捨てる。 - let parsed_json: Value = parse_result.unwrap(); - let event_id_opt = utils::get_event_value(&utils::get_event_id_key(), &parsed_json); - return event_id_opt - .and_then(|event_id| event_id.as_i64()) - .and_then(|event_id| { - if event_id_set.contains(&event_id) { - return Option::Some(parsed_json); - } else { - return Option::None; - } - }); - }); - - return (parser_idx, handle); - }; - // 非同期で実行するスレッドを生成し、実行する。 - let event_id_set_arc = Arc::new(Detection::get_event_ids(rules)); - let evtx_files_arc = Arc::new(evtx_files.clone()); - let handles: Vec<(usize, JoinHandle>)> = xml_records - .into_iter() - .map(|xml_record_pair| { - let event_id_set_clone = Arc::clone(&event_id_set_arc); - let evtx_files_clone = Arc::clone(&evtx_files_arc); - return async_job(xml_record_pair, event_id_set_clone, evtx_files_clone); - }) - .collect(); - - // スレッドの終了待ちをしている。 - let mut ret = vec![]; - for (parser_idx, handle) in handles { - let future = handle.await; - // スレッドが正常に完了しなかった場合はエラーメッセージを出力する。 - if future.is_err() { - let evtx_filepath = &evtx_files[parser_idx].display(); - let errmsg = format!( - "Failed to serialize from event xml to json. EventFile:{} Error:{}", - evtx_filepath, - future.unwrap_err() - ); - AlertMessage::alert(&mut std::io::stdout().lock(), errmsg).ok(); - continue; - } - - // パース失敗やルールファイルで検知しようとしていないEventIDの場合等はis_none()==trueになる。 - let parse_result = future.unwrap(); - if parse_result.is_none() { - continue; - } - - ret.push((parser_idx, parse_result.unwrap())); - } - - return ret; - } - // 検知ロジックを実行します。 async fn execute_rule(&mut self, rules: Vec, records: Vec) { // 複数スレッドで所有権を共有するため、recordsをArcでwwap @@ -340,13 +147,13 @@ impl Detection { } } - fn get_event_ids(rules: &Vec) -> HashSet { - return rules - .iter() - .map(|rule| rule.get_event_ids()) - .flatten() - .collect(); - } + // fn get_event_ids(rules: &Vec) -> HashSet { + // return rules + // .iter() + // .map(|rule| rule.get_event_ids()) + // .flatten() + // .collect(); + // } // 配列を指定したサイズで分割する。Vector.chunksと同じ動作をするが、Vectorの関数だとinto的なことができないので自作 fn chunks(ary: Vec, size: usize) -> Vec> { diff --git a/src/detections/mod.rs b/src/detections/mod.rs index 48034d62..3bfab408 100644 --- a/src/detections/mod.rs +++ b/src/detections/mod.rs @@ -2,4 +2,4 @@ pub mod configs; pub mod detection; pub mod print; mod rule; -mod utils; +pub mod utils; diff --git a/src/detections/rule.rs b/src/detections/rule.rs index 92461e8c..53837060 100644 --- a/src/detections/rule.rs +++ b/src/detections/rule.rs @@ -1,5 +1,7 @@ extern crate regex; +use mopa::mopafy; + use std::vec; use crate::detections::utils; @@ -124,39 +126,40 @@ impl RuleNode { return selection.unwrap().select(event_record); } - pub fn get_event_ids(&self) -> Vec { - let selection = self - .detection - .as_ref() - .and_then(|detection| detection.selection.as_ref()); - if selection.is_none() { - return vec![]; - } + // pub fn get_event_ids(&self) -> Vec { + // let selection = self + // .detection + // .as_ref() + // .and_then(|detection| detection.selection.as_ref()); + // if selection.is_none() { + // return vec![]; + // } - return selection - .unwrap() - .get_leaf_nodes() - .iter() - .filter(|node| { - // alias.txtのevent_keyに一致するかどうか - let key = utils::get_event_id_key(); - if node.get_key() == key { - return true; - } + // return selection + // .unwrap() + // .get_descendants() + // .iter() + // .filter_map(|node| return node.downcast_ref::()) // mopaというライブラリを使うと簡単にダウンキャストできるらしいです。https://crates.io/crates/mopa + // .filter(|node| { + // // キーがEventIDのノードである + // let key = utils::get_event_id_key(); + // if node.get_key() == key { + // return true; + // } - // alias.txtの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(); - } + // // 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を表すノード @@ -175,11 +178,13 @@ impl DetectionNode { } // Ruleファイルの detection- selection配下のノードはこのtraitを実装する。 -trait SelectionNode { +trait SelectionNode: mopa::Any { fn select(&self, event_record: &Value) -> bool; fn init(&mut self) -> Result<(), Vec>; - fn get_leaf_nodes(&self) -> Vec<&LeafSelectionNode>; + fn get_childs(&self) -> Vec<&Box>; + fn get_descendants(&self) -> Vec<&Box>; } +mopafy!(SelectionNode); // detection - selection配下でAND条件を表すノード struct AndSelectionNode { @@ -230,17 +235,26 @@ impl SelectionNode for AndSelectionNode { } } - fn get_leaf_nodes(&self) -> Vec<&LeafSelectionNode> { + fn get_childs(&self) -> Vec<&Box> { let mut ret = vec![]; + self.child_nodes.iter().for_each(|child_node| { + ret.push(child_node); + }); + + return ret; + } + + fn get_descendants(&self) -> Vec<&Box> { + let mut ret = self.get_childs(); self.child_nodes .iter() - .map(|child| { - return child.get_leaf_nodes(); + .map(|child_node| { + return child_node.get_descendants(); }) .flatten() - .for_each(|descendant| { - ret.push(descendant); + .for_each(|descendant_node| { + ret.push(descendant_node); }); return ret; @@ -296,17 +310,26 @@ impl SelectionNode for OrSelectionNode { } } - fn get_leaf_nodes(&self) -> Vec<&LeafSelectionNode> { + fn get_childs(&self) -> Vec<&Box> { let mut ret = vec![]; + self.child_nodes.iter().for_each(|child_node| { + ret.push(child_node); + }); + + return ret; + } + + fn get_descendants(&self) -> Vec<&Box> { + let mut ret = self.get_childs(); self.child_nodes .iter() - .map(|child| { - return child.get_leaf_nodes(); + .map(|child_node| { + return child_node.get_descendants(); }) .flatten() - .for_each(|descendant| { - ret.push(descendant); + .for_each(|descendant_node| { + ret.push(descendant_node); }); return ret; @@ -453,8 +476,12 @@ impl SelectionNode for LeafSelectionNode { .init(&match_key_list, &self.select_value); } - fn get_leaf_nodes(&self) -> Vec<&LeafSelectionNode> { - return vec![&self]; + fn get_childs(&self) -> Vec<&Box> { + return vec![]; + } + + fn get_descendants(&self) -> Vec<&Box> { + return vec![]; } } @@ -463,13 +490,14 @@ impl SelectionNode for LeafSelectionNode { // // 新規にLeafMatcherを実装するクラスを作成した場合、 // LeafSelectionNodeのget_matchersクラスの戻り値の配列に新規作成したクラスのインスタンスを追加する。 -trait LeafMatcher { +trait LeafMatcher: mopa::Any { fn is_target_key(&self, key_list: &Vec) -> bool; fn is_match(&self, event_value: Option<&Value>) -> bool; fn init(&mut self, key_list: &Vec, select_value: &Yaml) -> Result<(), Vec>; } +mopafy!(LeafMatcher); // 正規表現で比較するロジックを表すクラス struct RegexMatcher { @@ -720,10 +748,1464 @@ impl LeafMatcher for WhitelistFileMatcher { 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), - _ => false, + 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::(), true); + let child_node = child_node.downcast_ref::().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::(), true); + let matcher = matcher.downcast_ref::().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::(), true); + let child_node = child_node.downcast_ref::().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::(), true); + let matcher = matcher.downcast_ref::().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::(), true); + let child_node = child_node.downcast_ref::().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::(), true); + let hostapp_en_node = hostapp_en_node.downcast_ref::().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::(), true); + let hostapp_en_matcher = hostapp_en_matcher.downcast_ref::().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::(), true); + let hostapp_jp_node = hostapp_jp_node.downcast_ref::().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::(), true); + let hostapp_jp_matcher = hostapp_jp_matcher.downcast_ref::().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::(), true); + let child_node = child_node.downcast_ref::().unwrap(); + let ancestors = child_node.get_childs(); + assert_eq!(ancestors.len(), 3); + + // min-lenが正しく読み込めることを確認 + { + let ancestor_node = ancestors[0]; + assert_eq!(ancestor_node.is::(), true); + let ancestor_node = ancestor_node.downcast_ref::().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::(), true); + let ancestor_matcher = ancestor_matcher.downcast_ref::().unwrap(); + assert_eq!(ancestor_matcher.min_len, 1234321); + } + + // regexesが正しく読み込めることを確認 + { + let ancestor_node = ancestors[1]; + assert_eq!(ancestor_node.is::(), true); + let ancestor_node = ancestor_node.downcast_ref::().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::(), true); + let ancestor_matcher = ancestor_matcher + .downcast_ref::() + .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::(), true); + let ancestor_node = ancestor_node.downcast_ref::().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::(), true); + let ancestor_matcher = ancestor_matcher + .downcast_ref::() + .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を検知するテスト + /* - + - + + 4672 + 0 + 0 + 12548 + 0 + 0x8020000000000000 + + 244666 + + + Security + + + - + SYSTEM + NT AUTHORITY + SeAssignPrimaryTokenPrivilege SeTcbPrivilege SeSecurityPrivilege SeTakeOwnershipPrivilege SeLoadDriverPrivilege SeBackupPrivilege SeRestorePrivilege SeDebugPrivilege SeAuditPrivilege SeSystemEnvironmentPrivilege SeImpersonatePrivilege SeDelegateSessionUserImpersonatePrivilege + + */ + + 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=の部分にキー的なものが来る。 + /* - + S-1-5-21-2673273881-979819022-3746999991-1001 + takai + DESKTOP-ICHIICH + 0x312cd + DESKTOP-ICHIICH + Administrator + DESKTOP-ICHIICH + */ + + // その場合、イベントパーサーの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; + } +} diff --git a/src/lib.rs b/src/lib.rs index 06371c5a..623298ad 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub mod afterfact; pub mod detections; pub mod omikuji; +pub mod timeline; pub mod yaml; diff --git a/src/main.rs b/src/main.rs index 79f62399..ade1ad82 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,18 @@ extern crate serde; extern crate serde_derive; -use std::{fs, path::PathBuf}; -use yamato_event_analyzer::afterfact::after_fact; -use yamato_event_analyzer::detections::configs; +use evtx::{err, EvtxParser, ParserSettings, SerializedEvtxRecord}; +use std::{ + fs::{self, File}, + path::PathBuf, +}; +use tokio::{spawn, task::JoinHandle}; use yamato_event_analyzer::detections::detection; +use yamato_event_analyzer::detections::detection::EvtxRecordInfo; use yamato_event_analyzer::detections::print::AlertMessage; use yamato_event_analyzer::omikuji::Omikuji; +use yamato_event_analyzer::{afterfact::after_fact, detections::utils}; +use yamato_event_analyzer::{detections::configs, timeline::timeline::Timeline}; fn main() { if let Some(filepath) = configs::CONFIG.read().unwrap().args.value_of("filepath") { @@ -64,12 +70,111 @@ fn print_credits() { } fn detect_files(evtx_files: Vec) { + let evnt_records = evtx_to_jsons(&evtx_files); + + let mut tl = Timeline::new(); + tl.start(&evnt_records); + let mut detection = detection::Detection::new(); - &detection.start(evtx_files); + &detection.start(evnt_records); after_fact(); } +// evtxファイルをjsonに変換します。 +fn evtx_to_jsons(evtx_files: &Vec) -> Vec { + // EvtxParserを生成する。 + let evtx_parsers: Vec> = evtx_files + .clone() + .into_iter() + .filter_map(|evtx_file| { + // convert to evtx parser + // println!("PathBuf:{}", evtx_file.display()); + match EvtxParser::from_path(evtx_file) { + Ok(parser) => Option::Some(parser), + Err(e) => { + eprintln!("{}", e); + return Option::None; + } + } + }) + .collect(); + + let tokio_rt = utils::create_tokio_runtime(); + let ret = tokio_rt.block_on(evtx_to_json(evtx_parsers, &evtx_files)); + tokio_rt.shutdown_background(); + + return ret; +} + +// evtxファイルからEvtxRecordInfoを生成する。 +// 戻り値は「どのイベントファイルから生成されたXMLかを示すindex」と「変換されたXML」のタプルです。 +// タプルのindexは、引数で指定されるevtx_filesのindexに対応しています。 +async fn evtx_to_json( + evtx_parsers: Vec>, + evtx_files: &Vec, +) -> Vec { + // evtx_parser.records_json()でevtxをxmlに変換するJobを作成 + let handles: Vec>>>> = + evtx_parsers + .into_iter() + .map(|mut evtx_parser| { + return spawn(async move { + let mut parse_config = ParserSettings::default(); + parse_config = parse_config.separate_json_attributes(true); + parse_config = parse_config.num_threads(utils::get_thread_num()); + + evtx_parser = evtx_parser.with_configuration(parse_config); + let values = evtx_parser.records_json_value().collect(); + return values; + }); + }) + .collect(); + + // 作成したjobを実行し(handle.awaitの部分)、スレッドの実行時にエラーが発生した場合、標準エラー出力に出しておく + let mut ret = vec![]; + for (parser_idx, handle) in handles.into_iter().enumerate() { + let future_result = handle.await; + if future_result.is_err() { + let evtx_filepath = &evtx_files[parser_idx].display(); + let errmsg = format!( + "Failed to parse event file. EventFile:{} Error:{}", + evtx_filepath, + future_result.unwrap_err() + ); + AlertMessage::alert(&mut std::io::stdout().lock(), errmsg).ok(); + continue; + } + + future_result.unwrap().into_iter().for_each(|parse_result| { + ret.push((parser_idx, parse_result)); + }); + } + + return ret + .into_iter() + .filter_map(|(parser_idx, parse_result)| { + // パースに失敗している場合、エラーメッセージを出力 + if parse_result.is_err() { + let evtx_filepath = &evtx_files[parser_idx].display(); + let errmsg = format!( + "Failed to parse event file. EventFile:{} Error:{}", + evtx_filepath, + parse_result.unwrap_err() + ); + AlertMessage::alert(&mut std::io::stdout().lock(), errmsg).ok(); + return Option::None; + } + + let record_info = EvtxRecordInfo::new( + evtx_files[parser_idx].display().to_string(), + parse_result.unwrap().data, + ); + return Option::Some(record_info); + }) + .collect(); +} + fn _output_with_omikuji(omikuji: Omikuji) { let fp = &format!("art/omikuji/{}", omikuji); let content = fs::read_to_string(fp).unwrap(); diff --git a/src/timeline/mod.rs b/src/timeline/mod.rs new file mode 100644 index 00000000..5c4b7d84 --- /dev/null +++ b/src/timeline/mod.rs @@ -0,0 +1,2 @@ +pub mod statistics; +pub mod timeline; diff --git a/src/timeline/statistics.rs b/src/timeline/statistics.rs new file mode 100644 index 00000000..f0cfe1c1 --- /dev/null +++ b/src/timeline/statistics.rs @@ -0,0 +1,36 @@ +use crate::detections::{configs, detection::EvtxRecordInfo}; + +#[derive(Debug)] +pub struct EventStatistics {} +/** +* Windows Event Logの統計情報を出力する +*/ +impl EventStatistics { + pub fn new() -> EventStatistics { + return EventStatistics {}; + } + + // この関数の戻り値として、コンソールに出力する内容をStringの可変配列(Vec)として返却してください。 + // 可変配列にしているのは改行を表すためで、可変配列にコンソールに出力する内容を1行ずつ追加してください。 + // 引数の_recordsが読み込んだWindowsイベントログのを表す、EvtxRecordInfo構造体の配列になっています。 + // EvtxRecordInfo構造体の pub record: Value というメンバーがいて、それがWindowsイベントログの1レコード分を表していますので、 + // EvtxRecordInfo構造体のrecordから、EventIDとか統計情報を取得するようにしてください。 + // recordからEventIDを取得するには、detection::utils::get_event_value()という関数があるので、それを使うと便利かもしれません。 + + // 現状では、この関数の戻り値として返すVecを表示するコードは実装していません。 + pub fn start(&mut self, _records: &Vec) -> Vec { + // 引数でstatisticsオプションが指定されている時だけ、統計情報を出力する。 + if !configs::CONFIG + .read() + .unwrap() + .args + .is_present("statistics") + { + return vec![]; + } + + // TODO ここから下を書いて欲しいです。 + + return vec![]; + } +} diff --git a/src/timeline/timeline.rs b/src/timeline/timeline.rs new file mode 100644 index 00000000..1ac008d6 --- /dev/null +++ b/src/timeline/timeline.rs @@ -0,0 +1,17 @@ +use crate::detections::detection::EvtxRecordInfo; + +use super::statistics::EventStatistics; + +#[derive(Debug)] +pub struct Timeline {} + +impl Timeline { + pub fn new() -> Timeline { + return Timeline {}; + } + + pub fn start(&mut self, records: &Vec) { + let mut statistic = EventStatistics::new(); + statistic.start(records); + } +}