diff --git a/Cargo.lock b/Cargo.lock index 0d6a9c2e..4242015b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -316,6 +316,7 @@ dependencies = [ "tempfile", "uucore", "walkdir", + "xattr", ] [[package]] @@ -932,9 +933,9 @@ checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" [[package]] name = "syn" -version = "2.0.18" +version = "2.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" dependencies = [ "proc-macro2", "quote", @@ -1246,6 +1247,16 @@ dependencies = [ "bitflags 2.4.1", ] +[[package]] +name = "xattr" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d65cbf2f12c15564212d48f4e3dfb87923d25d611f2aed18f4cb23f0413d89e" +dependencies = [ + "libc", + "rustix 1.0.0", +] + [[package]] name = "yansi" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index e4c7056d..1f591c2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,9 @@ uucore = { version = "0.0.30", features = ["entries", "fs", "fsext", "mode"] } nix = { version = "0.30", features = ["fs", "user"] } argmax = "0.3.1" +[target.'cfg(target_os = "linux")'.dependencies] +xattr = "1.5.0" + [dev-dependencies] assert_cmd = "2" filetime = "0.2" diff --git a/src/find/matchers/context.rs b/src/find/matchers/context.rs new file mode 100644 index 00000000..6a0d84c5 --- /dev/null +++ b/src/find/matchers/context.rs @@ -0,0 +1,182 @@ +//! SELinux context matcher +//! +//! This matcher will match files based on their +//! SELinux context, only available on Linux. + +#[cfg(target_os = "linux")] +use nix::{libc::SELINUX_MAGIC, sys::statvfs::FsFlags}; + +use std::error::Error; +#[cfg(target_os = "linux")] +use std::{ + fs::File, + io::{stderr, BufRead, BufReader, Read, Write}, +}; + +#[cfg(target_os = "linux")] +use super::glob::Pattern; +use super::{Matcher, MatcherIO, WalkEntry}; + +#[cfg(target_os = "linux")] +const XATTR_NAME_SELINUX: &str = "security.selinux"; +#[cfg(target_os = "linux")] +const SELINUX_FS: &str = "selinuxfs"; +#[cfg(target_os = "linux")] +const SELINUX_MNT: &str = "/sys/fs/selinux"; +#[cfg(target_os = "linux")] +const OLD_SELINUX_MNT: &str = "/selinux"; + +/// Verify if SELinux mount point exists and is writable. +/// +/// This function will return true if the SELinux mount point +/// exists and is writable, false otherwise. +#[cfg(target_os = "linux")] +fn verify_selinux_mnt(mnt: &str) -> bool { + use nix::sys::statfs::{statfs, FsType}; + use nix::sys::statvfs::statvfs; + + let Ok(stat) = statfs(mnt) else { return false }; + + if stat.filesystem_type() == FsType(SELINUX_MAGIC) { + match statvfs(mnt) { + Ok(stat) => { + if stat.flags().contains(FsFlags::ST_RDONLY) { + return false; + } + return true; + } + Err(_) => return false, + } + } + false +} + +/// Check if SELinux filesystem exists. +/// +/// This function will try to open the `/proc/filesystems` file and +/// check if the SELinux filesystem is listed. +#[cfg(target_os = "linux")] +fn selinuxfs_exists() -> bool { + let Ok(fp) = File::open("/proc/filesystems") else { + return true; // Fail as if it exists + }; + + let reader = BufReader::new(fp); + for line in reader.lines().map_while(Result::ok) { + if line.contains(SELINUX_FS) { + return true; + } + } + false +} + +/// Get SELinux mount point. +#[cfg(target_os = "linux")] +fn get_selinux_mnt() -> Option { + if verify_selinux_mnt(SELINUX_MNT) { + return Some(SELINUX_MNT.to_string()); + } + + if verify_selinux_mnt(OLD_SELINUX_MNT) { + return Some(OLD_SELINUX_MNT.to_string()); + } + + if !selinuxfs_exists() { + return None; + } + + let Ok(fp) = File::open("/proc/mounts") else { + return None; + }; + + let reader = BufReader::new(fp); + for line in reader.lines().map_while(Result::ok) { + let mut parts = line.splitn(3, ' '); + if let (Some(_), Some(mnt), Some(fs_type)) = (parts.next(), parts.next(), parts.next()) { + if fs_type.starts_with(SELINUX_FS) && verify_selinux_mnt(mnt) { + return Some(mnt.to_string()); + } + } + } + None +} + +/// Check if SELinux is enforced. +#[cfg(target_os = "linux")] +fn get_selinux_enforced() -> Result> { + let Some(mnt) = get_selinux_mnt() else { + return Ok(false); + }; + + let path = format!("{mnt}/enforce"); + let Ok(mut fd) = File::open(path) else { + return Ok(false); + }; + + let mut buf = String::with_capacity(20); + if fd.read_to_string(&mut buf).is_err() { + return Ok(false); + } + let enforce = buf.parse::()?; + + Ok(enforce != 0) +} + +/// Matcher for SELinux context. +pub struct ContextMatcher { + #[cfg(target_os = "linux")] + pattern: Pattern, +} + +impl ContextMatcher { + #[cfg(target_os = "linux")] + pub fn new(pattern: &str) -> Result> { + if !get_selinux_enforced()? { + return Err(From::from("SELinux is not enabled")); + } + + let pattern = Pattern::new(pattern, false); + + Ok(Self { pattern }) + } + + #[cfg(not(target_os = "linux"))] + pub fn new(_pattern: &str) -> Result> { + Ok(Self {}) + } +} + +impl Matcher for ContextMatcher { + #[cfg(target_os = "linux")] + fn matches(&self, path: &WalkEntry, _: &mut MatcherIO) -> bool { + let attr = match xattr::get(path.path(), XATTR_NAME_SELINUX) { + Ok(attr) => match attr { + Some(attr) => attr, + None => { + return false; + } + }, + Err(e) => { + writeln!(&mut stderr(), "Failed to get SELinux context: {e}").unwrap(); + return false; + } + }; + let selinux_ctx = match String::from_utf8(attr) { + Ok(selinux_ctx) => selinux_ctx, + Err(e) => { + writeln!( + &mut stderr(), + "Failed to convert SELinux context to UTF-8: {e}" + ) + .unwrap(); + return false; + } + }; + self.pattern.matches(&selinux_ctx) + } + + #[cfg(not(target_os = "linux"))] + fn matches(&self, _: &WalkEntry, _: &mut MatcherIO) -> bool { + false + } +} diff --git a/src/find/matchers/mod.rs b/src/find/matchers/mod.rs index ca2ed15e..ce8d6510 100644 --- a/src/find/matchers/mod.rs +++ b/src/find/matchers/mod.rs @@ -5,6 +5,7 @@ // https://opensource.org/licenses/MIT. mod access; +mod context; mod delete; mod empty; mod entry; @@ -60,6 +61,7 @@ use self::type_matcher::{TypeMatcher, XtypeMatcher}; use self::user::{NoUserMatcher, UserMatcher}; use ::regex::Regex; use chrono::{DateTime, Datelike, NaiveDateTime, Utc}; +use context::ContextMatcher; use fs::FileSystemMatcher; use ls::Ls; use std::{ @@ -774,6 +776,13 @@ fn build_matcher_tree( i += 1; Some(PermMatcher::new(args[i])?.into_box()) } + "-context" => { + if i >= args.len() - 1 { + return Err(From::from(format!("missing argument to {}", args[i]))); + } + i += 1; + Some(ContextMatcher::new(args[i])?.into_box()) + } "-prune" => Some(PruneMatcher::new().into_box()), "-quit" => Some(QuitMatcher.into_box()), "-writable" => Some(AccessMatcher::Writable.into_box()),