use crate::linter::{Diagnostic, Fix, LintResult, Severity, Span};
fn check_tmp_assignment(line: &str, line_num: usize, result: &mut LintResult) {
if !line.contains("/tmp/") || line.contains("mktemp") {
return;
}
let Some(eq_pos) = line.find('=') else { return };
let after_eq = &line[eq_pos + 1..].trim_start();
if !after_eq.starts_with("\"/tmp/") && !after_eq.starts_with("'/tmp/") {
return;
}
if let Some(col) = line.find("/tmp/") {
let span = Span::new(line_num + 1, col + 1, line_num + 1, col + 6);
let diag = Diagnostic::new(
"SEC006",
Severity::Warning,
"Unsafe temp file - use mktemp for secure random names",
span,
)
.with_fix(Fix::new("$(mktemp)"));
result.add(diag);
}
}
pub fn check(source: &str) -> LintResult {
if source.is_empty() { return LintResult::new(); }
contract_pre_classify_filesystem!(source);
let mut result = LintResult::new();
for (line_num, line) in source.lines().enumerate() {
check_tmp_assignment(line, line_num, &mut result);
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_SEC006_detects_predictable_tmp() {
let script = r#"TMPFILE="/tmp/myapp.$$""#;
let result = check(script);
assert_eq!(result.diagnostics.len(), 1);
let diag = &result.diagnostics[0];
assert_eq!(diag.code, "SEC006");
assert_eq!(diag.severity, Severity::Warning);
}
#[test]
fn test_SEC006_detects_static_tmp() {
let script = "TMP_DIR=\"/tmp/build_cache\"";
let result = check(script);
assert_eq!(result.diagnostics.len(), 1);
}
#[test]
fn test_SEC006_no_warning_mktemp() {
let script = "TMPFILE=\"$(mktemp)\"";
let result = check(script);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_SEC006_no_warning_mktemp_dir() {
let script = "TMPDIR=\"$(mktemp -d)\"";
let result = check(script);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_SEC006_provides_fix() {
let script = "TMPFILE=\"/tmp/script_temp\"";
let result = check(script);
assert!(result.diagnostics[0].fix.is_some());
let fix = result.diagnostics[0].fix.as_ref().unwrap();
assert_eq!(fix.replacement, "$(mktemp)");
}
#[test]
fn test_mutation_sec006_tmp_start_col_exact() {
let bash_code = r#"TMPFILE="/tmp/myapp.$$""#;
let result = check(bash_code);
assert_eq!(result.diagnostics.len(), 1);
let span = result.diagnostics[0].span;
assert_eq!(
span.start_col, 10,
"Start column must use col + 1, not col * 1"
);
}
#[test]
fn test_mutation_sec006_tmp_end_col_exact() {
let bash_code = r#"TMPFILE="/tmp/myapp.$$""#;
let result = check(bash_code);
assert_eq!(result.diagnostics.len(), 1);
let span = result.diagnostics[0].span;
assert_eq!(
span.end_col, 15,
"End column must be col + 6, not col * 6 or col - 6"
);
}
#[test]
fn test_mutation_sec006_line_num_calculation() {
let bash_code = "# comment\nTMPFILE=\"/tmp/script_temp\"";
let result = check(bash_code);
assert_eq!(result.diagnostics.len(), 1);
assert_eq!(
result.diagnostics[0].span.start_line, 2,
"Line number must use +1, not *1"
);
}
#[test]
fn test_mutation_sec006_column_with_leading_whitespace() {
let bash_code = r#" TMP_DIR="/tmp/build_cache""#;
let result = check(bash_code);
assert_eq!(result.diagnostics.len(), 1);
let span = result.diagnostics[0].span;
assert!(span.start_col > 10, "Must account for leading whitespace");
assert_eq!(
span.end_col - span.start_col,
5,
"Span should cover /tmp/ (5 chars)"
);
}
}