diff --git a/Cargo.toml b/Cargo.toml index 05dc4ed7d..0c1d3fb29 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,6 +51,7 @@ feat_selinux = [ "id/selinux", "ls/selinux", "mkdir/selinux", + "stat/selinux", "selinux", "feat_require_selinux", ] diff --git a/src/uu/stat/Cargo.toml b/src/uu/stat/Cargo.toml index 3e59da516..940a3ddbf 100644 --- a/src/uu/stat/Cargo.toml +++ b/src/uu/stat/Cargo.toml @@ -22,6 +22,9 @@ clap = { workspace = true } uucore = { workspace = true, features = ["entries", "libc", "fs", "fsext"] } chrono = { workspace = true } +[features] +selinux = ["uucore/selinux"] + [[bin]] name = "stat" path = "src/main.rs" diff --git a/src/uu/stat/src/stat.rs b/src/uu/stat/src/stat.rs index 0e2de8661..6a4a343e4 100644 --- a/src/uu/stat/src/stat.rs +++ b/src/uu/stat/src/stat.rs @@ -902,7 +902,27 @@ impl Stater { // FIXME: blocksize differs on various platform // See coreutils/gnulib/lib/stat-size.h ST_NBLOCKSIZE // spell-checker:disable-line 'B' => OutputType::Unsigned(512), - + // SELinux security context string + 'C' => { + #[cfg(feature = "selinux")] + { + if uucore::selinux::check_selinux_enabled().is_ok() { + match uucore::selinux::get_selinux_security_context(Path::new(file)) + { + Ok(ctx) => OutputType::Str(ctx), + Err(_) => OutputType::Str( + "failed to get security context".to_string(), + ), + } + } else { + OutputType::Str("unsupported on this system".to_string()) + } + } + #[cfg(not(feature = "selinux"))] + { + OutputType::Str("unsupported for this operating system".to_string()) + } + } // device number in decimal 'd' => OutputType::Unsigned(meta.dev()), // device number in hex diff --git a/src/uucore/src/lib/features/selinux.rs b/src/uucore/src/lib/features/selinux.rs index d708fe715..2f2d2b6a1 100644 --- a/src/uucore/src/lib/features/selinux.rs +++ b/src/uucore/src/lib/features/selinux.rs @@ -10,6 +10,20 @@ use selinux::SecurityContext; #[derive(Debug)] pub enum Error { SELinuxNotEnabled, + FileOpenFailure, + ContextRetrievalFailure, + ContextConversionFailure, +} + +impl From for i32 { + fn from(error: Error) -> i32 { + match error { + Error::SELinuxNotEnabled => 1, + Error::FileOpenFailure => 2, + Error::ContextRetrievalFailure => 3, + Error::ContextConversionFailure => 4, + } + } } /// Checks if SELinux is enabled on the system. @@ -109,6 +123,73 @@ pub fn set_selinux_security_context(path: &Path, context: Option<&String>) -> Re } } +/// Gets the SELinux security context for the given filesystem path. +/// +/// Retrieves the security context of the specified filesystem path if SELinux is enabled +/// on the system. +/// +/// # Arguments +/// +/// * `path` - Filesystem path for which to retrieve the SELinux context. +/// +/// # Returns +/// +/// * `Ok(String)` - The SELinux context string if successfully retrieved. Returns an empty +/// string if no context was found. +/// * `Err(Error)` - An error variant indicating the type of failure: +/// - `Error::SELinuxNotEnabled` - SELinux is not enabled on the system. +/// - `Error::FileOpenFailure` - Failed to open the specified file. +/// - `Error::ContextRetrievalFailure` - Failed to retrieve the security context. +/// - `Error::ContextConversionFailure` - Failed to convert the security context to a string. +/// +/// # Examples +/// +/// ``` +/// use std::path::Path; +/// use uucore::selinux::{get_selinux_security_context, Error}; +/// +/// // Get the SELinux context for a file +/// match get_selinux_security_context(Path::new("/path/to/file")) { +/// Ok(context) => { +/// if context.is_empty() { +/// println!("No SELinux context found for the file"); +/// } else { +/// println!("SELinux context: {}", context); +/// } +/// }, +/// Err(Error::SELinuxNotEnabled) => println!("SELinux is not enabled on this system"), +/// Err(Error::FileOpenFailure) => println!("Failed to open the file"), +/// Err(Error::ContextRetrievalFailure) => println!("Failed to retrieve the security context"), +/// Err(Error::ContextConversionFailure) => println!("Failed to convert the security context to a string"), +/// } +/// ``` + +pub fn get_selinux_security_context(path: &Path) -> Result { + if selinux::kernel_support() == selinux::KernelSupport::Unsupported { + return Err(Error::SELinuxNotEnabled); + } + + let f = std::fs::File::open(path).map_err(|_| Error::FileOpenFailure)?; + + // Get the security context of the file + let context = match SecurityContext::of_file(&f, false) { + Ok(Some(ctx)) => ctx, + Ok(None) => return Ok(String::new()), // No context found, return empty string + Err(_) => return Err(Error::ContextRetrievalFailure), + }; + + let context_c_string = context + .to_c_string() + .map_err(|_| Error::ContextConversionFailure)?; + + if let Some(c_str) = context_c_string { + // Convert the C string to a Rust String + Ok(c_str.to_string_lossy().to_string()) + } else { + Ok(String::new()) + } +} + #[cfg(test)] mod tests { use super::*; @@ -171,4 +252,43 @@ mod tests { } } } + + #[test] + fn test_get_selinux_security_context() { + let tmpfile = NamedTempFile::new().expect("Failed to create tempfile"); + let path = tmpfile.path(); + + std::fs::write(path, b"test content").expect("Failed to write to tempfile"); + + let result = get_selinux_security_context(path); + + if result.is_ok() { + println!("Retrieved SELinux context: {}", result.unwrap()); + } else { + let err = result.unwrap_err(); + + // Valid error types + match err { + Error::SELinuxNotEnabled => assert!(true, "SELinux not supported"), + Error::ContextRetrievalFailure => assert!(true, "Context retrieval failure"), + Error::ContextConversionFailure => assert!(true, "Context conversion failure"), + Error::FileOpenFailure => { + panic!("File open failure occurred despite file being created") + } + } + } + } + + #[test] + fn test_get_selinux_context_nonexistent_file() { + let path = Path::new("/nonexistent/file/that/does/not/exist"); + + let result = get_selinux_security_context(path); + + assert!(result.is_err()); + assert!( + matches!(result.unwrap_err(), Error::FileOpenFailure), + "Expected file open error for nonexistent file" + ); + } } diff --git a/tests/by-util/test_stat.rs b/tests/by-util/test_stat.rs index 8fbf62906..9587367e8 100644 --- a/tests/by-util/test_stat.rs +++ b/tests/by-util/test_stat.rs @@ -490,3 +490,27 @@ fn test_printf_invalid_directive() { .fails_with_code(1) .stderr_contains("'%9%': invalid directive"); } + +#[test] +#[cfg(feature = "feat_selinux")] +fn test_stat_selinux() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + at.touch("f"); + ts.ucmd() + .arg("--printf='%C'") + .arg("f") + .succeeds() + .no_stderr() + .stdout_contains("unconfined_u"); + ts.ucmd() + .arg("--printf='%C'") + .arg("/bin/") + .succeeds() + .no_stderr() + .stdout_contains("system_u"); + // Count that we have 4 fields + let result = ts.ucmd().arg("--printf='%C'").arg("/bin/").succeeds(); + let s: Vec<_> = result.stdout_str().split(":").collect(); + assert!(s.len() == 4); +}