Merge branch 'main'
This commit is contained in:
+114
-32
@@ -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
|
||||
|
||||
@@ -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
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
@@ -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(§ion_name) {
|
||||
md_data.push(format!("## {}\n", §ion_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
@@ -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
@@ -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
@@ -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!();
|
||||
|
||||
Reference in New Issue
Block a user