Files
tetanus/src/appstate.rs
T
pyro aff72fc275 changed the portscanning function to make it faster. It's using a
combination of multithreading and async now, each host or target for the
scan runs on its own thread, then each port check is run asynchronously.
2026-09-04 17:46:20 -05:00

2629 lines
129 KiB
Rust

use crate::Project;
use crate::Prompt;
use crate::PromptQuery;
use crate::ToolMessage;
use crate::ToolProgress;
use crate::client::Server;
use crate::host::Host;
use crate::modules::ModuleLoader;
use crate::project::DistroBox;
use crate::rust_tools::rustwitness;
use crate::rust_tools::*;
use crate::victim;
use clipboard::ClipboardContext;
use clipboard::ClipboardProvider;
use dns_lookup::lookup_host;
use fs_extra::dir::{CopyOptions, copy};
use ipnet::IpNet;
use keyring::Entry;
use rayon::prelude::*;
use rhai::{Dynamic, Scope};
use std::collections::HashMap;
use std::env;
use std::error::Error;
use std::fs::OpenOptions;
use std::fs::{self, File, create_dir_all, read_dir, read_to_string, remove_dir, remove_dir_all};
use std::io::Write;
use std::net::IpAddr;
use std::path::PathBuf;
use std::process::Command;
use std::sync::RwLock;
use std::sync::{Arc, Mutex, mpsc::Sender, mpsc::channel};
use std::thread;
use std::time::Duration;
use std::time::{self, Instant};
use walkdir::WalkDir;
pub struct AppState {
pub projects: Vec<Project>,
pub servers: Vec<Arc<Mutex<Server>>>,
pub config: HashMap<String, String>,
pub config_file: PathBuf,
pub workers: rayon::ThreadPool,
pub worker_txes: Vec<Sender<ToolMessage>>,
pub main_tx: Sender<ToolMessage>,
pub history: Vec<String>,
pub log: Vec<String>,
pub output: Vec<String>,
pub selected_server: usize,
pub selected_project: usize,
pub selected_host: usize,
pub curent_intput: String,
pub module_loader: ModuleLoader,
pub output_scroll: u16,
pub output_follow: bool,
pub prompt: Prompt,
pub info_scroll: u16,
pub server_broker_running: bool,
pub app_state_running: bool,
pub remoting: bool,
pub help: Vec<String>,
pub name: String,
pub progress_bars: Vec<Arc<RwLock<ToolProgress>>>,
pub selecting_host: bool,
}
impl AppState {
pub fn new() -> Self {
let (main_tx, _) = channel();
Self {
projects: Vec::new(),
servers: Vec::new(),
config: HashMap::new(),
config_file: PathBuf::new(),
workers: rayon::ThreadPoolBuilder::new()
.num_threads(4)
.build()
.unwrap(),
worker_txes: Vec::new(),
main_tx,
history: Vec::new(),
log: Vec::new(),
output: Vec::new(),
selected_server: 0,
selected_project: 0,
selected_host: 0,
curent_intput: String::new(),
module_loader: ModuleLoader::new(),
output_scroll: 0,
output_follow: true,
prompt: Prompt {
action: None,
responses: Vec::new(),
execute_command: String::new(),
num_responses: 0,
prompts: Vec::new(),
last_prompted: None,
},
info_scroll: 0,
server_broker_running: true,
app_state_running: true,
remoting: false,
help: Vec::new(),
name: String::new(),
progress_bars: Vec::new(),
selecting_host: false,
}
}
pub fn load_config(&mut self, file: PathBuf, display: bool) -> Result<(), Box<dyn Error>> {
self.config_file = file.clone();
let config_contents = read_to_string(file)?;
for line in config_contents.lines() {
let parts: Vec<&str> = line.split(": ").collect();
if parts.len() == 2 {
match parts[0].trim() {
"servers" => {
for server_data in parts[1].trim().split(", ").collect::<Vec<&str>>() {
if let Some((address, name)) = server_data.split_once("|") {
let new_server = Server {
address: address.trim().to_string(),
connected: false,
timer: Duration::from_secs(60),
last_check: time::Instant::now(),
message_que: Vec::new(),
action_que: Vec::new(),
client_id: 0,
selected_client: 0,
config: None,
logged_in: false,
password: String::new(),
name: name.to_string(),
cert_text: String::new(),
};
self.servers.push(Arc::new(Mutex::new(new_server)));
}
}
self.config
.insert("servers".to_string(), parts[1].trim().to_string());
}
_ => {
if parts[0].len() > 1 {
self.config
.insert(parts[0].trim().to_string(), parts[1].trim().to_string());
}
}
}
}
}
let mut projet_folder_path = self.config_file.clone();
projet_folder_path.pop();
projet_folder_path.push("projects");
let project_dir_reses = read_dir(projet_folder_path)?;
for res in project_dir_reses {
if let Ok(project_folder) = res {
let project_folder_file_reses = read_dir(project_folder.path())?;
for res in project_folder_file_reses {
if let Ok(conf_file) = res {
if conf_file.file_name().to_string_lossy() == "project.conf".to_string() {
let mut new_project = Project::new();
new_project.config_folder(conf_file.path());
new_project.load_config(display)?;
let template_box = self.config.get("template_box").unwrap();
let tools = self.config.get("tools").unwrap();
let db = DistroBox {
name: format!(
"{}-{}-{}",
template_box,
new_project.org_name.clone(),
new_project.name.clone()
),
volumes: vec![
new_project.files.display().to_string(),
new_project.notes.display().to_string(),
tools.clone(),
],
created: false,
template: template_box.clone(),
};
new_project.db = Some(db);
self.projects.push(new_project);
}
}
}
}
}
let mut server_folder_path = self.config_file.clone();
server_folder_path.pop();
server_folder_path.push("client_servers");
if server_folder_path.exists() {
let server_confs = read_dir(server_folder_path)?;
server_confs.into_iter().for_each(|entry| {
if let Ok(entry) = entry {
if let Ok(text) = read_to_string(entry.path()) {
let mut name = String::new();
let mut address = String::new();
let mut cert = String::new();
text.lines().into_iter().for_each(|line| {
if let Some((setting, data)) = line.split_once(": ") {
match setting.trim() {
"name" => name = data.trim().to_string(),
"address" => address = data.trim().to_string(),
"cert" => cert = data.trim().to_string(),
_ => {}
}
}
});
let new_server = Server {
address,
connected: false,
timer: Duration::from_secs(90),
config: None,
last_check: Instant::now(),
message_que: Vec::new(),
action_que: Vec::new(),
client_id: 0,
selected_client: 0,
logged_in: false,
password: String::new(),
name,
cert_text: cert,
};
self.servers.push(Arc::new(Mutex::new(new_server)));
}
}
});
}
self.help.push("help\nThat's MEEEEEEE\n".to_string());
self.help.push("new_project\nadd a project to the upcoming project pool and create default notes based on templates\nusage:\nnew_project organization_name, project_name\n".to_string());
self.help.push(
"current_project\nshow information on the currently selected project\n".to_string(),
);
self.help
.push("promote_project\npromote a project from upcoming to current\n".to_string());
self.help.push(
"remove_project\nremove a project, delete its folders, and destroy its distrobox\n"
.to_string(),
);
self.help.push("new_terminal\nopen a new terminal in the distrobox of the currently selected project\n".to_string());
self.help
.push("new_server\ncreate a new server object\n".to_string());
self.help
.push("list_servers\nlist all servers currently tracked by the tool\n".to_string());
self.help
.push("connect_server\nconnect to a tracked server\n".to_string());
self.help
.push("save_all\nsave all projects and settings\n".to_string());
self.help
.push("save_projects\nsave just the projects\n".to_string());
self.help
.push("test_server\ntest the server connection\n".to_string());
self.help.push(
"add_scope\nadd a host, cidr range, or ip address to the current project's scope\n"
.to_string(),
);
self.help.push("parse_scope\nparse the projects general.md notes file for the scope copied from the workbook\n".to_string());
self.help.push("parse_portscan | pps\nParse a services.tsv file exported from cobalt stirke in your current project's files directory\n".to_string());
self.help
.push("stop_db\nrestart this project's distrobox".to_string());
self.help.push("rustwitness\nRun a scan on the hosts in the urls.txt file in yoru project files directory that captures screenshots of those urls, optionally through a proxy.\n".to_string());
self.help.push("portscan\nRun a simple TCP portscan on either the scope for the project, or a given list of ips.\nportscan ip,ip,ip,ip port,port,port,port or portscan port,port,port\n".to_string());
self.help.push("rustbuster\nRun the Subdomain/Subdirectory bruteforcetool RustBuster (included in tetanus now!) against a given domain name or url.\nrustbuster target,target,target subs=/path/to/subwordlist dirs=/path/to/dirwordlist\n".to_string());
self.help
.push("clear_operations\nClear the completed operation statuses.\n".to_string());
self.help
.push("shodan\nGet the shodan info for the currently selected host\n".to_string());
self.help
.push("select_host\nSelect a host in the current project.\n".to_string());
self.help.push(
"shodan_all\nGet the shodan info for all hosts in the current project.\n".to_string(),
);
self.help.push(
"set_shodan_key\nSet your shodan api key, and save it to your system's keyring.\n"
.to_string(),
);
self.help.push(
"save_server\nsave the server and password to your system's keyring.\n".to_string(),
);
self.help.push("parse_nessus\nparse a nessus csv to add hosts and ports to the currently selected project\nexport nessus csv report, only checking the host, protocol, and port boxes.\n".to_string());
self.help.push(
"Shortcuts:\nCTRL+P = new_project\nCTRL+s = save\nCTRL_+h = select_host\n".to_string(),
);
self.help.push("exit\nquit the tool\n".to_string());
self.initialize_modules();
return Ok(());
}
pub fn initialize_modules(&mut self) {
if let Some(base_path_str) = self.config.get("module_path") {
let base_path = PathBuf::from(base_path_str);
println!("Loading Tetanus modules from: {:?}", base_path);
if let Err(e) = self.module_loader.load_all(&base_path) {
eprintln!("Failed to load modules: {}", e);
} else {
for module in &self.module_loader.commands {
self.help.push(format!(
"{}\n{}\n",
module.1.name.clone(),
module.1.help.clone()
));
}
println!(
"Successfully loaded {} modules.",
self.module_loader.commands.len()
);
}
} else {
eprintln!("Warning: 'module_path' is missing from AppState config map!");
}
}
pub fn execute_command(
&mut self,
command_name: &str,
command_args: Option<String>,
rid: usize,
) -> Result<(), Box<dyn Error>> {
let tx = self.main_tx.clone();
match command_name {
"remote" => {
if let Some(server) = self.servers.get(self.selected_server) {
if let Ok(mut server) = server.lock() {
if server.selected_client != 0 {
if let Some(args) = command_args.clone() {
let args = args.trim();
if args.contains(" ") {
let (cmd, iargs) = args.split_once(" ").unwrap();
server.message_que.push(format!("CMD|{} {}", cmd, iargs));
} else {
server.message_que.push(format!("CMD|{}", args));
}
let _ = self.main_tx.send(ToolMessage::Output((
0,
format!("Tasked {} to run {}", server.selected_client, args),
)));
}
}
}
}
}
"new_project" | "np" => {
let args = command_args.unwrap();
let (org, name) = args.split_once(" ").unwrap();
if let Err(e) = self.new_project(org.to_string(), name.to_string(), rid) {
tx.send(ToolMessage::Output((
rid,
format!("[error] making {}-{}: {e}", org, name),
)))?;
} else {
tx.send(ToolMessage::Output((
rid,
format!("[success] {}-{} created!", org, name),
)))?;
}
}
"generate_victim" => {
let mut success = false;
if let Some(args) = command_args {
if args.split(" ").into_iter().count() == 1 {
let mut address = String::new();
let mut password = String::new();
let config_file = self.config_file.clone();
let files = self.projects[self.selected_project].files.clone();
let mut cert_text = String::new();
if let Ok(lock) = self.servers[self.selected_server].lock() {
address = lock.address.clone();
password = lock.password.clone();
cert_text = lock.cert_text.clone();
}
match victim::generate_code(
address,
password,
config_file,
files,
args,
self.main_tx.clone(),
cert_text,
) {
Ok(_) => {
let _ = self.main_tx.send(ToolMessage::Output((
rid,
"[Success] Victim code generated! Compilation thread spawned!"
.to_string(),
)));
success = true;
}
Err(e) => {
let _ = self.main_tx.send(ToolMessage::Output((
0,
format!("[error] generating victim code: {e}"),
)));
}
}
}
}
if !success {
let _ = self.main_tx.send(ToolMessage::Output((
0,
format!("Usage: generate_victim (windows/linux)"),
)));
}
}
"current_project" | "cp" => {
let mut out_vec = Vec::new();
out_vec.push(format!(
"org: {}",
&self.projects[self.selected_project].org_name
));
out_vec.push(format!(
"name: {}",
&self.projects[self.selected_project].name
));
out_vec.push(format!(
"files: {}",
&self.projects[self.selected_project].files.display()
));
out_vec.push(format!(
"notes: {}",
&self.projects[self.selected_project].notes.display()
));
out_vec.push(format!(
"config_folder: {}",
&self.projects[self.selected_project].config_folder.display()
));
if let Some(db) = self.projects[self.selected_project].db.clone() {
out_vec.push(format!("distrobox: {}", db.name));
}
if self.projects[self.selected_project].current {
out_vec.push(format!("status: current"));
} else {
out_vec.push(format!("status: upcoming"));
}
out_vec.push(format!(
"Num hosts: {}",
&self.projects[self.selected_project].hosts.iter().count()
));
for line in out_vec {
let _ = self.main_tx.send(ToolMessage::Output((rid, line)));
}
}
"rebuild_distro_box" | "rdb" => {
if let Some(db) = self.projects[self.selected_project].db.clone() {
if db.created {
let _ = self.main_tx.send(ToolMessage::DestroyDB(db.clone()));
}
let _ = self.main_tx.send(ToolMessage::RebuildDB);
let _ = self.main_tx.send(ToolMessage::Output((
rid,
"[success] Distorbox made!".to_string(),
)));
}
}
"promote_project" | "pp" => {
self.main_tx.send(ToolMessage::Output((
rid,
String::from("promote_project command recieved!"),
)))?;
match self.promote_project(rid) {
Ok(_) => {
self.main_tx.send(ToolMessage::Output((
rid,
String::from("[success] project promoted!"),
)))?;
}
Err(e) => {
self.main_tx.send(ToolMessage::Output((
rid,
format!("[error] promoting project: {}", e),
)))?;
}
}
}
"remove_project_confirm" => {
if self.prompt.responses.len() > 0 {
if self.prompt.responses[0]
.response
.to_lowercase()
.contains("y")
{
let project = self.projects[self.selected_project].clone();
let mut config_file = project.config_folder.clone();
config_file.pop();
if let Err(e) = remove_dir_all(config_file) {
let _ = self.main_tx.send(ToolMessage::Output((
rid,
format!(
"failed to delete config: {e} on {}",
&project.config_folder.display()
),
)));
}
if let Err(e) = remove_dir_all(&project.notes) {
let _ = self.main_tx.send(ToolMessage::Output((
rid,
format!(
"failed to delete notes: {e} on {}",
&project.notes.display()
),
)));
} else if let Some(parent) = project.notes.parent() {
if let Ok(mut entries) = read_dir(parent) {
if entries.next().is_none() {
if let Err(e) = remove_dir(parent) {
let _ = self.main_tx.send(ToolMessage::Output((rid, format!("failed to delete the empty client notes folder: {e} on {}", parent.display()))));
}
}
}
}
if let Err(e) = remove_dir_all(&project.files) {
let _ = self.main_tx.send(ToolMessage::Output((
rid,
format!(
"failed to delete files: {e} on {}",
&project.files.display()
),
)));
} else if let Some(parent) = project.files.parent() {
if let Ok(mut entries) = read_dir(parent) {
if entries.next().is_none() {
if let Err(e) = remove_dir(parent) {
let _ = self.main_tx.send(ToolMessage::Output((
rid,
format!(
"failed to delete empty files parent: {e} on {}",
parent.display()
),
)));
}
}
}
}
let remove_id = self.selected_project.clone();
let project = self.projects[remove_id].clone();
if let Some(db) = project.db {
self.main_tx.send(ToolMessage::DestroyDB(db))?;
}
self.selected_project -= 1;
self.projects.remove(remove_id);
self.prompt.responses.clear();
self.prompt.action = None;
let _ = self.main_tx.send(ToolMessage::Output((
rid,
format!(
"{}-{} was sucessfully removed!",
project.org_name, project.name
),
)));
}
}
}
"remove_project" | "rp" => {
let project = self.projects[self.selected_project].clone();
self.prompt.action = Some(ToolMessage::RemoveProject);
self.prompt.num_responses = 1;
self.prompt.execute_command = String::from("remove_project_confirm");
self.prompt.prompts.push(PromptQuery {
id: 0,
query: format!(
"{}, {} and all contentes wil lbe deleted. Contineue? (y/N",
project.files.display(),
project.notes.display()
),
});
let _ = self
.main_tx
.send(ToolMessage::Input((rid, "prompt".to_string())));
}
"new_terminal" | "nt" => {
let project = self.projects[self.selected_project].clone();
if let Some(cmd) = self.config.get("term_cmd") {
if let Some(db) = project.db {
db.launch_terminal(cmd.to_string());
} else {
if let Some((prog, _)) = cmd.split_once(" ") {
let mut launch_cmd = Command::new(prog);
match launch_cmd.spawn() {
Ok(_) => {}
Err(e) => {
let _ = self.main_tx.send(ToolMessage::Output((
rid,
format!("[error] launching terminal: {e}"),
)));
}
}
}
}
}
}
"new_server" | "ns" => {
self.prompt.num_responses = 2;
self.prompt.action = Some(ToolMessage::AddServer);
self.prompt.execute_command = String::from("add_server");
self.prompt.prompts.push(PromptQuery {
id: 0,
query: "What is the IP of the server?".to_string(),
});
self.prompt.prompts.push(PromptQuery {
id: 1,
query: "What is the port for the server?".to_string(),
});
let _ = self
.main_tx
.send(ToolMessage::Input((0, "prompt".to_string())));
}
"add_server" => {
let mut ip = String::new();
let mut port = String::new();
self.prompt.responses.iter().for_each(|res| {
if res.query.id == 0 {
ip = res.response.clone();
} else if res.query.id == 1 {
port = res.response.clone();
}
});
let new_server = Server {
address: format!("{}:{}", ip, port),
connected: false,
timer: Duration::from_secs(5),
last_check: time::Instant::now(),
message_que: Vec::new(),
action_que: Vec::new(),
client_id: 0,
selected_client: 0,
config: None,
logged_in: false,
password: String::new(),
name: String::new(),
cert_text: String::new(),
};
self.servers.push(Arc::new(Mutex::new(new_server)));
self.save_all()?;
self.prompt.reset();
}
"connect_server" => {
let _ = self.main_tx.send(ToolMessage::ConnectServer);
}
"prompt" => {
if self.prompt.responses.len() == self.prompt.num_responses {
let _ = self
.main_tx
.send(ToolMessage::Input((0, self.prompt.execute_command.clone())));
let _ = self.main_tx.send(ToolMessage::EndPrompt);
} else {
for query in &self.prompt.prompts {
if !self
.prompt
.responses
.iter()
.any(|res| res.query.id == query.id)
{
self.prompt.last_prompted = Some(query.clone());
let _ = self
.main_tx
.send(ToolMessage::Output((rid, query.query.clone())));
break;
}
}
}
}
"list_servers" => {
self.servers.iter().enumerate().for_each(|(id, m)| {
if let Ok(s) = m.lock() {
if id == self.selected_server {
let _ = self.main_tx.send(ToolMessage::Output((
rid,
format!(
"Server ID:{} Name:{} Address:{} ClientID:{} Sleep:{} Last:{} Loggedin: {} - Currently Selected",
id, s.name, s.address, s.client_id, s.timer.as_secs(), s.last_check.elapsed().as_secs(), s.logged_in
),
)));
} else {
let _ = self.main_tx.send(ToolMessage::Output((
rid,
format!(
"Server ID:{} Name:{} Address:{} ClientID:{} Sleep:{} Last:{}, loggedin: {}",
id, s.name, s.address, s.client_id, s.timer.as_secs(), s.last_check.elapsed().as_secs(), s.logged_in
),
)));
}
}
});
}
"remove_server" => {
let _ = self.main_tx.send(ToolMessage::DisconnectServer);
}
"server_remove_confim" => {
let args = command_args.unwrap();
let id = args.trim().parse::<usize>().unwrap();
self.servers.remove(id);
if self.selected_server == id && self.selected_server >= 1 {
let _ = self.main_tx.send(ToolMessage::Output((
0,
format!("[success] server {} removed!", self.selected_server),
)));
self.selected_server = self.selected_server - 1;
}
}
"select_server" => {
self.prompt.action = Some(ToolMessage::SelectServer);
self.prompt.num_responses = 1;
self.prompt.prompts.push(PromptQuery {
id: 0,
query: String::from("Selection?"),
});
self.prompt.execute_command = String::from("server_selected");
self.servers.iter().enumerate().for_each(|(id, servermut)| {
if let Ok(server) = servermut.lock() {
if id == self.selected_server {
let _ = self.main_tx.send(ToolMessage::Output((
rid,
format!("{}:{} - Currently Selected", id, server.address.clone()),
)));
} else {
let _ = self.main_tx.send(ToolMessage::Output((
rid,
format!("{}: {}", id, server.address.clone()),
)));
}
}
});
}
"server_selected" => {
if self.prompt.responses.len() == 1 {
if let Ok(id) = self.prompt.responses[0].response.parse::<usize>() {
self.selected_server = id;
if let Ok(server) = self.servers[id].lock() {
let _ = self.main_tx.send(ToolMessage::Output((
rid,
format!("{} Selected!", server.address),
)));
}
} else {
let _ = self.main_tx.send(ToolMessage::Output((
rid,
String::from("Invalid selection."),
)));
}
} else {
let _ = self.main_tx.send(ToolMessage::Output((
rid,
String::from("[error] selecting server: no prompt responses found!"),
)));
}
let _ = self.main_tx.send(ToolMessage::EndPrompt);
}
"test_server" => {
if let Some(server) = self.servers.get(self.selected_server) {
if let Ok(mut server) = server.lock() {
server.message_que.push("TEST|NONE".to_string());
} else {
let _ = self.main_tx.send(ToolMessage::Output((
rid,
"[error] getting server lock!".to_string(),
)));
}
} else {
self.output
.push(String::from("[error] getting selected server!"));
}
}
"server_sleep" => {
if let Some(args) = command_args {
if let Ok(seconds) = args.trim().parse::<u64>() {
if let Some(server) = self.servers.get(self.selected_server) {
if let Ok(mut server) = server.lock() {
server.timer = Duration::from_secs(seconds);
self.output
.push(format!("Server timer set to {} seconds", seconds));
} else {
self.output
.push(String::from("[error] locking server object!"));
}
} else {
self.output
.push(String::from("[error] not a valid server selected!"));
}
} else {
self.output
.push(String::from("invalid second count provided!"));
}
} else {
let _ = self.main_tx.send(ToolMessage::Output((
rid,
"[error] no seconds provided! please use server_sleep number_of_seconds"
.to_string(),
)));
}
}
"save_all" => {
self.save_all()?;
}
"save_projects" => {
self.save_projects()?;
}
"list_clients" => {
if let Some(server) = self.servers.get(self.selected_server) {
if let Ok(mut server) = server.lock() {
server.message_que.push("LIST_CLIENTS|NONE".to_string());
} else {
let _ = self.main_tx.send(ToolMessage::Output((
rid,
"[error] getting server lock!".to_string(),
)));
}
} else {
self.output
.push(String::from("[error] getting selected server!"));
}
}
"select_client" => {
if let Some(arg) = command_args {
if let Some(server) = self.servers.get(self.selected_server) {
if let Ok(mut server) = server.lock() {
if let Ok(id) = arg.parse::<usize>() {
server.message_que.push(format!("STOP_CONTROL"));
server.message_que.push(format!("CONTROL|{}", id.clone()));
server.selected_client = id;
let _ = self.main_tx.send(ToolMessage::Output((
0,
format!("client {} selected!", id),
)));
} else {
let _ = self.main_tx.send(ToolMessage::Output((
rid,
format!("[error] parsing client id from arg: {}", arg),
)));
}
}
}
} else {
let _ = self.main_tx.send(ToolMessage::Output((
rid,
format!("No client identified. Usage: select_client client_id"),
)));
}
}
"deselect_client" => {
if let Some(server) = self.servers.get(self.selected_server) {
if let Ok(mut server) = server.lock() {
server.message_que.push(format!("STOP_CONTROL"));
server.selected_client = 0;
let _ = self
.main_tx
.send(ToolMessage::Output((rid, format!("client deselected!"))));
}
}
}
"add_scope" => {
if let Some(args) = command_args {
args.split_whitespace().into_iter().for_each(|host| {
self.projects[self.selected_project].add_scope(host.to_string());
});
}
}
"parse_scope" => {
let mut general_path = self.projects[self.selected_project].notes.clone();
general_path.push("general.md");
let general_string = read_to_string(general_path)?;
if let Some((_, scope_section)) = general_string.split_once("# Scope") {
if let Some((scope_section, _)) = scope_section.split_once("#") {
scope_section.lines().into_iter().for_each(|line| {
if line.contains("|") {
if !line.contains("Subnet") {
if !line.contains("--") {
let host =
line.split("|").collect::<Vec<&str>>()[1].to_string();
self.projects[self.selected_project].add_scope(host);
}
}
}
});
}
}
}
"show_config" | "sc" => {
let _ = self
.main_tx
.send(ToolMessage::Output((rid, "Current Config:".to_string())));
self.config.iter().for_each(|setting| {
let _ = self.main_tx.send(ToolMessage::Output((
0,
format!("{}:{}", setting.0, setting.1),
)));
});
}
"set" => {
let mut ready = false;
let mut key = String::new();
let mut value = String::new();
if let Some(args) = command_args.clone() {
if let Some((setting, data)) = args.split_once(" ") {
key = setting.to_string();
value = data.to_string();
if self.config.contains_key(&key) {
ready = true;
} else {
let _ = self.main_tx.send(ToolMessage::Output((
0,
"[error] Unknown setting! {key}, {value}".to_string(),
)));
return Ok(());
}
}
}
if ready {
self.config.insert(key, value);
self.save_config()?;
let _ = self.main_tx.send(ToolMessage::Output((
0,
"[success]Setting changed!".to_string(),
)));
} else {
let _ = self.main_tx.send(ToolMessage::Output((
0,
"[error] malformed command!".to_string(),
)));
let _ = self.main_tx.send(ToolMessage::Output((
0,
"Usag: set setting value".to_string(),
)));
}
}
"list_modules" => {
self.module_loader.commands.iter().for_each(|module| {
let _ = self
.main_tx
.send(ToolMessage::Output((rid, format!("{}", module.0))));
});
}
"server_login" => {
if let Some(pass) = command_args {
let pass = pass.trim().to_string();
if let Some(server) = self.servers.get(self.selected_server) {
if let Ok(mut lock) = server.lock() {
lock.login(pass)?;
let _ = self.main_tx.send(ToolMessage::Output((
rid,
"[success] Logged into Server!".to_string(),
)));
}
}
}
}
"new_config" => {
self.prompt.num_responses = 8;
self.prompt.action = Some(ToolMessage::Input((0, "add_config".to_string())));
self.prompt.execute_command = String::from("add_config");
self.prompt.prompts.push(PromptQuery {
id: 0,
query: "name for the new config?".to_string(),
});
self.prompt.prompts.push(PromptQuery {
id: 1,
query: "Path to save the config folder?".to_string(),
});
self.prompt.prompts.push(PromptQuery {
id: 2,
query: "Path for upcoming project files (not notes)?".to_string(),
});
self.prompt.prompts.push(PromptQuery {
id: 3,
query: "Path for current project files (not notes)?".to_string(),
});
self.prompt.prompts.push(PromptQuery {
id: 4,
query: "Path for upcoming notes?".to_string(),
});
self.prompt.prompts.push(PromptQuery {
id: 5,
query: "Path for current notes?".to_string(),
});
self.prompt.prompts.push(PromptQuery {
id: 6,
query: "Path to tool directory?".to_string(),
});
self.prompt.prompts.push(PromptQuery {
id: 7,
query: "Template box name?".to_string(),
});
let _ = self
.main_tx
.send(ToolMessage::Input((0, "prompt".to_string())));
}
"add_config" => {
self.prompt.responses.iter().for_each(|res| {
let _ = self.main_tx.send(ToolMessage::Output((
rid,
format!("{} | {}", res.query.query, res.response),
)));
});
self.new_config()?;
self.save_config()?;
}
"list_configs" => {
if let Some(config_line) = self.config.get("alternate_configs") {
config_line.split(",").into_iter().for_each(|entry| {
if let Some((name, path)) = entry.split_once("|") {
let _ = self
.main_tx
.send(ToolMessage::Output((rid, format!("{}: {}", name, path))));
}
});
}
}
"collab_with" => {
if let Some(args) = command_args {
if let Ok(id) = args.trim().parse::<usize>() {
if let Some(server) = self.servers.get(self.selected_server) {
if let Ok(mut lock) = server.lock() {
lock.action_que.push(format!("COLLAB|{}", id));
}
}
}
}
}
"collab" => {
if let Some(args) = command_args {
if let Some(server) = self.servers.get(self.selected_server) {
if let Ok(mut lock) = server.lock() {
lock.action_que.push(format!("CMD|{}", args));
}
}
}
}
"parse_port_scan" | "pps" => {
self.parse_cs_portscan()?;
}
"stop_db" => {
if let Some(project) = self.projects.get_mut(self.selected_project) {
if let Some(db) = project.db.as_mut() {
db.stop(self.main_tx.clone(), rid)?;
}
}
}
"rustwitness" => {
if let Some(project) = self.projects.get(self.selected_project) {
if let Some(args) = command_args {
if let Some((outfile, proxy)) = args.split_once(" ") {
let outfile = format!("{}/{}", project.files.display(), outfile);
let mut proxy_string = None;
if let Some((ip, port)) = proxy.split_once(" ") {
proxy_string = Some(format!("socks5://{}:{}", ip, port));
}
let _ = self.main_tx.send(ToolMessage::Output((
rid,
format!(
"[success] rustwitness running will save screenshots to {}",
outfile
),
)));
let mut urls = Vec::new();
WalkDir::new(&project.files)
.into_iter()
.filter(|res| res.is_ok())
.for_each(|res| {
if let Ok(entry) = res {
if entry.file_name().to_string_lossy().contains("urls.txt")
{
if let Ok(input) = read_to_string(entry.path()) {
input.lines().into_iter().for_each(|url| {
urls.push(url.to_string());
});
}
}
}
});
let proxy_string_clone = proxy_string.clone();
let out_folder = PathBuf::from(outfile);
let tx_clone = self.main_tx.clone();
self.workers.spawn(move || {
urls.par_iter().for_each(|url| {
if url.contains("://"){
match rustwitness(
url.clone(),
out_folder.clone(),
proxy_string_clone.clone(),
) {
Ok(_) => {
let _ = tx_clone.send(ToolMessage::Output((
rid,
format!("[success] {} captured!", url),
)));
}
Err(e) => {
let _ = tx_clone.send(ToolMessage::Output((
rid,
format!(
"[error] capturing screenshot of {url}: {e}"
),
)));
}
}
}
else{
let _ = tx_clone.send(ToolMessage::Output((
rid,
format!(
"[error] capturing screenshot of {url}: not a valid url."
),
)));
}
});
let _ = tx_clone.send(ToolMessage::Output((
rid,
"[success] all urls scanned for screenshots!".to_string(),
)));
});
}
} else {
let _ = self.main_tx.send(ToolMessage::Output((
0,
"[error] incorrect command usage!".to_string(),
)));
let _ = self.main_tx.send(ToolMessage::Output((
0,
"usage: rustwitness output_file_name optional_proxy_ip optional_proxy_port".to_string(),
)));
}
}
}
"portscan" => {
if let Some(project) = self.projects.get(self.selected_project) {
let mut ports = Vec::new();
let mut hosts = project.hosts.clone();
if let Some(args) = command_args {
let args_vec: Vec<&str> = args.split_whitespace().collect();
match args_vec.len() {
1 => {
args_vec[0].split(",").into_iter().for_each(|p| {
if p.contains("-") {
let (first, last) = p.split_once("-").unwrap();
if let Ok(start) = first.parse::<u16>() {
if let Ok(end) = last.parse::<u16>() {
ports.extend(start..=end);
}
}
} else if let Ok(port) = p.trim().parse::<u16>() {
ports.push(port);
}
});
}
2 => {
args_vec[0].split(",").into_iter().for_each(|ip| {
if let Ok(ip) = ip.parse::<IpAddr>() {
let mut new_host = Host::new();
new_host.ip = ip.to_string();
hosts.push(new_host);
} else if let Ok(net) = ip.parse::<IpNet>() {
for host_addr in net.hosts() {
let mut new_host = Host::new();
new_host.ip = host_addr.to_string();
hosts.push(new_host);
}
}
});
args_vec[1].split(",").into_iter().for_each(|p| {
if p.contains("-") {
if let Some((start, end)) = p.split_once("-") {
if let Ok(start) = start.parse::<u16>() {
if let Ok(end) = end.parse::<u16>() {
for range in [start..end] {
range.into_iter().for_each(|port| {
ports.push(port.clone());
});
}
}
}
}
} else if let Ok(port) = p.trim().parse::<u16>() {
ports.push(port);
}
});
}
_ => {}
}
if !ports.is_empty() && !hosts.is_empty() {
let _ = self.main_tx.send(ToolMessage::Output((
rid,
format!("scanning {} ports on {} hosts", ports.len(), hosts.len()),
)));
let progress_bar = Arc::new(RwLock::new(ToolProgress {
name: format!("portscan {} - {}", project.org_name, project.name),
total: hosts.len() * ports.len(),
complete: 0,
finished: false,
}));
self.progress_bars.push(progress_bar.clone());
let project_clone = Arc::new(Mutex::new(project.clone()));
let id = self.selected_project.clone();
let tx_clone = self.main_tx.clone();
let pb_clone = progress_bar.clone();
self.workers.spawn(move || {
hosts.par_iter_mut().for_each(|h| {
let rt = tokio::runtime::Runtime::new().unwrap();
let host = rt.block_on(h.portscan(
ports.clone(),
tx_clone.clone(),
pb_clone.clone(),
));
if let Ok(mut lock) = project_clone.lock() {
if !lock.hosts.iter().any(|ph| ph.ip == host.ip) {
lock.hosts.push(host);
} else {
lock.hosts
.iter_mut()
.filter(|ph| ph.ip == host.ip)
.for_each(|ph| {
host.open_ports.iter().for_each(|p| {
if !ph.open_ports.contains(p) {
ph.open_ports.push(p.clone());
}
});
});
}
}
});
if let Ok(mut lock) = pb_clone.write() {
lock.finished = true;
}
if let Ok(lock) = project_clone.lock() {
let _ =
tx_clone.send(ToolMessage::UpdateProject(id, lock.clone()));
let _ = tx_clone.send(ToolMessage::Output((
0,
"[success] portscan finished!".to_string(),
)));
}
});
}
}
}
}
"rustbuster" => {
if let Some(args) = command_args {
let args: Vec<&str> = args.split(" ").collect();
let mut dirs = Vec::new();
let mut subs = Vec::new();
let mut targets = Vec::new();
if args
.iter()
.any(|a| a.contains("subs=") || a.contains("dirs="))
{
args.iter().for_each(|arg| {
if arg.contains("subs=") {
if let Some((_, sub_path)) = arg.split_once("=") {
if let Ok(subtext) = read_to_string(sub_path) {
subtext.lines().into_iter().for_each(|sub| {
subs.push(sub.to_string());
});
}
}
}
if arg.contains("dirs=") {
if let Some((_, dir_path)) = arg.split_once("=") {
if let Ok(dir_text) = read_to_string(dir_path) {
dir_text.lines().into_iter().for_each(|dir| {
dirs.push(dir.to_string());
});
}
}
} else {
if arg.contains(",") {
arg.split(",").into_iter().for_each(|t| {
targets.push(t.to_string());
});
} else if !arg.contains("=") {
targets.push(arg.to_string());
}
}
});
let mut num_dir = 0;
let mut num_sub = 0;
let tx_clone = self.main_tx.clone();
let out_path = self.projects[self.selected_project].files.clone();
let project_clone =
Arc::new(Mutex::new(self.projects[self.selected_project].clone()));
let mut buster_targets = Vec::new();
for target in targets {
if target.contains("://") {
for dir in &dirs {
num_dir += 1;
buster_targets
.push(BusterTarget::Dir(format!("{}/{}", target, dir)));
}
} else if target.contains(".") {
for sub in &subs {
num_sub += 1;
buster_targets
.push(BusterTarget::Sub(format!("{}.{}", sub, target)));
}
}
}
let _ = self.main_tx.send(ToolMessage::Output((
rid,
format!("{} targets found scanning...", num_dir + num_sub),
)));
let new_progress = Arc::new(RwLock::new(ToolProgress {
name: format!(
"rustbuster - {} {}",
self.projects[self.selected_project].org_name.clone(),
self.projects[self.selected_project].name.clone()
),
total: num_dir + num_sub,
complete: 0,
finished: false,
}));
self.progress_bars.push(new_progress.clone());
self.workers.spawn(move || {
buster_targets.par_iter().for_each(|t| match t {
BusterTarget::Dir(_) => {
if let Some(res) = rustbuster(t.clone()) {
let mut url_path = out_path.clone();
url_path.push("urls.txt");
if let Ok(mut url_file) = OpenOptions::new()
.write(true)
.append(true)
.create(true)
.open(&url_path)
{
if let Err(e) = writeln!(
url_file,
"{}",
format!("[success] {}", res)
) {
let _ = tx_clone.send(ToolMessage::Output((
rid,
format!(
"[error] saving url to {}: {e}",
url_path.display()
),
)));
}
}
let _ = tx_clone.send(ToolMessage::Output((
rid,
format!("[success] {}", res),
)));
}
if let Ok(mut lock) = new_progress.write() {
lock.complete += 1;
}
}
BusterTarget::Sub(_) => {
if let Some(res) = rustbuster(t.clone()) {
let mut url_path = out_path.clone();
url_path.push("domains.txt");
if let Ok(mut url_file) = OpenOptions::new()
.write(true)
.append(true)
.create(true)
.open(&url_path)
{
if let Err(e) =
writeln!(url_file, "{}", format!("{}", res))
{
let _ = tx_clone.send(ToolMessage::Output((
rid,
format!(
"[error] saving domain to {}: {e}",
url_path.display()
),
)));
}
}
let _ = tx_clone.send(ToolMessage::Output((
rid,
format!("[success] {}", res),
)));
if let Some((domain, ip)) = res.split_once(":") {
let mut new_host = Host::new();
new_host.ip = ip.to_string();
new_host.hostname = domain.to_string();
if let Ok(mut project_lock) = project_clone.lock() {
project_lock.add_host(new_host);
}
}
}
if let Ok(mut lock) = new_progress.write() {
lock.complete += 1;
}
}
});
if let Ok(mut lock) = new_progress.write() {
lock.finished = true;
}
drop(project_clone);
});
} else {
let _ = self.main_tx.send(ToolMessage::Output((
rid,
"[error] no sub or dir wordlist provided!".to_string(),
)));
let _ = self
.main_tx
.send(ToolMessage::Output((rid, "USAGE:".to_string())));
let _ = self.main_tx.send(ToolMessage::Output((
rid,
"rustbuster target,target,target subs=/path/to/subwordlist dirs=/path/to/dirwordlist".to_string(),
)));
let _ = self.main_tx.send(ToolMessage::Output((
rid,
"be sure to include the subs= or dirs= part of the command above!"
.to_string(),
)));
}
} else {
let _ = self.main_tx.send(ToolMessage::Output((
rid,
"[error] no arguments provided!".to_string(),
)));
let _ = self
.main_tx
.send(ToolMessage::Output((rid, "USAGE:".to_string())));
let _ = self.main_tx.send(ToolMessage::Output((
rid,
"rustbuster target,target,target subs=/path/to/subwordlist dirs=/path/to/dirwordlist".to_string(),
)));
}
}
"clear_operations" => {
self.progress_bars.retain(|pb| {
let lock = pb.read().unwrap();
!lock.finished
});
}
"shodan" => {
if let Some(project) = self.projects.get(self.selected_project) {
if let Some(host) = project.hosts.get(self.selected_host) {
if let Ok(entry) = Entry::new("tetanus", "shodan_key") {
if let Ok(key) = entry.get_password() {
let mut host_clone = host.clone();
let tx_clone = self.main_tx.clone();
thread::spawn(move || host_clone.get_shodan(key, tx_clone));
} else {
let _ = self.main_tx.send(ToolMessage::Output((
rid,
format!("[error] couldn't get shodan key from keyring!"),
)));
}
} else {
let _ = self.main_tx.send(ToolMessage::Output((
rid,
format!("[error] couldn't find shodan key in keyring!"),
)));
}
}
}
}
"shodan_all" => {
if let Some(project) = self.projects.get(self.selected_project) {
if !project.hosts.is_empty() {
if let Ok(entry) = Entry::new("tetanus", "shodan_key") {
if let Ok(key) = entry.get_password() {
project.hosts.iter().for_each(|h| {
let mut host_clone = h.clone();
let tx_clone = self.main_tx.clone();
let key_clone = key.clone();
thread::spawn(move || {
host_clone.get_shodan(key_clone, tx_clone)
});
});
} else {
let _ = self.main_tx.send(ToolMessage::Output((
rid,
format!("[error] couldn't get shodan key from keyring!"),
)));
}
} else {
let _ = self.main_tx.send(ToolMessage::Output((
rid,
format!("[error] couldn't find shodan key in keyring!"),
)));
}
}
}
}
"set_shodan_key" => {
if let Some(args) = command_args {
if let Ok(_) = Entry::store_status() {
if let Ok(entry) = Entry::new("tetanus", "shodan_key") {
if let Err(e) = entry.set_password(args.trim()) {
let _ = self.main_tx.send(ToolMessage::Output((
rid,
format!("[error] couldn't set shodan apikey!: {e}"),
)));
}
} else {
let _ = self.main_tx.send(ToolMessage::Output((
rid,
format!("[error] couldn't create new entry!"),
)));
}
} else {
let _ = self.main_tx.send(ToolMessage::Output((
rid,
format!("[error] store status bad!"),
)));
}
}
}
"save_server" => {
if let Some(server) = self.servers.get(self.selected_server) {
let mut path = self.config_file.clone();
path.pop();
path.push("client_servers");
if !path.exists() {
create_dir_all(&path).unwrap();
}
if let Ok(mut lock) = server.lock() {
match lock.save(path) {
Ok(_) => {
let _ = self.main_tx.send(ToolMessage::Output((
rid,
format!("[success] {} saved!", lock.address),
)));
}
Err(e) => {
let _ = self.main_tx.send(ToolMessage::Output((
rid,
format!("[error] saving server: {e}"),
)));
}
}
}
}
}
"parse_nessus" => {
if let Some(arg) = command_args {
let path = PathBuf::from(arg);
let csv_text = read_to_string(path)?;
let mut parsed_hosts: Vec<Host> = Vec::new();
csv_text
.lines()
.into_iter()
.filter(|line| !line.contains("Host,Protocol,Port") && line.contains(","))
.for_each(|line| {
let parts: Vec<&str> = line.split(",").collect();
let name = parts[0].replace("\"", "").trim().to_string();
let port = parts[2].replace("\"", "").trim().to_string();
if let Ok(_) = name.parse::<IpAddr>() {
if !parsed_hosts.iter().any(|h| h.ip == name) {
let mut new_host = Host::new();
new_host.ip = name.clone();
new_host.add_port(&port);
parsed_hosts.push(new_host);
} else {
parsed_hosts.iter_mut().filter(|h| h.ip == name).for_each(
|h| {
h.add_port(&port);
},
);
}
} else {
if let Ok(ips) = lookup_host(name.as_str()) {
ips.for_each(|ip| {
if !parsed_hosts.iter().any(|h| h.ip == ip.to_string()) {
let mut new_host = Host::new();
new_host.ip = ip.to_string().clone();
new_host.hostname = name.clone();
new_host.add_port(&port);
parsed_hosts.push(new_host)
} else {
parsed_hosts
.iter_mut()
.filter(|h| h.ip == ip.to_string())
.for_each(|h| {
if h.hostname != name {
h.hostname = name.clone();
}
if port != "0" {
h.add_port(&port);
}
});
}
});
}
}
});
parsed_hosts.iter().for_each(|h| {
if !self.projects[self.selected_project]
.hosts
.iter()
.any(|ph| ph.ip == h.ip)
{
self.projects[self.selected_project].add_host(h.clone());
} else {
self.projects[self.selected_project]
.hosts
.iter_mut()
.filter(|ph| ph.ip == h.ip)
.for_each(|ph| {
h.open_ports.iter().for_each(|p| {
ph.add_port(&format!("{}", p));
});
if ph.hostname != h.hostname {
ph.hostname = h.hostname.clone();
}
});
}
});
self.save_all()?;
let _ = self.main_tx.send(ToolMessage::Output((
rid,
"[success] Project hosts saved!".to_string(),
)));
let mut host_notes_folder = self.projects[self.selected_project].notes.clone();
host_notes_folder.push("host_notes.md");
let _ = self.main_tx.send(ToolMessage::Output((
rid,
format!("writing to {}...", host_notes_folder.display()),
)));
let mut host_notes_file = OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.open(host_notes_folder)?;
writeln!(
host_notes_file,
"#{} #external #host_notes",
self.projects[self.selected_project].org_name.clone()
)?;
self.projects[self.selected_project]
.hosts
.iter()
.filter(|h| !h.open_ports.is_empty())
.for_each(|h| {
let _ = writeln!(host_notes_file, "# {}", h.ip);
let _ = writeln!(host_notes_file, "hostname: {}", h.hostname);
let _ = writeln!(host_notes_file, "ports:\n");
let _ = writeln!(host_notes_file, "| port | service | vulns |",);
let _ = writeln!(host_notes_file, "| ---- | ------- | ----- |",);
h.open_ports.iter().for_each(|p| {
let _ = writeln!(host_notes_file, "| {} | | |", p);
});
let _ = writeln!(host_notes_file, "\n\n",);
let _ = writeln!(host_notes_file, "---",);
let _ = self.main_tx.send(ToolMessage::Output((
rid,
format!("[success] notes for {} written!", h.ip),
)));
});
let _ = writeln!(host_notes_file, "# Hosts with no ports open:\n");
self.projects[self.selected_project]
.hosts
.iter()
.filter(|h| h.open_ports.is_empty())
.for_each(|h| {
let _ = writeln!(host_notes_file, "- {}:{}", h.ip, h.hostname);
});
}
}
"exit" => {
self.save_all()?;
let _ = self.main_tx.send(ToolMessage::AppStateExit);
let _ = self
.main_tx
.send(ToolMessage::Output((rid, String::from("Good bye!"))));
}
_ => {
if self.module_loader.asts.get(command_name).is_none() {
let _ = self.main_tx.send(ToolMessage::Output((
rid,
format!("[error] command not found: {}", command_name),
)));
return Ok(());
}
let ast = self
.module_loader
.asts
.get(command_name)
.ok_or_else(|| format!("[error] Command not found: {}", command_name))?
.clone();
let engine = Arc::clone(&self.module_loader.engine);
let cmd_meta = self
.module_loader
.commands
.get(command_name)
.ok_or_else(|| format!("Metadata not found for command: {}", command_name))?
.clone();
let mut scope = Scope::new();
for arg_requirement in &cmd_meta.args {
match arg_requirement.trim() {
"project" => {
scope.push("project", self.projects[self.selected_project].clone());
}
"projects" => {
let mut arr = rhai::Array::new();
for proj in &self.projects {
arr.push(rhai::Dynamic::from(proj.clone()));
}
scope.push("projects", arr);
}
"host" => {
scope.push("host", "todo");
}
"hosts" => {
scope.push("hosts", self.projects[self.selected_project].clone());
}
"config" => {
let mut map = rhai::Map::new();
for (k, v) in &self.config {
map.insert(k.clone().into(), v.clone().into());
}
scope.push("config", map);
}
"distrobox" => {
if let Some(db) = self.projects[self.selected_project].db.clone() {
scope.push("distrobox", db);
}
}
_ if arg_requirement.starts_with("string") => {
let input_val = command_args
.clone()
.unwrap_or_else(|| self.curent_intput.clone());
scope.push("input_string", input_val);
}
_ if arg_requirement.starts_with("bool") => {
let mut input_val = false;
if command_args
.clone()
.unwrap_or_else(|| self.curent_intput.clone())
.contains("true")
{
input_val = true;
}
scope.push("input_bool", input_val);
}
_ => {}
}
}
let worker_tx = self.main_tx.clone();
self.workers.spawn(move || {
match engine.eval_ast_with_scope::<Dynamic>(&mut scope, &ast) {
Ok(result) => {
let result_str = if result.is_array() {
if let Ok(arr) = result.into_array() {
let lines: Vec<String> = arr
.into_iter()
.map(|item| {
item.into_string().unwrap_or_else(|_| "".into())
})
.collect();
lines.join("\n")
} else {
"failed to process array output".into()
}
} else if result.is_string() {
let out_string = result
.into_string()
.unwrap_or_else(|_| "failed to parse string".into());
if cmd_meta.calls == "clipboard" {
let mut ctx: ClipboardContext =
ClipboardProvider::new().unwrap();
ctx.set_contents(out_string.clone()).unwrap();
} else if cmd_meta.calls != "none" {
let _ =
worker_tx.send(ToolMessage::Input((0, out_string.clone())));
}
out_string
} else {
format!("{:?}", result)
};
if result_str.contains("\n") {
result_str.lines().into_iter().for_each(|line| {
let _ = tx.send(ToolMessage::Output((rid, line.to_string())));
});
} else {
let _ = tx.send(ToolMessage::Output((rid, result_str)));
}
}
Err(err) => {
let _ = tx.send(ToolMessage::Output((
rid,
format!("[error] in script execution: {}", err),
)));
}
}
});
}
}
Ok(())
}
pub fn edit_config(&mut self, key: String, value: String) {
if self.config.contains_key(&key) {
self.config.insert(key.clone(), value.clone());
}
let _ = self.main_tx.send(ToolMessage::Output((
0,
format!("Setting saved! {key}:{value}"),
)));
}
pub fn new_project(
&mut self,
org: String,
name: String,
rid: usize,
) -> Result<(), Box<dyn Error>> {
let mut main_config_file = self.config_file.clone();
main_config_file.pop();
let _ = self.main_tx.send(ToolMessage::Output((
rid,
format!("{} is the main config folder.", main_config_file.display()),
)));
let template_box = self.config.get("template_box").unwrap();
let project_files = PathBuf::from(self.config.get("upcoming_files").unwrap())
.join(format!("{}/{}", org, name));
let _ = self.main_tx.send(ToolMessage::Output((
rid,
format!("{} is the project_files folder", project_files.display()),
)));
let project_notes = PathBuf::from(self.config.get("upcoming_notes").unwrap())
.join(format!("{}/{}", org, name));
let _ = self.main_tx.send(ToolMessage::Output((
rid,
format!("{} is the project_notes folder", project_notes.display()),
)));
let mut template_path = main_config_file.clone();
let mut project_conf_folder = main_config_file.clone();
let tools = self.config.get("tools").unwrap();
template_path.push("note_templates");
let _ = self.main_tx.send(ToolMessage::Output((
rid,
format!("{} is the note template folder", template_path.display()),
)));
project_conf_folder.push(format!("projects/{}-{}", org, name));
let _ = self.main_tx.send(ToolMessage::Output((
rid,
format!(
"{} is the project config folder",
project_conf_folder.display()
),
)));
let mut options = CopyOptions::new();
options.overwrite = true;
if !project_files.exists() {
match create_dir_all(&project_files) {
Ok(_) => {}
Err(e) => {
let _ = self.main_tx.send(ToolMessage::Output((
rid,
format!(
"[error] making project files! {e} on {}",
project_files.display()
),
)));
}
}
}
if !project_notes.exists() {}
match create_dir_all(&project_notes) {
Ok(_) => {}
Err(e) => {
let _ = self.main_tx.send(ToolMessage::Output((
rid,
format!(
"[error] making project notes! {e} on {}",
project_notes.display()
),
)));
}
}
if !project_conf_folder.exists() {
match create_dir_all(&project_conf_folder) {
Ok(_) => {}
Err(e) => {
let _ = self.main_tx.send(ToolMessage::Output((
rid,
format!(
"[error] making config folder! {e} on {}",
project_conf_folder.display()
),
)));
}
}
}
let mut project_config_file = project_conf_folder.clone();
project_config_file.push("project.conf");
for entry in read_dir(template_path)? {
let entry = entry?;
let template_name_os = entry.file_name();
let template_name = template_name_os.to_string_lossy();
if name.clone().contains(template_name.as_ref()) {
for entry in read_dir(entry.path())? {
let entry = entry?;
let file_name_os = entry.file_name();
let file_name = file_name_os.to_string_lossy();
if entry.file_type()?.is_dir() {
let dest = project_notes.join(file_name.as_ref());
match copy(entry.path(), dest, &options) {
Ok(_) => {}
Err(e) => {
let _ = self.main_tx.send(ToolMessage::Output((
rid,
format!("[error] copying note template! {e}"),
)));
}
}
} else {
match fs::copy(entry.path(), project_notes.join(file_name.as_ref())) {
Ok(_) => {}
Err(e) => {
let _ = self.main_tx.send(ToolMessage::Output((
rid,
format!("[error] copying note template! {e}"),
)));
}
}
}
}
}
}
let mut db = None;
if template_box != &"NONE".to_string() {
db = Some(DistroBox {
name: format!("{}-{}-{}", template_box, org, name),
volumes: vec![
project_files.display().to_string(),
project_notes.display().to_string(),
tools.to_string(),
],
created: false,
template: template_box.to_string(),
});
};
let mut new_project = Project {
org_name: org,
name: name,
notes: project_notes,
files: project_files,
hosts: Vec::new(),
config_folder: project_config_file,
current: false,
db: db,
scope: Vec::new(),
};
match new_project.save_config() {
Ok(_) => {}
Err(e) => {
let _ = self.main_tx.send(ToolMessage::Output((
0,
format!("[error] saving project! {e}"),
)));
}
}
new_project.save_config()?;
self.projects.push(new_project);
return Ok(());
}
pub fn promote_project(&mut self, rid: usize) -> Result<String, Box<dyn Error>> {
self.main_tx.send(ToolMessage::Output((
rid,
String::from("promoting project..."),
)))?;
let return_string = String::from("[success] Project promoted!");
let mut new_project = self.projects[self.selected_project].clone();
let mut new_files_path = PathBuf::from(self.config.get("current_files").unwrap())
.join(format!("{}", new_project.org_name));
let mut new_notes_path = PathBuf::from(self.config.get("current_notes").unwrap())
.join(format!("{}", new_project.org_name));
let mut options = CopyOptions::new();
create_dir_all(&new_files_path)?;
create_dir_all(&new_notes_path)?;
options.overwrite = true;
copy(new_project.files.clone(), new_files_path.clone(), &options)?;
copy(new_project.notes.clone(), new_notes_path.clone(), &options)?;
self.main_tx.send(ToolMessage::Output((
rid,
String::from("[success] folders copied!"),
)))?;
remove_dir_all(&new_project.files)?;
remove_dir_all(&new_project.notes)?;
new_files_path.push(&new_project.name);
new_notes_path.push(&new_project.name);
new_project.files(new_files_path.clone());
new_project.notes(new_notes_path.clone());
new_project.current = true;
self.main_tx.send(ToolMessage::Output((
rid,
String::from("upcoming folders cleaned up!"),
)))?;
self.main_tx.send(ToolMessage::Output((
rid,
String::from("creating distrobox..."),
)))?;
if let Some(mut db) = new_project.db {
db.volumes = vec![
new_files_path.display().to_string(),
new_notes_path.display().to_string(),
self.config.get("tools").unwrap().to_string(),
];
new_project.db = Some(db);
let _ = self.main_tx.send(ToolMessage::RebuildDB);
self.main_tx.send(ToolMessage::Output((
rid,
String::from("distrobox sent to rebuild function..."),
)))?;
}
new_project.save_config()?;
self.projects[self.selected_project] = new_project;
return Ok(return_string);
}
pub fn save_projects(&mut self) -> Result<(), Box<dyn Error>> {
for mut project in self.projects.clone() {
project.save_config()?;
}
return Ok(());
}
pub fn save_config(&mut self) -> Result<(), Box<dyn Error>> {
let mut config_vec = Vec::new();
for key in self.config.keys() {
if let Some(value) = self.config.get(key) {
config_vec.push(format!("{}: {}", key, value));
}
}
if config_vec.len() > 0 {
fs::write(self.config_file.clone(), config_vec.join("\n"))?;
} else {
return Err("No config loaded!".into());
}
return Ok(());
}
pub fn save_all(&mut self) -> Result<(), Box<dyn Error>> {
self.save_config()?;
self.save_projects()?;
return Ok(());
}
pub fn new_server(&mut self) -> Result<(), Box<dyn Error>> {
let mut ip = String::new();
let mut port = String::new();
self.prompt.responses.iter().for_each(|res| {
if res.query.id == 0 {
ip = res.response.clone();
} else if res.query.id == 1 {
port = res.response.clone();
}
});
let new_server = Server {
address: format!("{}:{}", ip, port),
connected: false,
timer: Duration::from_secs(60),
last_check: time::Instant::now(),
message_que: Vec::new(),
action_que: Vec::new(),
client_id: 0,
selected_client: 0,
config: None,
logged_in: false,
password: String::new(),
name: String::new(),
cert_text: String::new(),
};
self.servers.push(Arc::new(Mutex::new(new_server)));
self.prompt.reset();
Ok(())
}
pub fn new_config(&mut self) -> Result<(), Box<dyn Error>> {
let mut name = String::new();
let mut config_path = PathBuf::new();
let mut upcoming_files_path = PathBuf::new();
let mut current_files_path = PathBuf::new();
let mut upcoming_notes_path = PathBuf::new();
let mut current_notes_path = PathBuf::new();
let mut tools_folder = PathBuf::new();
let mut template_box = String::new();
let term_cmd = self.config.get("term_cmd").unwrap().clone();
self.prompt
.responses
.iter()
.for_each(|res| match res.query.id {
0 => name = res.response.clone(),
1 => config_path = PathBuf::from(res.response.clone()),
2 => upcoming_files_path = PathBuf::from(res.response.clone()),
3 => current_files_path = PathBuf::from(res.response.clone()),
4 => upcoming_notes_path = PathBuf::from(res.response.clone()),
5 => current_notes_path = PathBuf::from(res.response.clone()),
6 => tools_folder = PathBuf::from(res.response.clone()),
7 => template_box = res.response.clone(),
_ => {}
});
let mut existing = false;
if let Some(line) = self.config.get("alternate_configs") {
line.split(",").into_iter().for_each(|line| {
if let Some((ename, _)) = line.split_once("|") {
if ename.trim().to_string() == name {
existing = true;
}
}
});
}
if existing {
let _ = self.main_tx.send(ToolMessage::Output((
0,
format!("{} already exists!", name),
)));
return Ok(());
}
let root_config_folder_path = config_path.clone();
let mut projects_path = config_path.clone();
projects_path.push("projects");
let mut module_path = config_path.clone();
module_path.push("modules");
let mut note_templates_path = config_path.clone();
note_templates_path.push("note_templates");
create_dir_all(&module_path)?;
config_path.push("client.conf");
let client_conf_path = config_path.clone();
projects_path.push("default");
match create_dir_all(&projects_path) {
Ok(_) => {
let _ = self.main_tx.send(ToolMessage::Output((
0,
format!(
"[success] create projects path at {}",
projects_path.display()
),
)));
}
Err(e) => {
let _ = self.main_tx.send(ToolMessage::Output((
0,
format!(
"[error] couldn't create projects path at {}: {e}",
projects_path.display()
),
)));
return Ok(());
}
}
projects_path.push("project.conf");
let mut client_config_file = File::create(&config_path)?;
client_config_file.write(format!("projects: {}\n", projects_path.display()).as_bytes())?;
client_config_file.write("servers: 127.0.0.1:31337\n".as_bytes())?;
let mut default_project_file = File::create(projects_path)?;
config_path.pop();
let mut server_path = config_path.clone();
server_path.push("server");
create_dir_all(&server_path)?;
server_path.push("server.conf");
let mut server_config_file = File::create(&server_path)?;
server_config_file.write("address: 127.0.0.1:31337\n".as_bytes())?;
server_config_file.write("running: false\n".as_bytes())?;
default_project_file.write("org_name: default\n".as_bytes())?;
default_project_file.write("name: default\n".as_bytes())?;
default_project_file
.write(format!("notes: {}\n", current_files_path.display()).as_bytes())?;
default_project_file
.write(format!("files: {}\n", current_notes_path.display()).as_bytes())?;
default_project_file.write("stage: current".as_bytes())?;
client_config_file
.write(format!("current_files: {}\n", current_files_path.display()).as_bytes())?;
client_config_file
.write(format!("current_notes: {}\n", current_notes_path.display()).as_bytes())?;
client_config_file
.write(format!("upcoming_files: {}\n", upcoming_files_path.display()).as_bytes())?;
client_config_file
.write(format!("upcoming_notes: {}\n", upcoming_notes_path.display()).as_bytes())?;
client_config_file.write(format!("module_path: {}\n", module_path.display()).as_bytes())?;
client_config_file.write(format!("template_box: {}\n", template_box).as_bytes())?;
client_config_file.write(format!("term_cmd: {}\n", term_cmd).as_bytes())?;
client_config_file.write(format!("tools: {}\n", tools_folder.display()).as_bytes())?;
create_dir_all("./temp")?;
env::set_current_dir("./temp")?;
let git_clone_output = Command::new("git")
.arg("clone")
.arg("https://git.pyro.monster/pyro/tetanus.git")
.output()?;
let git_clone_status = git_clone_output.status.success();
if git_clone_status {
let mut options = CopyOptions::new();
options.overwrite = true;
options.copy_inside = true;
copy(
"./tetanus/note_templates",
root_config_folder_path.clone(),
&options,
)?;
copy(
"./tetanus/default-modules",
module_path.join("default"),
&options,
)?;
copy(
"./tetanus/victim_templates",
root_config_folder_path.join("victim"),
&options,
)?;
env::set_current_dir("../")?;
remove_dir_all("./temp")?;
let _ = self.main_tx.send(ToolMessage::Output((
0,
"New config structure created!".to_string(),
)));
}
if let Some(original_value) = self.config.get("alternate_configs") {
let new_value = format!(
"{}|{},{}",
name,
client_conf_path.display().to_string(),
original_value,
);
self.config
.insert("alternate_configs".to_string(), new_value);
} else {
self.config.insert(
"alternate_configs".to_string(),
format!("{}|{},", name, client_conf_path.display().to_string()),
);
}
self.prompt.responses.clear();
self.prompt.prompts.clear();
self.prompt.action = None;
self.prompt.execute_command.clear();
Ok(())
}
pub fn parse_cs_portscan(&mut self) -> Result<(), Box<dyn Error>> {
if let Some(project) = self.projects.get_mut(self.selected_project) {
let walk = WalkDir::new(project.files.clone());
walk.into_iter().for_each(|res| {
if let Ok(entry) = res {
if entry.file_name().to_string_lossy().contains("services.tsv") {
if let Ok(scan) = read_to_string(entry.path()) {
let mut url_lines = Vec::new();
let mut web_host_lines = Vec::new();
let mut rdp_host_lines = Vec::new();
let mut ftp_host_lines = Vec::new();
let mut telnet_host_lines = Vec::new();
let mut winrm_host_lines = Vec::new();
let mut mysql_host_lines = Vec::new();
let mut mssql_host_lines = Vec::new();
let mut snmp_host_lines = Vec::new();
let mut kerberos_host_lines = Vec::new();
let mut ldap_host_lines = Vec::new();
let mut smb_host_lines = Vec::new();
let mut unknonw_host_lines = Vec::new();
let mut ssh_host_lines = Vec::new();
scan.lines().into_iter().for_each(|line| {
if line.len() > 0 {
let data: Vec<&str> = line.split_whitespace().collect();
match data[1].trim() {
"80" | "443" | "8080" | "8443" | "8000" | "4433" => {
let http_line =
format!("http://{}:{}", data[0], data[1]);
let https_line =
format!("https://{}:{}", data[0], data[1]);
url_lines.push(http_line);
url_lines.push(https_line);
web_host_lines.push(data[0].to_string());
let mut existing = false;
project
.hosts
.iter_mut()
.filter(|h| h.ip == data[0].to_string())
.for_each(|h| {
h.add_port(data[1]);
existing = true;
});
if !existing {
let mut new_host = Host::new();
new_host.ip = data[0].trim().to_string();
new_host.add_port(data[1]);
project.add_host(new_host);
}
}
"3389" => {
rdp_host_lines.push(data[0].to_string());
let mut existing = false;
project
.hosts
.iter_mut()
.filter(|h| h.ip == data[0].to_string())
.for_each(|h| {
h.add_port(data[1]);
existing = true;
});
if !existing {
let mut new_host = Host::new();
new_host.ip = data[0].trim().to_string();
new_host.add_port(data[1]);
project.add_host(new_host);
}
}
"21" => {
ftp_host_lines.push(data[0].to_string());
let mut existing = false;
project
.hosts
.iter_mut()
.filter(|h| h.ip == data[0].to_string())
.for_each(|h| {
h.add_port(data[1]);
existing = true;
});
if !existing {
let mut new_host = Host::new();
new_host.ip = data[0].trim().to_string();
new_host.add_port(data[1]);
project.add_host(new_host);
}
}
"23" => {
telnet_host_lines.push(data[0].to_string());
let mut existing = false;
project
.hosts
.iter_mut()
.filter(|h| h.ip == data[0].to_string())
.for_each(|h| {
h.add_port(data[1]);
existing = true;
});
if !existing {
let mut new_host = Host::new();
new_host.ip = data[0].trim().to_string();
new_host.add_port(data[1]);
project.add_host(new_host);
}
}
"22" => {
ssh_host_lines.push(data[0].to_string());
let mut existing = false;
project
.hosts
.iter_mut()
.filter(|h| h.ip == data[0].to_string())
.for_each(|h| {
h.add_port(data[1]);
existing = true;
});
if !existing {
let mut new_host = Host::new();
new_host.ip = data[0].trim().to_string();
new_host.add_port(data[1]);
project.add_host(new_host);
}
}
"5985" | "5986" => {
winrm_host_lines.push(data[0].to_string());
let mut existing = false;
project
.hosts
.iter_mut()
.filter(|h| h.ip == data[0].to_string())
.for_each(|h| {
h.add_port(data[1]);
existing = true;
});
if !existing {
let mut new_host = Host::new();
new_host.ip = data[0].trim().to_string();
new_host.add_port(data[1]);
project.add_host(new_host);
}
}
"3306" => {
mysql_host_lines.push(data[0].to_string());
let mut existing = false;
project
.hosts
.iter_mut()
.filter(|h| h.ip == data[0].to_string())
.for_each(|h| {
h.add_port(data[1]);
existing = true;
});
if !existing {
let mut new_host = Host::new();
new_host.ip = data[0].trim().to_string();
new_host.add_port(data[1]);
project.add_host(new_host);
}
}
"1433" => {
mssql_host_lines.push(data[0].to_string());
let mut existing = false;
project
.hosts
.iter_mut()
.filter(|h| h.ip == data[0].to_string())
.for_each(|h| {
h.add_port(data[1]);
existing = true;
});
if !existing {
let mut new_host = Host::new();
new_host.ip = data[0].trim().to_string();
new_host.add_port(data[1]);
project.add_host(new_host);
}
}
"161" => {
snmp_host_lines.push(data[0].to_string());
let mut existing = false;
project
.hosts
.iter_mut()
.filter(|h| h.ip == data[0].to_string())
.for_each(|h| {
h.add_port(data[1]);
existing = true;
});
if !existing {
let mut new_host = Host::new();
new_host.ip = data[0].trim().to_string();
new_host.add_port(data[1]);
project.add_host(new_host);
}
}
"88" => {
kerberos_host_lines.push(data[0].to_string());
let mut existing = false;
project
.hosts
.iter_mut()
.filter(|h| h.ip == data[0].to_string())
.for_each(|h| {
h.add_port(data[1]);
existing = true;
});
if !existing {
let mut new_host = Host::new();
new_host.ip = data[0].trim().to_string();
new_host.add_port(data[1]);
project.add_host(new_host);
}
}
"389" | "636" => {
ldap_host_lines.push(data[0].to_string());
let mut existing = false;
project
.hosts
.iter_mut()
.filter(|h| h.ip == data[0].to_string())
.for_each(|h| {
h.add_port(data[1]);
existing = true;
});
if !existing {
let mut new_host = Host::new();
new_host.ip = data[0].trim().to_string();
new_host.add_port(data[1]);
project.add_host(new_host);
}
}
"445" | "139" => {
smb_host_lines.push(data[0].to_string());
let mut existing = false;
project
.hosts
.iter_mut()
.filter(|h| h.ip == data[0].to_string())
.for_each(|h| {
h.add_port(data[1]);
existing = true;
});
if !existing {
let mut new_host = Host::new();
new_host.ip = data[0].trim().to_string();
new_host.add_port(data[1]);
project.add_host(new_host);
}
}
_ => {
unknonw_host_lines.push(data[0].to_string());
let mut existing = false;
project
.hosts
.iter_mut()
.filter(|h| h.ip == data[0].to_string())
.for_each(|h| {
h.add_port(data[1]);
existing = true;
});
if !existing {
let mut new_host = Host::new();
new_host.ip = data[0].trim().to_string();
new_host.add_port(data[1]);
project.add_host(new_host);
}
}
}
}
});
if !url_lines.is_empty() {
let mut file_path = project.files.clone();
file_path.push("urls.txt");
if let Ok(mut file) = File::create(file_path) {
let _ = write!(file, "{}", url_lines.join("\n"));
}
}
if !web_host_lines.is_empty() {
let mut file_path = project.files.clone();
file_path.push("web_hosts.txt");
if let Ok(mut file) = File::create(file_path) {
let _ = write!(file, "{}", web_host_lines.join("\n"));
}
}
if !rdp_host_lines.is_empty() {
let mut file_path = project.files.clone();
file_path.push("rdp_hosts.txt");
if let Ok(mut file) = File::create(file_path) {
let _ = write!(file, "{}", rdp_host_lines.join("\n"));
}
}
if !ftp_host_lines.is_empty() {
let mut file_path = project.files.clone();
file_path.push("ftp_hosts.txt");
if let Ok(mut file) = File::create(file_path) {
let _ = write!(file, "{}", ftp_host_lines.join("\n"));
}
}
if !telnet_host_lines.is_empty() {
let mut file_path = project.files.clone();
file_path.push("telnet_hosts.txt");
if let Ok(mut file) = File::create(file_path) {
let _ = write!(file, "{}", telnet_host_lines.join("\n"));
}
}
if !winrm_host_lines.is_empty() {
let mut file_path = project.files.clone();
file_path.push("winrm_hosts.txt");
if let Ok(mut file) = File::create(file_path) {
let _ = write!(file, "{}", winrm_host_lines.join("\n"));
}
}
if !mysql_host_lines.is_empty() {
let mut file_path = project.files.clone();
file_path.push("mysql_hosts.txt");
if let Ok(mut file) = File::create(file_path) {
let _ = write!(file, "{}", mysql_host_lines.join("\n"));
}
}
if !mssql_host_lines.is_empty() {
let mut file_path = project.files.clone();
file_path.push("mssql_hosts.txt");
if let Ok(mut file) = File::create(file_path) {
let _ = write!(file, "{}", mssql_host_lines.join("\n"));
}
}
if !snmp_host_lines.is_empty() {
let mut file_path = project.files.clone();
file_path.push("snmp_hosts.txt");
if let Ok(mut file) = File::create(file_path) {
let _ = write!(file, "{}", snmp_host_lines.join("\n"));
}
}
if !kerberos_host_lines.is_empty() {
let mut file_path = project.files.clone();
file_path.push("kerberos_hosts.txt");
if let Ok(mut file) = File::create(file_path) {
let _ = write!(file, "{}", kerberos_host_lines.join("\n"));
}
}
if !ldap_host_lines.is_empty() {
let mut file_path = project.files.clone();
file_path.push("ldap_hosts.txt");
if let Ok(mut file) = File::create(file_path) {
let _ = write!(file, "{}", ldap_host_lines.join("\n"));
}
}
if !smb_host_lines.is_empty() {
let mut file_path = project.files.clone();
file_path.push("smb_hosts.txt");
if let Ok(mut file) = File::create(file_path) {
let _ = write!(file, "{}", smb_host_lines.join("\n"));
}
}
if !unknonw_host_lines.is_empty() {
let mut file_path = project.files.clone();
file_path.push("unknown_hosts.txt");
if let Ok(mut file) = File::create(file_path) {
let _ = write!(file, "{}", unknonw_host_lines.join("\n"));
}
}
if !ssh_host_lines.is_empty() {
let mut file_path = project.files.clone();
file_path.push("ssh_hosts.txt");
if let Ok(mut file) = File::create(file_path) {
let _ = write!(file, "{}", ssh_host_lines.join("\n"));
}
}
}
}
}
});
project.save_config()?;
}
Ok(())
}
pub fn run_cmd(&mut self, cmd: String) {
if let Some(db) = self.projects[self.selected_project].db.clone() {
db.run_command(&cmd);
} else {
if let Some(term_cmd) = self.config.get("term_cmd") {
if let Some((prog, args)) = term_cmd.split_once(" ") {
let mut launch_cmd = Command::new(prog);
args.split(" ").into_iter().for_each(|arg| {
launch_cmd.arg(arg);
});
launch_cmd.arg(&cmd);
match launch_cmd.spawn() {
Ok(_) => {
let _ = self.main_tx.send(ToolMessage::Output((
0,
format!("[success] running {}", cmd),
)));
}
Err(e) => {
let _ = self.main_tx.send(ToolMessage::Output((
0,
format!("[error] running {}, {e}", cmd),
)));
}
}
}
}
}
}
}