From 2a3b15b4b21d69d83a01a138b87fb3578ee84210 Mon Sep 17 00:00:00 2001 From: sudolulo Date: Mon, 15 Jun 2026 15:58:29 +0000 Subject: [PATCH 1/4] Fix two findings from full codebase audit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - apply.sh: correct comment from 'one prior generation' to 'two prior generations (.1 and .2)' — rotation has always kept three log files - uninstall.sh: add sync comment on find paths to match WEBUI_CANDIDATES in patch/patch_ui.py, preventing silent drift if a new path is added --- patch/apply.sh | 2 +- uninstall.sh | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/patch/apply.sh b/patch/apply.sh index d63287e..8240c19 100755 --- a/patch/apply.sh +++ b/patch/apply.sh @@ -22,7 +22,7 @@ PATCH_DIR="/data/truecloud-patch" LOG="$PATCH_DIR/apply.log" # Rotate log at 512 KB to avoid unbounded growth on a system volume. -# Keep one prior generation (.1) so the last two boots are always available. +# Keep two prior generations (.1 and .2) so the last three boots are always available. if [ -f "$LOG" ] && [ "$(wc -c < "$LOG")" -gt 524288 ]; then [ -f "${LOG}.1" ] && mv "${LOG}.1" "${LOG}.2" mv "$LOG" "${LOG}.1" diff --git a/uninstall.sh b/uninstall.sh index b7a0426..c7103b0 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -107,6 +107,7 @@ while IFS= read -r backup; do mv "$backup" "$original" echo " Restored: $original" RESTORED=1 +# Keep these paths in sync with WEBUI_CANDIDATES in patch/patch_ui.py done < <(find /usr/share/truenas /usr/share/truenas-ui /var/www/truenas \ -name "*.js.pre-truecloud-patch" 2>/dev/null) From 7c9aa7159ab061ad7425ce691f518fb3c96126ff Mon Sep 17 00:00:00 2001 From: sudolulo Date: Mon, 15 Jun 2026 16:03:19 +0000 Subject: [PATCH 2/4] Fix two confirmed findings from full codebase audit - patch_ui.py: wrap shutil.copy2 backup in try/except OSError so a permission or read-only filesystem error prints a specific diagnostic instead of crashing the script with a generic 'exited non-zero' message - install.sh: add early guard that detects pipe-install (bash <(curl ...)) and exits with a clear error pointing to the git clone workflow --- install.sh | 8 ++++++++ patch/patch_ui.py | 6 +++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 4b59482..2a99a69 100755 --- a/install.sh +++ b/install.sh @@ -15,6 +15,14 @@ set -euo pipefail PATCH_DIR="/data/truecloud-patch" REPO_DIR="$(cd "$(dirname "$0")" && pwd)" +if [ ! -f "$REPO_DIR/patch/sitecustomize.py" ]; then + echo "ERROR: patch files not found at $REPO_DIR/patch/" >&2 + echo "Run install.sh from the cloned repository, not via pipe:" >&2 + echo " git clone https://github.com/sudolulo/truenas-truecloud-patch" >&2 + echo " cd truenas-truecloud-patch && bash install.sh" >&2 + exit 1 +fi + echo "=== TrueNAS TrueCloud Provider Patch — Install ===" echo "" diff --git a/patch/patch_ui.py b/patch/patch_ui.py index b93f73b..cc65625 100644 --- a/patch/patch_ui.py +++ b/patch/patch_ui.py @@ -90,7 +90,11 @@ def main(): backup = path + ".pre-truecloud-patch" if not os.path.exists(backup): - shutil.copy2(path, backup) + try: + shutil.copy2(path, backup) + except OSError as exc: + print(f"[truecloud-patch] ERROR: Could not create backup {backup}: {exc}") + return patched, count = FIND.subn(REPLACE, content) if count != 1: From 8f64e4964cd8c4248c3533f064096d99d5c99815 Mon Sep 17 00:00:00 2001 From: sudolulo Date: Mon, 15 Jun 2026 16:06:26 +0000 Subject: [PATCH 3/4] Fix two findings from adversarial IX-perspective audit - create_task.py: add MITM risk warning to --insecure flag help text; common home-user pattern (self-signed cert) exposes API key in transit - install.sh: replace bare systemctl restart with explicit failure check that prints a recovery hint when middlewared fails to start post-install --- install.sh | 10 +++++++++- patch/create_task.py | 4 +++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/install.sh b/install.sh index 2a99a69..653587b 100755 --- a/install.sh +++ b/install.sh @@ -106,7 +106,15 @@ fi # ── Restart middlewared ─────────────────────────────────────────────────────── echo "Restarting middlewared so the backend patch takes effect ..." -systemctl restart middlewared +if ! systemctl restart middlewared; then + echo "" >&2 + echo "ERROR: middlewared failed to restart. The patch files are installed but" >&2 + echo "the hook is not yet active. Check the system log for the root cause:" >&2 + echo " journalctl -u middlewared -n 50" >&2 + echo "If the problem is unrelated to this patch, recover with:" >&2 + echo " bash $PATCH_DIR/recover.sh" >&2 + exit 1 +fi echo "Done." echo "" echo "Refresh your browser to pick up the UI change." diff --git a/patch/create_task.py b/patch/create_task.py index 7cb8116..8869199 100755 --- a/patch/create_task.py +++ b/patch/create_task.py @@ -195,7 +195,9 @@ def main(): p.add_argument("--api-key", default=None, metavar="KEY", help="TrueNAS API key — System → API Keys (required except for verify)") p.add_argument("--insecure", action="store_true", - help="Skip TLS certificate verification (self-signed certs)") + help="Skip TLS certificate verification (self-signed certs). " + "WARNING: exposes your API key to network interception. " + "Prefer adding your cert to the trust store instead.") sub = p.add_subparsers(dest="cmd", required=True) From 9481bf55f023780b068cb458e17ffff995853a55 Mon Sep 17 00:00:00 2001 From: sudolulo Date: Mon, 15 Jun 2026 16:10:09 +0000 Subject: [PATCH 4/4] Fix three findings from final clean-pass audit - apply.sh: gate sitecustomize.py install on backup success; a failed backup cp (disk full, read-only mount) previously fell through and overwrote the vendor file with no recovery path - create_task.py: handle unexpected 2xx response schema in cmd_create; bare KeyError on result['id'] is replaced with a diagnostic print - uninstall.sh: mv inside while loop had no error handling; under set -euo pipefail a failed mv aborted the script before rm -rf PATCH_DIR, leaving the system in partial-uninstall limbo --- patch/apply.sh | 21 ++++++++++++++------- patch/create_task.py | 5 ++++- uninstall.sh | 9 ++++++--- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/patch/apply.sh b/patch/apply.sh index 8240c19..9fd9fa8 100755 --- a/patch/apply.sh +++ b/patch/apply.sh @@ -85,17 +85,24 @@ if [ -z "$SITE_PKG" ]; then echo " Run: $PYTHON -c \"import site; print(site.getsitepackages())\"" else # Back up any pre-existing sitecustomize.py that isn't ours. + _can_install=true if [ -f "$SITE_PKG/sitecustomize.py" ] && \ ! grep -q "truecloud-patch" "$SITE_PKG/sitecustomize.py" 2>/dev/null; then - cp "$SITE_PKG/sitecustomize.py" \ - "$SITE_PKG/sitecustomize.py.pre-truecloud-patch" - echo "OK: Backed up existing sitecustomize.py" + if cp "$SITE_PKG/sitecustomize.py" \ + "$SITE_PKG/sitecustomize.py.pre-truecloud-patch"; then + echo "OK: Backed up existing sitecustomize.py" + else + echo "WARNING: Could not back up existing sitecustomize.py; skipping install to avoid data loss." + _can_install=false + fi fi - if cp "$PATCH_DIR/sitecustomize.py" "$SITE_PKG/sitecustomize.py" 2>/dev/null; then - echo "OK: Installed sitecustomize.py → $SITE_PKG/sitecustomize.py" - else - echo "WARNING: Failed to write $SITE_PKG/sitecustomize.py (permission error?)" + if $_can_install; then + if cp "$PATCH_DIR/sitecustomize.py" "$SITE_PKG/sitecustomize.py" 2>/dev/null; then + echo "OK: Installed sitecustomize.py → $SITE_PKG/sitecustomize.py" + else + echo "WARNING: Failed to write $SITE_PKG/sitecustomize.py (permission error?)" + fi fi fi diff --git a/patch/create_task.py b/patch/create_task.py index 8869199..cd3dfcd 100755 --- a/patch/create_task.py +++ b/patch/create_task.py @@ -179,7 +179,10 @@ def cmd_create(client, args): } result = client("POST", "/cloud_backup", body) - print(f"Created task id={result['id']} name={result['description']!r}") + try: + print(f"Created task id={result['id']} name={result['description']!r}") + except (KeyError, TypeError): + print(f"Task created but response schema was unexpected: {result}") # ── CLI ─────────────────────────────────────────────────────────────────────── diff --git a/uninstall.sh b/uninstall.sh index c7103b0..41ac44d 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -104,9 +104,12 @@ echo "Restoring UI bundle backup ..." RESTORED=0 while IFS= read -r backup; do original="${backup%.pre-truecloud-patch}" - mv "$backup" "$original" - echo " Restored: $original" - RESTORED=1 + if mv "$backup" "$original"; then + echo " Restored: $original" + RESTORED=1 + else + echo " WARNING: Could not restore $original — backup left at $backup" + fi # Keep these paths in sync with WEBUI_CANDIDATES in patch/patch_ui.py done < <(find /usr/share/truenas /usr/share/truenas-ui /var/www/truenas \ -name "*.js.pre-truecloud-patch" 2>/dev/null)