Repository navigation
How It Works
This page documents what Bull actually does, end to end, at the level of functions, files, and on-disk artifacts. If Architecture is the map, this is the walkthrough.
Everything below traces real code in lib/ and configs/. Function names are given so you can read along in the source.
bull create flows through cmd_create (in bull.sh) into create_vm() (lib/vagrant.sh:1199). Step by step:
-
Resolve inputs. Username defaults to the OS default via
_os_default_user()(kalifor Kali,userfor Parrot). Password is taken from--password-file(read once, then the file is truncated),--password(deprecated), or generated withopenssl rand(24 chars). -
Pre-flight checks.
ensure_dependencies()offers to install anything missing. Thenvalidate_vm_name()(letter-led, alphanumeric+hyphen, ≤11 chars),validate_positive_int()for RAM/CPU, a duplicate check against the inventory, andcheck_disk_space(). -
Cache the box.
ensure_vagrant_box()(lib/vagrant.sh:310) makes sure the Vagrant box for the chosen OS exists locally, offering to download it if not. Parrot has a dedicated path (_ensure_parrot_box, OVA/qcow2 handling and patching) because its box is not a standard Vagrant artifact. -
Build the VM directory.
get_vm_dir()resolves${BULL_HOME}/vms/<name>. It is createdchmod 700because the Vagrantfile will briefly hold the plaintext password. -
Generate the Vagrantfile.
generate_vagrantfile()(lib/vagrant.sh:1089) rendersconfigs/Vagrantfile.template(see below). -
Copy the provisioner.
copy_provision_script()copiesconfigs/<os>-provision.shto<vm-dir>/provision.sh. -
Record in inventory.
inventory_add()writes the VM withstatus: not_createdbefore boot, so a crash mid-boot still leaves a tracked, cleanable record. -
Boot.
vagrant up --provider=$BULL_PROVIDERruns in the VM directory, output teed to both the terminal and<vm-dir>/vagrant-up.log. The shell provisioner runs inside the guest as root. -
Post-boot fixups. Status flips to
running, the IP is fetched and stored. On libvirt,_fix_spice_cursor()rewrites the domain XML to kill the SPICE double-cursor. On VirtualBox, a two-pass resolution apply runs (setvideomodehinthost-side +xrandrguest-side). - Sanitize. The Vagrantfile is regenerated with an empty password slot, the credential is GPG-encrypted to disk, and the plaintext is shown once on screen (with consent) then wiped from memory.
If any step before boot fails, the VM directory is removed and the inventory entry rolled back.
configs/Vagrantfile.template is a static Ruby file with %%PLACEHOLDER%% tokens. generate_vagrantfile() substitutes them with a single sed pass and writes the result chmod 600.
| Placeholder | Source |
|---|---|
%%VM_NAME%%, %%RAM%%, %%CPU%%
|
create arguments |
%%PROVIDER%% |
BULL_PROVIDER |
%%BOX%% |
_os_to_box() |
%%USERNAME%%, %%PLAIN_PASSWORD%%
|
credentials (password empty after sanitize) |
%%KEYBOARD%%, %%RESOLUTION%%
|
layout / display |
%%SKIP_UPGRADE%% |
BULL_SKIP_UPGRADE env |
%%TOOLKIT_URLS%%, %%EXTRA_PACKAGES%%
|
optional extras |
%%UEFI%%, %%OVMF_LOADER%%, %%OVMF_VARS%%
|
Parrot UEFI boot |
Key things the template does:
-
Disables the synced folder (
config.vm.synced_folder ".", "/vagrant", disabled: true) so the guest cannot reach the host filesystem. - Disables clipboard, drag-and-drop, and audio on VirtualBox.
- Configures per-provider graphics: VMSVGA + 128 MB VRAM on VirtualBox; SPICE + QXL + a virtio-serial channel and a USB tablet on libvirt (the tablet sends absolute coordinates, fixing cursor offset).
- For Parrot (
%%UEFI%% == true), points libvirt at the OVMF loader/nvram so it boots under UEFI instead of SeaBIOS, and sets the SSH user touser. - Passes credentials and settings to the provisioner via the
env:block, not the command line, so they never appear in the guest's process list.
Parrot uses a GPT + EFI layout and will not boot under SeaBIOS. generate_vagrantfile() checks that /usr/share/OVMF/OVMF_CODE_4M.fd and OVMF_VARS_4M.fd are readable; if so it enables UEFI, otherwise it falls back to SeaBIOS with a warning. BULL_FORCE_UEFI=1 forces UEFI regardless. The VARS file has several possible names across distros, so Bull probes .fd, .ms.fd, and .snakeoil.fd in order.
provision.sh (from configs/kali-provision.sh / parrot-provision.sh) runs as root inside the VM and reads its inputs from the BULL_VM_* environment variables set by the Vagrantfile. The hardening it performs:
-
User creation. Creates
${BULL_VM_USER}if absent and adds it tosudo. -
Password. Sets it with
chpasswd(plaintext piped in) — the most reliable method across Kali versions regardless of the default hashing scheme.BULL_VM_PASSisunsetimmediately after. -
Encrypt
/home. Installsecryptfs-utils, generates a 32-char random passphrase, wraps it withecryptfs-wrap-passphraseinto the user's.ecryptfs/, and adds it to the keyring. -
SSH key propagation. Copies Vagrant's insecure authorized key from
/home/vagrant/.sshto the custom user sobull connectworks as that user. -
Lock the default account.
passwd -l kali(or the Parrot equivalent) locks the distro's shipped account. -
Passwordless sudo. Writes
/etc/sudoers.d/bull-<user>withNOPASSWD:ALL(pentest convenience),chmod 440. -
Keyboard layout. Applied through seven mechanisms for robustness across display managers:
/etc/default/keyboard, debconf preseed,dpkg-reconfigure, an X11xorg.conf.dsnippet,/etc/xprofile, the XFCE xfconf channel, a system-wide XDG autostart runningsetxkbmap, andlocalectl. -
Resolution. Installs
/usr/local/bin/bull-set-resolution.sh, which detects the connected output and the active X session'sXAUTHORITYcookie, then forces the mode withxrandr(creating the modeline if needed). LightDM display/session setup scripts re-apply it at login.
The Kali and Parrot scripts share this structure; Parrot's differs where its desktop and boot layout require it.
The plaintext password exists in exactly three transient places, and is removed from each:
-
In
create_vm's memory — wiped (plain_password="") right after it is used. -
In the Vagrantfile (
%%PLAIN_PASSWORD%%) — needed so the provisioner canchpasswd. After boot,generate_vagrantfilere-runs with an empty slot, overwriting it. The file ischmod 600the whole time. -
In the guest's environment (
BULL_VM_PASS) —unsetimmediately afterchpasswd.
For persistence, _credentials_save() (lib/core.sh) GPG-encrypts the password (AES256, SHA512, 65M S2K iterations) to <vm-dir>/.credentials.gpg, while <vm-dir>/.credentials holds only the username. The GPG key is a per-user key under ${BULL_HOME}/.gnupg (chmod 700), generated on first use with an empty passphrase. bull show-pass calls _credentials_show() which decrypts on demand. See the Security Model.
The inventory is a single JSON file at data/inventory.json (relative to the install, not committed). Each VM record:
{
"name": "kali-lab",
"os": "kali",
"status": "running",
"ip": "192.168.122.50",
"created": "2026-06-17T12:00:00Z",
"ram": "4096",
"cpu": "2",
"resolution": "1920x1080",
"snapshots": [],
"vpn_configured": false,
"vm_dir": "/root/.bull/vms/kali-lab"
}All mutations go through inventory_*() functions in lib/inventory.sh, which build the new JSON with jq and write it atomically (mktemp in the same directory, then mv) so a crash never leaves a half-written file. status moves not_created → running → poweroff over a VM's life.
bull sync (inventory_sync()) reconciles the file with reality: for each VM it reads vagrant status --machine-readable, updates the stored status, and refreshes the IP from hostname -I when running. Use it after changing a VM outside Bull.
BULL_PROVIDER is set once in core.sh (libvirt if /dev/kvm exists, else VirtualBox, overridable via the environment), and VAGRANT_DEFAULT_PROVIDER is exported so Vagrant never enumerates providers — this sidesteps a Vagrant 2.4.x Hyper-V detection crash on WSL2.
All Vagrant calls go through ${VAGRANT_CMD} (vagrant, or vagrant.exe on WSL2+VirtualBox). The libvirt-specific helpers (_libvirt_uri, _libvirt_domain, _libvirt_start/stop/destroy, _libvirt_get_ip, _libvirt_ensure_network, _libvirt_ssh) wrap virsh so the rest of the code does not care which hypervisor is underneath. The SPICE cursor fix (_fix_spice_cursor) and the VirtualBox path/health helpers (_ensure_vbox_in_windows_path, _verify_vbox_functional, _repair_vbox_services) are the only places that reach into provider-specific internals.
See Architecture for the high-level structure and Troubleshooting for what to do when one of these steps fails.
Bull · MIT License · Technical hardening only — see the Security Model.
Getting started
Features
Internals
Help