BUNTSTARTER/REVIEW.md
jing 529039dc87 the great unbuntening: ui goes brrr + full review in REVIEW.md
- splash no longer does the 5.5s slow-scroll flex
- apt-get update only runs if lists are >6h old (-r/--refresh to force)
- log() fork-free, debug spam gated behind ULTRABUNT_DEBUG=1
- category menu counts in one pass, no more dpkg fork storm per redraw
- deleted refresh_cache_silent (600 background forks doing literally nothing)
- rc-state packages no longer show as installed; flatpak exact-match
- bulk ops no longer die after first package (((x++)) set -e trap)
- bulk checklists no longer scramble themselves when sorting
- ESC no longer nukes the whole script in buntage/log-viewer menus
- swappiness menu: fixed regex crash + duplicate sysctl lines
- wp hardening: domain validated (no more chown -R /var via ".."), options actually apply now
- fun category (15 buntages) rescued from the void
- --no-media/--no-web/--no-network/-minimal/-core now exclude real categories

REVIEW.md has the full findings list incl. everything not fixed yet

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 11:37:48 +10:00

125 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Ultrabunt Code Review — 2026-07-17
Full review of `ultrabunt.sh` (17k lines) and `ultrabunt-accessible.sh`.
Scope: real breakage and danger only — style ignored, silliness preserved.
Line numbers are approximate (the performance pass shifted things by a few
dozen lines in places) — search for the quoted function name if a number is off.
---
## Fixed in this pass (performance + crashers)
These are already applied to `ultrabunt.sh`:
| What | Why it was slow/broken |
|---|---|
| Splash screen printed instantly | 78 lines × `sleep 0.05` + `sleep 1.5` = ~5.5s of dead air at every launch |
| `apt-get update` skipped when lists < 6h old (`--refresh` to force) | Ran unconditionally at startup: 1060s per launch on old hardware |
| `log()` rewritten fork-free (`printf %(...)T` + direct append) | Old version forked ~3 processes per line (`$(date)`, pipe, `tee`) and was called per package per menu redraw; also sprayed stderr underneath whiptail. Set `ULTRABUNT_DEBUG=1` to mirror logs to stderr |
| Removed per-check logging from `is_package_installed` | 3+ log lines (≈9 forks) per package, per menu redraw |
| Category counts computed in ONE pass over `PACKAGES` | Old loop re-walked all ~600 packages once per category (~15,000 iterations) on every main-menu redraw |
| `is_apt_installed` cache miss is now authoritative after startup scan | Old fallback forked `dpkg -l` for every NOT-installed package on every redraw hundreds of forks per screen |
| `build_package_cache` always does one full dump per package manager | "Selective mode" forked one `dpkg-query` per package strictly slower than dumping everything (which is 1 fork) |
| Deleted `refresh_cache_silent` + the 🔄 indicator | It forked ~600 background processes whose cache writes happened in a **child shell and were thrown away** a subshell can't modify the parent's `INSTALLED_CACHE`. 100% wasted CPU on a machine that has none to spare |
| Removed `log "Menu items array contents: ..."` | Wrote the entire ~5KB menu array to the log on every redraw |
| dpkg checks filter on `Status-Status == installed` | `dpkg-query -W` also lists removed-but-not-purged (`rc`) packages, so uninstalled packages showed Installed |
| Flatpak checks use exact match (`grep -Fxq` on the app-id column) | Bare `grep -q "$pkg"` matched substrings |
| `((installed++))`-style counters `x=$((x+1))` (6 sites in bulk ops) | `((x++))` returns exit 1 on the 01 increment; under `set -e` every bulk install/remove **killed the whole script after the first package** |
| Bulk-select checklists sort names first, then build triples | Old code joined tag/desc/OFF triples into lines and sorted ALL lines, scrambling the checklist alignment |
| `show_buntage_actions` menu guarded with `\|\| return`; install/reinstall success message now conditional on actual success | Cancel killed the script via `set -e`; failures reported "installed successfully!" |
| `view_log_file` menu guarded with `\|\| return` | ESC/Cancel in the log viewer killed the whole script |
| Swappiness regexes fixed (`vm\\.swappiness` `vm\.swappiness`) + no-match grep guarded | The `\\.` inside single quotes is a literal backslash: "show entry" **crashed the script** and a duplicate `vm.swappiness` line was appended to sysctl.conf on every persist |
| `wordpress_security_hardening`: domain validated, wp-config.php required | Entering `..` made `site_dir=/var/www/..` = `/var`, then "Fix File Permissions" ran `chown -R www-data:www-data /var` **system-wrecking** |
| Hardening checklist strips whiptail's literal quotes | No case arm ever matched `"file-permissions"` (with quotes), so every hardening option was silently skipped while reporting success |
| `fun` category added to `CATEGORIES` | 15 packages were tagged `fun` but the category wasn't in the menu permanently invisible |
| `--no-media/--no-web/--no-network/-minimal/-core` use real category ids | They referenced ids that don't exist (`multimedia`, `music`, `web-browsers`, `network`, `education`…), so the exclusions silently did nothing |
| Package-list sort uses `mapfile` instead of unquoted `$(sort)` | Glob chars in descriptions (`(_*_)`) could expand against files in the cwd |
Expected result: startup goes from ~4070s to a few seconds (splash + one dpkg/snap/flatpak dump), and menu redraws go from seconds of fork-storm to near-instant pure-bash.
---
## Outstanding — HIGH (dangerous or feature completely broken)
### WordPress / database
- **`finalize_wordpress_setup`**: custom setup's user-entered `site_dir` is ignored by the actual installers (they all install to `/var/www/$domain`) but the *finalizer still runs* `chown -R $(whoami):www-data "$site_dir"` + recursive chmod on the unvalidated path. Enter `/home` and it re-owns it. Validate/derive `site_dir` the same way the installers do.
- **SQL injection everywhere a password/name is interpolated into `mysql -e "... IDENTIFIED BY '$pass'"`** (`setup_wordpress_database*`, `create_database_user_interactive` ~L16381, DB repair `eval` ~L13870). A single `'` in a password breaks or injects as MariaDB root. The repair path pipes it through `eval` too. Fix: validate names as `[A-Za-z0-9_]+`, pass passwords via `--defaults-extra-file` or here-doc `SET PASSWORD` with proper quoting.
- **`setup_wordpress_database_custom` runs under command substitution but its whiptail dialogs aren't redirected to `/dev/tty`** dialogs are captured into the result variable instead of being shown; flow appears hung and wp-config gets garbage credentials.
- **wp-config salts**: the awk insert anchors on `/** MySQL settings` which WordPress renamed in 5.8 the sed first *deletes* all AUTH_KEYNONCE_SALT lines, then the insert never fires **modern WP sites end up with no security keys at all**, silently.
- **`setup_nginx_ssl`** writes the 443 block to `sites-available/$domain` while `.localhost` sites were created under the stripped base name new file is never enabled; reports success, HTTPS never serves. Also appends a duplicate 443 block on every rerun (`tee -a`).
- **phpMyAdmin installer** writes the MariaDB root password to predictable `/tmp/mysql_defaults_phpmyadmin.cnf` (pre-creatable by any local user; 644 window before chmod), and the follow-up `mysql --defaults-file` runs as the wrong user so it silently no-ops. Root password also logged in plaintext to `/var/log/ultrabunt.log` (~L10883) and wp-config gets loosened to 0664 (group-writable by www-data, world-readable).
- **`delete_wordpress_site`** reads `/root/.mysql_root_password` *without sudo* DB/user never actually dropped while reporting "completely removed including database"; also deletes the nginx vhost matching the *first label* of the domain (deleting `shop.example.com` can nuke `shop.localhost`'s config), and will DROP a database shared by other sites without cross-checking.
- **`grant_file_access_to_user`**: `chmod -R 2775 /var/www` makes every *file* group-writable+setgid and world-readable www-data can rewrite all PHP (persistence vector) and wp-config passwords are world-readable. Use 664 files / 2775 dirs.
- **`show_individual_site_management`** uses `$site_dir` (never set locally dynamically inherits the caller's *last* loop value) instead of the `site_root` it computes wp-config view/edit/DB-info act on the **wrong site** whenever >1 site exists.
### Cloudflare / firewall
- **`cloudflare_create_rule` builds its JSON with raw printf and no escaping** — every filter expression contains quotes (some contain newlines) → malformed JSON → **every rule creation fails**, including "light". The whole feature can't work as written. Also uses the deprecated legacy `firewall/rules` endpoint, and the country code `UK` should be `GB` (would block UK visitors incl. you).
### Mirror management
- **The entire "fastest mirror" feature is dead**: it invokes `mirrorselect` (a Gentoo tool) which is never installed (`install_mirrorselect` actually installs `netselect-apt`). The auto mode uses netselect-apt's **Debian** mirror list and seds those hosts over `archive.ubuntu.com` → apt breaks. The "restore" path recomputes `$(date +%s)`-style backup names so it restores from a filename that doesn't exist and dies under `set -e` with sources.list already clobbered. `use_local_mirror`/`setup_local_mirror` write to `/etc/apt/` **without sudo** → instant permission-denied crash. On 24.04+ the real sources live in `/etc/apt/sources.list.d/ubuntu.sources` anyway. Recommend: rip this section out or rewrite around `netselect-apt`-for-Ubuntu with sudo and proper backup naming.
- `run_mirrorselect_custom` does `eval "sudo mirrorselect $custom_flags"` on free-typed input — arbitrary shell as root by design.
### Reverse-proxy add-ons
- **Umami**: publishes `3000:3000` on 0.0.0.0 (Docker's iptables bypass UFW) with hardcoded `APP_SECRET: replace-me-with-a-random-string` and default admin/umami login → internet-reachable with forgeable sessions on any VPS.
- **Mailcow**: mailcow's own nginx defaults to 80/443 (collides with host nginx) while the proxy snippet assumes `127.0.0.1:8080` which nothing configures — either compose fails or the proxy points at a dead port; completion message advertises `https://` URLs no config provides.
- Both add-on flows have zero error handling: any failed step under `set -e` kills the whole script with containers half-started and nginx confs already symlinked.
### Installers (selection — full list in agent notes)
- **`install_arduino-ide`**: `unzip --strip-components=1` — flag doesn't exist; wipes `/opt/arduino-ide` then always fails, and `set -e` kills the script.
- **`install_mailhog`**: `curl -Lo` without `-f` → 404 page installed as a root systemd-managed binary; also downloads into whatever cwd the script happens to be in (deno/zed installers `cd` into temp dirs and delete them, leaving a dead cwd).
- **Plex**: `curl | sudo apt-key add -``apt-key` is gone on newer Ubuntu, so it aborts *after* deleting the old sources; key would land in the global trusted keyring.
- **Fixed predictable `/tmp` paths + `sudo mv`/`dpkg -i`** (lazydocker, glow, cheat, broot, dog, discord, ums, gollama, koboldcpp, LM Studio…): a local user can pre-plant/race the file and get a root-installed binary. Use `mktemp -d`.
- **`dpkg -i || apt-get install -f -y`** pattern (6+ sites): non-dependency dpkg failures are masked and reported as success.
- **whisper.cpp** builds unpinned master and copies `main`/`quantize` — upstream renamed the binaries; always fails now, `make 2>/dev/null` hides why.
- Version-pinned URLs that 404 now or soon: ctop (latest/download + 0.7.7), kind v0.20.0, Insomnia 8.6.1, mergerfs jammy-only deb, LM Studio 0.3.5, UMS deb name, balena-etcher remove targets the old package name.
- `curl | bash` as root (nodesource, ollama, starship, ddev — ddev also points at the moved `drud/ddev` repo) — accepted risk for a personal tool, but at least pin/checksum the big ones.
- `gpg --dearmor` without `--yes` (caddy, ngrok, docker, warp): reruns abort mid-way after old keys were already deleted.
### Packages that can never install (verified against live registries)
- **apt, wrong/absent name**: `sqlitestudio` (26.04+ only), `chromium` (→ `chromium-browser`), `asciiquarium`, `batcat` (→ `bat`, and duplicates the existing `bat` entry), `mysql-workbench-community` (needs MySQL repo), `pgadmin4` (needs pgadmin repo), `bottom`, `google-drive-ocamlfuse` (PPA-only), `jellyfin` (needs jellyfin repo), `citra`, `dosbox-staging`, `python3-esptool` (→ `esptool`), `webcamize`; version-dependent: `exa` (gone in 24.04+, keep `eza`), `pastel`, `fastfetch` (25.04+), `qalculate` (→ `qalculate-gtk`), `bsnes`, `git-delta` (23.10+).
- **snap, 404**: `tableplus`, `mongodb-compass`, `nextcloud-server` (→ `nextcloud`), `heroic`, `dosbox`, `platformio`.
- **cargo/npm**: `qmasa` (→ `qmassa`), `dust` (→ `du-dust`), `adminmongo` (not on npm).
- **flatpak**: citra + yuzu delisted from Flathub (Nintendo takedown); `freetube`/`stremio` use bare names instead of app IDs.
---
## Outstanding — MEDIUM
- MariaDB/MySQL passwords passed as `-p"$pass"` on the command line **everywhere** (~20 sites) — visible in `/proc/*/cmdline` to any local user (matters most on the VPS). Standard fix: `--defaults-extra-file=<(...)` or `MYSQL_PWD` for a personal box.
- DB dumps from `backup_database_interactive` written world-readable (default umask) into `$BACKUP_DIR`; `grep -q "^${database}$"` treats the name as a regex.
- wp-config credential extraction via `grep | cut -d"'" -f4` matches commented lines, no `head -1`, breaks on `'` in passwords (DB test/repair flows).
- `restart_web_services` probes only php 8.1/8.0/7.4-fpm — never restarts the script's own PHP 8.3 FPM, so "optimize uploads" silently doesn't land. Same class: SQL-import optimizer derives paths from CLI `php -v` (can be a different version than FPM); `client_max_body_size` sed lowers larger values across *all* vhosts; `<IfModule mod_php8.c>` never matches; `mysql.connect_timeout` should be `mysqli.connect_timeout`.
- MySQL "import optimization" permanently sets `innodb_flush_log_at_trx_commit=2` and blind `buffer_pool=512M` with no revert or RAM check; PHP `memory_limit=8G` per worker can OOM a small machine.
- `modify_php_setting` interpolates unvalidated input into sed (a `/`, `&` or `\` corrupts php.ini).
- Bulk ops: `install_package ... | tee` runs in a pipeline subshell, so cache updates inside are discarded — status can go stale for the session (harmless-ish now that redraws are cheap; re-run `build_package_cache` after bulk ops to be exact).
- `ui_menu` returns the literal string `"back"` on ESC — any future package named `back` breaks navigation; bulk category picker can pass `"back"` into the match loop.
- Web-config-summary site pickers push 3 args per non-live site into a *menu* (that's checklist syntax) — misaligned rows; "No live sites" branch unreachable.
- Log viewer: Ctrl+C on `tail -f` SIGINTs the whole script (no trap); pager temp file at predictable `/tmp/ultrabunt_log_view_$$` copies sudo-read logs world-readable.
- `wordpress_cleanup_menu` find: `-name "*.localhost" -o -name "*.local" -print0``-print0` binds to one branch; `.localhost` sites never listed.
- certbot always adds `-d www.$domain` — fails for domains without a www record.
- Hotkeys: >24 categories get lowercase tags but the handler uppercases before lookup (opens the wrong category); >48 reuses letters and silently overwrites. The "cycling" code for shared hotkeys is dead (assignment overwrites, never appends). Currently 27 categories, so the first overflow row (`a`) is already live.
- `ultrabunt-accessible.sh`: launches `./ultrabunt.sh` relative to caller's cwd; instructs running the whole thing via `sudo`, which breaks `$USER`-based ownership logic in the main script and makes TTS silent (root has no Pulse session); `timeout 10s echo | festival` applies the timeout to `echo` (festival can hang forever); `sudo -u user cat > file` redirection still runs as root.
## Outstanding — LOW (grab-bag)
- `show_system_info` hardcodes a 50×140 dialog — breaks on a stock 80×24 SSH window.
- Export list writes to predictable `/tmp/ultrabunt-packages-<ts>.txt`.
- `get_status()` is dead code (never called; no arms for custom/npm/cargo methods).
- `silversearcher-ag` and `ag` are duplicate entries for the same package.
- `remove_phpmyadmin` only restarts services that are `is-enabled` — a running-but-disabled DB/web server stays down. `--force-yes` no longer exists in modern apt.
- `__site_is_live` needs `host` (not on minimal Ubuntu) → everything reports "not live".
- Keyboard config writes to `$HOME`/gsettings — configures root's session if run under sudo.
- bun installer greps live `$PATH` instead of rc files → duplicate export lines per rerun.
- README drift: claims pip is removed but `install_pip_package`/npm/cargo helpers and CUSTOM npm tools are still in the script; claims 25+ categories/560+ apps — counts don't match the code.
---
## Suggested priorities
1. **WordPress DB quoting + the `finalize_wordpress_setup` chown** — it's the feature you actually use, on machines you keep.
2. **Delete or rewrite mirror management** — it cannot work and it edits sources.list.
3. **Umami/Mailcow port + secret fixes** if you ever run them on the VPS.
4. **Purge the ~25 never-installable package entries** (list above) — they just waste menu space and end in failure dialogs.
5. Sweep the installers for `curl -f`, `mktemp -d`, and `gpg --dearmor --yes` — three mechanical patterns fix most of the installer findings.