160 lines
8.3 KiB
Bash
Executable File
160 lines
8.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# The Sharing page must report what is true, and never claim a security
|
|
# property it cannot back up.
|
|
#
|
|
# The two failures worth a test:
|
|
#
|
|
# A service that is not installed shown as a switch. That is what the panel
|
|
# this replaces does, and the switch does nothing.
|
|
#
|
|
# "Keys only" claimed for SSH when the configuration is silent. OpenSSH's
|
|
# default accepts passwords, so stating the stronger thing without evidence
|
|
# would tell someone their machine is safer than it is.
|
|
#
|
|
# Read-only: this reads service state and never enables or disables anything.
|
|
|
|
set -uo pipefail
|
|
|
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
helper="$repo_dir/config/dot/quickshell/scripts/panama-sharing"
|
|
service="$repo_dir/config/dot/quickshell/services/Sharing.qml"
|
|
page="$repo_dir/config/dot/quickshell/modules/settings/SharingPage.qml"
|
|
|
|
fail() {
|
|
printf 'sharing contract: %s\n' "$1" >&2
|
|
exit 1
|
|
}
|
|
|
|
for path in "$helper" "$service" "$page"; do
|
|
[[ -r "$path" ]] || fail "missing $path"
|
|
done
|
|
[[ -x "$helper" ]] || fail 'panama-sharing is not executable'
|
|
|
|
# ── The password-authentication claim is evidence-based ─────────────────────
|
|
summary="$(sed -n '/function passwordLoginSummary/,/^ }/p' "$service")"
|
|
[[ -n "$summary" ]] || fail 'the service does not summarize password sign-in'
|
|
grep -q 'stated === ""' <<<"$summary" \
|
|
|| fail 'the summary does not distinguish "configured" from "silent"'
|
|
grep -qE 'system default' <<<"$summary" \
|
|
|| fail 'a silent configuration is not reported as the system default'
|
|
# "Keys only" may only be said when the file actually says no.
|
|
keys_line="$(grep -n 'keys only' <<<"$summary" | head -1)"
|
|
[[ -n "$keys_line" ]] || fail 'the summary never reports keys-only'
|
|
grep -q 'toLowerCase() === "no"' <<<"$summary" \
|
|
|| fail 'keys-only is claimed without checking what the configuration says'
|
|
|
|
# ── Absent software is reported, not offered ────────────────────────────────
|
|
grep -q 'is not installed' "$page" \
|
|
|| fail 'the page does not say when the software for a row is missing'
|
|
# A switch for a service that is not installed must be disabled.
|
|
grep -q 'Sharing.remoteLogin?.installed === true' "$page" \
|
|
|| fail 'the remote login switch is enabled regardless of whether SSH is installed'
|
|
grep -q 'Sharing.remoteDesktop?.available === true' "$page" \
|
|
|| fail 'the remote desktop switch is enabled regardless of whether it is available'
|
|
|
|
# Turning on remote desktop without credentials would start a service nobody
|
|
# can connect to; the page must require them first.
|
|
grep -q 'hasCredentials === true' "$page" \
|
|
|| fail 'remote desktop can be enabled with no credentials set'
|
|
grep -q 'hasCredentials' "$helper" \
|
|
|| fail 'the helper does not know whether credentials exist'
|
|
grep -qiE 'grdctl.*(password|username)[^)]*\)' "$helper" \
|
|
&& fail 'the helper passes remote desktop credentials on a command line'
|
|
|
|
# ── Privilege boundaries ────────────────────────────────────────────────────
|
|
# Remote login is system-wide and must go through a prompt; remote desktop is a
|
|
# user service and must not ask for one.
|
|
login_body="$(sed -n '/^def set_remote_login/,/^def /p' "$helper")"
|
|
grep -q 'pkexec' <<<"$login_body" \
|
|
|| fail 'changing a system-wide service does not ask for authorization'
|
|
desktop_body="$(sed -n '/^def set_remote_desktop/,/^def /p' "$helper")"
|
|
grep -q 'pkexec' <<<"$desktop_body" \
|
|
&& fail 'a user service asks for administrator rights it does not need'
|
|
grep -q '"--user"' <<<"$desktop_body" \
|
|
|| fail 'remote desktop is not managed as a user service'
|
|
|
|
# ── The remote desktop password never passes through Panama ────────────────
|
|
# grdctl takes it on a terminal and core-dumps without one, so the only two
|
|
# options were a terminal hand-off or an argument -- and an argument publishes
|
|
# it through /proc to every process on this machine.
|
|
grep -q 'set-credentials' "$service" \
|
|
|| fail 'the service cannot set remote desktop credentials at all'
|
|
grep -qE 'set-credentials".*(password|secret)' "$service" \
|
|
&& fail 'the service puts a password on the command line'
|
|
grep -q 'set-credentials' "$helper" \
|
|
&& fail 'the helper handles credentials; that path cannot prompt and must stay in a terminal'
|
|
grep -q 'kitty' "$service" \
|
|
|| fail 'credentials are not handed to a terminal, so nothing can prompt for them'
|
|
grep -q 'clear-rdp-credentials' "$helper" \
|
|
|| fail 'stored credentials cannot be cleared'
|
|
|
|
# ── The page says the true thing in the right place ─────────────────────────
|
|
#
|
|
# Three copy rules, each of which was a real failure before it was a rule.
|
|
#
|
|
# The failure banner floated above every card as a full-width red bar, so a
|
|
# grdctl error that concerned one row repainted the whole page as broken. It
|
|
# belongs inside the card whose action failed -- the machine card, which is
|
|
# where refresh and the hostname live.
|
|
page_code="$(grep -vE '^\s*//' "$page")"
|
|
python3 - "$page" <<'PY' || fail 'the failure message is not inside the first card, so one row failing reads as the page failing'
|
|
import re
|
|
import sys
|
|
|
|
lines = open(sys.argv[1], encoding="utf-8").read().splitlines()
|
|
start = next((i for i, line in enumerate(lines) if re.search(r"\bSettingsCard\s*\{", line)), None)
|
|
if start is None:
|
|
raise SystemExit(1)
|
|
depth = 0
|
|
end = None
|
|
for index in range(start, len(lines)):
|
|
depth += lines[index].count("{") - lines[index].count("}")
|
|
if depth <= 0:
|
|
end = index
|
|
break
|
|
if end is None:
|
|
raise SystemExit(1)
|
|
card = "\n".join(lines[start:end + 1])
|
|
raise SystemExit(0 if "Sharing.lastError" in card else 1)
|
|
PY
|
|
|
|
# The terminal is the mechanism, not the explanation. "Opens kitty" tells
|
|
# somebody the name of a program they did not ask about and still leaves them
|
|
# wondering why a settings page cannot take a password; the reason it cannot is
|
|
# the sentence worth printing.
|
|
! grep -qi 'kitty' <<<"$page_code" \
|
|
|| fail 'the page names the terminal application in text the user reads; that belongs to the service'
|
|
grep -q 'never passes through Panama' <<<"$page_code" \
|
|
|| fail 'the credentials row does not say why the password is set elsewhere'
|
|
|
|
# A row for software that is not here has to be worth reading. "Not installed"
|
|
# on its own is a dead end; what installing it would give you is not.
|
|
grep -q 'install it and this becomes a switch' <<<"$page_code" \
|
|
|| fail 'the file sharing row does not say what installing Samba would unlock'
|
|
grep -q 'does not install software' <<<"$page_code" \
|
|
|| fail 'the file sharing row does not say that Settings will not install it for you'
|
|
|
|
# ── The snapshot reflects the machine ───────────────────────────────────────
|
|
command -v jq >/dev/null 2>&1 || { printf 'sharing contract: SKIP (no jq)\n'; exit 0; }
|
|
snapshot="$("$helper" snapshot 2>/dev/null)" || fail 'snapshot failed'
|
|
jq -e '.hostname | length > 0' <<<"$snapshot" >/dev/null || fail 'no hostname reported'
|
|
jq -e '.remoteLogin | has("installed") and has("active") and has("enabled")' <<<"$snapshot" >/dev/null \
|
|
|| fail 'remote login state is incomplete'
|
|
jq -e '.remoteDesktop | has("available") and has("hasCredentials")' <<<"$snapshot" >/dev/null \
|
|
|| fail 'remote desktop state is incomplete'
|
|
|
|
# Installed-ness must match what is actually on this machine, not a guess.
|
|
expected_samba=$(command -v smbd >/dev/null 2>&1 && echo true || echo false)
|
|
actual_samba="$(jq -r '.fileSharing.installed' <<<"$snapshot")"
|
|
[[ "$expected_samba" == "$actual_samba" ]] \
|
|
|| fail "file sharing reports installed=$actual_samba but smbd presence is $expected_samba"
|
|
|
|
# No credential may appear in the snapshot.
|
|
offenders="$(jq -r '[paths | map(tostring) | join(".")] | map(select(test("(password|secret|credential)$";"i"))) | join(", ")' <<<"$snapshot")"
|
|
[[ -z "$offenders" ]] || fail "the snapshot carries credential-shaped fields: $offenders"
|
|
|
|
printf 'sharing contract: PASS (remote login %s, remote desktop %s)\n' \
|
|
"$(jq -r 'if .remoteLogin.active then "on" else "off" end' <<<"$snapshot")" \
|
|
"$(jq -r 'if .remoteDesktop.active then "on" else "off" end' <<<"$snapshot")"
|