feat: added basic template management

This commit is contained in:
Andrew Rioux 2025-02-01 01:07:25 -05:00
parent 71b2f70686
commit ba5145c5ae
Signed by: andrew.rioux
GPG Key ID: 9B8BAC47C17ABB94
8 changed files with 537 additions and 21 deletions

View File

@ -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! {
<main class="main">
<h1>"Welcome to sparse!"</h1>
<p>"To get started:"</p>
<ol>
<li>"Sign in"</li>
<li>"Go to beacon management"</li>
<li>"Create a listener"</li>
<li>"(Optional) Create a category"</li>
<li>"Create a template"</li>
<li>"Download the installer"</li>
<li>"Run the installer on a target system"</li>
</ol>
</main>
}
}

View File

@ -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<categories::RenameCategory>,
add_beacon_config: ServerAction<configs::AddBeaconConfig>,
remove_beacon_config: ServerAction<configs::RemoveBeaconConfig>,
add_template: ServerAction<templates::AddTemplate>,
remove_template: ServerAction<templates::RemoveTemplate>,
listeners: Resource<Result<Vec<listeners::PubListener>, ServerFnError>>,
categories: Resource<Result<Vec<categories::Category>, ServerFnError>>,
configs: Resource<Result<Vec<configs::BeaconConfig>, ServerFnError>>
configs: Resource<Result<Vec<configs::BeaconConfig>, ServerFnError>>,
templates: Resource<Result<Vec<templates::BeaconTemplate>, ServerFnError>>
}
pub fn provide_beacon_resources() {
@ -43,6 +52,9 @@ pub fn provide_beacon_resources() {
let add_beacon_config = ServerAction::<configs::AddBeaconConfig>::new();
let remove_beacon_config = ServerAction::<configs::RemoveBeaconConfig>::new();
let add_template = ServerAction::<templates::AddTemplate>::new();
let remove_template = ServerAction::<templates::RemoveTemplate>::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
});
}

View File

@ -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]

View File

@ -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::<NoCustomError>::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::<NoCustomError>::ServerError("You are not signed in!".to_owned()));
}
let db = expect_context::<SqlitePool>();
sqlx::query!(
"DELETE FROM beacon_config WHERE config_id = ?",
id
)
.execute(&db)
.await?;
Ok(())
}
#[component]

View File

@ -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]

View File

@ -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<Self> {
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<String>,
#[cfg_attr(feature = "ssr", sqlx(flatten))]
source_mode: BeaconSourceMode,
config_id: i64,
listener_id: i64,
default_category: Option<i64>
}
cfg_if::cfg_if! {
if #[cfg(feature = "ssr")] {
macro_rules! srverr {
($err:expr) => {
return Err(ServerFnError::<NoCustomError>::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::<Ipv4Addr>().is_err() {
srverr!("Source IP address is formatted incorrectly");
}
if &*source_mode == "custom" && source_gateway.parse::<Ipv4Addr>().is_err() {
srverr!("Gateway address is formatted incorrectly");
}
let mac_parts = source_mac.split(":").collect::<Vec<_>>();
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::<SqlitePool>();
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::<SqlitePool>();
sqlx::query!("DELETE FROM beacon_template WHERE template_id = ?", template_id)
.execute(&db)
.await?;
Ok(())
}
#[server]
pub async fn get_templates() -> Result<Vec<BeaconTemplate>, ServerFnError> {
let user = user::get_auth_session().await?;
if user.is_none() {
return Err(ServerFnError::<NoCustomError>::ServerError("You are not signed in!".to_owned()));
}
let db = expect_context::<SqlitePool>();
Ok(sqlx::query_as("SELECT * FROM beacon_template")
.fetch_all(&db)
.await?)
}
#[component]
pub fn TemplatesView() -> impl IntoView {
view! {
<div>
let BeaconResources { configs, listeners, categories, templates, .. } = expect_context();
view! {
<div class="templates">
<h2>"Beacon templates"</h2>
<p>
"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."
</p>
<p>
"To create a new template, you will need:"
</p>
<ul>
<li>"Operating system: the OS the beacon will run on"</li>
<li>"Listener: where the beacon will call back to, and what cert will be used"</li>
<li>"Configuration: what configuration the beacon will use to call back with"</li>
<li>"Default category: when a new beacon calls back, place it in this category (can be left empty for no category)"</li>
<li>"Source IP address: Sparse can't use any IP addresses already in use on the network, so choose one here"</li>
<li>"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"</li>
<li>"Source networking mode: choose host to gather the gateway and netmask from the host, or specify your own (maybe custom NAT is going on)"</li>
</ul>
<Suspense fallback=|| view! { <p>"Loading..."</p> }>
{ move || Suspend::new(async move {
let configs = match configs.await {
Ok(cs) => cs,
Err(e) => return Either::Left(view! {
<p>{"There was an error loading configs:".to_string()}</p>
<p>{format!("error: {}", e)}</p>
})
};
let listeners = match listeners.await {
Ok(cs) => cs,
Err(e) => return Either::Left(view! {
<p>{"There was an error loading listeners:".to_string()}</p>
<p>{format!("error: {}", e)}</p>
})
};
let categories = match categories.await {
Ok(cs) => cs,
Err(e) => return Either::Left(view! {
<p>{"There was an error loading categories:".to_string()}</p>
<p>{format!("error: {}", e)}</p>
})
};
let templates = match templates.await {
Ok(ts) => ts,
Err(e) => return Either::Left(view! {
<p>{"There was an error loading templates:".to_string()}</p>
<p>{format!("error: {}", e)}</p>
})
};
Either::Right(view! {
<AddTemplateForm
configs=configs.clone()
listeners=listeners.clone()
categories=categories.clone()
/>
<DisplayTemplates
configs
listeners
categories
templates
/>
})
})}
</Suspense>
</div>
}
}
#[component]
pub fn AddTemplateForm(
configs: Vec<super::configs::BeaconConfig>,
listeners: Vec<super::listeners::PubListener>,
categories: Vec<super::categories::Category>,
) -> impl IntoView {
let BeaconResources { add_template, .. } = expect_context();
view! {
{configs.is_empty()
.then(|| view! {
<span class="error">"Missing configurations! Cannot create a template without a configuration"</span>
})}
{listeners.is_empty()
.then(|| view! {
<span class="error">"Missing listeners! Cannot create a template without a listener"</span>
})}
<ActionForm action=add_template>
<fieldset>
{move || match add_template.value().get() {
Some(Ok(_)) => Either::Right(()),
None => Either::Right(()),
Some(Err(e)) => Either::Left(view! {
<p>"Error creating template:"</p>
<p>{format!("{e:?}")}</p>
})
}}
<legend>"Create new template"</legend>
<label>"Name"</label>
<input name="template_name"/>
<label>"Operating System"</label>
<select name="operating_system">
<option value="linux">"Linux"</option>
<option value="windows">"Windows"</option>
<option value="freebsd">"Freebsd"</option>
</select>
<label>"Listener"</label>
<select name="listener_id">
<option value="0">"---"</option>
{listeners
.iter()
.map(|listener| view! {
<option value=listener.listener_id.to_string()>{listener.domain_name.clone()}</option>
})
.collect_view()}
</select>
<label>"Configuration"</label>
<select name="config_id">
<option value="0">"---"</option>
{configs
.iter()
.map(|config| view! {
<option value=config.config_id.to_string()>{config.config_name.clone()}</option>
})
.collect_view()}
</select>
<label>"Default category for new instances"</label>
<select name="default_category">
<option value="0">"---"</option>
{categories
.iter()
.map(|category| view! {
<option value=category.category_id.to_string()>{category.category_name.clone()}</option>
})
.collect_view()}
</select>
<label>"Source IP address"</label>
<input name="source_ip"/>
<label>"Source MAC address (can be empty)"</label>
<input name="source_mac" value="00:00:00:00:00:00"/>
<label>"Source networking mode"</label>
<select name="source_mode">
<option value="host">"Host"</option>
<option value="custom">"Custom"</option>
</select>
<label class="mode-custom">"Custom CIDR mask"</label>
<input class="mode-custom" type="number" name="source_netmask" value="24"/>
<label class="mode-custom">"Network gateway"</label>
<input class="mode-custom" name="source_gateway"/>
<div></div>
<input type="submit" value="Submit" />
</fieldset>
</ActionForm>
}
}
#[component]
pub fn DisplayTemplates(
configs: Vec<super::configs::BeaconConfig>,
listeners: Vec<super::listeners::PubListener>,
categories: Vec<super::categories::Category>,
templates: Vec<BeaconTemplate>
) -> impl IntoView {
let BeaconResources { remove_template, .. } = expect_context();
let templates_view = templates
.iter()
.map(|template| view! {
<li>
<h4>
{template.template_id}
": "
{template.template_name.clone()}
" ("
{template.operating_system.clone()}
")"
</h4>
<div>
<button>
"Download installer"
</button>
<button
on:click={
let template_id = template.template_id;
move |_| {
remove_template.dispatch(RemoveTemplate { template_id });
}
}
>
"Delete template"
</button>
</div>
<div>
<ul>
<li>"Source IP: "{template.source_ip.clone()}</li>
<li>"Source MAC: "{template.source_mac.clone().unwrap_or("00:00:00:00:00:00".to_owned())}</li>
<li>
"Source mode: "
{match template.source_mode.clone() {
BeaconSourceMode::Host => "Host".to_owned(),
BeaconSourceMode::Custom(nm, gw) => format!("Custom; netmask: {nm}, gateway: {gw}")
}}
</li>
<li>
"Configuration: "
{configs
.iter()
.find(|config| config.config_id == template.config_id)
.map(|config| config.config_name.clone())}
</li>
<li>
"Listener: "
{listeners
.iter()
.find(|listener| listener.listener_id == template.listener_id)
.map(|listener| listener.domain_name.clone())}
</li>
{template
.default_category
.map(|cat_id|
categories
.iter()
.find(|cat| cat.category_id == cat_id)
)
.flatten()
.map(|cat| view! {
<li>
"Default category: "
{cat.category_name.clone()}
</li>
})}
</ul>
</div>
</li>
})
.collect_view();
view! {
<h3>"Current beacon templates"</h3>
{move || match remove_template.value().get() {
Some(Ok(_)) => Either::Right(()),
None => Either::Right(()),
Some(Err(e)) => Either::Left(view! {
<p>"Error deleting template:"</p>
<p>{format!("{e:?}")}</p>
})
}}
<ul>
{templates_view}
</ul>
}
}

View File

@ -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;
}
}

View File

@ -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;