the server will now save the password that is provided or generated for

it to the systems keyring, and attempt to load it at run time. I also
added the beginnings for ingesting shodan results for hosts into the
tool. The attack client can now save servers, load them from the folder,
and save/load the passwords to/from the system keyring.
This commit is contained in:
2026-09-03 15:25:27 -05:00
parent 96672190c4
commit 911189c095
7 changed files with 1204 additions and 403 deletions
+426 -336
View File
@@ -88,6 +88,7 @@ pub fn run_tui(
let (event_tx, event_rx) = channel::<AppEvent>();
let input_tx = event_tx.clone();
let mut project_list_state = ListState::default();
let mut host_list_state = ListState::default();
let mut system = System::new_all();
let system_timer = Duration::from_secs(1);
let mut last_print = Instant::now();
@@ -97,6 +98,7 @@ pub fn run_tui(
project_list_state.select(Some(0));
state.selected_project = 0;
}
host_list_state.select(Some(0));
std::thread::spawn(move || {
loop {
if !state.server_broker_running && !state.app_state_running {
@@ -143,245 +145,293 @@ pub fn run_tui(
last_print = Instant::now();
}
let mut text_area_width = 0;
terminal.draw(|f| {
let main_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(0), Constraint::Max(3)])
.split(f.area());
let top_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage(20),
Constraint::Percentage(45),
Constraint::Percentage(35),
])
.split(main_chunks[0]);
text_area_width = top_chunks[1].width.saturating_sub(2);
let projects: Vec<ListItem> = state
.projects
.iter()
.map(|p| ListItem::new(format!(" {} | {}", p.org_name, p.name)))
.collect();
let projects_list = List::new(projects)
.block(
Block::default()
.borders(Borders::ALL)
.title(" Projects (ctrl + arrow keys to select) "),
)
.highlight_style(
Style::default()
.bg(Color::Blue)
.fg(Color::White)
.add_modifier(Modifier::BOLD),
)
.highlight_symbol(">> ");
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(format!(
"|CPU: {}% | RAM: {}GB / {}GB |",
system.global_cpu_usage().floor(),
system.used_memory() / BYTES_PER_GB,
system.total_memory() / BYTES_PER_GB,
)));
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"));
if state.selecting_host {
if !state.projects[state.selected_project].hosts.is_empty() {
terminal.draw(|f| {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(0), Constraint::Max(3)])
.split(f.area());
let project_name = Paragraph::new(format!(
"{} {}",
state.projects[state.selected_project].org_name,
state.projects[state.selected_project].name
));
let hosts: Vec<ListItem> = state.projects[state.selected_project]
.hosts
.iter()
.map(|h| ListItem::new(format!("{}: {}", h.ip, h.hostname)))
.collect();
let hosts_list = List::new(hosts)
.block(
Block::default()
.borders(Borders::ALL)
.title("Select a host."),
)
.highlight_style(
Style::default()
.bg(Color::Blue)
.fg(Color::White)
.add_modifier(Modifier::BOLD),
)
.highlight_symbol(">> ");
f.render_stateful_widget(hosts_list, chunks[0], &mut host_list_state);
f.render_widget(project_name, chunks[1]);
})?;
} else {
info_lines.push(Line::from("STATUS: UPCOMING"));
let _ = state.main_tx.send(ToolMessage::Output((
0,
"No hosts in the current project to select!".to_string(),
)));
state.selecting_host = false;
}
if !project.hosts.is_empty() {
} else {
terminal.draw(|f| {
let main_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(0), Constraint::Max(3)])
.split(f.area());
let top_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage(20),
Constraint::Percentage(45),
Constraint::Percentage(35),
])
.split(main_chunks[0]);
text_area_width = top_chunks[1].width.saturating_sub(2);
let projects: Vec<ListItem> = state
.projects
.iter()
.map(|p| ListItem::new(format!(" {} | {}", p.org_name, p.name)))
.collect();
let projects_list = List::new(projects)
.block(
Block::default()
.borders(Borders::ALL)
.title(" Projects (ctrl + arrow keys to select) "),
)
.highlight_style(
Style::default()
.bg(Color::Blue)
.fg(Color::White)
.add_modifier(Modifier::BOLD),
)
.highlight_symbol(">> ");
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(format!(
"|CPU: {}% | RAM: {}GB / {}GB |",
system.global_cpu_usage().floor(),
system.used_memory() / BYTES_PER_GB,
system.total_memory() / BYTES_PER_GB,
)));
info_lines.push(Line::from(Span::styled(
"--- PROJECT HOST INFORMATION ---",
"--- PROJECT 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
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),
)));
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)));
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()
)));
}
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();
let mut color_differnt = false;
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),
Style::default().fg(Color::Green),
)));
progress_lines.push(Line::from(Span::styled(
format!(
" {}/{} ({}%)",
status.complete, status.total, percentage
),
Style::default().fg(Color::Green),
)));
} else {
match color_differnt {
true => {
progress_lines.push(Line::from(Span::styled(
format!("{}:)", status.name),
Style::default().fg(Color::Cyan),
)));
progress_lines.push(Line::from(Span::styled(
format!(
" {}/{} ({}%)",
status.complete, status.total, percentage
),
Style::default().fg(Color::Cyan),
)));
color_differnt = false;
}
false => {
progress_lines.push(Line::from(Span::styled(
format!("{}:)", status.name),
Style::default().fg(Color::Magenta),
)));
progress_lines.push(Line::from(Span::styled(
format!(
" {}/{} ({}%)",
status.complete, status.total, percentage
),
Style::default().fg(Color::Magenta),
)));
color_differnt = true;
}
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()
)));
}
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();
let mut color_differnt = false;
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),
Style::default().fg(Color::Green),
)));
progress_lines.push(Line::from(Span::styled(
format!(
" {}/{} ({}%)",
status.complete, status.total, percentage
),
Style::default().fg(Color::Green),
)));
} else {
match color_differnt {
true => {
progress_lines.push(Line::from(Span::styled(
format!("{}:)", status.name),
Style::default().fg(Color::Cyan),
)));
progress_lines.push(Line::from(Span::styled(
format!(
" {}/{} ({}%)",
status.complete, status.total, percentage
),
Style::default().fg(Color::Cyan),
)));
color_differnt = false;
}
false => {
progress_lines.push(Line::from(Span::styled(
format!("{}:)", status.name),
Style::default().fg(Color::Magenta),
)));
progress_lines.push(Line::from(Span::styled(
format!(
" {}/{} ({}%)",
status.complete, status.total, percentage
),
Style::default().fg(Color::Magenta),
)));
color_differnt = true;
}
}
}
}
});
let progress_paragraph = Paragraph::new(progress_lines).block(
Block::default()
.borders(Borders::ALL)
.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));
if text.to_lowercase().contains("error") {
let line = line.style(Style::default().fg(Color::Red));
output_lines.push(line);
} else if text.to_lowercase().contains("success") {
let line = line.style(Style::default().fg(Color::Green));
output_lines.push(line);
} else {
output_lines.push(line);
}
});
let progress_paragraph = Paragraph::new(progress_lines).block(
let text_area_height = top_chunks[1].height.saturating_sub(2) as usize;
let max_scroll = state.output.len().saturating_sub(text_area_height);
if state.output_follow {
state.output_scroll = max_scroll as u16;
}
state.output_scroll = state.output_scroll.min(max_scroll as u16);
if state.output_scroll == u16::MAX {
if state.output.len() > text_area_height {
state.output_scroll = (state.output.len() - text_area_height) as u16;
} else {
state.output_scroll = 0;
}
}
let output_paragraph = Paragraph::new(output_lines)
.block(
Block::default()
.borders(Borders::ALL)
.title(" Script Engine Output "),
)
.scroll((state.output_scroll, 0));
f.render_widget(output_paragraph, top_chunks[1]);
let input_paragraph = Paragraph::new(state.curent_intput.as_str()).block(
Block::default()
.borders(Borders::ALL)
.title("Operations in progress"),
.title(" What is thy bidding, my master? "),
);
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));
if text.to_lowercase().contains("error") {
let line = line.style(Style::default().fg(Color::Red));
output_lines.push(line);
} else if text.to_lowercase().contains("success") {
let line = line.style(Style::default().fg(Color::Green));
output_lines.push(line);
} else {
output_lines.push(line);
}
});
let text_area_height = top_chunks[1].height.saturating_sub(2) as usize;
let max_scroll = state.output.len().saturating_sub(text_area_height);
if state.output_follow {
state.output_scroll = max_scroll as u16;
}
state.output_scroll = state.output_scroll.min(max_scroll as u16);
if state.output_scroll == u16::MAX {
if state.output.len() > text_area_height {
state.output_scroll = (state.output.len() - text_area_height) as u16;
} else {
state.output_scroll = 0;
}
}
let output_paragraph = Paragraph::new(output_lines)
.block(
Block::default()
.borders(Borders::ALL)
.title(" Script Engine Output "),
)
.scroll((state.output_scroll, 0));
f.render_widget(output_paragraph, top_chunks[1]);
let input_paragraph = Paragraph::new(state.curent_intput.as_str()).block(
Block::default()
.borders(Borders::ALL)
.title(" What is thy bidding, my master? "),
);
f.render_widget(input_paragraph, main_chunks[1]);
})?;
f.render_widget(input_paragraph, main_chunks[1]);
})?;
}
if let Ok(event) = event_rx.recv_timeout(Duration::from_millis(100)) {
match event {
AppEvent::Worker(msg) => match msg {
@@ -697,134 +747,152 @@ pub fn run_tui(
break;
}
KeyCode::Enter => {
let trimmed = state.curent_intput.trim().to_string();
if !trimmed.is_empty() {
state.history.push(trimmed.clone());
let _ = state
.main_tx
.send(ToolMessage::Output((0, "\n".to_string())));
let _ = state.main_tx.send(ToolMessage::Output((
0,
format!("[user input] > {}", trimmed),
)));
let _ = state
.main_tx
.send(ToolMessage::Output((0, "\n".to_string())));
let prompt = state.prompt.clone();
if prompt.action.is_some() {
state.prompt.responses.push(PromptResponse {
query: prompt.last_prompted.clone().unwrap(),
response: trimmed.clone(),
});
if state.prompt.responses.len() == state.prompt.num_responses {
state.execute_command(
prompt.execute_command.as_str(),
None,
0,
)?;
let _ = state.main_tx.send(ToolMessage::Output((
0,
format!("{}", prompt.execute_command),
)));
} else {
state.execute_command("prompt", None, 0)?;
}
} else {
let (command, args) =
trimmed.split_once(' ').unwrap_or((&trimmed, ""));
match command {
"exit" | "quit" => break,
"reload-modules" => {
state.initialize_modules();
if state.selecting_host {
state.selecting_host = false;
} else {
let trimmed = state.curent_intput.trim().to_string();
if !trimmed.is_empty() {
state.history.push(trimmed.clone());
let _ = state
.main_tx
.send(ToolMessage::Output((0, "\n".to_string())));
let _ = state.main_tx.send(ToolMessage::Output((
0,
format!("[user input] > {}", trimmed),
)));
let _ = state
.main_tx
.send(ToolMessage::Output((0, "\n".to_string())));
let prompt = state.prompt.clone();
if prompt.action.is_some() {
state.prompt.responses.push(PromptResponse {
query: prompt.last_prompted.clone().unwrap(),
response: trimmed.clone(),
});
if state.prompt.responses.len()
== state.prompt.num_responses
{
state.execute_command(
prompt.execute_command.as_str(),
None,
0,
)?;
let _ = state.main_tx.send(ToolMessage::Output((
0,
"Reloading Module Paths...".to_string(),
format!("{}", prompt.execute_command),
)));
} else {
state.execute_command("prompt", None, 0)?;
}
"help" => {
let help_text = state.help.clone().join("\n");
for line in help_text.lines() {
} else {
let (command, args) =
trimmed.split_once(' ').unwrap_or((&trimmed, ""));
match command {
"exit" | "quit" => break,
"reload-modules" => {
state.initialize_modules();
let _ = state.main_tx.send(ToolMessage::Output((
0,
line.to_string(),
"Reloading Module Paths...".to_string(),
)));
}
}
"new_project" | "np" => {
if args.split_once(' ').is_some() {
let _ = state.execute_command(
command,
Some(args.to_string()),
0,
);
} else {
state.output.push(
"[error]: USAGE -> np <org> <name>".into(),
);
"help" => {
let help_text = state.help.clone().join("\n");
for line in help_text.lines() {
let _ = state.main_tx.send(
ToolMessage::Output((0, line.to_string())),
);
}
}
}
"switch_config" => {
switch_config = true;
state.curent_intput.clear();
break;
}
command_name => {
if state.module_loader.commands.contains_key(command) {
let _ = state.main_tx.send(ToolMessage::Output((
0,
format!(
"[Worker] Executing script {}...",
command_name
),
)));
if let Err(e) = state.execute_command(
command,
Some(args.to_string()),
0,
) {
"new_project" | "np" => {
if args.split_once(' ').is_some() {
let _ = state.execute_command(
command,
Some(args.to_string()),
0,
);
} else {
state.output.push(
"[error]: USAGE -> np <org> <name>".into(),
);
}
}
"switch_config" => {
switch_config = true;
state.curent_intput.clear();
break;
}
"select_host" => {
state.selecting_host = true;
}
command_name => {
if state
.module_loader
.commands
.contains_key(command)
{
let _ =
state.main_tx.send(ToolMessage::Output((
0,
format!("[error] Pipeline faile: {e}"),
format!(
"[Worker] Executing script {}...",
command_name
),
)));
}
} else {
if args == "" {
match state.execute_command(command, None, 0) {
Ok(_) => {}
Err(e) => {
let _ = state.main_tx.send(
ToolMessage::Output((
0,
e.to_string(),
)),
);
}
}
} else {
match state.execute_command(
if let Err(e) = state.execute_command(
command,
Some(args.to_string()),
0,
) {
Ok(_) => {}
Err(e) => {
let _ = state.main_tx.send(
ToolMessage::Output((
0,
e.to_string(),
)),
);
let _ = state.main_tx.send(
ToolMessage::Output((
0,
format!(
"[error] Pipeline faile: {e}"
),
)),
);
}
} else {
if args == "" {
match state
.execute_command(command, None, 0)
{
Ok(_) => {}
Err(e) => {
let _ = state.main_tx.send(
ToolMessage::Output((
0,
e.to_string(),
)),
);
}
}
} else {
match state.execute_command(
command,
Some(args.to_string()),
0,
) {
Ok(_) => {}
Err(e) => {
let _ = state.main_tx.send(
ToolMessage::Output((
0,
e.to_string(),
)),
);
}
}
}
}
}
}
}
state.curent_intput.clear();
}
state.curent_intput.clear();
history_index = state.history.len();
}
history_index = state.history.len();
}
KeyCode::Char(c) => {
state.curent_intput.push(c);
@@ -833,18 +901,40 @@ pub fn run_tui(
state.curent_intput.pop();
}
KeyCode::Up if key.modifiers.is_empty() => {
if !state.history.is_empty() && history_index > 0 {
history_index -= 1;
state.curent_intput = state.history[history_index].clone();
if state.selecting_host {
if let Some(selected) = host_list_state.selected() {
if selected > 0 {
let new_index = selected - 1;
host_list_state.select(Some(new_index));
state.selected_host = new_index;
}
}
} else {
if !state.history.is_empty() && history_index > 0 {
history_index -= 1;
state.curent_intput = state.history[history_index].clone();
}
}
}
KeyCode::Down if key.modifiers.is_empty() => {
if history_index < state.history.len() {
history_index += 1;
if history_index == state.history.len() {
state.curent_intput.clear(); // Clear back to a fresh prompt
} else {
state.curent_intput = state.history[history_index].clone();
if state.selecting_host {
if let Some(selected) = host_list_state.selected() {
if selected + 1
< state.projects[state.selected_project].hosts.len()
{
let new_index = selected + 1;
host_list_state.select(Some(new_index));
state.selected_host = new_index;
}
}
} else {
if history_index < state.history.len() {
history_index += 1;
if history_index == state.history.len() {
state.curent_intput.clear(); // Clear back to a fresh prompt
} else {
state.curent_intput = state.history[history_index].clone();
}
}
}
}