Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/install-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4

# ── Build ───────────────────────────────────────────────────────────────
- name: Build test image
Expand Down
7 changes: 7 additions & 0 deletions simplerisk-setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,13 @@ setup_ubuntu_debian(){
run_cmd apt-get install -y cron
fi

# Unlike apache2/mysql-server, the cron package's postinst does not
# reliably start the daemon in every environment (e.g. containers with
# policy-rc.d denying service auto-start by default). Every other OS
# branch explicitly enables/starts its cron daemon; do the same here.
print_status 'Ensuring cron is running...'
exec_cmd 'service cron status > /dev/null 2>&1 || service cron start'

print_status 'Installing PHP development libraries...'
run_cmd apt-get install -y "php${apt_php_version:-}-dev"

Expand Down
14 changes: 14 additions & 0 deletions tests/dockerfiles/Dockerfile.centos-stream-10
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,20 @@ ENV container=docker
RUN dnf -y install --allowerasing curl wget sudo cronie which && \
dnf clean all

# The base image's default nsswitch.conf lists `sss` (SSSD) ahead of `files`
# for passwd/group lookups, but SSSD is neither installed nor running here.
# crond's PAM account/session check (initgroups() for the job's user) then
# fails outright - "Authentication service cannot retrieve authentication
# info" - instead of falling through to /etc/passwd, silently preventing
# every cron.d job from ever running as a non-root user. Real RHEL/CentOS
# servers enrolled in SSSD (or not) have a consistent, working nsswitch
# config either way; this minimal container just has a stale default with
# nothing backing it. Strip sss so lookups go straight to files.
RUN sed -i \
-e 's/^\(passwd:\s*\).*/\1files systemd/' \
-e 's/^\(group:\s*\).*/\1files systemd/' \
/etc/nsswitch.conf

# 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
Expand Down
14 changes: 14 additions & 0 deletions tests/dockerfiles/Dockerfile.centos-stream-9
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,20 @@ ENV container=docker
RUN dnf -y install --allowerasing curl wget sudo cronie which && \
dnf clean all

# The base image's default nsswitch.conf lists `sss` (SSSD) ahead of `files`
# for passwd/group lookups, but SSSD is neither installed nor running here.
# crond's PAM account/session check (initgroups() for the job's user) then
# fails outright - "Authentication service cannot retrieve authentication
# info" - instead of falling through to /etc/passwd, silently preventing
# every cron.d job from ever running as a non-root user. Real RHEL/CentOS
# servers enrolled in SSSD (or not) have a consistent, working nsswitch
# config either way; this minimal container just has a stale default with
# nothing backing it. Strip sss so lookups go straight to files.
RUN sed -i \
-e 's/^\(passwd:\s*\).*/\1files systemd/' \
-e 's/^\(group:\s*\).*/\1files systemd/' \
/etc/nsswitch.conf

# 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
Expand Down
32 changes: 31 additions & 1 deletion tests/dockerfiles/systemctl-shim-centos.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,17 @@
# on CentOS/RHEL without requiring a running systemd PID 1.

# Strip --quiet / --system / other flags; find the action and unit name.
# --now is tracked separately (not just discarded) because real systemd
# treats `enable --now` as enable *and* start - a real server would already
# have the unit running from that command alone, so simplerisk-setup.sh
# never issues a separate `start` for cron/crond.
now_flag=
args=()
for arg in "$@"; do
if [[ "$arg" == "--now" ]]; then
now_flag=1
continue
fi
[[ "$arg" == --* ]] && continue
args+=("$arg")
done
Expand Down Expand Up @@ -80,11 +89,17 @@ stop_httpd() {
httpd -k stop 2>/dev/null || true
}

start_crond() {
pgrep crond >/dev/null 2>&1 && return 0
crond
}

case "$action" in
start)
case "$unit" in
mysqld|mysql) start_mysqld ;;
httpd) start_httpd ;;
crond|cron) start_crond ;;
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 ;;
Expand All @@ -105,15 +120,30 @@ case "$action" in
case "$unit" in
mysqld|mysql) mysqladmin ping --silent >/dev/null 2>&1 ;;
httpd) pgrep httpd >/dev/null 2>&1 ;;
crond|cron) pgrep crond >/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 ;;
crond|cron) pgrep crond >/dev/null 2>&1 && echo "active" || exit 3 ;;
*) exit 3 ;;
esac ;;
enable|disable|daemon-reload|mask|unmask|is-enabled|reset-failed)
enable)
# We don't manage boot-time units, but `enable --now` also means
# "start it now" on a real system - honor the --now part.
if [ -n "$now_flag" ]; then
case "$unit" in
mysqld|mysql) start_mysqld ;;
httpd) start_httpd ;;
crond|cron) start_crond ;;
*) exit 0 ;;
esac
else
exit 0
fi ;;
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 ;;
Expand Down
105 changes: 105 additions & 0 deletions tests/verify-install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,14 @@ 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')"
# Waiting for an actual tick (rather than checking the daemon/script/config
# are in place) proved unreliable on some CI backends for reasons unrelated
# to the setup script itself - e.g. a container's PAM/audit stack rejecting
# crond's non-root job user outright regardless of nsswitch.conf. Verifying
# the daemon is actually running is what those attempts were missing; script
# presence and cron.d wiring are already covered above.
check "Cron daemon is running" \
bash -c "pgrep -x crond >/dev/null 2>&1 || pgrep -x cron >/dev/null 2>&1"

# ── PHP ──────────────────────────────────────────────────────────────────────
echo "--- PHP ---"
Expand Down Expand Up @@ -149,6 +157,103 @@ check "HTTP request reaches the app (following any http->https redirect)" \
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\"'"

# ── End-to-end: create the admin account, log in, check the health page ───────
# Drives the real first-run flow with plain curl (no browser/JS dependency -
# every step here is a server-side form POST) rather than just probing
# individual endpoints, so a break anywhere in that chain - account creation,
# login, or the app's own self-reported health - fails the build.
#
# This must be curl's/this script's *first* interaction with the app on this
# container: SimpleRisk's simplerisk_base_url setting is written once, from
# whichever host/port the very first request used, and is never recomputed
# after that (get_base_url() checks the stored setting before ever looking at
# the live request again). Since every check in this script - including the
# ones above - already goes through the container's real internal address via
# `docker exec ... curl https://localhost/...` with no host port published,
# that's consistent for the whole run and this doesn't get a chance to drift.
echo "--- End-to-end (create admin account -> log in -> health check) ---"

E2E_DIR=$(mktemp -d)
E2E_COOKIES="$E2E_DIR/cookies.txt"
E2E_USER="ci-admin"
E2E_PASS="CI-Test-Passw0rd!"
E2E_EMAIL="ci-admin@example.com"

curl -sk -c "$E2E_COOKIES" -b "$E2E_COOKIES" https://localhost/ -o "$E2E_DIR/00-fresh.html"

curl -sk -c "$E2E_COOKIES" -b "$E2E_COOKIES" -L https://localhost/ \
--data-urlencode "username=${E2E_USER}" \
--data-urlencode "full_name=CI Admin" \
--data-urlencode "email=${E2E_EMAIL}" \
--data-urlencode "password=${E2E_PASS}" \
--data-urlencode "confirm_password=${E2E_PASS}" \
-d "verify_create_default_admin_account=CREATE" \
-o "$E2E_DIR/01-post-create.html"
check "Default admin account was created (login page now shown)" \
grep -q 'name="authenticate"' "$E2E_DIR/01-post-create.html"

E2E_CSRF=$(grep -oE 'name="csrf_token" value="[a-f0-9]+"' "$E2E_DIR/01-post-create.html" | grep -oE '[a-f0-9]{20,}')

curl -sk -c "$E2E_COOKIES" -b "$E2E_COOKIES" -L https://localhost/ \
-d "csrf_token=${E2E_CSRF}" \
--data-urlencode "user=${E2E_USER}" \
--data-urlencode "pass=${E2E_PASS}" \
-d "submit=submit" \
-o "$E2E_DIR/02-post-login.html"
check "Logged in as the newly-created admin account" \
grep -qi 'logout' "$E2E_DIR/02-post-login.html"

curl -sk -c "$E2E_COOKIES" -b "$E2E_COOKIES" https://localhost/admin/health_check.php \
-o "$E2E_DIR/03-health-check.html" -w '%{http_code}' > "$E2E_DIR/03-health-check.code"
E2E_HEALTH_CODE=$(cat "$E2E_DIR/03-health-check.code")
check "Health check page loads (HTTP 200)" \
test "${E2E_HEALTH_CODE}" = "200"
check "Health check: base URL matches the URL used to connect" \
grep -q 'Base URL matches the URL you are using to connect to SimpleRisk' "$E2E_DIR/03-health-check.html"
check "Health check: communicated with the SimpleRisk API" \
grep -q 'Communicated with the SimpleRisk API successfully' "$E2E_DIR/03-health-check.html"
# Two specific leaf checks can never pass in this environment, and their
# failure also flips two summary rollup rows to bad - none of this reflects
# a script or app defect:
# - "a DNS lookup was not successful": check_simplerisk_base_url_dns()
# calls dns_get_record() against SERVER_NAME ("localhost" here), which
# can never resolve via real DNS - only /etc/hosts would, and
# dns_get_record() doesn't consult it. This would be false on any real
# deployment tested via http://localhost/ too, before a real domain is
# configured.
# - "hasn't run in the past hour": check_cron_configured() only reports
# healthy if cron ticked within the last 3600s. This script verifies
# cron is installed, configured, and running instead (see the Cron
# section above) rather than waiting up to an hour for a real tick.
# - "SimpleRisk Core" and "Connectivity" are summary rollups that go bad
# whenever any check in their group fails, including the two above.
# They're excluded here only alongside their known-bad members - their
# other sibling checks (app/db version, session handling, data
# integrity, base URL match, API/database/web connectivity) are all
# still verified above/below and would surface their own distinct
# failure text here if something else broke.
KNOWN_ENVIRONMENT_LIMITATIONS="$E2E_DIR/known-environment-limitations.txt"
cat > "$KNOWN_ENVIRONMENT_LIMITATIONS" <<'EOF'
SimpleRisk Core
Connectivity
The detected server name is a valid domain, but a DNS lookup was not successful.
The automation cron hasn&#039;t run in the past hour. Check the &#039;Backups&#039; tab under Configure-&gt; Settings to learn more.
EOF

E2E_HEALTH_FAILURES=$(grep -oE 'x-mark-5-16[^&]*&nbsp;&nbsp;[^<]*' "$E2E_DIR/03-health-check.html" | sed -E 's/^.*&nbsp;&nbsp;//')
if [ -n "$E2E_HEALTH_FAILURES" ]; then
echo " --- health check page has failing item(s) ---"
echo "$E2E_HEALTH_FAILURES" | sed 's/^/ - /'
echo " --- end health check failures ---"
E2E_UNEXPECTED_FAILURES=$(echo "$E2E_HEALTH_FAILURES" | grep -vxFf "$KNOWN_ENVIRONMENT_LIMITATIONS" || true)
else
E2E_UNEXPECTED_FAILURES=""
fi
check "Health check: no unexpected failures (excluding known environment limitations above)" \
test -z "$E2E_UNEXPECTED_FAILURES"

rm -rf "$E2E_DIR"

# ── Summary ──────────────────────────────────────────────────────────────────
echo ""
echo "=== Verification Summary ==="
Expand Down
Loading