use russh::client::Handle;
use super::{FacetKey, FacetValue, HostFacts};
use crate::monitor::Platform;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Privilege {
User,
Root,
}
pub struct FacetSpec {
pub key: FacetKey,
pub privilege: Privilege,
pub linux: Option<&'static str>,
pub macos: Option<&'static str>,
pub parse: fn(&str) -> Option<FacetValue>,
}
impl FacetSpec {
pub fn command(&self, platform: &Platform) -> Option<&'static str> {
match platform {
Platform::Linux => self.linux,
Platform::MacOS => self.macos,
_ => None,
}
}
}
fn first_line(raw: &str) -> Option<FacetValue> {
let t = raw.trim();
(!t.is_empty()).then(|| FacetValue::Text(t.lines().next().unwrap_or("").trim().to_string()))
}
fn number(raw: &str) -> Option<FacetValue> {
raw.split_whitespace()
.next()?
.parse::<f64>()
.ok()
.map(FacetValue::Number)
}
fn root_disk_pct(raw: &str) -> Option<FacetValue> {
let line = raw.lines().nth(1)?;
let tok = line
.split_whitespace()
.rev()
.find(|t| t.ends_with('%') && t.trim_end_matches('%').parse::<u64>().is_ok())?;
tok.trim_end_matches('%')
.parse::<f64>()
.ok()
.map(FacetValue::Number)
}
fn listening_ports(raw: &str) -> Option<FacetValue> {
fn port_of(token: &str) -> Option<u32> {
let tail = token.rsplit([':', '.']).next()?;
let port = tail.parse::<u32>().ok()?;
(port > 0 && port <= 65535).then_some(port)
}
let mut ports: Vec<u32> = raw
.lines()
.filter(|l| l.contains("LISTEN") || l.trim_start().starts_with("tcp"))
.filter_map(|l| {
l.split_whitespace()
.filter(|t| t.contains(':') || t.contains('.'))
.find_map(port_of)
})
.collect();
if ports.is_empty() {
return None;
}
ports.sort_unstable();
ports.dedup();
Some(FacetValue::Text(
ports
.iter()
.map(|p| p.to_string())
.collect::<Vec<_>>()
.join(","),
))
}
fn host_key_algos(raw: &str) -> Option<FacetValue> {
let mut algos: Vec<&str> = raw
.lines()
.map(|l| l.trim())
.filter(|l| !l.is_empty() && !l.starts_with('#'))
.filter_map(|l| l.split_whitespace().nth(1))
.filter(|a| a.starts_with("ssh-") || a.starts_with("ecdsa-") || a.starts_with("sk-"))
.collect();
if algos.is_empty() {
return None;
}
algos.sort_unstable();
algos.dedup();
Some(FacetValue::Text(algos.join(",")))
}
fn enabled_units(raw: &str) -> Option<FacetValue> {
let mut units: Vec<&str> = raw
.lines()
.filter_map(|l| l.split_whitespace().next())
.filter(|u| u.ends_with(".service"))
.collect();
if units.is_empty() {
return None;
}
units.sort_unstable();
units.dedup();
Some(FacetValue::Text(units.join(",")))
}
fn uptime_days(raw: &str) -> Option<FacetValue> {
let secs = if raw.contains("sec =") {
let mut lines = raw.lines().filter(|l| !l.trim().is_empty());
let boot = lines.next()?;
let now = lines.next()?;
crate::monitor::parser::parse_boottime(boot, now)? as f64
} else {
raw.split_whitespace().next()?.parse().ok()?
};
Some(FacetValue::Number((secs / 86_400.0).floor()))
}
pub fn facet_specs(config_paths: &[String], packages: &[String]) -> Vec<FacetSpec> {
let mut specs = vec![
FacetSpec {
key: FacetKey::Kernel,
privilege: Privilege::User,
linux: Some("uname -r"),
macos: Some("uname -r"),
parse: first_line,
},
FacetSpec {
key: FacetKey::OsRelease,
privilege: Privilege::User,
linux: Some(". /etc/os-release 2>/dev/null && echo \"$ID $VERSION_ID\""),
macos: Some("sw_vers -productVersion"),
parse: first_line,
},
FacetSpec {
key: FacetKey::CpuModel,
privilege: Privilege::User,
linux: Some("awk -F: '/model name/{print $2; exit}' /proc/cpuinfo"),
macos: Some("sysctl -n machdep.cpu.brand_string"),
parse: first_line,
},
FacetSpec {
key: FacetKey::CpuCount,
privilege: Privilege::User,
linux: Some("nproc"),
macos: Some("sysctl -n hw.ncpu"),
parse: number,
},
FacetSpec {
key: FacetKey::MemTotal,
privilege: Privilege::User,
linux: Some("awk '/MemTotal/{print int($2/1048576)}' /proc/meminfo"),
macos: Some("echo $(( $(sysctl -n hw.memsize) / 1073741824 ))"),
parse: number,
},
FacetSpec {
key: FacetKey::OpenSsl,
privilege: Privilege::User,
linux: Some("openssl version 2>/dev/null"),
macos: Some("openssl version 2>/dev/null"),
parse: first_line,
},
FacetSpec {
key: FacetKey::Timezone,
privilege: Privilege::User,
linux: Some("date +%Z%z"),
macos: Some("date +%Z%z"),
parse: first_line,
},
FacetSpec {
key: FacetKey::NtpSync,
privilege: Privilege::User,
linux: Some("timedatectl show -p NTPSynchronized --value 2>/dev/null"),
macos: Some("sntp -q time.apple.com >/dev/null 2>&1 && echo yes || echo no"),
parse: first_line,
},
FacetSpec {
key: FacetKey::SshHostKeyAlgo,
privilege: Privilege::User,
linux: Some("ssh-keyscan -T 2 localhost 2>/dev/null"),
macos: Some("ssh-keyscan -T 2 localhost 2>/dev/null"),
parse: host_key_algos,
},
FacetSpec {
key: FacetKey::DiskRootPct,
privilege: Privilege::User,
linux: Some("df -Pk /"),
macos: Some("df -Pk /"),
parse: root_disk_pct,
},
FacetSpec {
key: FacetKey::UptimeDays,
privilege: Privilege::User,
linux: Some("cat /proc/uptime"),
macos: Some("sysctl -n kern.boottime; date +%s"),
parse: uptime_days,
},
FacetSpec {
key: FacetKey::LoadPerCore,
privilege: Privilege::User,
linux: Some("awk -v c=$(nproc) '{printf \"%.2f\", $1/c}' /proc/loadavg"),
macos: Some(
"sysctl -n vm.loadavg | awk -v c=$(sysctl -n hw.ncpu) '{printf \"%.2f\", $2/c}'",
),
parse: number,
},
FacetSpec {
key: FacetKey::ListeningPorts,
privilege: Privilege::User,
linux: Some("ss -lnt 2>/dev/null || netstat -lnt 2>/dev/null"),
macos: Some("netstat -an -p tcp | grep LISTEN"),
parse: listening_ports,
},
FacetSpec {
key: FacetKey::SystemdUnits,
privilege: Privilege::User,
linux: Some("systemctl list-unit-files --state=enabled --no-legend 2>/dev/null"),
macos: None,
parse: enabled_units,
},
];
for path in config_paths {
specs.push(FacetSpec {
key: FacetKey::FileHash(path.clone()),
privilege: Privilege::Root,
linux: None, macos: None,
parse: first_line,
});
}
for pkg in packages {
specs.push(FacetSpec {
key: FacetKey::PkgVersion(pkg.clone()),
privilege: Privilege::User,
linux: None,
macos: None,
parse: first_line,
});
}
specs
}
pub fn command_for_file_hash(path: &str) -> String {
format!(
"if [ ! -e '{p}' ]; then echo 'ESSH_ABSENT'; \
elif [ ! -r '{p}' ]; then echo 'ESSH_DENIED'; \
else (sha256sum '{p}' 2>/dev/null || shasum -a 256 '{p}' 2>/dev/null) | cut -c1-16; fi",
p = path
)
}
pub fn command_for_package(pkg: &str, platform: &Platform) -> Option<String> {
match platform {
Platform::Linux => Some(format!(
"(dpkg-query -W -f='${{Version}}' {p} 2>/dev/null || rpm -q --qf '%{{VERSION}}' {p} 2>/dev/null || echo ESSH_ABSENT)",
p = pkg
)),
Platform::MacOS => Some(format!(
"(brew list --versions {p} 2>/dev/null | awk '{{print $2}}' || echo ESSH_ABSENT)",
p = pkg
)),
_ => None,
}
}
pub fn interpret_sentinel(raw: &str) -> Option<FacetValue> {
match raw.trim() {
"ESSH_ABSENT" => Some(FacetValue::Missing("not installed".into())),
"ESSH_DENIED" => Some(FacetValue::Missing("permission denied".into())),
_ => None,
}
}
pub async fn collect_facts<H: russh::client::Handler>(
handle: &Handle<H>,
host: &str,
platform: &Platform,
config_paths: &[String],
packages: &[String],
) -> HostFacts {
let mut facts = HostFacts::new(host);
let specs = facet_specs(config_paths, packages);
let mut script = String::new();
let mut order: Vec<(usize, &FacetSpec)> = Vec::new();
for (i, spec) in specs.iter().enumerate() {
let cmd: Option<String> = match &spec.key {
FacetKey::FileHash(p) => Some(command_for_file_hash(p)),
FacetKey::PkgVersion(p) => command_for_package(p, platform),
_ => spec.command(platform).map(|s| s.to_string()),
};
match cmd {
Some(c) => {
script.push_str(&format!("printf '\\n==={}===\\n'; {}; ", i, c));
order.push((i, spec));
}
None => {
facts.facets.insert(
spec.key.clone(),
FacetValue::Missing(format!("not available on {}", platform.label())),
);
}
}
}
script.push_str("printf '\\n===END===\\n'");
let raw = match exec(handle, &script).await {
Ok(r) => r,
Err(e) => {
let reason = format!("collector failed: {}", e);
for (_, spec) in &order {
facts
.facets
.insert(spec.key.clone(), FacetValue::Missing(reason.clone()));
}
return facts;
}
};
let sections = crate::monitor::collector::split_sections(&raw);
for (i, spec) in order {
let out = sections.get(&i.to_string()).cloned().unwrap_or_default();
let value = interpret_sentinel(&out)
.or_else(|| (spec.parse)(&out))
.unwrap_or_else(|| {
FacetValue::Missing(if out.trim().is_empty() {
"command produced no output".into()
} else {
"unreadable output".into()
})
});
facts.facets.insert(spec.key.clone(), value);
}
facts
}
pub async fn probe_uname<H: russh::client::Handler>(handle: &Handle<H>) -> Option<String> {
exec(handle, "uname -s").await.ok()
}
async fn exec<H: russh::client::Handler>(
handle: &Handle<H>,
command: &str,
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
let mut channel = handle.channel_open_session().await?;
channel.exec(true, command.as_bytes()).await?;
let mut out = Vec::new();
while let Some(msg) = channel.wait().await {
match msg {
russh::ChannelMsg::Data { data } => out.extend_from_slice(&data),
russh::ChannelMsg::ExtendedData { data, .. } => out.extend_from_slice(&data),
russh::ChannelMsg::Eof | russh::ChannelMsg::Close => break,
russh::ChannelMsg::ExitStatus { .. } => break,
_ => {}
}
}
Ok(String::from_utf8_lossy(&out).to_string())
}
pub fn collectable_count(
platform: &Platform,
config_paths: &[String],
packages: &[String],
) -> (usize, usize) {
let specs = facet_specs(config_paths, packages);
let total = specs.len();
let usable = specs.iter().filter(|s| runs_on(s, platform)).count();
(usable, total)
}
pub fn privileged_count(
platform: &Platform,
config_paths: &[String],
packages: &[String],
) -> usize {
facet_specs(config_paths, packages)
.iter()
.filter(|s| runs_on(s, platform) && s.privilege == Privilege::Root)
.count()
}
fn runs_on(spec: &FacetSpec, platform: &Platform) -> bool {
match &spec.key {
FacetKey::FileHash(_) => matches!(platform, Platform::Linux | Platform::MacOS),
FacetKey::PkgVersion(p) => command_for_package(p, platform).is_some(),
_ => spec.command(platform).is_some(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_facet_declares_a_privilege_and_at_least_one_platform() {
let specs = facet_specs(&[], &[]);
assert!(specs.len() >= 14, "expected the base facet set");
for s in &specs {
let has_platform = s.linux.is_some() || s.macos.is_some();
assert!(has_platform, "{:?} runs nowhere", s.key);
let _ = s.privilege;
}
}
#[test]
fn systemd_is_unsupported_on_macos_rather_than_divergent() {
let specs = facet_specs(&[], &[]);
let units = specs
.iter()
.find(|s| s.key == FacetKey::SystemdUnits)
.unwrap();
assert!(units.command(&Platform::Linux).is_some());
assert!(
units.command(&Platform::MacOS).is_none(),
"a Mac without systemd is not a Mac that disagrees about systemd"
);
}
#[test]
fn privileged_facets_are_counted_separately() {
let paths = vec!["/etc/nginx/nginx.conf".to_string()];
let priv_n = privileged_count(&Platform::Linux, &paths, &[]);
assert_eq!(priv_n, 1, "the config hash needs privileges");
assert_eq!(privileged_count(&Platform::Linux, &[], &[]), 0);
assert_eq!(
privileged_count(&Platform::Other("FreeBSD".into()), &paths, &[]),
0
);
}
#[test]
fn collectable_count_does_not_overclaim() {
let (mac_usable, mac_total) = collectable_count(&Platform::MacOS, &[], &[]);
let (linux_usable, _) = collectable_count(&Platform::Linux, &[], &[]);
assert!(
mac_usable < mac_total,
"macOS cannot collect every facet and must say so"
);
assert!(linux_usable > mac_usable);
let (other, _) = collectable_count(&Platform::Other("FreeBSD".into()), &[], &[]);
assert_eq!(other, 0);
}
#[test]
fn unreadable_config_files_report_why_rather_than_vanishing() {
assert_eq!(
interpret_sentinel("ESSH_DENIED"),
Some(FacetValue::Missing("permission denied".into()))
);
assert_eq!(
interpret_sentinel("ESSH_ABSENT"),
Some(FacetValue::Missing("not installed".into()))
);
assert_eq!(interpret_sentinel("abc123"), None);
}
#[test]
fn file_hash_command_distinguishes_absent_from_denied() {
let cmd = command_for_file_hash("/etc/nginx/nginx.conf");
assert!(cmd.contains("ESSH_ABSENT"));
assert!(cmd.contains("ESSH_DENIED"));
assert!(cmd.contains("sha256sum") && cmd.contains("shasum"));
}
#[test]
fn listening_ports_are_sorted_so_ordering_is_not_drift() {
let a = "LISTEN 0 128 0.0.0.0:443 0.0.0.0:*\nLISTEN 0 128 0.0.0.0:80 0.0.0.0:*\n";
let b = "LISTEN 0 128 0.0.0.0:80 0.0.0.0:*\nLISTEN 0 128 0.0.0.0:443 0.0.0.0:*\n";
assert_eq!(listening_ports(a), listening_ports(b));
assert_eq!(listening_ports(a), Some(FacetValue::Text("80,443".into())));
}
#[test]
fn listening_ports_parse_on_macos_where_the_separator_is_a_dot() {
let raw = "tcp6 0 0 *.49419 *.* LISTEN\n\
tcp4 0 0 *.49419 *.* LISTEN\n\
tcp4 0 0 127.0.0.1.11434 *.* LISTEN\n";
assert_eq!(
listening_ports(raw),
Some(FacetValue::Text("11434,49419".into()))
);
}
#[test]
fn host_key_algos_skip_the_keyscan_comment_lines() {
let raw = "# localhost:22 SSH-2.0-OpenSSH_9.9\n\
localhost ssh-ed25519 AAAAC3Nza...\n\
# localhost:22 SSH-2.0-OpenSSH_9.9\n\
localhost ssh-rsa AAAAB3Nza...\n";
let v = host_key_algos(raw).expect("algos parse");
assert_eq!(v, FacetValue::Text("ssh-ed25519,ssh-rsa".into()));
match v {
FacetValue::Text(s) => {
assert!(!s.contains("localhost"), "host:port leaked in: {}", s);
assert!(!s.ends_with(','), "trailing separator: {}", s);
}
_ => unreachable!(),
}
assert!(host_key_algos("# only a comment\n").is_none());
}
#[test]
fn systemd_units_are_sorted_and_deduplicated() {
let a = "nginx.service enabled\nssh.service enabled\n";
let b = "ssh.service enabled\nnginx.service enabled\nnginx.service enabled\n";
assert_eq!(enabled_units(a), enabled_units(b));
}
#[test]
fn macos_uptime_survives_the_greedy_regex_trap() {
let raw = "{ sec = 1786755015, usec = 938379 } Sat Aug 15 10:50:15 2026\n1786766473\n";
assert_eq!(uptime_days(raw), Some(FacetValue::Number(0.0)));
let raw = "{ sec = 1785038473, usec = 1 } Sat Aug 15 10:50:15 2026\n1786766473\n";
assert_eq!(uptime_days(raw), Some(FacetValue::Number(20.0)));
}
#[test]
fn uptime_parser_handles_both_platform_shapes() {
assert_eq!(
uptime_days("864000.12 1200.5"),
Some(FacetValue::Number(10.0))
);
assert_eq!(
uptime_days("{ sec = 1786000000, usec = 5 }\n1786864000\n"),
Some(FacetValue::Number(10.0))
);
}
#[test]
fn uptime_compares_in_days_not_seconds() {
let a = uptime_days("864000.12 1200.5").unwrap();
let b = uptime_days("867600.44 1300.1").unwrap();
assert_eq!(a, b);
assert_eq!(a, FacetValue::Number(10.0));
}
#[test]
fn root_disk_pct_reads_the_capacity_column() {
let raw = "Filesystem 1024-blocks Used Available Capacity Mounted on\n\
/dev/disk3s1s1 971350180 17380616 562007844 3% /\n";
assert_eq!(root_disk_pct(raw), Some(FacetValue::Number(3.0)));
assert_eq!(root_disk_pct(""), None);
}
#[test]
fn section_markers_survive_commands_that_omit_a_trailing_newline() {
let script_uses_printf = {
let specs = facet_specs(&[], &[]);
let mut s = String::new();
for (i, spec) in specs.iter().enumerate() {
if spec.command(&Platform::Linux).is_some() {
s.push_str(&format!("printf '\\n==={}===\\n'; ", i));
}
}
s
};
assert!(
script_uses_printf.contains("printf '\\n===0===\\n'"),
"markers must be newline-delimited on both sides"
);
let raw = "\n===0===\n6.1.0-18\n\n===1===\n0.07\n\n===2===\n4%\n\n===END===\n";
let s = crate::monitor::collector::split_sections(raw);
assert_eq!(s.get("0").map(|v| v.trim()), Some("6.1.0-18"));
assert_eq!(s.get("1").map(|v| v.trim()), Some("0.07"));
assert_eq!(s.get("2").map(|v| v.trim()), Some("4%"));
}
#[test]
fn a_marker_glued_to_previous_output_is_the_failure_being_prevented() {
let glued = "===0===\nssh-rsa,===1===\n4%\n===END===\n";
let s = crate::monitor::collector::split_sections(glued);
assert!(
!s.contains_key("1"),
"a glued marker cannot be recovered — which is why we emit \\n first"
);
}
#[test]
fn parsers_return_none_rather_than_a_confident_default() {
assert_eq!(first_line(""), None);
assert_eq!(number(""), None);
assert_eq!(number("not a number"), None);
assert_eq!(listening_ports(""), None);
assert_eq!(enabled_units(""), None);
assert_eq!(uptime_days(""), None);
}
}