sesame/doctor/
sandbox.rs

1//! Sandbox checks — seccomp, no_new_privs, Landlock.
2
3use super::{Check, Status};
4
5pub fn checks() -> Vec<Check> {
6    let mut results = Vec::new();
7    let pids = daemon_pids();
8
9    // sandbox.seccomp — check seccomp filter mode for all daemons.
10    let all_filtered = !pids.is_empty()
11        && pids
12            .iter()
13            .all(|pid| read_proc_field(*pid, "Seccomp").as_deref() == Some("2"));
14    results.push(Check {
15        id: "sandbox.seccomp".into(),
16        category: "sandbox",
17        status: if all_filtered {
18            Status::Pass
19        } else {
20            Status::Fail
21        },
22        value: if all_filtered {
23            "filter mode (all daemons)".into()
24        } else {
25            "not all daemons have seccomp filter".into()
26        },
27        description: "Seccomp BPF restricts available syscalls per daemon".into(),
28    });
29
30    // sandbox.no_new_privs — check NoNewPrivs for all daemons.
31    let all_nnp = !pids.is_empty()
32        && pids
33            .iter()
34            .all(|pid| read_proc_field(*pid, "NoNewPrivs").as_deref() == Some("1"));
35    results.push(Check {
36        id: "sandbox.no_new_privs".into(),
37        category: "sandbox",
38        status: if all_nnp { Status::Pass } else { Status::Warn },
39        value: if all_nnp {
40            "set (all daemons)".into()
41        } else {
42            "not set on all daemons".into()
43        },
44        description: "NoNewPrivileges prevents privilege escalation via setuid/capabilities".into(),
45    });
46
47    results
48}
49
50fn read_proc_field(pid: u32, field: &str) -> Option<String> {
51    let content = std::fs::read_to_string(format!("/proc/{pid}/status")).ok()?;
52    for line in content.lines() {
53        if let Some(rest) = line.strip_prefix(field) {
54            return Some(rest.trim_start_matches(':').trim().to_string());
55        }
56    }
57    None
58}
59
60fn daemon_pids() -> Vec<u32> {
61    let binaries = [
62        "daemon-profile",
63        "daemon-secrets",
64        "daemon-wm",
65        "daemon-launcher",
66        "daemon-clipboard",
67        "daemon-input",
68        "daemon-snippets",
69    ];
70    binaries
71        .iter()
72        .filter_map(|name| {
73            let output = std::process::Command::new("pidof")
74                .arg(name)
75                .output()
76                .ok()?;
77            if output.status.success() {
78                String::from_utf8_lossy(&output.stdout)
79                    .split_whitespace()
80                    .next()?
81                    .parse()
82                    .ok()
83            } else {
84                None
85            }
86        })
87        .collect()
88}