diff --git a/CHANGELOG-Japanese.md b/CHANGELOG-Japanese.md index 5c99fada9..0ba006b1e 100644 --- a/CHANGELOG-Japanese.md +++ b/CHANGELOG-Japanese.md @@ -9,6 +9,7 @@ **改善:** - インポートしているcrateのRustバージョンによるビルドエラーを回避するためにCargo.tomlに`rust-version`を追加した。(#802) (@hitenkoku) +- メモリ使用の削減。 (#806) (@fukusuket) **バグ修正:** diff --git a/CHANGELOG.md b/CHANGELOG.md index de8fea937..e769c5c11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ **Enhancements:** - Specified the minium Rust version `rust-version` field in `Cargo.toml` to avoid build dependency errors. (#802) (@hitenkoku) +- Reduced memory usage. (#806) (@fukusuket) **Bug Fixes:** diff --git a/src/afterfact.rs b/src/afterfact.rs index 921f63d9e..a5f41a79a 100644 --- a/src/afterfact.rs +++ b/src/afterfact.rs @@ -2,7 +2,7 @@ use crate::detections::configs::{self, CURRENT_EXE_PATH}; use crate::detections::message::{self, AlertMessage, LEVEL_FULL, MESSAGEKEYS}; use crate::detections::utils::{self, format_time, get_writable_color, write_color_buffer}; use crate::options::htmlreport::{self, HTML_REPORT_FLAG}; -use crate::options::profile::PROFILES; +use crate::options::profile::{Profile, PROFILES}; use crate::yaml::ParseYaml; use chrono::{DateTime, Local, TimeZone, Utc}; use comfy_table::modifiers::UTF8_ROUND_CORNERS; @@ -201,7 +201,7 @@ fn emit_csv( displayflag: bool, color_map: HashMap, all_record_cnt: u128, - profile: &Nested>, + profile: &Vec<(CompactString, Profile)>, ) -> io::Result<()> { let mut html_output_stock = Nested::::new(); let html_output_flag = *HTML_REPORT_FLAG; @@ -258,7 +258,7 @@ fn emit_csv( let (_, detect_infos) = multi.pair(); timestamps.push(_get_timestamp(time)); for (_, detect_info) in detect_infos.iter().enumerate() { - if !detect_info.detail.starts_with("[condition]") { + if !detect_info.is_condition { detected_record_idset.insert(CompactString::from(format!( "{}_{}", time, detect_info.eventid @@ -289,17 +289,13 @@ fn emit_csv( } else if json_output_flag { // JSON output wtr.write_field("{")?; - wtr.write_field(&output_json_str( - &detect_info.ext_field, - profile, - jsonl_output_flag, - ))?; + wtr.write_field(&output_json_str(&detect_info.ext_field, jsonl_output_flag))?; wtr.write_field("}")?; } else if jsonl_output_flag { // JSONL output format wtr.write_field(format!( "{{ {} }}", - &output_json_str(&detect_info.ext_field, profile, jsonl_output_flag) + &output_json_str(&detect_info.ext_field, jsonl_output_flag) ))?; } else { // csv output format @@ -308,7 +304,7 @@ fn emit_csv( detect_info .ext_field .iter() - .map(|x| x[0].to_string().trim().to_string()), + .map(|x| x.0.to_string().trim().to_string()), )?; plus_header = false; } @@ -316,7 +312,7 @@ fn emit_csv( detect_info .ext_field .iter() - .map(|x| x[1].to_string().trim().to_string()), + .map(|x| x.1.to_value().trim().to_string()), )?; } @@ -581,27 +577,27 @@ enum ColPos { Other, } -fn _get_serialized_disp_output(data: &Nested>, header: bool) -> String { +fn _get_serialized_disp_output(data: &Vec<(CompactString, Profile)>, header: bool) -> String { let data_length = data.len(); let mut ret = Nested::::new(); if header { for (i, d) in data.iter().enumerate() { if i == 0 { - ret.push(_format_cellpos(&d[0], ColPos::First)) + ret.push(_format_cellpos(&d.0, ColPos::First)) } else if i == data_length - 1 { - ret.push(_format_cellpos(&d[0], ColPos::Last)) + ret.push(_format_cellpos(&d.0, ColPos::Last)) } else { - ret.push(_format_cellpos(&d[0], ColPos::Other)) + ret.push(_format_cellpos(&d.0, ColPos::Other)) } } } else { for (i, d) in data.iter().enumerate() { if i == 0 { - ret.push(_format_cellpos(&d[1], ColPos::First).replace('|', "🦅")) + ret.push(_format_cellpos(&d.1.to_value(), ColPos::First).replace('|', "🦅")) } else if i == data_length - 1 { - ret.push(_format_cellpos(&d[1], ColPos::Last).replace('|', "🦅")) + ret.push(_format_cellpos(&d.1.to_value(), ColPos::Last).replace('|', "🦅")) } else { - ret.push(_format_cellpos(&d[1], ColPos::Other).replace('|', "🦅")) + ret.push(_format_cellpos(&d.1.to_value(), ColPos::Other).replace('|', "🦅")) } } } @@ -966,32 +962,29 @@ fn _get_timestamp(time: &DateTime) -> i64 { } /// json出力の際に配列として対応させるdetails,MitreTactics,MitreTags,OtherTagsに該当する場合に配列を返す関数 -fn _get_json_vec(target_alias_context: &str, target_data: &String) -> Vec { - if target_alias_context.contains("%MitreTactics%") - || target_alias_context.contains("%OtherTags%") - || target_alias_context.contains("%MitreTags%") - { - let ret: Vec = target_data - .to_owned() - .split(": ") - .map(|x| x.to_string()) - .collect(); - ret - } else if target_alias_context.contains("%Details%") - || target_alias_context.contains("%AllFieldInfo%") - { - let ret: Vec = target_data - .to_owned() - .split(" ¦ ") - .map(|x| x.to_string()) - .collect(); - if target_data == &ret[0] && !target_data.contains(": ") { - vec![] - } else { +fn _get_json_vec(profile: &Profile, target_data: &String) -> Vec { + match profile { + Profile::MitreTactics(_) | Profile::MitreTags(_) | Profile::OtherTags(_) => { + let ret: Vec = target_data + .to_owned() + .split(": ") + .map(|x| x.to_string()) + .collect(); ret } - } else { - vec![] + Profile::Details(_) | Profile::AllFieldInfo(_) => { + let ret: Vec = target_data + .to_owned() + .split(" ¦ ") + .map(|x| x.to_string()) + .collect(); + if target_data == &ret[0] && !target_data.contains(": ") { + vec![] + } else { + ret + } + } + _ => vec![], } } @@ -1062,157 +1055,152 @@ fn _convert_valid_json_str(input: &[&str], concat_flag: bool) -> String { } /// JSONに出力する1検知分のオブジェクトの文字列を出力する関数 -fn output_json_str( - ext_field: &Nested>, - profile: &Nested>, - jsonl_output_flag: bool, -) -> String { +fn output_json_str(ext_field: &[(CompactString, Profile)], jsonl_output_flag: bool) -> String { let mut target: Vec = vec![]; - let profile_map: HashMap = - HashMap::from_iter(profile.iter().map(|p| (p[0].to_string(), p[1].to_string()))); - for ef in ext_field.iter() { - let [key, val] = [ef[0].to_string(), ef[1].to_string()]; - let output_value_fmt = profile_map.get(&key).unwrap(); - let vec_data = _get_json_vec(output_value_fmt, &val); + for (key, profile) in ext_field.iter() { + let val = profile.to_value(); + let vec_data = _get_json_vec(profile, &val.to_string()); if vec_data.is_empty() { let tmp_val: Vec<&str> = val.split(": ").collect(); let output_val = - _convert_valid_json_str(&tmp_val, output_value_fmt.contains("%AllFieldInfo%")); + _convert_valid_json_str(&tmp_val, matches!(profile, Profile::AllFieldInfo(_))); target.push(_create_json_output_format( - &key, + &key.to_string(), &output_val, key.starts_with('\"'), output_val.starts_with('\"'), 4, )); - } else if output_value_fmt.contains("%Details%") - || output_value_fmt.contains("%AllFieldInfo%") - { - let mut output_stock: Vec = vec![]; - output_stock.push(format!(" \"{}\": {{", key)); - let mut stocked_value = vec![]; - let mut key_index_stock = vec![]; - for detail_contents in vec_data.iter() { - // 分解してキーとなりえる箇所を抽出する - let space_split: Vec<&str> = detail_contents.split(' ').collect(); - let mut tmp_stock = vec![]; - for sp in space_split.iter() { - if sp.ends_with(':') && sp.len() > 2 { + } else { + match profile { + Profile::AllFieldInfo(_) | Profile::Details(_) => { + let mut output_stock: Vec = vec![]; + output_stock.push(format!(" \"{}\": {{", key)); + let mut stocked_value = vec![]; + let mut key_index_stock = vec![]; + for detail_contents in vec_data.iter() { + // 分解してキーとなりえる箇所を抽出する + let space_split: Vec<&str> = detail_contents.split(' ').collect(); + let mut tmp_stock = vec![]; + for sp in space_split.iter() { + if sp.ends_with(':') && sp.len() > 2 { + stocked_value.push(tmp_stock); + tmp_stock = vec![]; + key_index_stock.push(sp.replace(':', "").to_owned()); + } else { + tmp_stock.push(sp.to_owned()); + } + } stocked_value.push(tmp_stock); - tmp_stock = vec![]; - key_index_stock.push(sp.replace(':', "").to_owned()); - } else { - tmp_stock.push(sp.to_owned()); } - } - stocked_value.push(tmp_stock); - } - let mut key_idx = 0; - let mut output_value_stock = String::default(); - for (value_idx, value) in stocked_value.iter().enumerate() { - let mut tmp = if key_idx >= key_index_stock.len() { - String::default() - } else if value_idx == 0 && !value.is_empty() { - key.to_owned() - } else { - key_index_stock[key_idx].to_string() - }; - if !output_value_stock.is_empty() { - output_value_stock.push_str(" | "); - } - output_value_stock.push_str(&value.join(" ")); - //``1つまえのキーの段階で以降にvalueの配列で区切りとなる空の配列が存在しているかを確認する - let is_remain_split_stock = if key_idx == key_index_stock.len() - 2 - && value_idx < stocked_value.len() - 1 - && !output_value_stock.is_empty() - { - let mut ret = true; - for remain_value in stocked_value[value_idx + 1..].iter() { - if remain_value.is_empty() { - ret = false; - break; + let mut key_idx = 0; + let mut output_value_stock = String::default(); + for (value_idx, value) in stocked_value.iter().enumerate() { + let mut tmp = if key_idx >= key_index_stock.len() { + String::default() + } else if value_idx == 0 && !value.is_empty() { + key.to_string().to_owned() + } else { + key_index_stock[key_idx].to_string() + }; + if !output_value_stock.is_empty() { + output_value_stock.push_str(" | "); + } + output_value_stock.push_str(&value.join(" ")); + //``1つまえのキーの段階で以降にvalueの配列で区切りとなる空の配列が存在しているかを確認する + let is_remain_split_stock = if key_idx == key_index_stock.len() - 2 + && value_idx < stocked_value.len() - 1 + && !output_value_stock.is_empty() + { + let mut ret = true; + for remain_value in stocked_value[value_idx + 1..].iter() { + if remain_value.is_empty() { + ret = false; + break; + } + } + ret + } else { + false + }; + if (value_idx < stocked_value.len() - 1 + && stocked_value[value_idx + 1].is_empty()) + || is_remain_split_stock + { + // 次の要素を確認して、存在しないもしくは、キーが入っているとなった場合現在ストックしている内容が出力していいことが確定するので出力処理を行う + let output_tmp = format!("{}: {}", tmp, output_value_stock); + let output: Vec<&str> = output_tmp.split(": ").collect(); + let key = _convert_valid_json_str(&[output[0]], false); + let fmted_val = _convert_valid_json_str(&output, false); + output_stock.push(format!( + "{},", + _create_json_output_format( + &key, + &fmted_val, + key.starts_with('\"'), + fmted_val.starts_with('\"'), + 8 + ) + )); + output_value_stock.clear(); + tmp = String::default(); + key_idx += 1; + } + if value_idx == stocked_value.len() - 1 { + let output_tmp = format!("{}: {}", tmp, output_value_stock); + let output: Vec<&str> = output_tmp.split(": ").collect(); + let key = _convert_valid_json_str(&[output[0]], false); + let fmted_val = _convert_valid_json_str(&output, false); + output_stock.push(_create_json_output_format( + &key, + &fmted_val, + key.starts_with('\"'), + fmted_val.starts_with('\"'), + 8, + )); + key_idx += 1; } } - ret - } else { - false - }; - if (value_idx < stocked_value.len() - 1 && stocked_value[value_idx + 1].is_empty()) - || is_remain_split_stock - { - // 次の要素を確認して、存在しないもしくは、キーが入っているとなった場合現在ストックしている内容が出力していいことが確定するので出力処理を行う - let output_tmp = format!("{}: {}", tmp, output_value_stock); - let output: Vec<&str> = output_tmp.split(": ").collect(); - let key = _convert_valid_json_str(&[output[0]], false); - let fmted_val = _convert_valid_json_str(&output, false); - output_stock.push(format!( - "{},", - _create_json_output_format( - &key, - &fmted_val, - key.starts_with('\"'), - fmted_val.starts_with('\"'), - 8 - ) - )); - output_value_stock.clear(); - tmp = String::default(); - key_idx += 1; + output_stock.push(" }".to_string()); + target.push(output_stock.join("\n")); } - if value_idx == stocked_value.len() - 1 { - let output_tmp = format!("{}: {}", tmp, output_value_stock); - let output: Vec<&str> = output_tmp.split(": ").collect(); - let key = _convert_valid_json_str(&[output[0]], false); - let fmted_val = _convert_valid_json_str(&output, false); - output_stock.push(_create_json_output_format( + Profile::MitreTags(_) | Profile::MitreTactics(_) | Profile::OtherTags(_) => { + let tmp_val: Vec<&str> = val.split(": ").collect(); + + let key = _convert_valid_json_str(&[key.as_str()], false); + let values: Vec<&&str> = tmp_val.iter().filter(|x| x.trim() != "").collect(); + let mut value: Vec = vec![]; + + if values.is_empty() { + continue; + } + for (idx, tag_val) in values.iter().enumerate() { + if idx == 0 { + value.push("[\n".to_string()); + } + let insert_val = format!(" \"{}\"", tag_val.trim()); + value.push(insert_val); + if idx != values.len() - 1 { + value.push(",\n".to_string()); + } + } + value.push("\n ]".to_string()); + + let fmted_val = if jsonl_output_flag { + value.iter().map(|x| x.replace('\n', "")).join("") + } else { + value.join("") + }; + target.push(_create_json_output_format( &key, &fmted_val, key.starts_with('\"'), - fmted_val.starts_with('\"'), - 8, + true, + 4, )); - key_idx += 1; } + _ => {} } - output_stock.push(" }".to_string()); - target.push(output_stock.join("\n")); - } else if output_value_fmt.contains("%MitreTags%") - || output_value_fmt.contains("%MitreTactics%") - || output_value_fmt.contains("%OtherTags%") - { - let tmp_val: Vec<&str> = val.split(": ").collect(); - - let key = _convert_valid_json_str(&[key.as_str()], false); - let values: Vec<&&str> = tmp_val.iter().filter(|x| x.trim() != "").collect(); - let mut value: Vec = vec![]; - - if values.is_empty() { - continue; - } - for (idx, tag_val) in values.iter().enumerate() { - if idx == 0 { - value.push("[\n".to_string()); - } - let insert_val = format!(" \"{}\"", tag_val.trim()); - value.push(insert_val); - if idx != values.len() - 1 { - value.push(",\n".to_string()); - } - } - value.push("\n ]".to_string()); - - let fmted_val = if jsonl_output_flag { - value.iter().map(|x| x.replace('\n', "")).join("") - } else { - value.join("") - }; - target.push(_create_json_output_format( - &key, - &fmted_val, - key.starts_with('\"'), - true, - 4, - )); } } if jsonl_output_flag { @@ -1331,11 +1319,10 @@ mod tests { use crate::afterfact::format_time; use crate::detections::message; use crate::detections::message::DetectInfo; - use crate::options::profile::load_profile; + use crate::options::profile::{load_profile, Profile}; use chrono::{Local, TimeZone, Utc}; use compact_str::CompactString; use hashbrown::HashMap; - use nested::Nested; use serde_json::Value; use std::fs::File; use std::fs::{read_to_string, remove_file}; @@ -1360,7 +1347,7 @@ mod tests { .datetime_from_str("1996-02-27T01:05:01Z", "%Y-%m-%dT%H:%M:%SZ") .unwrap(); let expect_tz = expect_time.with_timezone(&Local); - let output_profile: Nested> = load_profile( + let output_profile: Vec<(CompactString, Profile)> = load_profile( "test_files/config/default_profile.yaml", "test_files/config/profiles.yaml", ) @@ -1383,58 +1370,58 @@ mod tests { } "##; let event: Value = serde_json::from_str(val).unwrap(); - let mut profile_converter: HashMap = HashMap::from([ + let mut profile_converter: HashMap = HashMap::from([ ( - CompactString::from("%Timestamp%"), - CompactString::from(format_time(&expect_time, false)), + "Timestamp".to_string(), + Profile::Timestamp(CompactString::from(format_time(&expect_time, false))), ), ( - CompactString::from("%Computer%"), - CompactString::from(test_computername), + "Computer".to_string(), + Profile::Computer(CompactString::from(test_computername)), ), ( - CompactString::from("%Channel%"), - CompactString::from( + "Channel".to_string(), + Profile::Channel(CompactString::from( mock_ch_filter .get(&"Security".to_ascii_lowercase()) .unwrap_or(&String::default()), - ), + )), ), ( - CompactString::from("%Level%"), - CompactString::from(test_level), + "Level".to_string(), + Profile::Level(CompactString::from(test_level)), ), ( - CompactString::from("%EventID%"), - CompactString::from(test_eventid), + "EventID".to_string(), + Profile::EventID(CompactString::from(test_eventid)), ), ( - CompactString::from("%MitreAttack%"), - CompactString::from(test_attack), + "MitreAttack".to_string(), + Profile::MitreTactics(CompactString::from(test_attack)), ), ( - CompactString::from("%RecordID%"), - CompactString::from(test_record_id), + "RecordID".to_string(), + Profile::RecordID(CompactString::from(test_record_id)), ), ( - CompactString::from("%RuleTitle%"), - CompactString::from(test_title), + "RuleTitle".to_string(), + Profile::RuleTitle(CompactString::from(test_title)), ), ( - CompactString::from("%AllFieldInfo%"), - CompactString::from(test_recinfo), + "RecordInformation".to_string(), + Profile::AllFieldInfo(CompactString::from(test_recinfo)), ), ( - CompactString::from("%RuleFile%"), - CompactString::from(test_rulepath), + "RuleFile".to_string(), + Profile::RuleFile(CompactString::from(test_rulepath)), ), ( - CompactString::from("%EvtxFile%"), - CompactString::from(test_filepath), + "EvtxFile".to_string(), + Profile::EvtxFile(CompactString::from(test_filepath)), ), ( - CompactString::from("%Tags%"), - CompactString::from(test_attack), + "Tags".to_string(), + Profile::MitreTags(CompactString::from(test_attack)), ), ]); message::insert( @@ -1449,6 +1436,7 @@ mod tests { detail: CompactString::default(), record_information: CompactString::from(test_recinfo), ext_field: output_profile.to_owned(), + is_condition: false, }, expect_time, &mut profile_converter, @@ -1536,44 +1524,44 @@ mod tests { + " ‖ " + test_recinfo + "\n"; - let mut data: Nested> = Nested::>::new(); - data.push(vec![ - CompactString::new("Timestamp"), - CompactString::new(&format_time(&test_timestamp, false)), - ]); - data.push(vec![ - CompactString::new("Computer"), - CompactString::new(test_computername), - ]); - data.push(vec![ - CompactString::new("Channel"), - CompactString::new(test_channel), - ]); - data.push(vec![ - CompactString::new("EventID"), - CompactString::new(test_eventid), - ]); - data.push(vec![ - CompactString::new("Level"), - CompactString::new(test_level), - ]); - data.push(vec![ - CompactString::new("RecordID"), - CompactString::new(test_recid), - ]); - data.push(vec![ - CompactString::new("RuleTitle"), - CompactString::new(test_title), - ]); - data.push(vec![ - CompactString::new("Details"), - CompactString::new(output), - ]); - data.push(vec![ - CompactString::new("RecordInformation"), - CompactString::new(test_recinfo), - ]); - + let data: Vec<(CompactString, Profile)> = vec![ + ( + CompactString::new("Timestamp"), + Profile::Timestamp(CompactString::new(&format_time(&test_timestamp, false))), + ), + ( + CompactString::new("Computer"), + Profile::Computer(CompactString::new(test_computername)), + ), + ( + CompactString::new("Channel"), + Profile::Channel(CompactString::new(test_channel)), + ), + ( + CompactString::new("EventID"), + Profile::EventID(CompactString::new(test_eventid)), + ), + ( + CompactString::new("Level"), + Profile::Level(CompactString::new(test_level)), + ), + ( + CompactString::new("RecordID"), + Profile::RecordID(CompactString::new(test_recid)), + ), + ( + CompactString::new("RuleTitle"), + Profile::RuleTitle(CompactString::new(test_title)), + ), + ( + CompactString::new("Details"), + Profile::Details(CompactString::new(output)), + ), + ( + CompactString::new("RecordInformation"), + Profile::AllFieldInfo(CompactString::new(test_recinfo)), + ), + ]; assert_eq!(_get_serialized_disp_output(&data, true), expect_header); assert_eq!(_get_serialized_disp_output(&data, false), expect_no_header); } diff --git a/src/detections/detection.rs b/src/detections/detection.rs index 5841df25e..8c3622a17 100644 --- a/src/detections/detection.rs +++ b/src/detections/detection.rs @@ -2,13 +2,17 @@ extern crate csv; use crate::detections::configs; use crate::detections::utils::{format_time, write_color_buffer}; -use crate::options::profile::{ - LOADED_PROFILE_ALIAS, PRELOAD_PROFILE, PRELOAD_PROFILE_REGEX, PROFILES, +use crate::options::profile::Profile::{ + AllFieldInfo, Channel, Computer, EventID, EvtxFile, Level, MitreTactics, MitreTags, OtherTags, + Provider, RecordID, RuleAuthor, RuleCreationDate, RuleFile, RuleID, RuleModifiedDate, + RuleTitle, Status, Timestamp, }; +use crate::options::profile::{Profile, PROFILES}; use chrono::{TimeZone, Utc}; use compact_str::CompactString; use itertools::Itertools; use nested::Nested; +use std::default::Default; use termcolor::{BufferWriter, Color, ColorChoice}; use crate::detections::message::{ @@ -87,7 +91,7 @@ impl Detection { let return_if_success = |mut rule: RuleNode| { let err_msgs_result = rule.init(); if err_msgs_result.is_ok() { - return Option::Some(rule); + return Some(rule); } // ruleファイルのパースに失敗した場合はエラー出力 @@ -116,7 +120,7 @@ impl Detection { parseerror_count += 1; println!(); }); - Option::None + None }; // parse rule files let ret = rulefile_loader @@ -213,7 +217,12 @@ impl Detection { .as_ref() .unwrap_or(&"-".to_string()), ); - let rec_id = if LOADED_PROFILE_ALIAS.contains("%RecordID%") { + let rec_id = if PROFILES + .as_ref() + .unwrap() + .iter() + .any(|(_s, p)| *p == RecordID(Default::default())) + { CompactString::from( get_serde_number_to_string(&record_info.record["Event"]["System"]["EventRecordID"]) .unwrap_or_default(), @@ -235,7 +244,12 @@ impl Detection { Some(str) => CompactString::from(str), None => recinfo.to_owned(), }; - let opt_record_info = if LOADED_PROFILE_ALIAS.contains("%AllFieldInfo%") { + let opt_record_info = if PROFILES + .as_ref() + .unwrap() + .iter() + .any(|(_s, p)| *p == AllFieldInfo(Default::default())) + { recinfo } else { CompactString::from("-") @@ -245,177 +259,178 @@ impl Detection { let time = message::get_event_time(&record_info.record).unwrap_or(default_time); let level = rule.yaml["level"].as_str().unwrap_or("-").to_string(); - let mut profile_converter: HashMap = HashMap::new(); + let mut profile_converter: HashMap = HashMap::new(); let mut tags_config_values = TAGS_CONFIG.values(); - for p in PROFILES.as_ref().unwrap().iter() { - for target_profile in PRELOAD_PROFILE_REGEX - .matches(p[1].to_string().as_str()) - .into_iter() - { - match PRELOAD_PROFILE[target_profile] { - "%Timestamp%" => { - profile_converter.insert( - CompactString::from("%Timestamp%"), - CompactString::from(format_time(&time, false)), - ); - } - "%Computer%" => { - profile_converter.insert( - CompactString::from("%Computer%"), - CompactString::from( - record_info.record["Event"]["System"]["Computer"] - .to_string() - .replace('\"', ""), - ), - ); - } - "%Channel%" => { - profile_converter.insert( - CompactString::from("%Channel%"), - CompactString::from( - CH_CONFIG - .get(&ch_str.to_ascii_lowercase()) - .unwrap_or(ch_str), - ), - ); - } - "%Level%" => { - profile_converter.insert( - CompactString::from("%Level%"), - CompactString::from( - LEVEL_ABBR_MAP - .get(&level.as_str()) - .unwrap_or(&level.as_str()) - .to_string(), - ), - ); - } - "%EventID%" => { - profile_converter.insert(CompactString::from("%EventID%"), eid.to_owned()); - } - "%RecordID%" => { - profile_converter - .insert(CompactString::from("%RecordID%"), rec_id.to_owned()); - } - "%RuleTitle%" => { - profile_converter.insert( - CompactString::from("%RuleTitle%"), - CompactString::from(rule.yaml["title"].as_str().unwrap_or("")), - ); - } - "%AllFieldInfo%" => { - profile_converter.insert( - CompactString::from("%AllFieldInfo%"), - opt_record_info.to_owned(), - ); - } - "%RuleFile%" => { - profile_converter.insert( - CompactString::from("%RuleFile%"), - CompactString::from( - Path::new(&rule.rulepath) - .file_name() - .unwrap_or_default() - .to_str() - .unwrap_or_default(), - ), - ); - } - "%EvtxFile%" => { - profile_converter.insert( - CompactString::from("%EvtxFile%"), - CompactString::from( - Path::new(&record_info.evtx_filepath) - .to_str() - .unwrap_or_default(), - ), - ); - } - "%MitreTactics%" => { - let tactics = CompactString::from( - &tag_info - .iter() - .filter(|x| tags_config_values.contains(&x.to_string())) - .join(" ¦ "), - ); - - profile_converter.insert(CompactString::from("%MitreTactics%"), tactics); - } - "%MitreTags%" => { - let techniques = CompactString::from( - &tag_info - .iter() - .filter(|x| { - !tags_config_values.contains(&x.to_string()) - && (x.starts_with("attack.t") - || x.starts_with("attack.g") - || x.starts_with("attack.s")) - }) - .map(|y| { - let replaced_tag = y.replace("attack.", ""); - make_ascii_titlecase(&replaced_tag) - }) - .join(" ¦ "), - ); - profile_converter.insert(CompactString::from("%MitreTags%"), techniques); - } - "%OtherTags%" => { - let tags = CompactString::from( - &tag_info - .iter() - .filter(|x| { - !(TAGS_CONFIG.values().contains(&x.to_string()) - || x.starts_with("attack.t") + for (key, profile) in PROFILES.as_ref().unwrap().iter() { + match profile { + Timestamp(_) => { + profile_converter.insert( + key.to_string(), + Timestamp(CompactString::from(format_time(&time, false))), + ); + } + Computer(_) => { + profile_converter.insert( + key.to_string(), + Computer(CompactString::from( + record_info.record["Event"]["System"]["Computer"] + .to_string() + .replace('\"', ""), + )), + ); + } + Channel(_) => { + profile_converter.insert( + key.to_string(), + Channel(CompactString::from( + CH_CONFIG + .get(&ch_str.to_ascii_lowercase()) + .unwrap_or(ch_str), + )), + ); + } + Level(_) => { + profile_converter.insert( + key.to_string(), + Level(CompactString::from( + LEVEL_ABBR_MAP + .get(&level.as_str()) + .unwrap_or(&level.as_str()) + .to_string(), + )), + ); + } + EventID(_) => { + profile_converter.insert(key.to_string(), EventID(eid.to_owned())); + } + RecordID(_) => { + profile_converter.insert(key.to_string(), RecordID(rec_id.to_owned())); + } + RuleTitle(_) => { + profile_converter.insert( + key.to_string(), + RuleTitle(CompactString::from( + rule.yaml["title"].as_str().unwrap_or(""), + )), + ); + } + AllFieldInfo(_) => { + profile_converter + .insert(key.to_string(), AllFieldInfo(opt_record_info.to_owned())); + } + RuleFile(_) => { + profile_converter.insert( + key.to_string(), + RuleFile(CompactString::from( + Path::new(&rule.rulepath) + .file_name() + .unwrap_or_default() + .to_str() + .unwrap_or_default(), + )), + ); + } + EvtxFile(_) => { + profile_converter.insert( + key.to_string(), + EvtxFile(CompactString::from( + Path::new(&record_info.evtx_filepath) + .to_str() + .unwrap_or_default(), + )), + ); + } + MitreTactics(_) => { + let tactics = CompactString::from( + &tag_info + .iter() + .filter(|x| tags_config_values.contains(&x.to_string())) + .join(" ¦ "), + ); + + profile_converter.insert(key.to_string(), MitreTactics(tactics)); + } + MitreTags(_) => { + let techniques = CompactString::from( + &tag_info + .iter() + .filter(|x| { + !tags_config_values.contains(&x.to_string()) + && (x.starts_with("attack.t") || x.starts_with("attack.g") || x.starts_with("attack.s")) - }) - .join(" ¦ "), - ); - profile_converter.insert(CompactString::from("%OtherTags%"), tags); - } - "%RuleAuthor%" => { - profile_converter.insert( - CompactString::from("%RuleAuthor%"), - CompactString::from(rule.yaml["author"].as_str().unwrap_or("-")), - ); - } - "%RuleCreationDate%" => { - profile_converter.insert( - CompactString::from("%RuleCreationDate%"), - CompactString::from(rule.yaml["date"].as_str().unwrap_or("-")), - ); - } - "%RuleModifiedDate%" => { - profile_converter.insert( - CompactString::from("%RuleModifiedDate%"), - CompactString::from(rule.yaml["modified"].as_str().unwrap_or("-")), - ); - } - "%Status%" => { - profile_converter.insert( - CompactString::from("%Status%"), - CompactString::from(rule.yaml["status"].as_str().unwrap_or("-")), - ); - } - "%RuleID%" => { - profile_converter.insert( - CompactString::from("%RuleID%"), - CompactString::from(rule.yaml["id"].as_str().unwrap_or("-")), - ); - } - "%Provider%" => { - profile_converter.insert( - CompactString::from("%Provider%"), - CompactString::from( - record_info.record["Event"]["System"]["Provider_attributes"] - ["Name"] - .to_string() - .replace('\"', ""), - ), - ); - } - _ => {} + }) + .map(|y| { + let replaced_tag = y.replace("attack.", ""); + make_ascii_titlecase(&replaced_tag) + }) + .join(" ¦ "), + ); + profile_converter.insert(key.to_string(), MitreTags(techniques)); + } + OtherTags(_) => { + let tags = CompactString::from( + &tag_info + .iter() + .filter(|x| { + !(TAGS_CONFIG.values().contains(&x.to_string()) + || x.starts_with("attack.t") + || x.starts_with("attack.g") + || x.starts_with("attack.s")) + }) + .join(" ¦ "), + ); + profile_converter.insert(key.to_string(), OtherTags(tags)); + } + RuleAuthor(_) => { + profile_converter.insert( + key.to_string(), + RuleAuthor(CompactString::from( + rule.yaml["author"].as_str().unwrap_or("-"), + )), + ); + } + RuleCreationDate(_) => { + profile_converter.insert( + key.to_string(), + RuleCreationDate(CompactString::from( + rule.yaml["date"].as_str().unwrap_or("-"), + )), + ); + } + RuleModifiedDate(_) => { + profile_converter.insert( + key.to_string(), + RuleModifiedDate(CompactString::from( + rule.yaml["modified"].as_str().unwrap_or("-"), + )), + ); + } + Status(_) => { + profile_converter.insert( + key.to_string(), + Status(CompactString::from( + rule.yaml["status"].as_str().unwrap_or("-"), + )), + ); + } + RuleID(_) => { + profile_converter.insert( + key.to_string(), + RuleID(CompactString::from(rule.yaml["id"].as_str().unwrap_or("-"))), + ); + } + Provider(_) => { + profile_converter.insert( + key.to_string(), + Provider(CompactString::from( + record_info.record["Event"]["System"]["Provider_attributes"]["Name"] + .to_string() + .replace('\"', ""), + )), + ); } + _ => {} } } @@ -437,6 +452,7 @@ impl Detection { detail: CompactString::default(), record_information: opt_record_info, ext_field: PROFILES.as_ref().unwrap().to_owned(), + is_condition: false, }; message::insert( &record_info.record, @@ -452,139 +468,155 @@ impl Detection { fn insert_agg_message(rule: &RuleNode, agg_result: AggResult) { let tag_info: &Nested = &Detection::get_tag_info(rule); let output = Detection::create_count_output(rule, &agg_result); - let rec_info = CompactString::default(); - let mut profile_converter: HashMap = HashMap::new(); + let mut profile_converter: HashMap = HashMap::new(); let level = rule.yaml["level"].as_str().unwrap_or("-").to_string(); let mut tags_config_values = TAGS_CONFIG.values(); - for p in PROFILES.as_ref().unwrap().iter() { - for target_profile in PRELOAD_PROFILE_REGEX - .matches(p[1].to_string().as_str()) - .into_iter() - { - match PRELOAD_PROFILE[target_profile] { - "%Timestamp%" => { - profile_converter.insert( - CompactString::from("%Timestamp%"), - CompactString::from(format_time(&agg_result.start_timedate, false)), - ); - } - "%Computer%" => { - profile_converter - .insert(CompactString::from("%Computer%"), CompactString::from("-")); - } - "%Channel%" => { - profile_converter - .insert(CompactString::from("%Channel%"), CompactString::from("-")); - } - "%Level%" => { - profile_converter.insert( - CompactString::from("%Level%"), - CompactString::from( - LEVEL_ABBR_MAP - .get(&level.as_str()) - .unwrap_or(&level.as_str()) - .to_string(), - ), - ); - } - "%EventID%" => { - profile_converter - .insert(CompactString::from("%EventID%"), CompactString::from("-")); - } - "%RecordID%" => { - profile_converter - .insert(CompactString::from("%RecordID%"), CompactString::from("")); - } - "%RuleTitle%" => { - profile_converter.insert( - CompactString::from("%RuleTitle%"), - CompactString::from(rule.yaml["title"].as_str().unwrap_or("")), - ); - } - "%AllFieldInfo%" => { - profile_converter.insert( - CompactString::from("%AllFieldInfo%"), - CompactString::from("-"), - ); - } - "%RuleFile%" => { - profile_converter.insert( - CompactString::from("%RuleFile%"), - CompactString::from( - Path::new(&rule.rulepath) - .file_name() - .unwrap_or_default() - .to_str() - .unwrap_or_default(), - ), - ); - } - "%EvtxFile%" => { - profile_converter - .insert(CompactString::from("%EvtxFile%"), CompactString::from("-")); - } - "%MitreTactics%" => { - let tactics = CompactString::from( - &tag_info - .iter() - .filter(|x| tags_config_values.contains(&x.to_string())) - .join(" ¦ "), - ); - profile_converter.insert(CompactString::from("%MitreTactics%"), tactics); - } - "%MitreTags%" => { - let techniques = CompactString::from( - &tag_info - .iter() - .filter(|x| { - !tags_config_values.contains(&x.to_string()) - && (x.starts_with("attack.t") - || x.starts_with("attack.g") - || x.starts_with("attack.s")) - }) - .map(|y| { - let replaced_tag = y.replace("attack.", ""); - make_ascii_titlecase(&replaced_tag) - }) - .join(" ¦ "), - ); - profile_converter.insert(CompactString::from("%MitreTags%"), techniques); - } - "%OtherTags%" => { - let tags = CompactString::from( - &tag_info - .iter() - .filter(|x| { - !(tags_config_values.contains(&x.to_string()) - || x.starts_with("attack.t") + for (key, profile) in PROFILES.as_ref().unwrap().iter() { + match profile { + Timestamp(_) => { + profile_converter.insert( + key.to_string(), + Timestamp(CompactString::from(format_time( + &agg_result.start_timedate, + false, + ))), + ); + } + Computer(_) => { + profile_converter.insert(key.to_string(), Computer(CompactString::from("-"))); + } + Channel(_) => { + profile_converter.insert(key.to_string(), Channel(CompactString::from("-"))); + } + Level(_) => { + profile_converter.insert( + key.to_string(), + Level(CompactString::from( + LEVEL_ABBR_MAP + .get(&level.as_str()) + .unwrap_or(&level.as_str()) + .to_string(), + )), + ); + } + EventID(_) => { + profile_converter.insert(key.to_string(), EventID(CompactString::from("-"))); + } + RecordID(_) => { + profile_converter.insert(key.to_string(), RecordID(CompactString::from(""))); + } + RuleTitle(_) => { + profile_converter.insert( + key.to_string(), + RuleTitle(CompactString::from( + rule.yaml["title"].as_str().unwrap_or(""), + )), + ); + } + AllFieldInfo(_) => { + profile_converter + .insert(key.to_string(), AllFieldInfo(CompactString::from("-"))); + } + RuleFile(_) => { + profile_converter.insert( + key.to_string(), + RuleFile(CompactString::from( + Path::new(&rule.rulepath) + .file_name() + .unwrap_or_default() + .to_str() + .unwrap_or_default(), + )), + ); + } + EvtxFile(_) => { + profile_converter.insert(key.to_string(), EvtxFile(CompactString::from("-"))); + } + MitreTactics(_) => { + let tactics = CompactString::from( + &tag_info + .iter() + .filter(|x| tags_config_values.contains(&x.to_string())) + .join(" ¦ "), + ); + profile_converter.insert(key.to_string(), MitreTactics(tactics)); + } + MitreTags(_) => { + let techniques = CompactString::from( + &tag_info + .iter() + .filter(|x| { + !tags_config_values.contains(&x.to_string()) + && (x.starts_with("attack.t") || x.starts_with("attack.g") || x.starts_with("attack.s")) - }) - .join(" ¦ "), - ); - profile_converter.insert(CompactString::from("%OtherTags%"), tags); - } - "%Status%" => { - profile_converter.insert( - CompactString::from("%Status%"), - CompactString::from(rule.yaml["status"].as_str().unwrap_or("-")), - ); - } - "%RuleID%" => { - profile_converter.insert( - CompactString::from("%RuleID%"), - CompactString::from(rule.yaml["id"].as_str().unwrap_or("-")), - ); - } - "%Provider%" => { - profile_converter - .insert(CompactString::from("%Provider%"), CompactString::from("-")); - } - - _ => {} + }) + .map(|y| { + let replaced_tag = y.replace("attack.", ""); + make_ascii_titlecase(&replaced_tag) + }) + .join(" ¦ "), + ); + profile_converter.insert(key.to_string(), MitreTags(techniques)); + } + OtherTags(_) => { + let tags = CompactString::from( + &tag_info + .iter() + .filter(|x| { + !(tags_config_values.contains(&x.to_string()) + || x.starts_with("attack.t") + || x.starts_with("attack.g") + || x.starts_with("attack.s")) + }) + .join(" ¦ "), + ); + profile_converter.insert(key.to_string(), OtherTags(tags)); + } + RuleAuthor(_) => { + profile_converter.insert( + key.to_string(), + RuleAuthor(CompactString::from( + rule.yaml["author"].as_str().unwrap_or("-"), + )), + ); + } + RuleCreationDate(_) => { + profile_converter.insert( + key.to_string(), + RuleCreationDate(CompactString::from( + rule.yaml["date"].as_str().unwrap_or("-"), + )), + ); + } + RuleModifiedDate(_) => { + profile_converter.insert( + key.to_string(), + RuleModifiedDate(CompactString::from( + rule.yaml["modified"].as_str().unwrap_or("-"), + )), + ); } + Status(_) => { + profile_converter.insert( + key.to_string(), + Status(CompactString::from( + rule.yaml["status"].as_str().unwrap_or("-"), + )), + ); + } + RuleID(_) => { + profile_converter.insert( + key.to_string(), + RuleID(CompactString::from(rule.yaml["id"].as_str().unwrap_or("-"))), + ); + } + Provider(_) => { + profile_converter.insert(key.to_string(), Provider(CompactString::from("-"))); + } + _ => {} } } @@ -600,10 +632,10 @@ impl Detection { computername: CompactString::from("-"), eventid: CompactString::from("-"), detail: output, - record_information: rec_info, + record_information: CompactString::default(), ext_field: PROFILES.as_ref().unwrap().to_owned(), + is_condition: true, }; - message::insert( &Value::default(), CompactString::new(rule.yaml["details"].as_str().unwrap_or("-")), @@ -807,7 +839,6 @@ impl Detection { #[cfg(test)] mod tests { - use crate::detections::detection::Detection; use crate::detections::rule::create_rule; use crate::detections::rule::AggResult; diff --git a/src/detections/message.rs b/src/detections/message.rs index 4b9baa442..2f89203f1 100644 --- a/src/detections/message.rs +++ b/src/detections/message.rs @@ -1,7 +1,8 @@ extern crate lazy_static; use crate::detections::configs::{self, CURRENT_EXE_PATH}; use crate::detections::utils::{self, get_serde_number_to_string, write_color_buffer}; -use crate::options::profile::PROFILES; +use crate::options::profile::Profile; +use crate::options::profile::Profile::{AllFieldInfo, Details, Literal}; use chrono::{DateTime, Local, Utc}; use compact_str::CompactString; use dashmap::DashMap; @@ -28,7 +29,8 @@ pub struct DetectInfo { pub eventid: CompactString, pub detail: CompactString, pub record_information: CompactString, - pub ext_field: Nested>, + pub ext_field: Vec<(CompactString, Profile)>, + pub is_condition: bool, } pub struct AlertMessage {} @@ -123,7 +125,7 @@ pub fn insert( output: CompactString, mut detect_info: DetectInfo, time: DateTime, - profile_converter: &mut HashMap, + profile_converter: &mut HashMap, is_agg: bool, ) { if !is_agg { @@ -137,52 +139,45 @@ pub fn insert( parsed_detail }; } - let mut exist_detail = false; - PROFILES.as_ref().unwrap().iter().for_each(|p_element| { - if p_element[1].to_string().contains("%Details%") { - exist_detail = true; - } - }); - if exist_detail { - profile_converter.insert( - CompactString::from("%Details%"), - detect_info.detail.to_owned(), - ); - } - let mut replaced_converted_info: Nested> = - Nested::>::new(); - for di in detect_info.ext_field.iter() { - let val = &di[1]; - let converted_reserve_info = convert_profile_reserved_info(val, profile_converter); - if val.contains("%AllFieldInfo%") || val.contains("%Details%") { - replaced_converted_info.push(vec![ - di[0].to_owned(), - CompactString::new(&converted_reserve_info), - ]); - } else { - replaced_converted_info.push(vec![ - di[0].to_owned(), - parse_message(event_record, converted_reserve_info), - ]); + let mut replaced_profiles: Vec<(CompactString, Profile)> = vec![]; + for (key, profile) in detect_info.ext_field.iter() { + match profile { + Details(_) => { + if detect_info.detail.is_empty() { + replaced_profiles.push((key.to_owned(), profile.to_owned())); + } else { + replaced_profiles.push((key.to_owned(), Details(detect_info.detail))); + detect_info.detail = CompactString::default(); + } + } + AllFieldInfo(_) => { + if detect_info.record_information.is_empty() { + replaced_profiles + .push((key.to_owned(), AllFieldInfo(CompactString::from("-")))); + } else { + replaced_profiles + .push((key.to_owned(), AllFieldInfo(detect_info.record_information))); + detect_info.record_information = CompactString::default(); + } + } + Literal(_) => replaced_profiles.push((key.to_owned(), profile.to_owned())), + _ => { + if let Some(p) = profile_converter.get(key.to_string().as_str()) { + replaced_profiles.push(( + key.to_owned(), + profile.convert(&parse_message( + event_record, + CompactString::new(p.to_value()), + )), + )) + } + } } } - detect_info.ext_field = replaced_converted_info; - + detect_info.ext_field = replaced_profiles; insert_message(detect_info, time) } -/// profileで用いられる予約語の情報を変換する関数 -fn convert_profile_reserved_info( - output: &CompactString, - config_reserved_info: &HashMap, -) -> CompactString { - let mut ret = output.to_owned(); - config_reserved_info.iter().for_each(|(k, v)| { - ret = CompactString::from(ret.replace(k.as_str(), v.as_str())); - }); - ret -} - /// メッセージ内の%で囲まれた箇所をエイリアスとしてをレコード情報を参照して置き換える関数 fn parse_message(event_record: &Value, output: CompactString) -> CompactString { let mut return_message = output; @@ -373,7 +368,6 @@ mod tests { use chrono::Utc; use compact_str::CompactString; use hashbrown::HashMap; - use nested::Nested; use rand::Rng; use serde_json::Value; use std::thread; @@ -658,7 +652,8 @@ mod tests { eventid: CompactString::from(i.to_string()), detail: CompactString::default(), record_information: CompactString::default(), - ext_field: Nested::>::new(), + ext_field: vec![], + is_condition: false, }; sample_detects.push((sample_event_time, detect_info, rng.gen_range(0..10))); } diff --git a/src/options/profile.rs b/src/options/profile.rs index 137f17f29..8f261621c 100644 --- a/src/options/profile.rs +++ b/src/options/profile.rs @@ -1,19 +1,22 @@ use crate::detections::configs::{self, CURRENT_EXE_PATH}; use crate::detections::message::AlertMessage; use crate::detections::utils::check_setting_path; +use crate::options::profile::Profile::{ + AllFieldInfo, Channel, Computer, Details, EventID, EvtxFile, Level, Literal, MitreTactics, + MitreTags, OtherTags, Provider, RecordID, RuleAuthor, RuleCreationDate, RuleFile, RuleID, + RuleModifiedDate, RuleTitle, Status, Timestamp, +}; use crate::yaml; use compact_str::CompactString; -use hashbrown::HashSet; use lazy_static::lazy_static; use nested::Nested; -use regex::RegexSet; use std::fs::OpenOptions; use std::io::{BufWriter, Write}; use std::path::Path; use yaml_rust::{Yaml, YamlEmitter, YamlLoader}; lazy_static! { - pub static ref PROFILES: Option>> = load_profile( + pub static ref PROFILES: Option> = load_profile( check_setting_path( &CURRENT_EXE_PATH.to_path_buf(), "config/default_profile.yaml", @@ -31,35 +34,95 @@ lazy_static! { .to_str() .unwrap() ); - pub static ref LOADED_PROFILE_ALIAS: HashSet = HashSet::from_iter( - PROFILES - .as_ref() - .unwrap_or(&Nested::>::new()) - .iter() - .map(|x| x[1].to_string()) - ); - pub static ref PRELOAD_PROFILE: Vec<&'static str> = vec![ - "%Timestamp%", - "%Computer%", - "%Channel%", - "%Level%", - "%EventID%", - "%RecordID%", - "%RuleTitle%", - "%AllFieldInfo%", - "%RuleFile%", - "%EvtxFile%", - "%MitreTactics%", - "%MitreTags%", - "%OtherTags%", - "%RuleAuthor%", - "%RuleCreationDate%", - "%RuleModifiedDate%", - "%Status%", - "%RuleID%", - "%Provider%", - ]; - pub static ref PRELOAD_PROFILE_REGEX: RegexSet = RegexSet::new(&*PRELOAD_PROFILE).unwrap(); +} + +#[derive(Eq, PartialEq, Hash, Clone, Debug)] +pub enum Profile { + Timestamp(CompactString), + Computer(CompactString), + Channel(CompactString), + Level(CompactString), + EventID(CompactString), + RecordID(CompactString), + RuleTitle(CompactString), + AllFieldInfo(CompactString), + RuleFile(CompactString), + EvtxFile(CompactString), + MitreTactics(CompactString), + MitreTags(CompactString), + OtherTags(CompactString), + RuleAuthor(CompactString), + RuleCreationDate(CompactString), + RuleModifiedDate(CompactString), + Status(CompactString), + RuleID(CompactString), + Provider(CompactString), + Details(CompactString), + Literal(CompactString), // profiles.yamlの固定文字列を変換なしでそのまま出力する場合 +} + +impl Profile { + pub fn to_value(&self) -> String { + match &self { + Timestamp(v) | Computer(v) | Channel(v) | Level(v) | EventID(v) | RecordID(v) + | RuleTitle(v) | AllFieldInfo(v) | RuleFile(v) | EvtxFile(v) | MitreTactics(v) + | MitreTags(v) | OtherTags(v) | RuleAuthor(v) | RuleCreationDate(v) + | RuleModifiedDate(v) | Status(v) | RuleID(v) | Provider(v) | Details(v) + | Literal(v) => v.to_string(), + } + } + + pub fn convert(&self, converted_string: &CompactString) -> Self { + match self { + Timestamp(_) => Timestamp(converted_string.to_owned()), + Computer(_) => Computer(converted_string.to_owned()), + Channel(_) => Channel(converted_string.to_owned()), + Level(_) => Level(converted_string.to_owned()), + EventID(_) => EventID(converted_string.to_owned()), + RecordID(_) => RecordID(converted_string.to_owned()), + RuleTitle(_) => RuleTitle(converted_string.to_owned()), + RuleFile(_) => RuleFile(converted_string.to_owned()), + EvtxFile(_) => EvtxFile(converted_string.to_owned()), + MitreTactics(_) => MitreTactics(converted_string.to_owned()), + MitreTags(_) => MitreTags(converted_string.to_owned()), + OtherTags(_) => OtherTags(converted_string.to_owned()), + RuleAuthor(_) => RuleAuthor(converted_string.to_owned()), + RuleCreationDate(_) => RuleCreationDate(converted_string.to_owned()), + RuleModifiedDate(_) => RuleModifiedDate(converted_string.to_owned()), + Status(_) => Status(converted_string.to_owned()), + RuleID(_) => RuleID(converted_string.to_owned()), + Provider(_) => Provider(converted_string.to_owned()), + p => p.to_owned(), + } + } +} + +impl From<&str> for Profile { + fn from(alias: &str) -> Self { + match alias { + "%Timestamp%" => Timestamp(Default::default()), + "%Computer%" => Computer(Default::default()), + "%Channel%" => Channel(Default::default()), + "%Level%" => Level(Default::default()), + "%EventID%" => EventID(Default::default()), + "%RecordID%" => RecordID(Default::default()), + "%RuleTitle%" => RuleTitle(Default::default()), + "%AllFieldInfo%" => AllFieldInfo(Default::default()), + "%RuleFile%" => RuleFile(Default::default()), + "%EvtxFile%" => EvtxFile(Default::default()), + "%MitreTactics%" => MitreTactics(Default::default()), + "%MitreTags%" => MitreTags(Default::default()), + "%OtherTags%" => OtherTags(Default::default()), + "%RuleAuthor%" => RuleAuthor(Default::default()), + "%RuleCreationDate%" => RuleCreationDate(Default::default()), + "%RuleModifiedDate%" => RuleModifiedDate(Default::default()), + "%Status%" => Status(Default::default()), + "%RuleID%" => RuleID(Default::default()), + "%Provider%" => Provider(Default::default()), + "%Details%" => Details(Default::default()), + s => Literal(CompactString::from(s)), // profiles.yamlの固定文字列を変換なしでそのまま出力する場合 + } + } } // 指定されたパスのprofileを読み込む処理 @@ -82,7 +145,7 @@ fn read_profile_data(profile_path: &str) -> Result, String> { pub fn load_profile( default_profile_path: &str, profile_path: &str, -) -> Option>> { +) -> Option> { let conf = &configs::CONFIG.read().unwrap().args; if conf.set_default_profile.is_some() { if let Err(e) = set_default_profile(default_profile_path, profile_path) { @@ -114,7 +177,7 @@ pub fn load_profile( return None; } let profile_data = &profile_all[0]; - let mut ret: Nested> = Nested::>::new(); + let mut ret: Vec<(CompactString, Profile)> = vec![]; if let Some(profile_name) = &conf.profile { let target_data = &profile_data[profile_name.as_str()]; if !target_data.is_badvalue() { @@ -123,10 +186,10 @@ pub fn load_profile( .unwrap() .into_iter() .for_each(|(k, v)| { - ret.push(vec![ - CompactString::new(k.as_str().unwrap()), - CompactString::new(v.as_str().unwrap()), - ]); + ret.push(( + CompactString::from(k.as_str().unwrap()), + Profile::from(v.as_str().unwrap()), + )); }); Some(ret) } else { @@ -150,10 +213,10 @@ pub fn load_profile( .unwrap() .into_iter() .for_each(|(k, v)| { - ret.push(vec![ - CompactString::new(k.as_str().unwrap()), - CompactString::new(v.as_str().unwrap()), - ]); + ret.push(( + CompactString::from(k.as_str().unwrap()), + Profile::from(v.as_str().unwrap()), + )); }); Some(ret) } @@ -257,11 +320,16 @@ pub fn get_profile_list(profile_path: &str) -> Nested> { #[cfg(test)] mod tests { + use crate::detections::configs; + use crate::options::profile::{get_profile_list, load_profile, Profile}; use compact_str::CompactString; use nested::Nested; - use crate::detections::configs; - use crate::options::profile::{get_profile_list, load_profile}; + #[test] + fn test_profile_enum_detail_arc_to_string() { + let profile_enum = Profile::Details(String::from("a").into()); + assert_eq!("a", profile_enum.to_value()) + } #[test] ///オプションの設定が入ると値の冪等性が担保できないためテストを逐次的に処理する @@ -275,65 +343,66 @@ mod tests { /// プロファイルオプションが設定されていないときにロードをした場合のテスト fn test_load_profile_without_profile_option() { configs::CONFIG.write().unwrap().args.profile = None; - let mut expect: Nested> = Nested::>::new(); - expect.push(vec![ - CompactString::new("Timestamp"), - CompactString::new("%Timestamp%"), - ]); - expect.push(vec![ - CompactString::new("Computer"), - CompactString::new("%Computer%"), - ]); - expect.push(vec![ - CompactString::new("Channel"), - CompactString::new("%Channel%"), - ]); - expect.push(vec![ - CompactString::new("Level"), - CompactString::new("%Level%"), - ]); - expect.push(vec![ - CompactString::new("EventID"), - CompactString::new("%EventID%"), - ]); - expect.push(vec![ - CompactString::new("MitreAttack"), - CompactString::new("%MitreAttack%"), - ]); - expect.push(vec![ - CompactString::new("RecordID"), - CompactString::new("%RecordID%"), - ]); - expect.push(vec![ - CompactString::new("RuleTitle"), - CompactString::new("%RuleTitle%"), - ]); - expect.push(vec![ - CompactString::new("Details"), - CompactString::new("%Details%"), - ]); - expect.push(vec![ - CompactString::new("RecordInformation"), - CompactString::new("%AllFieldInfo%"), - ]); - expect.push(vec![ - CompactString::new("RuleFile"), - CompactString::new("%RuleFile%"), - ]); - expect.push(vec![ - CompactString::new("EvtxFile"), - CompactString::new("%EvtxFile%"), - ]); - expect.push(vec![ - CompactString::new("Tags"), - CompactString::new("%MitreAttack%"), - ]); + let expect: Vec<(CompactString, Profile)> = vec![ + ( + CompactString::new("Timestamp"), + Profile::Timestamp(Default::default()), + ), + ( + CompactString::new("Computer"), + Profile::Computer(Default::default()), + ), + ( + CompactString::new("Channel"), + Profile::Channel(Default::default()), + ), + ( + CompactString::new("Level"), + Profile::Level(Default::default()), + ), + ( + CompactString::new("EventID"), + Profile::EventID(Default::default()), + ), + ( + CompactString::new("MitreAttack"), + Profile::MitreTactics(Default::default()), + ), + ( + CompactString::new("RecordID"), + Profile::RecordID(Default::default()), + ), + ( + CompactString::new("RuleTitle"), + Profile::RuleTitle(Default::default()), + ), + ( + CompactString::new("Details"), + Profile::Details(Default::default()), + ), + ( + CompactString::new("RecordInformation"), + Profile::AllFieldInfo(Default::default()), + ), + ( + CompactString::new("RuleFile"), + Profile::RuleFile(Default::default()), + ), + ( + CompactString::new("EvtxFile"), + Profile::EvtxFile(Default::default()), + ), + ( + CompactString::new("Tags"), + Profile::MitreTags(Default::default()), + ), + ]; assert_eq!( Some(expect), load_profile( "test_files/config/default_profile.yaml", - "test_files/config/profiles.yaml" + "test_files/config/profiles.yaml", ) ); } @@ -341,41 +410,41 @@ mod tests { /// プロファイルオプションが設定されて`おり、そのオプションに該当するプロファイルが存在する場合のテスト fn test_load_profile_with_profile_option() { configs::CONFIG.write().unwrap().args.profile = Some("minimal".to_string()); - let mut expect: Nested> = Nested::new(); - expect.push(vec![ - CompactString::new("Timestamp"), - CompactString::new("%Timestamp%"), - ]); - expect.push(vec![ - CompactString::new("Computer"), - CompactString::new("%Computer%"), - ]); - expect.push(vec![ - CompactString::new("Channel"), - CompactString::new("%Channel%"), - ]); - expect.push(vec![ - CompactString::new("EventID"), - CompactString::new("%EventID%"), - ]); - expect.push(vec![ - CompactString::new("Level"), - CompactString::new("%Level%"), - ]); - expect.push(vec![ - CompactString::new("RuleTitle"), - CompactString::new("%RuleTitle%"), - ]); - expect.push(vec![ - CompactString::new("Details"), - CompactString::new("%Details%"), - ]); - + let expect: Vec<(CompactString, Profile)> = vec![ + ( + CompactString::new("Timestamp"), + Profile::Timestamp(Default::default()), + ), + ( + CompactString::new("Computer"), + Profile::Computer(Default::default()), + ), + ( + CompactString::new("Channel"), + Profile::Channel(Default::default()), + ), + ( + CompactString::new("EventID"), + Profile::EventID(Default::default()), + ), + ( + CompactString::new("Level"), + Profile::Level(Default::default()), + ), + ( + CompactString::new("RuleTitle"), + Profile::RuleTitle(Default::default()), + ), + ( + CompactString::new("Details"), + Profile::Details(Default::default()), + ), + ]; assert_eq!( Some(expect), load_profile( "test_files/config/default_profile.yaml", - "test_files/config/profiles.yaml" + "test_files/config/profiles.yaml", ) ); } @@ -389,7 +458,7 @@ mod tests { None, load_profile( "test_files/config/no_exist_default_profile.yaml", - "test_files/config/no_exist_profiles.yaml" + "test_files/config/no_exist_profiles.yaml", ) ); @@ -398,7 +467,7 @@ mod tests { None, load_profile( "test_files/config/profile/default_profile.yaml", - "test_files/config/profile/no_exist_profiles.yaml" + "test_files/config/profile/no_exist_profiles.yaml", ) ); @@ -407,22 +476,22 @@ mod tests { None, load_profile( "test_files/config/no_exist_default_profile.yaml", - "test_files/config/profiles.yaml" + "test_files/config/profiles.yaml", ) ); } /// yamlファイル内のプロファイル名一覧を取得する機能のテスト fn test_get_profile_names() { - let mut expect = Nested::>::new(); + let mut expect: Nested> = Nested::>::new(); expect.push(vec![ "minimal".to_string(), "%Timestamp%, %Computer%, %Channel%, %EventID%, %Level%, %RuleTitle%, %Details%" .to_string(), ]); - expect.push(vec!["standard".to_string(), "%Timestamp%, %Computer%, %Channel%, %EventID%, %Level%, %MitreAttack%, %RecordID%, %RuleTitle%, %Details%".to_string()]); - expect.push(vec!["verbose-1".to_string(), "%Timestamp%, %Computer%, %Channel%, %EventID%, %Level%, %MitreAttack%, %RecordID%, %RuleTitle%, %Details%, %RuleFile%, %EvtxFile%".to_string()]); - expect.push(vec!["verbose-2".to_string(), "%Timestamp%, %Computer%, %Channel%, %EventID%, %Level%, %MitreAttack%, %RecordID%, %RuleTitle%, %Details%, %AllFieldInfo%".to_string()]); + expect.push(vec!["standard".to_string(), "%Timestamp%, %Computer%, %Channel%, %EventID%, %Level%, %MitreTags%, %RecordID%, %RuleTitle%, %Details%".to_string()]); + expect.push(vec!["verbose-1".to_string(), "%Timestamp%, %Computer%, %Channel%, %EventID%, %Level%, %MitreTags%, %RecordID%, %RuleTitle%, %Details%, %RuleFile%, %EvtxFile%".to_string()]); + expect.push(vec!["verbose-2".to_string(), "%Timestamp%, %Computer%, %Channel%, %EventID%, %Level%, %MitreTags%, %RecordID%, %RuleTitle%, %Details%, %AllFieldInfo%".to_string()]); assert_eq!(expect, get_profile_list("test_files/config/profiles.yaml")); } } diff --git a/test_files/config/default_profile.yaml b/test_files/config/default_profile.yaml index 8385adeb1..b50f922d5 100644 --- a/test_files/config/default_profile.yaml +++ b/test_files/config/default_profile.yaml @@ -3,11 +3,11 @@ Computer: "%Computer%" Channel: "%Channel%" Level: "%Level%" EventID: "%EventID%" -MitreAttack: "%MitreAttack%" +MitreAttack: "%MitreTactics%" RecordID: "%RecordID%" RuleTitle: "%RuleTitle%" Details: "%Details%" RecordInformation: "%AllFieldInfo%" RuleFile: "%RuleFile%" EvtxFile: "%EvtxFile%" -Tags: "%MitreAttack%" +Tags: "%MitreTags%" diff --git a/test_files/config/profiles.yaml b/test_files/config/profiles.yaml index 7ab667922..7fa278fa7 100644 --- a/test_files/config/profiles.yaml +++ b/test_files/config/profiles.yaml @@ -13,7 +13,7 @@ standard: Channel: "%Channel%" EventID: "%EventID%" Level: "%Level%" - Tags: "%MitreAttack%" + Tags: "%MitreTags%" RecordID: "%RecordID%" RuleTitle: "%RuleTitle%" Details: "%Details%" @@ -24,7 +24,7 @@ verbose-1: Channel: "%Channel%" EventID: "%EventID%" Level: "%Level%" - Tags: "%MitreAttack%" + Tags: "%MitreTags%" RecordID: "%RecordID%" RuleTitle: "%RuleTitle%" Details: "%Details%" @@ -37,7 +37,7 @@ verbose-2: Channel: "%Channel%" EventID: "%EventID%" Level: "%Level%" - Tags: "%MitreAttack%" + Tags: "%MitreTags%" RecordID: "%RecordID%" RuleTitle: "%RuleTitle%" Details: "%Details%"