Files
Panama/config/dot/quickshell/services/DeviceSecurity.qml
T

95 lines
3.5 KiB
QML

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;
}
}
}