use leptos::{either::Either, prelude::*}; use serde::{Deserialize, Serialize}; #[cfg(feature = "ssr")] use { crate::db::user, leptos::server_fn::error::NoCustomError, sqlx::{sqlite::SqliteRow, FromRow, Row, SqlitePool}, std::net::Ipv4Addr, }; 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, source_interface: 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::(); let listener = sqlx::query!( "SELECT certificate, privkey FROM beacon_listener WHERE listener_id = ?", listener_id ) .fetch_one(&db) .await?; use rcgen::{Certificate, CertificateParams, KeyPair}; let keypair = KeyPair::from_der_and_sign_algo( match &rustls_pki_types::PrivateKeyDer::try_from(&*listener.privkey) { Ok(pk) => pk, Err(e) => { srverr!("Could not parse private key: {e}"); } }, &rcgen::PKCS_ECDSA_P256_SHA256, )?; let ca_params = CertificateParams::from_ca_cert_der(&(*listener.certificate).into())?; let ca_cert = ca_params.self_signed(&keypair)?; let client_key = KeyPair::generate()?; let client_params = CertificateParams::default(); let client_cert = client_params.signed_by(&client_key, &ca_cert, &keypair)?; let client_key_der = client_key.serialize_der(); let client_cert_der = client_cert.der().to_vec(); let interface = Some(source_interface).filter(|s| !s.is_empty()); 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, client_key, client_cert, source_interface) VALUES (?, ?, ?, ?, ?, ?, 'host', ?, ?, ?, ?)", template_name, operating_system, config_id, listener_id, source_ip, source_mac, default_category, client_key_der, client_cert_der, interface ) .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, client_key, client_cert, source_interface) VALUES (?, ?, ?, ?, ?, ?, 'host', ?, ?, ?, ?, ?, ?)", template_name, operating_system, config_id, listener_id, source_ip, source_mac, source_netmask, source_gateway, default_category, client_key_der, client_cert_der, interface ) .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 { 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()} ")"

    "Download installer"
    • "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}
    } }