Some checks failed
CI/CD Pipeline / Code Format (push) Successful in 4s
CI/CD Pipeline / Clippy Lints (push) Failing after 44s
CI/CD Pipeline / Enrollment Tests (push) Has been skipped
CI/CD Pipeline / Verify Enrollment CLI Flag (push) Has been skipped
CI/CD Pipeline / All Unit Tests (push) Successful in 1m15s
CI/CD Pipeline / Build Debian Package (push) Has been skipped
CI/CD Pipeline / Build Debian Package (Ubuntu 22.04) (push) Has been skipped
CI/CD Pipeline / Build RPM Package (push) Has been skipped
CI/CD Pipeline / Build Alpine Package (push) Has been skipped
CI/CD Pipeline / Build Arch Package (push) Has been skipped
CI/CD Pipeline / Security Audit (push) Successful in 4s
Format all enrollment module source files and tests per rustfmt standards. Resolves Gitea CI workflow cargo fmt check failures.
182 lines
6.0 KiB
Rust
182 lines
6.0 KiB
Rust
//! Cross-distribution identity extraction for Linux systems.
|
|
//!
|
|
//! Provides machine-id, FQDN, IP address, and OS-detail collection
|
|
//! compatible with Debian/Ubuntu, RHEL/CentOS/Fedora, Alpine, and Arch Linux.
|
|
|
|
use anyhow::{anyhow, Context, Result};
|
|
use std::fs;
|
|
use std::net::IpAddr;
|
|
use std::process::Command;
|
|
|
|
/// Read the D-Bus machine identifier from `/etc/machine-id`.
|
|
/// Falls back to `/var/lib/dbus/machine-id` on older systems.
|
|
pub fn get_machine_id() -> Result<String> {
|
|
let primary = "/etc/machine-id";
|
|
let fallback = "/var/lib/dbus/machine-id";
|
|
|
|
if let Ok(id) = fs::read_to_string(primary) {
|
|
let trimmed = id.trim().to_string();
|
|
if !trimmed.is_empty() {
|
|
return Ok(trimmed);
|
|
}
|
|
}
|
|
|
|
let id = fs::read_to_string(fallback)
|
|
.with_context(|| format!("Failed to read machine-id from {} or {}", primary, fallback))?;
|
|
let trimmed = id.trim().to_string();
|
|
if trimmed.is_empty() {
|
|
return Err(anyhow!("machine-id file is empty"));
|
|
}
|
|
Ok(trimmed)
|
|
}
|
|
|
|
/// Resolve the fully-qualified domain name.
|
|
/// Strategy: `gethostname` via std → fallback to `hostname` CLI → "localhost".
|
|
pub fn get_fqdn() -> Result<String> {
|
|
// Try reading from hostname file first (common on systemd systems)
|
|
if let Ok(name) = fs::read_to_string("/etc/hostname") {
|
|
let trimmed = name.trim().to_string();
|
|
if !trimmed.is_empty() && trimmed != "(none)" {
|
|
return Ok(trimmed);
|
|
}
|
|
}
|
|
|
|
// Fallback to hostname command
|
|
if let Ok(output) = Command::new("hostname").arg("-f").output() {
|
|
if output.status.success() {
|
|
let name = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
|
if !name.is_empty() {
|
|
return Ok(name);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback to plain hostname
|
|
if let Ok(output) = Command::new("hostname").output() {
|
|
if output.status.success() {
|
|
let name = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
|
if !name.is_empty() {
|
|
return Ok(name);
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok("localhost".into())
|
|
}
|
|
|
|
/// Collect all non-loopback IPv4 addresses from network interfaces.
|
|
pub fn get_ip_addresses() -> Result<Vec<String>> {
|
|
let ifaces = if_addrs::get_if_addrs().context("Failed to enumerate network interfaces")?;
|
|
|
|
let mut addrs: Vec<String> = ifaces
|
|
.iter()
|
|
.filter_map(|iface| {
|
|
if iface.is_loopback() {
|
|
return None;
|
|
}
|
|
match &iface.ip() {
|
|
IpAddr::V4(addr) => Some(addr.to_string()),
|
|
IpAddr::V6(_) => None,
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
addrs.sort();
|
|
addrs.dedup();
|
|
Ok(addrs)
|
|
}
|
|
|
|
/// Extract OS distribution details from `/etc/os-release` and kernel version.
|
|
/// Returns a JSON object with: distro, version, id_like, kernel.
|
|
pub fn get_os_details() -> Result<serde_json::Value> {
|
|
let mut details = serde_json::Map::new();
|
|
|
|
// Parse /etc/os-release (exists on all target distros)
|
|
if let Ok(content) = fs::read_to_string("/etc/os-release") {
|
|
for line in content.lines() {
|
|
let line = line.trim();
|
|
if line.is_empty() || line.starts_with('#') {
|
|
continue;
|
|
}
|
|
|
|
if let Some((key, value)) = line.split_once('=') {
|
|
// Strip surrounding quotes from value
|
|
let unquoted = value.trim().trim_matches('"').trim_matches('\'');
|
|
match key {
|
|
"NAME" => {
|
|
details.insert(
|
|
"distro".into(),
|
|
serde_json::Value::String(unquoted.to_string()),
|
|
);
|
|
}
|
|
"VERSION_ID" => {
|
|
details.insert(
|
|
"version".into(),
|
|
serde_json::Value::String(unquoted.to_string()),
|
|
);
|
|
}
|
|
"ID_LIKE" => {
|
|
details.insert(
|
|
"id_like".into(),
|
|
serde_json::Value::String(unquoted.to_string()),
|
|
);
|
|
}
|
|
"VERSION_CODENAME" => {
|
|
details.insert(
|
|
"codename".into(),
|
|
serde_json::Value::String(unquoted.to_string()),
|
|
);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Fallback for systems without os-release (very rare)
|
|
details.insert("distro".into(), serde_json::Value::String("unknown".into()));
|
|
details.insert(
|
|
"version".into(),
|
|
serde_json::Value::String("unknown".into()),
|
|
);
|
|
}
|
|
|
|
// Kernel version via uname -r
|
|
if let Ok(output) = Command::new("uname").arg("-r").output() {
|
|
if output.status.success() {
|
|
let kernel = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
|
details.insert("kernel".into(), serde_json::Value::String(kernel));
|
|
}
|
|
} else {
|
|
details.insert("kernel".into(), serde_json::Value::String("unknown".into()));
|
|
}
|
|
|
|
Ok(serde_json::Value::Object(details))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn machine_id_is_not_empty() {
|
|
let id = get_machine_id().expect("Failed to get machine-id");
|
|
assert!(!id.is_empty(), "machine-id should not be empty");
|
|
assert_eq!(id.len(), 32, "machine-id should be 32 hex chars");
|
|
}
|
|
|
|
#[test]
|
|
fn fqdn_is_not_empty() {
|
|
let fqdn = get_fqdn().expect("Failed to get FQDN");
|
|
assert!(!fqdn.is_empty(), "FQDN should not be empty");
|
|
}
|
|
|
|
#[test]
|
|
fn os_details_contains_kernel() {
|
|
let details = get_os_details().expect("Failed to get OS details");
|
|
assert!(
|
|
details.get("kernel").is_some(),
|
|
"OS details must contain kernel version"
|
|
);
|
|
}
|
|
}
|