From f284cf47eb33b97732d2b88977a68c0a113fdd00 Mon Sep 17 00:00:00 2001 From: Andrew Rioux Date: Sat, 22 Feb 2025 20:00:28 -0500 Subject: [PATCH] feat: made beacon search and list better --- Cargo.lock | 40 +- sparse-beacon/Cargo.toml | 10 +- sparse-server/Cargo.toml | 8 +- sparse-server/src/beacons/sidebar.rs | 427 +++++++++++++++++++--- sparse-server/src/users.rs | 7 +- sparse-server/style/beacons/_sidebar.scss | 66 ++++ sparse-server/style/main.scss | 9 +- 7 files changed, 497 insertions(+), 70 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d7400bc..297e8eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -95,6 +95,12 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "asn1-rs" version = "0.6.2" @@ -725,13 +731,13 @@ dependencies = [ [[package]] name = "cron" -version = "0.15.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5877d3fbf742507b66bc2a1945106bd30dd8504019d596901ddd012a4dd01740" +checksum = "eee8b2b4516038bc0f1d3c9934bcb4a13dd316e04abbc63c96757a6d75978532" dependencies = [ "chrono", + "nom", "once_cell", - "winnow 0.6.26", ] [[package]] @@ -938,6 +944,18 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "669a445ee724c5c69b1b06fe0b63e70a1c84bc9bb7d9696cd4f4e3ec45050408" +[[package]] +name = "duration-str" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99b55e40ba8fc1ef074c9f9031b4cb88bb1f30c946f80a9305df44973c0b9a2d" +dependencies = [ + "chrono", + "rust_decimal", + "thiserror 2.0.11", + "winnow 0.6.8", +] + [[package]] name = "dyn-clone" version = "1.0.18" @@ -2984,6 +3002,16 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "rust_decimal" +version = "1.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b082d80e3e3cc52b2ed634388d436fe1f4de6af5786cc2de9ba9737527bdf555" +dependencies = [ + "arrayvec", + "num-traits", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -3497,6 +3525,7 @@ dependencies = [ "codee 0.2.0", "console_error_panic_hook", "cron", + "duration-str", "futures", "futures-util", "hex", @@ -3509,6 +3538,7 @@ dependencies = [ "pbkdf2", "rand 0.9.0", "rcgen", + "regex", "rpassword", "rustls-pki-types", "send_wrapper", @@ -5026,9 +5056,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.6.26" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e90edd2ac1aa278a5c4599b1d89cf03074b610800f866d4026dc199d7929a28" +checksum = "c3c52e9c97a68071b23e836c9380edae937f17b9c4667bd021973efc689f618d" dependencies = [ "memchr", ] diff --git a/sparse-beacon/Cargo.toml b/sparse-beacon/Cargo.toml index ce232fd..c2b765c 100644 --- a/sparse-beacon/Cargo.toml +++ b/sparse-beacon/Cargo.toml @@ -23,15 +23,15 @@ simple_logger = "5.0.0" http = "1.2.0" bytes = "1.10.0" http-body-util = "0.1.2" - -pcap-sys = { version = "0.1.0", path = "../pcap-sys" } -sparse-actions = { version = "2.0.0", path = "../sparse-actions" } -packets = { version = "0.1.0", path = "../packets" } serde_json = "1.0.139" serde = "1.0.217" http-body = "1.0.1" rmp-serde = "1.3.0" -cron = "0.15.0" +cron = "0.13.0" + +pcap-sys = { version = "0.1.0", path = "../pcap-sys" } +sparse-actions = { version = "2.0.0", path = "../sparse-actions" } +packets = { version = "0.1.0", path = "../packets" } [features] openssl = ["dep:rustls-openssl"] diff --git a/sparse-server/Cargo.toml b/sparse-server/Cargo.toml index 9d793ef..36b1ce6 100644 --- a/sparse-server/Cargo.toml +++ b/sparse-server/Cargo.toml @@ -40,14 +40,16 @@ hex = { version = "0.4", optional = true } serde = "1.0" cfg-if = "1.0.0" rcgen = { version = "0.13.2", features = ["pem", "x509-parser", "crypto"], optional = true } -cron = { version = "0.15.0", optional = true } +cron = { version = "0.13.0", optional = true } rustls-pki-types = { version = "1.7", optional = true } rand = { version = "0.9", optional = true } +serde_json = "1.0.139" +send_wrapper = "0.6.0" +duration-str = { version = "0.13.0", default-features = false, features = ["chrono"] } +regex = "1.11.1" sparse-actions = { path = "../sparse-actions", optional = true } sparse-handler = { path = "../sparse-handler", optional = true } -serde_json = "1.0.139" -send_wrapper = "0.6.0" [features] embed-beacons = [] diff --git a/sparse-server/src/beacons/sidebar.rs b/sparse-server/src/beacons/sidebar.rs index 7b6a2a5..f6a5f51 100644 --- a/sparse-server/src/beacons/sidebar.rs +++ b/sparse-server/src/beacons/sidebar.rs @@ -1,16 +1,13 @@ use std::sync::Arc; -use leptos::prelude::*; +use leptos::{either::Either, prelude::*}; #[cfg(feature = "hydrate")] -use leptos_use::{use_websocket, UseWebSocketReturn}; +use leptos_use::use_websocket; use serde::{Deserialize, Serialize}; use crate::beacons::BeaconResources; -#[cfg(feature = "hydrate")] -use super::templates::BeaconTemplate; - -#[derive(Clone)] +#[derive(Clone, PartialEq, Eq)] enum SortMethod { Listener, Config, @@ -69,11 +66,13 @@ unsafe impl Send for SidebarEvents {} pub enum SidebarEvents { BeaconList(Vec), NewBeacon(CurrentBeaconInstance), + BeaconUpdate(String, CurrentBeaconInstance), Checkin(String), } #[component] pub fn BeaconSidebar() -> impl IntoView { + #[cfg(not(feature = "ssr"))] let BeaconResources { listeners, templates, @@ -82,16 +81,17 @@ pub fn BeaconSidebar() -> impl IntoView { .. } = expect_context::(); + #[cfg_attr(feature = "ssr", allow(unused_variables))] let current_beacons = RwSignal::new(None::>); - #[cfg(feature = "hydrate")] + #[cfg(not(feature = "ssr"))] let (web_socket, rebuild_websocket) = signal(use_websocket::< (), SidebarEvents, codee::string::JsonSerdeCodec, >("/api/subscribe/listener")); - #[cfg(feature = "hydrate")] + #[cfg(not(feature = "ssr"))] Effect::new(move |_| { web_socket.with(move |uwsr| { uwsr.message.with(move |message| { @@ -112,6 +112,14 @@ pub fn BeaconSidebar() -> impl IntoView { } }); } + SidebarEvents::BeaconUpdate(bid, bnew) => current_beacons.update(|bs| { + let Some(ref mut bs) = bs else { + return; + }; + if let Some(ref mut bold) = bs.iter_mut().find(|b| b.beacon_id == *bid) { + **bold = bnew.clone(); + } + }), SidebarEvents::Checkin(bid) => current_beacons.update(|bs| { let Some(ref mut bs) = bs else { return; @@ -125,7 +133,7 @@ pub fn BeaconSidebar() -> impl IntoView { }); }); - #[cfg(feature = "hydrate")] + #[cfg(not(feature = "ssr"))] Effect::new(move |_| { let user = expect_context::>>(); user.with(move |_| { @@ -139,9 +147,11 @@ pub fn BeaconSidebar() -> impl IntoView { let (sort_method, set_sort_method) = signal(None::); let search_input = RwSignal::new("".to_string()); + let checkin_filter = RwSignal::new("".to_string()); struct BeaconPartition { title: Option, + key: String, beacons: Arc Vec + Send + Sync>, } @@ -151,38 +161,103 @@ pub fn BeaconSidebar() -> impl IntoView { #[cfg(not(feature = "ssr"))] let partitions = move || -> Vec { + use regex::Regex; + leptos::logging::log!( "There are {:?} beacons", current_beacons.read().as_ref().map(Vec::len) ); let sm = sort_method.read(); + let search_regex = Regex::new(&*search_input.read()); + let str_match_search = { + let search_inp = search_input.get(); + move |inp: &str| { + search_inp.is_empty() + || search_regex + .as_ref() + .map(|re| re.is_match(inp)) + .unwrap_or_default() + } + }; - //let Some(Ok(ref listeners)) = *listeners.read() else { - // return vec![]; - //}; - //let Some(Ok(ref templates)) = *templates.read() else { - // return vec![]; - //}; - //let Some(Ok(ref categories)) = *categories.read() else { - // return vec![]; - //}; + let partition_filter = { + let str_match_search = str_match_search.clone(); + move |partition: &BeaconPartition| { + partition + .title + .as_deref() + .map(|title| (str_match_search)(&title)) + .unwrap_or_default() + || !(partition.beacons)().is_empty() + } + }; + + macro_rules! beacon_filter { + ($part_title:expr, $search_input:expr, $beacon:expr, $templates:expr, $categories:expr) => {{ + let search_inp = &*search_input.read(); + let duration_inp = &*checkin_filter.read(); + + let query_empty = search_inp.is_empty(); + let duration_empty = duration_inp.is_empty(); + + let search_regex = ::regex::Regex::new(search_inp); + let matches = |inp: &str| { + search_regex + .as_ref() + .map(|re| re.is_match(inp)) + .unwrap_or(true) + }; + + let match_search = query_empty + || $part_title.as_deref().map(matches).unwrap_or_default() + || matches(&$beacon.beacon_id) + || matches(&$beacon.nickname) + || matches(&$beacon.ip) + || matches(&$beacon.cwd) + || matches(&$beacon.userent) + || matches(&$beacon.hostname) + || $templates + .iter() + .find(|t| t.template_id == $beacon.template_id) + .map(|t| matches(&t.template_name)) + .unwrap_or(false) + || $categories + .iter() + .filter(|c| $beacon.category_ids.contains(&c.category_id)) + .any(|c| matches(&c.category_name)); + + let now = chrono::Utc::now(); + + let pass_timefilter = duration_empty + || duration_str::parse_chrono(duration_inp) + .map(|dur| $beacon.last_checkin + dur > now) + .unwrap_or(true); + + match_search && pass_timefilter + }}; + } match *sm { Some(SortMethod::Config) => { - let Some(Ok(ref configs)) = *configs.read() else { + let Some(Ok(ref configs_super)) = *configs.read() else { return vec![]; }; - configs + configs_super .iter() .map(|config| { let config = config.clone(); BeaconPartition { title: Some(config.config_name.clone()), + key: format!("config-{}", &config.config_id), beacons: Arc::new(move || { let Some(Ok(ref templates)) = *templates.read() else { return vec![]; }; + let Some(Ok(ref categories)) = *categories.read() else { + return vec![]; + }; + current_beacons .get() .unwrap_or(vec![]) @@ -194,28 +269,42 @@ pub fn BeaconSidebar() -> impl IntoView { .map(|t| t.config_id)) == Some(config.config_id) }) + .filter(|beacon| { + beacon_filter!( + Some(config.config_name.clone()), + search_input, + beacon, + templates, + categories + ) + }) .map(Clone::clone) .collect() }), } }) - .collect::>() + .filter(partition_filter) + .collect() } Some(SortMethod::Listener) => { - let Some(Ok(ref listeners)) = *listeners.read() else { + let Some(Ok(ref listeners_super)) = *listeners.read() else { return vec![]; }; - listeners + listeners_super .iter() .map(|listener| { let listener = listener.clone(); BeaconPartition { title: Some(listener.domain_name.clone()), + key: format!("template-{}", &listener.listener_id), beacons: Arc::new(move || { let Some(Ok(ref templates)) = *templates.read() else { return vec![]; }; + let Some(Ok(ref categories)) = *categories.read() else { + return vec![]; + }; current_beacons .get() @@ -228,16 +317,167 @@ pub fn BeaconSidebar() -> impl IntoView { .map(|t| t.listener_id == listener.listener_id) .unwrap_or_default() }) + .filter(|beacon| { + beacon_filter!( + Some(listener.domain_name.clone()), + search_input, + beacon, + templates, + categories + ) + }) .map(Clone::clone) .collect() }), } }) + .filter(partition_filter) + .collect() + } + Some(SortMethod::Category) => { + let Some(Ok(ref categories_super)) = *categories.read() else { + return vec![]; + }; + + let mut category_partitions = vec![BeaconPartition { + title: None, + key: "uncategorized".to_string(), + beacons: Arc::new(move || { + let Some(Ok(ref templates)) = *templates.read() else { + return vec![]; + }; + let Some(Ok(ref categories)) = *categories.read() else { + return vec![]; + }; + + current_beacons + .get() + .unwrap_or(vec![]) + .iter() + .filter(|b| b.category_ids.is_empty()) + .filter(|beacon| { + beacon_filter!( + None::, + search_input, + beacon, + templates, + categories + ) + }) + .map(Clone::clone) + .collect() + }), + }]; + + category_partitions.extend( + categories_super + .iter() + .map(|category| { + let category = category.clone(); + BeaconPartition { + title: Some(category.category_name.clone()), + key: format!("category-{}", category.category_id), + beacons: Arc::new(move || { + let Some(Ok(ref templates)) = *templates.read() else { + return vec![]; + }; + let Some(Ok(ref categories)) = *categories.read() else { + return vec![]; + }; + + current_beacons + .get() + .unwrap_or(vec![]) + .iter() + .filter(|b| b.category_ids.contains(&category.category_id)) + .filter(|beacon| { + beacon_filter!( + Some(category.category_name.clone()), + search_input, + beacon, + templates, + categories + ) + }) + .map(Clone::clone) + .collect() + }), + } + }) + .filter(partition_filter), + ); + + category_partitions + } + Some(SortMethod::Template) => { + let Some(Ok(ref templates_super)) = *templates.read() else { + return vec![]; + }; + + templates_super + .iter() + .map(|template| { + let template = template.clone(); + BeaconPartition { + title: Some(template.template_name.clone()), + key: format!("template-{}", &template.template_id), + beacons: Arc::new(move || { + let Some(Ok(ref templates)) = *templates.read() else { + return vec![]; + }; + let Some(Ok(ref categories)) = *categories.read() else { + return vec![]; + }; + + current_beacons + .get() + .unwrap_or(vec![]) + .iter() + .filter(|b| b.template_id == template.template_id) + .filter(|beacon| { + beacon_filter!( + Some(template.template_name.clone()), + search_input, + beacon, + templates, + categories + ) + }) + .map(Clone::clone) + .collect() + }), + } + }) + .filter(partition_filter) .collect() } _ => vec![BeaconPartition { title: None, - beacons: Arc::new(move || current_beacons.get().unwrap_or(vec![]).clone()), + key: "unsorted".to_string(), + beacons: Arc::new(move || { + let Some(Ok(ref templates)) = *templates.read() else { + return vec![]; + }; + let Some(Ok(ref categories)) = *categories.read() else { + return vec![]; + }; + + current_beacons + .get() + .unwrap_or(vec![]) + .iter() + .filter(|beacon| { + beacon_filter!( + None::, + search_input, + beacon, + templates, + categories + ) + }) + .map(Clone::clone) + .collect() + }), }], } }; @@ -248,14 +488,31 @@ pub fn BeaconSidebar() -> impl IntoView { let partitions = || { vec![BeaconPartition { title: None, + key: "unsorted".to_string(), beacons: Arc::new(move || vec![]), }] }; + #[cfg(not(feature = "ssr"))] + let counter = { + use leptos_use::{use_interval, UseIntervalReturn}; + let UseIntervalReturn { counter, .. } = use_interval(1000); + counter + }; + + #[cfg(not(feature = "ssr"))] + let now = move || { + counter.get(); + chrono::Utc::now() + }; + + #[cfg(feature = "ssr")] + let now = move || chrono::Utc::now(); + view! { diff --git a/sparse-server/src/users.rs b/sparse-server/src/users.rs index 5cf68cb..088b1bf 100644 --- a/sparse-server/src/users.rs +++ b/sparse-server/src/users.rs @@ -23,7 +23,7 @@ pub fn format_delta(time: chrono::TimeDelta) -> String { } 3600..=86399 => { let hours = seconds / 3600; - format!("{} hours{} ago", hours, if hours == 1 { "" } else { "s" }) + format!("{} hour{} ago", hours, if hours == 1 { "" } else { "s" }) } _ => { let days = seconds / 86400; @@ -96,9 +96,10 @@ async fn reset_password(user_id: i64, password: String) -> Result<(), ServerFnEr #[component] pub fn RenderUser(refresh_user_list: Action<(), ()>, user: PubUser) -> impl IntoView { + #[cfg(not(feature = "ssr"))] use leptos_use::{use_interval, UseIntervalReturn}; - #[cfg(feature = "hydrate")] + #[cfg(not(feature = "ssr"))] let UseIntervalReturn { counter, .. } = use_interval(1000); #[cfg_attr(not(feature = "hydrate"), allow(unused_variables))] let (time_ago, set_time_ago) = signal( @@ -106,7 +107,7 @@ pub fn RenderUser(refresh_user_list: Action<(), ()>, user: PubUser) -> impl Into .map(|active| format_delta(Utc::now() - active)), ); - #[cfg(feature = "hydrate")] + #[cfg(not(feature = "ssr"))] Effect::watch( move || counter.get(), move |_, _, _| { diff --git a/sparse-server/style/beacons/_sidebar.scss b/sparse-server/style/beacons/_sidebar.scss index 9dc1291..1ce628c 100644 --- a/sparse-server/style/beacons/_sidebar.scss +++ b/sparse-server/style/beacons/_sidebar.scss @@ -3,4 +3,70 @@ aside.beacons { background-color: #11111c; overflow-y: auto; overflow-x: hidden; + + * { + box-sizing: border-box; + } + + .sort-method, .search, .checkin-filter { + padding: 10px; + padding-bottom: 5px; + + p { + display: inline-block; + width: 50%; + margin: 0; + } + + select, input { + display: inline-block; + width: 50%; + } + } + + .checkin-filter { + padding-bottom: 12px; + border-bottom: 1px solid #2e2e59; + } + + .beacon-list { + padding: 0; + margin: 0; + } + + .beacon-partition { + padding: 0; + margin: 0; + } + + .partition-title { + padding: 10px 15px; + margin: 0; + position: sticky; + top: 0; + background-color: #11111c; + border-bottom: 1px solid #2e2e59; + font-weight: bold; + font-size: 1.2em; + } + + .beacon-instance { + padding: 10px; + } + + .beacon-instance span { + color: gray; + } + + .beacon-instance { + border-bottom: 1px solid #2e2e59; + } + + .search-match { + color: yellow; + } + + .beacon-instance-id { + + } } diff --git a/sparse-server/style/main.scss b/sparse-server/style/main.scss index 694d930..424dd96 100644 --- a/sparse-server/style/main.scss +++ b/sparse-server/style/main.scss @@ -20,7 +20,7 @@ html, body { body { display: grid; - grid-template-columns: 350px 1fr; + grid-template-columns: 450px 1fr; grid-template-rows: 79px 1fr; grid-template-areas: "nav nav" @@ -53,13 +53,6 @@ nav { } } -aside.beacons { - grid-area: beacons; - background-color: #11111c; - overflow-y: auto; - overflow-x: hidden; -} - main { grid-area: main;