516 lines
18 KiB
Rust
516 lines
18 KiB
Rust
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<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,
|
|
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::<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>();
|
|
|
|
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::<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 {
|
|
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"/>
|
|
<label class="mode-custom">"Network interface name"</label>
|
|
<input class="mode-custom" name="source_interface"/>
|
|
<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>
|
|
<a
|
|
class="button"
|
|
download=""
|
|
href=format!("/installer/{}", template.template_id)
|
|
>
|
|
"Download installer"
|
|
</a>
|
|
<button
|
|
on:click={
|
|
let template_id = template.template_id;
|
|
move |_| {
|
|
remove_template.dispatch(RemoveTemplate { template_id });
|
|
}
|
|
}
|
|
class="warning"
|
|
>
|
|
"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>
|
|
}
|
|
}
|