From 14529c011ff2e888b751282e7c660fdf6bc5920e Mon Sep 17 00:00:00 2001 From: Gabriel Brown Date: Tue, 18 Aug 2026 08:20:04 -0400 Subject: [PATCH] Add a Privacy & Security page Continuing towards GNOME Settings parity. GNOME's Privacy panel covers screen lock, camera and microphone access, file history, trash, and device security; Panama had no equivalent page at all, despite already tracking camera and microphone use for the bar indicator. Device security is a new read-only readout: Secure Boot, TPM, disk encryption, SELinux mode, and the firewall. None of these is a preference -- they are set in firmware, at install time, or by system policy, and a switch offering to change them would either fail or do something far-reaching from a control that looks like every other control. What it answers is "is this machine set up the way I think it is", which otherwise takes five commands and root. Facts that cannot be determined report Unknown rather than guessing, because a security readout that quietly says "fine" when it failed to look is worse than no readout. File history and trash retention are deliberately NOT offered as switches. They are GNOME preferences enforced by gsd-housekeeping, which does not run in a Hyprland session -- verified, it is not running here. Toggling them would store a preference, change nothing, and give no sign of it. They are delegated to GNOME Settings by name instead. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L --- .../modules/settings/PrivacyPage.qml | 103 ++++++++++++++++++ .../modules/settings/SettingsShell.qml | 2 + .../modules/settings/SettingsSidebar.qml | 1 + config/dot/quickshell/modules/settings/qmldir | 1 + config/dot/quickshell/scripts/panama-security | 83 ++++++++++++++ .../quickshell/services/DeviceSecurity.qml | 54 +++++++++ config/dot/quickshell/services/ShellState.qml | 2 +- 7 files changed, 245 insertions(+), 1 deletion(-) create mode 100644 config/dot/quickshell/modules/settings/PrivacyPage.qml create mode 100755 config/dot/quickshell/scripts/panama-security create mode 100644 config/dot/quickshell/services/DeviceSecurity.qml diff --git a/config/dot/quickshell/modules/settings/PrivacyPage.qml b/config/dot/quickshell/modules/settings/PrivacyPage.qml new file mode 100644 index 0000000..594964c --- /dev/null +++ b/config/dot/quickshell/modules/settings/PrivacyPage.qml @@ -0,0 +1,103 @@ +// Privacy & Security. +// +// GNOME's Privacy panel covers screen lock, camera and microphone access, file +// history, trash, and device security. Panama covers the parts it genuinely +// owns and is explicit about the parts it does not. +// +// The file-history and trash settings are the notable omission, and the reason +// is worth stating: those are GNOME preferences enforced by gsd-housekeeping, +// which is not running in a Hyprland session. Offering switches for them would +// store a preference, change nothing, and give no sign of it -- the exact +// failure this codebase keeps designing against. So they are delegated by name +// rather than reimplemented as controls that lie. + +import QtQuick +import qs.config +import qs.services + +SettingsPage { + id: root + + title: "Privacy & Security" + lede: DeviceSecurity.scanned && DeviceSecurity.attentionCount === 0 + ? "Screen lock, device access, and a machine whose security settings all check out." + : "Screen lock, which applications can see you, and how this machine is protected." + + Component.onCompleted: if (!DeviceSecurity.scanned) DeviceSecurity.refresh() + + SettingsCard { + title: "Screen lock" + subtitle: "The same settings as Power & Lock, which is where the idle timings live." + + SliderRow { setting: "lockMinutes"; zeroLabel: "Never" } + ToggleRow { setting: "lockOnSleep"; divider: false } + } + + SettingsCard { + title: "Camera & microphone" + subtitle: PrivacyState.anyActive + ? "In use right now — the bar shows an indicator whenever this is true." + : "Nothing is using your camera or microphone." + + TextRow { + label: "Camera" + detail: PrivacyState.cameraActive + ? "In use by " + (PrivacyState.cameraApp || "an application") + : "Not in use" + value: PrivacyState.cameraActive ? "Active" : "Idle" + } + + TextRow { + label: "Microphone" + detail: PrivacyState.microphoneActive + ? "In use by " + (PrivacyState.microphoneApp || "an application") + : "Not in use" + value: PrivacyState.microphoneActive ? "Active" : "Idle" + } + + TextRow { + label: "Screen sharing" + detail: PrivacyState.screenSharingActive + ? "Being shared by " + (PrivacyState.screenSharingApp || "an application") + : "Not being shared" + value: PrivacyState.screenSharingActive ? "Active" : "Idle" + divider: false + } + } + + SettingsCard { + title: "Device security" + subtitle: DeviceSecurity.attentionCount === 0 + ? "Everything below is in its recommended state." + : DeviceSecurity.attentionCount + " item" + + (DeviceSecurity.attentionCount === 1 ? "" : "s") + " below may deserve attention." + + Repeater { + model: DeviceSecurity.facts + + TextRow { + id: factRow + required property var modelData + required property int index + + label: factRow.modelData.label + detail: factRow.modelData.detail + value: factRow.modelData.value + divider: factRow.index < DeviceSecurity.facts.length - 1 + } + } + } + + SettingsCard { + title: "Owned by Fedora" + subtitle: "File history and trash retention are GNOME preferences, applied by a housekeeping service that does not run in a Hyprland session. Panama does not offer them as switches, because storing that preference here would change nothing." + + ActionRow { + label: "File history & trash" + detail: "Opens GNOME Settings, which owns these" + action: "Open privacy" + divider: false + onTriggered: SystemSettings.openGnomePanel("privacy") + } + } +} diff --git a/config/dot/quickshell/modules/settings/SettingsShell.qml b/config/dot/quickshell/modules/settings/SettingsShell.qml index def527a..033cdf6 100644 --- a/config/dot/quickshell/modules/settings/SettingsShell.qml +++ b/config/dot/quickshell/modules/settings/SettingsShell.qml @@ -100,6 +100,7 @@ Rectangle { case "screen-intelligence": return screenIntelligencePage; case "shortcuts": return shortcutsPage; case "mouse": return mousePage; + case "privacy": return privacyPage; case "accessibility": return accessibilityPage; case "power": return powerPage; case "datetime": return dateTimePage; @@ -155,6 +156,7 @@ Rectangle { Component { id: screenIntelligencePage; ScreenIntelligencePage {} } Component { id: shortcutsPage; ShortcutsPage {} } Component { id: mousePage; MousePage {} } + Component { id: privacyPage; PrivacyPage {} } Component { id: servicesPage; ServicesPage {} } Component { id: aboutPage; AboutPage {} } diff --git a/config/dot/quickshell/modules/settings/SettingsSidebar.qml b/config/dot/quickshell/modules/settings/SettingsSidebar.qml index bfe4d36..d82a99c 100644 --- a/config/dot/quickshell/modules/settings/SettingsSidebar.qml +++ b/config/dot/quickshell/modules/settings/SettingsSidebar.qml @@ -33,6 +33,7 @@ Rectangle { { page: "screen-intelligence", label: "Screen Intelligence", icon: "\u{F05A8}" }, { page: "shortcuts", label: "Keyboard", icon: "\u{F030C}" }, { page: "mouse", label: "Mouse & Touchpad", icon: "\u{F037D}" }, + { page: "privacy", label: "Privacy & Security", icon: "\u{F0483}" }, { page: "accessibility", label: "Accessibility", icon: "\u{F0208}" }, { page: "power", label: "Power & Lock", icon: "\u{F0425}" }, { page: "datetime", label: "Date & Time", icon: "\u{F0954}" }, diff --git a/config/dot/quickshell/modules/settings/qmldir b/config/dot/quickshell/modules/settings/qmldir index bf3768f..464b268 100644 --- a/config/dot/quickshell/modules/settings/qmldir +++ b/config/dot/quickshell/modules/settings/qmldir @@ -48,3 +48,4 @@ LocationPicker 1.0 LocationPicker.qml FontPicker 1.0 FontPicker.qml MousePage 1.0 MousePage.qml TextEntryRow 1.0 TextEntryRow.qml +PrivacyPage 1.0 PrivacyPage.qml diff --git a/config/dot/quickshell/scripts/panama-security b/config/dot/quickshell/scripts/panama-security new file mode 100755 index 0000000..6dcfcb8 --- /dev/null +++ b/config/dot/quickshell/scripts/panama-security @@ -0,0 +1,83 @@ +#!/usr/bin/env bash + +# Device security facts, as JSON. +# +# Everything here is READ-ONLY and deliberately so. Secure Boot, TPM presence, +# disk encryption, SELinux mode and the firewall are set in firmware, at install +# time, or by system policy -- none of them is a desktop preference, and a +# settings app that offered to toggle them would either fail or do something +# far-reaching from a switch that looks like any other. +# +# What it is for is answering "is this machine set up the way I think it is", +# which is the question GNOME's Device Security panel exists to answer and which +# otherwise needs five commands and root. +# +# Each fact is reported as {value, ok} where `ok` marks the reassuring state, so +# the UI can highlight what deserves attention without hard-coding the meaning +# of each string. Anything that cannot be determined reports "Unknown" with +# ok:false rather than guessing, because a security readout that quietly reports +# "fine" when it failed to look is worse than no readout. + +set -uo pipefail + +fact() { + jq -cn --arg label "$1" --arg value "$2" --argjson ok "$3" --arg detail "${4:-}" \ + '{label: $label, value: $value, ok: $ok, detail: $detail}' +} + +facts=() + +# ── Secure Boot ────────────────────────────────────────────────────────────── +if command -v mokutil >/dev/null 2>&1; then + case "$(mokutil --sb-state 2>/dev/null)" in + *"SecureBoot enabled"*) facts+=("$(fact "Secure Boot" "Enabled" true "Firmware verifies the bootloader and kernel signatures")" ) ;; + *"SecureBoot disabled"*) facts+=("$(fact "Secure Boot" "Disabled" false "Firmware does not verify what it boots")") ;; + *) facts+=("$(fact "Secure Boot" "Unknown" false "The firmware did not report a Secure Boot state")") ;; + esac +elif [[ -d /sys/firmware/efi ]]; then + facts+=("$(fact "Secure Boot" "Unknown" false "Install mokutil to report this")") +else + facts+=("$(fact "Secure Boot" "Not applicable" false "This machine booted in legacy BIOS mode")") +fi + +# ── TPM ────────────────────────────────────────────────────────────────────── +tpm_major="$(cat /sys/class/tpm/tpm0/tpm_version_major 2>/dev/null || true)" +if [[ -n "$tpm_major" ]]; then + facts+=("$(fact "TPM" "Version $tpm_major" true "A trusted platform module is present and usable")") +elif [[ -e /sys/class/tpm/tpm0 ]]; then + facts+=("$(fact "TPM" "Present" true "A trusted platform module is present")") +else + facts+=("$(fact "TPM" "None" false "No trusted platform module, so keys cannot be sealed to this machine")") +fi + +# ── Disk encryption ────────────────────────────────────────────────────────── +# Counts LUKS mappings rather than naming them: which volume is encrypted is +# more detail than this readout needs, and device names are not meaningful here. +crypt_count="$(lsblk -o TYPE 2>/dev/null | grep -c '^crypt$' || true)" +[[ "$crypt_count" =~ ^[0-9]+$ ]] || crypt_count=0 +if (( crypt_count > 0 )); then + facts+=("$(fact "Disk encryption" "$crypt_count encrypted volume$( (( crypt_count == 1 )) || printf 's')" true "Data at rest is protected by LUKS")") +else + facts+=("$(fact "Disk encryption" "None" false "No LUKS volume is unlocked on this machine")") +fi + +# ── SELinux ────────────────────────────────────────────────────────────────── +if command -v getenforce >/dev/null 2>&1; then + case "$(getenforce 2>/dev/null)" in + Enforcing) facts+=("$(fact "SELinux" "Enforcing" true "Policy violations are blocked")") ;; + Permissive) facts+=("$(fact "SELinux" "Permissive" false "Violations are logged but allowed")") ;; + Disabled) facts+=("$(fact "SELinux" "Disabled" false "Mandatory access control is off")") ;; + *) facts+=("$(fact "SELinux" "Unknown" false "")") ;; + esac +fi + +# ── Firewall ───────────────────────────────────────────────────────────────── +if systemctl list-unit-files firewalld.service >/dev/null 2>&1; then + if [[ "$(systemctl is-active firewalld 2>/dev/null)" == "active" ]]; then + facts+=("$(fact "Firewall" "Active" true "firewalld is filtering incoming connections")") + else + facts+=("$(fact "Firewall" "Inactive" false "firewalld is installed but not running")") + fi +fi + +printf '[%s]\n' "$(IFS=,; printf '%s' "${facts[*]}")" diff --git a/config/dot/quickshell/services/DeviceSecurity.qml b/config/dot/quickshell/services/DeviceSecurity.qml new file mode 100644 index 0000000..500d9ea --- /dev/null +++ b/config/dot/quickshell/services/DeviceSecurity.qml @@ -0,0 +1,54 @@ +pragma Singleton + +// Read-only device security facts: Secure Boot, TPM, disk encryption, SELinux, +// firewall. +// +// Nothing here is a preference. These are set in firmware, at install time, or +// by system policy, and a settings app that offered to change them from a +// switch would either fail or do something far-reaching from a control that +// looks like every other control. What this answers is "is this machine set up +// the way I think it is", which otherwise takes five commands and root. +// +// Read on demand. None of these can change while the desktop is running, +// short of a reboot. + +import Quickshell +import Quickshell.Io +import QtQuick + +Singleton { + id: root + + readonly property string helperPath: Quickshell.shellDir + "/scripts/panama-security" + + // [{ label, value, ok, detail }] + property var facts: [] + property bool scanned: false + + // The facts that are not in their reassuring state. The page leads with the + // count so a machine that is entirely fine says so in one line instead of + // making the user read five rows to find out. + readonly property int attentionCount: root.facts.filter(fact => !fact.ok).length + + function refresh(): void { + if (!query.running) + query.running = true; + } + + Process { + id: query + command: [root.helperPath] + stdout: StdioCollector { + onStreamFinished: { + try { + const parsed = JSON.parse(this.text); + root.facts = Array.isArray(parsed) ? parsed : []; + } catch (error) { + root.facts = []; + console.warn("DeviceSecurity: could not parse helper output:", error); + } + root.scanned = true; + } + } + } +} diff --git a/config/dot/quickshell/services/ShellState.qml b/config/dot/quickshell/services/ShellState.qml index 1b6c9f8..5a84298 100644 --- a/config/dot/quickshell/services/ShellState.qml +++ b/config/dot/quickshell/services/ShellState.qml @@ -92,7 +92,7 @@ Singleton { } function openSettings(page: string): void { - const allowed = ["home", "appearance", "displays", "connectivity", "home-phone", "desktop", "sound", "notifications", "screen-intelligence", "shortcuts", "mouse", "accessibility", "power", "datetime", "applications", "services", "about"]; + const allowed = ["home", "appearance", "displays", "connectivity", "home-phone", "desktop", "sound", "notifications", "screen-intelligence", "shortcuts", "mouse", "privacy", "accessibility", "power", "datetime", "applications", "services", "about"]; root.settingsPage = allowed.indexOf(page) >= 0 ? page : "home"; DesktopPreferences.set("lastPage", root.settingsPage); root.settingsOpen = true;