added a module to copy the command to run a portscan for the scope, and

added the ability to add new configs to separate projects, like work vs
personal. Also added a way to switch between these configs.
This commit is contained in:
2026-08-19 16:56:27 -05:00
parent e28306d07d
commit 8ab7a2bec4
10 changed files with 759 additions and 291 deletions
+376 -108
View File
@@ -1,3 +1,5 @@
use clipboard::ClipboardContext;
use clipboard::ClipboardProvider;
use fs_extra::dir::{CopyOptions, copy};
use ipnet::IpNet;
use keyring::Entry;
@@ -5,6 +7,7 @@ use ratatui::crossterm::event;
use rhai::{AST, Dynamic, Engine, Scope};
use rustls::{ClientConfig, ClientConnection, RootCertStore, StreamOwned, pki_types::ServerName};
use std::collections::HashMap;
use std::env;
use std::error::Error;
use std::fs::{self, File, create_dir_all, read_dir, read_to_string, remove_dir, remove_dir_all};
use std::io::{self, BufRead, BufReader, Read, Write};
@@ -94,48 +97,46 @@ pub struct AppState {
}
impl AppState {
pub fn new() -> (Self, Receiver<ToolMessage>) {
let (main_tx, main_rx) = 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,
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(),
},
info_scroll: 0,
server_broker_running: true,
app_state_running: true,
remoting: false,
help: Vec::new(),
name: String::new(),
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,
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,
},
main_rx,
)
info_scroll: 0,
server_broker_running: true,
app_state_running: true,
remoting: false,
help: Vec::new(),
name: String::new(),
}
}
pub fn load_config(&mut self, file: PathBuf) -> Result<(), Box<dyn Error>> {
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() {
@@ -187,7 +188,7 @@ impl AppState {
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()?;
new_project.load_config(display)?;
let template_box = self.config.get("template_box").unwrap();
let tools = self.config.get("tools").unwrap();
let db = DistroBox {
@@ -242,6 +243,7 @@ impl AppState {
);
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("exit\nquit the tool\n".to_string());
self.initialize_modules();
return Ok(());
}
@@ -427,10 +429,12 @@ impl AppState {
}
}
"remove_project_confirm" => {
self.prompt.action = None;
self.prompt.num_responses = 0;
if self.prompt.responses.len() > 0 {
if self.prompt.responses[0] == "y".to_string() {
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();
@@ -507,14 +511,17 @@ impl AppState {
self.prompt.action = Some(ToolMessage::RemoveProject);
self.prompt.num_responses = 1;
self.prompt.execute_command = String::from("remove_project_confirm");
let _ = self.main_tx.send(ToolMessage::Output((
rid,
format!(
"{}, {} and all contents will be deleted. Continue? (y/N)",
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();
@@ -528,17 +535,30 @@ impl AppState {
self.prompt.num_responses = 2;
self.prompt.action = Some(ToolMessage::AddServer);
self.prompt.execute_command = String::from("add_server");
self.prompt.prompts = vec![
"What is the IP for the server?".to_string(),
"What is the port for the server?".to_string(),
];
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!("{}:{}", self.prompt.responses[0], self.prompt.responses[1]),
address: format!("{}:{}", ip, port),
connected: false,
timer: Duration::from_secs(5),
last_check: time::Instant::now(),
@@ -565,12 +585,20 @@ impl AppState {
.send(ToolMessage::Input((0, self.prompt.execute_command.clone())));
let _ = self.main_tx.send(ToolMessage::EndPrompt);
} else {
let mut prompt_index = 0;
if self.prompt.responses.len() > 0 {
prompt_index = self.prompt.num_responses - self.prompt.responses.len();
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;
}
}
let query = self.prompt.prompts[prompt_index].clone();
let _ = self.main_tx.send(ToolMessage::Output((rid, query)));
}
}
"list_servers" => {
@@ -614,7 +642,10 @@ impl AppState {
"select_server" => {
self.prompt.action = Some(ToolMessage::SelectServer);
self.prompt.num_responses = 1;
self.prompt.prompts.push(String::from("Selection?"));
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() {
@@ -634,7 +665,7 @@ impl AppState {
}
"server_selected" => {
if self.prompt.responses.len() == 1 {
if let Ok(id) = self.prompt.responses[0].parse::<usize>() {
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((
@@ -855,6 +886,67 @@ impl AppState {
}
}
}
"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))));
}
});
}
}
"exit" => {
self.save_all()?;
let _ = self.main_tx.send(ToolMessage::AppStateExit);
@@ -938,7 +1030,11 @@ impl AppState {
let out_string = result
.into_string()
.unwrap_or_else(|_| "failed to parse string".into());
if cmd_meta.calls != "none" {
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())));
}
@@ -1173,50 +1269,193 @@ impl AppState {
return Ok(());
}
pub fn new_server(&mut self, rid: usize) -> Result<(), Box<dyn Error>> {
let prompt = self.prompt.clone();
if prompt.responses.len() == prompt.num_responses {
let new_server = Server {
address: format!(
"{}:{}",
self.prompt.responses[0].clone(),
self.prompt.responses[1].clone()
),
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.action = None;
self.prompt.responses.clear();
self.prompt.num_responses = 0;
return Ok(());
} else {
match prompt.responses.len() {
0 => {
let _ = self.main_tx.send(ToolMessage::Output((
rid,
String::from("What is the IP for the server?"),
)));
}
1 => {
let _ = self.main_tx.send(ToolMessage::Output((
rid,
String::from("What is the port for the server?"),
)));
}
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(())
}
}
@@ -1413,7 +1652,7 @@ impl Project {
}
}
pub fn load_config(&mut self) -> Result<(), Box<dyn Error>> {
pub fn load_config(&mut self, display: bool) -> Result<(), Box<dyn Error>> {
let config_contents = read_to_string(&self.config_folder)?;
for line in config_contents.lines() {
let parts: Vec<&str> = line.split(": ").collect();
@@ -1438,8 +1677,10 @@ impl Project {
}
}
}
println!("{} | {} config loaded!", self.org_name, self.name);
println!("loading hosts...");
if display {
println!("{} | {} config loaded!", self.org_name, self.name);
println!("loading hosts...");
}
let mut hosts_folder = self.config_folder.clone();
let mut users_folder = self.config_folder.clone();
hosts_folder.pop();
@@ -1512,7 +1753,9 @@ impl Project {
}
}
}
println!("{} | {} loaded!", self.org_name, self.name);
if display {
println!("{} | {} loaded!", self.org_name, self.name);
}
return Ok(());
}
@@ -1811,6 +2054,17 @@ impl ModuleLoader {
}
results
});
engine.register_fn("filter_ips", |s: &str| -> rhai::Array {
let mut results = rhai::Array::new();
if let Ok(net) = s.trim().parse::<IpNet>() {
results.push(net.addr().to_string().trim().into());
} else if let Ok(ip) = s.trim().parse::<IpAddr>() {
results.push(ip.to_string().trim().into());
} else if s.contains(".") {
results.push(s.to_string().trim().into());
}
results
});
engine.register_fn("write_file", |path: &str, contents: &str| {
std::fs::write(path, contents).is_ok()
});
@@ -2133,10 +2387,11 @@ impl DistroBox {
#[derive(Clone)]
struct Prompt {
action: Option<ToolMessage>,
responses: Vec<String>,
responses: Vec<PromptResponse>,
execute_command: String,
num_responses: usize,
prompts: Vec<String>,
prompts: Vec<PromptQuery>,
last_prompted: Option<PromptQuery>,
}
impl Prompt {
@@ -2146,5 +2401,18 @@ impl Prompt {
self.execute_command.clear();
self.num_responses = 0;
self.prompts.clear();
self.last_prompted = None;
}
}
#[derive(Clone)]
struct PromptResponse {
query: PromptQuery,
response: String,
}
#[derive(Clone)]
struct PromptQuery {
id: usize,
query: String,
}