diff --git a/Cargo.lock b/Cargo.lock index 8900c785..c79f5655 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -48,9 +48,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.55" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "159bb86af3a200e19a068f4224eae4c8bb2d0fa054c7e5d1cacd5cef95e684cd" +checksum = "4361135be9122e0870de935d7c439aef945b9f9ddd4199a553b5270b49c82a27" [[package]] name = "atty" @@ -359,12 +359,12 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.2" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e54ea8bc3fb1ee042f5aace6e3c6e025d3874866da222930f70ce62aceba0bfa" +checksum = "5aaa7bd5fb665c6864b5f963dd9097905c54125909c7aa94c9e18507cdbe6c53" dependencies = [ "cfg-if 1.0.0", - "crossbeam-utils 0.8.7", + "crossbeam-utils 0.8.8", ] [[package]] @@ -385,8 +385,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" dependencies = [ "cfg-if 1.0.0", - "crossbeam-epoch 0.9.7", - "crossbeam-utils 0.8.7", + "crossbeam-epoch 0.9.8", + "crossbeam-utils 0.8.8", ] [[package]] @@ -406,12 +406,13 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c00d6d2ea26e8b151d99093005cb442fb9a37aeaca582a03ec70946f49ab5ed9" +checksum = "1145cf131a2c6ba0615079ab6a638f7e1973ac9c2634fcbeaaad6114246efe8c" dependencies = [ + "autocfg 1.1.0", "cfg-if 1.0.0", - "crossbeam-utils 0.8.7", + "crossbeam-utils 0.8.8", "lazy_static", "memoffset 0.6.5", "scopeguard", @@ -441,9 +442,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e5bed1f1c269533fa816a0a5492b3545209a205ca1a54842be180eb63a16a6" +checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38" dependencies = [ "cfg-if 1.0.0", "lazy_static", @@ -776,7 +777,7 @@ checksum = "d39cd93900197114fa1fcb7ae84ca742095eed9442088988ae74fa744e930e77" dependencies = [ "cfg-if 1.0.0", "libc", - "wasi", + "wasi 0.10.0+wasi-snapshot-preview1", ] [[package]] @@ -1113,9 +1114,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.119" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf2e165bb3457c8e098ea76f3e3bc9db55f87aa90d52d0e6be741470916aaa4" +checksum = "efaa7b300f3b5fe8eb6bf21ce3895e1751d9665086af2d64b42f19701015ff4f" [[package]] name = "libgit2-sys" @@ -1147,9 +1148,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.3" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de5435b8549c16d423ed0c03dbaafe57cf6c3344744f1242520d59c9d8ecec66" +checksum = "6f35facd4a5673cb5a48822be2be1d4236c1c99cb4113cab7061ac720d5bf859" dependencies = [ "cc", "libc", @@ -1183,9 +1184,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.14" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8" dependencies = [ "cfg-if 1.0.0", ] @@ -1273,14 +1274,15 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba272f85fa0b41fc91872be579b3bbe0f56b792aa361a380eb669469f68dafb2" +checksum = "52da4364ffb0e4fe33a9841a98a3f3014fb964045ce4f7a45a398243c8d6b0c9" dependencies = [ "libc", "log", "miow 0.3.7", "ntapi", + "wasi 0.11.0+wasi-snapshot-preview1", "winapi 0.3.9", ] @@ -1394,9 +1396,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" +checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" [[package]] name = "openssl" @@ -1420,9 +1422,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "111.17.0+1.1.1m" +version = "111.18.0+1.1.1n" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d6a336abd10814198f66e2a91ccd7336611f30334119ca8ce300536666fcf4" +checksum = "7897a926e1e8d00219127dc020130eca4292e5ca666dd592480d72c3eca2ff6c" dependencies = [ "cc", ] @@ -1485,7 +1487,7 @@ checksum = "28141e0cc4143da2443301914478dc976a61ffdb3f043058310c70df2fed8954" dependencies = [ "cfg-if 1.0.0", "libc", - "redox_syscall 0.2.11", + "redox_syscall 0.2.12", "smallvec 1.8.0", "windows-sys", ] @@ -1580,9 +1582,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" +checksum = "632d02bff7f874a36f33ea8bb416cd484b90cc66c1194b1a1110d067a7013f58" dependencies = [ "proc-macro2", ] @@ -1713,7 +1715,7 @@ checksum = "d78120e2c850279833f1dd3582f730c4ab53ed95aeaaaa862a2a5c71b1656d8e" dependencies = [ "crossbeam-channel", "crossbeam-deque 0.8.1", - "crossbeam-utils 0.8.7", + "crossbeam-utils 0.8.8", "lazy_static", "num_cpus", ] @@ -1735,18 +1737,18 @@ checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" [[package]] name = "redox_syscall" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8380fe0152551244f0747b1bf41737e0f8a74f97a14ccefd1148187271634f3c" +checksum = "8ae183fc1b06c149f0c1793e1eb447c8b04bfe46d48e9e48bfb8d2d7ed64ecf0" dependencies = [ "bitflags", ] [[package]] name = "regex" -version = "1.5.4" +version = "1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286" dependencies = [ "aho-corasick", "memchr", @@ -2142,9 +2144,9 @@ checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" [[package]] name = "syn" -version = "1.0.86" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" +checksum = "ea297be220d52398dcc07ce15a209fce436d361735ac1db700cab3b6cdfb9f54" dependencies = [ "proc-macro2", "quote", @@ -2172,16 +2174,16 @@ dependencies = [ "cfg-if 1.0.0", "fastrand", "libc", - "redox_syscall 0.2.11", + "redox_syscall 0.2.12", "remove_dir_all", "winapi 0.3.9", ] [[package]] name = "termcolor" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" dependencies = [ "winapi-util", ] @@ -2232,7 +2234,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" dependencies = [ "libc", - "wasi", + "wasi 0.10.0+wasi-snapshot-preview1", "winapi 0.3.9", ] @@ -2317,7 +2319,7 @@ dependencies = [ "bytes 1.1.0", "libc", "memchr", - "mio 0.8.0", + "mio 0.8.2", "num_cpus", "once_cell", "parking_lot 0.12.0", @@ -2598,6 +2600,12 @@ version = "0.10.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + [[package]] name = "wasm-bindgen" version = "0.2.79" @@ -2786,6 +2794,6 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.5.3" +version = "1.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50344758e2f40e3a1fcfc8f6f91aa57b5f8ebd8d27919fe6451f15aaaf9ee608" +checksum = "7eb5728b8afd3f280a869ce1d4c554ffaed35f45c231fc41bfbd0381bef50317" diff --git a/src/detections/rule/matchers.rs b/src/detections/rule/matchers.rs index 7c559411..744b45a0 100644 --- a/src/detections/rule/matchers.rs +++ b/src/detections/rule/matchers.rs @@ -1,5 +1,5 @@ use regex::Regex; -use std::collections::VecDeque; +use std::{cmp::Ordering, collections::VecDeque}; use yaml_rust::Yaml; use crate::detections::{detection::EvtxRecordInfo, utils}; @@ -192,6 +192,7 @@ pub struct DefaultMatcher { re: Option, pipes: Vec, key_list: Vec, + eqfield_key: Option, } impl DefaultMatcher { @@ -200,9 +201,14 @@ impl DefaultMatcher { re: Option::None, pipes: Vec::new(), key_list: Vec::new(), + eqfield_key: Option::None, } } + pub fn get_eqfield_key(&self) -> Option<&String> { + self.eqfield_key.as_ref() + } + /// このmatcherの正規表現とマッチするかどうか判定します。 /// 判定対象の文字列とこのmatcherが保持する正規表現が完全にマッチした場合のTRUEを返します。 /// 例えば、判定対象文字列が"abc"で、正規表現が"ab"の場合、正規表現は判定対象文字列の一部分にしか一致していないので、この関数はfalseを返します。 @@ -265,6 +271,7 @@ impl LeafMatcher for DefaultMatcher { "endswith" => Option::Some(PipeElement::Endswith), "contains" => Option::Some(PipeElement::Contains), "re" => Option::Some(PipeElement::Re), + "equalsfield" => Option::Some(PipeElement::EqualsField), _ => Option::None, }; if pipe_element.is_none() { @@ -285,34 +292,54 @@ impl LeafMatcher for DefaultMatcher { ); return Result::Err(vec![errmsg]); } - let is_re = &self + + let is_eqfield = self .pipes .iter() - .any(|pipe_element| matches!(pipe_element, PipeElement::Re)); - // 正規表現ではない場合、ワイルドカードであることを表す。 - // ワイルドカードは正規表現でマッチングするので、ワイルドカードを正規表現に変換するPipeを内部的に追加することにする。 - if !is_re { - self.pipes.push(PipeElement::Wildcard); - } + .any(|pipe_element| matches!(pipe_element, PipeElement::EqualsField)); + if is_eqfield { + // PipeElement::EqualsFieldは特別 + self.eqfield_key = Option::Some(pattern); + } else { + // 正規表現ではない場合、ワイルドカードであることを表す。 + // ワイルドカードは正規表現でマッチングするので、ワイルドカードを正規表現に変換するPipeを内部的に追加することにする。 + let is_re = self + .pipes + .iter() + .any(|pipe_element| matches!(pipe_element, PipeElement::Re)); + if !is_re { + self.pipes.push(PipeElement::Wildcard); + } - // パターンをPipeで処理する。 - let pattern = DefaultMatcher::from_pattern_to_regex_str(pattern, &self.pipes); - // Pipeで処理されたパターンを正規表現に変換 - let re_result = Regex::new(&pattern); - if re_result.is_err() { - let errmsg = format!( - "Cannot parse regex. [regex:{}, key:{}]", - pattern, - utils::concat_selection_key(key_list) - ); - return Result::Err(vec![errmsg]); + let pattern = DefaultMatcher::from_pattern_to_regex_str(pattern, &self.pipes); + // Pipeで処理されたパターンを正規表現に変換 + let re_result = Regex::new(&pattern); + if re_result.is_err() { + let errmsg = format!( + "Cannot parse regex. [regex:{}, key:{}]", + pattern, + utils::concat_selection_key(key_list) + ); + return Result::Err(vec![errmsg]); + } + self.re = re_result.ok(); } - self.re = re_result.ok(); Result::Ok(()) } - fn is_match(&self, event_value: Option<&String>, _recinfo: &EvtxRecordInfo) -> bool { + fn is_match(&self, event_value: Option<&String>, recinfo: &EvtxRecordInfo) -> bool { + // PipeElement::EqualsFieldが設定されていた場合 + if let Some(eqfield_key) = &self.eqfield_key { + let another_value = recinfo.get_value(eqfield_key); + // Evtxのレコードに存在しないeventkeyを指定された場合はfalseにする + if event_value.is_none() || another_value.is_none() { + return false; + } + + return another_value.unwrap().cmp(event_value.unwrap()) == Ordering::Equal; + } + // yamlにnullが設定されていた場合 // keylistが空(==JSONのgrep検索)の場合、無視する。 if self.key_list.is_empty() && self.re.is_none() { @@ -341,6 +368,7 @@ enum PipeElement { Contains, Re, Wildcard, + EqualsField, } impl PipeElement { @@ -377,10 +405,9 @@ impl PipeElement { PipeElement::Endswith => fn_add_asterisk_begin(pattern), // containsの場合はpatternの前後にwildcardを足すことで対応する PipeElement::Contains => fn_add_asterisk_end(fn_add_asterisk_begin(pattern)), - // 正規表現の場合は特に処理する必要無い - PipeElement::Re => pattern, // WildCardは正規表現に変換する。 PipeElement::Wildcard => PipeElement::pipe_pattern_wildcard(pattern), + _ => pattern, } } @@ -1707,4 +1734,128 @@ mod tests { } } } + + #[test] + fn test_eq_field() { + // equalsfieldsで正しく検知できることを確認 + let rule_str = r#" + detection: + selection: + Channel|equalsfield: Computer + details: 'command=%CommandLine%' + "#; + + let record_json_str = r#" + { + "Event": {"System": {"EventID": 4103, "Channel": "Security", "Computer": "Security" }}, + "Event_attributes": {"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"} + }"#; + + let mut rule_node = parse_rule_from_str(rule_str); + match serde_json::from_str(record_json_str) { + Ok(record) => { + let keys = detections::rule::get_detection_keys(&rule_node); + let recinfo = utils::create_rec_info(record, "testpath".to_owned(), &keys); + assert!(rule_node.select(&recinfo)); + } + Err(_) => { + panic!("Failed to parse json record."); + } + } + } + + #[test] + fn test_eq_field_notdetect() { + // equalsfieldsの検知できないパターン + // equalsfieldsで正しく検知できることを確認 + let rule_str = r#" + detection: + selection: + Channel|equalsfield: Computer + details: 'command=%CommandLine%' + "#; + + let record_json_str = r#" + { + "Event": {"System": {"EventID": 4103, "Channel": "Security", "Computer": "Powershell" }}, + "Event_attributes": {"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"} + }"#; + + let mut rule_node = parse_rule_from_str(rule_str); + match serde_json::from_str(record_json_str) { + Ok(record) => { + let keys = detections::rule::get_detection_keys(&rule_node); + let recinfo = utils::create_rec_info(record, "testpath".to_owned(), &keys); + assert!(!rule_node.select(&recinfo)); + } + Err(_) => { + panic!("Failed to parse json record."); + } + } + } + + #[test] + fn test_eq_field_emptyfield() { + // 存在しないフィールドを指定した場合は検知しない + let rule_str = r#" + detection: + selection: + Channel|equalsfield: NoField + details: 'command=%CommandLine%' + "#; + + let record_json_str = r#" + { + "Event": {"System": {"EventID": 4103, "Channel": "Security", "Computer": "Securiti" }}, + "Event_attributes": {"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"} + }"#; + + let mut rule_node = parse_rule_from_str(rule_str); + match serde_json::from_str(record_json_str) { + Ok(record) => { + let keys = detections::rule::get_detection_keys(&rule_node); + let recinfo = utils::create_rec_info(record, "testpath".to_owned(), &keys); + assert!(!rule_node.select(&recinfo)); + } + Err(_) => { + panic!("Failed to parse json record."); + } + } + + let rule_str = r#" + detection: + selection: + NoField|equalsfield: Channel + details: 'command=%CommandLine%' + "#; + let mut rule_node = parse_rule_from_str(rule_str); + match serde_json::from_str(record_json_str) { + Ok(record) => { + let keys = detections::rule::get_detection_keys(&rule_node); + let recinfo = utils::create_rec_info(record, "testpath".to_owned(), &keys); + assert!(!rule_node.select(&recinfo)); + } + Err(_) => { + panic!("Failed to parse json record."); + } + } + + let rule_str = r#" + detection: + selection: + NoField|equalsfield: NoField1 + details: 'command=%CommandLine%' + "#; + let mut rule_node = parse_rule_from_str(rule_str); + match serde_json::from_str(record_json_str) { + Ok(record) => { + let keys = detections::rule::get_detection_keys(&rule_node); + let recinfo = utils::create_rec_info(record, "testpath".to_owned(), &keys); + assert!(!rule_node.select(&recinfo)); + } + Err(_) => { + panic!("Failed to parse json record."); + } + } + } } diff --git a/src/detections/rule/mod.rs b/src/detections/rule/mod.rs index ec2b56b9..cfa1173b 100644 --- a/src/detections/rule/mod.rs +++ b/src/detections/rule/mod.rs @@ -106,19 +106,21 @@ pub fn get_detection_keys(node: &RuleNode) -> Vec { for key in detection.name_to_selection.keys() { let selection = &detection.name_to_selection[key]; let desc = selection.get_descendants(); - let keys = desc.iter().filter_map(|node| { + desc.iter().for_each(|node| { if !node.is::() { - return Option::None; + return; } let node = node.downcast_ref::().unwrap(); - let key = node.get_key(); - if key.is_empty() { - return Option::None; - } - Option::Some(key.to_string()) + let keys = node.get_keys(); + let keys = keys.iter().filter_map(|key| { + if key.is_empty() { + return None; + } + Some(key.to_string()) + }); + ret.extend(keys); }); - ret.extend(keys); } ret diff --git a/src/detections/rule/selectionnodes.rs b/src/detections/rule/selectionnodes.rs index becb3253..2c094d14 100644 --- a/src/detections/rule/selectionnodes.rs +++ b/src/detections/rule/selectionnodes.rs @@ -3,7 +3,7 @@ use downcast_rs::Downcast; use std::{sync::Arc, vec}; use yaml_rust::Yaml; -use super::matchers; +use super::matchers::{self, DefaultMatcher}; // Ruleファイルの detection- selection配下のノードはこのtraitを実装する。 pub trait SelectionNode: Downcast { @@ -250,6 +250,24 @@ impl LeafSelectionNode { &self.key } + pub fn get_keys(&self) -> Vec<&String> { + let mut keys = vec![]; + if !self.key.is_empty() { + keys.push(&self.key); + } + + if let Some(matcher) = &self.matcher { + let matcher = matcher.downcast_ref::(); + if let Some(matcher) = matcher { + if let Some(eq_key) = matcher.get_eqfield_key() { + keys.push(eq_key); + } + } + } + + keys + } + fn _create_key(&self) -> String { if self.key_list.is_empty() { return String::default();