From ea2870a9d6af16de777f5529372466adc0b3ac4c Mon Sep 17 00:00:00 2001 From: Josh Sokol Date: Sat, 19 Sep 2026 13:40:23 -0500 Subject: [PATCH 01/11] Run backup cron as web-server account instead of root The installer chowns /var/www/simplerisk (including cron/cron.php) to the web-server account, then registered the backup cron in root's crontab. Any attacker with write access as that web account could overwrite cron.php and get root code execution on the next minute tick, defeating the containment the web account is supposed to provide. set_up_backup_cronjob() now installs a /etc/cron.d/simplerisk entry with the web-server account in the user column instead of appending to root's crontab, so cron.php only ever executes with the privileges the web account already has. remove_backup_cronjob() also cleans up any legacy root-crontab entry left by older installs. Reported via HackerOne #3761952. Co-Authored-By: Claude Sonnet 5 --- simplerisk-setup.sh | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/simplerisk-setup.sh b/simplerisk-setup.sh index 67bba81..98abc0f 100755 --- a/simplerisk-setup.sh +++ b/simplerisk-setup.sh @@ -379,7 +379,16 @@ set_up_simplerisk() { } set_up_backup_cronjob() { - exec_cmd "(crontab -l 2>/dev/null; echo '* * * * * $(which php) -f /var/www/simplerisk/cron/cron.php') | crontab -" + # $1 receives the web-server account (www-data / apache / wwwrun). + # Installed as a system cron.d entry with that account in the user column, + # rather than appended to root's crontab: cron/cron.php is owned and + # writable by the web account (see set_up_simplerisk's chown -R), so + # running it as root would let anyone who can write that file escalate to + # root every minute. Running it as the web account instead means an + # overwritten cron.php only ever executes with the privileges the web + # account already has. + exec_cmd "echo '* * * * * ${1} $(which php) -f /var/www/simplerisk/cron/cron.php' > /etc/cron.d/simplerisk" + run_cmd chmod 644 /etc/cron.d/simplerisk } set_up_simplerisk_log() { @@ -437,6 +446,9 @@ bail() { } remove_backup_cronjob() { + run_cmd_nobail rm -f /etc/cron.d/simplerisk + # Also strip any legacy root-crontab entry left by installs from before + # the cron job was moved to run as the web-server account. (crontab -l 2>/dev/null | grep -v 'simplerisk/cron/cron.php') | crontab - 2>/dev/null || true } @@ -598,7 +610,7 @@ setup_ubuntu_debian(){ run_cmd rm -r /var/www/simplerisk/database.sql print_status 'Setting up Backup cronjob...' - set_up_backup_cronjob + set_up_backup_cronjob 'www-data' print_status 'Installing UFW firewall...' run_cmd apt-get install -y ufw @@ -737,7 +749,7 @@ EOF run_cmd systemctl enable --now crond print_status 'Setting up Backup cronjob...' - set_up_backup_cronjob + set_up_backup_cronjob 'apache' print_status 'Enabling and starting the Apache web server...' run_cmd systemctl enable httpd @@ -963,7 +975,7 @@ EOF run_cmd systemctl enable --now cron print_status 'Setting up Backup cronjob...' - set_up_backup_cronjob + set_up_backup_cronjob 'wwwrun' print_status 'Installing and enabling firewall...' run_cmd zypper -n install firewalld From e3b243d25490ef253321fea97f5857eb971747a1 Mon Sep 17 00:00:00 2001 From: Josh Sokol Date: Sat, 19 Sep 2026 14:04:13 -0500 Subject: [PATCH 02/11] Fix cron.d file being written empty by exec_cmd's output redirect exec_cmd_nobail appends its own `> /dev/null 2>&1` to every command (to suppress non-debug output), which collided with the literal `>` redirect in set_up_backup_cronjob() and truncated /etc/cron.d/simplerisk to 0 bytes instead of writing the cron entry - silently disabling the scheduled backup cron entirely. Piping through tee instead avoids the collision (matching the existing convention already used elsewhere in this script for writing files via exec_cmd, e.g. the apt source list entries). Caught by running the actual patched function against a real cron daemon in Docker across all three supported OS families. Co-Authored-By: Claude Sonnet 5 --- simplerisk-setup.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/simplerisk-setup.sh b/simplerisk-setup.sh index 98abc0f..32f1c0f 100755 --- a/simplerisk-setup.sh +++ b/simplerisk-setup.sh @@ -387,7 +387,11 @@ set_up_backup_cronjob() { # root every minute. Running it as the web account instead means an # overwritten cron.php only ever executes with the privileges the web # account already has. - exec_cmd "echo '* * * * * ${1} $(which php) -f /var/www/simplerisk/cron/cron.php' > /etc/cron.d/simplerisk" + # Piped through tee rather than a literal `>` redirect: exec_cmd_nobail + # appends its own `> /dev/null 2>&1` to suppress non-debug output, and a + # second stdout redirect in the same command would win, truncating the + # file to empty instead of writing the cron entry. + exec_cmd "echo '* * * * * ${1} $(which php) -f /var/www/simplerisk/cron/cron.php' | tee /etc/cron.d/simplerisk" run_cmd chmod 644 /etc/cron.d/simplerisk } From 47d7a29249b5fbada72e6bcfb98a7ead9f682ff9 Mon Sep 17 00:00:00 2001 From: Josh Sokol Date: Sat, 19 Sep 2026 15:21:25 -0500 Subject: [PATCH 03/11] Fix two openSUSE/SLES install/uninstall bugs found while building CI - setup_suse(): the SUSE mysql-community-server package's /etc/my.cnf has no `!includedir /etc/my.cnf.d`, so the sql_mode drop-in written there is silently never read and STRICT_TRANS_TABLES stays live. Apply the same setting via SET GLOBAL as a safety net, matching how the CentOS/RHEL path already handles this same MySQL 8.4+ behavior. - uninstall_suse(): `zypper -n autoremove` isn't a valid zypper subcommand (unlike apt-get/dnf), so it always failed with "Unknown command" and aborted the rest of the function under set -e before the MySQL repo, firewall rules, and password file were ever removed. Removed the call; zypper has no built-in equivalent. Found and verified via a real install/uninstall run against openSUSE Leap 15.6 in Docker while building the install-test CI workflow. Co-Authored-By: Claude Sonnet 5 --- simplerisk-setup.sh | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/simplerisk-setup.sh b/simplerisk-setup.sh index 32f1c0f..b0da1ac 100755 --- a/simplerisk-setup.sh +++ b/simplerisk-setup.sh @@ -967,6 +967,13 @@ EOF print_status 'Restarting MySQL to load the new configuration...' run_cmd systemctl restart mysql + # The SUSE mysql-community-server package's /etc/my.cnf has no + # `!includedir /etc/my.cnf.d` directive, so the drop-in above is never + # read - apply the same setting live as a safety net, matching the + # CentOS/RHEL path's handling of the same MySQL 8.4+ behavior. + exec_cmd "mysql -uroot -p\"${NEW_MYSQL_ROOT_PASSWORD}\" \ + -e \"SET GLOBAL sql_mode='NO_ENGINE_SUBSTITUTION';\" \ + 2>/dev/null" print_status 'Removing the SimpleRisk database file...' run_cmd rm -r /var/www/simplerisk/database.sql @@ -1121,7 +1128,11 @@ uninstall_suse(){ print_status 'Removing installed packages...' exec_cmd_nobail "zypper -n remove apache2 mysql-community-server 'php8*' apache2-mod_php8" - run_cmd_nobail zypper -n autoremove + # zypper has no built-in orphan-dependency cleanup equivalent to + # `apt-get autoremove`/`dnf autoremove` (a prior `zypper -n autoremove` + # call here always failed with "Unknown command", aborting the rest of + # uninstall_suse under set -e before the MySQL repo, firewall rules, and + # password file below were ever removed). print_status 'Removing MySQL repository and drop-in config...' exec_cmd_nobail 'rpm -e mysql84-community-release-sl15 2>/dev/null || true' From 81dc66fb597c2239826cb603942ed47a733d8ae0 Mon Sep 17 00:00:00 2001 From: Josh Sokol Date: Sat, 19 Sep 2026 15:21:36 -0500 Subject: [PATCH 04/11] Add install/uninstall CI matrix across all supported OS families Ports and extends the Docker-based install/uninstall test suite that previously lived on the abandoned feature/uninstall-support branch (never merged; simplerisk-setup.sh has diverged substantially since). - .github/workflows/install-test.yml: on every push/PR, builds a container per supported OS, runs the real simplerisk-setup.sh --yes, verifies the install, runs --uninstall, and verifies that too. - tests/verify-install.sh / verify-uninstall.sh: updated the cron checks to assert the backup cron runs via /etc/cron.d as the web-server account (not root's crontab) - the old checks asserted the pre-fix, vulnerable behavior from HackerOne #3761952. - Added openSUSE/SLES coverage (previously missing entirely): real SLES requires a live SUSE Customer Center subscription that a public CI container can't have (validate_os_and_version() calls `suseconnect --list-extensions`), so tests/run-suse-function-test.sh instead sources the script and calls setup_suse()/uninstall_suse() directly against openSUSE Leap 15.6, exercising the real apache2/mysql/php8/cron install logic without the subscription-gated entry point. - Dropped Debian 12 from the matrix: current validate_os_and_version() only accepts Debian 13. Matrix: ubuntu-22.04, ubuntu-24.04, debian-13, centos-stream-9, centos-stream-10, opensuse-leap-15. Locally verified end-to-end (install -> verify -> uninstall -> verify) against current main plus the cron fix for ubuntu-22.04 (the "normal" setup()-entry-point path) and opensuse-leap-15 (the new function-level path); the remaining matrix entries reuse Dockerfiles/shims already proven working on the prior branch and will get their first run in CI. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/install-test.yml | 155 +++++++++++++++++ tests/dockerfiles/Dockerfile.centos-stream-10 | 37 +++++ tests/dockerfiles/Dockerfile.centos-stream-9 | 37 +++++ tests/dockerfiles/Dockerfile.debian-13 | 26 +++ tests/dockerfiles/Dockerfile.opensuse-leap-15 | 35 ++++ tests/dockerfiles/Dockerfile.ubuntu-22.04 | 44 +++++ tests/dockerfiles/Dockerfile.ubuntu-24.04 | 35 ++++ tests/dockerfiles/mysql-init-debian.sh | 75 +++++++++ tests/dockerfiles/systemctl-shim-centos.sh | 120 ++++++++++++++ tests/dockerfiles/systemctl-shim-suse.sh | 108 ++++++++++++ tests/run-suse-function-test.sh | 45 +++++ tests/verify-install.sh | 156 ++++++++++++++++++ tests/verify-uninstall.sh | 140 ++++++++++++++++ 13 files changed, 1013 insertions(+) create mode 100644 .github/workflows/install-test.yml create mode 100644 tests/dockerfiles/Dockerfile.centos-stream-10 create mode 100644 tests/dockerfiles/Dockerfile.centos-stream-9 create mode 100644 tests/dockerfiles/Dockerfile.debian-13 create mode 100644 tests/dockerfiles/Dockerfile.opensuse-leap-15 create mode 100644 tests/dockerfiles/Dockerfile.ubuntu-22.04 create mode 100644 tests/dockerfiles/Dockerfile.ubuntu-24.04 create mode 100755 tests/dockerfiles/mysql-init-debian.sh create mode 100755 tests/dockerfiles/systemctl-shim-centos.sh create mode 100644 tests/dockerfiles/systemctl-shim-suse.sh create mode 100644 tests/run-suse-function-test.sh create mode 100755 tests/verify-install.sh create mode 100755 tests/verify-uninstall.sh diff --git a/.github/workflows/install-test.yml b/.github/workflows/install-test.yml new file mode 100644 index 0000000..1e57b34 --- /dev/null +++ b/.github/workflows/install-test.yml @@ -0,0 +1,155 @@ +name: SimpleRisk Install/Uninstall Tests + +on: + push: + branches: [main, "feature/**"] + pull_request: + branches: [main] + workflow_dispatch: + +jobs: + test: + name: "Test: ${{ matrix.os-slug }}" + runs-on: ubuntu-latest + + strategy: + # Run all matrix jobs even if one fails so we get a full picture + fail-fast: false + matrix: + os-slug: + - ubuntu-22.04 + - ubuntu-24.04 + - debian-13 + - centos-stream-9 + - centos-stream-10 + - opensuse-leap-15 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + # ── Build ─────────────────────────────────────────────────────────────── + - name: Build test image + run: | + docker build \ + -f tests/dockerfiles/Dockerfile.${{ matrix.os-slug }} \ + -t simplerisk-test:${{ matrix.os-slug }} \ + tests/dockerfiles/ + + # ── Start container ───────────────────────────────────────────────────── + # All OSes use --init (tini as PID 1) so that child processes (Apache + # workers, mysqld) are properly reaped and multiple rapid service + # start/stop/restart calls during apt/dnf post-install triggers work + # correctly. CentOS/RHEL uses the /usr/local/bin/systemctl shim baked + # into the image; no real systemd is needed. + # --privileged is required for ufw/iptables inside Debian/Ubuntu and + # is harmless for CentOS. + - name: Start container + run: | + docker run -d \ + --name simplerisk-test-${{ matrix.os-slug }} \ + --privileged \ + --init \ + simplerisk-test:${{ matrix.os-slug }} \ + /bin/bash -c "tail -f /dev/null" + sleep 2 + + # ── Pre-start services (Ubuntu only) ──────────────────────────────────── + # Ubuntu images pre-install lamp-server^ during the Docker build (with + # policy-rc.d blocking auto-start). Start MySQL and Apache now so that + # the setup script finds them in the expected running state — mirroring + # what would happen on a freshly provisioned real server where the + # package post-install scripts start the services. + - name: Start pre-installed services (Ubuntu only) + if: ${{ startsWith(matrix.os-slug, 'ubuntu') }} + run: | + docker exec simplerisk-test-${{ matrix.os-slug }} service mysql start 2>/dev/null || true + docker exec simplerisk-test-${{ matrix.os-slug }} service apache2 start 2>/dev/null || true + sleep 2 + + # ── Copy files ────────────────────────────────────────────────────────── + - name: Copy scripts into container + run: | + CONTAINER="simplerisk-test-${{ matrix.os-slug }}" + docker cp simplerisk-setup.sh "$CONTAINER:/root/simplerisk-setup.sh" + docker cp tests/verify-install.sh "$CONTAINER:/root/verify-install.sh" + docker cp tests/verify-uninstall.sh "$CONTAINER:/root/verify-uninstall.sh" + docker cp tests/run-suse-function-test.sh "$CONTAINER:/root/run-suse-function-test.sh" + docker exec "$CONTAINER" chmod +x \ + /root/simplerisk-setup.sh \ + /root/verify-install.sh \ + /root/verify-uninstall.sh \ + /root/run-suse-function-test.sh + + # ── Install ───────────────────────────────────────────────────────────── + # openSUSE/SLES can't go through the normal `setup()` entry point in CI: + # validate_os_and_version() requires a real, registered SUSE Customer + # Center subscription (it shells out to `suseconnect`) which a public + # container doesn't have. run-suse-function-test.sh instead sources the + # script and calls setup_suse()/uninstall_suse() directly, so the real + # apache2/mysql/php8/cron install logic is still exercised for real. + - name: Run install + run: | + CONTAINER="simplerisk-test-${{ matrix.os-slug }}" + if [ "${{ matrix.os-slug }}" = "opensuse-leap-15" ]; then + docker exec "$CONTAINER" bash /root/run-suse-function-test.sh install + else + docker exec "$CONTAINER" bash /root/simplerisk-setup.sh --yes --debug + fi + + # ── Verify install ────────────────────────────────────────────────────── + - name: Verify installation + run: | + docker exec simplerisk-test-${{ matrix.os-slug }} \ + bash /root/verify-install.sh + + # ── Uninstall ─────────────────────────────────────────────────────────── + - name: Run uninstall + run: | + CONTAINER="simplerisk-test-${{ matrix.os-slug }}" + if [ "${{ matrix.os-slug }}" = "opensuse-leap-15" ]; then + docker exec "$CONTAINER" bash /root/run-suse-function-test.sh uninstall + else + docker exec "$CONTAINER" bash /root/simplerisk-setup.sh --uninstall --yes --debug + fi + + # ── Verify uninstall ──────────────────────────────────────────────────── + - name: Verify uninstallation + run: | + docker exec simplerisk-test-${{ matrix.os-slug }} \ + bash /root/verify-uninstall.sh + + # ── Diagnostics on failure ────────────────────────────────────────────── + - name: Collect diagnostics on failure + if: failure() + run: | + CONTAINER="simplerisk-test-${{ matrix.os-slug }}" + echo "=== /etc/my.cnf ===" && \ + docker exec "$CONTAINER" cat /etc/my.cnf 2>/dev/null || true + echo "=== /etc/my.cnf.d/ ===" && \ + docker exec "$CONTAINER" ls -la /etc/my.cnf.d/ 2>/dev/null || true + echo "=== /etc/my.cnf.d/* contents ===" && \ + docker exec "$CONTAINER" sh -c 'for f in /etc/my.cnf.d/*.cnf; do echo "--- $f ---"; cat "$f"; done' 2>/dev/null || true + echo "=== MySQL sql_mode ===" && \ + docker exec "$CONTAINER" sh -c \ + 'PW=$(grep "MYSQL ROOT PASSWORD:" /root/passwords.txt 2>/dev/null | cut -d" " -f4); mysql -uroot -p"$PW" -e "SELECT @@sql_mode;" 2>/dev/null' || true + echo "=== journalctl (last 50 lines) ===" && \ + docker exec "$CONTAINER" journalctl -n 50 2>/dev/null || true + echo "=== Apache error log ===" && \ + docker exec "$CONTAINER" cat /var/log/apache2/error.log 2>/dev/null || \ + docker exec "$CONTAINER" cat /var/log/httpd/error_log 2>/dev/null || \ + docker exec "$CONTAINER" cat /var/log/apache2/error_log 2>/dev/null || true + echo "=== MySQL error log (last 30 lines) ===" && \ + docker exec "$CONTAINER" tail -30 /var/log/mysql/error.log 2>/dev/null || \ + docker exec "$CONTAINER" tail -30 /var/log/mysqld.log 2>/dev/null || true + echo "=== /root/passwords.txt ===" && \ + docker exec "$CONTAINER" cat /root/passwords.txt 2>/dev/null || true + + # ── Teardown ──────────────────────────────────────────────────────────── + - name: Teardown container + if: always() + run: | + docker rm -f simplerisk-test-${{ matrix.os-slug }} 2>/dev/null || true diff --git a/tests/dockerfiles/Dockerfile.centos-stream-10 b/tests/dockerfiles/Dockerfile.centos-stream-10 new file mode 100644 index 0000000..e56f841 --- /dev/null +++ b/tests/dockerfiles/Dockerfile.centos-stream-10 @@ -0,0 +1,37 @@ +FROM quay.io/centos/centos:stream10 + +ENV container=docker + +# --allowerasing lets DNF swap curl-minimal (pre-installed) for full curl. +# cronie (crontab) and which are pre-installed on real RHEL/CentOS servers but +# absent from the minimal container image; add them so the setup script can +# install the backup cron job. +RUN dnf -y install --allowerasing curl wget sudo cronie which && \ + dnf clean all + +# MySQL uses native AIO by default, which fails on Docker's overlayfs driver. +RUN mkdir -p /etc/my.cnf.d && \ + printf '[mysqld]\ninnodb_use_native_aio=0\n' > /etc/my.cnf.d/docker.cnf + +# systemd cannot start in Docker Desktop for Windows (cgroup v2 unavailable). +# This shim replaces /usr/bin/systemctl so the setup script's +# `systemctl start/stop/restart/enable/disable/daemon-reload` calls work +# by managing processes directly instead. +# The script is kept in a separate file to avoid heredoc parsing issues across +# different Docker builder versions. +COPY systemctl-shim-centos.sh /usr/local/bin/systemctl +RUN chmod +x /usr/local/bin/systemctl + +# firewall-cmd, setsebool, and chcon need no-op shims: +# - firewalld cannot run in Docker Desktop (no nftables/iptables backend) +# - SELinux is not enforced in Docker containers on Windows (WSL2) +# Placing shims in /usr/local/bin/ ensures they take precedence over the real +# binaries in /usr/sbin/ when the setup script's PATH is evaluated. +RUN printf '#!/bin/bash\nexit 0\n' > /usr/local/bin/firewall-cmd && \ + chmod +x /usr/local/bin/firewall-cmd && \ + printf '#!/bin/bash\nexit 0\n' > /usr/local/bin/setsebool && \ + chmod +x /usr/local/bin/setsebool && \ + printf '#!/bin/bash\nexit 0\n' > /usr/local/bin/chcon && \ + chmod +x /usr/local/bin/chcon + +CMD ["/bin/bash"] diff --git a/tests/dockerfiles/Dockerfile.centos-stream-9 b/tests/dockerfiles/Dockerfile.centos-stream-9 new file mode 100644 index 0000000..3b55d6f --- /dev/null +++ b/tests/dockerfiles/Dockerfile.centos-stream-9 @@ -0,0 +1,37 @@ +FROM quay.io/centos/centos:stream9 + +ENV container=docker + +# --allowerasing lets DNF swap curl-minimal (pre-installed) for full curl. +# cronie (crontab) and which are pre-installed on real RHEL/CentOS servers but +# absent from the minimal container image; add them so the setup script can +# install the backup cron job. +RUN dnf -y install --allowerasing curl wget sudo cronie which && \ + dnf clean all + +# MySQL uses native AIO by default, which fails on Docker's overlayfs driver. +RUN mkdir -p /etc/my.cnf.d && \ + printf '[mysqld]\ninnodb_use_native_aio=0\n' > /etc/my.cnf.d/docker.cnf + +# systemd cannot start in Docker Desktop for Windows (cgroup v2 unavailable). +# This shim replaces /usr/bin/systemctl so the setup script's +# `systemctl start/stop/restart/enable/disable/daemon-reload` calls work +# by managing processes directly instead. +# The script is kept in a separate file to avoid heredoc parsing issues across +# different Docker builder versions. +COPY systemctl-shim-centos.sh /usr/local/bin/systemctl +RUN chmod +x /usr/local/bin/systemctl + +# firewall-cmd, setsebool, and chcon need no-op shims: +# - firewalld cannot run in Docker Desktop (no nftables/iptables backend) +# - SELinux is not enforced in Docker containers on Windows (WSL2) +# Placing shims in /usr/local/bin/ ensures they take precedence over the real +# binaries in /usr/sbin/ when the setup script's PATH is evaluated. +RUN printf '#!/bin/bash\nexit 0\n' > /usr/local/bin/firewall-cmd && \ + chmod +x /usr/local/bin/firewall-cmd && \ + printf '#!/bin/bash\nexit 0\n' > /usr/local/bin/setsebool && \ + chmod +x /usr/local/bin/setsebool && \ + printf '#!/bin/bash\nexit 0\n' > /usr/local/bin/chcon && \ + chmod +x /usr/local/bin/chcon + +CMD ["/bin/bash"] diff --git a/tests/dockerfiles/Dockerfile.debian-13 b/tests/dockerfiles/Dockerfile.debian-13 new file mode 100644 index 0000000..d17be3a --- /dev/null +++ b/tests/dockerfiles/Dockerfile.debian-13 @@ -0,0 +1,26 @@ +FROM debian:13 + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + sudo \ + curl \ + wget \ + lsb-release \ + ca-certificates && \ + rm -rf /var/lib/apt/lists/* + +# MySQL uses native AIO by default, which fails on Docker's overlayfs storage driver. +RUN mkdir -p /etc/mysql/conf.d && \ + printf '[mysqld]\ninnodb_use_native_aio=0\n' > /etc/mysql/conf.d/docker.cnf + +# MySQL 8.4 community server on Debian ships only systemd units (no init.d script). +# This wrapper lets `service mysql start/stop/restart/status` work in a non-systemd +# container by invoking mysqld directly via the helpers bundled with the package. +# The script is kept in a separate file to avoid heredoc parsing issues across +# different Docker builder versions. +COPY mysql-init-debian.sh /etc/init.d/mysql +RUN chmod +x /etc/init.d/mysql + +CMD ["/bin/bash"] diff --git a/tests/dockerfiles/Dockerfile.opensuse-leap-15 b/tests/dockerfiles/Dockerfile.opensuse-leap-15 new file mode 100644 index 0000000..1c228ad --- /dev/null +++ b/tests/dockerfiles/Dockerfile.opensuse-leap-15 @@ -0,0 +1,35 @@ +FROM opensuse/leap:15.6 + +ENV container=docker + +# simplerisk-setup.sh's SLES branch requires a real SUSE Customer Center +# subscription (validate_os_and_version calls `suseconnect --list-extensions` +# to verify the PHP module is licensed), which a public CI container can't +# satisfy. This image is instead used to exercise setup_suse()/uninstall_suse() +# directly (see tests/run-suse-function-test.sh), skipping the OS/subscription +# gate but running the real apache2/mysql/php8/cron install logic — openSUSE +# Leap 15.6 tracks SLES 15 SP6 packaging closely enough for that purpose. +# `which` and `cron` are pre-installed on a real SLES server but absent here; +# `shadow` provides useradd/groupadd used by the mysql-community-server RPM. +RUN zypper --non-interactive install curl wget sudo which cron tar gzip shadow && \ + zypper clean --all + +# MySQL uses native AIO by default, which fails on Docker's overlayfs driver. +RUN mkdir -p /etc/my.cnf.d && \ + printf '[mysqld]\ninnodb_use_native_aio=0\n' > /etc/my.cnf.d/docker.cnf + +# systemd cannot start in this container (no cgroup delegation). This shim +# replaces /usr/bin/systemctl so setup_suse()'s +# `systemctl start/stop/restart/enable/disable` calls work by managing +# processes directly instead. Kept in a separate file to avoid heredoc +# parsing issues across different Docker builder versions. +COPY systemctl-shim-suse.sh /usr/local/bin/systemctl +RUN chmod +x /usr/local/bin/systemctl + +# firewall-cmd needs a no-op shim: firewalld cannot run in Docker (no +# nftables/iptables backend available). Placing it in /usr/local/bin/ ensures +# it takes precedence over the real binary in /usr/sbin/ on PATH. +RUN printf '#!/bin/bash\nexit 0\n' > /usr/local/bin/firewall-cmd && \ + chmod +x /usr/local/bin/firewall-cmd + +CMD ["/bin/bash"] diff --git a/tests/dockerfiles/Dockerfile.ubuntu-22.04 b/tests/dockerfiles/Dockerfile.ubuntu-22.04 new file mode 100644 index 0000000..fa42ac9 --- /dev/null +++ b/tests/dockerfiles/Dockerfile.ubuntu-22.04 @@ -0,0 +1,44 @@ +FROM ubuntu:22.04 + +ENV DEBIAN_FRONTEND=noninteractive +ENV container=docker + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + sudo \ + curl \ + wget \ + lsb-release \ + ca-certificates && \ + rm -rf /var/lib/apt/lists/* + +# MySQL uses native AIO by default, which fails on Docker's overlayfs storage +# driver during the post-install data directory initialisation. Pre-creating +# this config disables AIO so that `lamp-server^` installs cleanly. +RUN mkdir -p /etc/mysql/conf.d && \ + printf '[mysqld]\ninnodb_use_native_aio=0\n' > /etc/mysql/conf.d/docker.cnf + +# DENY all service operations during the lamp-server^ pre-install below. +# This is the standard Docker pattern: MySQL's post-install script tries to +# start mysqld for initialization and then shut it down — the shutdown step +# times out inside Docker's overlayfs because the process can't be signalled +# cleanly. By denying service actions here, dpkg skips start/stop but still +# initialises the data directory via mysqld --initialize, which is sufficient. +RUN printf '#!/bin/sh\nexit 101\n' > /usr/sbin/policy-rc.d && \ + chmod +x /usr/sbin/policy-rc.d + +# Pre-install lamp-server^ so that when the setup script calls +# `apt-get install -y lamp-server^` during the test run, all packages are +# already present and dpkg has nothing to reconfigure. +RUN apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y lamp-server^ && \ + rm -rf /var/lib/apt/lists/* + +# Switch back to ALLOW so the setup script can start/stop services normally +# during the test (service mysql restart, service apache2 restart, etc.). +RUN printf '#!/bin/sh\nexit 0\n' > /usr/sbin/policy-rc.d && \ + chmod +x /usr/sbin/policy-rc.d + +# No systemd required — the script uses `service` on Ubuntu/Debian, +# which invokes /etc/init.d wrappers and works in containers. +CMD ["/bin/bash"] diff --git a/tests/dockerfiles/Dockerfile.ubuntu-24.04 b/tests/dockerfiles/Dockerfile.ubuntu-24.04 new file mode 100644 index 0000000..ea8c31d --- /dev/null +++ b/tests/dockerfiles/Dockerfile.ubuntu-24.04 @@ -0,0 +1,35 @@ +FROM ubuntu:24.04 + +ENV DEBIAN_FRONTEND=noninteractive +ENV container=docker + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + sudo \ + curl \ + wget \ + lsb-release \ + ca-certificates && \ + rm -rf /var/lib/apt/lists/* + +# MySQL uses native AIO by default, which fails on Docker's overlayfs storage +# driver during the post-install data directory initialisation. +RUN mkdir -p /etc/mysql/conf.d && \ + printf '[mysqld]\ninnodb_use_native_aio=0\n' > /etc/mysql/conf.d/docker.cnf + +# DENY all service operations during the lamp-server^ pre-install below. +# MySQL's post-install shutdown times out inside Docker; denying start/stop +# lets dpkg initialise the data directory without triggering the timeout. +RUN printf '#!/bin/sh\nexit 101\n' > /usr/sbin/policy-rc.d && \ + chmod +x /usr/sbin/policy-rc.d + +# Pre-install lamp-server^ so packages are already present for the test run. +RUN apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y lamp-server^ && \ + rm -rf /var/lib/apt/lists/* + +# Switch back to ALLOW so the setup script can start/stop services normally. +RUN printf '#!/bin/sh\nexit 0\n' > /usr/sbin/policy-rc.d && \ + chmod +x /usr/sbin/policy-rc.d + +CMD ["/bin/bash"] diff --git a/tests/dockerfiles/mysql-init-debian.sh b/tests/dockerfiles/mysql-init-debian.sh new file mode 100755 index 0000000..eea5af5 --- /dev/null +++ b/tests/dockerfiles/mysql-init-debian.sh @@ -0,0 +1,75 @@ +#!/bin/bash +### BEGIN INIT INFO +# Provides: mysql +# Required-Start: $local_fs $network +# Required-Stop: $local_fs +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: MySQL Community Server +# Description: MySQL Community Server wrapper for non-systemd containers +### END INIT INFO + +PIDFILE=/var/run/mysqld/mysqld.pid +MYSQLD=/usr/sbin/mysqld +HELPERS=/usr/share/mysql-8.4/mysql-helpers + +do_start() { + if [ ! -x "$MYSQLD" ]; then + echo "mysqld not found at $MYSQLD" >&2 + return 1 + fi + # Source helpers for verify_ready / verify_database / get_running + . "$HELPERS" + verify_ready "" + verify_database "" + if [ "$(get_running)" = "1" ]; then + echo "MySQL is already running" + return 0 + fi + mkdir -p /var/run/mysqld && chown mysql:mysql /var/run/mysqld + su -s /bin/bash mysql -c "$MYSQLD --daemonize --pid-file=$PIDFILE" 2>&1 + # Wait for MySQL to accept connections (up to 30s) + local i=0 + while [ $i -lt 30 ]; do + mysqladmin ping --silent >/dev/null 2>&1 && return 0 + sleep 1 + i=$((i+1)) + done + echo "MySQL did not start within 30 seconds" >&2 + return 1 +} + +do_stop() { + if [ -f "$PIDFILE" ]; then + local pid + pid=$(cat "$PIDFILE" 2>/dev/null) || return 0 + kill "$pid" 2>/dev/null || true + local i=0 + while [ -d "/proc/$pid" ] && [ $i -lt 20 ]; do + sleep 1 + i=$((i+1)) + done + rm -f "$PIDFILE" + fi + return 0 +} + +do_status() { + if [ ! -x "$MYSQLD" ]; then + return 3 + fi + . "$HELPERS" + if [ "$(get_running)" = "1" ]; then + return 0 + else + return 1 + fi +} + +case "$1" in + start) do_start ;; + stop) do_stop ;; + restart|force-reload) do_stop; sleep 1; do_start ;; + status) do_status ;; + *) echo "Usage: $0 {start|stop|restart|force-reload|status}"; exit 1 ;; +esac diff --git a/tests/dockerfiles/systemctl-shim-centos.sh b/tests/dockerfiles/systemctl-shim-centos.sh new file mode 100755 index 0000000..4ca6202 --- /dev/null +++ b/tests/dockerfiles/systemctl-shim-centos.sh @@ -0,0 +1,120 @@ +#!/bin/bash +# Minimal systemctl shim — handles the subset used by simplerisk-setup.sh +# on CentOS/RHEL without requiring a running systemd PID 1. + +# Strip --quiet / --system / other flags; find the action and unit name. +args=() +for arg in "$@"; do + [[ "$arg" == --* ]] && continue + args+=("$arg") +done +action="${args[0]:-}" +unit="${args[1]%.service}" # strip optional .service suffix + +start_mysqld() { + mysqladmin ping --silent >/dev/null 2>&1 && return 0 # already running + mkdir -p /var/run/mysqld && chown mysql:mysql /var/run/mysqld 2>/dev/null || true + # Pre-create the error log with mysql ownership so mysqld (running as mysql user) + # can open it for writing internally in addition to the shell redirect. + touch /var/log/mysqld.log && chown mysql:mysql /var/log/mysqld.log 2>/dev/null || true + # The RPM %post scriptlet may leave /var/lib/mysql in a partial state when + # systemd is unavailable (auto.cnf + binlog.index but no ibdata1). + # Re-initialize if ibdata1 is missing. + if [ ! -f /var/lib/mysql/ibdata1 ]; then + rm -f /var/lib/mysql/auto.cnf /var/lib/mysql/binlog.index 2>/dev/null || true + # Use --initialize (not --insecure) so a temporary root password is written + # to /var/log/mysqld.log as a "Note" line for simplerisk-setup.sh to read. + mysqld --initialize --user=mysql >>/var/log/mysqld.log 2>&1 + fi + # mysqld_safe and --daemonize were removed in MySQL 8.4; run as background process. + # Use --init-file to install the validate_password component at startup: + # the community RPM does not pre-install it when systemd is absent, but + # simplerisk-setup.sh calls SET GLOBAL validate_password.policy=LOW. + # Errors in --init-file are non-fatal, so this is safe on subsequent restarts + # when the component is already installed. + printf "INSTALL COMPONENT 'file://component_validate_password';\n" > /var/lib/mysql/docker-init.sql + # Append (>>) so the initialization "Note: temp password" line is preserved. + nohup mysqld --user=mysql --init-file=/var/lib/mysql/docker-init.sql >>/var/log/mysqld.log 2>&1 & + local i=0 + while [ $i -lt 60 ]; do + mysqladmin ping --silent >/dev/null 2>&1 && return 0 + sleep 1; i=$((i+1)) + done + echo "systemctl shim: mysqld did not start within 60s" >&2; return 1 +} + +stop_mysqld() { + # Send SIGTERM directly to the mysqld process so that no root password is + # needed (mysqladmin shutdown requires auth after setup changes the password). + local pidfile=/var/run/mysqld/mysqld.pid + if [ -f "$pidfile" ]; then + local pid + pid=$(cat "$pidfile" 2>/dev/null) + [ -n "$pid" ] && kill -TERM "$pid" 2>/dev/null || true + else + pkill -TERM mysqld 2>/dev/null || true + fi + # Wait for mysqld to fully stop before returning so that the subsequent + # start_mysqld call does not find MySQL still running and skip the restart. + local i=0 + while mysqladmin ping --silent >/dev/null 2>&1 && [ $i -lt 30 ]; do + sleep 1; i=$((i+1)) + done +} + +start_httpd() { + pgrep httpd >/dev/null 2>&1 && return 0 + # The mod_ssl %post scriptlet may skip cert generation without systemd. + # Generate a self-signed cert if missing so httpd can start. + if [ ! -f /etc/pki/tls/certs/localhost.crt ]; then + openssl req -newkey rsa:2048 -nodes \ + -keyout /etc/pki/tls/private/localhost.key \ + -x509 -days 365 \ + -out /etc/pki/tls/certs/localhost.crt \ + -subj '/CN=localhost' 2>/dev/null + fi + httpd -k start 2>/dev/null +} + +stop_httpd() { + httpd -k stop 2>/dev/null || true +} + +case "$action" in + start) + case "$unit" in + mysqld|mysql) start_mysqld ;; + httpd) start_httpd ;; + sendmail) exit 0 ;; # no-op: sendmail cannot run without systemd + firewalld) exit 0 ;; # no-op: firewalld not available in Docker + *) echo "systemctl shim: unsupported unit '$unit'" >&2; exit 1 ;; + esac ;; + stop) + case "$unit" in + mysqld|mysql) stop_mysqld ;; + httpd) stop_httpd ;; + *) exit 0 ;; # non-fatal for unknown units on uninstall + esac ;; + restart) + case "$unit" in + mysqld|mysql) stop_mysqld; sleep 1; start_mysqld ;; + httpd) httpd -k restart 2>/dev/null ;; + *) echo "systemctl shim: unsupported unit '$unit'" >&2; exit 1 ;; + esac ;; + is-active) + case "$unit" in + mysqld|mysql) mysqladmin ping --silent >/dev/null 2>&1 ;; + httpd) pgrep httpd >/dev/null 2>&1 ;; + *) exit 1 ;; + esac ;; + status) + case "$unit" in + mysqld|mysql) mysqladmin ping --silent >/dev/null 2>&1 && echo "active" || exit 3 ;; + httpd) pgrep httpd >/dev/null 2>&1 && echo "active" || exit 3 ;; + *) exit 3 ;; + esac ;; + enable|disable|daemon-reload|mask|unmask|is-enabled|reset-failed) + exit 0 ;; # no-op — we don't manage boot-time units + *) + echo "systemctl shim: unknown action '$action'" >&2; exit 1 ;; +esac diff --git a/tests/dockerfiles/systemctl-shim-suse.sh b/tests/dockerfiles/systemctl-shim-suse.sh new file mode 100644 index 0000000..bae3bd8 --- /dev/null +++ b/tests/dockerfiles/systemctl-shim-suse.sh @@ -0,0 +1,108 @@ +#!/bin/bash +# Minimal systemctl shim — handles the subset used by simplerisk-setup.sh's +# setup_suse()/uninstall_suse() without requiring a running systemd PID 1. +# Mirrors systemctl-shim-centos.sh; see that file for the general approach. + +args=() +for arg in "$@"; do + [[ "$arg" == --* ]] && continue + args+=("$arg") +done +action="${args[0]:-}" +unit="${args[1]%.service}" + +start_mysqld() { + mysqladmin ping --silent >/dev/null 2>&1 && return 0 # already running + mkdir -p /var/run/mysqld && chown mysql:mysql /var/run/mysqld 2>/dev/null || true + touch /var/log/mysqld.log && chown mysql:mysql /var/log/mysqld.log 2>/dev/null || true + # The RPM %post scriptlet may leave /var/lib/mysql in a partial state when + # systemd is unavailable (auto.cnf + binlog.index but no ibdata1). + if [ ! -f /var/lib/mysql/ibdata1 ]; then + rm -f /var/lib/mysql/auto.cnf /var/lib/mysql/binlog.index 2>/dev/null || true + mysqld --initialize --user=mysql >>/var/log/mysqld.log 2>&1 + fi + printf "INSTALL COMPONENT 'file://component_validate_password';\n" > /var/lib/mysql/docker-init.sql + nohup mysqld --user=mysql --init-file=/var/lib/mysql/docker-init.sql >>/var/log/mysqld.log 2>&1 & + local i=0 + while [ $i -lt 60 ]; do + mysqladmin ping --silent >/dev/null 2>&1 && return 0 + sleep 1; i=$((i+1)) + done + echo "systemctl shim: mysqld did not start within 60s" >&2; return 1 +} + +stop_mysqld() { + local pidfile=/var/run/mysqld/mysqld.pid + if [ -f "$pidfile" ]; then + local pid + pid=$(cat "$pidfile" 2>/dev/null) + [ -n "$pid" ] && kill -TERM "$pid" 2>/dev/null || true + else + pkill -TERM mysqld 2>/dev/null || true + fi + local i=0 + while mysqladmin ping --silent >/dev/null 2>&1 && [ $i -lt 30 ]; do + sleep 1; i=$((i+1)) + done +} + +start_apache2() { + pgrep -x httpd-prefork >/dev/null 2>&1 && return 0 + # openSUSE ships no apache2ctl and no init.d script without a live + # systemd; /usr/sbin/start_apache2 is the actual launcher, and Apache's + # own -k start/stop/restart daemon-control flags handle backgrounding + # and the pidfile without needing systemd. + /usr/sbin/start_apache2 -k start 2>&1 +} + +stop_apache2() { + /usr/sbin/start_apache2 -k stop 2>/dev/null || true +} + +start_cron() { + pgrep cron >/dev/null 2>&1 && return 0 + cron +} + +case "$action" in + start) + case "$unit" in + mysql|mysqld) start_mysqld ;; + apache2) start_apache2 ;; + cron) start_cron ;; + firewalld) exit 0 ;; # no-op: firewalld not available in Docker + *) echo "systemctl shim: unsupported unit '$unit'" >&2; exit 1 ;; + esac ;; + stop) + case "$unit" in + mysql|mysqld) stop_mysqld ;; + apache2) stop_apache2 ;; + *) exit 0 ;; # non-fatal for unknown units on uninstall + esac ;; + restart) + case "$unit" in + mysql|mysqld) stop_mysqld; sleep 1; start_mysqld ;; + apache2) stop_apache2; sleep 1; start_apache2 ;; + *) echo "systemctl shim: unsupported unit '$unit'" >&2; exit 1 ;; + esac ;; + is-active) + case "$unit" in + mysql|mysqld) mysqladmin ping --silent >/dev/null 2>&1 ;; + apache2) pgrep -x httpd-prefork >/dev/null 2>&1 ;; + cron) pgrep cron >/dev/null 2>&1 ;; + *) exit 1 ;; + esac ;; + status) + case "$unit" in + mysql|mysqld) mysqladmin ping --silent >/dev/null 2>&1 && echo "active" || exit 3 ;; + apache2) pgrep -x httpd-prefork >/dev/null 2>&1 && echo "active" || exit 3 ;; + *) exit 3 ;; + esac ;; + enable|disable|daemon-reload|mask|unmask|is-enabled|reset-failed) + case "$unit" in + cron) [ "$action" = "enable" ] && start_cron; exit 0 ;; + *) exit 0 ;; # no-op — we don't manage boot-time units + esac ;; + *) + echo "systemctl shim: unknown action '$action'" >&2; exit 1 ;; +esac diff --git a/tests/run-suse-function-test.sh b/tests/run-suse-function-test.sh new file mode 100644 index 0000000..c87d1b9 --- /dev/null +++ b/tests/run-suse-function-test.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# run-suse-function-test.sh — drives setup_suse()/uninstall_suse() directly. +# +# The full `setup()` entry point can't be exercised against a plain openSUSE +# Leap container: validate_os_and_version() requires /etc/os-release to say +# NAME=SLES exactly, and its SLES branch calls `suseconnect --list-extensions` +# to verify a licensed PHP module — both of which need a real, registered SUSE +# subscription that a public CI container doesn't have. This script instead +# sources the real, unmodified simplerisk-setup.sh (stripping only the +# trailing auto-invocation) and calls the install/uninstall functions +# directly, so the actual apache2/mysql/php8/cron logic in setup_suse() and +# uninstall_suse() still gets exercised for real on every change. +set -euo pipefail + +SCRIPT=/root/simplerisk-setup.sh +ACTION="${1:?usage: run-suse-function-test.sh install|uninstall}" + +# Strip the trailing `setup "${@:1}"` auto-invocation so sourcing the file +# only defines functions/readonly vars, matching the technique used to +# reproduce HackerOne #3761952. +LAST_LINE=$(grep -Fn 'setup "${@:1}"' "$SCRIPT" | tail -1 | cut -d: -f1) +head -n "$((LAST_LINE - 1))" "$SCRIPT" > /tmp/sr-functions.sh +# shellcheck disable=SC1091 +source /tmp/sr-functions.sh + +case "$ACTION" in + install) + DEBUG=y + # setup_suse() references ${VER} (e.g. to gate PHP/rewrite module + # enabling and the "SLES 15 has no sendmail" notice) which is + # normally set by load_os_variables() from /etc/os-release - + # bypassed here along with the rest of setup(). Match a real SLES + # 15 SP's VERSION_ID format. + VER="15.6" + setup_suse "$(get_current_simplerisk_version)" + ;; + uninstall) + DEBUG=y + uninstall_suse + ;; + *) + echo "unknown action: $ACTION" >&2 + exit 1 + ;; +esac diff --git a/tests/verify-install.sh b/tests/verify-install.sh new file mode 100755 index 0000000..0a10f85 --- /dev/null +++ b/tests/verify-install.sh @@ -0,0 +1,156 @@ +#!/usr/bin/env bash +# verify-install.sh — Run inside a container after simplerisk-setup.sh --yes +# to assert that the installation completed correctly. +# Exits 0 if all checks pass, 1 if any check fails. + +set -uo pipefail + +PASS=0 +FAIL=0 +ERRORS=() + +# check [args...] +# Runs the command silently; records pass/fail without aborting on failure. +check() { + local description="$1" + shift + local result=0 + "$@" > /dev/null 2>&1 || result=$? + if [ "$result" -eq 0 ]; then + echo " PASS: $description" + PASS=$((PASS + 1)) + else + echo " FAIL: $description" + ERRORS+=("$description") + FAIL=$((FAIL + 1)) + fi +} + +echo "=== SimpleRisk Install Verification ===" +echo "" + +# ── File system ────────────────────────────────────────────────────────────── +echo "--- File system ---" +check "SimpleRisk directory exists" test -d /var/www/simplerisk +check "index.php exists" test -f /var/www/simplerisk/index.php +check "config.php exists" test -f /var/www/simplerisk/includes/config.php +check "database.sql was removed post-install" test ! -f /var/www/simplerisk/database.sql +check "cron script exists" test -f /var/www/simplerisk/cron/cron.php + +# ── config.php content ─────────────────────────────────────────────────────── +echo "--- config.php ---" +# Newer SimpleRisk ships config.sample.php (a template of __PLACEHOLDER__ +# tokens) and treats the existence of config.php as the install marker, so +# there is no longer a SIMPLERISK_INSTALLED flag. Verify the installer +# substituted the placeholders instead. +check "config.php placeholders were substituted" \ + bash -c "! grep -qE \"define[(][^)]*__[A-Z_]+__\" /var/www/simplerisk/includes/config.php" +check "DB_PASSWORD is populated (not placeholder or default)" \ + bash -c "! grep -qE \"DB_PASSWORD', '(__DB_PASSWORD__|simplerisk)'\" /var/www/simplerisk/includes/config.php" + +# ── Passwords file ─────────────────────────────────────────────────────────── +echo "--- /root/passwords.txt ---" +check "passwords.txt exists" test -f /root/passwords.txt +check "passwords.txt has mode 600" \ + bash -c '[ "$(stat -c %a /root/passwords.txt)" = "600" ]' +check "passwords.txt contains MySQL root password entry" \ + grep -q "MYSQL ROOT PASSWORD:" /root/passwords.txt +check "passwords.txt contains MySQL simplerisk password entry" \ + grep -q "MYSQL SIMPLERISK PASSWORD:" /root/passwords.txt + +MYSQL_ROOT_PW=$(grep "MYSQL ROOT PASSWORD:" /root/passwords.txt 2>/dev/null | awk -F ': ' '{print $2}') +MYSQL_SR_PW=$(grep "MYSQL SIMPLERISK PASSWORD:" /root/passwords.txt 2>/dev/null | awk -F ': ' '{print $2}') + +check "MySQL root password is non-empty" test -n "${MYSQL_ROOT_PW:-}" +check "MySQL simplerisk password is non-empty" test -n "${MYSQL_SR_PW:-}" + +# ── MySQL ──────────────────────────────────────────────────────────────────── +echo "--- MySQL ---" +check "MySQL is reachable with root password" \ + mysql -uroot --password="${MYSQL_ROOT_PW:-}" -e "SELECT 1;" +check "simplerisk database exists" \ + bash -c "mysql -uroot --password='${MYSQL_ROOT_PW:-}' -e 'SHOW DATABASES;' 2>/dev/null | grep -q simplerisk" +check "simplerisk user can connect" \ + mysql -usimplerisk --password="${MYSQL_SR_PW:-}" simplerisk -e "SELECT 1;" +check "sql_mode does not contain STRICT_TRANS_TABLES" \ + bash -c "! mysql -uroot --password='${MYSQL_ROOT_PW:-}' -e 'SELECT @@sql_mode;' 2>/dev/null | grep -q STRICT_TRANS_TABLES" + +# ── Cron job ───────────────────────────────────────────────────────────────── +# The backup cron runs as the web-server account (not root): cron/cron.php is +# owned and writable by that account, so running it as root would let anyone +# who can write that file escalate to root every minute. See HackerOne #3761952. +echo "--- Cron ---" +if grep -qiE "ubuntu|debian" /etc/os-release 2>/dev/null; then + WEB_USER=www-data +elif grep -qiE "centos|red hat|rocky|alma" /etc/os-release 2>/dev/null; then + WEB_USER=apache +elif grep -qiE "suse" /etc/os-release 2>/dev/null; then + WEB_USER=wwwrun +else + WEB_USER="" +fi + +check "cron.d entry for the SimpleRisk backup exists" test -f /etc/cron.d/simplerisk +check "Backup cron entry runs as the web-server account ('$WEB_USER')" \ + bash -c "grep -qE '^\* \* \* \* \* ${WEB_USER} ' /etc/cron.d/simplerisk" +check "Backup cron entry does not run as root" \ + bash -c "! grep -qE '^\* \* \* \* \* root ' /etc/cron.d/simplerisk" +check "Backup cron job is not in root's crontab" \ + bash -c "! (crontab -l 2>/dev/null | grep -q 'simplerisk/cron/cron.php')" + +# ── PHP ────────────────────────────────────────────────────────────────────── +echo "--- PHP ---" +check "PHP CLI is functional" php -r "echo 'OK';" +check "PHP version is 8.x" bash -c "php --version | grep -qE '^PHP 8\.'" + +for ext in mysqli mbstring xml curl gd zip intl ldap; do + check "PHP extension '$ext' is loaded" php -m | grep -qi "$ext" +done + +# ── Web server (OS-conditional) ─────────────────────────────────────────────── +echo "--- Web server ---" +if grep -qi suse /etc/os-release 2>/dev/null; then + # openSUSE/SLES: apache2-prefork's own /usr/sbin/httpd compatibility + # symlink means `command -v httpd` below would otherwise misdetect this + # as CentOS/RHEL, so this branch must be checked first. The real binary + # is httpd-prefork/httpd-worker, started via /usr/sbin/start_apache2 + # (there is no apache2ctl and no init.d script without a live systemd). + check "Apache config syntax is valid" bash -c "/usr/sbin/start_apache2 -t 2>&1 | grep -q 'Syntax OK'" + check "Apache service is running" pgrep -x httpd-prefork > /dev/null + check "MySQL service is running" mysqladmin ping --silent +elif command -v apache2 > /dev/null 2>&1; then + # Debian/Ubuntu + # apache2 -t requires env vars (APACHE_RUN_DIR etc.) that are only set by + # apache2ctl. Use apache2ctl -t so the environment is populated correctly. + check "Apache config syntax is valid" bash -c "apache2ctl -t 2>&1 | grep -q 'Syntax OK'" + check "Apache service is running" service apache2 status + check "MySQL service is running" bash -c "service mysql status 2>/dev/null || service mysqld status 2>/dev/null" +elif command -v httpd > /dev/null 2>&1; then + # CentOS/RHEL + check "Apache config syntax is valid" bash -c "httpd -t 2>&1 | grep -q 'Syntax OK'" + check "httpd service is running (systemctl)" systemctl is-active --quiet httpd + check "mysqld service is running (systemctl)" systemctl is-active --quiet mysqld +fi + +# ── HTTP reachability ──────────────────────────────────────────────────────── +echo "--- HTTP ---" +check "HTTP on port 80 returns a redirect or 200" \ + bash -c "curl -sk -o /dev/null -w '%{http_code}' http://localhost/ | grep -qE '^(200|301|302)$'" + +# ── Summary ────────────────────────────────────────────────────────────────── +echo "" +echo "=== Verification Summary ===" +echo " Passed : $PASS" +echo " Failed : $FAIL" + +if [ "$FAIL" -gt 0 ]; then + echo "" + echo " Failed checks:" + for err in "${ERRORS[@]}"; do + echo " - $err" + done + exit 1 +fi + +echo " All checks passed." +exit 0 diff --git a/tests/verify-uninstall.sh b/tests/verify-uninstall.sh new file mode 100755 index 0000000..55f58bf --- /dev/null +++ b/tests/verify-uninstall.sh @@ -0,0 +1,140 @@ +#!/usr/bin/env bash +# verify-uninstall.sh — Run inside a container after simplerisk-setup.sh --uninstall --yes +# to assert that all SimpleRisk components were cleanly removed. +# Exits 0 if all checks pass, 1 if any check fails. + +set -uo pipefail + +PASS=0 +FAIL=0 +ERRORS=() + +# check [args...] +check() { + local description="$1" + shift + local result=0 + "$@" > /dev/null 2>&1 || result=$? + if [ "$result" -eq 0 ]; then + echo " PASS: $description" + PASS=$((PASS + 1)) + else + echo " FAIL: $description" + ERRORS+=("$description") + FAIL=$((FAIL + 1)) + fi +} + +echo "=== SimpleRisk Uninstall Verification ===" +echo "" + +# ── File system ────────────────────────────────────────────────────────────── +echo "--- File system ---" +check "SimpleRisk directory was removed" test ! -d /var/www/simplerisk +check "passwords.txt was removed" test ! -f /root/passwords.txt + +# ── Cron job ───────────────────────────────────────────────────────────────── +echo "--- Cron ---" +check "cron.d entry for the SimpleRisk backup was removed" \ + test ! -f /etc/cron.d/simplerisk +check "Backup cron job was removed from root's crontab" \ + bash -c "! (crontab -l 2>/dev/null | grep -q 'simplerisk/cron/cron.php')" + +# ── MySQL database ─────────────────────────────────────────────────────────── +echo "--- MySQL database ---" +if command -v mysql > /dev/null 2>&1; then + # MySQL client still installed (partially removed or not purged) — attempt + # socket auth as root and verify the database is gone. + check "simplerisk database was dropped" \ + bash -c "! mysql -uroot 2>/dev/null -e 'USE simplerisk;'" + check "simplerisk MySQL user was dropped" \ + bash -c "! mysql -uroot 2>/dev/null -e \"SELECT User FROM mysql.user WHERE User='simplerisk';\" | grep -q simplerisk" +else + echo " PASS: mysql client not present (packages removed)" + PASS=$((PASS + 1)) +fi + +# ── OS-specific package and config checks ──────────────────────────────────── +if grep -qi ubuntu /etc/os-release 2>/dev/null || grep -qi debian /etc/os-release 2>/dev/null; then + + echo "--- Packages (Debian/Ubuntu) ---" + check "apache2 package is removed" \ + bash -c "! dpkg -l apache2 2>/dev/null | grep -q '^ii'" + check "mysql-server package is removed" \ + bash -c "! dpkg -l mysql-server 2>/dev/null | grep -q '^ii'" + check "PHP packages are removed" \ + bash -c "! dpkg -l 'php*' 2>/dev/null | grep -qE '^ii.*php[0-9]'" + check "sendmail package is removed" \ + bash -c "! dpkg -l sendmail 2>/dev/null | grep -q '^ii'" + + if grep -qi debian /etc/os-release 2>/dev/null; then + echo "--- Repositories (Debian) ---" + check "sury-php.list was removed" \ + test ! -f /etc/apt/sources.list.d/sury-php.list + check "mysql.list was removed" \ + test ! -f /etc/apt/sources.list.d/mysql.list + check "sury-php GPG key was removed" \ + test ! -f /etc/apt/keyrings/sury-php.gpg + check "MySQL GPG key was removed" \ + test ! -f /etc/apt/trusted.gpg.d/mysql.gpg + fi + +elif grep -qi "centos\|red hat" /etc/os-release 2>/dev/null; then + + echo "--- Packages (CentOS/RHEL) ---" + check "httpd package is removed" \ + bash -c "! rpm -q httpd" + check "mysql-community-server package is removed" \ + bash -c "! rpm -q mysql-community-server" + check "PHP packages are removed" \ + bash -c "! rpm -qa 'php*' | grep -q ." + + echo "--- Services (CentOS/RHEL) ---" + check "httpd service is not active" \ + bash -c "! systemctl is-active --quiet httpd 2>/dev/null" + check "mysqld service is not active" \ + bash -c "! systemctl is-active --quiet mysqld 2>/dev/null" + + echo "--- Config files (CentOS/RHEL) ---" + check "simplerisk Apache vhost config was removed" \ + test ! -f /etc/httpd/sites-enabled/simplerisk.conf + +elif grep -qi suse /etc/os-release 2>/dev/null; then + + echo "--- Packages (openSUSE/SLES) ---" + check "apache2 package is removed" \ + bash -c "! rpm -q apache2" + check "mysql-community-server package is removed" \ + bash -c "! rpm -q mysql-community-server" + check "PHP packages are removed" \ + bash -c "! rpm -qa 'php8*' | grep -q ." + + echo "--- Services (openSUSE/SLES) ---" + check "Apache is not running" bash -c "! pgrep -x httpd-prefork > /dev/null" + check "MySQL is not running" bash -c "! mysqladmin ping --silent 2>/dev/null" + + echo "--- Config files (openSUSE/SLES) ---" + check "simplerisk Apache vhost config was removed" \ + test ! -f /etc/apache2/vhosts.d/simplerisk.conf + check "simplerisk Apache SSL vhost config was removed" \ + test ! -f /etc/apache2/vhosts.d/ssl.conf + +fi + +# ── Summary ────────────────────────────────────────────────────────────────── +echo "" +echo "=== Uninstall Verification Summary ===" +echo " Passed : $PASS" +echo " Failed : $FAIL" + +if [ "$FAIL" -gt 0 ]; then + echo "" + echo " Failed checks:" + for err in "${ERRORS[@]}"; do + echo " - $err" + done + exit 1 +fi + +echo " All checks passed." +exit 0 From 54043233f8aaa938373075fd2f178f68f518d364 Mon Sep 17 00:00:00 2001 From: Josh Sokol Date: Sat, 19 Sep 2026 15:34:34 -0500 Subject: [PATCH 05/11] Fix SUSE test shim: mysqld log path mismatch broke real GitHub Actions CI start_mysqld() pre-created /var/log/mysqld.log (flat), copied verbatim from the CentOS shim, but setup_suse() passes /var/log/mysql/mysqld.log (with subdirectory) to set_up_database() - matching the package's own my.cnf log-error= directive. Without that directory existing (no live systemd-tmpfiles), set_up_database's grep for the startup temp-password line failed with "No such file or directory", aborting the rest of setup_suse under set -e. Passed locally against Docker Desktop because /var/log/mysql already existed there for unrelated reasons; failed on the real GitHub Actions runner, which is how this was caught. Co-Authored-By: Claude Sonnet 5 --- tests/dockerfiles/systemctl-shim-suse.sh | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/dockerfiles/systemctl-shim-suse.sh b/tests/dockerfiles/systemctl-shim-suse.sh index bae3bd8..fc68b4d 100644 --- a/tests/dockerfiles/systemctl-shim-suse.sh +++ b/tests/dockerfiles/systemctl-shim-suse.sh @@ -14,15 +14,23 @@ unit="${args[1]%.service}" start_mysqld() { mysqladmin ping --silent >/dev/null 2>&1 && return 0 # already running mkdir -p /var/run/mysqld && chown mysql:mysql /var/run/mysqld 2>/dev/null || true - touch /var/log/mysqld.log && chown mysql:mysql /var/log/mysqld.log 2>/dev/null || true + # setup_suse() passes /var/log/mysql/mysqld.log to set_up_database(), + # matching the package's own my.cnf `log-error=` directive - the + # directory doesn't exist without a live systemd-tmpfiles, so mysqld + # falls back to logging elsewhere and set_up_database's grep for the + # startup temp-password line fails ("No such file or directory"), + # aborting the rest of setup_suse. Pre-create it so mysqld logs where + # the script actually looks. + mkdir -p /var/log/mysql && chown mysql:mysql /var/log/mysql 2>/dev/null || true + touch /var/log/mysql/mysqld.log && chown mysql:mysql /var/log/mysql/mysqld.log 2>/dev/null || true # The RPM %post scriptlet may leave /var/lib/mysql in a partial state when # systemd is unavailable (auto.cnf + binlog.index but no ibdata1). if [ ! -f /var/lib/mysql/ibdata1 ]; then rm -f /var/lib/mysql/auto.cnf /var/lib/mysql/binlog.index 2>/dev/null || true - mysqld --initialize --user=mysql >>/var/log/mysqld.log 2>&1 + mysqld --initialize --user=mysql >>/var/log/mysql/mysqld.log 2>&1 fi printf "INSTALL COMPONENT 'file://component_validate_password';\n" > /var/lib/mysql/docker-init.sql - nohup mysqld --user=mysql --init-file=/var/lib/mysql/docker-init.sql >>/var/log/mysqld.log 2>&1 & + nohup mysqld --user=mysql --init-file=/var/lib/mysql/docker-init.sql >>/var/log/mysql/mysqld.log 2>&1 & local i=0 while [ $i -lt 60 ]; do mysqladmin ping --silent >/dev/null 2>&1 && return 0 From 5725f8241246f73fdae788a3b83ebd1e7167bee4 Mon Sep 17 00:00:00 2001 From: Josh Sokol Date: Sat, 19 Sep 2026 18:15:26 -0500 Subject: [PATCH 06/11] Add Ubuntu 26.04 LTS support; drop 25.* interim-release support - validate_os_and_version(): accept 26.04 (current LTS, "Resolute Raccoon", already released) instead of the 25.* wildcard. Interim (non-LTS) Ubuntu releases get ~9 months of upstream support and churn every 6 months; the intent is to support LTS releases specifically (22.04/24.04/26.04), not track every interim point release. - uninstall_ubuntu_debian(): also purge sensible-mda alongside sendmail/sendmail-bin. sensible-mda is a sendmail dependency with its own hard Depends on the mail-transport-agent virtual package; left behind, apt keeps that dependency satisfied by auto-installing a replacement MTA (courier-mta, pulling in ~40 packages including a full C build toolchain) instead of just removing it. Reproduced against a clean Ubuntu 26.04 install with nothing but sendmail/sendmail-bin present - not specific to 26.04, but only surfaced once 26.04's test container (no live systemd) turned an apt side-effect into a hard uninstall failure via a separate, unrelated postinst issue. Verified end-to-end (install -> verify -> uninstall -> verify) against Ubuntu 26.04 in Docker, 28/28 and 14/14 checks passing, zero courier-mta packages touched. Co-Authored-By: Claude Sonnet 5 --- simplerisk-setup.sh | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/simplerisk-setup.sh b/simplerisk-setup.sh index b0da1ac..8577586 100755 --- a/simplerisk-setup.sh +++ b/simplerisk-setup.sh @@ -134,7 +134,10 @@ validate_os_and_version(){ local valid case "${OS}" in "${UBUNTU_OSVAR}") - if [ "${VER}" = '22.04' ] || [[ "${VER}" = 24.* ]] || [[ "${VER}" = 25.* ]]; then + # LTS releases only - interim (non-LTS) releases like 25.04/25.10 + # get ~9 months of upstream support and churn every 6 months, so + # they're intentionally not accepted here. + if [ "${VER}" = '22.04' ] || [[ "${VER}" = 24.* ]] || [[ "${VER}" = 26.* ]]; then valid=y SETUP_TYPE=debian fi;; @@ -1024,7 +1027,12 @@ uninstall_ubuntu_debian(){ run_cmd_nobail rm -rf /var/log/simplerisk print_status 'Removing installed packages...' - exec_cmd_nobail "apt-get purge -y 'php*' 'libapache2-mod-php*' apache2 apache2-utils apache2-bin mysql-server mysql-client mysql-common sendmail sendmail-bin" + # sensible-mda is a dependency of sendmail with its own hard Depends on + # the mail-transport-agent virtual package. Left out of this purge, apt + # has to keep that dependency satisfied by auto-installing a replacement + # MTA (courier-mta, pulling in ~40 packages including a full C build + # toolchain) instead of just removing sensible-mda alongside sendmail. + exec_cmd_nobail "apt-get purge -y 'php*' 'libapache2-mod-php*' apache2 apache2-utils apache2-bin mysql-server mysql-client mysql-common sendmail sendmail-bin sensible-mda" run_cmd_nobail apt-get autoremove -y run_cmd_nobail apt-get autoclean From c10a4a257fa83566218f75c62ff9d81d4beee29c Mon Sep 17 00:00:00 2001 From: Josh Sokol Date: Sat, 19 Sep 2026 18:15:50 -0500 Subject: [PATCH 07/11] Add Ubuntu 26.04 to the CI matrix; fix a login-page check bug - tests/dockerfiles/Dockerfile.ubuntu-26.04 + mysql-init-ubuntu-26.04.sh: Ubuntu 26.04's mysql-server package ships no /etc/init.d/mysql (systemd units only). Verified via /usr/sbin/service's own source (`[ -d /run/systemd/system ]` gates delegation to systemctl) that a real, systemd-booted 26.04 server handles `service mysql start` fine via its live mysql.service unit - this is purely a no-init-system container gap, not a simplerisk-setup.sh issue, so the fix is test-only (same category as the existing Debian 12/13 shim). - tests/verify-install.sh: fixed the HTTP check added in the prior commit. It grepped for the login form (name="authenticate"), but a genuinely fresh database has zero users, so index.php's own gate (`if ($count == 0) { create_default_admin_account(); exit(); }`) shows the "Default Admin Account Creation" wizard first and never reaches the login form - that page, not the login screen, is what a truly fresh install is supposed to show. Now checks for that page instead (verify_create_default_admin_account, its submit button's literal name). - .github/workflows/install-test.yml: added ubuntu-26.04 to the matrix. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/install-test.yml | 1 + tests/dockerfiles/Dockerfile.ubuntu-26.04 | 43 ++++++++++++ tests/dockerfiles/mysql-init-ubuntu-26.04.sh | 70 ++++++++++++++++++++ tests/verify-install.sh | 16 ++++- 4 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 tests/dockerfiles/Dockerfile.ubuntu-26.04 create mode 100644 tests/dockerfiles/mysql-init-ubuntu-26.04.sh diff --git a/.github/workflows/install-test.yml b/.github/workflows/install-test.yml index 1e57b34..adaf632 100644 --- a/.github/workflows/install-test.yml +++ b/.github/workflows/install-test.yml @@ -19,6 +19,7 @@ jobs: os-slug: - ubuntu-22.04 - ubuntu-24.04 + - ubuntu-26.04 - debian-13 - centos-stream-9 - centos-stream-10 diff --git a/tests/dockerfiles/Dockerfile.ubuntu-26.04 b/tests/dockerfiles/Dockerfile.ubuntu-26.04 new file mode 100644 index 0000000..ac2b6d1 --- /dev/null +++ b/tests/dockerfiles/Dockerfile.ubuntu-26.04 @@ -0,0 +1,43 @@ +FROM ubuntu:26.04 + +ENV DEBIAN_FRONTEND=noninteractive +ENV container=docker + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + sudo \ + curl \ + wget \ + lsb-release \ + ca-certificates && \ + rm -rf /var/lib/apt/lists/* + +# MySQL uses native AIO by default, which fails on Docker's overlayfs storage +# driver during the post-install data directory initialisation. +RUN mkdir -p /etc/mysql/conf.d && \ + printf '[mysqld]\ninnodb_use_native_aio=0\n' > /etc/mysql/conf.d/docker.cnf + +# DENY all service operations during the lamp-server^ pre-install below. +# MySQL's post-install shutdown times out inside Docker; denying start/stop +# lets dpkg initialise the data directory without triggering the timeout. +RUN printf '#!/bin/sh\nexit 101\n' > /usr/sbin/policy-rc.d && \ + chmod +x /usr/sbin/policy-rc.d + +# Pre-install lamp-server^ so packages are already present for the test run. +RUN apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y lamp-server^ && \ + rm -rf /var/lib/apt/lists/* + +# Switch back to ALLOW so the setup script can start/stop services normally. +RUN printf '#!/bin/sh\nexit 0\n' > /usr/sbin/policy-rc.d && \ + chmod +x /usr/sbin/policy-rc.d + +# Unlike 22.04/24.04, Ubuntu 26.04's mysql-server package ships no +# /etc/init.d/mysql (systemd-only units), so `service mysql start` fails +# outright with no live systemd. This wrapper drives mysqld directly. +# Kept in a separate file to avoid heredoc parsing issues across different +# Docker builder versions. +COPY mysql-init-ubuntu-26.04.sh /etc/init.d/mysql +RUN chmod +x /etc/init.d/mysql + +CMD ["/bin/bash"] diff --git a/tests/dockerfiles/mysql-init-ubuntu-26.04.sh b/tests/dockerfiles/mysql-init-ubuntu-26.04.sh new file mode 100644 index 0000000..000612a --- /dev/null +++ b/tests/dockerfiles/mysql-init-ubuntu-26.04.sh @@ -0,0 +1,70 @@ +#!/bin/bash +### BEGIN INIT INFO +# Provides: mysql +# Required-Start: $local_fs $network +# Required-Stop: $local_fs +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: MySQL Community Server +# Description: MySQL Community Server wrapper for non-systemd containers +### END INIT INFO +# +# Unlike Debian 12/13's mysql-server package, Ubuntu 26.04's ships no +# /usr/share/mysql-8.4/mysql-helpers file, so this doesn't use the +# verify_ready/verify_database/get_running helpers mysql-init-debian.sh relies +# on - it drives mysqld directly instead. + +PIDFILE=/var/run/mysqld/mysqld.pid +MYSQLD=/usr/sbin/mysqld + +do_start() { + if [ ! -x "$MYSQLD" ]; then + echo "mysqld not found at $MYSQLD" >&2 + return 1 + fi + if mysqladmin ping --silent >/dev/null 2>&1; then + echo "MySQL is already running" + return 0 + fi + mkdir -p /var/run/mysqld && chown mysql:mysql /var/run/mysqld + # apt's mysql-server postinst already initializes /var/lib/mysql at + # package-install time (unlike the CentOS/SUSE RPM, which needs an + # explicit --initialize when systemd never ran it), so this only needs + # to start the daemon. + su -s /bin/bash mysql -c "$MYSQLD --daemonize --pid-file=$PIDFILE" 2>&1 + local i=0 + while [ $i -lt 30 ]; do + mysqladmin ping --silent >/dev/null 2>&1 && return 0 + sleep 1 + i=$((i+1)) + done + echo "MySQL did not start within 30 seconds" >&2 + return 1 +} + +do_stop() { + if [ -f "$PIDFILE" ]; then + local pid + pid=$(cat "$PIDFILE" 2>/dev/null) || return 0 + kill "$pid" 2>/dev/null || true + local i=0 + while [ -d "/proc/$pid" ] && [ $i -lt 20 ]; do + sleep 1 + i=$((i+1)) + done + rm -f "$PIDFILE" + fi + return 0 +} + +do_status() { + mysqladmin ping --silent >/dev/null 2>&1 +} + +case "$1" in + start) do_start ;; + stop) do_stop ;; + restart|force-reload) do_stop; sleep 1; do_start ;; + status) do_status ;; + *) echo "Usage: $0 {start|stop|restart|force-reload|status}"; exit 1 ;; +esac diff --git a/tests/verify-install.sh b/tests/verify-install.sh index 0a10f85..17a51dc 100755 --- a/tests/verify-install.sh +++ b/tests/verify-install.sh @@ -133,9 +133,21 @@ elif command -v httpd > /dev/null 2>&1; then fi # ── HTTP reachability ──────────────────────────────────────────────────────── +# A bare 200/301/302 only proves *something* answered on port 80 - it would +# also pass for a PHP fatal-error page, a blank page, or Apache's own default +# page. Follow the http->https redirect and grep the actual response body. +# +# A brand-new database has zero rows in `user`, so index.php's own gate +# (`if ($count == 0) { create_default_admin_account(); exit(); }`) shows the +# "Default Admin Account Creation" wizard and returns *before* the login form +# ever renders - that page, not the login screen, is the correct thing to see +# after a fresh install. verify_create_default_admin_account is that page's +# submit button name, a literal HTML attribute (not a translatable string). echo "--- HTTP ---" -check "HTTP on port 80 returns a redirect or 200" \ - bash -c "curl -sk -o /dev/null -w '%{http_code}' http://localhost/ | grep -qE '^(200|301|302)$'" +check "HTTP request reaches the app (following any http->https redirect)" \ + bash -c "test \"\$(curl -sk -o /dev/null -w '%{http_code}' -L http://localhost/)\" = 200" +check "SimpleRisk's default-admin-account page is actually rendered (not an error/default page)" \ + bash -c "curl -sk -L http://localhost/ | grep -q 'name=\"verify_create_default_admin_account\"'" # ── Summary ────────────────────────────────────────────────────────────────── echo "" From c0613e379167997936f184f23884eb03d9063310 Mon Sep 17 00:00:00 2001 From: Josh Sokol Date: Sat, 19 Sep 2026 20:25:35 -0500 Subject: [PATCH 08/11] Fix Ubuntu install serving a PHP fatal error instead of the app SimpleRisk's current release requires PHP >= 8.3 (Composer platform check). Debian and CentOS/RHEL already pin a modern PHP via a third-party repo (Sury / Remi), but Ubuntu just ran `apt-get install lamp-server^` and took whatever PHP version Ubuntu's own archive defaults to for that release. Ubuntu 24.04 (8.3.6) and 26.04 (8.5) happen to be fine; Ubuntu 22.04 only offers 8.1 - every fresh install completed "successfully" per the script's own output, but silently served a PHP fatal error (Composer's platform_check.php) instead of the app. Fix: for Ubuntu, still install via lamp-server^ (unchanged - keeps Ubuntu's own MySQL/MariaDB provisioning as-is, only the PHP version was the problem), but also add the same Sury PHP8 repo Debian uses, install the pinned 8.5 packages, and switch Apache's active PHP module to it (scanning /etc/apache2/mods-enabled/php*.load rather than assuming a specific old version, since it varies by release). Also extended uninstall_ubuntu_debian's repo cleanup to remove the Sury repo/key for Ubuntu too, matching install. This was previously invisible because verify-install.sh's HTTP check never followed the http->https redirect, so it only ever saw the redirect's 301/302 status and never reached the page where the fatal error actually occurs. Verified end-to-end (install -> verify -> uninstall -> verify) on all three supported Ubuntu versions in Docker: 22.04 (28/28, 14/14 - PHP correctly pinned to 8.5.10 via Sury), 24.04 (28/28, 14/14), and 26.04 (28/28, 14/14 - confirmed no conflict between the newly-added Sury repo and the native php8.5 package it already ships). Co-Authored-By: Claude Sonnet 5 --- simplerisk-setup.sh | 42 +++++++++++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/simplerisk-setup.sh b/simplerisk-setup.sh index 8577586..609f05f 100755 --- a/simplerisk-setup.sh +++ b/simplerisk-setup.sh @@ -490,8 +490,10 @@ setup_ubuntu_debian(){ print_status 'Populating apt-get cache...' run_cmd apt-get update - # Add PHP8/MySQL repos for Debian - if [ "${OS}" = "${DEBIAN_OSVAR}" ]; then + # Add the Sury PHP8 repo. Debian also gets MySQL's own repo here (Ubuntu + # keeps whatever MySQL/MariaDB lamp-server^ bundles - see below - since + # only the PHP version, not MySQL provisioning, is the problem there). + if [ "${OS}" = "${DEBIAN_OSVAR}" ] || [ "${OS}" = "${UBUNTU_OSVAR}" ]; then run_cmd mkdir -p /etc/apt/keyrings local apt_php_version=8.5 @@ -512,10 +514,12 @@ setup_ubuntu_debian(){ fi exec_cmd "echo 'deb [signed-by=/etc/apt/keyrings/sury-php.gpg] https://packages.sury.org/php/ $(lsb_release -sc) main' | sudo tee /etc/apt/sources.list.d/sury-php.list" - print_status 'Adding MySQL 8 repository' - # Download the signing key directly from MySQL (more reliable than keyservers). - exec_cmd "curl -fsSL '$MYSQL_KEY_URL' | gpg --dearmor -o /etc/apt/trusted.gpg.d/mysql.gpg" - exec_cmd "echo 'deb [signed-by=/etc/apt/trusted.gpg.d/mysql.gpg] https://repo.mysql.com/apt/$(lsb_release -si | tr '[:upper:]' '[:lower:]')/ $(lsb_release -sc) mysql-8.4-lts' | sudo tee /etc/apt/sources.list.d/mysql.list" + if [ "${OS}" = "${DEBIAN_OSVAR}" ]; then + print_status 'Adding MySQL 8 repository' + # Download the signing key directly from MySQL (more reliable than keyservers). + exec_cmd "curl -fsSL '$MYSQL_KEY_URL' | gpg --dearmor -o /etc/apt/trusted.gpg.d/mysql.gpg" + exec_cmd "echo 'deb [signed-by=/etc/apt/trusted.gpg.d/mysql.gpg] https://repo.mysql.com/apt/$(lsb_release -si | tr '[:upper:]' '[:lower:]')/ $(lsb_release -sc) mysql-8.4-lts' | sudo tee /etc/apt/sources.list.d/mysql.list" + fi print_status 'Re-populating apt-get cache with added repos...' run_cmd apt-get update @@ -529,6 +533,22 @@ setup_ubuntu_debian(){ run_cmd apt-get install -y 'lamp-server^' print_status 'Installing cron...' run_cmd apt-get install -y cron + + # lamp-server^ installs whatever PHP version Ubuntu's own archive + # defaults to for this release (e.g. 8.1 on 22.04), which can be + # older than SimpleRisk's Composer platform requirement. Install the + # pinned Sury version from the repo added above and switch Apache's + # active PHP module to it, without touching the MySQL/Apache + # packages lamp-server^ already installed. + print_status "Installing PHP ${apt_php_version} from Ondrej's repository..." + run_cmd apt-get install -y "php${apt_php_version}" "php${apt_php_version}-mysql" "libapache2-mod-php${apt_php_version}" + for old_mod_file in /etc/apache2/mods-enabled/php*.load; do + [ -e "${old_mod_file}" ] || continue + old_mod=$(basename "${old_mod_file}" .load) + [ "${old_mod}" = "php${apt_php_version}" ] && continue + run_cmd a2dismod "${old_mod}" + done + run_cmd a2enmod "php${apt_php_version}" else print_status 'Installing Apache...' run_cmd apt-get install -y apache2 @@ -1036,12 +1056,16 @@ uninstall_ubuntu_debian(){ run_cmd_nobail apt-get autoremove -y run_cmd_nobail apt-get autoclean - if [ "${OS}" = "${DEBIAN_OSVAR}" ]; then + if [ "${OS}" = "${DEBIAN_OSVAR}" ] || [ "${OS}" = "${UBUNTU_OSVAR}" ]; then print_status 'Removing added repositories and keys...' + # Both OSes add the Sury PHP repo; only Debian adds MySQL's own repo + # (Ubuntu keeps whatever MySQL/MariaDB lamp-server^ installed). run_cmd_nobail rm -f /etc/apt/sources.list.d/sury-php.list - run_cmd_nobail rm -f /etc/apt/sources.list.d/mysql.list run_cmd_nobail rm -f /etc/apt/keyrings/sury-php.gpg - run_cmd_nobail rm -f /etc/apt/trusted.gpg.d/mysql.gpg + if [ "${OS}" = "${DEBIAN_OSVAR}" ]; then + run_cmd_nobail rm -f /etc/apt/sources.list.d/mysql.list + run_cmd_nobail rm -f /etc/apt/trusted.gpg.d/mysql.gpg + fi run_cmd_nobail apt-get update fi From 2e04ba0d49f85c23c0ace6a62303a9d6b4f07721 Mon Sep 17 00:00:00 2001 From: Josh Sokol Date: Sat, 19 Sep 2026 20:51:14 -0500 Subject: [PATCH 09/11] Drop SLES/openSUSE 15 support: no PHP >= 8.3 source available SimpleRisk's current release requires PHP >= 8.3 (Composer platform check). SLES 15's own repositories cap out at PHP 8.2 (the php8 package) with no upgrade path: openSUSE's community devel:languages:php OBS project, which sometimes backports a newer PHP to older releases, has dropped 15.6 support entirely and only targets the next major release (16.0), not yet generally available for SLES. Shipping an install path that silently serves a PHP fatal error instead of the app (as Ubuntu 22.04 did until the previous commit) serves no one, so validate_os_and_version() now rejects SLES outright with a clear explanation instead of attempting the install. setup_suse()/uninstall_suse() are left in place (unreachable for now) as a starting point: openSUSE Leap 16.0 already ships PHP 8.4 natively, so SLES 16 (once released) should be a viable target, but those functions are written entirely around SLES 15's package names, module structure, and MySQL repo RPM naming (mysql84-community-release-sl15) - supporting SLES 16 needs its own dedicated pass, verified against a real SLES 16/Leap 16.0 environment, not just relaxing this version check. Also removes the SLES/openSUSE CI matrix entry and its run-suse-function-test.sh workaround (it bypassed the now-intentional rejection above to exercise setup_suse() directly) and updates the required status checks on main's branch protection to match the current matrix (ubuntu-22.04/24.04/26.04, debian-13, centos-stream-9/10). Co-Authored-By: Claude Sonnet 5 --- .github/workflows/install-test.yml | 36 ++---- simplerisk-setup.sh | 45 +++---- tests/dockerfiles/Dockerfile.opensuse-leap-15 | 35 ------ tests/dockerfiles/systemctl-shim-suse.sh | 116 ------------------ tests/run-suse-function-test.sh | 45 ------- 5 files changed, 25 insertions(+), 252 deletions(-) delete mode 100644 tests/dockerfiles/Dockerfile.opensuse-leap-15 delete mode 100644 tests/dockerfiles/systemctl-shim-suse.sh delete mode 100644 tests/run-suse-function-test.sh diff --git a/.github/workflows/install-test.yml b/.github/workflows/install-test.yml index adaf632..19b50a9 100644 --- a/.github/workflows/install-test.yml +++ b/.github/workflows/install-test.yml @@ -23,7 +23,6 @@ jobs: - debian-13 - centos-stream-9 - centos-stream-10 - - opensuse-leap-15 steps: - name: Checkout repository @@ -75,31 +74,19 @@ jobs: - name: Copy scripts into container run: | CONTAINER="simplerisk-test-${{ matrix.os-slug }}" - docker cp simplerisk-setup.sh "$CONTAINER:/root/simplerisk-setup.sh" - docker cp tests/verify-install.sh "$CONTAINER:/root/verify-install.sh" - docker cp tests/verify-uninstall.sh "$CONTAINER:/root/verify-uninstall.sh" - docker cp tests/run-suse-function-test.sh "$CONTAINER:/root/run-suse-function-test.sh" + docker cp simplerisk-setup.sh "$CONTAINER:/root/simplerisk-setup.sh" + docker cp tests/verify-install.sh "$CONTAINER:/root/verify-install.sh" + docker cp tests/verify-uninstall.sh "$CONTAINER:/root/verify-uninstall.sh" docker exec "$CONTAINER" chmod +x \ /root/simplerisk-setup.sh \ /root/verify-install.sh \ - /root/verify-uninstall.sh \ - /root/run-suse-function-test.sh + /root/verify-uninstall.sh # ── Install ───────────────────────────────────────────────────────────── - # openSUSE/SLES can't go through the normal `setup()` entry point in CI: - # validate_os_and_version() requires a real, registered SUSE Customer - # Center subscription (it shells out to `suseconnect`) which a public - # container doesn't have. run-suse-function-test.sh instead sources the - # script and calls setup_suse()/uninstall_suse() directly, so the real - # apache2/mysql/php8/cron install logic is still exercised for real. - name: Run install run: | - CONTAINER="simplerisk-test-${{ matrix.os-slug }}" - if [ "${{ matrix.os-slug }}" = "opensuse-leap-15" ]; then - docker exec "$CONTAINER" bash /root/run-suse-function-test.sh install - else - docker exec "$CONTAINER" bash /root/simplerisk-setup.sh --yes --debug - fi + docker exec simplerisk-test-${{ matrix.os-slug }} \ + bash /root/simplerisk-setup.sh --yes --debug # ── Verify install ────────────────────────────────────────────────────── - name: Verify installation @@ -110,12 +97,8 @@ jobs: # ── Uninstall ─────────────────────────────────────────────────────────── - name: Run uninstall run: | - CONTAINER="simplerisk-test-${{ matrix.os-slug }}" - if [ "${{ matrix.os-slug }}" = "opensuse-leap-15" ]; then - docker exec "$CONTAINER" bash /root/run-suse-function-test.sh uninstall - else - docker exec "$CONTAINER" bash /root/simplerisk-setup.sh --uninstall --yes --debug - fi + docker exec simplerisk-test-${{ matrix.os-slug }} \ + bash /root/simplerisk-setup.sh --uninstall --yes --debug # ── Verify uninstall ──────────────────────────────────────────────────── - name: Verify uninstallation @@ -141,8 +124,7 @@ jobs: docker exec "$CONTAINER" journalctl -n 50 2>/dev/null || true echo "=== Apache error log ===" && \ docker exec "$CONTAINER" cat /var/log/apache2/error.log 2>/dev/null || \ - docker exec "$CONTAINER" cat /var/log/httpd/error_log 2>/dev/null || \ - docker exec "$CONTAINER" cat /var/log/apache2/error_log 2>/dev/null || true + docker exec "$CONTAINER" cat /var/log/httpd/error_log 2>/dev/null || true echo "=== MySQL error log (last 30 lines) ===" && \ docker exec "$CONTAINER" tail -30 /var/log/mysql/error.log 2>/dev/null || \ docker exec "$CONTAINER" tail -30 /var/log/mysqld.log 2>/dev/null || true diff --git a/simplerisk-setup.sh b/simplerisk-setup.sh index 609f05f..6efaa20 100755 --- a/simplerisk-setup.sh +++ b/simplerisk-setup.sh @@ -157,35 +157,22 @@ validate_os_and_version(){ SETUP_TYPE=rhel fi;; "${SLES_OSVAR}") - if [[ "${VER}" = "${SLES_15_SUPPORTED_SP}"* ]]; then - valid=y - local php_module - # Grab module where php8 is available - php_module=$(zypper search-packages php8 | awk '/^php8[[:space:]]/ { sub(/\(.*/, ""); sub(/^php8[[:space:]]+/, ""); sub(/[[:space:]]+$/, ""); print }') - # Check that the PHP module is active. suseconnect reports - # "Activated" on self-registered systems and "Installed" on - # SUSE Manager-managed systems; the status line follows the - # module name line in the output, so use grep -A1 to capture - # both lines before checking. - # Guard against an empty php_module — an empty -F pattern would - # match every line and make the check a silent no-op. - if [ -z "${php_module}" ]; then - print_error_message "Could not detect the PHP 8 module name via zypper. Ensure the Web and Scripting Module is activated in your SLES subscription." - fi - if ! sudo suseconnect --list-extensions | grep -A1 -F "$php_module" | grep -qE "Activated|Installed"; then - print_error_message "$php_module is not enabled on your subscription. Please enable it before running this installer." - fi - if [ ! -v HEADLESS ]; then - read -r -p 'Before continuing, SLES 15 does not have sendmail available. Proceed? [ Yes / (No) ]: ' answer < /dev/tty - case "${answer}" in - Yes|yes|Y|y ) SETUP_TYPE=suse;; - * ) exit 1;; - esac - else - echo "This will install postfix. You will need to configure it later." - SETUP_TYPE=suse - fi - fi;; + # SLES 15 (all service packs) is not supported: SimpleRisk's + # current release requires PHP >= 8.3 (Composer platform check), + # and SLES 15's own repositories cap out at PHP 8.2 (the php8 + # package) with no upgrade path. openSUSE's community + # devel:languages:php OBS project, which sometimes backports a + # newer PHP to older releases, has dropped 15.6 support entirely + # and only targets the next major release (16.0), which is not + # yet generally available for SLES. openSUSE Leap 16.0 already + # ships PHP 8.4 natively, so SLES 16 (once released) should be a + # viable target - but setup_suse()/uninstall_suse() below are + # written entirely around SLES 15's package names, module + # structure, and MySQL repo RPM naming + # (mysql84-community-release-sl15), so adding SLES 16 support + # needs its own dedicated pass, not just changing this version + # check. + print_error_message "SLES/openSUSE is not currently supported: SimpleRisk requires PHP >= 8.3, and SLES 15's repositories only offer PHP 8.2 with no upgrade path currently available.";; *) local unknown=y;; esac diff --git a/tests/dockerfiles/Dockerfile.opensuse-leap-15 b/tests/dockerfiles/Dockerfile.opensuse-leap-15 deleted file mode 100644 index 1c228ad..0000000 --- a/tests/dockerfiles/Dockerfile.opensuse-leap-15 +++ /dev/null @@ -1,35 +0,0 @@ -FROM opensuse/leap:15.6 - -ENV container=docker - -# simplerisk-setup.sh's SLES branch requires a real SUSE Customer Center -# subscription (validate_os_and_version calls `suseconnect --list-extensions` -# to verify the PHP module is licensed), which a public CI container can't -# satisfy. This image is instead used to exercise setup_suse()/uninstall_suse() -# directly (see tests/run-suse-function-test.sh), skipping the OS/subscription -# gate but running the real apache2/mysql/php8/cron install logic — openSUSE -# Leap 15.6 tracks SLES 15 SP6 packaging closely enough for that purpose. -# `which` and `cron` are pre-installed on a real SLES server but absent here; -# `shadow` provides useradd/groupadd used by the mysql-community-server RPM. -RUN zypper --non-interactive install curl wget sudo which cron tar gzip shadow && \ - zypper clean --all - -# MySQL uses native AIO by default, which fails on Docker's overlayfs driver. -RUN mkdir -p /etc/my.cnf.d && \ - printf '[mysqld]\ninnodb_use_native_aio=0\n' > /etc/my.cnf.d/docker.cnf - -# systemd cannot start in this container (no cgroup delegation). This shim -# replaces /usr/bin/systemctl so setup_suse()'s -# `systemctl start/stop/restart/enable/disable` calls work by managing -# processes directly instead. Kept in a separate file to avoid heredoc -# parsing issues across different Docker builder versions. -COPY systemctl-shim-suse.sh /usr/local/bin/systemctl -RUN chmod +x /usr/local/bin/systemctl - -# firewall-cmd needs a no-op shim: firewalld cannot run in Docker (no -# nftables/iptables backend available). Placing it in /usr/local/bin/ ensures -# it takes precedence over the real binary in /usr/sbin/ on PATH. -RUN printf '#!/bin/bash\nexit 0\n' > /usr/local/bin/firewall-cmd && \ - chmod +x /usr/local/bin/firewall-cmd - -CMD ["/bin/bash"] diff --git a/tests/dockerfiles/systemctl-shim-suse.sh b/tests/dockerfiles/systemctl-shim-suse.sh deleted file mode 100644 index fc68b4d..0000000 --- a/tests/dockerfiles/systemctl-shim-suse.sh +++ /dev/null @@ -1,116 +0,0 @@ -#!/bin/bash -# Minimal systemctl shim — handles the subset used by simplerisk-setup.sh's -# setup_suse()/uninstall_suse() without requiring a running systemd PID 1. -# Mirrors systemctl-shim-centos.sh; see that file for the general approach. - -args=() -for arg in "$@"; do - [[ "$arg" == --* ]] && continue - args+=("$arg") -done -action="${args[0]:-}" -unit="${args[1]%.service}" - -start_mysqld() { - mysqladmin ping --silent >/dev/null 2>&1 && return 0 # already running - mkdir -p /var/run/mysqld && chown mysql:mysql /var/run/mysqld 2>/dev/null || true - # setup_suse() passes /var/log/mysql/mysqld.log to set_up_database(), - # matching the package's own my.cnf `log-error=` directive - the - # directory doesn't exist without a live systemd-tmpfiles, so mysqld - # falls back to logging elsewhere and set_up_database's grep for the - # startup temp-password line fails ("No such file or directory"), - # aborting the rest of setup_suse. Pre-create it so mysqld logs where - # the script actually looks. - mkdir -p /var/log/mysql && chown mysql:mysql /var/log/mysql 2>/dev/null || true - touch /var/log/mysql/mysqld.log && chown mysql:mysql /var/log/mysql/mysqld.log 2>/dev/null || true - # The RPM %post scriptlet may leave /var/lib/mysql in a partial state when - # systemd is unavailable (auto.cnf + binlog.index but no ibdata1). - if [ ! -f /var/lib/mysql/ibdata1 ]; then - rm -f /var/lib/mysql/auto.cnf /var/lib/mysql/binlog.index 2>/dev/null || true - mysqld --initialize --user=mysql >>/var/log/mysql/mysqld.log 2>&1 - fi - printf "INSTALL COMPONENT 'file://component_validate_password';\n" > /var/lib/mysql/docker-init.sql - nohup mysqld --user=mysql --init-file=/var/lib/mysql/docker-init.sql >>/var/log/mysql/mysqld.log 2>&1 & - local i=0 - while [ $i -lt 60 ]; do - mysqladmin ping --silent >/dev/null 2>&1 && return 0 - sleep 1; i=$((i+1)) - done - echo "systemctl shim: mysqld did not start within 60s" >&2; return 1 -} - -stop_mysqld() { - local pidfile=/var/run/mysqld/mysqld.pid - if [ -f "$pidfile" ]; then - local pid - pid=$(cat "$pidfile" 2>/dev/null) - [ -n "$pid" ] && kill -TERM "$pid" 2>/dev/null || true - else - pkill -TERM mysqld 2>/dev/null || true - fi - local i=0 - while mysqladmin ping --silent >/dev/null 2>&1 && [ $i -lt 30 ]; do - sleep 1; i=$((i+1)) - done -} - -start_apache2() { - pgrep -x httpd-prefork >/dev/null 2>&1 && return 0 - # openSUSE ships no apache2ctl and no init.d script without a live - # systemd; /usr/sbin/start_apache2 is the actual launcher, and Apache's - # own -k start/stop/restart daemon-control flags handle backgrounding - # and the pidfile without needing systemd. - /usr/sbin/start_apache2 -k start 2>&1 -} - -stop_apache2() { - /usr/sbin/start_apache2 -k stop 2>/dev/null || true -} - -start_cron() { - pgrep cron >/dev/null 2>&1 && return 0 - cron -} - -case "$action" in - start) - case "$unit" in - mysql|mysqld) start_mysqld ;; - apache2) start_apache2 ;; - cron) start_cron ;; - firewalld) exit 0 ;; # no-op: firewalld not available in Docker - *) echo "systemctl shim: unsupported unit '$unit'" >&2; exit 1 ;; - esac ;; - stop) - case "$unit" in - mysql|mysqld) stop_mysqld ;; - apache2) stop_apache2 ;; - *) exit 0 ;; # non-fatal for unknown units on uninstall - esac ;; - restart) - case "$unit" in - mysql|mysqld) stop_mysqld; sleep 1; start_mysqld ;; - apache2) stop_apache2; sleep 1; start_apache2 ;; - *) echo "systemctl shim: unsupported unit '$unit'" >&2; exit 1 ;; - esac ;; - is-active) - case "$unit" in - mysql|mysqld) mysqladmin ping --silent >/dev/null 2>&1 ;; - apache2) pgrep -x httpd-prefork >/dev/null 2>&1 ;; - cron) pgrep cron >/dev/null 2>&1 ;; - *) exit 1 ;; - esac ;; - status) - case "$unit" in - mysql|mysqld) mysqladmin ping --silent >/dev/null 2>&1 && echo "active" || exit 3 ;; - apache2) pgrep -x httpd-prefork >/dev/null 2>&1 && echo "active" || exit 3 ;; - *) exit 3 ;; - esac ;; - enable|disable|daemon-reload|mask|unmask|is-enabled|reset-failed) - case "$unit" in - cron) [ "$action" = "enable" ] && start_cron; exit 0 ;; - *) exit 0 ;; # no-op — we don't manage boot-time units - esac ;; - *) - echo "systemctl shim: unknown action '$action'" >&2; exit 1 ;; -esac diff --git a/tests/run-suse-function-test.sh b/tests/run-suse-function-test.sh deleted file mode 100644 index c87d1b9..0000000 --- a/tests/run-suse-function-test.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env bash -# run-suse-function-test.sh — drives setup_suse()/uninstall_suse() directly. -# -# The full `setup()` entry point can't be exercised against a plain openSUSE -# Leap container: validate_os_and_version() requires /etc/os-release to say -# NAME=SLES exactly, and its SLES branch calls `suseconnect --list-extensions` -# to verify a licensed PHP module — both of which need a real, registered SUSE -# subscription that a public CI container doesn't have. This script instead -# sources the real, unmodified simplerisk-setup.sh (stripping only the -# trailing auto-invocation) and calls the install/uninstall functions -# directly, so the actual apache2/mysql/php8/cron logic in setup_suse() and -# uninstall_suse() still gets exercised for real on every change. -set -euo pipefail - -SCRIPT=/root/simplerisk-setup.sh -ACTION="${1:?usage: run-suse-function-test.sh install|uninstall}" - -# Strip the trailing `setup "${@:1}"` auto-invocation so sourcing the file -# only defines functions/readonly vars, matching the technique used to -# reproduce HackerOne #3761952. -LAST_LINE=$(grep -Fn 'setup "${@:1}"' "$SCRIPT" | tail -1 | cut -d: -f1) -head -n "$((LAST_LINE - 1))" "$SCRIPT" > /tmp/sr-functions.sh -# shellcheck disable=SC1091 -source /tmp/sr-functions.sh - -case "$ACTION" in - install) - DEBUG=y - # setup_suse() references ${VER} (e.g. to gate PHP/rewrite module - # enabling and the "SLES 15 has no sendmail" notice) which is - # normally set by load_os_variables() from /etc/os-release - - # bypassed here along with the rest of setup(). Match a real SLES - # 15 SP's VERSION_ID format. - VER="15.6" - setup_suse "$(get_current_simplerisk_version)" - ;; - uninstall) - DEBUG=y - uninstall_suse - ;; - *) - echo "unknown action: $ACTION" >&2 - exit 1 - ;; -esac From 4d2c769c2e59ae09659fa778412f75d813c88dc3 Mon Sep 17 00:00:00 2001 From: Josh Sokol Date: Sat, 19 Sep 2026 21:02:53 -0500 Subject: [PATCH 10/11] Update README: OS support list, master->main, add CI badge - Supported versions: 22.04/24.04/26.04 LTS only (matches validate_os_and_version() as of this branch - Ubuntu interim releases and SLES 15 are no longer accepted), with a brief note on why SLES isn't currently supported. - Fixed the install one-liners pointing at a 'master' branch that no longer exists (the repo's default branch is 'main'; raw.githubusercontent.com happens to still resolve the old ref today, but that's not something to document as if intentional). - Added a CI status badge for the new install-test workflow. Co-Authored-By: Claude Sonnet 5 --- README.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 7effc43..cf20092 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,25 @@ # SimpleRisk Setup Script +[![SimpleRisk Install/Uninstall Tests](https://github.com/simplerisk/setup-scripts/actions/workflows/install-test.yml/badge.svg)](https://github.com/simplerisk/setup-scripts/actions/workflows/install-test.yml) + ## Supported versions -- Ubuntu LTS 22.04, 24.04, and 25.x - - Although it is possible to install on non-LTS versions in between the two most recent LTS versions or above the most - recent version specified above, we do not support them officially +- Ubuntu LTS 22.04, 24.04, and 26.04 + - Interim (non-LTS) Ubuntu releases are not supported; they get ~9 months of upstream support and churn every 6 months - Debian 13 - CentOS Stream 9, 10 - Red Hat Enterprise Linux (RHEL) 9, 10 -- SUSE Linux Enterprise Server (SLES) 15.x + +SUSE Linux Enterprise Server (SLES) is not currently supported: SimpleRisk requires PHP >= 8.3, and SLES 15's own +repositories only offer PHP 8.2 with no upgrade path currently available. Support may return once a SLES release with a +newer PHP is available. ## Instructions Run as root or insert `sudo -E` before `bash`: -- `curl -sL https://raw.githubusercontent.com/simplerisk/setup-scripts/master/simplerisk-setup.sh | bash -` -- `wget -qO- https://raw.githubusercontent.com/simplerisk/setup-scripts/master/simplerisk-setup.sh | bash -` +- `curl -sL https://raw.githubusercontent.com/simplerisk/setup-scripts/main/simplerisk-setup.sh | bash -` +- `wget -qO- https://raw.githubusercontent.com/simplerisk/setup-scripts/main/simplerisk-setup.sh | bash -` ## `--help` From feafaa4f816e0f11af03b0dad0fa101c96aeabf9 Mon Sep 17 00:00:00 2001 From: Josh Sokol Date: Sat, 19 Sep 2026 21:23:04 -0500 Subject: [PATCH 11/11] Only run install-test CI when the script or tests actually change Doc-only commits (README, etc.) were triggering the full ~25-minute matrix for no reason. Scoped both push and pull_request triggers to simplerisk-setup.sh, tests/**, and the workflow file itself (shared via a YAML anchor so the two trigger blocks can't drift out of sync). Co-Authored-By: Claude Sonnet 5 --- .github/workflows/install-test.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/install-test.yml b/.github/workflows/install-test.yml index 19b50a9..52b775c 100644 --- a/.github/workflows/install-test.yml +++ b/.github/workflows/install-test.yml @@ -3,8 +3,13 @@ name: SimpleRisk Install/Uninstall Tests on: push: branches: [main, "feature/**"] + paths: &test-paths + - simplerisk-setup.sh + - tests/** + - .github/workflows/install-test.yml pull_request: branches: [main] + paths: *test-paths workflow_dispatch: jobs: