Merge branch 'main'

This commit is contained in:
garigariganzy
2022-09-29 23:12:44 +09:00
28 changed files with 1180 additions and 404 deletions
+114 -32
View File
@@ -1,9 +1,7 @@
use crate::detections::configs;
use crate::detections::configs::{CURRENT_EXE_PATH, TERM_SIZE};
use crate::detections::message::{self, LEVEL_ABBR};
use crate::detections::message::{AlertMessage, LEVEL_FULL};
use crate::detections::utils::{self, format_time};
use crate::detections::utils::{get_writable_color, write_color_buffer};
use crate::detections::configs::{self, CURRENT_EXE_PATH, TERM_SIZE};
use crate::detections::message::{self, AlertMessage, LEVEL_ABBR, LEVEL_FULL};
use crate::detections::utils::{self, format_time, get_writable_color, write_color_buffer};
use crate::options::htmlreport;
use crate::options::profile::PROFILES;
use bytesize::ByteSize;
use chrono::{DateTime, Local, TimeZone, Utc};
@@ -24,12 +22,9 @@ use num_format::{Locale, ToFormattedString};
use std::cmp::min;
use std::error::Error;
use std::fs::File;
use std::io;
use std::io::BufWriter;
use std::io::Write;
use std::io::{self, BufWriter, Write};
use std::fs;
use std::fs::{self, File};
use std::process;
use termcolor::{BufferWriter, Color, ColorChoice, ColorSpec, WriteColor};
use terminal_size::Width;
@@ -210,6 +205,8 @@ fn emit_csv<W: std::io::Write>(
all_record_cnt: u128,
profile: LinkedHashMap<String, String>,
) -> io::Result<()> {
let mut html_output_stock: Vec<String> = vec![];
let html_output_flag = configs::CONFIG.read().unwrap().args.html_report.is_some();
let disp_wtr = BufferWriter::stdout(ColorChoice::Always);
let mut disp_wtr_buf = disp_wtr.buffer();
let json_output_flag = configs::CONFIG.read().unwrap().args.json_timeline;
@@ -238,7 +235,7 @@ fn emit_csv<W: std::io::Write>(
HashMap::new();
let mut detect_counts_by_rule_and_level: HashMap<String, HashMap<String, i128>> =
HashMap::new();
let mut rule_title_path_map: HashMap<String, String> = HashMap::new();
let levels = Vec::from(["crit", "high", "med ", "low ", "info", "undefined"]);
// レベル別、日ごとの集計用変数の初期化
for level_init in levels {
@@ -372,6 +369,7 @@ fn emit_csv<W: std::io::Write>(
.unwrap()
})
.clone();
rule_title_path_map.insert(detect_info.ruletitle.clone(), detect_info.rulepath.clone());
*detect_counts_by_rules
.entry(Clone::clone(&detect_info.ruletitle))
.or_insert(0) += 1;
@@ -454,31 +452,35 @@ fn emit_csv<W: std::io::Write>(
)
.ok();
write_color_buffer(&disp_wtr, get_writable_color(None), ": ", false).ok();
let saved_alerts_output =
(all_record_cnt - reducted_record_cnt).to_formatted_string(&Locale::en);
write_color_buffer(
&disp_wtr,
get_writable_color(Some(Color::Rgb(255, 255, 0))),
&(all_record_cnt - reducted_record_cnt).to_formatted_string(&Locale::en),
&saved_alerts_output,
false,
)
.ok();
write_color_buffer(&disp_wtr, get_writable_color(None), " / ", false).ok();
let all_record_output = all_record_cnt.to_formatted_string(&Locale::en);
write_color_buffer(
&disp_wtr,
get_writable_color(Some(Color::Rgb(0, 255, 255))),
&all_record_cnt.to_formatted_string(&Locale::en),
&all_record_output,
false,
)
.ok();
write_color_buffer(&disp_wtr, get_writable_color(None), " (", false).ok();
let reduction_output = format!(
"Data reduction: {} events ({:.2}%)",
reducted_record_cnt.to_formatted_string(&Locale::en),
reducted_percent
);
write_color_buffer(
&disp_wtr,
get_writable_color(Some(Color::Rgb(0, 255, 0))),
&format!(
"Data reduction: {} events ({:.2}%)",
reducted_record_cnt.to_formatted_string(&Locale::en),
reducted_percent
),
&reduction_output,
false,
)
.ok();
@@ -487,6 +489,15 @@ fn emit_csv<W: std::io::Write>(
println!();
println!();
if html_output_flag {
html_output_stock.push(format!(
"- Saved alerts and events: {}",
&saved_alerts_output
));
html_output_stock.push(format!("- Total events analyzed: {}", &all_record_output));
html_output_stock.push(format!("- {}", reduction_output));
}
_print_unique_results(
total_detect_counts_by_level,
unique_detect_counts_by_level,
@@ -496,17 +507,44 @@ fn emit_csv<W: std::io::Write>(
);
println!();
_print_detection_summary_by_date(detect_counts_by_date_and_level, &color_map);
_print_detection_summary_by_date(
detect_counts_by_date_and_level,
&color_map,
&mut html_output_stock,
);
println!();
println!();
if html_output_flag {
html_output_stock.push("".to_string());
}
_print_detection_summary_by_computer(detect_counts_by_computer_and_level, &color_map);
_print_detection_summary_by_computer(
detect_counts_by_computer_and_level,
&color_map,
&mut html_output_stock,
);
println!();
if html_output_flag {
html_output_stock.push("".to_string());
}
_print_detection_summary_tables(detect_counts_by_rule_and_level, &color_map);
_print_detection_summary_tables(
detect_counts_by_rule_and_level,
&color_map,
rule_title_path_map,
&mut html_output_stock,
);
println!();
if html_output_flag {
html_output_stock.push("".to_string());
}
}
if html_output_flag {
htmlreport::add_md_data(
"Results Summary {#results_summary}".to_string(),
html_output_stock,
);
}
Ok(())
}
@@ -634,13 +672,16 @@ fn _print_unique_results(
fn _print_detection_summary_by_date(
detect_counts_by_date: HashMap<String, HashMap<String, u128>>,
color_map: &HashMap<String, Colors>,
html_output_stock: &mut Vec<String>,
) {
let buf_wtr = BufferWriter::stdout(ColorChoice::Always);
let mut wtr = buf_wtr.buffer();
wtr.set_color(ColorSpec::new().set_fg(None)).ok();
writeln!(wtr, "Dates with most total detections:").ok();
let output_header = "Dates with most total detections:";
writeln!(wtr, "{}", output_header).ok();
if configs::CONFIG.read().unwrap().args.html_report.is_some() {
html_output_stock.push(format!("- {}", output_header));
}
for (idx, level) in LEVEL_ABBR.values().enumerate() {
// output_levelsはlevelsからundefinedを除外した配列であり、各要素は必ず初期化されているのでSomeであることが保証されているのでunwrapをそのまま実施
let detections_by_day = detect_counts_by_date.get(level).unwrap();
@@ -662,26 +703,28 @@ fn _print_detection_summary_by_date(
if !exist_max_data {
max_detect_str = "n/a".to_string();
}
write!(
wtr,
let output_str = format!(
"{}: {}",
LEVEL_FULL.get(level.as_str()).unwrap(),
&max_detect_str
)
.ok();
);
write!(wtr, "{}", output_str).ok();
if idx != LEVEL_ABBR.len() - 1 {
wtr.set_color(ColorSpec::new().set_fg(None)).ok();
write!(wtr, ", ").ok();
}
if configs::CONFIG.read().unwrap().args.html_report.is_some() {
html_output_stock.push(format!(" - {}", output_str));
}
}
buf_wtr.print(&wtr).ok();
}
/// 各レベル毎で最も高い検知数を出した日付を出力する
/// 各レベル毎で最も高い検知数を出したコンピュータ名を出力する
fn _print_detection_summary_by_computer(
detect_counts_by_computer: HashMap<String, HashMap<String, i128>>,
color_map: &HashMap<String, Colors>,
html_output_stock: &mut Vec<String>,
) {
let buf_wtr = BufferWriter::stdout(ColorChoice::Always);
let mut wtr = buf_wtr.buffer();
@@ -700,6 +743,22 @@ fn _print_detection_summary_by_computer(
sorted_detections.sort_by(|a, b| (-a.1).cmp(&(-b.1)));
// html出力は各種すべてのコンピュータ名を表示するようにする
if configs::CONFIG.read().unwrap().args.html_report.is_some() {
html_output_stock.push(format!(
"### Computers with most unique {} detections: {{#computers_with_most_unique_{}_detections}}",
LEVEL_FULL.get(level.as_str()).unwrap(),
LEVEL_FULL.get(level.as_str()).unwrap()
));
for x in sorted_detections.iter() {
html_output_stock.push(format!(
"- {} ({})",
x.0,
x.1.to_formatted_string(&Locale::en)
));
}
html_output_stock.push("".to_string());
}
for x in sorted_detections.iter().take(5) {
result_vec.push(format!(
"{} ({})",
@@ -733,6 +792,8 @@ fn _print_detection_summary_by_computer(
fn _print_detection_summary_tables(
detect_counts_by_rule_and_level: HashMap<String, HashMap<String, i128>>,
color_map: &HashMap<String, Colors>,
rule_title_path_map: HashMap<String, String>,
html_output_stock: &mut Vec<String>,
) {
let buf_wtr = BufferWriter::stdout(ColorChoice::Always);
let mut wtr = buf_wtr.buffer();
@@ -757,6 +818,27 @@ fn _print_detection_summary_tables(
sorted_detections.sort_by(|a, b| (-a.1).cmp(&(-b.1)));
// html出力の場合はすべての内容を出力するようにする
if configs::CONFIG.read().unwrap().args.html_report.is_some() {
html_output_stock.push(format!(
"### Top {} alerts: {{#top_{}_alerts}}",
LEVEL_FULL.get(level.as_str()).unwrap(),
LEVEL_FULL.get(level.as_str()).unwrap()
));
for x in sorted_detections.iter() {
html_output_stock.push(format!(
"- [{}]({}) ({})",
x.0,
rule_title_path_map
.get(x.0)
.unwrap_or(&"<Not Found Path>".to_string())
.replace('\\', "/"),
x.1.to_formatted_string(&Locale::en)
));
}
html_output_stock.push("".to_string());
}
let take_cnt =
if LEVEL_FULL.get(level.as_str()).unwrap_or(&"-".to_string()) == "informational" {
10
+5 -2
View File
@@ -1,6 +1,5 @@
use crate::detections::message::AlertMessage;
use crate::detections::pivot::PivotKeyword;
use crate::detections::pivot::PIVOT_KEYWORD;
use crate::detections::pivot::{PivotKeyword, PIVOT_KEYWORD};
use crate::detections::utils;
use chrono::{DateTime, Utc};
use clap::{App, CommandFactory, Parser};
@@ -248,6 +247,10 @@ pub struct Config {
/// Do not display result summary
#[clap(help_heading = Some("DISPLAY-SETTINGS"), long = "no-summary")]
pub no_summary: bool,
/// Save detail Results Summary in html (ex: results.html)
#[clap(help_heading = Some("OUTPUT"), short = 'H', long="html-report", value_name = "FILE")]
pub html_report: Option<PathBuf>,
}
impl ConfigReader<'_> {
+40 -21
View File
@@ -9,19 +9,15 @@ use chrono::{TimeZone, Utc};
use itertools::Itertools;
use termcolor::{BufferWriter, Color, ColorChoice};
use crate::detections::message::AlertMessage;
use crate::detections::message::DetectInfo;
use crate::detections::message::ERROR_LOG_STACK;
use crate::detections::message::{CH_CONFIG, DEFAULT_DETAILS, TAGS_CONFIG};
use crate::detections::message::{
LOGONSUMMARY_FLAG, METRICS_FLAG, PIVOT_KEYWORD_LIST_FLAG, QUIET_ERRORS_FLAG,
AlertMessage, DetectInfo, CH_CONFIG, DEFAULT_DETAILS, ERROR_LOG_STACK, LOGONSUMMARY_FLAG,
METRICS_FLAG, PIVOT_KEYWORD_LIST_FLAG, QUIET_ERRORS_FLAG, TAGS_CONFIG,
};
use crate::detections::pivot::insert_pivot_keyword;
use crate::detections::rule;
use crate::detections::rule::AggResult;
use crate::detections::rule::RuleNode;
use crate::detections::rule::{self, AggResult, RuleNode};
use crate::detections::utils::{get_serde_number_to_string, make_ascii_titlecase};
use crate::filter;
use crate::options::htmlreport::{self};
use crate::yaml::ParseYaml;
use hashbrown::HashMap;
use serde_json::Value;
@@ -31,8 +27,7 @@ use std::path::Path;
use std::sync::Arc;
use tokio::{runtime::Runtime, spawn, task::JoinHandle};
use super::message;
use super::message::LEVEL_ABBR;
use super::message::{self, LEVEL_ABBR};
// イベントファイルの1レコード分の情報を保持する構造体
#[derive(Clone, Debug)]
@@ -605,6 +600,7 @@ impl Detection {
let mut sorted_ld_rc: Vec<(&String, &u128)> = ld_rc.iter().collect();
sorted_ld_rc.sort_by(|a, b| a.0.cmp(b.0));
let args = &configs::CONFIG.read().unwrap().args;
let mut html_report_stock = Vec::new();
sorted_ld_rc.into_iter().for_each(|(key, value)| {
if value != &0_u128 {
@@ -614,12 +610,16 @@ impl Detection {
""
};
//タイトルに利用するものはascii文字であることを前提として1文字目を大文字にするように変更する
println!(
let output_str = format!(
"{} rules: {}{}",
make_ascii_titlecase(key.clone().as_mut()),
value,
disable_flag,
disable_flag
);
println!("{}", output_str);
if configs::CONFIG.read().unwrap().args.html_report.is_some() {
html_report_stock.push(format!("- {}", output_str));
}
}
});
if err_rc != &0_u128 {
@@ -644,20 +644,24 @@ impl Detection {
} else {
""
};
let output_str = format!(
"{} rules: {} ({:.2}%){}",
make_ascii_titlecase(key.clone().as_mut()),
value,
rate,
deprecated_flag
);
//タイトルに利用するものはascii文字であることを前提として1文字目を大文字にするように変更する
write_color_buffer(
&BufferWriter::stdout(ColorChoice::Always),
None,
&format!(
"{} rules: {} ({:.2}%){}",
make_ascii_titlecase(key.clone().as_mut()),
value,
rate,
deprecated_flag
),
&output_str,
true,
)
.ok();
if configs::CONFIG.read().unwrap().args.html_report.is_some() {
html_report_stock.push(format!("- {}", output_str));
}
}
});
println!();
@@ -665,17 +669,32 @@ impl Detection {
let mut sorted_rc: Vec<(&String, &u128)> = rc.iter().collect();
sorted_rc.sort_by(|a, b| a.0.cmp(b.0));
sorted_rc.into_iter().for_each(|(key, value)| {
let output_str = format!("{} rules: {}", key, value);
write_color_buffer(
&BufferWriter::stdout(ColorChoice::Always),
None,
&format!("{} rules: {}", key, value),
&output_str,
true,
)
.ok();
if configs::CONFIG.read().unwrap().args.html_report.is_some() {
html_report_stock.push(format!("- {}", output_str));
}
});
println!("Total enabled detection rules: {}", total_loaded_rule_cnt);
let tmp_total_detect_output =
format!("Total enabled detection rules: {}", total_loaded_rule_cnt);
println!("{}", tmp_total_detect_output);
println!();
if configs::CONFIG.read().unwrap().args.html_report.is_some() {
html_report_stock.push(format!("- {}", tmp_total_detect_output));
}
if !html_report_stock.is_empty() {
htmlreport::add_md_data(
"General Overview {#general_overview}".to_string(),
html_report_stock,
);
}
}
}
+4 -9
View File
@@ -1,9 +1,6 @@
extern crate lazy_static;
use crate::detections::configs;
use crate::detections::configs::CURRENT_EXE_PATH;
use crate::detections::utils;
use crate::detections::utils::get_serde_number_to_string;
use crate::detections::utils::write_color_buffer;
use crate::detections::configs::{self, CURRENT_EXE_PATH};
use crate::detections::utils::{self, get_serde_number_to_string, write_color_buffer};
use crate::options::profile::PROFILES;
use chrono::{DateTime, Local, Utc};
use dashmap::DashMap;
@@ -13,10 +10,8 @@ use linked_hash_map::LinkedHashMap;
use regex::Regex;
use serde_json::Value;
use std::env;
use std::fs::create_dir;
use std::fs::File;
use std::io::BufWriter;
use std::io::{self, Write};
use std::fs::{create_dir, File};
use std::io::{self, BufWriter, Write};
use std::path::Path;
use std::sync::Mutex;
use termcolor::{BufferWriter, ColorChoice};
+14 -6
View File
@@ -2,17 +2,15 @@ extern crate base64;
extern crate csv;
extern crate regex;
use crate::detections::configs;
use crate::detections::configs::CURRENT_EXE_PATH;
use crate::detections::configs::{self, CURRENT_EXE_PATH};
use hashbrown::HashMap;
use std::path::Path;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use chrono::Local;
use termcolor::Color;
use tokio::runtime::Builder;
use tokio::runtime::Runtime;
use tokio::runtime::{Builder, Runtime};
use chrono::{DateTime, TimeZone, Utc};
use regex::Regex;
@@ -28,6 +26,7 @@ use std::vec;
use termcolor::{BufferWriter, ColorSpec, WriteColor};
use super::detection::EvtxRecordInfo;
use super::message::AlertMessage;
pub fn concat_selection_key(key_list: &[String]) -> String {
return key_list
@@ -481,6 +480,15 @@ where
}
}
/// Check file path exist. If path is existed, output alert message.
pub fn check_file_expect_not_exist(path: &Path, exist_alert_str: String) -> bool {
let ret = path.exists();
if ret {
AlertMessage::alert(&exist_alert_str).ok();
}
ret
}
#[cfg(test)]
mod tests {
use std::path::Path;
+1 -3
View File
@@ -1,7 +1,5 @@
use crate::detections::configs;
use crate::detections::message::AlertMessage;
use crate::detections::message::ERROR_LOG_STACK;
use crate::detections::message::QUIET_ERRORS_FLAG;
use crate::detections::message::{AlertMessage, ERROR_LOG_STACK, QUIET_ERRORS_FLAG};
use hashbrown::HashMap;
use regex::Regex;
use std::fs::File;
+5
View File
@@ -6,3 +6,8 @@ pub mod omikuji;
pub mod options;
pub mod timeline;
pub mod yaml;
<<<<<<< HEAD
=======
#[macro_use]
extern crate horrorshow;
>>>>>>> d91fd31392813c79a33cf5dc10eae06db2ce2613
+140 -28
View File
@@ -7,8 +7,9 @@ use bytesize::ByteSize;
use chrono::{DateTime, Datelike, Local};
use evtx::{EvtxParser, ParserSettings};
use hashbrown::{HashMap, HashSet};
use hayabusa::detections::configs::{load_pivot_keywords, TargetEventTime, TARGET_EXTENSIONS};
use hayabusa::detections::configs::{CONFIG, CURRENT_EXE_PATH};
use hayabusa::detections::configs::{
load_pivot_keywords, TargetEventTime, CONFIG, CURRENT_EXE_PATH, TARGET_EXTENSIONS,
};
use hayabusa::detections::detection::{self, EvtxRecordInfo};
use hayabusa::detections::message::{
AlertMessage, ERROR_LOG_PATH, ERROR_LOG_STACK, LOGONSUMMARY_FLAG, METRICS_FLAG,
@@ -18,8 +19,9 @@ use hayabusa::detections::pivot::PivotKeyword;
use hayabusa::detections::pivot::PIVOT_KEYWORD;
use hayabusa::detections::rule::{get_detection_keys, RuleNode};
use hayabusa::omikuji::Omikuji;
use hayabusa::options::htmlreport::{self, HTML_REPORTER};
use hayabusa::options::profile::PROFILES;
use hayabusa::options::{level_tuning::LevelTuning, update_rules::UpdateRules};
use hayabusa::options::{level_tuning::LevelTuning, update::Update};
use hayabusa::{afterfact::after_fact, detections::utils};
use hayabusa::{detections::configs, timeline::timelines::Timeline};
use hayabusa::{detections::utils::write_color_buffer, filter};
@@ -91,6 +93,17 @@ impl App {
return;
}
let analysis_start_time: DateTime<Local> = Local::now();
if configs::CONFIG.read().unwrap().args.html_report.is_some() {
let output_data = vec![format!(
"- Start time: {}",
analysis_start_time.format("%Y/%m/%d %H:%M")
)];
htmlreport::add_md_data(
"General Overview {#general_overview}".to_string(),
output_data,
);
}
// Show usage when no arguments.
if std::env::args().len() == 1 {
self.output_logo();
@@ -107,7 +120,6 @@ impl App {
&analysis_start_time.day().to_owned()
));
}
if !self.is_matched_architecture_and_binary() {
AlertMessage::alert(
"The hayabusa version you ran does not match your PC architecture.\nPlease use the correct architecture. (Binary ending in -x64.exe for 64-bit and -x86.exe for 32-bit.)",
@@ -118,9 +130,19 @@ impl App {
}
if configs::CONFIG.read().unwrap().args.update_rules {
match UpdateRules::update_rules(
configs::CONFIG.read().unwrap().args.rules.to_str().unwrap(),
) {
// エラーが出た場合はインターネット接続がそもそもできないなどの問題点もあるためエラー等の出力は行わない
let latest_version_data = if let Ok(data) = Update::get_latest_hayabusa_version() {
data
} else {
None
};
let now_version = &format!(
"v{}",
configs::CONFIG.read().unwrap().app.get_version().unwrap()
);
match Update::update_rules(configs::CONFIG.read().unwrap().args.rules.to_str().unwrap())
{
Ok(output) => {
if output != "You currently have the latest rules." {
write_color_buffer(
@@ -137,6 +159,33 @@ impl App {
}
}
println!();
if latest_version_data.is_some()
&& now_version
!= &latest_version_data
.as_ref()
.unwrap_or(now_version)
.replace('\"', "")
{
write_color_buffer(
&BufferWriter::stdout(ColorChoice::Always),
None,
&format!(
"There is a new version of Hayabusa: {}",
latest_version_data.unwrap().replace('\"', "")
),
true,
)
.ok();
write_color_buffer(
&BufferWriter::stdout(ColorChoice::Always),
None,
"You can download it at https://github.com/Yamato-Security/hayabusa/releases",
true,
)
.ok();
}
println!();
return;
}
// 実行時のexeファイルのパスをベースに変更する必要があるためデフォルトの値であった場合はそのexeファイルと同一階層を探すようにする
@@ -170,20 +219,21 @@ impl App {
pivot_key_unions.iter().for_each(|(key, _)| {
let keywords_file_name =
csv_path.as_path().display().to_string() + "-" + key + ".txt";
if Path::new(&keywords_file_name).exists() {
AlertMessage::alert(&format!(
utils::check_file_expect_not_exist(
Path::new(&keywords_file_name),
format!(
" The file {} already exists. Please specify a different filename.",
&keywords_file_name
))
.ok();
}
),
);
});
if csv_path.exists() {
AlertMessage::alert(&format!(
if utils::check_file_expect_not_exist(
csv_path,
format!(
" The file {} already exists. Please specify a different filename.",
csv_path.as_os_str().to_str().unwrap()
))
.ok();
),
) {
return;
}
}
@@ -214,6 +264,29 @@ impl App {
println!();
}
if let Some(html_path) = &configs::CONFIG.read().unwrap().args.html_report {
// if already exists same html report file. output alert message and exit
if utils::check_file_expect_not_exist(
html_path.as_path(),
format!(
" The file {} already exists. Please specify a different filename.",
html_path.to_str().unwrap()
),
) {
return;
}
}
write_color_buffer(
&BufferWriter::stdout(ColorChoice::Always),
None,
&format!(
"Start time: {}\n",
analysis_start_time.format("%Y/%m/%d %H:%M")
),
true,
)
.ok();
if configs::CONFIG.read().unwrap().args.live_analysis {
let live_analysis_list = self.collect_liveanalysis_files();
if live_analysis_list.is_none() {
@@ -322,15 +395,22 @@ impl App {
let analysis_end_time: DateTime<Local> = Local::now();
let analysis_duration = analysis_end_time.signed_duration_since(analysis_start_time);
let elapsed_output_str = format!("Elapsed Time: {}", &analysis_duration.hhmmssxxx());
write_color_buffer(
&BufferWriter::stdout(ColorChoice::Always),
None,
&format!("Elapsed Time: {}", &analysis_duration.hhmmssxxx()),
&elapsed_output_str,
true,
)
.ok();
println!();
if configs::CONFIG.read().unwrap().args.html_report.is_some() {
let output_data = vec![format!("- {}", elapsed_output_str)];
htmlreport::add_md_data(
"General Overview {#general_overview}".to_string(),
output_data,
);
}
// Qオプションを付けた場合もしくはパースのエラーがない場合はerrorのstackが0となるのでエラーログファイル自体が生成されない。
if ERROR_LOG_STACK.lock().unwrap().len() > 0 {
AlertMessage::create_error_log(ERROR_LOG_PATH.to_string());
@@ -408,6 +488,22 @@ impl App {
});
}
}
if configs::CONFIG.read().unwrap().args.html_report.is_some() {
let html_str = HTML_REPORTER.read().unwrap().clone().create_html();
htmlreport::create_html_file(
html_str,
configs::CONFIG
.read()
.unwrap()
.args
.html_report
.as_ref()
.unwrap()
.to_str()
.unwrap_or("")
.to_string(),
)
}
}
#[cfg(not(target_os = "windows"))]
@@ -504,7 +600,6 @@ impl App {
}
}
}
fn analysis_files(&mut self, evtx_files: Vec<PathBuf>, time_filter: &TargetEventTime) {
let level = configs::CONFIG
.read()
@@ -525,11 +620,23 @@ impl App {
let meta = fs::metadata(file_path).ok();
total_file_size += ByteSize::b(meta.unwrap().len());
}
println!("Total file size: {}", total_file_size.to_string_as(false));
let total_size_output = format!("Total file size: {}", total_file_size.to_string_as(false));
println!("{}", total_size_output);
println!();
println!("Loading detections rules. Please wait.");
println!();
if configs::CONFIG.read().unwrap().args.html_report.is_some() {
let output_data = vec![
format!("- Analyzed event files: {}", evtx_files.len()),
format!("- {}", total_size_output),
];
htmlreport::add_md_data(
"General Overview #{general_overview}".to_string(),
output_data,
);
}
let rule_files = detection::Detection::parse_rule_files(
level,
&configs::CONFIG.read().unwrap().args.rules,
@@ -549,15 +656,23 @@ impl App {
self.rule_keys = self.get_all_keys(&rule_files);
let mut detection = detection::Detection::new(rule_files);
let mut total_records: usize = 0;
let mut tl = Timeline::new();
for evtx_file in evtx_files {
if configs::CONFIG.read().unwrap().args.verbose {
println!("Checking target evtx FilePath: {:?}", &evtx_file);
}
let cnt_tmp: usize;
(detection, cnt_tmp) = self.analysis_file(evtx_file, detection, time_filter);
(detection, cnt_tmp, tl) =
self.analysis_file(evtx_file, detection, time_filter, tl.clone());
total_records += cnt_tmp;
pb.inc();
}
if *METRICS_FLAG {
tl.tm_stats_dsp_msg();
}
if *LOGONSUMMARY_FLAG {
tl.tm_logon_stats_dsp_msg();
}
if configs::CONFIG.read().unwrap().args.output.is_some() {
println!();
println!();
@@ -576,15 +691,15 @@ impl App {
evtx_filepath: PathBuf,
mut detection: detection::Detection,
time_filter: &TargetEventTime,
) -> (detection::Detection, usize) {
mut tl: Timeline,
) -> (detection::Detection, usize, Timeline) {
let path = evtx_filepath.display();
let parser = self.evtx_to_jsons(evtx_filepath.clone());
let mut record_cnt = 0;
if parser.is_none() {
return (detection, record_cnt);
return (detection, record_cnt, tl);
}
let mut tl = Timeline::new();
let mut parser = parser.unwrap();
let mut records = parser.records_json_value();
@@ -653,10 +768,7 @@ impl App {
}
}
tl.tm_stats_dsp_msg();
tl.tm_logon_stats_dsp_msg();
(detection, record_cnt)
(detection, record_cnt, tl)
}
async fn create_rec_infos(
+165
View File
@@ -0,0 +1,165 @@
use hashbrown::HashMap;
use horrorshow::helper::doctype;
use horrorshow::prelude::*;
use lazy_static::lazy_static;
use pulldown_cmark::{html, Options, Parser};
use std::fs::{create_dir, File};
use std::io::{BufWriter, Write};
use std::path::Path;
use std::sync::RwLock;
lazy_static! {
pub static ref HTML_REPORTER: RwLock<HtmlReporter> = RwLock::new(HtmlReporter::new());
}
#[derive(Clone)]
pub struct HtmlReporter {
pub section_order: Vec<String>,
pub md_datas: HashMap<String, Vec<String>>,
}
impl HtmlReporter {
pub fn new() -> HtmlReporter {
let (init_section_order, init_data) = get_init_md_data_map();
HtmlReporter {
section_order: init_section_order,
md_datas: init_data,
}
}
/// return converted String from md_data(markdown fmt string).
pub fn create_html(self) -> String {
let mut options = Options::empty();
options.insert(Options::ENABLE_TABLES);
options.insert(Options::ENABLE_HEADING_ATTRIBUTES);
options.insert(Options::ENABLE_FOOTNOTES);
let mut md_data = vec![];
for section_name in self.section_order {
if let Some(v) = self.md_datas.get(&section_name) {
md_data.push(format!("## {}\n", &section_name));
if v.is_empty() {
md_data.push("not found data.\n".to_string());
} else {
md_data.push(v.join("\n"));
}
}
}
let md_str = md_data.join("\n");
let parser = Parser::new_ext(&md_str, options);
let mut ret = String::new();
html::push_html(&mut ret, parser);
ret
}
}
impl Default for HtmlReporter {
fn default() -> Self {
Self::new()
}
}
/// get html report section data in LinkedHashMap
fn get_init_md_data_map() -> (Vec<String>, HashMap<String, Vec<String>>) {
let mut ret = HashMap::new();
let section_order = vec![
"General Overview {#general_overview}".to_string(),
"Results Summary {#results_summary}".to_string(),
];
for section in section_order.iter() {
ret.insert(section.to_owned(), vec![]);
}
(section_order, ret)
}
pub fn add_md_data(section_name: String, data: Vec<String>) {
let mut md_with_section_data = HTML_REPORTER.write().unwrap().md_datas.clone();
for c in data {
let entry = md_with_section_data
.entry(section_name.clone())
.or_insert(Vec::new());
entry.push(c);
}
HTML_REPORTER.write().unwrap().md_datas = md_with_section_data;
}
/// create html file
pub fn create_html_file(input_html: String, path_str: String) {
let path = Path::new(&path_str);
if !path.parent().unwrap().exists() {
create_dir(path.parent().unwrap()).ok();
}
let mut html_writer = BufWriter::new(File::create(path).unwrap());
let html_data = format!(
"{}",
html! {
: doctype::HTML;
html {
head {
meta(charset="UTF-8");
link(rel="stylesheet", type="text/css", href="./config/html_report/hayabusa_report.css");
link(rel="icon", type="image/png", href="./config/html_report/favicon.png");
}
body {
section {
img(id="logo", src = "./config/html_report/logo.png");
: Raw(input_html.as_str());
}
}
}
}
);
writeln!(html_writer, "{}", html_data).ok();
println!(
"HTML Report was generated. Please check {} for details.",
path_str
);
println!();
}
#[cfg(test)]
mod tests {
use crate::options::htmlreport::HtmlReporter;
#[test]
fn test_create_html() {
let mut html_reporter = HtmlReporter::new();
let general_data = vec![
"- Analyzed event files: 581".to_string(),
"- Total file size: 148.5 MB".to_string(),
"- Excluded rules: 12".to_string(),
"- Noisy rules: 5 (Disabled)".to_string(),
"- Experimental rules: 1935 (65.97%)".to_string(),
"- Stable rules: 215 (7.33%)".to_string(),
"- Test rules: 783 (26.70%)".to_string(),
"- Hayabusa rules: 138".to_string(),
"- Sigma rules: 2795".to_string(),
"- Total enabled detection rules: 2933".to_string(),
"- Elapsed Time: 00:00:29.035".to_string(),
"".to_string(),
];
html_reporter.md_datas.insert(
"General Overview {#general_overview}".to_string(),
general_data.clone(),
);
let general_overview_str = format!(
"<ul>\n<li>{}</li>\n</ul>",
general_data[..general_data.len() - 1]
.join("</li>\n<li>")
.replace("- ", "")
);
let expect_str = format!(
"<h2 id=\"general_overview\">General Overview</h2>\n{}\n<h2 id=\"results_summary\">Results Summary</h2>\n<p>not found data.</p>\n",
general_overview_str
);
assert_eq!(html_reporter.create_html(), expect_str);
}
}
+2 -1
View File
@@ -1,3 +1,4 @@
pub mod htmlreport;
pub mod level_tuning;
pub mod profile;
pub mod update_rules;
pub mod update;
@@ -4,7 +4,8 @@ use crate::filter;
use crate::yaml::ParseYaml;
use chrono::{DateTime, Local, TimeZone};
use git2::Repository;
use std::fs::{self};
use serde_json::Value;
use std::fs::{self, create_dir};
use std::path::Path;
use hashbrown::{HashMap, HashSet};
@@ -12,13 +13,28 @@ use std::cmp::Ordering;
use std::time::SystemTime;
use std::fs::create_dir;
use termcolor::{BufferWriter, ColorChoice};
pub struct UpdateRules {}
pub struct Update {}
impl Update {
/// get latest hayabusa version number.
pub fn get_latest_hayabusa_version() -> Result<Option<String>, Box<dyn std::error::Error>> {
let res = reqwest::blocking::Client::new()
.get("https://api.github.com/repos/Yamato-Security/hayabusa/releases/latest")
.header("User-Agent", "HayabusaUpdateChecker")
.header("Accept", "application/vnd.github.v3+json")
.send()?;
let text = res.text()?;
let json_res: Value = serde_json::from_str(&text)?;
if json_res["tag_name"].is_null() {
Ok(None)
} else {
Ok(Some(json_res["tag_name"].to_string()))
}
}
impl UpdateRules {
/// update rules(hayabusa-rules subrepository)
pub fn update_rules(rule_path: &str) -> Result<String, git2::Error> {
let mut result;
@@ -35,14 +51,14 @@ impl UpdateRules {
)
.ok();
// execution git clone of hayabusa-rules repository when failed open hayabusa repository.
result = UpdateRules::clone_rules(Path::new(rule_path));
result = Update::clone_rules(Path::new(rule_path));
} else if hayabusa_rule_repo.is_ok() {
// case of exist hayabusa-rules repository
UpdateRules::_repo_main_reset_hard(hayabusa_rule_repo.as_ref().unwrap())?;
Update::_repo_main_reset_hard(hayabusa_rule_repo.as_ref().unwrap())?;
// case of failed fetching origin/main, git clone is not executed so network error has occurred possibly.
prev_modified_rules = UpdateRules::get_updated_rules(rule_path, &prev_modified_time);
prev_modified_rules = Update::get_updated_rules(rule_path, &prev_modified_time);
prev_modified_time = fs::metadata(rule_path).unwrap().modified().unwrap();
result = UpdateRules::pull_repository(&hayabusa_rule_repo.unwrap());
result = Update::pull_repository(&hayabusa_rule_repo.unwrap());
} else {
// case of no exist hayabusa-rules repository in rules.
// execute update because submodule information exists if hayabusa repository exists submodule information.
@@ -61,7 +77,7 @@ impl UpdateRules {
for mut submodule in submodules {
submodule.update(true, None)?;
let submodule_repo = submodule.open()?;
if let Err(e) = UpdateRules::pull_repository(&submodule_repo) {
if let Err(e) = Update::pull_repository(&submodule_repo) {
AlertMessage::alert(&format!("Failed submodule update. {}", e)).ok();
is_success_submodule_update = false;
}
@@ -80,16 +96,13 @@ impl UpdateRules {
)
.ok();
// execution git clone of hayabusa-rules repository when failed open hayabusa repository.
result = UpdateRules::clone_rules(rules_path);
result = Update::clone_rules(rules_path);
}
}
if result.is_ok() {
let updated_modified_rules =
UpdateRules::get_updated_rules(rule_path, &prev_modified_time);
result = UpdateRules::print_diff_modified_rule_dates(
prev_modified_rules,
updated_modified_rules,
);
let updated_modified_rules = Update::get_updated_rules(rule_path, &prev_modified_time);
result =
Update::print_diff_modified_rule_dates(prev_modified_rules, updated_modified_rules);
}
result
}
@@ -254,7 +267,7 @@ impl UpdateRules {
#[cfg(test)]
mod tests {
use crate::options::update_rules::UpdateRules;
use crate::options::update::Update;
use std::time::SystemTime;
#[test]
@@ -262,12 +275,12 @@ mod tests {
let prev_modified_time: SystemTime = SystemTime::UNIX_EPOCH;
let prev_modified_rules =
UpdateRules::get_updated_rules("test_files/rules/level_yaml", &prev_modified_time);
Update::get_updated_rules("test_files/rules/level_yaml", &prev_modified_time);
assert_eq!(prev_modified_rules.len(), 5);
let target_time: SystemTime = SystemTime::now();
let prev_modified_rules2 =
UpdateRules::get_updated_rules("test_files/rules/level_yaml", &target_time);
Update::get_updated_rules("test_files/rules/level_yaml", &target_time);
assert_eq!(prev_modified_rules2.len(), 0);
}
}
+51 -79
View File
@@ -2,25 +2,13 @@ use crate::detections::message::{LOGONSUMMARY_FLAG, METRICS_FLAG};
use crate::detections::{detection::EvtxRecordInfo, utils};
use hashbrown::HashMap;
#[derive(Debug)]
pub struct LogEventInfo {
pub channel: String,
pub eventid: String,
}
impl LogEventInfo {
pub fn new(channel: String, eventid: String) -> LogEventInfo {
LogEventInfo { channel, eventid }
}
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct EventMetrics {
pub total: usize,
pub filepath: String,
pub start_time: String,
pub end_time: String,
pub stats_list: HashMap<String, usize>,
pub stats_list: HashMap<(String, String), usize>,
pub stats_login_list: HashMap<String, [usize; 2]>,
}
/**
@@ -32,7 +20,7 @@ impl EventMetrics {
filepath: String,
start_time: String,
end_time: String,
stats_list: HashMap<String, usize>,
stats_list: HashMap<(String, String), usize>,
stats_login_list: HashMap<String, [usize; 2]>,
) -> EventMetrics {
EventMetrics {
@@ -78,87 +66,71 @@ impl EventMetrics {
self.filepath = records[0].evtx_filepath.as_str().to_owned();
// sortしなくてもイベントログのTimeframeを取得できるように修正しました。
// sortしないことにより計算量が改善されています。
// もうちょっと感じに書けるといえば書けます。
for record in records.iter() {
let evttime = utils::get_event_value(
if let Some(evttime) = utils::get_event_value(
"Event.System.TimeCreated_attributes.SystemTime",
&record.record,
)
.map(|evt_value| evt_value.to_string());
if evttime.is_none() {
continue;
}
let evttime = evttime.unwrap();
if self.start_time.is_empty() || evttime < self.start_time {
self.start_time = evttime.to_string();
}
if self.end_time.is_empty() || evttime > self.end_time {
self.end_time = evttime;
}
.map(|evt_value| evt_value.to_string())
{
if self.start_time.is_empty() || evttime < self.start_time {
self.start_time = evttime.to_string();
}
if self.end_time.is_empty() || evttime > self.end_time {
self.end_time = evttime;
}
};
}
self.total += records.len();
}
// EventIDで集計
/// EventID`で集計
fn stats_eventid(&mut self, records: &[EvtxRecordInfo]) {
// let mut evtstat_map = HashMap::new();
for record in records.iter() {
let channel = utils::get_event_value("Channel", &record.record);
let evtid = utils::get_event_value("EventID", &record.record);
if channel.is_none() {
continue;
}
if evtid.is_none() {
continue;
}
let ch = channel.unwrap().to_string();
let id = evtid.unwrap().to_string();
let mut chandid = ch + "," + &id;
chandid.retain(|c| c != '"');
//let logdata = LogEventInfo::new(ch , id);
//println!("{:?},{:?}", logdata.channel, logdata.eventid);
let count: &mut usize = self.stats_list.entry(chandid).or_insert(0);
*count += 1;
let channel = if let Some(ch) = utils::get_event_value("Channel", &record.record) {
ch.to_string()
} else {
"-".to_string()
};
if let Some(idnum) = utils::get_event_value("EventID", &record.record) {
let count: &mut usize = self
.stats_list
.entry((idnum.to_string(), channel))
.or_insert(0);
*count += 1;
};
}
// return evtstat_map;
}
// Login event
fn stats_login_eventid(&mut self, records: &[EvtxRecordInfo]) {
for record in records.iter() {
let evtid = utils::get_event_value("EventID", &record.record);
if evtid.is_none() {
continue;
}
let idnum: i64 = if evtid.unwrap().is_number() {
evtid.unwrap().as_i64().unwrap()
} else {
evtid
.unwrap()
.as_str()
.unwrap()
.parse::<i64>()
.unwrap_or_default()
};
if !(idnum == 4624 || idnum == 4625) {
continue;
}
if let Some(evtid) = utils::get_event_value("EventID", &record.record) {
let idnum: i64 = if evtid.is_number() {
evtid.as_i64().unwrap()
} else {
evtid.as_str().unwrap().parse::<i64>().unwrap_or_default()
};
if !(idnum == 4624 || idnum == 4625) {
continue;
}
let username = utils::get_event_value("TargetUserName", &record.record);
let countlist: [usize; 2] = [0, 0];
if idnum == 4624 {
let count: &mut [usize; 2] = self
.stats_login_list
.entry(username.unwrap().to_string())
.or_insert(countlist);
count[0] += 1;
} else if idnum == 4625 {
let count: &mut [usize; 2] = self
.stats_login_list
.entry(username.unwrap().to_string())
.or_insert(countlist);
count[1] += 1;
}
let username = utils::get_event_value("TargetUserName", &record.record);
let countlist: [usize; 2] = [0, 0];
if idnum == 4624 {
let count: &mut [usize; 2] = self
.stats_login_list
.entry(username.unwrap().to_string())
.or_insert(countlist);
count[0] += 1;
} else if idnum == 4625 {
let count: &mut [usize; 2] = self
.stats_login_list
.entry(username.unwrap().to_string())
.or_insert(countlist);
count[1] += 1;
}
};
}
}
}
+131 -50
View File
@@ -1,11 +1,18 @@
use crate::detections::message::{LOGONSUMMARY_FLAG, METRICS_FLAG};
use std::fs::File;
use std::io::BufWriter;
use crate::detections::message::{AlertMessage, CH_CONFIG, LOGONSUMMARY_FLAG, METRICS_FLAG};
use crate::detections::{configs::CONFIG, detection::EvtxRecordInfo};
use comfy_table::modifiers::UTF8_ROUND_CORNERS;
use comfy_table::presets::UTF8_FULL;
use comfy_table::*;
use csv::WriterBuilder;
use downcast_rs::__std::process;
use super::metrics::EventMetrics;
use hashbrown::HashMap;
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct Timeline {
pub stats: EventMetrics,
}
@@ -41,21 +48,62 @@ impl Timeline {
}
// 出力メッセージ作成
let mut sammsges: Vec<String> = Vec::new();
sammsges.push("---------------------------------------".to_string());
sammsges.push(format!("Evtx File Path: {}", self.stats.filepath));
sammsges.push(format!("Total Event Records: {}\n", self.stats.total));
sammsges.push(format!("First Timestamp: {}", self.stats.start_time));
sammsges.push(format!("Last Timestamp: {}\n", self.stats.end_time));
let total_event_record = format!("\nTotal Event Records: {}\n", self.stats.total);
if CONFIG.read().unwrap().args.filepath.is_some() {
sammsges.push(format!("Evtx File Path: {}", self.stats.filepath));
sammsges.push(total_event_record);
sammsges.push(format!("First Timestamp: {}", self.stats.start_time));
sammsges.push(format!("Last Timestamp: {}\n", self.stats.end_time));
} else {
sammsges.push(total_event_record);
}
let header = vec!["Count", "Percent", "Channel", "ID", "Event"];
let target;
let mut wtr = if let Some(csv_path) = &CONFIG.read().unwrap().args.output {
// output to file
match File::create(csv_path) {
Ok(file) => {
target = Box::new(BufWriter::new(file));
Some(WriterBuilder::new().from_writer(target))
}
Err(err) => {
AlertMessage::alert(&format!("Failed to open file. {}", err)).ok();
process::exit(1);
}
}
} else {
None
};
if let Some(ref mut w) = wtr {
w.write_record(&header).ok();
}
let mut stats_tb = Table::new();
stats_tb
.load_preset(UTF8_FULL)
.apply_modifier(UTF8_ROUND_CORNERS);
stats_tb.set_header(header);
// 集計件数でソート
let mut mapsorted: Vec<_> = self.stats.stats_list.iter().collect();
mapsorted.sort_by(|x, y| y.1.cmp(x.1));
// イベントID毎の出力メッセージ生成
let stats_msges: Vec<Vec<String>> = self.tm_stats_set_msg(mapsorted);
for msgprint in sammsges.iter() {
println!("{}", msgprint);
}
// イベントID毎の出力メッセージ生成
self.tm_stats_set_msg(mapsorted);
if CONFIG.read().unwrap().args.output.is_some() {
for msg in stats_msges.iter() {
if let Some(ref mut w) = wtr {
w.write_record(msg).ok();
}
}
}
stats_tb.add_rows(stats_msges);
println!("{stats_tb}");
}
pub fn tm_logon_stats_dsp_msg(&mut self) {
@@ -64,12 +112,15 @@ impl Timeline {
}
// 出力メッセージ作成
let mut sammsges: Vec<String> = Vec::new();
sammsges.push("---------------------------------------".to_string());
sammsges.push(format!("Evtx File Path: {}", self.stats.filepath));
sammsges.push(format!("Total Event Records: {}\n", self.stats.total));
sammsges.push(format!("First Timestamp: {}", self.stats.start_time));
sammsges.push(format!("Last Timestamp: {}\n", self.stats.end_time));
sammsges.push("---------------------------------------".to_string());
let total_event_record = format!("\nTotal Event Records: {}\n", self.stats.total);
if CONFIG.read().unwrap().args.filepath.is_some() {
sammsges.push(format!("Evtx File Path: {}", self.stats.filepath));
sammsges.push(total_event_record);
sammsges.push(format!("First Timestamp: {}", self.stats.start_time));
sammsges.push(format!("Last Timestamp: {}\n", self.stats.end_time));
} else {
sammsges.push(total_event_record);
}
for msgprint in sammsges.iter() {
println!("{}", msgprint);
}
@@ -78,11 +129,13 @@ impl Timeline {
}
// イベントID毎の出力メッセージ生成
fn tm_stats_set_msg(&self, mapsorted: Vec<(&std::string::String, &usize)>) {
let mut eid_metrics_tb = Table::new();
eid_metrics_tb.set_header(vec!["Count", "Percent(%)", "channel,ID", "Eventtitle"]);
fn tm_stats_set_msg(
&self,
mapsorted: Vec<(&(std::string::String, std::string::String), &usize)>,
) -> Vec<Vec<String>> {
let mut msges: Vec<Vec<String>> = Vec::new();
for (event_id, event_cnt) in mapsorted.iter() {
for ((event_id, channel), event_cnt) in mapsorted.iter() {
// 件数の割合を算出
let rate: f32 = **event_cnt as f32 / self.stats.total as f32;
@@ -98,38 +151,44 @@ impl Timeline {
.read()
.unwrap()
.event_timeline_config
.get_event_id(*event_id)
.get_event_id(event_id)
.is_some();
// event_id_info.txtに登録あるものは情報設定
// 出力メッセージ1行作成
let fmted_channel = channel.replace('\"', "");
let ch = CH_CONFIG
.get(fmted_channel.to_lowercase().as_str())
.unwrap_or(&fmted_channel)
.to_string();
if conf {
eid_metrics_tb.add_row(vec![
Cell::new(&event_cnt),
Cell::new(&rate),
Cell::new(&event_id),
Cell::new(
&CONFIG
.read()
.unwrap()
.event_timeline_config
.get_event_id(*event_id)
.unwrap()
.evttitle,
),
msges.push(vec![
event_cnt.to_string(),
format!("{:.1}%", (rate * 1000.0).round() / 10.0),
ch,
event_id.to_string(),
CONFIG
.read()
.unwrap()
.event_timeline_config
.get_event_id(event_id)
.unwrap()
.evttitle
.to_string(),
]);
} else {
// 出力メッセージ1行作成
eid_metrics_tb.add_row(vec![
Cell::new(&event_cnt),
Cell::new(&rate),
Cell::new(&event_id),
Cell::new(&"Unknown".to_string()),
msges.push(vec![
event_cnt.to_string(),
format!("{:.1}%", (rate * 1000.0).round() / 10.0),
ch,
event_id.replace('\"', ""),
"Unknown".to_string(),
]);
}
}
println!("{eid_metrics_tb}");
println!();
msges
}
// ユーザ毎のログイン統計情報出力メッセージ生成
/// ユーザ毎のログイン統計情報出力メッセージ生成
fn tm_loginstats_tb_set_msg(&self) {
println!("Logon Summary");
if self.stats.stats_login_list.is_empty() {
@@ -141,23 +200,45 @@ impl Timeline {
println!("{}", msgprint);
}
} else {
let header = vec!["User", "Failed", "Successful"];
let target;
let mut wtr = if let Some(csv_path) = &CONFIG.read().unwrap().args.output {
// output to file
match File::create(csv_path) {
Ok(file) => {
target = Box::new(BufWriter::new(file));
Some(WriterBuilder::new().from_writer(target))
}
Err(err) => {
AlertMessage::alert(&format!("Failed to open file. {}", err)).ok();
process::exit(1);
}
}
} else {
None
};
if let Some(ref mut w) = wtr {
w.write_record(&header).ok();
}
let mut logins_stats_tb = Table::new();
logins_stats_tb.set_header(vec!["User", "Failed", "Successful"]);
logins_stats_tb
.load_preset(UTF8_FULL)
.apply_modifier(UTF8_ROUND_CORNERS);
logins_stats_tb.set_header(&header);
// 集計件数でソート
let mut mapsorted: Vec<_> = self.stats.stats_login_list.iter().collect();
mapsorted.sort_by(|x, y| x.0.cmp(y.0));
for (key, values) in &mapsorted {
let mut username: String = key.to_string();
//key.to_string().retain(|c| c != '\"');
//key.to_string().pop();
username.pop();
username.remove(0);
logins_stats_tb.add_row(vec![
Cell::new(&username),
Cell::new(&values[1].to_string()),
Cell::new(&values[0].to_string()),
]);
let record_data = vec![username, values[1].to_string(), values[0].to_string()];
if let Some(ref mut w) = wtr {
w.write_record(&record_data).ok();
}
logins_stats_tb.add_row(record_data);
}
println!("{logins_stats_tb}");
println!();