aprender_shell/
security.rs1const SENSITIVE_PATTERNS: &[&str] = &[
14 "password=",
16 "passwd=",
17 "pwd=",
18 "secret=",
19 "token=",
20 "api_key=",
21 "apikey=",
22 "AWS_SECRET",
24 "GITHUB_TOKEN",
25 "API_KEY",
26 "PRIVATE_KEY",
27 "ACCESS_TOKEN",
28 "AUTH_TOKEN",
29 "SECRET_KEY",
30 "curl -u",
32 "curl --user",
33 "wget --password",
34 "ssh-keygen",
36 "gpg --gen-key",
37 "mysql -p",
39 "psql -W",
40 "mongo --password",
41 "aws configure",
43 "gcloud auth",
44 "az login",
45];
46
47pub fn is_sensitive_command(cmd: &str) -> bool {
63 check_known_patterns(cmd)
64 || check_export_secrets(cmd)
65 || check_database_inline_password(cmd)
66 || check_inline_key_value(cmd)
67}
68
69fn check_known_patterns(cmd: &str) -> bool {
71 let upper = cmd.to_uppercase();
72 SENSITIVE_PATTERNS
73 .iter()
74 .any(|pattern| upper.contains(&pattern.to_uppercase()))
75}
76
77fn check_export_secrets(cmd: &str) -> bool {
79 let upper = cmd.to_uppercase();
80 if !upper.contains("EXPORT") || !upper.contains('=') {
81 return false;
82 }
83
84 const SECRET_KEYWORDS: &[&str] = &["SECRET", "TOKEN", "KEY", "PASSWORD", "CREDENTIAL", "AUTH"];
85 SECRET_KEYWORDS.iter().any(|kw| upper.contains(kw))
86}
87
88fn check_database_inline_password(cmd: &str) -> bool {
90 let lower = cmd.to_lowercase();
91 let is_db_command =
92 lower.contains("mysql") || lower.contains("psql") || lower.contains("mongo");
93
94 if !is_db_command {
95 return false;
96 }
97
98 lower.find("-p").is_some_and(|pos| {
100 let after_p = &lower[pos + 2..];
101 !after_p.is_empty() && !after_p.starts_with(' ') && !after_p.starts_with('\t')
102 })
103}
104
105fn check_inline_key_value(cmd: &str) -> bool {
107 const SUSPICIOUS_KEYS: &[&str] = &[
108 "password",
109 "passwd",
110 "secret",
111 "token",
112 "key",
113 "credential",
114 "auth",
115 ];
116 let lower = cmd.to_lowercase();
117
118 SUSPICIOUS_KEYS.iter().any(|key| {
119 let pattern = format!("{key}=");
120 let pattern_spaced = format!("{key} =");
121
122 if !lower.contains(&pattern) && !lower.contains(&pattern_spaced) {
123 return false;
124 }
125
126 lower.find(&pattern).is_some_and(|pos| {
128 let after_eq = &cmd[pos + pattern.len()..];
129 has_actual_value(after_eq)
130 })
131 })
132}
133
134fn has_actual_value(after_eq: &str) -> bool {
136 let trimmed = after_eq.trim();
137 !trimmed.is_empty() && !trimmed.starts_with('-') && trimmed != "\"" && trimmed != "'"
138}
139
140pub fn filter_sensitive_commands(commands: &[String]) -> Vec<String> {
144 commands
145 .iter()
146 .filter(|cmd| !is_sensitive_command(cmd))
147 .cloned()
148 .collect()
149}
150
151pub fn filter_sensitive_suggestions(suggestions: Vec<(String, f32)>) -> Vec<(String, f32)> {
153 suggestions
154 .into_iter()
155 .filter(|(cmd, _)| !is_sensitive_command(cmd))
156 .collect()
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 #[test]
168 fn test_detect_password_in_curl() {
169 assert!(is_sensitive_command(
170 "curl -u admin:password123 https://api.example.com"
171 ));
172 assert!(is_sensitive_command(
173 "curl --user admin:pass https://api.example.com"
174 ));
175 }
176
177 #[test]
178 fn test_detect_env_export() {
179 assert!(is_sensitive_command("export AWS_SECRET_ACCESS_KEY=abc123"));
180 assert!(is_sensitive_command("export GITHUB_TOKEN=ghp_xxxx"));
181 assert!(is_sensitive_command("export API_KEY=sk-xxxxx"));
182 assert!(is_sensitive_command("export AUTH_TOKEN=bearer_xxx"));
183 }
184
185 #[test]
186 fn test_detect_mysql_password() {
187 assert!(is_sensitive_command("mysql -u root -pMyPassword"));
188 assert!(is_sensitive_command("mysql -p'secret'"));
189 }
190
191 #[test]
192 fn test_detect_psql_password() {
193 assert!(is_sensitive_command("psql -W"));
194 }
195
196 #[test]
197 fn test_detect_inline_secrets() {
198 assert!(is_sensitive_command("password=hunter2"));
199 assert!(is_sensitive_command("secret=mysecret"));
200 assert!(is_sensitive_command("token=abc123"));
201 assert!(is_sensitive_command("api_key=sk-xxxx"));
202 }
203
204 #[test]
205 fn test_detect_cloud_auth() {
206 assert!(is_sensitive_command("aws configure"));
207 assert!(is_sensitive_command("gcloud auth login"));
208 assert!(is_sensitive_command("az login"));
209 }
210
211 #[test]
212 fn test_detect_key_generation() {
213 assert!(is_sensitive_command("ssh-keygen -t rsa"));
214 assert!(is_sensitive_command("gpg --gen-key"));
215 }
216
217 #[test]
222 fn test_allow_normal_commands() {
223 assert!(!is_sensitive_command("git status"));
224 assert!(!is_sensitive_command("git commit -m 'message'"));
225 assert!(!is_sensitive_command("docker ps"));
226 assert!(!is_sensitive_command("cargo build --release"));
227 assert!(!is_sensitive_command("kubectl get pods"));
228 assert!(!is_sensitive_command("npm install"));
229 }
230
231 #[test]
232 fn test_allow_curl_without_auth() {
233 assert!(!is_sensitive_command("curl https://api.example.com"));
234 assert!(!is_sensitive_command(
235 "curl -X POST https://api.example.com"
236 ));
237 assert!(!is_sensitive_command(
238 "curl -H 'Content-Type: application/json' https://api.example.com"
239 ));
240 }
241
242 #[test]
243 fn test_allow_git_config() {
244 assert!(!is_sensitive_command("git config user.name"));
246 assert!(!is_sensitive_command("git config user.email"));
247 }
248
249 #[test]
250 fn test_allow_export_without_secrets() {
251 assert!(!is_sensitive_command("export PATH=/usr/bin:$PATH"));
252 assert!(!is_sensitive_command("export EDITOR=vim"));
253 assert!(!is_sensitive_command("export LANG=en_US.UTF-8"));
254 }
255
256 #[test]
257 fn test_case_insensitive() {
258 assert!(is_sensitive_command("PASSWORD=secret"));
259 assert!(is_sensitive_command("Token=abc123"));
260 assert!(is_sensitive_command("EXPORT github_token=xxx"));
261 }
262
263 #[test]
268 fn test_filter_sensitive_commands() {
269 let commands = vec![
270 "git status".to_string(),
271 "export SECRET=xxx".to_string(),
272 "cargo build".to_string(),
273 "curl -u user:pass http://localhost".to_string(),
274 ];
275
276 let filtered = filter_sensitive_commands(&commands);
277
278 assert_eq!(filtered.len(), 2);
279 assert!(filtered.contains(&"git status".to_string()));
280 assert!(filtered.contains(&"cargo build".to_string()));
281 assert!(!filtered.contains(&"export SECRET=xxx".to_string()));
282 }
283
284 #[test]
285 fn test_filter_sensitive_suggestions() {
286 let suggestions = vec![
287 ("git status".to_string(), 0.9),
288 ("export TOKEN=abc".to_string(), 0.8),
289 ("docker ps".to_string(), 0.7),
290 ];
291
292 let filtered = filter_sensitive_suggestions(suggestions);
293
294 assert_eq!(filtered.len(), 2);
295 assert_eq!(filtered[0].0, "git status");
296 assert_eq!(filtered[1].0, "docker ps");
297 }
298}