fixed a bug in project promotion where the folder for the project name

would be duplicated, and added rustbuster to the rust tools available!
This commit is contained in:
2026-09-02 14:29:50 -05:00
parent d14f02ad53
commit 86c3525493
5 changed files with 1168 additions and 18 deletions
Generated
+708 -3
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -9,6 +9,7 @@ anyhow = "1.0.103"
clap = { version = "4.6.1", features = ["derive"] }
clipboard = "0.5.0"
crossterm = "0.29.0"
dns-lookup = "4.0.1"
fs_extra = "1.3.0"
headless_chrome = "1.0.22"
ipnet = "2.12.0"
@@ -17,6 +18,7 @@ rand = "0.10.2"
ratatui = "0.30.0"
rayon = "1.12.0"
rcgen = "0.14.8"
reqwest = { version = "0.13.4", features = ["blocking"] }
rhai = { version = "1.24.0", features = ["metadata", "sync"] }
rustc-hash = "2.1.2"
rustls = "0.23.43"
@@ -25,4 +27,5 @@ sysinfo = "0.39.6"
textwrap = "0.16.2"
tokio = { version = "1.52.3", features = ["full"] }
tokio-rustls = "0.26.4"
trust-dns-resolver = "0.23.2"
walkdir = "2.5.0"
+61 -11
View File
@@ -6,6 +6,7 @@ use crossterm::{
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use ratatui::{
layout::{Constraint, Layout},
prelude::*,
widgets::{Block, Borders, List, ListItem, ListState, Paragraph},
};
@@ -248,19 +249,68 @@ pub fn run_tui(
state.config.get(setting).unwrap()
)));
}
let info_area_height = top_chunks[1].height.saturating_sub(2) as usize;
if state.info_scroll as usize > info_lines.len().saturating_sub(info_area_height) {
state.info_scroll = info_lines.len().saturating_sub(info_area_height) as u16;
}
let info_paragraph = Paragraph::new(info_lines)
.block(
if state.progress_bars.is_empty() {
let info_area_height = top_chunks[2].height.saturating_sub(2) as usize;
if state.info_scroll as usize > info_lines.len().saturating_sub(info_area_height) {
state.info_scroll = info_lines.len().saturating_sub(info_area_height) as u16;
}
let info_paragraph = Paragraph::new(info_lines)
.block(
Block::default()
.borders(Borders::ALL)
.title("Selected Project Information"),
)
.scroll((state.info_scroll, 0))
.wrap(ratatui::widgets::Wrap { trim: false });
f.render_widget(info_paragraph, top_chunks[2]);
} else {
let progress_section = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Percentage(60), Constraint::Percentage(40)])
.split(top_chunks[2]);
let info_area_height = top_chunks[2].height.saturating_sub(2) as usize;
if state.info_scroll as usize > info_lines.len().saturating_sub(info_area_height) {
state.info_scroll = info_lines.len().saturating_sub(info_area_height) as u16;
}
let info_paragraph = Paragraph::new(info_lines)
.block(
Block::default()
.borders(Borders::ALL)
.title("Selected Project Information"),
)
.scroll((state.info_scroll, 0))
.wrap(ratatui::widgets::Wrap { trim: false });
f.render_widget(info_paragraph, progress_section[0]);
let mut progress_lines = Vec::new();
state.progress_bars.iter().for_each(|progress| {
if let Ok(status) = progress.read() {
let percentage = status.complete / status.total * 100;
if percentage == 100 {
progress_lines.push(Line::from(Span::styled(
format!(
"{} - {}/{} ({}%)",
status.name, status.complete, status.total, percentage
),
Style::default().fg(Color::Green),
)));
} else {
progress_lines.push(Line::from(Span::styled(
format!(
"{} - {}/{} ({}%)",
status.name, status.complete, status.total, percentage
),
Style::default().fg(Color::Magenta),
)));
}
}
});
let progress_paragraph = Paragraph::new(progress_lines).block(
Block::default()
.borders(Borders::ALL)
.title("Selected Project Information"),
)
.scroll((state.info_scroll, 0))
.wrap(ratatui::widgets::Wrap { trim: false });
f.render_widget(info_paragraph, top_chunks[2]);
.title("Operations in progress"),
);
f.render_widget(progress_paragraph, progress_section[1]);
}
let mut output_lines = Vec::new();
state.output.iter().for_each(|text| {
let line = Line::from(Span::raw(text));
+304 -4
View File
@@ -1,3 +1,4 @@
use crate::rust_tools::rustwitness;
use clipboard::ClipboardContext;
use clipboard::ClipboardProvider;
use fs_extra::dir::{CopyOptions, copy};
@@ -6,24 +7,26 @@ use keyring::Entry;
use ratatui::crossterm::event;
use rayon::prelude::*;
use rhai::{AST, Dynamic, Engine, Scope};
use rust_tools::{BusterTarget, rustbuster};
use rustls::{ClientConfig, ClientConnection, RootCertStore, StreamOwned, pki_types::ServerName};
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::{self, BufRead, BufReader, Read, Write};
use std::net::IpAddr;
use std::net::SocketAddr;
use std::net::TcpStream;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::sync::RwLock;
use std::sync::mpsc::Receiver;
use std::sync::{Arc, Mutex, mpsc::Sender, mpsc::channel};
use std::time::Duration;
use std::time::{self, Instant};
use walkdir::WalkDir;
use crate::rust_tools::rustwitness;
pub mod funcs;
pub mod rust_tools;
pub mod server;
@@ -99,6 +102,7 @@ pub struct AppState {
pub remoting: bool,
pub help: Vec<String>,
pub name: String,
pub progress_bars: Vec<Arc<RwLock<ToolProgress>>>,
}
impl AppState {
@@ -138,6 +142,7 @@ impl AppState {
remoting: false,
help: Vec::new(),
name: String::new(),
progress_bars: Vec::new(),
}
}
@@ -251,6 +256,10 @@ impl AppState {
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.".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".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".to_string());
self.help
.push("clear_operations\nClear the progress section of the tool.".to_string());
self.help.push("exit\nquit the tool\n".to_string());
self.initialize_modules();
return Ok(());
@@ -1077,6 +1086,271 @@ impl AppState {
}
}
}
"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 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 project_clone = Arc::new(Mutex::new(project.clone()));
let id = self.selected_project.clone();
let tx_clone = self.main_tx.clone();
self.workers.spawn(move || {
hosts.par_iter_mut().for_each(|h| {
ports.par_iter().for_each(|p| {
let mut host = h.clone();
match host.check_port(p.clone(), tx_clone.clone()) {
Ok(_) => {
if let Ok(mut lock) = project_clone.lock() {
lock.add_host(host.clone());
}
}
Err(_) => {}
}
});
});
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 _ = self.main_tx.send(ToolMessage::Output((
rid,
format!("{} targets found scanning...", num_dir + num_sub),
)));
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 new_progress = Arc::new(RwLock::new(ToolProgress {
name: "rustbuster".to_string(),
total: num_dir + num_sub,
complete: 0,
}));
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;
}
}
});
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.clear();
}
"exit" => {
self.save_all()?;
let _ = self.main_tx.send(ToolMessage::AppStateExit);
@@ -1382,9 +1656,9 @@ impl AppState {
let return_string = String::from("[success] Project promoted!");
let mut new_project = self.projects[self.selected_project].clone();
let new_files_path = PathBuf::from(self.config.get("current_files").unwrap())
.join(format!("{}/{}", new_project.org_name, new_project.name));
.join(format!("{}", new_project.org_name));
let new_notes_path = PathBuf::from(self.config.get("current_notes").unwrap())
.join(format!("{}/{}", new_project.org_name, new_project.name));
.join(format!("{}", new_project.org_name));
let mut options = CopyOptions::new();
create_dir_all(&new_files_path)?;
create_dir_all(&new_notes_path)?;
@@ -2505,6 +2779,26 @@ id: {}",
host_file.sync_all()?;
Ok(())
}
pub fn check_port(&mut self, port: u16, tx: Sender<ToolMessage>) -> Result<(), Box<dyn Error>> {
if let Ok(ip) = self.ip.parse::<IpAddr>() {
let address = SocketAddr::new(ip, port);
if let Ok(_) = TcpStream::connect_timeout(&address, Duration::from_secs(3)) {
self.add_port(&format!("{}", port));
let _ = tx.send(ToolMessage::Output((
0,
format!("[success] {} is open on {}", port, self.ip),
)));
}
} else {
let _ = tx.send(ToolMessage::Output((
0,
format!("[error] {} is not a valid ip address!", self.ip),
)));
return Err("not valid IP".into());
}
return Ok(());
}
}
#[derive(Clone, Debug)]
@@ -3013,3 +3307,9 @@ struct PromptQuery {
id: usize,
query: String,
}
struct ToolProgress {
name: String,
total: usize,
complete: usize,
}
+92
View File
@@ -1,6 +1,16 @@
use crate::*;
use anyhow::Context;
use dns_lookup::lookup_host;
use headless_chrome::{Browser, LaunchOptions};
use reqwest::StatusCode;
use std::net::SocketAddr;
use trust_dns_resolver::lookup::Ipv4Lookup;
#[derive(Clone)]
pub enum BusterTarget {
Sub(String),
Dir(String),
}
pub fn rustwitness(
input: String,
@@ -61,3 +71,85 @@ pub fn rustwitness(
capture_screenshot(&input, proxy.clone(), output.clone())?;
Ok(())
}
pub fn rustbuster(target: BusterTarget) -> Option<String> {
match target {
BusterTarget::Dir(url) => {
if let Ok(resp) = reqwest::blocking::get(&url) {
let status = resp.status();
match status {
StatusCode::OK => {
return Some(format!("{}", url));
}
StatusCode::ACCEPTED => {
return Some(format!("{}", url));
}
StatusCode::CONTINUE => {
return Some(format!("{}", url));
}
StatusCode::CREATED => {
return Some(format!("{}", url));
}
StatusCode::FOUND => {
return Some(format!("{}", url));
}
StatusCode::IM_A_TEAPOT => {
return Some(format!("{}", url));
}
StatusCode::MOVED_PERMANENTLY => {
return Some(format!("{}", url));
}
StatusCode::PERMANENT_REDIRECT => {
return Some(format!("{}", url));
}
StatusCode::TEMPORARY_REDIRECT => {
return Some(format!("{}", url));
}
_ => {
return None;
}
}
} else {
return None;
}
}
BusterTarget::Sub(name) => {
let mut base = String::new();
if let Some((given_name, tld)) = name.rsplit_once(".") {
if let Some((_, base_name)) = name.rsplit_once(".") {
base = format!("{}.{}", base_name, tld);
} else {
base = format!("{}.{}", given_name, tld);
}
}
let mut wildcards = Vec::new();
if base.len() > 0 {
if let Ok(ips) = lookup_host(&format!("burstpyrofoo.{}", base)) {
ips.into_iter().for_each(|ip| {
wildcards.push(ip);
});
}
}
if let Ok(mut ips) = lookup_host(&name) {
let mut found_ips = Vec::new();
let not_wild = !ips.all(|ip| wildcards.contains(&ip));
if not_wild {
ips.for_each(|ip| {
found_ips.push(ip.to_string());
});
}
if found_ips.len() > 0 {
let mut out = String::new();
found_ips.iter().for_each(|ip| {
out.push_str(&format!("{}:{}\n", name, ip));
});
return Some(out);
} else {
return None;
}
} else {
return None;
}
}
}
}