feat: added most beacon DS and listener CRUD

This commit is contained in:
Andrew Rioux 2025-01-30 20:59:14 -05:00
parent 0d6b2b4c16
commit b381261cea
Signed by: andrew.rioux
GPG Key ID: 9B8BAC47C17ABB94
26 changed files with 599 additions and 26 deletions

33
Cargo.lock generated
View File

@ -2191,6 +2191,16 @@ dependencies = [
"tokio-stream", "tokio-stream",
] ]
[[package]]
name = "pem"
version = "3.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae"
dependencies = [
"base64",
"serde",
]
[[package]] [[package]]
name = "pem-rfc7468" name = "pem-rfc7468"
version = "0.7.0" version = "0.7.0"
@ -2430,6 +2440,19 @@ dependencies = [
"getrandom", "getrandom",
] ]
[[package]]
name = "rcgen"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2"
dependencies = [
"pem",
"ring",
"rustls-pki-types",
"time",
"yasna",
]
[[package]] [[package]]
name = "reactive_graph" name = "reactive_graph"
version = "0.1.4" version = "0.1.4"
@ -2971,6 +2994,7 @@ dependencies = [
"leptos_meta", "leptos_meta",
"leptos_router", "leptos_router",
"pbkdf2", "pbkdf2",
"rcgen",
"rpassword", "rpassword",
"serde", "serde",
"sha2", "sha2",
@ -4291,6 +4315,15 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
[[package]]
name = "yasna"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd"
dependencies = [
"time",
]
[[package]] [[package]]
name = "yoke" name = "yoke"
version = "0.7.5" version = "0.7.5"

View File

@ -39,6 +39,7 @@ sha2 = { version = "0.10", optional = true }
hex = { version = "0.4", optional = true } hex = { version = "0.4", optional = true }
serde = "1.0" serde = "1.0"
cfg-if = "1.0.0" cfg-if = "1.0.0"
rcgen = { version = "0.13.2", optional = true }
[features] [features]
hydrate = ["leptos/hydrate", "chrono/wasmbind"] hydrate = ["leptos/hydrate", "chrono/wasmbind"]
@ -60,6 +61,7 @@ ssr = [
"dep:pbkdf2", "dep:pbkdf2",
"dep:sha2", "dep:sha2",
"dep:hex", "dep:hex",
"dep:rcgen",
"leptos/ssr", "leptos/ssr",
"leptos_meta/ssr", "leptos_meta/ssr",
"leptos_router/ssr", "leptos_router/ssr",

View File

@ -0,0 +1,121 @@
CREATE TABLE beacon_listener (
listener_id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
port int NOT NULL,
public_ip varchar NOT NULL,
domain_name varchar NOT NULL,
certificate varchar NOT NULL,
privkey varchar NOT NULL
);
CREATE TABLE beacon_config (
config_id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
mode text check (mode in ('single', 'regular', 'random', 'cron')),
regular_interval int,
random_min_time int,
random_max_time int,
cron_schedule varchar
);
CREATE TABLE beacon_category (
category_id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
category_name varchar NOT NULL
);
CREATE TABLE beacon_template (
template_id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
template_name varchar NOT NULL,
operating_system varchar NOT NULL,
config_id int NOT NULL,
listener_id int NOT NULL,
source_ip varchar NOT NULL,
source_mac varchar,
source_mode text check (source_mode in ('host', 'custom')),
source_netmask int,
source_gateway varchar,
FOREIGN KEY (config_id) REFERENCES beacon_config
FOREIGN KEY (listener_id) REFERENCES beacon_listener
);
CREATE TABLE beacon_instance (
beacon_id varchar PRIMARY KEY NOT NULL,
template_id int NOT NULL,
peer_ip varchar NOT NULL,
nickname varchar NOT NULL,
cwd varchar NOT NULL,
operating_system varchar NOT NULL,
beacon_userent varchar NOT NULL,
hostname varchar NOT NULL,
FOREIGN KEY (template_id) REFERENCES beacon_template
);
CREATE TABLE beacon_category_assignment (
category_id int,
beacon_id varchar,
PRIMARY KEY (category_id, beacon_id),
FOREIGN KEY (category_id) REFERENCES beacon_category,
FOREIGN KEY (beacon_id) REFERENCES beacon_instance
);
CREATE TABLE beacon_config_assignment (
config_id int,
beacon_id varchar,
PRIMARY KEY (config_id, beacon_id),
FOREIGN KEY (config_id) REFERENCES beacon_config,
FOREIGN KEY (beacon_id) REFERENCES beacon_instance
);
CREATE TABLE beacon_command (
command_id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
cmd_type text check (cmd_type in ('update', 'exec', 'install', 'upload', 'download', 'reload_config', 'chdir', 'ls')),
exec_command varchar,
install_target varchar,
upload_src varchar,
upload_dest varchar,
download_path varchar,
config_id int,
FOREIGN KEY (config_id) REFERENCES beacon_config
);
CREATE TABLE beacon_command_invocation (
beacon_id varchar NOT NULL,
command_id int NOT NULL,
invoker_id int NOT NULL,
invocation_date int,
invocation_result varchar,
PRIMARY KEY (beacon_id, command_id),
FOREIGN KEY (command_id) REFERENCES beacon_command,
FOREIGN KEY (beacon_id) REFERENCES beacon_instance,
FOREIGN KEY (invoker_id) REFERENCES users
);
CREATE TABLE beacon_checkin (
beacon_id varchar NOT NULL,
checkin_date int,
FOREIGN KEY (beacon_id) REFERENCES beacon_instance
);

View File

@ -1,7 +1,7 @@
use leptos::prelude::*; use leptos::prelude::*;
use leptos_meta::{provide_meta_context, MetaTags, Stylesheet, Title}; use leptos_meta::{provide_meta_context, MetaTags, Stylesheet, Title};
use leptos_router::{ use leptos_router::{
components::{A, Route, Router, Routes}, components::{A, ParentRoute, Route, Router, Routes},
hooks::use_query_map, hooks::use_query_map,
path path
}; };
@ -30,12 +30,8 @@ pub struct User {
#[server] #[server]
pub async fn me() -> Result<Option<User>, ServerFnError> { pub async fn me() -> Result<Option<User>, ServerFnError> {
tracing::info!("I'm being checked!");
let user = crate::db::user::get_auth_session().await?; let user = crate::db::user::get_auth_session().await?;
tracing::debug!("User returned: {:?}", user);
Ok(user.map(|user| User { Ok(user.map(|user| User {
user_id: user.user_id, user_id: user.user_id,
user_name: user.user_name user_name: user.user_name
@ -149,12 +145,22 @@ pub fn App() -> impl IntoView {
</Suspense> </Suspense>
</nav> </nav>
<aside class="beacons"> <crate::beacons::BeaconSidebar />
</aside>
<Routes fallback=|| "Page not found.".into_view()> <Routes fallback=|| "Page not found.".into_view()>
<Route path=path!("users") view=crate::users::UserView /> <Route path=path!("users") view=crate::users::UserView />
<Route path=path!("login") view=move || view! { <LoginPage login/> }/> <Route path=path!("login") view=move || view! { <LoginPage login/> }/>
<ParentRoute path=path!("beacons") view=crate::beacons::BeaconView>
<Route path=path!("categories") view=crate::beacons::CategoriesView/>
<Route path=path!("commands") view=crate::beacons::CommandsView/>
<Route path=path!("configs") view=crate::beacons::ConfigsView/>
<Route path=path!("templates") view=crate::beacons::TemplatesView/>
<Route path=path!("instances") view=crate::beacons::InstancesView/>
<Route path=path!("listeners") view=crate::beacons::ListenersView/>
<Route path=path!("") view=|| view! {
<p>"Select a menu item on the left to get started"</p>
}/>
</ParentRoute>
<Route path=path!("") view=HomePage/> <Route path=path!("") view=HomePage/>
</Routes> </Routes>
</Router> </Router>

View File

@ -0,0 +1,10 @@
use std::{
collections::HashMap,
sync::{Arc, RwLock},
};
use tokio::task::JoinHandle;
pub type BeaconListenerHandle = JoinHandle<()>;
pub type BeaconListenerMap = Arc<RwLock<HashMap<i64, BeaconListenerHandle>>>;

View File

@ -0,0 +1,54 @@
use leptos::prelude::*;
use leptos_router::{components::A, nested_router::Outlet};
mod categories;
mod commands;
mod configs;
mod instances;
mod listeners;
mod templates;
pub use categories::CategoriesView;
pub use commands::CommandsView;
pub use configs::ConfigsView;
pub use instances::InstancesView;
pub use listeners::ListenersView;
pub use templates::TemplatesView;
#[component]
pub fn BeaconView() -> impl IntoView {
#[cfg(feature = "hydrate")]
Effect::new(move || {
let user = expect_context::<ReadSignal<Option<crate::app::User>>>();
if user.get().is_none() {
let navigate = leptos_router::hooks::use_navigate();
navigate("/login?next=beacons", Default::default());
}
});
view! {
<main class="beacons">
<aside class="beacon-menu">
<ul>
<li><A href="/beacons/listeners">"Listeners"</A></li>
<li><A href="/beacons/configs">"Configs"</A></li>
<li><A href="/beacons/categories">"Categories"</A></li>
<li><A href="/beacons/templates">"Templates"</A></li>
<li><A href="/beacons/instances">"Instances"</A></li>
<li><A href="/beacons/commands">"Commands"</A></li>
</ul>
</aside>
<Outlet />
</main>
}
}
#[component]
pub fn BeaconSidebar() -> impl IntoView {
view! {
<aside class="beacons">
</aside>
}
}

View File

@ -0,0 +1,13 @@
use leptos::prelude::*;
#[component]
pub fn CategoriesView() -> impl IntoView {
view! {
<div>
<p>"Categories"</p>
<ul>
<li>"Windows"</li>
</ul>
</div>
}
}

View File

@ -0,0 +1,10 @@
use leptos::prelude::*;
#[component]
pub fn CommandsView() -> impl IntoView {
view! {
<div>
</div>
}
}

View File

@ -0,0 +1,10 @@
use leptos::prelude::*;
#[component]
pub fn ConfigsView() -> impl IntoView {
view! {
<div>
</div>
}
}

View File

@ -0,0 +1,10 @@
use leptos::prelude::*;
#[component]
pub fn InstancesView() -> impl IntoView {
view! {
<div>
</div>
}
}

View File

@ -0,0 +1,226 @@
use std::net::Ipv4Addr;
use leptos::{either::Either, prelude::*};
use serde::{Serialize, Deserialize};
#[cfg(feature = "ssr")]
use {
sqlx::SqlitePool,
leptos::server_fn::error::NoCustomError,
crate::{db::user, beacon_handler::BeaconListenerMap},
rcgen::{generate_simple_self_signed, CertifiedKey},
};
#[cfg(feature = "ssr")]
struct DbListener {
listener_id: i64,
port: i64,
public_ip: String,
domain_name: String
}
#[derive(Clone, Serialize, Deserialize)]
pub struct PubListener {
listener_id: i64,
port: i64,
public_ip: String,
domain_name: String,
active: bool
}
#[server]
pub async fn get_listeners() -> Result<Vec<PubListener>, 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>();
let beacon_handles = expect_context::<BeaconListenerMap>();
let listeners = sqlx::query_as!(
DbListener,
"SELECT listener_id, port, public_ip, domain_name FROM beacon_listener"
)
.fetch_all(&db)
.await?;
let Ok(beacon_handles_handle) = beacon_handles.read() else {
return Err(ServerFnError::<NoCustomError>::ServerError("".to_string()));
};
Ok(listeners
.into_iter()
.map(|b| PubListener {
listener_id: b.listener_id,
port: b.port,
public_ip: b.public_ip,
domain_name: b.domain_name,
active: beacon_handles_handle
.get(&b.listener_id)
.map(|h| !h.is_finished())
.unwrap_or(false)
})
.collect())
}
#[server]
pub async fn add_listener(public_ip: String, port: i16, domain_name: String) -> Result<(), ServerFnError> {
let user = user::get_auth_session().await?;
if user.is_none() {
return Err(ServerFnError::<NoCustomError>::ServerError("You are not signed in!".to_owned()));
}
if public_ip.parse::<Ipv4Addr>().is_err() {
return Err(ServerFnError::<NoCustomError>::ServerError("Unable to parse public IP address".to_owned()));
}
let subject_alt_names = vec![public_ip.to_string(), domain_name.clone()];
let CertifiedKey { cert, key_pair } = tokio::task::spawn_blocking(|| {
generate_simple_self_signed(subject_alt_names)
}).await??;
let db = expect_context::<SqlitePool>();
let public_ip = public_ip.to_string();
let cert = cert.pem().to_string();
let key_pair = key_pair.serialize_pem().to_string();
sqlx::query!(
"INSERT INTO beacon_listener (port, public_ip, domain_name, certificate, privkey) VALUES (?, ?, ?, ?, ?)",
port,
public_ip,
domain_name,
cert,
key_pair
)
.execute(&db)
.await?;
Ok(())
}
#[server]
pub async fn start_listener(listener_id: i64) -> Result<(), ServerFnError> {
unimplemented!()
}
#[component]
pub fn ListenersView() -> impl IntoView {
let add_listener = ServerAction::<AddListener>::new();
let listeners = Resource::new(
move || add_listener.version().get(),
|_| async { get_listeners().await }
);
view! {
<div class="listeners">
<ActionForm action=add_listener>
<fieldset>
{move || match add_listener.value().get() {
Some(Ok(_)) => Either::Right(()),
None => Either::Right(()),
Some(Err(e)) => Either::Left(view! {
<p>"Error creating listener:"</p>
<p>{format!("{e:?}")}</p>
})
}}
<legend>"Add a new listener"</legend>
<label>"Public IP address"</label>
<input name="public_ip"/>
<label>"Port"</label>
<input name="port" type="number"/>
<label>"Domain name (for HTTPS)"</label>
<input name="domain_name"/>
<div></div>
<input type="submit" value="Submit"/>
</fieldset>
</ActionForm>
<Suspense fallback=|| view! { <p>"Loading..."</p> }>
{ move || match listeners.get() {
Some(inner) => Either::Right(match inner {
Err(e) => Either::Left(view! {
<p>"There was an error loading listeners:"</p>
<p>{format!("error: {}", e)}</p>
}),
Ok(ls) => Either::Right(view! {
<DisplayListeners
listener_resource=listeners
listeners=ls
/>
})
}),
None => Either::Left(view! {
<p>"Loading..."</p>
})
}}
</Suspense>
</div>
}
}
#[component]
fn DisplayListeners(listener_resource: Resource<Result<Vec<PubListener>, ServerFnError>>, listeners: Vec<PubListener>) -> impl IntoView {
let (error_msg, set_error_msg) = signal(None);
let start_listener_action = Action::new(move |&id: &i64| async move {
match start_listener(id).await {
Ok(()) => {
listener_resource.refetch();
}
Err(e) => {
set_error_msg(Some(e.to_string()));
}
}
});
let listeners_view = listeners
.iter()
.map(|listener| view! {
<li>
{listener.listener_id}
": "
{listener.domain_name.clone()}
" ("
{listener.public_ip.clone()}
":"
{listener.port}
")"
{match listener.active {
true => Either::Left(view! {
<span>"active!"</span>
}),
false => Either::Right(view! {
<button
on:click={
let id = listener.listener_id;
move |e| {
let _ = e.prevent_default();
start_listener_action.dispatch(id);
}
}
>
"activate"
</button>
})
}}
</li>
})
.collect_view();
view! {
{move || error_msg
.get()
.map(|err| view! {
<p>
"Error starting listener: "
{err}
</p>
})}
<ul>
{listeners_view}
</ul>
}
}

View File

@ -0,0 +1,10 @@
use leptos::prelude::*;
#[component]
pub fn TemplatesView() -> impl IntoView {
view! {
<div>
</div>
}
}

View File

@ -1,10 +1,10 @@
pub mod app; pub mod app;
#[cfg(feature = "ssr")]
pub mod users; pub mod beacon_handler;
pub mod beacons;
pub mod error;
pub mod db; pub mod db;
pub mod error;
pub mod users;
#[cfg(feature = "hydrate")] #[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen] #[wasm_bindgen::prelude::wasm_bindgen]

View File

@ -1,5 +1,5 @@
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
pub(crate) mod beacons { pub(crate) mod beacon_binaries {
#[allow(dead_code)] #[allow(dead_code)]
pub const LINUX_BEACON: &'static [u8] = include_bytes!(std::env!("SPARSE_BEACON_LINUX")); pub const LINUX_BEACON: &'static [u8] = include_bytes!(std::env!("SPARSE_BEACON_LINUX"));
#[allow(dead_code)] #[allow(dead_code)]
@ -15,16 +15,15 @@ pub(crate) mod beacons {
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
mod cli; mod cli;
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
mod webserver; mod webserver;
#[cfg(feature = "ssr")]
mod beacons;
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
pub mod users; pub mod users;
pub mod error; pub mod error;
pub mod db; pub mod db;
pub mod beacon_handler;
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
#[tokio::main] #[tokio::main]
@ -56,7 +55,7 @@ async fn main() -> anyhow::Result<std::process::ExitCode> {
let db_exists = std::fs::metadata(&db_location); let db_exists = std::fs::metadata(&db_location);
let run_init = if let Err(e) = db_exists { if let Err(e) = db_exists {
if !options.init_ok { if !options.init_ok {
tracing::error!("Database doesn't exist, and initialization not allowed!"); tracing::error!("Database doesn't exist, and initialization not allowed!");
tracing::error!("{:?}", e); tracing::error!("{:?}", e);
@ -64,10 +63,7 @@ async fn main() -> anyhow::Result<std::process::ExitCode> {
} }
tracing::info!("Database doesn't exist, readying initialization"); tracing::info!("Database doesn't exist, readying initialization");
true }
} else {
false
};
let pool = SqlitePool::connect_with( let pool = SqlitePool::connect_with(
SqliteConnectOptions::from_str(&format!("sqlite://{}", db_location.to_string_lossy()))? SqliteConnectOptions::from_str(&format!("sqlite://{}", db_location.to_string_lossy()))?

View File

@ -255,7 +255,7 @@ pub fn UserView() -> impl IntoView {
let user = expect_context::<ReadSignal<Option<crate::app::User>>>(); let user = expect_context::<ReadSignal<Option<crate::app::User>>>();
if user.get().is_none() { if user.get().is_none() {
let navigate = leptos_router::hooks::use_navigate(); let navigate = leptos_router::hooks::use_navigate();
navigate("/login?next=/users", Default::default()); navigate("/login?next=users", Default::default());
} }
}); });

View File

@ -12,6 +12,7 @@ pub async fn serve_web(management_address: SocketAddrV4, _bind_address: SocketAd
let conf = get_configuration(None).unwrap(); let conf = get_configuration(None).unwrap();
let leptos_options = conf.leptos_options; let leptos_options = conf.leptos_options;
let routes = generate_route_list(App); let routes = generate_route_list(App);
let beacon_listeners = crate::beacon_handler::BeaconListenerMap::default();
let compression_layer = tower_http::compression::CompressionLayer::new() let compression_layer = tower_http::compression::CompressionLayer::new()
.gzip(true) .gzip(true)
@ -23,7 +24,10 @@ pub async fn serve_web(management_address: SocketAddrV4, _bind_address: SocketAd
.leptos_routes_with_context( .leptos_routes_with_context(
&leptos_options, &leptos_options,
routes, routes,
move || provide_context(db.clone()), move || {
provide_context(std::sync::Arc::clone(&beacon_listeners));
provide_context(db.clone())
},
{ {
let leptos_options = leptos_options.clone(); let leptos_options = leptos_options.clone();
move || shell(leptos_options.clone()) move || shell(leptos_options.clone())

View File

@ -0,0 +1,38 @@
@use 'beacons/listeners';
@use 'beacons/configs';
@use 'beacons/categories';
@use 'beacons/templates';
@use 'beacons/instances';
@use 'beacons/commands';
main.beacons {
display: grid;
grid-template-columns: 120px 1fr;
padding: 0;
aside.beacon-menu {
border-right: 1px solid #2e2e59;
ul {
list-style-type: none;
padding: 0;
margin: 0;
}
li a, li a:visited {
display: inline-block;
padding: 10px;
color: white;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
}
aside.beacons {
}

View File

@ -1,3 +1,14 @@
main.main { main.main {
} }
main.login {
form {
display: grid;
grid-template-columns: 125px 200px;
input, label {
margin: 10px;
}
}
}

View File

@ -0,0 +1,14 @@
main.beacons div.listeners {
form {
margin: 10px;
}
fieldset {
display: grid;
grid-template-columns: 300px 200px;
input, label {
margin: 10px;
}
}
}

View File

@ -1,5 +1,10 @@
@use '_users'; @use '_users';
@use '_main'; @use '_main';
@use '_beacons';
* {
box-sizing: border-box;
}
html, body { html, body {
font-family: sans-serif; font-family: sans-serif;

View File

@ -10,10 +10,12 @@ typedef struct {
typedef union SourceIp { typedef union SourceIp {
struct { struct {
char mode; // set to 0 char mode; // set to 0
char source_mac[6];
ipaddr_t source_ip; ipaddr_t source_ip;
} use_host_networking; } use_host_networking;
struct { struct {
char mode; // set to 1 char mode; // set to 1
char source_mac[6];
unsigned short netmask; unsigned short netmask;
ipaddr_t source_ip; ipaddr_t source_ip;
ipaddr_t gateway; ipaddr_t gateway;
@ -25,8 +27,6 @@ typedef struct Parameters {
SourceIp_t source_ip; SourceIp_t source_ip;
unsigned short destination_port; unsigned short destination_port;
unsigned short pubkey_cert_size; unsigned short pubkey_cert_size;
unsigned short privkey_size;
unsigned short privkey_cert_size;
unsigned short beacon_name_length; unsigned short beacon_name_length;
unsigned short domain_name_length; unsigned short domain_name_length;
char pubkey_cert[1024]; char pubkey_cert[1024];