safe_shell_scanner/
env_scrub.rs1use std::collections::HashMap;
2
3use crate::scanner::Scanner;
4
5pub fn scrub_env(
14 env: &HashMap<String, String>,
15 scrub_patterns: &[String],
16 pass_patterns: &[String],
17 scanner: &Scanner,
18) -> HashMap<String, String> {
19 env.iter()
20 .filter(|(key, value)| {
21 if pass_patterns.iter().any(|p| glob_match(p, key)) {
23 return true;
24 }
25
26 if scrub_patterns.iter().any(|p| glob_match(p, key)) {
28 return false;
29 }
30
31 if scanner.contains_secret(value) {
33 return false;
34 }
35
36 true
38 })
39 .map(|(k, v)| (k.clone(), v.clone()))
40 .collect()
41}
42
43fn glob_match(pattern: &str, text: &str) -> bool {
46 if pattern == "*" {
47 return true;
48 }
49
50 if let Some(suffix) = pattern.strip_prefix('*') {
51 return text.ends_with(suffix);
52 }
53
54 if let Some(prefix) = pattern.strip_suffix('*') {
55 return text.starts_with(prefix);
56 }
57
58 pattern == text
59}
60
61#[cfg(test)]
62mod tests {
63 use super::*;
64
65 #[test]
66 fn glob_match_suffix() {
67 assert!(glob_match("*_KEY", "AWS_SECRET_KEY"));
68 assert!(glob_match("*_KEY", "STRIPE_KEY"));
69 assert!(!glob_match("*_KEY", "PATH"));
70 assert!(!glob_match("*_KEY", "KEY_NAME"));
71 }
72
73 #[test]
74 fn glob_match_prefix() {
75 assert!(glob_match("NODE_*", "NODE_ENV"));
76 assert!(glob_match("NODE_*", "NODE_PATH"));
77 assert!(!glob_match("NODE_*", "PATH"));
78 }
79
80 #[test]
81 fn glob_match_exact() {
82 assert!(glob_match("PATH", "PATH"));
83 assert!(!glob_match("PATH", "HOME"));
84 }
85
86 #[test]
87 fn glob_match_wildcard_all() {
88 assert!(glob_match("*", "ANYTHING"));
89 }
90
91 #[test]
92 fn scrubs_secret_keys() {
93 let scanner = Scanner::new();
94 let mut env = HashMap::new();
95 env.insert("AWS_SECRET_KEY".into(), "mysecret".into());
96 env.insert("GITHUB_TOKEN".into(), "ghp_fake".into());
97 env.insert("PATH".into(), "/usr/bin".into());
98 env.insert("HOME".into(), "/home/user".into());
99
100 let scrub = vec!["*_KEY".into(), "*_TOKEN".into()];
101 let pass = vec!["PATH".into(), "HOME".into()];
102
103 let result = scrub_env(&env, &scrub, &pass, &scanner);
104 assert!(!result.contains_key("AWS_SECRET_KEY"));
105 assert!(!result.contains_key("GITHUB_TOKEN"));
106 assert!(result.contains_key("PATH"));
107 assert!(result.contains_key("HOME"));
108 }
109
110 #[test]
111 fn pass_overrides_scrub() {
112 let scanner = Scanner::new();
113 let mut env = HashMap::new();
114 env.insert("NPM_TOKEN".into(), "some-token".into());
116
117 let scrub = vec!["*_TOKEN".into()];
118 let pass = vec!["NPM_*".into()];
119
120 let result = scrub_env(&env, &scrub, &pass, &scanner);
121 assert!(
122 result.contains_key("NPM_TOKEN"),
123 "pass should override scrub"
124 );
125 }
126
127 #[test]
128 fn scrubs_by_value_content() {
129 let scanner = Scanner::new();
130 let mut env = HashMap::new();
131 env.insert("MY_CUSTOM_VAR".into(), "AKIAIOSFODNN7EXAMPLE".into());
133 env.insert("NORMAL_VAR".into(), "hello world".into());
134
135 let scrub: Vec<String> = vec![];
136 let pass: Vec<String> = vec![];
137
138 let result = scrub_env(&env, &scrub, &pass, &scanner);
139 assert!(
140 !result.contains_key("MY_CUSTOM_VAR"),
141 "should scrub value with AWS key"
142 );
143 assert!(result.contains_key("NORMAL_VAR"));
144 }
145
146 #[test]
147 fn keeps_unmatched_vars() {
148 let scanner = Scanner::new();
149 let mut env = HashMap::new();
150 env.insert("EDITOR".into(), "vim".into());
151 env.insert("TERM".into(), "xterm-256color".into());
152
153 let scrub = vec!["*_KEY".into()];
154 let pass: Vec<String> = vec![];
155
156 let result = scrub_env(&env, &scrub, &pass, &scanner);
157 assert!(result.contains_key("EDITOR"));
158 assert!(result.contains_key("TERM"));
159 }
160
161 #[test]
162 fn empty_env_returns_empty() {
163 let scanner = Scanner::new();
164 let env = HashMap::new();
165 let result = scrub_env(&env, &[], &[], &scanner);
166 assert!(result.is_empty());
167 }
168}