remove color option (#518)

* removed used crate in color option and add term color #481

* removed level_color.txt due to fix output color #481

* removed color definition by file

* update cargo

* removed color definiton by true type vec

* added hex crate

* added level_color.txt and color output to command prompt and powershell #481

* adjust termcolor crate
* restored level_color.txt

* remove c option #481

* fixed document #481

* fixed stdoutput test

* add no-color option #481

- disable color output when no-color option set

* added no-color option document

* Fixed clipy err

* doc, changelog, cargo pkg update

* changelog and rules update

* version up to 1.2.2

* readme and changelog update

* reformat to markdown lint

* adjusted logon summary generator section in japanese readme to english
 readme

* fixed typo in readme

Co-authored-by: garigariganzy <tosada31@hotmail.co.jp>
Co-authored-by: Tanaka Zakku <71482215+YamatoSecurity@users.noreply.github.com>
This commit is contained in:
DustInDark
2022-05-17 11:32:57 +09:00
committed by GitHub
parent d654c2cb6b
commit b47561a79c
10 changed files with 308 additions and 339 deletions

View File

@@ -3,9 +3,9 @@ use crate::detections::print;
use crate::detections::print::AlertMessage;
use crate::detections::utils;
use chrono::{DateTime, Local, TimeZone, Utc};
use colored::*;
use csv::QuoteStyle;
use hashbrown::HashMap;
use lazy_static::lazy_static;
use serde::Serialize;
use std::error::Error;
use std::fs::File;
@@ -13,6 +13,7 @@ use std::io;
use std::io::BufWriter;
use std::io::Write;
use std::process;
use termcolor::{BufferWriter, Color, ColorChoice, ColorSpec, WriteColor};
#[derive(Debug, Serialize)]
#[serde(rename_all = "PascalCase")]
@@ -45,12 +46,17 @@ pub struct DisplayFormat<'a> {
pub record_information: Option<&'a str>,
}
lazy_static! {
pub static ref OUTPUT_COLOR: HashMap<String, Color> = set_output_color();
}
/// level_color.txtファイルを読み込み対応する文字色のマッピングを返却する関数
pub fn set_output_color() -> Option<HashMap<String, Vec<u8>>> {
if !configs::CONFIG.read().unwrap().args.is_present("color") {
return None;
}
pub fn set_output_color() -> HashMap<String, Color> {
let read_result = utils::read_csv("config/level_color.txt");
let mut color_map: HashMap<String, Color> = HashMap::new();
if configs::CONFIG.read().unwrap().args.is_present("no-color") {
return color_map;
}
if read_result.is_err() {
// color情報がない場合は通常の白色の出力が出てくるのみで動作への影響を与えない為warnとして処理する
AlertMessage::warn(
@@ -58,9 +64,8 @@ pub fn set_output_color() -> Option<HashMap<String, Vec<u8>>> {
read_result.as_ref().unwrap_err(),
)
.ok();
return None;
return color_map;
}
let mut color_map: HashMap<String, Vec<u8>> = HashMap::new();
read_result.unwrap().into_iter().for_each(|line| {
if line.len() != 2 {
return;
@@ -80,9 +85,17 @@ pub fn set_output_color() -> Option<HashMap<String, Vec<u8>>> {
if level.is_empty() || color_code.len() < 3 {
return;
}
color_map.insert(level.to_string(), color_code);
color_map.insert(level.to_lowercase(), Color::Rgb(color_code[0], color_code[1], color_code[2]));
});
Some(color_map)
color_map
}
fn _get_output_color(color_map: &HashMap<String, Color>, level: &str) -> Option<Color> {
let mut color = None;
if let Some(c) = color_map.get(&level.to_lowercase()) {
color = Some(c.to_owned());
}
color
}
pub fn after_fact() {
@@ -98,7 +111,7 @@ pub fn after_fact() {
let mut displayflag = false;
let mut target: Box<dyn io::Write> =
if let Some(csv_path) = configs::CONFIG.read().unwrap().args.value_of("output") {
// ファイル出力する場合
// output to file
match File::create(csv_path) {
Ok(file) => Box::new(BufWriter::new(file)),
Err(err) => {
@@ -112,7 +125,7 @@ pub fn after_fact() {
}
} else {
displayflag = true;
// 標準出力に出力する場合
// stdoutput (termcolor crate color output is not csv writer)
Box::new(BufWriter::new(io::stdout()))
};
let color_map = set_output_color();
@@ -124,24 +137,21 @@ pub fn after_fact() {
fn emit_csv<W: std::io::Write>(
writer: &mut W,
displayflag: bool,
color_map: Option<HashMap<String, Vec<u8>>>,
color_map: HashMap<String, Color>,
) -> io::Result<()> {
let mut wtr = if displayflag {
csv::WriterBuilder::new()
.double_quote(false)
.quote_style(QuoteStyle::Never)
.delimiter(b'|')
.from_writer(writer)
} else {
csv::WriterBuilder::new().from_writer(writer)
};
let disp_wtr = BufferWriter::stdout(ColorChoice::Always);
let mut disp_wtr_buf = disp_wtr.buffer();
let mut wtr = csv::WriterBuilder::new().from_writer(writer);
let messages = print::MESSAGES.lock().unwrap();
// levelの区分が"Critical","High","Medium","Low","Informational","Undefined"の6つであるため
// level is devided by "Critical","High","Medium","Low","Informational","Undefined".
let mut total_detect_counts_by_level: Vec<u128> = vec![0; 6];
let mut unique_detect_counts_by_level: Vec<u128> = vec![0; 6];
let mut detected_rule_files: Vec<String> = Vec::new();
println!();
let mut plus_header = true;
for (time, detect_infos) in messages.iter() {
for detect_info in detect_infos {
let mut level = detect_info.level.to_string();
@@ -149,15 +159,10 @@ fn emit_csv<W: std::io::Write>(
level = "info".to_string();
}
if displayflag {
let colors = color_map
.as_ref()
.map(|cl_mp| _get_output_color(cl_mp, &detect_info.level));
let colors = colors.as_ref();
let recinfo = detect_info
.record_information
.as_ref()
.map(|recinfo| _format_cell(recinfo, ColPos::Last, colors));
.map(|recinfo| _format_cellpos(recinfo, ColPos::Last));
let details = detect_info
.detail
.chars()
@@ -165,18 +170,30 @@ fn emit_csv<W: std::io::Write>(
.collect::<String>();
let dispformat = DisplayFormat {
timestamp: &_format_cell(&format_time(time), ColPos::First, colors),
level: &_format_cell(&level, ColPos::Other, colors),
computer: &_format_cell(&detect_info.computername, ColPos::Other, colors),
event_i_d: &_format_cell(&detect_info.eventid, ColPos::Other, colors),
channel: &_format_cell(&detect_info.channel, ColPos::Other, colors),
rule_title: &_format_cell(&detect_info.alert, ColPos::Other, colors),
details: &_format_cell(&details, ColPos::Other, colors),
timestamp: &_format_cellpos(&format_time(time), ColPos::First),
level: &_format_cellpos(&level, ColPos::Other),
computer: &_format_cellpos(&detect_info.computername, ColPos::Other),
event_i_d: &_format_cellpos(&detect_info.eventid, ColPos::Other),
channel: &_format_cellpos(&detect_info.channel, ColPos::Other),
rule_title: &_format_cellpos(&detect_info.alert, ColPos::Other),
details: &_format_cellpos(&details, ColPos::Other),
record_information: recinfo.as_deref(),
};
wtr.serialize(dispformat)?;
disp_wtr_buf
.set_color(
ColorSpec::new().set_fg(_get_output_color(&color_map, &detect_info.level)),
)
.ok();
write!(
disp_wtr_buf,
"{}",
_get_serialized_disp_output(dispformat, plus_header)
)
.ok();
plus_header = false;
} else {
// csv出力時フォーマット
// csv output format
wtr.serialize(CsvFormat {
timestamp: &format_time(time),
level: &level,
@@ -201,9 +218,11 @@ fn emit_csv<W: std::io::Write>(
total_detect_counts_by_level[level_suffix] += 1;
}
}
println!();
wtr.flush()?;
if displayflag {
disp_wtr.print(&disp_wtr_buf)?;
} else {
wtr.flush()?;
}
println!();
_print_unique_results(
total_detect_counts_by_level,
@@ -220,13 +239,31 @@ fn emit_csv<W: std::io::Write>(
Ok(())
}
/// columnt position. in cell
/// First: |<str> |
/// Last: | <str>|
/// Othre: | <str> |
enum ColPos {
First, // 先頭
Last, // 最後
Other, // それ以外
First,
Last,
Other,
}
fn _format_cellpos(column: ColPos, colval: &str) -> String {
fn _get_serialized_disp_output(dispformat: DisplayFormat, plus_header: bool) -> String {
let mut disp_serializer = csv::WriterBuilder::new()
.double_quote(false)
.quote_style(QuoteStyle::Never)
.delimiter(b'|')
.has_headers(plus_header)
.from_writer(vec![]);
disp_serializer.serialize(dispformat).ok();
String::from_utf8(disp_serializer.into_inner().unwrap_or_default()).unwrap_or_default()
}
/// return str position in output file
fn _format_cellpos(colval: &str, column: ColPos) -> String {
return match column {
ColPos::First => format!("{} ", colval),
ColPos::Last => format!(" {}", colval),
@@ -234,23 +271,17 @@ fn _format_cellpos(column: ColPos, colval: &str) -> String {
};
}
fn _format_cell(word: &str, column: ColPos, output_color: Option<&Vec<u8>>) -> String {
if let Some(color) = output_color {
let colval = format!("{}", word.truecolor(color[0], color[1], color[2]));
_format_cellpos(column, &colval)
} else {
_format_cellpos(column, word)
}
}
/// 与えられたユニークな検知数と全体の検知数の情報(レベル別と総計)を元に結果文を標準出力に表示する関数
/// output info which unique detection count and all detection count information(devided by level and total) to stdout.
fn _print_unique_results(
mut counts_by_level: Vec<u128>,
head_word: String,
tail_word: String,
color_map: &Option<HashMap<String, Vec<u8>>>,
color_map: &HashMap<String, Color>,
) {
let mut wtr = BufWriter::new(io::stdout());
let buf_wtr = BufferWriter::stdout(ColorChoice::Always);
let mut wtr = buf_wtr.buffer();
wtr.set_color(ColorSpec::new().set_fg(None)).ok();
let levels = Vec::from([
"critical",
"high",
@@ -260,10 +291,10 @@ fn _print_unique_results(
"undefined",
]);
// configsの登録順番と表示をさせたいlevelの順番が逆であるため
// the order in which are registered and the order of levels to be displayed are reversed
counts_by_level.reverse();
// 全体の集計(levelの記載がないためformatの第二引数は空の文字列)
// output total results
writeln!(
wtr,
"{} {}: {}",
@@ -272,33 +303,18 @@ fn _print_unique_results(
counts_by_level.iter().sum::<u128>()
)
.ok();
for (i, level_name) in levels.iter().enumerate() {
let output_raw_str = format!(
"{} {} {}: {}",
head_word, level_name, tail_word, counts_by_level[i]
);
let output_str = if color_map.is_none() {
output_raw_str
} else {
let output_color = _get_output_color(color_map.as_ref().unwrap(), level_name);
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: &str) -> Vec<u8> {
// カラーをつけない場合は255,255,255で出力する
let mut output_color: Vec<u8> = vec![255, 255, 255];
let target_color = color_map.get(level);
if let Some(color) = target_color {
output_color = color.to_vec();
wtr.set_color(ColorSpec::new().set_fg(_get_output_color(color_map, level_name)))
.ok();
writeln!(wtr, "{}", output_raw_str).ok();
}
output_color
buf_wtr.print(&wtr).ok();
}
fn format_time(time: &DateTime<Utc>) -> String {
@@ -309,6 +325,7 @@ fn format_time(time: &DateTime<Utc>) -> String {
}
}
/// return rfc time format string by option
fn format_rfc<Tz: TimeZone>(time: &DateTime<Tz>) -> String
where
Tz::Offset: std::fmt::Display,
@@ -324,11 +341,15 @@ where
#[cfg(test)]
mod tests {
use crate::afterfact::DisplayFormat;
use crate::afterfact::_get_serialized_disp_output;
use crate::afterfact::emit_csv;
use crate::afterfact::format_time;
use crate::detections::print;
use crate::detections::print::DetectInfo;
use crate::detections::print::CH_CONFIG;
use chrono::{Local, TimeZone, Utc};
use hashbrown::HashMap;
use serde_json::Value;
use std::fs::File;
use std::fs::{read_to_string, remove_file};
@@ -423,7 +444,7 @@ mod tests {
+ test_filepath
+ "\n";
let mut file: Box<dyn io::Write> = Box::new(File::create("./test_emit_csv.csv").unwrap());
assert!(emit_csv(&mut file, false, None).is_ok());
assert!(emit_csv(&mut file, false, HashMap::default()).is_ok());
match read_to_string("./test_emit_csv.csv") {
Err(_) => panic!("Failed to open file."),
Ok(s) => {
@@ -435,119 +456,72 @@ mod tests {
}
fn check_emit_csv_display() {
let test_filepath: &str = "test2.evtx";
let test_rulepath: &str = "test-rule2.yml";
let test_title = "test_title2";
let test_level = "medium";
let test_computername = "testcomputer2";
let test_eventid = "2222";
let expect_channel = "Sysmon";
let test_channel = "Sysmon";
let output = "displaytest";
let test_attack = "execution/txxxx.zzz";
{
let mut messages = print::MESSAGES.lock().unwrap();
messages.clear();
let val = r##"
{
"Event": {
"EventData": {
"CommandRLine": "hoge"
},
"System": {
"TimeCreated_attributes": {
"SystemTime": "1996-02-27T01:05:01Z"
}
}
}
}
"##;
let event: Value = serde_json::from_str(val).unwrap();
messages.insert(
&event,
output.to_string(),
DetectInfo {
filepath: test_filepath.to_string(),
rulepath: test_rulepath.to_string(),
level: test_level.to_string(),
computername: test_computername.to_string(),
eventid: test_eventid.to_string(),
channel: CH_CONFIG
.get("Microsoft-Windows-Sysmon/Operational")
.unwrap_or(&String::default())
.to_string(),
alert: test_title.to_string(),
detail: String::default(),
tag_info: test_attack.to_string(),
record_information: Option::Some(String::default()),
},
);
messages.debug();
}
let expect_time = Utc
let test_recinfo = "testinfo";
let test_timestamp = Utc
.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_header =
"Timestamp|Computer|Channel|EventID|Level|RuleTitle|Details|RecordInformation\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(expect_channel)
+ " | "
+ &get_white_color_string(test_eventid)
+ " | "
+ &get_white_color_string(test_level)
+ " | "
+ &get_white_color_string(test_title)
+ " | "
+ &get_white_color_string(output)
+ " | "
+ &get_white_color_string("")
+ "\n";
let expect_nocoloed = expect_header.to_string()
+ &expect_tz
.clone()
.format("%Y-%m-%d %H:%M:%S%.3f %:z")
.to_string()
+ " | "
let expect_tz = test_timestamp.with_timezone(&Local);
let expect_no_header = expect_tz
.clone()
.format("%Y-%m-%d %H:%M:%S%.3f %:z")
.to_string()
+ "|"
+ test_computername
+ " | "
+ expect_channel
+ " | "
+ "|"
+ test_channel
+ "|"
+ test_eventid
+ " | "
+ "|"
+ test_level
+ " | "
+ "|"
+ test_title
+ " | "
+ "|"
+ output
+ " | "
+ ""
+ "|"
+ test_recinfo
+ "\n";
let mut file: Box<dyn io::Write> =
Box::new(File::create("./test_emit_csv_display.txt").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!(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";
white_color_header.to_owned() + target + white_color_footer
let expect_with_header = expect_header.to_string() + &expect_no_header;
assert_eq!(
_get_serialized_disp_output(
DisplayFormat {
timestamp: &format_time(&test_timestamp),
level: test_level,
computer: test_computername,
event_i_d: test_eventid,
channel: test_channel,
rule_title: test_title,
details: output,
record_information: Some(test_recinfo),
},
true
),
expect_with_header
);
assert_eq!(
_get_serialized_disp_output(
DisplayFormat {
timestamp: &format_time(&test_timestamp),
level: test_level,
computer: test_computername,
event_i_d: test_eventid,
channel: test_channel,
rule_title: test_title,
details: output,
record_information: Some(test_recinfo),
},
false
),
expect_no_header
);
}
}

View File

@@ -74,7 +74,6 @@ fn build_app<'a>() -> ArgMatches<'a> {
-f --filepath=[FILEPATH] 'File path to one .evtx file.'
-F --full-data 'Print all field information.'
-r --rules=[RULEDIRECTORY/RULEFILE] 'Rule file or directory (default: ./rules)'
-c --color 'Output with color. (Terminal needs to support True Color.)'
-C --config=[RULECONFIGDIRECTORY] 'Rule config folder. (Default: ./rules/config)'
-o --output=[CSV_TIMELINE] 'Save the timeline in CSV format. (Example: results.csv)'
-v --verbose 'Output verbose information.'
@@ -88,6 +87,7 @@ fn build_app<'a>() -> ArgMatches<'a> {
--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)'
--no-color 'Disable color output'
-t --thread-number=[NUMBER] 'Thread number. (Default: Optimal number for performance.)'
-s --statistics 'Prints statistics of event IDs.'
-L --logon-summary 'User logon and failed logon summary'
@@ -97,7 +97,7 @@ fn build_app<'a>() -> ArgMatches<'a> {
--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.2.1")
.version("1.2.2")
.author("Yamato Security (https://github.com/Yamato-Security/hayabusa) @SecurityYamato")
.setting(AppSettings::VersionlessSubcommands)
.arg(