diff --git a/.github/workflows/install-test.yml b/.github/workflows/install-test.yml new file mode 100644 index 0000000..52b775c --- /dev/null +++ b/.github/workflows/install-test.yml @@ -0,0 +1,143 @@ +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: + 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 + - ubuntu-26.04 + - debian-13 + - centos-stream-9 + - centos-stream-10 + + 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 exec "$CONTAINER" chmod +x \ + /root/simplerisk-setup.sh \ + /root/verify-install.sh \ + /root/verify-uninstall.sh + + # ── Install ───────────────────────────────────────────────────────────── + - name: Run install + run: | + docker exec simplerisk-test-${{ matrix.os-slug }} \ + bash /root/simplerisk-setup.sh --yes --debug + + # ── Verify install ────────────────────────────────────────────────────── + - name: Verify installation + run: | + docker exec simplerisk-test-${{ matrix.os-slug }} \ + bash /root/verify-install.sh + + # ── Uninstall ─────────────────────────────────────────────────────────── + - name: Run uninstall + run: | + docker exec simplerisk-test-${{ matrix.os-slug }} \ + bash /root/simplerisk-setup.sh --uninstall --yes --debug + + # ── 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 || 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/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` diff --git a/simplerisk-setup.sh b/simplerisk-setup.sh index 67bba81..6efaa20 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;; @@ -154,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 @@ -379,7 +369,20 @@ 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. + # 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 } set_up_simplerisk_log() { @@ -437,6 +440,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 } @@ -471,8 +477,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 @@ -493,10 +501,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 @@ -510,6 +520,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 @@ -598,7 +624,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 +763,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 @@ -951,6 +977,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 @@ -963,7 +996,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 @@ -1001,16 +1034,25 @@ 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 - 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 @@ -1105,7 +1147,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' 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.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/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-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/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/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/verify-install.sh b/tests/verify-install.sh new file mode 100755 index 0000000..17a51dc --- /dev/null +++ b/tests/verify-install.sh @@ -0,0 +1,168 @@ +#!/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 ──────────────────────────────────────────────────────── +# 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 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 "" +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