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 // Why the last read produced nothing, when it produced nothing. // // A security readout with no facts is a read that failed, not a machine // with nothing to say, and the two are indistinguishable downstream: // attentionCount is 0 for an empty list exactly as it is for a clean one, // so a page reading only that would answer "everything is in its // recommended state" on top of a helper that never ran. The helper builds // its JSON with jq and prints `[]` when jq is missing, so this is a real // failure mode and not a hypothetical one. // // Read it against an empty `facts`: a read that produced facts AND wrote // something to stderr is a helper being chatty, not a failed check. property string lastError: "" // 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) { root.lastError = ""; query.running = true; } } // Only fills in a reason nothing else has given, so whichever of stderr, // the exit code and the parse notices the failure first keeps the say. function blame(reason: string): void { if (root.lastError === "") root.lastError = reason; } function absorb(text: string): void { const answer = text.trim(); if (answer === "") { root.facts = []; root.blame("the security helper answered with nothing."); } else { try { const parsed = JSON.parse(answer); root.facts = Array.isArray(parsed) ? parsed : []; if (root.facts.length === 0) root.blame("the security helper reported no facts at all."); } catch (error) { root.facts = []; root.blame("the security helper's answer could not be read."); console.warn("DeviceSecurity: could not parse helper output:", error); } } root.scanned = true; } Process { id: query command: [root.helperPath] stdout: StdioCollector { onStreamFinished: root.absorb(this.text) } stderr: StdioCollector { onStreamFinished: if (this.text.trim() !== "") root.blame(this.text.trim()) } onExited: (code, status) => { if (code !== 0) root.blame("the security helper exited with code " + code + "."); root.scanned = true; } } }