diff --git a/CHANGELOG-Japanese.md b/CHANGELOG-Japanese.md index 1ccf422dc..53bf82b43 100644 --- a/CHANGELOG-Japanese.md +++ b/CHANGELOG-Japanese.md @@ -65,6 +65,7 @@ - `metrics`と`logon-summary`コマンドのレコード数の表示が`csv-timeline`のコマンドでのレコード数の表示と異なっている状態を修正した。 (#1105) (@hitenkoku) - パスの代わりにルールIDでルール数を数えるように変更した。 (#1113) (@hitenkoku) +- JSON出力で`CommandLine`フィールド内で誤ったフィールドの分割が行われてしまう問題を修正した。 (#1145) (@hitenkoku) - `json-timeline`コマンドで`--timeline-start`と`--timeline-end`オプションが動作しなかったのを修正した。 (#1148) (@hitenkoku) - `pivot-keywords-list`コマンドで`--timeline-start`と`--timeline-end`オプションが動作しなかったのを修正した。 (#1150) (@hitenkoku) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c9d9c070..55886e735 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,7 @@ - The total number of records being displayed in the `metrics` and `logon-summary` commands differed from the `csv-timeline` command. (#1105) (@hitenkoku) - Changed rule count by rule ID instead of path. (#1113) (@hitenkoku) +- Fixed a problem with incorrect field splitting in the `CommandLine` field in JSON output. (#1145) (@hitenkoku) - `--timeline-start` and `--timeline-end` were not working correctly with the `json-timeline` command. (#1148) (@hitenkoku) - `--timeline-start` and `--timeline-end` were not working correctly with the `pivot-keywords-list` command. (#1150) (@hitenkoku) diff --git a/Cargo.lock b/Cargo.lock index ed7ef2e17..7a1e5cac3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -74,9 +74,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15c4c2c83f81532e5845a733998b6971faca23490340a418e9b72a3ec9de12ea" +checksum = "b84bf0a05bbb2a83e5eb6fa36bb6e87baa08193c35ff52bbf6b38d8af2890e46" [[package]] name = "anstyle-parse" @@ -158,9 +158,9 @@ checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" [[package]] name = "base64" -version = "0.21.3" +version = "0.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "414dcefbc63d77c526a76b3afcf6fbb9b5e2791c19c3aa2297733208750c6e53" +checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" [[package]] name = "bitflags" @@ -176,9 +176,9 @@ checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" [[package]] name = "bumpalo" -version = "3.13.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "bytecount" @@ -262,9 +262,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.30" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "defd4e7873dbddba6c7c91e199c7fcb946abc4a6a4ac3195400bcfb01b5de877" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" dependencies = [ "android-tzdata", "iana-time-zone", @@ -305,9 +305,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.2" +version = "4.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a13b88d2c62ff462f88e4a121f17a82c1af05693a2f192b5c38d14de73c19f6" +checksum = "84ed82781cea27b43c9b106a979fe450a13a31aab0500595fb3fc06616de08e6" dependencies = [ "clap_builder", "clap_derive", @@ -334,7 +334,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.35", ] [[package]] @@ -688,7 +688,7 @@ dependencies = [ [[package]] name = "evtx" version = "0.8.7" -source = "git+https://github.com/Yamato-Security/hayabusa-evtx.git?rev=fe38ad6#fe38ad649d6c73c1c7d7fcba1d38245f62582fb5" +source = "git+https://github.com/Yamato-Security/hayabusa-evtx.git?rev=c8391f1#c8391f173eb5d80b9def72ffd68e2a5c6867c945" dependencies = [ "anyhow", "bitflags 2.4.0", @@ -842,7 +842,7 @@ dependencies = [ "bytesize", "chrono", "cidr-utils", - "clap 4.4.2", + "clap 4.4.3", "comfy-table", "compact_str", "crossbeam-utils", @@ -881,7 +881,7 @@ dependencies = [ "serde_derive", "serde_json", "termcolor", - "terminal_size", + "terminal_size 0.3.0", "tokio", "ureq", "yaml-rust", @@ -1110,14 +1110,14 @@ checksum = "8244e0ff6c548152c07559ee9779dec5a5411eeee5bfd6146b38bd414a6841c6" dependencies = [ "anyhow", "chrono", - "clap 4.4.2", + "clap 4.4.3", "file-chunker", "memmap2 0.7.1", "num_cpus", "rayon", "regex", "tempfile", - "terminal_size", + "terminal_size 0.2.6", ] [[package]] @@ -1128,9 +1128,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.147" +version = "0.2.148" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" [[package]] name = "libgit2-sys" @@ -1148,9 +1148,9 @@ dependencies = [ [[package]] name = "libmimalloc-sys" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25d058a81af0d1c22d7a1c948576bee6d673f7af3c0f35564abd6c81122f513d" +checksum = "3979b5c37ece694f1f5e51e7ecc871fdb0f517ed04ee45f88d15d6d553cb9664" dependencies = [ "cc", "cty", @@ -1197,9 +1197,9 @@ checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "linux-raw-sys" -version = "0.4.5" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" +checksum = "1a9bad9f94746442c783ca431b22403b519cd7fbeed0533fdd6328b2f2212128" [[package]] name = "lock_api" @@ -1264,9 +1264,9 @@ dependencies = [ [[package]] name = "mimalloc" -version = "0.1.38" +version = "0.1.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "972e5f23f6716f62665760b0f4cbf592576a80c7b879ba9beaafc0e558894127" +checksum = "fa01922b5ea280a911e323e4d2fd24b7fe5cc4042e0d2cda3c40775cdc4bdc9c" dependencies = [ "libmimalloc-sys", ] @@ -1491,7 +1491,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.35", ] [[package]] @@ -1619,9 +1619,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.66" +version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" dependencies = [ "unicode-ident", ] @@ -1812,14 +1812,14 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.11" +version = "0.38.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0c3dde1fc030af041adc40e79c0e7fbcf431dd24870053d187d7c66e4b87453" +checksum = "d7db8590df6dfcd144d22afd1b83b36c21a18d7cbc1dc4bb5295a8712e9eb662" dependencies = [ "bitflags 2.4.0", "errno", "libc", - "linux-raw-sys 0.4.5", + "linux-raw-sys 0.4.7", "windows-sys 0.48.0", ] @@ -1831,15 +1831,15 @@ checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" dependencies = [ "log", "ring", - "rustls-webpki 0.101.4", + "rustls-webpki 0.101.5", "sct", ] [[package]] name = "rustls-webpki" -version = "0.100.2" +version = "0.100.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e98ff011474fa39949b7e5c0428f9b4937eda7da7848bbb947786b7be0b27dab" +checksum = "5f6a5fc258f1c1276dfe3016516945546e2d5383911efc0fc4f1cdc5df3a4ae3" dependencies = [ "ring", "untrusted", @@ -1847,9 +1847,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.101.4" +version = "0.101.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d93931baf2d282fff8d3a532bbfd7653f734643161b87e3e01e59a04439bf0d" +checksum = "45a27e3b59326c16e23d30aeb7a36a24cc0d29e71d68ff611cdfb4a01d013bed" dependencies = [ "ring", "untrusted", @@ -1933,14 +1933,14 @@ checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.35", ] [[package]] name = "serde_json" -version = "1.0.105" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" +checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" dependencies = [ "itoa", "ryu", @@ -2032,9 +2032,9 @@ checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" [[package]] name = "socket2" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" dependencies = [ "libc", "windows-sys 0.48.0", @@ -2148,9 +2148,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.31" +version = "2.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "718fa2415bcb8d8bd775917a1bf12a7931b6dfa890753378538118181e0cb398" +checksum = "59bf04c28bee9043ed9ea1e41afc0552288d3aba9c6efdd78903b802926f4879" dependencies = [ "proc-macro2", "quote", @@ -2166,7 +2166,7 @@ dependencies = [ "cfg-if", "fastrand", "redox_syscall", - "rustix 0.38.11", + "rustix 0.38.13", "windows-sys 0.48.0", ] @@ -2189,6 +2189,16 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "terminal_size" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" +dependencies = [ + "rustix 0.38.13", + "windows-sys 0.48.0", +] + [[package]] name = "termtree" version = "0.4.1" @@ -2218,7 +2228,7 @@ checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" dependencies = [ "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.35", ] [[package]] @@ -2331,7 +2341,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.35", ] [[package]] @@ -2351,9 +2361,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" @@ -2387,7 +2397,7 @@ dependencies = [ "log", "once_cell", "rustls", - "rustls-webpki 0.100.2", + "rustls-webpki 0.100.3", "url", "webpki-roots", ] @@ -2458,7 +2468,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.35", "wasm-bindgen-shared", ] @@ -2480,7 +2490,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.35", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2507,7 +2517,7 @@ version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b03058f88386e5ff5310d9111d53f48b17d732b401aeb83a8d5190f2ac459338" dependencies = [ - "rustls-webpki 0.100.2", + "rustls-webpki 0.100.3", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 38296ef9b..c1ca840e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ include = ["src/**/*", "LICENSE.txt", "README.md", "CHANGELOG.md"] itertools = "*" dashmap = "*" clap = { version = "4.*", features = ["derive", "cargo", "color"]} -evtx = { git = "https://github.com/Yamato-Security/hayabusa-evtx.git" , features = ["fast-alloc"] , rev = "fe38ad6" } # 0.8.7 2023/08/30 update +evtx = { git = "https://github.com/Yamato-Security/hayabusa-evtx.git" , features = ["fast-alloc"] , rev = "c8391f1" } # 0.8.7 2023/08/30 update quick-xml = {version = "0.*", features = ["serialize"] } serde = { version = "1.*", features = ["derive"] } serde_json = { version = "1.0"} diff --git a/src/afterfact.rs b/src/afterfact.rs index 7d08426a1..c5c5c1e64 100644 --- a/src/afterfact.rs +++ b/src/afterfact.rs @@ -328,6 +328,7 @@ fn emit_csv( // remove duplicate dataのための前レコード分の情報を保持する変数 let mut prev_message: HashMap = HashMap::new(); + let mut prev_details_convert_map: HashMap> = HashMap::new(); for (message_idx, time) in MESSAGEKEYS .lock() .unwrap() @@ -410,8 +411,11 @@ fn emit_csv( jsonl_output_flag, GEOIP_DB_PARSER.read().unwrap().is_some(), remove_duplicate_data_flag, + detect_info.is_condition, + &[&detect_info.details_convert_map, &prev_details_convert_map], ); prev_message = result.1; + prev_details_convert_map = detect_info.details_convert_map.clone(); wtr.write_field(format!("{{ {} }}", &result.0))?; } else if json_output_flag { // JSON output @@ -422,8 +426,11 @@ fn emit_csv( jsonl_output_flag, GEOIP_DB_PARSER.read().unwrap().is_some(), remove_duplicate_data_flag, + detect_info.is_condition, + &[&detect_info.details_convert_map, &prev_details_convert_map], ); prev_message = result.1; + prev_details_convert_map = detect_info.details_convert_map.clone(); wtr.write_field(&result.0)?; wtr.write_field("}")?; } else { @@ -1426,6 +1433,8 @@ pub fn output_json_str( jsonl_output_flag: bool, is_included_geo_ip: bool, remove_duplicate_flag: bool, + is_condition: bool, + details_infos: &[&HashMap>], ) -> (String, HashMap) { let mut target: Vec = vec![]; let mut target_ext_field = Vec::new(); @@ -1435,15 +1444,32 @@ pub fn output_json_str( for (field_name, profile) in ext_field.iter() { match profile { Profile::Details(_) | Profile::AllFieldInfo(_) | Profile::ExtraFieldInfo(_) => { - if prev_message - .get(field_name) - .unwrap_or(&Profile::Literal("-".into())) - .to_value() - == profile.to_value() - { + let details_key = match profile { + Profile::Details(_) => "Details", + Profile::AllFieldInfo(_) => "AllFieldInfo", + Profile::ExtraFieldInfo(_) => "ExtraFieldInfo", + _ => "", + }; + + let empty = vec![]; + let now = details_infos[0] + .get(format!("#{details_key}").as_str()) + .unwrap_or(&empty); + let prev = details_infos[1] + .get(format!("#{details_key}").as_str()) + .unwrap_or(&empty); + let dup_flag = (!profile.to_value().is_empty() + && prev_message + .get(field_name) + .unwrap_or(&Profile::Literal("".into())) + .to_value() + == profile.to_value()) + || (!&now.is_empty() && !&prev.is_empty() && now == prev); + if dup_flag { // 合致する場合は前回レコード分のメッセージを更新する合致している場合は出力用のフィールドマップの内容を変更する。 // 合致しているので前回分のメッセージは更新しない - target_ext_field.push((field_name.clone(), profile.convert(&"DUP".into()))); + //DUPという通常の文字列を出すためにProfile::Literalを使用する + target_ext_field.push((field_name.clone(), Profile::Literal("DUP".into()))); } else { // 合致しない場合は前回レコード分のメッセージを更新する next_prev_message.insert(field_name.clone(), profile.clone()); @@ -1466,6 +1492,7 @@ pub fn output_json_str( "TgtCountry", "TgtCity", ]; + let valid_key_add_to_details: Vec<&str> = key_add_to_details .iter() .filter(|target_key| { @@ -1477,7 +1504,13 @@ pub fn output_json_str( for (key, profile) in target_ext_field.iter() { let val = profile.to_value(); let vec_data = _get_json_vec(profile, &val.to_string()); - if !key_add_to_details.contains(&key.as_str()) && vec_data.is_empty() { + if (!key_add_to_details.contains(&key.as_str()) + && !matches!( + profile, + Profile::AllFieldInfo(_) | Profile::ExtraFieldInfo(_) + )) + && vec_data.is_empty() + { let tmp_val: Vec<&str> = val.split(": ").collect(); let output_val = _convert_valid_json_str(&tmp_val, matches!(profile, Profile::AllFieldInfo(_))); @@ -1508,106 +1541,59 @@ pub fn output_json_str( } Profile::Details(_) | Profile::AllFieldInfo(_) | Profile::ExtraFieldInfo(_) => { let mut output_stock: Vec = vec![]; - output_stock.push(format!(" \"{key}\": {{")); - let mut stocked_value: Vec> = vec![]; - let mut key_index_stock = vec![]; - for detail_contents in vec_data.iter() { - // 分解してキーとなりえる箇所を抽出する - let mut tmp_stock = vec![]; - let mut space_split_contents = detail_contents.split(' '); - while let Some(sp) = space_split_contents.next() { - if !sp.contains('\\') && sp.ends_with(':') && sp.len() > 2 { - key_index_stock.push(sp.replace(':', "")); - if sp == "Payload:" { - stocked_value.push(vec![]); - stocked_value.push( - space_split_contents.map(|s| s.to_string()).collect(), - ); - break; - } else { - stocked_value.push(tmp_stock); - tmp_stock = vec![]; - } - } else { - tmp_stock.push(sp.to_owned()); - } - } - if !tmp_stock.is_empty() { - stocked_value.push(tmp_stock); - } - } - if stocked_value - .iter() - .counts_by(|x| x.len()) - .get(&0) - .unwrap_or(&0) - != &key_index_stock.len() - { - if let Some((target_idx, _)) = key_index_stock - .iter() - .enumerate() - .rfind(|(_, y)| "CmdLine" == *y) - { - let cmd_line_vec_idx_len = - stocked_value[2 * (target_idx + 1) - 1].len(); - stocked_value[2 * (target_idx + 1) - 1][cmd_line_vec_idx_len - 1] - .push_str(&format!(" {}:", key_index_stock[target_idx + 1])); - key_index_stock.remove(target_idx + 1); - } + let details_key = match profile { + Profile::Details(_) => "Details", + Profile::AllFieldInfo(_) => "AllFieldInfo", + Profile::ExtraFieldInfo(_) => "ExtraFieldInfo", + _ => "", + }; + let details_target_stocks = + details_infos[0].get(&CompactString::from(format!("#{details_key}"))); + if details_target_stocks.is_none() { + continue; } - let mut key_idx = 0; - let mut output_value_stock = String::default(); - for (value_idx, value) in stocked_value.iter().enumerate() { - if key_idx >= key_index_stock.len() { - break; - } - let mut tmp = if value_idx == 0 && !value.is_empty() { - key.as_str() + let details_target_stock = details_target_stocks.unwrap(); + // aggregation conditionの場合は分解せずにそのまま出力する + if is_condition { + let details_val = + if details_target_stock.is_empty() || details_target_stock[0] == "-" { + "-".into() + } else { + details_target_stock[0].clone() + }; + output_stock.push(_create_json_output_format( + key, + &details_val, + key.starts_with('\"'), + details_val.starts_with('\"'), + 4, + )); + if jsonl_output_flag { + target.push(output_stock.join("")); } else { - key_index_stock[key_idx].as_str() - }; - if !output_value_stock.is_empty() { - output_value_stock.push_str(" | "); + target.push(output_stock.join("\n")); } - output_value_stock.push_str(&value.join(" ")); - //1つまえのキーの段階で以降にvalueの配列で区切りとなる空の配列が存在しているかを確認する - let is_remain_split_stock = key_index_stock.len() > 1 - && key_idx == key_index_stock.len() - 2 - && value_idx < stocked_value.len() - 1 - && !output_value_stock.is_empty() - && !stocked_value[value_idx + 1..] - .iter() - .any(|remain_value| remain_value.is_empty()); - 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); + continue; + } else { + output_stock.push(format!(" \"{key}\": {{")); + }; + for (idx, contents) in details_target_stock.iter().enumerate() { + 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], false); + + if idx != details_target_stock.len() - 1 { output_stock.push(format!( "{},", _create_json_output_format( - &key, + &output_key, &fmted_val, key.starts_with('\"'), fmted_val.starts_with('\"'), 8 ) )); - output_value_stock.clear(); - tmp = ""; - key_idx += 1; - } - if value_idx == stocked_value.len() - 1 - && !(tmp.is_empty() && stocked_value.is_empty()) - { - 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); + } else { let last_contents_end = if is_included_geo_ip && !valid_key_add_to_details.is_empty() { "," @@ -1617,14 +1603,13 @@ pub fn output_json_str( output_stock.push(format!( "{}{last_contents_end}", _create_json_output_format( - &key, + key, &fmted_val, key.starts_with('\"'), fmted_val.starts_with('\"'), 8, ) )); - key_idx += 1; } } if is_included_geo_ip { @@ -2054,6 +2039,7 @@ mod tests { detail: CompactString::default(), ext_field: output_profile.to_owned(), is_condition: false, + details_convert_map: HashMap::default(), }, expect_time, &profile_converter, @@ -2076,6 +2062,7 @@ mod tests { detail: CompactString::default(), ext_field: output_profile.to_owned(), is_condition: false, + details_convert_map: HashMap::default(), }, expect_time, &profile_converter, @@ -2377,6 +2364,7 @@ mod tests { detail: CompactString::default(), ext_field: output_profile.to_owned(), is_condition: false, + details_convert_map: HashMap::default(), }, expect_time, &profile_converter, @@ -2399,6 +2387,7 @@ mod tests { detail: CompactString::default(), ext_field: output_profile.to_owned(), is_condition: false, + details_convert_map: HashMap::default(), }, expect_time, &profile_converter, @@ -2690,6 +2679,7 @@ mod tests { detail: CompactString::default(), ext_field: output_profile.to_owned(), is_condition: false, + details_convert_map: HashMap::default(), }, expect_time, &profile_converter, @@ -2712,6 +2702,7 @@ mod tests { detail: CompactString::default(), ext_field: output_profile.to_owned(), is_condition: false, + details_convert_map: HashMap::default(), }, expect_time, &profile_converter, @@ -2998,6 +2989,8 @@ mod tests { .to_str() .unwrap(), ); + let details_convert_map: HashMap> = + HashMap::from_iter([("#AllFieldInfo".into(), vec![test_recinfo.into()])]); message::insert( &event, CompactString::new(output), @@ -3011,10 +3004,11 @@ mod tests { detail: CompactString::default(), ext_field: output_profile.to_owned(), is_condition: false, + details_convert_map, }, expect_time, &profile_converter, - (false, false, false), + (false, true, true), (&eventkey_alias, &FieldDataMapKey::default(), &None), ); *profile_converter.get_mut("Computer").unwrap() = @@ -3033,10 +3027,11 @@ mod tests { detail: CompactString::default(), ext_field: output_profile.to_owned(), is_condition: false, + details_convert_map: HashMap::default(), }, expect_time, &profile_converter, - (false, false, false), + (false, true, true), (&eventkey_alias, &FieldDataMapKey::default(), &None), ); let multi = message::MESSAGES.get(&expect_time).unwrap(); @@ -3541,6 +3536,8 @@ mod tests { ); let messages = &message::MESSAGES; messages.clear(); + let details_convert_map: HashMap> = + HashMap::from_iter([("#AllFieldInfo".into(), vec![test_recinfo.into()])]); message::insert( &event, CompactString::new(output), @@ -3554,10 +3551,11 @@ mod tests { detail: CompactString::default(), ext_field: output_profile.to_owned(), is_condition: false, + details_convert_map, }, expect_time, &profile_converter, - (false, false, false), + (false, true, true), (&eventkey_alias, &FieldDataMapKey::default(), &None), ); *profile_converter.get_mut("Computer").unwrap() = @@ -3789,6 +3787,8 @@ mod tests { ("EvtxFile", Profile::EvtxFile(test_filepath.into())), ("Tags", Profile::MitreTags(test_attack.into())), ]); + let details_convert_map: HashMap> = + HashMap::from_iter([("#AllFieldInfo".into(), vec![test_recinfo.into()])]); let eventkey_alias = load_eventkey_alias( utils::check_setting_path( &CURRENT_EXE_PATH.to_path_buf(), @@ -3814,10 +3814,11 @@ mod tests { detail: CompactString::default(), ext_field: output_profile.to_owned(), is_condition: false, + details_convert_map, }, expect_time, &profile_converter, - (false, false, false), + (false, true, true), (&eventkey_alias, &FieldDataMapKey::default(), &None), ); *profile_converter.get_mut("Computer").unwrap() = diff --git a/src/detections/detection.rs b/src/detections/detection.rs index 07027d615..d9040e98a 100644 --- a/src/detections/detection.rs +++ b/src/detections/detection.rs @@ -655,6 +655,7 @@ impl Detection { _ => {} } } + //ルール側にdetailsの項目があればそれをそのまま出力し、そうでない場合はproviderとeventidの組で設定したdetailsの項目を出力する let details_fmt_str = match rule.yaml["details"].as_str() { Some(s) => s.to_string(), None => match stored_static @@ -662,10 +663,11 @@ impl Detection { .get(&CompactString::from(format!("{provider}_{eid}"))) { Some(str) => str.to_string(), - None => create_recordinfos(&record_info.record, &FieldDataMapKey::default(), &None), + None => create_recordinfos(&record_info.record, &FieldDataMapKey::default(), &None) + .join(" ¦ "), }, }; - let field_data_map_key = if stored_static.field_data_map.is_none() { + let field_data_map_key: FieldDataMapKey = if stored_static.field_data_map.is_none() { FieldDataMapKey::default() } else { FieldDataMapKey { @@ -693,6 +695,7 @@ impl Detection { detail: CompactString::default(), ext_field: stored_static.profiles.as_ref().unwrap().to_owned(), is_condition: false, + details_convert_map: HashMap::default(), }; message::insert( &record_info.record, @@ -911,6 +914,7 @@ impl Detection { detail: output, ext_field: stored_static.profiles.as_ref().unwrap().to_owned(), is_condition: true, + details_convert_map: HashMap::default(), }; let binding = STORED_EKEY_ALIAS.read().unwrap(); let eventkey_alias = binding.as_ref().unwrap(); @@ -1144,9 +1148,9 @@ impl Detection { is_csv_output: bool, ) -> CompactString { for alias in target_alias { - let search_data = message::parse_message( + let (search_data, _) = message::parse_message( record, - CompactString::from(alias), + &CompactString::from(alias), eventkey_alias, is_csv_output, &FieldDataMapKey::default(), diff --git a/src/detections/field_data_map.rs b/src/detections/field_data_map.rs index c2aca4b5e..3b6ee9041 100644 --- a/src/detections/field_data_map.rs +++ b/src/detections/field_data_map.rs @@ -333,7 +333,7 @@ mod tests { Ok(record) => { let ret = utils::create_recordinfos(&record, &key, &Some(map)); let expected = "ElevatedToken: NO ¦ ImpersonationLevel: A ¦ NewProcessId: 6528 ¦ ProcessId: 1100".to_string(); - assert_eq!(ret, expected); + assert_eq!(ret.join(" ¦ "), expected); } Err(_) => { panic!("Failed to parse json record."); diff --git a/src/detections/message.rs b/src/detections/message.rs index c7b0a53e3..d61389420 100644 --- a/src/detections/message.rs +++ b/src/detections/message.rs @@ -25,7 +25,7 @@ use termcolor::{BufferWriter, ColorChoice}; use super::configs::EventKeyAliasConfig; -#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] +#[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct DetectInfo { pub rulepath: CompactString, pub ruleid: CompactString, @@ -36,6 +36,7 @@ pub struct DetectInfo { pub detail: CompactString, pub ext_field: Vec<(CompactString, Profile)>, pub is_condition: bool, + pub details_convert_map: HashMap>, } pub struct AlertMessage {} @@ -122,37 +123,52 @@ pub fn insert( &Option, ), ) { + let mut record_details_info_map = HashMap::new(); if !is_agg { - let mut prev = 'a'; - let mut removed_sp_parsed_detail = parse_message( + //ここの段階でdetailsの内容でaliasを置き換えた内容と各種、key,valueの組み合わせのmapを取得する + let (removed_sp_parsed_detail, details_in_record) = parse_message( event_record, - output, + &output, eventkey_alias, is_json_timeline, field_data_map_key, field_data_map, ); - removed_sp_parsed_detail.retain(|ch| { - let retain_flag = prev == ' ' && ch == ' ' && ch.is_control(); - if !retain_flag { - prev = ch; - } - !retain_flag + + let removed_sp_char = |cs: CompactString| -> CompactString { + let mut newline_replaced_cs = cs + .replace('\n', "🛂n") + .replace('\r', "🛂r") + .replace('\t', "🛂t"); + let mut prev = 'a'; + newline_replaced_cs.retain(|ch| { + let retain_flag = (prev == ' ' && ch == ' ') || ch.is_control(); + if !retain_flag { + prev = ch; + } + !retain_flag + }); + newline_replaced_cs.into() + }; + let mut sp_removed_details_in_record = vec![]; + details_in_record.iter().for_each(|v| { + sp_removed_details_in_record.push(removed_sp_char(v.clone())); }); - let parsed_detail = removed_sp_parsed_detail - .replace('\n', "🛂n") - .replace('\r', "🛂r") - .replace('\t', "🛂t"); + record_details_info_map.insert("#Details".into(), sp_removed_details_in_record); + // 特殊文字の除外のためのretain処理 + // Details内にある改行文字は除外しないために絵文字を含めた特殊な文字に変換することで対応する + let parsed_detail = removed_sp_char(removed_sp_parsed_detail); detect_info.detail = if parsed_detail.is_empty() { CompactString::from("-") } else { - parsed_detail.into() + parsed_detail }; } let mut replaced_profiles: Vec<(CompactString, Profile)> = vec![]; for (key, profile) in detect_info.ext_field.iter() { match profile { Details(_) => { + // Detailsの要素がすでにreplaced_profilesに存在する場合は次の処理に進み let existed_flag = replaced_profiles .iter() .any(|(_, y)| matches!(y, Details(_))); @@ -160,72 +176,121 @@ pub fn insert( continue; } if detect_info.detail.is_empty() { + //Detailsの中身が何も入っていない場合はそのままの値を入れる replaced_profiles.push((key.to_owned(), profile.to_owned())); } else { replaced_profiles .push((key.to_owned(), Details(detect_info.detail.clone().into()))); + detect_info.details_convert_map.insert( + "#Details".into(), + detect_info.detail.split(" ¦ ").map(|x| x.into()).collect(), + ); + if is_agg { + if output != "-" { + record_details_info_map.insert("#Details".into(), vec![output.clone()]); + } else if detect_info.detail != "-" { + record_details_info_map + .insert("#Details".into(), vec![detect_info.detail.clone()]); + } else { + record_details_info_map.insert("#Details".into(), vec!["-".into()]); + } + } + // メモリの節約のためにDetailsの中身を空にする detect_info.detail = CompactString::default(); } } AllFieldInfo(_) => { - let existed_flag = replaced_profiles - .iter() - .any(|(_, y)| matches!(y, AllFieldInfo(_))); - if existed_flag { - continue; - } if is_agg { replaced_profiles.push((key.to_owned(), AllFieldInfo("-".into()))); + } else if record_details_info_map.get("#AllFieldInfo").is_some() { + // ExtraFieldInfoの要素の作成の際に、record_details_info_mapに要素を追加しているときにはAllFieldInfoの要素をすでに追加しているためスキップする + continue; } else { - let rec = + let recinfos = utils::create_recordinfos(event_record, field_data_map_key, field_data_map); - let rec = if rec.is_empty() { "-".to_string() } else { rec }; - replaced_profiles.push((key.to_owned(), AllFieldInfo(rec.into()))); + let rec = if recinfos.is_empty() { + "-".to_string() + } else if !is_json_timeline { + recinfos.join(" ¦ ") + } else { + String::default() + }; + if is_json_timeline { + record_details_info_map.insert("#AllFieldInfo".into(), recinfos); + replaced_profiles.push((key.to_owned(), AllFieldInfo("".into()))); + } else { + replaced_profiles.push((key.to_owned(), AllFieldInfo(rec.into()))); + } } } Literal(_) => replaced_profiles.push((key.to_owned(), profile.to_owned())), ExtraFieldInfo(_) => { - let mut profile_all_field_info_prof = None; - let mut profile_details_prof = None; - replaced_profiles.iter().for_each(|(_, y)| match y { - AllFieldInfo(_) => profile_all_field_info_prof = Some(y.to_value()), - Details(_) => profile_details_prof = Some(y.to_value()), - _ => {} - }); - let profile_details = - profile_details_prof.unwrap_or(detect_info.detail.clone().into()); + if is_agg { + if is_json_timeline { + record_details_info_map + .insert("#ExtraFieldInfo".into(), vec![CompactString::from("-")]); + replaced_profiles.push((key.to_owned(), ExtraFieldInfo("".into()))); + } else { + replaced_profiles.push((key.to_owned(), ExtraFieldInfo("-".into()))); + } + continue; + } + let empty = vec![]; + let record_details_info_ref = record_details_info_map.clone(); + let profile_all_field_info_prof = record_details_info_ref.get("#AllFieldInfo"); + let details_splits: HashSet<&str> = HashSet::from_iter( + record_details_info_ref + .get("#Details") + .unwrap_or(&empty) + .iter() + .map(|x| x.split_once(": ").unwrap_or_default().1), + ); let profile_all_field_info = if let Some(all_field_info_val) = profile_all_field_info_prof { - all_field_info_val - } else if is_agg { - if included_all_field_info { - // AllFieldInfoがまだ読み込まれていない場合は、AllFieldInfoを追加する - replaced_profiles.push((key.to_owned(), AllFieldInfo("-".into()))); - } - "-".to_string() + all_field_info_val.to_owned() } else { - let rec = + let recinfos = utils::create_recordinfos(event_record, field_data_map_key, field_data_map); - let rec = if rec.is_empty() { "-".to_string() } else { rec }; + let rec = if recinfos.is_empty() { + "-".to_string() + } else if !is_json_timeline { + recinfos.join(" ¦ ") + } else { + String::default() + }; + if included_all_field_info { - replaced_profiles.push((key.to_owned(), AllFieldInfo(rec.clone().into()))); + record_details_info_map.insert("#AllFieldInfo".into(), recinfos.clone()); + if is_json_timeline { + replaced_profiles.push((key.to_owned(), AllFieldInfo("".into()))); + } else { + replaced_profiles + .push((key.to_owned(), AllFieldInfo(rec.clone().into()))); + } } - rec + recinfos }; - let details_splits: HashSet<&str> = HashSet::from_iter( - profile_details - .split(" ¦ ") - .map(|x| x.split_once(": ").unwrap_or_default().1), - ); - let extra_field_val = profile_all_field_info - .split(" ¦ ") + let extra_field_vec = profile_all_field_info + .iter() .filter(|x| { let value = x.split_once(": ").unwrap_or_default().1; !details_splits.contains(value) }) - .join(" ¦ "); - replaced_profiles.push((key.to_owned(), ExtraFieldInfo(extra_field_val.into()))); + .map(|y| y.to_owned()) + .sorted_unstable() + .collect(); + if is_json_timeline { + record_details_info_map.insert("#ExtraFieldInfo".into(), extra_field_vec); + replaced_profiles.push((key.to_owned(), ExtraFieldInfo("".into()))); + } else if extra_field_vec.is_empty() { + replaced_profiles.push((key.to_owned(), ExtraFieldInfo("-".into()))); + } else { + replaced_profiles.push(( + key.to_owned(), + ExtraFieldInfo(extra_field_vec.join(" ¦ ").into()), + )); + } } SrcASN(_) | SrcCountry(_) | SrcCity(_) | TgtASN(_) | TgtCountry(_) | TgtCity(_) => { replaced_profiles.push(( @@ -235,36 +300,36 @@ pub fn insert( } _ => { if let Some(p) = profile_converter.get(key.as_str()) { - replaced_profiles.push(( - key.to_owned(), - profile.convert(&parse_message( - event_record, - CompactString::new(p.to_value()), - eventkey_alias, - is_json_timeline, - field_data_map_key, - field_data_map, - )), - )) + let (parsed_message, _) = &parse_message( + event_record, + &CompactString::new(p.to_value()), + eventkey_alias, + is_json_timeline, + field_data_map_key, + field_data_map, + ); + replaced_profiles.push((key.to_owned(), profile.convert(parsed_message))) } } } } detect_info.ext_field = replaced_profiles; + detect_info.details_convert_map = record_details_info_map; insert_message(detect_info, time) } -/// メッセージ内の%で囲まれた箇所をエイリアスとしてをレコード情報を参照して置き換える関数 +/// メッセージ内の%で囲まれた箇所をエイリアスとしてレコード情報を参照して置き換える関数 pub fn parse_message( event_record: &Value, - output: CompactString, + output: &CompactString, eventkey_alias: &EventKeyAliasConfig, json_timeline_flag: bool, field_data_map_key: &FieldDataMapKey, field_data_map: &Option, -) -> CompactString { - let mut return_message = output; - let mut hash_map: HashMap = HashMap::new(); +) -> (CompactString, Vec) { + let mut return_message = output.clone(); + let mut hash_map: HashMap> = HashMap::new(); + let details_key: Vec<&str> = output.split(" ¦ ").collect(); for caps in ALIASREGEX.captures_iter(&return_message) { let full_target_str = &caps[0]; let target_str = full_target_str @@ -313,23 +378,37 @@ pub fn parse_message( converted_str.unwrap_or(hash_value) }; if json_timeline_flag { - hash_map.insert(CompactString::from(full_target_str), field_data); + hash_map.insert(CompactString::from(full_target_str), [field_data].to_vec()); } else { hash_map.insert( CompactString::from(full_target_str), - field_data.split_ascii_whitespace().join(" ").into(), + [field_data.split_ascii_whitespace().join(" ").into()].to_vec(), ); } } } else { - hash_map.insert(CompactString::from(full_target_str), "n/a".into()); + hash_map.insert( + CompactString::from(full_target_str), + ["n/a".into()].to_vec(), + ); } } - - for (k, v) in hash_map { - return_message = CompactString::new(return_message.replace(k.as_str(), v.as_str())); + let mut details_key_and_value: Vec = vec![]; + for (k, v) in hash_map.iter() { + // JSON出力の場合は各種のaliasを置き換える処理はafterfactの出力用の関数で行うため、ここでは行わない + if !json_timeline_flag { + return_message = CompactString::new(return_message.replace(k.as_str(), v[0].as_str())); + } + for detail_contents in details_key.iter() { + if detail_contents.contains(k.as_str()) { + let key = detail_contents.split_once(": ").unwrap_or_default().0; + details_key_and_value.push(format!("{}: {}", key, v[0]).into()); + break; + } + } } - return_message + details_key_and_value.sort_unstable(); + (return_message, details_key_and_value) } /// メッセージを返す @@ -458,7 +537,7 @@ mod tests { assert_eq!( parse_message( &event_record, - CompactString::new("commandline:%CommandLine% computername:%ComputerName%"), + &CompactString::new("commandline:%CommandLine% computername:%ComputerName%"), &load_eventkey_alias( utils::check_setting_path( &CURRENT_EXE_PATH.to_path_buf(), @@ -469,10 +548,11 @@ mod tests { .to_str() .unwrap(), ), - true, + false, &FieldDataMapKey::default(), &None - ), + ) + .0, expected, ); } @@ -494,7 +574,7 @@ mod tests { assert_eq!( parse_message( &event_record, - CompactString::new("alias:%NoAlias%"), + &CompactString::new("alias:%NoAlias%"), &load_eventkey_alias( utils::check_setting_path( &CURRENT_EXE_PATH.to_path_buf(), @@ -505,10 +585,11 @@ mod tests { .to_str() .unwrap(), ), - true, + false, &FieldDataMapKey::default(), &None - ), + ) + .0, expected, ); } @@ -536,7 +617,7 @@ mod tests { assert_eq!( parse_message( &event_record, - CompactString::new("NoExistAlias:%NoAliasNoHit%"), + &CompactString::new("NoExistAlias:%NoAliasNoHit%"), &load_eventkey_alias( utils::check_setting_path( &CURRENT_EXE_PATH.to_path_buf(), @@ -547,10 +628,11 @@ mod tests { .to_str() .unwrap(), ), - true, + false, &FieldDataMapKey::default(), &None - ), + ) + .0, expected, ); } @@ -577,7 +659,7 @@ mod tests { assert_eq!( parse_message( &event_record, - CompactString::new("commandline:%CommandLine% computername:%ComputerName%"), + &CompactString::new("commandline:%CommandLine% computername:%ComputerName%"), &load_eventkey_alias( utils::check_setting_path( &CURRENT_EXE_PATH.to_path_buf(), @@ -588,10 +670,11 @@ mod tests { .to_str() .unwrap(), ), - true, + false, &FieldDataMapKey::default(), &None - ), + ) + .0, expected, ); } @@ -623,7 +706,7 @@ mod tests { assert_eq!( parse_message( &event_record, - CompactString::new("commandline:%CommandLine% data:%Data%"), + &CompactString::new("commandline:%CommandLine% data:%Data%"), &load_eventkey_alias( utils::check_setting_path( &CURRENT_EXE_PATH.to_path_buf(), @@ -634,10 +717,11 @@ mod tests { .to_str() .unwrap(), ), - true, + false, &FieldDataMapKey::default(), &None - ), + ) + .0, expected, ); } @@ -669,7 +753,7 @@ mod tests { assert_eq!( parse_message( &event_record, - CompactString::new("commandline:%CommandLine% data:%Data[2]%"), + &CompactString::new("commandline:%CommandLine% data:%Data[2]%"), &load_eventkey_alias( utils::check_setting_path( &CURRENT_EXE_PATH.to_path_buf(), @@ -680,10 +764,11 @@ mod tests { .to_str() .unwrap(), ), - true, + false, &FieldDataMapKey::default(), &None - ), + ) + .0, expected, ); } @@ -715,7 +800,7 @@ mod tests { assert_eq!( parse_message( &event_record, - CompactString::new("commandline:%CommandLine% data:%Data[0]%"), + &CompactString::new("commandline:%CommandLine% data:%Data[0]%"), &load_eventkey_alias( utils::check_setting_path( &CURRENT_EXE_PATH.to_path_buf(), @@ -726,10 +811,11 @@ mod tests { .to_str() .unwrap(), ), - true, + false, &FieldDataMapKey::default(), &None - ), + ) + .0, expected, ); } @@ -800,6 +886,7 @@ mod tests { detail: CompactString::default(), ext_field: vec![], is_condition: false, + details_convert_map: HashMap::default(), }; sample_detects.push((sample_event_time, detect_info, rng.gen_range(0..10))); } diff --git a/src/detections/utils.rs b/src/detections/utils.rs index 1e548bd66..45ea19cb3 100644 --- a/src/detections/utils.rs +++ b/src/detections/utils.rs @@ -375,7 +375,7 @@ pub fn create_recordinfos( record: &Value, field_data_map_key: &FieldDataMapKey, field_data_map: &Option, -) -> String { +) -> Vec { let mut output = HashSet::new(); _collect_recordinfo(&mut vec![], "", record, &mut output); @@ -398,13 +398,13 @@ pub fn create_recordinfos( convert_field_data(map, field_data_map_key, &key.to_lowercase(), value) { let val = converted_str.strip_suffix(',').unwrap_or(&converted_str); - return format!("{key}: {val}"); + return format!("{key}: {val}").into(); } } let val = value.strip_suffix(',').unwrap_or(value); - format!("{key}: {val}") + format!("{key}: {val}").into() }) - .join(" ¦ ") + .collect() } /** @@ -716,7 +716,7 @@ mod tests { let ret = utils::create_recordinfos(&record, &FieldDataMapKey::default(), &None); // Systemは除外される/属性(_attributesも除外される)/key順に並ぶ let expected = "AccessMask: %%1369 ¦ Process: lsass.exe ¦ User: u1".to_string(); - assert_eq!(ret, expected); + assert_eq!(ret.join(" ¦ "), expected); } Err(_) => { panic!("Failed to parse json record."); @@ -751,7 +751,7 @@ mod tests { // Systemは除外される/属性(_attributesも除外される)/key順に並ぶ let expected = "Binary: hogehoge ¦ Data: ¦ Data: Data1 ¦ Data: DataData2 ¦ Data: DataDataData3" .to_string(); - assert_eq!(ret, expected); + assert_eq!(ret.join(" ¦ "), expected); } Err(_) => { panic!("Failed to parse json record."); diff --git a/src/timeline/search.rs b/src/timeline/search.rs index ecf664a6c..ae3300e2e 100644 --- a/src/timeline/search.rs +++ b/src/timeline/search.rs @@ -310,7 +310,7 @@ fn extract_search_event_info( let datainfo = utils::create_recordinfos(&record.record, &FieldDataMapKey::default(), &None); let allfieldinfo = if !datainfo.is_empty() { - datainfo.into() + datainfo.join(" ¦ ").into() } else { CompactString::new("-") }; @@ -465,6 +465,8 @@ pub fn search_result_dsp_msg( jsonl_output, false, false, + false, + &[&HashMap::default(), &HashMap::default()], ); file_wtr