Added a third panel to display current projecct information and tool

information.
This commit is contained in:
2026-05-21 17:04:37 -05:00
parent 95b86f80aa
commit 283a336643
2 changed files with 269 additions and 5 deletions
+114 -5
View File
@@ -5,6 +5,7 @@ use crossterm::{
execute, execute,
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
}; };
use iced::alignment::Vertical::Bottom;
use ratatui::{ use ratatui::{
prelude::*, prelude::*,
widgets::{Block, Borders, List, ListItem, ListState, Paragraph}, widgets::{Block, Borders, List, ListItem, ListState, Paragraph},
@@ -82,7 +83,11 @@ pub fn run_tui(
.split(f.area()); .split(f.area());
let top_chunks = Layout::default() let top_chunks = Layout::default()
.direction(Direction::Horizontal) .direction(Direction::Horizontal)
.constraints([Constraint::Percentage(30), Constraint::Percentage(70)]) .constraints([
Constraint::Percentage(20),
Constraint::Percentage(45),
Constraint::Percentage(35),
])
.split(main_chunks[0]); .split(main_chunks[0]);
let projects: Vec<ListItem> = state let projects: Vec<ListItem> = state
.projects .projects
@@ -103,6 +108,86 @@ pub fn run_tui(
) )
.highlight_symbol(">> "); .highlight_symbol(">> ");
f.render_stateful_widget(projects_list, top_chunks[0], &mut project_list_state); f.render_stateful_widget(projects_list, top_chunks[0], &mut project_list_state);
let mut info_lines: Vec<Line> = 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<Line> = state let output_lines: Vec<Line> = state
.output .output
.iter() .iter()
@@ -341,10 +426,34 @@ pub fn run_tui(
} }
} }
AppEvent::Mouse(mouse) => { AppEvent::Mouse(mouse) => {
if mouse.kind == crossterm::event::MouseEventKind::ScrollUp { if let Ok(size) = terminal.size() {
state.output_scroll = state.output_scroll.saturating_sub(1); let main_chunks = Layout::default()
} else if mouse.kind == crossterm::event::MouseEventKind::ScrollDown { .direction(Direction::Vertical)
state.output_scroll = state.output_scroll.saturating_add(1); .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);
}
}
} }
} }
} }
+155
View File
@@ -29,6 +29,7 @@ pub struct AppState {
pub module_loader: ModuleLoader, pub module_loader: ModuleLoader,
pub output_scroll: u16, pub output_scroll: u16,
prompt: Prompt, prompt: Prompt,
pub info_scroll: u16,
} }
impl AppState { impl AppState {
@@ -60,6 +61,7 @@ impl AppState {
execute_command: String::new(), execute_command: String::new(),
num_responses: 0, num_responses: 0,
}, },
info_scroll: 0,
}, },
main_rx, 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::<Vec<String>>()
}
"open_ports" => {
new_host.open_ports = data
.trim()
.split(",")
.into_iter()
.map(|port| port.parse().unwrap())
.collect::<Vec<usize>>()
}
"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); println!("{} | {} loaded!", self.org_name, self.name);
return Ok(()); return Ok(());
} }
pub fn save_config(&mut self) -> Result<(), Box<dyn Error>> { pub fn save_config(&mut self) -> Result<(), Box<dyn Error>> {
create_dir_all(&self.config_folder)?; 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(); let mut conf_file = self.config_folder.clone();
conf_file.push("project.conf"); conf_file.push("project.conf");
let mut file = File::create(conf_file)?; let mut file = File::create(conf_file)?;
@@ -604,6 +686,67 @@ impl Project {
out_string.push_str("stage: upcoming"); out_string.push_str("stage: upcoming");
} }
file.write_all(out_string.as_bytes())?; 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::<Vec<String>>()
.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::<Vec<String>>()
.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(()); return Ok(());
} }
} }
@@ -710,6 +853,18 @@ pub struct User {
pub compromised: bool, pub compromised: bool,
} }
impl User {
pub fn new() -> Self {
Self {
name: String::new(),
password: None,
hash: None,
ticket: None,
compromised: false,
}
}
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum ToolMessage { pub enum ToolMessage {
Input(String), Input(String),