use crate::executor::ExecutorError;
pub fn validated_username(s: &str, param: &'static str) -> Result<String, ExecutorError> {
if s.is_empty() || s.len() > 32 {
return Err(ExecutorError::InvalidParam(param));
}
if s.starts_with('-') || s.starts_with('.') || s.contains("..") {
return Err(ExecutorError::InvalidParam(param));
}
if !s
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '_' || c == '-')
{
return Err(ExecutorError::InvalidParam(param));
}
Ok(s.to_string())
}
pub fn validated_group(s: &str, param: &'static str) -> Result<String, ExecutorError> {
validated_username(s, param)
}
pub fn validated_unit_name(s: &str, param: &'static str) -> Result<String, ExecutorError> {
if s.is_empty() || s.starts_with('-') {
return Err(ExecutorError::InvalidParam(param));
}
if !s
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '@' | '.' | '_' | ':' | '-'))
{
return Err(ExecutorError::InvalidParam(param));
}
Ok(s.to_string())
}
pub fn validated_hostname(s: &str, param: &'static str) -> Result<String, ExecutorError> {
if s.is_empty() || s.len() > 253 || s.starts_with('-') {
return Err(ExecutorError::InvalidParam(param));
}
if !s
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-')
{
return Err(ExecutorError::InvalidParam(param));
}
for label in s.split('.') {
if label.is_empty() || label.len() > 63 {
return Err(ExecutorError::InvalidParam(param));
}
}
Ok(s.to_string())
}
pub fn validated_timezone(s: &str, param: &'static str) -> Result<String, ExecutorError> {
if s.is_empty() || s.starts_with('-') {
return Err(ExecutorError::InvalidParam(param));
}
if s.contains("..") {
return Err(ExecutorError::InvalidParam(param));
}
if !s
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '/' | '_' | '+' | '-'))
{
return Err(ExecutorError::InvalidParam(param));
}
Ok(s.to_string())
}
pub fn validated_locale(s: &str, param: &'static str) -> Result<String, ExecutorError> {
if s.is_empty() || s.starts_with('-') {
return Err(ExecutorError::InvalidParam(param));
}
if !s
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '_' || c == '-')
{
return Err(ExecutorError::InvalidParam(param));
}
Ok(s.to_string())
}
pub fn validated_ppa_name(s: &str, param: &'static str) -> Result<String, ExecutorError> {
let parts: Vec<&str> = s.splitn(3, '/').collect();
if parts.len() != 2 {
return Err(ExecutorError::InvalidParam(param));
}
let (user, ppa) = (parts[0], parts[1]);
if user.is_empty() || ppa.is_empty() {
return Err(ExecutorError::InvalidParam(param));
}
let is_valid_component = |c: char| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-');
if !user.chars().all(is_valid_component) || !ppa.chars().all(is_valid_component) {
return Err(ExecutorError::InvalidParam(param));
}
if s.len() > SAFE_ARG_MAX_BYTES {
return Err(ExecutorError::InvalidParam(param));
}
Ok(s.to_string())
}
const APPARMOR_PROFILE_NAME_MAX: usize = 128;
pub fn validated_apparmor_profile(s: &str, param: &'static str) -> Result<String, ExecutorError> {
const PREFIX: &str = "/etc/apparmor.d/";
if s.is_empty() {
return Err(ExecutorError::InvalidParam(param));
}
if s.starts_with('/') {
if !s.starts_with(PREFIX) {
return Err(ExecutorError::InvalidParam(param));
}
if s.contains("..") {
return Err(ExecutorError::InvalidParam(param));
}
let suffix = &s[PREFIX.len()..];
if suffix.is_empty() {
return Err(ExecutorError::InvalidParam(param));
}
let ok = suffix
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-' | '/'));
if !ok {
return Err(ExecutorError::InvalidParam(param));
}
} else {
if s.contains('/') {
return Err(ExecutorError::InvalidParam(param));
}
if s.starts_with('.') || s.starts_with('-') {
return Err(ExecutorError::InvalidParam(param));
}
if s.len() > APPARMOR_PROFILE_NAME_MAX {
return Err(ExecutorError::InvalidParam(param));
}
let ok = s
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-'));
if !ok {
return Err(ExecutorError::InvalidParam(param));
}
}
Ok(s.to_string())
}
const UFW_APP_NAME_MAX: usize = 64;
pub fn validated_port_or_service(s: &str, param: &'static str) -> Result<String, ExecutorError> {
if s.is_empty() {
return Err(ExecutorError::InvalidParam(param));
}
if let Some(slash_pos) = s.find('/') {
let port_part = &s[..slash_pos];
let proto_part = &s[slash_pos + 1..];
if proto_part != "tcp" && proto_part != "udp" {
return Err(ExecutorError::InvalidParam(param));
}
if port_part.is_empty() || !port_part.chars().all(|c| c.is_ascii_digit()) {
return Err(ExecutorError::InvalidParam(param));
}
let port: u32 = port_part
.parse()
.map_err(|_| ExecutorError::InvalidParam(param))?;
if port == 0 || port > 65535 {
return Err(ExecutorError::InvalidParam(param));
}
return Ok(s.to_string());
}
if s.chars().all(|c| c.is_ascii_digit()) {
let port: u32 = s.parse().map_err(|_| ExecutorError::InvalidParam(param))?;
if port == 0 || port > 65535 {
return Err(ExecutorError::InvalidParam(param));
}
return Ok(s.to_string());
}
if !s.starts_with(|c: char| c.is_ascii_alphabetic()) {
return Err(ExecutorError::InvalidParam(param));
}
if s.len() > UFW_APP_NAME_MAX {
return Err(ExecutorError::InvalidParam(param));
}
let ok = s
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-'));
if !ok {
return Err(ExecutorError::InvalidParam(param));
}
Ok(s.to_string())
}
pub const SAFE_ARG_MAX_BYTES: usize = 254;
pub fn validated_safe_arg(s: &str, param: &'static str) -> Result<String, ExecutorError> {
if s.is_empty() || s.len() > SAFE_ARG_MAX_BYTES {
return Err(ExecutorError::InvalidParam(param));
}
if s.starts_with('-') {
return Err(ExecutorError::InvalidParam(param));
}
if !s
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | ':' | '/' | '+' | '@' | '-'))
{
return Err(ExecutorError::InvalidParam(param));
}
Ok(s.to_string())
}
pub fn validated_lvm_name(s: &str, param: &'static str) -> Result<String, ExecutorError> {
if s.is_empty() || s.len() > 127 {
return Err(ExecutorError::InvalidParam(param));
}
let mut chars = s.chars();
let first = chars.next().unwrap();
if !(first.is_ascii_alphanumeric() || first == '_') {
return Err(ExecutorError::InvalidParam(param));
}
if !s
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '_' | '.' | '-'))
{
return Err(ExecutorError::InvalidParam(param));
}
Ok(s.to_string())
}
pub fn validated_lvm_size(s: &str, param: &'static str) -> Result<String, ExecutorError> {
if s.len() > 32 {
return Err(ExecutorError::InvalidParam(param));
}
let body = s.strip_prefix('+').unwrap_or(s);
let (digits, suffix) = match body.chars().last() {
Some(c) if c.is_ascii_alphabetic() => (&body[..body.len() - 1], Some(c)),
_ => (body, None),
};
if let Some(c) = suffix {
if !matches!(c, 'k' | 'K' | 'm' | 'M' | 'g' | 'G' | 't' | 'T' | 'p' | 'P') {
return Err(ExecutorError::InvalidParam(param));
}
}
if digits.is_empty()
|| digits.matches('.').count() > 1
|| !digits.chars().all(|c| c.is_ascii_digit() || c == '.')
|| digits.starts_with('.')
|| digits.ends_with('.')
{
return Err(ExecutorError::InvalidParam(param));
}
Ok(s.to_string())
}
const JOURNAL_PRIORITY_NAMES: &[&str] = &[
"emerg", "alert", "crit", "err", "warning", "notice", "info", "debug",
];
pub fn validated_journal_priority(s: &str, param: &'static str) -> Result<String, ExecutorError> {
let is_level = |lvl: &str| -> bool {
matches!(lvl, "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7")
|| JOURNAL_PRIORITY_NAMES.contains(&lvl)
};
let ok = match s.split_once("..") {
Some((lo, hi)) => is_level(lo) && is_level(hi),
None => is_level(s),
};
if ok {
Ok(s.to_string())
} else {
Err(ExecutorError::InvalidParam(param))
}
}
pub fn validated_journal_time(s: &str, param: &'static str) -> Result<String, ExecutorError> {
if s.is_empty() || s.len() > 64 {
return Err(ExecutorError::InvalidParam(param));
}
if !s
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, ' ' | ':' | '-' | '+' | '.' | ','))
{
return Err(ExecutorError::InvalidParam(param));
}
Ok(s.to_string())
}
pub fn validated_journal_grep(s: &str, param: &'static str) -> Result<String, ExecutorError> {
if s.is_empty() || s.len() > 256 {
return Err(ExecutorError::InvalidParam(param));
}
if s.chars().any(|c| c.is_control()) {
return Err(ExecutorError::InvalidParam(param));
}
Ok(s.to_string())
}
pub fn validated_sysctl_key(s: &str, param: &'static str) -> Result<String, ExecutorError> {
if s.is_empty() || s.len() > 128 {
return Err(ExecutorError::InvalidParam(param));
}
let first = s.chars().next().unwrap();
if !first.is_ascii_lowercase() && !first.is_ascii_digit() {
return Err(ExecutorError::InvalidParam(param));
}
if !s
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || matches!(c, '.' | '_' | '-'))
{
return Err(ExecutorError::InvalidParam(param));
}
Ok(s.to_string())
}
pub fn validated_sysctl_value(s: &str, param: &'static str) -> Result<String, ExecutorError> {
if s.is_empty() || s.len() > 200 {
return Err(ExecutorError::InvalidParam(param));
}
if !s
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, ' ' | '.' | '_' | '/' | ':' | ',' | '-'))
{
return Err(ExecutorError::InvalidParam(param));
}
Ok(s.to_string())
}
pub fn validated_memory_limit(s: &str, param: &'static str) -> Result<String, ExecutorError> {
if s == "infinity" {
return Ok(s.to_string());
}
if s.len() > 24 {
return Err(ExecutorError::InvalidParam(param));
}
let digits = match s.chars().last() {
Some('K' | 'M' | 'G' | 'T') => &s[..s.len() - 1],
_ => s,
};
if digits.is_empty() || !digits.chars().all(|c| c.is_ascii_digit()) {
return Err(ExecutorError::InvalidParam(param));
}
Ok(s.to_string())
}
pub fn validated_cpu_quota(s: &str, param: &'static str) -> Result<String, ExecutorError> {
let digits = s
.strip_suffix('%')
.ok_or(ExecutorError::InvalidParam(param))?;
if digits.is_empty() || digits.len() > 7 || !digits.chars().all(|c| c.is_ascii_digit()) {
return Err(ExecutorError::InvalidParam(param));
}
Ok(s.to_string())
}
pub fn validated_tasks_max(s: &str, param: &'static str) -> Result<String, ExecutorError> {
if s == "infinity" {
return Ok(s.to_string());
}
if s.is_empty() || s.len() > 12 || !s.chars().all(|c| c.is_ascii_digit()) {
return Err(ExecutorError::InvalidParam(param));
}
Ok(s.to_string())
}
const FSTYPE_ALLOW: &[&str] = &[
"ext4", "ext3", "ext2", "xfs", "btrfs", "vfat", "exfat", "ntfs3", "nfs", "nfs4", "cifs",
];
const CRITICAL_MOUNTPOINTS: &[&str] = &[
"/",
"/boot",
"/boot/efi",
"/etc",
"/usr",
"/bin",
"/sbin",
"/lib",
"/lib64",
"/var",
"/home",
"/root",
"/proc",
"/sys",
"/dev",
"/run",
];
pub fn validated_mount_device(s: &str, param: &'static str) -> Result<String, ExecutorError> {
if s.is_empty() || s.len() > 256 || s.starts_with('-') || s.contains("..") {
return Err(ExecutorError::InvalidParam(param));
}
let dev_like = s.starts_with("/dev/")
&& s[5..]
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '/' | '_' | '.' | '-'));
let uuid = s
.strip_prefix("UUID=")
.is_some_and(|u| u.len() >= 8 && u.chars().all(|c| c.is_ascii_hexdigit() || c == '-'));
let label = s.strip_prefix("LABEL=").is_some_and(|l| {
!l.is_empty()
&& l.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '.' | ':' | '-'))
});
let cifs = s.starts_with("//")
&& s.matches('/').count() >= 3
&& s.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '/' | '.' | '_' | '-'));
let nfs = {
match s.split_once(":/") {
Some((host, _)) => {
!host.is_empty()
&& !host.starts_with('/')
&& s.chars().all(|c| {
c.is_ascii_alphanumeric() || matches!(c, ':' | '/' | '.' | '_' | '-')
})
}
None => false,
}
};
if dev_like || uuid || label || cifs || nfs {
Ok(s.to_string())
} else {
Err(ExecutorError::InvalidParam(param))
}
}
pub fn validated_mount_point(s: &str, param: &'static str) -> Result<String, ExecutorError> {
if !s.starts_with('/') || s.len() > 256 || s.contains("..") {
return Err(ExecutorError::InvalidParam(param));
}
if !s
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '/' | '_' | '.' | '-'))
{
return Err(ExecutorError::InvalidParam(param));
}
if CRITICAL_MOUNTPOINTS.contains(&s) {
return Err(ExecutorError::InvalidParam(param));
}
Ok(s.to_string())
}
pub fn validated_fstype(s: &str, param: &'static str) -> Result<String, ExecutorError> {
if FSTYPE_ALLOW.contains(&s) {
Ok(s.to_string())
} else {
Err(ExecutorError::InvalidParam(param))
}
}
pub fn validated_mount_options(s: &str, param: &'static str) -> Result<String, ExecutorError> {
if s.len() > 256 {
return Err(ExecutorError::InvalidParam(param));
}
if !s.chars().all(|c| {
c.is_ascii_alphanumeric()
|| matches!(c, '=' | ',' | '.' | '_' | ':' | '@' | '/' | '+' | '-')
}) {
return Err(ExecutorError::InvalidParam(param));
}
Ok(s.to_string())
}
pub fn validated_swap_path(s: &str, param: &'static str) -> Result<String, ExecutorError> {
if !s.starts_with('/') || s.len() > 256 || s.contains("..") {
return Err(ExecutorError::InvalidParam(param));
}
if !s
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '/' | '_' | '.' | '-'))
{
return Err(ExecutorError::InvalidParam(param));
}
Ok(s.to_string())
}
pub fn validated_sudoers_name(s: &str, param: &'static str) -> Result<String, ExecutorError> {
if s.is_empty() || s.len() > 64 {
return Err(ExecutorError::InvalidParam(param));
}
let first = s.chars().next().unwrap();
if !(first.is_ascii_lowercase() || first.is_ascii_digit()) {
return Err(ExecutorError::InvalidParam(param));
}
if !s
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || matches!(c, '_' | '-'))
{
return Err(ExecutorError::InvalidParam(param));
}
Ok(s.to_string())
}
pub fn validated_apt_pin_name(s: &str, param: &'static str) -> Result<String, ExecutorError> {
validated_sudoers_name(s, param)
}
pub fn validated_log_path(s: &str, param: &'static str) -> Result<String, ExecutorError> {
if !s.starts_with('/') || s.len() > 255 || s.contains("..") {
return Err(ExecutorError::InvalidParam(param));
}
if !s
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '/' | '.' | '_' | '*' | '-'))
{
return Err(ExecutorError::InvalidParam(param));
}
Ok(s.to_string())
}
pub fn validated_syslog_host(s: &str, param: &'static str) -> Result<String, ExecutorError> {
if s.is_empty() || s.len() > 255 || s.contains("..") || s.starts_with('-') {
return Err(ExecutorError::InvalidParam(param));
}
if !s
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | ':' | '_' | '-'))
{
return Err(ExecutorError::InvalidParam(param));
}
Ok(s.to_string())
}
pub fn validated_apt_package(s: &str, param: &'static str) -> Result<String, ExecutorError> {
if s.is_empty()
|| s.len() > 128
|| !s.chars().all(|c| {
c.is_ascii_alphanumeric() || matches!(c, '.' | '+' | '*' | '?' | '_' | ':' | '-')
})
{
return Err(ExecutorError::InvalidParam(param));
}
Ok(s.to_string())
}
pub fn validated_apt_pin_expr(s: &str, param: &'static str) -> Result<String, ExecutorError> {
if s.is_empty()
|| s.len() > 200
|| !s.chars().all(|c| {
c.is_ascii_alphanumeric()
|| matches!(c, ' ' | '=' | '.' | ',' | ':' | '/' | '*' | '_' | '+' | '-')
})
{
return Err(ExecutorError::InvalidParam(param));
}
Ok(s.to_string())
}
pub fn validated_sudo_commands(s: &str, param: &'static str) -> Result<String, ExecutorError> {
if s == "ALL" {
return Ok(s.to_string());
}
if s.is_empty() || s.len() > 1024 {
return Err(ExecutorError::InvalidParam(param));
}
let cmds: Vec<&str> = s.split(',').filter(|c| !c.is_empty()).collect();
if cmds.is_empty() {
return Err(ExecutorError::InvalidParam(param));
}
for c in cmds {
if !c.starts_with('/')
|| c.contains("..")
|| c.contains('*')
|| !c
.chars()
.all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '/' | '.' | '_' | '-'))
{
return Err(ExecutorError::InvalidParam(param));
}
}
Ok(s.to_string())
}
pub fn validated_pro_service(s: &str, param: &'static str) -> Result<String, ExecutorError> {
const SERVICES: &[&str] = &[
"esm-apps",
"esm-infra",
"livepatch",
"usg",
"fips",
"fips-updates",
"fips-preview",
"cis",
"ros",
"ros-updates",
"cc-eal",
"realtime-kernel",
"landscape",
"anbox-cloud",
];
if SERVICES.contains(&s) {
Ok(s.to_string())
} else {
Err(ExecutorError::InvalidParam(param))
}
}
pub fn validated_audit_path(s: &str, param: &'static str) -> Result<String, ExecutorError> {
if !s.starts_with('/') || s.len() > 255 || s.contains("..") {
return Err(ExecutorError::InvalidParam(param));
}
if !s
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '/' | '.' | '_' | '-'))
{
return Err(ExecutorError::InvalidParam(param));
}
Ok(s.to_string())
}
pub fn validated_audit_perms(s: &str, param: &'static str) -> Result<String, ExecutorError> {
if s.is_empty() || s.len() > 4 || !s.chars().all(|c| matches!(c, 'r' | 'w' | 'x' | 'a')) {
return Err(ExecutorError::InvalidParam(param));
}
let mut seen = [false; 4];
for c in s.chars() {
let i = match c {
'r' => 0,
'w' => 1,
'x' => 2,
_ => 3,
};
if seen[i] {
return Err(ExecutorError::InvalidParam(param));
}
seen[i] = true;
}
Ok(s.to_string())
}
pub fn validated_domain(s: &str, param: &'static str) -> Result<String, ExecutorError> {
if s.is_empty()
|| s.len() > 253
|| s.contains("..")
|| s.starts_with('-')
|| s.starts_with('.')
|| s.ends_with('.')
{
return Err(ExecutorError::InvalidParam(param));
}
if !s
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '-'))
{
return Err(ExecutorError::InvalidParam(param));
}
Ok(s.to_string())
}
pub fn validated_email(s: &str, param: &'static str) -> Result<String, ExecutorError> {
if s.len() < 3 || s.len() > 254 {
return Err(ExecutorError::InvalidParam(param));
}
let parts: Vec<&str> = s.split('@').collect();
if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() || !parts[1].contains('.') {
return Err(ExecutorError::InvalidParam(param));
}
if !s
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '@' | '.' | '_' | '+' | '-'))
{
return Err(ExecutorError::InvalidParam(param));
}
Ok(s.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn audit_path_and_perms() {
assert!(validated_audit_path("/etc/passwd", "p").is_ok());
assert!(validated_audit_path("relative", "p").is_err());
assert!(validated_audit_path("/etc/../shadow", "p").is_err());
assert!(validated_audit_path("/var/log/*.log", "p").is_err()); assert!(validated_audit_perms("wa", "p").is_ok());
assert!(validated_audit_perms("rwxa", "p").is_ok());
assert!(validated_audit_perms("ww", "p").is_err()); assert!(validated_audit_perms("z", "p").is_err());
assert!(validated_audit_perms("", "p").is_err());
}
#[test]
fn domain_and_email() {
assert!(validated_domain("example.com", "d").is_ok());
assert!(validated_domain("a.b.example.co.uk", "d").is_ok());
assert!(validated_domain("-bad.com", "d").is_err());
assert!(validated_domain("a..b.com", "d").is_err());
assert!(validated_domain("ex ample.com", "d").is_err());
assert!(validated_email("ops@example.com", "e").is_ok());
assert!(validated_email("no-at-sign", "e").is_err());
assert!(validated_email("a@b", "e").is_err()); assert!(validated_email("a@b@c.com", "e").is_err());
}
#[test]
fn pro_service_allowlist() {
assert!(validated_pro_service("esm-apps", "service").is_ok());
assert!(validated_pro_service("livepatch", "service").is_ok());
assert!(validated_pro_service("realtime-kernel", "service").is_ok());
assert!(validated_pro_service("bogus", "service").is_err());
assert!(validated_pro_service("--help", "service").is_err());
assert!(validated_pro_service("esm-apps; rm -rf /", "service").is_err());
assert!(validated_pro_service("", "service").is_err());
}
#[test]
fn username_accepts_valid() {
assert_eq!(
validated_username("alice", "username").unwrap(),
"alice".to_string()
);
assert_eq!(
validated_username("bob_99", "username").unwrap(),
"bob_99".to_string()
);
assert_eq!(
validated_username("user.name", "username").unwrap(),
"user.name".to_string()
);
assert_eq!(
validated_username("a-b", "username").unwrap(),
"a-b".to_string()
);
}
#[test]
fn username_rejects_empty() {
assert!(validated_username("", "username").is_err());
}
#[test]
fn username_rejects_starts_with_dash() {
assert!(validated_username("-alice", "username").is_err());
}
#[test]
fn username_rejects_traversal_forms() {
assert!(validated_username("..", "username").is_err());
assert!(validated_username(".", "username").is_err());
assert!(validated_username(".hidden", "username").is_err());
assert!(validated_username("a..b", "username").is_err());
}
#[test]
fn username_rejects_too_long() {
let long = "a".repeat(33);
assert!(validated_username(&long, "username").is_err());
}
#[test]
fn username_accepts_max_length() {
let max = "a".repeat(32);
assert!(validated_username(&max, "username").is_ok());
}
#[test]
fn username_rejects_spaces() {
assert!(validated_username("al ice", "username").is_err());
}
#[test]
fn username_rejects_slashes() {
assert!(validated_username("al/ice", "username").is_err());
}
#[test]
fn username_rejects_null_bytes() {
assert!(validated_username("al\0ice", "username").is_err());
}
#[test]
fn group_delegates_to_username_rules() {
assert!(validated_group("wheel", "group").is_ok());
assert!(validated_group("-bad", "group").is_err());
assert!(validated_group("", "group").is_err());
}
#[test]
fn unit_name_accepts_valid() {
assert!(validated_unit_name("sshd.service", "unit").is_ok());
assert!(validated_unit_name("NetworkManager.service", "unit").is_ok());
assert!(validated_unit_name("user@1000.service", "unit").is_ok());
assert!(validated_unit_name("dbus-org.freedesktop.resolve1.service", "unit").is_ok());
assert!(validated_unit_name("system-getty.slice:0", "unit").is_ok());
}
#[test]
fn unit_name_rejects_empty() {
assert!(validated_unit_name("", "unit").is_err());
}
#[test]
fn unit_name_rejects_leading_dash() {
assert!(validated_unit_name("--version", "unit").is_err());
assert!(validated_unit_name("-.mount", "unit").is_err());
}
#[test]
fn unit_name_rejects_slashes() {
assert!(validated_unit_name("foo/bar.service", "unit").is_err());
}
#[test]
fn unit_name_rejects_spaces() {
assert!(validated_unit_name("foo bar.service", "unit").is_err());
}
#[test]
fn unit_name_rejects_null_bytes() {
assert!(validated_unit_name("foo\0.service", "unit").is_err());
}
#[test]
fn hostname_accepts_valid() {
assert!(validated_hostname("sysknife-lab", "hostname").is_ok());
assert!(validated_hostname("my.host.example", "hostname").is_ok());
assert!(validated_hostname("a", "hostname").is_ok());
}
#[test]
fn hostname_rejects_empty() {
assert!(validated_hostname("", "hostname").is_err());
}
#[test]
fn hostname_rejects_too_long() {
let long = format!(
"{}.{}",
"a".repeat(63),
"b".repeat(253 - 63 - 1 + 1) );
assert!(validated_hostname(&long, "hostname").is_err());
}
#[test]
fn hostname_accepts_max_length() {
let hostname = format!("{}.{}.{}", "a".repeat(63), "b".repeat(63), "c".repeat(63));
assert!(validated_hostname(&hostname, "hostname").is_ok());
}
#[test]
fn hostname_rejects_label_too_long() {
let long_label = "a".repeat(64);
assert!(validated_hostname(&long_label, "hostname").is_err());
}
#[test]
fn hostname_rejects_empty_label() {
assert!(validated_hostname("foo..bar", "hostname").is_err());
assert!(validated_hostname(".foo", "hostname").is_err());
assert!(validated_hostname("foo.", "hostname").is_err());
}
#[test]
fn hostname_rejects_spaces() {
assert!(validated_hostname("my host", "hostname").is_err());
}
#[test]
fn hostname_rejects_underscores() {
assert!(validated_hostname("my_host", "hostname").is_err());
}
#[test]
fn hostname_rejects_leading_dash() {
assert!(validated_hostname("-host", "hostname").is_err());
}
#[test]
fn timezone_accepts_valid() {
assert!(validated_timezone("America/Mexico_City", "timezone").is_ok());
assert!(validated_timezone("UTC", "timezone").is_ok());
assert!(validated_timezone("Etc/GMT+5", "timezone").is_ok());
assert!(validated_timezone("US/Eastern", "timezone").is_ok());
}
#[test]
fn timezone_rejects_empty() {
assert!(validated_timezone("", "timezone").is_err());
}
#[test]
fn timezone_rejects_dot_dot() {
assert!(validated_timezone("America/../etc/passwd", "timezone").is_err());
assert!(validated_timezone("..", "timezone").is_err());
}
#[test]
fn timezone_rejects_spaces() {
assert!(validated_timezone("US/ Eastern", "timezone").is_err());
}
#[test]
fn timezone_rejects_leading_dash() {
assert!(validated_timezone("-America/Mexico_City", "timezone").is_err());
}
#[test]
fn timezone_rejects_null_bytes() {
assert!(validated_timezone("UTC\0", "timezone").is_err());
}
#[test]
fn locale_accepts_valid() {
assert!(validated_locale("en_US.UTF-8", "locale").is_ok());
assert!(validated_locale("C", "locale").is_ok());
assert!(validated_locale("POSIX", "locale").is_ok());
}
#[test]
fn locale_rejects_empty() {
assert!(validated_locale("", "locale").is_err());
}
#[test]
fn locale_rejects_spaces() {
assert!(validated_locale("en US.UTF-8", "locale").is_err());
}
#[test]
fn locale_rejects_slashes() {
assert!(validated_locale("en/US", "locale").is_err());
}
#[test]
fn locale_rejects_leading_dash() {
assert!(validated_locale("-en_US.UTF-8", "locale").is_err());
}
#[test]
fn locale_rejects_null_bytes() {
assert!(validated_locale("en\0US", "locale").is_err());
}
#[test]
fn safe_arg_accepts_valid() {
assert!(validated_safe_arg("org.mozilla.firefox", "app_id").is_ok());
assert!(validated_safe_arg("flathub", "remote").is_ok());
assert!(validated_safe_arg("my-container", "name").is_ok());
assert!(validated_safe_arg("registry.example.com/image:tag", "image").is_ok());
}
#[test]
fn safe_arg_rejects_empty() {
assert!(validated_safe_arg("", "name").is_err());
}
#[test]
fn safe_arg_rejects_null_bytes() {
assert!(validated_safe_arg("hello\0world", "name").is_err());
}
#[test]
fn safe_arg_rejects_starts_with_dash() {
assert!(validated_safe_arg("-evil", "name").is_err());
assert!(validated_safe_arg("--rm", "name").is_err());
}
#[test]
fn safe_arg_accepts_dash_not_at_start() {
assert!(validated_safe_arg("my-container", "name").is_ok());
}
#[test]
fn safe_arg_rejects_unicode_and_non_ascii() {
assert!(validated_safe_arg("café", "name").is_err());
assert!(validated_safe_arg("über", "name").is_err());
}
#[test]
fn safe_arg_rejects_every_shell_metacharacter() {
for meta in [
"a b", "a\tb", "a\nb", "a\rb", "a\0b", "a;b", "a&b", "a|b", "a$b", "a`b`", "a$(b)", "a>b", "a<b", "a\\b", "a\"b", "a'b", "a*b", "a?b", "a[b]", "a{b}", "a~b", "a!b", "a#b", "a%b", "a^b", "a=b", "a,b", "a(b)", ] {
assert!(
validated_safe_arg(meta, "arg").is_err(),
"should reject metacharacter sequence {meta:?}"
);
}
}
#[test]
fn safe_arg_rejects_oversized_input() {
let over = "a".repeat(SAFE_ARG_MAX_BYTES + 1);
assert!(validated_safe_arg(&over, "name").is_err());
let max = "a".repeat(SAFE_ARG_MAX_BYTES);
assert!(validated_safe_arg(&max, "name").is_ok());
}
#[test]
fn ppa_name_accepts_valid() {
assert!(validated_ppa_name("deadsnakes/ppa", "name").is_ok());
assert!(validated_ppa_name("user123/my-ppa", "name").is_ok());
assert!(validated_ppa_name("team.name/repo_name", "name").is_ok());
}
#[test]
fn ppa_name_rejects_no_slash() {
assert!(validated_ppa_name("nodeownerppa", "name").is_err());
}
#[test]
fn ppa_name_rejects_empty_user() {
assert!(validated_ppa_name("/ppa", "name").is_err());
}
#[test]
fn ppa_name_rejects_empty_ppa() {
assert!(validated_ppa_name("user/", "name").is_err());
}
#[test]
fn ppa_name_rejects_multiple_slashes() {
assert!(validated_ppa_name("a/b/c", "name").is_err());
}
#[test]
fn ppa_name_rejects_shell_metacharacters() {
assert!(validated_ppa_name("user/ppa;evil", "name").is_err());
assert!(validated_ppa_name("user$(cmd)/ppa", "name").is_err());
}
#[test]
fn apparmor_profile_accepts_absolute_path() {
assert!(
validated_apparmor_profile("/etc/apparmor.d/usr.bin.firefox", "profile_path").is_ok()
);
assert!(
validated_apparmor_profile("/etc/apparmor.d/abstractions/base", "profile_path").is_ok()
);
}
#[test]
fn apparmor_profile_accepts_profile_name() {
assert!(validated_apparmor_profile("usr.bin.firefox", "profile_path").is_ok());
}
#[test]
fn apparmor_profile_rejects_traversal_relative() {
assert!(validated_apparmor_profile("../../../tmp/evil", "profile_path").is_err());
}
#[test]
fn apparmor_profile_rejects_wrong_prefix() {
assert!(validated_apparmor_profile("/etc/passwd", "profile_path").is_err());
}
#[test]
fn apparmor_profile_rejects_traversal_in_path() {
assert!(
validated_apparmor_profile("/etc/apparmor.d/../../etc/passwd", "profile_path").is_err()
);
}
#[test]
fn apparmor_profile_rejects_relative_with_slash() {
assert!(validated_apparmor_profile("evil/profile", "profile_path").is_err());
}
#[test]
fn apparmor_profile_rejects_shell_metachars() {
assert!(validated_apparmor_profile("; rm -rf /", "profile_path").is_err());
}
#[test]
fn apparmor_profile_rejects_empty() {
assert!(validated_apparmor_profile("", "profile_path").is_err());
}
#[test]
fn apparmor_profile_rejects_too_long() {
let long = "a".repeat(APPARMOR_PROFILE_NAME_MAX + 1);
assert!(validated_apparmor_profile(&long, "profile_path").is_err());
}
#[test]
fn port_or_service_accepts_bare_ports() {
assert!(validated_port_or_service("22", "port_or_service").is_ok());
assert!(validated_port_or_service("1", "port_or_service").is_ok());
assert!(validated_port_or_service("65535", "port_or_service").is_ok());
}
#[test]
fn port_or_service_accepts_port_protocol() {
assert!(validated_port_or_service("22/tcp", "port_or_service").is_ok());
assert!(validated_port_or_service("53/udp", "port_or_service").is_ok());
assert!(validated_port_or_service("8080/tcp", "port_or_service").is_ok());
}
#[test]
fn port_or_service_accepts_app_profile_names() {
assert!(validated_port_or_service("OpenSSH", "port_or_service").is_ok());
assert!(validated_port_or_service("Apache", "port_or_service").is_ok());
assert!(validated_port_or_service("Nginx-Full", "port_or_service").is_ok());
}
#[test]
fn port_or_service_rejects_out_of_range_ports() {
assert!(validated_port_or_service("0", "port_or_service").is_err());
assert!(validated_port_or_service("65536", "port_or_service").is_err());
assert!(validated_port_or_service("99999", "port_or_service").is_err());
}
#[test]
fn port_or_service_rejects_bad_protocol_forms() {
assert!(validated_port_or_service("22/sctp", "port_or_service").is_err());
assert!(validated_port_or_service("22/tcp/extra", "port_or_service").is_err());
assert!(validated_port_or_service("22/", "port_or_service").is_err());
}
#[test]
fn port_or_service_rejects_port_without_slash() {
assert!(validated_port_or_service("22tcp", "port_or_service").is_err());
}
#[test]
fn port_or_service_rejects_empty() {
assert!(validated_port_or_service("", "port_or_service").is_err());
}
#[test]
fn port_or_service_rejects_too_long_app_name() {
let long = "A".repeat(UFW_APP_NAME_MAX + 1);
assert!(validated_port_or_service(&long, "port_or_service").is_err());
}
#[test]
fn port_or_service_rejects_shell_metachars() {
assert!(validated_port_or_service("; rm -rf /", "port_or_service").is_err());
}
#[test]
fn port_or_service_rejects_space_in_app_name() {
assert!(validated_port_or_service("hello world", "port_or_service").is_err());
}
#[test]
fn port_or_service_rejects_digit_leading_non_port() {
assert!(validated_port_or_service("2hello", "port_or_service").is_err());
}
#[test]
fn validators_return_invalid_param_with_correct_field_name() {
let err = validated_username("", "username").unwrap_err();
assert!(matches!(err, ExecutorError::InvalidParam("username")));
let err = validated_group("-bad", "group").unwrap_err();
assert!(matches!(err, ExecutorError::InvalidParam("group")));
let err = validated_unit_name("foo/bar", "unit").unwrap_err();
assert!(matches!(err, ExecutorError::InvalidParam("unit")));
let err = validated_hostname("", "hostname").unwrap_err();
assert!(matches!(err, ExecutorError::InvalidParam("hostname")));
let err = validated_timezone("..", "timezone").unwrap_err();
assert!(matches!(err, ExecutorError::InvalidParam("timezone")));
let err = validated_locale("", "locale").unwrap_err();
assert!(matches!(err, ExecutorError::InvalidParam("locale")));
let err = validated_safe_arg("-x", "name").unwrap_err();
assert!(matches!(err, ExecutorError::InvalidParam("name")));
}
#[test]
fn lvm_name_accepts_valid_and_rejects_injection() {
assert!(validated_lvm_name("ubuntu-vg", "vg").is_ok());
assert!(validated_lvm_name("root_lv.0", "lv").is_ok());
assert!(validated_lvm_name("data+cache", "lv").is_ok());
assert!(validated_lvm_name("-rf", "lv").is_err());
assert!(validated_lvm_name(".", "lv").is_err());
assert!(validated_lvm_name("..", "lv").is_err());
assert!(validated_lvm_name("vg/lv", "lv").is_err());
assert!(validated_lvm_name("", "lv").is_err());
}
#[test]
fn lvm_size_accepts_absolute_relative_decimal() {
assert!(validated_lvm_size("20G", "size").is_ok());
assert!(validated_lvm_size("+10G", "size").is_ok());
assert!(validated_lvm_size("512M", "size").is_ok());
assert!(validated_lvm_size("1.5T", "size").is_ok());
assert!(validated_lvm_size("4096", "size").is_ok()); }
#[test]
fn lvm_size_rejects_shrink_and_junk() {
assert!(validated_lvm_size("-10G", "size").is_err()); assert!(validated_lvm_size("10X", "size").is_err()); assert!(validated_lvm_size("G", "size").is_err()); assert!(validated_lvm_size("1.2.3G", "size").is_err()); assert!(validated_lvm_size("50%FREE", "size").is_err()); }
#[test]
fn journal_priority_accepts_levels_and_ranges() {
assert!(validated_journal_priority("err", "priority").is_ok());
assert!(validated_journal_priority("3", "priority").is_ok());
assert!(validated_journal_priority("0..3", "priority").is_ok());
assert!(validated_journal_priority("err..info", "priority").is_ok());
assert!(validated_journal_priority("8", "priority").is_err());
assert!(validated_journal_priority("fatal", "priority").is_err());
assert!(validated_journal_priority("err;info", "priority").is_err());
}
#[test]
fn journal_time_allows_forms_and_rejects_control() {
assert!(validated_journal_time("2026-07-22 10:00:00", "since").is_ok());
assert!(validated_journal_time("yesterday", "since").is_ok());
assert!(validated_journal_time("-1h", "since").is_ok());
assert!(validated_journal_time("2 days ago", "since").is_ok());
assert!(validated_journal_time("a\nb", "since").is_err());
assert!(validated_journal_time("", "since").is_err());
}
#[test]
fn journal_grep_rejects_control_chars() {
assert!(validated_journal_grep("connection timed out", "grep").is_ok());
assert!(validated_journal_grep("err.*fatal", "grep").is_ok());
assert!(validated_journal_grep("bad\nline", "grep").is_err());
assert!(validated_journal_grep("", "grep").is_err());
}
#[test]
fn sysctl_key_accepts_dotted_and_rejects_injection() {
assert!(validated_sysctl_key("net.ipv4.ip_forward", "key").is_ok());
assert!(validated_sysctl_key("vm.swappiness", "key").is_ok());
assert!(validated_sysctl_key("kernel.kptr_restrict", "key").is_ok());
assert!(validated_sysctl_key("-net.ipv4.ip_forward", "key").is_err()); assert!(validated_sysctl_key("net/ipv4/ip_forward", "key").is_err()); assert!(validated_sysctl_key("Net.Ipv4", "key").is_err()); assert!(validated_sysctl_key("", "key").is_err());
}
#[test]
fn sysctl_value_accepts_numbers_and_lists() {
assert!(validated_sysctl_value("1", "value").is_ok());
assert!(validated_sysctl_value("4096 87380 6291456", "value").is_ok()); assert!(validated_sysctl_value("kernel.core", "value").is_ok());
assert!(validated_sysctl_value("bad\nvalue", "value").is_err());
assert!(validated_sysctl_value("v$(id)", "value").is_err()); assert!(validated_sysctl_value("", "value").is_err());
}
#[test]
fn memory_limit_accepts_bytes_suffix_infinity() {
assert!(validated_memory_limit("infinity", "m").is_ok());
assert!(validated_memory_limit("500M", "m").is_ok());
assert!(validated_memory_limit("2G", "m").is_ok());
assert!(validated_memory_limit("1048576", "m").is_ok()); assert!(validated_memory_limit("500m", "m").is_err()); assert!(validated_memory_limit("500MB", "m").is_err()); assert!(validated_memory_limit("-5M", "m").is_err());
assert!(validated_memory_limit("", "m").is_err());
}
#[test]
fn cpu_quota_requires_percent() {
assert!(validated_cpu_quota("50%", "q").is_ok());
assert!(validated_cpu_quota("200%", "q").is_ok()); assert!(validated_cpu_quota("50", "q").is_err()); assert!(validated_cpu_quota("%", "q").is_err());
assert!(validated_cpu_quota("5.5%", "q").is_err());
}
#[test]
fn tasks_max_positive_int_or_infinity() {
assert!(validated_tasks_max("4096", "t").is_ok());
assert!(validated_tasks_max("infinity", "t").is_ok());
assert!(validated_tasks_max("40.5", "t").is_err());
assert!(validated_tasks_max("-1", "t").is_err());
assert!(validated_tasks_max("", "t").is_err());
}
#[test]
fn mount_device_accepts_forms_and_rejects_junk() {
assert!(validated_mount_device("/dev/sdb1", "d").is_ok());
assert!(validated_mount_device("UUID=1234abcd-5678", "d").is_ok());
assert!(validated_mount_device("LABEL=data", "d").is_ok());
assert!(validated_mount_device("//nas/share", "d").is_ok());
assert!(validated_mount_device("nas.local:/export", "d").is_ok());
assert!(validated_mount_device("-rf", "d").is_err()); assert!(validated_mount_device("/dev/../etc", "d").is_err()); assert!(validated_mount_device("$(id)", "d").is_err());
}
#[test]
fn mount_point_rejects_critical_and_traversal() {
assert!(validated_mount_point("/mnt/data", "m").is_ok());
assert!(validated_mount_point("/srv/backups", "m").is_ok());
assert!(validated_mount_point("/", "m").is_err()); assert!(validated_mount_point("/boot", "m").is_err()); assert!(validated_mount_point("/etc", "m").is_err()); assert!(validated_mount_point("/mnt/../etc", "m").is_err()); assert!(validated_mount_point("relative", "m").is_err()); }
#[test]
fn fstype_allowlist() {
assert!(validated_fstype("ext4", "f").is_ok());
assert!(validated_fstype("xfs", "f").is_ok());
assert!(validated_fstype("nfs", "f").is_ok());
assert!(validated_fstype("proc", "f").is_err());
assert!(validated_fstype("", "f").is_err());
}
#[test]
fn mount_options_and_swap_path() {
assert!(validated_mount_options("noatime,ro", "o").is_ok());
assert!(validated_mount_options("", "o").is_ok()); assert!(validated_mount_options("bad opt", "o").is_err()); assert!(validated_swap_path("/swapfile", "p").is_ok());
assert!(validated_swap_path("/var/swap/sk.swap", "p").is_ok());
assert!(validated_swap_path("swapfile", "p").is_err()); assert!(validated_swap_path("/../etc/x", "p").is_err()); }
#[test]
fn sudoers_name_no_dots_or_tildes() {
assert!(validated_sudoers_name("deploy-restart", "n").is_ok());
assert!(validated_sudoers_name("ci_01", "n").is_ok());
assert!(validated_sudoers_name("bad.name", "n").is_err()); assert!(validated_sudoers_name("bad~", "n").is_err()); assert!(validated_sudoers_name("-lead", "n").is_err());
assert!(validated_sudoers_name("", "n").is_err());
}
#[test]
fn sudo_commands_all_or_abs_paths() {
assert!(validated_sudo_commands("ALL", "c").is_ok());
assert!(validated_sudo_commands("/usr/bin/systemctl", "c").is_ok());
assert!(validated_sudo_commands("/usr/bin/systemctl,/usr/sbin/service", "c").is_ok());
assert!(validated_sudo_commands("systemctl", "c").is_err()); assert!(validated_sudo_commands("/usr/bin/*", "c").is_err()); assert!(validated_sudo_commands("/usr/bin/../bin/sh", "c").is_err()); assert!(validated_sudo_commands("/bin/sh; rm -rf /", "c").is_err()); }
#[test]
fn apt_pin_name_package_expr() {
assert!(validated_apt_pin_name("hold-nginx", "n").is_ok());
assert!(validated_apt_pin_name("bad.name", "n").is_err()); assert!(validated_apt_package("nginx", "p").is_ok());
assert!(validated_apt_package("libc6*", "p").is_ok()); assert!(validated_apt_package("bad pkg", "p").is_err()); assert!(validated_apt_pin_expr("release a=noble-security", "e").is_ok());
assert!(validated_apt_pin_expr("version 1.24.*", "e").is_ok());
assert!(validated_apt_pin_expr("origin\nrepo", "e").is_err()); assert!(validated_apt_pin_expr("", "e").is_err());
}
#[test]
fn log_path_and_syslog_host() {
assert!(validated_log_path("/var/log/nginx/*.log", "p").is_ok());
assert!(validated_log_path("/var/log/app.log", "p").is_ok());
assert!(validated_log_path("relative.log", "p").is_err());
assert!(validated_log_path("/var/log/../etc/x", "p").is_err());
assert!(validated_syslog_host("logs.example.com", "h").is_ok());
assert!(validated_syslog_host("10.0.0.5", "h").is_ok());
assert!(validated_syslog_host("fe80::1", "h").is_ok());
assert!(validated_syslog_host("-bad", "h").is_err());
assert!(validated_syslog_host("bad host", "h").is_err());
}
}