From 29ae9dfcba795f7225e5bf359c44a69a12bb039b Mon Sep 17 00:00:00 2001 From: Jan Starke Date: Tue, 7 May 2024 18:10:10 +0200 Subject: [PATCH] use ratatui --- Cargo.lock | 385 +++++++++++--------------- Cargo.toml | 7 +- src/bin/evtxview/app.rs | 209 ++++++++++++++ src/bin/evtxview/cli.rs | 4 +- src/bin/evtxview/evtx_column.rs | 7 - src/bin/evtxview/evtx_line.rs | 37 --- src/bin/evtxview/evtx_view.rs | 99 ------- src/bin/evtxview/main.rs | 32 +-- src/bin/evtxview/tui/evtx_iterator.rs | 105 +++++++ src/bin/evtxview/tui/mod.rs | 24 ++ src/bin/evtxview/ui_main.rs | 46 --- 11 files changed, 515 insertions(+), 440 deletions(-) create mode 100644 src/bin/evtxview/app.rs delete mode 100644 src/bin/evtxview/evtx_column.rs delete mode 100644 src/bin/evtxview/evtx_line.rs delete mode 100644 src/bin/evtxview/evtx_view.rs create mode 100644 src/bin/evtxview/tui/evtx_iterator.rs create mode 100644 src/bin/evtxview/tui/mod.rs delete mode 100644 src/bin/evtxview/ui_main.rs diff --git a/Cargo.lock b/Cargo.lock index 9e54afe..28c11be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,7 +24,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", - "getrandom", "once_cell", "version_check", "zerocopy", @@ -45,6 +44,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -225,7 +230,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42c3da1db1f384ad643e27cb6ba4f08f2a7cd8753bef8dd95c39fe45d7a474c8" dependencies = [ "binwrite_derive", - "paste", + "paste 0.1.18", ] [[package]] @@ -326,6 +331,21 @@ dependencies = [ "serde_json", ] +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" version = "1.0.83" @@ -515,6 +535,19 @@ dependencies = [ "yansi", ] +[[package]] +name = "compact_str" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "ryu", + "static_assertions", +] + [[package]] name = "console" version = "0.15.7" @@ -562,28 +595,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "crossbeam" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" -dependencies = [ - "crossbeam-channel", - "crossbeam-deque", - "crossbeam-epoch", - "crossbeam-queue", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "crossbeam-deque" version = "0.8.5" @@ -604,19 +615,35 @@ dependencies = [ ] [[package]] -name = "crossbeam-queue" -version = "0.3.11" +name = "crossbeam-utils" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" + +[[package]] +name = "crossterm" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" dependencies = [ - "crossbeam-utils", + "bitflags 2.4.0", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi 0.3.9", ] [[package]] -name = "crossbeam-utils" -version = "0.8.19" +name = "crossterm_winapi" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi 0.3.9", +] [[package]] name = "crypto-common" @@ -649,72 +676,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "cursive" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5438eb16bdd8af51b31e74764fef5d0a9260227a5ec82ba75c9d11ce46595839" -dependencies = [ - "ahash", - "cfg-if", - "crossbeam-channel", - "cursive_core", - "lazy_static", - "libc", - "log", - "maplit", - "ncurses", - "signal-hook", - "term_size", - "unicode-segmentation", - "unicode-width", -] - -[[package]] -name = "cursive-async-view" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "192c058a725c80d9759b3e8957d6ee9e169a0b46c9f11216ec8c8d7990644761" -dependencies = [ - "crossbeam", - "cursive_core", - "doc-comment", - "interpolation", - "lazy_static", - "log", - "num", - "send_wrapper", -] - -[[package]] -name = "cursive_core" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4db3b58161228d0dcb45c7968c5e74c3f03ad39e8983e58ad7d57061aa2cd94d" -dependencies = [ - "ahash", - "crossbeam-channel", - "enum-map", - "enumset", - "lazy_static", - "log", - "num", - "owning_ref", - "time", - "unicode-segmentation", - "unicode-width", - "xi-unicode", -] - -[[package]] -name = "cursive_table_view" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8935dd87d19c54b7506b245bc988a7b4e65b1058e1d0d64c0ad9b3188e48060" -dependencies = [ - "cursive_core", -] - [[package]] name = "darling" version = "0.13.4" @@ -785,12 +746,6 @@ dependencies = [ "syn 2.0.37", ] -[[package]] -name = "deranged" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" - [[package]] name = "derive-getters" version = "0.3.0" @@ -821,10 +776,8 @@ dependencies = [ "clio", "colored", "colored_json", + "crossterm", "csv", - "cursive", - "cursive-async-view", - "cursive_table_view", "dfirtk-eventdata", "dfirtk-sessionevent-derive", "duplicate", @@ -852,14 +805,15 @@ dependencies = [ "ouroboros", "phf", "rand", + "ratatui", "regex", "serde", "serde_json", "sha2", "sigpipe", "simplelog", - "strum", - "strum_macros", + "strum 0.25.0", + "strum_macros 0.25.2", "term-table", "termsize", "thiserror", @@ -1060,47 +1014,6 @@ dependencies = [ "encoding_rs", ] -[[package]] -name = "enum-map" -version = "2.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6866f3bfdf8207509a033af1a75a7b08abda06bbaaeae6669323fd5a097df2e9" -dependencies = [ - "enum-map-derive", -] - -[[package]] -name = "enum-map-derive" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.37", -] - -[[package]] -name = "enumset" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "226c0da7462c13fb57e5cc9e0dc8f0635e7d27f276a3a7fd30054647f669007d" -dependencies = [ - "enumset_derive", -] - -[[package]] -name = "enumset_derive" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08b6c6ab82d70f08844964ba10c7babb716de2ecaeab9be5717918a5177d3af" -dependencies = [ - "darling 0.20.3", - "proc-macro2", - "quote", - "syn 2.0.37", -] - [[package]] name = "equivalent" version = "1.0.1" @@ -1151,7 +1064,7 @@ dependencies = [ "crc32fast", "dialoguer", "encoding", - "indoc", + "indoc 1.0.9", "log", "quick-xml", "rayon", @@ -1393,6 +1306,10 @@ name = "hashbrown" version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +dependencies = [ + "ahash", + "allocator-api2", +] [[package]] name = "heck" @@ -1564,6 +1481,12 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306" +[[package]] +name = "indoc" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" + [[package]] name = "instant" version = "0.1.12" @@ -1573,12 +1496,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "interpolation" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3b7357d2bbc5ee92f8e899ab645233e43d21407573cceb37fed8bc3dede2c02" - [[package]] name = "ipnet" version = "2.8.0" @@ -1605,6 +1522,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.9" @@ -1702,10 +1628,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] -name = "maplit" -version = "1.0.2" +name = "lru" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" +checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" +dependencies = [ + "hashbrown 0.14.3", +] [[package]] name = "marvin32" @@ -1759,6 +1688,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", + "log", "wasi", "windows-sys 0.48.0", ] @@ -1787,17 +1717,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "ncurses" -version = "5.101.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e2c5d34d72657dc4b638a1c25d40aae81e4f1c699062f72f467237920752032" -dependencies = [ - "cc", - "libc", - "pkg-config", -] - [[package]] name = "nt_hive2" version = "4.2.1" @@ -2034,22 +1953,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8cad0c4b129e9696e37cb712b243777b90ef489a0bfaa0ac34e7d9b860e4f134" dependencies = [ "heck", - "itertools", + "itertools 0.11.0", "proc-macro-error", "proc-macro2", "quote", "syn 2.0.37", ] -[[package]] -name = "owning_ref" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff55baddef9e4ad00f88b6c743a2a8062d4c6ade126c2a528644b8e444d52ce" -dependencies = [ - "stable_deref_trait", -] - [[package]] name = "parking_lot" version = "0.12.1" @@ -2092,6 +2002,12 @@ dependencies = [ "proc-macro-hack", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "paste-impl" version = "0.1.18" @@ -2183,7 +2099,7 @@ checksum = "6dfc28575c2e3f19cb3c73b93af36460ae898d426eba6fc15b9bd2a5220758a0" dependencies = [ "anstyle", "difflib", - "itertools", + "itertools 0.11.0", "predicates-core", ] @@ -2301,6 +2217,26 @@ dependencies = [ "getrandom", ] +[[package]] +name = "ratatui" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a564a852040e82671dc50a37d88f3aa83bbc690dfc6844cfe7a2591620206a80" +dependencies = [ + "bitflags 2.4.0", + "cassowary", + "compact_str", + "crossterm", + "indoc 2.0.5", + "itertools 0.12.1", + "lru", + "paste 1.0.15", + "stability", + "strum 0.26.2", + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "rayon" version = "1.8.0" @@ -2527,12 +2463,6 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" -[[package]] -name = "send_wrapper" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "930c0acf610d3fdb5e2ab6213019aaa04e227ebe9547b0649ba599b16d788bd7" - [[package]] name = "serde" version = "1.0.188" @@ -2625,6 +2555,17 @@ dependencies = [ "signal-hook-registry", ] +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -2711,10 +2652,14 @@ dependencies = [ ] [[package]] -name = "stable_deref_trait" -version = "1.2.0" +name = "stability" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "2ff9eaf853dec4c8802325d8b6d3dffa86cc707fd7a1a4cdbf416e13b061787a" +dependencies = [ + "quote", + "syn 2.0.37", +] [[package]] name = "static_assertions" @@ -2734,7 +2679,16 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" dependencies = [ - "strum_macros", + "strum_macros 0.25.2", +] + +[[package]] +name = "strum" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" +dependencies = [ + "strum_macros 0.26.2", ] [[package]] @@ -2750,6 +2704,19 @@ dependencies = [ "syn 2.0.37", ] +[[package]] +name = "strum_macros" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.37", +] + [[package]] name = "syn" version = "1.0.109" @@ -2796,16 +2763,6 @@ dependencies = [ "unicode-width", ] -[[package]] -name = "term_size" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e4129646ca0ed8f45d09b929036bafad5377103edd06e50bf574b353d2b08d9" -dependencies = [ - "libc", - "winapi 0.3.9", -] - [[package]] name = "termcolor" version = "1.1.3" @@ -2884,33 +2841,21 @@ dependencies = [ [[package]] name = "time" -version = "0.3.29" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "426f806f4089c493dcac0d24c29c01e2c38baf8e30f1b716ee37e83d200b18fe" +checksum = "c2702e08a7a860f005826c6815dcac101b19b5eb330c27fe4a5928fec1d20ddd" dependencies = [ - "deranged", "itoa", "libc", "num_threads", - "serde", - "time-core", "time-macros", ] -[[package]] -name = "time-core" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" - [[package]] name = "time-macros" -version = "0.2.15" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" -dependencies = [ - "time-core", -] +checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" [[package]] name = "tinyvec" @@ -3438,12 +3383,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "xi-unicode" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a67300977d3dc3f8034dae89778f502b6ba20b269527b3223ba59c0cf393bb8a" - [[package]] name = "yansi" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index 1fc8438..5cc02ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -103,7 +103,7 @@ ipgrep = [] ts2date = ["regex"] lnk2bodyfile = ["lnk"] pf2bodyfile = ["num", "libc", "frnsc-prefetch", "forensic-rs"] -evtxview = ["cursive", "cursive_table_view", "cursive-async-view"] +evtxview = ["ratatui", "crossterm", "ouroboros"] regdump = ["nt_hive2"] hivescan = ["nt_hive2"] @@ -185,9 +185,8 @@ frnsc-prefetch = {version="0.9", optional=true} forensic-rs = {version="0.9.1", optional=true} # evtxview -cursive = {version="0.20", optional=true} -cursive_table_view = {version="0.14", optional=true} -cursive-async-view = {version="^0", optional=true} +ratatui = {version="0.26.2", optional=true} +crossterm = {version="^0", optional=true} [dev-dependencies] diff --git a/src/bin/evtxview/app.rs b/src/bin/evtxview/app.rs new file mode 100644 index 0000000..3de98f4 --- /dev/null +++ b/src/bin/evtxview/app.rs @@ -0,0 +1,209 @@ +use std::io; + +use crate::{ + cli::Cli, + tui::{self, EvtxTable}, +}; +use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind}; +use ratatui::{ + prelude::*, + style::palette::tailwind, + widgets::{block::*, *}, +}; + +const PALETTES: [tailwind::Palette; 4] = [ + tailwind::BLUE, + tailwind::EMERALD, + tailwind::INDIGO, + tailwind::RED, +]; +const INFO_TEXT: &str = + "(Esc) quit | (↑) move up | (↓) move down | (→) next color | (←) previous color"; + +pub struct App { + evtx_table: EvtxTable, + exit: bool, + state: TableState, + scroll_state: ScrollbarState, + colors: TableColors, +} + +struct TableColors { + buffer_bg: Color, + _header_bg: Color, + _header_fg: Color, + row_fg: Color, + selected_style_fg: Color, + _normal_row_color: Color, + _alt_row_color: Color, + footer_border_color: Color, +} + +impl TableColors { + const fn new(color: &tailwind::Palette) -> Self { + Self { + buffer_bg: tailwind::SLATE.c950, + _header_bg: color.c900, + _header_fg: tailwind::SLATE.c200, + row_fg: tailwind::SLATE.c200, + selected_style_fg: color.c400, + _normal_row_color: tailwind::SLATE.c950, + _alt_row_color: tailwind::SLATE.c900, + footer_border_color: color.c400, + } + } +} + +impl App { + pub fn new(cli: Cli) -> Self { + let evtx_table = EvtxTable::try_from(cli.evtx_file.path().path()).unwrap(); + let table_len = evtx_table.len(); + Self { + evtx_table, + exit: Default::default(), + state: TableState::default().with_selected(0), + scroll_state: ScrollbarState::new(table_len - 1), + colors: TableColors::new(&PALETTES[0]), + } + } + /// runs the application's main loop until the user quits + pub fn run(&mut self, terminal: &mut tui::Tui) -> io::Result<()> { + while !self.exit { + terminal.draw(|frame| self.render_frame(frame))?; + self.handle_events()?; + } + Ok(()) + } + + fn render_frame(&mut self, frame: &mut Frame) { + let rects = + Layout::vertical([Constraint::Min(5), Constraint::Length(3)]).split(frame.size()); + let cols = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]) + .split(rects[0]); + frame.render_widget(Clear, rects[0]); + self.render_table(frame, cols[0]); + self.render_scrollbar(frame, cols[0]); + self.render_content(frame, cols[1]); + self.render_footer(frame, rects[1]); + } + + fn render_table(&mut self, frame: &mut Frame, area: Rect) { + let selected_style = Style::default() + .add_modifier(Modifier::REVERSED) + .fg(self.colors.selected_style_fg); + let table = self.evtx_table.table().highlight_style(selected_style); + frame.render_stateful_widget(table, area, &mut self.state); + } + fn render_content(&mut self, frame: &mut Frame, area: Rect) { + match self.state.selected() { + Some(i) => { + match self.evtx_table.content(i) { + Some(value) => { + match serde_json::to_string_pretty(value) { + Ok(content) => { + frame.render_widget(Paragraph::new(content), area) + } + Err(why) => { + frame.render_widget(Paragraph::new(format!("{why}")), area)} + } + } + None => frame.render_widget(Clear, area), + } + }, + None => frame.render_widget(Clear, area), + } + } + + fn render_scrollbar(&mut self, frame: &mut Frame, area: Rect) { + frame.render_stateful_widget( + Scrollbar::default() + .orientation(ScrollbarOrientation::VerticalRight) + .begin_symbol(None) + .end_symbol(None), + area.inner(&Margin { + vertical: 1, + horizontal: 1, + }), + &mut self.scroll_state, + ) + } + + fn render_footer(&mut self, frame: &mut Frame, area: Rect) { + let info_footer = Paragraph::new(Line::from(INFO_TEXT)) + .style( + Style::new() + .fg(self.colors.row_fg) + .bg(self.colors.buffer_bg), + ) + .centered() + .block( + Block::bordered() + .border_type(BorderType::Double) + .border_style(Style::new().fg(self.colors.footer_border_color)), + ); + frame.render_widget(info_footer, area); + } + + fn handle_events(&mut self) -> io::Result<()> { + match event::read()? { + // it's important to check that the event is a key press event as + // crossterm also emits key release and repeat events on Windows. + Event::Key(key_event) if key_event.kind == KeyEventKind::Press => { + self.handle_key_event(key_event) + } + _ => {} + }; + Ok(()) + } + fn handle_key_event(&mut self, key_event: KeyEvent) { + match key_event.code { + KeyCode::Char('q') => self.exit(), + KeyCode::Char('g') => self.set_selected(0), + KeyCode::Char('G') => self.set_selected(self.evtx_table.len()-1), + KeyCode::Down => self.next(1), + KeyCode::Up => self.previous(1), + KeyCode::PageDown => self.next(10), + KeyCode::PageUp => self.previous(10), + _ => {} + } + } + fn exit(&mut self) { + self.exit = true; + } + + fn set_selected(&mut self, idx: usize) { + self.state.select(Some(idx)); + self.scroll_state = self.scroll_state.position(idx); + } + + fn next(&mut self, steps: usize) { + assert_ne!(steps, 0); + let i = match self.state.selected() { + Some(i) => { + (i + steps) % self.evtx_table.len() + } + None => 0, + }; + self.set_selected(i); + } + + fn previous(&mut self, steps: usize) { + assert_ne!(steps, 0); + let i = match self.state.selected() { + Some(i) => { + if i > steps { + i - steps + } else { + let steps = steps % self.evtx_table.len(); + if steps == 0 { + 0 + } else { + self.evtx_table.len() - steps + } + } + } + None => 0, + }; + self.set_selected(i); + } +} diff --git a/src/bin/evtxview/cli.rs b/src/bin/evtxview/cli.rs index 2aac4db..622c1e9 100644 --- a/src/bin/evtxview/cli.rs +++ b/src/bin/evtxview/cli.rs @@ -19,13 +19,13 @@ pub(crate) enum SortOrder { /// Display one or more events from an evtx file #[derive(Parser)] #[clap(name=env!("CARGO_BIN_NAME"), author, version,long_about=None)] -pub(crate) struct Cli { +pub struct Cli { /// Name of the evtx files to read from #[clap(value_hint=ValueHint::FilePath)] pub(crate) evtx_file: InputPath, #[clap(flatten)] - verbose: clap_verbosity_flag::Verbosity, + pub(crate) verbose: clap_verbosity_flag::Verbosity, } impl HasVerboseFlag for Cli { diff --git a/src/bin/evtxview/evtx_column.rs b/src/bin/evtxview/evtx_column.rs deleted file mode 100644 index 8c4106a..0000000 --- a/src/bin/evtxview/evtx_column.rs +++ /dev/null @@ -1,7 +0,0 @@ - -#[derive(Copy, Clone, PartialEq, Eq, Hash)] -pub enum EvtxColumn { - Timestamp, - EventRecordId, - EventId -} \ No newline at end of file diff --git a/src/bin/evtxview/evtx_line.rs b/src/bin/evtxview/evtx_line.rs deleted file mode 100644 index e34c854..0000000 --- a/src/bin/evtxview/evtx_line.rs +++ /dev/null @@ -1,37 +0,0 @@ -use cursive_table_view::TableViewItem; -use dfirtk_eventdata::EventId; -use evtx::SerializedEvtxRecord; -use serde_json::Value; - -use crate::evtx_column::EvtxColumn; - -#[derive(Clone, Debug)] -pub struct EvtxLine { - record: SerializedEvtxRecord, -} - -impl From> for EvtxLine { - fn from(record: SerializedEvtxRecord) -> Self { - Self { - record - } - } -} - -impl TableViewItem for EvtxLine { - fn to_column(&self, column: EvtxColumn) -> String { - match column { - EvtxColumn::Timestamp => { - self.record.timestamp.to_rfc3339() - }, - EvtxColumn::EventRecordId => self.record.event_record_id.to_string(), - EvtxColumn::EventId => EventId::try_from(&self.record).ok().map(|e| e.to_string()).unwrap_or_default(), - } - } - - fn cmp(&self, other: &Self, column: EvtxColumn) -> std::cmp::Ordering - where - Self: Sized { - self.to_column(column).cmp(&other.to_column(column)) - } -} diff --git a/src/bin/evtxview/evtx_view.rs b/src/bin/evtxview/evtx_view.rs deleted file mode 100644 index 504dc2a..0000000 --- a/src/bin/evtxview/evtx_view.rs +++ /dev/null @@ -1,99 +0,0 @@ -use std::{ - mem, - path::Path, - sync::{Arc, Mutex}, - thread::{self, JoinHandle}, -}; - -use cursive::{Cursive, View}; -use cursive_async_view::{AsyncProgressState, AsyncProgressView}; -use cursive_table_view::TableView; -use num_traits::cast::AsPrimitive; - -use crate::{evtx_column::EvtxColumn, evtx_line::EvtxLine}; - -pub struct EvtxView { - records_table: AsyncProgressView>, -} - -impl EvtxView { - pub fn new(siv: &mut Cursive, evtx_file: &Path) -> anyhow::Result { - let number_of_records = Arc::new(Mutex::new(usize::MAX)); - let records = Arc::new(Mutex::new(Vec::new())); - - let reader = Self::start_reader_thread( - evtx_file, - Arc::clone(&number_of_records), - Arc::clone(&records), - ); - - let async_view = AsyncProgressView::new(siv, move || { - if reader.is_finished() { - // take the vector stored in `records` and replace it with an empty record. - // the result will be stored in `my_records`. This approach takes care - // that no value is moved out of `reader`, which would be not allowed - let mut my_records = Vec::new(); - if let Ok(mut records) = records.lock() { - my_records = mem::replace(&mut *records, my_records); - } - - let records_table = TableView::::new() - .column(EvtxColumn::Timestamp, "Time", |c| c) - .column(EvtxColumn::EventRecordId, "Record#", |c| c) - .column(EvtxColumn::EventId, "Event#", |c| c) - .items(my_records); - AsyncProgressState::Available(records_table) - } else { - match records.lock() { - Ok(records) => match number_of_records.lock() { - Ok(number_of_records) => { - if *number_of_records == usize::MAX { - AsyncProgressState::Pending(0.0) - } else { - let r_len: f32 = records.len().as_(); - let number_of_records: f32 = number_of_records.as_(); - AsyncProgressState::Pending(r_len / number_of_records) - } - } - Err(why) => AsyncProgressState::Error(format!("{why}")), - }, - Err(why) => AsyncProgressState::Error(format!("{why}")), - } - } - }); - - Ok(Self { - records_table: async_view, - }) - } - - fn start_reader_thread( - evtx_file: &Path, - number_of_records: Arc>, - records: Arc>>, - ) -> JoinHandle<()> { - let evtx_file = evtx_file.to_path_buf(); - thread::spawn(move || { - if let Ok(mut number_of_records) = number_of_records.lock() { - *number_of_records = evtx::EvtxParser::from_path(&evtx_file) - .unwrap() - .records() - .filter(Result::is_ok) - .count(); - } - - let mut parser = evtx::EvtxParser::from_path(evtx_file).unwrap(); - for res in parser.records_json_value().filter_map(Result::ok) { - if let Ok(mut records) = records.lock() { - records.push(EvtxLine::from(res)); - } - } - }) - } -} - -impl View for EvtxView { - fn draw(&self, printer: &cursive::Printer) { - self.records_table.draw(printer) - } -} diff --git a/src/bin/evtxview/main.rs b/src/bin/evtxview/main.rs index 0977829..ad604fc 100644 --- a/src/bin/evtxview/main.rs +++ b/src/bin/evtxview/main.rs @@ -1,28 +1,16 @@ +mod app; mod cli; -mod system_field; -mod ui_main; -mod evtx_line; -mod evtx_column; -mod evtx_view; +mod tui; -use anyhow::{bail, Result}; +use anyhow::Result; +use app::App; use cli::Cli; -use clap::Parser; -use ui_main::UIMain; +use dfir_toolkit::common::FancyParser; fn main() -> Result<()> { - let cli = Cli::parse(); - - if ! cli.evtx_file.path().exists() { - bail!("invalid filename specified: file does not exist"); - } - - if ! cli.evtx_file.path().is_file() { - bail!("invalid filename specified: filename does not point to a file"); - } - - cursive::logger::init(); - - let mut ui = UIMain::new(cli.evtx_file.path().path())?; - ui.run() + let cli = Cli::parse_cli(); + let mut terminal = tui::init()?; + let app_result = App::new(cli).run(&mut terminal); + tui::restore()?; + Ok(app_result?) } diff --git a/src/bin/evtxview/tui/evtx_iterator.rs b/src/bin/evtxview/tui/evtx_iterator.rs new file mode 100644 index 0000000..6d334bb --- /dev/null +++ b/src/bin/evtxview/tui/evtx_iterator.rs @@ -0,0 +1,105 @@ +use std::{fs::File, path::Path}; + +use dfirtk_eventdata::EventId; +use evtx::{EvtxParser, SerializedEvtxRecord}; +use ouroboros::self_referencing; +use ratatui::widgets::{Row, Table}; +use serde_json::Value; + +pub struct EvtxTable { + rows: Vec, +} + +impl TryFrom<&Path> for EvtxTable { + type Error = anyhow::Error; + + fn try_from(path: &Path) -> Result { + let rows = RowContentsIterator::try_from(path)?.collect(); + Ok(EvtxTable { rows }) + } +} + +impl EvtxTable { + pub fn table(&self) -> Table<'_> { + Table::new(&self.rows, vec![10, 10, 10]) + } + + pub fn len(&self) -> usize { + self.rows.len() + } + + pub fn content(&self, idx: usize) -> Option<&Value> { + self.rows.get(idx).map(|r| &r.value) + } +} + +#[self_referencing] +pub struct RowContentsIterator { + parser: EvtxParser, + + #[borrows(mut parser)] + #[covariant] + iterator: Box< + dyn Iterator>> + 'this, + >, +} + +impl TryFrom<&Path> for RowContentsIterator { + type Error = anyhow::Error; + + fn try_from(evtx_file: &Path) -> Result { + let parser = EvtxParser::from_path(evtx_file)?; + Ok(RowContentsIteratorBuilder { + parser, + iterator_builder: |parser| { + Box::new(parser.serialized_records(|record| { + record.and_then(|record| record.into_json_value()) + })) + }, + } + .build()) + } +} + +impl Iterator for RowContentsIterator { + type Item = RowContents; + + fn next(&mut self) -> Option { + self.with_iterator_mut(|iterator| match iterator.next() { + Some(Err(why)) => panic!("Error while reading record: {why}"), + Some(Ok(r)) => Some((&r).into()), + None => None, + }) + } +} + +pub struct RowContents { + timestamp: String, + record_id: String, + event_id: String, + value: Value, +} + +impl<'r> From<&'r SerializedEvtxRecord> for RowContents { + fn from(record: &'r SerializedEvtxRecord) -> Self { + Self { + timestamp: record.timestamp.to_rfc3339(), + record_id: record.event_record_id.to_string(), + event_id: EventId::try_from(record) + .ok() + .map(|e| e.to_string()) + .unwrap_or_default(), + value: record.data.clone(), + } + } +} + +impl<'r> From<&'r RowContents> for Row<'r> { + fn from(contents: &'r RowContents) -> Self { + Row::new(vec![ + &contents.timestamp[..], + &contents.record_id[..], + &contents.event_id[..], + ]) + } +} diff --git a/src/bin/evtxview/tui/mod.rs b/src/bin/evtxview/tui/mod.rs new file mode 100644 index 0000000..aed43bc --- /dev/null +++ b/src/bin/evtxview/tui/mod.rs @@ -0,0 +1,24 @@ +mod evtx_iterator; +pub use evtx_iterator::*; + +use std::io::{self, stdout, Stdout}; + +use crossterm::{execute, terminal::*}; +use ratatui::prelude::*; + +/// A type alias for the terminal type used in this application +pub type Tui = Terminal>; + +/// Initialize the terminal +pub fn init() -> io::Result { + execute!(stdout(), EnterAlternateScreen)?; + enable_raw_mode()?; + Terminal::new(CrosstermBackend::new(stdout())) +} + +/// Restore the terminal to its original state +pub fn restore() -> io::Result<()> { + execute!(stdout(), LeaveAlternateScreen)?; + disable_raw_mode()?; + Ok(()) +} \ No newline at end of file diff --git a/src/bin/evtxview/ui_main.rs b/src/bin/evtxview/ui_main.rs deleted file mode 100644 index 2a775aa..0000000 --- a/src/bin/evtxview/ui_main.rs +++ /dev/null @@ -1,46 +0,0 @@ -use std::path::Path; - -use cursive::{ - view::SizeConstraint, - views::{LinearLayout, Panel, ResizedView, TextView}, - CursiveRunnable, -}; - -use crate::evtx_view::EvtxView; - -pub struct UIMain { - siv: CursiveRunnable, -} - -impl UIMain { - pub fn new(evtx_file: &Path) -> anyhow::Result { - let mut siv = cursive::default(); - siv.add_global_callback('q', |s| s.quit()); - - let evtx_table = EvtxView::new(&mut siv, evtx_file)?; - - let details_view = TextView::new(""); - let root_view = LinearLayout::vertical() - .child(evtx_table) - .child(details_view); - - siv.add_layer( - Panel::new(ResizedView::new( - SizeConstraint::Full, - SizeConstraint::Full, - root_view, - )) - .title(format!( - "{} v{}", - env!("CARGO_BIN_NAME"), - env!("CARGO_PKG_VERSION") - )), - ); - Ok(Self { siv }) - } - - pub fn run(&mut self) -> anyhow::Result<()> { - self.siv.run(); - Ok(()) - } -}