From 947f65a7bc9b86782a27e17beb6634868ecf219c Mon Sep 17 00:00:00 2001 From: DustInDark Date: Mon, 23 May 2022 21:32:49 +0900 Subject: [PATCH] output timeline histogram (#535) * added krapslog in cargo * added output timeline histgram feature #533 * added termilan_size to cargo * adjust timeline histgram width size to terminal width #533 * added section output in timeline histogram #533 * centering timeline histgram title #533 --- Cargo.lock | 116 +++++++++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 2 + src/afterfact.rs | 44 ++++++++++++++++++ 3 files changed, 158 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a265c20e..d7c820e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -271,12 +271,36 @@ dependencies = [ "ansi_term", "atty", "bitflags", - "strsim", - "textwrap", + "strsim 0.8.0", + "textwrap 0.11.0", "unicode-width", "vec_map", ] +[[package]] +name = "clap" +version = "3.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2dbdf4bdacb33466e854ce889eee8dfd5729abf7ccd7664d0a2d60cd384440b" +dependencies = [ + "atty", + "bitflags", + "clap_lex", + "indexmap", + "strsim 0.10.0", + "termcolor", + "textwrap 0.15.0", +] + +[[package]] +name = "clap_lex" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213" +dependencies = [ + "os_str_bytes", +] + [[package]] name = "cloudabi" version = "0.0.3" @@ -662,7 +686,7 @@ dependencies = [ "bitflags", "byteorder", "chrono", - "clap", + "clap 2.34.0", "crc", "dialoguer", "encoding", @@ -711,6 +735,16 @@ dependencies = [ "instant", ] +[[package]] +name = "file-chunker" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92828425bb36590609d28014a252d6f0f71b698875a2b131fae367c627b6c656" +dependencies = [ + "anyhow", + "memmap2", +] + [[package]] name = "flate2" version = "1.0.23" @@ -886,7 +920,7 @@ version = "1.2.2" dependencies = [ "base64 0.13.0", "chrono", - "clap", + "clap 2.34.0", "csv", "dotenv", "downcast-rs", @@ -897,6 +931,7 @@ dependencies = [ "hex 0.4.3", "hhmmss", "is_elevated", + "krapslog", "lazy_static", "linked-hash-map", "num_cpus", @@ -911,6 +946,7 @@ dependencies = [ "slack-hook", "static_vcruntime", "termcolor", + "terminal_size", "tokio 1.18.2", "yaml-rust", ] @@ -1059,6 +1095,18 @@ dependencies = [ "hashbrown 0.11.2", ] +[[package]] +name = "indicatif" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d207dc617c7a380ab07ff572a6e52fa202a2a8f355860ac9c38e23f8196be1b" +dependencies = [ + "console", + "lazy_static", + "number_prefix", + "regex", +] + [[package]] name = "indoc" version = "1.0.6" @@ -1144,6 +1192,27 @@ dependencies = [ "winapi-build", ] +[[package]] +name = "krapslog" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d4d54b2c8b875b6692487e5269cb66f12cd51af11fe1807f135ad0d6b771de" +dependencies = [ + "anyhow", + "atty", + "chrono", + "clap 3.1.18", + "file-chunker", + "indicatif", + "memmap2", + "num_cpus", + "progress-streams", + "rayon", + "regex", + "tempfile", + "terminal_size", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -1248,6 +1317,15 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "memmap2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "057a3db23999c867821a7a59feb06a578fcb03685e983dff90daf9e7d24ac08f" +dependencies = [ + "libc", +] + [[package]] name = "memoffset" version = "0.5.6" @@ -1403,6 +1481,12 @@ dependencies = [ "libc", ] +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + [[package]] name = "object" version = "0.28.4" @@ -1473,6 +1557,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "os_str_bytes" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "029d8d0b2f198229de29dca79676f2738ff952edf3fde542eb8bf94d8c21b435" + [[package]] name = "parking_lot" version = "0.9.0" @@ -1587,6 +1677,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "progress-streams" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e965d96c8162c607b0cd8d66047ad3c9fd35273c134d994327882c6e47f986a7" + [[package]] name = "publicsuffix" version = "1.5.6" @@ -2208,6 +2304,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "syn" version = "1.0.95" @@ -2284,6 +2386,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "textwrap" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" + [[package]] name = "thiserror" version = "1.0.31" diff --git a/Cargo.toml b/Cargo.toml index 68399b4e..6931b86b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,8 @@ hex = "0.4.*" git2="0.13" termcolor="*" prettytable-rs = "0.8" +krapslog="*" +terminal_size = "*" [target.'cfg(windows)'.dependencies] is_elevated = "0.1.2" diff --git a/src/afterfact.rs b/src/afterfact.rs index c59712bc..2986d832 100644 --- a/src/afterfact.rs +++ b/src/afterfact.rs @@ -5,6 +5,7 @@ use crate::detections::utils; use chrono::{DateTime, Local, TimeZone, Utc}; use csv::QuoteStyle; use hashbrown::HashMap; +use krapslog::{build_sparkline, build_time_markers}; use lazy_static::lazy_static; use serde::Serialize; use std::error::Error; @@ -14,6 +15,7 @@ use std::io::BufWriter; use std::io::Write; use std::process; use termcolor::{BufferWriter, Color, ColorChoice, ColorSpec, WriteColor}; +use terminal_size::{terminal_size, Width}; #[derive(Debug, Serialize)] #[serde(rename_all = "PascalCase")] @@ -98,6 +100,30 @@ fn _get_output_color(color_map: &HashMap, level: &str) -> Option< color } +/// print timeline histogram +fn _print_timeline_hist(timestamps: Vec, marker_count: usize, length: usize) { + if timestamps.is_empty() { + return; + } + + let buf_wtr = BufferWriter::stdout(ColorChoice::Always); + let mut wtr = buf_wtr.buffer(); + + let title = "Event Frequency Timeline"; + let header_row_space = (length - title.len()) / 2; + writeln!(wtr).ok(); + write!(wtr, "{}", " ".repeat(header_row_space)).ok(); + writeln!(wtr, "{}", title).ok(); + writeln!(wtr).ok(); + + let (header, footer) = build_time_markers(×tamps, marker_count, length); + let sparkline = build_sparkline(×tamps, length); + writeln!(wtr, "{}", header).ok(); + writeln!(wtr, "{}", sparkline.unwrap_or_default()).ok(); + writeln!(wtr, "{}", footer).ok(); + buf_wtr.print(&wtr).ok(); +} + pub fn after_fact() { let fn_emit_csv_err = |err: Box| { AlertMessage::alert( @@ -151,8 +177,10 @@ fn emit_csv( let mut detected_rule_files: Vec = Vec::new(); println!(); + let mut timestamps: Vec = Vec::new(); let mut plus_header = true; for (time, detect_infos) in messages.iter() { + timestamps.push(_get_timestamp(time)); for detect_info in detect_infos { let mut level = detect_info.level.to_string(); if level == "informational" { @@ -224,6 +252,13 @@ fn emit_csv( wtr.flush()?; } println!(); + let size = terminal_size(); + let terminal_width = match size { + Some((Width(w), _)) => w as usize, + None => 100, + }; + _print_timeline_hist(timestamps, 10, terminal_width); + println!(); _print_unique_results( total_detect_counts_by_level, "Total".to_string(), @@ -325,6 +360,15 @@ fn format_time(time: &DateTime) -> String { } } +/// get timestamp to input datetime. +fn _get_timestamp(time: &DateTime) -> i64 { + if configs::CONFIG.read().unwrap().args.is_present("utc") { + time.timestamp() + } else { + time.with_timezone(&Local).timestamp() + } +} + /// return rfc time format string by option fn format_rfc(time: &DateTime) -> String where