Merge pull request #709 from Yamato-Security/689-new-feature-html-summary-output
Added html summary output
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -9,4 +9,7 @@ test_*
|
||||
*.csv
|
||||
*.json
|
||||
*.jsonl
|
||||
hayabusa*
|
||||
hayabusa*
|
||||
*.html
|
||||
*.htm
|
||||
*.css
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
**新機能:**
|
||||
|
||||
- HTMLレポート機能の追加。 (#689) (@hitenkoku)
|
||||
|
||||
**改善:**
|
||||
|
||||
- EventID解析のオプションをmetricsオプションに変更した。(旧: -s -> 新: -M) (#706) (@hitenkoku)
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
**New Features:**
|
||||
|
||||
- Added html summary output. (``-H, --html-report` option) (#689) (@hitenkoku)
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
- Changed Event ID Statistics option to Event ID Metrics option. (`-s, --statistics` -> `-M, --metrics`) (#706) (@hitenkoku)
|
||||
|
||||
8
Cargo.lock
generated
8
Cargo.lock
generated
@@ -769,6 +769,7 @@ dependencies = [
|
||||
"hashbrown",
|
||||
"hex",
|
||||
"hhmmss",
|
||||
"horrorshow",
|
||||
"hyper",
|
||||
"is_elevated",
|
||||
"itertools",
|
||||
@@ -781,6 +782,7 @@ dependencies = [
|
||||
"openssl",
|
||||
"pbr",
|
||||
"prettytable-rs",
|
||||
"pulldown-cmark",
|
||||
"quick-xml",
|
||||
"rand",
|
||||
"regex",
|
||||
@@ -826,6 +828,12 @@ dependencies = [
|
||||
"time 0.2.27",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "horrorshow"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8371fb981840150b1a54f7cb117bf6699f7466a1d4861daac33bc6fe2b5abea0"
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "0.2.8"
|
||||
|
||||
@@ -39,7 +39,9 @@ lock_api = "0.4.*"
|
||||
crossbeam-utils = "0.8.*"
|
||||
num-format = "*"
|
||||
comfy-table = "6.*"
|
||||
pulldown-cmark = { version = "0.9.*", default-features = false, features = ["simd"] }
|
||||
reqwest = {version = "0.11.*", features = ["blocking", "json"]}
|
||||
horrorshow = "0.8.*"
|
||||
|
||||
[build-dependencies]
|
||||
static_vcruntime = "2.*"
|
||||
|
||||
@@ -393,6 +393,7 @@ ADVANCED:
|
||||
--target-file-ext <EVTX_FILE_EXT>... evtx以外の拡張子を解析対象に追加する。 (例1: evtx_data 例2:evtx1 evtx2)
|
||||
|
||||
OUTPUT:
|
||||
-H, --html-report <FILE> HTML形式で詳細な結果を出力する (例: results.html)
|
||||
-j, --json タイムラインの出力をJSON形式で保存する(例: -j -o results.json)
|
||||
-J, --jsonl タイムラインの出力をJSONL形式で保存する (例: -J -o results.jsonl)
|
||||
-o, --output <FILE> タイムラインをCSV形式で保存する (例: results.csv)
|
||||
|
||||
@@ -384,10 +384,11 @@ ADVANCED:
|
||||
--target-file-ext <EVTX_FILE_EXT>... Specify additional target file extensions (ex: evtx_data) (ex: evtx1 evtx2)
|
||||
|
||||
OUTPUT:
|
||||
-j, --json Save the timeline in JSON format (ex: -j -o results.json)
|
||||
-J, --jsonl Save the timeline in JSONL format (ex: -J -o results.jsonl)
|
||||
-o, --output <FILE> Save the timeline in CSV format (ex: results.csv)
|
||||
-P, --profile <PROFILE> Specify output profile (minimal, standard, verbose, verbose-all-field-info, verbose-details-and-all-field-info)
|
||||
-H, --html-report <FILE> Save detail Results Summary in html (ex: results.html)
|
||||
-j, --json Save the timeline in JSON format (ex: -j -o results.json)
|
||||
-J, --jsonl Save the timeline in JSONL format (ex: -J -o results.jsonl)
|
||||
-o, --output <FILE> Save the timeline in CSV format (ex: results.csv)
|
||||
-P, --profile <PROFILE> Specify output profile (minimal, standard, verbose, verbose-all-field-info, verbose-details-and-all-field-info)
|
||||
|
||||
DISPLAY-SETTINGS:
|
||||
--no-color Disable color output
|
||||
|
||||
4
hayabusa_report.css
Normal file
4
hayabusa_report.css
Normal file
@@ -0,0 +1,4 @@
|
||||
h2 {
|
||||
background-color: blue;
|
||||
color: white;
|
||||
}
|
||||
2
rules
2
rules
Submodule rules updated: 2b0f88d1c0...aaf910cdca
130
src/afterfact.rs
130
src/afterfact.rs
@@ -4,6 +4,7 @@ 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::options::htmlreport;
|
||||
use crate::options::profile::PROFILES;
|
||||
use bytesize::ByteSize;
|
||||
use chrono::{DateTime, Local, TimeZone, Utc};
|
||||
@@ -210,6 +211,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 +241,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 +375,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 +458,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 +495,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 +513,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 +678,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 +709,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 +749,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 +798,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 +824,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
|
||||
|
||||
@@ -248,6 +248,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<'_> {
|
||||
|
||||
@@ -22,6 +22,7 @@ use crate::detections::rule::AggResult;
|
||||
use crate::detections::rule::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;
|
||||
@@ -605,6 +606,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 +616,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 +650,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 +675,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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,3 +8,5 @@ pub mod timeline;
|
||||
pub mod yaml;
|
||||
#[macro_use]
|
||||
extern crate prettytable;
|
||||
#[macro_use]
|
||||
extern crate horrorshow;
|
||||
|
||||
77
src/main.rs
77
src/main.rs
@@ -18,6 +18,7 @@ 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::Update};
|
||||
use hayabusa::{afterfact::after_fact, detections::utils};
|
||||
@@ -91,6 +92,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 +119,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.)",
|
||||
@@ -251,6 +262,28 @@ impl App {
|
||||
println!();
|
||||
}
|
||||
|
||||
if let Some(path) = &configs::CONFIG.read().unwrap().args.html_report {
|
||||
// if already exists same html report file. output alert message and exit
|
||||
if path.exists() {
|
||||
AlertMessage::alert(&format!(
|
||||
" The file {} already exists. Please specify a different filename.",
|
||||
path.to_str().unwrap()
|
||||
))
|
||||
.ok();
|
||||
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() {
|
||||
@@ -359,15 +392,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());
|
||||
@@ -445,6 +485,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"))]
|
||||
@@ -541,7 +597,6 @@ impl App {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn analysis_files(&mut self, evtx_files: Vec<PathBuf>, time_filter: &TargetEventTime) {
|
||||
let level = configs::CONFIG
|
||||
.read()
|
||||
@@ -562,11 +617,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,
|
||||
|
||||
160
src/options/htmlreport.rs
Normal file
160
src/options/htmlreport.rs
Normal file
@@ -0,0 +1,160 @@
|
||||
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;
|
||||
use std::fs::File;
|
||||
use std::io::BufWriter;
|
||||
use std::io::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="./hayabusa_report.css");
|
||||
link(rel="icon", type="image/png", href="./favicon.png");
|
||||
}
|
||||
body : Raw(input_html.clone().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);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod htmlreport;
|
||||
pub mod level_tuning;
|
||||
pub mod profile;
|
||||
pub mod update;
|
||||
|
||||
Reference in New Issue
Block a user