Merge pull request #720 from Yamato-Security/707-analyze-metrics-of-event-ids-when-scanning-directory-together

Unified table of  analyze metrics and logon summary of event ids when scanning directory together
This commit is contained in:
Yamato Security
2022-09-29 07:52:21 +09:00
committed by GitHub
8 changed files with 204 additions and 188 deletions

View File

@@ -11,6 +11,12 @@
- EventID解析のオプションをmetricsオプションに変更した。(旧: `-s, --statistics` -> 新: `-M, --metrics`) (#706) (@hitenkoku)
- ルール更新オプション(`-u`)を利用したときにHayabusaの新バージョンがないかを確認し、表示するようにした。 (#710) (@hitenkoku)
- HTMLレポート内にロゴを追加した。 (#714) (@hitenkoku)
- メトリクスオプション(`-M --metrics`)もしくはログオン情報(`-L --logon-summary`)と`-d`オプションを利用した場合に1つのテーブルで表示されるように修正した。 (#707) (@hitenkoku)
- メトリクスオプションの結果出力にチャンネル列を追加した。 (#707) (@hitenkoku)
- メトリクスオプション(`-M --metrics`)もしくはログオン情報(`-L --logon-summary`)と`-d`オプションを利用した場合に「First Timestamp」と「Last Timestamp」の出力を行わないように修正した。 (#707) (@hitenkoku)
- メトリクスオプションとログオン情報オプションに対してcsv出力機能(`-o --output`)を追加した。 (#707) (@hitenkoku)
- メトリクスオプションの出力を検出回数と全体の割合が1つのセルで表示されていた箇所を2つの列に分けた。 (#707) (@hitenkoku)
- メトリクスオプションとログオン情報の画面出力に利用していたprettytable-rsクレートをcomfy_tableクレートに修正した. (#707) (@hitenkoku)
## v1.6.0 [2022/09/16]

View File

@@ -12,6 +12,12 @@
(Note: `statistics_event_info.txt` was changed to `event_id_info.txt`.)
- Display new version of Hayabusa link when updating if there is a newer version. (#710) (@hitenkoku)
- Added logo in HTML summary output. (#714) (@hitenkoku)
- Unified output one table of -M or -L option with -d option. (#707) (@hitenkoku)
- Added Channel column to metrics output. (#707) (@hitenkoku)
- Removed First Timestamp and Last Timestamp of -M and -L option with -d option. (#707) (@hitenkoku)
- Added csv output option(`-o --output`) when -M and -L option is used. (#707) (@hitenkoku)
- Separated Count and Percent columns in metric output. (#707) (@hitenkoku)
- Changed output table format of metric option and logon information crate from prettytable-rs to comfy_table. (#707) (@hitenkoku)
## v1.6.0 [2022/09/16]

66
Cargo.lock generated
View File

@@ -248,7 +248,7 @@ version = "0.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89eab4d20ce20cea182308bca13088fecea9c05f6776cf287205d41a0ed3c847"
dependencies = [
"encode_unicode 0.3.6",
"encode_unicode",
"libc",
"once_cell",
"terminal_size",
@@ -403,27 +403,6 @@ dependencies = [
"zeroize",
]
[[package]]
name = "dirs-next"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
dependencies = [
"cfg-if",
"dirs-sys-next",
]
[[package]]
name = "dirs-sys-next"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
dependencies = [
"libc",
"redox_users",
"winapi",
]
[[package]]
name = "discard"
version = "1.0.4"
@@ -448,12 +427,6 @@ version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
[[package]]
name = "encode_unicode"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
[[package]]
name = "encoding"
version = "0.2.33"
@@ -781,7 +754,6 @@ dependencies = [
"num_cpus",
"openssl",
"pbr",
"prettytable-rs",
"pulldown-cmark",
"quick-xml",
"rand",
@@ -1399,20 +1371,6 @@ version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
[[package]]
name = "prettytable-rs"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f375cb74c23b51d23937ffdeb48b1fbf5b6409d4b9979c1418c1de58bc8f801"
dependencies = [
"atty",
"csv",
"encode_unicode 1.0.0",
"lazy_static",
"term",
"unicode-width",
]
[[package]]
name = "proc-macro-error"
version = "1.0.4"
@@ -1551,17 +1509,6 @@ dependencies = [
"bitflags",
]
[[package]]
name = "redox_users"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
dependencies = [
"getrandom",
"redox_syscall",
"thiserror",
]
[[package]]
name = "regex"
version = "1.6.0"
@@ -1997,17 +1944,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "term"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f"
dependencies = [
"dirs-next",
"rustversion",
"winapi",
]
[[package]]
name = "termcolor"
version = "1.1.3"

View File

@@ -30,7 +30,6 @@ hashbrown = "0.12.*"
hex = "0.4.*"
git2 = "0.*"
termcolor = "*"
prettytable-rs = "0.*"
krapslog = "*"
terminal_size = "*"
bytesize = "1.*"

View File

@@ -7,6 +7,4 @@ pub mod options;
pub mod timeline;
pub mod yaml;
#[macro_use]
extern crate prettytable;
#[macro_use]
extern crate horrorshow;

View File

@@ -656,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!();
@@ -683,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();
@@ -760,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(

View File

@@ -2,13 +2,13 @@ use crate::detections::message::{LOGONSUMMARY_FLAG, METRICS_FLAG};
use crate::detections::{detection::EvtxRecordInfo, utils};
use hashbrown::HashMap;
#[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]>,
}
/**
@@ -20,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 {
@@ -66,79 +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 evtid = utils::get_event_value("EventID", &record.record);
if evtid.is_none() {
continue;
}
let idnum = evtid.unwrap();
let count: &mut usize = self.stats_list.entry(idnum.to_string()).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;
}
};
}
}
}

View File

@@ -1,11 +1,18 @@
use crate::detections::message::{LOGONSUMMARY_FLAG, METRICS_FLAG};
use std::fs::File;
use std::io::BufWriter;
use crate::detections::message::{AlertMessage, CH_CONFIG, LOGONSUMMARY_FLAG, METRICS_FLAG};
use crate::detections::{configs::CONFIG, detection::EvtxRecordInfo};
use prettytable::{Cell, Row, Table};
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,27 +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));
sammsges.push("Count (Percent)\tID\tEvent\t".to_string());
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);
}
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<String> = self.tm_stats_set_msg(mapsorted);
let stats_msges: Vec<Vec<String>> = self.tm_stats_set_msg(mapsorted);
for msgprint in sammsges.iter() {
println!("{}", msgprint);
}
for msgprint in stats_msges.iter() {
println!("{}", msgprint);
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) {
@@ -70,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);
}
@@ -84,10 +129,13 @@ impl Timeline {
}
// イベントID毎の出力メッセージ生成
fn tm_stats_set_msg(&self, mapsorted: Vec<(&std::string::String, &usize)>) -> Vec<String> {
let mut msges: Vec<String> = Vec::new();
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;
@@ -96,40 +144,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 {
// 出力メッセージ1行作成
msges.push(format!(
"{0} ({1:.1}%)\t{2}\t{3}",
event_cnt,
(rate * 1000.0).round() / 10.0,
event_id,
&CONFIG
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)
.get_event_id(event_id)
.unwrap()
.evttitle,
));
.evttitle
.to_string(),
]);
} else {
// 出力メッセージ1行作成
msges.push(format!(
"{0} ({1:.1}%)\t{2}\t{3}",
event_cnt,
(rate * 1000.0).round() / 10.0,
event_id,
"Unknown",
));
msges.push(vec![
event_cnt.to_string(),
format!("{:.1}%", (rate * 1000.0).round() / 10.0),
ch,
event_id.replace('\"', ""),
"Unknown".to_string(),
]);
}
}
msges.push("---------------------------------------".to_string());
msges
}
// ユーザ毎のログイン統計情報出力メッセージ生成
/// ユーザ毎のログイン統計情報出力メッセージ生成
fn tm_loginstats_tb_set_msg(&self) {
println!("Logon Summary");
if self.stats.stats_login_list.is_empty() {
@@ -141,25 +193,47 @@ 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_titles(row!["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(Row::new(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);
}
logins_stats_tb.printstd();
println!("{logins_stats_tb}");
println!();
}
}