From b183e615962d8f5c2faa6c07bf98b2ef35613036 Mon Sep 17 00:00:00 2001 From: akiranishikawa Date: Fri, 20 Nov 2020 16:32:40 +0900 Subject: [PATCH 01/16] add regexes and whitelist functions --- src/detections/system.rs | 6 +-- src/detections/utils.rs | 111 +++++++++++++++++++++++++++++++++------ 2 files changed, 99 insertions(+), 18 deletions(-) diff --git a/src/detections/system.rs b/src/detections/system.rs index 14619d60..68ab263c 100644 --- a/src/detections/system.rs +++ b/src/detections/system.rs @@ -30,7 +30,7 @@ impl System { let default = String::from(""); let servicename = &event_data.get("ServiceName").unwrap_or(&default); let commandline = &event_data.get("ImagePath").unwrap_or(&default); - let text = utils::check_regex(&servicename, 1); + let text = utils::check_regex_old(&servicename, 1); if !text.is_empty() { println!("Message : New Service Created"); println!("Command : {}", commandline); @@ -56,7 +56,7 @@ impl System { println!("Message : Interactive service warning"); println!("Results : Service name: {}", servicename); println!("Results : Malware (and some third party software) trigger this warning"); - println!("{}", utils::check_regex(&servicename, 1)); + println!("{}", utils::check_regex_old(&servicename, 1)); } fn suspicious_service_name(&mut self, event_id: &String, event_data: &HashMap) { @@ -66,7 +66,7 @@ impl System { let default = String::from(""); let servicename = &event_data.get("param1").unwrap_or(&default); - let text = utils::check_regex(&servicename, 1); + let text = utils::check_regex_old(&servicename, 1); if !text.is_empty() { println!("Message : Suspicious Service Name"); println!("Results : Service name: {}", servicename); diff --git a/src/detections/utils.rs b/src/detections/utils.rs index 2e7026e0..728d3199 100644 --- a/src/detections/utils.rs +++ b/src/detections/utils.rs @@ -5,6 +5,7 @@ extern crate regex; use crate::detections::configs; use flate2::read::GzDecoder; use regex::Regex; +use std::fs::File; use std::io::prelude::*; use std::str; use std::string::String; @@ -20,17 +21,9 @@ pub fn check_command( let mut text = "".to_string(); let mut base64 = "".to_string(); - let empty = "".to_string(); - for line in configs::singleton().whitelist { - let r_str = line.get(0).unwrap_or(&empty); - if r_str.is_empty() { - continue; - } - - let r = Regex::new(r_str); - if r.is_ok() && r.unwrap().is_match(commandline) { - return; - } + let ret = check_whitelist(commandline, "whitelist.txt"); + if (ret) { + return; } if commandline.len() > minlength { @@ -39,7 +32,7 @@ pub fn check_command( text.push_str("bytes\n"); } text.push_str(&check_obfu(commandline)); - text.push_str(&check_regex(commandline, 0)); + text.push_str(&check_regex_old(commandline, 0)); text.push_str(&check_creator(commandline, creator)); if Regex::new(r"\-enc.*[A-Za-z0-9/+=]{100}") .unwrap() @@ -71,7 +64,10 @@ pub fn check_command( println!("Decoded : {}", str::from_utf8(decoded.as_slice()).unwrap()); text.push_str("Base64-encoded function\n"); text.push_str(&check_obfu(str::from_utf8(decoded.as_slice()).unwrap())); - text.push_str(&check_regex(str::from_utf8(decoded.as_slice()).unwrap(), 0)); + text.push_str(&check_regex_old( + str::from_utf8(decoded.as_slice()).unwrap(), + 0, + )); } } } @@ -126,7 +122,7 @@ fn check_obfu(string: &str) -> std::string::String { return obfutext; } -pub fn check_regex(string: &str, r#type: usize) -> std::string::String { +pub fn check_regex_old(string: &str, r#type: usize) -> std::string::String { let empty = "".to_string(); let mut regextext = "".to_string(); for line in configs::singleton().regex { @@ -157,6 +153,79 @@ pub fn check_regex(string: &str, r#type: usize) -> std::string::String { return regextext; } +pub fn check_regex(string: &str, r#type: usize, regex_path: &str) -> std::string::String { + let empty = "".to_string(); + let mut regextext = "".to_string(); + let regex_list = read_csv(regex_path); + for line in regex_list { + let type_str = line.get(0).unwrap_or(&empty); + if type_str != &r#type.to_string() { + continue; + } + + let regex_str = line.get(1).unwrap_or(&empty); + if regex_str.is_empty() { + continue; + } + + let re = Regex::new(regex_str); + if re.is_err() || re.unwrap().is_match(string) == false { + continue; + } + + let text = line.get(2).unwrap_or(&empty); + if text.is_empty() { + continue; + } + + regextext.push_str(text); + regextext.push_str("\n"); + } + + return regextext; +} + +pub fn check_whitelist(target: &str, whitelist_path: &str) -> bool { + let empty = "".to_string(); + let whitelist = read_csv(whitelist_path); + for line in whitelist { + let r_str = line.get(0).unwrap_or(&empty); + if r_str.is_empty() { + continue; + } + + let r = Regex::new(r_str); + if r.is_ok() && r.unwrap().is_match(target) { + return true; + } + } + + return false; +} + +fn read_csv(filename: &str) -> Vec> { + let mut f = File::open(filename).expect("file not found!!!"); + let mut contents: String = String::new(); + let mut ret = vec![]; + if f.read_to_string(&mut contents).is_err() { + return ret; + } + + let mut rdr = csv::Reader::from_reader(contents.as_bytes()); + rdr.records().for_each(|r| { + if r.is_err() { + return; + } + + let line = r.unwrap(); + let mut v = vec![]; + line.iter().for_each(|s| v.push(s.to_string())); + ret.push(v); + }); + + return ret; +} + fn check_creator(command: &str, creator: &str) -> std::string::String { let mut creatortext = "".to_string(); if !creator.is_empty() { @@ -180,8 +249,11 @@ mod tests { use crate::detections::utils; #[test] fn test_check_regex() { - let regextext = utils::check_regex("\\cvtres.exe", 0); + let regextext = utils::check_regex("\\cvtres.exe", 0, "regexes.txt"); assert!(regextext == "Resource File To COFF Object Conversion Utility cvtres.exe\n"); + + let regextext = utils::check_regex("\\hogehoge.exe", 0, "regexes.txt"); + assert!(regextext == ""); } #[test] @@ -212,4 +284,13 @@ mod tests { "dir", ); } + + #[test] + fn test_check_whitelist() { + let commandline = "\"C:\\Program Files\\Google\\Update\\GoogleUpdate.exe\""; + assert!(true == utils::check_whitelist(commandline, "whitelist.txt")); + + let commandline = "\"C:\\Program Files\\Google\\Update\\GoogleUpdate2.exe\""; + assert!(false == utils::check_whitelist(commandline, "whitelist.txt")); + } } From 1abdbafb5a32c2e7d565a314277465a53cdc9a7e Mon Sep 17 00:00:00 2001 From: ichiichi11 Date: Sat, 21 Nov 2020 15:04:28 +0900 Subject: [PATCH 02/16] under constructing --- .gitignore | 3 +- Cargo.lock | 1 + Cargo.toml | 1 + config/eventkey_alias.txt | 12 ++ rules/deep_blue_cli/powershell/4103.yml | 20 ++ rules/deep_blue_cli/powershell/4104.yml | 19 ++ .../powershell/powershell4103.toml | 4 - .../powershell/powershell4104.toml | 4 - src/detections/configs.rs | 42 +++- src/detections/detection.rs | 112 +++++----- src/detections/mod.rs | 1 + src/detections/rule.rs | 204 ++++++++++++++++++ src/yaml.rs | 8 +- 13 files changed, 360 insertions(+), 71 deletions(-) create mode 100644 config/eventkey_alias.txt create mode 100644 rules/deep_blue_cli/powershell/4103.yml create mode 100644 rules/deep_blue_cli/powershell/4104.yml delete mode 100644 rules/deep_blue_cli/powershell/powershell4103.toml delete mode 100644 rules/deep_blue_cli/powershell/powershell4104.toml create mode 100644 src/detections/rule.rs diff --git a/.gitignore b/.gitignore index 5bfa00b5..5ad768f8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target /samples -*.test \ No newline at end of file +*.test +/.vscode/ \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 988e8b95..62bf1e6a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1192,6 +1192,7 @@ dependencies = [ "evtx", "flate2", "lazy_static", + "linked-hash-map", "quick-xml 0.17.2", "regex", "serde", diff --git a/Cargo.toml b/Cargo.toml index 51077334..6bd34971 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ flate2 = "1.0" lazy_static = "1.4.0" chrono = "0.4.19" yaml-rust = "0.4" +linked-hash-map = "0.5.3" [target.x86_64-pc-windows-gnu] linker = "x86_64-w64-mingw32-gcc" diff --git a/config/eventkey_alias.txt b/config/eventkey_alias.txt new file mode 100644 index 00000000..dda9ee24 --- /dev/null +++ b/config/eventkey_alias.txt @@ -0,0 +1,12 @@ +alias,event_key +EventID,Event.System.EventId +Channel,Event.System.Channel +CommandLine,Event.EventData.CommandLine +Signed,Event.EventData.Signed +ProcessName,Event.EventData.ProcessName +AccessMask,Event.EventData.AccessMask +TargetUserName,Event.EventData.TargetUserName +param1,Event.EventData.param1 +param2,Event.EventData.param2 +ServiceName,Event.EventData.ServiceName +ImagePath,Event.EventData.ImagePath \ No newline at end of file diff --git a/rules/deep_blue_cli/powershell/4103.yml b/rules/deep_blue_cli/powershell/4103.yml new file mode 100644 index 00000000..709e360a --- /dev/null +++ b/rules/deep_blue_cli/powershell/4103.yml @@ -0,0 +1,20 @@ +title: PowerShell Execution Pipeline +description: hogehoge +enabled: true +author: Yea +logsource: + product: windows +detection: + selection: + EventLog: PowerShell + EventID: 4103 + ContextInfo: + - Host Application + - ホスト アプリケーション + condition: selection +falsepositives: + - unknown +level: medium +output: 'command=%CommandLine%' +creation_date: 2020/11/8 +uodated_date: 2020/11/8 diff --git a/rules/deep_blue_cli/powershell/4104.yml b/rules/deep_blue_cli/powershell/4104.yml new file mode 100644 index 00000000..0b34b70f --- /dev/null +++ b/rules/deep_blue_cli/powershell/4104.yml @@ -0,0 +1,19 @@ +title: PowerShell Execution Remote Command +description: hogehoge +enabled: true +author: Yea +logsource: + product: windows +detection: + selection: + EventLog: PowerShell + EventID: 4104 + Path: '' + ScriptBlockText: '.' + condition: selection +falsepositives: + - unknown +level: medium +output: 'command=%CommandLine%' +creation_date: 2020/11/8 +uodated_date: 2020/11/8 diff --git a/rules/deep_blue_cli/powershell/powershell4103.toml b/rules/deep_blue_cli/powershell/powershell4103.toml deleted file mode 100644 index d70c52bf..00000000 --- a/rules/deep_blue_cli/powershell/powershell4103.toml +++ /dev/null @@ -1,4 +0,0 @@ -[rule] -severity = "high" -name = "4103" -message = "Execute Pipeline" \ No newline at end of file diff --git a/rules/deep_blue_cli/powershell/powershell4104.toml b/rules/deep_blue_cli/powershell/powershell4104.toml deleted file mode 100644 index a4262b86..00000000 --- a/rules/deep_blue_cli/powershell/powershell4104.toml +++ /dev/null @@ -1,4 +0,0 @@ -[rule] -severity = "high" -name = "4104" -message = "Excute Remote Command" \ No newline at end of file diff --git a/src/detections/configs.rs b/src/detections/configs.rs index 763f50f1..a3d97eb1 100644 --- a/src/detections/configs.rs +++ b/src/detections/configs.rs @@ -2,12 +2,12 @@ use clap::{App, AppSettings, Arg, ArgMatches}; use std::fs::File; use std::io::prelude::*; use std::sync::Once; +use std::collections::HashMap; #[derive(Clone)] pub struct SingletonReader { - pub regex: Vec>, - pub whitelist: Vec>, pub args: ArgMatches<'static>, + pub event_key_alias_config: EventKeyAliasConfig, } pub fn singleton() -> Box { @@ -17,9 +17,8 @@ pub fn singleton() -> Box { unsafe { ONCE.call_once(|| { let singleton = SingletonReader { - regex: read_csv("regexes.txt"), - whitelist: read_csv("whitelist.txt"), args: build_app().get_matches(), + event_key_alias_config: load_eventkey_alias(), }; SINGLETON = Some(Box::new(singleton)); @@ -56,6 +55,41 @@ fn build_app() -> clap::App<'static, 'static> { .arg(Arg::from_usage("--credits 'Zachary Mathis, Akira Nishikawa'")) } +#[derive(Clone)] +pub struct EventKeyAliasConfig { + key_to_eventkey: HashMap, +} + +impl EventKeyAliasConfig { + pub fn new() -> EventKeyAliasConfig { + return EventKeyAliasConfig{ key_to_eventkey: HashMap::new() }; + } + + pub fn get_event_key(&self, alias: String ) -> Option<&String> { + return self.key_to_eventkey.get(&alias); + } +} + +fn load_eventkey_alias() -> EventKeyAliasConfig { + let config = EventKeyAliasConfig::new(); + + read_csv("config/eventkey_alias.txt").into_iter().for_each( | line| { + if line.len() != 2 { + return; + } + + let alias = line[0]; + let event_key = line[1]; + if alias.len() == 0 || event_key.len() == 0 { + return; + } + + config.key_to_eventkey.insert(alias, event_key); + }); + + return config; +} + fn read_csv(filename: &str) -> Vec> { let mut f = File::open(filename).expect("file not found!!!"); let mut contents: String = String::new(); diff --git a/src/detections/detection.rs b/src/detections/detection.rs index 0c3ce7cc..45d7612a 100644 --- a/src/detections/detection.rs +++ b/src/detections/detection.rs @@ -1,74 +1,78 @@ extern crate csv; -extern crate quick_xml; +extern crate chrono; -use crate::detections::application; -use crate::detections::applocker; -use crate::detections::common; -use crate::detections::powershell; -use crate::detections::security; -use crate::detections::sysmon; -use crate::detections::system; -use crate::models::event; +use crate::detections::rule; +use crate::detections::rule::RuleNode; +use crate::detections::print::{Message}; +use crate::yaml::ParseYaml; + +use chrono::{TimeZone, Utc}; use evtx::EvtxParser; -use quick_xml::de::DeError; -use std::collections::BTreeMap; +use serde_json::{Error, Value}; + +const DIRPATH_RULES: &str = "rules"; #[derive(Debug)] pub struct Detection { - timeline_list: BTreeMap, + } impl Detection { pub fn new() -> Detection { Detection { - timeline_list: BTreeMap::new(), } } - pub fn start(&mut self, mut parser: EvtxParser) -> Result<(), DeError> { - let mut common: common::Common = common::Common::new(); - let mut security = security::Security::new(); - let mut system = system::System::new(); - let mut application = application::Application::new(); - let mut applocker = applocker::AppLocker::new(); - let mut sysmon = sysmon::Sysmon::new(); - let mut powershell = powershell::PowerShell::new(); - - for record in parser.records() { - match record { - Ok(r) => { - let event: event::Evtx = quick_xml::de::from_str(&r.data)?; - let event_id = event.system.event_id.to_string(); - let channel = event.system.channel.to_string(); - let event_data = event.parse_event_data(); - - &common.detection(&event.system, &event_data); - if channel == "Security" { - &security.detection(event_id, &event.system, &event.user_data, event_data); - } else if channel == "System" { - &system.detection(event_id, &event.system, event_data); - } else if channel == "Application" { - &application.detection(event_id, &event.system, event_data); - } else if channel == "Microsoft-Windows-PowerShell/Operational" { - &powershell.detection(event_id, &event.system, event_data); - } else if channel == "Microsoft-Windows-Sysmon/Operational" { - &sysmon.detection(event_id, &event.system, event_data); - } else if channel == "Microsoft-Windows-AppLocker/EXE and DLL" { - &applocker.detection(event_id, &event.system, event_data); - } else { - //&other.detection(); - } + pub fn start(&mut self, mut parser: EvtxParser) { + // from .etvx to json + let event_records: Vec = parser + .records_json() + .filter_map(|result_record| { + if result_record.is_err() { + eprintln!("{}", result_record.unwrap_err()); + return Option::None; } - Err(e) => eprintln!("{}", e), - } + + //// refer https://rust-lang-nursery.github.io/rust-cookbook/encoding/complex.html + let result_json: Result = + serde_json::from_str(&result_record.unwrap().data); + if result_json.is_err() { + eprintln!("{}", result_json.unwrap_err()); + return Option::None; + } + return result_json.ok(); + }) + .collect(); + + event_records.iter().for_each(|event_rec| { + println!("{}", event_rec["Event"]); + }); + + // load rule files + let mut rulefile_loader = ParseYaml::new(); + let resutl_readdir = rulefile_loader.read_dir(DIRPATH_RULES); + if resutl_readdir.is_err() { + eprintln!("{}", resutl_readdir.unwrap_err()); + return; } - //////////////////////////// - // 表示 - //////////////////////////// - common.disp(); - security.disp(); + // parse rule files + let rules: Vec = rulefile_loader + .files + .into_iter() + .map(|rule_file| rule::parse_rule(rule_file)) + .collect(); - return Ok(()); + // selection rule files and collect log + let mut message = Message::new(); + rules.iter().for_each(|rule| { + &event_records + .iter() + .filter(|event_record| rule.detection.select(event_record)) + .for_each(|event_record| message.insert(Utc.ymd(1996, 2, 27).and_hms(1, 5, 1), event_record.to_string())); + }); + + // output message + message.debug(); } } diff --git a/src/detections/mod.rs b/src/detections/mod.rs index 4aaee9ff..c11cdb13 100644 --- a/src/detections/mod.rs +++ b/src/detections/mod.rs @@ -5,6 +5,7 @@ pub mod configs; pub mod detection; mod powershell; pub mod print; +mod rule; mod security; mod sysmon; mod system; diff --git a/src/detections/rule.rs b/src/detections/rule.rs new file mode 100644 index 00000000..8d263ff3 --- /dev/null +++ b/src/detections/rule.rs @@ -0,0 +1,204 @@ +use serde_json::Value; +use yaml_rust::Yaml; +use crate::detections::configs; + +pub fn parse_rule(yaml: Yaml) -> RuleNode { + let selection = parse_selection(&yaml); + return RuleNode { + yaml: yaml, + detection: DetectionNode { + selection: selection, + }, + }; +} + +fn parse_selection(yaml: &Yaml) -> Option> { + let selection_yaml = &yaml["detection"]["selection"]; + return Option::Some(parse_selection_recursively(vec![], &selection_yaml)); +} + +fn parse_selection_recursively(mut key_list: Vec, yaml: &Yaml) -> Box { + if yaml.as_hash().is_some() { + 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() { + 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(FieldSelectionNode::new(key_list, yaml.clone())); + } +} + +/////////////// RuleNode +pub struct RuleNode { + pub yaml: Yaml, + pub detection: DetectionNode, +} + +//////////////// Detection Node +pub struct DetectionNode { + selection: Option>, +} + +impl DetectionNode { + pub fn select(&self, event_record: &Value) -> bool { + if self.selection.is_none() { + return false; + } + + return self.selection.as_ref().unwrap().select(event_record); + } +} + +//////////// Selection Node +trait SelectionNode { + fn select(&self, event_record: &Value) -> bool; +} + +///////////////// AndSelectionNode +struct AndSelectionNode { + pub child_nodes: Vec>, +} + +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.as_ref().select(event_record); + }); + } +} + +////////// OrSelectionNode +struct OrSelectionNode { + pub child_nodes: Vec>, +} + +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.as_ref().select(event_record); + }); + } +} + +////////////// Field Selection Node +struct FieldSelectionNode { + key_list: Vec, + select_value: Yaml, +} + +impl FieldSelectionNode { + fn new(key_list: Vec, value_yaml: Yaml) -> FieldSelectionNode { + return FieldSelectionNode { + key_list: key_list, + select_value: value_yaml, + }; + } + + // JSON形式のEventJSONから値を取得する関数 aliasも考慮されている。 + // TODO Messageを出力する際も利用するので、共通して使えるようにrefactoringする。 + fn get_event_value<'a>(&self, event_value: &'a Value) -> Option<&'a Value> { + if self.key_list.is_empty() { + return Option::None; + } + + let key: &str = &self.key_list[0]; + if key.len() == 0 { + return Option::None; + } + + let event_key = match configs::singleton().event_key_alias_config.get_event_key(key.to_string()) { + Some(alias_event_key) => { alias_event_key } + None => { key } + }; + + let mut ret: &Value = event_value; + for key in event_key.split(".") { + if ret.is_object() == false { + return Option::None; + } + ret = &ret[key]; + } + + return Option::Some(ret); + } + + // TODO Matcherのインスタンスが都度生成されないようにする。 + fn get_matchers(&self) -> Vec> { + return vec![Box::new(ValueMatcher {})]; + } +} + +impl SelectionNode for FieldSelectionNode { + fn select(&self, event_record: &Value) -> bool { + let matchers = self.get_matchers(); + let matcher = matchers + .into_iter() + .find(|matcher| matcher.is_target_key(&self.key_list)); + if matcher.is_none() { + return false; + } + + let event_value = self.get_event_value(event_record); + return matcher + .unwrap() + .is_match(&self.key_list, &self.select_value, event_value); + } +} + +trait FieldSelectionMatcher { + fn is_target_key(&self, key_list: &Vec) -> bool; + fn is_match( + &self, + key_list: &Vec, + select_value: &Yaml, + event_value: Option<&Value>, + ) -> bool; +} + +struct ValueMatcher {} + +impl FieldSelectionMatcher for ValueMatcher { + fn is_target_key(&self, key_list: &Vec) -> bool { + return key_list.is_empty(); + } + + fn is_match( + &self, + key_list: &Vec, + select_value: &Yaml, + event_value: Option<&Value>, + ) -> bool { + return true; + } +} \ No newline at end of file diff --git a/src/yaml.rs b/src/yaml.rs index 8636eb9a..781a5100 100644 --- a/src/yaml.rs +++ b/src/yaml.rs @@ -8,12 +8,12 @@ use std::path::{Path, PathBuf}; use yaml_rust::YamlLoader; pub struct ParseYaml { - pub rules: Vec, + pub files: Vec, } impl ParseYaml { pub fn new() -> ParseYaml { - ParseYaml { rules: Vec::new() } + ParseYaml { files: Vec::new() } } pub fn read_file(&self, path: PathBuf) -> Result { @@ -39,7 +39,7 @@ impl ParseYaml { let docs = YamlLoader::load_from_str(&s).unwrap(); for i in docs { if i["enabled"].as_bool().unwrap() { - &self.rules.push(i); + &self.files.push(i); } } } @@ -64,7 +64,7 @@ mod tests { fn test_read_yaml() { let mut yaml = yaml::ParseYaml::new(); &yaml.read_dir("test_files/rules/yaml/".to_string()); - for rule in yaml.rules { + for rule in yaml.files { if rule["title"].as_str().unwrap() == "Sysmon Check command lines" { assert_eq!( "*", From d976ddc4d09c2ce820b2b729bfd75379621023ec Mon Sep 17 00:00:00 2001 From: ichiichi11 Date: Sat, 21 Nov 2020 22:56:21 +0900 Subject: [PATCH 03/16] regex rule implemented --- config/eventkey_alias.txt | 7 +- rules/deep_blue_cli/powershell/4103.yml | 4 +- rules/deep_blue_cli/powershell/4104.yml | 8 +- rules/deep_blue_cli/security/1102.yml | 17 + rules/deep_blue_cli/security/4673.yml | 17 + rules/deep_blue_cli/security/4674.yml | 19 + rules/deep_blue_cli/security/4688.yml | 18 + rules/deep_blue_cli/security/4720.yml | 17 + rules/deep_blue_cli/security/4728.yml | 18 + rules/deep_blue_cli/security/4732.yml | 18 + rules/deep_blue_cli/security/4756.yml | 18 + rules/deep_blue_cli/security/_4625.yml | 17 + rules/deep_blue_cli/security/_4648.yml | 17 + rules/deep_blue_cli/security/_4672.yml | 19 + rules/deep_blue_cli/sysmon/1.yml | 19 + rules/deep_blue_cli/sysmon/7.yml | 18 + rules/deep_blue_cli/system/104.yml | 17 + rules/deep_blue_cli/system/7030.yml | 19 + rules/deep_blue_cli/system/7036.yml | 19 + rules/deep_blue_cli/system/7040.yml | 21 + rules/deep_blue_cli/system/7045.yml | 22 + src/detections/application.rs | 57 - src/detections/applocker.rs | 53 - src/detections/configs.rs | 43 +- src/detections/detection.rs | 47 +- src/detections/mod.rs | 6 - src/detections/powershell.rs | 61 - src/detections/rule.rs | 351 ++++-- src/detections/security.rs | 1348 ----------------------- src/detections/sysmon.rs | 59 - src/detections/system.rs | 110 -- 31 files changed, 675 insertions(+), 1809 deletions(-) create mode 100644 rules/deep_blue_cli/security/1102.yml create mode 100644 rules/deep_blue_cli/security/4673.yml create mode 100644 rules/deep_blue_cli/security/4674.yml create mode 100644 rules/deep_blue_cli/security/4688.yml create mode 100644 rules/deep_blue_cli/security/4720.yml create mode 100644 rules/deep_blue_cli/security/4728.yml create mode 100644 rules/deep_blue_cli/security/4732.yml create mode 100644 rules/deep_blue_cli/security/4756.yml create mode 100644 rules/deep_blue_cli/security/_4625.yml create mode 100644 rules/deep_blue_cli/security/_4648.yml create mode 100644 rules/deep_blue_cli/security/_4672.yml create mode 100644 rules/deep_blue_cli/sysmon/1.yml create mode 100644 rules/deep_blue_cli/sysmon/7.yml create mode 100644 rules/deep_blue_cli/system/104.yml create mode 100644 rules/deep_blue_cli/system/7030.yml create mode 100644 rules/deep_blue_cli/system/7036.yml create mode 100644 rules/deep_blue_cli/system/7040.yml create mode 100644 rules/deep_blue_cli/system/7045.yml delete mode 100644 src/detections/application.rs delete mode 100644 src/detections/applocker.rs delete mode 100644 src/detections/powershell.rs delete mode 100644 src/detections/security.rs delete mode 100644 src/detections/sysmon.rs delete mode 100644 src/detections/system.rs diff --git a/config/eventkey_alias.txt b/config/eventkey_alias.txt index dda9ee24..be9beeac 100644 --- a/config/eventkey_alias.txt +++ b/config/eventkey_alias.txt @@ -1,5 +1,5 @@ alias,event_key -EventID,Event.System.EventId +EventID,Event.System.EventID Channel,Event.System.Channel CommandLine,Event.EventData.CommandLine Signed,Event.EventData.Signed @@ -9,4 +9,7 @@ TargetUserName,Event.EventData.TargetUserName param1,Event.EventData.param1 param2,Event.EventData.param2 ServiceName,Event.EventData.ServiceName -ImagePath,Event.EventData.ImagePath \ No newline at end of file +ImagePath,Event.EventData.ImagePath +ContextInfo,Event.EventData.ContextInfo +Path,Event.EventData.Path +ScriptBlockText,Event.EventData.ScriptBlockText#Name \ No newline at end of file diff --git a/rules/deep_blue_cli/powershell/4103.yml b/rules/deep_blue_cli/powershell/4103.yml index 709e360a..8bcefcff 100644 --- a/rules/deep_blue_cli/powershell/4103.yml +++ b/rules/deep_blue_cli/powershell/4103.yml @@ -6,12 +6,12 @@ logsource: product: windows detection: selection: - EventLog: PowerShell + Channel: PowerShell EventID: 4103 ContextInfo: - Host Application - ホスト アプリケーション - condition: selection + # condition: selection falsepositives: - unknown level: medium diff --git a/rules/deep_blue_cli/powershell/4104.yml b/rules/deep_blue_cli/powershell/4104.yml index 0b34b70f..d9d3a8b4 100644 --- a/rules/deep_blue_cli/powershell/4104.yml +++ b/rules/deep_blue_cli/powershell/4104.yml @@ -6,11 +6,11 @@ logsource: product: windows detection: selection: - EventLog: PowerShell + Channel: PowerShell EventID: 4104 - Path: '' - ScriptBlockText: '.' - condition: selection + Path: null + ScriptBlockText: null + # condition: selection falsepositives: - unknown level: medium diff --git a/rules/deep_blue_cli/security/1102.yml b/rules/deep_blue_cli/security/1102.yml new file mode 100644 index 00000000..b7281755 --- /dev/null +++ b/rules/deep_blue_cli/security/1102.yml @@ -0,0 +1,17 @@ +title: The Audit log file was cleared +description: hogehoge +enabled: true +author: Yea +logsource: + product: windows +detection: + selection: + Channel: Security + EventID: 1102 + # condition: selection +falsepositives: + - unknown +level: medium +output: 'Audit Log Clear¥n The Audit log was cleared.¥m%user_data.log_file_cleared%%user_data.subject_user_name%' +creation_date: 2020/11/8 +uodated_date: 2020/11/8 diff --git a/rules/deep_blue_cli/security/4673.yml b/rules/deep_blue_cli/security/4673.yml new file mode 100644 index 00000000..35d813bc --- /dev/null +++ b/rules/deep_blue_cli/security/4673.yml @@ -0,0 +1,17 @@ +title: Sensitive Privilede Use (Mimikatz) +description: hogehoge +enabled: true +author: Yea +logsource: + product: windows +detection: + selection: + Channel: Security + EventID: 4673 + # condition: selection | count(EventID) > 4 +falsepositives: + - unknown +level: medium +output: 'Sensitive Privilege Use Exceeds Threshold¥n Potentially indicative of Mimikatz, multiple sensitive priviledge calls have been made.¥nUserName:SubjectUserName% Domain Name:%DomainName%' +creation_date: 2020/11/8 +uodated_date: 2020/11/8 diff --git a/rules/deep_blue_cli/security/4674.yml b/rules/deep_blue_cli/security/4674.yml new file mode 100644 index 00000000..eb169a0d --- /dev/null +++ b/rules/deep_blue_cli/security/4674.yml @@ -0,0 +1,19 @@ +title: An Operation was attempted on a privileged object +description: hogehoge +enabled: true +author: Yea +logsource: + product: windows +detection: + selection: + Channel: Security + EventID: 4674 + ProcessName: '(?i)C:\WINDOWS\SYSTEM32\SERVICE.EXE' # (?i) means case insesitive for Rust Regex + AccessMask: '%%1539' + # condition: selection +falsepositives: + - unknown +level: medium +output: 'Possible Hidden Service Attempt¥nUser requested to modify the Dynamic Access Control (DAC) permissions of a service, possibly to hide it from view.¥nUser: %SubjectUserName%¥nTarget service:%ObjectName¥nDesired Access:WRITE_DAC' +creation_date: 2020/11/8 +uodated_date: 2020/11/8 diff --git a/rules/deep_blue_cli/security/4688.yml b/rules/deep_blue_cli/security/4688.yml new file mode 100644 index 00000000..7a4d6f3a --- /dev/null +++ b/rules/deep_blue_cli/security/4688.yml @@ -0,0 +1,18 @@ +title: Command Line Logging +description: hogehoge +enabled: true +author: Yea +logsource: + product: windows +detection: + selection: + Channel: Security + EventID: 4688 + CommandLine: '.+' + # condition: selection +falsepositives: + - unknown +level: medium +output: 'CommandLine:%CommandLine% ParentProcessName:%ParentProcessName%' +creation_date: 2020/11/8 +uodated_date: 2020/11/8 diff --git a/rules/deep_blue_cli/security/4720.yml b/rules/deep_blue_cli/security/4720.yml new file mode 100644 index 00000000..e35d5c05 --- /dev/null +++ b/rules/deep_blue_cli/security/4720.yml @@ -0,0 +1,17 @@ +title: A user account was created. +description: hogehoge +enabled: true +author: Yea +logsource: + product: windows +detection: + selection: + Channel: Security + EventID: 4720 + # condition: selection +falsepositives: + - unknown +level: low +output: 'New User Created UserName:%TargetUserName% SID:%TargetSid%' +creation_date: 2020/11/8 +uodated_date: 2020/11/8 diff --git a/rules/deep_blue_cli/security/4728.yml b/rules/deep_blue_cli/security/4728.yml new file mode 100644 index 00000000..47ed1866 --- /dev/null +++ b/rules/deep_blue_cli/security/4728.yml @@ -0,0 +1,18 @@ +title: A member was added to a security-enabled global group. +description: hogehoge +enabled: true +author: Yea +logsource: + product: windows +detection: + selection: + Channel: Security + EventID: 4728 + TargetUserName: Administrators + # condition: selection +falsepositives: + - unknown +level: low +output: 'user added to global Administrators UserName: %MemberName% SID: %MemberSid%' +creation_date: 2020/11/8 +uodated_date: 2020/11/8 diff --git a/rules/deep_blue_cli/security/4732.yml b/rules/deep_blue_cli/security/4732.yml new file mode 100644 index 00000000..05daa583 --- /dev/null +++ b/rules/deep_blue_cli/security/4732.yml @@ -0,0 +1,18 @@ +title: A member was added to a security-enabled local group. +description: hogehoge +enabled: true +author: Yea +logsource: + product: windows +detection: + selection: + Channel: Security + EventID: 4732 + TargetUserName: Administrators + # condition: selection +falsepositives: + - unknown +level: low +output: 'user added to local Administrators UserName: %MemberName% SID: %MemberSid%' +creation_date: 2020/11/8 +uodated_date: 2020/11/8 diff --git a/rules/deep_blue_cli/security/4756.yml b/rules/deep_blue_cli/security/4756.yml new file mode 100644 index 00000000..c7af8718 --- /dev/null +++ b/rules/deep_blue_cli/security/4756.yml @@ -0,0 +1,18 @@ +title: A member was added to a security-enabled universal group. +description: hogehoge +enabled: true +author: Yea +logsource: + product: windows +detection: + selection: + Channel: Security + EventID: 4756 + TargetUserName: Administrators + # condition: selection +falsepositives: + - unknown +level: low +output: 'user added to universal Administrators UserName: %MemberName% SID: %MemberSid%' +creation_date: 2020/11/8 +uodated_date: 2020/11/8 diff --git a/rules/deep_blue_cli/security/_4625.yml b/rules/deep_blue_cli/security/_4625.yml new file mode 100644 index 00000000..0fbabcd4 --- /dev/null +++ b/rules/deep_blue_cli/security/_4625.yml @@ -0,0 +1,17 @@ +title: An account failed to log on +description: hogehoge +enabled: false +author: Yea +logsource: + product: windows +detection: + selection: + Channel: Security + EventID: 4625 + # condition: selection | count(TargetUserName) > 3 +falsepositives: + - unknown +level: medium +output: 'High number of logon failures for one account UserName:%event_data.SubjectUserName% Total logon faiures:%count%' +creation_date: 2020/11/8 +uodated_date: 2020/11/8 diff --git a/rules/deep_blue_cli/security/_4648.yml b/rules/deep_blue_cli/security/_4648.yml new file mode 100644 index 00000000..4b9735c6 --- /dev/null +++ b/rules/deep_blue_cli/security/_4648.yml @@ -0,0 +1,17 @@ +title: An account failed to log on +description: hogehoge +enabled: false +author: Yea +logsource: + product: windows +detection: + selection: + Channel: Security + EventID: 4648 + # condition: selection | count(TargetUserName) > 3 +falsepositives: + - unknown +level: High +output: 'Distributed Account Explicit Credential Use (Password Spray Attack)¥n The use of multiple user account access attempts with explicit credentials is ¥nan indicator of a password spray attack.¥nTarget Usernames:%TargetUserName$¥nAccessing Username: %SubjectUserName%¥nAccessing Host Name: %SubjectDomainName%' +creation_date: 2020/11/8 +uodated_date: 2020/11/8 diff --git a/rules/deep_blue_cli/security/_4672.yml b/rules/deep_blue_cli/security/_4672.yml new file mode 100644 index 00000000..b1763b97 --- /dev/null +++ b/rules/deep_blue_cli/security/_4672.yml @@ -0,0 +1,19 @@ +title: Command Line Logging +description: hogehoge +enabled: false +author: Yea +logsource: + product: windows +detection: + selection: + Channel: Security + EventID: 4672 + PrivilegeList: + contain: SeDebugPrivilege + # condition: selection +falsepositives: + - unknown +level: medium +output: 'CommandLine:%CommandLine% ParentProcessName:%ParentProcessName%' +creation_date: 2020/11/8 +uodated_date: 2020/11/8 diff --git a/rules/deep_blue_cli/sysmon/1.yml b/rules/deep_blue_cli/sysmon/1.yml new file mode 100644 index 00000000..3f2b36c6 --- /dev/null +++ b/rules/deep_blue_cli/sysmon/1.yml @@ -0,0 +1,19 @@ +title: Sysmon Check command lines +description: hogehoge +enabled: true +author: Yea +logsource: + product: windows +detection: + selection: + Channel: Sysmon + EventID: 1 + CommandLine: '.+' + # condition: selection +falsepositives: + - unknown +level: medium +output: 'CommandLine=%CommandLine%¥nParentImage=%ParentImage%' +creation_date: 2020/11/8 +uodated_date: 2020/11/8 + diff --git a/rules/deep_blue_cli/sysmon/7.yml b/rules/deep_blue_cli/sysmon/7.yml new file mode 100644 index 00000000..4f321af4 --- /dev/null +++ b/rules/deep_blue_cli/sysmon/7.yml @@ -0,0 +1,18 @@ +title: Check for unsigned EXEs/DLLs +description: hogehoge +enabled: true +author: Yea +logsource: + product: windows +detection: + selection: + Channel: Sysmon + EventID: 7 + Signed: "false" # Compare by string + # condition: selection +falsepositives: + - unknown +level: low +output: 'Message: Unsigned Image(DLL)¥n Result : Loaded by: %event_data.Image%¥nCommand : %event_data.ImageLoaded%' +creation_date: 2020/11/8 +uodated_date: 2020/11/8 diff --git a/rules/deep_blue_cli/system/104.yml b/rules/deep_blue_cli/system/104.yml new file mode 100644 index 00000000..19301965 --- /dev/null +++ b/rules/deep_blue_cli/system/104.yml @@ -0,0 +1,17 @@ +title: The System log file was cleared +description: hogehoge +enabled: true +author: Yea +logsource: + product: windows +detection: + selection: + Channel: System + EventID: 104 + # condition: selection +falsepositives: + - unknown +level: medium +output: 'System Log Clear¥nThe System log was cleared.' +creation_date: 2020/11/8 +uodated_date: 2020/11/8 diff --git a/rules/deep_blue_cli/system/7030.yml b/rules/deep_blue_cli/system/7030.yml new file mode 100644 index 00000000..5e168427 --- /dev/null +++ b/rules/deep_blue_cli/system/7030.yml @@ -0,0 +1,19 @@ +title: This service may not function properly +description: hogehoge +enabled: true +author: Yea +logsource: + product: windows +detection: + selection: + Channel: System + EventID: 7030 + param1: + regexes: ./regexes.txt + # condition: selection +falsepositives: + - unknown +level: low +output: 'Interactive service warning¥nService name: %ServiceName%¥nMalware (and some third party software) trigger this warning' +creation_date: 2020/11/8 +uodated_date: 2020/11/8 diff --git a/rules/deep_blue_cli/system/7036.yml b/rules/deep_blue_cli/system/7036.yml new file mode 100644 index 00000000..f17ee1ad --- /dev/null +++ b/rules/deep_blue_cli/system/7036.yml @@ -0,0 +1,19 @@ +title: The ... service entered the stopped|running state +description: hogehoge +enabled: true +author: Yea +logsource: + product: windows +detection: + selection: + Channel: System + EventID: 7036 + param1: + regexes: ./regexes.txt + condition: selection +falsepositives: + - unknown +level: low +output: 'Suspicious Service Name¥nService name: %ServiceName%' +creation_date: 2020/11/8 +uodated_date: 2020/11/8 diff --git a/rules/deep_blue_cli/system/7040.yml b/rules/deep_blue_cli/system/7040.yml new file mode 100644 index 00000000..7ff699f2 --- /dev/null +++ b/rules/deep_blue_cli/system/7040.yml @@ -0,0 +1,21 @@ +title: The start type of the Windows Event Log service was changed from auto start to disabled +description: hogehoge +enabled: true +author: Yea +logsource: + product: windows +detection: + selection: + Channel: System + EventID: 7040 + param1: 'Windows Event Log' + param2: + - "disabled" + - "auto start" + condition: selection +falsepositives: + - unknown +level: low +output: 'Service name : %param1%¥nMessage : Event Log Service Stopped¥nResults: Selective event log manipulation may follow this event.' +creation_date: 2020/11/8 +uodated_date: 2020/11/8 diff --git a/rules/deep_blue_cli/system/7045.yml b/rules/deep_blue_cli/system/7045.yml new file mode 100644 index 00000000..a733cf54 --- /dev/null +++ b/rules/deep_blue_cli/system/7045.yml @@ -0,0 +1,22 @@ +title: A service was installed in the system +description: hogehoge +enabled: true +author: Yea +logsource: + product: windows +detection: + selection: + Channel: System + EventID: 7045 + ServiceName: + regexes: ./regexes.txt + ImagePath: + min_length: 1000 + whitelist: ./whitelist.txt + condition: selection +falsepositives: + - unknown +level: low +output: 'New Service Created¥n%ImagePath¥nService name: %ServiceName%' +creation_date: 2020/11/8 +uodated_date: 2020/11/8 diff --git a/src/detections/application.rs b/src/detections/application.rs deleted file mode 100644 index 624c5997..00000000 --- a/src/detections/application.rs +++ /dev/null @@ -1,57 +0,0 @@ -extern crate regex; - -use crate::models::event; -use regex::Regex; -use std::collections::HashMap; - -pub struct Application {} - -impl Application { - pub fn new() -> Application { - Application {} - } - - pub fn detection( - &mut self, - event_id: String, - system: &event::System, - event_data: HashMap, - ) { - self.emet(&event_id, system); - } - - fn emet(&mut self, event_id: &String, system: &event::System) { - if event_id != "2" { - return; - } - - match &system.provider.name { - Some(name) => { - if name != "EMET" { - return; - } - } - None => return, - } - match &system.message { - Some(message) => { - let message_split: Vec<&str> = message.split("\n").collect(); - if !message_split.is_empty() && message_split.len() >= 5 { - let text = message_split[0]; - let application = message_split[3]; - let re = Regex::new(r"^Application: ").unwrap(); - let command = re.replace_all(application, ""); - let username = message_split[4]; - - println!("Message EMET Block"); - println!("Command : {}", command); - println!("Results : {}", text); - println!("Results : {}", username); - } - } - None => { - println!("Warning: EMET Message field is blank. Install EMET locally to see full details of this alert"); - } - } - } -} diff --git a/src/detections/applocker.rs b/src/detections/applocker.rs deleted file mode 100644 index 2862bc3c..00000000 --- a/src/detections/applocker.rs +++ /dev/null @@ -1,53 +0,0 @@ -extern crate regex; - -use crate::models::event; -use regex::Regex; -use std::collections::HashMap; - -pub struct AppLocker {} - -impl AppLocker { - pub fn new() -> AppLocker { - AppLocker {} - } - - pub fn detection( - &mut self, - event_id: String, - _system: &event::System, - _event_data: HashMap, - ) { - self.applocker_log_warning(&event_id, &_system); - self.applocker_log_block(&event_id, &_system); - } - - fn applocker_log_warning(&mut self, event_id: &String, system: &event::System) { - if event_id != "8003" { - return; - } - - let re = Regex::new(r" was .*$").unwrap(); - let default = "".to_string(); - let message = &system.message.as_ref().unwrap_or(&default); - let command = re.replace_all(&message, ""); - - println!("Message Applocker Warning"); - println!("Command : {}", command); - println!("Results : {}", message); - } - - fn applocker_log_block(&mut self, event_id: &String, system: &event::System) { - if event_id != "8004" { - return; - } - - let re = Regex::new(r" was .*$").unwrap(); - let default = "".to_string(); - let message = &system.message.as_ref().unwrap_or(&default); - let command = re.replace_all(&message, ""); - - println!("Message Applocker Block"); - println!("Command : {}", command); - println!("Results : {}", message); - } -} diff --git a/src/detections/configs.rs b/src/detections/configs.rs index a3d97eb1..d0a8ef83 100644 --- a/src/detections/configs.rs +++ b/src/detections/configs.rs @@ -1,11 +1,13 @@ use clap::{App, AppSettings, Arg, ArgMatches}; +use std::collections::HashMap; use std::fs::File; use std::io::prelude::*; use std::sync::Once; -use std::collections::HashMap; #[derive(Clone)] pub struct SingletonReader { + pub regex: Vec>, + pub whitelist: Vec>, pub args: ArgMatches<'static>, pub event_key_alias_config: EventKeyAliasConfig, } @@ -17,6 +19,8 @@ pub fn singleton() -> Box { unsafe { ONCE.call_once(|| { let singleton = SingletonReader { + regex: read_csv("regexes.txt"), + whitelist: read_csv("whitelist.txt"), args: build_app().get_matches(), event_key_alias_config: load_eventkey_alias(), }; @@ -57,35 +61,42 @@ fn build_app() -> clap::App<'static, 'static> { #[derive(Clone)] pub struct EventKeyAliasConfig { - key_to_eventkey: HashMap, + key_to_eventkey: HashMap, } impl EventKeyAliasConfig { pub fn new() -> EventKeyAliasConfig { - return EventKeyAliasConfig{ key_to_eventkey: HashMap::new() }; + return EventKeyAliasConfig { + key_to_eventkey: HashMap::new(), + }; } - pub fn get_event_key(&self, alias: String ) -> Option<&String> { + pub fn get_event_key(&self, alias: String) -> Option<&String> { return self.key_to_eventkey.get(&alias); } } fn load_eventkey_alias() -> EventKeyAliasConfig { - let config = EventKeyAliasConfig::new(); + let mut config = EventKeyAliasConfig::new(); - read_csv("config/eventkey_alias.txt").into_iter().for_each( | line| { - if line.len() != 2 { - return; - } + read_csv("config/eventkey_alias.txt") + .into_iter() + .for_each(|line| { + if line.len() != 2 { + return; + } - let alias = line[0]; - let event_key = line[1]; - if alias.len() == 0 || event_key.len() == 0 { - return; - } + let empty = &"".to_string(); + let alias = line.get(0).unwrap_or(empty); + let event_key = line.get(1).unwrap_or(empty); + if alias.len() == 0 || event_key.len() == 0 { + return; + } - config.key_to_eventkey.insert(alias, event_key); - }); + config + .key_to_eventkey + .insert(alias.to_owned(), event_key.to_owned()); + }); return config; } diff --git a/src/detections/detection.rs b/src/detections/detection.rs index 45d7612a..b3c6c3bd 100644 --- a/src/detections/detection.rs +++ b/src/detections/detection.rs @@ -1,9 +1,9 @@ -extern crate csv; extern crate chrono; +extern crate csv; +use crate::detections::print::Message; use crate::detections::rule; use crate::detections::rule::RuleNode; -use crate::detections::print::{Message}; use crate::yaml::ParseYaml; use chrono::{TimeZone, Utc}; @@ -13,18 +13,15 @@ use serde_json::{Error, Value}; const DIRPATH_RULES: &str = "rules"; #[derive(Debug)] -pub struct Detection { - -} +pub struct Detection {} impl Detection { pub fn new() -> Detection { - Detection { - } + Detection {} } pub fn start(&mut self, mut parser: EvtxParser) { - // from .etvx to json + // serialize from .etvx to jsons let event_records: Vec = parser .records_json() .filter_map(|result_record| { @@ -44,10 +41,6 @@ impl Detection { }) .collect(); - event_records.iter().for_each(|event_rec| { - println!("{}", event_rec["Event"]); - }); - // load rule files let mut rulefile_loader = ParseYaml::new(); let resutl_readdir = rulefile_loader.read_dir(DIRPATH_RULES); @@ -57,19 +50,39 @@ impl Detection { } // parse rule files - let rules: Vec = rulefile_loader + let selection_rules: Vec = rulefile_loader .files .into_iter() .map(|rule_file| rule::parse_rule(rule_file)) + .filter_map(|mut rule| { + return rule + .init() + .or_else(|err_msgs| { + print!( + "Failed to parse Rule file. See following detail. [rule file title:{}]", + rule.yaml["title"].as_str().unwrap_or("") + ); + err_msgs.iter().for_each(|err_msg| println!("{}", err_msg)); + println!("\n"); + return Result::Err(err_msgs); + }) + .and_then(|_empty| Result::Ok(rule)) + .ok(); + }) .collect(); - // selection rule files and collect log + // selection rule files and collect message let mut message = Message::new(); - rules.iter().for_each(|rule| { + selection_rules.iter().for_each(|rule| { &event_records .iter() - .filter(|event_record| rule.detection.select(event_record)) - .for_each(|event_record| message.insert(Utc.ymd(1996, 2, 27).and_hms(1, 5, 1), event_record.to_string())); + .filter(|event_record| rule.select(event_record)) + .for_each(|event_record| { + message.insert( + Utc.ymd(1996, 2, 27).and_hms(1, 5, 1), + event_record.to_string(), + ) + }); }); // output message diff --git a/src/detections/mod.rs b/src/detections/mod.rs index c11cdb13..2c864e71 100644 --- a/src/detections/mod.rs +++ b/src/detections/mod.rs @@ -1,12 +1,6 @@ -mod application; -mod applocker; mod common; pub mod configs; pub mod detection; -mod powershell; pub mod print; mod rule; -mod security; -mod sysmon; -mod system; mod utils; diff --git a/src/detections/powershell.rs b/src/detections/powershell.rs deleted file mode 100644 index 4fa956b0..00000000 --- a/src/detections/powershell.rs +++ /dev/null @@ -1,61 +0,0 @@ -use crate::detections::utils; -use crate::models::event; -use regex::Regex; -use std::collections::HashMap; - -pub struct PowerShell {} - -impl PowerShell { - pub fn new() -> PowerShell { - PowerShell {} - } - - pub fn detection( - &mut self, - event_id: String, - _system: &event::System, - event_data: HashMap, - ) { - self.execute_pipeline(&event_id, &event_data); - self.execute_remote_command(&event_id, &event_data); - } - - fn execute_pipeline(&mut self, event_id: &String, event_data: &HashMap) { - if event_id != "4103" { - return; - } - - let default = String::from(""); - let commandline = event_data.get("ContextInfo").unwrap_or(&default); - - if commandline.contains("Host Application") - || commandline.contains("ホスト アプリケーション") - { - let rm_before = - Regex::new("(?ms)^.*(ホスト アプリケーション|Host Application) = ").unwrap(); - let rm_after = Regex::new("(?ms)\n.*$").unwrap(); - - let temp_command_with_extra = rm_before.replace_all(commandline, ""); - let command = rm_after.replace_all(&temp_command_with_extra, ""); - - if command != "" { - utils::check_command(4103, &command, 1000, 0, &default, &default); - } - } - } - - fn execute_remote_command(&mut self, event_id: &String, event_data: &HashMap) { - if event_id != "4104" { - return; - } - - let default = String::from(""); - let path = event_data.get("Path").unwrap().to_string(); - if path == "".to_string() { - let commandline = event_data.get("ScriptBlockText").unwrap_or(&default); - if commandline.to_string() != default { - utils::check_command(4104, &commandline, 1000, 0, &default, &default); - } - } - } -} diff --git a/src/detections/rule.rs b/src/detections/rule.rs index 8d263ff3..2623dff3 100644 --- a/src/detections/rule.rs +++ b/src/detections/rule.rs @@ -1,24 +1,73 @@ +extern crate regex; + +use crate::detections::configs; +use regex::Regex; use serde_json::Value; use yaml_rust::Yaml; -use crate::detections::configs; pub fn parse_rule(yaml: Yaml) -> RuleNode { - let selection = parse_selection(&yaml); + let detection = parse_detection(&yaml); + return RuleNode { yaml: yaml, - detection: DetectionNode { - selection: selection, - }, + detection: detection, }; } +fn parse_detection(yaml: &Yaml) -> Option { + if yaml["detection"].is_badvalue() { + return Option::None; + } else { + let node = DetectionNode { + selection: parse_selection(&yaml), + }; + return Option::Some(node); + } +} + +pub fn get_event_value<'a>(key: &String, event_value: &'a Value) -> Option<&'a Value> { + if key.len() == 0 { + return Option::None; + } + + let alias_config = configs::singleton().event_key_alias_config; + let event_key = match alias_config.get_event_key(key.to_string()) { + Some(alias_event_key) => alias_event_key, + None => key, + }; + + let mut ret: &Value = event_value; + for key in event_key.split(".") { + if ret.is_object() == false { + return Option::None; + } + ret = &ret[key]; + } + + return Option::Some(ret); +} + +fn concat_selection_key(key_list: &Vec) -> String { + return key_list + .iter() + .fold("detection -> selection".to_string(), |mut acc, cur| { + acc = acc + " -> " + cur; + return acc; + }); +} + fn parse_selection(yaml: &Yaml) -> Option> { + // 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(mut key_list: Vec, yaml: &Yaml) -> Box { +fn parse_selection_recursively(key_list: Vec, yaml: &Yaml) -> Box { if yaml.as_hash().is_some() { + // 連想配列はAND条件と解釈する let yaml_hash = yaml.as_hash().unwrap(); let mut and_node = AndSelectionNode::new(); @@ -31,6 +80,7 @@ fn parse_selection_recursively(mut key_list: Vec, yaml: &Yaml) -> Box, yaml: &Yaml) -> Box, } -//////////////// Detection Node -pub struct DetectionNode { - selection: Option>, -} +impl RuleNode { + pub fn init(&mut self) -> Result<(), Vec> { + if self.detection.is_none() { + return Result::Ok(()); + } + + return self.detection.as_mut().unwrap().init(); + } -impl DetectionNode { pub fn select(&self, event_record: &Value) -> bool { - if self.selection.is_none() { + let selection = self + .detection + .as_ref() + .and_then(|detect_node| detect_node.selection.as_ref()); + if selection.is_none() { return false; } - return self.selection.as_ref().unwrap().select(event_record); + return selection.unwrap().select(event_record); } } -//////////// Selection Node -trait SelectionNode { - fn select(&self, event_record: &Value) -> bool; +// Ruleファイルのdetectionを表すノード +struct DetectionNode { + pub selection: Option>, } -///////////////// AndSelectionNode +impl DetectionNode { + fn init(&mut self) -> Result<(), Vec> { + if self.selection.is_none() { + return Result::Ok(()); + } + + return self.selection.as_mut().unwrap().init(); + } +} + +// Ruleファイルの detection- selection配下のノードはこのtraitを実装する。 +trait SelectionNode { + fn select(&self, event_record: &Value) -> bool; + fn init(&mut self) -> Result<(), Vec>; +} + +// detection - selection配下でAND条件を表すノード struct AndSelectionNode { pub child_nodes: Vec>, } @@ -88,9 +162,36 @@ impl SelectionNode for AndSelectionNode { return child_node.as_ref().select(event_record); }); } + + fn init(&mut self) -> Result<(), Vec> { + 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, cur: Vec| -> Vec { + acc.extend(cur.into_iter()); + return acc; + }, + ); + + if err_msgs.is_empty() { + return Result::Ok(()); + } else { + return Result::Err(err_msgs); + } + } } -////////// OrSelectionNode +// detection - selection配下でOr条件を表すノード struct OrSelectionNode { pub child_nodes: Vec>, } @@ -109,96 +210,198 @@ impl SelectionNode for OrSelectionNode { return child_node.as_ref().select(event_record); }); } + + fn init(&mut self) -> Result<(), Vec> { + 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, cur: Vec| -> Vec { + acc.extend(cur.into_iter()); + return acc; + }, + ); + + if err_msgs.is_empty() { + return Result::Ok(()); + } else { + return Result::Err(err_msgs); + } + } } -////////////// Field Selection Node -struct FieldSelectionNode { +// detection - selection配下の末端ノード +struct LeafSelectionNode { key_list: Vec, select_value: Yaml, + matcher: Option>, } -impl FieldSelectionNode { - fn new(key_list: Vec, value_yaml: Yaml) -> FieldSelectionNode { - return FieldSelectionNode { +impl LeafSelectionNode { + fn new(key_list: Vec, value_yaml: Yaml) -> LeafSelectionNode { + return LeafSelectionNode { key_list: key_list, select_value: value_yaml, + matcher: Option::None, }; } // JSON形式のEventJSONから値を取得する関数 aliasも考慮されている。 - // TODO Messageを出力する際も利用するので、共通して使えるようにrefactoringする。 fn get_event_value<'a>(&self, event_value: &'a Value) -> Option<&'a Value> { if self.key_list.is_empty() { return Option::None; } - let key: &str = &self.key_list[0]; - if key.len() == 0 { - return Option::None; - } - - let event_key = match configs::singleton().event_key_alias_config.get_event_key(key.to_string()) { - Some(alias_event_key) => { alias_event_key } - None => { key } - }; - - let mut ret: &Value = event_value; - for key in event_key.split(".") { - if ret.is_object() == false { - return Option::None; - } - ret = &ret[key]; - } - - return Option::Some(ret); + return get_event_value(&self.key_list[0].to_string(), event_value); } - // TODO Matcherのインスタンスが都度生成されないようにする。 - fn get_matchers(&self) -> Vec> { - return vec![Box::new(ValueMatcher {})]; + // LeafMatcherの一覧を取得する。 + fn get_matchers(&self) -> Vec> { + return vec![Box::new(RegexMatcher::new())]; + } + + // LeafMatcherを取得する。 + fn get_matcher(&self) -> Option> { + let matchers = self.get_matchers(); + let mut match_key_list = self.key_list.clone(); + match_key_list.remove(0); + return matchers + .into_iter() + .find(|matcher| matcher.is_target_key(&match_key_list)); } } -impl SelectionNode for FieldSelectionNode { +impl SelectionNode for LeafSelectionNode { fn select(&self, event_record: &Value) -> bool { - let matchers = self.get_matchers(); - let matcher = matchers - .into_iter() - .find(|matcher| matcher.is_target_key(&self.key_list)); - if matcher.is_none() { + if self.matcher.is_none() { return false; } let event_value = self.get_event_value(event_record); - return matcher + return self.matcher.as_ref().unwrap().is_match(event_value); + } + + fn init(&mut self) -> Result<(), Vec> { + 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) + )]); + } + + return self + .matcher + .as_mut() .unwrap() - .is_match(&self.key_list, &self.select_value, event_value); + .init(&match_key_list, &self.select_value); } } -trait FieldSelectionMatcher { +// 末端ノードがEventLogの値を比較するロジックを表す。 +// 正規条件のマッチや文字数制限など、比較ロジック毎にこのtraitを実装したクラスが存在する。 +// +// 新規にLeafMatcherを実装するクラスを作成した場合、 +// LeafSelectionNodeのget_matchersクラスの戻り値の配列に新規作成したクラスのインスタンスを追加する。 +trait LeafMatcher { fn is_target_key(&self, key_list: &Vec) -> bool; - fn is_match( - &self, - key_list: &Vec, - select_value: &Yaml, - event_value: Option<&Value>, - ) -> bool; + + fn is_match(&self, event_value: Option<&Value>) -> bool; + + fn init(&mut self, key_list: &Vec, select_value: &Yaml) -> Result<(), Vec>; } -struct ValueMatcher {} +// 正規表現で比較するロジックを表すクラス +struct RegexMatcher { + re: Option, +} -impl FieldSelectionMatcher for ValueMatcher { +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) -> bool { return key_list.is_empty(); } - fn is_match( - &self, - key_list: &Vec, - select_value: &Yaml, - event_value: Option<&Value>, - ) -> bool { - return true; + fn init(&mut self, key_list: &Vec, select_value: &Yaml) -> Result<(), Vec> { + 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]); + } + + return Result::Ok(()); } -} \ No newline at end of file + + fn is_match(&self, event_value: Option<&Value>) -> bool { + 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, + }; + } +} diff --git a/src/detections/security.rs b/src/detections/security.rs deleted file mode 100644 index 44ce23ae..00000000 --- a/src/detections/security.rs +++ /dev/null @@ -1,1348 +0,0 @@ -use crate::detections::utils; -use crate::models::event; -use std::collections::HashMap; - -#[derive(Debug)] -pub struct Security { - max_total_sensitive_privuse: i32, - max_passspray_login: i32, - max_passspray_uniquser: i32, - max_failed_logons: i32, - alert_all_admin: i32, - total_admin_logons: i32, - total_failed_logons: i32, - total_failed_account: i32, - total_sensitive_privuse: i32, - admin_logons: HashMap>, - multiple_admin_logons: HashMap, - account_2_failedcnt: HashMap, - passspray_2_user: HashMap, - empty_str: String, -} - -impl Security { - pub fn new() -> Security { - Security { - max_total_sensitive_privuse: 4, - max_passspray_login: 6, - max_passspray_uniquser: 6, - max_failed_logons: 5, - alert_all_admin: 0, - total_admin_logons: 0, - total_failed_logons: 0, - total_failed_account: 0, - total_sensitive_privuse: 0, - admin_logons: HashMap::new(), - multiple_admin_logons: HashMap::new(), - account_2_failedcnt: HashMap::new(), - passspray_2_user: HashMap::new(), - empty_str: String::default(), - } - } - - pub fn disp(&self) { - self.disp_admin_logons().and_then(Security::print_console); - self.disp_login_failed().and_then(Security::print_console); - } - - fn disp_admin_logons(&self) -> Option> { - if self.total_admin_logons < 1 { - return Option::None; - } - - let mut msges: Vec = Vec::new(); - msges.push(format!("total_admin_logons:{}", self.total_admin_logons)); - msges.push(format!("admin_logons:{:?}", self.admin_logons)); - msges.push(format!( - "multiple_admin_logons:{:?}", - self.multiple_admin_logons - )); - - return Option::Some(msges); - } - - fn disp_login_failed(&self) -> Option> { - let exceed_failed_logons = self.total_failed_logons <= self.max_failed_logons; - - let exist_failed_account = self.account_2_failedcnt.keys().count() as i32 <= 1; - if exceed_failed_logons || exist_failed_account { - return Option::None; - } - - let mut msges: Vec = Vec::new(); - msges.push(format!( - "High number of total logon failures for multiple accounts" - )); - msges.push(format!( - "Total accounts: {}", - self.account_2_failedcnt.keys().count() - )); - msges.push(format!( - "Total logon failures: {}", - self.total_failed_logons - )); - - return Option::Some(msges); - } - - pub fn detection( - &mut self, - event_id: String, - _system: &event::System, - user_data: &Option, - event_data: HashMap, - ) { - self.process_created(&event_id, &event_data); - self.se_debug_privilege(&event_id, &event_data); - self.account_created(&event_id, &event_data) - .and_then(Security::print_console); - self.add_member_security_group(&event_id, &event_data) - .and_then(Security::print_console); - self.failed_logon(&event_id, &event_data); - self.sensitive_priviledge(&event_id, &event_data) - .and_then(Security::print_console); - self.attempt_priviledge(&event_id, &event_data) - .and_then(Security::print_console); - self.pass_spray(&event_id, &event_data) - .and_then(Security::print_console); - self.audit_log_cleared(&event_id, &user_data) - .and_then(Security::print_console); - } - - fn print_console(v: Vec) -> Option> { - v.iter().for_each(|s| println!("{}", s)); - println!("\n"); - return Option::Some(v); - } - - fn process_created(&mut self, event_id: &String, event_data: &HashMap) { - if event_id != "4688" { - return; - } - - let commandline = event_data.get("CommandLine").unwrap_or(&self.empty_str); - let creator = event_data - .get("ParentProcessName") - .unwrap_or(&self.empty_str); - utils::check_command(4688, &commandline, 1000, 0, &self.empty_str, &creator); - } - - // - // Special privileges assigned to new logon (possible admin access) - // - fn se_debug_privilege(&mut self, event_id: &String, event_data: &HashMap) { - if event_id != "4672" { - return; - } - - if let Some(privileage_list) = event_data.get("PrivilegeList") { - if let Some(_data) = privileage_list.find("SeDebugPrivilege") { - // alert_all_adminが有効であれば、標準出力して知らせる - // DeepBlueCLIでは必ず0になっていて、基本的には表示されない。 - if self.alert_all_admin == 1 { - println!("Logon with SeDebugPrivilege (admin access)"); - println!("Username:{}", event_data["SubjectUserName"]); - println!("Domain:{}", event_data["SubjectDomainName"]); - println!("User SID:{}", event_data["SubjectUserSid"]); - println!("Domain:{}", event_data["PrivilegeList"]); - } - - self.total_admin_logons += 1; - - // admin_logons配列にusernameが含まれているか確認 - match self.admin_logons.get(&event_data["SubjectUserName"]) { - Some(sid) => { - // 含まれていれば、マルチユーザが管理者としてログインしているか確認 - // マルチログオンのデータをセット - if event_data["SubjectUserName"] != event_data["SubjectUserSid"] { - // One username with multiple admin logon SIDs - self.multiple_admin_logons - .insert(event_data["SubjectUserName"].to_string(), 1); - - let mut count_hash: HashMap = HashMap::new(); - count_hash.insert( - event_data["SubjectUserSid"].to_string(), - sid[&event_data["SubjectUserSid"]] + 1, - ); - self.admin_logons - .insert(event_data["SubjectUserName"].to_string(), count_hash); - } - } - None => { - // admin_logons配列にセットUserNameとSIDとカウンタをセット - let mut count_hash: HashMap = HashMap::new(); - count_hash.insert(event_data["SubjectUserSid"].to_string(), 1); - self.admin_logons - .insert(event_data["SubjectUserName"].to_string(), count_hash); - } - } - } - } - } - - // account craeted:OK - fn account_created( - &mut self, - event_id: &String, - event_data: &HashMap, - ) -> Option> { - if event_id != "4720" { - return Option::None; - } - - let mut msges: Vec = Vec::new(); - msges.push("New User Created".to_string()); - - let username = event_data.get("TargetUserName").unwrap_or(&self.empty_str); - msges.push(format!("Username: {}", username)); - let sid = event_data.get("TargetSid").unwrap_or(&self.empty_str); - msges.push(format!("User SID: {}", sid)); - - return Option::Some(msges); - } - - // add member to security group - fn add_member_security_group( - &mut self, - event_id: &String, - event_data: &HashMap, - ) -> Option> { - // check if group is Administrator, may later expand to all groups - if event_data.get("TargetUserName").unwrap_or(&self.empty_str) != "Administrators" { - return Option::None; - } - - // A member was added to a security-enabled (global|local|universal) group. - let mut msges: Vec = Vec::new(); - if event_id == "4728" { - msges.push("User added to global Administrators group".to_string()); - } else if event_id == "4732" { - msges.push("User added to local Administrators group".to_string()); - } else if event_id == "4756" { - msges.push("User added to universal Administrators group".to_string()); - } else { - return Option::None; - } - - let username = event_data.get("MemberName").unwrap_or(&self.empty_str); - msges.push(format!("Username: {}", username)); - let sid = event_data.get("MemberSid").unwrap_or(&self.empty_str); - msges.push(format!("User SID: {}", sid)); - - return Option::Some(msges); - } - - // An account failed to log on.:OK - // Requires auditing logon failures - // https://technet.microsoft.com/en-us/library/cc976395.aspx - fn failed_logon(&mut self, event_id: &String, event_data: &HashMap) { - if event_id != "4625" { - return; - } - - // see fn disp() - self.total_failed_logons += 1; - let username = event_data.get("TargetUserName").unwrap_or(&self.empty_str); - let failed_cnt = self.account_2_failedcnt.get(username).unwrap_or(&0) + 1; - self.account_2_failedcnt - .insert(username.to_string(), failed_cnt); - } - - // Sensitive Privilege Use (Mimikatz) - fn sensitive_priviledge( - &mut self, - event_id: &String, - event_data: &HashMap, - ) -> Option> { - if event_id != "4673" { - return Option::None; - } - - self.total_sensitive_privuse += 1; - let mut msges: Vec = Vec::new(); - // use == operator here to avoid multiple log notices - if self.max_total_sensitive_privuse != self.total_sensitive_privuse { - return Option::None; - } - - msges.push("Sensititive Privilege Use Exceeds Threshold".to_string()); - msges.push( - "Potentially indicative of Mimikatz, multiple sensitive privilege calls have been made" - .to_string(), - ); - - let username = event_data.get("SubjectUserName").unwrap_or(&self.empty_str); - msges.push(format!("Username: {}", username)); - - let domainname = event_data - .get("SubjectDomainName") - .unwrap_or(&self.empty_str); - msges.push(format!("Domain Name: {}", domainname)); - - return Option::Some(msges); - } - - fn attempt_priviledge( - &mut self, - event_id: &String, - event_data: &HashMap, - ) -> Option> { - if event_id != "4674" { - return Option::None; - } - - // "%%1539" means WRITE_DAC(see detail: https://docs.microsoft.com/ja-jp/windows/security/threat-protection/auditing/event-4663) - let servicename = event_data - .get("ProcessName") - .unwrap_or(&self.empty_str) - .to_uppercase(); - let accessname = event_data.get("AccessMask").unwrap_or(&self.empty_str); - if servicename != r"C:\WINDOWS\SYSTEM32\SERVICES.EXE" || accessname != "%%1539" { - return Option::None; - } - - let mut msges: Vec = Vec::new(); - msges.push("Possible Hidden Service Attempt".to_string()); - msges.push("User requested to modify the Dynamic Access Control (DAC) permissions of a sevice, possibly to hide it from view".to_string()); - - let username = event_data.get("SubjectUserName").unwrap_or(&self.empty_str); - msges.push(format!("User: {}", username)); - - let servicename = event_data.get("ObjectName").unwrap_or(&self.empty_str); - msges.push(format!("Target service: {}", servicename)); - - msges.push("WRITE_DAC".to_string()); - - return Option::Some(msges); - } - - // A logon was attempted using explicit credentials. - fn pass_spray( - &mut self, - event_id: &String, - event_data: &HashMap, - ) -> Option> { - if event_id != "4648" { - return Option::None; - } - - let targetusername = event_data.get("TargetUserName").unwrap_or(&self.empty_str); - let spray_cnt = self.passspray_2_user.get(targetusername).unwrap_or(&0) + 1; - self.passspray_2_user - .insert(targetusername.to_string(), spray_cnt); - - // check exceeded targetuser count. - let spray_uniq_user = self - .passspray_2_user - .values() - .filter(|value| value > &&self.max_passspray_login) - .count() as i32; - if spray_uniq_user <= self.max_passspray_uniquser { - return Option::None; - } - - // let v_username = Vec::new(); - let mut v_username = Vec::new(); - self.passspray_2_user - .keys() - .for_each(|u| v_username.push(u)); - v_username.sort(); - let usernames: String = v_username.iter().fold( - self.empty_str.to_string(), - |mut acc: String, cur| -> String { - acc.push_str(cur); - acc.push_str(" "); - return acc; - }, - ); - - let mut msges: Vec = Vec::new(); - msges.push( - "Distributed Account Explicit Credential Use (Password Spray Attack)".to_string(), - ); - msges.push( - "The use of multiple user account access attempts with explicit credentials is " - .to_string(), - ); - msges.push("an indicator of a password spray attack".to_string()); - - msges.push(format!("Target Usernames: {}", usernames.trim())); - let access_username = event_data.get("SubjectUserName").unwrap_or(&self.empty_str); - - msges.push(format!("Accessing Username: {}", access_username)); - - let access_hostname = event_data - .get("SubjectDomainName") - .unwrap_or(&self.empty_str); - msges.push(format!("Accessing Host Name: {}", access_hostname)); - - // reset - self.passspray_2_user = HashMap::new(); - - return Option::Some(msges); - } - - fn audit_log_cleared( - &mut self, - event_id: &String, - user_data: &Option, - ) -> Option> { - if event_id != "1102" { - return Option::None; - } - - let mut msges: Vec = Vec::new(); - msges.push("Audit Log Clear".to_string()); - msges.push("The Audit log was cleared".to_string()); - let username = user_data - .as_ref() - .and_then(|u| u.log_file_cleared.as_ref()) - .and_then(|l| l.subject_user_name.as_ref()); - msges.push(format!( - "Security ID: {}", - username.unwrap_or(&self.empty_str) - )); - - return Option::Some(msges); - } -} - -#[cfg(test)] -mod tests { - extern crate quick_xml; - - use crate::detections::security; - use crate::models::event; - - // 正しくヒットするパターン - #[test] - fn test_account_created_hit() { - let xml_str = get_account_created_xml(); - let event: event::Evtx = quick_xml::de::from_str(&xml_str) - .map_err(|e| { - println!("{}", e.to_string()); - }) - .unwrap(); - - let mut sec = security::Security::new(); - let option_v = sec.account_created( - &event.system.event_id.to_string(), - &event.parse_event_data(), - ); - - let v = option_v.unwrap(); - let mut ite = v.iter(); - assert_eq!( - &"New User Created".to_string(), - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!( - &"Username: IEUser".to_string(), - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!( - &"User SID: S-1-5-21-3463664321-2923530833-3546627382-1000", - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!(Option::None, ite.next()); - } - - // event idが異なるパターン - #[test] - fn test_account_created_noteq_eventid() { - let xml_str = - get_account_created_xml().replace("4720", "4721"); - let event: event::Evtx = quick_xml::de::from_str(&xml_str) - .map_err(|e| { - println!("{}", e.to_string()); - }) - .unwrap(); - - let mut sec = security::Security::new(); - let option_v = sec.account_created( - &event.system.event_id.to_string(), - &event.parse_event_data(), - ); - - assert_eq!(Option::None, option_v); - } - - // 実在するかどうか不明だが、EventDataの必要なフィールドがないパターン - #[test] - fn test_account_created_none_check() { - let xml_str = r#" - - - - - 4720 - 0 - 0 - 13824 - 0 - 0x8020000000000000 - - 112 - - - Security - IE8Win7 - - - - "#; - let event: event::Evtx = quick_xml::de::from_str(&xml_str) - .map_err(|e| { - println!("{}", e.to_string()); - }) - .unwrap(); - - let mut sec = security::Security::new(); - let option_v = sec.account_created( - &event.system.event_id.to_string(), - &event.parse_event_data(), - ); - - let v = option_v.unwrap(); - let mut ite = v.iter(); - assert_eq!( - &"New User Created".to_string(), - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!( - &"Username: ".to_string(), - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!(&"User SID: ", ite.next().unwrap_or(&"".to_string())); - assert_eq!(Option::None, ite.next()); - } - - fn get_account_created_xml() -> String { - return r#" - - - - - 4720 - 0 - 0 - 13824 - 0 - 0x8020000000000000 - - 112 - - - Security - IE8Win7 - - - - IEUser - IE8Win7 - S-1-5-21-3463664321-2923530833-3546627382-1000 - S-1-5-18 - WIN-QALA5Q3KJ43$ - WORKGROUP - 0x3e7 - - - IEUserSam - %%1793 - - - %%1793 - %%1793 - %%1793 - %%1793 - %%1793 - %%1794 - %%1794 - 513 - - - 0x0 - 0x15 - - %%2080 - %%2082 - %%2084 - %%1793 - - - %%1797 - - "#.to_string(); - } - - // 正しくヒットするパターン(eventid=4732) - #[test] - fn test_add_member_security_group_hit_4732() { - let xml_str = get_add_member_security_group_xml(); - let event: event::Evtx = quick_xml::de::from_str(&xml_str) - .map_err(|e| { - println!("{}", e.to_string()); - }) - .unwrap(); - - let mut sec = security::Security::new(); - let option_v = sec.add_member_security_group( - &event.system.event_id.to_string(), - &event.parse_event_data(), - ); - - let v = option_v.unwrap(); - let mut ite = v.iter(); - assert_eq!( - &"User added to local Administrators group".to_string(), - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!( - &"Username: testnamess".to_string(), - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!( - &"User SID: S-1-5-21-3463664321-2923530833-3546627382-1000", - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!(Option::None, ite.next()); - } - - // 正しくヒットするパターン(eventid=4728は一行目が変わる) - #[test] - fn test_add_member_security_group_hit_4728() { - let xml_str = get_add_member_security_group_xml() - .replace(r"4732", r"4728"); - let event: event::Evtx = quick_xml::de::from_str(&xml_str) - .map_err(|e| { - println!("{}", e.to_string()); - }) - .unwrap(); - - let mut sec = security::Security::new(); - let option_v = sec.add_member_security_group( - &event.system.event_id.to_string(), - &event.parse_event_data(), - ); - - let v = option_v.unwrap(); - let mut ite = v.iter(); - assert_eq!( - &"User added to global Administrators group".to_string(), - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!( - &"Username: testnamess".to_string(), - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!( - &"User SID: S-1-5-21-3463664321-2923530833-3546627382-1000", - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!(Option::None, ite.next()); - } - - // 正しくヒットするパターン(eventid=4756は一行目が変わる) - #[test] - fn test_add_member_security_group_hit_4756() { - let xml_str = get_add_member_security_group_xml() - .replace(r"4732", r"4756"); - let event: event::Evtx = quick_xml::de::from_str(&xml_str) - .map_err(|e| { - println!("{}", e.to_string()); - }) - .unwrap(); - - let mut sec = security::Security::new(); - let option_v = sec.add_member_security_group( - &event.system.event_id.to_string(), - &event.parse_event_data(), - ); - - let v = option_v.unwrap(); - let mut ite = v.iter(); - assert_eq!( - &"User added to universal Administrators group".to_string(), - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!( - &"Username: testnamess".to_string(), - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!( - &"User SID: S-1-5-21-3463664321-2923530833-3546627382-1000", - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!(Option::None, ite.next()); - } - - // eventidが異なりヒットしないパターン - #[test] - fn test_add_member_security_group_noteq_eventid() { - let xml_str = get_add_member_security_group_xml() - .replace(r"4732", r"4757"); - let event: event::Evtx = quick_xml::de::from_str(&xml_str) - .map_err(|e| { - println!("{}", e.to_string()); - }) - .unwrap(); - - let mut sec = security::Security::new(); - let option_v = sec.add_member_security_group( - &event.system.event_id.to_string(), - &event.parse_event_data(), - ); - assert_eq!(Option::None, option_v); - } - - // グループがAdministratorsじゃなくてHitしないパターン - #[test] - fn test_add_member_security_not_administrators() { - let xml_str = get_add_member_security_group_xml().replace( - r"Administrators", - r"local", - ); - let event: event::Evtx = quick_xml::de::from_str(&xml_str) - .map_err(|e| { - println!("{}", e.to_string()); - }) - .unwrap(); - - let mut sec = security::Security::new(); - let option_v = sec.add_member_security_group( - &event.system.event_id.to_string(), - &event.parse_event_data(), - ); - assert_eq!(Option::None, option_v); - } - - // hitするけど表示するフィールドがない場合 - #[test] - fn test_add_member_security_group_none() { - let xml_str = r#" - - - - - 4732 - 0 - 0 - 13826 - 0 - 0x8020000000000000 - - 116 - - - Security - IE8Win7 - - - - Administrators - - "#; - let event: event::Evtx = quick_xml::de::from_str(&xml_str) - .map_err(|e| { - println!("{}", e.to_string()); - }) - .unwrap(); - - let mut sec = security::Security::new(); - let option_v = sec.add_member_security_group( - &event.system.event_id.to_string(), - &event.parse_event_data(), - ); - - let v = option_v.unwrap(); - let mut ite = v.iter(); - assert_eq!( - &"User added to local Administrators group".to_string(), - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!( - &"Username: ".to_string(), - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!(&"User SID: ", ite.next().unwrap_or(&"".to_string())); - assert_eq!(Option::None, ite.next()); - } - - fn get_add_member_security_group_xml() -> String { - return r#" - - - - - 4732 - 0 - 0 - 13826 - 0 - 0x8020000000000000 - - 116 - - - Security - IE8Win7 - - - - testnamess - S-1-5-21-3463664321-2923530833-3546627382-1000 - Administrators - Builtin - S-1-5-32-544 - S-1-5-18 - WIN-QALA5Q3KJ43$ - WORKGROUP - 0x3e7 - - - - "#.to_string(); - } - - // ユーザー数が一つなら、ログ数が幾らあっても、メッセージは表示されないはず。 - #[test] - fn test_failed_logon_nothit_onlyoneuser() { - let xml_str = get_failed_logon_xml(); - let event: event::Evtx = quick_xml::de::from_str(&xml_str).unwrap(); - - let mut sec = security::Security::new(); - - sec.max_failed_logons = 5; - let ite = [1, 2, 3, 4, 5, 6, 7].iter(); - ite.for_each(|i| { - sec.failed_logon( - &event.system.event_id.to_string(), - &event.parse_event_data(), - ); - assert_eq!(i, &sec.total_failed_logons); - assert_eq!(Option::None, sec.disp_login_failed()); - }); - } - - // 失敗回数を増やしていき、境界値でメッセージが表示されることのテスト。 - #[test] - fn test_failed_logon_hit() { - let xml_str = get_failed_logon_xml(); - let event: event::Evtx = quick_xml::de::from_str(&xml_str).unwrap(); - let event_another: event::Evtx = quick_xml::de::from_str(&xml_str.replace( - r"Administrator", - r"localuser", - )) - .unwrap(); - - let mut sec = security::Security::new(); - sec.max_failed_logons = 5; - - // メッセージが表示されるには2ユーザー以上失敗している必要がある。まず一人目 - sec.failed_logon( - &event.system.event_id.to_string(), - &event.parse_event_data(), - ); - assert_eq!(1, sec.total_failed_logons); - - let ite = [1, 2, 3, 4, 5, 6, 7].iter(); - ite.for_each(|i| { - sec.failed_logon( - &event_another.system.event_id.to_string(), - &event_another.parse_event_data(), - ); - let fail_cnt = i + 1; - assert_eq!(fail_cnt, sec.total_failed_logons); - if fail_cnt > 5 { - let v = sec.disp_login_failed().unwrap(); - let mut ite = v.iter(); - assert_eq!( - &"High number of total logon failures for multiple accounts".to_string(), - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!( - &"Total accounts: 2".to_string(), - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!( - &format!("Total logon failures: {}", fail_cnt), - ite.next().unwrap_or(&"".to_string()) - ); - // assert_eq!(Option::None, ite.next()); - } else { - assert_eq!(Option::None, sec.disp_login_failed()); - } - }); - - // hitするけど表示するフィールドがない場合 - let xml_nofield = r#" - - - 4625001254400x80100000000000006016SecurityDESKTOP-M5SN04R - - "#; - - // エラーにならなければOK - let event_nofield: event::Evtx = quick_xml::de::from_str(xml_nofield).unwrap(); - sec.failed_logon( - &event_nofield.system.event_id.to_string(), - &event_nofield.parse_event_data(), - ); - } - - // 失敗回数を増やしていき、境界値でメッセージが表示されることのテスト。 - #[test] - fn test_failed_logon_noteq_eventid() { - let xml_str = get_failed_logon_xml(); - let event: event::Evtx = quick_xml::de::from_str( - &xml_str.replace(r"4625", r"4626"), - ) - .unwrap(); - let event_another: event::Evtx = quick_xml::de::from_str( - &xml_str - .replace(r"4625", r"4626") - .replace( - r"Administrator", - r"localuser", - ), - ) - .unwrap(); - - let mut sec = security::Security::new(); - sec.max_failed_logons = 5; - - // メッセージが表示されるには2ユーザー以上失敗している必要がある。まず一人目 - sec.failed_logon( - &event.system.event_id.to_string(), - &event.parse_event_data(), - ); - assert_eq!(0, sec.total_failed_logons); - - let ite = [1, 2, 3, 4, 5, 6, 7].iter(); - ite.for_each(|_i| { - sec.failed_logon( - &event_another.system.event_id.to_string(), - &event_another.parse_event_data(), - ); - assert_eq!(0, sec.total_failed_logons); - assert_eq!(Option::None, sec.disp_login_failed()); - }); - } - - fn get_failed_logon_xml() -> String { - return r#" - - - - 4625 - 0 - 0 - 12544 - 0 - 0x8010000000000000 - - 6016 - - - Security - DESKTOP-M5SN04R - - - - S-1-0-0 - - - - - 0x0 - S-1-0-0 - Administrator - . - 0xc000006d - %%2313 - 0xc000006a - 3 - NtLmSsp - NTLM - fpEbpiox2Q3Qf8av - - - - - 0 - 0x0 - - - 192.168.198.149 - 33083 - - "# - .to_string(); - } - - // Hitするパターンとしないパターンをまとめてテスト - #[test] - fn test_sensitive_priviledge_hit() { - let xml_str = get_sensitive_prividedge_hit(); - let event: event::Evtx = quick_xml::de::from_str(&xml_str).unwrap(); - - let mut sec = security::Security::new(); - sec.max_total_sensitive_privuse = 6; - - let ite = [1, 2, 3, 4, 5, 6, 7].iter(); - ite.for_each(|i| { - let msg = sec.sensitive_priviledge(&event.system.event_id.to_string(), &event.parse_event_data()); - // i == 7ときにHitしない - if i == &6 { - let v = msg.unwrap(); - let mut ite = v.iter(); - assert_eq!( - &"Sensititive Privilege Use Exceeds Threshold".to_string(), - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!( - &"Potentially indicative of Mimikatz, multiple sensitive privilege calls have been made".to_string(), - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!( - &"Username: Sec504".to_string(), - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!( - &"Domain Name: SEC504STUDENT", - ite.next().unwrap_or(&"".to_string()) - ); - } else { - assert_eq!(Option::None, msg); - } - }); - } - - // eventidが異なるので、Hitしないテスト - #[test] - fn test_sensitive_priviledge_noteq_eventid() { - let xml_str = get_sensitive_prividedge_hit() - .replace(r"4673", r"4674"); - let event: event::Evtx = quick_xml::de::from_str(&xml_str).unwrap(); - - let mut sec = security::Security::new(); - sec.max_total_sensitive_privuse = 6; - - let ite = [1, 2, 3, 4, 5, 6, 7].iter(); - ite.for_each(|_i| { - let msg = sec.sensitive_priviledge( - &event.system.event_id.to_string(), - &event.parse_event_data(), - ); - assert_eq!(Option::None, msg); - }); - } - - fn get_sensitive_prividedge_hit() -> String { - return r#" - - - - 4673 - 0 - 0 - 13056 - 0 - 0x8010000000000000 - - 8936 - - - Security - Sec504Student - - - - S-1-5-21-2977773840-2930198165-1551093962-1000 - Sec504 - SEC504STUDENT - 0x1e3dd - Security - - - SeTcbPrivilege - 0x15a8 - C:\Tools\mimikatz\mimikatz.exe - - "#.to_string(); - } - - // Hitするテスト - #[test] - fn test_attempt_priviledge_hit() { - let xml_str = get_attempt_priviledge_xml(); - let event: event::Evtx = quick_xml::de::from_str(&xml_str).unwrap(); - - let mut sec = security::Security::new(); - let msg = sec.attempt_priviledge( - &event.system.event_id.to_string(), - &event.parse_event_data(), - ); - - assert_ne!(Option::None, msg); - let v = msg.unwrap(); - let mut ite = v.iter(); - assert_eq!( - &"Possible Hidden Service Attempt".to_string(), - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!(&"User requested to modify the Dynamic Access Control (DAC) permissions of a sevice, possibly to hide it from view".to_string(), ite.next().unwrap_or(&"".to_string())); - assert_eq!( - &"User: Sec504".to_string(), - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!( - &"Target service: nginx".to_string(), - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!( - &"WRITE_DAC".to_string(), - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!(Option::None, ite.next()); - } - - // accessmaskが異なるので、Hitしないテスト - #[test] - fn test_attempt_priviledge_noteq_accessmask() { - let xml_str = get_attempt_priviledge_xml(); - let event: event::Evtx = quick_xml::de::from_str(&xml_str.replace( - r"%%1539", - r"%%1538", - )) - .unwrap(); - - let mut sec = security::Security::new(); - let msg = sec.attempt_priviledge( - &event.system.event_id.to_string(), - &event.parse_event_data(), - ); - - assert_eq!(Option::None, msg); - } - - // Serviceが違うのでHitしないテスト - #[test] - fn test_attempt_priviledge_noteq_service() { - let xml_str = get_attempt_priviledge_xml(); - let event: event::Evtx = quick_xml::de::from_str(&xml_str.replace( - r"C:\Windows\System32\services.exe", - r"C:\Windows\System32\lsass.exe", - )) - .unwrap(); - - let mut sec = security::Security::new(); - let msg = sec.attempt_priviledge( - &event.system.event_id.to_string(), - &event.parse_event_data(), - ); - - assert_eq!(Option::None, msg); - } - - // EventIDが違うのでHitしないテスト - #[test] - fn test_attempt_priviledge_noteq_eventid() { - let xml_str = get_attempt_priviledge_xml(); - let event: event::Evtx = quick_xml::de::from_str( - &xml_str.replace(r"4674", r"4675"), - ) - .unwrap(); - - let mut sec = security::Security::new(); - let msg = sec.attempt_priviledge( - &event.system.event_id.to_string(), - &event.parse_event_data(), - ); - - assert_eq!(Option::None, msg); - } - - fn get_attempt_priviledge_xml() -> String { - return r#" - - - - 4674 - 0 - 0 - 13056 - 0 - 0x8020000000000000 - - 39406 - - - Security - Sec504Student - - - - S-1-5-21-2977773840-2930198165-1551093962-1000 - Sec504 - SEC504STUDENT - 0x99e3d - SC Manager - SERVICE OBJECT - nginx - 0xffff820cb1d95928 - %%1539 - - SeSecurityPrivilege - 0x21c - C:\Windows\System32\services.exe - - "#.to_string(); - } - - #[test] - fn test_pass_spray_hit() { - let mut sec = security::Security::new(); - // 6ユーザまでは表示されず、7ユーザー以上で表示されるようになる。 - sec.max_passspray_login = 6; - sec.max_passspray_uniquser = 6; - - test_pass_spray_hit_1cycle(&mut sec, "4648".to_string(), true); - // counterがreset確認のため、2回実行 - test_pass_spray_hit_1cycle(&mut sec, "4648".to_string(), true); - } - - // eventid異なるので、Hitしないはず - #[test] - fn test_pass_spray_noteq_eventid() { - let mut sec = security::Security::new(); - // 6ユーザまでは表示されず、7ユーザー以上で表示されるようになる。 - sec.max_passspray_login = 6; - sec.max_passspray_uniquser = 6; - - test_pass_spray_hit_1cycle(&mut sec, "4649".to_string(), false); - // counterがreset確認のため、2回実行 - test_pass_spray_hit_1cycle(&mut sec, "4649".to_string(), false); - } - - fn test_pass_spray_hit_1cycle(sec: &mut security::Security, event_id: String, is_eq: bool) { - [1,2,3,4,5,6,7].iter().for_each(|i| { - let rep_str = format!(r#"smisenar{}"#,i); - let event_id_tag = format!("{}", event_id); - let xml_str = get_passs_pray_hit().replace(r#"smisenar"#, &rep_str).replace(r"4648", &event_id_tag); - let event: event::Evtx = quick_xml::de::from_str(&xml_str).unwrap(); - [1,2,3,4,5,6,7].iter().for_each(|k|{ - let ret = sec.pass_spray(&event.system.event_id.to_string(), &event.parse_event_data()); - if i == &7 && k == &7 && is_eq { - let v = ret.unwrap(); - let mut ret_ite = v.iter(); - assert_eq!(&"Distributed Account Explicit Credential Use (Password Spray Attack)".to_string(),ret_ite.next().unwrap()); - assert_eq!(&"The use of multiple user account access attempts with explicit credentials is ".to_string(),ret_ite.next().unwrap()); - assert_eq!(&"an indicator of a password spray attack".to_string(),ret_ite.next().unwrap()); - assert_eq!("Target Usernames: smisenar1 smisenar2 smisenar3 smisenar4 smisenar5 smisenar6 smisenar7",ret_ite.next().unwrap()); - assert_eq!(&"Accessing Username: jwrig".to_string(),ret_ite.next().unwrap()); - assert_eq!(&"Accessing Host Name: DESKTOP-JR78RLP".to_string(),ret_ite.next().unwrap()); - assert_eq!(Option::None,ret_ite.next()); - } else { - assert_eq!(Option::None,ret); - } - }); - }); - } - - fn get_passs_pray_hit() -> String { - return r#" - - - - 4648 - 0 - 0 - 12544 - 0 - 0x8020000000000000 - - 43097 - - - Security - DESKTOP-JR78RLP - - - - S-1-5-21-979008924-657238111-836329461-1002 - jwrig - DESKTOP-JR78RLP - 0x3069d - {00000000-0000-0000-0000-000000000000} - smisenar - DOMAIN - {00000000-0000-0000-0000-000000000000} - DESKTOP-JR78RLP - DESKTOP-JR78RLP - 0x4 - - 172.16.144.128 - 445 - - "#.to_string(); - } - - // 普通にHitするテスト - #[test] - fn test_audit_log_cleared_hit() { - let xml_str = get_audit_log_cleared_xml(); - let event: event::Evtx = quick_xml::de::from_str(&xml_str).unwrap(); - - let mut sec = security::Security::new(); - let msg = sec.audit_log_cleared(&event.system.event_id.to_string(), &event.user_data); - - assert_ne!(Option::None, msg); - let v = msg.unwrap(); - let mut ite = v.iter(); - assert_eq!( - &"Audit Log Clear".to_string(), - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!( - &"The Audit log was cleared".to_string(), - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!( - &"Security ID: jwrig".to_string(), - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!(Option::None, ite.next()); - } - - // eventid違うのでHitしないはず - #[test] - fn test_audit_log_cleared_noteq_eventid() { - let xml_str = get_audit_log_cleared_xml() - .replace(r"1102", r"1103"); - let event: event::Evtx = quick_xml::de::from_str(&xml_str).unwrap(); - - let mut sec = security::Security::new(); - let msg = sec.audit_log_cleared(&event.system.event_id.to_string(), &event.user_data); - assert_eq!(Option::None, msg); - } - - fn get_audit_log_cleared_xml() -> String { - return r#" - - - - 1102 - 0 - 4 - 104 - 0 - 0x4020000000000000 - - 42803 - - - Security - DESKTOP-JR78RLP - - - - - S-1-5-21-979008924-657238111-836329461-1002 - jwrig - DESKTOP-JR78RLP - 0x30550 - - - "#.to_string(); - } -} diff --git a/src/detections/sysmon.rs b/src/detections/sysmon.rs deleted file mode 100644 index 37f01874..00000000 --- a/src/detections/sysmon.rs +++ /dev/null @@ -1,59 +0,0 @@ -use crate::detections::utils::check_command; -use crate::models::event; -use std::collections::HashMap; - -pub struct Sysmon { - checkunsigned: u16, -} - -impl Sysmon { - pub fn new() -> Sysmon { - Sysmon { checkunsigned: 0 } - } - - pub fn detection( - &mut self, - event_id: String, - _system: &event::System, - event_data: HashMap, - ) { - self.check_command_lines(&event_id, &event_data); - self.check_for_unsigned_files(&event_id, &event_data); - } - - fn check_command_lines(&mut self, event_id: &String, event_data: &HashMap) { - if event_id != "1" { - return; - } - - if let Some(_command_line) = event_data.get("CommandLine") { - let default = "".to_string(); - let _creater = event_data.get("ParentImage").unwrap_or(&default); - - check_command(1, _command_line, 1000, 0, "", _creater); - } - } - - fn check_for_unsigned_files( - &mut self, - event_id: &String, - event_data: &HashMap, - ) { - if event_id != "7" { - return; - } - - if self.checkunsigned == 1 { - let default = "".to_string(); - let _signed = event_data.get("Signed").unwrap_or(&default); - if _signed == "false" { - let _image = event_data.get("Image").unwrap_or(&default); - let _command_line = event_data.get("ImageLoaded").unwrap_or(&default); - - println!("Message : Unsigned Image (DLL)"); - println!("Result : Loaded by: {}", _image); - println!("Command : {}", _command_line); - } - }; - } -} diff --git a/src/detections/system.rs b/src/detections/system.rs deleted file mode 100644 index 14619d60..00000000 --- a/src/detections/system.rs +++ /dev/null @@ -1,110 +0,0 @@ -use crate::detections::utils; -use crate::models::event; -use std::collections::HashMap; - -pub struct System {} - -impl System { - pub fn new() -> System { - System {} - } - - pub fn detection( - &mut self, - event_id: String, - system: &event::System, - event_data: HashMap, - ) { - self.system_log_clear(&event_id); - self.windows_event_log(&event_id, &event_data); - self.new_service_created(&event_id, &event_data); - self.interactive_service_warning(&event_id, &event_data); - self.suspicious_service_name(&event_id, &event_data); - } - - fn new_service_created(&mut self, event_id: &String, event_data: &HashMap) { - if event_id != "7045" { - return; - } - - let default = String::from(""); - let servicename = &event_data.get("ServiceName").unwrap_or(&default); - let commandline = &event_data.get("ImagePath").unwrap_or(&default); - let text = utils::check_regex(&servicename, 1); - if !text.is_empty() { - println!("Message : New Service Created"); - println!("Command : {}", commandline); - println!("Results : Service name: {}", servicename); - println!("Results : {}", text); - } - if !commandline.is_empty() { - utils::check_command(7045, &commandline, 1000, 0, &servicename, &""); - } - } - - fn interactive_service_warning( - &mut self, - event_id: &String, - event_data: &HashMap, - ) { - if event_id != "7030" { - return; - } - - let default = String::from(""); - let servicename = &event_data.get("param1").unwrap_or(&default); - println!("Message : Interactive service warning"); - println!("Results : Service name: {}", servicename); - println!("Results : Malware (and some third party software) trigger this warning"); - println!("{}", utils::check_regex(&servicename, 1)); - } - - fn suspicious_service_name(&mut self, event_id: &String, event_data: &HashMap) { - if event_id != "7036" { - return; - } - - let default = String::from(""); - let servicename = &event_data.get("param1").unwrap_or(&default); - let text = utils::check_regex(&servicename, 1); - if !text.is_empty() { - println!("Message : Suspicious Service Name"); - println!("Results : Service name: {}", servicename); - println!("Results : {}", text); - } - } - - fn system_log_clear(&mut self, event_id: &String) { - if event_id != "104" { - return; - } - - println!("Message : System Log Clear"); - println!("Results : The System log was cleared."); - } - - fn windows_event_log(&mut self, event_id: &String, event_data: &HashMap) { - if event_id != "7040" { - return; - } - - if let Some(_param1) = event_data.get("param1") { - if _param1 == "Windows Event Log" { - println!("Service name : {}", _param1); - if let Some(_param2) = event_data.get("param2") { - if _param2 == "disabled" { - println!("Message : Event Log Service Stopped"); - println!( - "Results : Selective event log manipulation may follow this event." - ); - } else if _param2 == "auto start" { - println!("Message : Event Log Service Started"); - println!( - "Results : Selective event log manipulation may precede this event." - ); - } - } - } - } - } -} From da5f4119fb1b4f661189fad4a8f1399714fc8f9d Mon Sep 17 00:00:00 2001 From: ichiichi11 Date: Sun, 22 Nov 2020 11:05:52 +0900 Subject: [PATCH 04/16] rulefile error handling is implemented --- src/detections/detection.rs | 64 ++++++++++++++++++++++++++----------- src/detections/rule.rs | 1 + 2 files changed, 46 insertions(+), 19 deletions(-) diff --git a/src/detections/detection.rs b/src/detections/detection.rs index b3c6c3bd..e0d8e3da 100644 --- a/src/detections/detection.rs +++ b/src/detections/detection.rs @@ -6,7 +6,7 @@ use crate::detections::rule; use crate::detections::rule::RuleNode; use crate::yaml::ParseYaml; -use chrono::{TimeZone, Utc}; +use chrono::{DateTime, FixedOffset, ParseError, ParseResult, TimeZone, Utc}; use evtx::EvtxParser; use serde_json::{Error, Value}; @@ -30,7 +30,7 @@ impl Detection { return Option::None; } - //// refer https://rust-lang-nursery.github.io/rust-cookbook/encoding/complex.html + //// https://rust-lang-nursery.github.io/rust-cookbook/encoding/complex.html let result_json: Result = serde_json::from_str(&result_record.unwrap().data); if result_json.is_err() { @@ -55,19 +55,24 @@ impl Detection { .into_iter() .map(|rule_file| rule::parse_rule(rule_file)) .filter_map(|mut rule| { - return rule - .init() - .or_else(|err_msgs| { - print!( - "Failed to parse Rule file. See following detail. [rule file title:{}]", - rule.yaml["title"].as_str().unwrap_or("") - ); - err_msgs.iter().for_each(|err_msg| println!("{}", err_msg)); - println!("\n"); - return Result::Err(err_msgs); - }) - .and_then(|_empty| Result::Ok(rule)) - .ok(); + let err_msgs_result = rule.init(); + if err_msgs_result.is_ok() { + return Option::Some(rule); + } + + // ruleファイルの初期化失敗時のエラーを表示する部分 + err_msgs_result.err().iter().for_each(|err_msgs| { + // TODO 本当はファイルパスを出力したい + // ParseYamlの変更が必要なので、一旦yamlのタイトルを表示。 + println!( + "[Failed to parse Rule file. See following detail. rule file title:{}]", + rule.yaml["title"].as_str().unwrap_or("") + ); + err_msgs.iter().for_each(|err_msg| println!("{}", err_msg)); + println!(""); + }); + + return Option::None; }) .collect(); @@ -78,14 +83,35 @@ impl Detection { .iter() .filter(|event_record| rule.select(event_record)) .for_each(|event_record| { - message.insert( - Utc.ymd(1996, 2, 27).and_hms(1, 5, 1), - event_record.to_string(), - ) + let event_time = Detection::get_event_time(event_record); + // TODO ログから日付がとれない場合に本当は時刻不明という感じで表示したい。 + // しかし、Messageクラスのinsertメソッドが、UTCクラスのインスタンスを必ず渡すようなインタフェースになっているので、 + // やむなくUtc.ymd(1970, 1, 1).and_hms(0, 0, 0)を渡している。 + + // Messageクラスのinsertメソッドの引数をDateTimeからOption>に変更して、 + // insertメソッドでOption::Noneが渡された場合に時刻不明だと分かるように表示させるような実装にした方がいいかも + let utc_event_time = event_time + .and_then(|datetime| { + let utc = Utc.from_local_datetime(&datetime.naive_utc()).unwrap(); + return Option::Some(utc); + }) + .or(Option::Some(Utc.ymd(1970, 1, 1).and_hms(0, 0, 0))); + message.insert(utc_event_time.unwrap(), event_record.to_string()) }); }); // output message message.debug(); } + + fn get_event_time(event_record: &Value) -> Option> { + let system_time = + &event_record["Event"]["System"]["TimeCreated"]["#attributes"]["SystemTime"]; + let system_time_str = system_time.as_str().unwrap_or(""); + if system_time_str.is_empty() { + return Option::None; + } + + return DateTime::parse_from_rfc3339(system_time_str).ok(); + } } diff --git a/src/detections/rule.rs b/src/detections/rule.rs index 2623dff3..db1d249e 100644 --- a/src/detections/rule.rs +++ b/src/detections/rule.rs @@ -383,6 +383,7 @@ impl LeafMatcher for RegexMatcher { ); return Result::Err(vec![errmsg]); } + self.re = re_result.ok(); return Result::Ok(()); } From 129db6f76cb5df8bd28b771bd9974b53a03670ae Mon Sep 17 00:00:00 2001 From: ichiichi11 Date: Sun, 22 Nov 2020 11:18:21 +0900 Subject: [PATCH 05/16] refactoring --- src/detections/detection.rs | 2 +- src/detections/rule.rs | 10 ---------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/src/detections/detection.rs b/src/detections/detection.rs index e0d8e3da..c958f931 100644 --- a/src/detections/detection.rs +++ b/src/detections/detection.rs @@ -6,7 +6,7 @@ use crate::detections::rule; use crate::detections::rule::RuleNode; use crate::yaml::ParseYaml; -use chrono::{DateTime, FixedOffset, ParseError, ParseResult, TimeZone, Utc}; +use chrono::{DateTime, FixedOffset, TimeZone, Utc}; use evtx::EvtxParser; use serde_json::{Error, Value}; diff --git a/src/detections/rule.rs b/src/detections/rule.rs index db1d249e..dfe48b8b 100644 --- a/src/detections/rule.rs +++ b/src/detections/rule.rs @@ -268,16 +268,6 @@ impl LeafSelectionNode { fn get_matchers(&self) -> Vec> { return vec![Box::new(RegexMatcher::new())]; } - - // LeafMatcherを取得する。 - fn get_matcher(&self) -> Option> { - let matchers = self.get_matchers(); - let mut match_key_list = self.key_list.clone(); - match_key_list.remove(0); - return matchers - .into_iter() - .find(|matcher| matcher.is_target_key(&match_key_list)); - } } impl SelectionNode for LeafSelectionNode { From 1adcb8c44bd07dc73eccb226e38eb9a9dff6d16e Mon Sep 17 00:00:00 2001 From: ichiichi11 Date: Sun, 22 Nov 2020 12:12:05 +0900 Subject: [PATCH 06/16] refactoring --- src/detections/configs.rs | 33 +----- src/detections/rule.rs | 27 +---- src/detections/utils.rs | 231 ++++++-------------------------------- 3 files changed, 39 insertions(+), 252 deletions(-) diff --git a/src/detections/configs.rs b/src/detections/configs.rs index d0a8ef83..6f7fc38d 100644 --- a/src/detections/configs.rs +++ b/src/detections/configs.rs @@ -1,13 +1,9 @@ +use crate::detections::utils; use clap::{App, AppSettings, Arg, ArgMatches}; use std::collections::HashMap; -use std::fs::File; -use std::io::prelude::*; use std::sync::Once; - #[derive(Clone)] pub struct SingletonReader { - pub regex: Vec>, - pub whitelist: Vec>, pub args: ArgMatches<'static>, pub event_key_alias_config: EventKeyAliasConfig, } @@ -19,8 +15,6 @@ pub fn singleton() -> Box { unsafe { ONCE.call_once(|| { let singleton = SingletonReader { - regex: read_csv("regexes.txt"), - whitelist: read_csv("whitelist.txt"), args: build_app().get_matches(), event_key_alias_config: load_eventkey_alias(), }; @@ -79,7 +73,7 @@ impl EventKeyAliasConfig { fn load_eventkey_alias() -> EventKeyAliasConfig { let mut config = EventKeyAliasConfig::new(); - read_csv("config/eventkey_alias.txt") + utils::read_csv("config/eventkey_alias.txt") .into_iter() .for_each(|line| { if line.len() != 2 { @@ -100,26 +94,3 @@ fn load_eventkey_alias() -> EventKeyAliasConfig { return config; } - -fn read_csv(filename: &str) -> Vec> { - let mut f = File::open(filename).expect("file not found!!!"); - let mut contents: String = String::new(); - let mut ret = vec![]; - if f.read_to_string(&mut contents).is_err() { - return ret; - } - - let mut rdr = csv::Reader::from_reader(contents.as_bytes()); - rdr.records().for_each(|r| { - if r.is_err() { - return; - } - - let line = r.unwrap(); - let mut v = vec![]; - line.iter().for_each(|s| v.push(s.to_string())); - ret.push(v); - }); - - return ret; -} diff --git a/src/detections/rule.rs b/src/detections/rule.rs index dfe48b8b..89d0bb63 100644 --- a/src/detections/rule.rs +++ b/src/detections/rule.rs @@ -1,6 +1,7 @@ extern crate regex; -use crate::detections::configs; +use crate::detections::utils; + use regex::Regex; use serde_json::Value; use yaml_rust::Yaml; @@ -25,28 +26,6 @@ fn parse_detection(yaml: &Yaml) -> Option { } } -pub fn get_event_value<'a>(key: &String, event_value: &'a Value) -> Option<&'a Value> { - if key.len() == 0 { - return Option::None; - } - - let alias_config = configs::singleton().event_key_alias_config; - let event_key = match alias_config.get_event_key(key.to_string()) { - Some(alias_event_key) => alias_event_key, - None => key, - }; - - let mut ret: &Value = event_value; - for key in event_key.split(".") { - if ret.is_object() == false { - return Option::None; - } - ret = &ret[key]; - } - - return Option::Some(ret); -} - fn concat_selection_key(key_list: &Vec) -> String { return key_list .iter() @@ -261,7 +240,7 @@ impl LeafSelectionNode { return Option::None; } - return get_event_value(&self.key_list[0].to_string(), event_value); + return utils::get_event_value(&self.key_list[0].to_string(), event_value); } // LeafMatcherの一覧を取得する。 diff --git a/src/detections/utils.rs b/src/detections/utils.rs index 728d3199..db3ddc97 100644 --- a/src/detections/utils.rs +++ b/src/detections/utils.rs @@ -3,160 +3,21 @@ extern crate csv; extern crate regex; use crate::detections::configs; -use flate2::read::GzDecoder; + use regex::Regex; +use serde_json::Value; use std::fs::File; use std::io::prelude::*; use std::str; use std::string::String; -pub fn check_command( - event_id: usize, - commandline: &str, - minlength: usize, - servicecmd: usize, - servicename: &str, - creator: &str, -) { - let mut text = "".to_string(); - let mut base64 = "".to_string(); - - let ret = check_whitelist(commandline, "whitelist.txt"); - if (ret) { - return; - } - - if commandline.len() > minlength { - text.push_str("Long Command Line: greater than "); - text.push_str(&minlength.to_string()); - text.push_str("bytes\n"); - } - text.push_str(&check_obfu(commandline)); - text.push_str(&check_regex_old(commandline, 0)); - text.push_str(&check_creator(commandline, creator)); - if Regex::new(r"\-enc.*[A-Za-z0-9/+=]{100}") - .unwrap() - .is_match(commandline) - { - let re = Regex::new(r"^.* \-Enc(odedCommand)? ").unwrap(); - base64.push_str(&re.replace_all(commandline, "")); - } else if Regex::new(r":FromBase64String\(") - .unwrap() - .is_match(commandline) - { - let re = Regex::new(r"^.*:FromBase64String\('*").unwrap(); - base64.push_str(&re.replace_all(commandline, "")); - let re = Regex::new(r"'.*$").unwrap(); - base64.push_str(&re.replace_all(&base64.to_string(), "")); - } - if let Ok(decoded) = base64::decode(&base64) { - if !base64.is_empty() { - if Regex::new(r"Compression.GzipStream.*Decompress") - .unwrap() - .is_match(commandline) - { - let mut d = GzDecoder::new(decoded.as_slice()); - let mut uncompressed = String::new(); - d.read_to_string(&mut uncompressed).unwrap(); - println!("Decoded : {}", uncompressed); - text.push_str("Base64-encoded and compressed function\n"); - } else { - println!("Decoded : {}", str::from_utf8(decoded.as_slice()).unwrap()); - text.push_str("Base64-encoded function\n"); - text.push_str(&check_obfu(str::from_utf8(decoded.as_slice()).unwrap())); - text.push_str(&check_regex_old( - str::from_utf8(decoded.as_slice()).unwrap(), - 0, - )); - } - } - } - if !text.is_empty() { - println!("EventID : {}", event_id); - if servicecmd != 0 { - println!("Message : Suspicious Service Command"); - println!("Results : Service name: {}\n", servicename); - } else { - println!("Message : Suspicious Command Line"); - } - println!("command : {}", commandline); - println!("result : {}", text); - } -} - -fn check_obfu(string: &str) -> std::string::String { - let mut obfutext = "".to_string(); - let lowercasestring = string.to_lowercase(); - let length = lowercasestring.len() as f64; - let mut minpercent = 0.65; - let maxbinary = 0.50; - - let mut re = Regex::new(r"[a-z0-9/¥;:|.]").unwrap(); - let noalphastring = re.replace_all(&lowercasestring, ""); - - re = Regex::new(r"[01]").unwrap(); - let nobinarystring = re.replace_all(&lowercasestring, ""); - - if length > 0.0 { - let mut percent = (length - noalphastring.len() as f64) / length; - if ((length / 100.0) as f64) < minpercent { - minpercent = length / 100.0; - } - - if percent < minpercent { - obfutext.push_str("Possible command obfuscation: only "); - let percent = (percent * 100.0) as usize; - obfutext.push_str(&percent.to_string()); - obfutext.push_str("% alphanumeric and common symbols\n"); - } - - percent = ((nobinarystring.len().wrapping_sub(length as usize) as f64) / length) / length; - let binarypercent = 1.0 - percent; - if binarypercent > maxbinary { - obfutext.push_str("Possible command obfuscation: "); - let binarypercent = (binarypercent * 100.0) as usize; - obfutext.push_str(&binarypercent.to_string()); - obfutext.push_str("% zeroes and ones (possible numeric or binary encoding)\n"); - } - } - return obfutext; -} - -pub fn check_regex_old(string: &str, r#type: usize) -> std::string::String { +pub fn check_regex( + string: &str, + r#type: usize, + regex_list: &Vec>, +) -> std::string::String { let empty = "".to_string(); let mut regextext = "".to_string(); - for line in configs::singleton().regex { - let type_str = line.get(0).unwrap_or(&empty); - if type_str != &r#type.to_string() { - continue; - } - - let regex_str = line.get(1).unwrap_or(&empty); - if regex_str.is_empty() { - continue; - } - - let re = Regex::new(regex_str); - if re.is_err() || re.unwrap().is_match(string) == false { - continue; - } - - let text = line.get(2).unwrap_or(&empty); - if text.is_empty() { - continue; - } - - regextext.push_str(text); - regextext.push_str("\n"); - } - - return regextext; -} - -pub fn check_regex(string: &str, r#type: usize, regex_path: &str) -> std::string::String { - let empty = "".to_string(); - let mut regextext = "".to_string(); - let regex_list = read_csv(regex_path); for line in regex_list { let type_str = line.get(0).unwrap_or(&empty); if type_str != &r#type.to_string() { @@ -185,9 +46,8 @@ pub fn check_regex(string: &str, r#type: usize, regex_path: &str) -> std::string return regextext; } -pub fn check_whitelist(target: &str, whitelist_path: &str) -> bool { +pub fn check_whitelist(target: &str, whitelist: &Vec>) -> bool { let empty = "".to_string(); - let whitelist = read_csv(whitelist_path); for line in whitelist { let r_str = line.get(0).unwrap_or(&empty); if r_str.is_empty() { @@ -203,7 +63,7 @@ pub fn check_whitelist(target: &str, whitelist_path: &str) -> bool { return false; } -fn read_csv(filename: &str) -> Vec> { +pub fn read_csv(filename: &str) -> Vec> { let mut f = File::open(filename).expect("file not found!!!"); let mut contents: String = String::new(); let mut ret = vec![]; @@ -226,22 +86,26 @@ fn read_csv(filename: &str) -> Vec> { return ret; } -fn check_creator(command: &str, creator: &str) -> std::string::String { - let mut creatortext = "".to_string(); - if !creator.is_empty() { - if command == "powershell" { - if creator == "PSEXESVC" { - creatortext.push_str("PowerShell launched via PsExec: "); - creatortext.push_str(creator); - creatortext.push_str("\n"); - } else if creator == "WmiPrvSE" { - creatortext.push_str("PowerShell launched via WMI: "); - creatortext.push_str(creator); - creatortext.push_str("\n"); - } - } +pub fn get_event_value<'a>(key: &String, event_value: &'a Value) -> Option<&'a Value> { + if key.len() == 0 { + return Option::None; } - return creatortext; + + let alias_config = configs::singleton().event_key_alias_config; + let event_key = match alias_config.get_event_key(key.to_string()) { + Some(alias_event_key) => alias_event_key, + None => key, + }; + + let mut ret: &Value = event_value; + for key in event_key.split(".") { + if ret.is_object() == false { + return Option::None; + } + ret = &ret[key]; + } + + return Option::Some(ret); } #[cfg(test)] @@ -249,48 +113,21 @@ mod tests { use crate::detections::utils; #[test] fn test_check_regex() { - let regextext = utils::check_regex("\\cvtres.exe", 0, "regexes.txt"); + let regexes = utils::read_csv("regexes.txt"); + let regextext = utils::check_regex("\\cvtres.exe", 0, ®exes); assert!(regextext == "Resource File To COFF Object Conversion Utility cvtres.exe\n"); - let regextext = utils::check_regex("\\hogehoge.exe", 0, "regexes.txt"); + let regextext = utils::check_regex("\\hogehoge.exe", 0, ®exes); assert!(regextext == ""); } - #[test] - fn test_check_creator() { - let mut creatortext = utils::check_creator("powershell", "PSEXESVC"); - assert!(creatortext == "PowerShell launched via PsExec: PSEXESVC\n"); - creatortext = utils::check_creator("powershell", "WmiPrvSE"); - assert!(creatortext == "PowerShell launched via WMI: WmiPrvSE\n"); - } - - #[test] - fn test_check_obfu() { - let obfutext = utils::check_obfu("string"); - assert!(obfutext == "Possible command obfuscation: 100% zeroes and ones (possible numeric or binary encoding)\n"); - } - - #[test] - fn test_check_command() { - utils::check_command(1, "dir", 100, 100, "dir", "dir"); - - //test return with whitelist. - utils::check_command( - 1, - "\"C:\\Program Files\\Google\\Update\\GoogleUpdate.exe\"", - 100, - 100, - "dir", - "dir", - ); - } - #[test] fn test_check_whitelist() { let commandline = "\"C:\\Program Files\\Google\\Update\\GoogleUpdate.exe\""; - assert!(true == utils::check_whitelist(commandline, "whitelist.txt")); + let whitelist = utils::read_csv("whitelist.txt"); + assert!(true == utils::check_whitelist(commandline, &whitelist)); let commandline = "\"C:\\Program Files\\Google\\Update\\GoogleUpdate2.exe\""; - assert!(false == utils::check_whitelist(commandline, "whitelist.txt")); + assert!(false == utils::check_whitelist(commandline, &whitelist)); } } From 30b35837a7b1983de8c42bce61aada19c8f4ccf1 Mon Sep 17 00:00:00 2001 From: ichiichi11 Date: Sun, 22 Nov 2020 12:42:45 +0900 Subject: [PATCH 07/16] refactoring --- src/detections/detection.rs | 6 +++++- src/detections/rule.rs | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/detections/detection.rs b/src/detections/detection.rs index c958f931..58684d39 100644 --- a/src/detections/detection.rs +++ b/src/detections/detection.rs @@ -12,6 +12,7 @@ use serde_json::{Error, Value}; const DIRPATH_RULES: &str = "rules"; +// TODO テストケースかかなきゃ... #[derive(Debug)] pub struct Detection {} @@ -64,8 +65,11 @@ impl Detection { err_msgs_result.err().iter().for_each(|err_msgs| { // TODO 本当はファイルパスを出力したい // ParseYamlの変更が必要なので、一旦yamlのタイトルを表示。 + + // TODO エラーの出力方法を統一したい。 + // エラー出力用のクラスを作成してもいいかも println!( - "[Failed to parse Rule file. See following detail. rule file title:{}]", + "[ERROR] Failed to parse Rule file. (Error Rule Title : {})", rule.yaml["title"].as_str().unwrap_or("") ); err_msgs.iter().for_each(|err_msg| println!("{}", err_msg)); diff --git a/src/detections/rule.rs b/src/detections/rule.rs index 89d0bb63..ccdf89fa 100644 --- a/src/detections/rule.rs +++ b/src/detections/rule.rs @@ -6,6 +6,7 @@ use regex::Regex; use serde_json::Value; use yaml_rust::Yaml; +// TODO テストケースかかなきゃ... pub fn parse_rule(yaml: Yaml) -> RuleNode { let detection = parse_detection(&yaml); @@ -358,6 +359,8 @@ impl LeafMatcher for RegexMatcher { } 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; From 712f090919ce426e138d1d1e9d5c0262f4f72a69 Mon Sep 17 00:00:00 2001 From: ichiichi11 Date: Sun, 22 Nov 2020 14:42:10 +0900 Subject: [PATCH 08/16] rule file implemented. --- src/detections/configs.rs | 32 +++---- src/detections/rule.rs | 191 +++++++++++++++++++++++++++++++++++++- src/detections/utils.rs | 11 ++- 3 files changed, 211 insertions(+), 23 deletions(-) diff --git a/src/detections/configs.rs b/src/detections/configs.rs index 6f7fc38d..601210e0 100644 --- a/src/detections/configs.rs +++ b/src/detections/configs.rs @@ -73,24 +73,24 @@ impl EventKeyAliasConfig { fn load_eventkey_alias() -> EventKeyAliasConfig { let mut config = EventKeyAliasConfig::new(); - utils::read_csv("config/eventkey_alias.txt") - .into_iter() - .for_each(|line| { - if line.len() != 2 { - return; - } + let read_result = utils::read_csv("config/eventkey_alias.txt"); + // eventkey_alisasが読み込めなかったらエラーで終了とする。 + read_result.unwrap().into_iter().for_each(|line| { + if line.len() != 2 { + return; + } - let empty = &"".to_string(); - let alias = line.get(0).unwrap_or(empty); - let event_key = line.get(1).unwrap_or(empty); - if alias.len() == 0 || event_key.len() == 0 { - return; - } + let empty = &"".to_string(); + let alias = line.get(0).unwrap_or(empty); + let event_key = line.get(1).unwrap_or(empty); + if alias.len() == 0 || event_key.len() == 0 { + return; + } - config - .key_to_eventkey - .insert(alias.to_owned(), event_key.to_owned()); - }); + config + .key_to_eventkey + .insert(alias.to_owned(), event_key.to_owned()); + }); return config; } diff --git a/src/detections/rule.rs b/src/detections/rule.rs index ccdf89fa..f8ffbeef 100644 --- a/src/detections/rule.rs +++ b/src/detections/rule.rs @@ -246,7 +246,12 @@ impl LeafSelectionNode { // LeafMatcherの一覧を取得する。 fn get_matchers(&self) -> Vec> { - return vec![Box::new(RegexMatcher::new())]; + return vec![ + Box::new(RegexMatcher::new()), + Box::new(MinlengthMatcher::new()), + Box::new(RegexesFileMatcher::new()), + Box::new(WhitelistFileMatcher::new()), + ]; } } @@ -275,6 +280,13 @@ impl SelectionNode for LeafSelectionNode { )]); } + 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() @@ -316,7 +328,15 @@ impl RegexMatcher { impl LeafMatcher for RegexMatcher { fn is_target_key(&self, key_list: &Vec) -> bool { - return key_list.is_empty(); + 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, select_value: &Yaml) -> Result<(), Vec> { @@ -378,3 +398,170 @@ impl LeafMatcher for RegexMatcher { }; } } + +// 指定された文字数以上であることをチェックするクラス。 +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) -> bool { + if key_list.len() != 1 { + return false; + } + + return key_list.get(0).unwrap() == "min_length"; + } + + fn init(&mut self, key_list: &Vec, select_value: &Yaml) -> Result<(), Vec> { + 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>, +} + +impl RegexesFileMatcher { + fn new() -> RegexesFileMatcher { + return RegexesFileMatcher { + regexes_csv_content: vec![], + }; + } +} + +impl LeafMatcher for RegexesFileMatcher { + fn is_target_key(&self, key_list: &Vec) -> bool { + if key_list.len() != 1 { + return false; + } + + return key_list.get(0).unwrap() == "regexes"; + } + + fn init(&mut self, key_list: &Vec, select_value: &Yaml) -> Result<(), Vec> { + 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>, +} + +impl WhitelistFileMatcher { + fn new() -> WhitelistFileMatcher { + return WhitelistFileMatcher { + whitelist_csv_content: vec![], + }; + } +} + +impl LeafMatcher for WhitelistFileMatcher { + fn is_target_key(&self, key_list: &Vec) -> bool { + if key_list.len() != 1 { + return false; + } + + return key_list.get(0).unwrap() == "whitelist"; + } + + fn init(&mut self, key_list: &Vec, select_value: &Yaml) -> Result<(), Vec> { + 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_regex(s, 0, &self.whitelist_csv_content).is_empty(), + Value::Number(n) => { + !utils::check_regex(&n.to_string(), 0, &self.whitelist_csv_content).is_empty() + } + _ => false, + }; + } +} diff --git a/src/detections/utils.rs b/src/detections/utils.rs index db3ddc97..704716a6 100644 --- a/src/detections/utils.rs +++ b/src/detections/utils.rs @@ -63,12 +63,13 @@ pub fn check_whitelist(target: &str, whitelist: &Vec>) -> bool { return false; } -pub fn read_csv(filename: &str) -> Vec> { +pub fn read_csv(filename: &str) -> Result>, String> { let mut f = File::open(filename).expect("file not found!!!"); let mut contents: String = String::new(); let mut ret = vec![]; + let read_res = f.read_to_string(&mut contents); if f.read_to_string(&mut contents).is_err() { - return ret; + return Result::Err(read_res.unwrap_err().to_string()); } let mut rdr = csv::Reader::from_reader(contents.as_bytes()); @@ -83,7 +84,7 @@ pub fn read_csv(filename: &str) -> Vec> { ret.push(v); }); - return ret; + return Result::Ok(ret); } pub fn get_event_value<'a>(key: &String, event_value: &'a Value) -> Option<&'a Value> { @@ -113,7 +114,7 @@ mod tests { use crate::detections::utils; #[test] fn test_check_regex() { - let regexes = utils::read_csv("regexes.txt"); + let regexes = utils::read_csv("regexes.txt").unwrap(); let regextext = utils::check_regex("\\cvtres.exe", 0, ®exes); assert!(regextext == "Resource File To COFF Object Conversion Utility cvtres.exe\n"); @@ -124,7 +125,7 @@ mod tests { #[test] fn test_check_whitelist() { let commandline = "\"C:\\Program Files\\Google\\Update\\GoogleUpdate.exe\""; - let whitelist = utils::read_csv("whitelist.txt"); + let whitelist = utils::read_csv("whitelist.txt").unwrap(); assert!(true == utils::check_whitelist(commandline, &whitelist)); let commandline = "\"C:\\Program Files\\Google\\Update\\GoogleUpdate2.exe\""; From 10873650a83f3007daa70460dad75fcc6caed4de Mon Sep 17 00:00:00 2001 From: ichiichi11 Date: Sun, 22 Nov 2020 15:51:36 +0900 Subject: [PATCH 09/16] fix whitelist bug --- src/detections/rule.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/detections/rule.rs b/src/detections/rule.rs index f8ffbeef..cdd1237c 100644 --- a/src/detections/rule.rs +++ b/src/detections/rule.rs @@ -504,7 +504,7 @@ impl LeafMatcher for RegexesFileMatcher { } } -// ファイルに列挙された文字列に一致しない場合に検知するロジックを表す +// ファイルに列挙された文字列に一致する場合に検知するロジックを表す // DeepBlueCLIのcheck_cmdメソッドの一部に同様の処理が実装されていた。 struct WhitelistFileMatcher { whitelist_csv_content: Vec>, @@ -557,10 +557,9 @@ 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_regex(s, 0, &self.whitelist_csv_content).is_empty(), - Value::Number(n) => { - !utils::check_regex(&n.to_string(), 0, &self.whitelist_csv_content).is_empty() - } + 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, }; } From 2663d3001b95b3210bf56ea1fc617997b631352b Mon Sep 17 00:00:00 2001 From: ichiichi11 Date: Sun, 22 Nov 2020 16:04:39 +0900 Subject: [PATCH 10/16] refacotring --- src/detections/detection.rs | 4 ++-- src/detections/rule.rs | 34 +++++++++++++++++----------------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/detections/detection.rs b/src/detections/detection.rs index 58684d39..2546b2bf 100644 --- a/src/detections/detection.rs +++ b/src/detections/detection.rs @@ -51,7 +51,7 @@ impl Detection { } // parse rule files - let selection_rules: Vec = rulefile_loader + let mut selection_rules: Vec = rulefile_loader .files .into_iter() .map(|rule_file| rule::parse_rule(rule_file)) @@ -82,7 +82,7 @@ impl Detection { // selection rule files and collect message let mut message = Message::new(); - selection_rules.iter().for_each(|rule| { + selection_rules.iter_mut().for_each(|rule| { &event_records .iter() .filter(|event_record| rule.select(event_record)) diff --git a/src/detections/rule.rs b/src/detections/rule.rs index cdd1237c..ea81f59b 100644 --- a/src/detections/rule.rs +++ b/src/detections/rule.rs @@ -89,11 +89,11 @@ impl RuleNode { return self.detection.as_mut().unwrap().init(); } - pub fn select(&self, event_record: &Value) -> bool { + pub fn select(&mut self, event_record: &Value) -> bool { let selection = self .detection - .as_ref() - .and_then(|detect_node| detect_node.selection.as_ref()); + .as_mut() + .and_then(|detect_node| detect_node.selection.as_mut()); if selection.is_none() { return false; } @@ -119,7 +119,7 @@ impl DetectionNode { // Ruleファイルの detection- selection配下のノードはこのtraitを実装する。 trait SelectionNode { - fn select(&self, event_record: &Value) -> bool; + fn select(&mut self, event_record: &Value) -> bool; fn init(&mut self) -> Result<(), Vec>; } @@ -137,9 +137,9 @@ impl AndSelectionNode { } impl SelectionNode for AndSelectionNode { - fn select(&self, event_record: &Value) -> bool { - return self.child_nodes.iter().all(|child_node| { - return child_node.as_ref().select(event_record); + fn select(&mut self, event_record: &Value) -> bool { + return self.child_nodes.iter_mut().all(|child_node| { + return child_node.select(event_record); }); } @@ -185,9 +185,9 @@ impl OrSelectionNode { } impl SelectionNode for OrSelectionNode { - fn select(&self, event_record: &Value) -> bool { - return self.child_nodes.iter().any(|child_node| { - return child_node.as_ref().select(event_record); + fn select(&mut self, event_record: &Value) -> bool { + return self.child_nodes.iter_mut().any(|child_node| { + return child_node.select(event_record); }); } @@ -256,13 +256,13 @@ impl LeafSelectionNode { } impl SelectionNode for LeafSelectionNode { - fn select(&self, event_record: &Value) -> bool { + fn select(&mut self, event_record: &Value) -> bool { if self.matcher.is_none() { return false; } let event_value = self.get_event_value(event_record); - return self.matcher.as_ref().unwrap().is_match(event_value); + return self.matcher.as_mut().unwrap().is_match(event_value); } fn init(&mut self) -> Result<(), Vec> { @@ -303,7 +303,7 @@ impl SelectionNode for LeafSelectionNode { trait LeafMatcher { fn is_target_key(&self, key_list: &Vec) -> bool; - fn is_match(&self, event_value: Option<&Value>) -> bool; + fn is_match(&mut self, event_value: Option<&Value>) -> bool; fn init(&mut self, key_list: &Vec, select_value: &Yaml) -> Result<(), Vec>; } @@ -378,7 +378,7 @@ impl LeafMatcher for RegexMatcher { return Result::Ok(()); } - fn is_match(&self, event_value: Option<&Value>) -> bool { + fn is_match(&mut 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() @@ -433,7 +433,7 @@ impl LeafMatcher for MinlengthMatcher { return Result::Ok(()); } - fn is_match(&self, event_value: Option<&Value>) -> bool { + fn is_match(&mut 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, @@ -493,7 +493,7 @@ impl LeafMatcher for RegexesFileMatcher { return Result::Ok(()); } - fn is_match(&self, event_value: Option<&Value>) -> bool { + fn is_match(&mut 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) => { @@ -555,7 +555,7 @@ impl LeafMatcher for WhitelistFileMatcher { return Result::Ok(()); } - fn is_match(&self, event_value: Option<&Value>) -> bool { + fn is_match(&mut 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), From b2a2b5e6727e3d7e7b04aaa46e4ac7d75d1d5878 Mon Sep 17 00:00:00 2001 From: ichiichi11 Date: Sun, 22 Nov 2020 16:16:40 +0900 Subject: [PATCH 11/16] remove unneccesary file. --- src/detections/system.rs | 110 --------------------------------------- 1 file changed, 110 deletions(-) delete mode 100644 src/detections/system.rs diff --git a/src/detections/system.rs b/src/detections/system.rs deleted file mode 100644 index 68ab263c..00000000 --- a/src/detections/system.rs +++ /dev/null @@ -1,110 +0,0 @@ -use crate::detections::utils; -use crate::models::event; -use std::collections::HashMap; - -pub struct System {} - -impl System { - pub fn new() -> System { - System {} - } - - pub fn detection( - &mut self, - event_id: String, - system: &event::System, - event_data: HashMap, - ) { - self.system_log_clear(&event_id); - self.windows_event_log(&event_id, &event_data); - self.new_service_created(&event_id, &event_data); - self.interactive_service_warning(&event_id, &event_data); - self.suspicious_service_name(&event_id, &event_data); - } - - fn new_service_created(&mut self, event_id: &String, event_data: &HashMap) { - if event_id != "7045" { - return; - } - - let default = String::from(""); - let servicename = &event_data.get("ServiceName").unwrap_or(&default); - let commandline = &event_data.get("ImagePath").unwrap_or(&default); - let text = utils::check_regex_old(&servicename, 1); - if !text.is_empty() { - println!("Message : New Service Created"); - println!("Command : {}", commandline); - println!("Results : Service name: {}", servicename); - println!("Results : {}", text); - } - if !commandline.is_empty() { - utils::check_command(7045, &commandline, 1000, 0, &servicename, &""); - } - } - - fn interactive_service_warning( - &mut self, - event_id: &String, - event_data: &HashMap, - ) { - if event_id != "7030" { - return; - } - - let default = String::from(""); - let servicename = &event_data.get("param1").unwrap_or(&default); - println!("Message : Interactive service warning"); - println!("Results : Service name: {}", servicename); - println!("Results : Malware (and some third party software) trigger this warning"); - println!("{}", utils::check_regex_old(&servicename, 1)); - } - - fn suspicious_service_name(&mut self, event_id: &String, event_data: &HashMap) { - if event_id != "7036" { - return; - } - - let default = String::from(""); - let servicename = &event_data.get("param1").unwrap_or(&default); - let text = utils::check_regex_old(&servicename, 1); - if !text.is_empty() { - println!("Message : Suspicious Service Name"); - println!("Results : Service name: {}", servicename); - println!("Results : {}", text); - } - } - - fn system_log_clear(&mut self, event_id: &String) { - if event_id != "104" { - return; - } - - println!("Message : System Log Clear"); - println!("Results : The System log was cleared."); - } - - fn windows_event_log(&mut self, event_id: &String, event_data: &HashMap) { - if event_id != "7040" { - return; - } - - if let Some(_param1) = event_data.get("param1") { - if _param1 == "Windows Event Log" { - println!("Service name : {}", _param1); - if let Some(_param2) = event_data.get("param2") { - if _param2 == "disabled" { - println!("Message : Event Log Service Stopped"); - println!( - "Results : Selective event log manipulation may follow this event." - ); - } else if _param2 == "auto start" { - println!("Message : Event Log Service Started"); - println!( - "Results : Selective event log manipulation may precede this event." - ); - } - } - } - } - } -} From 540eb8f4f562046d61f8046e811ebfe63e2c40fb Mon Sep 17 00:00:00 2001 From: ichiichi11 Date: Sun, 22 Nov 2020 23:49:46 +0900 Subject: [PATCH 12/16] modify comment --- src/detections/configs.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/detections/configs.rs b/src/detections/configs.rs index 601210e0..4404b147 100644 --- a/src/detections/configs.rs +++ b/src/detections/configs.rs @@ -74,7 +74,7 @@ fn load_eventkey_alias() -> EventKeyAliasConfig { let mut config = EventKeyAliasConfig::new(); let read_result = utils::read_csv("config/eventkey_alias.txt"); - // eventkey_alisasが読み込めなかったらエラーで終了とする。 + // eventkey_aliasが読み込めなかったらエラーで終了とする。 read_result.unwrap().into_iter().for_each(|line| { if line.len() != 2 { return; From 43cfd814a52b0bf4d121126e9b2ecb7345bc7d07 Mon Sep 17 00:00:00 2001 From: akiranishikawa Date: Sun, 29 Nov 2020 10:16:08 +0900 Subject: [PATCH 13/16] message print --- config/eventkey_alias.txt | 6 +- src/detections/common.rs | 44 ------------ src/detections/configs.rs | 2 +- src/detections/detection.rs | 37 +++++----- src/detections/mod.rs | 1 - src/detections/print.rs | 112 ++++++++++++++++++++++++++++-- src/lib.rs | 1 - src/main.rs | 7 +- src/models/event.rs | 133 ------------------------------------ src/models/mod.rs | 1 - 10 files changed, 130 insertions(+), 214 deletions(-) delete mode 100644 src/detections/common.rs delete mode 100644 src/models/event.rs delete mode 100644 src/models/mod.rs diff --git a/config/eventkey_alias.txt b/config/eventkey_alias.txt index be9beeac..12fb0ef3 100644 --- a/config/eventkey_alias.txt +++ b/config/eventkey_alias.txt @@ -2,6 +2,7 @@ alias,event_key EventID,Event.System.EventID Channel,Event.System.Channel CommandLine,Event.EventData.CommandLine +ParentProcessName,Event.EventData.ParentProcessName Signed,Event.EventData.Signed ProcessName,Event.EventData.ProcessName AccessMask,Event.EventData.AccessMask @@ -12,4 +13,7 @@ ServiceName,Event.EventData.ServiceName ImagePath,Event.EventData.ImagePath ContextInfo,Event.EventData.ContextInfo Path,Event.EventData.Path -ScriptBlockText,Event.EventData.ScriptBlockText#Name \ No newline at end of file +ScriptBlockText,Event.EventData.ScriptBlockText#Name +MemberName,Event.EventData.SubjectUserName +MemberSid,Event.EventData.SubjectUserSid +TargetSid,Event.EventData.TargetSid diff --git a/src/detections/common.rs b/src/detections/common.rs deleted file mode 100644 index bd8782ef..00000000 --- a/src/detections/common.rs +++ /dev/null @@ -1,44 +0,0 @@ -use crate::models::event; -use std::collections::HashMap; - -#[derive(Debug)] -pub struct Common { - record_id: u64, - date: String, - record_id_list: HashMap, -} - -impl Common { - pub fn new() -> Common { - Common { - record_id: 0, - date: "".to_string(), - record_id_list: HashMap::new(), - } - } - - pub fn disp(&self) { - for (record_id, date) in self.record_id_list.iter() { - println!("date:{:?} record-id: {:?}", date, record_id); - } - } - - pub fn detection(&mut self, system: &event::System, event_data: &HashMap) { - self.check_record_id(system); - } - - // - // Record IDがシーケンスになっているかチェック - // - fn check_record_id(&mut self, system: &event::System) { - let event_record_id: u64 = system.event_record_id.parse().unwrap(); - if self.record_id > 0 && event_record_id - self.record_id > 1 { - self.record_id_list.insert( - self.record_id.to_string() + " - " + &system.event_record_id.to_string(), - self.date.to_string() + " - " + &system.time_created.system_time.to_string(), - ); - } - self.record_id = event_record_id; - self.date = system.time_created.system_time.to_string(); - } -} diff --git a/src/detections/configs.rs b/src/detections/configs.rs index 4404b147..d4879f12 100644 --- a/src/detections/configs.rs +++ b/src/detections/configs.rs @@ -53,7 +53,7 @@ fn build_app() -> clap::App<'static, 'static> { .arg(Arg::from_usage("--credits 'Zachary Mathis, Akira Nishikawa'")) } -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct EventKeyAliasConfig { key_to_eventkey: HashMap, } diff --git a/src/detections/detection.rs b/src/detections/detection.rs index 2546b2bf..a62cb6dd 100644 --- a/src/detections/detection.rs +++ b/src/detections/detection.rs @@ -83,29 +83,28 @@ impl Detection { // selection rule files and collect message let mut message = Message::new(); selection_rules.iter_mut().for_each(|rule| { - &event_records - .iter() - .filter(|event_record| rule.select(event_record)) - .for_each(|event_record| { - let event_time = Detection::get_event_time(event_record); - // TODO ログから日付がとれない場合に本当は時刻不明という感じで表示したい。 - // しかし、Messageクラスのinsertメソッドが、UTCクラスのインスタンスを必ず渡すようなインタフェースになっているので、 - // やむなくUtc.ymd(1970, 1, 1).and_hms(0, 0, 0)を渡している。 + event_records.iter().for_each(|event_record| { + if !rule.select(event_record) { + return; + } - // Messageクラスのinsertメソッドの引数をDateTimeからOption>に変更して、 - // insertメソッドでOption::Noneが渡された場合に時刻不明だと分かるように表示させるような実装にした方がいいかも - let utc_event_time = event_time - .and_then(|datetime| { - let utc = Utc.from_local_datetime(&datetime.naive_utc()).unwrap(); - return Option::Some(utc); - }) - .or(Option::Some(Utc.ymd(1970, 1, 1).and_hms(0, 0, 0))); - message.insert(utc_event_time.unwrap(), event_record.to_string()) - }); + let event_time = Detection::get_event_time(event_record); + let utc_event_time = event_time + .and_then(|datetime| { + let utc = Utc.from_local_datetime(&datetime.naive_utc()).unwrap(); + return Option::Some(utc); + }) + .or(Option::None); + message.insert( + utc_event_time, + event_record, + Some(rule.yaml["output"].as_str().unwrap().to_string()), + ) + }); }); // output message - message.debug(); + message.print(); } fn get_event_time(event_record: &Value) -> Option> { diff --git a/src/detections/mod.rs b/src/detections/mod.rs index 2c864e71..48034d62 100644 --- a/src/detections/mod.rs +++ b/src/detections/mod.rs @@ -1,4 +1,3 @@ -mod common; pub mod configs; pub mod detection; pub mod print; diff --git a/src/detections/print.rs b/src/detections/print.rs index 793737e4..1ae3c316 100644 --- a/src/detections/print.rs +++ b/src/detections/print.rs @@ -1,8 +1,12 @@ extern crate chrono; extern crate lazy_static; +use crate::detections::configs; use chrono::{DateTime, TimeZone, Utc}; use lazy_static::lazy_static; +use regex::Regex; +use serde_json::Value; use std::collections::BTreeMap; +use std::collections::HashMap; use std::sync::Mutex; #[derive(Debug)] @@ -21,18 +25,75 @@ impl Message { } /// メッセージを設定 - pub fn insert(&mut self, time: DateTime, message: String) { - match self.map.get_mut(&time) { + pub fn insert( + &mut self, + mut time: Option>, + event_record: &Value, + output: Option, + ) { + if Option::None == output { + return; + } + + let message = &self.parse_message(event_record, output); + + if Option::None == time { + time = Option::Some(Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)); + } + + match self.map.get_mut(&time.unwrap()) { Some(v) => { v.push(message.to_string()); } None => { let m = vec![message.to_string(); 1]; - self.map.insert(time, m); + self.map.insert(time.unwrap(), m); } } } + fn parse_message(&mut self, event_record: &Value, output: Option) -> String { + if Option::None == output { + return "".to_string(); + } + + let mut return_message: String = output.unwrap(); + let mut hash_map: HashMap = HashMap::new(); + let re = Regex::new(r"%[a-zA-Z0-9-_]+%").unwrap(); + for caps in re.captures_iter(&return_message) { + let full_target_str = &caps[0]; + let target_length = full_target_str.chars().count() - 2; // The meaning of 2 is two percent + let target_str = full_target_str + .chars() + .skip(1) + .take(target_length) + .collect::(); + + if let Some(array_str) = configs::singleton() + .event_key_alias_config + .get_event_key(target_str.to_string()) + { + let split: Vec<&str> = array_str.split(".").collect(); + let mut tmp_event_record: &Value = event_record.into(); + for s in split { + if let Some(record) = tmp_event_record.get(s) { + tmp_event_record = record; + } + } + hash_map.insert( + full_target_str.to_string(), + tmp_event_record.as_str().unwrap_or("").to_string(), + ); + } + } + + for (k, v) in &hash_map { + return_message = return_message.replace(k, v); + } + + return_message + } + /// メッセージを返す pub fn get(&self, time: DateTime) -> Vec { match self.map.get(&time) { @@ -45,6 +106,15 @@ impl Message { pub fn debug(&self) { println!("{:?}", self.map); } + + /// 最後に表示を行う + pub fn print(&self) { + for (key, values) in self.map.iter() { + for value in values.iter() { + println!("{} : {}", key, value); + } + } + } } #[test] @@ -53,11 +123,39 @@ fn test_create_and_append_message() { let poke = Utc.ymd(1996, 2, 27).and_hms(1, 5, 1); let taka = Utc.ymd(2000, 1, 21).and_hms(9, 6, 1); - message.insert(poke, "TEST".to_string()); - message.insert(poke, "TEST2".to_string()); - message.insert(taka, "TEST3".to_string()); + let json_str = r#" + { + "Event": { + "EventData": { + "CommandLine": "hoge" + } + } + } + "#; + let event_record: Value = serde_json::from_str(json_str).unwrap(); + + message.insert( + Some(poke), + &event_record, + Some("CommandLine1: %CommandLine%".to_string()), + ); + message.insert( + Some(poke), + &event_record, + Some("CommandLine2: %CommandLine%".to_string()), + ); + message.insert( + Some(taka), + &event_record, + Some("CommandLine3: %CommandLine%".to_string()), + ); + message.insert( + Option::None, + &event_record, + Some("CommandLine4: %CommandLine%".to_string()), + ); let display = format!("{}", format_args!("{:?}", message)); - let expect = "Message { map: {1996-02-27T01:05:01Z: [\"TEST\", \"TEST2\"], 2000-01-21T09:06:01Z: [\"TEST3\"]} }"; + let expect = "Message { map: {1970-01-01T00:00:00Z: [\"CommandLine4: hoge\"], 1996-02-27T01:05:01Z: [\"CommandLine1: hoge\", \"CommandLine2: hoge\"], 2000-01-21T09:06:01Z: [\"CommandLine3: hoge\"]} }"; assert_eq!(display, expect); } diff --git a/src/lib.rs b/src/lib.rs index d9abfe41..450cf595 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,3 @@ pub mod detections; -pub mod models; pub mod omikuji; pub mod yaml; diff --git a/src/main.rs b/src/main.rs index c2e44c76..8b4d3264 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,12 +8,7 @@ use yamato_event_analyzer::detections::detection; use yamato_event_analyzer::omikuji::Omikuji; fn main() -> Result<(), DeError> { - let filepath: String = configs::singleton() - .args - .value_of("filepath") - .unwrap_or("") - .to_string(); - if filepath != "" { + if let Some(filepath) = configs::singleton().args.value_of("filepath") { parse_file(&filepath); } diff --git a/src/models/event.rs b/src/models/event.rs deleted file mode 100644 index aaea9312..00000000 --- a/src/models/event.rs +++ /dev/null @@ -1,133 +0,0 @@ -extern crate serde; -use serde::Deserialize; -use std::collections::HashMap; - -#[derive(Debug, Deserialize, PartialEq)] -pub struct Data { - #[serde(rename = "Name")] - pub name: Option, - #[serde(rename = "$value")] - pub text: Option, -} - -#[derive(Debug, Deserialize, PartialEq)] -pub struct TimeCreated { - #[serde(rename = "SystemTime")] - pub system_time: String, -} - -#[derive(Debug, Deserialize, PartialEq)] -struct Execution { - #[serde(rename = "ProcessID")] - process_id: i32, - #[serde(rename = "ThreadID")] - thread_id: i32, -} - -#[derive(Debug, Deserialize, PartialEq)] -pub struct Provider { - #[serde(rename = "Name")] - pub name: Option, - #[serde(rename = "Guid")] - guid: Option, -} - -#[derive(Debug, Deserialize, PartialEq)] -pub struct System { - #[serde(rename = "Provider")] - pub provider: Provider, - #[serde(rename = "EventID")] - pub event_id: String, - #[serde(rename = "Version")] - version: Option, - #[serde(rename = "Level")] - level: String, - #[serde(rename = "Task")] - task: String, - #[serde(rename = "Opcode")] - opcode: Option, - #[serde(rename = "Keywords")] - keywords: String, - #[serde(rename = "TimeCreated")] - pub time_created: TimeCreated, - #[serde(rename = "EventRecordID")] - pub event_record_id: String, - #[serde(rename = "Correlation")] - correlation: Option, - #[serde(rename = "Execution")] - execution: Option, - #[serde(rename = "Channel")] - pub channel: String, // Security, System, Application ...etc - #[serde(rename = "Computer")] - computer: String, - #[serde(rename = "Security")] - security: String, - #[serde(rename = "Message")] - pub message: Option, -} - -#[derive(Debug, Deserialize, PartialEq)] -pub struct EventData { - #[serde(rename = "Data")] - pub data: Option>, -} - -#[derive(Debug, Deserialize, PartialEq)] -pub struct UserData { - #[serde(rename = "LogFileCleared")] - pub log_file_cleared: Option, -} - -#[derive(Debug, Deserialize, PartialEq)] -pub struct LogFileCleared { - #[serde(rename = "SubjectUserSid")] - pub subject_user_sid: Option, - #[serde(rename = "SubjectUserName")] - pub subject_user_name: Option, - #[serde(rename = "SubjectDomainName")] - pub subject_domain_name: Option, - #[serde(rename = "SubjectLogonId")] - pub subject_logon_id: Option, -} - -#[derive(Debug, Deserialize, PartialEq)] -pub struct Evtx { - #[serde(rename = "System")] - pub system: System, - #[serde(rename = "EventData")] - pub event_data: Option, - #[serde(rename = "UserData")] - pub user_data: Option, -} - -impl Evtx { - // - // 文字列データを取得する - // - fn get_string(v: &Data) -> String { - let mut ret = "".to_string(); - if let Some(text) = &v.text { - ret = text.to_string(); - } - return ret; - } - - // - // EventDataをHashMapとして取得する - // - pub fn parse_event_data(&self) -> HashMap { - let mut values = HashMap::new(); - - if let Some(event_data) = &self.event_data { - if let Some(data) = &event_data.data { - for v in data.iter() { - if let Some(name) = &v.name { - values.insert(name.to_string(), Evtx::get_string(v)); - } - } - } - } - - values - } -} diff --git a/src/models/mod.rs b/src/models/mod.rs deleted file mode 100644 index 53f11265..00000000 --- a/src/models/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod event; From 0e3d2ebaf456b73f294ccba8d8d63718f43226df Mon Sep 17 00:00:00 2001 From: ichiichi11 Date: Mon, 30 Nov 2020 21:17:30 +0900 Subject: [PATCH 14/16] refactoring --- src/afterfact.rs | 53 ++++++++----- src/detections/detection.rs | 21 +---- src/detections/print.rs | 152 ++++++++++++++++++++++++------------ src/detections/rule.rs | 22 +++++- 4 files changed, 154 insertions(+), 94 deletions(-) diff --git a/src/afterfact.rs b/src/afterfact.rs index 4d62238c..002d3d1a 100644 --- a/src/afterfact.rs +++ b/src/afterfact.rs @@ -1,6 +1,6 @@ use crate::detections::configs; use crate::detections::print; -use chrono::{DateTime, TimeZone, Utc}; +use chrono::{DateTime, Utc}; use serde::Serialize; use std::error::Error; use std::process; @@ -37,39 +37,50 @@ fn emit_csv(path: &str) -> Result<(), Box> { Ok(()) } -use serde_json::Value; -use std::fs::{read_to_string, remove_file}; -use std::io::Read; +#[cfg(test)] +mod tests { -#[test] -fn test_emit_csv() { - { - let mut messages = print::MESSAGES.lock().unwrap(); - let poke = Utc.ymd(1996, 2, 27).and_hms(1, 5, 1); - let json_str = r#" + use crate::afterfact::emit_csv; + use crate::detections::print; + use serde_json::Value; + use std::fs::{read_to_string, remove_file}; + + #[test] + fn test_emit_csv() { + { + let mut messages = print::MESSAGES.lock().unwrap(); + let json_str = r##" { "Event": { "EventData": { "CommandLine": "hoge" + }, + "System": { + "TimeCreated": { + "#attributes":{ + "SystemTime": "1996-02-27T01:05:01Z" + } + } } } } - "#; - let event_record: Value = serde_json::from_str(json_str).unwrap(); + "##; + let event_record: Value = serde_json::from_str(json_str).unwrap(); - messages.insert(Some(poke), &event_record, Some("pokepoke".to_string())); - } + messages.insert(&event_record, "pokepoke".to_string()); + } - let expect = "Time,Message + let expect = "Time,Message 1996-02-27T01:05:01Z,pokepoke "; - assert!(emit_csv(&"./test_emit_csv.csv".to_string()).is_ok()); + assert!(emit_csv(&"./test_emit_csv.csv".to_string()).is_ok()); - match read_to_string("./test_emit_csv.csv") { - Err(_) => panic!("Failed to open file"), - Ok(s) => assert_eq!(s, expect), - }; + match read_to_string("./test_emit_csv.csv") { + Err(_) => panic!("Failed to open file"), + Ok(s) => assert_eq!(s, expect), + }; - assert!(remove_file("./test_emit_csv.csv").is_ok()); + assert!(remove_file("./test_emit_csv.csv").is_ok()); + } } diff --git a/src/detections/detection.rs b/src/detections/detection.rs index a62cb6dd..20977de4 100644 --- a/src/detections/detection.rs +++ b/src/detections/detection.rs @@ -88,17 +88,9 @@ impl Detection { return; } - let event_time = Detection::get_event_time(event_record); - let utc_event_time = event_time - .and_then(|datetime| { - let utc = Utc.from_local_datetime(&datetime.naive_utc()).unwrap(); - return Option::Some(utc); - }) - .or(Option::None); message.insert( - utc_event_time, event_record, - Some(rule.yaml["output"].as_str().unwrap().to_string()), + rule.yaml["output"].as_str().unwrap_or("").to_string(), ) }); }); @@ -106,15 +98,4 @@ impl Detection { // output message message.print(); } - - fn get_event_time(event_record: &Value) -> Option> { - let system_time = - &event_record["Event"]["System"]["TimeCreated"]["#attributes"]["SystemTime"]; - let system_time_str = system_time.as_str().unwrap_or(""); - if system_time_str.is_empty() { - return Option::None; - } - - return DateTime::parse_from_rfc3339(system_time_str).ok(); - } } diff --git a/src/detections/print.rs b/src/detections/print.rs index 85189485..606d9f6e 100644 --- a/src/detections/print.rs +++ b/src/detections/print.rs @@ -25,39 +25,28 @@ impl Message { } /// メッセージを設定 - pub fn insert( - &mut self, - mut time: Option>, - event_record: &Value, - output: Option, - ) { - if Option::None == output { + pub fn insert(&mut self, event_record: &Value, output: String) { + if output.is_empty() { return; } let message = &self.parse_message(event_record, output); + let default_time = Utc.ymd(1970, 1, 1).and_hms(0, 0, 0); + let time = Message::get_event_time(event_record).unwrap_or(default_time); - if Option::None == time { - time = Option::Some(Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)); - } - - match self.map.get_mut(&time.unwrap()) { + match self.map.get_mut(&time) { Some(v) => { v.push(message.to_string()); } None => { let m = vec![message.to_string(); 1]; - self.map.insert(time.unwrap(), m); + self.map.insert(time, m); } } } - fn parse_message(&mut self, event_record: &Value, output: Option) -> String { - if Option::None == output { - return "".to_string(); - } - - let mut return_message: String = output.unwrap(); + fn parse_message(&mut self, event_record: &Value, output: String) -> String { + let mut return_message: String = output; let mut hash_map: HashMap = HashMap::new(); let re = Regex::new(r"%[a-zA-Z0-9-_]+%").unwrap(); for caps in re.captures_iter(&return_message) { @@ -119,47 +108,110 @@ impl Message { pub fn iter(&self) -> &BTreeMap, Vec> { &self.map } + + fn get_event_time(event_record: &Value) -> Option> { + let system_time = + &event_record["Event"]["System"]["TimeCreated"]["#attributes"]["SystemTime"]; + let system_time_str = system_time.as_str().unwrap_or(""); + if system_time_str.is_empty() { + return Option::None; + } + + let rfc3339_time = DateTime::parse_from_rfc3339(system_time_str); + if rfc3339_time.is_err() { + return Option::None; + } + let datetime = Utc + .from_local_datetime(&rfc3339_time.unwrap().naive_utc()) + .single(); + if datetime.is_none() { + return Option::None; + } else { + return Option::Some(datetime.unwrap()); + } + } } -#[test] -fn test_create_and_append_message() { - let mut message = Message::new(); - let poke = Utc.ymd(1996, 2, 27).and_hms(1, 5, 1); - let taka = Utc.ymd(2000, 1, 21).and_hms(9, 6, 1); +#[cfg(test)] +mod tests { + use crate::detections::print::Message; + use serde_json::Value; - let json_str = r#" + #[test] + fn test_create_and_append_message() { + let mut message = Message::new(); + let json_str_1 = r##" { "Event": { "EventData": { "CommandLine": "hoge" + }, + "System": { + "TimeCreated": { + "#attributes":{ + "SystemTime": "1996-02-27T01:05:01Z" + } + } } } } - "#; - let event_record: Value = serde_json::from_str(json_str).unwrap(); + "##; + let event_record_1: Value = serde_json::from_str(json_str_1).unwrap(); + message.insert(&event_record_1, "CommandLine1: %CommandLine%".to_string()); - message.insert( - Some(poke), - &event_record, - Some("CommandLine1: %CommandLine%".to_string()), - ); - message.insert( - Some(poke), - &event_record, - Some("CommandLine2: %CommandLine%".to_string()), - ); - message.insert( - Some(taka), - &event_record, - Some("CommandLine3: %CommandLine%".to_string()), - ); - message.insert( - Option::None, - &event_record, - Some("CommandLine4: %CommandLine%".to_string()), - ); + let json_str_2 = r##" + { + "Event": { + "EventData": { + "CommandLine": "hoge" + }, + "System": { + "TimeCreated": { + "#attributes":{ + "SystemTime": "1996-02-27T01:05:01Z" + } + } + } + } + } + "##; + let event_record_2: Value = serde_json::from_str(json_str_2).unwrap(); + message.insert(&event_record_2, "CommandLine2: %CommandLine%".to_string()); - let display = format!("{}", format_args!("{:?}", message)); - let expect = "Message { map: {1970-01-01T00:00:00Z: [\"CommandLine4: hoge\"], 1996-02-27T01:05:01Z: [\"CommandLine1: hoge\", \"CommandLine2: hoge\"], 2000-01-21T09:06:01Z: [\"CommandLine3: hoge\"]} }"; - assert_eq!(display, expect); + let json_str_3 = r##" + { + "Event": { + "EventData": { + "CommandLine": "hoge" + }, + "System": { + "TimeCreated": { + "#attributes":{ + "SystemTime": "2000-01-21T09:06:01Z" + } + } + } + } + } + "##; + let event_record_3: Value = serde_json::from_str(json_str_3).unwrap(); + message.insert(&event_record_3, "CommandLine3: %CommandLine%".to_string()); + + let json_str_4 = r##" + { + "Event": { + "EventData": { + "CommandLine": "hoge" + } + } + } + "##; + let event_record_4: Value = serde_json::from_str(json_str_4).unwrap(); + message.insert(&event_record_4, "CommandLine4: %CommandLine%".to_string()); + + let display = format!("{}", format_args!("{:?}", message)); + println!("display::::{}", display); + let expect = "Message { map: {1970-01-01T00:00:00Z: [\"CommandLine4: hoge\"], 1996-02-27T01:05:01Z: [\"CommandLine1: hoge\", \"CommandLine2: hoge\"], 2000-01-21T09:06:01Z: [\"CommandLine3: hoge\"]} }"; + assert_eq!(display, expect); + } } diff --git a/src/detections/rule.rs b/src/detections/rule.rs index ea81f59b..161e0462 100644 --- a/src/detections/rule.rs +++ b/src/detections/rule.rs @@ -82,11 +82,27 @@ pub struct RuleNode { impl RuleNode { pub fn init(&mut self) -> Result<(), Vec> { - if self.detection.is_none() { - return Result::Ok(()); + let mut errmsgs: Vec = vec![]; + + // field check + if self.yaml["output"].as_str().is_none() { + errmsgs.push("Cannot find required key. key:output".to_string()); } - return self.detection.as_mut().unwrap().init(); + // 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(&mut self, event_record: &Value) -> bool { From ee96fec8140b959932e969c7aca2eed25981784b Mon Sep 17 00:00:00 2001 From: ichiichi11 Date: Mon, 30 Nov 2020 21:21:24 +0900 Subject: [PATCH 15/16] remove .DS_Store --- .DS_Store | Bin 6148 -> 0 bytes .gitignore | 3 ++- 2 files changed, 2 insertions(+), 1 deletion(-) delete mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index fdd6d02292f1552cb8b744a28e1ffcaf10ff5ceb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK%TB{U3>?!^RpQbk$NdHVAgbCg=noW9sE{Hek@lV&zs>jo+A8JB0c^>h?5w@3 z%pr~e$ohPE09F8&bVYnH zE#7fPhi44+u`>+ZXlrVL6COz(nR}`?uHTw$Dv%1K0;xbM@P8H17E4>+Ic7`+Qh`+9 zTLJw(6uM#)I6B&=gTY1sqW;F4@!4hxVo`$F1dfhmXyTmqocs1yX@OrGU&gyUm)f6uouw xa@uPPeNX=~=2|*~wPK>RVlK25-&c7>pSj-zj*eDN-pYyjBcQsZr2@a9z$YgEB*Xv! diff --git a/.gitignore b/.gitignore index 5ad768f8..7a8a82ab 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /target /samples *.test -/.vscode/ \ No newline at end of file +/.vscode/ +.DS_Store \ No newline at end of file From 9b5f243ff9a6579ecd640a70ba274103f2601ed5 Mon Sep 17 00:00:00 2001 From: ichiichi11 Date: Mon, 30 Nov 2020 21:52:56 +0900 Subject: [PATCH 16/16] refactoring --- src/detections/rule.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/detections/rule.rs b/src/detections/rule.rs index 161e0462..c65e492a 100644 --- a/src/detections/rule.rs +++ b/src/detections/rule.rs @@ -85,7 +85,7 @@ impl RuleNode { let mut errmsgs: Vec = vec![]; // field check - if self.yaml["output"].as_str().is_none() { + if self.yaml["output"].as_str().unwrap_or("").is_empty() { errmsgs.push("Cannot find required key. key:output".to_string()); }