use super::*;
use crate::checks::caps::{CAP_CHOWN, CAP_FOWNER, CAP_FSETID, CAP_SYS_ADMIN};
use crate::operation::{MetadataParams, Operation, XattrNamespace};
use crate::test_helpers::StateBuilder;
#[test]
fn chmod_as_owner_passes() {
let params = MetadataParams::default();
let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Chmod)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/file.txt", 1000, 1000, 0o644)
.build();
let result = check_metadata(&state, ¶ms);
assert!(result.is_pass(), "expected Pass, got {result:?}");
}
#[test]
fn chmod_as_non_owner_no_cap_fowner_fails() {
let params = MetadataParams::default();
let state = StateBuilder::new()
.subject(2000, 2000, vec![])
.operation(Operation::Chmod)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/file.txt", 1000, 1000, 0o644)
.build();
let result = check_metadata(&state, ¶ms);
assert!(result.is_fail(), "expected Fail, got {result:?}");
if let LayerResult::Fail { detail, .. } = &result {
assert!(
detail.contains("subject"),
"detail must mention subject: {detail}"
);
assert!(
detail.contains("CAP_FOWNER"),
"detail must mention CAP_FOWNER: {detail}"
);
}
}
#[test]
fn chmod_as_non_owner_with_cap_fowner_passes() {
let params = MetadataParams::default();
let state = StateBuilder::new()
.subject(2000, 2000, vec![])
.with_capabilities(CAP_FOWNER)
.operation(Operation::Chmod)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/file.txt", 1000, 1000, 0o644)
.build();
let result = check_metadata(&state, ¶ms);
assert!(result.is_pass(), "expected Pass, got {result:?}");
}
#[test]
fn chmod_with_cap_fowner_warns_about_setgid_strip() {
let params = MetadataParams::default();
let state = StateBuilder::new()
.subject(2000, 2000, vec![])
.with_capabilities(CAP_FOWNER)
.operation(Operation::Chmod)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/file.txt", 1000, 1000, 0o644)
.build();
let result = check_metadata(&state, ¶ms);
assert!(result.is_pass(), "expected Pass, got {result:?}");
if let LayerResult::Pass { warnings, .. } = &result {
assert!(
!warnings.is_empty(),
"expected non-empty warnings for CAP_FOWNER without CAP_FSETID"
);
}
}
#[test]
fn chown_uid_with_cap_chown_passes() {
let params = MetadataParams {
new_uid: Some(2000),
..MetadataParams::default()
};
let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.with_capabilities(CAP_CHOWN)
.operation(Operation::ChownUid)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/file.txt", 1000, 1000, 0o644)
.build();
let result = check_metadata(&state, ¶ms);
assert!(result.is_pass(), "expected Pass, got {result:?}");
}
#[test]
fn chown_uid_without_cap_chown_fails() {
let params = MetadataParams {
new_uid: Some(2000),
..MetadataParams::default()
};
let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::ChownUid)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/file.txt", 1000, 1000, 0o644)
.build();
let result = check_metadata(&state, ¶ms);
assert!(result.is_fail(), "expected Fail, got {result:?}");
if let LayerResult::Fail { detail, .. } = &result {
assert!(
detail.contains("CAP_CHOWN"),
"detail must mention CAP_CHOWN: {detail}"
);
}
}
#[test]
fn chown_gid_missing_new_gid_is_degraded() {
let params = MetadataParams::default(); let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::ChownGid)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/file.txt", 1000, 1000, 0o644)
.build();
let result = check_metadata(&state, ¶ms);
assert!(result.is_degraded(), "expected Degraded, got {result:?}");
}
#[test]
fn chown_gid_with_cap_chown_passes() {
let params = MetadataParams {
new_gid: Some(2000),
..MetadataParams::default()
};
let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.with_capabilities(CAP_CHOWN)
.operation(Operation::ChownGid)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/file.txt", 1000, 1000, 0o644)
.build();
let result = check_metadata(&state, ¶ms);
assert!(result.is_pass(), "expected Pass, got {result:?}");
}
#[test]
fn chown_gid_owner_in_target_group_passes() {
let new_gid = 500u32;
let params = MetadataParams {
new_gid: Some(new_gid),
..MetadataParams::default()
};
let state = StateBuilder::new()
.subject(1000, 1000, vec![new_gid])
.operation(Operation::ChownGid)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/file.txt", 1000, 1000, 0o644)
.build();
let result = check_metadata(&state, ¶ms);
assert!(result.is_pass(), "expected Pass, got {result:?}");
}
#[test]
fn chown_gid_non_owner_fails_with_ownership_message() {
let params = MetadataParams {
new_gid: Some(500),
..MetadataParams::default()
};
let state = StateBuilder::new()
.subject(2000, 2000, vec![500]) .operation(Operation::ChownGid)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/file.txt", 1000, 1000, 0o644)
.build();
let result = check_metadata(&state, ¶ms);
assert!(result.is_fail(), "expected Fail, got {result:?}");
if let LayerResult::Fail { detail, .. } = &result {
assert!(
detail.contains("is not the file owner"),
"detail must mention 'is not the file owner': {detail}"
);
assert!(
detail.contains("CAP_CHOWN"),
"detail must mention CAP_CHOWN: {detail}"
);
}
}
#[test]
fn chown_gid_owner_not_in_group_fails_with_membership_message() {
let params = MetadataParams {
new_gid: Some(500),
..MetadataParams::default()
};
let state = StateBuilder::new()
.subject(1000, 1000, vec![]) .operation(Operation::ChownGid)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/file.txt", 1000, 1000, 0o644)
.build();
let result = check_metadata(&state, ¶ms);
assert!(result.is_fail(), "expected Fail, got {result:?}");
if let LayerResult::Fail { detail, .. } = &result {
assert!(
detail.contains("not a member of group"),
"detail must mention 'not a member of group': {detail}"
);
assert!(
detail.contains("CAP_CHOWN"),
"detail must mention CAP_CHOWN: {detail}"
);
}
}
#[test]
fn setxattr_user_as_owner_passes() {
let params = MetadataParams::default();
let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::SetXattr {
namespace: XattrNamespace::User,
})
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/file.txt", 1000, 1000, 0o644)
.build();
let result = check_metadata(&state, ¶ms);
assert!(result.is_pass(), "expected Pass, got {result:?}");
}
#[test]
fn setxattr_user_as_non_owner_fails() {
let params = MetadataParams::default();
let state = StateBuilder::new()
.subject(2000, 2000, vec![])
.operation(Operation::SetXattr {
namespace: XattrNamespace::User,
})
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/file.txt", 1000, 1000, 0o644)
.build();
let result = check_metadata(&state, ¶ms);
assert!(result.is_fail(), "expected Fail, got {result:?}");
}
#[test]
fn setxattr_trusted_without_cap_sys_admin_fails() {
let params = MetadataParams::default();
let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::SetXattr {
namespace: XattrNamespace::Trusted,
})
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/file.txt", 1000, 1000, 0o644)
.build();
let result = check_metadata(&state, ¶ms);
assert!(result.is_fail(), "expected Fail, got {result:?}");
}
#[test]
fn setxattr_trusted_with_cap_sys_admin_passes() {
let params = MetadataParams::default();
let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.with_capabilities(CAP_SYS_ADMIN)
.operation(Operation::SetXattr {
namespace: XattrNamespace::Trusted,
})
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/file.txt", 1000, 1000, 0o644)
.build();
let result = check_metadata(&state, ¶ms);
assert!(result.is_pass(), "expected Pass, got {result:?}");
}
#[test]
fn setxattr_security_without_cap_sys_admin_fails() {
let params = MetadataParams::default();
let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::SetXattr {
namespace: XattrNamespace::Security,
})
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/file.txt", 1000, 1000, 0o644)
.build();
let result = check_metadata(&state, ¶ms);
assert!(result.is_fail(), "expected Fail, got {result:?}");
}
#[test]
fn setxattr_security_with_cap_sys_admin_passes() {
let params = MetadataParams::default();
let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.with_capabilities(CAP_SYS_ADMIN)
.operation(Operation::SetXattr {
namespace: XattrNamespace::Security,
})
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/file.txt", 1000, 1000, 0o644)
.build();
let result = check_metadata(&state, ¶ms);
assert!(result.is_pass(), "expected Pass, got {result:?}");
}
#[test]
fn setxattr_posix_acl_as_owner_passes() {
let params = MetadataParams::default();
let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::SetXattr {
namespace: XattrNamespace::SystemPosixAcl,
})
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/file.txt", 1000, 1000, 0o644)
.build();
let result = check_metadata(&state, ¶ms);
assert!(result.is_pass(), "expected Pass, got {result:?}");
}
#[test]
fn setxattr_posix_acl_non_owner_without_fowner_fails() {
let params = MetadataParams::default();
let state = StateBuilder::new()
.subject(2000, 2000, vec![])
.operation(Operation::SetXattr {
namespace: XattrNamespace::SystemPosixAcl,
})
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/file.txt", 1000, 1000, 0o644)
.build();
let result = check_metadata(&state, ¶ms);
assert!(result.is_fail(), "expected Fail, got {result:?}");
}
#[test]
fn chmod_on_empty_walk_returns_degraded() {
use crate::state::mac::MacState;
use crate::state::mount::MountTable;
use crate::state::subject::ResolvedSubject;
use crate::state::{Probe, SystemState};
let params = MetadataParams::default();
let state = SystemState {
subject: ResolvedSubject {
uid: 1000,
gid: 1000,
groups: vec![],
capabilities: Probe::Unknown,
},
walk: vec![],
mounts: MountTable(vec![]),
operation: Operation::Chmod,
mac_state: MacState::default(),
};
let result = check_metadata(&state, ¶ms);
assert!(
result.is_degraded(),
"expected Degraded for Chmod on empty walk, got {result:?}"
);
}
#[test]
fn non_metadata_op_returns_pass_skip() {
let params = MetadataParams::default();
let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/file.txt", 1000, 1000, 0o644)
.build();
let result = check_metadata(&state, ¶ms);
assert!(result.is_pass(), "expected Pass, got {result:?}");
if let LayerResult::Pass { detail, .. } = &result {
assert!(
detail.contains("not a metadata operation"),
"detail must mention 'not a metadata operation': {detail}"
);
}
}
const _: u64 = CAP_FSETID;