use super::*;
use crate::checks::caps::CAP_DAC_OVERRIDE;
use crate::operation::{MetadataParams, Operation};
use crate::test_helpers::StateBuilder;
#[cfg(not(feature = "selinux"))]
#[test]
fn run_checks_includes_selinux_degraded_when_feature_absent() {
let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/tmp/test", 1000, 1000, 0o644)
.build();
let report = run_checks(&state, &MetadataParams::default());
assert_eq!(report.mac_results.len(), 2);
assert!(
matches!(
&report.mac_results[0],
(MacLayer::SeLinux, LayerResult::Degraded { .. })
),
"expected (SeLinux, Degraded), got {:?}",
report.mac_results[0]
);
if let LayerResult::Degraded { reason } = &report.mac_results[0].1 {
assert!(
reason.to_ascii_lowercase().contains("selinux"),
"reason should mention SELinux: {reason}"
);
}
}
#[test]
fn run_checks_mac_results_always_has_both_entries() {
let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/tmp/test", 1000, 1000, 0o644)
.build();
let report = run_checks(&state, &MetadataParams::default());
assert_eq!(report.mac_results.len(), 2);
assert!(matches!(report.mac_results[0].0, MacLayer::SeLinux));
assert!(matches!(report.mac_results[1].0, MacLayer::AppArmor));
}
#[cfg(not(feature = "apparmor"))]
#[test]
fn run_checks_includes_apparmor_degraded_when_feature_absent() {
let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/tmp/test", 1000, 1000, 0o644)
.build();
let report = run_checks(&state, &MetadataParams::default());
assert!(
matches!(
&report.mac_results[1],
(MacLayer::AppArmor, LayerResult::Degraded { .. })
),
"expected (AppArmor, Degraded), got {:?}",
report.mac_results[1]
);
if let LayerResult::Degraded { reason } = &report.mac_results[1].1 {
assert!(
reason.to_ascii_lowercase().contains("apparmor"),
"reason should mention AppArmor: {reason}"
);
}
}
#[test]
fn ro_mount_causes_write_fail_in_pipeline() {
let state = StateBuilder::new()
.operation(Operation::Write)
.mount("/var", "ext4", "ro")
.component("/var", 0, 0, 0o755)
.component_file("/var/data.txt", 0, 0, 0o644)
.build();
let report = run_checks(&state, &MetadataParams::default());
assert!(report.core_results[CoreLayer::Mount].is_fail());
assert!(!report.is_allowed());
assert!(report.failed_layers().contains(&CoreLayer::Mount));
}
#[test]
fn rw_mount_all_layers_pass_for_root() {
let state = StateBuilder::new()
.subject(0, 0, vec![])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/file.txt", 0, 0, 0o644)
.build();
let report = run_checks(&state, &MetadataParams::default());
assert!(report.core_results[CoreLayer::Mount].is_pass());
assert!(report.core_results[CoreLayer::Traversal].is_pass());
assert!(report.core_results[CoreLayer::Dac].is_pass());
assert!(report.is_allowed());
assert!(report.failed_layers().is_empty());
}
#[test]
fn failed_layers_returns_empty_when_all_pass_or_degraded() {
let state = StateBuilder::new()
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.build();
let report = run_checks(&state, &MetadataParams::default());
assert!(report.failed_layers().is_empty());
}
#[test]
fn is_allowed_false_when_any_layer_fails() {
let core_results = EnumMap::from_array([
LayerResult::Pass {
detail: String::new(),
warnings: vec![],
},
LayerResult::Fail {
detail: "blocked".into(),
component_index: Some(0),
},
LayerResult::Degraded {
reason: "stub".into(),
},
LayerResult::Degraded {
reason: "stub".into(),
},
LayerResult::Degraded {
reason: "stub".into(),
},
LayerResult::Degraded {
reason: "stub".into(),
},
]);
let report = CheckReport {
core_results,
mac_results: vec![],
};
assert!(!report.is_allowed());
assert_eq!(report.failed_layers(), vec![CoreLayer::FsFlags]);
}
#[test]
fn ro_mount_and_restrictive_mode_both_fail() {
let state = StateBuilder::new()
.subject(2000, 2000, vec![])
.operation(Operation::Write)
.mount("/mnt", "ext4", "ro")
.component("/mnt", 0, 0, 0o755)
.component_file("/mnt/secret.txt", 0, 0, 0o600)
.build();
let report = run_checks(&state, &MetadataParams::default());
assert!(report.core_results[CoreLayer::Mount].is_fail());
assert!(report.core_results[CoreLayer::Dac].is_fail());
let failed = report.failed_layers();
assert!(failed.contains(&CoreLayer::Mount));
assert!(failed.contains(&CoreLayer::Dac));
}
#[test]
fn all_pass_for_owner_on_rw_mount() {
let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component("/home", 0, 0, 0o755)
.component_file("/home/file.txt", 1000, 1000, 0o644)
.build();
let report = run_checks(&state, &MetadataParams::default());
assert!(report.core_results[CoreLayer::Mount].is_pass());
assert!(report.core_results[CoreLayer::Traversal].is_pass());
assert!(report.core_results[CoreLayer::Dac].is_pass());
assert!(report.is_allowed());
}
#[test]
fn root_bypasses_dac_but_not_ro_mount() {
let state = StateBuilder::new()
.subject(0, 0, vec![])
.operation(Operation::Write)
.mount("/mnt", "ext4", "ro")
.component("/mnt", 0, 0, 0o755)
.component_file("/mnt/data.txt", 1000, 1000, 0o000)
.build();
let report = run_checks(&state, &MetadataParams::default());
assert!(report.core_results[CoreLayer::Mount].is_fail());
assert!(report.core_results[CoreLayer::Dac].is_pass());
if let LayerResult::Pass { detail, .. } = &report.core_results[CoreLayer::Dac] {
assert!(detail.contains("CAP_DAC_OVERRIDE"));
}
}
#[test]
fn nonroot_with_known_cap_dac_override_bypasses_dac_in_pipeline() {
let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.with_capabilities(CAP_DAC_OVERRIDE)
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/secret.txt", 0, 0, 0o000)
.build();
let report = run_checks(&state, &MetadataParams::default());
assert!(report.core_results[CoreLayer::Dac].is_pass());
if let LayerResult::Pass { detail, .. } = &report.core_results[CoreLayer::Dac] {
assert!(
detail.contains("CAP_DAC_OVERRIDE"),
"expected CAP_DAC_OVERRIDE in detail: {detail}"
);
}
assert!(report.is_allowed());
}
#[test]
fn core_layer_metadata_variant_exists() {
let layer = CoreLayer::Metadata;
assert!(matches!(layer, CoreLayer::Metadata));
}
#[test]
fn run_checks_chmod_produces_metadata_layer_result() {
use crate::operation::MetadataParams;
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 report = run_checks(&state, ¶ms);
let result = &report.core_results[CoreLayer::Metadata];
if let LayerResult::Degraded { reason } = result {
assert!(
!reason.contains("not a metadata operation"),
"Chmod should not produce a passthrough Degraded: {reason}"
);
}
}
#[test]
fn run_checks_read_metadata_layer_is_pass_skip() {
use crate::operation::MetadataParams;
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 report = run_checks(&state, ¶ms);
let result = &report.core_results[CoreLayer::Metadata];
if let LayerResult::Pass { detail, .. } = result {
assert!(
detail.contains("not a metadata operation"),
"Read op metadata layer detail should contain 'not a metadata operation': {detail}"
);
} else {
panic!("expected Pass for Read op metadata layer, got {result:?}");
}
}
#[test]
fn run_checks_chown_uid_no_cap_metadata_layer_fails() {
use crate::operation::MetadataParams;
let params = MetadataParams::default();
let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::ChownUid)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/file.txt", 0, 0, 0o644)
.build();
let report = run_checks(&state, ¶ms);
assert!(
report.core_results[CoreLayer::Metadata].is_fail(),
"expected Metadata layer Fail for ChownUid without CAP_CHOWN, got {:?}",
report.core_results[CoreLayer::Metadata]
);
}