use std::path::PathBuf;
use super::apply::apply_fix;
use crate::checks::CoreLayer;
use crate::fix::scoring;
use crate::fix::{Fix, FixAction};
use crate::operation::Operation;
use crate::state::acl::AclTag;
use crate::state::fsflags::FsFlags;
use crate::state::Probe;
use crate::test_helpers::StateBuilder;
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(),
}
}
fn chattr_fix(path: &str, flags: &str) -> Fix {
Fix {
layer: CoreLayer::FsFlags,
action: FixAction::Chattr {
path: PathBuf::from(path),
flags: flags.to_owned(),
},
impact: scoring::REMOVE_FS_FLAG,
description: "chattr".into(),
}
}
fn remount_fix(mountpoint: &str, options: &str) -> Fix {
Fix {
layer: CoreLayer::Mount,
action: FixAction::Remount {
mountpoint: PathBuf::from(mountpoint),
options: options.to_owned(),
},
impact: scoring::REMOUNT,
description: "remount".into(),
}
}
#[test]
fn apply_setacl_invalid_entry_string_is_noop() {
let mut state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component_file_with_acl("/file.txt", 0, 0, 0o600, vec![])
.build();
let fix = Fix {
layer: CoreLayer::Dac,
action: FixAction::SetAcl {
path: PathBuf::from("/file.txt"),
entry: "not-a-valid-acl-entry".into(),
},
impact: 1,
description: "invalid".into(),
};
apply_fix(&fix, &mut state);
let Probe::Known(acl) = &state.walk[0].acl else {
panic!()
};
assert!(acl.0.is_empty());
}
#[test]
fn apply_setacl_group_entry() {
let mut state = StateBuilder::new()
.subject(1000, 1000, vec![50])
.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", "g:50: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 group_entry = acl
.0
.iter()
.find(|e| e.tag == AclTag::Group && e.qualifier == Some(50));
assert!(group_entry.is_some());
assert!(group_entry.unwrap().perms.read);
}
#[test]
fn apply_chattr_minus_i_clears_immutable() {
let mut state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Write)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file_with_flags(
"/locked.txt",
1000,
1000,
0o644,
FsFlags {
immutable: true,
append_only: false,
},
)
.build();
let fix = chattr_fix("/locked.txt", "-i");
apply_fix(&fix, &mut state);
let comp = state
.walk
.iter()
.find(|c| c.path.as_os_str() == "/locked.txt")
.unwrap();
let Probe::Known(flags) = &comp.flags else {
panic!("flags should be known")
};
assert!(!flags.immutable, "immutable should be cleared");
}
#[test]
fn apply_chattr_minus_a_clears_append_only() {
let mut state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Write)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file_with_flags(
"/append.txt",
1000,
1000,
0o644,
FsFlags {
immutable: false,
append_only: true,
},
)
.build();
let fix = chattr_fix("/append.txt", "-a");
apply_fix(&fix, &mut state);
let comp = state
.walk
.iter()
.find(|c| c.path.as_os_str() == "/append.txt")
.unwrap();
let Probe::Known(flags) = &comp.flags else {
panic!()
};
assert!(!flags.append_only, "append_only should be cleared");
}
#[test]
fn apply_chattr_unknown_flag_is_noop() {
let mut state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Write)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file_with_flags(
"/file.txt",
1000,
1000,
0o644,
FsFlags {
immutable: true,
append_only: true,
},
)
.build();
let fix = chattr_fix("/file.txt", "-z");
apply_fix(&fix, &mut state);
let comp = state
.walk
.iter()
.find(|c| c.path.as_os_str() == "/file.txt")
.unwrap();
let Probe::Known(flags) = &comp.flags else {
panic!()
};
assert!(flags.immutable);
assert!(flags.append_only);
}
#[test]
fn apply_chattr_on_unknown_flags_is_noop() {
let mut state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Write)
.mount("/", "ext4", "rw")
.component_unknown("/mystery.txt")
.build();
let fix = chattr_fix("/mystery.txt", "-i");
apply_fix(&fix, &mut state);
assert!(matches!(state.walk[0].flags, Probe::Unknown));
}
#[test]
fn apply_remount_rw_clears_read_only() {
let mut state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Write)
.mount("/mnt", "ext4", "ro")
.component("/mnt", 0, 0, 0o755)
.component_file("/mnt/file.txt", 1000, 1000, 0o644)
.build();
let fix = remount_fix("/mnt", "rw");
apply_fix(&fix, &mut state);
let mount = state
.mounts
.0
.iter()
.find(|m| m.mountpoint.as_os_str() == "/mnt")
.unwrap();
assert!(
!mount.options.read_only,
"read_only should be cleared after remount rw"
);
}
#[test]
fn apply_remount_exec_clears_noexec() {
let mut state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Execute)
.mount("/mnt", "tmpfs", "noexec")
.component("/mnt", 0, 0, 0o755)
.component_file("/mnt/script.sh", 1000, 1000, 0o755)
.build();
let fix = remount_fix("/mnt", "exec");
apply_fix(&fix, &mut state);
let mount = state
.mounts
.0
.iter()
.find(|m| m.mountpoint.as_os_str() == "/mnt")
.unwrap();
assert!(
!mount.options.noexec,
"noexec should be cleared after remount exec"
);
}
#[test]
fn apply_remount_unknown_option_is_noop() {
let mut state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Write)
.mount("/mnt", "ext4", "ro")
.component("/mnt", 0, 0, 0o755)
.component_file("/mnt/file.txt", 1000, 1000, 0o644)
.build();
let fix = remount_fix("/mnt", "unknown_option");
apply_fix(&fix, &mut state);
let mount = state
.mounts
.0
.iter()
.find(|m| m.mountpoint.as_os_str() == "/mnt")
.unwrap();
assert!(mount.options.read_only);
}
#[test]
fn apply_remount_nonexistent_mountpoint_is_noop() {
let mut state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Write)
.mount("/mnt", "ext4", "ro")
.component("/mnt", 0, 0, 0o755)
.component_file("/mnt/file.txt", 1000, 1000, 0o644)
.build();
let fix = remount_fix("/does-not-exist", "rw");
apply_fix(&fix, &mut state);
let mount = state
.mounts
.0
.iter()
.find(|m| m.mountpoint.as_os_str() == "/mnt")
.unwrap();
assert!(mount.options.read_only);
}
#[test]
fn apply_chown_updates_uid() {
let mut state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component_file("/file.txt", 0, 0, 0o600)
.build();
let fix = Fix {
layer: CoreLayer::Dac,
action: FixAction::Chown {
path: PathBuf::from("/file.txt"),
owner: Some(1000),
group: None,
},
impact: 2,
description: "chown".into(),
};
apply_fix(&fix, &mut state);
let Probe::Known(stat) = &state.walk[0].stat else {
panic!()
};
assert_eq!(stat.uid, 1000, "uid should be updated to 1000");
}