use std::path::PathBuf;
use super::*;
use crate::checks::CoreLayer;
use crate::fix::scoring;
use crate::fix::{Fix, FixAction};
use crate::operation::Operation;
use crate::test_helpers::StateBuilder;
#[test]
fn single_fix_passes_through_unchanged() {
let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Write)
.mount("/", "ext4", "ro")
.component("/", 0, 0, 0o755)
.component_file("/file.txt", 1000, 1000, 0o644)
.build();
let fixes = vec![Fix {
layer: CoreLayer::Mount,
action: FixAction::Remount {
mountpoint: PathBuf::from("/"),
options: "rw".into(),
},
impact: scoring::REMOUNT,
description: "remount / as read-write".into(),
}];
let plan = simulate_cascade(fixes, vec![], &state);
assert_eq!(plan.fixes.len(), 1);
assert_eq!(plan.fixes[0].layer, CoreLayer::Mount);
}
#[test]
fn independent_layers_not_pruned() {
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 fixes = vec![
Fix {
layer: CoreLayer::Mount,
action: FixAction::Remount {
mountpoint: PathBuf::from("/mnt"),
options: "rw".into(),
},
impact: scoring::REMOUNT,
description: "remount /mnt rw".into(),
},
Fix {
layer: CoreLayer::Dac,
action: FixAction::SetAcl {
path: PathBuf::from("/mnt/secret.txt"),
entry: "u:2000:w".into(),
},
impact: scoring::ACL_USER_GRANT,
description: "grant write via ACL".into(),
},
];
let plan = simulate_cascade(fixes, vec![], &state);
assert_eq!(plan.fixes.len(), 2);
assert!(plan.fixes.iter().any(|f| f.layer == CoreLayer::Mount));
assert!(plan.fixes.iter().any(|f| f.layer == CoreLayer::Dac));
}
#[test]
fn empty_fixes_returns_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 plan = simulate_cascade(vec![], vec![], &state);
assert!(plan.fixes.is_empty());
assert!(plan.warnings.is_empty());
}
#[test]
fn same_layer_alternatives_preserved() {
let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o711)
.component("/var", 0, 0, 0o700)
.component_file("/var/file.txt", 0, 0, 0o644)
.build();
let fixes = vec![
Fix {
layer: CoreLayer::Traversal,
action: FixAction::SetAcl {
path: PathBuf::from("/var"),
entry: "u:1000:--x".into(),
},
impact: scoring::ACL_USER_GRANT,
description: "ACL execute on /var".into(),
},
Fix {
layer: CoreLayer::Traversal,
action: FixAction::Chmod {
path: PathBuf::from("/var"),
mode_change: "o+x".into(),
},
impact: scoring::CHMOD_OTHER,
description: "chmod o+x /var".into(),
},
];
let plan = simulate_cascade(fixes, vec![], &state);
assert_eq!(plan.fixes.len(), 2);
assert_eq!(plan.fixes[0].impact, scoring::ACL_USER_GRANT);
assert_eq!(plan.fixes[1].impact, scoring::CHMOD_OTHER);
}
#[test]
fn warnings_preserved_through_cascade() {
let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Write)
.mount("/", "ext4", "ro")
.component("/", 0, 0, 0o755)
.component_file("/file.txt", 1000, 1000, 0o644)
.build();
let fixes = vec![Fix {
layer: CoreLayer::Mount,
action: FixAction::Remount {
mountpoint: PathBuf::from("/"),
options: "rw".into(),
},
impact: scoring::REMOUNT,
description: "remount / rw".into(),
}];
let warnings = vec!["existing warning".to_string()];
let plan = simulate_cascade(fixes, warnings, &state);
assert!(plan.warnings.contains(&"existing warning".to_string()));
}
#[test]
fn max_iterations_bounded() {
let state = StateBuilder::new()
.subject(2000, 2000, vec![])
.operation(Operation::Write)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/data.txt", 0, 0, 0o600)
.build();
let fixes = vec![Fix {
layer: CoreLayer::Dac,
action: FixAction::SetAcl {
path: PathBuf::from("/data.txt"),
entry: "u:2000:w".into(),
},
impact: scoring::ACL_USER_GRANT,
description: "grant write".into(),
}];
let plan = simulate_cascade(fixes, vec![], &state);
assert_eq!(plan.fixes.len(), 1);
}
#[test]
fn duplicate_plan_hash_triggers_cycle_warning() {
let state = StateBuilder::new()
.subject(2000, 2000, vec![])
.operation(Operation::Write)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/data.txt", 0, 0, 0o600)
.build();
let fixes = vec![
Fix {
layer: CoreLayer::Mount,
action: FixAction::Remount {
mountpoint: PathBuf::from("/does-not-exist"),
options: "rw".into(),
},
impact: scoring::REMOUNT,
description: "remount nonexistent".into(),
},
Fix {
layer: CoreLayer::Dac,
action: FixAction::SetAcl {
path: PathBuf::from("/data.txt"),
entry: "u:2000:w".into(),
},
impact: scoring::ACL_USER_GRANT,
description: "grant write via ACL".into(),
},
];
let plan = simulate_cascade(fixes, vec![], &state);
assert!(!plan.fixes.is_empty());
assert!(
plan.warnings.iter().any(|w| w.contains("cycle")),
"expected cycle warning when fix has no effect on failure set; warnings: {:?}",
plan.warnings,
);
}
#[test]
fn multi_layer_fix_does_not_trigger_spurious_cycle_warning() {
let state = StateBuilder::new()
.subject(2000, 2000, vec![])
.operation(Operation::Write)
.mount("/", "ext4", "ro")
.component("/", 0, 0, 0o755)
.component_file("/data.txt", 0, 0, 0o600)
.build();
let fixes = vec![
Fix {
layer: CoreLayer::Mount,
action: FixAction::Remount {
mountpoint: std::path::PathBuf::from("/"),
options: "rw".into(),
},
impact: scoring::REMOUNT,
description: "remount / rw".into(),
},
Fix {
layer: CoreLayer::Dac,
action: FixAction::SetAcl {
path: std::path::PathBuf::from("/data.txt"),
entry: "u:2000:w".into(),
},
impact: scoring::ACL_USER_GRANT,
description: "grant write via ACL".into(),
},
];
let plan = simulate_cascade(fixes, vec![], &state);
let cycle_warning = "fix cascade cycle detected \u{2014} manual review required";
assert!(
!plan.warnings.iter().any(|w| w.contains("cycle")),
"spurious cycle warning emitted; warnings: {:?}; cycle_warning: {cycle_warning}",
plan.warnings,
);
}
#[test]
fn hash_fix_plan_deterministic() {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let fixes = vec![Fix {
layer: CoreLayer::Dac,
action: FixAction::Chmod {
path: PathBuf::from("/tmp/test"),
mode_change: "o+r".into(),
},
impact: scoring::CHMOD_OTHER,
description: "chmod o+r /tmp/test".into(),
}];
let hash1 = {
let mut h = DefaultHasher::new();
fixes.hash(&mut h);
h.finish()
};
let hash2 = {
let mut h = DefaultHasher::new();
fixes.hash(&mut h);
h.finish()
};
assert_eq!(hash1, hash2, "same fixes must produce same hash");
}