1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
//! SEC007: Running Commands as Root Without Validation
//!
//! **Rule**: Detect sudo/root operations without input validation
//!
//! **Why this matters**:
//! Unvalidated root operations can destroy entire systems if variables are
//! empty or contain dangerous values like "/".
//!
//! **Auto-fix**: Manual review required (context-dependent)
//!
//! ## Examples
//!
//! ❌ **UNSAFE ROOT OPERATIONS**:
//! ```bash
//! sudo rm -rf $DIR
//! sudo chmod 777 $FILE
//! sudo chown $USER $PATH
//! ```
//!
//! ✅ **ADD VALIDATION**:
//! ```bash
//! if [ -z "$DIR" ] || [ "$DIR" = "/" ]; then
//! echo "Error: Invalid directory"
//! exit 1
//! fi
//! sudo rm -rf "${DIR}"
//! ```
use crate::linter::{Diagnostic, LintResult, Severity, Span};
/// Dangerous commands that should never be run with sudo + unquoted vars
const DANGEROUS_SUDO_COMMANDS: &[&str] = &["rm -rf", "chmod 777", "chmod -R", "chown -R"];
/// Check a single line for unsafe sudo + dangerous command + unquoted variable
fn check_sudo_line(line: &str, line_num: usize, result: &mut LintResult) {
if !line.contains("sudo") {
return;
}
for cmd in DANGEROUS_SUDO_COMMANDS {
if line.contains(cmd) && line.contains(" $") {
if let Some(col) = line.find("sudo") {
let span = Span::new(line_num + 1, col + 1, line_num + 1, col + 5);
let diag = Diagnostic::new(
"SEC007",
Severity::Warning,
format!(
"Unsafe root operation: sudo {} with unquoted variable - add validation",
cmd
),
span,
);
result.add(diag);
break;
}
}
}
}
/// Check for unsafe sudo operations
pub fn check(source: &str) -> LintResult {
if source.is_empty() { return LintResult::new(); }
// Contract: safety-classifier-v1.yaml precondition (pv codegen)
contract_pre_classify_filesystem!(source);
let mut result = LintResult::new();
for (line_num, line) in source.lines().enumerate() {
check_sudo_line(line, line_num, &mut result);
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_SEC007_detects_sudo_rm_rf() {
let script = "sudo rm -rf $DIR";
let result = check(script);
assert_eq!(result.diagnostics.len(), 1);
let diag = &result.diagnostics[0];
assert_eq!(diag.code, "SEC007");
assert_eq!(diag.severity, Severity::Warning);
}
#[test]
fn test_SEC007_detects_sudo_chmod_777() {
let script = "sudo chmod 777 $FILE";
let result = check(script);
assert_eq!(result.diagnostics.len(), 1);
}
#[test]
fn test_SEC007_no_warning_with_quotes() {
let script = "sudo rm -rf \"${DIR}\"";
let result = check(script);
// Still warns because even quoted vars need validation
// But this is a simpler pattern matcher
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_SEC007_no_warning_safe_command() {
let script = "sudo systemctl restart nginx";
let result = check(script);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_SEC007_no_auto_fix() {
let script = "sudo rm -rf $TMPDIR";
let result = check(script);
assert_eq!(result.diagnostics.len(), 1);
let diag = &result.diagnostics[0];
assert!(diag.fix.is_none(), "SEC007 should not provide auto-fix");
}
// ===== Mutation Coverage Tests - Following SEC001 pattern (100% kill rate) =====
#[test]
fn test_mutation_sec007_sudo_start_col_exact() {
// MUTATION: Line 47:33 - replace + with * in line_num + 1
// MUTATION: Line 48:33 - replace + with * in col + 1
let bash_code = "sudo rm -rf $DIR";
let result = check(bash_code);
assert_eq!(result.diagnostics.len(), 1);
let span = result.diagnostics[0].span;
// "sudo" starts at column 1 (0-indexed)
assert_eq!(
span.start_col, 1,
"Start column must use col + 1, not col * 1"
);
}
#[test]
fn test_mutation_sec007_sudo_end_col_exact() {
// MUTATION: Line 50:33 - replace + with * in col + 5
// MUTATION: Line 50:33 - replace + with - in col + 5
let bash_code = "sudo rm -rf $DIR";
let result = check(bash_code);
assert_eq!(result.diagnostics.len(), 1);
let span = result.diagnostics[0].span;
// "sudo" is 4 chars, ends at col + 5
assert_eq!(
span.end_col, 5,
"End column must be col + 5, not col * 5 or col - 5"
);
}
#[test]
fn test_mutation_sec007_line_num_calculation() {
// MUTATION: Line 47:33 - replace + with * in line_num + 1
// Tests line number calculation for multiline input
let bash_code = "# comment\nsudo chmod 777 $FILE";
let result = check(bash_code);
assert_eq!(result.diagnostics.len(), 1);
// With +1: line 2
// With *1: line 0
assert_eq!(
result.diagnostics[0].span.start_line, 2,
"Line number must use +1, not *1"
);
}
#[test]
fn test_mutation_sec007_column_with_leading_whitespace() {
// Tests column calculations with offset
let bash_code = " sudo chown -R $USER";
let result = check(bash_code);
assert_eq!(result.diagnostics.len(), 1);
let span = result.diagnostics[0].span;
// "sudo" starts after leading whitespace (4 spaces + 1)
assert_eq!(span.start_col, 5, "Must account for leading whitespace");
assert_eq!(span.end_col, 9, "End must be start + 4 (sudo length)");
}
}