95 lines
3.5 KiB
QML
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;
|
|
}
|
|
}
|
|
}
|