use std::path::PathBuf;
use super::apply::apply_fix;
use crate::checks::caps::{CAP_CHOWN, CAP_FOWNER, CAP_SYS_ADMIN};
use crate::checks::CoreLayer;
use crate::fix::scoring;
use crate::fix::{Fix, FixAction};
use crate::operation::Operation;
use crate::state::acl::{AclEntry, AclPerms, AclTag};
use crate::state::Probe;
use crate::test_helpers::StateBuilder;
fn chmod_fix(path: &str, mode_change: &str) -> Fix {
Fix {
layer: CoreLayer::Dac,
action: FixAction::Chmod {
path: PathBuf::from(path),
mode_change: mode_change.to_owned(),
},
impact: scoring::CHMOD_OTHER,
description: "chmod".into(),
}
}
fn setacl_fix(path: &str, entry: &str) -> Fix {
Fix {
layer: CoreLayer::Dac,
action: FixAction::SetAcl {
path: PathBuf::from(path),
entry: entry.to_owned(),
},
impact: scoring::ACL_USER_GRANT,
description: "setacl".into(),
}
}
#[test]
fn apply_chmod_o_plus_r_sets_other_read_bit() {
let mut state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/file.txt", 0, 0, 0o600)
.build();
let fix = chmod_fix("/file.txt", "o+r");
apply_fix(&fix, &mut state);
let comp = state
.walk
.iter()
.find(|c| c.path.as_os_str() == "/file.txt")
.unwrap();
let Probe::Known(stat) = &comp.stat else {
panic!("stat should be known")
};
assert_ne!(stat.mode & 0o004, 0, "other read bit should be set");
}
#[test]
fn apply_chmod_g_plus_w_sets_group_write_bit() {
let mut state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Write)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/file.txt", 0, 0, 0o644)
.build();
let fix = chmod_fix("/file.txt", "g+w");
apply_fix(&fix, &mut state);
let comp = state
.walk
.iter()
.find(|c| c.path.as_os_str() == "/file.txt")
.unwrap();
let Probe::Known(stat) = &comp.stat else {
panic!("stat should be known")
};
assert_ne!(stat.mode & 0o020, 0, "group write bit should be set");
}
#[test]
fn apply_chmod_u_plus_x_sets_user_execute_bit() {
let mut state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Execute)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/script.sh", 1000, 1000, 0o644)
.build();
let fix = chmod_fix("/script.sh", "u+x");
apply_fix(&fix, &mut state);
let comp = state
.walk
.iter()
.find(|c| c.path.as_os_str() == "/script.sh")
.unwrap();
let Probe::Known(stat) = &comp.stat else {
panic!("stat should be known")
};
assert_ne!(stat.mode & 0o100, 0, "user execute bit should be set");
}
#[test]
fn apply_chmod_nonexistent_path_is_noop() {
let mut state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component_file("/file.txt", 0, 0, 0o600)
.build();
let original_mode = if let Probe::Known(stat) = &state.walk[0].stat {
stat.mode
} else {
panic!("expected known stat")
};
let fix = chmod_fix("/does_not_exist.txt", "o+r");
apply_fix(&fix, &mut state);
let Probe::Known(stat) = &state.walk[0].stat else {
panic!()
};
assert_eq!(stat.mode, original_mode);
}
#[test]
fn apply_chmod_invalid_mode_string_is_noop() {
let mut state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component_file("/file.txt", 0, 0, 0o600)
.build();
let original_mode = if let Probe::Known(stat) = &state.walk[0].stat {
stat.mode
} else {
panic!()
};
let fix = chmod_fix("/file.txt", "o-r");
apply_fix(&fix, &mut state);
let Probe::Known(stat) = &state.walk[0].stat else {
panic!()
};
assert_eq!(stat.mode, original_mode);
}
#[test]
fn apply_chmod_on_unknown_stat_is_noop() {
let mut state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component_unknown("/mystery.txt")
.build();
let fix = chmod_fix("/mystery.txt", "o+r");
apply_fix(&fix, &mut state);
assert!(matches!(state.walk[0].stat, Probe::Unknown));
}
#[test]
fn apply_setacl_adds_new_user_entry_to_known_acl() {
let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file_with_acl(
"/file.txt",
0,
0,
0o600,
vec![AclEntry {
tag: AclTag::UserObj,
qualifier: None,
perms: AclPerms {
read: true,
write: true,
execute: false,
},
}],
)
.build();
let mut state = state;
let fix = setacl_fix("/file.txt", "u:1000:r");
apply_fix(&fix, &mut state);
let comp = state
.walk
.iter()
.find(|c| c.path.as_os_str() == "/file.txt")
.unwrap();
let Probe::Known(acl) = &comp.acl else {
panic!("acl should be known")
};
let user_entry = acl
.0
.iter()
.find(|e| e.tag == AclTag::User && e.qualifier == Some(1000));
assert!(
user_entry.is_some(),
"named user entry should have been added"
);
assert!(user_entry.unwrap().perms.read);
}
#[test]
fn apply_setacl_replaces_existing_entry_same_tag_qualifier() {
let existing_entry = AclEntry {
tag: AclTag::User,
qualifier: Some(1000),
perms: AclPerms {
read: false,
write: false,
execute: false,
},
};
let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file_with_acl("/file.txt", 0, 0, 0o600, vec![existing_entry])
.build();
let mut state = state;
let fix = setacl_fix("/file.txt", "u:1000:r");
apply_fix(&fix, &mut state);
let comp = state
.walk
.iter()
.find(|c| c.path.as_os_str() == "/file.txt")
.unwrap();
let Probe::Known(acl) = &comp.acl else {
panic!()
};
let entries: Vec<_> = acl
.0
.iter()
.filter(|e| e.tag == AclTag::User && e.qualifier == Some(1000))
.collect();
assert_eq!(entries.len(), 1, "should not duplicate, should replace");
assert!(entries[0].perms.read);
}
#[test]
fn apply_setacl_on_unknown_acl_creates_new_acl() {
let mut state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component_unknown("/mystery.txt")
.build();
let fix = setacl_fix("/mystery.txt", "u:1000:r");
apply_fix(&fix, &mut state);
let Probe::Known(acl) = &state.walk[0].acl else {
panic!("acl should be known after setacl on unknown")
};
assert_eq!(acl.0.len(), 1);
}
#[test]
fn apply_chmod_empty_bits_after_plus_is_noop() {
let mut state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component_file("/file.txt", 0, 0, 0o600)
.build();
let original_mode = if let Probe::Known(s) = &state.walk[0].stat {
s.mode
} else {
panic!()
};
let fix = Fix {
layer: CoreLayer::Dac,
action: FixAction::Chmod {
path: PathBuf::from("/file.txt"),
mode_change: "o+".into(),
},
impact: 1,
description: "test".into(),
};
apply_fix(&fix, &mut state);
let Probe::Known(stat) = &state.walk[0].stat else {
panic!()
};
assert_eq!(stat.mode, original_mode);
}
#[test]
fn apply_setacl_rwx_perms_parsed_correctly() {
let mut state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file_with_acl("/file.txt", 0, 0, 0o600, vec![])
.build();
let fix = setacl_fix("/file.txt", "u:1000:rwx");
apply_fix(&fix, &mut state);
let comp = state
.walk
.iter()
.find(|c| c.path.as_os_str() == "/file.txt")
.unwrap();
let Probe::Known(acl) = &comp.acl else {
panic!()
};
let entry = acl
.0
.iter()
.find(|e| e.tag == AclTag::User && e.qualifier == Some(1000))
.unwrap();
assert!(entry.perms.read);
assert!(entry.perms.write);
assert!(entry.perms.execute);
}
#[test]
fn apply_setacl_dash_perms_parsed_as_no_perms() {
let mut state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file_with_acl("/file.txt", 0, 0, 0o600, vec![])
.build();
let fix = setacl_fix("/file.txt", "u:1000:--x");
apply_fix(&fix, &mut state);
let comp = state
.walk
.iter()
.find(|c| c.path.as_os_str() == "/file.txt")
.unwrap();
let Probe::Known(acl) = &comp.acl else {
panic!()
};
let entry = acl
.0
.iter()
.find(|e| e.tag == AclTag::User && e.qualifier == Some(1000))
.unwrap();
assert!(!entry.perms.read);
assert!(!entry.perms.write);
assert!(entry.perms.execute);
}
fn grant_cap_fix(capability: &str) -> Fix {
Fix {
layer: CoreLayer::Metadata,
action: FixAction::GrantCap {
path: None,
capability: capability.to_owned(),
},
impact: 5,
description: "grant cap".into(),
}
}
fn chown_fix(path: &str, owner: Option<u32>, group: Option<u32>) -> Fix {
Fix {
layer: CoreLayer::Metadata,
action: FixAction::Chown {
path: PathBuf::from(path),
owner,
group,
},
impact: 4,
description: "chown".into(),
}
}
#[test]
fn grant_cap_fowner_sets_bit_in_subject_capabilities() {
let mut state = StateBuilder::new()
.subject(1000, 1000, vec![])
.with_capabilities(0)
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component_file("/file.txt", 0, 0, 0o600)
.build();
apply_fix(&grant_cap_fix("cap_fowner"), &mut state);
let Probe::Known(caps) = state.subject.capabilities else {
panic!("capabilities should be Known after grant")
};
assert_ne!(caps & CAP_FOWNER, 0, "CAP_FOWNER should be set");
}
#[test]
fn grant_cap_chown_sets_bit_in_subject_capabilities() {
let mut state = StateBuilder::new()
.subject(1000, 1000, vec![])
.with_capabilities(0)
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component_file("/file.txt", 0, 0, 0o600)
.build();
apply_fix(&grant_cap_fix("cap_chown"), &mut state);
let Probe::Known(caps) = state.subject.capabilities else {
panic!("capabilities should be Known")
};
assert_ne!(caps & CAP_CHOWN, 0, "CAP_CHOWN should be set");
}
#[test]
fn grant_cap_sys_admin_sets_bit() {
let mut state = StateBuilder::new()
.subject(1000, 1000, vec![])
.with_capabilities(0)
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component_file("/file.txt", 0, 0, 0o600)
.build();
apply_fix(&grant_cap_fix("cap_sys_admin"), &mut state);
let Probe::Known(caps) = state.subject.capabilities else {
panic!("capabilities should be Known")
};
assert_ne!(caps & CAP_SYS_ADMIN, 0, "CAP_SYS_ADMIN should be set");
}
#[test]
fn grant_cap_unknown_string_is_noop() {
let mut state = StateBuilder::new()
.subject(1000, 1000, vec![])
.with_capabilities(0)
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component_file("/file.txt", 0, 0, 0o600)
.build();
apply_fix(&grant_cap_fix("cap_does_not_exist"), &mut state);
let Probe::Known(caps) = state.subject.capabilities else {
panic!("capabilities should remain Known(0)")
};
assert_eq!(caps, 0, "no caps should be granted for unknown string");
}
#[test]
fn chown_fix_updates_target_uid() {
let mut state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/file.txt", 0, 0, 0o644)
.build();
apply_fix(&chown_fix("/file.txt", Some(1000), None), &mut state);
let comp = state
.walk
.iter()
.find(|c| c.path.as_os_str() == "/file.txt")
.unwrap();
let Probe::Known(stat) = &comp.stat else {
panic!("stat must be known")
};
assert_eq!(stat.uid, 1000, "uid should be updated to 1000");
}
#[test]
fn chown_fix_clears_setuid_bit() {
let mut state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/file.txt", 0, 0, 0o4755) .build();
apply_fix(&chown_fix("/file.txt", Some(1000), None), &mut state);
let comp = state
.walk
.iter()
.find(|c| c.path.as_os_str() == "/file.txt")
.unwrap();
let Probe::Known(stat) = &comp.stat else {
panic!()
};
assert_eq!(
stat.mode & 0o4000,
0,
"S_ISUID should be cleared after chown"
);
}
#[test]
fn chown_fix_clears_setgid_when_group_execute_set() {
let mut state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/file.txt", 0, 0, 0o2755) .build();
apply_fix(&chown_fix("/file.txt", None, Some(500)), &mut state);
let comp = state
.walk
.iter()
.find(|c| c.path.as_os_str() == "/file.txt")
.unwrap();
let Probe::Known(stat) = &comp.stat else {
panic!()
};
assert_eq!(
stat.mode & 0o2000,
0,
"S_ISGID should be cleared when S_IXGRP set"
);
}
#[test]
fn chown_fix_preserves_setgid_when_group_execute_not_set() {
let mut state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/file.txt", 0, 0, 0o2644) .build();
apply_fix(&chown_fix("/file.txt", None, Some(500)), &mut state);
let comp = state
.walk
.iter()
.find(|c| c.path.as_os_str() == "/file.txt")
.unwrap();
let Probe::Known(stat) = &comp.stat else {
panic!()
};
assert_ne!(
stat.mode & 0o2000,
0,
"S_ISGID should be preserved when S_IXGRP not set"
);
}
#[test]
fn chown_fix_updates_gid() {
let mut state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/file.txt", 0, 0, 0o644)
.build();
apply_fix(&chown_fix("/file.txt", None, Some(500)), &mut state);
let comp = state
.walk
.iter()
.find(|c| c.path.as_os_str() == "/file.txt")
.unwrap();
let Probe::Known(stat) = &comp.stat else {
panic!()
};
assert_eq!(stat.gid, 500, "gid should be updated to 500");
}