diff --git a/CHANGELOG-Japanese.md b/CHANGELOG-Japanese.md index 1a79b748d..14881b0c1 100644 --- a/CHANGELOG-Japanese.md +++ b/CHANGELOG-Japanese.md @@ -14,6 +14,8 @@ - JSON入力でデータが配列内にある場合に解析できるようにした。 (#1248) (@hitenkoku) - 古いターミナルでも正しく表示されるように、また読みやすくするために、`‖`区切り文字を`·`区切り文字に変更した。(#1258) (@YamatoSecurity) - General Optionsに`-h --help`オプションを追加した。 (#1255) (@hitenkoku) +- `json-timeline`コマンドの`Details`の出力で、要素がアルファベット順に並んでいたのをルールに記載されているオリジナルの順番に変更した。 (#1264) (@hitenkoku) +- ルールをロードする必要のないコマンドを実行した場合、検出ルールのロードをスキップするようにした。 (#1263) (@hitenkoku) **バグ修正:** diff --git a/CHANGELOG.md b/CHANGELOG.md index c15c8bb9f..075a19edc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ - Added support for parsing JSON input when the data is inside an array. (#1248) (@hitenkoku) - Changed the `‖` separator into a `·` separator to make it easier to read and render properly on older terminals. (#1258) (@YamatoSecurity) - Added back `-h --help` option to General Options. (#1255) (@hitenkoku) +- Changed the `Details` output in `json-timeline` command from alphabetical order to the original order. +- Skiped loading detection rules when running to command which is no need to load rule. (#1263) (@hitenkoku) **Bug Fixes:** diff --git a/src/afterfact.rs b/src/afterfact.rs index 7d52d9ee9..8476dc902 100644 --- a/src/afterfact.rs +++ b/src/afterfact.rs @@ -16,6 +16,7 @@ use chrono::{DateTime, Local, TimeZone, Utc}; use comfy_table::modifiers::UTF8_ROUND_CORNERS; use comfy_table::presets::UTF8_FULL; use compact_str::CompactString; +use hashbrown::hash_map::RawEntryMut; use terminal_size::terminal_size; use csv::{QuoteStyle, WriterBuilder}; @@ -1633,20 +1634,32 @@ pub fn output_json_str( }; let mut children_output_stock: HashMap> = HashMap::new(); + let mut children_output_order = vec![]; for contents in details_target_stock.iter() { let (key, value) = contents.split_once(':').unwrap_or_default(); let output_key = _convert_valid_json_str(&[key], false); let fmted_val = _convert_valid_json_str(&[value.trim_start()], false); + if let RawEntryMut::Vacant(_) = children_output_stock + .raw_entry_mut() + .from_key(output_key.as_str()) + { + children_output_order.push(output_key.clone()); + } children_output_stock .entry(output_key.into()) .or_insert(vec![]) .push(fmted_val.into()); } + // ルール内での表示順に合わせた表示順を戻した配列 let mut sorted_children_output_stock: Vec<( &CompactString, &Vec, )> = children_output_stock.iter().collect_vec(); - sorted_children_output_stock.sort_by(|a, b| a.0.cmp(b.0)); + for (k, v) in children_output_stock.iter() { + let index_in_rule = + children_output_order.iter().position(|x| x == k).unwrap(); + sorted_children_output_stock[index_in_rule] = (k, v); + } for (idx, (c_key, c_val)) in sorted_children_output_stock.iter().enumerate() { let fmted_c_val = if c_val.len() == 1 { c_val[0].to_string() @@ -1656,7 +1669,7 @@ pub fn output_json_str( c_val.iter().map(|x| { format!("\"{x}\"") }).join(", ") ) }; - if idx != sorted_children_output_stock.len() - 1 { + if idx != children_output_stock.len() - 1 { output_stock.push(format!( "{},", _create_json_output_format( diff --git a/src/detections/message.rs b/src/detections/message.rs index 65bb7f6b2..40bab12f9 100644 --- a/src/detections/message.rs +++ b/src/detections/message.rs @@ -287,7 +287,7 @@ pub fn parse_message( field_data_map: &Option, ) -> (CompactString, Vec) { let mut return_message = output.clone(); - let mut hash_map: HashMap> = HashMap::new(); + let mut hash_map: Vec<(CompactString, Vec)> = vec![]; let details_key: Vec<&str> = output.split(" ¦ ").collect(); for caps in ALIASREGEX.captures_iter(&return_message) { let full_target_str = &caps[0]; @@ -337,19 +337,19 @@ pub fn parse_message( converted_str.unwrap_or(hash_value) }; if json_timeline_flag { - hash_map.insert(CompactString::from(full_target_str), [field_data].to_vec()); + hash_map.push((CompactString::from(full_target_str), [field_data].to_vec())); } else { - hash_map.insert( + hash_map.push(( CompactString::from(full_target_str), [field_data.split_ascii_whitespace().join(" ").into()].to_vec(), - ); + )); } } } else { - hash_map.insert( + hash_map.push(( CompactString::from(full_target_str), ["n/a".into()].to_vec(), - ); + )); } } let mut details_key_and_value: Vec = vec![]; @@ -366,7 +366,6 @@ pub fn parse_message( } } } - details_key_and_value.sort_unstable(); (return_message, details_key_and_value) } diff --git a/src/detections/rule/condition_parser.rs b/src/detections/rule/condition_parser.rs index 8e256d1b8..d92f1daea 100644 --- a/src/detections/rule/condition_parser.rs +++ b/src/detections/rule/condition_parser.rs @@ -33,74 +33,79 @@ pub enum ConditionToken { SelectionReference(String), // パースの時に上手く処理するために作った疑似的なトークン - ParenthesisContainer(IntoIter), // 括弧を表すトークン - AndContainer(IntoIter), // ANDでつながった条件をまとめるためのトークン - OrContainer(IntoIter), // ORでつながった条件をまとめるためのトークン - NotContainer(IntoIter), // 「NOT」と「NOTで否定される式」をまとめるためのトークン この配列には要素が一つしか入らないが、他のContainerと同じように扱えるようにするためにVecにしている。あんまり良くない。 - OperandContainer(IntoIter), // ANDやORやNOT等の演算子に対して、非演算子を表す -} - -// ここを参考にしました。https://qiita.com/yasuo-ozu/items/7ce2f8ff846ba00dd244 -impl IntoIterator for ConditionToken { - type Item = ConditionToken; - type IntoIter = std::vec::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - match self { - ConditionToken::ParenthesisContainer(sub_tokens) => sub_tokens, - ConditionToken::AndContainer(sub_tokens) => sub_tokens, - ConditionToken::OrContainer(sub_tokens) => sub_tokens, - ConditionToken::NotContainer(sub_tokens) => sub_tokens, - ConditionToken::OperandContainer(sub_tokens) => sub_tokens, - _ => vec![].into_iter(), - } - } + ParenthesisContainer(Box), // 括弧を表すトークン + AndContainer(IntoIter), // ANDでつながった条件をまとめるためのトークン + OrContainer(IntoIter), // ORでつながった条件をまとめるためのトークン + NotContainer(Box), // 「NOT」と「NOTで否定される式」をまとめるためのトークン この配列には要素が一つしか入らないが、他のContainerと同じように扱えるようにするためにVecにしている。あんまり良くない。 } impl ConditionToken { - fn replace_subtoken(self, sub_tokens: Vec) -> ConditionToken { - match self { - ConditionToken::ParenthesisContainer(_) => { - ConditionToken::ParenthesisContainer(sub_tokens.into_iter()) + /// convert from ConditionToken into SelectionNode + pub fn into_selection_node( + self, + name_2_node: &HashMap>>, + ) -> Result, String> { + return match self { + ConditionToken::SelectionReference(selection_name) => { + let selection_node = name_2_node.get(&selection_name); + if let Some(select_node) = selection_node { + let selection_node = select_node; + let selection_node = Arc::clone(selection_node); + let ref_node = RefSelectionNode::new(selection_node); + return Result::Ok(Box::new(ref_node)); + } else { + let err_msg = format!("{selection_name} is not defined."); + return Result::Err(err_msg); + } } - ConditionToken::AndContainer(_) => ConditionToken::AndContainer(sub_tokens.into_iter()), - ConditionToken::OrContainer(_) => ConditionToken::OrContainer(sub_tokens.into_iter()), - ConditionToken::NotContainer(_) => ConditionToken::NotContainer(sub_tokens.into_iter()), - ConditionToken::OperandContainer(_) => { - ConditionToken::OperandContainer(sub_tokens.into_iter()) + ConditionToken::ParenthesisContainer(sub_token) => { + Result::Ok((*sub_token).into_selection_node(name_2_node)?) } - ConditionToken::LeftParenthesis => self, - ConditionToken::RightParenthesis => self, - ConditionToken::Space => self, - ConditionToken::Not => self, - ConditionToken::And => self, - ConditionToken::Or => self, - ConditionToken::SelectionReference(_) => self, - } - } - - pub fn sub_tokens(&self) -> Vec { - // TODO ここでcloneを使わずに実装できるようにしたい。 - match self { - ConditionToken::ParenthesisContainer(sub_tokens) => sub_tokens.as_slice().to_vec(), - ConditionToken::AndContainer(sub_tokens) => sub_tokens.as_slice().to_vec(), - ConditionToken::OrContainer(sub_tokens) => sub_tokens.as_slice().to_vec(), - ConditionToken::NotContainer(sub_tokens) => sub_tokens.as_slice().to_vec(), - ConditionToken::OperandContainer(sub_tokens) => sub_tokens.as_slice().to_vec(), - ConditionToken::LeftParenthesis => vec![], - ConditionToken::RightParenthesis => vec![], - ConditionToken::Space => vec![], - ConditionToken::Not => vec![], - ConditionToken::And => vec![], - ConditionToken::Or => vec![], - ConditionToken::SelectionReference(_) => vec![], - } + ConditionToken::AndContainer(sub_tokens) => { + let mut select_and_node = AndSelectionNode::new(); + for sub_token in sub_tokens { + let sub_node = sub_token.into_selection_node(name_2_node)?; + select_and_node.child_nodes.push(sub_node); + } + return Result::Ok(Box::new(select_and_node)); + } + ConditionToken::OrContainer(sub_tokens) => { + let mut select_or_node = OrSelectionNode::new(); + for sub_token in sub_tokens { + let sub_node = sub_token.into_selection_node(name_2_node)?; + select_or_node.child_nodes.push(sub_node); + } + return Result::Ok(Box::new(select_or_node)); + } + ConditionToken::NotContainer(sub_token) => { + let select_sub_node = sub_token.into_selection_node(name_2_node)?; + let select_not_node = NotSelectionNode::new(select_sub_node); + return Result::Ok(Box::new(select_not_node)); + } + ConditionToken::LeftParenthesis => Result::Err("Unknown error".to_string()), + ConditionToken::RightParenthesis => Result::Err("Unknown error".to_string()), + ConditionToken::Space => Result::Err("Unknown error".to_string()), + ConditionToken::Not => Result::Err("Unknown error".to_string()), + ConditionToken::And => Result::Err("Unknown error".to_string()), + ConditionToken::Or => Result::Err("Unknown error".to_string()), + }; } - pub fn sub_tokens_without_parenthesis(&self) -> Vec { - match self { - ConditionToken::ParenthesisContainer(_) => vec![], - _ => self.sub_tokens(), + pub fn to_condition_token(token: String) -> ConditionToken { + if token == "(" { + ConditionToken::LeftParenthesis + } else if token == ")" { + ConditionToken::RightParenthesis + } else if token == " " { + ConditionToken::Space + } else if token == "not" { + ConditionToken::Not + } else if token == "and" { + ConditionToken::And + } else if token == "or" { + ConditionToken::Or + } else { + ConditionToken::SelectionReference(token) } } } @@ -172,44 +177,16 @@ impl ConditionCompiler { let parsed = self.parse(tokens.into_iter())?; - Self::to_selectnode(parsed, name_2_node) + parsed.into_selection_node(name_2_node) } /// 構文解析を実行する。 fn parse(&self, tokens: IntoIter) -> Result { // 括弧で囲まれた部分を解析します。 - // (括弧で囲まれた部分は後で解析するため、ここでは一時的にConditionToken::ParenthesisContainerに変換しておく) - // 括弧の中身を解析するのはparse_rest_parenthesis()で行う。 let tokens = self.parse_parenthesis(tokens)?; // AndとOrをパースする。 - let tokens = self.parse_and_or_operator(tokens)?; - - // OperandContainerトークンの中身をパースする。(現状、Notを解析するためだけにある。将来的に修飾するキーワードが増えたらここを変える。) - let token = Self::parse_operand_container(tokens)?; - - // 括弧で囲まれている部分を探して、もしあればその部分を再帰的に構文解析します。 - self.parse_rest_parenthesis(token) - } - - /// 括弧で囲まれている部分を探して、もしあればその部分を再帰的に構文解析します。 - fn parse_rest_parenthesis(&self, token: ConditionToken) -> Result { - if let ConditionToken::ParenthesisContainer(sub_token) = token { - let new_token = self.parse(sub_token)?; - return Result::Ok(new_token); - } - - let sub_tokens = token.sub_tokens(); - if sub_tokens.is_empty() { - return Result::Ok(token); - } - - let mut new_sub_tokens = vec![]; - for sub_token in sub_tokens { - let new_token = self.parse_rest_parenthesis(sub_token)?; - new_sub_tokens.push(new_token); - } - Result::Ok(token.replace_subtoken(new_sub_tokens)) + self.parse_and_or_operator(tokens) } /// 字句解析を行う @@ -227,7 +204,7 @@ impl ConditionCompiler { } let mached_str = captured.unwrap().get(0).unwrap().as_str(); - let token = self.to_enum(mached_str.to_string()); + let token = ConditionToken::to_condition_token(mached_str.to_string()); if let ConditionToken::Space = token { // 空白は特に意味ないので、読み飛ばす。 cur_condition_str = cur_condition_str.replacen(mached_str, "", 1); @@ -241,25 +218,6 @@ impl ConditionCompiler { Result::Ok(tokens) } - /// 文字列をConditionTokenに変換する。 - fn to_enum(&self, token: String) -> ConditionToken { - if token == "(" { - ConditionToken::LeftParenthesis - } else if token == ")" { - ConditionToken::RightParenthesis - } else if token == " " { - ConditionToken::Space - } else if token == "not" { - ConditionToken::Not - } else if token == "and" { - ConditionToken::And - } else if token == "or" { - ConditionToken::Or - } else { - ConditionToken::SelectionReference(token) - } - } - /// 右括弧と左括弧をだけをパースする。戻り値の配列にはLeftParenthesisとRightParenthesisが含まれず、代わりにTokenContainerに変換される。TokenContainerが括弧で囲まれた部分を表現している。 fn parse_parenthesis( &self, @@ -295,7 +253,10 @@ impl ConditionCompiler { } // ここで再帰的に呼び出す。 - ret.push(ConditionToken::ParenthesisContainer(sub_tokens.into_iter())); + let parsed_sub_token = self.parse(sub_tokens.into_iter())?; + let parenthesis_token = + ConditionToken::ParenthesisContainer(Box::new(parsed_sub_token)); + ret.push(parenthesis_token); } // この時点で右括弧が残っている場合は右括弧の数が左括弧よりも多いことを表している。 @@ -359,125 +320,54 @@ impl ConditionCompiler { } // 次にOrでつながっている部分をまとめる - let or_contaienr = ConditionToken::OrContainer(operands.into_iter()); - Result::Ok(or_contaienr) + Result::Ok(ConditionToken::OrContainer(operands.into_iter())) } /// OperandContainerの中身をパースする。現状はNotをパースするためだけに存在している。 - fn parse_operand_container(parent_token: ConditionToken) -> Result { - if let ConditionToken::OperandContainer(sub_tokens) = parent_token { - // 現状ではNOTの場合は、「not」と「notで修飾されるselectionノードの名前」の2つ入っているはず - // NOTが無い場合、「selectionノードの名前」の一つしか入っていないはず。 - - // 上記の通り、3つ以上入っていることはないはず。 - if sub_tokens.len() >= 3 { - return Result::Err( - "Unknown error. Maybe it is because there are multiple names of selection nodes." - .to_string(), - ); - } - - // 0はありえないはず - if sub_tokens.len() == 0 { - return Result::Err("Unknown error.".to_string()); - } - - // 1つだけ入っている場合、NOTはありえない。 - if sub_tokens.len() == 1 { - let operand_subtoken = sub_tokens.into_iter().next().unwrap(); - if let ConditionToken::Not = operand_subtoken { - return Result::Err("An illegal not was found.".to_string()); - } - - return Result::Ok(operand_subtoken); - } - - // 2つ入っている場合、先頭がNotで次はNotじゃない何かのはず - let mut sub_tokens_ite = sub_tokens; - let first_token = sub_tokens_ite.next().unwrap(); - let second_token = sub_tokens_ite.next().unwrap(); - if let ConditionToken::Not = first_token { - if let ConditionToken::Not = second_token { - Result::Err("Not is continuous.".to_string()) - } else { - let not_container = - ConditionToken::NotContainer(vec![second_token].into_iter()); - Result::Ok(not_container) - } - } else { - Result::Err( - "Unknown error. Maybe it is because there are multiple names of selection nodes." - .to_string(), - ) - } - } else { - let sub_tokens = parent_token.sub_tokens_without_parenthesis(); - if sub_tokens.is_empty() { - return Result::Ok(parent_token); - } - - let mut new_sub_tokens = vec![]; - for sub_token in sub_tokens { - let new_sub_token = Self::parse_operand_container(sub_token)?; - new_sub_tokens.push(new_sub_token); - } - - Result::Ok(parent_token.replace_subtoken(new_sub_tokens)) + fn parse_operand_container(sub_tokens: Vec) -> Result { + // 現状ではNOTの場合は、「not」と「notで修飾されるselectionノードの名前」の2つ入っているはず + // NOTが無い場合、「selectionノードの名前」の一つしか入っていないはず。 + + // 上記の通り、3つ以上入っていることはないはず。 + if sub_tokens.len() >= 3 { + return Result::Err( + "Unknown error. Maybe it is because there are multiple names of selection nodes." + .to_string(), + ); } - } - /// ConditionTokenからSelectionNodeトレイトを実装した構造体に変換します。 - fn to_selectnode( - token: ConditionToken, - name_2_node: &HashMap>>, - ) -> Result, String> { - // RefSelectionNodeに変換 - if let ConditionToken::SelectionReference(selection_name) = token { - let selection_node = name_2_node.get(&selection_name); - if let Some(select_node) = selection_node { - let selection_node = select_node; - let selection_node = Arc::clone(selection_node); - let ref_node = RefSelectionNode::new(selection_node); - return Result::Ok(Box::new(ref_node)); - } else { - let err_msg = format!("{selection_name} is not defined."); - return Result::Err(err_msg); - } + // 0はありえないはず + if sub_tokens.is_empty() { + return Result::Err("Unknown error.".to_string()); } - // AndSelectionNodeに変換 - if let ConditionToken::AndContainer(sub_tokens) = token { - let mut select_and_node = AndSelectionNode::new(); - for sub_token in sub_tokens { - let sub_node = Self::to_selectnode(sub_token, name_2_node)?; - select_and_node.child_nodes.push(sub_node); + // 1つだけ入っている場合、NOTはありえない。 + if sub_tokens.len() == 1 { + let operand_subtoken = sub_tokens.into_iter().next().unwrap(); + if let ConditionToken::Not = operand_subtoken { + return Result::Err("An illegal not was found.".to_string()); } - return Result::Ok(Box::new(select_and_node)); - } - // OrSelectionNodeに変換 - if let ConditionToken::OrContainer(sub_tokens) = token { - let mut select_or_node = OrSelectionNode::new(); - for sub_token in sub_tokens { - let sub_node = Self::to_selectnode(sub_token, name_2_node)?; - select_or_node.child_nodes.push(sub_node); - } - return Result::Ok(Box::new(select_or_node)); + return Result::Ok(operand_subtoken); } - // NotSelectionNodeに変換 - if let ConditionToken::NotContainer(sub_tokens) = token { - if sub_tokens.len() > 1 { - return Result::Err("Unknown error".to_string()); + // 2つ入っている場合、先頭がNotで次はNotじゃない何かのはず + let mut sub_ite = sub_tokens.into_iter(); + let first_token = sub_ite.next().unwrap(); + let second_token = sub_ite.next().unwrap(); + if let ConditionToken::Not = first_token { + if let ConditionToken::Not = second_token { + Result::Err("Not is continuous.".to_string()) + } else { + let not_container = ConditionToken::NotContainer(Box::new(second_token)); + Result::Ok(not_container) } - - let select_sub_node = - Self::to_selectnode(sub_tokens.into_iter().next().unwrap(), name_2_node)?; - let select_not_node = NotSelectionNode::new(select_sub_node); - return Result::Ok(Box::new(select_not_node)); + } else { + Result::Err( + "Unknown error. Maybe it is because there are multiple names of selection nodes." + .to_string(), + ) } - - Result::Err("Unknown error".to_string()) } /// ConditionTokenがAndまたはOrTokenならばTrue @@ -499,9 +389,10 @@ impl ConditionCompiler { ret.push(token); continue; } - ret.push(ConditionToken::OperandContainer( - grouped_operands.into_iter(), - )); + + ret.push(ConditionCompiler::parse_operand_container( + grouped_operands, + )?); ret.push(token); grouped_operands = vec![]; continue; @@ -510,9 +401,9 @@ impl ConditionCompiler { grouped_operands.push(token); } if !grouped_operands.is_empty() { - ret.push(ConditionToken::OperandContainer( - grouped_operands.into_iter(), - )); + ret.push(ConditionCompiler::parse_operand_container( + grouped_operands, + )?); } Result::Ok(ret) diff --git a/src/main.rs b/src/main.rs index e29d379ad..1a512b348 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1282,9 +1282,6 @@ impl App { } else { stored_static.include_status.insert("*".into()); } - println!(); - println!("Loading detection rules. Please wait."); - println!(); if stored_static.html_report_flag { let mut output_data = Nested::::new(); @@ -1317,28 +1314,53 @@ impl App { .min_level .to_uppercase(); - let rule_files = detection::Detection::parse_rule_files( - &level, - &target_level, - &stored_static.output_option.as_ref().unwrap().rules, - &filter::exclude_ids(stored_static), - stored_static, - ); - CHECKPOINT - .lock() - .as_mut() - .unwrap() - .rap_check_point("Rule Parse Processing Time"); - let unused_rules_option = stored_static.logon_summary_flag + println!(); + if !(stored_static.logon_summary_flag || stored_static.search_flag - || stored_static.computer_metrics_flag - || stored_static.metrics_flag; - if !unused_rules_option && rule_files.is_empty() { - AlertMessage::alert( - "No rules were loaded. Please download the latest rules with the update-rules command.\r\n", - ) - .ok(); - return; + || stored_static.metrics_flag + || stored_static.computer_metrics_flag) + { + println!("Loading detection rules. Please wait."); + } else if stored_static.logon_summary_flag { + println!("Currently analyzing Logon Summary. Please wait."); + } else if stored_static.search_flag { + println!("Currently searching. Please wait."); + } else if stored_static.metrics_flag { + println!("Currently analyzing Event ID Metrics. Please wait."); + } else if stored_static.computer_metrics_flag { + println!("Currently analyzing Compute Metrics. Please wait."); + } + println!(); + + let mut rule_files = vec![]; + if !(stored_static.logon_summary_flag + || stored_static.search_flag + || stored_static.metrics_flag + || stored_static.computer_metrics_flag) + { + rule_files = detection::Detection::parse_rule_files( + &level, + &target_level, + &stored_static.output_option.as_ref().unwrap().rules, + &filter::exclude_ids(stored_static), + stored_static, + ); + CHECKPOINT + .lock() + .as_mut() + .unwrap() + .rap_check_point("Rule Parse Processing Time"); + let unused_rules_option = stored_static.logon_summary_flag + || stored_static.search_flag + || stored_static.computer_metrics_flag + || stored_static.metrics_flag; + if !unused_rules_option && rule_files.is_empty() { + AlertMessage::alert( + "No rules were loaded. Please download the latest rules with the update-rules command.\r\n", + ) + .ok(); + return; + } } let template =