From 5a90424b40bb54e83feb008ef998bab23222eff5 Mon Sep 17 00:00:00 2001 From: DustInDark <2350416+hitenkoku@users.noreply.github.com> Date: Sun, 27 Aug 2023 09:43:27 +0900 Subject: [PATCH 01/17] feat(configs): added timeline-offset option #1159 --- src/detections/configs.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/detections/configs.rs b/src/detections/configs.rs index 2bee0b67b..ea3c8ea23 100644 --- a/src/detections/configs.rs +++ b/src/detections/configs.rs @@ -6,7 +6,7 @@ use crate::options::htmlreport; use crate::options::pivot::{PivotKeyword, PIVOT_KEYWORD}; use crate::options::profile::{load_profile, Profile}; use aho_corasick::{AhoCorasick, AhoCorasickBuilder, MatchKind}; -use chrono::{DateTime, Utc}; +use chrono::{DateTime, Utc, Local, Duration}; use clap::{ArgGroup, Args, ColorChoice, Command, CommandFactory, Parser, Subcommand}; use compact_str::CompactString; use hashbrown::{HashMap, HashSet}; @@ -1491,6 +1491,10 @@ pub struct InputOption { /// Carve evtx records from empty pages (default: disabled) #[arg(help_heading = Some("Input"), short = 'x', long = "recover-records", conflicts_with = "json_input", display_order = 440)] pub recover_records: bool, + + /// Scan just the most recent number of days(example: 1y, 30d, 24h, etc...) + #[arg(help_heading = Some("Filtering"), long = "timeline-offset", conflicts_with = "start_timeline", display_order = 440)] + pub timeline_offset: Option, } #[derive(Args, Clone, Debug)] @@ -2149,6 +2153,7 @@ fn extract_output_options(config: &Config) -> Option { filepath: None, live_analysis: false, recover_records: false, + timeline_offset: None, }, enable_deprecated_rules: false, enable_noisy_rules: false, @@ -2200,6 +2205,7 @@ fn extract_output_options(config: &Config) -> Option { filepath: None, live_analysis: false, recover_records: false, + timeline_offset: None, }, enable_deprecated_rules: true, enable_noisy_rules: true, From 2be418617c89f2c8c014714305d872263efaec4f Mon Sep 17 00:00:00 2001 From: DustInDark <2350416+hitenkoku@users.noreply.github.com> Date: Sun, 27 Aug 2023 09:45:00 +0900 Subject: [PATCH 02/17] feat(configs): added timefilter processing via timeline-offset option #1159 --- src/detections/configs.rs | 116 +++++++++++++++++++++++++++++++++++--- 1 file changed, 108 insertions(+), 8 deletions(-) diff --git a/src/detections/configs.rs b/src/detections/configs.rs index ea3c8ea23..d5f3e2f7a 100644 --- a/src/detections/configs.rs +++ b/src/detections/configs.rs @@ -1708,12 +1708,55 @@ impl TargetEventTime { None } }; + + let get_timeline_offset = |timeline_offset: &Option| { + if let Some(timeline_offline) = timeline_offset { + let mut start_prev_sec = 0; + let timekey = ["y", "d", "h", "m", "s"]; + for key in timekey { + let mut timekey_splitter = timeline_offline.split(key); + let mix_check = timekey_splitter.next(); + let mixed_checker: Vec<&str> = mix_check.unwrap_or_default().split(['y', 'd', 'h', 'm', 's']).collect(); + let target_num = if mixed_checker.is_empty() { + mix_check.unwrap() + } else { + mixed_checker[mixed_checker.len() - 1] + }; + if let Ok(num) = target_num.parse::() { + if num < 0 { + AlertMessage::alert("timeline-offset field: the timestamp format is not correct.") + .ok(); + return None; + } + match key { + "y" => start_prev_sec += num * 365 * 24 * 60 * 60, + "d" => start_prev_sec += num * 24 * 60 * 60, + "h" => start_prev_sec += num * 60 * 60, + "m" => start_prev_sec += num * 60, + "s" => start_prev_sec += num, + _ => {} + } + } + } + let target_start_time = Local::now() - Duration::seconds(start_prev_sec); + Some(target_start_time.format("%Y-%m-%d %H:%M:%S %z").to_string()) + } else { + None + } + }; match &stored_static.config.action.as_ref().unwrap() { Action::CsvTimeline(option) => { - let start_time = get_time( + let timeoffset = get_timeline_offset(&option.output_options.input_args.timeline_offset); + let start_time = if timeoffset.is_some() { + get_time( + timeoffset.as_ref(), + "timeline-offset field: the timestamp format is not correct.", + ) + } else { + get_time( option.output_options.start_timeline.as_ref(), "start-timeline field: the timestamp format is not correct.", - ); + )}; let end_time = get_time( option.output_options.end_timeline.as_ref(), "end-timeline field: the timestamp format is not correct.", @@ -1721,10 +1764,17 @@ impl TargetEventTime { Self::set(parse_success_flag, start_time, end_time) } Action::JsonTimeline(option) => { - let start_time = get_time( + let timeoffset = get_timeline_offset(&option.output_options.input_args.timeline_offset); + let start_time = if timeoffset.is_some() { + get_time( + timeoffset.as_ref(), + "timeline-offset field: the timestamp format is not correct.", + ) + } else { + get_time( option.output_options.start_timeline.as_ref(), "start-timeline field: the timestamp format is not correct.", - ); + )}; let end_time = get_time( option.output_options.end_timeline.as_ref(), "end-timeline field: the timestamp format is not correct.", @@ -1732,10 +1782,17 @@ impl TargetEventTime { Self::set(parse_success_flag, start_time, end_time) } Action::PivotKeywordsList(option) => { - let start_time = get_time( + let timeoffset = get_timeline_offset(&option.input_args.timeline_offset); + let start_time = if timeoffset.is_some() { + get_time( + timeoffset.as_ref(), + "timeline-offset field: the timestamp format is not correct.", + ) + } else { + get_time( option.start_timeline.as_ref(), "start-timeline field: the timestamp format is not correct.", - ); + )}; let end_time = get_time( option.end_timeline.as_ref(), "end-timeline field: the timestamp format is not correct.", @@ -1743,15 +1800,58 @@ impl TargetEventTime { Self::set(parse_success_flag, start_time, end_time) } Action::LogonSummary(option) => { - let start_time = get_time( + let timeoffset = get_timeline_offset(&option.input_args.timeline_offset); + let start_time = if timeoffset.is_some() { + get_time( + timeoffset.as_ref(), + "timeline-offset field: the timestamp format is not correct.", + ) + } else { + get_time( option.start_timeline.as_ref(), "start-timeline field: the timestamp format is not correct.", - ); + )}; let end_time = get_time( option.end_timeline.as_ref(), "end-timeline field: the timestamp format is not correct.", ); Self::set(parse_success_flag, start_time, end_time) + }, + Action::ComputerMetrics(option) => { + let timeoffset = get_timeline_offset(&option.input_args.timeline_offset); + let start_time = if timeoffset.is_some() { + get_time( + timeoffset.as_ref(), + "timeline-offset field: the timestamp format is not correct.", + ) + } else { + None + }; + Self::set(parse_success_flag, start_time, None) + }, + Action::EidMetrics(option) => { + let timeoffset = get_timeline_offset(&option.input_args.timeline_offset); + let start_time = if timeoffset.is_some() { + get_time( + timeoffset.as_ref(), + "timeline-offset field: the timestamp format is not correct.", + ) + } else { + None + }; + Self::set(parse_success_flag, start_time, None) + }, + Action::Search(option) => { + let timeoffset = get_timeline_offset(&option.input_args.timeline_offset); + let start_time = if timeoffset.is_some() { + get_time( + timeoffset.as_ref(), + "timeline-offset field: the timestamp format is not correct.", + ) + } else { + None + }; + Self::set(parse_success_flag, start_time, None) } _ => Self::set(parse_success_flag, None, None), } From 52cc8b894019b7c05b4e5e1626fb3f97f141b43e Mon Sep 17 00:00:00 2001 From: DustInDark <2350416+hitenkoku@users.noreply.github.com> Date: Sun, 27 Aug 2023 09:46:23 +0900 Subject: [PATCH 03/17] test: modified test due to add timeline-offset option #1159 --- src/afterfact.rs | 13 +++++++++++++ src/detections/detection.rs | 5 +++++ src/detections/rule/condition_parser.rs | 1 + src/detections/rule/count.rs | 1 + src/detections/rule/matchers.rs | 1 + src/detections/rule/mod.rs | 1 + src/detections/rule/selectionnodes.rs | 1 + src/detections/utils.rs | 1 + src/main.rs | 11 +++++++++++ src/options/htmlreport.rs | 4 ++++ src/options/profile.rs | 4 ++++ src/timeline/computer_metrics.rs | 1 + src/timeline/metrics.rs | 1 + src/timeline/timelines.rs | 3 +++ src/yaml.rs | 1 + 15 files changed, 49 insertions(+) diff --git a/src/afterfact.rs b/src/afterfact.rs index a624f1e31..6604e7ed1 100644 --- a/src/afterfact.rs +++ b/src/afterfact.rs @@ -1869,6 +1869,7 @@ mod tests { filepath: None, live_analysis: false, recover_records: false, + timeline_offset: None, }, profile: None, enable_deprecated_rules: false, @@ -1956,6 +1957,7 @@ mod tests { filepath: None, live_analysis: false, recover_records: false, + timeline_offset: None, }, profile: None, enable_deprecated_rules: false, @@ -2191,6 +2193,7 @@ mod tests { filepath: None, live_analysis: false, recover_records: false, + timeline_offset: None, }, profile: Some("verbose-2".to_string()), enable_deprecated_rules: false, @@ -2280,6 +2283,7 @@ mod tests { filepath: None, live_analysis: false, recover_records: false, + timeline_offset: None, }, profile: Some("verbose-2".to_string()), enable_deprecated_rules: false, @@ -2501,6 +2505,7 @@ mod tests { filepath: None, live_analysis: false, recover_records: false, + timeline_offset: None, }, profile: None, enable_deprecated_rules: false, @@ -2588,6 +2593,7 @@ mod tests { filepath: None, live_analysis: false, recover_records: false, + timeline_offset: None, }, profile: None, enable_deprecated_rules: false, @@ -2820,6 +2826,7 @@ mod tests { filepath: None, live_analysis: false, recover_records: false, + timeline_offset: None, }, profile: None, enable_deprecated_rules: false, @@ -2907,6 +2914,7 @@ mod tests { filepath: None, live_analysis: false, recover_records: false, + timeline_offset: None, }, profile: None, enable_deprecated_rules: false, @@ -3223,6 +3231,7 @@ mod tests { filepath: None, live_analysis: false, recover_records: false, + timeline_offset: None, }, profile: None, enable_deprecated_rules: false, @@ -3360,6 +3369,7 @@ mod tests { filepath: None, live_analysis: false, recover_records: false, + timeline_offset: None, }, profile: None, enable_deprecated_rules: false, @@ -3446,6 +3456,7 @@ mod tests { filepath: None, live_analysis: false, recover_records: false, + timeline_offset: None, }, profile: None, enable_deprecated_rules: false, @@ -3618,6 +3629,7 @@ mod tests { filepath: None, live_analysis: false, recover_records: false, + timeline_offset: None, }, profile: None, enable_deprecated_rules: false, @@ -3704,6 +3716,7 @@ mod tests { filepath: None, live_analysis: false, recover_records: false, + timeline_offset: None, }, profile: None, enable_deprecated_rules: false, diff --git a/src/detections/detection.rs b/src/detections/detection.rs index 66956cc68..b8a526a2a 100644 --- a/src/detections/detection.rs +++ b/src/detections/detection.rs @@ -1199,6 +1199,7 @@ mod tests { filepath: None, live_analysis: false, recover_records: false, + timeline_offset: None, }, profile: None, enable_deprecated_rules: false, @@ -1458,6 +1459,7 @@ mod tests { filepath: None, live_analysis: false, recover_records: false, + timeline_offset: None, }, profile: None, enable_deprecated_rules: false, @@ -1591,6 +1593,7 @@ mod tests { filepath: None, live_analysis: false, recover_records: false, + timeline_offset: None, }, profile: None, enable_deprecated_rules: false, @@ -1720,6 +1723,7 @@ mod tests { filepath: None, live_analysis: false, recover_records: false, + timeline_offset: None, }, profile: None, enable_deprecated_rules: false, @@ -1862,6 +1866,7 @@ mod tests { filepath: None, live_analysis: false, recover_records: false, + timeline_offset: None, }, profile: None, enable_deprecated_rules: false, diff --git a/src/detections/rule/condition_parser.rs b/src/detections/rule/condition_parser.rs index cf2cf0605..989c7164c 100644 --- a/src/detections/rule/condition_parser.rs +++ b/src/detections/rule/condition_parser.rs @@ -560,6 +560,7 @@ mod tests { filepath: None, live_analysis: false, recover_records: false, + timeline_offset: None, }, profile: None, enable_deprecated_rules: false, diff --git a/src/detections/rule/count.rs b/src/detections/rule/count.rs index 3dac18535..bc7b06de4 100644 --- a/src/detections/rule/count.rs +++ b/src/detections/rule/count.rs @@ -584,6 +584,7 @@ mod tests { filepath: None, live_analysis: false, recover_records: false, + timeline_offset: None, }, profile: None, enable_deprecated_rules: false, diff --git a/src/detections/rule/matchers.rs b/src/detections/rule/matchers.rs index 39864e412..a14d6f9f6 100644 --- a/src/detections/rule/matchers.rs +++ b/src/detections/rule/matchers.rs @@ -823,6 +823,7 @@ mod tests { filepath: None, live_analysis: false, recover_records: false, + timeline_offset: None, }, profile: None, enable_deprecated_rules: false, diff --git a/src/detections/rule/mod.rs b/src/detections/rule/mod.rs index 0f3469b0a..26f9ea3aa 100644 --- a/src/detections/rule/mod.rs +++ b/src/detections/rule/mod.rs @@ -404,6 +404,7 @@ mod tests { filepath: None, live_analysis: false, recover_records: false, + timeline_offset: None, }, profile: None, enable_deprecated_rules: false, diff --git a/src/detections/rule/selectionnodes.rs b/src/detections/rule/selectionnodes.rs index 3375530d9..829c38285 100644 --- a/src/detections/rule/selectionnodes.rs +++ b/src/detections/rule/selectionnodes.rs @@ -524,6 +524,7 @@ mod tests { filepath: None, live_analysis: false, recover_records: false, + timeline_offset: None, }, profile: None, enable_deprecated_rules: false, diff --git a/src/detections/utils.rs b/src/detections/utils.rs index 7619e88c7..37b2efa2d 100644 --- a/src/detections/utils.rs +++ b/src/detections/utils.rs @@ -975,6 +975,7 @@ mod tests { filepath: None, live_analysis: false, recover_records: false, + timeline_offset: None, }, profile: Some("super-verbose".to_string()), enable_deprecated_rules: false, diff --git a/src/main.rs b/src/main.rs index dda11a06d..db32aad1a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1682,6 +1682,7 @@ mod tests { filepath: None, live_analysis: false, recover_records: false, + timeline_offset: None, }, profile: None, enable_deprecated_rules: false, @@ -1842,6 +1843,7 @@ mod tests { filepath: Some(Path::new("test_files/evtx/test.json").to_path_buf()), live_analysis: false, recover_records: false, + timeline_offset: None, }, profile: None, enable_deprecated_rules: false, @@ -1923,6 +1925,7 @@ mod tests { filepath: Some(Path::new("test_files/evtx/test.json").to_path_buf()), live_analysis: false, recover_records: false, + timeline_offset: None, }, profile: None, enable_deprecated_rules: false, @@ -2002,6 +2005,7 @@ mod tests { filepath: Some(Path::new("test_files/evtx/test.json").to_path_buf()), live_analysis: false, recover_records: false, + timeline_offset: None, }, profile: None, enable_deprecated_rules: false, @@ -2083,6 +2087,7 @@ mod tests { filepath: Some(Path::new("test_files/evtx/test.json").to_path_buf()), live_analysis: false, recover_records: false, + timeline_offset: None, }, profile: None, enable_deprecated_rules: false, @@ -2162,6 +2167,7 @@ mod tests { filepath: Some(Path::new("test_files/evtx/test_metrics.json").to_path_buf()), live_analysis: false, recover_records: false, + timeline_offset: None, }, common_options: CommonOptions { no_color: false, @@ -2216,6 +2222,7 @@ mod tests { filepath: Some(Path::new("test_files/evtx/test_metrics.json").to_path_buf()), live_analysis: false, recover_records: false, + timeline_offset: None, }, common_options: CommonOptions { no_color: false, @@ -2267,6 +2274,7 @@ mod tests { directory: None, filepath: Some(Path::new("test_files/evtx/test_metrics.json").to_path_buf()), live_analysis: false, + timeline_offset: None, recover_records: false, }, common_options: CommonOptions { @@ -2324,6 +2332,7 @@ mod tests { filepath: Some(Path::new("test_files/evtx/test_metrics.json").to_path_buf()), live_analysis: false, recover_records: false, + timeline_offset: None, }, common_options: CommonOptions { no_color: false, @@ -2379,6 +2388,7 @@ mod tests { filepath: Some(Path::new("test_files/evtx/test_metrics.json").to_path_buf()), live_analysis: false, recover_records: false, + timeline_offset: None, }, common_options: CommonOptions { no_color: false, @@ -2421,6 +2431,7 @@ mod tests { filepath: Some(Path::new("test_files/evtx/test_metrics.json").to_path_buf()), live_analysis: false, recover_records: false, + timeline_offset: None, }, common_options: CommonOptions { no_color: false, diff --git a/src/options/htmlreport.rs b/src/options/htmlreport.rs index f4414e44a..6b7d32aa5 100644 --- a/src/options/htmlreport.rs +++ b/src/options/htmlreport.rs @@ -250,6 +250,7 @@ mod tests { filepath: None, live_analysis: false, recover_records: false, + timeline_offset: None, }, profile: None, enable_deprecated_rules: false, @@ -312,6 +313,7 @@ mod tests { filepath: None, live_analysis: false, recover_records: false, + timeline_offset: None, }, profile: None, enable_deprecated_rules: false, @@ -377,6 +379,7 @@ mod tests { filepath: None, live_analysis: false, recover_records: false, + timeline_offset: None, }, profile: None, enable_deprecated_rules: false, @@ -439,6 +442,7 @@ mod tests { filepath: None, live_analysis: false, recover_records: false, + timeline_offset: None, }, profile: None, enable_deprecated_rules: false, diff --git a/src/options/profile.rs b/src/options/profile.rs index f27bc6a55..7ab6336e3 100644 --- a/src/options/profile.rs +++ b/src/options/profile.rs @@ -427,6 +427,7 @@ mod tests { filepath: None, live_analysis: false, recover_records: false, + timeline_offset: None, }, profile: None, enable_deprecated_rules: false, @@ -550,6 +551,7 @@ mod tests { filepath: None, live_analysis: false, recover_records: false, + timeline_offset: None, }, profile: None, enable_deprecated_rules: false, @@ -623,6 +625,7 @@ mod tests { filepath: None, live_analysis: false, recover_records: false, + timeline_offset: None, }, profile: Some("minimal".to_string()), enable_deprecated_rules: false, @@ -726,6 +729,7 @@ mod tests { filepath: None, live_analysis: false, recover_records: false, + timeline_offset: None, }, profile: Some("not_exist".to_string()), enable_deprecated_rules: false, diff --git a/src/timeline/computer_metrics.rs b/src/timeline/computer_metrics.rs index 688fb304c..fe8ec4ee0 100644 --- a/src/timeline/computer_metrics.rs +++ b/src/timeline/computer_metrics.rs @@ -129,6 +129,7 @@ mod tests { filepath: Some(Path::new("./dummy.evtx").to_path_buf()), live_analysis: false, recover_records: false, + timeline_offset: None, }, common_options: CommonOptions { no_color: false, diff --git a/src/timeline/metrics.rs b/src/timeline/metrics.rs index b80539c63..a614aafcb 100644 --- a/src/timeline/metrics.rs +++ b/src/timeline/metrics.rs @@ -341,6 +341,7 @@ mod tests { filepath: None, live_analysis: false, recover_records: false, + timeline_offset: None, }, common_options: CommonOptions { no_color: false, diff --git a/src/timeline/timelines.rs b/src/timeline/timelines.rs index 45eb4fad0..4c06a26a6 100644 --- a/src/timeline/timelines.rs +++ b/src/timeline/timelines.rs @@ -529,6 +529,7 @@ mod tests { filepath: None, live_analysis: false, recover_records: false, + timeline_offset: None, }, common_options: CommonOptions { no_color: false, @@ -712,6 +713,7 @@ mod tests { filepath: None, live_analysis: false, recover_records: false, + timeline_offset: None, }, common_options: CommonOptions { no_color: false, @@ -800,6 +802,7 @@ mod tests { filepath: Some(Path::new("./dummy.evtx").to_path_buf()), live_analysis: false, recover_records: false, + timeline_offset: None, }, common_options: CommonOptions { no_color: false, diff --git a/src/yaml.rs b/src/yaml.rs index 3dc8283df..0fcb97e1e 100644 --- a/src/yaml.rs +++ b/src/yaml.rs @@ -492,6 +492,7 @@ mod tests { filepath: None, live_analysis: false, recover_records: false, + timeline_offset: None, }, profile: None, enable_deprecated_rules: false, From 159f1a99672e0539af6953d851849b72bca164cd Mon Sep 17 00:00:00 2001 From: DustInDark <2350416+hitenkoku@users.noreply.github.com> Date: Sun, 27 Aug 2023 09:47:04 +0900 Subject: [PATCH 04/17] feat(main): added timeline-offset feature in search command #1159 --- src/main.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main.rs b/src/main.rs index db32aad1a..8d2b47b2c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1231,11 +1231,11 @@ impl App { continue; } - // EventID側の条件との条件の混同を防ぐため時間でのフィルタリングの条件分岐を分離した - let timestamp = record_result.as_ref().unwrap().timestamp; - if !time_filter.is_target(&Some(timestamp)) { - continue; - } + } + // EventID側の条件との条件の混同を防ぐため時間でのフィルタリングの条件分岐を分離した + let timestamp = record_result.as_ref().unwrap().timestamp; + if !time_filter.is_target(&Some(timestamp)) { + continue; } let recover_record_flag = record_result.is_ok() From f9f0ccd8dfc474dea44918f07b42d5dafe33c136 Mon Sep 17 00:00:00 2001 From: DustInDark <2350416+hitenkoku@users.noreply.github.com> Date: Sun, 27 Aug 2023 09:48:22 +0900 Subject: [PATCH 05/17] style(main, configs): cargo fmt #1159 --- src/detections/configs.rs | 55 +++++++++++++++++++++++---------------- src/main.rs | 1 - 2 files changed, 33 insertions(+), 23 deletions(-) diff --git a/src/detections/configs.rs b/src/detections/configs.rs index d5f3e2f7a..e80e2a3f2 100644 --- a/src/detections/configs.rs +++ b/src/detections/configs.rs @@ -6,7 +6,7 @@ use crate::options::htmlreport; use crate::options::pivot::{PivotKeyword, PIVOT_KEYWORD}; use crate::options::profile::{load_profile, Profile}; use aho_corasick::{AhoCorasick, AhoCorasickBuilder, MatchKind}; -use chrono::{DateTime, Utc, Local, Duration}; +use chrono::{DateTime, Duration, Local, Utc}; use clap::{ArgGroup, Args, ColorChoice, Command, CommandFactory, Parser, Subcommand}; use compact_str::CompactString; use hashbrown::{HashMap, HashSet}; @@ -1716,7 +1716,10 @@ impl TargetEventTime { for key in timekey { let mut timekey_splitter = timeline_offline.split(key); let mix_check = timekey_splitter.next(); - let mixed_checker: Vec<&str> = mix_check.unwrap_or_default().split(['y', 'd', 'h', 'm', 's']).collect(); + let mixed_checker: Vec<&str> = mix_check + .unwrap_or_default() + .split(['y', 'd', 'h', 'm', 's']) + .collect(); let target_num = if mixed_checker.is_empty() { mix_check.unwrap() } else { @@ -1724,9 +1727,11 @@ impl TargetEventTime { }; if let Ok(num) = target_num.parse::() { if num < 0 { - AlertMessage::alert("timeline-offset field: the timestamp format is not correct.") - .ok(); - return None; + AlertMessage::alert( + "timeline-offset field: the timestamp format is not correct.", + ) + .ok(); + return None; } match key { "y" => start_prev_sec += num * 365 * 24 * 60 * 60, @@ -1746,7 +1751,8 @@ impl TargetEventTime { }; match &stored_static.config.action.as_ref().unwrap() { Action::CsvTimeline(option) => { - let timeoffset = get_timeline_offset(&option.output_options.input_args.timeline_offset); + let timeoffset = + get_timeline_offset(&option.output_options.input_args.timeline_offset); let start_time = if timeoffset.is_some() { get_time( timeoffset.as_ref(), @@ -1754,9 +1760,10 @@ impl TargetEventTime { ) } else { get_time( - option.output_options.start_timeline.as_ref(), - "start-timeline field: the timestamp format is not correct.", - )}; + option.output_options.start_timeline.as_ref(), + "start-timeline field: the timestamp format is not correct.", + ) + }; let end_time = get_time( option.output_options.end_timeline.as_ref(), "end-timeline field: the timestamp format is not correct.", @@ -1764,7 +1771,8 @@ impl TargetEventTime { Self::set(parse_success_flag, start_time, end_time) } Action::JsonTimeline(option) => { - let timeoffset = get_timeline_offset(&option.output_options.input_args.timeline_offset); + let timeoffset = + get_timeline_offset(&option.output_options.input_args.timeline_offset); let start_time = if timeoffset.is_some() { get_time( timeoffset.as_ref(), @@ -1772,9 +1780,10 @@ impl TargetEventTime { ) } else { get_time( - option.output_options.start_timeline.as_ref(), - "start-timeline field: the timestamp format is not correct.", - )}; + option.output_options.start_timeline.as_ref(), + "start-timeline field: the timestamp format is not correct.", + ) + }; let end_time = get_time( option.output_options.end_timeline.as_ref(), "end-timeline field: the timestamp format is not correct.", @@ -1790,9 +1799,10 @@ impl TargetEventTime { ) } else { get_time( - option.start_timeline.as_ref(), - "start-timeline field: the timestamp format is not correct.", - )}; + option.start_timeline.as_ref(), + "start-timeline field: the timestamp format is not correct.", + ) + }; let end_time = get_time( option.end_timeline.as_ref(), "end-timeline field: the timestamp format is not correct.", @@ -1808,15 +1818,16 @@ impl TargetEventTime { ) } else { get_time( - option.start_timeline.as_ref(), - "start-timeline field: the timestamp format is not correct.", - )}; + option.start_timeline.as_ref(), + "start-timeline field: the timestamp format is not correct.", + ) + }; let end_time = get_time( option.end_timeline.as_ref(), "end-timeline field: the timestamp format is not correct.", ); Self::set(parse_success_flag, start_time, end_time) - }, + } Action::ComputerMetrics(option) => { let timeoffset = get_timeline_offset(&option.input_args.timeline_offset); let start_time = if timeoffset.is_some() { @@ -1828,7 +1839,7 @@ impl TargetEventTime { None }; Self::set(parse_success_flag, start_time, None) - }, + } Action::EidMetrics(option) => { let timeoffset = get_timeline_offset(&option.input_args.timeline_offset); let start_time = if timeoffset.is_some() { @@ -1840,7 +1851,7 @@ impl TargetEventTime { None }; Self::set(parse_success_flag, start_time, None) - }, + } Action::Search(option) => { let timeoffset = get_timeline_offset(&option.input_args.timeline_offset); let start_time = if timeoffset.is_some() { diff --git a/src/main.rs b/src/main.rs index 8d2b47b2c..8f4f9894a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1230,7 +1230,6 @@ impl App { ) { continue; } - } // EventID側の条件との条件の混同を防ぐため時間でのフィルタリングの条件分岐を分離した let timestamp = record_result.as_ref().unwrap().timestamp; From 3b3f11e08232871129d71fe5c4234f11dc74ca89 Mon Sep 17 00:00:00 2001 From: DustInDark <2350416+hitenkoku@users.noreply.github.com> Date: Mon, 28 Aug 2023 09:16:47 +0900 Subject: [PATCH 06/17] test(configs): added timeline-offset test in csv-timeline #1159 --- src/detections/configs.rs | 80 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 77 insertions(+), 3 deletions(-) diff --git a/src/detections/configs.rs b/src/detections/configs.rs index e80e2a3f2..f34df5b04 100644 --- a/src/detections/configs.rs +++ b/src/detections/configs.rs @@ -2453,12 +2453,14 @@ fn create_control_chat_replace_map() -> HashMap { #[cfg(test)] mod tests { + use std::path::Path; + use crate::detections::configs; - use chrono::{DateTime, Utc}; + use chrono::{DateTime, Utc, Duration}; use compact_str::CompactString; use hashbrown::{HashMap, HashSet}; - - use super::create_control_chat_replace_map; + use super::{create_control_chat_replace_map, Action, CommonOptions, Config, + CsvOutputOption, DetectCommonOption, InputOption, OutputOption, StoredStatic, TargetEventTime}; // #[test] // #[ignore] @@ -2541,4 +2543,76 @@ mod tests { let actual = create_control_chat_replace_map(); assert_eq!(expect, actual); } + + #[test] + fn test_timeline_offset_csv() { + + let csv_timeline = StoredStatic::create_static_data(Some(Config { + action: Some(Action::CsvTimeline(CsvOutputOption { + output_options: OutputOption { + input_args: InputOption { + directory: None, + filepath: None, + live_analysis: false, + recover_records: false, + timeline_offset: Some("1d".to_string()), + }, + profile: None, + enable_deprecated_rules: false, + exclude_status: None, + min_level: "informational".to_string(), + exact_level: None, + enable_noisy_rules: false, + end_timeline: None, + start_timeline: None, + eid_filter: false, + european_time: false, + iso_8601: false, + rfc_2822: false, + rfc_3339: false, + us_military_time: false, + us_time: false, + utc: false, + visualize_timeline: false, + rules: Path::new("./rules").to_path_buf(), + html_report: None, + no_summary: false, + common_options: CommonOptions { + no_color: false, + quiet: false, + }, + detect_common_options: DetectCommonOption { + evtx_file_ext: None, + thread_number: None, + quiet_errors: false, + config: Path::new("./rules/config").to_path_buf(), + verbose: false, + json_input: true, + include_computer: None, + exclude_computer: None, + }, + enable_unsupported_rules: false, + clobber: false, + proven_rules: false, + include_tag: None, + exclude_tag: None, + include_category: None, + exclude_category: None, + include_eid: None, + exclude_eid: None, + no_field: false, + remove_duplicate_data: false, + remove_duplicate_detections: false, + }, + geo_ip: None, + output: None, + multiline: false, + })), + debug: false, + })); + let now = Utc::now(); + let actual = TargetEventTime::new(&csv_timeline); + let actual_diff = now - actual.start_time.unwrap(); + assert!(actual_diff.num_days() == 1); + } } From d15659da3840de0a833d0fc8c82dd6c5331fa691 Mon Sep 17 00:00:00 2001 From: DustInDark <2350416+hitenkoku@users.noreply.github.com> Date: Mon, 28 Aug 2023 10:32:29 +0900 Subject: [PATCH 07/17] test(configs): added timeline-offset option test #1159 --- src/detections/configs.rs | 270 +++++++++++++++++++++++++++++++++++++- 1 file changed, 264 insertions(+), 6 deletions(-) diff --git a/src/detections/configs.rs b/src/detections/configs.rs index f34df5b04..0e7e24c11 100644 --- a/src/detections/configs.rs +++ b/src/detections/configs.rs @@ -2455,12 +2455,17 @@ fn create_control_chat_replace_map() -> HashMap { mod tests { use std::path::Path; - use crate::detections::configs; - use chrono::{DateTime, Utc, Duration}; + use super::{ + create_control_chat_replace_map, Action, CommonOptions, Config, CsvOutputOption, + DetectCommonOption, InputOption, JSONOutputOption, OutputOption, StoredStatic, + TargetEventTime, + }; + use crate::detections::configs::{ + self, EidMetricsOption, LogonSummaryOption, PivotKeywordOption, SearchOption, + }; + use chrono::{DateTime, Utc}; use compact_str::CompactString; use hashbrown::{HashMap, HashSet}; - use super::{create_control_chat_replace_map, Action, CommonOptions, Config, - CsvOutputOption, DetectCommonOption, InputOption, OutputOption, StoredStatic, TargetEventTime}; // #[test] // #[ignore] @@ -2546,8 +2551,7 @@ mod tests { #[test] fn test_timeline_offset_csv() { - - let csv_timeline = StoredStatic::create_static_data(Some(Config { + let csv_timeline = StoredStatic::create_static_data(Some(Config { action: Some(Action::CsvTimeline(CsvOutputOption { output_options: OutputOption { input_args: InputOption { @@ -2615,4 +2619,258 @@ mod tests { let actual_diff = now - actual.start_time.unwrap(); assert!(actual_diff.num_days() == 1); } + + #[test] + fn test_timeline_offset_json() { + let json_timeline = StoredStatic::create_static_data(Some(Config { + action: Some(Action::JsonTimeline(JSONOutputOption { + output_options: OutputOption { + input_args: InputOption { + directory: None, + filepath: None, + live_analysis: false, + recover_records: false, + timeline_offset: Some("1y".to_string()), + }, + profile: None, + enable_deprecated_rules: false, + exclude_status: None, + min_level: "informational".to_string(), + exact_level: None, + enable_noisy_rules: false, + end_timeline: None, + start_timeline: None, + eid_filter: false, + european_time: false, + iso_8601: false, + rfc_2822: false, + rfc_3339: false, + us_military_time: false, + us_time: false, + utc: false, + visualize_timeline: false, + rules: Path::new("./rules").to_path_buf(), + html_report: None, + no_summary: false, + common_options: CommonOptions { + no_color: false, + quiet: false, + }, + detect_common_options: DetectCommonOption { + evtx_file_ext: None, + thread_number: None, + quiet_errors: false, + config: Path::new("./rules/config").to_path_buf(), + verbose: false, + json_input: true, + include_computer: None, + exclude_computer: None, + }, + enable_unsupported_rules: false, + clobber: false, + proven_rules: false, + include_tag: None, + exclude_tag: None, + include_category: None, + exclude_category: None, + include_eid: None, + exclude_eid: None, + no_field: false, + remove_duplicate_data: false, + remove_duplicate_detections: false, + }, + geo_ip: None, + output: None, + jsonl_timeline: false, + })), + debug: false, + })); + let now = Utc::now(); + let actual = TargetEventTime::new(&json_timeline); + let actual_diff = now - actual.start_time.unwrap(); + assert!(actual_diff.num_days() == 365 || actual_diff.num_days() == 366); + } + + #[test] + fn test_timeline_offset_search() { + let json_timeline = StoredStatic::create_static_data(Some(Config { + action: Some(Action::Search(SearchOption { + output: None, + common_options: CommonOptions { + no_color: false, + quiet: false, + }, + input_args: InputOption { + directory: None, + filepath: None, + live_analysis: false, + recover_records: false, + timeline_offset: Some("1h".to_string()), + }, + keywords: Some(vec!["mimikatz".to_string()]), + regex: None, + ignore_case: true, + and_logic: false, + filter: vec![], + evtx_file_ext: None, + thread_number: None, + quiet_errors: false, + config: Path::new("./rules/config").to_path_buf(), + verbose: false, + multiline: false, + clobber: true, + json_output: false, + jsonl_output: false, + european_time: false, + iso_8601: false, + rfc_2822: false, + rfc_3339: false, + us_military_time: false, + us_time: false, + utc: false, + })), + debug: false, + })); + let now = Utc::now(); + let actual = TargetEventTime::new(&json_timeline); + let actual_diff = now - actual.start_time.unwrap(); + assert!(actual_diff.num_hours() == 1); + } + + #[test] + fn test_timeline_offset_eid_metrics() { + let eid_metrics = StoredStatic::create_static_data(Some(Config { + action: Some(Action::EidMetrics(EidMetricsOption { + output: None, + common_options: CommonOptions { + no_color: false, + quiet: false, + }, + input_args: InputOption { + directory: None, + filepath: None, + live_analysis: false, + recover_records: false, + timeline_offset: Some("1h1m".to_string()), + }, + clobber: true, + european_time: false, + iso_8601: false, + rfc_2822: false, + rfc_3339: false, + us_military_time: false, + us_time: false, + utc: false, + detect_common_options: DetectCommonOption { + evtx_file_ext: None, + thread_number: None, + quiet_errors: false, + config: Path::new("./rules/config").to_path_buf(), + verbose: false, + json_input: true, + include_computer: None, + exclude_computer: None, + }, + })), + debug: false, + })); + let now = Utc::now(); + let actual = TargetEventTime::new(&eid_metrics); + let actual_diff = now - actual.start_time.unwrap(); + assert!(actual_diff.num_hours() == 1 && actual_diff.num_minutes() == 61); + } + + #[test] + fn test_timeline_offset_logon_summary() { + let logon_summary = StoredStatic::create_static_data(Some(Config { + action: Some(Action::LogonSummary(LogonSummaryOption { + output: None, + common_options: CommonOptions { + no_color: false, + quiet: false, + }, + input_args: InputOption { + directory: None, + filepath: None, + live_analysis: false, + recover_records: false, + timeline_offset: Some("1y1d1h".to_string()), + }, + clobber: true, + european_time: false, + iso_8601: false, + rfc_2822: false, + rfc_3339: false, + us_military_time: false, + us_time: false, + utc: false, + detect_common_options: DetectCommonOption { + evtx_file_ext: None, + thread_number: None, + quiet_errors: false, + config: Path::new("./rules/config").to_path_buf(), + verbose: false, + json_input: true, + include_computer: None, + exclude_computer: None, + }, + end_timeline: None, + start_timeline: None, + })), + debug: false, + })); + let now = Utc::now(); + let actual = TargetEventTime::new(&logon_summary); + let actual_diff = now - actual.start_time.unwrap(); + let days = actual_diff.num_days(); + assert!(days == 366 && actual_diff.num_hours() == days * 24 + 1); + } + + #[test] + fn test_timeline_offset_pivot() { + let pivot_keywords_list = StoredStatic::create_static_data(Some(Config { + action: Some(Action::PivotKeywordsList(PivotKeywordOption { + output: None, + common_options: CommonOptions { + no_color: false, + quiet: false, + }, + input_args: InputOption { + directory: None, + filepath: None, + live_analysis: false, + recover_records: false, + timeline_offset: Some("10y1s".to_string()), + }, + clobber: true, + detect_common_options: DetectCommonOption { + evtx_file_ext: None, + thread_number: None, + quiet_errors: false, + config: Path::new("./rules/config").to_path_buf(), + verbose: false, + json_input: true, + include_computer: None, + exclude_computer: None, + }, + end_timeline: None, + start_timeline: None, + enable_deprecated_rules: false, + enable_unsupported_rules: false, + exclude_status: None, + min_level: "informational".to_string(), + exact_level: None, + enable_noisy_rules: false, + eid_filter: false, + include_eid: None, + exclude_eid: None, + })), + debug: false, + })); + let now = Utc::now(); + let actual = TargetEventTime::new(&pivot_keywords_list); + let actual_diff = now - actual.start_time.unwrap(); + let days = actual_diff.num_days(); + assert!(days == 3650 && actual_diff.num_seconds() == days * 24 * 60 * 60 + 1); + } } From c6d8bf934ec73826231fd45a57be4427a875021e Mon Sep 17 00:00:00 2001 From: DustInDark <2350416+hitenkoku@users.noreply.github.com> Date: Mon, 28 Aug 2023 11:44:07 +0900 Subject: [PATCH 08/17] UI(configs): reordered timeline-offset option #1159 --- src/detections/configs.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/detections/configs.rs b/src/detections/configs.rs index 0e7e24c11..ab10b5b91 100644 --- a/src/detections/configs.rs +++ b/src/detections/configs.rs @@ -1493,7 +1493,7 @@ pub struct InputOption { pub recover_records: bool, /// Scan just the most recent number of days(example: 1y, 30d, 24h, etc...) - #[arg(help_heading = Some("Filtering"), long = "timeline-offset", conflicts_with = "start_timeline", display_order = 440)] + #[arg(help_heading = Some("Filtering"), long = "timeline-offset", conflicts_with = "start_timeline", display_order = 460)] pub timeline_offset: Option, } From 6327fa20a692d0bacc20997c04e4bc283d8b7a6a Mon Sep 17 00:00:00 2001 From: DustInDark <2350416+hitenkoku@users.noreply.github.com> Date: Mon, 28 Aug 2023 11:45:49 +0900 Subject: [PATCH 09/17] docs(CHANGELOG): added #1159 --- CHANGELOG-Japanese.md | 1 + CHANGELOG.md | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG-Japanese.md b/CHANGELOG-Japanese.md index 24f4228ab..947d9749b 100644 --- a/CHANGELOG-Japanese.md +++ b/CHANGELOG-Japanese.md @@ -12,6 +12,7 @@ - `search`に`-a, --and-logic`オプションを追加し、複数のキーワードをAND条件で検索できるようにした。 (#1162) (@hitenkoku) - 出力プロファイルに、回復されたかどうかを示す `%RecoveredRecord%` フィールドを追加した。 (#1170) (@hitenkoku) +- `csv-timeline`、`json-timeline`、`logon-summary`、`eid-metrics`、`pivot-keywords-list`、`search` コマンドに、直近の日数だけをスキャンするための `--timeline-offset` オプションを追加した。 (#1159) (@hitenkoku) ## 2.7.0 [2023/08/03] "SANS DFIR Summit Release" diff --git a/CHANGELOG.md b/CHANGELOG.md index ea258e8d0..5fd0e8429 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - Added a `-a, --and-logic` option in the `search` command to search keywords with AND logic. (#1162) (@hitenkoku) - When using `-x, --recover-records`, an additional `%RecoveredRecord%` field will be added to the output profile and will output `Y` to indicate if a record was recovered. (#1160) (@hitenkoku) +- Added a `--timeline-offset` option in `csv-timeline`, `json-timeline`, `logon-summary`, `eid-metrics`, `pivot-keywords-list`, `search` command to scan just the most recent number of days. (#1159) (@hitenkoku) ## 2.7.0 [2023/08/03] "SANS DFIR Summit Release" From 1a3811e1241884789a6cb123699cf9b9ae817e15 Mon Sep 17 00:00:00 2001 From: DustInDark <2350416+hitenkoku@users.noreply.github.com> Date: Mon, 28 Aug 2023 11:54:07 +0900 Subject: [PATCH 10/17] style: fixed cargo clippy --- src/afterfact.rs | 24 ++++++++++++------------ src/detections/detection.rs | 16 ++++++++-------- src/detections/field_data_map.rs | 28 ++++++++++++++-------------- src/detections/message.rs | 28 ++++++++++++++-------------- src/detections/utils.rs | 16 ++++++++-------- 5 files changed, 56 insertions(+), 56 deletions(-) diff --git a/src/afterfact.rs b/src/afterfact.rs index 6604e7ed1..1a53a4dc4 100644 --- a/src/afterfact.rs +++ b/src/afterfact.rs @@ -1936,7 +1936,7 @@ mod tests { { let messages = &message::MESSAGES; messages.clear(); - let val = r##" + let val = r#" { "Event": { "EventData": { @@ -1949,7 +1949,7 @@ mod tests { } } } - "##; + "#; let event: Value = serde_json::from_str(val).unwrap(); let output_option = OutputOption { input_args: InputOption { @@ -2260,7 +2260,7 @@ mod tests { { let messages = &message::MESSAGES; messages.clear(); - let val = r##" + let val = r#" { "Event": { "EventData": { @@ -2275,7 +2275,7 @@ mod tests { } } } - "##; + "#; let event: Value = serde_json::from_str(val).unwrap(); let output_option = OutputOption { input_args: InputOption { @@ -2572,7 +2572,7 @@ mod tests { { let messages = &message::MESSAGES; messages.clear(); - let val = r##" + let val = r#" { "Event": { "EventData": { @@ -2585,7 +2585,7 @@ mod tests { } } } - "##; + "#; let event: Value = serde_json::from_str(val).unwrap(); let output_option = OutputOption { input_args: InputOption { @@ -2893,7 +2893,7 @@ mod tests { { let messages = &message::MESSAGES; messages.clear(); - let val = r##" + let val = r#" { "Event": { "EventData": { @@ -2906,7 +2906,7 @@ mod tests { } } } - "##; + "#; let event: Value = serde_json::from_str(val).unwrap(); let output_option = OutputOption { input_args: InputOption { @@ -3435,7 +3435,7 @@ mod tests { ) .unwrap_or_default(); { - let val = r##" + let val = r#" { "Event": { "EventData": { @@ -3448,7 +3448,7 @@ mod tests { } } } - "##; + "#; let event: Value = serde_json::from_str(val).unwrap(); let output_option = OutputOption { input_args: InputOption { @@ -3695,7 +3695,7 @@ mod tests { ) .unwrap_or_default(); { - let val = r##" + let val = r#" { "Event": { "EventData": { @@ -3708,7 +3708,7 @@ mod tests { } } } - "##; + "#; let event: Value = serde_json::from_str(val).unwrap(); let output_option = OutputOption { input_args: InputOption { diff --git a/src/detections/detection.rs b/src/detections/detection.rs index b8a526a2a..5e98aba93 100644 --- a/src/detections/detection.rs +++ b/src/detections/detection.rs @@ -1532,7 +1532,7 @@ mod tests { let messages = &message::MESSAGES; messages.clear(); - let val = r##" + let val = r#" { "Event": { "EventData": { @@ -1550,7 +1550,7 @@ mod tests { } } } - "##; + "#; let event: Value = serde_json::from_str(val).unwrap(); let dummy_rule = RuleNode::new(test_rulepath.to_string(), Yaml::from_str("")); let keys = detections::rule::get_detection_keys(&dummy_rule); @@ -1666,7 +1666,7 @@ mod tests { let messages = &message::MESSAGES; messages.clear(); - let val = r##" + let val = r#" { "Event": { "EventData": { @@ -1684,7 +1684,7 @@ mod tests { } } } - "##; + "#; let event: Value = serde_json::from_str(val).unwrap(); let dummy_rule = RuleNode::new(test_rulepath.to_string(), Yaml::from_str("")); let keys = detections::rule::get_detection_keys(&dummy_rule); @@ -1800,7 +1800,7 @@ mod tests { let messages = &message::MESSAGES; messages.clear(); - let val = r##" + let val = r#" { "Event": { "EventData": { @@ -1818,7 +1818,7 @@ mod tests { } } } - "##; + "#; let rule_str = r#" enabled: true author: "Test, Test2/Test3; Test4 " @@ -1944,7 +1944,7 @@ mod tests { let messages = &message::MESSAGES; messages.clear(); - let val = r##" + let val = r#" { "Event": { "EventData": { @@ -1962,7 +1962,7 @@ mod tests { } } } - "##; + "#; let rule_str = r#" enabled: true author: "Test, Test2/Test3; Test4 " diff --git a/src/detections/field_data_map.rs b/src/detections/field_data_map.rs index 75b0711d8..1b294b001 100644 --- a/src/detections/field_data_map.rs +++ b/src/detections/field_data_map.rs @@ -194,14 +194,14 @@ mod tests { #[test] fn test_convert_field_data() { - let s = r##" + let s = r#" Channel: Security EventID: 4624 RewriteFieldData: LogonType: - '0': '0 - SYSTEM' - '2': '2 - INTERACTIVE' - "##; + "#; let (key, entry) = build_field_data_map(build_yaml(s)); let mut map = HashMap::new(); map.insert(key.clone(), entry); @@ -211,31 +211,31 @@ mod tests { #[test] fn test_build_field_data_map_invalid0() { - let s = r##" + let s = r#" INVALID - "##; + "#; let r = build_field_data_map(build_yaml(s)); assert_eq!(r.0, FieldDataMapKey::default()); } #[test] fn test_build_field_data_map_invalid1() { - let s = r##" + let s = r#" Foo: Bar: - 'A': '1' - "##; + "#; let r = build_field_data_map(build_yaml(s)); assert_eq!(r.0, FieldDataMapKey::default()); } #[test] fn test_build_field_data_map_invalid2() { - let s = r##" + let s = r#" Channel: Security EventID: 4624 INVALID: 1 - "##; + "#; let r = build_field_data_map(build_yaml(s)); assert_eq!(r.0, FieldDataMapKey::default()); assert!(r.1.is_empty()); @@ -243,11 +243,11 @@ mod tests { #[test] fn test_build_field_data_map_invalid3() { - let s = r##" + let s = r#" Channel: Security EventID: 4624 RewriteFieldData: 'INVALID' - "##; + "#; let r = build_field_data_map(build_yaml(s)); assert_eq!(r.0, FieldDataMapKey::default()); assert!(r.1.is_empty()); @@ -255,7 +255,7 @@ mod tests { #[test] fn test_build_field_data_map_valid() { - let s = r##" + let s = r#" Channel: Security EventID: 4624 RewriteFieldData: @@ -265,7 +265,7 @@ mod tests { ImpersonationLevel: - '%%1832': 'A' - '%%1833': 'B' - "##; + "#; let r = build_field_data_map(build_yaml(s)); let mut wtr = vec![]; match r.1.get("elevatedtoken").unwrap() { @@ -309,7 +309,7 @@ mod tests { }, "Event_attributes": {"xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"} }"#; - let s = r##" + let s = r#" Channel: Security EventID: 4624 RewriteFieldData: @@ -322,7 +322,7 @@ mod tests { HexToDecimal: - 'NewProcessId' - 'ProcessId' - "##; + "#; let (key, entry) = build_field_data_map(build_yaml(s)); let mut map: FieldDataMap = HashMap::new(); map.insert(key.clone(), entry); diff --git a/src/detections/message.rs b/src/detections/message.rs index 90adf6fa5..0cb342ae9 100644 --- a/src/detections/message.rs +++ b/src/detections/message.rs @@ -439,7 +439,7 @@ mod tests { /// outputで指定されているキー(eventkey_alias.txt内で設定済み)から対象のレコード内の情報でメッセージをパースしているか確認する関数 fn test_parse_message() { MESSAGES.clear(); - let json_str = r##" + let json_str = r#" { "Event": { "EventData": { @@ -453,7 +453,7 @@ mod tests { } } } - "##; + "#; let event_record: Value = serde_json::from_str(json_str).unwrap(); let expected = "commandline:parsetest1 computername:testcomputer1"; assert_eq!( @@ -481,7 +481,7 @@ mod tests { #[test] fn test_parse_message_auto_search() { MESSAGES.clear(); - let json_str = r##" + let json_str = r#" { "Event": { "EventData": { @@ -489,7 +489,7 @@ mod tests { } } } - "##; + "#; let event_record: Value = serde_json::from_str(json_str).unwrap(); let expected = "alias:no_alias"; assert_eq!( @@ -518,7 +518,7 @@ mod tests { /// outputで指定されているキーが、eventkey_alias.txt内で設定されていない場合の出力テスト fn test_parse_message_not_exist_key_in_output() { MESSAGES.clear(); - let json_str = r##" + let json_str = r#" { "Event": { "EventData": { @@ -531,7 +531,7 @@ mod tests { } } } - "##; + "#; let event_record: Value = serde_json::from_str(json_str).unwrap(); let expected = "NoExistAlias:n/a"; assert_eq!( @@ -559,7 +559,7 @@ mod tests { /// output test when no exist info in target record output and described key-value data in eventkey_alias.txt fn test_parse_message_not_exist_value_in_record() { MESSAGES.clear(); - let json_str = r##" + let json_str = r#" { "Event": { "EventData": { @@ -572,7 +572,7 @@ mod tests { } } } - "##; + "#; let event_record: Value = serde_json::from_str(json_str).unwrap(); let expected = "commandline:parsetest3 computername:n/a"; assert_eq!( @@ -600,7 +600,7 @@ mod tests { /// output test when no exist info in target record output and described key-value data in eventkey_alias.txt fn test_parse_message_multiple_no_suffix_in_record() { MESSAGES.clear(); - let json_str = r##" + let json_str = r#" { "Event": { "EventData": { @@ -618,7 +618,7 @@ mod tests { } } } - "##; + "#; let event_record: Value = serde_json::from_str(json_str).unwrap(); let expected = "commandline:parsetest3 data:[\"data1\",\"data2\",\"data3\"]"; assert_eq!( @@ -646,7 +646,7 @@ mod tests { /// output test when no exist info in target record output and described key-value data in eventkey_alias.txt fn test_parse_message_multiple_with_suffix_in_record() { MESSAGES.clear(); - let json_str = r##" + let json_str = r#" { "Event": { "EventData": { @@ -664,7 +664,7 @@ mod tests { } } } - "##; + "#; let event_record: Value = serde_json::from_str(json_str).unwrap(); let expected = "commandline:parsetest3 data:data2"; assert_eq!( @@ -692,7 +692,7 @@ mod tests { /// output test when no exist info in target record output and described key-value data in eventkey_alias.txt fn test_parse_message_multiple_no_exist_in_record() { MESSAGES.clear(); - let json_str = r##" + let json_str = r#" { "Event": { "EventData": { @@ -710,7 +710,7 @@ mod tests { } } } - "##; + "#; let event_record: Value = serde_json::from_str(json_str).unwrap(); let expected = "commandline:parsetest3 data:n/a"; assert_eq!( diff --git a/src/detections/utils.rs b/src/detections/utils.rs index 37b2efa2d..1e548bd66 100644 --- a/src/detections/utils.rs +++ b/src/detections/utils.rs @@ -792,7 +792,7 @@ mod tests { #[test] /// Serde::Valueの数値型の値を文字列として返却することを確かめるテスト fn test_get_serde_number_to_string() { - let json_str = r##" + let json_str = r#" { "Event": { "System": { @@ -800,7 +800,7 @@ mod tests { } } } - "##; + "#; let event_record: Value = serde_json::from_str(json_str).unwrap(); assert_eq!( @@ -812,7 +812,7 @@ mod tests { #[test] /// Serde::Valueの文字列型の値を文字列として返却することを確かめるテスト fn test_get_serde_number_serde_string_to_string() { - let json_str = r##" + let json_str = r#" { "Event": { "EventData": { @@ -820,7 +820,7 @@ mod tests { } } } - "##; + "#; let event_record: Value = serde_json::from_str(json_str).unwrap(); assert_eq!( @@ -836,7 +836,7 @@ mod tests { #[test] /// Serde::Valueのオブジェクト型の内容を誤って渡した際にNoneを返却することを確かめるテスト fn test_get_serde_number_serde_object_ret_none() { - let json_str = r##" + let json_str = r#" { "Event": { "EventData": { @@ -844,7 +844,7 @@ mod tests { } } } - "##; + "#; let event_record: Value = serde_json::from_str(json_str).unwrap(); assert!( @@ -1048,7 +1048,7 @@ mod tests { #[test] /// Computerの値をもとにフィルタリングされることを確認するテスト fn test_is_filtered_by_computer_name() { - let json_str = r##" + let json_str = r#" { "Event": { "System": { @@ -1056,7 +1056,7 @@ mod tests { } } } - "##; + "#; let event_record: Value = serde_json::from_str(json_str).unwrap(); // include_computer, exclude_computerが指定されていない場合はフィルタリングされない From e5b0fbed8cacfe8b150e78e4f73dd26e77ce6c96 Mon Sep 17 00:00:00 2001 From: DustInDark <2350416+hitenkoku@users.noreply.github.com> Date: Tue, 29 Aug 2023 09:57:17 +0900 Subject: [PATCH 11/17] style: fixed cargo clippy error --- src/afterfact.rs | 4 ++-- src/detections/rule/selectionnodes.rs | 6 +++--- src/timeline/timelines.rs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/afterfact.rs b/src/afterfact.rs index 1a53a4dc4..6c392a4c5 100644 --- a/src/afterfact.rs +++ b/src/afterfact.rs @@ -539,7 +539,7 @@ fn emit_csv( disp_wtr_buf.clear(); let level_abbr: Nested> = Nested::from_iter( - vec![ + [ [CompactString::from("critical"), CompactString::from("crit")].to_vec(), [CompactString::from("high"), CompactString::from("high")].to_vec(), [CompactString::from("medium"), CompactString::from("med ")].to_vec(), @@ -1458,7 +1458,7 @@ pub fn output_json_str( } else { target_ext_field = ext_field.to_owned(); } - let key_add_to_details = vec![ + let key_add_to_details = [ "SrcASN", "SrcCountry", "SrcCity", diff --git a/src/detections/rule/selectionnodes.rs b/src/detections/rule/selectionnodes.rs index 829c38285..cb1c549d2 100644 --- a/src/detections/rule/selectionnodes.rs +++ b/src/detections/rule/selectionnodes.rs @@ -62,7 +62,7 @@ impl SelectionNode for AndSelectionNode { .fold( vec![], |mut acc: Vec, cur: Vec| -> Vec { - acc.extend(cur.into_iter()); + acc.extend(cur); acc }, ); @@ -132,7 +132,7 @@ impl SelectionNode for AllSelectionNode { .fold( vec![], |mut acc: Vec, cur: Vec| -> Vec { - acc.extend(cur.into_iter()); + acc.extend(cur); acc }, ); @@ -202,7 +202,7 @@ impl SelectionNode for OrSelectionNode { .fold( vec![], |mut acc: Vec, cur: Vec| -> Vec { - acc.extend(cur.into_iter()); + acc.extend(cur); acc }, ); diff --git a/src/timeline/timelines.rs b/src/timeline/timelines.rs index 4c06a26a6..7df9cbbd6 100644 --- a/src/timeline/timelines.rs +++ b/src/timeline/timelines.rs @@ -201,7 +201,7 @@ impl Timeline { None => 100, }; - let constraints = vec![ + let constraints = [ LowerBoundary(Fixed(7)), // Minimum number of characters for "Total" UpperBoundary(Fixed(9)), // Maximum number of characters for "percent" UpperBoundary(Fixed(20)), // Maximum number of characters for "Channel" From 9478b12136cfac6b5677ba1cd3704cecbf351801 Mon Sep 17 00:00:00 2001 From: DustInDark <2350416+hitenkoku@users.noreply.github.com> Date: Tue, 29 Aug 2023 10:12:28 +0900 Subject: [PATCH 12/17] style: fixed cargo clippy error --- src/detections/rule/matchers.rs | 32 ++++++++++++++++---------------- src/detections/rule/mod.rs | 8 ++++---- src/timeline/computer_metrics.rs | 2 +- src/timeline/timelines.rs | 2 +- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/detections/rule/matchers.rs b/src/detections/rule/matchers.rs index a14d6f9f6..b0a728a3c 100644 --- a/src/detections/rule/matchers.rs +++ b/src/detections/rule/matchers.rs @@ -2552,14 +2552,14 @@ mod tests { #[test] fn test_detect_backslash_exact_match() { - let rule_str = r#" + let rule_str = r" enabled: true detection: selection: Channel: 'Microsoft-Windows-Sysmon/Operational' EventID: 1 CurrentDirectory: 'C:\Windows\' - "#; + "; let record_json_str = r#" { @@ -2579,13 +2579,13 @@ mod tests { #[test] fn test_detect_startswith_backslash1() { - let rule_str = r#" + let rule_str = r" enabled: true detection: selection: EventID: 1040 Data|startswith: C:\Windows\ - "#; + "; let record_json_str = r#" { @@ -2605,13 +2605,13 @@ mod tests { #[test] fn test_detect_startswith_backslash2() { - let rule_str = r#" + let rule_str = r" enabled: true detection: selection: EventID: 1040 Data|startswith: C:\Windows\ - "#; + "; let record_json_str = r#" { @@ -2631,13 +2631,13 @@ mod tests { #[test] fn test_detect_contains_backslash1() { - let rule_str = r#" + let rule_str = r" enabled: true detection: selection: EventID: 1040 Data|contains: \Windows\ - "#; + "; let record_json_str = r#" { @@ -2657,13 +2657,13 @@ mod tests { #[test] fn test_detect_contains_backslash2() { - let rule_str = r#" + let rule_str = r" enabled: true detection: selection: EventID: 1040 Data|contains: \Windows\ - "#; + "; let record_json_str = r#" { @@ -2683,14 +2683,14 @@ mod tests { #[test] fn test_detect_backslash_endswith() { - let rule_str = r#" + let rule_str = r" enabled: true detection: selection: Channel: 'Microsoft-Windows-Sysmon/Operational' EventID: 1 CurrentDirectory|endswith: 'C:\Windows\system32\' - "#; + "; let record_json_str = r#" { @@ -2710,14 +2710,14 @@ mod tests { #[test] fn test_detect_backslash_regex() { - let rule_str = r#" + let rule_str = r" enabled: true detection: selection: Channel: 'Microsoft-Windows-Sysmon/Operational' EventID: 1 CurrentDirectory|re: '.*system32\\' - "#; + "; let record_json_str = r#" { @@ -2737,7 +2737,7 @@ mod tests { #[test] fn test_all_only_true() { - let rule_str = r#" + let rule_str = r" enabled: true detection: selection1: @@ -2748,7 +2748,7 @@ mod tests { - 1 - 2 condition: selection1 and selection2 - "#; + "; let record_json_str = r#" { diff --git a/src/detections/rule/mod.rs b/src/detections/rule/mod.rs index 26f9ea3aa..e268d29da 100644 --- a/src/detections/rule/mod.rs +++ b/src/detections/rule/mod.rs @@ -802,14 +802,14 @@ mod tests { // 上記テストケースのEventDataの更に特殊ケースで下記のようにDataタグの中にNameキーがないケースがある。 // そのためにruleファイルでEventDataというキーだけ特別対応している。 // 現状、downgrade_attack.ymlというルールの場合だけで確認出来ているケース - let rule_str = r#" + let rule_str = r" enabled: true detection: selection: EventID: 403 EventData|re: '[\s\S]*EngineVersion=2\.0[\s\S]*' details: 'command=%CommandLine%' - "#; + "; let record_json_str = r#" { @@ -856,14 +856,14 @@ mod tests { // 上記テストケースのEventDataの更に特殊ケースで下記のようにDataタグの中にNameキーがないケースがある。 // そのためにruleファイルでEventDataというキーだけ特別対応している。 // 現状、downgrade_attack.ymlというルールの場合だけで確認出来ているケース - let rule_str = r#" + let rule_str = r" enabled: true detection: selection: EventID: 403 EventData: '[\s\S]*EngineVersion=3.0[\s\S]*' details: 'command=%CommandLine%' - "#; + "; let record_json_str = r#" { diff --git a/src/timeline/computer_metrics.rs b/src/timeline/computer_metrics.rs index fe8ec4ee0..66339dd77 100644 --- a/src/timeline/computer_metrics.rs +++ b/src/timeline/computer_metrics.rs @@ -198,7 +198,7 @@ mod tests { computer_metrics_dsp_msg(&timeline.stats.stats_list, &output); - let header = vec!["\"Computer\"", "\"Events\""]; + let header = ["\"Computer\"", "\"Events\""]; let expect = vec![vec!["\"HAYABUSA-DESKTOP\"", "1"], vec!["\"FALCON\"", "1"]]; let expect_str = diff --git a/src/timeline/timelines.rs b/src/timeline/timelines.rs index 7df9cbbd6..b7440b336 100644 --- a/src/timeline/timelines.rs +++ b/src/timeline/timelines.rs @@ -890,7 +890,7 @@ mod tests { .logon_stats_start(&input_datas, true, &dummy_stored_static.eventkey_alias); timeline.tm_logon_stats_dsp_msg(&dummy_stored_static); - let mut header = vec![ + let mut header = [ "Successful", "Target Account", "Target Computer", From 3f0ddf2797f94e76ba97d2e581ffbf2402b6b2bb Mon Sep 17 00:00:00 2001 From: DustInDark <2350416+hitenkoku@users.noreply.github.com> Date: Tue, 29 Aug 2023 17:36:36 +0900 Subject: [PATCH 13/17] style: fixed cargo clippy error(arc_with_non_send_sync) --- src/detections/rule/selectionnodes.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/detections/rule/selectionnodes.rs b/src/detections/rule/selectionnodes.rs index cb1c549d2..ba6d3f5eb 100644 --- a/src/detections/rule/selectionnodes.rs +++ b/src/detections/rule/selectionnodes.rs @@ -8,7 +8,7 @@ use yaml_rust::Yaml; use super::matchers::{self, DefaultMatcher}; // Ruleファイルの detection- selection配下のノードはこのtraitを実装する。 -pub trait SelectionNode: Downcast { +pub trait SelectionNode: Downcast + Send + Sync { // 引数で指定されるイベントログのレコードが、条件に一致するかどうかを判定する // このトレイトを実装する構造体毎に適切な判定処理を書く必要がある。 fn select(&self, event_record: &EvtxRecordInfo, eventkey_alias: &EventKeyAliasConfig) -> bool; @@ -308,6 +308,9 @@ pub struct LeafSelectionNode { pub matcher: Option>, } +unsafe impl Sync for LeafSelectionNode {} +unsafe impl Send for LeafSelectionNode {} + impl LeafSelectionNode { pub fn new(keys: Nested, value_yaml: Yaml) -> LeafSelectionNode { LeafSelectionNode { From 74b2db4862c343d7c005633ef35fe51d038af15d Mon Sep 17 00:00:00 2001 From: DustInDark <2350416+hitenkoku@users.noreply.github.com> Date: Tue, 29 Aug 2023 19:34:55 +0900 Subject: [PATCH 14/17] Update src/detections/configs.rs Co-authored-by: Fukusuke Takahashi <41001169+fukusuket@users.noreply.github.com> --- src/detections/configs.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/detections/configs.rs b/src/detections/configs.rs index ab10b5b91..244275fe1 100644 --- a/src/detections/configs.rs +++ b/src/detections/configs.rs @@ -1492,7 +1492,7 @@ pub struct InputOption { #[arg(help_heading = Some("Input"), short = 'x', long = "recover-records", conflicts_with = "json_input", display_order = 440)] pub recover_records: bool, - /// Scan just the most recent number of days(example: 1y, 30d, 24h, etc...) + /// Scan just the most recent number of days(ex: 1y, 30d, 24h, etc...) #[arg(help_heading = Some("Filtering"), long = "timeline-offset", conflicts_with = "start_timeline", display_order = 460)] pub timeline_offset: Option, } From e46046a88f31d69d7326147f929fc11a19901666 Mon Sep 17 00:00:00 2001 From: DustInDark <2350416+hitenkoku@users.noreply.github.com> Date: Tue, 29 Aug 2023 23:17:18 +0900 Subject: [PATCH 15/17] feat(configs): added 'M'(month) in timeline offset #1159 --- src/detections/configs.rs | 68 ++++++++++++++++++++++----------------- 1 file changed, 38 insertions(+), 30 deletions(-) diff --git a/src/detections/configs.rs b/src/detections/configs.rs index 244275fe1..eccd3fee4 100644 --- a/src/detections/configs.rs +++ b/src/detections/configs.rs @@ -6,7 +6,7 @@ use crate::options::htmlreport; use crate::options::pivot::{PivotKeyword, PIVOT_KEYWORD}; use crate::options::profile::{load_profile, Profile}; use aho_corasick::{AhoCorasick, AhoCorasickBuilder, MatchKind}; -use chrono::{DateTime, Duration, Local, Utc}; +use chrono::{DateTime, Days, Duration, Local, Months, Utc}; use clap::{ArgGroup, Args, ColorChoice, Command, CommandFactory, Parser, Subcommand}; use compact_str::CompactString; use hashbrown::{HashMap, HashSet}; @@ -1711,40 +1711,45 @@ impl TargetEventTime { let get_timeline_offset = |timeline_offset: &Option| { if let Some(timeline_offline) = timeline_offset { - let mut start_prev_sec = 0; - let timekey = ["y", "d", "h", "m", "s"]; - for key in timekey { - let mut timekey_splitter = timeline_offline.split(key); + let timekey = ['y', 'M', 'd', 'h', 'm', 's']; + let mut time_num = [0, 0, 0, 0, 0, 0]; + for (idx, key) in timekey.iter().enumerate() { + let mut timekey_splitter = timeline_offline.split(*key); let mix_check = timekey_splitter.next(); - let mixed_checker: Vec<&str> = mix_check - .unwrap_or_default() - .split(['y', 'd', 'h', 'm', 's']) - .collect(); + let mixed_checker: Vec<&str> = + mix_check.unwrap_or_default().split(timekey).collect(); let target_num = if mixed_checker.is_empty() { mix_check.unwrap() } else { mixed_checker[mixed_checker.len() - 1] }; - if let Ok(num) = target_num.parse::() { - if num < 0 { - AlertMessage::alert( - "timeline-offset field: the timestamp format is not correct.", - ) - .ok(); - return None; - } - match key { - "y" => start_prev_sec += num * 365 * 24 * 60 * 60, - "d" => start_prev_sec += num * 24 * 60 * 60, - "h" => start_prev_sec += num * 60 * 60, - "m" => start_prev_sec += num * 60, - "s" => start_prev_sec += num, - _ => {} - } + if target_num.is_empty() { + continue; } + if let Ok(num) = target_num.parse::() { + time_num[idx] = num; + } else { + AlertMessage::alert( + "timeline-offset field: the timestamp format is not correct.", + ) + .ok(); + return None; + } + } + let target_start_time = Local::now() + .checked_sub_months(Months::new(time_num[0] * 12)) + .and_then(|dt| dt.checked_sub_months(Months::new(time_num[1]))) + .and_then(|dt| dt.checked_sub_days(Days::new(time_num[2].into()))) + .and_then(|dt| dt.checked_sub_signed(Duration::hours(time_num[3].into()))) + .and_then(|dt| dt.checked_sub_signed(Duration::minutes(time_num[4].into()))) + .and_then(|dt| dt.checked_sub_signed(Duration::seconds(time_num[5].into()))); + if let Some(start_time) = target_start_time { + Some(start_time.format("%Y-%m-%d %H:%M:%S %z").to_string()) + } else { + AlertMessage::alert("timeline-offset field: the timestamp value is too large.") + .ok(); + None } - let target_start_time = Local::now() - Duration::seconds(start_prev_sec); - Some(target_start_time.format("%Y-%m-%d %H:%M:%S %z").to_string()) } else { None } @@ -2840,7 +2845,7 @@ mod tests { filepath: None, live_analysis: false, recover_records: false, - timeline_offset: Some("10y1s".to_string()), + timeline_offset: Some("1y1M1s".to_string()), }, clobber: true, detect_common_options: DetectCommonOption { @@ -2870,7 +2875,10 @@ mod tests { let now = Utc::now(); let actual = TargetEventTime::new(&pivot_keywords_list); let actual_diff = now - actual.start_time.unwrap(); - let days = actual_diff.num_days(); - assert!(days == 3650 && actual_diff.num_seconds() == days * 24 * 60 * 60 + 1); + let actual_diff_day = actual_diff.num_days(); + assert!( + (393..=397).contains(&actual_diff_day) + && actual_diff.num_seconds() - (actual_diff_day * 24 * 60 * 60) == 1 + ); } } From df870c9a8bd2d21f438bbd32e9c70f83b3e02cd9 Mon Sep 17 00:00:00 2001 From: Yamato Security <71482215+YamatoSecurity@users.noreply.github.com> Date: Thu, 31 Aug 2023 07:33:12 +0900 Subject: [PATCH 16/17] update help msg --- src/detections/configs.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/detections/configs.rs b/src/detections/configs.rs index eccd3fee4..44a57ef76 100644 --- a/src/detections/configs.rs +++ b/src/detections/configs.rs @@ -1492,8 +1492,8 @@ pub struct InputOption { #[arg(help_heading = Some("Input"), short = 'x', long = "recover-records", conflicts_with = "json_input", display_order = 440)] pub recover_records: bool, - /// Scan just the most recent number of days(ex: 1y, 30d, 24h, etc...) - #[arg(help_heading = Some("Filtering"), long = "timeline-offset", conflicts_with = "start_timeline", display_order = 460)] + /// Scan recent events based on an offset (ex: 1y, 3M, 30d, 24h, 30m) + #[arg(help_heading = Some("Filtering"), long = "timeline-offset", value_name = "OFFSET", conflicts_with = "start_timeline", display_order = 460)] pub timeline_offset: Option, } From 4fe71f10fad3bf1adbae023a53652a522ec6303a Mon Sep 17 00:00:00 2001 From: DustInDark <2350416+hitenkoku@users.noreply.github.com> Date: Thu, 31 Aug 2023 17:33:35 +0900 Subject: [PATCH 17/17] fix(configs): fixed error case in timeline-offset option #1159 --- src/detections/configs.rs | 197 +++++++++++++++++++++++--------------- 1 file changed, 118 insertions(+), 79 deletions(-) diff --git a/src/detections/configs.rs b/src/detections/configs.rs index 44a57ef76..471bdc244 100644 --- a/src/detections/configs.rs +++ b/src/detections/configs.rs @@ -84,6 +84,7 @@ pub struct StoredStatic { pub exclude_eid: HashSet, pub field_data_map: Option, pub enable_recover_records: bool, + pub timeline_offset: Option, } impl StoredStatic { /// main.rsでパースした情報からデータを格納する関数 @@ -504,6 +505,18 @@ impl StoredStatic { Some(Action::Search(opt)) => opt.input_args.recover_records, _ => false, }; + let timeline_offset = match &input_config.as_ref().unwrap().action { + Some(Action::CsvTimeline(opt)) => opt.output_options.input_args.timeline_offset.clone(), + Some(Action::JsonTimeline(opt)) => { + opt.output_options.input_args.timeline_offset.clone() + } + Some(Action::EidMetrics(opt)) => opt.input_args.timeline_offset.clone(), + Some(Action::LogonSummary(opt)) => opt.input_args.timeline_offset.clone(), + Some(Action::PivotKeywordsList(opt)) => opt.input_args.timeline_offset.clone(), + Some(Action::Search(opt)) => opt.input_args.timeline_offset.clone(), + Some(Action::ComputerMetrics(opt)) => opt.input_args.timeline_offset.clone(), + _ => None, + }; let mut ret = StoredStatic { config: input_config.as_ref().unwrap().to_owned(), @@ -617,6 +630,7 @@ impl StoredStatic { exclude_eid, field_data_map, enable_recover_records, + timeline_offset, }; ret.profiles = load_profile( check_setting_path( @@ -696,7 +710,7 @@ impl StoredStatic { } } -/// config情報からthred_numberの情報を抽出する関数 +/// config情報からthread_numberの情報を抽出する関数 fn check_thread_number(config: &Config) -> Option { match config.action.as_ref()? { Action::CsvTimeline(opt) => opt.output_options.detect_common_options.thread_number, @@ -1690,179 +1704,204 @@ pub struct TargetEventTime { impl TargetEventTime { pub fn new(stored_static: &StoredStatic) -> Self { - let mut parse_success_flag = true; - let mut get_time = |input_time: Option<&String>, error_contents: &str| { - if let Some(time) = input_time { - match DateTime::parse_from_str(time, "%Y-%m-%d %H:%M:%S %z") // 2014-11-28 21:00:09 +09:00 + let get_time = + |input_time: Option<&String>, error_contents: &str, parse_success_flag: &mut bool| { + if let Some(time) = input_time { + match DateTime::parse_from_str(time, "%Y-%m-%d %H:%M:%S %z") // 2014-11-28 21:00:09 +09:00 .or_else(|_| DateTime::parse_from_str(time, "%Y/%m/%d %H:%M:%S %z")) // 2014/11/28 21:00:09 +09:00 { Ok(dt) => Some(dt.with_timezone(&Utc)), Err(_) => { AlertMessage::alert(error_contents) .ok(); - parse_success_flag = false; + *parse_success_flag = false; None } } - } else { - None - } - }; + } else { + None + } + }; - let get_timeline_offset = |timeline_offset: &Option| { - if let Some(timeline_offline) = timeline_offset { - let timekey = ['y', 'M', 'd', 'h', 'm', 's']; - let mut time_num = [0, 0, 0, 0, 0, 0]; - for (idx, key) in timekey.iter().enumerate() { - let mut timekey_splitter = timeline_offline.split(*key); - let mix_check = timekey_splitter.next(); - let mixed_checker: Vec<&str> = - mix_check.unwrap_or_default().split(timekey).collect(); - let target_num = if mixed_checker.is_empty() { - mix_check.unwrap() - } else { - mixed_checker[mixed_checker.len() - 1] - }; - if target_num.is_empty() { - continue; + let get_timeline_offset = + |timeline_offset: &Option, parse_success_flag: &mut bool| { + if let Some(timeline_offline) = timeline_offset { + let timekey = ['y', 'M', 'd', 'h', 'm', 's']; + let mut time_num = [0, 0, 0, 0, 0, 0]; + for (idx, key) in timekey.iter().enumerate() { + let mut timekey_splitter = timeline_offline.split(*key); + let mix_check = timekey_splitter.next(); + let mixed_checker: Vec<&str> = + mix_check.unwrap_or_default().split(timekey).collect(); + let target_num = if mixed_checker.is_empty() { + mix_check.unwrap() + } else { + mixed_checker[mixed_checker.len() - 1] + }; + if target_num.is_empty() { + continue; + } + if let Ok(num) = target_num.parse::() { + time_num[idx] = num; + } else { + AlertMessage::alert( + "Invalid timeline offset. Please use one of the following formats: 1y, 3M, 30d, 24h, 30m", + ) + .ok(); + *parse_success_flag = false; + return None; + } + } + if time_num.iter().all(|&x| x == 0) { + AlertMessage::alert( + "Invalid timeline offset. Please use one of the following formats: 1y, 3M, 30d, 24h, 30m", + ) + .ok(); + *parse_success_flag = false; + return None; } - if let Ok(num) = target_num.parse::() { - time_num[idx] = num; + let target_start_time = Local::now() + .checked_sub_months(Months::new(time_num[0] * 12)) + .and_then(|dt| dt.checked_sub_months(Months::new(time_num[1]))) + .and_then(|dt| dt.checked_sub_days(Days::new(time_num[2].into()))) + .and_then(|dt| dt.checked_sub_signed(Duration::hours(time_num[3].into()))) + .and_then(|dt| dt.checked_sub_signed(Duration::minutes(time_num[4].into()))) + .and_then(|dt| { + dt.checked_sub_signed(Duration::seconds(time_num[5].into())) + }); + if let Some(start_time) = target_start_time { + Some(start_time.format("%Y-%m-%d %H:%M:%S %z").to_string()) } else { AlertMessage::alert( - "timeline-offset field: the timestamp format is not correct.", + "timeline-offset field: the timestamp value is too large.", ) .ok(); - return None; + *parse_success_flag = false; + None } - } - let target_start_time = Local::now() - .checked_sub_months(Months::new(time_num[0] * 12)) - .and_then(|dt| dt.checked_sub_months(Months::new(time_num[1]))) - .and_then(|dt| dt.checked_sub_days(Days::new(time_num[2].into()))) - .and_then(|dt| dt.checked_sub_signed(Duration::hours(time_num[3].into()))) - .and_then(|dt| dt.checked_sub_signed(Duration::minutes(time_num[4].into()))) - .and_then(|dt| dt.checked_sub_signed(Duration::seconds(time_num[5].into()))); - if let Some(start_time) = target_start_time { - Some(start_time.format("%Y-%m-%d %H:%M:%S %z").to_string()) } else { - AlertMessage::alert("timeline-offset field: the timestamp value is too large.") - .ok(); None } - } else { - None - } - }; + }; + + let mut parse_success_flag = true; + let timeline_offset = + get_timeline_offset(&stored_static.timeline_offset, &mut parse_success_flag); match &stored_static.config.action.as_ref().unwrap() { Action::CsvTimeline(option) => { - let timeoffset = - get_timeline_offset(&option.output_options.input_args.timeline_offset); - let start_time = if timeoffset.is_some() { + let start_time = if timeline_offset.is_some() { get_time( - timeoffset.as_ref(), - "timeline-offset field: the timestamp format is not correct.", + timeline_offset.as_ref(), + "Invalid timeline offset. Please use one of the following formats: 1y, 3M, 30d, 24h, 30m", + &mut parse_success_flag, ) } else { get_time( option.output_options.start_timeline.as_ref(), "start-timeline field: the timestamp format is not correct.", + &mut parse_success_flag, ) }; let end_time = get_time( option.output_options.end_timeline.as_ref(), "end-timeline field: the timestamp format is not correct.", + &mut parse_success_flag, ); Self::set(parse_success_flag, start_time, end_time) } Action::JsonTimeline(option) => { - let timeoffset = - get_timeline_offset(&option.output_options.input_args.timeline_offset); - let start_time = if timeoffset.is_some() { + let start_time = if timeline_offset.is_some() { get_time( - timeoffset.as_ref(), - "timeline-offset field: the timestamp format is not correct.", + timeline_offset.as_ref(), + "Invalid timeline offset. Please use one of the following formats: 1y, 3M, 30d, 24h, 30m", + &mut parse_success_flag, ) } else { get_time( option.output_options.start_timeline.as_ref(), "start-timeline field: the timestamp format is not correct.", + &mut parse_success_flag, ) }; let end_time = get_time( option.output_options.end_timeline.as_ref(), "end-timeline field: the timestamp format is not correct.", + &mut parse_success_flag, ); Self::set(parse_success_flag, start_time, end_time) } Action::PivotKeywordsList(option) => { - let timeoffset = get_timeline_offset(&option.input_args.timeline_offset); - let start_time = if timeoffset.is_some() { + let start_time = if timeline_offset.is_some() { get_time( - timeoffset.as_ref(), - "timeline-offset field: the timestamp format is not correct.", + timeline_offset.as_ref(), + "Invalid timeline offset. Please use one of the following formats: 1y, 3M, 30d, 24h, 30m", + &mut parse_success_flag, ) } else { get_time( option.start_timeline.as_ref(), "start-timeline field: the timestamp format is not correct.", + &mut parse_success_flag, ) }; let end_time = get_time( option.end_timeline.as_ref(), "end-timeline field: the timestamp format is not correct.", + &mut parse_success_flag, ); Self::set(parse_success_flag, start_time, end_time) } Action::LogonSummary(option) => { - let timeoffset = get_timeline_offset(&option.input_args.timeline_offset); - let start_time = if timeoffset.is_some() { + let start_time = if timeline_offset.is_some() { get_time( - timeoffset.as_ref(), - "timeline-offset field: the timestamp format is not correct.", + timeline_offset.as_ref(), + "Invalid timeline offset. Please use one of the following formats: 1y, 3M, 30d, 24h, 30m", + &mut parse_success_flag, ) } else { get_time( option.start_timeline.as_ref(), "start-timeline field: the timestamp format is not correct.", + &mut parse_success_flag, ) }; let end_time = get_time( option.end_timeline.as_ref(), "end-timeline field: the timestamp format is not correct.", + &mut parse_success_flag, ); Self::set(parse_success_flag, start_time, end_time) } - Action::ComputerMetrics(option) => { - let timeoffset = get_timeline_offset(&option.input_args.timeline_offset); - let start_time = if timeoffset.is_some() { + Action::ComputerMetrics(_) => { + let start_time = if timeline_offset.is_some() { get_time( - timeoffset.as_ref(), - "timeline-offset field: the timestamp format is not correct.", + timeline_offset.as_ref(), + "Invalid timeline offset. Please use one of the following formats: 1y, 3M, 30d, 24h, 30m", + &mut parse_success_flag, ) } else { None }; Self::set(parse_success_flag, start_time, None) } - Action::EidMetrics(option) => { - let timeoffset = get_timeline_offset(&option.input_args.timeline_offset); - let start_time = if timeoffset.is_some() { + Action::EidMetrics(_) => { + let start_time = if timeline_offset.is_some() { get_time( - timeoffset.as_ref(), - "timeline-offset field: the timestamp format is not correct.", + timeline_offset.as_ref(), + "Invalid timeline offset. Please use one of the following formats: 1y, 3M, 30d, 24h, 30m", + &mut parse_success_flag, ) } else { None }; Self::set(parse_success_flag, start_time, None) } - Action::Search(option) => { - let timeoffset = get_timeline_offset(&option.input_args.timeline_offset); - let start_time = if timeoffset.is_some() { + Action::Search(_) => { + let start_time = if timeline_offset.is_some() { get_time( - timeoffset.as_ref(), - "timeline-offset field: the timestamp format is not correct.", + timeline_offset.as_ref(), + "Invalid timeline offset. Please use one of the following formats: 1y, 3M, 30d, 24h, 30m", + &mut parse_success_flag, ) } else { None