use super::*;
use crate::checks::{run_checks, CoreLayer};
use crate::operation::{MetadataParams, Operation};
use crate::state::fsflags::FsFlags;
use crate::test_helpers::StateBuilder;
#[test]
fn all_pass_report_yields_empty_plan() {
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 = crate::checks::run_checks(&state, &MetadataParams::default());
assert!(report.is_allowed());
let plan = generate_fixes(&report, &state, &MetadataParams::default());
assert!(plan.fixes.is_empty());
assert!(plan.warnings.is_empty());
}
#[test]
fn ro_mount_generates_remount_fix_with_warning() {
let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Write)
.mount("/mnt", "ext4", "ro")
.component("/mnt", 0, 0, 0o755)
.component_file("/mnt/data.txt", 1000, 1000, 0o644)
.build();
let report = crate::checks::run_checks(&state, &MetadataParams::default());
let plan = generate_fixes(&report, &state, &MetadataParams::default());
let mount_fixes: Vec<_> = plan
.fixes
.iter()
.filter(|f| f.layer == CoreLayer::Mount)
.collect();
assert_eq!(mount_fixes.len(), 1);
assert_eq!(mount_fixes[0].impact, scoring::REMOUNT);
assert!(!plan.warnings.is_empty());
}
#[test]
fn immutable_file_generates_chattr_fix_with_warning() {
let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Write)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file_with_flags(
"/data.txt",
1000,
1000,
0o644,
FsFlags {
immutable: true,
append_only: false,
},
)
.build();
let report = crate::checks::run_checks(&state, &MetadataParams::default());
let plan = generate_fixes(&report, &state, &MetadataParams::default());
let flag_fixes: Vec<_> = plan
.fixes
.iter()
.filter(|f| f.layer == CoreLayer::FsFlags)
.collect();
assert_eq!(flag_fixes.len(), 1);
assert_eq!(flag_fixes[0].impact, scoring::REMOVE_FS_FLAG);
assert!(plan
.warnings
.iter()
.any(|w| w.contains("removes filesystem protection")));
}
#[test]
fn traversal_fail_generates_acl_and_chmod_fixes() {
let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component("/var", 0, 0, 0o700)
.component_file("/var/file.txt", 0, 0, 0o644)
.build();
let report = crate::checks::run_checks(&state, &MetadataParams::default());
assert!(report.core_results[CoreLayer::Traversal].is_fail());
let plan = generate_fixes(&report, &state, &MetadataParams::default());
let trav_fixes: Vec<_> = plan
.fixes
.iter()
.filter(|f| f.layer == CoreLayer::Traversal)
.collect();
assert!(trav_fixes.len() >= 2);
assert!(trav_fixes[0].impact < trav_fixes[1].impact);
assert_eq!(trav_fixes[0].impact, scoring::ACL_USER_GRANT);
assert_eq!(trav_fixes[1].impact, scoring::CHMOD_OTHER);
}
#[test]
fn dac_fail_generates_multiple_ranked_fixes() {
let state = StateBuilder::new()
.subject(1000, 100, vec![])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/secret.txt", 0, 100, 0o600)
.build();
let report = crate::checks::run_checks(&state, &MetadataParams::default());
assert!(report.core_results[CoreLayer::Dac].is_fail());
let plan = generate_fixes(&report, &state, &MetadataParams::default());
let dac_fixes: Vec<_> = plan
.fixes
.iter()
.filter(|f| f.layer == CoreLayer::Dac)
.collect();
assert_eq!(dac_fixes.len(), 3);
assert_eq!(dac_fixes[0].impact, scoring::ACL_USER_GRANT);
assert_eq!(dac_fixes[1].impact, scoring::CHMOD_GROUP);
assert_eq!(dac_fixes[2].impact, scoring::CHMOD_OTHER);
}
#[test]
fn dac_fail_no_group_fix_when_not_in_group() {
let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/secret.txt", 0, 0, 0o600)
.build();
let report = crate::checks::run_checks(&state, &MetadataParams::default());
assert!(report.core_results[CoreLayer::Dac].is_fail());
let plan = generate_fixes(&report, &state, &MetadataParams::default());
let dac_fixes: Vec<_> = plan
.fixes
.iter()
.filter(|f| f.layer == CoreLayer::Dac)
.collect();
assert_eq!(dac_fixes.len(), 2);
assert_eq!(dac_fixes[0].impact, scoring::ACL_USER_GRANT);
assert_eq!(dac_fixes[1].impact, scoring::CHMOD_OTHER);
}
#[test]
fn grant_cap_variant_constructable_without_path() {
let action = FixAction::GrantCap {
path: None,
capability: "cap_fowner".into(),
};
assert!(matches!(action, FixAction::GrantCap { .. }));
}
#[test]
fn grant_cap_variant_constructable_with_path() {
let action = FixAction::GrantCap {
path: Some(std::path::PathBuf::from("/usr/bin/myapp")),
capability: "cap_chown".into(),
};
assert!(matches!(action, FixAction::GrantCap { .. }));
}
#[test]
fn collect_mount_fixes_warns_only_for_its_own_new_fixes() {
let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Write)
.mount("/mnt", "ext4", "ro")
.component("/mnt", 0, 0, 0o755)
.component_file_with_flags(
"/mnt/data.txt",
1000,
1000,
0o644,
FsFlags {
immutable: true,
append_only: false,
},
)
.build();
let report = run_checks(&state, &MetadataParams::default());
assert!(report.core_results[CoreLayer::Mount].is_fail());
assert!(report.core_results[CoreLayer::FsFlags].is_fail());
let mut fixes: Vec<Fix> = Vec::new();
let mut warnings: Vec<String> = Vec::new();
collect_fsflags_fixes(&report, &state, &mut fixes, &mut warnings);
let fsflags_warning_count = warnings.len();
collect_mount_fixes(&report, &state, &mut fixes, &mut warnings);
let mount_warnings: Vec<_> = warnings[fsflags_warning_count..].to_vec();
assert_eq!(
mount_warnings.len(),
1,
"collect_mount_fixes must produce exactly one warning, got {mount_warnings:?}"
);
assert!(
mount_warnings[0].contains("affects entire filesystem"),
"mount warning text must contain 'affects entire filesystem': {mount_warnings:?}"
);
}
#[test]
fn collect_fsflags_fixes_warns_only_for_its_own_new_fixes() {
let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Write)
.mount("/mnt", "ext4", "ro")
.component("/mnt", 0, 0, 0o755)
.component_file_with_flags(
"/mnt/data.txt",
1000,
1000,
0o644,
FsFlags {
immutable: true,
append_only: false,
},
)
.build();
let report = run_checks(&state, &MetadataParams::default());
assert!(report.core_results[CoreLayer::Mount].is_fail());
assert!(report.core_results[CoreLayer::FsFlags].is_fail());
let mut fixes: Vec<Fix> = Vec::new();
let mut warnings: Vec<String> = Vec::new();
collect_mount_fixes(&report, &state, &mut fixes, &mut warnings);
let mount_warning_count = warnings.len();
collect_fsflags_fixes(&report, &state, &mut fixes, &mut warnings);
let fsflags_warnings: Vec<_> = warnings[mount_warning_count..].to_vec();
assert_eq!(
fsflags_warnings.len(),
1,
"collect_fsflags_fixes must produce exactly one warning, got {fsflags_warnings:?}"
);
assert!(
fsflags_warnings[0].contains("removes filesystem protection"),
"fsflags warning text must contain 'removes filesystem protection': {fsflags_warnings:?}"
);
}
#[test]
fn root_generates_no_dac_fixes() {
let state = StateBuilder::new()
.subject(0, 0, vec![])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/file.txt", 1000, 1000, 0o000)
.build();
let report = crate::checks::run_checks(&state, &MetadataParams::default());
assert!(report.core_results[CoreLayer::Dac].is_pass());
let plan = generate_fixes(&report, &state, &MetadataParams::default());
let dac_fixes: Vec<_> = plan
.fixes
.iter()
.filter(|f| f.layer == CoreLayer::Dac)
.collect();
assert!(dac_fixes.is_empty());
}