Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 28 additions & 16 deletions clients/wraith-redops/operator-client/package.json
Original file line number Diff line number Diff line change
@@ -1,35 +1,47 @@
{
"name": "wraith-redops-client",
"private": true,
"version": "0.0.0",
"version": "2.2.5",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"build": "tsc -b && vite build",
"preview": "vite preview",
"tauri": "tauri"
"tauri": "tauri",
"lint": "eslint ."
},
"dependencies": {
"@tauri-apps/api": "^1.5.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"@tailwindcss/vite": "^4.1.17",
"@tauri-apps/api": "^2.9.1",
"@tauri-apps/plugin-dialog": "^2.4.2",
"@tauri-apps/plugin-fs": "^2.4.4",
"@tauri-apps/plugin-shell": "^2.4.2",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"xterm": "^5.3.0",
"xterm-addon-fit": "^0.8.0",
"zustand": "^4.4.0",
"zustand": "^5.0.9",
"@tanstack/react-query": "^5.0.0",
"lucide-react": "^0.290.0",
"clsx": "^2.0.0",
"tailwind-merge": "^2.0.0"
},
"devDependencies": {
"@tauri-apps/cli": "^1.5.0",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@vitejs/plugin-react": "^4.0.0",
"autoprefixer": "^10.4.16",
"postcss": "^8.4.31",
"tailwindcss": "^3.3.0",
"typescript": "^5.0.2",
"vite": "^4.4.0"
"@eslint/js": "^9.39.1",
"@tauri-apps/cli": "^2.0.0",
"@types/node": "^24.10.9",
"@types/react": "^19.2.5",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"autoprefixer": "^10.4.22",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.17",
"typescript": "~5.9.3",
"typescript-eslint": "^8.46.4",
"vite": "^7.3.1"
}
}
48 changes: 39 additions & 9 deletions clients/wraith-redops/operator-client/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,22 +1,52 @@
[package]
name = "wraith-redops-client"
version = "0.0.0"
version = "2.2.5"
description = "WRAITH RedOps Operator Console"
authors = ["you"]
edition = "2021"
authors = ["WRAITH Protocol Contributors"]
license = "MIT OR Apache-2.0"
repository = "https://github.com/doublegate/WRAITH-Protocol"
edition = "2024"
rust-version = "1.88"

[lib]
name = "wraith_redops_client_lib"
crate-type = ["staticlib", "cdylib", "rlib"]

[[bin]]
name = "wraith-redops-client"
path = "src/main.rs"

[build-dependencies]
tauri-build = { version = "1.5", features = [] }
tonic-build = "0.10"
tauri-build = { version = "2.2", features = [] }
tonic-build = "0.12"

[dependencies]
tauri = { version = "1.5", features = ["shell-open"] }
# Tauri 2.x
tauri = { version = "2.2", features = [] }
tauri-plugin-dialog = "2.2"
tauri-plugin-fs = "2.2"
tauri-plugin-shell = "2.2"

# Serialization
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tonic = "0.10"
prost = "0.12"
prost-types = "0.12"

# gRPC
tonic = "0.12"
prost = "0.13"
prost-types = "0.13"

# Async runtime
tokio = { version = "1", features = ["full"] }

# Logging
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

# WRAITH Protocol
wraith-core = { path = "../../../../crates/wraith-core" }
wraith-crypto = { path = "../../../../crates/wraith-crypto" }

[features]
default = ["custom-protocol"]
custom-protocol = ["tauri/custom-protocol"]
5 changes: 1 addition & 4 deletions clients/wraith-redops/operator-client/src-tauri/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
tonic_build::configure()
.build_server(false)
.build_client(true) // This is the client
.compile(
&["../../proto/redops.proto"],
&["../../proto"],
)?;
.compile_protos(&["../../proto/redops.proto"], &["../../proto"])?;
tauri_build::build();
Ok(())
}
236 changes: 236 additions & 0 deletions clients/wraith-redops/operator-client/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
//! WRAITH RedOps Operator Console
//!
//! A Tauri desktop application for red team operations.
//! Provides a GUI interface for managing campaigns, implants, and commands.
//!
//! ## Features
//! - Campaign management
//! - Implant monitoring
//! - Command execution
//! - Real-time status updates
//!
//! ## Architecture
//! - Frontend: React 19 + TypeScript + Tailwind CSS v4
//! - Backend: Tauri 2.x + Rust
//! - Communication: gRPC to team server

use serde::{Deserialize, Serialize};
use tauri::State;
use tokio::sync::Mutex;
use tonic::transport::Channel;

// Import generated protos
pub mod wraith {
pub mod redops {
tonic::include_proto!("wraith.redops");
}
}
use wraith::redops::operator_service_client::OperatorServiceClient;
use wraith::redops::*;

// JSON-friendly wrapper types for frontend communication
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CampaignJson {
pub id: String,
pub name: String,
pub description: String,
pub status: String,
pub implant_count: i32,
pub active_implant_count: i32,
}

impl From<Campaign> for CampaignJson {
fn from(c: Campaign) -> Self {
Self {
id: c.id,
name: c.name,
description: c.description,
status: c.status,
implant_count: c.implant_count,
active_implant_count: c.active_implant_count,
}
}
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImplantJson {
pub id: String,
pub campaign_id: String,
pub hostname: String,
pub internal_ip: String,
pub external_ip: String,
pub os_type: String,
pub os_version: String,
pub architecture: String,
pub username: String,
pub domain: String,
pub privileges: String,
pub implant_version: String,
pub checkin_interval: i32,
pub jitter_percent: i32,
pub status: String,
}

impl From<Implant> for ImplantJson {
fn from(i: Implant) -> Self {
Self {
id: i.id,
campaign_id: i.campaign_id,
hostname: i.hostname,
internal_ip: i.internal_ip,
external_ip: i.external_ip,
os_type: i.os_type,
os_version: i.os_version,
architecture: i.architecture,
username: i.username,
domain: i.domain,
privileges: i.privileges,
implant_version: i.implant_version,
checkin_interval: i.checkin_interval,
jitter_percent: i.jitter_percent,
status: i.status,
}
}
}

struct ClientState {
client: Mutex<Option<OperatorServiceClient<Channel>>>,
}

#[tauri::command]
async fn connect_to_server(
address: String,
state: State<'_, ClientState>,
) -> Result<String, String> {
let endpoint = if address.starts_with("http") {
address
} else {
format!("http://{}", address)
};

let client = OperatorServiceClient::connect(endpoint)
.await
.map_err(|e| format!("Failed to connect: {}", e))?;

let mut lock = state.client.lock().await;
*lock = Some(client);

Ok("Connected successfully".to_string())
}

#[tauri::command]
async fn create_campaign(
name: String,
description: String,
state: State<'_, ClientState>,
) -> Result<String, String> {
let mut lock = state.client.lock().await;
let client = lock.as_mut().ok_or("Not connected")?;

let request = tonic::Request::new(CreateCampaignRequest {
name,
description,
roe_document: vec![],
roe_signature: vec![],
});

let response = client
.create_campaign(request)
.await
.map_err(|e| format!("gRPC error: {}", e))?;

let campaign_json: CampaignJson = response.into_inner().into();
serde_json::to_string(&campaign_json).map_err(|e| e.to_string())
}

#[tauri::command]
async fn list_implants(state: State<'_, ClientState>) -> Result<String, String> {
let mut lock = state.client.lock().await;
let client = lock.as_mut().ok_or("Not connected")?;

let request = tonic::Request::new(ListImplantsRequest {
campaign_id: String::new(), // List all for now
status_filter: String::new(),
page_size: 100,
page_token: String::new(),
});

let response = client
.list_implants(request)
.await
.map_err(|e| format!("gRPC error: {}", e))?;

let implants: Vec<ImplantJson> = response
.into_inner()
.implants
.into_iter()
.map(|i| i.into())
.collect();

serde_json::to_string(&implants).map_err(|e| e.to_string())
}

#[tauri::command]
async fn send_command(
implant_id: String,
command_type: String,
payload: String,
state: State<'_, ClientState>,
) -> Result<String, String> {
let mut lock = state.client.lock().await;
let client = lock.as_mut().ok_or("Not connected")?;

let request = tonic::Request::new(SendCommandRequest {
implant_id,
command_type,
payload: payload.into_bytes(),
priority: 1,
timeout_seconds: 30,
});

let response = client
.send_command(request)
.await
.map_err(|e| format!("gRPC error: {}", e))?;

Ok(response.into_inner().id)
}

/// Initialize and run the Tauri application
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
// Initialize logging
if std::env::var("RUST_LOG").is_err() {
// SAFETY: This is called at the start of main before any threads are spawned
unsafe { std::env::set_var("RUST_LOG", "info") };
}
tracing_subscriber::fmt::init();

tracing::info!("Starting WRAITH RedOps Operator Console");
tracing::warn!("IMPORTANT: This tool is for AUTHORIZED red team operations ONLY");

tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_shell::init())
.manage(ClientState {
client: Mutex::new(None),
})
.invoke_handler(tauri::generate_handler![
connect_to_server,
create_campaign,
list_implants,
send_command
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

#[cfg(test)]
mod tests {
#[test]
fn test_module_compiles() {
// This test verifies that the module compiles correctly
assert!(true);
}
}
Loading