diff --git a/Cargo.lock b/Cargo.lock index 2498b74..5b4ad8f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -533,6 +533,12 @@ version = "0.2.77" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f96b10ec2560088a8e76961b00d47107b3a625fecb76dedb29ee7ccbf98235" +[[package]] +name = "linked-hash-map" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" + [[package]] name = "log" version = "0.4.11" @@ -875,6 +881,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", + "yaml-rust", ] [[package]] @@ -1191,3 +1198,12 @@ dependencies = [ "serde_json", "thiserror", ] + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] diff --git a/Cargo.toml b/Cargo.toml index 5d3aa65..e158803 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ csv = "1.1" base64 = "*" flate2 = "1.0" lazy_static = "1.4.0" +yaml-rust = "0.4.5" [target.x86_64-pc-windows-gnu] linker = "x86_64-w64-mingw32-gcc" diff --git a/config.yml b/config.yml new file mode 100644 index 0000000..e797cbc --- /dev/null +++ b/config.yml @@ -0,0 +1,22 @@ +{ + # Minimum length of command line to alert + "minlength": 1000, + # Set to 1 to alert every admin logon (set to 0 disable this) + "alert_all_admin": 0, + # Set to 1 to show total admin logon (set to 0 disable this) + "show_total_admin_logons": 0, + # if failed logon count exceed this value, Rusty Blue show message, "High number of total logon failures for multiple accounts". + "max_total_failed_logons": 5, + # if failed logon count for specified user exceed this value, Rusty Blue show message, "High number of logon failures for one account". + "max_failed_logons": 5, + # if logon count by specified user exceed this value, Rusty Blue count the user as passspray uniqe user. + "max_passspray_login": 6, + # if passspray uniq user count exceed this value, Rusty Blue show message, "Sensitive Privilege Use Exceeds Threshold". + "max_passspray_uniquser": 6, + # if Sensitive Privilege Use count exceed this value, Rusty Blue show message "Sensitive Privilege Use Exceeds Threshold". + "max_total_sensitive_privuse": 4, + # if rate of non-ascii data exceed this value, Rusty Blue show message Possible command obfuscation + "obfuscation_minpercent": 0.65, + # if rate of binary format data exceed this value, Rusty Blue show message Possible command obfuscation + "obfuscation_maxbinary": 0.50 +} \ No newline at end of file diff --git a/credits.txt b/credits.txt index a403168..4dce1bb 100644 --- a/credits.txt +++ b/credits.txt @@ -6,9 +6,9 @@ hachiyone (twitter:@hach1yone) Developer DustInDark (Github: @hitenkoku) Developer garigariganzy (twitter:@garigariganzy31) Developer 7itoh (twitter:@yNitocrypto22) Developer -dai (twitter: @@__da13__) Developer +dai (twitter: @__da13__) Developer siam(GitHun: @siamease) Developer -mimura (twitter: @@mimura1133) Developer +mimura (twitter: @mimura1133) Developer apt773 (twitter: @apt773) Rule testing and supporter TAKIZAWA Hiroki (twitter:@hr_zwtk) Rule testing and supporter su (GitHub: @su-10) supporter diff --git a/src/detections/configs.rs b/src/detections/configs.rs index c2e5776..1fd11f8 100644 --- a/src/detections/configs.rs +++ b/src/detections/configs.rs @@ -1,10 +1,12 @@ use crate::detections::print::MessageNotation; +use crate::detections::yaml::ParseYaml; use clap::{App, AppSettings, ArgMatches}; use lazy_static::lazy_static; use regex::Regex; use std::collections::HashMap; use std::fs::File; use std::io::prelude::*; +use std::path::PathBuf; lazy_static! { pub static ref CONFIG: ConfigReader = ConfigReader::new(); @@ -28,6 +30,7 @@ pub struct ConfigReader { pub nobinary_regex: Regex, pub regexes: HashMap, pub compress_regex: Regex, + pub configs: yaml_rust::Yaml, } impl ConfigReader { @@ -52,6 +55,7 @@ impl ConfigReader { nobinary_regex: Regex::new(r"[01]").unwrap(), regexes: get_regexes(read_csv("regexes.txt")), compress_regex: Regex::new(r"Compression.GzipStream.*Decompress").unwrap(), + configs: load_config_file(), } } } @@ -83,6 +87,18 @@ fn build_app<'a>() -> ArgMatches<'a> { .get_matches() } +// config.ymlを読み込みます +fn load_config_file() -> yaml_rust::Yaml { + // read file + let mut parser = ParseYaml::new(); + let result = parser.read_yaml_file(PathBuf::from("./config.yml")); + if result.is_err() { + panic!("canot read config file(config.yml)."); + } + + return parser.files.into_iter().next().unwrap(); +} + fn is_test_mode() -> bool { for i in std::env::args() { if i == "--test" { diff --git a/src/detections/mod.rs b/src/detections/mod.rs index 4aaee9f..f6387b6 100644 --- a/src/detections/mod.rs +++ b/src/detections/mod.rs @@ -9,3 +9,4 @@ mod security; mod sysmon; mod system; mod utils; +mod yaml; diff --git a/src/detections/powershell.rs b/src/detections/powershell.rs index 4430f03..e9f54f1 100644 --- a/src/detections/powershell.rs +++ b/src/detections/powershell.rs @@ -2,6 +2,7 @@ use crate::detections::configs; use crate::detections::utils; use crate::models::event; use std::collections::HashMap; +use std::usize; pub struct PowerShell {} @@ -44,7 +45,17 @@ impl PowerShell { .replace_all(&temp_command_with_extra, ""); if command != "" { - utils::check_command(4103, &command, 1000, 0, &default, &default, &system_time); + let configs: &yaml_rust::Yaml = &configs::CONFIG.configs; + let value = configs["minlength"].as_i64().unwrap_or(1000).clone(); + utils::check_command( + 4103, + &command, + value as usize, + 0, + &default, + &default, + &system_time, + ); } } } @@ -64,10 +75,13 @@ impl PowerShell { if path == "".to_string() { let commandline = event_data.get("ScriptBlockText").unwrap_or(&default); if commandline.to_string() != default { + let configs: &yaml_rust::Yaml = &configs::CONFIG.configs; + let value = configs["minlength"].as_i64().unwrap_or(1000).clone(); + utils::check_command( 4104, &commandline, - 1000, + value as usize, 0, &default, &default, diff --git a/src/detections/security.rs b/src/detections/security.rs index 8cd0850..209dd25 100644 --- a/src/detections/security.rs +++ b/src/detections/security.rs @@ -1,21 +1,26 @@ +use lazy_static::__Deref; + use crate::detections::print::MessageNotation; use crate::detections::utils; use crate::models::event; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; + +use super::configs; #[derive(Debug)] pub struct Security { max_total_sensitive_privuse: i32, max_passspray_login: i32, max_passspray_uniquser: i32, + max_total_failed_logons: i32, max_failed_logons: i32, alert_all_admin: i32, + show_total_admin_logons: i32, total_admin_logons: i32, total_failed_logons: i32, total_failed_account: i32, total_sensitive_privuse: i32, - admin_logons: HashMap>, - multiple_admin_logons: HashMap, + admin_logons: HashMap>, account_2_failedcnt: HashMap, passspray_2_user: HashMap, empty_str: String, @@ -23,54 +28,102 @@ pub struct Security { impl Security { pub fn new() -> Security { - Security { + let mut sec = Security { max_total_sensitive_privuse: 4, max_passspray_login: 6, max_passspray_uniquser: 6, + max_total_failed_logons: 5, max_failed_logons: 5, alert_all_admin: 0, + show_total_admin_logons: 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(), - } + }; + sec.setup_configs(); + + return sec; } pub fn disp(&self) { self.disp_admin_logons().and_then(Security::print_console); + self.disp_multiple_sid_logon() + .into_iter() + .for_each(|msges| { + if msges.is_empty() { + return; + } + Security::print_console(msges); + }); self.disp_login_failed().and_then(Security::print_console); + self.disp_login_failed_for_oneuser() + .into_iter() + .for_each(|msges| { + if msges.is_empty() { + return; + } + + Security::print_console(msges); + }); } fn disp_admin_logons(&self) -> Option> { + if self.show_total_admin_logons == 0 { + return Option::None; + } + 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 - )); + msges.push("EventID : 4672".to_string()); + msges.push(format!("Total Admin Logon: {}", self.total_admin_logons)); return Option::Some(msges); } - fn disp_login_failed(&self) -> Option> { - let exceed_failed_logons = self.total_failed_logons <= self.max_failed_logons; + fn disp_multiple_sid_logon(&self) -> Vec> { + if self.total_admin_logons < 1 { + return vec![]; + } + + return self + .admin_logons + .iter() + .filter_map(|(username, sids)| { + if sids.len() < 2 { + return Option::None; + } + + let mut msges = vec![]; + msges.push("EventID : 4672".to_string()); + msges.push("Message : Multiple admin logons for one account".to_string()); + msges.push(format!("Username: {}", username)); + msges.push(format!("User SID Access Count: {}", sids.len())); + + return Option::Some(msges); + }) + .collect(); + } - let exist_failed_account = self.account_2_failedcnt.keys().count() as i32 <= 1; - if exceed_failed_logons || exist_failed_account { + fn disp_login_failed(&self) -> Option> { + if self.total_failed_logons <= self.max_total_failed_logons { + return Option::None; + } + // 複数ユーザーで検知した場合にしか表示しない。 + if self.account_2_failedcnt.keys().count() < 2 { return Option::None; } let mut msges: Vec = Vec::new(); + msges.push(format!("EventID : 4625")); msges.push(format!( "Message: High number of total logon failures for multiple accounts" )); @@ -86,6 +139,73 @@ impl Security { return Option::Some(msges); } + // ユーザー毎にログインの失敗回数の閾値を超えたら、メッセージを出力 + fn disp_login_failed_for_oneuser(&self) -> Vec> { + return self + .account_2_failedcnt + .iter() + .filter_map(|(key, failed_cnt)| { + let mut msges = vec![]; + if failed_cnt <= &self.max_failed_logons { + return Option::None; + } + + msges.push("EventID : 4625".to_string()); + msges.push("Message : High number of logon failures for one account".to_string()); + msges.push(format!("Username: {}", key)); + msges.push(format!("Total logon failures: {}", failed_cnt)); + + return Option::Some(msges); + }) + .collect(); + } + + fn setup_configs(&mut self) { + let configs: &yaml_rust::Yaml = &configs::CONFIG.configs; + { + let config_value = configs["alert_all_admin"].as_i64(); + if config_value.is_some() { + self.alert_all_admin = config_value.unwrap() as i32; + } + } + { + let config_value = configs["show_total_admin_logons"].as_i64(); + if config_value.is_some() { + self.show_total_admin_logons = config_value.unwrap() as i32; + } + } + { + let config_value = configs["max_total_failed_logons"].as_i64(); + if config_value.is_some() { + self.max_total_failed_logons = config_value.unwrap() as i32; + } + } + { + let config_value = configs["max_failed_logons"].as_i64(); + if config_value.is_some() { + self.max_failed_logons = config_value.unwrap() as i32; + } + } + { + let config_value = configs["max_passspray_login"].as_i64(); + if config_value.is_some() { + self.max_passspray_login = config_value.unwrap() as i32; + } + } + { + let config_value = configs["max_passspray_uniquser"].as_i64(); + if config_value.is_some() { + self.max_passspray_uniquser = config_value.unwrap() as i32; + } + } + { + let config_value = configs["max_total_sensitive_privuse"].as_i64(); + if config_value.is_some() { + self.max_total_sensitive_privuse = config_value.unwrap() as i32; + } + } + } + pub fn detection( &mut self, event_id: String, @@ -134,10 +254,12 @@ impl Security { let creator = event_data .get("ParentProcessName") .unwrap_or(&self.empty_str); + let configs: &yaml_rust::Yaml = &configs::CONFIG.configs; + let value = configs["minlength"].as_i64().unwrap_or(1000).clone(); utils::check_command( 4688, &commandline, - 1000, + value as usize, 0, &self.empty_str, &creator, @@ -158,6 +280,8 @@ impl Security { return; } + //// "Multiple admin logons for one account" + if let Some(privileage_list) = event_data.get("PrivilegeList") { if let Some(_data) = privileage_list.find("SeDebugPrivilege") { // alert_all_adminが有効であれば、標準出力して知らせる @@ -196,34 +320,15 @@ impl Security { } self.total_admin_logons += 1; + let subject_username = &event_data["SubjectUserName"]; + let subject_sid = &event_data["SubjectUserSid"]; - // 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); - } + if !self.admin_logons.contains_key(subject_username) { + self.admin_logons + .insert(subject_username.clone(), HashSet::new()); } + let sid_sets = self.admin_logons.get_mut(subject_username).unwrap(); + sid_sets.insert(subject_sid.clone()); } } } @@ -951,7 +1056,7 @@ mod tests { let mut sec = security::Security::new(); - sec.max_failed_logons = 5; + sec.max_total_failed_logons = 5; let ite = [1, 2, 3, 4, 5, 6, 7].iter(); ite.for_each(|i| { sec.failed_logon( @@ -975,7 +1080,8 @@ mod tests { .unwrap(); let mut sec = security::Security::new(); - sec.max_failed_logons = 5; + sec.max_total_failed_logons = 5; + sec.max_failed_logons = 4; // メッセージが表示されるには2ユーザー以上失敗している必要がある。まず一人目 sec.failed_logon( @@ -995,6 +1101,10 @@ mod tests { if fail_cnt > 5 { let v = sec.disp_login_failed().unwrap(); let mut ite = v.iter(); + assert_eq!( + &"EventID : 4625".to_string(), + ite.next().unwrap_or(&"".to_string()) + ); assert_eq!( &"Message: High number of total logon failures for multiple accounts" .to_string(), @@ -1012,6 +1122,33 @@ mod tests { } else { assert_eq!(Option::None, sec.disp_login_failed()); } + + if fail_cnt > (4 + 1) { + let msges = sec.disp_login_failed_for_oneuser(); + assert_eq!(1, msges.len()); + let msges = msges.into_iter().next().unwrap(); + let mut ite = msges.iter(); + + assert_eq!( + &"EventID : 4625".to_string(), + ite.next().unwrap_or(&"".to_string()) + ); + assert_eq!( + &"Message : High number of logon failures for one account".to_string(), + ite.next().unwrap_or(&"".to_string()) + ); + assert_eq!( + &"Username: localuser".to_string(), + ite.next().unwrap_or(&"".to_string()) + ); + // Administratorの分があるので一つ引く + assert_eq!( + &format!("Total logon failures: {}", (fail_cnt - 1)), + ite.next().unwrap_or(&"".to_string()) + ); + } else { + assert_eq!(0, sec.disp_login_failed_for_oneuser().len()); + } }); // hitするけど表示するフィールドがない場合 @@ -1049,7 +1186,7 @@ mod tests { .unwrap(); let mut sec = security::Security::new(); - sec.max_failed_logons = 5; + sec.max_total_failed_logons = 5; // メッセージが表示されるには2ユーザー以上失敗している必要がある。まず一人目 sec.failed_logon( @@ -1495,6 +1632,142 @@ mod tests { assert_eq!(Option::None, msg); } + #[test] + fn test_se_debug_priviledge() { + let mut sec = security::Security::new(); + + let event: event::Evtx = + quick_xml::de::from_str(&get_4672("hogehoge".to_string(), "hogehoge".to_string())) + .unwrap(); + let eventid = "4672".to_string(); + let system_time = "2021-07-11T06:16:25.6374739Z".to_string(); + let event_data = event.parse_event_data(); + // 1Userで1SIDでは表示されない + sec.se_debug_privilege(&eventid, &event_data, &system_time); + assert_eq!(0, sec.disp_multiple_sid_logon().len()); + // 1Userで1SIDなら何回やっても表示されない + sec.se_debug_privilege(&eventid, &event_data, &system_time); + sec.se_debug_privilege(&eventid, &event_data, &system_time); + sec.se_debug_privilege(&eventid, &event_data, &system_time); + sec.se_debug_privilege(&eventid, &event_data, &system_time); + assert_eq!(0, sec.disp_multiple_sid_logon().len()); + + // SIDを追加 + let event2: event::Evtx = + quick_xml::de::from_str(&get_4672("hogehoge".to_string(), "hogehoge2".to_string())) + .unwrap(); + let event_data2 = event2.parse_event_data(); + sec.se_debug_privilege(&eventid, &event_data2, &system_time); + let msges = sec.disp_multiple_sid_logon(); + assert_eq!(1, msges.len()); + { + let mut ite = msges.iter().next().unwrap().iter(); + assert_eq!( + &"EventID : 4672".to_string(), + ite.next().unwrap_or(&"".to_string()) + ); + assert_eq!( + &"Message : Multiple admin logons for one account".to_string(), + ite.next().unwrap_or(&"".to_string()) + ); + assert_eq!( + &"Username: hogehoge".to_string(), + ite.next().unwrap_or(&"".to_string()) + ); + assert_eq!( + &"User SID Access Count: 2".to_string(), + ite.next().unwrap_or(&"".to_string()) + ); + assert_eq!(Option::None, ite.next()); + } + + // SIDを追加 + let event2: event::Evtx = + quick_xml::de::from_str(&get_4672("hogehoge".to_string(), "hogehoge3".to_string())) + .unwrap(); + let event_data2 = event2.parse_event_data(); + sec.se_debug_privilege(&eventid, &event_data2, &system_time); + let msges = sec.disp_multiple_sid_logon(); + assert_eq!(1, msges.len()); + { + let mut ite = msges.iter().next().unwrap().iter(); + assert_eq!( + &"EventID : 4672".to_string(), + ite.next().unwrap_or(&"".to_string()) + ); + assert_eq!( + &"Message : Multiple admin logons for one account".to_string(), + ite.next().unwrap_or(&"".to_string()) + ); + assert_eq!( + &"Username: hogehoge".to_string(), + ite.next().unwrap_or(&"".to_string()) + ); + assert_eq!( + &"User SID Access Count: 3".to_string(), + ite.next().unwrap_or(&"".to_string()) + ); + assert_eq!(Option::None, ite.next()); + } + + // Userを追加 + let event_user2: event::Evtx = + quick_xml::de::from_str(&get_4672("ariai".to_string(), "ariari".to_string())).unwrap(); + let event_user2_data = event_user2.parse_event_data(); + sec.se_debug_privilege(&eventid, &event_user2_data, &system_time); + let msges = sec.disp_multiple_sid_logon(); + assert_eq!(1, msges.len()); + + sec.se_debug_privilege(&eventid, &event_user2_data, &system_time); + sec.se_debug_privilege(&eventid, &event_user2_data, &system_time); + let msges = sec.disp_multiple_sid_logon(); + assert_eq!(1, msges.len()); + + let event2_user2: event::Evtx = + quick_xml::de::from_str(&get_4672("ariai".to_string(), "ariari2".to_string())).unwrap(); + let event2_user2_data = event2_user2.parse_event_data(); + sec.se_debug_privilege(&eventid, &event2_user2_data, &system_time); + let msges = sec.disp_multiple_sid_logon(); + assert_eq!(2, msges.len()); + } + // msges.push("EventID : 4672".to_string()); + // msges.push("Message : Multiple admin logons for one account".to_string()); + // msges.push(format!("Username: {}",username)); + // msges.push(format!("User SID Access Count: {}",sids.len())); + + fn get_4672(username: String, user_sid: String) -> String { + let xml = r#" + + + + 4672 + 0 + 0 + 12548 + 0 + 0x8020000000000000 + + 368476 + + + Security + DESKTOP-ICHIICHI + + + + $SubjectUserSid_Value + $SubjectUserName_Value + NT AUTHORITY + 0x3e7 + SeAssignPrimaryTokenPrivilege SeTcbPrivilege SeSecurityPrivilege SeTakeOwnershipPrivilege SeLoadDriverPrivilege SeBackupPrivilege SeRestorePrivilege SeDebugPrivilege SeAuditPrivilege SeSystemEnvironmentPrivilege SeImpersonatePrivilege SeDelegateSessionUserImpersonatePrivilege + + "#.to_string(); + + return xml + .replace("$SubjectUserSid_Value", &user_sid) + .replace("$SubjectUserName_Value", &username); + } + fn get_audit_log_cleared_xml() -> String { return r#" diff --git a/src/detections/sysmon.rs b/src/detections/sysmon.rs index e748f7c..9bc1e4c 100644 --- a/src/detections/sysmon.rs +++ b/src/detections/sysmon.rs @@ -2,6 +2,9 @@ use crate::detections::print::MessageNotation; use crate::detections::utils::check_command; use crate::models::event; use std::collections::HashMap; +use std::usize; + +use super::configs; pub struct Sysmon { checkunsigned: u16, @@ -36,7 +39,17 @@ impl Sysmon { let default = "".to_string(); let _creater = event_data.get("ParentImage").unwrap_or(&default); - check_command(1, _command_line, 1000, 0, "", _creater, &system_time); + let configs: &yaml_rust::Yaml = &configs::CONFIG.configs; + let value = configs["minlength"].as_i64().unwrap_or(1000).clone(); + check_command( + 1, + _command_line, + value as usize, + 0, + "", + _creater, + &system_time, + ); } } diff --git a/src/detections/system.rs b/src/detections/system.rs index 9818aa1..ad6ec9c 100644 --- a/src/detections/system.rs +++ b/src/detections/system.rs @@ -3,6 +3,8 @@ use crate::detections::utils; use crate::models::event; use std::collections::HashMap; +use super::configs; + #[derive(Debug)] pub struct System {} @@ -65,7 +67,17 @@ impl System { msges.push(format!("Results: {}", text)); } if !commandline.is_empty() { - utils::check_command(7045, &commandline, 1000, 1, &servicename, &"", &system_time); + let configs: &yaml_rust::Yaml = &configs::CONFIG.configs; + let value = configs["minlength"].as_i64().unwrap_or(1000).clone(); + utils::check_command( + 7045, + &commandline, + value as usize, + 1, + &servicename, + &"", + &system_time, + ); } return Option::Some(msges); } diff --git a/src/detections/utils.rs b/src/detections/utils.rs index 2cea95c..d9f2e2a 100644 --- a/src/detections/utils.rs +++ b/src/detections/utils.rs @@ -110,8 +110,9 @@ 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 configs: &yaml_rust::Yaml = &configs::CONFIG.configs; + let mut minpercent = configs["obfuscation_minpercent"].as_f64().unwrap_or(0.65); + let maxbinary = configs["obfuscation_maxbinary"].as_f64().unwrap_or(0.5); let noalphastring = configs::CONFIG .noalpha_regex diff --git a/src/detections/yaml.rs b/src/detections/yaml.rs new file mode 100644 index 0000000..3d25199 --- /dev/null +++ b/src/detections/yaml.rs @@ -0,0 +1,127 @@ +extern crate serde_derive; +extern crate yaml_rust; + +use std::io::{BufReader, Read}; +use std::path::{Path, PathBuf}; +use std::{fs, io}; + +use yaml_rust::YamlLoader; + +use crate::detections::print::MessageNotation; + +pub struct ParseYaml { + pub files: Vec, +} + +impl ParseYaml { + pub fn new() -> ParseYaml { + ParseYaml { files: Vec::new() } + } + + pub fn read_file(&self, path: PathBuf) -> Result { + let mut file_content = String::new(); + + let mut fr = fs::File::open(path) + .map(|f| BufReader::new(f)) + .map_err(|e| e.to_string())?; + + fr.read_to_string(&mut file_content) + .map_err(|e| e.to_string())?; + + Ok(file_content) + } + + pub fn read_yaml_file(&mut self, path: PathBuf) -> Result<(), String> { + let file = self.read_file(path)?; + + let load_result = YamlLoader::load_from_str(&file); + if load_result.is_err() { + return Result::Err(format!("fail to read file\n{} ", load_result.unwrap_err())); + } + + let load_yamls = load_result.unwrap(); + load_yamls.into_iter().for_each(|loaded_yaml| { + self.files.push(loaded_yaml); + }); + + return Result::Ok(()); + } + + pub fn read_dir>(&mut self, path: P) -> io::Result { + Ok(fs::read_dir(path)? + .filter_map(|entry| { + let entry = entry.ok()?; + if entry.file_type().ok()?.is_file() { + let stdout = std::io::stdout(); + let mut stdout = stdout.lock(); + match self.read_file(entry.path()) { + Ok(s) => { + match YamlLoader::load_from_str(&s) { + Ok(docs) => { + for i in docs { + // If there is no "enabled" it does not load + if i["enabled"].as_bool().unwrap_or(false) { + &self.files.push(i); + } + } + } + Err(e) => { + MessageNotation::info_noheader( + &mut stdout, + format!("fail to read file\n{}\n{} ", s, e), + ) + .ok(); + } + } + } + Err(e) => { + MessageNotation::info_noheader( + &mut stdout, + format!("fail to read file: {}\n{} ", entry.path().display(), e), + ) + .ok(); + } + }; + } + if entry.file_type().ok()?.is_dir() { + let _ = self.read_dir(entry.path()); + } + Some("") + }) + .collect()) + } +} + +#[cfg(test)] +mod tests { + + use crate::detections::yaml; + use std::path::Path; + use yaml_rust::YamlLoader; + + #[test] + fn test_read_yaml() { + let yaml = yaml::ParseYaml::new(); + let path = Path::new("test_files/rules/yaml/1.yml"); + let ret = yaml.read_file(path.to_path_buf()).unwrap(); + let rule = YamlLoader::load_from_str(&ret).unwrap(); + for i in rule { + if i["title"].as_str().unwrap() == "Sysmon Check command lines" { + assert_eq!( + "*", + i["detection"]["selection"]["CommandLine"].as_str().unwrap() + ); + assert_eq!(1, i["detection"]["selection"]["EventID"].as_i64().unwrap()); + } + } + } + + #[test] + fn test_failed_read_yaml() { + let yaml = yaml::ParseYaml::new(); + let path = Path::new("test_files/rules/yaml/error.yml"); + let ret = yaml.read_file(path.to_path_buf()).unwrap(); + let rule = YamlLoader::load_from_str(&ret); + assert_eq!(rule.is_err(), true); + } +} diff --git a/test_files/rules/yaml/1.yml b/test_files/rules/yaml/1.yml new file mode 100644 index 0000000..5f844d2 --- /dev/null +++ b/test_files/rules/yaml/1.yml @@ -0,0 +1,19 @@ +title: Sysmon Check command lines +description: hogehoge +enabled: true +author: Yea +logsource: + product: windows +detection: + selection: + EventLog: Sysmon + EventID: 1 + CommandLine: '*' + condition: selection +falsepositives: + - unknown +level: medium +output: 'CommandLine=%CommandLine%¥nParentImage=%ParentImage%' +creation_date: 2020/11/8 +updated_date: 2020/11/8 + diff --git a/test_files/rules/yaml/error.yml b/test_files/rules/yaml/error.yml new file mode 100644 index 0000000..b897264 --- /dev/null +++ b/test_files/rules/yaml/error.yml @@ -0,0 +1,19 @@ +title: Sysmon Check command lines +description: hogehoge +enabled: truea +author: Yea +logsource: + product: windows +detection: + selection: + EventLog: Sysmon + EventID: 1 + CommandLine: % + condition: selection +falsepositives: + - unknown +level: medium +output: 'CommandLine=%CommandLine%¥nParentImage=%ParentImage%' +creation_date: 2020/11/8 +updated_date: 2020/11/8 +