use anyhow::Result;
use serial_test::serial;
use tempfile::TempDir;
use xchecker::OrchestratorHandle;
use xchecker::artifact::{Artifact, ArtifactType};
use xchecker::fixup::{DiffHunk, FixupMode, FixupParser, UnifiedDiff};
#[allow(clippy::duplicate_mod)]
#[path = "test_support/mod.rs"]
mod test_support;
struct SecurityTestEnvironment {
#[allow(dead_code)]
_cwd_guard: test_support::CwdGuard,
_temp_dir: TempDir,
handle: OrchestratorHandle,
}
impl SecurityTestEnvironment {
fn new(test_name: &str) -> Result<Self> {
let temp_dir = TempDir::new()?;
let cwd_guard = test_support::CwdGuard::new(temp_dir.path())?;
std::fs::create_dir_all(temp_dir.path().join(".xchecker/specs"))?;
let spec_id = format!("security-{test_name}");
let handle = OrchestratorHandle::new(&spec_id)?;
Ok(Self {
_cwd_guard: cwd_guard,
_temp_dir: temp_dir,
handle,
})
}
}
#[test]
#[serial]
fn test_artifact_path_traversal_rejection() -> Result<()> {
let env = SecurityTestEnvironment::new("artifact-traversal")?;
let manager = env.handle.artifact_manager();
let malicious_artifact = Artifact::new(
"../evil.md".to_string(),
"malicious content".to_string(),
ArtifactType::Markdown,
);
let result = manager.store_artifact(&malicious_artifact);
assert!(result.is_err(), "Should reject parent directory traversal");
let err = result.unwrap_err();
assert!(
err.to_string().contains("traversal") || err.to_string().contains("parent"),
"Error should mention traversal or parent directory"
);
Ok(())
}
#[test]
#[serial]
fn test_fixup_path_traversal_rejection() -> Result<()> {
let env = SecurityTestEnvironment::new("fixup-traversal")?;
let base_path = env.handle.artifact_manager().base_path().to_path_buf();
std::fs::create_dir_all(base_path.as_path())?;
let abs_base_path = std::env::current_dir()?.join(base_path.as_std_path());
let parser =
FixupParser::new(FixupMode::Apply, abs_base_path).expect("Failed to create FixupParser");
let traversal_diff = UnifiedDiff {
path: "a/../evil.txt".to_string(),
target_file: "../evil.txt".to_string(),
diff_content: "diff --git a/../evil.txt b/../evil.txt\n...".to_string(),
hunks: vec![DiffHunk {
start: 1,
remove_count: 1,
add_count: 1,
remove_lines: vec!["old".to_string()],
add_lines: vec!["new".to_string()],
old_range: (1, 1),
new_range: (1, 1),
content: "@@ -1,1 +1,1 @@\n-old\n+new".to_string(),
}],
};
let result_traversal = parser.apply_changes(&[traversal_diff]);
assert!(result_traversal.is_ok());
let fixup_result_traversal = result_traversal.unwrap();
assert!(fixup_result_traversal.applied_files.is_empty());
assert_eq!(fixup_result_traversal.failed_files.len(), 1);
assert_eq!(fixup_result_traversal.failed_files[0], "../evil.txt");
let warnings_traversal = fixup_result_traversal.warnings.join("\n");
assert!(
warnings_traversal.contains("Path validation failed")
|| warnings_traversal.contains("traversal")
|| warnings_traversal.contains("parent"),
"Warnings should mention path validation failure: {}",
warnings_traversal
);
#[cfg(unix)]
let abs_path = "/tmp/evil.txt";
#[cfg(windows)]
let abs_path = "C:\\Windows\\Temp\\evil.txt";
let abs_diff = UnifiedDiff {
path: format!("a{}", abs_path),
target_file: abs_path.to_string(),
diff_content: format!("diff --git a{} b{}\n...", abs_path, abs_path),
hunks: vec![DiffHunk {
start: 1,
remove_count: 1,
add_count: 1,
remove_lines: vec!["old".to_string()],
add_lines: vec!["new".to_string()],
old_range: (1, 1),
new_range: (1, 1),
content: "@@ -1,1 +1,1 @@\n-old\n+new".to_string(),
}],
};
let result_abs = parser.apply_changes(&[abs_diff]);
assert!(result_abs.is_ok());
let fixup_result_abs = result_abs.unwrap();
assert!(fixup_result_abs.applied_files.is_empty());
assert_eq!(fixup_result_abs.failed_files.len(), 1);
assert_eq!(fixup_result_abs.failed_files[0], abs_path);
let warnings_abs = fixup_result_abs.warnings.join("\n");
assert!(
warnings_abs.contains("Path validation failed")
|| warnings_abs.contains("Absolute")
|| warnings_abs.contains("absolute"),
"Warnings should mention absolute path rejection: {}",
warnings_abs
);
Ok(())
}