diff --git a/Cargo.lock b/Cargo.lock index cab6a4bf..cb982884 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -773,6 +773,7 @@ dependencies = [ "hashbrown", "hhmmss", "lazy_static", + "linecount", "linked-hash-map", "mopa", "num_cpus", @@ -1015,6 +1016,12 @@ version = "0.2.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2a5ac8f984bfcf3a823267e5fde638acc3325f6496633a5da6bb6eb2171e103" +[[package]] +name = "linecount" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5d5a4a243b9cf052d37af99679cc93b08a791f444a4a1b21bb4efcaf01847d8" + [[package]] name = "linked-hash-map" version = "0.5.3" diff --git a/Cargo.toml b/Cargo.toml index af29763b..7d74f4f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ dotenv = "0.15.0" hhmmss = "*" pbr = "*" hashbrown = "0.11.2" +linecount = "*" [target.x86_64-pc-windows-gnu] linker = "x86_64-w64-mingw32-gcc" diff --git a/src/afterfact.rs b/src/afterfact.rs index 8cbe21a5..4275cf7e 100644 --- a/src/afterfact.rs +++ b/src/afterfact.rs @@ -189,6 +189,8 @@ where mod tests { use crate::afterfact::emit_csv; use crate::detections::print; + use crate::detections::print::AlertMessage; + use crate::detections::print::ERROR_LOG_PATH; use chrono::{Local, TimeZone, Utc}; use serde_json::Value; use std::fs::File; @@ -203,6 +205,7 @@ mod tests { } fn test_emit_csv_output() { + AlertMessage::create_error_log(ERROR_LOG_PATH.to_string()); let testfilepath: &str = "test.evtx"; let testrulepath: &str = "test-rule.yml"; let test_title = "test_title"; diff --git a/src/detections/configs.rs b/src/detections/configs.rs index 06f60e3d..492f209e 100644 --- a/src/detections/configs.rs +++ b/src/detections/configs.rs @@ -68,6 +68,7 @@ fn build_app<'a>() -> ArgMatches<'a> { -t --thread-number=[NUMBER] 'Thread number (default: optimal number for performance)' -s --statistics 'Prints statistics of event IDs' -q --quiet 'Quiet mode. Do not display the launch banner' + -Q --quiet-errors 'Quiet errors mode. Do not display errors or save error logs' --contributors 'Prints the list of contributors'"; App::new(&program) .about("Hayabusa: Aiming to be the world's greatest Windows event log analysis tool!") @@ -140,7 +141,7 @@ impl TargetEventTime { Err(err) => { AlertMessage::alert( &mut std::io::stderr().lock(), - format!("starttimeline field: {}", err), + format!("start-timeline field: {}", err), ) .ok(); None @@ -157,7 +158,7 @@ impl TargetEventTime { Err(err) => { AlertMessage::alert( &mut std::io::stderr().lock(), - format!("endtimeline field: {}", err), + format!("end-timeline field: {}", err), ) .ok(); None diff --git a/src/detections/detection.rs b/src/detections/detection.rs index d63a73e8..30a90572 100644 --- a/src/detections/detection.rs +++ b/src/detections/detection.rs @@ -1,6 +1,7 @@ extern crate csv; use crate::detections::print::AlertMessage; +use crate::detections::print::ERROR_LOG_PATH; use crate::detections::print::MESSAGES; use crate::detections::rule; use crate::detections::rule::AggResult; @@ -11,6 +12,8 @@ use crate::yaml::ParseYaml; use hashbrown; use serde_json::Value; use std::collections::HashMap; +use std::fs::OpenOptions; +use std::io::BufWriter; use tokio::{runtime::Runtime, spawn, task::JoinHandle}; use std::sync::Arc; @@ -58,7 +61,12 @@ impl Detection { rulefile_loader.read_dir(rulespath.unwrap_or(DIRPATH_RULES), &level, exclude_ids); if result_readdir.is_err() { AlertMessage::alert( - &mut std::io::stderr().lock(), + &mut BufWriter::new( + OpenOptions::new() + .append(true) + .open(ERROR_LOG_PATH.to_string()) + .unwrap(), + ), format!("{}", result_readdir.unwrap_err()), ) .ok(); diff --git a/src/detections/print.rs b/src/detections/print.rs index 594aa504..810f61ba 100644 --- a/src/detections/print.rs +++ b/src/detections/print.rs @@ -2,13 +2,20 @@ extern crate lazy_static; use crate::detections::configs; use crate::detections::utils; use crate::detections::utils::get_serde_number_to_string; -use chrono::{DateTime, TimeZone, Utc}; +use chrono::{DateTime, Local, TimeZone, Utc}; use lazy_static::lazy_static; +use linecount::count_lines; use regex::Regex; use serde_json::Value; use std::collections::BTreeMap; use std::collections::HashMap; +use std::env; +use std::fs::create_dir; +use std::fs::remove_file; +use std::fs::File; +use std::io::BufWriter; use std::io::{self, Write}; +use std::path::Path; use std::sync::Mutex; #[derive(Debug)] @@ -32,6 +39,15 @@ pub struct AlertMessage {} lazy_static! { pub static ref MESSAGES: Mutex = Mutex::new(Message::new()); pub static ref ALIASREGEX: Regex = Regex::new(r"%[a-zA-Z0-9-_]+%").unwrap(); + pub static ref ERROR_LOG_PATH: String = format!( + "./logs/errorlog-{}.log", + Local::now().format("%Y%m%d_%H%M%S") + ); + pub static ref QUIET_ERRORS_FLAG: bool = configs::CONFIG + .read() + .unwrap() + .args + .is_present("quiet-errors"); } impl Message { @@ -180,11 +196,68 @@ impl Message { } impl AlertMessage { - pub fn alert(w: &mut W, contents: String) -> io::Result<()> { - writeln!(w, "[ERROR] {}", contents) + //対象のディレクトリが存在することを確認後、最初の定型文を追加して、ファイルのbufwriterを返す関数 + pub fn create_error_log(path_str: String) { + let path = Path::new(&path_str); + if !path.parent().unwrap().exists() { + create_dir(path.parent().unwrap()).ok(); + } + // 1行目は必ず実行したコマンド情報を入れておく。 + let mut ret = BufWriter::new(File::create(path).unwrap()); + + ret.write( + format!( + "user input: {:?}\n", + format_args!( + "{}", + env::args() + .map(|arg| arg) + .collect::>() + .join(" ") + ) + ) + .as_bytes(), + ) + .unwrap(); + ret.flush().ok(); } + + /// ERRORメッセージを表示する関数。error_log_flagでfalseの場合は外部へのエラーログの書き込みは行わずに指定されたwを用いた出力のみ行う。trueの場合はwを用いた出力を行わずにエラーログへの出力を行う + pub fn alert(w: &mut W, contents: String) -> io::Result<()> { + if *QUIET_ERRORS_FLAG { + writeln!(w, "[ERROR] {}", contents) + } else { + Ok(()) + } + } + + // WARNメッセージを表示する関数 pub fn warn(w: &mut W, contents: String) -> io::Result<()> { - writeln!(w, "[WARN] {}", contents) + if *QUIET_ERRORS_FLAG { + writeln!(w, "[WARN] {}", contents) + } else { + Ok(()) + } + } + + /// エラーログへのERRORメッセージの出力数を確認して、0であったらファイルを削除する。1以上あればエラーを書き出した旨を標準出力に表示する + pub fn output_error_log_exist() { + let error_log_path_str = ERROR_LOG_PATH.to_string(); + // 1行しかなかった場合は最初に書いたコマンド情報のみと判断して削除する + if count_lines(File::open(&error_log_path_str).unwrap()).unwrap() == 1 { + if remove_file(&error_log_path_str).is_err() { + AlertMessage::alert( + &mut std::io::stderr().lock(), + format!("failed to remove file. filepath:{}", &error_log_path_str), + ) + .ok(); + } + return; + } + println!( + "Generated error was output to {}. Please see the file for details.", + &error_log_path_str + ); } } diff --git a/src/detections/rule/count.rs b/src/detections/rule/count.rs index c675589a..6b161296 100644 --- a/src/detections/rule/count.rs +++ b/src/detections/rule/count.rs @@ -1,4 +1,5 @@ use crate::detections::print::AlertMessage; +use crate::detections::print::ERROR_LOG_PATH; use crate::detections::rule::AggResult; use crate::detections::rule::AggregationParseInfo; use crate::detections::rule::Message; @@ -6,6 +7,8 @@ use crate::detections::rule::RuleNode; use chrono::{DateTime, TimeZone, Utc}; use hashbrown::HashMap; use serde_json::Value; +use std::fs::OpenOptions; +use std::io::BufWriter; use std::num::ParseIntError; use std::path::Path; @@ -183,7 +186,12 @@ impl TimeFrameInfo { tnum.retain(|c| c != 'd'); } else { AlertMessage::alert( - &mut std::io::stderr().lock(), + &mut BufWriter::new( + OpenOptions::new() + .append(true) + .open(ERROR_LOG_PATH.to_string()) + .unwrap(), + ), format!("Timeframe is invalid. Input value:{}", value), ) .ok(); @@ -215,8 +223,13 @@ pub fn get_sec_timeframe(timeframe: &Option) -> Option { } Err(err) => { AlertMessage::alert( - &mut std::io::stderr().lock(), - format!("Timeframe number is invalid. timeframe.{}", err), + &mut BufWriter::new( + OpenOptions::new() + .append(true) + .open(ERROR_LOG_PATH.to_string()) + .unwrap(), + ), + format!("Timeframe number is invalid. timeframe: {}", err), ) .ok(); return Option::None; diff --git a/src/main.rs b/src/main.rs index 50abee7d..087ba29c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ use chrono::{DateTime, Local}; use evtx::{EvtxParser, ParserSettings}; use hayabusa::detections::detection::{self, EvtxRecordInfo}; use hayabusa::detections::print::AlertMessage; +use hayabusa::detections::print::ERROR_LOG_PATH; use hayabusa::detections::rule::{get_detection_keys, RuleNode}; use hayabusa::filter; use hayabusa::omikuji::Omikuji; @@ -16,6 +17,9 @@ use pbr::ProgressBar; use serde_json::Value; use std::collections::{HashMap, HashSet}; use std::fmt::Display; +use std::fs::OpenOptions; +use std::io::BufWriter; +use std::path::Path; use std::sync::Arc; use std::{ fs::{self, File}, @@ -66,6 +70,17 @@ impl App { ); return; } + if let Some(csv_path) = configs::CONFIG.read().unwrap().args.value_of("output") { + if Path::new(csv_path).exists() { + AlertMessage::alert( + &mut std::io::stderr().lock(), + " file name in --output already exist other file. Please input unique file path.".to_owned(), + ) + .ok(); + return; + } + } + AlertMessage::create_error_log(ERROR_LOG_PATH.to_string()); if let Some(filepath) = configs::CONFIG.read().unwrap().args.value_of("filepath") { if !filepath.ends_with(".evtx") { AlertMessage::alert( @@ -100,14 +115,22 @@ impl App { let analysis_duration = analysis_end_time.signed_duration_since(analysis_start_time); println!("Elapsed Time: {}", &analysis_duration.hhmmssxxx()); println!(""); + AlertMessage::output_error_log_exist(); } fn collect_evtxfiles(&self, dirpath: &str) -> Vec { let entries = fs::read_dir(dirpath); if entries.is_err() { - let stderr = std::io::stderr(); - let mut stderr = stderr.lock(); - AlertMessage::alert(&mut stderr, format!("{}", entries.unwrap_err())).ok(); + AlertMessage::alert( + &mut BufWriter::new( + OpenOptions::new() + .append(true) + .open(ERROR_LOG_PATH.to_string()) + .unwrap(), + ), + format!("{}", entries.unwrap_err()), + ) + .ok(); return vec![]; } @@ -171,6 +194,8 @@ impl App { pb.inc(); } after_fact(); + println!(""); + AlertMessage::output_error_log_exist(); } // Windowsイベントログファイルを1ファイル分解析する。 @@ -206,7 +231,16 @@ impl App { evtx_filepath, record_result.unwrap_err() ); - AlertMessage::alert(&mut std::io::stderr().lock(), errmsg).ok(); + AlertMessage::alert( + &mut BufWriter::new( + OpenOptions::new() + .append(true) + .open(ERROR_LOG_PATH.to_string()) + .unwrap(), + ), + errmsg, + ) + .ok(); continue; } diff --git a/src/yaml.rs b/src/yaml.rs index 4411afb2..9c389714 100644 --- a/src/yaml.rs +++ b/src/yaml.rs @@ -3,11 +3,14 @@ extern crate yaml_rust; use crate::detections::configs; use crate::detections::print::AlertMessage; +use crate::detections::print::ERROR_LOG_PATH; use crate::filter::RuleExclude; use std::collections::HashMap; use std::ffi::OsStr; use std::fs; +use std::fs::OpenOptions; use std::io; +use std::io::BufWriter; use std::io::{BufReader, Read}; use std::path::{Path, PathBuf}; use yaml_rust::Yaml; @@ -72,7 +75,12 @@ impl ParseYaml { let read_content = self.read_file(path); if read_content.is_err() { AlertMessage::warn( - &mut std::io::stdout().lock(), + &mut BufWriter::new( + OpenOptions::new() + .append(true) + .open(ERROR_LOG_PATH.to_string()) + .unwrap(), + ), format!( "fail to read file: {}\n{} ", entry.path().display(), @@ -87,7 +95,12 @@ impl ParseYaml { let yaml_contents = YamlLoader::load_from_str(&read_content.unwrap()); if yaml_contents.is_err() { AlertMessage::warn( - &mut std::io::stdout().lock(), + &mut BufWriter::new( + OpenOptions::new() + .append(true) + .open(ERROR_LOG_PATH.to_string()) + .unwrap(), + ), format!( "Failed to parse yml: {}\n{} ", entry.path().display(),