Merge pull request #709 from Yamato-Security/689-new-feature-html-summary-output

Added html summary output
This commit is contained in:
DustInDark
2022-09-27 21:59:35 +09:00
committed by GitHub
16 changed files with 413 additions and 43 deletions

5
.gitignore vendored
View File

@@ -9,4 +9,7 @@ test_*
*.csv
*.json
*.jsonl
hayabusa*
hayabusa*
*.html
*.htm
*.css

View File

@@ -4,6 +4,8 @@
**新機能:**
- HTMLレポート機能の追加。 (#689) (@hitenkoku)
**改善:**
- EventID解析のオプションをmetricsオプションに変更した。(旧: -s -> 新: -M) (#706) (@hitenkoku)

View File

@@ -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
View File

@@ -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"

View File

@@ -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.*"

View File

@@ -393,6 +393,7 @@ ADVANCED:
--target-file-ext <EVTX_FILE_EXT>... evtx以外の拡張子を解析対象に追加する。 (例1: evtx_data 例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)

View File

@@ -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
View File

@@ -0,0 +1,4 @@
h2 {
background-color: blue;
color: white;
}

2
rules

Submodule rules updated: 2b0f88d1c0...aaf910cdca

View File

@@ -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

View File

@@ -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<'_> {

View File

@@ -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,
);
}
}
}

View File

@@ -8,3 +8,5 @@ pub mod timeline;
pub mod yaml;
#[macro_use]
extern crate prettytable;
#[macro_use]
extern crate horrorshow;

View File

@@ -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
View 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(&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="./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);
}
}

View File

@@ -1,3 +1,4 @@
pub mod htmlreport;
pub mod level_tuning;
pub mod profile;
pub mod update;