From 283a336643d76f837db374e4bdad51b2ce26a898 Mon Sep 17 00:00:00 2001 From: pyro Date: Thu, 21 May 2026 17:04:37 -0500 Subject: [PATCH] Added a third panel to display current projecct information and tool information. --- src/funcs.rs | 119 +++++++++++++++++++++++++++++++++++++-- src/lib.rs | 155 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 269 insertions(+), 5 deletions(-) diff --git a/src/funcs.rs b/src/funcs.rs index 16c5e20..4c543cc 100644 --- a/src/funcs.rs +++ b/src/funcs.rs @@ -5,6 +5,7 @@ use crossterm::{ execute, terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, }; +use iced::alignment::Vertical::Bottom; use ratatui::{ prelude::*, widgets::{Block, Borders, List, ListItem, ListState, Paragraph}, @@ -82,7 +83,11 @@ pub fn run_tui( .split(f.area()); let top_chunks = Layout::default() .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(30), Constraint::Percentage(70)]) + .constraints([ + Constraint::Percentage(20), + Constraint::Percentage(45), + Constraint::Percentage(35), + ]) .split(main_chunks[0]); let projects: Vec = state .projects @@ -103,6 +108,86 @@ pub fn run_tui( ) .highlight_symbol(">> "); f.render_stateful_widget(projects_list, top_chunks[0], &mut project_list_state); + let mut info_lines: Vec = Vec::new(); + let project = state.projects[state.selected_project].clone(); + info_lines.push(Line::from(Span::styled( + "--- PROJECT INFORMATION ---", + Style::default().fg(Color::Green), + ))); + info_lines.push(Line::from(format!("ORG: {}", project.org_name))); + info_lines.push(Line::from(format!("NAME: {}", project.name))); + info_lines.push(Line::from(format!("NOTES: {}", project.notes.display()))); + info_lines.push(Line::from(format!("Files: {}", project.files.display()))); + if let Some(db) = project.db { + info_lines.push(Line::from(format!("DISTROBOX: {}", db.name))); + } + if project.current { + info_lines.push(Line::from("STATUS: CURRENT")); + } else { + info_lines.push(Line::from("STATUS: UPCOMING")); + } + if !project.hosts.is_empty() { + info_lines.push(Line::from(Span::styled( + "--- PROJECT HOST INFORMATION ---", + Style::default().fg(Color::Green), + ))); + for host in &project.hosts { + info_lines.push(Line::from(format!("- {}:{}", host.hostname, host.ip))); + info_lines.push(Line::from(format!(" - PWNED: {}", host.pwned))); + info_lines.push(Line::from(format!( + " - CONTROL PORT: {}", + host.control_port + ))); + if !host.open_ports.is_empty() { + info_lines.push(Line::from(format!(" - PORTS:"))); + for port in &host.open_ports { + info_lines.push(Line::from(format!(" - {}", port))); + } + } + if !host.users.is_empty() { + info_lines.push(Line::from(format!(" - USERS:"))); + for user in &host.users { + info_lines.push(Line::from(format!( + " - {} - PWNED: {}", + user.name, user.compromised + ))); + } + } + } + } + let spacer = format!("{}", "+".repeat(top_chunks[2].width as usize - 2)); + info_lines.push(Line::from(Span::styled( + &spacer, + Style::default().fg(Color::Green), + ))); + info_lines.push(Line::from(Span::styled( + "--- Tool Information ---", + Style::default().fg(Color::Green), + ))); + info_lines.push(Line::from(format!( + "CONFIG FILE: {}", + state.config_file.display() + ))); + for setting in state.config.keys() { + info_lines.push(Line::from(format!( + "{}: {}", + setting.to_uppercase(), + 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( + 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]); let output_lines: Vec = state .output .iter() @@ -341,10 +426,34 @@ pub fn run_tui( } } AppEvent::Mouse(mouse) => { - if mouse.kind == crossterm::event::MouseEventKind::ScrollUp { - state.output_scroll = state.output_scroll.saturating_sub(1); - } else if mouse.kind == crossterm::event::MouseEventKind::ScrollDown { - state.output_scroll = state.output_scroll.saturating_add(1); + if let Ok(size) = terminal.size() { + let main_chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(0), Constraint::Max(3)]) + .split(ratatui::layout::Rect::from(size)); + + let top_chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage(20), + Constraint::Percentage(45), + Constraint::Percentage(35), + ]) + .split(main_chunks[0]); + let left_side_cutoff = top_chunks[0].width + top_chunks[1].width; + if mouse.column < left_side_cutoff { + if mouse.kind == crossterm::event::MouseEventKind::ScrollUp { + state.output_scroll = state.output_scroll.saturating_sub(1); + } else if mouse.kind == crossterm::event::MouseEventKind::ScrollDown { + state.output_scroll = state.output_scroll.saturating_add(1); + } + } else { + if mouse.kind == crossterm::event::MouseEventKind::ScrollUp { + state.info_scroll = state.info_scroll.saturating_sub(1); + } else if mouse.kind == crossterm::event::MouseEventKind::ScrollDown { + state.info_scroll = state.info_scroll.saturating_add(1); + } + } } } } diff --git a/src/lib.rs b/src/lib.rs index ff89fe3..684d00e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,6 +29,7 @@ pub struct AppState { pub module_loader: ModuleLoader, pub output_scroll: u16, prompt: Prompt, + pub info_scroll: u16, } impl AppState { @@ -60,6 +61,7 @@ impl AppState { execute_command: String::new(), num_responses: 0, }, + info_scroll: 0, }, main_rx, ) @@ -582,12 +584,92 @@ impl Project { } } } + 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(); + hosts_folder.push("hosts"); + users_folder.pop(); + users_folder.push("users"); + let mut users = HashMap::new(); + if users_folder.exists() { + for entry in read_dir(users_folder)? { + let entry = entry?; + let user_config_string = read_to_string(entry.path())?; + let mut new_user = User::new(); + for line in user_config_string.lines() { + if line.contains(": ") { + let (setting, data) = line.split_once(": ").unwrap(); + match setting.trim() { + "username" => new_user.name = data.trim().to_string(), + "password" => new_user.password = Some(data.trim().to_string()), + "hash" => new_user.hash = Some(data.trim().to_string()), + "compromised" => new_user.compromised = data.trim().parse().unwrap(), + "ticket" => new_user.ticket = Some(PathBuf::from(data.trim())), + _ => {} + } + } + } + users.insert(new_user.name.clone(), new_user); + } + } + if hosts_folder.exists() { + for entry in read_dir(hosts_folder)? { + let entry = entry?; + let host_path = entry.path(); + let mut new_host = Host::new(); + let host_conf_text = read_to_string(host_path)?; + for line in host_conf_text.lines() { + if line.contains(": ") { + let (setting, data) = line.split_once(": ").unwrap(); + match setting.trim() { + "ip" => new_host.ip = data.trim().to_string(), + "hostname" => new_host.hostname = data.trim().to_string(), + "control_port" => new_host.control_port = data.trim().parse()?, + "pwned" => new_host.pwned = data.trim().parse()?, + "id" => new_host.id = data.trim().parse()?, + "findings" => { + new_host.findings = data + .trim() + .split(",") + .map(|finding| finding.to_string()) + .collect::>() + } + "open_ports" => { + new_host.open_ports = data + .trim() + .split(",") + .into_iter() + .map(|port| port.parse().unwrap()) + .collect::>() + } + "users" => { + let names: Vec<&str> = data.trim().split(",").collect(); + names.into_iter().for_each(|name| { + if let Some(user) = users.get(name) { + new_host.users.push(user.clone()); + } + }); + } + _ => {} + } + } + } + } + } println!("{} | {} loaded!", self.org_name, self.name); return Ok(()); } pub fn save_config(&mut self) -> Result<(), Box> { create_dir_all(&self.config_folder)?; + let mut hosts_folder = self.config_folder.clone(); + hosts_folder.push("hosts"); + let mut users_folder = self.config_folder.clone(); + users_folder.push("users"); + create_dir_all(&hosts_folder)?; + create_dir_all(&users_folder)?; let mut conf_file = self.config_folder.clone(); conf_file.push("project.conf"); let mut file = File::create(conf_file)?; @@ -604,6 +686,67 @@ impl Project { out_string.push_str("stage: upcoming"); } file.write_all(out_string.as_bytes())?; + if !self.hosts.is_empty() { + for host in &self.hosts { + let mut host_conf_file = + File::create(hosts_folder.join(format!("{}.conf", host.id)))?; + let mut out_string = format!( + " +ip: {} +hostname: {} +control_port: {} +pwned: {} +id: {}", + host.ip, host.hostname, host.control_port, host.pwned, host.id, + ); + if !host.findings.is_empty() { + out_string.push_str(&format!("\nfindings: ")); + out_string.push_str(&host.findings.join(",")); + } + if !host.open_ports.is_empty() { + out_string.push_str(&format!("\nopen_ports: ")); + out_string.push_str( + &host + .open_ports + .iter() + .map(|port| port.to_string()) + .collect::>() + .join(","), + ); + } + if !host.users.is_empty() { + out_string.push_str(&format!("\nusers: ")); + out_string.push_str( + &host + .users + .iter() + .map(|user| user.name.clone()) + .collect::>() + .join(","), + ); + for user in &host.users { + let mut user_conf_file = + File::create(users_folder.join(format!("{}.conf", user.name)))?; + let mut user_string = format!("username: {}", &user.name); + user_string.push_str(&format!("\ncompromised: {}", user.compromised)); + if user.hash.is_some() { + let hash = user.hash.clone().unwrap(); + user_string.push_str(&format!("hash: {hash}")); + } + if user.password.is_some() { + let password = user.password.clone().unwrap(); + user_string.push_str(&format!("password: {password}")); + } + if user.ticket.is_some() { + let ticket = user.ticket.clone().unwrap().display().to_string(); + user_string.push_str(&format!("ticket: {ticket}")); + } + user_conf_file.write_all(user_string.as_bytes())?; + } + } + host_conf_file.write_all(out_string.as_bytes())?; + } + } return Ok(()); } } @@ -710,6 +853,18 @@ pub struct User { pub compromised: bool, } +impl User { + pub fn new() -> Self { + Self { + name: String::new(), + password: None, + hash: None, + ticket: None, + compromised: false, + } + } +} + #[derive(Clone, Debug)] pub enum ToolMessage { Input(String),