use std::path::{Path, PathBuf};
pub mod action_family;
pub mod config;
pub mod distro;
pub use distro::{detect, detect_distro, parse_os_release, DistroFamily, DistroId};
pub const PRODUCTION_LISTEN_URI: &str = "unix:///run/sysknife/daemon.sock";
pub const PRODUCTION_DATABASE_PATH: &str = "/var/lib/sysknife/daemon.sqlite";
pub fn default_listen_uri() -> String {
if let Ok(uri) = std::env::var("SYSKNIFE_LISTEN_URI") {
return uri;
}
if let Ok(runtime) = std::env::var("XDG_RUNTIME_DIR") {
let p = PathBuf::from(runtime).join("sysknife/daemon.sock");
return format!("unix://{}", p.display());
}
let uid = current_uid();
format!("unix:///tmp/sysknife-{uid}.sock")
}
pub fn default_database_path() -> PathBuf {
if let Ok(path) = std::env::var("SYSKNIFE_DATABASE_PATH") {
return PathBuf::from(path);
}
if let Ok(state) = std::env::var("XDG_STATE_HOME") {
return PathBuf::from(state).join("sysknife/daemon.sqlite");
}
if let Ok(home) = std::env::var("HOME") {
return PathBuf::from(home).join(".local/state/sysknife/daemon.sqlite");
}
PathBuf::from(PRODUCTION_DATABASE_PATH)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AuditStoreChoice {
Explicit(PathBuf),
PerUser(PathBuf),
System(PathBuf),
}
impl AuditStoreChoice {
pub fn path(&self) -> &Path {
match self {
Self::Explicit(p) | Self::PerUser(p) | Self::System(p) => p,
}
}
pub fn note(&self) -> Option<String> {
match self {
Self::System(path) => Some(format!(
"no per-user audit store found; reading the system daemon's store at {}",
path.display()
)),
Self::Explicit(_) | Self::PerUser(_) => None,
}
}
}
pub fn choose_audit_store(
explicit: Option<PathBuf>,
per_user: PathBuf,
per_user_exists: bool,
system_exists: bool,
) -> AuditStoreChoice {
if let Some(path) = explicit {
return AuditStoreChoice::Explicit(path);
}
if per_user_exists {
return AuditStoreChoice::PerUser(per_user);
}
if system_exists {
return AuditStoreChoice::System(PathBuf::from(PRODUCTION_DATABASE_PATH));
}
AuditStoreChoice::PerUser(per_user)
}
pub fn resolve_audit_store() -> AuditStoreChoice {
let explicit = std::env::var_os("SYSKNIFE_DATABASE_PATH").map(PathBuf::from);
let per_user = default_database_path();
let system = PathBuf::from(PRODUCTION_DATABASE_PATH);
let per_user_exists = per_user.exists();
let system_exists = system.exists();
choose_audit_store(explicit, per_user, per_user_exists, system_exists)
}
fn current_uid() -> u32 {
std::fs::read_to_string("/proc/self/status")
.ok()
.and_then(|s| {
s.lines()
.find(|l| l.starts_with("Uid:"))
.and_then(|l| l.split_whitespace().nth(1))
.and_then(|v| v.parse::<u32>().ok())
})
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::{
default_database_path, default_listen_uri, PRODUCTION_DATABASE_PATH, PRODUCTION_LISTEN_URI,
};
use std::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::new(());
#[test]
fn production_constants_are_absolute() {
assert!(PRODUCTION_LISTEN_URI.starts_with("unix:///run/"));
assert!(PRODUCTION_DATABASE_PATH.starts_with("/var/lib/"));
}
#[test]
fn database_env_var_wins() {
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
unsafe {
std::env::set_var("SYSKNIFE_DATABASE_PATH", "/explicit/path/db.sqlite");
}
let p = default_database_path();
unsafe {
std::env::remove_var("SYSKNIFE_DATABASE_PATH");
}
assert_eq!(p.to_str(), Some("/explicit/path/db.sqlite"));
}
#[test]
fn database_xdg_state_used_when_set() {
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
unsafe {
std::env::remove_var("SYSKNIFE_DATABASE_PATH");
std::env::set_var("XDG_STATE_HOME", "/xdg/state");
}
let p = default_database_path();
unsafe {
std::env::remove_var("XDG_STATE_HOME");
}
assert_eq!(p.to_str(), Some("/xdg/state/sysknife/daemon.sqlite"));
}
#[test]
fn database_falls_back_to_home_local_state() {
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
unsafe {
std::env::remove_var("SYSKNIFE_DATABASE_PATH");
std::env::remove_var("XDG_STATE_HOME");
std::env::set_var("HOME", "/home/contributor");
}
let p = default_database_path();
unsafe {
std::env::remove_var("HOME");
}
assert_eq!(
p.to_str(),
Some("/home/contributor/.local/state/sysknife/daemon.sqlite")
);
}
#[test]
fn database_last_resort_is_production_path() {
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let saved_home = std::env::var("HOME").ok();
unsafe {
std::env::remove_var("SYSKNIFE_DATABASE_PATH");
std::env::remove_var("XDG_STATE_HOME");
std::env::remove_var("HOME");
}
let p = default_database_path();
unsafe {
if let Some(h) = saved_home {
std::env::set_var("HOME", h);
}
}
assert_eq!(p.to_str(), Some(PRODUCTION_DATABASE_PATH));
}
#[test]
fn listen_env_var_wins() {
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
unsafe {
std::env::set_var("SYSKNIFE_LISTEN_URI", "unix:///explicit.sock");
}
let u = default_listen_uri();
unsafe {
std::env::remove_var("SYSKNIFE_LISTEN_URI");
}
assert_eq!(u, "unix:///explicit.sock");
}
#[test]
fn listen_uses_xdg_runtime_dir_when_set() {
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
unsafe {
std::env::remove_var("SYSKNIFE_LISTEN_URI");
std::env::set_var("XDG_RUNTIME_DIR", "/run/user/1000");
}
let u = default_listen_uri();
unsafe {
std::env::remove_var("XDG_RUNTIME_DIR");
}
assert_eq!(u, "unix:///run/user/1000/sysknife/daemon.sock");
}
#[test]
fn listen_last_resort_is_per_uid_tmp() {
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
unsafe {
std::env::remove_var("SYSKNIFE_LISTEN_URI");
std::env::remove_var("XDG_RUNTIME_DIR");
}
let u = default_listen_uri();
assert!(u.starts_with("unix:///tmp/sysknife-"));
assert!(u.ends_with(".sock"));
}
use std::path::{Path, PathBuf};
#[test]
fn an_explicit_database_path_is_never_second_guessed() {
let choice = super::choose_audit_store(
Some(PathBuf::from("/srv/audit.sqlite")),
PathBuf::from("/home/u/.local/state/sysknife/daemon.sqlite"),
true,
true,
);
assert_eq!(choice.path(), Path::new("/srv/audit.sqlite"));
assert_eq!(
choice.note(),
None,
"an explicit choice needs no explanation"
);
}
#[test]
fn a_present_per_user_store_wins_over_the_system_one() {
let per_user = PathBuf::from("/home/u/.local/state/sysknife/daemon.sqlite");
let choice = super::choose_audit_store(None, per_user.clone(), true, true);
assert_eq!(choice.path(), per_user.as_path());
}
#[test]
fn the_system_store_is_used_when_no_per_user_store_exists() {
let choice = super::choose_audit_store(
None,
PathBuf::from("/home/u/.local/state/sysknife/daemon.sqlite"),
false,
true,
);
assert_eq!(choice.path(), Path::new(super::PRODUCTION_DATABASE_PATH));
let note = choice
.note()
.expect("reading another store must be announced");
assert!(
note.contains(super::PRODUCTION_DATABASE_PATH),
"note names the store: {note}"
);
}
#[test]
fn with_no_store_anywhere_the_per_user_path_is_reported() {
let per_user = PathBuf::from("/home/u/.local/state/sysknife/daemon.sqlite");
let choice = super::choose_audit_store(None, per_user.clone(), false, false);
assert_eq!(choice.path(), per_user.as_path());
assert_eq!(choice.note(), None);
}
#[test]
fn the_system_store_constant_matches_the_packaged_unit() {
let unit = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../packaging/sysknife-daemon.service");
let contents = std::fs::read_to_string(&unit)
.unwrap_or_else(|e| panic!("read {}: {e}", unit.display()));
let expected = format!("SYSKNIFE_DATABASE_PATH={}", super::PRODUCTION_DATABASE_PATH);
assert!(
contents.contains(&expected),
"packaged unit must set {expected}; got:\n{contents}"
);
}
}