This commit is contained in:
Alan Smithee
2022-02-13 23:21:01 +09:00
11 changed files with 464 additions and 233 deletions

View File

@@ -11,6 +11,7 @@ use std::error::Error;
use std::fs::File;
use std::io;
use std::io::BufWriter;
use std::io::Write;
use std::process;
#[derive(Debug, Serialize)]
@@ -39,6 +40,9 @@ pub struct DisplayFormat<'a> {
/// level_color.txtファイルを読み込み対応する文字色のマッピングを返却する関数
pub fn set_output_color() -> Option<HashMap<String, Vec<u8>>> {
if !configs::CONFIG.read().unwrap().args.is_present("color") {
return None;
}
let read_result = utils::read_csv("config/level_color.txt");
if read_result.is_err() {
// color情報がない場合は通常の白色の出力が出てくるのみで動作への影響を与えない為warnとして処理する
@@ -135,64 +139,69 @@ fn emit_csv<W: std::io::Write>(
for (time, detect_infos) in messages.iter() {
for detect_info in detect_infos {
if displayflag {
// カラーをつけない場合は255,255,255で出力する
let mut output_color: Vec<u8> = vec![255, 255, 255];
if color_map.is_some() {
let target_color = color_map.as_ref().unwrap().get(&detect_info.level);
if target_color.is_some() {
output_color = target_color.unwrap().to_vec();
}
let output_color =
_get_output_color(&color_map.as_ref().unwrap(), &detect_info.level);
wtr.serialize(DisplayFormat {
timestamp: &format!(
"{} ",
&format_time(time).truecolor(
output_color[0],
output_color[1],
output_color[2]
)
),
level: &format!(
" {} ",
&detect_info.level.truecolor(
output_color[0],
output_color[1],
output_color[2]
)
),
computer: &format!(
" {} ",
&detect_info.computername.truecolor(
output_color[0],
output_color[1],
output_color[2]
)
),
event_i_d: &format!(
" {} ",
&detect_info.eventid.truecolor(
output_color[0],
output_color[1],
output_color[2]
)
),
rule_title: &format!(
" {} ",
&detect_info.alert.truecolor(
output_color[0],
output_color[1],
output_color[2]
)
),
details: &format!(
" {}",
&detect_info.detail.truecolor(
output_color[0],
output_color[1],
output_color[2]
)
),
})?;
} else {
wtr.serialize(DisplayFormat {
timestamp: &format!("{} ", &format_time(time)),
level: &format!(" {} ", &detect_info.level),
computer: &format!(" {} ", &detect_info.computername),
event_i_d: &format!(" {} ", &detect_info.eventid),
rule_title: &format!(" {} ", &detect_info.alert),
details: &format!(" {}", &detect_info.detail),
})?;
}
wtr.serialize(DisplayFormat {
timestamp: &format!(
"{} ",
&format_time(time).truecolor(
output_color[0],
output_color[1],
output_color[2]
)
),
level: &format!(
" {} ",
&detect_info.level.truecolor(
output_color[0],
output_color[1],
output_color[2]
)
),
computer: &format!(
" {} ",
&detect_info.computername.truecolor(
output_color[0],
output_color[1],
output_color[2]
)
),
event_i_d: &format!(
" {} ",
&detect_info.eventid.truecolor(
output_color[0],
output_color[1],
output_color[2]
)
),
rule_title: &format!(
" {} ",
&detect_info.alert.truecolor(
output_color[0],
output_color[1],
output_color[2]
)
),
details: &format!(
" {}",
&detect_info.detail.truecolor(
output_color[0],
output_color[1],
output_color[2]
)
),
})?;
} else {
// csv出力時フォーマット
wtr.serialize(CsvFormat {
@@ -224,17 +233,25 @@ fn emit_csv<W: std::io::Write>(
total_detect_counts_by_level,
"Total".to_string(),
"detections".to_string(),
&color_map,
);
_print_unique_results(
unique_detect_counts_by_level,
"Unique".to_string(),
"rules".to_string(),
&color_map,
);
Ok(())
}
/// 与えられたユニークな検知数と全体の検知数の情報(レベル別と総計)を元に結果文を標準出力に表示する関数
fn _print_unique_results(mut counts_by_level: Vec<u128>, head_word: String, tail_word: String) {
fn _print_unique_results(
mut counts_by_level: Vec<u128>,
head_word: String,
tail_word: String,
color_map: &Option<HashMap<String, Vec<u8>>>,
) {
let mut wtr = BufWriter::new(io::stdout());
let levels = Vec::from([
"critical",
"high",
@@ -248,19 +265,45 @@ fn _print_unique_results(mut counts_by_level: Vec<u128>, head_word: String, tail
counts_by_level.reverse();
// 全体の集計(levelの記載がないためformatの第二引数は空の文字列)
println!(
writeln!(
wtr,
"{} {}: {}",
head_word,
tail_word,
counts_by_level.iter().sum::<u128>()
);
)
.ok();
for (i, level_name) in levels.iter().enumerate() {
println!(
let output_str;
let output_raw_str = format!(
"{} {} {}: {}",
head_word, level_name, tail_word, counts_by_level[i]
);
if color_map.is_none() {
output_str = output_raw_str;
} else {
let output_color =
_get_output_color(&color_map.as_ref().unwrap(), &level_name.to_string());
output_str = output_raw_str
.truecolor(output_color[0], output_color[1], output_color[2])
.to_string();
}
writeln!(wtr, "{}", output_str).ok();
}
wtr.flush().ok();
}
/// levelに対応したtruecolorの値の配列を返す関数
fn _get_output_color(color_map: &HashMap<String, Vec<u8>>, level: &String) -> Vec<u8> {
// カラーをつけない場合は255,255,255で出力する
let mut output_color: Vec<u8> = vec![255, 255, 255];
let target_color = color_map.get(level);
if target_color.is_some() {
output_color = target_color.unwrap().to_vec();
}
return output_color;
}
fn format_time(time: &DateTime<Utc>) -> String {
if configs::CONFIG.read().unwrap().args.is_present("utc") {
format_rfc(time)
@@ -416,7 +459,26 @@ mod tests {
.datetime_from_str("1996-02-27T01:05:01Z", "%Y-%m-%dT%H:%M:%SZ")
.unwrap();
let expect_tz = expect_time.with_timezone(&Local);
let expect = "Timestamp|Computer|EventID|Level|RuleTitle|Details\n".to_string()
let expect_header = "Timestamp|Computer|EventID|Level|RuleTitle|Details\n";
let expect_colored = expect_header.to_string()
+ &get_white_color_string(
&expect_tz
.clone()
.format("%Y-%m-%d %H:%M:%S%.3f %:z")
.to_string(),
)
+ " | "
+ &get_white_color_string(test_computername)
+ " | "
+ &get_white_color_string(test_eventid)
+ " | "
+ &get_white_color_string(test_level)
+ " | "
+ &get_white_color_string(test_title)
+ " | "
+ &get_white_color_string(output)
+ "\n";
let expect_nocoloed = expect_header.to_string()
+ &expect_tz
.clone()
.format("%Y-%m-%d %H:%M:%S%.3f %:z")
@@ -432,15 +494,23 @@ mod tests {
+ " | "
+ output
+ "\n";
let mut file: Box<dyn io::Write> =
Box::new(File::create("./test_emit_csv_display.txt".to_string()).unwrap());
assert!(emit_csv(&mut file, true, None).is_ok());
match read_to_string("./test_emit_csv_display.txt") {
Err(_) => panic!("Failed to open file."),
Ok(s) => {
assert_eq!(s, expect);
assert!(s == expect_colored || s == expect_nocoloed);
}
};
assert!(remove_file("./test_emit_csv_display.txt").is_ok());
}
fn get_white_color_string(target: &str) -> String {
let white_color_header = "\u{1b}[38;2;255;255;255m";
let white_color_footer = "\u{1b}[0m";
return white_color_header.to_owned() + target + white_color_footer;
}
}

View File

@@ -53,29 +53,30 @@ fn build_app<'a>() -> ArgMatches<'a> {
return ArgMatches::default();
}
let usages = "-d --directory=[DIRECTORY] 'Directory of multiple .evtx files'
-f --filepath=[FILEPATH] 'File path to one .evtx file'
let usages = "-d --directory=[DIRECTORY] 'Directory of multiple .evtx files.'
-f --filepath=[FILEPATH] 'File path to one .evtx file.'
-r --rules=[RULEDIRECTORY/RULEFILE] 'Rule file or directory (default: ./rules)'
-o --output=[CSV_TIMELINE] 'Save the timeline in CSV format. Example: results.csv'
-v --verbose 'Output verbose information'
-D --enable-deprecated-rules 'Enable sigma rules marked as deprecated'
-n --enable-noisy-rules 'Enable rules marked as noisy'
-c --color 'Output with color. (Terminal needs to support True Color.)'
-o --output=[CSV_TIMELINE] 'Save the timeline in CSV format. (example: results.csv)'
-v --verbose 'Output verbose information.'
-D --enable-deprecated-rules 'Enable sigma rules marked as deprecated.'
-n --enable-noisy-rules 'Enable rules marked as noisy.'
-u --update-rules 'Clone latest hayabusa-rule'
-m --min-level=[LEVEL] 'Minimum level for rules (default: informational)'
--start-timeline=[STARTTIMELINE] 'Start time of the event to load from event file. Example: '2018/11/28 12:00:00 +09:00''
--end-timeline=[ENDTIMELINE] 'End time of the event to load from event file. Example: '2018/11/28 12:00:00 +09:00''
--rfc-2822 'Output date and time in RFC 2822 format. Example: Mon, 07 Aug 2006 12:34:56 -0600'
--rfc-3339 'Output date and time in RFC 3339 format. Example: 2006-08-07T12:34:56.485214 -06:00'
-U --utc 'Output time in UTC format (default: local time)'
-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'
-m --min-level=[LEVEL] 'Minimum level for rules. (default: informational)'
--start-timeline=[STARTTIMELINE] 'Start time of the event to load from event file. (example: '2018/11/28 12:00:00 +09:00')'
--end-timeline=[ENDTIMELINE] 'End time of the event to load from event file. (example: '2018/11/28 12:00:00 +09:00')'
--rfc-2822 'Output date and time in RFC 2822 format. (example: Mon, 07 Aug 2006 12:34:56 -0600)'
--rfc-3339 'Output date and time in RFC 3339 format. (example: 2006-08-07T12:34:56.485214 -06:00)'
-U --utc 'Output time in UTC format. (default: local time)'
-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 save error logs.'
--contributors 'Prints the list of contributors'";
--contributors 'Prints the list of contributors.'";
App::new(&program)
.about("Hayabusa: Aiming to be the world's greatest Windows event log analysis tool!")
.version("1.1.0")
.author("Yamato-Security(https://github.com/Yamato-Security/hayabusa)")
.author("Yamato Security (https://github.com/Yamato-Security/hayabusa)")
.setting(AppSettings::VersionlessSubcommands)
.usage(usages)
.args_from_usage(usages)

View File

@@ -13,8 +13,8 @@ use crate::detections::utils::get_serde_number_to_string;
use crate::filter;
use crate::yaml::ParseYaml;
use hashbrown;
use hashbrown::HashMap;
use serde_json::Value;
use std::collections::HashMap;
use std::io::BufWriter;
use std::sync::Arc;
use tokio::{runtime::Runtime, spawn, task::JoinHandle};

View File

@@ -5,7 +5,8 @@ use self::selectionnodes::{
AndSelectionNode, NotSelectionNode, OrSelectionNode, RefSelectionNode, SelectionNode,
};
use super::selectionnodes;
use std::{collections::HashMap, sync::Arc};
use hashbrown::HashMap;
use std::sync::Arc;
lazy_static! {
pub static ref CONDITION_REGEXMAP: Vec<Regex> = vec![

View File

@@ -3,7 +3,8 @@ use crate::detections::print::Message;
use chrono::{DateTime, Utc};
use std::{collections::HashMap, fmt::Debug, sync::Arc, vec};
use hashbrown::HashMap;
use std::{fmt::Debug, sync::Arc, vec};
use yaml_rust::Yaml;

View File

@@ -2,9 +2,9 @@ use crate::detections::configs;
use crate::detections::print::AlertMessage;
use crate::detections::print::ERROR_LOG_STACK;
use crate::detections::print::QUIET_ERRORS_FLAG;
use hashbrown::HashSet;
use lazy_static::lazy_static;
use regex::Regex;
use std::collections::HashSet;
use std::fs::File;
use std::io::BufWriter;
use std::io::{BufRead, BufReader};

View File

@@ -1,5 +1,5 @@
use crate::detections::{configs, detection::EvtxRecordInfo, utils};
use std::collections::HashMap;
use hashbrown::HashMap;
#[derive(Debug)]
pub struct EventStatistics {

View File

@@ -1,7 +1,7 @@
use crate::detections::{configs, detection::EvtxRecordInfo};
use super::statistics::EventStatistics;
use std::collections::HashMap;
use hashbrown::HashMap;
#[derive(Debug)]
pub struct Timeline {

View File

@@ -6,7 +6,7 @@ use crate::detections::print::AlertMessage;
use crate::detections::print::ERROR_LOG_STACK;
use crate::detections::print::QUIET_ERRORS_FLAG;
use crate::filter::RuleExclude;
use std::collections::HashMap;
use hashbrown::HashMap;
use std::ffi::OsStr;
use std::fs;
use std::io;
@@ -201,34 +201,6 @@ impl ParseYaml {
let files: Vec<(String, Yaml)> = yaml_docs
.into_iter()
.filter_map(|(filepath, yaml_doc)| {
// ignoreフラグがONになっているルールは無視する。
if yaml_doc["ignore"].as_bool().unwrap_or(false) {
self.ignorerule_count += 1;
return Option::None;
}
self.rulecounter.insert(
yaml_doc["ruletype"].as_str().unwrap_or("Other").to_string(),
self.rulecounter
.get(&yaml_doc["ruletype"].as_str().unwrap_or("Other").to_string())
.unwrap_or(&0)
+ 1,
);
if configs::CONFIG.read().unwrap().args.is_present("verbose") {
println!("Loaded yml file path: {}", filepath);
}
// 指定されたレベルより低いルールは無視する
let doc_level = &yaml_doc["level"]
.as_str()
.unwrap_or("informational")
.to_string()
.to_uppercase();
let doc_level_num = configs::LEVELMAP.get(doc_level).unwrap_or(&1);
let args_level_num = configs::LEVELMAP.get(level).unwrap_or(&1);
if doc_level_num < args_level_num {
return Option::None;
}
//除外されたルールは無視する
let rule_id = &yaml_doc["id"].as_str();
if rule_id.is_some() {
@@ -244,6 +216,30 @@ impl ParseYaml {
}
}
self.rulecounter.insert(
yaml_doc["ruletype"].as_str().unwrap_or("Other").to_string(),
self.rulecounter
.get(&yaml_doc["ruletype"].as_str().unwrap_or("Other").to_string())
.unwrap_or(&0)
+ 1,
);
if configs::CONFIG.read().unwrap().args.is_present("verbose") {
println!("Loaded yml file path: {}", filepath);
}
// 指定されたレベルより低いルールは無視する
let doc_level = &yaml_doc["level"]
.as_str()
.unwrap_or("informational")
.to_string()
.to_uppercase();
let doc_level_num = configs::LEVELMAP.get(doc_level).unwrap_or(&1);
let args_level_num = configs::LEVELMAP.get(level).unwrap_or(&1);
if doc_level_num < args_level_num {
return Option::None;
}
if !configs::CONFIG
.read()
.unwrap()
@@ -273,7 +269,7 @@ mod tests {
use crate::filter;
use crate::yaml;
use crate::yaml::RuleExclude;
use std::collections::HashSet;
use hashbrown::HashSet;
use std::path::Path;
use yaml_rust::YamlLoader;