diff --git a/CHANGELOG.md b/CHANGELOG.md index 66fd23b0..9dbed4f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added the `FileEntry.linkto` field that is a target of a symbolic link. - Function `Package::get_file_entries` returns an empty vector for an RPM package without any files. - `FileEntry` structs returned by (`Package::get_file_entries`) now include IMA signature information as well as digests for file entries. +- Add mod `rpm::filecaps` instead of capctl crate ## 0.12.1 diff --git a/Cargo.toml b/Cargo.toml index 84311680..429754e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,7 +53,6 @@ itertools = "0.12" hex = { version = "0.4", features = ["std"] } zstd = "0.13" xz2 = "0.1" -capctl = "0.2.3" [dev-dependencies] env_logger = "0.10.0" diff --git a/src/errors.rs b/src/errors.rs index 8c2abb4b..07f7820d 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -92,6 +92,9 @@ pub enum Error { #[error("timestamp conversion error: {0:?}")] TimestampConv(TimestampError), + + #[error("{0}")] + InvalidFileCaps(String), } impl From> for Error { diff --git a/src/rpm/filecaps.rs b/src/rpm/filecaps.rs new file mode 100644 index 00000000..bf2233c7 --- /dev/null +++ b/src/rpm/filecaps.rs @@ -0,0 +1,213 @@ +// ref. https://github.com/cptpcrd/capctl/blob/4b9ec47b48c6d6669c1d52f73831ad1633562a05/src/caps/cap_text.rs#L5 +use std::{fmt::Display, str::FromStr}; + +use crate::Error; + +const CAPS: &[&str; 41] = &[ + "CAP_CHOWN", + "CAP_DAC_OVERRIDE", + "CAP_DAC_READ_SEARCH", + "CAP_FOWNER", + "CAP_FSETID", + "CAP_KILL", + "CAP_SETGID", + "CAP_SETUID", + "CAP_SETPCAP", + "CAP_LINUX_IMMUTABLE", + "CAP_NET_BIND_SERVICE", + "CAP_NET_BROADCAST", + "CAP_NET_ADMIN", + "CAP_NET_RAW", + "CAP_IPC_LOCK", + "CAP_IPC_OWNER", + "CAP_SYS_MODULE", + "CAP_SYS_RAWIO", + "CAP_SYS_CHROOT", + "CAP_SYS_PTRACE", + "CAP_SYS_PACCT", + "CAP_SYS_ADMIN", + "CAP_SYS_BOOT", + "CAP_SYS_NICE", + "CAP_SYS_RESOURCE", + "CAP_SYS_TIME", + "CAP_SYS_TTY_CONFIG", + "CAP_MKNOD", + "CAP_LEASE", + "CAP_AUDIT_WRITE", + "CAP_AUDIT_CONTROL", + "CAP_SETFCAP", + "CAP_MAC_OVERRIDE", + "CAP_MAC_ADMIN", + "CAP_SYSLOG", + "CAP_WAKE_ALARM", + "CAP_BLOCK_SUSPEND", + "CAP_AUDIT_READ", + "CAP_PERFMON", + "CAP_BPF", + "CAP_CHECKPOINT_RESTORE", +]; + +#[derive(Debug, Clone)] +pub struct FileCaps(String); + +impl Display for FileCaps { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl FromStr for FileCaps { + type Err = Error; + + fn from_str(s: &str) -> Result { + validate_caps_text(s)?; + + Ok(Self(s.to_owned())) + } +} + +fn validate_capset(s: &str) -> Result<(), Error> { + if s.is_empty() || s.eq_ignore_ascii_case("all") { + return Ok(()); + } + + for part in s.split(',') { + if !CAPS.contains(&part.to_uppercase().as_str()) { + return Err(Error::InvalidFileCaps(format!("Unknown cap {}", &part))); + } + } + + Ok(()) +} + +fn validate_suffix(s: &str) -> Result<(), Error> { + let mut last_ch = None; + for ch in s.chars() { + match ch { + '=' | '+' | '-' => match last_ch { + // No "+/-/=" following each other + Some('=') | Some('+') | Some('-') => { + return Err(Error::InvalidFileCaps( + "No `+/-/=` following each other".to_owned(), + )) + } + _ => (), + }, + + 'p' | 'i' | 'e' => debug_assert!(last_ch.is_some()), + + _ => { + return Err(Error::InvalidFileCaps(format!( + "Invalid suffix char {}", + ch + ))) + } + } + + last_ch = Some(ch); + } + + Ok(()) +} + +pub fn validate_caps_text(s: &str) -> Result<(), Error> { + let s = s.trim(); + if s.is_empty() { + return Err(Error::InvalidFileCaps("Empty text".to_owned())); + } + + for part in s.split_whitespace() { + let index = match part.find(|c| c == '+' || c == '-' || c == '=') { + Some(i) => i, + None => return Err(Error::InvalidFileCaps("`+/-/=` not found".to_owned())), + }; + + if index == 0 && !s.starts_with('=') { + // Example: "+eip" or "-eip" + return Err(Error::InvalidFileCaps(format!( + "Unexpected first char of `{}`", + &part + ))); + } + + validate_capset(&part[..index])?; + validate_suffix(&part[index..])?; + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::{validate_caps_text, validate_capset, validate_suffix}; + + #[test] + fn test_validate_capset() { + validate_capset("").unwrap(); + validate_capset("all").unwrap(); + validate_capset("ALL").unwrap(); + validate_capset("cap_chown").unwrap(); + validate_capset("CAP_CHOWN").unwrap(); + validate_capset("cap_chown,cap_syslog").unwrap(); + + assert_eq!( + validate_capset("cap_noexist").unwrap_err().to_string(), + "Unknown cap cap_noexist" + ); + assert_eq!( + validate_capset(",").unwrap_err().to_string(), + "Unknown cap " + ); + } + + #[test] + fn test_validate_suffix() { + validate_suffix("+p").unwrap(); + } + + #[test] + fn test_validate_caps_text() { + assert_eq!( + validate_caps_text("").unwrap_err().to_string(), + "Empty text" + ); + assert_eq!( + validate_caps_text(" ").unwrap_err().to_string(), + "Empty text" + ); + assert_eq!( + validate_caps_text("cap_chown").unwrap_err().to_string(), + "`+/-/=` not found" + ); + assert_eq!( + validate_caps_text("+eip").unwrap_err().to_string(), + "Unexpected first char of `+eip`" + ); + assert_eq!( + validate_caps_text("-eip").unwrap_err().to_string(), + "Unexpected first char of `-eip`" + ); + assert_eq!( + validate_caps_text("cap_chown+-p").unwrap_err().to_string(), + "No `+/-/=` following each other" + ); + assert_eq!( + validate_caps_text("cap_chown=-p").unwrap_err().to_string(), + "No `+/-/=` following each other" + ); + assert_eq!( + validate_caps_text("cap_chown+y").unwrap_err().to_string(), + "Invalid suffix char y" + ); + assert_eq!( + validate_caps_text("cap_noexist+p").unwrap_err().to_string(), + "Unknown cap cap_noexist" + ); + validate_caps_text("cap_chown=p").unwrap(); + validate_caps_text("cap_chown+p").unwrap(); + validate_caps_text("cap_chown+ie").unwrap(); + validate_caps_text("=e cap_chown-e").unwrap(); + validate_caps_text("=e").unwrap(); + validate_caps_text("all=e").unwrap(); + } +} diff --git a/src/rpm/headers/types.rs b/src/rpm/headers/types.rs index 7c20b870..e58b52b4 100644 --- a/src/rpm/headers/types.rs +++ b/src/rpm/headers/types.rs @@ -1,6 +1,5 @@ //! A collection of types used in various header records. -use crate::{constants::*, errors, Timestamp}; -use capctl::FileCaps; +use crate::{constants::*, errors, FileCaps, Timestamp}; use digest::Digest; use std::str::FromStr; diff --git a/src/rpm/mod.rs b/src/rpm/mod.rs index a74c394a..ff4c4077 100644 --- a/src/rpm/mod.rs +++ b/src/rpm/mod.rs @@ -1,5 +1,6 @@ mod builder; mod compressor; +mod filecaps; mod headers; mod package; mod timestamp; @@ -18,3 +19,5 @@ pub use timestamp::*; #[cfg(feature = "chrono")] pub use ::chrono; + +pub use filecaps::*; diff --git a/src/tests.rs b/src/tests.rs index 6da8b8f7..d37065d6 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -72,7 +72,7 @@ However, it does nothing.", if f.path.as_os_str() == "/etc/awesome/second.toml" { assert_eq!( f.clone().caps.unwrap(), - "cap_sys_ptrace,cap_sys_admin=ep".to_string() + "cap_sys_admin,cap_sys_ptrace=pe".to_string() ); assert_eq!(f.ownership.user, "hugo".to_string()); } else if f.path.as_os_str() == "/etc/awesome/config.toml" {