use std::fs;
use std::os::unix::fs::MetadataExt;
use std::path::{Path, PathBuf};
#[derive(Debug, Default, Clone)]
pub struct CompauditError {
pub insecure_dirs: Vec<PathBuf>,
pub insecure_files: Vec<PathBuf>,
}
impl CompauditError {
pub fn discriminator(&self) -> &'static str {
match (self.insecure_dirs.is_empty(), self.insecure_files.is_empty()) {
(true, true) => "",
(true, false) => "files",
(false, true) => "directories",
(false, false) => "directories and files",
}
}
pub fn is_empty(&self) -> bool {
self.insecure_dirs.is_empty() && self.insecure_files.is_empty()
}
}
pub fn compaudit(dirs: &[PathBuf]) -> Result<(), CompauditError> {
let fpath: Vec<PathBuf> = if !dirs.is_empty() {
dirs.to_vec()
} else {
let arr = crate::ported::params::getaparam("fpath").unwrap_or_default();
if arr.is_empty() {
eprintln!("compaudit: No directories in $fpath, cannot continue");
return Err(CompauditError {
insecure_dirs: Vec::new(),
insecure_files: Vec::new(),
});
}
arr.into_iter().map(PathBuf::from).collect()
};
let fpath: Vec<PathBuf> = fpath
.into_iter()
.filter(|d| d.as_os_str() != ".")
.collect();
let trusted_owners = trusted_owners();
let on_debian = Path::new("/etc/debian_version").exists();
let staff_gid = if on_debian { lookup_group_gid("staff") } else { None };
let user_private_gid = detect_user_private_group();
let mut audit = CompauditError::default();
for dir in &fpath {
check_directory(dir, &trusted_owners, on_debian, staff_gid, user_private_gid, &mut audit);
if let Some(parent) = dir.parent() {
if !fpath.iter().any(|p| p == parent) {
check_directory(
parent,
&trusted_owners,
on_debian,
staff_gid,
user_private_gid,
&mut audit,
);
}
}
}
for dir in &fpath {
let mut zwc = dir.as_os_str().to_os_string();
zwc.push(".zwc");
let zwc_path = PathBuf::from(zwc);
if let Ok(meta) = fs::metadata(&zwc_path) {
let name = zwc_path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default();
if name.starts_with('_') && !name.ends_with('~') {
if !is_owned_by(&meta, &trusted_owners) {
audit.insecure_files.push(zwc_path);
}
}
}
}
for dir in &fpath {
if let Ok(entries) = fs::read_dir(dir) {
for ent in entries.flatten() {
let name = ent.file_name().to_string_lossy().into_owned();
if !name.starts_with('_') || name.ends_with('~') {
continue;
}
let path = ent.path();
let meta = match ent.metadata() {
Ok(m) => m,
Err(_) => continue,
};
if !is_owned_by(&meta, &trusted_owners) {
audit.insecure_files.push(path);
}
}
}
}
if audit.is_empty() {
Ok(())
} else {
Err(audit)
}
}
fn check_directory(
dir: &Path,
trusted: &[u32],
on_debian: bool,
staff_gid: Option<u32>,
user_private_gid: Option<u32>,
audit: &mut CompauditError,
) {
let meta = match fs::metadata(dir) {
Ok(m) => m,
Err(_) => return,
};
if on_debian {
if let Some(staff) = staff_gid {
if dir.starts_with("/usr/local/")
&& meta.gid() == staff
&& meta.uid() == 0
{
return;
}
}
}
let group_write = (meta.mode() & 0o020) != 0;
let world_write = (meta.mode() & 0o002) != 0;
let owner_trusted = is_owned_by(&meta, trusted);
let group_write_bad = if group_write {
match user_private_gid {
Some(g) if meta.gid() == g => false,
_ => true,
}
} else {
false
};
if !owner_trusted || group_write_bad || world_write {
audit.insecure_dirs.push(dir.to_path_buf());
}
}
fn trusted_owners() -> Vec<u32> {
let mut out: Vec<u32> = vec![0]; let euid = unsafe { libc::geteuid() };
if euid != 0 {
out.push(euid);
}
let pid = std::process::id();
let exes = [
format!("/proc/{}/exe", pid),
format!("/proc/{}/object/a.out", pid),
];
for path in &exes {
if let Ok(meta) = fs::metadata(path) {
let uid = meta.uid();
if uid != 0 && !out.contains(&uid) {
out.push(uid);
}
break;
}
}
out
}
fn detect_user_private_group() -> Option<u32> {
let egid = unsafe { libc::getegid() };
let euid = unsafe { libc::geteuid() };
let user_name = match std::env::var("LOGNAME").or_else(|_| std::env::var("USER")) {
Ok(n) => n,
Err(_) => return None,
};
let content = fs::read_to_string("/etc/group").ok()?;
for line in content.lines() {
let cols: Vec<&str> = line.split(':').collect();
if cols.len() < 4 {
continue;
}
let gname = cols[0];
let gid: u32 = cols[2].parse().ok()?;
if gid != egid {
continue;
}
let members = cols[3];
let members_ok = members.is_empty()
|| members.split(',').all(|m| m.trim() == user_name);
if gname == user_name && members_ok && euid == unsafe { libc::getuid() } {
return Some(gid);
}
return None;
}
None
}
fn lookup_group_gid(name: &str) -> Option<u32> {
let content = fs::read_to_string("/etc/group").ok()?;
for line in content.lines() {
let cols: Vec<&str> = line.split(':').collect();
if cols.len() < 3 {
continue;
}
if cols[0] == name {
return cols[2].parse().ok();
}
}
None
}
fn is_owned_by(meta: &fs::Metadata, trusted: &[u32]) -> bool {
trusted.contains(&meta.uid())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_input_with_empty_fpath_returns_err() {
let _g = crate::test_util::global_state_lock();
crate::ported::params::setaparam("fpath", Vec::new());
let r = compaudit(&[]);
assert!(r.is_err());
}
#[test]
fn nonexistent_dirs_pass_silently() {
let r = compaudit(&[PathBuf::from("/definitely/not/here/xyz")]);
assert!(r.is_ok());
}
#[test]
fn temp_dir_owned_by_root_is_secure_when_invoker_is_root_or_world_not_writable() {
let euid = unsafe { libc::geteuid() };
let r = compaudit(&[PathBuf::from("/tmp")]);
if euid == 0 {
assert!(r.is_ok() || r.is_err());
} else {
assert!(
r.is_err(),
"expected /tmp to be flagged for non-root invoker; got {:?}",
r
);
}
}
#[test]
fn discriminator_categorization() {
let e = CompauditError {
insecure_dirs: Vec::new(),
insecure_files: Vec::new(),
};
assert_eq!(e.discriminator(), "");
let e = CompauditError {
insecure_dirs: Vec::new(),
insecure_files: vec![PathBuf::from("/x")],
};
assert_eq!(e.discriminator(), "files");
let e = CompauditError {
insecure_dirs: vec![PathBuf::from("/x")],
insecure_files: Vec::new(),
};
assert_eq!(e.discriminator(), "directories");
let e = CompauditError {
insecure_dirs: vec![PathBuf::from("/x")],
insecure_files: vec![PathBuf::from("/y")],
};
assert_eq!(e.discriminator(), "directories and files");
}
#[test]
fn owner_check_trusts_root_and_euid() {
let owners = trusted_owners();
assert!(owners.contains(&0));
let euid = unsafe { libc::geteuid() };
if euid != 0 {
assert!(owners.contains(&euid));
}
}
#[test]
fn is_owned_by_root_meta() {
if let Ok(meta) = fs::metadata("/etc/hosts") {
assert!(is_owned_by(&meta, &[0]));
assert!(!is_owned_by(&meta, &[12345]));
}
}
#[test]
fn home_dir_is_secure_for_owner() {
if let Ok(home) = std::env::var("HOME") {
let r = compaudit(&[PathBuf::from(&home)]);
let _ = r;
}
}
}