diff --git a/sparse-server/migrations/20250130040842_add_beacons.sql b/sparse-server/migrations/20250130040842_add_beacons.sql index 24efdea..22069fc 100644 --- a/sparse-server/migrations/20250130040842_add_beacons.sql +++ b/sparse-server/migrations/20250130040842_add_beacons.sql @@ -39,8 +39,11 @@ CREATE TABLE beacon_template ( source_netmask int, source_gateway varchar, - FOREIGN KEY (config_id) REFERENCES beacon_config - FOREIGN KEY (listener_id) REFERENCES beacon_listener + default_category int, + + FOREIGN KEY (config_id) REFERENCES beacon_config, + FOREIGN KEY (listener_id) REFERENCES beacon_listener, + FOREIGN KEY (default_category) REFERENCES beacon_category ); CREATE TABLE beacon_instance ( diff --git a/sparse-server/src/app.rs b/sparse-server/src/app.rs index a3423cd..c15d2e3 100644 --- a/sparse-server/src/app.rs +++ b/sparse-server/src/app.rs @@ -7,6 +7,8 @@ use leptos_router::{ }; use serde::{Serialize, Deserialize}; +use crate::users::User; + #[server] pub async fn test_retrieve() -> Result { use leptos::server_fn::error::NoCustomError; @@ -22,12 +24,6 @@ pub async fn test_retrieve() -> Result { Ok(since_the_epoch) } -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct User { - user_id: i64, - user_name: String, -} - #[server] pub async fn me() -> Result, ServerFnError> { let user = crate::db::user::get_auth_session().await?; @@ -103,6 +99,8 @@ pub fn App() -> impl IntoView { leptos::logging::log!("User resource: {:?}", user.get()); }); + crate::beacons::provide_beacon_resources(); + view! { diff --git a/sparse-server/src/beacons.rs b/sparse-server/src/beacons.rs index 3e12803..a482aa5 100644 --- a/sparse-server/src/beacons.rs +++ b/sparse-server/src/beacons.rs @@ -15,11 +15,63 @@ pub use instances::InstancesView; pub use listeners::ListenersView; pub use templates::TemplatesView; +#[derive(Clone)] +pub struct BeaconResources { + add_listener: ServerAction, + remove_listener: ServerAction, + add_category: ServerAction, + remove_category: ServerAction, + rename_category: ServerAction, + + listeners: Resource, ServerFnError>>, + categories: Resource, ServerFnError>>, +} + +pub fn provide_beacon_resources() { + let user = expect_context::>>(); + + let add_listener = ServerAction::::new(); + let remove_listener = ServerAction::::new(); + + let add_category = ServerAction::::new(); + let remove_category = ServerAction::::new(); + let rename_category = ServerAction::::new(); + + let listeners = Resource::new( + move || ( + user.get(), + add_listener.version().get(), + remove_listener.version().get(), + ), + |_| async { listeners::get_listeners().await } + ); + + let categories = Resource::new( + move || ( + user.get(), + add_category.version().get(), + remove_category.version().get(), + rename_category.version().get(), + ), + |_| async { categories::get_categories().await } + ); + + provide_context(BeaconResources { + add_listener, + remove_listener, + add_category, + remove_category, + rename_category, + + listeners, + categories + }); +} + #[component] pub fn BeaconView() -> impl IntoView { - #[cfg(feature = "hydrate")] Effect::new(move || { - let user = expect_context::>>(); + let user = expect_context::>>(); if user.get().is_none() { let navigate = leptos_router::hooks::use_navigate(); navigate("/login?next=beacons", Default::default()); @@ -44,11 +96,52 @@ pub fn BeaconView() -> impl IntoView { } } +enum SortMethod { + Listener, + Config, + Category, + Template +} + +impl std::str::FromStr for SortMethod { + type Err = (); + + fn from_str(s: &str) -> Result { + match s { + "Listener" => Ok(Self::Listener), + "Config" => Ok(Self::Config), + "Category" => Ok(Self::Category), + "Template" => Ok(Self::Template), + &_ => Err(()) + } + } +} + +impl std::string::ToString for SortMethod { + fn to_string(&self) -> String { + use SortMethod as SM; + match self { + SM::Listener => "Listener", + SM::Config => "Config", + SM::Category => "Category", + SM::Template => "Template", + }.to_string() + } +} + #[component] pub fn BeaconSidebar() -> impl IntoView { + let (sort_method, set_sort_method) = signal(SortMethod::Category); + let search_input = RwSignal::new("".to_string()); + view! { } } diff --git a/sparse-server/src/beacons/categories.rs b/sparse-server/src/beacons/categories.rs index 3f5daa6..40fa1bf 100644 --- a/sparse-server/src/beacons/categories.rs +++ b/sparse-server/src/beacons/categories.rs @@ -1,13 +1,244 @@ -use leptos::prelude::*; +use leptos::{either::Either, prelude::*}; +use serde::{Serialize, Deserialize}; +#[cfg(feature = "ssr")] +use { + sqlx::SqlitePool, + leptos::server_fn::error::NoCustomError, + crate::db::user, +}; + +use super::BeaconResources; + +#[derive(Clone, Serialize, Deserialize)] +pub struct Category { + category_id: i64, + category_name: String +} + +#[server] +pub async fn get_categories() -> Result, ServerFnError> { + let user = user::get_auth_session().await?; + + if user.is_none() { + return Err(ServerFnError::::ServerError("You are not signed in!".to_owned())); + } + + let db = expect_context::(); + + Ok( + sqlx::query_as!( + Category, + "SELECT * FROM beacon_category" + ) + .fetch_all(&db) + .await? + ) +} + + +#[server] +pub async fn add_category(name: String) -> Result<(), ServerFnError> { + let user = user::get_auth_session().await?; + + if user.is_none() { + return Err(ServerFnError::::ServerError("You are not signed in!".to_owned())); + } + + let db = expect_context::(); + + sqlx::query!( + "INSERT INTO beacon_category (category_name) VALUES (?)", + name + ) + .execute(&db) + .await?; + + Ok(()) +} + +#[server] +pub async fn remove_category(id: i64) -> Result<(), ServerFnError> { + let user = user::get_auth_session().await?; + + if user.is_none() { + return Err(ServerFnError::::ServerError("You are not signed in!".to_owned())); + } + + let db = expect_context::(); + + sqlx::query!( + "DELETE FROM beacon_category WHERE category_id = ?", + id + ) + .execute(&db) + .await?; + + Ok(()) +} + +#[server] +pub async fn rename_category(id: i64, name: String) -> Result<(), ServerFnError> { + let user = user::get_auth_session().await?; + + if user.is_none() { + return Err(ServerFnError::::ServerError("You are not signed in!".to_owned())); + } + + let db = expect_context::(); + + sqlx::query!( + "UPDATE beacon_category SET category_name = ? WHERE category_id = ?", + name, + id + ) + .execute(&db) + .await?; + + Ok(()) +} #[component] pub fn CategoriesView() -> impl IntoView { + let BeaconResources { add_category, categories, .. } = expect_context(); + + view! { -
-

"Categories"

-
    -
  • "Windows"
  • -
+
+

"Categories"

+

+ "Categories are an optional organization method that can be used to group beacons. Beacons can be assigned to multiple categories" +

+ + +
+ {move || match add_category.value().get() { + Some(Ok(_)) => Either::Right(()), + None => Either::Right(()), + Some(Err(e)) => Either::Left(view! { +

"Error creating category:"

+

{format!("{e:?}")}

+ }) + }} + "Add a new beacon category" + + +
+ +
+
+ + "Loading..."

}> + { move || match categories.get() { + Some(inner) => Either::Right(match inner { + Err(e) => Either::Left(view! { +

"There was an error loading categories:"

+

{format!("error: {e:?}")}

+ }), + Ok(cs) => Either::Right(view! { + + }) + }), + None => Either::Left(view! { +

"Loading..."

+ }) + }} +
} } + +#[component] +fn DisplayCategories(categories: Vec) -> impl IntoView { + let BeaconResources { remove_category, rename_category, .. } = expect_context(); + + let (target_rename_id, set_target_rename_id) = signal(0); + let target_rename_name = RwSignal::new("".to_owned()); + + let dialog_ref = NodeRef::::new(); + + let categories_view = categories + .iter() + .map(|category| view! { +
  • + {category.category_id} + ": " + {category.category_name.clone()} + + +
  • + }) + .collect_view(); + + Effect::watch( + move || ( + rename_category.version().get(), + rename_category.value().get(), + dialog_ref.get() + ), + move |(_, res,dialog_ref),_,_| { + if let (Some(Ok(())), Some(dialog)) = (res, dialog_ref) { + let _ = dialog.close(); + } + }, + false + ); + + view! { + + + {move || match rename_category.value().get() { + Some(Ok(_)) => Either::Right(()), + None => Either::Right(()), + Some(Err(e)) => Either::Left(view! { +

    "Failed to rename category:"

    +

    {format!("{e:?}")}

    + }) + }} +
    + "Rename category" + + + + +
    +
    +
    + {move || match remove_category.value().get() { + Some(Ok(_)) => Either::Right(()), + None => Either::Right(()), + Some(Err(e)) => Either::Left(view! { +

    "Failed to remove category:"

    +

    {format!("{e:?}")}

    + }) + }} +
      + {categories_view} +
    + } +} diff --git a/sparse-server/src/beacons/listeners.rs b/sparse-server/src/beacons/listeners.rs index c80ddd2..11e33a8 100644 --- a/sparse-server/src/beacons/listeners.rs +++ b/sparse-server/src/beacons/listeners.rs @@ -10,6 +10,8 @@ use { rcgen::{generate_simple_self_signed, CertifiedKey}, }; +use super::BeaconResources; + #[cfg(feature = "ssr")] struct DbListener { listener_id: i64, @@ -101,6 +103,11 @@ pub async fn add_listener(public_ip: String, port: i16, domain_name: String) -> Ok(()) } +#[server] +pub async fn remove_listener(listener_id: i64) -> Result<(), ServerFnError> { + unimplemented!() +} + #[server] pub async fn start_listener(listener_id: i64) -> Result<(), ServerFnError> { unimplemented!() @@ -108,15 +115,14 @@ pub async fn start_listener(listener_id: i64) -> Result<(), ServerFnError> { #[component] pub fn ListenersView() -> impl IntoView { - let add_listener = ServerAction::::new(); - - let listeners = Resource::new( - move || add_listener.version().get(), - |_| async { get_listeners().await } - ); + let super::BeaconResources { add_listener, listeners, .. } = expect_context::(); view! {
    +

    "Listeners"

    +

    + "Listeners bind to an IP and port and have a public/private key pair that beacons can use to maintain TLS encrypted connections back to the C2 server" +

    {move || match add_listener.value().get() { @@ -147,10 +153,7 @@ pub fn ListenersView() -> impl IntoView {

    {format!("error: {}", e)}

    }), Ok(ls) => Either::Right(view! { - + }) }), None => Either::Left(view! { @@ -163,7 +166,9 @@ pub fn ListenersView() -> impl IntoView { } #[component] -fn DisplayListeners(listener_resource: Resource, ServerFnError>>, listeners: Vec) -> impl IntoView { +fn DisplayListeners(listeners: Vec) -> impl IntoView { + let BeaconResources { listeners: listener_resource, remove_listener, .. } = expect_context::(); + let (error_msg, set_error_msg) = signal(None); let start_listener_action = Action::new(move |&id: &i64| async move { match start_listener(id).await { @@ -191,6 +196,17 @@ fn DisplayListeners(listener_resource: Resource, ServerF {match listener.active { true => Either::Left(view! { "active!" + }), false => Either::Right(view! {