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.
2629 lines
129 KiB
Rust
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),
|
|
)));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|