use super::*;
use crate::linter::Severity;
#[test]
fn test_SEC010_detects_cp_with_user_file() {
let script = r#"cp "$USER_FILE" /destination/"#;
let result = check(script);
assert_eq!(result.diagnostics.len(), 1);
let diag = &result.diagnostics[0];
assert_eq!(diag.code, "SEC010");
assert_eq!(diag.severity, Severity::Error);
assert!(diag.message.contains("Path traversal"));
}
#[test]
fn test_SEC010_detects_cat_with_input_path() {
let script = r#"cat "$INPUT_PATH""#;
let result = check(script);
assert_eq!(result.diagnostics.len(), 1);
assert_eq!(result.diagnostics[0].code, "SEC010");
}
#[test]
fn test_SEC010_detects_tar_with_archive() {
let script = r#"tar -xf "$ARCHIVE""#;
let result = check(script);
assert_eq!(result.diagnostics.len(), 1);
assert_eq!(result.diagnostics[0].code, "SEC010");
}
#[test]
fn test_SEC010_detects_mkdir_with_user_dir() {
let script = r#"mkdir -p "$USER_DIR""#;
let result = check(script);
assert_eq!(result.diagnostics.len(), 1);
assert_eq!(result.diagnostics[0].code, "SEC010");
}
#[test]
fn test_SEC010_detects_cd_with_user_path() {
let script = r#"cd "$USER_PATH""#;
let result = check(script);
assert_eq!(result.diagnostics.len(), 1);
assert_eq!(result.diagnostics[0].code, "SEC010");
}
#[test]
fn test_SEC010_safe_with_hardcoded_path() {
let script = r#"cp /etc/config /backup/"#;
let result = check(script);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_SEC010_detects_explicit_traversal() {
let script = r#"cp file.txt ../../sensitive/"#;
let result = check(script);
assert!(!result.diagnostics.is_empty());
}
#[test]
fn test_SEC010_no_false_positive_validation() {
let script = r#"if [[ "$FILE" == *".."* ]]; then exit 1; fi"#;
let result = check(script);
}
#[test]
fn test_SEC010_no_auto_fix() {
let script = r#"cp "$USER_FILE" /dest/"#;
let result = check(script);
assert_eq!(result.diagnostics.len(), 1);
let diag = &result.diagnostics[0];
assert!(diag.fix.is_none(), "SEC010 should not provide auto-fix");
}
#[test]
fn test_SEC010_multiple_vulnerabilities() {
let script = r#"
cp "$USER_FILE" /dest/
cat "$INPUT_PATH"
"#;
let result = check(script);
assert_eq!(result.diagnostics.len(), 2);
}
#[test]
fn test_SEC010_no_false_positive_comment() {
let script = r#"# cp "$USER_FILE" is dangerous"#;
let result = check(script);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_SEC010_106_heredoc_not_file_read() {
let script = r#"content=$(cat <<EOF
some content here
EOF
)"#;
let result = check(script);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_SEC010_106_heredoc_multiline() {
let script = r#"cargo_content=$(cat <<'EOF'
[build]
jobs = 4
EOF
)"#;
let result = check(script);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_SEC010_106_heredoc_with_tee() {
let script = r#"tee /etc/config <<EOF
config here
EOF"#;
let result = check(script);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_SEC010_real_cat_still_flagged() {
let script = r#"cat "$USER_FILE""#;
let result = check(script);
assert_eq!(result.diagnostics.len(), 1);
assert_eq!(result.diagnostics[0].code, "SEC010");
}
#[test]
fn test_SEC010_104_validated_path_not_flagged() {
let script = r#"
if [[ "$USER_FILE" == *".."* ]]; then
echo "Invalid path" >&2
exit 1
fi
cp "$USER_FILE" /destination/
"#;
let result = check(script);
assert_eq!(
result.diagnostics.len(),
0,
"Expected no diagnostics for validated path, got: {:?}",
result.diagnostics
);
}
#[test]
fn test_SEC010_104_realpath_validated() {
let script = r#"
SAFE_PATH=$(realpath -m "$USER_INPUT")
cp "$SAFE_PATH" /destination/
"#;
let result = check(script);
assert_eq!(
result.diagnostics.len(),
0,
"Expected no diagnostics for realpath-validated path"
);
}
#[test]
fn test_SEC010_104_readlink_validated() {
let script = r#"
RESOLVED=$(readlink -f "$USER_PATH")
cat "$RESOLVED"
"#;
let result = check(script);
assert_eq!(
result.diagnostics.len(),
0,
"Expected no diagnostics for readlink-f-validated path"
);
}
#[test]
fn test_SEC010_104_unvalidated_still_flagged() {
let script = r#"
echo "Processing file..."
cp "$USER_FILE" /destination/
"#;
let result = check(script);
assert_eq!(result.diagnostics.len(), 1);
assert_eq!(result.diagnostics[0].code, "SEC010");
}
#[test]
fn test_SEC010_104_different_var_still_flagged() {
let script = r#"
if [[ "$SAFE_VAR" == *".."* ]]; then
exit 1
fi
cp "$USER_FILE" /destination/
"#;
let result = check(script);
assert_eq!(result.diagnostics.len(), 1);
assert_eq!(result.diagnostics[0].code, "SEC010");
}
#[test]
fn test_SEC010_104_absolute_path_check() {
let script = r#"
if [[ "$INPUT_PATH" == /* ]]; then
echo "Absolute paths not allowed" >&2
exit 1
fi
cp "$INPUT_PATH" /destination/
"#;
let result = check(script);
assert_eq!(
result.diagnostics.len(),
0,
"Expected no diagnostics after absolute path validation"
);
}
#[test]
fn test_SEC010_127_validate_function_tracks_var() {
let script = r#"
validate_path() {
local path="$1"
if [[ "$path" == *".."* ]]; then
echo "Invalid path" >&2
exit 1
fi
}
validate_path "$RAID_PATH"
mkdir -p "$RAID_PATH/targets"
"#;
let result = check(script);
assert_eq!(
result.diagnostics.len(),
0,
"Expected no diagnostics for variable passed to validate_path()"
);
}
#[test]
fn test_SEC010_127_check_function_tracks_var() {
let script = r#"
check_path "$SRC_PATH"
cp "$SRC_PATH/file" /destination/
"#;
let result = check(script);
assert_eq!(
result.diagnostics.len(),
0,
"Expected no diagnostics for variable passed to check_path()"
);
}
#[test]
fn test_SEC010_127_sanitize_function_tracks_var() {
let script = r#"
sanitize_input "$USER_FILE"
cat "$USER_FILE"
"#;
let result = check(script);
assert_eq!(
result.diagnostics.len(),
0,
"Expected no diagnostics for variable passed to sanitize_input()"
);
}
#[test]
fn test_SEC010_127_unvalidated_still_flagged() {
let script = r#"
validate_path "$OTHER_PATH"
mkdir -p "$USER_DIR"
"#;
let result = check(script);
assert_eq!(result.diagnostics.len(), 1);
assert_eq!(result.diagnostics[0].code, "SEC010");
}
#[test]
fn test_SEC010_127_function_definition_not_call() {
let script = r#"
validate_path() {
echo "validating"
}
mkdir -p "$USER_DIR"
"#;
let result = check(script);
assert_eq!(result.diagnostics.len(), 1);
}
#[test]
fn test_is_validation_function_call_various_prefixes() {
assert!(is_validation_function_call(r#"validate_path "$PATH""#));
assert!(is_validation_function_call(r#"check_input "$INPUT""#));
assert!(is_validation_function_call(r#"verify_file "$FILE""#));
assert!(is_validation_function_call(r#"sanitize_input "$INPUT""#));
assert!(is_validation_function_call(r#"clean_path "$PATH""#));
assert!(is_validation_function_call(r#"safe_copy "$FILE""#));
assert!(is_validation_function_call(r#"is_valid_path "$PATH""#));
assert!(is_validation_function_call(r#"is_safe_input "$INPUT""#));
assert!(is_validation_function_call(r#"assert_path "$PATH""#));
assert!(!is_validation_function_call("validate_path /fixed/path"));
assert!(!is_validation_function_call("validate_path() {"));
assert!(!is_validation_function_call("validate_path()"));
}
#[test]
fn test_extract_function_argument_variable_formats() {
assert_eq!(
extract_function_argument_variable(r#"validate_path "${PATH}""#),
Some("PATH".to_string())
);
assert_eq!(
extract_function_argument_variable(r#"validate_path "${ARGS[0]}""#),
Some("ARGS".to_string())
);
assert_eq!(
extract_function_argument_variable(r#"validate_path "$PATH""#),
Some("PATH".to_string())
);
assert_eq!(
extract_function_argument_variable("validate_path /fixed/path"),
None
);
}
#[test]
fn test_is_heredoc_pattern_variants() {
assert!(is_heredoc_pattern("cat <<EOF"));
assert!(is_heredoc_pattern("cat <<'EOF'"));
assert!(is_heredoc_pattern("cat <<-EOF"));
assert!(is_heredoc_pattern("cat<<<'EOF'"));
assert!(is_heredoc_pattern("echo <<EOF"));
assert!(is_heredoc_pattern("read <<EOF"));
assert!(is_heredoc_pattern("tee <<EOF"));
assert!(is_heredoc_pattern("content=$(cat <<EOF"));
assert!(is_heredoc_pattern("x=$(cat<<EOF"));
assert!(!is_heredoc_pattern("cat /etc/passwd"));
assert!(!is_heredoc_pattern(r#"cat "$FILE""#));
}
#[cfg(test)]
mod property_tests {
use super::*;
use crate::linter::Severity;
use proptest::prelude::*;
proptest! {
#![proptest_config(proptest::test_runner::Config::with_cases(10))]
#[test]
fn prop_sec010_never_panics(s in ".*") {
let _ = check(&s);
}
#[test]
fn prop_sec010_safe_hardcoded_paths(
src in "/[a-z/]{1,20}",
dst in "/[a-z/]{1,20}",
) {
let cmd = format!("cp {} {}", src, dst);
let result = check(&cmd);
prop_assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn prop_sec010_detects_user_variables(
file_op_idx in 0..9usize,
var_name in "(USER|INPUT|FILE|PATH|DIR|ARCHIVE|NAME|ARG)_[A-Z]{1,5}",
) {
let file_op = match file_op_idx {
0 => "cp",
1 => "mv",
2 => "cat",
3 => "tar",
4 => "unzip",
5 => "rm",
6 => "mkdir",
7 => "cd",
_ => "ln",
};
let cmd = format!(r#"{} "${{{}}}""#, file_op, var_name);
let result = check(&cmd);
prop_assert!(!result.diagnostics.is_empty());
prop_assert_eq!(result.diagnostics[0].code.as_str(), "SEC010");
}
}
}