diff --git a/CHANGELOG-Japanese.md b/CHANGELOG-Japanese.md index c6376e94f..e34ba41cd 100644 --- a/CHANGELOG-Japanese.md +++ b/CHANGELOG-Japanese.md @@ -4,7 +4,9 @@ **改善:** -xxx +- 初心者のユーザのために有効にしたいルールを選択するようにスキャンウィザードを追加した。`-w, --no-wizard`オプションを追加すると、従来の形式でHayabusaを実行できる。(すべてのイベントとアラートをスキャンし、オプションを手動でカスタマイズする) (#1188) (@hitenkoku) +- `pivot-keywords-list`コマンドに`--include-tag`オプションを追加し、指定した`tags`フィールドを持つルールのみをロードするようにした。(#1195) (@hitenkoku) +- `pivot-keywords-list`コマンドに`--exclude-tag`オプションを追加し、指定した`tags`フィールドを持つルールをロードしないようにした。(#1195) (@hitenkoku) **バグ修正:** diff --git a/CHANGELOG.md b/CHANGELOG.md index 393ae849d..e879ed5d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,9 @@ **Enhancements:** -xxx +- Added a scan wizard to help new users choose which rules they want to enable. Add the `-w, --no-wizard` option to run Hayabusa in the traditional way. (Scan for all events and alerts, and customize options manually.) (#1188) (@hitenkoku) +- Added the `--include-tag` option to the `pivot-keywords-list` command to only load rules with the specified `tags` field. (#1195) (@hitenkoku) +- Added the `--exclude-tag` option to the `pivot-keywords-list` command to exclude rules with specific `tags` from being loaded. (#1195) (@hitenkoku) **Bug Fixes:** diff --git a/Cargo.lock b/Cargo.lock index 65c591f86..3b26d36e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -802,6 +802,7 @@ dependencies = [ "crossbeam-utils", "csv", "dashmap", + "dialoguer", "downcast-rs", "evtx", "flate2", diff --git a/Cargo.toml b/Cargo.toml index 5733bfd12..08aaa2409 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,6 +52,7 @@ aho-corasick = "*" memchr = "2.*" num = "0.4.0" indexmap = "2.*" +dialoguer = "*" [profile.dev] debug = 0 diff --git a/src/afterfact.rs b/src/afterfact.rs index 6008fcbd9..0e7995b78 100644 --- a/src/afterfact.rs +++ b/src/afterfact.rs @@ -629,7 +629,7 @@ fn emit_csv( ) ), "Results Summary {#results_summary}", - stored_static.html_report_flag, + &stored_static.html_report_flag, ); } if tl_start_end_time.1.is_some() { @@ -643,7 +643,7 @@ fn emit_csv( ) ), "Results Summary {#results_summary}", - stored_static.html_report_flag, + &stored_static.html_report_flag, ); println!(); } @@ -1903,6 +1903,7 @@ mod tests { no_field: false, remove_duplicate_data: false, remove_duplicate_detections: false, + no_wizard: true, }, geo_ip: None, output: Some(Path::new("./test_emit_csv.csv").to_path_buf()), @@ -1991,6 +1992,7 @@ mod tests { no_field: false, remove_duplicate_data: false, remove_duplicate_detections: false, + no_wizard: true, }; let ch = mock_ch_filter .get(&CompactString::from("security")) @@ -2229,6 +2231,7 @@ mod tests { no_field: false, remove_duplicate_data: false, remove_duplicate_detections: false, + no_wizard: true, }, geo_ip: None, output: Some(Path::new("./test_emit_csv_multiline.csv").to_path_buf()), @@ -2319,6 +2322,7 @@ mod tests { no_field: false, remove_duplicate_data: false, remove_duplicate_detections: false, + no_wizard: true, }; let ch = mock_ch_filter .get(&CompactString::from("security")) @@ -2543,6 +2547,7 @@ mod tests { no_field: false, remove_duplicate_data: true, remove_duplicate_detections: false, + no_wizard: true, }, geo_ip: None, output: Some(Path::new("./test_emit_csv_remove_duplicate.csv").to_path_buf()), @@ -2631,6 +2636,7 @@ mod tests { no_field: false, remove_duplicate_data: false, remove_duplicate_detections: false, + no_wizard: true, }; let ch = mock_ch_filter .get(&CompactString::from("security")) @@ -2866,6 +2872,7 @@ mod tests { no_field: false, remove_duplicate_data: true, remove_duplicate_detections: false, + no_wizard: true, }, geo_ip: None, output: Some(Path::new("./test_emit_csv_remove_duplicate.json").to_path_buf()), @@ -2954,6 +2961,7 @@ mod tests { no_field: false, remove_duplicate_data: true, remove_duplicate_detections: false, + no_wizard: true, }; let ch = mock_ch_filter .get(&CompactString::from("security")) @@ -3274,6 +3282,7 @@ mod tests { no_field: false, remove_duplicate_data: false, remove_duplicate_detections: false, + no_wizard: true, }; let data: Vec<(CompactString, Profile)> = vec![ ( @@ -3412,6 +3421,7 @@ mod tests { no_field: false, remove_duplicate_data: false, remove_duplicate_detections: false, + no_wizard: true, }, geo_ip: None, output: Some(Path::new("./test_emit_csv_json.json").to_path_buf()), @@ -3499,6 +3509,7 @@ mod tests { no_field: false, remove_duplicate_data: false, remove_duplicate_detections: false, + no_wizard: true, }; let ch = mock_ch_filter .get(&CompactString::from("security")) @@ -3675,6 +3686,7 @@ mod tests { no_field: false, remove_duplicate_data: false, remove_duplicate_detections: false, + no_wizard: true, }, geo_ip: None, output: Some(Path::new("./test_emit_csv_jsonl.jsonl").to_path_buf()), @@ -3762,6 +3774,7 @@ mod tests { no_field: false, remove_duplicate_data: false, remove_duplicate_detections: false, + no_wizard: true, }; let ch = mock_ch_filter .get(&CompactString::from("security")) diff --git a/src/detections/configs.rs b/src/detections/configs.rs index 366db0054..a3b4b5eff 100644 --- a/src/detections/configs.rs +++ b/src/detections/configs.rs @@ -81,6 +81,7 @@ pub struct StoredStatic { pub exclude_computer: HashSet, pub include_eid: HashSet, pub exclude_eid: HashSet, + pub include_status: HashSet, // 読み込み対象ルールのステータスのセット。*はすべてのステータスを読み込む pub field_data_map: Option, pub enable_recover_records: bool, pub timeline_offset: Option, @@ -630,6 +631,7 @@ impl StoredStatic { field_data_map, enable_recover_records, timeline_offset, + include_status: HashSet::new(), }; ret.profiles = load_profile( check_setting_path( @@ -1191,9 +1193,17 @@ pub struct PivotKeywordOption { pub enable_unsupported_rules: bool, /// Do not load rules according to status (ex: experimental) (ex: stable,test) - #[arg(help_heading = Some("Filtering"), long = "exclude-status", value_name = "STATUS...", use_value_delimiter = true, value_delimiter = ',', display_order = 316)] + #[arg(help_heading = Some("Filtering"), long = "exclude-status", value_name = "STATUS...", requires="no_wizard", use_value_delimiter = true, value_delimiter = ',', display_order = 316)] pub exclude_status: Option>, + /// Only load rules with specific tags (ex: attack.execution,attack.discovery) + #[arg(help_heading = Some("Filtering"), long = "include-tag", value_name = "TAG...", requires="no_wizard", conflicts_with = "exclude_tag", use_value_delimiter = true, value_delimiter = ',', display_order = 353)] + pub include_tag: Option>, + + /// Do not load rules with specific tags (ex: sysmon) + #[arg(help_heading = Some("Filtering"), long = "exclude-tag", value_name = "TAG...", requires="no_wizard", conflicts_with = "include_tag", use_value_delimiter = true, value_delimiter = ',', display_order = 316)] + pub exclude_tag: Option>, + /// Minimum level for rules to load (default: informational) #[arg( help_heading = Some("Filtering"), @@ -1202,6 +1212,7 @@ pub struct PivotKeywordOption { default_value = "informational", hide_default_value = true, value_name = "LEVEL", + requires="no_wizard", conflicts_with = "exact_level", display_order = 390 )] @@ -1213,6 +1224,7 @@ pub struct PivotKeywordOption { short = 'e', long = "exact-level", value_name = "LEVEL", + requires="no_wizard", conflicts_with = "min_level", display_order = 313 )] @@ -1248,6 +1260,10 @@ pub struct PivotKeywordOption { /// Overwrite files when saving #[arg(help_heading = Some("General Options"), short='C', long = "clobber", display_order = 290, requires = "output")] pub clobber: bool, + + /// Do not ask questions. Scan for all events and alerts. + #[arg(help_heading = Some("General Options"), short = 'w', long = "no-wizard", display_order = 400)] + pub no_wizard: bool, } #[derive(Args, Clone, Debug)] @@ -1329,11 +1345,11 @@ pub struct OutputOption { pub enable_unsupported_rules: bool, /// Do not load rules according to status (ex: experimental) (ex: stable,test) - #[arg(help_heading = Some("Filtering"), long = "exclude-status", value_name = "STATUS...", use_value_delimiter = true, value_delimiter = ',', display_order = 316)] + #[arg(help_heading = Some("Filtering"), long = "exclude-status", value_name = "STATUS...", requires="no_wizard", use_value_delimiter = true, value_delimiter = ',', display_order = 316)] pub exclude_status: Option>, /// Only load rules with specific tags (ex: attack.execution,attack.discovery) - #[arg(help_heading = Some("Filtering"), long = "include-tag", value_name = "TAG...", conflicts_with = "exclude_tag", use_value_delimiter = true, value_delimiter = ',', display_order = 353)] + #[arg(help_heading = Some("Filtering"), long = "include-tag", value_name = "TAG...", requires="no_wizard", conflicts_with = "exclude_tag", use_value_delimiter = true, value_delimiter = ',', display_order = 353)] pub include_tag: Option>, /// Only load rules with specified logsource categories (ex: process_creation,pipe_created) @@ -1350,6 +1366,7 @@ pub struct OutputOption { short = 'm', long = "min-level", default_value = "informational", + requires="no_wizard", hide_default_value = true, value_name = "LEVEL", display_order = 390, @@ -1362,6 +1379,7 @@ pub struct OutputOption { short = 'e', long = "exact-level", value_name = "LEVEL", + requires="no_wizard", conflicts_with = "min-level", display_order = 313 )] @@ -1388,7 +1406,7 @@ pub struct OutputOption { pub proven_rules: bool, /// Do not load rules with specific tags (ex: sysmon) - #[arg(help_heading = Some("Filtering"), long = "exclude-tag", value_name = "TAG...", conflicts_with = "include_tag", use_value_delimiter = true, value_delimiter = ',', display_order = 316)] + #[arg(help_heading = Some("Filtering"), long = "exclude-tag", value_name = "TAG...", requires="no_wizard", conflicts_with = "include_tag", use_value_delimiter = true, value_delimiter = ',', display_order = 316)] pub exclude_tag: Option>, /// Scan only specified EIDs for faster speed (ex: 1) (ex: 1,4688) @@ -1474,6 +1492,10 @@ pub struct OutputOption { /// Remove duplicate detections (default: disabled) #[arg(help_heading = Some("Output"), short = 'X', long = "remove-duplicate-detections", display_order = 441)] pub remove_duplicate_detections: bool, + + /// Do not ask questions. Scan for all events and alerts. + #[arg(help_heading = Some("General Options"), short = 'w', long = "no-wizard", display_order = 400)] + pub no_wizard: bool, } #[derive(Copy, Args, Clone, Debug)] @@ -2131,8 +2153,8 @@ fn extract_output_options(config: &Config) -> Option { enable_unsupported_rules: option.enable_unsupported_rules, clobber: option.clobber, proven_rules: false, - include_tag: None, - exclude_tag: None, + include_tag: option.include_tag.clone(), + exclude_tag: option.exclude_tag.clone(), include_category: None, exclude_category: None, include_eid: option.include_eid.clone(), @@ -2140,6 +2162,7 @@ fn extract_output_options(config: &Config) -> Option { no_field: false, remove_duplicate_data: false, remove_duplicate_detections: false, + no_wizard: option.no_wizard, }), Action::EidMetrics(option) => Some(OutputOption { input_args: option.input_args.clone(), @@ -2177,6 +2200,7 @@ fn extract_output_options(config: &Config) -> Option { no_field: false, remove_duplicate_data: false, remove_duplicate_detections: false, + no_wizard: true, }), Action::LogonSummary(option) => Some(OutputOption { input_args: option.input_args.clone(), @@ -2214,6 +2238,7 @@ fn extract_output_options(config: &Config) -> Option { no_field: false, remove_duplicate_data: false, remove_duplicate_detections: false, + no_wizard: true, }), Action::ComputerMetrics(option) => Some(OutputOption { input_args: option.input_args.clone(), @@ -2260,6 +2285,7 @@ fn extract_output_options(config: &Config) -> Option { no_field: false, remove_duplicate_data: false, remove_duplicate_detections: false, + no_wizard: true, }), Action::Search(option) => Some(OutputOption { input_args: option.input_args.clone(), @@ -2306,6 +2332,7 @@ fn extract_output_options(config: &Config) -> Option { no_field: false, remove_duplicate_data: false, remove_duplicate_detections: false, + no_wizard: true, }), Action::SetDefaultProfile(option) => Some(OutputOption { input_args: InputOption { @@ -2358,6 +2385,7 @@ fn extract_output_options(config: &Config) -> Option { no_field: false, remove_duplicate_data: false, remove_duplicate_detections: false, + no_wizard: true, }), Action::UpdateRules(option) => Some(OutputOption { input_args: InputOption { @@ -2410,6 +2438,7 @@ fn extract_output_options(config: &Config) -> Option { no_field: false, remove_duplicate_data: false, remove_duplicate_detections: false, + no_wizard: true, }), _ => None, } @@ -2658,6 +2687,7 @@ mod tests { no_field: false, remove_duplicate_data: false, remove_duplicate_detections: false, + no_wizard: true, }, geo_ip: None, output: None, @@ -2729,6 +2759,7 @@ mod tests { no_field: false, remove_duplicate_data: false, remove_duplicate_detections: false, + no_wizard: true, }, geo_ip: None, output: None, @@ -2915,6 +2946,9 @@ mod tests { eid_filter: false, include_eid: None, exclude_eid: None, + no_wizard: true, + include_tag: None, + exclude_tag: None, })), debug: false, })); diff --git a/src/detections/detection.rs b/src/detections/detection.rs index c4d9fec19..5aa3c0185 100644 --- a/src/detections/detection.rs +++ b/src/detections/detection.rs @@ -1248,6 +1248,7 @@ mod tests { no_field: false, remove_duplicate_data: false, remove_duplicate_detections: false, + no_wizard: true, }, geo_ip: None, output: None, @@ -1508,6 +1509,7 @@ mod tests { no_field: false, remove_duplicate_data: false, remove_duplicate_detections: false, + no_wizard: true, }, geo_ip: Some(Path::new("test_files/mmdb").to_path_buf()), output: Some(Path::new("./test_emit_csv.csv").to_path_buf()), @@ -1642,6 +1644,7 @@ mod tests { no_field: false, remove_duplicate_data: false, remove_duplicate_detections: false, + no_wizard: true, }, geo_ip: Some(Path::new("test_files/mmdb").to_path_buf()), output: Some(Path::new("./test_emit_csv.csv").to_path_buf()), @@ -1772,6 +1775,7 @@ mod tests { no_field: false, remove_duplicate_data: false, remove_duplicate_detections: false, + no_wizard: true, }, geo_ip: None, output: Some(Path::new("./test_emit_csv.csv").to_path_buf()), @@ -1915,6 +1919,7 @@ mod tests { no_field: false, remove_duplicate_data: false, remove_duplicate_detections: false, + no_wizard: true, }, geo_ip: None, output: Some(Path::new("./test_emit_csv.csv").to_path_buf()), diff --git a/src/detections/rule/condition_parser.rs b/src/detections/rule/condition_parser.rs index 989c7164c..0af6f71a4 100644 --- a/src/detections/rule/condition_parser.rs +++ b/src/detections/rule/condition_parser.rs @@ -608,6 +608,7 @@ mod tests { no_field: false, remove_duplicate_data: false, remove_duplicate_detections: false, + no_wizard: true, }, geo_ip: None, output: None, diff --git a/src/detections/rule/count.rs b/src/detections/rule/count.rs index 9d6906a3b..086f6fd13 100644 --- a/src/detections/rule/count.rs +++ b/src/detections/rule/count.rs @@ -632,6 +632,7 @@ mod tests { no_field: false, remove_duplicate_data: false, remove_duplicate_detections: false, + no_wizard: true, }, geo_ip: None, output: None, diff --git a/src/detections/rule/matchers.rs b/src/detections/rule/matchers.rs index b0a728a3c..9387504f7 100644 --- a/src/detections/rule/matchers.rs +++ b/src/detections/rule/matchers.rs @@ -871,6 +871,7 @@ mod tests { no_field: false, remove_duplicate_data: false, remove_duplicate_detections: false, + no_wizard: true, }, geo_ip: None, output: None, diff --git a/src/detections/rule/mod.rs b/src/detections/rule/mod.rs index e268d29da..0a734e4f6 100644 --- a/src/detections/rule/mod.rs +++ b/src/detections/rule/mod.rs @@ -452,6 +452,7 @@ mod tests { no_field: false, remove_duplicate_data: false, remove_duplicate_detections: false, + no_wizard: true, }, geo_ip: None, output: None, diff --git a/src/detections/rule/selectionnodes.rs b/src/detections/rule/selectionnodes.rs index ba6d3f5eb..c9a013940 100644 --- a/src/detections/rule/selectionnodes.rs +++ b/src/detections/rule/selectionnodes.rs @@ -575,6 +575,7 @@ mod tests { no_field: false, remove_duplicate_data: false, remove_duplicate_detections: false, + no_wizard: true, }, geo_ip: None, output: None, diff --git a/src/detections/utils.rs b/src/detections/utils.rs index e03479135..b44006446 100644 --- a/src/detections/utils.rs +++ b/src/detections/utils.rs @@ -607,7 +607,7 @@ pub fn check_file_expect_not_exist(path: &Path, exist_alert_str: String) -> bool pub fn output_and_data_stack_for_html( output_str: &str, section_name: &str, - html_report_flag: bool, + html_report_flag: &bool, ) { write_color_buffer( &BufferWriter::stdout(ColorChoice::Always), @@ -617,7 +617,7 @@ pub fn output_and_data_stack_for_html( ) .ok(); - if html_report_flag { + if *html_report_flag { let mut output_data = Nested::::new(); output_data.extend(vec![format!("- {output_str}")]); htmlreport::add_md_data(section_name, output_data); @@ -1058,6 +1058,7 @@ mod tests { no_field: false, remove_duplicate_data: false, remove_duplicate_detections: false, + no_wizard: true, }, geo_ip: None, output: None, diff --git a/src/main.rs b/src/main.rs index 281b4418b..85beda96b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,12 +8,14 @@ use bytesize::ByteSize; use chrono::{DateTime, Datelike, Local, NaiveDateTime, Utc}; use clap::Command; use compact_str::CompactString; +use dialoguer::Confirm; +use dialoguer::{theme::ColorfulTheme, Select}; use evtx::{EvtxParser, ParserSettings, RecordAllocation}; use hashbrown::{HashMap, HashSet}; use hayabusa::debug::checkpoint_process_timer::CHECKPOINT; use hayabusa::detections::configs::{ - load_pivot_keywords, Action, ConfigReader, EventInfoConfig, EventKeyAliasConfig, StoredStatic, - TargetEventTime, TargetIds, CURRENT_EXE_PATH, STORED_EKEY_ALIAS, STORED_STATIC, + load_pivot_keywords, Action, ConfigReader, EventKeyAliasConfig, StoredStatic, TargetEventTime, + TargetIds, CURRENT_EXE_PATH, STORED_EKEY_ALIAS, STORED_STATIC, }; use hayabusa::detections::detection::{self, EvtxRecordInfo}; use hayabusa::detections::message::{AlertMessage, ERROR_LOG_STACK}; @@ -39,6 +41,7 @@ use libmimalloc_sys::mi_stats_print_out; use mimalloc::MiMalloc; use nested::Nested; use serde_json::{Map, Value}; +use std::borrow::BorrowMut; use std::ffi::{OsStr, OsString}; use std::fmt::Display; use std::fmt::Write as _; @@ -236,22 +239,23 @@ impl App { HashSet::default() }; - let output_saved_file = |output_path: &Option, message: &str| { - if let Some(path) = output_path { - if let Ok(metadata) = fs::metadata(path) { - let output_saved_str = format!( - "{message}: {} ({})", - path.display(), - ByteSize::b(metadata.len()).to_string_as(false) - ); - output_and_data_stack_for_html( - &output_saved_str, - "General Overview {#general_overview}", - stored_static.html_report_flag, - ); + let output_saved_file = + |output_path: &Option, message: &str, html_report_flag: &bool| { + if let Some(path) = output_path { + if let Ok(metadata) = fs::metadata(path) { + let output_saved_str = format!( + "{message}: {} ({})", + path.display(), + ByteSize::b(metadata.len()).to_string_as(false) + ); + output_and_data_stack_for_html( + &output_saved_str, + "General Overview {#general_overview}", + html_report_flag, + ); + } } - } - }; + }; match &stored_static.config.action.as_ref().unwrap() { Action::CsvTimeline(_) | Action::JsonTimeline(_) => { @@ -301,7 +305,11 @@ impl App { self.analysis_start(&target_extensions, &time_filter, stored_static); output_profile_name(&stored_static.output_option, false); - output_saved_file(&stored_static.output_path, "Saved file"); + output_saved_file( + &stored_static.output_path, + "Saved file", + &stored_static.html_report_flag, + ); println!(); if stored_static.html_report_flag { let html_str = HTML_REPORTER.read().unwrap().to_owned().create_html(); @@ -351,7 +359,11 @@ impl App { if target_path.ends_with("-failed.csv") { msg = "Failed logon results" } - output_saved_file(&Some(Path::new(target_path).to_path_buf()), msg); + output_saved_file( + &Some(Path::new(target_path).to_path_buf()), + msg, + &stored_static.html_report_flag, + ); } println!(); } @@ -370,7 +382,11 @@ impl App { } } self.analysis_start(&target_extensions, &time_filter, stored_static); - output_saved_file(&stored_static.output_path, "Saved results"); + output_saved_file( + &stored_static.output_path, + "Saved results", + &stored_static.html_report_flag, + ); println!(); } Action::ComputerMetrics(_) => { @@ -388,7 +404,11 @@ impl App { } } self.analysis_start(&target_extensions, &time_filter, stored_static); - output_saved_file(&stored_static.output_path, "Saved results"); + output_saved_file( + &stored_static.output_path, + "Saved results", + &stored_static.html_report_flag, + ); println!(); } Action::PivotKeywordsList(_) => { @@ -704,7 +724,7 @@ impl App { output_and_data_stack_for_html( &elapsed_output_str, "General Overview {#general_overview}", - stored_static.html_report_flag, + &stored_static.html_report_flag, ); // Qオプションを付けた場合もしくはパースのエラーがない場合はerrorのstackが0となるのでエラーログファイル自体が生成されない。 @@ -728,7 +748,7 @@ impl App { &mut self, target_extensions: &HashSet, time_filter: &TargetEventTime, - stored_static: &StoredStatic, + stored_static: &mut StoredStatic, ) { if stored_static.output_option.is_none() { } else if stored_static @@ -746,9 +766,7 @@ impl App { self.analysis_files( live_analysis_list.unwrap(), time_filter, - &stored_static.event_timeline_config, - &stored_static.target_eventids, - stored_static, + stored_static.borrow_mut(), ); } else if let Some(directory) = &stored_static .output_option @@ -766,13 +784,7 @@ impl App { AlertMessage::alert("No .evtx files were found.").ok(); return; } - self.analysis_files( - evtx_files, - time_filter, - &stored_static.event_timeline_config, - &stored_static.target_eventids, - stored_static, - ); + self.analysis_files(evtx_files, time_filter, stored_static.borrow_mut()); } else { // directory, live_analysis以外はfilepathの指定の場合 if let Some(filepath) = &stored_static @@ -821,9 +833,7 @@ impl App { self.analysis_files( vec![check_path.to_path_buf()], time_filter, - &stored_static.event_timeline_config, - &stored_static.target_eventids, - stored_static, + stored_static.borrow_mut(), ); } } @@ -954,16 +964,10 @@ impl App { &mut self, evtx_files: Vec, time_filter: &TargetEventTime, - event_timeline_config: &EventInfoConfig, - target_event_ids: &TargetIds, - stored_static: &StoredStatic, + stored_static: &mut StoredStatic, ) { - let level = stored_static - .output_option - .as_ref() - .unwrap() - .min_level - .to_uppercase(); + let event_timeline_config = &stored_static.event_timeline_config; + let target_event_ids = &stored_static.target_eventids; let target_level = stored_static .output_option .as_ref() @@ -979,7 +983,6 @@ impl App { true, ) .ok(); - let mut total_file_size = ByteSize::b(0); for file_path in &evtx_files { let file_size = match fs::metadata(file_path) { @@ -1002,24 +1005,112 @@ impl App { let total_size_output = format!("Total file size: {}", total_file_size.to_string_as(false)); println!("{total_size_output}"); println!(); + let mut status_append_output = None; if !(stored_static.metrics_flag || stored_static.logon_summary_flag || stored_static.search_flag - || stored_static.computer_metrics_flag) + || stored_static.computer_metrics_flag + || stored_static.output_option.as_ref().unwrap().no_wizard) { - println!("Loading detection rules. Please wait."); + println!("Scan wizard:"); println!(); + let selections_status = &[ + ("1. Core ( status: test, stable | level: high, critical )", (vec!["test", "stable"], "high")), + ("2. Core+ ( status: test, stable | level: medium, high, critical )", (vec!["test", "stable"], "medium")), + ("3. Core++ ( status: experimental, test, stable | level: medium, high, critical )", (vec!["experimental", "test", "stable"], "medium")), + ("4. All alert rules ( status: * | level: low+ )", (vec!["*"], "low")), + ("5. All event and alert rules ( status: * | level: informational+ )", (vec!["*"], "informational")), + ]; + + let selections = selections_status.iter().map(|x| x.0).collect_vec(); + let selected_index = Select::with_theme(&ColorfulTheme::default()) + .with_prompt("Which set of detection rules would you like to load?") + .default(0) + .items(selections.as_slice()) + .interact() + .unwrap(); + status_append_output = Some(format!( + "- selected detection rule sets: {}", + selections_status[selected_index].0 + )); + stored_static.output_option.as_mut().unwrap().min_level = + selections_status[selected_index].1 .1.into(); + + stored_static.include_status.extend( + selections_status[selected_index] + .1 + .0 + .iter() + .map(|x| x.to_owned().into()), + ); + + // If anything other than "4. All alert rules" or "5. All event and alert rules" was selected, ask questions about tags. + if selected_index < 3 { + let mut output_option = stored_static.output_option.clone().unwrap(); + let exclude_tags = output_option.exclude_tag.get_or_insert_with(Vec::new); + let et_rules_load_flag = Confirm::with_theme(&ColorfulTheme::default()) + .with_prompt("Include Emerging Threats rules?") + .default(true) + .show_default(true) + .interact() + .unwrap(); + // If no is selected, then add "--exclude-tags detection.emerging_threats" + if !et_rules_load_flag { + exclude_tags.push("detection.emerging_threats".into()); + } + let th_rules_load_flag = Confirm::with_theme(&ColorfulTheme::default()) + .with_prompt("Include Threat Hunting rules?") + .default(false) + .show_default(true) + .interact() + .unwrap(); + // If no is selected, then add "--exclude-tags detection.threat_hunting" + if !th_rules_load_flag { + exclude_tags.push("detection.threat_hunting".into()); + } + if !exclude_tags.is_empty() { + stored_static.output_option.as_mut().unwrap().exclude_tag = + Some(exclude_tags.to_owned()); + } + } + } else { + stored_static.include_status.insert("*".into()); } + println!(); + println!("Loading detection rules. Please wait."); + println!(); if stored_static.html_report_flag { let mut output_data = Nested::::new(); - output_data.extend(vec![ + let mut html_report_data = Nested::::from_iter(vec![ format!("- Analyzed event files: {}", evtx_files.len()), format!("- {total_size_output}"), ]); + if let Some(status_report) = status_append_output { + html_report_data.push(format!("- Selected deteciton rule set: {status_report}")); + } + let exclude_tags_data = stored_static + .output_option + .as_ref() + .unwrap() + .exclude_tag + .clone() + .unwrap_or_default() + .join(" / "); + if !exclude_tags_data.is_empty() { + html_report_data.push(format!("- Excluded tags: {}", exclude_tags_data)); + } + output_data.extend(html_report_data.iter()); htmlreport::add_md_data("General Overview #{general_overview}", output_data); } + let level = stored_static + .output_option + .as_ref() + .unwrap() + .min_level + .to_uppercase(); + let rule_files = detection::Detection::parse_rule_files( &level, &target_level, @@ -1032,8 +1123,11 @@ impl App { .as_mut() .unwrap() .rap_check_point("Rule Parse Processing Time"); - - if rule_files.is_empty() { + let unused_rules_option = stored_static.logon_summary_flag + || stored_static.search_flag + || stored_static.computer_metrics_flag + || stored_static.metrics_flag; + if !unused_rules_option && rule_files.is_empty() { AlertMessage::alert( "No rules were loaded. Please download the latest rules with the update-rules command.\r\n", ) @@ -1736,6 +1830,7 @@ mod tests { no_field: false, remove_duplicate_data: false, remove_duplicate_detections: false, + no_wizard: true, }, geo_ip: None, output: None, @@ -1897,6 +1992,7 @@ mod tests { no_field: false, remove_duplicate_data: false, remove_duplicate_detections: false, + no_wizard: true, }, geo_ip: None, output: Some(Path::new("overwrite.csv").to_path_buf()), @@ -1979,6 +2075,7 @@ mod tests { no_field: false, remove_duplicate_data: false, remove_duplicate_detections: false, + no_wizard: true, }, geo_ip: None, output: Some(Path::new("overwrite.csv").to_path_buf()), @@ -2059,6 +2156,7 @@ mod tests { no_field: false, remove_duplicate_data: false, remove_duplicate_detections: false, + no_wizard: true, }, geo_ip: None, output: Some(Path::new("overwrite.json").to_path_buf()), @@ -2141,6 +2239,7 @@ mod tests { no_field: false, remove_duplicate_data: false, remove_duplicate_detections: false, + no_wizard: true, }, geo_ip: None, output: Some(Path::new("overwrite.json").to_path_buf()), diff --git a/src/options/htmlreport.rs b/src/options/htmlreport.rs index 6b7d32aa5..b8494a09e 100644 --- a/src/options/htmlreport.rs +++ b/src/options/htmlreport.rs @@ -298,6 +298,7 @@ mod tests { no_field: false, remove_duplicate_data: false, remove_duplicate_detections: false, + no_wizard: true, }, geo_ip: None, output: None, @@ -361,6 +362,7 @@ mod tests { no_field: false, remove_duplicate_data: false, remove_duplicate_detections: false, + no_wizard: true, }, geo_ip: None, output: None, @@ -427,6 +429,7 @@ mod tests { no_field: false, remove_duplicate_data: false, remove_duplicate_detections: false, + no_wizard: true, }, jsonl_timeline: false, geo_ip: None, @@ -490,6 +493,7 @@ mod tests { no_field: false, remove_duplicate_data: false, remove_duplicate_detections: false, + no_wizard: true, }, jsonl_timeline: false, geo_ip: None, diff --git a/src/options/profile.rs b/src/options/profile.rs index 7ab6336e3..1be2f49cd 100644 --- a/src/options/profile.rs +++ b/src/options/profile.rs @@ -475,6 +475,7 @@ mod tests { no_field: false, remove_duplicate_data: false, remove_duplicate_detections: false, + no_wizard: true, }, geo_ip: None, output: None, @@ -599,6 +600,7 @@ mod tests { no_field: false, remove_duplicate_data: false, remove_duplicate_detections: false, + no_wizard: true, }, geo_ip: None, output: None, @@ -673,6 +675,7 @@ mod tests { no_field: false, remove_duplicate_data: false, remove_duplicate_detections: false, + no_wizard: true, }, geo_ip: None, output: None, @@ -777,6 +780,7 @@ mod tests { no_field: false, remove_duplicate_data: false, remove_duplicate_detections: false, + no_wizard: true, }, geo_ip: None, output: None, diff --git a/src/yaml.rs b/src/yaml.rs index 367688971..328df971a 100644 --- a/src/yaml.rs +++ b/src/yaml.rs @@ -76,6 +76,7 @@ impl ParseYaml { stored_static: &StoredStatic, ) -> io::Result { let metadata = fs::metadata(path.as_ref()); + let is_contained_include_status_all_allowed = stored_static.include_status.contains("*"); if metadata.is_err() { let err_contents = if let Err(e) = metadata { e.to_string() @@ -306,10 +307,28 @@ impl ParseYaml { } }; + // 指定されたレベルより低いルールは無視する + let doc_level = &yaml_doc["level"] + .as_str() + .unwrap_or("informational") + .to_uppercase(); + let doc_level_num = self.level_map.get(doc_level).unwrap_or(&1); + let args_level_num = self.level_map.get(min_level).unwrap_or(&1); + let target_level_num = self.level_map.get(target_level).unwrap_or(&0); + if doc_level_num < args_level_num + || (target_level_num != &0_u128 && doc_level_num != target_level_num) + { + up_rule_load_cnt("excluded", rule_id.unwrap_or(&String::default())); + return Option::None; + } + let status = yaml_doc["status"].as_str(); if let Some(s) = yaml_doc["status"].as_str() { - // excluded status optionで指定されたstatusを除外する - if self.exclude_status.contains(&s.to_string()) { + // excluded status optionで指定されたstatusとinclude_status optionで指定されたstatus以外のルールは除外する + if self.exclude_status.contains(&s.to_string()) + || !(is_contained_include_status_all_allowed + || stored_static.include_status.contains(s)) + { up_rule_load_cnt("excluded", rule_id.unwrap_or(&String::default())); return Option::None; } @@ -453,20 +472,6 @@ impl ParseYaml { println!("Loaded rule: {filepath}"); } - // 指定されたレベルより低いルールは無視する - let doc_level = &yaml_doc["level"] - .as_str() - .unwrap_or("informational") - .to_uppercase(); - let doc_level_num = self.level_map.get(doc_level).unwrap_or(&1); - let args_level_num = self.level_map.get(min_level).unwrap_or(&1); - let target_level_num = self.level_map.get(target_level).unwrap_or(&0); - if doc_level_num < args_level_num - || (target_level_num != &0_u128 && doc_level_num != target_level_num) - { - return Option::None; - } - Option::Some((filepath, yaml_doc)) }); self.files.extend(files); @@ -488,7 +493,9 @@ mod tests { use crate::filter; use crate::yaml; use crate::yaml::RuleExclude; + use compact_str::CompactString; use hashbrown::HashMap; + use hashbrown::HashSet; use std::path::Path; use yaml_rust::YamlLoader; @@ -549,6 +556,7 @@ mod tests { no_field: false, remove_duplicate_data: false, remove_duplicate_detections: false, + no_wizard: true, }, geo_ip: None, output: None, @@ -741,7 +749,8 @@ mod tests { #[test] fn test_none_exclude_rules_file() { let path = Path::new("test_files/rules/yaml"); - let dummy_stored_static = create_dummy_stored_static(); + let mut dummy_stored_static = create_dummy_stored_static(); + dummy_stored_static.include_status = HashSet::from_iter(vec![CompactString::from("*")]); let mut yaml = yaml::ParseYaml::new(&dummy_stored_static); let exclude_ids = RuleExclude::new(); yaml.read_dir(path, "", "", &exclude_ids, &dummy_stored_static) @@ -751,7 +760,8 @@ mod tests { #[test] fn test_exclude_deprecated_rules_file() { let path = Path::new("test_files/rules/deprecated"); - let dummy_stored_static = create_dummy_stored_static(); + let mut dummy_stored_static = create_dummy_stored_static(); + dummy_stored_static.include_status = HashSet::from_iter(vec![CompactString::from("*")]); let mut yaml = yaml::ParseYaml::new(&dummy_stored_static); let exclude_ids = RuleExclude::new(); yaml.read_dir(path, "", "", &exclude_ids, &dummy_stored_static) @@ -765,7 +775,8 @@ mod tests { #[test] fn test_exclude_unsupported_rules_file() { let path = Path::new("test_files/rules/unsupported"); - let dummy_stored_static = create_dummy_stored_static(); + let mut dummy_stored_static = create_dummy_stored_static(); + dummy_stored_static.include_status = HashSet::from_iter(vec![CompactString::from("*")]); let mut yaml = yaml::ParseYaml::new(&dummy_stored_static); let exclude_ids = RuleExclude::new(); yaml.read_dir(path, "", "", &exclude_ids, &dummy_stored_static)