From a84cc48136d7a1de4bd01fc91925a42da1e944f3 Mon Sep 17 00:00:00 2001 From: Melvin_Petiit Date: Wed, 27 May 2026 20:03:53 +0200 Subject: [PATCH 1/7] chore: initial commit of existing codebase --- .gitattributes | 6 + .gitignore | 63 + LICENSE | 21 + README.md | 106 ++ bull.sh | 1717 +++++++++++++++++++++++++ configs/Vagrantfile.template | 104 ++ configs/ansible-playbook.yml | 133 ++ configs/kali-provision.sh | 621 +++++++++ configs/parrot-playbook.yml | 564 +++++++++ configs/parrot-provision.sh | 540 ++++++++ lib/core.sh | 1467 ++++++++++++++++++++++ lib/inventory.sh | 401 ++++++ lib/toolkits.sh | 356 ++++++ lib/vagrant.sh | 2294 ++++++++++++++++++++++++++++++++++ lib/vpn.sh | 390 ++++++ 15 files changed, 8783 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 bull.sh create mode 100644 configs/Vagrantfile.template create mode 100644 configs/ansible-playbook.yml create mode 100644 configs/kali-provision.sh create mode 100644 configs/parrot-playbook.yml create mode 100644 configs/parrot-provision.sh create mode 100644 lib/core.sh create mode 100644 lib/inventory.sh create mode 100644 lib/toolkits.sh create mode 100644 lib/vagrant.sh create mode 100644 lib/vpn.sh diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..846f57b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +# Force LF line endings for all shell scripts — CRLF breaks bash (set -o pipefail) +*.sh text eol=lf +*.conf text eol=lf +*.json text eol=lf +*.md text eol=lf +*.rb text eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..869207e --- /dev/null +++ b/.gitignore @@ -0,0 +1,63 @@ +# BULL runtime data +.bull/ +*.bull/ + +# Vagrant +.vagrant/ +*.vagrant/ + +# VM-specific files +vms/ +!vms/.gitkeep + +# Toolkit registry (user-specific) +toolkits.json + +# Inventory (user-specific) +data/inventory.json + +# Credentials (security-sensitive) +.credentials +.credentials.gpg +.gnupg/ +.ecryptfs/ + +# VPN configs (security-sensitive) +*.ovpn +*.conf + +# Logs +*.log + +# OS files +.DS_Store +Thumbs.db +*.swp +*.swo +*~ +.FUSE* +.Directory + +# IDE/Editor +.idea/ +.vscode/ +*.sublime-* +*.code-workspace +*.tmproj +.project +.settings/ + +# Shellcheck reports +shellcheck-report.* + +# Temporary files +*.tmp +*.bak +*.backup + +# WSL shim binaries +.shim/ + +# Project documentation +docs/ +!docs/.gitkeep \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..63a54fc --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 BULL - Pentest Environment Toolkit + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f070d9e --- /dev/null +++ b/README.md @@ -0,0 +1,106 @@ +# Your Pentest VM Toolkit + +Launch a fully-equipped pentest VM in seconds with pre-installed security tools, VPN protection, and encrypted storage. + +ScreenBull + +## Features + +- **Auto-provisioning** — Kali/Parrot ready to use +- **VPN kill switch** — Blocks traffic if VPN drops +- **Encrypted /home** — User data protected with ecryptfs +- **GPG credentials** — Passwords encrypted with AES256 (65M iterations) +- **Snapshot support** — Rollback before risky operations +- **Install and manage security tools** — Pre-installed toolkit with many pentest utilities, easily add more via the manager + +## Requirements + +- Linux with libvirt or VirtualBox +- Vagrant 2.3+, jq, sudo + +WSL2 supported via libvirt. + +## Quick Start + +```bash +# Run as root +sudo ./bull.sh init + +# Interactive menu +sudo ./bull.sh + +# Or CLI +sudo ./bull.sh create my-vm --os kali +``` + +## ⚠️ Important: First Run + +> **⚠️ First installation can take 10-15 minutes** depending on your internet speed. This includes: +> - Downloading and installing Vagrant (if not present) +> - Downloading the Vagrant box (~2-4 GB for Kali/Parrot) +> - Installing libvirt/VirtualBox dependencies +> - Installing the vagrant-libvirt plugin + +Following dependencies may be installed: + +- **Vagrant** — VM provisioning from HashiCorp +- **VirtualBox** — Oracle VM hypervisor (optional) +- **libvirt** — KVM/QEMU virtualization +- **qemu-kvm** — KVM kernel modules +- **OVMF** — UEFI firmware for secure boot +- **vagrant-libvirt** — Vagrant plugin for KVM +- **jq** — JSON processing +- **spice-vdagent** — Clipboard/resolution for SPICE +- **xdotool** — X11 automation + +## Storage + +- **Hypervisor**: VirtualBox (`~/VirtualBox VMs/`) or libvirt (`/var/lib/libvirt/images/`) +- **BULL data**: `~/.bull/` (inventory, credentials, GPG keys, toolkit registry) + +### ⚠️ WSL2 — Disk space reclamation + +WSL2 stores its entire Linux filesystem in a `.vhdx` file that **grows automatically but never shrinks on its own**. After destroying VMs, the disk space is freed inside WSL2 but Windows still sees the `.vhdx` at its peak size. + +To reclaim the space on the Windows side: + +```powershell +# 1. Shut down WSL2 +wsl --shutdown + +# 2. Compact the virtual disk (run as Administrator in PowerShell) +$vhdx = (Get-ChildItem "$env:LOCALAPPDATA\Packages\*\LocalState\ext4.vhdx" -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1).FullName +Optimize-VHD -Path $vhdx -Mode Full +``` + +> If `Optimize-VHD` is not available (requires Hyper-V), use `diskpart` instead: +> ``` +> diskpart +> select vdisk file="C:\Users\\AppData\Local\Packages\\LocalState\ext4.vhdx" +> compact vdisk +> exit +> ``` + +## Security + +- Scripts require root with 700 permissions +- Passwords never stored in plaintext +- Credentials: GPG + AES256 + SHA512 (65M iterations) +- /home encrypted with ecryptfs +- Default OS accounts locked after creation +- VPN kill switch blocks non-VPN traffic +- Synced folders disabled + +> **Note**: Technical measures only. Does not protect against user error or social engineering. + +## Commands + +```bash +sudo ./bull.sh help # Run as root +bull help # After init +``` + + +## Licence + +[**MIT**](https://github.com/WhiteMuush/Bull/blob/master/LICENSE) diff --git a/bull.sh b/bull.sh new file mode 100644 index 0000000..01f1047 --- /dev/null +++ b/bull.sh @@ -0,0 +1,1717 @@ +#!/usr/bin/env bash +# ============================================================================= +# BULL - Automated Pentest Environment Provisioning Toolkit +# Version: 1.0.0 +# +# Creates, manages, and configures pentest VMs (Kali/Parrot) via Vagrant +# with VPN kill switch, snapshot management, and inventory tracking. +# ============================================================================= +set -euo pipefail +IFS=$'\n\t' + +# Require root +if [[ "${EUID:-0}" -ne 0 ]]; then + echo "Error: BULL must be run as root (sudo)." + echo "Usage: sudo ./bull.sh " + exit 1 +fi + +# Resolve script directory (works with symlinks) +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +readonly SCRIPT_DIR + +# Source libraries (order matters: core first) +source "${SCRIPT_DIR}/lib/core.sh" +source "${SCRIPT_DIR}/lib/inventory.sh" +source "${SCRIPT_DIR}/lib/vagrant.sh" +source "${SCRIPT_DIR}/lib/vpn.sh" +source "${SCRIPT_DIR}/lib/toolkits.sh" + +# ============================================================================= +# ASCII ART BANNER (left side) +# ============================================================================= +ASCII_ART=$(cat <<'ASCII' +⠀⠀⠀⠀⠀⠀⠀⢀⣠⣤⣴⣶⣶⡦⠤⠤⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⢰⣿⣛⣉⣉⣉⣩⣭⣥⣤⣤⣤⡤⢀⡀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠈⠀⠉⠉⠁⠀⠀⠀⠀⠀⠀⠈⠉⢢⠆⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠤⠤⠤⠄⢀⣀⣀⣀⡘⡄⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠐⠁⠀⠀⠀⠀⡀⠀⠀⢴⣶⣧⡀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⡠⠊⠀⠀⠀⠀⠀⠀⠀⠹⡄⠀⠨⣿⣿⣷⡄⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⡸⠁⠀⠀⠀⠀⠀⠀⢰⠀⠀⠙⣤⣶⣿⣿⣿⣿⡄⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⡐⠁⠀⠀⠀⠀⠀⡠⣴⠾⣷⡆⠀⢿⣿⣿⣿⣿⣿⣧⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣧⣴⡄⢻⣿⣿⣿⣿⣿⠀⠀⠀⠀ +⠀⠀⠀⠀⢸⠀⠀⢠⠀⠀⠀⠀⠀⠀⠈⠉⢉⠿⢿⣆⢿⣿⣿⣿⣿⡀⠀⠀⠀ +⠀⠀⠀⠀⠎⠀⠀⣿⡄⠀⠀⠀⠀⠀⠀⠘⠋⢛⣟⠛⠃⠙⠻⠿⣿⡇⠀⠀⠀ +⠀⠀⠀⢸⡄⠀⠀⡘⠋⠉⡀⢠⣾⡰⢶⣶⡖⠁⣤⣳⣿⣶⢶⣶⡌⠳⠤⣀⣀ +⠀⠀⠀⢸⢠⠀⢀⣿⣿⣶⣿⣿⣿⠇⠀⠁⣷⣄⣈⣙⣛⣿⣿⣿⡲⡒⠒⠒⠊ +⠀⠀⠀⠀⣿⣾⣿⣿⣿⣿⣿⣿⡟⠀⠀⣰⣿⣿⣿⣿⣿⣿⣿⣟⣿⣶⡄⠀⠀ +⠀⠀⠀⢠⣿⣿⣿⣿⣿⣿⣿⣿⣇⠀⠐⣿⠿⣿⣿⣿⣿⡿⠋⠀⠙⣿⡇⠀⠀ +⠀⠀⠀⣿⣿⡿⠁⠸⣿⣿⣿⣿⣿⣦⠸⠋⢸⣿⣿⣿⡿⠁⠀⠀⠀⢸⣷⡀⠀ +⠀⠀⠀⣻⣿⡇⠀⠀⠀⣹⣿⡿⢻⣿⢠⡀⠸⣿⣿⣿⣧⠀⠀⠀⠀⠘⣿⣧⠀ +⠀⠀⢠⠉⣿⠇⠀⠀⢰⠋⣿⣰⣁⡟⠀⠁⢼⣿⡿⠿⠏⠀⠀⠀⠀⠀⠋⠟⠀ +⠀⠀⢰⣿⠋⠀⠀⠀⢀⣿⡏⠛⠐⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⣾⡇⠀⠀⠀⢀⠎⢹⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠜⢹⡇⠀⠀⠀⠾⣶⡾⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠮⣿⠿⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +ASCII +) +readonly ASCII_ART + +# OSC 8 hyperlink for creator +readonly CREATOR_LINK=$'\e]8;;https://github.com/WhiteMuush\aMelvin PETIT\e]8;;\a' + +# ============================================================================= +# MENU GENERATION +# Builds the right-hand column displayed next to the ASCII art. +# Each line MUST be a separate array element for side-by-side alignment. +# ============================================================================= +generate_menu() { + local -a menu_lines=( + "" + "${BRIGHT_RED}${BOLD}__________ ____ ___ .____ .____ ${RESET}" + "${BRIGHT_RED}${BOLD}\\______ \\| | \\| | | | ${RESET}" + "${BRIGHT_RED}${BOLD} | | _/| | /| | | | ${RESET}" + "${BRIGHT_RED}${BOLD} | | \\| | / | |___ | |___ ${RESET}" + "${BRIGHT_RED}${BOLD} |______ /|______/ |_______ \\|_______ \\ ${RESET}" + "${BRIGHT_RED}${BOLD} \\/ \\/ \\/ ${RESET}" + "" + "${BRIGHT_MAGENTA} Creator: ${CREATOR_LINK} ${RESET}${RED}v${BULL_VERSION}${RESET}" + "${RESET}" + " ${CYAN}Manage your Pentest VMs like you always wanted${RESET} 🔥" + "${BRIGHT_CYAN} ┌───────────────────────────────────────────────┐${RESET}" + "${BRIGHT_CYAN} │ │" + "${BRIGHT_CYAN} │${RESET} ${BRIGHT_RED}[1]${RESET} Create VM ${BRIGHT_RED}[8]${RESET} Snapshot ${BRIGHT_CYAN}│${RESET}" + "${BRIGHT_CYAN} │${RESET} ${BRIGHT_RED}[2]${RESET} List VMs ${BRIGHT_RED}[9]${RESET} Restore ${BRIGHT_CYAN}│${RESET}" + "${BRIGHT_CYAN} │${RESET} ${BRIGHT_RED}[3]${RESET} Start VM ${BRIGHT_RED}[10]${RESET} VPN Config ${BRIGHT_CYAN}│${RESET}" + "${BRIGHT_CYAN} │${RESET} ${BRIGHT_RED}[4]${RESET} Stop VM ${BRIGHT_RED}[11]${RESET} Tools Manager ${BRIGHT_CYAN}│${RESET}" + "${BRIGHT_CYAN} │${RESET} ${BRIGHT_RED}[5]${RESET} Destroy VM ${BRIGHT_RED}[12]${RESET} Status ${BRIGHT_CYAN}│${RESET}" + "${BRIGHT_CYAN} │${RESET} ${BRIGHT_RED}[6]${RESET} View VM (GUI) ${BRIGHT_RED}[13]${RESET} Sync Inventory ${BRIGHT_CYAN}│${RESET}" + "${BRIGHT_CYAN} │${RESET} ${BRIGHT_RED}[7]${RESET} Connect (SSH) ${BRIGHT_RED}[14]${RESET} Init / Check Deps ${BRIGHT_CYAN}│${RESET}" + "${BRIGHT_CYAN} │ │" + "${BRIGHT_CYAN} ├───────────────────────────────────────────────┘${RESET}" + "${BRIGHT_CYAN} │${RESET} ${BRIGHT_RED}[0]${RESET} Exit ${BRIGHT_CYAN}│${RESET}" + "${BRIGHT_CYAN} └────────────────────┘${RESET}" + "" + ) + + printf '%s\n' "${menu_lines[@]}" +} + +# ============================================================================= +# TITLE SCREEN (centered splash) +# ============================================================================= +display_title_middle_screen() { + local cols rows + cols=$(tput cols 2>/dev/null || echo 80) + rows=$(tput lines 2>/dev/null || echo 24) + + local -a lines=( + "${BOLD}${BRIGHT_RED}__________ ____ ___ .____ .____ ${RESET}" + "${BOLD}${BRIGHT_RED}\\______ \\| | \\| | | | ${RESET}" + "${BOLD}${BRIGHT_RED} | | _/| | /| | | | ${RESET}" + "${BOLD}${BRIGHT_RED} | | \\| | / | |___ | |___ ${RESET}" + "${BOLD}${BRIGHT_RED} |______ /|______/ |_______ \\|_______ \\ ${RESET}" + "${BOLD}${BRIGHT_RED} \\/ \\/ \\/ ${RESET}" + "" + "${CYAN}Automated Pentest Environment Provisioning${RESET}" + "" + "${CYAN}BY MELVIN PETIT${RESET}" + ) + local h=${#lines[@]} + + # Strip ANSI escapes to measure visible width + strip_esc() { + sed -E 's/\x1B\[[0-9;?]*[ -/]*[@-~]//g; s/\x1B\][^\a]*\a//g' + } + + local max_w=0 raw visible_len line + for line in "${lines[@]}"; do + raw=$(printf "%s" "$line" | strip_esc) + visible_len=${#raw} + (( visible_len > max_w )) && max_w=$visible_len + done + + local top=$(( (rows - h) / 2 )) + (( top < 0 )) && top=0 + local left=$(( (cols - max_w) / 2 )) + (( left < 0 )) && left=0 + + printf "\033c" + for ((i=0; i menu_count ? ascii_count : menu_count )) + + local max_ascii_width=0 + local _raw _w + for _raw in "${ascii_lines[@]}"; do + _raw=$(printf "%s" "${_raw}" | sed -E 's/\x1B\[[0-9;?]*[ -/]*[@-~]//g') + _w=${#_raw} + (( _w > max_ascii_width )) && max_ascii_width=${_w} + done + + local spacing=" " + local i + for (( i=0; i step_count ? ascii_count : step_count )) + + local max_ascii_width=0 + for line in "${ascii_lines[@]}"; do + local raw + raw=$(printf "%s" "${line}" | sed -E 's/\x1B\[[0-9;?]*[ -/]*[@-~]//g') + local w=${#raw} + (( w > max_ascii_width )) && max_ascii_width=$w + done + + local spacing=" " + for (( i=0; i/dev/null || echo 0) + + if [[ "${tk_count}" -eq 0 ]]; then + lines+=("${BRIGHT_CYAN} │${RESET} ${DIM}No tools registered. ${RESET}${BRIGHT_CYAN}│${RESET}") + else + while IFS=$'\t' read -r tk_name tk_url; do + # No numbering — name (24 chars) + URL hint (remaining) + local short_url="${tk_url#https://}" + short_url="${short_url#http://}" + short_url="${short_url%.git}" + + local v_name=${#tk_name} + local name_pad=$(( 24 - v_name < 0 ? 0 : 24 - v_name )) + # visible fixed: 2 + 24 + 2 = 28 ; remaining for url = BOX_W - 28 - 2 = 24 + [[ ${#short_url} -gt 24 ]] && short_url="${short_url:0:21}..." + + local v_url=${#short_url} + local trailing=$(( BOX_W - 28 - v_url )) + (( trailing < 1 )) && trailing=1 + + local name_pad_str trailing_str + printf -v name_pad_str "%${name_pad}s" "" + printf -v trailing_str "%${trailing}s" "" + + lines+=("${BRIGHT_CYAN} │${RESET} ${BOLD}${tk_name}${RESET}${name_pad_str} ${DIM}${short_url}${trailing_str}${RESET}${BRIGHT_CYAN}│${RESET}") + done < <(jq -r '.toolkits[] | [.name, .url] | @tsv' "${TOOLKITS_REGISTRY}") + fi + + lines+=( + "${BRIGHT_CYAN} │ │${RESET}" + "${BRIGHT_CYAN} └──────────────────────────────────────────────────────┘${RESET}" + "" + ) + + printf '%s\n' "${lines[@]}" +} + +# Generate a NUMBERED toolkit list for the right-hand panel (used in Delete/Modify). +# Same box style as _generate_toolkit_manager_lines but with [N] prefixes. +_generate_numbered_toolkit_lines() { + local title="${1:-Select tool}" + local BOX_W=54 + + local title_pad=$(( BOX_W - 4 - ${#title} )) + (( title_pad < 1 )) && title_pad=1 + local title_pad_str + # shellcheck disable=SC2034 + printf -v title_pad_str "%${title_pad}s" "" + + local -a lines=( + "" + "${BRIGHT_RED}${BOLD}__________ ____ ___ .____ .____ ${RESET}" + "${BRIGHT_RED}${BOLD}\\______ \\| | \\| | | | ${RESET}" + "${BRIGHT_RED}${BOLD} | | _/| | /| | | | ${RESET}" + "${BRIGHT_RED}${BOLD} | | \\| | / | |___ | |___ ${RESET}" + "${BRIGHT_RED}${BOLD} |______ /|______/ |_______ \\|_______ \\ ${RESET}" + "${BRIGHT_RED}${BOLD} \\/ \\/ \\/ ${RESET}" + "" + "${BRIGHT_MAGENTA} Creator: ${CREATOR_LINK} ${RESET}${RED}v${BULL_VERSION}${RESET}" + "${RESET}" + " ${CYAN}${title}${RESET}" + "${BRIGHT_CYAN} ┌──────────────────────────────────────────────────────┐${RESET}" + "${BRIGHT_CYAN} │ │${RESET}" + ) + + _toolkit_registry_init + local tk_count + tk_count=$(jq '.toolkits | length' "${TOOLKITS_REGISTRY}" 2>/dev/null || echo 0) + + if [[ "${tk_count}" -eq 0 ]]; then + lines+=("${BRIGHT_CYAN} │${RESET} ${DIM}No tools registered. ${RESET}${BRIGHT_CYAN}│${RESET}") + else + local idx=1 + while IFS=$'\t' read -r tk_name tk_url; do + local short_url="${tk_url#https://}" + short_url="${short_url#http://}" + short_url="${short_url%.git}" + + local v_name=${#tk_name} + local name_pad=$(( 20 - v_name < 0 ? 0 : 20 - v_name )) + # visible fixed: [N]·· + 20 name + ·· = 26 ; remaining for url = BOX_W - 26 - 2 = 26 + [[ ${#short_url} -gt 20 ]] && short_url="${short_url:0:17}..." + + local v_url=${#short_url} + local trailing=$(( BOX_W - 28 - v_url )) + (( trailing < 1 )) && trailing=1 + + local name_pad_str trailing_str + printf -v name_pad_str "%${name_pad}s" "" + printf -v trailing_str "%${trailing}s" "" + + lines+=("${BRIGHT_CYAN} │${RESET} ${BRIGHT_RED}[${idx}]${RESET} ${BOLD}${tk_name}${RESET}${name_pad_str}${DIM}${short_url}${trailing_str} ${RESET}${BRIGHT_CYAN}│${RESET}") + (( idx++ )) + done < <(jq -r '.toolkits[] | [.name, .url] | @tsv' "${TOOLKITS_REGISTRY}") + fi + + lines+=( + "${BRIGHT_CYAN} │ │${RESET}" + "${BRIGHT_CYAN} └──────────────────────────────────────────────────────┘${RESET}" + "" + ) + + printf '%s\n' "${lines[@]}" +} + +# Render: ASCII art + NUMBERED toolkit list side-by-side (selection interface). +# Used for Delete and Modify so the panel IS the selector — no separate box below. +_display_toolkit_select() { + local title="${1:-Select tool}" + local -a ascii_lines menu_lines + IFS=$'\n' read -r -d '' -a ascii_lines <<< "${ASCII_ART}" || true + IFS=$'\n' read -r -d '' -a menu_lines <<< "$(_generate_numbered_toolkit_lines "${title}")" || true + + local ascii_count=${#ascii_lines[@]} + local menu_count=${#menu_lines[@]} + local max_lines=$(( ascii_count > menu_count ? ascii_count : menu_count )) + + local max_ascii_width=0 + for line in "${ascii_lines[@]}"; do + local raw + raw=$(printf "%s" "$line" | sed -E 's/\x1B\[[0-9;?]*[ -/]*[@-~]//g') + local w=${#raw} + (( w > max_ascii_width )) && max_ascii_width=$w + done + + local spacing=" " + for ((i = 0; i < max_lines; i++)); do + local ascii_line="${ascii_lines[i]:-}" + local menu_line="${menu_lines[i]:-}" + local raw_line + raw_line=$(printf "%s" "$ascii_line" | sed -E 's/\x1B\[[0-9;?]*[ -/]*[@-~]//g') + local pad=$(( max_ascii_width - ${#raw_line} )) + (( pad < 0 )) && pad=0 + printf " %s%s%*s%s%s\n" \ + "${BRIGHT_CYAN}" "${ascii_line}${RESET}" \ + "$pad" "" \ + "$spacing" \ + "$menu_line" + done + echo "" +} + +# Render: ASCII art + toolkit list side-by-side (no actions box). +# Used when there is no other numbered list below (e.g. Install, modify detail). +_display_toolkit_context() { + local -a ascii_lines menu_lines + IFS=$'\n' read -r -d '' -a ascii_lines <<< "${ASCII_ART}" || true + IFS=$'\n' read -r -d '' -a menu_lines <<< "$(_generate_toolkit_manager_lines)" || true + + local ascii_count=${#ascii_lines[@]} + local menu_count=${#menu_lines[@]} + local max_lines=$(( ascii_count > menu_count ? ascii_count : menu_count )) + + local max_ascii_width=0 + for line in "${ascii_lines[@]}"; do + local raw + raw=$(printf "%s" "$line" | sed -E 's/\x1B\[[0-9;?]*[ -/]*[@-~]//g') + local w=${#raw} + (( w > max_ascii_width )) && max_ascii_width=$w + done + + local spacing=" " + for ((i = 0; i < max_lines; i++)); do + local ascii_line="${ascii_lines[i]:-}" + local menu_line="${menu_lines[i]:-}" + local raw_line + raw_line=$(printf "%s" "$ascii_line" | sed -E 's/\x1B\[[0-9;?]*[ -/]*[@-~]//g') + local pad=$(( max_ascii_width - ${#raw_line} )) + (( pad < 0 )) && pad=0 + printf " %s%s%*s%s%s\n" \ + "${BRIGHT_CYAN}" "${ascii_line}${RESET}" \ + "$pad" "" \ + "$spacing" \ + "$menu_line" + done + echo "" +} + +# Render: ASCII art + toolkit list side-by-side, then actions sub-menu below. +_display_toolkit_manager() { + _display_toolkit_context + + echo -e "${BRIGHT_CYAN} ┌───────────────────────────────┐${RESET}" + echo -e "${BRIGHT_CYAN} │${RESET} ${BRIGHT_RED}[1]${RESET} Quick Install ${BRIGHT_CYAN}│${RESET}" + echo -e "${BRIGHT_CYAN} │${RESET} ${BRIGHT_RED}[2]${RESET} Library Install ${BRIGHT_CYAN}│${RESET}" + echo -e "${BRIGHT_CYAN} │${RESET} ${BRIGHT_RED}[3]${RESET} Save to Library ${BRIGHT_CYAN}│${RESET}" + echo -e "${BRIGHT_CYAN} │${RESET} ${BRIGHT_RED}[4]${RESET} Delete from Lib ${BRIGHT_CYAN}│${RESET}" + echo -e "${BRIGHT_CYAN} │${RESET} ${BRIGHT_RED}[5]${RESET} Update / Modify ${BRIGHT_CYAN}│${RESET}" + echo -e "${BRIGHT_CYAN} │${RESET} ${BRIGHT_RED}[6]${RESET} Update Tools on all VMs ${BRIGHT_CYAN}│${RESET}" + echo -e "${BRIGHT_CYAN} ├───────────────────────────────┘${RESET}" + echo -e "${BRIGHT_CYAN} │${RESET} ${BRIGHT_RED}[0]${RESET} Exit ${BRIGHT_CYAN}│${RESET}" + echo -e "${BRIGHT_CYAN} └────────────────────┘${RESET}" + echo "" +} + +# Clear screen and render ASCII art + VM selection box side-by-side. +_display_banner_with_vm_select() { + local title="$1" + shift + local -a pairs=("$@") + + local -a ascii_lines menu_lines + IFS=$'\n' read -r -d '' -a ascii_lines <<< "${ASCII_ART}" || true + IFS=$'\n' read -r -d '' -a menu_lines <<< "$(_generate_vm_select_lines "${title}" "${pairs[@]}")" || true + + local ascii_count=${#ascii_lines[@]} + local menu_count=${#menu_lines[@]} + local max_lines=$(( ascii_count > menu_count ? ascii_count : menu_count )) + + local max_ascii_width=0 + for line in "${ascii_lines[@]}"; do + local raw + raw=$(printf "%s" "$line" | sed -E 's/\x1B\[[0-9;?]*[ -/]*[@-~]//g') + local w=${#raw} + (( w > max_ascii_width )) && max_ascii_width=$w + done + + local spacing=" " + for ((i = 0; i < max_lines; i++)); do + local ascii_line="${ascii_lines[i]:-}" + local menu_line="${menu_lines[i]:-}" + local raw_line + raw_line=$(printf "%s" "$ascii_line" | sed -E 's/\x1B\[[0-9;?]*[ -/]*[@-~]//g') + local pad=$(( max_ascii_width - ${#raw_line} )) + (( pad < 0 )) && pad=0 + printf " %s%s%*s%s%s\n" \ + "${BRIGHT_CYAN}" "${ascii_line}${RESET}" \ + "$pad" "" \ + "$spacing" \ + "$menu_line" + done + echo "" +} + +# Display a numbered list of VMs (full-screen, same style as main menu) and +# prompt the user to pick one. Prints the selected VM name to stdout. +# Returns 1 if inventory is empty, user Exits ([0]), or selection is invalid. +select_vm_from_list() { + local prompt="${1:-Select VM}" + + inventory_init + + local vm_data + vm_data=$(jq -r '.vms[] | [.name, .status] | @tsv' "${INVENTORY_FILE}" 2>/dev/null) + + if [[ -z "${vm_data}" ]]; then + { + echo "" + echo -e " ${BRIGHT_RED}No VMs in inventory.${RESET}" + echo -e " ${DIM}Create one first with option [1].${RESET}" + echo "" + echo -ne " ${DIM}Press Enter to return to the menu...${RESET}" + read -r + } > /dev/tty + return 1 + fi + + # Build a flat interleaved array: name0 status0 name1 status1 … + local -a pairs + while IFS=$'\t' read -r _name _status; do + pairs+=("${_name}" "${_status}") + done <<< "${vm_data}" + + local vm_count=$(( ${#pairs[@]} / 2 )) + + # All display + input MUST go through /dev/tty: this function is called + # inside $() which captures stdout, so normal echo/read would be invisible. + { clear; _display_banner_with_vm_select "${prompt}" "${pairs[@]}"; } > /dev/tty + echo -ne " ${BOLD}${BRIGHT_CYAN}BULL > ${RESET}" > /dev/tty + + local choice + read -r choice < /dev/tty + + if [[ "${choice}" == "0" ]]; then + return 1 + fi + + if [[ "${choice}" =~ ^[0-9]+$ ]] && \ + [[ "${choice}" -ge 1 ]] && \ + [[ "${choice}" -le "${vm_count}" ]]; then + # Only the selected name goes to stdout (captured by the caller's $()) + echo "${pairs[$((( choice - 1 ) * 2))]}" + return 0 + fi + + echo -e "${BRIGHT_RED}Invalid selection.${RESET}" > /dev/tty + return 1 +} + +# ============================================================================= +# OS SELECTION PROMPT +# Prompts for the guest OS (Kali Linux or Parrot Security). +# Sets global: BULL_OS +# ============================================================================= +_prompt_os() { + BULL_OS="" + + clear + _display_step "Operating System" "Select the OS for the VM" + echo "" + + echo -e " ${BRIGHT_RED}[1]${RESET} Kali Linux ${DIM}(kalilinux/rolling)${RESET}" + echo -e " ${BRIGHT_RED}[2]${RESET} Parrot Security ${DIM}(parrotsec/rolling-security)${RESET}" + echo "" + read -rp "$(echo -e " ${BRIGHT_CYAN}Choice [1-2] (default: 1) > ${RESET}")" _os_choice + + case "${_os_choice:-1}" in + 1) BULL_OS="kali" ;; + 2) BULL_OS="parrot" ;; + *) BULL_OS="kali" ;; + esac + + echo -e "\n ${DIM}OS set to:${RESET} ${BOLD}${BULL_OS}${RESET}" + unset _os_choice + return 0 +} + +# ============================================================================= +# CREDENTIALS PROMPT +# Prompts for VM username + password (masked input). +# Sets globals: BULL_CRED_USER BULL_CRED_PASS +# ============================================================================= +_prompt_credentials() { + BULL_CRED_USER="" + BULL_CRED_PASS="" + + clear + _display_step "VM Credentials" "Set the login user for the VM" + echo "" + + local _default_user + _default_user="$(_os_default_user "${BULL_OS:-kali}")" + while true; do + read -rp "$(echo -e " ${BRIGHT_CYAN}Username (${_default_user}) > ${RESET}")" BULL_CRED_USER + BULL_CRED_USER="${BULL_CRED_USER:-${_default_user}}" + if [[ "${BULL_CRED_USER}" =~ ^[a-zA-Z][a-zA-Z0-9_-]{0,31}$ ]]; then + break + fi + echo -e " ${BRIGHT_RED}Invalid username.${RESET} Use letters, digits, hyphens, underscores (max 32 chars)." + done + echo "" + + echo -e " ${BRIGHT_RED}[G]${RESET} Generate a random strong password ${DIM}(recommended)${RESET}" + echo -e " ${BRIGHT_RED}[M]${RESET} Enter your own password" + echo "" + read -rp "$(echo -e " ${BRIGHT_CYAN}Choice [G/m] > ${RESET}")" _pw_mode + + if [[ "${_pw_mode:-g}" =~ ^[Mm]$ ]]; then + while true; do + echo -ne " ${BRIGHT_CYAN}Password > ${RESET}" + read -rs BULL_CRED_PASS + echo + if [[ ${#BULL_CRED_PASS} -lt 12 ]]; then + echo -e " ${BRIGHT_RED}Password must be at least 12 characters.${RESET}" + continue + fi + echo -ne " ${BRIGHT_CYAN}Confirm > ${RESET}" + local _confirm + read -rs _confirm + echo + if [[ "${BULL_CRED_PASS}" == "${_confirm}" ]]; then + _confirm="" + break + fi + echo -e " ${BRIGHT_RED}Passwords do not match.${RESET}" + done + else + BULL_CRED_PASS=$(openssl rand -base64 48 \ + | tr -dc 'A-Za-z0-9!@#%^*_-' \ + | head -c 24) + echo "" + echo -e " ${DIM}Generated password:${RESET} ${BOLD}${BRIGHT_GREEN}${BULL_CRED_PASS}${RESET}" + echo -e " ${DIM}(encrypted in .credentials.gpg — use 'bull show-pass ' to reveal)${RESET}" + fi + + unset _pw_mode + return 0 +} + +# ============================================================================= +# POST-CREATION: OFFER SAVED TOOLKITS +# After a VM is created, offer to install saved toolkits if the registry is +# non-empty. Silently skips if no toolkits are saved. +# ============================================================================= +_offer_registry_toolkits() { + local vm_name="$1" + local count + count=$(jq '.toolkits | length' "${TOOLKITS_REGISTRY}" 2>/dev/null || echo 0) + [[ "${count}" -eq 0 ]] && return 0 + + clear + _display_step "Toolkits" "You have ${count} toolkit(s) in your registry" + echo "" + echo -e " ${BRIGHT_RED}[1]${RESET} Install ALL saved toolkits now" + echo -e " ${BRIGHT_RED}[2]${RESET} Pick individual toolkits" + echo -e " ${BRIGHT_RED}[0]${RESET} Skip" + echo "" + read -rp "$(echo -e " ${BRIGHT_CYAN}Choice > ${RESET}")" tk_offer + + case "${tk_offer:-0}" in + 1) + install_registry_toolkits "${vm_name}" || true + ;; + 2) + list_toolkit_registry || return 0 + local tk_idx + read -rp "$(echo -e " ${BRIGHT_CYAN}Select [1-${#BULL_TOOLKIT_URLS[@]}] > ${RESET}")" tk_idx + if [[ "${tk_idx}" =~ ^[0-9]+$ ]] && \ + [[ "${tk_idx}" -ge 1 ]] && \ + [[ "${tk_idx}" -le "${#BULL_TOOLKIT_URLS[@]}" ]]; then + install_toolkit "${vm_name}" "${BULL_TOOLKIT_URLS[$((tk_idx-1))]}" || true + fi + ;; + 0|"") + log_info "Skipping toolkit installation." + ;; + *) + echo -e "\n ${BRIGHT_RED}Invalid choice.${RESET}" + ;; + esac + unset tk_offer +} + +# ============================================================================= +# KEYBOARD LAYOUT PROMPT +# Prompts the user to pick a keyboard layout for the VM. +# Sets global: BULL_KB_LAYOUT +# ============================================================================= +_prompt_keyboard() { + BULL_KB_LAYOUT="" + + clear + _display_step "Keyboard Layout" "Select the keyboard layout for the VM" + echo "" + + echo -e " ${BRIGHT_RED}[1]${RESET} us — English US ${DIM}(QWERTY)${RESET}" + echo -e " ${BRIGHT_RED}[2]${RESET} fr — French ${DIM}(AZERTY)${RESET}" + echo -e " ${BRIGHT_RED}[3]${RESET} de — German ${DIM}(QWERTZ)${RESET}" + echo -e " ${BRIGHT_RED}[4]${RESET} gb — English UK ${DIM}(QWERTY)${RESET}" + echo -e " ${BRIGHT_RED}[5]${RESET} es — Spanish ${DIM}(QWERTY)${RESET}" + echo -e " ${BRIGHT_RED}[6]${RESET} it — Italian ${DIM}(QWERTY)${RESET}" + echo -e " ${BRIGHT_RED}[7]${RESET} be — Belgian ${DIM}(AZERTY)${RESET}" + echo -e " ${BRIGHT_RED}[8]${RESET} ch — Swiss ${DIM}(QWERTZ)${RESET}" + echo -e " ${BRIGHT_RED}[9]${RESET} pt — Portuguese ${DIM}(QWERTY)${RESET}" + echo "" + read -rp "$(echo -e " ${BRIGHT_CYAN}Choice [1-9] (default: 1 = us) > ${RESET}")" _kb_choice + + case "${_kb_choice:-1}" in + 1) BULL_KB_LAYOUT="us" ;; + 2) BULL_KB_LAYOUT="fr" ;; + 3) BULL_KB_LAYOUT="de" ;; + 4) BULL_KB_LAYOUT="gb" ;; + 5) BULL_KB_LAYOUT="es" ;; + 6) BULL_KB_LAYOUT="it" ;; + 7) BULL_KB_LAYOUT="be" ;; + 8) BULL_KB_LAYOUT="ch" ;; + 9) BULL_KB_LAYOUT="pt" ;; + *) BULL_KB_LAYOUT="us" ;; + esac + + echo -e "\n ${DIM}Keyboard set to:${RESET} ${BOLD}${BULL_KB_LAYOUT}${RESET}" + unset _kb_choice + return 0 +} + +# ============================================================================= +# RESOLUTION PROMPT +# Prompts the user to pick a display resolution for the VM. +# Sets global: BULL_RESOLUTION +# ============================================================================= +_prompt_resolution() { + BULL_RESOLUTION="" + + clear + _display_step "Display Resolution" "Select the screen resolution" + echo "" + + echo -e " ${BRIGHT_RED}[1]${RESET} 1920x1080 ${DIM}(Full HD — recommended)${RESET}" + echo -e " ${BRIGHT_RED}[2]${RESET} 2560x1440 ${DIM}(2K/QHD)${RESET}" + echo -e " ${BRIGHT_RED}[3]${RESET} 3840x2160 ${DIM}(4K/UHD)${RESET}" + echo -e " ${BRIGHT_RED}[4]${RESET} 1280x720 ${DIM}(HD)${RESET}" + echo -e " ${BRIGHT_RED}[5]${RESET} 1366x768 ${DIM}(Standard laptop)${RESET}" + echo -e " ${BRIGHT_RED}[6]${RESET} 1600x900 ${DIM}(HD+)${RESET}" + echo -e " ${BRIGHT_RED}[7]${RESET} Custom ${DIM}(enter manually)${RESET}" + echo "" + read -rp "$(echo -e " ${BRIGHT_CYAN}Choice [1-7] (default: 1) > ${RESET}")" _res_choice + + case "${_res_choice:-1}" in + 1) BULL_RESOLUTION="1920x1080" ;; + 2) BULL_RESOLUTION="2560x1440" ;; + 3) BULL_RESOLUTION="3840x2160" ;; + 4) BULL_RESOLUTION="1280x720" ;; + 5) BULL_RESOLUTION="1366x768" ;; + 6) BULL_RESOLUTION="1600x900" ;; + 7) + echo "" + read -rp "$(echo -e " ${BRIGHT_CYAN}Width (e.g. 1920) > ${RESET}")" _custom_w + read -rp "$(echo -e " ${BRIGHT_CYAN}Height (e.g. 1080) > ${RESET}")" _custom_h + if [[ -n "${_custom_w}" && -n "${_custom_h}" ]]; then + BULL_RESOLUTION="${_custom_w}x${_custom_h}" + else + BULL_RESOLUTION="1920x1080" + fi + ;; + *) BULL_RESOLUTION="1920x1080" ;; + esac + + echo -e "\n ${DIM}Resolution set to:${RESET} ${BOLD}${BULL_RESOLUTION}${RESET}" + unset _res_choice _custom_w _custom_h + return 0 +} + +# ============================================================================= +# INTERACTIVE MENU HANDLER +# ============================================================================= +handle_menu_choice() { + local choice="$1" + + case "$choice" in + 1) + clear + _display_step "Create VM" "Choose between quick or custom setup" + echo "" + echo -e " ${BRIGHT_RED}[1]${RESET} Quick create ${DIM}(${DEFAULT_RAM}MB RAM, ${DEFAULT_CPU} CPUs, auto name)${RESET}" + echo -e " ${BRIGHT_RED}[2]${RESET} Custom create ${DIM}(name, RAM, CPU, resolution)${RESET}" + echo -e " ${BRIGHT_RED}[0]${RESET} Back to menu" + echo "" + read -rp "$(echo -e " ${BRIGHT_CYAN}Choice > ${RESET}")" create_choice + + case "$create_choice" in + 1) + _prompt_os + local auto_name + auto_name="${BULL_OS}-$(date +%s | tail -c 5)" + _prompt_credentials || return + _prompt_keyboard + _prompt_resolution + clear + _display_step "VM Creation" "Setting up your VM" + echo "" + echo -e "\n${BRIGHT_MAGENTA}Creating VM '${auto_name}' [${BULL_OS}]...${RESET}" + echo -e " ${DIM}RAM: ${DEFAULT_RAM}MB | CPU: ${DEFAULT_CPU} | Resolution: ${BULL_RESOLUTION}${RESET}" + echo "" + if ! create_vm "${auto_name}" "${DEFAULT_RAM}" "${DEFAULT_CPU}" \ + "${BULL_CRED_USER}" "${BULL_CRED_PASS}" "${BULL_KB_LAYOUT}" "${BULL_RESOLUTION}" "${BULL_OS}"; then + log_error "VM creation failed. Check ${BULL_HOME}/bull-error.log for details." + echo -ne "\n ${DIM}Press Enter to continue...${RESET}" + read -r _ + fi + BULL_CRED_PASS="" + _offer_registry_toolkits "${auto_name}" + ;; + 2) + _prompt_os + clear + _display_step "Custom Create" "VM name" + echo "" + read -rp "$(echo -e " ${BRIGHT_CYAN}VM name > ${RESET}")" vm_name + if [[ -z "${vm_name}" ]]; then + echo -e "\n ${BRIGHT_RED}No name provided.${RESET}" + return + fi + validate_vm_name "${vm_name}" || return + + clear + _display_step "Custom Create" "Hardware resources" + echo "" + read -rp "$(echo -e " ${BRIGHT_CYAN}RAM in MB (${DEFAULT_RAM}) > ${RESET}")" ram + ram="${ram:-${DEFAULT_RAM}}" + read -rp "$(echo -e " ${BRIGHT_CYAN}CPU cores (${DEFAULT_CPU}) > ${RESET}")" cpu + cpu="${cpu:-${DEFAULT_CPU}}" + + _prompt_credentials || return + _prompt_keyboard + _prompt_resolution + clear + _display_step "VM Creation" "Setting up your VM" + echo "" + echo -e "\n${BRIGHT_MAGENTA}Creating VM '${vm_name}' [${BULL_OS}]...${RESET}" + echo -e " ${DIM}RAM: ${ram}MB | CPU: ${cpu} | Resolution: ${BULL_RESOLUTION}${RESET}" + echo "" + if ! create_vm "${vm_name}" "${ram}" "${cpu}" \ + "${BULL_CRED_USER}" "${BULL_CRED_PASS}" "${BULL_KB_LAYOUT}" "${BULL_RESOLUTION}" "${BULL_OS}"; then + log_error "VM creation failed. Check ${BULL_HOME}/bull-error.log for details." + echo -ne "\n ${DIM}Press Enter to continue...${RESET}" + read -r _ + fi + BULL_CRED_PASS="" + _offer_registry_toolkits "${vm_name}" + ;; + 0|"") + return + ;; + *) + echo -e "\n ${BRIGHT_RED}Invalid choice.${RESET}" + return + ;; + esac + ;; + 2) + echo -e "\n${BOLD}${BRIGHT_CYAN}[ VM Inventory ]${RESET}\n" + inventory_list + echo -e "\n ${BRIGHT_CYAN}Press Enter to continue...${RESET}" + read -r _ + ;; + 3) + local vm_name + vm_name=$(select_vm_from_list "Select VM to start") || return + start_vm "${vm_name}" || true + ;; + 4) + local vm_name + vm_name=$(select_vm_from_list "Select VM to stop") || return + stop_vm "${vm_name}" || true + ;; + 5) + local vm_name + vm_name=$(select_vm_from_list "Select VM to destroy") || return + destroy_vm "${vm_name}" || true + ;; + 6) + local vm_name + vm_name=$(select_vm_from_list "Select VM to view (GUI)") || return + view_vm "${vm_name}" || true + ;; + 7) + local vm_name + vm_name=$(select_vm_from_list "Select VM to connect") || return + connect_vm "${vm_name}" || true + ;; + 8) + local vm_name + vm_name=$(select_vm_from_list "Select VM (snapshot)") || return + + echo -e "" + echo -e "${BRIGHT_CYAN} ┌──────────────────────────────────────────────────────┐${RESET}" + echo -e "${BRIGHT_CYAN} │ │${RESET}" + echo -e "${BRIGHT_CYAN} │${RESET} ${BRIGHT_RED}[1]${RESET} Create snapshot ${BRIGHT_CYAN}│${RESET}" + echo -e "${BRIGHT_CYAN} │${RESET} ${BRIGHT_RED}[2]${RESET} Delete snapshot ${BRIGHT_CYAN}│${RESET}" + echo -e "${BRIGHT_CYAN} │${RESET} ${BRIGHT_RED}[0]${RESET} Back ${BRIGHT_CYAN}│${RESET}" + echo -e "${BRIGHT_CYAN} │ │${RESET}" + echo -e "${BRIGHT_CYAN} └──────────────────────────────────────────────────────┘${RESET}" + local snap_action + read -rp "$(echo -e " ${BRIGHT_CYAN}Choice > ${RESET}")" snap_action + + case "${snap_action}" in + 1) + local snap_label + read -rp "$(echo -e " ${BRIGHT_CYAN}Snapshot label (optional) > ${RESET}")" snap_label + snapshot_vm "${vm_name}" "${snap_label}" || true + ;; + 2) + local snap_to_delete + snap_to_delete=$(select_snapshot "${vm_name}") || { + echo -e " ${BRIGHT_RED}No snapshot selected.${RESET}" + return + } + delete_snapshot "${vm_name}" "${snap_to_delete}" && \ + echo -e "\n ${BRIGHT_CYAN}Snapshot '${snap_to_delete}' deleted.${RESET}" || \ + echo -e "\n ${BRIGHT_RED}Failed to delete.${RESET}" + ;; + 0|"") + return + ;; + *) + echo -e " ${BRIGHT_RED}Choice invalid.${RESET}" + ;; + esac + ;; + 9) + local vm_name + vm_name=$(select_vm_from_list "Select VM to restore") || return + local snap_label + snap_label=$(select_snapshot "${vm_name}") || { + echo -e " ${BRIGHT_RED}No snapshot selected.${RESET}" + return + } + echo -e " ${DIM}Restoring to:${RESET} ${BOLD}${snap_label}${RESET}" + restore_snapshot "${vm_name}" "${snap_label}" || true + ;; + 10) + local vm_name + vm_name=$(select_vm_from_list "Select VM for VPN") || return + local vpn_config + read -rp "$(echo -e " ${BRIGHT_CYAN}Path to VPN config (.ovpn / WireGuard) > ${RESET}")" vpn_config + [[ -z "${vpn_config}" ]] && { echo -e "${BRIGHT_RED}Config path required.${RESET}"; return; } + [[ ! -f "${vpn_config}" ]] && { echo -e "${BRIGHT_RED}File not found: ${vpn_config}${RESET}"; return; } + configure_vpn "${vm_name}" "${vpn_config}" || true + ;; + 11) + clear + list_toolkit_registry > /dev/null 2>&1 || true + _display_toolkit_manager + echo -ne " ${BOLD}${BRIGHT_CYAN}BULL > ${RESET}" + local tk_action + read -r tk_action + + case "${tk_action}" in + 1) + clear + local vm_name + vm_name=$(select_vm_from_list "Select VM") || return + local tk_url + read -rp "$(echo -e " ${BRIGHT_CYAN}Git URL > ${RESET}")" tk_url + [[ -z "${tk_url}" ]] && { echo -e " ${BRIGHT_RED}URL required.${RESET}"; read -r; return; } + install_toolkit "${vm_name}" "${tk_url}" || true + ;; + 2) + list_toolkit_registry > /dev/null 2>&1 || true + [[ "${#BULL_TOOLKIT_NAMES[@]}" -eq 0 ]] && { clear; echo -e " ${BRIGHT_RED}No tools in Library.${RESET}"; read -rp " ${BRIGHT_CYAN}Press Enter to continue...${RESET}"; return; } + clear + _display_toolkit_select "Select tool" + local tk_idx + read -rp "$(echo -e " ${BRIGHT_CYAN}Choice > ${RESET}")" tk_idx + [[ ! "${tk_idx}" =~ ^[0-9]+$ ]] || [[ "${tk_idx}" -lt 1 ]] || [[ "${tk_idx}" -gt "${#BULL_TOOLKIT_NAMES[@]}" ]] && { echo -e " ${BRIGHT_RED}Invalid selection.${RESET}"; read -r; return; } + local sel_url="${BULL_TOOLKIT_URLS[$((tk_idx-1))]}" + local vm_name + vm_name=$(select_vm_from_list "Select VM") || return + install_toolkit "${vm_name}" "${sel_url}" || true + ;; + 3) + clear + _display_toolkit_context + local save_url save_name _tmp_save_name + read -rp "$(echo -e " ${BRIGHT_CYAN}Git URL > ${RESET}")" save_url + [[ -z "${save_url}" ]] && { echo -e " ${BRIGHT_RED}URL required.${RESET}"; return; } + _validate_toolkit_url "${save_url}" || return + save_name="$(basename "${save_url}" .git)" + read -rp "$(echo -e " ${BRIGHT_CYAN}Name (default: ${save_name}) > ${RESET}")" _tmp_save_name + [[ -n "${_tmp_save_name}" ]] && save_name="${_tmp_save_name}" + toolkit_save "${save_name}" "${save_url}" && echo -e "\n ${BRIGHT_CYAN}'${save_name}' added to Library.${RESET}" + ;; + 4) + list_toolkit_registry > /dev/null 2>&1 || true + [[ "${#BULL_TOOLKIT_NAMES[@]}" -eq 0 ]] && { clear; echo -e " ${BRIGHT_RED}No tools in Library.${RESET}"; read -rp " ${BRIGHT_CYAN}Press Enter to continue...${RESET}"; return; } + clear + _display_toolkit_select "Remove from Library" + local tk_idx + read -rp "$(echo -e " ${BRIGHT_CYAN}Choice > ${RESET}")" tk_idx + [[ ! "${tk_idx}" =~ ^[0-9]+$ ]] || [[ "${tk_idx}" -lt 1 ]] || [[ "${tk_idx}" -gt "${#BULL_TOOLKIT_NAMES[@]}" ]] && { echo -e " ${BRIGHT_RED}Invalid selection.${RESET}"; return; } + local to_delete="${BULL_TOOLKIT_NAMES[$((tk_idx-1))]}" + toolkit_remove_from_registry "${to_delete}" && \ + echo -e "\n ${BRIGHT_CYAN}'${to_delete}' removed from Library.${RESET}" + ;; + 5) + list_toolkit_registry > /dev/null 2>&1 || true + [[ "${#BULL_TOOLKIT_NAMES[@]}" -eq 0 ]] && { clear; echo -e " ${BRIGHT_RED}No tools in Library.${RESET}"; read -rp " ${BRIGHT_CYAN}Press Enter to continue...${RESET}"; return; } + clear + _display_toolkit_select "Select tool" + local tk_idx + read -rp "$(echo -e " ${BRIGHT_CYAN}Choice > ${RESET}")" tk_idx + [[ ! "${tk_idx}" =~ ^[0-9]+$ ]] || [[ "${tk_idx}" -lt 1 ]] || [[ "${tk_idx}" -gt "${#BULL_TOOLKIT_NAMES[@]}" ]] && { echo -e " ${BRIGHT_RED}Invalid selection.${RESET}"; return; } + local sel_name="${BULL_TOOLKIT_NAMES[$((tk_idx-1))]}" + local sel_url="${BULL_TOOLKIT_URLS[$((tk_idx-1))]}" + clear + _display_toolkit_context + echo -e "${BRIGHT_CYAN} ┌──────────────────────────────────────────────────────┐${RESET}" + echo -e "${BRIGHT_CYAN} │${RESET} ${BOLD}${sel_name}${RESET}" + echo -e "${BRIGHT_CYAN} │${RESET} ${DIM}${sel_url}${RESET}" + echo -e "${BRIGHT_CYAN} │ │${RESET}" + echo -e "${BRIGHT_CYAN} ├──────────────────────────────────────────────────────┤${RESET}" + echo -e "${BRIGHT_CYAN} │${RESET} ${BRIGHT_RED}[1]${RESET} Update on VM ${BRIGHT_CYAN}│${RESET}" + echo -e "${BRIGHT_CYAN} │${RESET} ${BRIGHT_RED}[2]${RESET} Change URL ${BRIGHT_CYAN}│${RESET}" + echo -e "${BRIGHT_CYAN} │${RESET} ${BRIGHT_RED}[3]${RESET} Rename ${BRIGHT_CYAN}│${RESET}" + echo -e "${BRIGHT_CYAN} │${RESET} ${BRIGHT_RED}[0]${RESET} Back ${BRIGHT_CYAN}│${RESET}" + echo -e "${BRIGHT_CYAN} └──────────────────────────────────────────────────────┘${RESET}" + local mod_choice + read -rp "$(echo -e " ${BRIGHT_CYAN}Choice > ${RESET}")" mod_choice + + case "${mod_choice}" in + 1) + local vm_name + vm_name=$(select_vm_from_list "Select VM") || return + toolkit_pull "${vm_name}" "${sel_name}" || true + ;; + 2) + local new_url + read -rp "$(echo -e " ${BRIGHT_CYAN}Nouvelle URL > ${RESET}")" new_url + [[ -z "${new_url}" ]] && { echo -e " ${BRIGHT_RED}URL required.${RESET}"; return; } + toolkit_save "${sel_name}" "${new_url}" && \ + echo -e "\n ${BRIGHT_CYAN}URL updated.${RESET}" + ;; + 3) + local new_name + read -rp "$(echo -e " ${BRIGHT_CYAN}Nouveau nom > ${RESET}")" new_name + [[ -z "${new_name}" ]] && { echo -e " ${BRIGHT_RED}Nom requis.${RESET}"; return; } + toolkit_rename "${sel_name}" "${new_name}" && \ + echo -e "\n ${BRIGHT_CYAN}Renamed to '${new_name}'.${RESET}" + ;; + 0|"") ;; + *) + echo -e " ${BRIGHT_RED}Choice invalid.${RESET}" + ;; + esac + ;; + 6) + list_toolkit_registry > /dev/null 2>&1 || true + [[ "${#BULL_TOOLKIT_NAMES[@]}" -eq 0 ]] && { clear; echo -e " ${BRIGHT_RED}No tools in Library.${RESET}"; read -rp " ${BRIGHT_CYAN}Press Enter to continue...${RESET}"; return; } + clear + _display_step "Update ALL" "Installing all tools on ALL running VMs" + echo "" + inventory_init + local vm_data + vm_data=$(jq -r '.vms[] | select(.status == "running") | .name' "${INVENTORY_FILE}" 2>/dev/null || echo "") + if [[ -z "${vm_data}" ]]; then + echo -e " ${BRIGHT_RED}No running VMs found.${RESET}" + read -rp " ${BRIGHT_CYAN}Press Enter to continue...${RESET}" + else + local vms_updated=0 vms_failed=0 + while IFS= read -r vm_name; do + [[ -z "${vm_name}" ]] && continue + echo -e " ${CYAN}Updating on ${vm_name}...${RESET}" + if install_registry_toolkits "${vm_name}"; then + (( vms_updated++ )) + else + (( vms_failed++ )) + fi + done <<< "${vm_data}" + echo "" + echo -e " ${BRIGHT_CYAN}Done: ${vms_updated} VMs updated, ${vms_failed} failed.${RESET}" + read -rp " ${BRIGHT_CYAN}Press Enter to continue...${RESET}" + fi + ;; + 0|"") + _show_goodbye + exit 0 + ;; + *) + echo -e " ${BRIGHT_RED}Choice invalid.${RESET}" + ;; + esac + ;; + 12) + echo -e "\n${BOLD}${BRIGHT_CYAN}[ Global Status ]${RESET}\n" + show_status || true + read -rp " ${BRIGHT_CYAN}Press Enter to continue...${RESET}" + ;; + 13) + echo -e "\n${BOLD}${BRIGHT_CYAN}[ Sync Inventory ]${RESET}\n" + inventory_sync || true + ;; + 14) + echo -e "\n${BOLD}${BRIGHT_CYAN}[ System Check ]${RESET}\n" + check_dependencies || true + inventory_init + mkdir -p "${BULL_VM_DIR}" + + # Add alias to shell RC files + local bull_path="${SCRIPT_DIR}/bull.sh" + local alias_line="alias bull='sudo ${bull_path}'" + + for rc_file in ~/.bashrc ~/.zshrc ~/.profile; do + if [[ -f "${rc_file}" ]]; then + if ! grep -q "alias bull=" "${rc_file}" 2>/dev/null; then + echo "" >> "${rc_file}" + echo "# BULL alias (added by bull.sh)" >> "${rc_file}" + echo "${alias_line}" >> "${rc_file}" + echo " ${DIM}Added alias to ${rc_file}${RESET}" + fi + fi + done + + echo + log_info "BULL ready. Run 'bull' or 'source ~/.bashrc' to use." + ;; + 0) + _show_goodbye + exit 0 + ;; + *) + echo -e "\n${BRIGHT_RED}Invalid choice. Please try again.${RESET}" + ;; + esac +} + +# ============================================================================= +# CLI HELP (non-interactive usage) +# ============================================================================= +show_help() { + cat << EOF +${BOLD}BULL${RESET} v${BULL_VERSION} - Pentest Environment Toolkit + +${BOLD}USAGE:${RESET} + bull Launch interactive menu + bull [args] Run a command directly + +${BOLD}COMMANDS:${RESET} + init Check system dependencies + create [options] Create a new VM (Kali or Parrot) + list List all VMs with status + start Start a VM + stop Stop a VM + destroy Destroy a VM (irreversible) + connect SSH into a running VM + snapshot [label] Create a VM snapshot + restore