diff --git a/CHANGELOG-Japanese.md b/CHANGELOG-Japanese.md index 01a63380e..c2e9f1b7a 100644 --- a/CHANGELOG-Japanese.md +++ b/CHANGELOG-Japanese.md @@ -5,6 +5,7 @@ **新機能:** - `gt`、`gte`、`lt`、`lte`のフィールドモディファイアに対応した。(#1433) (@fukusuket) +- 新しい`log-metrics`コマンドで`.evtx`ファイルの情報を取得できるようになった。(コンピュータ名、イベント数、最初のタイムスタンプ、最後のタイムスタンプ、チャネル、プロバイダ) (#1474) (@fukusuket) **改善:** diff --git a/CHANGELOG.md b/CHANGELOG.md index 165668c3e..015ce8bac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ **New Features:** - Support for the `gt`, `gte`, `lt`, `lte` field modifiers. (#1433) (@fukusuket) +- New `log-metrics` command to get information about `.evtx` files. (computer names, event count, first timestamp, last timestamp, channels, providers) (#1474) (@fukusuket) **Enhancements:** diff --git a/src/afterfact.rs b/src/afterfact.rs index df09faa01..628a87f4f 100644 --- a/src/afterfact.rs +++ b/src/afterfact.rs @@ -25,7 +25,8 @@ use terminal_size::terminal_size; use terminal_size::Width; use crate::detections::configs::{ - Action, OutputOption, StoredStatic, CONTROL_CHAT_REPLACE_MAP, CURRENT_EXE_PATH, GEOIP_DB_PARSER, + Action, StoredStatic, TimeFormatOptions, CONTROL_CHAT_REPLACE_MAP, CURRENT_EXE_PATH, + GEOIP_DB_PARSER, }; use crate::detections::message::{AlertMessage, DetectInfo, COMPUTER_MITRE_ATTCK_MAP, LEVEL_FULL}; use crate::detections::utils::{ @@ -590,7 +591,11 @@ fn calc_statistic_info( countup_aggregation( &mut afterfact_info.detect_counts_by_date_and_level, &detect_info.level, - &format_time(&detect_info.detected_time, true, output_option), + &format_time( + &detect_info.detected_time, + true, + &output_option.time_format_options, + ), ); countup_aggregation( &mut afterfact_info.detect_counts_by_rule_and_level, @@ -692,7 +697,11 @@ pub fn output_additional_afterfact( utils::format_time( &afterfact_info.tl_starttime.unwrap(), false, - stored_static.output_option.as_ref().unwrap() + &stored_static + .output_option + .as_ref() + .unwrap() + .time_format_options ) ), "Results Summary {#results_summary}", @@ -706,7 +715,11 @@ pub fn output_additional_afterfact( utils::format_time( &afterfact_info.tl_endtime.unwrap(), false, - stored_static.output_option.as_ref().unwrap() + &stored_static + .output_option + .as_ref() + .unwrap() + .time_format_options ) ), "Results Summary {#results_summary}", @@ -1669,7 +1682,7 @@ fn _print_detection_summary_tables( } /// get timestamp to input datetime. -fn _get_timestamp(output_option: &OutputOption, time: &DateTime) -> i64 { +fn _get_timestamp(output_option: &TimeFormatOptions, time: &DateTime) -> i64 { if output_option.utc || output_option.iso_8601 { time.timestamp() } else { @@ -2258,7 +2271,6 @@ mod tests { use crate::afterfact::output_afterfact_inner; use crate::afterfact::AfterfactInfo; use crate::afterfact::Colors; - use crate::detections::configs::load_eventkey_alias; use crate::detections::configs::Action; use crate::detections::configs::CommonOptions; use crate::detections::configs::Config; @@ -2269,6 +2281,7 @@ mod tests { use crate::detections::configs::OutputOption; use crate::detections::configs::StoredStatic; use crate::detections::configs::CURRENT_EXE_PATH; + use crate::detections::configs::{load_eventkey_alias, TimeFormatOptions}; use crate::detections::field_data_map::FieldDataMapKey; use crate::detections::message; use crate::detections::message::DetectInfo; @@ -2320,13 +2333,15 @@ mod tests { 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, + time_format_options: TimeFormatOptions { + 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, @@ -2413,13 +2428,15 @@ mod tests { 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, + time_format_options: TimeFormatOptions { + 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, @@ -2465,7 +2482,9 @@ mod tests { let mut profile_converter: HashMap<&str, Profile> = HashMap::from([ ( "Timestamp", - Profile::Timestamp(format_time(&expect_time, false, &output_option).into()), + Profile::Timestamp( + format_time(&expect_time, false, &output_option.time_format_options).into(), + ), ), ("Computer", Profile::Computer(test_computername2.into())), ("Channel", Profile::Channel(ch.into())), @@ -2662,13 +2681,15 @@ mod tests { 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, + time_format_options: TimeFormatOptions { + 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, @@ -2765,13 +2786,15 @@ mod tests { 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, + time_format_options: TimeFormatOptions { + 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, @@ -2817,7 +2840,9 @@ mod tests { let mut profile_converter: HashMap<&str, Profile> = HashMap::from([ ( "Timestamp", - Profile::Timestamp(format_time(&expect_time, false, &output_option).into()), + Profile::Timestamp( + format_time(&expect_time, false, &output_option.time_format_options).into(), + ), ), ("Computer", Profile::Computer(test_computername2.into())), ("Channel", Profile::Channel(ch.into())), @@ -2998,13 +3023,15 @@ mod tests { 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, + time_format_options: TimeFormatOptions { + 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, @@ -3091,13 +3118,15 @@ mod tests { 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, + time_format_options: TimeFormatOptions { + 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, @@ -3143,7 +3172,9 @@ mod tests { let mut profile_converter: HashMap<&str, Profile> = HashMap::from([ ( "Timestamp", - Profile::Timestamp(format_time(&expect_time, false, &output_option).into()), + Profile::Timestamp( + format_time(&expect_time, false, &output_option.time_format_options).into(), + ), ), ("Computer", Profile::Computer(test_computername2.into())), ("Channel", Profile::Channel(ch.into())), @@ -3335,13 +3366,15 @@ mod tests { 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, + time_format_options: TimeFormatOptions { + 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, @@ -3428,13 +3461,15 @@ mod tests { 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, + time_format_options: TimeFormatOptions { + 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, @@ -3480,7 +3515,9 @@ mod tests { let mut profile_converter: HashMap<&str, Profile> = HashMap::from([ ( "Timestamp", - Profile::Timestamp(format_time(&expect_time, false, &output_option).into()), + Profile::Timestamp( + format_time(&expect_time, false, &output_option.time_format_options).into(), + ), ), ("Computer", Profile::Computer(test_computername2.into())), ("Channel", Profile::Channel(ch.into())), @@ -3745,13 +3782,15 @@ mod tests { 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, + time_format_options: TimeFormatOptions { + 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, @@ -3839,13 +3878,15 @@ mod tests { 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, + time_format_options: TimeFormatOptions { + 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, @@ -3891,7 +3932,9 @@ mod tests { let mut profile_converter: HashMap<&str, Profile> = HashMap::from([ ( "Timestamp", - Profile::Timestamp(format_time(&expect_time, false, &output_option).into()), + Profile::Timestamp( + format_time(&expect_time, false, &output_option.time_format_options).into(), + ), ), ("Computer", Profile::Computer(test_computername.into())), ("Channel", Profile::Channel(ch.into())), @@ -4100,13 +4143,15 @@ mod tests { 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, + time_format_options: TimeFormatOptions { + 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, @@ -4194,13 +4239,15 @@ mod tests { 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: true, + time_format_options: TimeFormatOptions { + european_time: false, + iso_8601: false, + rfc_2822: false, + rfc_3339: false, + us_military_time: false, + us_time: false, + utc: true, + }, visualize_timeline: false, rules: Path::new("./rules").to_path_buf(), html_report: None, @@ -4246,7 +4293,9 @@ mod tests { let mut profile_converter: HashMap<&str, Profile> = HashMap::from([ ( "Timestamp", - Profile::Timestamp(format_time(&expect_time, false, &output_option).into()), + Profile::Timestamp( + format_time(&expect_time, false, &output_option.time_format_options).into(), + ), ), ("Computer", Profile::Computer(test_computername2.into())), ("Channel", Profile::Channel(ch.into())), @@ -4380,13 +4429,15 @@ mod tests { 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, + time_format_options: TimeFormatOptions { + 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, @@ -4474,13 +4525,15 @@ mod tests { 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: true, + time_format_options: TimeFormatOptions { + european_time: false, + iso_8601: false, + rfc_2822: false, + rfc_3339: false, + us_military_time: false, + us_time: false, + utc: true, + }, visualize_timeline: false, rules: Path::new("./rules").to_path_buf(), html_report: None, @@ -4526,7 +4579,9 @@ mod tests { let mut profile_converter: HashMap<&str, Profile> = HashMap::from([ ( "Timestamp", - Profile::Timestamp(format_time(&expect_time, false, &output_option).into()), + Profile::Timestamp( + format_time(&expect_time, false, &output_option.time_format_options).into(), + ), ), ("Computer", Profile::Computer(test_computername2.into())), ("Channel", Profile::Channel(ch.into())), diff --git a/src/detections/configs.rs b/src/detections/configs.rs index 5e5062fad..735e0a3e9 100644 --- a/src/detections/configs.rs +++ b/src/detections/configs.rs @@ -110,6 +110,7 @@ pub struct StoredStatic { pub logon_summary_flag: bool, pub search_flag: bool, pub computer_metrics_flag: bool, + pub log_metrics_flag: bool, pub search_option: Option, pub output_option: Option, pub pivot_keyword_list_flag: bool, @@ -151,6 +152,7 @@ impl StoredStatic { Some(Action::PivotKeywordsList(opt)) => opt.detect_common_options.quiet_errors, Some(Action::Search(opt)) => opt.quiet_errors, Some(Action::ComputerMetrics(opt)) => opt.quiet_errors, + Some(Action::LogMetrics(opt)) => opt.detect_common_options.quiet_errors, _ => false, }; let common_options = match &input_config.as_ref().unwrap().action { @@ -165,6 +167,7 @@ impl StoredStatic { Some(Action::UpdateRules(opt)) => opt.common_options, Some(Action::Search(opt)) => opt.common_options, Some(Action::ComputerMetrics(opt)) => opt.common_options, + Some(Action::LogMetrics(opt)) => opt.common_options, None => CommonOptions { no_color: false, quiet: false, @@ -180,6 +183,7 @@ impl StoredStatic { Some(Action::PivotKeywordsList(opt)) => &opt.detect_common_options.config, Some(Action::Search(opt)) => &opt.config, Some(Action::ComputerMetrics(opt)) => &opt.config, + Some(Action::LogMetrics(opt)) => &opt.detect_common_options.config, _ => &binding, }; let verbose_flag = match &input_config.as_ref().unwrap().action { @@ -190,6 +194,7 @@ impl StoredStatic { Some(Action::PivotKeywordsList(opt)) => opt.detect_common_options.verbose, Some(Action::Search(opt)) => opt.verbose, Some(Action::ComputerMetrics(opt)) => opt.verbose, + Some(Action::LogMetrics(opt)) => opt.detect_common_options.verbose, _ => false, }; let json_input_flag = match &input_config.as_ref().unwrap().action { @@ -199,6 +204,7 @@ impl StoredStatic { Some(Action::EidMetrics(opt)) => opt.detect_common_options.json_input, Some(Action::PivotKeywordsList(opt)) => opt.detect_common_options.json_input, Some(Action::ComputerMetrics(opt)) => opt.json_input, + Some(Action::LogMetrics(opt)) => opt.detect_common_options.json_input, _ => false, }; let is_valid_min_level = match &input_config.as_ref().unwrap().action { @@ -347,6 +353,7 @@ impl StoredStatic { Some(Action::LogonSummary(opt)) => opt.output.as_ref(), Some(Action::Search(opt)) => opt.output.as_ref(), Some(Action::ComputerMetrics(opt)) => opt.output.as_ref(), + Some(Action::LogMetrics(opt)) => opt.output.as_ref(), _ => None, }; let general_ch_abbr = create_output_filter_config( @@ -366,6 +373,7 @@ impl StoredStatic { let multiline_flag = match &input_config.as_ref().unwrap().action { Some(Action::CsvTimeline(opt)) => opt.multiline, Some(Action::Search(opt)) => opt.multiline, + Some(Action::LogMetrics(opt)) => opt.multiline, _ => false, }; let proven_rule_flag = match &input_config.as_ref().unwrap().action { @@ -434,6 +442,14 @@ impl StoredStatic { .iter() .map(CompactString::from) .collect(), + Some(Action::LogMetrics(opt)) => opt + .detect_common_options + .include_computer + .as_ref() + .unwrap_or(&vec![]) + .iter() + .map(CompactString::from) + .collect(), _ => HashSet::default(), }; let exclude_computer: HashSet = match &input_config.as_ref().unwrap().action @@ -480,6 +496,14 @@ impl StoredStatic { .iter() .map(CompactString::from) .collect(), + Some(Action::LogMetrics(opt)) => opt + .detect_common_options + .exclude_computer + .as_ref() + .unwrap_or(&vec![]) + .iter() + .map(CompactString::from) + .collect(), _ => HashSet::default(), }; let include_eid: HashSet = match &input_config.as_ref().unwrap().action { @@ -570,6 +594,7 @@ impl StoredStatic { Some(Action::LogonSummary(opt)) => opt.input_args.recover_records, Some(Action::PivotKeywordsList(opt)) => opt.input_args.recover_records, Some(Action::Search(opt)) => opt.input_args.recover_records, + Some(Action::LogMetrics(opt)) => opt.input_args.recover_records, _ => false, }; let timeline_offset = match &input_config.as_ref().unwrap().action { @@ -582,6 +607,7 @@ impl StoredStatic { 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(), + Some(Action::LogMetrics(opt)) => opt.input_args.timeline_offset.clone(), _ => None, }; let include_status: HashSet = match &input_config.as_ref().unwrap().action { @@ -692,6 +718,7 @@ impl StoredStatic { metrics_flag: action_id == 3, search_flag: action_id == 10, computer_metrics_flag: action_id == 11, + log_metrics_flag: action_id == 12, search_option: extract_search_options(input_config.as_ref().unwrap()), output_option: extract_output_options(input_config.as_ref().unwrap()), pivot_keyword_list_flag: action_id == 4, @@ -830,6 +857,7 @@ fn check_thread_number(config: &Config) -> Option { Action::LogonSummary(opt) => opt.detect_common_options.thread_number, Action::EidMetrics(opt) => opt.detect_common_options.thread_number, Action::PivotKeywordsList(opt) => opt.detect_common_options.thread_number, + Action::LogMetrics(opt) => opt.detect_common_options.thread_number, _ => None, } } @@ -857,6 +885,16 @@ pub enum Action { /// Save the timeline in JSON/JSONL format. JsonTimeline(JSONOutputOption), + #[clap( + author = "Yamato Security (https://github.com/Yamato-Security/hayabusa - @SecurityYamato)", + help_template = "\nHayabusa v2.19.0 - Dev Build\n{author-with-newline}\n{usage-heading}\n hayabusa.exe log-metrics [OPTIONS]\n\n{all-args}", + term_width = 400, + display_order = 382, + disable_help_flag = true + )] + /// Print log file metrics + LogMetrics(LogMetricsOption), + #[clap( author = "Yamato Security (https://github.com/Yamato-Security/hayabusa - @SecurityYamato)", help_template = "\nHayabusa v2.19.0 - Dev Build\n{author-with-newline}\n{usage-heading}\n hayabusa.exe logon-summary [OPTIONS]\n\n{all-args}", @@ -962,6 +1000,7 @@ impl Action { Action::ListProfiles(_) => 9, Action::Search(_) => 10, Action::ComputerMetrics(_) => 11, + Action::LogMetrics(_) => 12, } } else { 100 @@ -982,6 +1021,7 @@ impl Action { Action::ListProfiles(_) => "list-profiles", Action::Search(_) => "search", Action::ComputerMetrics(_) => "computer-metrics", + Action::LogMetrics(_) => "log-metrics", } } else { "" @@ -1048,6 +1088,37 @@ pub struct DefaultProfileOption { pub profile: Option, } +#[derive(Args, Clone, Debug)] +pub struct TimeFormatOptions { + /// Output timestamp in European time format (ex: 22-02-2022 22:00:00.123 +02:00) + #[arg(help_heading = Some("Time Format"), long = "European-time", display_order = 50)] + pub european_time: bool, + + /// Output timestamp in original ISO-8601 format (ex: 2022-02-22T10:10:10.1234567Z) (Always UTC) + #[arg(help_heading = Some("Time Format"), short = 'O', long = "ISO-8601", display_order = 90)] + pub iso_8601: bool, + + /// Output timestamp in RFC 2822 format (ex: Fri, 22 Feb 2022 22:00:00 -0600) + #[arg(help_heading = Some("Time Format"), long = "RFC-2822", display_order = 180)] + pub rfc_2822: bool, + + /// Output timestamp in RFC 3339 format (ex: 2022-02-22 22:00:00.123456-06:00) + #[arg(help_heading = Some("Time Format"), long = "RFC-3339", display_order = 180)] + pub rfc_3339: bool, + + /// Output timestamp in US military time format (ex: 02-22-2022 22:00:00.123 -06:00) + #[arg(help_heading = Some("Time Format"), long = "US-military-time", display_order = 210)] + pub us_military_time: bool, + + /// Output timestamp in US time format (ex: 02-22-2022 10:00:00.123 PM -06:00) + #[arg(help_heading = Some("Time Format"), long = "US-time", display_order = 210)] + pub us_time: bool, + + /// Output time in UTC format (default: local time) + #[arg(help_heading = Some("Time Format"), short = 'U', long = "UTC", display_order = 210)] + pub utc: bool, +} + #[derive(Args, Clone, Debug)] #[clap(group(ArgGroup::new("search_input_filtering").args(["keywords", "regex"]).required(true)))] pub struct SearchOption { @@ -1170,33 +1241,8 @@ pub struct SearchOption { #[arg(help_heading = Some("Output"), short = 'L', long = "JSONL-output", conflicts_with_all = ["jsonl_output", "multiline"], requires = "output", display_order = 100)] pub jsonl_output: bool, - /// Output timestamp in European time format (ex: 22-02-2022 22:00:00.123 +02:00) - #[arg(help_heading = Some("Time Format"), long = "European-time", display_order = 50)] - pub european_time: bool, - - /// Output timestamp in original ISO-8601 format (ex: 2022-02-22T10:10:10.1234567Z) (Always UTC) - #[arg(help_heading = Some("Time Format"), short = 'O', long = "ISO-8601", display_order = 90)] - pub iso_8601: bool, - - /// Output timestamp in RFC 2822 format (ex: Fri, 22 Feb 2022 22:00:00 -0600) - #[arg(help_heading = Some("Time Format"), long = "RFC-2822", display_order = 180)] - pub rfc_2822: bool, - - /// Output timestamp in RFC 3339 format (ex: 2022-02-22 22:00:00.123456-06:00) - #[arg(help_heading = Some("Time Format"), long = "RFC-3339", display_order = 180)] - pub rfc_3339: bool, - - /// Output timestamp in US military time format (ex: 02-22-2022 22:00:00.123 -06:00) - #[arg(help_heading = Some("Time Format"), long = "US-military-time", display_order = 210)] - pub us_military_time: bool, - - /// Output timestamp in US time format (ex: 02-22-2022 10:00:00.123 PM -06:00) - #[arg(help_heading = Some("Time Format"), long = "US-time", display_order = 210)] - pub us_time: bool, - - /// Output time in UTC format (default: local time) - #[arg(help_heading = Some("Time Format"), short = 'U', long = "UTC", display_order = 210)] - pub utc: bool, + #[clap(flatten)] + pub time_format_options: TimeFormatOptions, } #[derive(Args, Clone, Debug)] @@ -1251,33 +1297,8 @@ pub struct EidMetricsOption { #[clap(flatten)] pub detect_common_options: DetectCommonOption, - /// Output timestamp in European time format (ex: 22-02-2022 22:00:00.123 +02:00) - #[arg(help_heading = Some("Time Format"), long = "European-time", display_order = 50)] - pub european_time: bool, - - /// Output timestamp in original ISO-8601 format (ex: 2022-02-22T10:10:10.1234567Z) (Always UTC) - #[arg(help_heading = Some("Time Format"), short = 'O', long = "ISO-8601", display_order = 90)] - pub iso_8601: bool, - - /// Output timestamp in RFC 2822 format (ex: Fri, 22 Feb 2022 22:00:00 -0600) - #[arg(help_heading = Some("Time Format"), long = "RFC-2822", display_order = 180)] - pub rfc_2822: bool, - - /// Output timestamp in RFC 3339 format (ex: 2022-02-22 22:00:00.123456-06:00) - #[arg(help_heading = Some("Time Format"), long = "RFC-3339", display_order = 180)] - pub rfc_3339: bool, - - /// Output timestamp in US military time format (ex: 02-22-2022 22:00:00.123 -06:00) - #[arg(help_heading = Some("Time Format"), long = "US-military-time", display_order = 210)] - pub us_military_time: bool, - - /// Output timestamp in US time format (ex: 02-22-2022 10:00:00.123 PM -06:00) - #[arg(help_heading = Some("Time Format"), long = "US-time", display_order = 210)] - pub us_time: bool, - - /// Output time in UTC format (default: local time) - #[arg(help_heading = Some("Time Format"), short = 'U', long = "UTC", display_order = 210)] - pub utc: bool, + #[clap(flatten)] + pub time_format_options: TimeFormatOptions, /// Overwrite files when saving #[arg(help_heading = Some("General Options"), short='C', long = "clobber", display_order = 290, requires = "output")] @@ -1399,33 +1420,8 @@ pub struct LogonSummaryOption { #[clap(flatten)] pub detect_common_options: DetectCommonOption, - /// Output timestamp in European time format (ex: 22-02-2022 22:00:00.123 +02:00) - #[arg(help_heading = Some("Time Format"), long = "European-time", display_order = 50)] - pub european_time: bool, - - /// Output timestamp in original ISO-8601 format (ex: 2022-02-22T10:10:10.1234567Z) (Always UTC) - #[arg(help_heading = Some("Time Format"), short = 'O', long = "ISO-8601", display_order = 90)] - pub iso_8601: bool, - - /// Output timestamp in RFC 2822 format (ex: Fri, 22 Feb 2022 22:00:00 -0600) - #[arg(help_heading = Some("Time Format"), long = "RFC-2822", display_order = 180)] - pub rfc_2822: bool, - - /// Output timestamp in RFC 3339 format (ex: 2022-02-22 22:00:00.123456-06:00) - #[arg(help_heading = Some("Time Format"), long = "RFC-3339", display_order = 180)] - pub rfc_3339: bool, - - /// Output timestamp in US military time format (ex: 02-22-2022 22:00:00.123 -06:00) - #[arg(help_heading = Some("Time Format"), long = "US-military-time", display_order = 210)] - pub us_military_time: bool, - - /// Output timestamp in US time format (ex: 02-22-2022 10:00:00.123 PM -06:00) - #[arg(help_heading = Some("Time Format"), long = "US-time", display_order = 210)] - pub us_time: bool, - - /// Output time in UTC format (default: local time) - #[arg(help_heading = Some("Time Format"), short = 'U', long = "UTC", display_order = 210)] - pub utc: bool, + #[clap(flatten)] + pub time_format_options: TimeFormatOptions, /// Overwrite files when saving #[arg(help_heading = Some("General Options"), short='C', long = "clobber", display_order = 290, requires = "output")] @@ -1542,33 +1538,8 @@ pub struct OutputOption { #[clap(flatten)] pub detect_common_options: DetectCommonOption, - /// Output timestamp in European time format (ex: 22-02-2022 22:00:00.123 +02:00) - #[arg(help_heading = Some("Time Format"), long = "European-time", display_order = 50)] - pub european_time: bool, - - /// Output timestamp in original ISO-8601 format (ex: 2022-02-22T10:10:10.1234567Z) (Always UTC) - #[arg(help_heading = Some("Time Format"), short = 'O', long = "ISO-8601", display_order = 90)] - pub iso_8601: bool, - - /// Output timestamp in RFC 2822 format (ex: Fri, 22 Feb 2022 22:00:00 -0600) - #[arg(help_heading = Some("Time Format"), long = "RFC-2822", display_order = 180)] - pub rfc_2822: bool, - - /// Output timestamp in RFC 3339 format (ex: 2022-02-22 22:00:00.123456-06:00) - #[arg(help_heading = Some("Time Format"), long = "RFC-3339", display_order = 180)] - pub rfc_3339: bool, - - /// Output timestamp in US military time format (ex: 02-22-2022 22:00:00.123 -06:00) - #[arg(help_heading = Some("Time Format"), long = "US-military-time", display_order = 210)] - pub us_military_time: bool, - - /// Output timestamp in US time format (ex: 02-22-2022 10:00:00.123 PM -06:00) - #[arg(help_heading = Some("Time Format"), long = "US-time", display_order = 210)] - pub us_time: bool, - - /// Output time in UTC format (default: local time) - #[arg(help_heading = Some("Time Format"), short = 'U', long = "UTC", display_order = 210)] - pub utc: bool, + #[clap(flatten)] + pub time_format_options: TimeFormatOptions, /// Output event frequency timeline (terminal needs to support unicode) #[arg(help_heading = Some("Display Settings"), short = 'T', long = "visualize-timeline", display_order = 490)] @@ -1781,6 +1752,33 @@ pub struct ComputerMetricsOption { pub clobber: bool, } +#[derive(Args, Clone, Debug)] +pub struct LogMetricsOption { + #[clap(flatten)] + pub input_args: InputOption, + + /// Save the Metrics in CSV format (ex: metrics.csv) + #[arg(help_heading = Some("Output"), short = 'o', long, value_name = "FILE", display_order = 410)] + pub output: Option, + + #[clap(flatten)] + pub common_options: CommonOptions, + + #[clap(flatten)] + pub detect_common_options: DetectCommonOption, + + #[clap(flatten)] + pub time_format_options: TimeFormatOptions, + + /// Output event field information in multiple rows for CSV output + #[arg(help_heading = Some("Output"), short = 'M', long="multiline", display_order = 390)] + pub multiline: bool, + + /// Overwrite files when saving + #[arg(help_heading = Some("General Options"), short='C', long = "clobber", display_order = 290, requires = "output")] + pub clobber: bool, +} + #[derive(Parser, Clone, Debug)] #[clap( author = "Yamato Security (https://github.com/Yamato-Security/hayabusa - @SecurityYamato)", @@ -2051,31 +2049,10 @@ impl TargetEventTime { ); Self::set(parse_success_flag, start_time, end_time) } - Action::ComputerMetrics(_) => { - let start_time = if timeline_offset.is_some() { - get_time( - 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(_) => { - let start_time = if timeline_offset.is_some() { - get_time( - 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(_) => { + Action::LogMetrics(_) + | Action::EidMetrics(_) + | Action::ComputerMetrics(_) + | Action::Search(_) => { let start_time = if timeline_offset.is_some() { get_time( timeline_offset.as_ref(), @@ -2263,13 +2240,7 @@ fn extract_search_options(config: &Config) -> Option { clobber: option.clobber, json_output: option.json_output, jsonl_output: option.jsonl_output, - european_time: option.european_time, - iso_8601: option.iso_8601, - rfc_2822: option.rfc_2822, - rfc_3339: option.rfc_3339, - us_military_time: option.us_military_time, - us_time: option.us_time, - utc: option.utc, + time_format_options: option.time_format_options.clone(), and_logic: option.and_logic, }), _ => None, @@ -2292,13 +2263,15 @@ fn extract_output_options(config: &Config) -> Option { end_timeline: option.end_timeline.clone(), start_timeline: option.start_timeline.clone(), eid_filter: option.eid_filter, - european_time: false, - iso_8601: false, - rfc_2822: false, - rfc_3339: false, - us_military_time: false, - us_time: false, - utc: false, + time_format_options: TimeFormatOptions { + 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, @@ -2335,13 +2308,7 @@ fn extract_output_options(config: &Config) -> Option { end_timeline: None, start_timeline: None, eid_filter: false, - european_time: option.european_time, - iso_8601: option.iso_8601, - rfc_2822: option.rfc_2822, - rfc_3339: option.rfc_3339, - us_military_time: option.us_military_time, - us_time: option.us_time, - utc: option.utc, + time_format_options: option.time_format_options.clone(), visualize_timeline: false, rules: Path::new("./rules").to_path_buf(), html_report: None, @@ -2378,13 +2345,7 @@ fn extract_output_options(config: &Config) -> Option { end_timeline: None, start_timeline: None, eid_filter: false, - european_time: option.european_time, - iso_8601: option.iso_8601, - rfc_2822: option.rfc_2822, - rfc_3339: option.rfc_3339, - us_military_time: option.us_military_time, - us_time: option.us_time, - utc: option.utc, + time_format_options: option.time_format_options.clone(), visualize_timeline: false, rules: Path::new("./rules").to_path_buf(), html_report: None, @@ -2438,13 +2399,52 @@ fn extract_output_options(config: &Config) -> Option { include_computer: None, exclude_computer: None, }, - european_time: false, - iso_8601: false, - rfc_2822: false, - rfc_3339: false, - us_military_time: false, - us_time: false, - utc: false, + time_format_options: TimeFormatOptions { + 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, + clobber: option.clobber, + include_eid: None, + exclude_eid: None, + no_field: false, + no_pwsh_field_extraction: false, + remove_duplicate_data: false, + remove_duplicate_detections: false, + no_wizard: true, + include_status: None, + sort_events: false, + enable_all_rules: false, + scan_all_evtx_files: false, + }), + Action::LogMetrics(option) => Some(OutputOption { + input_args: option.input_args.clone(), + profile: None, + common_options: option.common_options, + enable_deprecated_rules: false, + enable_unsupported_rules: false, + exclude_status: None, + include_tag: None, + include_category: None, + exclude_category: None, + min_level: String::default(), + exact_level: None, + enable_noisy_rules: false, + end_timeline: None, + start_timeline: None, + eid_filter: false, + proven_rules: false, + exclude_tag: None, + detect_common_options: option.detect_common_options.clone(), + time_format_options: option.time_format_options.clone(), visualize_timeline: false, rules: Path::new("./rules").to_path_buf(), html_report: None, @@ -2472,13 +2472,7 @@ fn extract_output_options(config: &Config) -> Option { end_timeline: None, start_timeline: None, eid_filter: false, - european_time: option.european_time, - iso_8601: option.iso_8601, - rfc_2822: option.rfc_2822, - rfc_3339: option.rfc_3339, - us_military_time: option.us_military_time, - us_time: option.us_time, - utc: option.utc, + time_format_options: option.time_format_options.clone(), visualize_timeline: false, rules: Path::new("./rules").to_path_buf(), html_report: None, @@ -2531,13 +2525,15 @@ fn extract_output_options(config: &Config) -> Option { 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, + time_format_options: TimeFormatOptions { + 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, @@ -2589,13 +2585,15 @@ fn extract_output_options(config: &Config) -> Option { 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, + time_format_options: TimeFormatOptions { + 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, @@ -2753,7 +2751,7 @@ mod tests { use super::{ create_control_chat_replace_map, Action, CommonOptions, Config, CsvOutputOption, DetectCommonOption, InputOption, JSONOutputOption, OutputOption, StoredStatic, - TargetEventTime, + TargetEventTime, TimeFormatOptions, }; use crate::detections::configs::{ self, EidMetricsOption, LogonSummaryOption, PivotKeywordOption, SearchOption, @@ -2865,13 +2863,15 @@ mod tests { 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, + time_format_options: TimeFormatOptions { + 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, @@ -2943,13 +2943,15 @@ mod tests { 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, + time_format_options: TimeFormatOptions { + 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, @@ -3031,13 +3033,15 @@ mod tests { 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, + time_format_options: TimeFormatOptions { + european_time: false, + iso_8601: false, + rfc_2822: false, + rfc_3339: false, + us_military_time: false, + us_time: false, + utc: false, + }, })), debug: false, })); @@ -3065,13 +3069,15 @@ mod tests { 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, + time_format_options: TimeFormatOptions { + 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, @@ -3109,13 +3115,15 @@ mod tests { 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, + time_format_options: TimeFormatOptions { + 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, diff --git a/src/detections/detection.rs b/src/detections/detection.rs index 3b7e11bcf..f5a8935e5 100644 --- a/src/detections/detection.rs +++ b/src/detections/detection.rs @@ -147,7 +147,8 @@ impl Detection { if !(stored_static.logon_summary_flag || stored_static.search_flag || stored_static.metrics_flag - || stored_static.computer_metrics_flag) + || stored_static.computer_metrics_flag + || stored_static.log_metrics_flag) { Detection::print_rule_load_info( &rulefile_loader.rulecounter, @@ -317,7 +318,11 @@ impl Detection { format_time( &time, false, - stored_static.output_option.as_ref().unwrap(), + &stored_static + .output_option + .as_ref() + .unwrap() + .time_format_options, ) .into(), ), @@ -786,7 +791,11 @@ impl Detection { format_time( &agg_result.start_timedate, false, - stored_static.output_option.as_ref().unwrap(), + &stored_static + .output_option + .as_ref() + .unwrap() + .time_format_options, ) .into(), ), @@ -1295,7 +1304,6 @@ mod tests { use yaml_rust2::YamlLoader; use crate::detections; - use crate::detections::configs::load_eventkey_alias; use crate::detections::configs::Action; use crate::detections::configs::CommonOptions; use crate::detections::configs::Config; @@ -1306,6 +1314,7 @@ mod tests { use crate::detections::configs::StoredStatic; use crate::detections::configs::CURRENT_EXE_PATH; use crate::detections::configs::STORED_EKEY_ALIAS; + use crate::detections::configs::{load_eventkey_alias, TimeFormatOptions}; use crate::detections::detection::Detection; use crate::detections::rule::create_rule; use crate::detections::rule::AggResult; @@ -1334,13 +1343,15 @@ mod tests { 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, + time_format_options: TimeFormatOptions { + 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, @@ -1591,13 +1602,15 @@ mod tests { 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, + time_format_options: TimeFormatOptions { + 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, @@ -1730,13 +1743,15 @@ mod tests { 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, + time_format_options: TimeFormatOptions { + 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, @@ -1864,13 +1879,15 @@ mod tests { 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, + time_format_options: TimeFormatOptions { + 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, @@ -2014,13 +2031,15 @@ mod tests { 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, + time_format_options: TimeFormatOptions { + 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, diff --git a/src/detections/rule/condition_parser.rs b/src/detections/rule/condition_parser.rs index 7ec14177c..af9a7a8ea 100644 --- a/src/detections/rule/condition_parser.rs +++ b/src/detections/rule/condition_parser.rs @@ -421,7 +421,7 @@ impl ConditionCompiler { mod tests { use crate::detections::configs::{ Action, CommonOptions, Config, CsvOutputOption, DetectCommonOption, InputOption, - OutputOption, StoredStatic, STORED_EKEY_ALIAS, + OutputOption, StoredStatic, TimeFormatOptions, STORED_EKEY_ALIAS, }; use crate::detections::rule::condition_parser::ConditionCompiler; use crate::detections::rule::create_rule; @@ -467,13 +467,15 @@ mod tests { 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, + time_format_options: TimeFormatOptions { + 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, diff --git a/src/detections/rule/count.rs b/src/detections/rule/count.rs index 00b145141..79877593a 100644 --- a/src/detections/rule/count.rs +++ b/src/detections/rule/count.rs @@ -542,7 +542,6 @@ mod tests { use std::path::Path; use crate::detections; - use crate::detections::configs::Action; use crate::detections::configs::CommonOptions; use crate::detections::configs::Config; use crate::detections::configs::CsvOutputOption; @@ -551,6 +550,7 @@ mod tests { use crate::detections::configs::OutputOption; use crate::detections::configs::StoredStatic; use crate::detections::configs::STORED_EKEY_ALIAS; + use crate::detections::configs::{Action, TimeFormatOptions}; use crate::detections::rule::create_rule; use crate::detections::rule::AggResult; use crate::detections::utils; @@ -598,13 +598,15 @@ mod tests { 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, + time_format_options: TimeFormatOptions { + 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, diff --git a/src/detections/rule/matchers.rs b/src/detections/rule/matchers.rs index 60cc20331..0557be7c1 100644 --- a/src/detections/rule/matchers.rs +++ b/src/detections/rule/matchers.rs @@ -1044,7 +1044,7 @@ mod tests { }; use crate::detections::configs::{ Action, CommonOptions, Config, CsvOutputOption, DetectCommonOption, InputOption, - OutputOption, StoredStatic, STORED_EKEY_ALIAS, + OutputOption, StoredStatic, TimeFormatOptions, STORED_EKEY_ALIAS, }; use crate::detections::rule::matchers::FastMatch; use crate::detections::rule::tests::parse_rule_from_str; @@ -1071,13 +1071,15 @@ mod tests { 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, + time_format_options: TimeFormatOptions { + 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, diff --git a/src/detections/rule/mod.rs b/src/detections/rule/mod.rs index b8d3aff26..601bcd994 100644 --- a/src/detections/rule/mod.rs +++ b/src/detections/rule/mod.rs @@ -409,6 +409,8 @@ mod tests { use yaml_rust2::YamlLoader; + use super::RuleNode; + use crate::detections::configs::TimeFormatOptions; use crate::detections::{ self, configs::{ @@ -419,8 +421,6 @@ mod tests { utils, }; - use super::RuleNode; - fn create_dummy_stored_static() -> StoredStatic { StoredStatic::create_static_data(Some(Config { action: Some(Action::CsvTimeline(CsvOutputOption { @@ -441,13 +441,15 @@ mod tests { 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, + time_format_options: TimeFormatOptions { + 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, diff --git a/src/detections/rule/selectionnodes.rs b/src/detections/rule/selectionnodes.rs index 18ecf6f54..f964f5239 100644 --- a/src/detections/rule/selectionnodes.rs +++ b/src/detections/rule/selectionnodes.rs @@ -509,6 +509,7 @@ impl SelectionNode for LeafSelectionNode { mod tests { use std::path::Path; + use crate::detections::configs::TimeFormatOptions; use crate::detections::{ self, configs::{ @@ -539,13 +540,15 @@ mod tests { 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, + time_format_options: TimeFormatOptions { + 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, diff --git a/src/detections/utils.rs b/src/detections/utils.rs index 9199aba90..1878c9eb5 100644 --- a/src/detections/utils.rs +++ b/src/detections/utils.rs @@ -26,7 +26,7 @@ use termcolor::{BufferWriter, ColorSpec, WriteColor}; use termcolor::{Color, ColorChoice}; use tokio::runtime::{Builder, Runtime}; -use crate::detections::configs::{CURRENT_EXE_PATH, ONE_CONFIG_MAP}; +use crate::detections::configs::{TimeFormatOptions, CURRENT_EXE_PATH, ONE_CONFIG_MAP}; use crate::detections::field_data_map::{convert_field_data, FieldDataMap, FieldDataMapKey}; use crate::detections::field_extract::extract_fields; use crate::options::htmlreport; @@ -628,7 +628,7 @@ pub fn check_rule_config(config_path: &PathBuf) -> Result<(), String> { pub fn format_time( time: &DateTime, date_only: bool, - output_option: &OutputOption, + output_option: &TimeFormatOptions, ) -> CompactString { if !(output_option.utc || output_option.iso_8601) { format_rfc(&time.with_timezone(&Local), date_only, output_option) @@ -641,7 +641,7 @@ pub fn format_time( fn format_rfc( time: &DateTime, date_only: bool, - time_args: &OutputOption, + time_args: &TimeFormatOptions, ) -> CompactString where Tz::Offset: std::fmt::Display, @@ -820,6 +820,8 @@ mod tests { use regex::Regex; use serde_json::Value; + use super::{output_duration, output_profile_name}; + use crate::detections::configs::TimeFormatOptions; use crate::detections::field_data_map::FieldDataMapKey; use crate::{ detections::{ @@ -832,8 +834,6 @@ mod tests { options::htmlreport::HTML_REPORTER, }; - use super::{output_duration, output_profile_name}; - #[test] fn test_create_recordinfos() { let record_json_str = r#" @@ -1121,13 +1121,15 @@ mod tests { 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, + time_format_options: TimeFormatOptions { + 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: Some(Path::new("dummy.html").to_path_buf()), diff --git a/src/main.rs b/src/main.rs index 997c08882..366507845 100644 --- a/src/main.rs +++ b/src/main.rs @@ -395,7 +395,10 @@ impl App { } println!(); } - Action::EidMetrics(_) | Action::Search(_) => { + Action::EidMetrics(_) + | Action::ComputerMetrics(_) + | Action::LogMetrics(_) + | Action::Search(_) => { if let Some(path) = &stored_static.output_path { if !(stored_static.output_option.as_ref().unwrap().clobber) && utils::check_file_expect_not_exist( @@ -416,28 +419,6 @@ impl App { &stored_static.html_report_flag, ); } - Action::ComputerMetrics(_) => { - if let Some(path) = &stored_static.output_path { - if !(stored_static.output_option.as_ref().unwrap().clobber) - && utils::check_file_expect_not_exist( - path.as_path(), - format!( - " The file {} already exists. Please specify a different filename or add the -C, --clobber option to overwrite.\n", - path.as_os_str().to_str().unwrap() - ), - ) - { - return; - } - } - self.analysis_start(&target_extensions, &time_filter, stored_static); - output_saved_file( - &stored_static.output_path, - "Saved results", - &stored_static.html_report_flag, - ); - println!(); - } Action::PivotKeywordsList(_) => { load_pivot_keywords( utils::check_setting_path( @@ -1103,6 +1084,7 @@ impl App { || stored_static.logon_summary_flag || stored_static.search_flag || stored_static.computer_metrics_flag + || stored_static.log_metrics_flag || stored_static.output_option.as_ref().unwrap().no_wizard) { CHECKPOINT @@ -1471,7 +1453,8 @@ impl App { if !(stored_static.logon_summary_flag || stored_static.search_flag || stored_static.metrics_flag - || stored_static.computer_metrics_flag) + || stored_static.computer_metrics_flag + || stored_static.log_metrics_flag) { println!("Loading detection rules. Please wait."); } else if stored_static.logon_summary_flag { @@ -1482,6 +1465,8 @@ impl App { println!("Currently scanning for event ID metrics. Please wait."); } else if stored_static.computer_metrics_flag { println!("Currently scanning for computer metrics. Please wait."); + } else if stored_static.log_metrics_flag { + println!("Currently scanning for log metrics. Please wait."); } println!(); @@ -1489,7 +1474,8 @@ impl App { if !(stored_static.logon_summary_flag || stored_static.search_flag || stored_static.metrics_flag - || stored_static.computer_metrics_flag) + || stored_static.computer_metrics_flag + || stored_static.log_metrics_flag) { rule_files = detection::Detection::parse_rule_files( &level, @@ -1511,7 +1497,8 @@ impl App { let unused_rules_option = stored_static.logon_summary_flag || stored_static.search_flag || stored_static.computer_metrics_flag - || stored_static.metrics_flag; + || stored_static.metrics_flag + || stored_static.log_metrics_flag; if !unused_rules_option && rule_files.is_empty() { AlertMessage::alert( "No rules were loaded. Please download the latest rules with the update-rules command.\r\n", @@ -1647,12 +1634,15 @@ impl App { tl.search_dsp_msg(event_timeline_config, stored_static); } else if stored_static.computer_metrics_flag { tl.computer_metrics_dsp_msg(stored_static) + } else if stored_static.log_metrics_flag { + tl.log_metrics_dsp_msg(stored_static) } if !(stored_static.metrics_flag || stored_static.logon_summary_flag || stored_static.search_flag || stored_static.pivot_keyword_list_flag - || stored_static.computer_metrics_flag) + || stored_static.computer_metrics_flag + || stored_static.log_metrics_flag) { let mut log_records = detection.add_aggcondition_msges(&self.rt, stored_static); if stored_static.is_low_memory { @@ -2178,6 +2168,7 @@ impl App { // 以下のコマンドの際にはルールにかけない if !(stored_static.metrics_flag || stored_static.logon_summary_flag + || stored_static.log_metrics_flag || stored_static.search_flag) { // ruleファイルの検知 @@ -2424,6 +2415,8 @@ mod tests { use itertools::Itertools; use yaml_rust2::YamlLoader; + use crate::App; + use hayabusa::detections::configs::TimeFormatOptions; use hayabusa::{ afterfact::{self, AfterfactInfo}, detections::{ @@ -2440,8 +2433,6 @@ mod tests { timeline::timelines::Timeline, }; - use crate::App; - fn create_dummy_stored_static() -> StoredStatic { StoredStatic::create_static_data(Some(Config { action: Some(Action::CsvTimeline(CsvOutputOption { @@ -2462,13 +2453,15 @@ mod tests { 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, + time_format_options: TimeFormatOptions { + 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, @@ -2636,13 +2629,15 @@ mod tests { 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, + time_format_options: TimeFormatOptions { + 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("./test_files/rules/yaml/test_json_detect.yml").to_path_buf(), html_report: None, @@ -2724,13 +2719,15 @@ mod tests { 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, + time_format_options: TimeFormatOptions { + 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("test_files/rules/yaml/test_json_detect.yml").to_path_buf(), html_report: None, @@ -2811,13 +2808,15 @@ mod tests { 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, + time_format_options: TimeFormatOptions { + 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("./test_files/rules/yaml/test_json_detect.yml").to_path_buf(), html_report: None, @@ -2899,13 +2898,15 @@ mod tests { 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, + time_format_options: TimeFormatOptions { + 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("test_files/rules/yaml/test_json_detect.yml").to_path_buf(), html_report: None, @@ -2992,13 +2993,15 @@ mod tests { include_computer: None, exclude_computer: None, }, - european_time: false, - iso_8601: false, - rfc_2822: false, - rfc_3339: false, - us_military_time: false, - us_time: false, - utc: false, + time_format_options: TimeFormatOptions { + european_time: false, + iso_8601: false, + rfc_2822: false, + rfc_3339: false, + us_military_time: false, + us_time: false, + utc: false, + }, clobber: false, }); let config = Some(Config { @@ -3047,13 +3050,15 @@ mod tests { include_computer: None, exclude_computer: None, }, - european_time: false, - iso_8601: false, - rfc_2822: false, - rfc_3339: false, - us_military_time: false, - us_time: false, - utc: false, + time_format_options: TimeFormatOptions { + european_time: false, + iso_8601: false, + rfc_2822: false, + rfc_3339: false, + us_military_time: false, + us_time: false, + utc: false, + }, clobber: true, }); let config = Some(Config { @@ -3100,13 +3105,15 @@ mod tests { include_computer: None, exclude_computer: None, }, - european_time: false, - iso_8601: false, - rfc_2822: false, - rfc_3339: false, - us_military_time: false, - us_time: false, - utc: false, + time_format_options: TimeFormatOptions { + european_time: false, + iso_8601: false, + rfc_2822: false, + rfc_3339: false, + us_military_time: false, + us_time: false, + utc: false, + }, clobber: false, end_timeline: None, start_timeline: None, @@ -3156,13 +3163,15 @@ mod tests { include_computer: None, exclude_computer: None, }, - european_time: false, - iso_8601: false, - rfc_2822: false, - rfc_3339: false, - us_military_time: false, - us_time: false, - utc: false, + time_format_options: TimeFormatOptions { + european_time: false, + iso_8601: false, + rfc_2822: false, + rfc_3339: false, + us_military_time: false, + us_time: false, + utc: false, + }, clobber: true, end_timeline: None, start_timeline: None, diff --git a/src/options/htmlreport.rs b/src/options/htmlreport.rs index 136b62f66..b1643f67e 100644 --- a/src/options/htmlreport.rs +++ b/src/options/htmlreport.rs @@ -188,6 +188,8 @@ mod tests { use nested::Nested; + use super::{img_to_base64, HTML_REPORTER}; + use crate::detections::configs::TimeFormatOptions; use crate::{ detections::configs::{ Action, CommonOptions, Config, CsvOutputOption, DetectCommonOption, InputOption, @@ -196,8 +198,6 @@ mod tests { options::htmlreport::{self, HtmlReporter}, }; - use super::{img_to_base64, HTML_REPORTER}; - fn create_dummy_stored_static(action: Option) -> StoredStatic { StoredStatic::create_static_data(Some(Config { action, @@ -268,13 +268,15 @@ mod tests { 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, + time_format_options: TimeFormatOptions { + 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: Some(Path::new("./dummy").to_path_buf()), @@ -338,13 +340,15 @@ mod tests { 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, + time_format_options: TimeFormatOptions { + 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, @@ -411,13 +415,15 @@ mod tests { 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, + time_format_options: TimeFormatOptions { + 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: Some(Path::new("./dummy").to_path_buf()), @@ -481,13 +487,15 @@ mod tests { 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, + time_format_options: TimeFormatOptions { + 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, diff --git a/src/options/profile.rs b/src/options/profile.rs index a81bd7fa4..5078619aa 100644 --- a/src/options/profile.rs +++ b/src/options/profile.rs @@ -399,7 +399,7 @@ mod tests { use crate::detections::configs::{ Action, CommonOptions, Config, CsvOutputOption, DetectCommonOption, InputOption, - OutputOption, StoredStatic, GEOIP_DB_PARSER, + OutputOption, StoredStatic, TimeFormatOptions, GEOIP_DB_PARSER, }; use crate::options::profile::{get_profile_list, load_profile, Profile}; use compact_str::CompactString; @@ -511,13 +511,15 @@ mod tests { 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, + time_format_options: TimeFormatOptions { + 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, @@ -592,13 +594,15 @@ mod tests { 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, + time_format_options: TimeFormatOptions { + 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, @@ -703,13 +707,15 @@ mod tests { 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, + time_format_options: TimeFormatOptions { + 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, diff --git a/src/timeline/log_metrics.rs b/src/timeline/log_metrics.rs new file mode 100644 index 000000000..ff78aa9a2 --- /dev/null +++ b/src/timeline/log_metrics.rs @@ -0,0 +1,58 @@ +use crate::detections::configs::StoredStatic; +use crate::detections::detection::EvtxRecordInfo; +use crate::detections::utils; +use chrono::{DateTime, Utc}; +use std::collections::HashSet; + +#[derive(Default, Debug, Clone)] +pub struct LogMetrics { + pub filename: String, + pub computers: HashSet, + pub event_count: usize, + pub first_timestamp: Option>, + pub last_timestamp: Option>, + pub channels: HashSet, + pub providers: HashSet, +} + +impl LogMetrics { + pub fn new(filename: &str) -> Self { + Self { + filename: filename.to_string(), + ..Default::default() + } + } + pub fn update( + &mut self, + records: &[EvtxRecordInfo], + stored_static: &StoredStatic, + start_time: Option>, + end_time: Option>, + ) { + for record in records { + if let Some(computer) = + utils::get_event_value("Computer", &record.record, &stored_static.eventkey_alias) + { + self.computers + .insert(computer.to_string().trim_matches('"').to_string()); + } + if let Some(channel) = + utils::get_event_value("Channel", &record.record, &stored_static.eventkey_alias) + { + self.channels + .insert(channel.to_string().trim_matches('"').to_string()); + } + if let Some(provider) = utils::get_event_value( + "ProviderName", + &record.record, + &stored_static.eventkey_alias, + ) { + self.providers + .insert(provider.to_string().trim_matches('"').to_string()); + } + self.event_count += 1; + } + self.first_timestamp = start_time; + self.last_timestamp = end_time; + } +} diff --git a/src/timeline/metrics.rs b/src/timeline/metrics.rs index cccd8b0e3..4f1a325c8 100644 --- a/src/timeline/metrics.rs +++ b/src/timeline/metrics.rs @@ -6,10 +6,12 @@ use crate::detections::{ message::AlertMessage, utils, }; +use crate::timeline::log_metrics::LogMetrics; use crate::timeline::metrics::Channel::{RdsGtw, RdsLsm, Sec}; use chrono::{DateTime, NaiveDate, NaiveDateTime, Utc}; use compact_str::CompactString; use hashbrown::{HashMap, HashSet}; +use std::path::Path; #[derive(Debug, Clone, Eq, Hash, PartialEq)] pub struct LoginEvent { @@ -32,6 +34,7 @@ pub struct EventMetrics { pub end_time: Option>, pub stats_list: HashMap<(CompactString, CompactString), usize>, pub stats_login_list: HashMap, + pub stats_logfile: Vec, } /** * Windows Event Logの統計情報を出力する @@ -52,6 +55,7 @@ impl EventMetrics { end_time, stats_list, stats_login_list, + stats_logfile: Vec::new(), } } @@ -78,12 +82,45 @@ impl EventMetrics { if !stored_static.logon_summary_flag { return; } - self.stats_time_cnt(records, stored_static); - self.stats_login_eventid(records, stored_static); } + pub fn logfile_stats_start( + &mut self, + records: &[EvtxRecordInfo], + stored_static: &StoredStatic, + ) { + if !stored_static.log_metrics_flag { + return; + } + + self.stats_time_cnt(records, stored_static); + let filename = Path::new(self.filepath.as_str()) + .file_name() + .unwrap_or_default() + .to_str() + .unwrap_or_default(); + if let Some(existing_lm) = self.stats_logfile.iter_mut().find(|lm| { + lm.filename == filename + && lm.computers.contains( + get_event_value_as_string( + "Computer", + &records[0].record, + &stored_static.eventkey_alias, + ) + .to_string() + .trim_matches('"'), + ) + }) { + existing_lm.update(records, stored_static, self.start_time, self.end_time); + } else { + let mut lm = LogMetrics::new(filename); + lm.update(records, stored_static, self.start_time, self.end_time); + self.stats_logfile.push(lm); + } + } + fn stats_time_cnt(&mut self, records: &[EvtxRecordInfo], stored_static: &StoredStatic) { if records.is_empty() { return; @@ -440,6 +477,7 @@ mod tests { use hashbrown::{HashMap, HashSet}; use nested::Nested; + use crate::detections::configs::TimeFormatOptions; use crate::{ detections::{ configs::{ @@ -485,13 +523,15 @@ mod tests { include_computer: None, exclude_computer: None, }, - european_time: false, - iso_8601: false, - rfc_2822: false, - rfc_3339: false, - us_military_time: false, - us_time: false, - utc: false, + time_format_options: TimeFormatOptions { + european_time: false, + iso_8601: false, + rfc_2822: false, + rfc_3339: false, + us_military_time: false, + us_time: false, + utc: false, + }, output: None, clobber: false, })); diff --git a/src/timeline/mod.rs b/src/timeline/mod.rs index 33f7c1341..fae221ec4 100644 --- a/src/timeline/mod.rs +++ b/src/timeline/mod.rs @@ -1,4 +1,5 @@ pub mod computer_metrics; +mod log_metrics; pub mod metrics; pub mod search; pub mod timelines; diff --git a/src/timeline/search.rs b/src/timeline/search.rs index 603746414..03be87112 100644 --- a/src/timeline/search.rs +++ b/src/timeline/search.rs @@ -284,7 +284,11 @@ fn extract_search_event_info( let default_time = Utc.with_ymd_and_hms(1970, 1, 1, 0, 0, 0).unwrap(); let timestamp_datetime = message::get_event_time(&record.record, false).unwrap_or(default_time); - let timestamp = format_time(×tamp_datetime, false, output_option); + let timestamp = format_time( + ×tamp_datetime, + false, + &output_option.time_format_options, + ); let hostname = CompactString::from( utils::get_serde_number_to_string( diff --git a/src/timeline/timelines.rs b/src/timeline/timelines.rs index 7edcaf6c8..2dc6d7a49 100644 --- a/src/timeline/timelines.rs +++ b/src/timeline/timelines.rs @@ -1,8 +1,3 @@ -use std::cmp; -use std::fs::File; -use std::io::BufWriter; -use std::path::PathBuf; - use crate::detections::configs::{Action, EventInfoConfig, StoredStatic}; use crate::detections::detection::EvtxRecordInfo; use crate::detections::message::AlertMessage; @@ -19,6 +14,10 @@ use csv::WriterBuilder; use downcast_rs::__std::process; use nested::Nested; use num_format::{Locale, ToFormattedString}; +use std::cmp; +use std::fs::File; +use std::io::BufWriter; +use std::path::PathBuf; use termcolor::{BufferWriter, Color, ColorChoice}; use terminal_size::terminal_size; use terminal_size::Width; @@ -26,7 +25,9 @@ use terminal_size::Width; use super::computer_metrics; use super::metrics::EventMetrics; use super::search::EventSearch; +use crate::timeline::log_metrics::LogMetrics; use hashbrown::{HashMap, HashSet}; +use itertools::Itertools; #[derive(Debug, Clone)] pub struct Timeline { @@ -77,6 +78,8 @@ impl Timeline { ); } else if stored_static.logon_summary_flag { self.stats.logon_stats_start(records, stored_static); + } else if stored_static.log_metrics_flag { + self.stats.logfile_stats_start(records, stored_static); } else if stored_static.search_flag { self.event_search.search_start( records, @@ -122,7 +125,11 @@ impl Timeline { utils::format_time( &self.stats.start_time.unwrap(), false, - stored_static.output_option.as_ref().unwrap() + &stored_static + .output_option + .as_ref() + .unwrap() + .time_format_options ) )); } @@ -132,7 +139,11 @@ impl Timeline { utils::format_time( &self.stats.end_time.unwrap(), false, - stored_static.output_option.as_ref().unwrap() + &stored_static + .output_option + .as_ref() + .unwrap() + .time_format_options ) )); } @@ -235,7 +246,11 @@ impl Timeline { utils::format_time( &self.stats.start_time.unwrap(), false, - stored_static.output_option.as_ref().unwrap() + &stored_static + .output_option + .as_ref() + .unwrap() + .time_format_options ) )); } @@ -245,7 +260,11 @@ impl Timeline { utils::format_time( &self.stats.end_time.unwrap(), false, - stored_static.output_option.as_ref().unwrap() + &stored_static + .output_option + .as_ref() + .unwrap() + .time_format_options ) )); } @@ -275,14 +294,7 @@ impl Timeline { // イベント情報取得(eventtitleなど) // channel_eid_info.txtに登録あるものは情報設定 // 出力メッセージ1行作成 - let ch = stored_static.disp_abbr_generic.replace_all( - stored_static - .ch_config - .get(fmted_channel.as_str()) - .unwrap_or(fmted_channel) - .as_str(), - &stored_static.disp_abbr_general_values, - ); + let ch = replace_channel_abbr(stored_static, fmted_channel); if event_timeline_config .get_event_id(fmted_channel, event_id) @@ -496,6 +508,144 @@ impl Timeline { } } } + + pub fn log_metrics_dsp_msg(&mut self, stored_static: &StoredStatic) { + if let Action::LogMetrics(opt) = &stored_static.config.action.as_ref().unwrap() { + let log_metrics = &mut self.stats.stats_logfile; + log_metrics.sort_by(|a, b| a.event_count.cmp(&b.event_count).reverse()); + let header = vec![ + "Filename", + "Computers", + "Events", + "First Timestamp", + "Last Timestamp", + "Channels", + "Providers", + ]; + if let Some(path) = &opt.output { + let file = File::create(path).expect("Failed to create output file"); + let mut wrt = WriterBuilder::new().from_writer(file); + let _ = wrt.write_record(header); + for rec in &mut *log_metrics { + if let Some(r) = Self::create_record_array(rec, stored_static, " ¦") { + let _ = wrt.write_record(r); + } + } + } else { + let mut tb = Table::new(); + tb.load_preset(UTF8_FULL) + .apply_modifier(UTF8_ROUND_CORNERS) + .set_content_arrangement(ContentArrangement::DynamicFullWidth) + .set_header(&header); + for rec in &mut *log_metrics { + if let Some(r) = Self::create_record_array(rec, stored_static, "\n") { + tb.add_row(vec![ + Cell::new(r[0].to_string()), + Cell::new(r[1].to_string()), + Cell::new(r[2].to_string()), + Cell::new(r[3].to_string()), + Cell::new(r[4].to_string()), + Cell::new(r[5].to_string()), + Cell::new(r[6].to_string()), + ]); + } + } + if log_metrics.is_empty() { + println!("No matches found."); + } else { + println!("{tb}"); + } + } + } + } + + fn create_record_array( + rec: &LogMetrics, + stored_static: &StoredStatic, + sep: &str, + ) -> Option<[String; 7]> { + let include_computer = &stored_static.include_computer; + let exclude_computer = &stored_static.exclude_computer; + if !include_computer.is_empty() + && rec + .computers + .iter() + .all(|comp| !include_computer.contains(&CompactString::from(comp))) + { + return None; + } + if !exclude_computer.is_empty() + && rec + .computers + .iter() + .any(|comp| exclude_computer.contains(&CompactString::from(comp))) + { + return None; + } + let sep = if stored_static.multiline_flag { + "\n" + } else { + sep + }; + let ab_ch: Vec = rec + .channels + .iter() + .map(|ch| replace_channel_abbr(stored_static, &CompactString::from(ch))) + .collect(); + let ab_provider: Vec = rec + .providers + .iter() + .map(|ch| replace_provider_abbr(stored_static, &CompactString::from(ch))) + .collect(); + Some([ + rec.filename.to_string(), + rec.computers.iter().sorted().join(sep), + rec.event_count.to_formatted_string(&Locale::en), + utils::format_time( + &rec.first_timestamp.unwrap_or_default(), + false, + &stored_static + .output_option + .as_ref() + .unwrap() + .time_format_options, + ) + .into(), + utils::format_time( + &rec.last_timestamp.unwrap_or_default(), + false, + &stored_static + .output_option + .as_ref() + .unwrap() + .time_format_options, + ) + .into(), + ab_ch.iter().sorted().join(sep), + ab_provider.iter().sorted().join(sep), + ]) + } +} + +fn replace_channel_abbr(stored_static: &StoredStatic, fmted_channel: &CompactString) -> String { + stored_static.disp_abbr_generic.replace_all( + stored_static + .ch_config + .get(&fmted_channel.to_ascii_lowercase()) + .unwrap_or(fmted_channel) + .as_str(), + &stored_static.disp_abbr_general_values, + ) +} + +fn replace_provider_abbr(stored_static: &StoredStatic, fmted_provider: &CompactString) -> String { + stored_static.disp_abbr_generic.replace_all( + stored_static + .provider_abbr_config + .get(fmted_provider) + .unwrap_or(fmted_provider), + &stored_static.disp_abbr_general_values, + ) } #[cfg(test)] @@ -510,6 +660,7 @@ mod tests { use hashbrown::{HashMap, HashSet}; use nested::Nested; + use crate::detections::configs::TimeFormatOptions; use crate::timeline::metrics::LoginEvent; use crate::{ detections::{ @@ -556,13 +707,15 @@ mod tests { include_computer: None, exclude_computer: None, }, - european_time: false, - iso_8601: false, - rfc_2822: false, - rfc_3339: false, - us_military_time: false, - us_time: false, - utc: false, + time_format_options: TimeFormatOptions { + european_time: false, + iso_8601: false, + rfc_2822: false, + rfc_3339: false, + us_military_time: false, + us_time: false, + utc: false, + }, output: None, clobber: false, end_timeline: None, @@ -742,13 +895,15 @@ mod tests { include_computer: None, exclude_computer: None, }, - european_time: false, - iso_8601: false, - rfc_2822: false, - rfc_3339: false, - us_military_time: false, - us_time: false, - utc: false, + time_format_options: TimeFormatOptions { + european_time: false, + iso_8601: false, + rfc_2822: false, + rfc_3339: false, + us_military_time: false, + us_time: false, + utc: false, + }, output: Some(Path::new("./test_tm_stats.csv").to_path_buf()), clobber: false, })); @@ -833,13 +988,15 @@ mod tests { include_computer: None, exclude_computer: None, }, - european_time: false, - iso_8601: false, - rfc_2822: false, - rfc_3339: false, - us_military_time: false, - us_time: false, - utc: false, + time_format_options: TimeFormatOptions { + european_time: false, + iso_8601: false, + rfc_2822: false, + rfc_3339: false, + us_military_time: false, + us_time: false, + utc: false, + }, output: Some(Path::new("./test_tm_logon_stats").to_path_buf()), clobber: false, end_timeline: None, diff --git a/src/yaml.rs b/src/yaml.rs index 3e41a0791..cb287feea 100644 --- a/src/yaml.rs +++ b/src/yaml.rs @@ -770,7 +770,6 @@ pub fn count_rules>( #[cfg(test)] mod tests { - use crate::detections::configs::Action; use crate::detections::configs::CommonOptions; use crate::detections::configs::Config; use crate::detections::configs::CsvOutputOption; @@ -778,6 +777,7 @@ mod tests { use crate::detections::configs::InputOption; use crate::detections::configs::OutputOption; use crate::detections::configs::StoredStatic; + use crate::detections::configs::{Action, TimeFormatOptions}; use crate::filter; use crate::yaml; use crate::yaml::ParseYaml; @@ -810,13 +810,15 @@ mod tests { 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, + time_format_options: TimeFormatOptions { + 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,