Added a third panel to display current projecct information and tool
information.
This commit is contained in:
+110
-1
@@ -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,11 +426,35 @@ pub fn run_tui(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
AppEvent::Mouse(mouse) => {
|
AppEvent::Mouse(mouse) => {
|
||||||
|
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 {
|
if mouse.kind == crossterm::event::MouseEventKind::ScrollUp {
|
||||||
state.output_scroll = state.output_scroll.saturating_sub(1);
|
state.output_scroll = state.output_scroll.saturating_sub(1);
|
||||||
} else if mouse.kind == crossterm::event::MouseEventKind::ScrollDown {
|
} else if mouse.kind == crossterm::event::MouseEventKind::ScrollDown {
|
||||||
state.output_scroll = state.output_scroll.saturating_add(1);
|
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
@@ -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),
|
||||||
|
|||||||
Reference in New Issue
Block a user