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:
@@ -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]
|
||||
|
||||
|
||||
@@ -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
66
Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -30,7 +30,6 @@ hashbrown = "0.12.*"
|
||||
hex = "0.4.*"
|
||||
git2 = "0.*"
|
||||
termcolor = "*"
|
||||
prettytable-rs = "0.*"
|
||||
krapslog = "*"
|
||||
terminal_size = "*"
|
||||
bytesize = "1.*"
|
||||
|
||||
@@ -7,6 +7,4 @@ pub mod options;
|
||||
pub mod timeline;
|
||||
pub mod yaml;
|
||||
#[macro_use]
|
||||
extern crate prettytable;
|
||||
#[macro_use]
|
||||
extern crate horrorshow;
|
||||
|
||||
21
src/main.rs
21
src/main.rs
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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!();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user