diff --git a/bin/omarchy-toggle-bar b/bin/omarchy-toggle-bar index f76fe049..be18a647 100755 --- a/bin/omarchy-toggle-bar +++ b/bin/omarchy-toggle-bar @@ -5,3 +5,9 @@ # omarchy:examples=omarchy toggle bar | omarchy toggle bar off | omarchy toggle bar on omarchy-toggle bar-off "${1:-toggle}" + +# The shell's watch on the toggles directory can miss flag changes that land in +# quick succession, stranding the bar off screen until the shell restarts. +# Nudge the bar to re-read the flag; quiet best-effort so the toggle still +# works when the shell is not up. +omarchy-shell -q omarchy.bar syncHidden diff --git a/shell/plugins/bar/Bar.qml b/shell/plugins/bar/Bar.qml index 9e736b3f..def615af 100644 --- a/shell/plugins/bar/Bar.qml +++ b/shell/plugins/bar/Bar.qml @@ -948,6 +948,21 @@ Item { onFileChanged: barHiddenProbe.running = true } + // The directory watch can permanently stop delivering events after flag + // changes land in quick succession, stranding the bar off screen until the + // shell restarts. `omarchy-toggle-bar` nudges this after flipping the flag + // so the probe re-reads it even when the watch has gone quiet. + IpcHandler { + target: "omarchy.bar" + + // Start rather than restart: a probe already in flight was launched by the + // directory watch after the flag flipped, so its answer is current, and + // killing it here can swallow the result entirely. + function syncHidden(): void { + barHiddenProbe.running = true + } + } + Variants { model: Quickshell.screens diff --git a/test/acceptance b/test/acceptance index 192e38f0..37a63f6e 100755 --- a/test/acceptance +++ b/test/acceptance @@ -19,7 +19,22 @@ mkdir -p "$OMARCHY_ACCEPTANCE_DIR" # the session environment is inherited. export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}" export DBUS_SESSION_BUS_ADDRESS="${DBUS_SESSION_BUS_ADDRESS:-unix:path=$XDG_RUNTIME_DIR/bus}" -export OMARCHY_PATH="${OMARCHY_PATH:-$ROOT}" +# The suite acts on the running session, and qs matches shell instances by +# config path — pointed at any other tree, every omarchy-shell call reads as +# "not running". So default OMARCHY_PATH to the tree the session was started +# from, then this checkout for standalone use, then the installed tree for +# when only test/ was synced in (omarchy-iso-test's --sync-omarchy). +if [[ -z ${OMARCHY_PATH:-} ]]; then + OMARCHY_PATH=$(systemctl --user show-environment 2>/dev/null | sed -n 's/^OMARCHY_PATH=//p' | tail -1) +fi +if [[ -z $OMARCHY_PATH ]]; then + OMARCHY_PATH=$ROOT +fi +if [[ ! -f $OMARCHY_PATH/shell/shell.qml && -f /usr/share/omarchy/shell/shell.qml ]]; then + OMARCHY_PATH=/usr/share/omarchy +fi +export OMARCHY_PATH + export PATH="$OMARCHY_PATH/bin:$PATH" if [[ -z ${DISPLAY:-} ]]; then diff --git a/test/acceptance.d/base-test.sh b/test/acceptance.d/base-test.sh index 717007c9..023223a1 100644 --- a/test/acceptance.d/base-test.sh +++ b/test/acceptance.d/base-test.sh @@ -36,7 +36,9 @@ screen_contains() { local text="$1" local snapshot="/tmp/omarchy-acceptance-ocr-$$.png" - if ! timeout 10 grim "$snapshot" 2>/dev/null; then + # Capture at 2x scale: tesseract routinely drops small caption text at + # native resolution (the weather panel's detail labels, for one). + if ! timeout 10 grim -s 2 "$snapshot" 2>/dev/null; then rm -f "$snapshot" return 1 fi diff --git a/test/acceptance.d/security-test.sh b/test/acceptance.d/security-test.sh new file mode 100755 index 00000000..0755c722 --- /dev/null +++ b/test/acceptance.d/security-test.sh @@ -0,0 +1,105 @@ +#!/bin/bash +# +# Verifies the security posture of an installed system: the unprivileged +# session-to-root paths closed for 4.0.2 (blanket input-group grant, shipped +# asdcontrol sudoers authorization) and the SSH hardening flow. +# +# The sshd section reconfigures the machine (enables sshd, opens the firewall, +# disables password logins), so it demands explicit opt-in: it only runs when +# OMARCHY_ACCEPTANCE_SUDO_PASSWORD is set, which omarchy-iso-test does for its +# throwaway VMs. A cached sudo timestamp alone never triggers it, so running +# the suite on a machine you care about cannot reconfigure sshd by accident. + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +# Membership of `input` gives raw access to /dev/input/event*: any process +# running as the user could log keystrokes. Only the opt-in controller and +# ydotool features may grant it. +verify_input_group() { + if id -nG | grep -qw input; then + if pacman -Q xpadneo-dkms &>/dev/null || pacman -Q ydotool &>/dev/null; then + pass "input group membership is backed by an opt-in feature" + else + fail "user is not in the input group" "no controller or ydotool support installed to justify it" + fi + else + pass "user is not in the input group" + fi +} + +sudo_available() { + if sudo -n true 2>/dev/null; then + return 0 + fi + + if [[ -n ${OMARCHY_ACCEPTANCE_SUDO_PASSWORD:-} ]]; then + printf '%s\n' "$OMARCHY_ACCEPTANCE_SUDO_PASSWORD" | sudo -S -v 2>/dev/null + return $? + fi + + return 1 +} + +verify_asdcontrol_sudoers() { + # Omarchy used to ship a passwordless sudoers grant for asdcontrol; that + # authorization now belongs to the package alone. + if sudo -n test -e /etc/sudoers.d/omarchy-asdcontrol; then + fail "no omarchy asdcontrol sudoers grant is shipped" "/etc/sudoers.d/omarchy-asdcontrol exists" + fi + pass "no omarchy asdcontrol sudoers grant is shipped" +} + +verify_sshd_hardening() { + local key_file=/tmp/omarchy-acceptance-sshd-key + local effective_config + + rm -f "$key_file" "$key_file.pub" + ssh-keygen -t ed25519 -N "" -q -C "omarchy-acceptance" -f "$key_file" + + if ! omarchy-setup-security-sshd --key="$(cat "$key_file.pub")" >"$ARTIFACTS/setup-security-sshd.log" 2>&1; then + fail "omarchy-setup-security-sshd completes unattended" "$(tail -5 "$ARTIFACTS/setup-security-sshd.log")" + fi + pass "omarchy-setup-security-sshd completes unattended" + + systemctl is-active sshd.service >/dev/null || fail "sshd is running after setup" + pass "sshd is running after setup" + + grep -qxF "$(cat "$key_file.pub")" "$HOME/.ssh/authorized_keys" || fail "the key is authorized" + pass "the key is authorized" + + # The command verifies its own hardening before keeping it, but assert the + # effective config independently: sshd honors the first value it reads, and + # regressions here reopen password logins. Keywords match case-insensitively + # because OpenSSH 9.x dumps them lowercase and 10.x in CamelCase. + effective_config=$(sudo -n sshd -T) || fail "sshd reports its effective config" + grep -qixF "passwordauthentication no" <<<"$effective_config" || fail "password authentication is off" + pass "password authentication is off" + grep -qixF "kbdinteractiveauthentication no" <<<"$effective_config" || fail "keyboard-interactive authentication is off" + pass "keyboard-interactive authentication is off" + + if omarchy-cmd-present ufw; then + sudo -n ufw status | grep -qE '^22/tcp\s+LIMIT' || fail "the SSH port is rate limited in the firewall" + pass "the SSH port is rate limited in the firewall" + fi + + # Leave the machine as found where cheap: the throwaway key stays useless + # once removed, while the hardening itself is the state under test. + sed -i "\#$(cat "$key_file.pub" | cut -d' ' -f2)#d" "$HOME/.ssh/authorized_keys" + rm -f "$key_file" "$key_file.pub" +} + +verify_input_group + +if sudo_available; then + verify_asdcontrol_sudoers +else + pass "asdcontrol sudoers check skipped: sudo needs a password" +fi + +if [[ -n ${OMARCHY_ACCEPTANCE_SUDO_PASSWORD:-} ]] && sudo_available; then + verify_sshd_hardening +else + pass "sshd hardening exercise skipped: set OMARCHY_ACCEPTANCE_SUDO_PASSWORD to run it" +fi diff --git a/test/acceptance.d/system-test.sh b/test/acceptance.d/system-test.sh index 610fcb4b..0d648e9c 100644 --- a/test/acceptance.d/system-test.sh +++ b/test/acceptance.d/system-test.sh @@ -8,12 +8,17 @@ status=0 verify_core_packages() { local package + local manifest="$OMARCHY_PATH/install/omarchy-base.packages" local -a missing=() + # Without this, a missing manifest reads as an empty package list and the + # audit passes having checked nothing. + [[ -f $manifest ]] || fail "all Omarchy core packages are installed" "package manifest not found: $manifest" + while IFS= read -r package; do [[ -z $package || $package == \#* ]] && continue pacman -Q "$package" >/dev/null 2>&1 || missing+=("$package") - done <"$OMARCHY_PATH/install/omarchy-base.packages" + done <"$manifest" (( ${#missing[@]} == 0 )) || fail "all Omarchy core packages are installed" "missing packages: ${missing[*]}" pass "all Omarchy core packages are installed (${#missing[@]} missing)"