safe_shell_scanner/
path_rules.rs1#[derive(Debug, Clone)]
3pub struct PathPattern {
4 pub pattern: String,
5 pub description: String,
6}
7
8pub fn get_sensitive_patterns() -> Vec<PathPattern> {
10 vec![
11 PathPattern {
12 pattern: "~/.ssh".into(),
13 description: "SSH keys and config".into(),
14 },
15 PathPattern {
16 pattern: "~/.aws".into(),
17 description: "AWS credentials".into(),
18 },
19 PathPattern {
20 pattern: "~/.gnupg".into(),
21 description: "GPG keys".into(),
22 },
23 PathPattern {
24 pattern: "~/.config/gcloud".into(),
25 description: "Google Cloud credentials".into(),
26 },
27 PathPattern {
28 pattern: "~/.azure".into(),
29 description: "Azure credentials".into(),
30 },
31 PathPattern {
32 pattern: "~/.docker".into(),
33 description: "Docker config (may contain registry auth)".into(),
34 },
35 PathPattern {
36 pattern: "~/.kube".into(),
37 description: "Kubernetes config".into(),
38 },
39 PathPattern {
40 pattern: "~/.npmrc".into(),
41 description: "npm auth tokens".into(),
42 },
43 PathPattern {
44 pattern: ".env".into(),
45 description: "Environment file".into(),
46 },
47 PathPattern {
48 pattern: ".env.*".into(),
49 description: "Environment file variants".into(),
50 },
51 PathPattern {
52 pattern: "*.pem".into(),
53 description: "PEM certificate/key".into(),
54 },
55 PathPattern {
56 pattern: "*.key".into(),
57 description: "Private key file".into(),
58 },
59 PathPattern {
60 pattern: "*.p12".into(),
61 description: "PKCS#12 certificate".into(),
62 },
63 PathPattern {
64 pattern: "*.pfx".into(),
65 description: "PFX certificate".into(),
66 },
67 PathPattern {
68 pattern: "*.jks".into(),
69 description: "Java keystore".into(),
70 },
71 PathPattern {
72 pattern: "*.keystore".into(),
73 description: "Keystore file".into(),
74 },
75 PathPattern {
76 pattern: "*.tfvars".into(),
77 description: "Terraform variables (may contain secrets)".into(),
78 },
79 PathPattern {
80 pattern: "*.tfstate".into(),
81 description: "Terraform state (contains resource details)".into(),
82 },
83 PathPattern {
84 pattern: "credentials.json".into(),
85 description: "Credentials file".into(),
86 },
87 PathPattern {
88 pattern: "secrets.*".into(),
89 description: "Secrets file".into(),
90 },
91 PathPattern {
92 pattern: "service-account*.json".into(),
93 description: "GCP service account key".into(),
94 },
95 PathPattern {
96 pattern: "*.kdbx".into(),
97 description: "KeePass database".into(),
98 },
99 PathPattern {
100 pattern: "*.kdb".into(),
101 description: "KeePass database (legacy)".into(),
102 },
103 ]
104}
105
106pub fn is_sensitive_path(path: &str) -> bool {
108 let home = dirs::home_dir()
109 .map(|h| h.to_string_lossy().to_string())
110 .unwrap_or_default();
111
112 let patterns = get_sensitive_patterns();
113 for pat in &patterns {
114 if pat.pattern.starts_with("~/") {
116 let resolved = pat.pattern.replacen('~', &home, 1);
117 if path == resolved || path.starts_with(&format!("{resolved}/")) {
118 return true;
119 }
120 continue;
121 }
122
123 if let Some(ext) = pat.pattern.strip_prefix("*.") {
125 if path.ends_with(&format!(".{ext}")) {
126 return true;
127 }
128 continue;
129 }
130
131 if pat.pattern.contains('*') {
133 let parts: Vec<&str> = pat.pattern.splitn(2, '*').collect();
134 if parts.len() == 2 {
135 let prefix = parts[0];
136 let suffix = parts[1];
137 if let Some(filename) = std::path::Path::new(path).file_name() {
138 let name = filename.to_string_lossy();
139 if name.starts_with(prefix)
140 && name.ends_with(suffix)
141 && name.len() >= prefix.len() + suffix.len()
142 {
143 return true;
144 }
145 }
146 }
147 continue;
148 }
149
150 if let Some(filename) = std::path::Path::new(path).file_name() {
152 if filename.to_string_lossy() == pat.pattern {
153 return true;
154 }
155 }
156 }
157
158 false
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 #[test]
166 fn has_enough_patterns() {
167 assert!(get_sensitive_patterns().len() >= 23);
168 }
169
170 #[test]
171 fn detects_ssh_dir() {
172 let home = dirs::home_dir().unwrap();
173 assert!(is_sensitive_path(&format!(
174 "{}/.ssh/id_rsa",
175 home.display()
176 )));
177 assert!(is_sensitive_path(&format!("{}/.ssh", home.display())));
178 }
179
180 #[test]
181 fn detects_aws_dir() {
182 let home = dirs::home_dir().unwrap();
183 assert!(is_sensitive_path(&format!(
184 "{}/.aws/credentials",
185 home.display()
186 )));
187 }
188
189 #[test]
190 fn detects_gnupg() {
191 let home = dirs::home_dir().unwrap();
192 assert!(is_sensitive_path(&format!(
193 "{}/.gnupg/pubring.kbx",
194 home.display()
195 )));
196 }
197
198 #[test]
199 fn detects_env_file() {
200 assert!(is_sensitive_path(".env"));
201 assert!(is_sensitive_path("/project/.env"));
202 }
203
204 #[test]
205 fn detects_env_variants() {
206 assert!(is_sensitive_path(".env.local"));
207 assert!(is_sensitive_path(".env.production"));
208 assert!(is_sensitive_path("/app/.env.staging"));
209 }
210
211 #[test]
212 fn detects_pem_file() {
213 assert!(is_sensitive_path("server.pem"));
214 assert!(is_sensitive_path("/etc/ssl/private/server.pem"));
215 }
216
217 #[test]
218 fn detects_key_file() {
219 assert!(is_sensitive_path("private.key"));
220 assert!(is_sensitive_path("/etc/ssl/server.key"));
221 }
222
223 #[test]
224 fn detects_tfvars() {
225 assert!(is_sensitive_path("terraform.tfvars"));
226 assert!(is_sensitive_path("prod.tfvars"));
227 }
228
229 #[test]
230 fn detects_credentials_json() {
231 assert!(is_sensitive_path("credentials.json"));
232 assert!(is_sensitive_path("/app/credentials.json"));
233 }
234
235 #[test]
236 fn detects_secrets_file() {
237 assert!(is_sensitive_path("secrets.yaml"));
238 assert!(is_sensitive_path("secrets.json"));
239 }
240
241 #[test]
242 fn detects_service_account() {
243 assert!(is_sensitive_path("service-account.json"));
244 assert!(is_sensitive_path("service-account-prod.json"));
245 }
246
247 #[test]
248 fn detects_keepass() {
249 assert!(is_sensitive_path("passwords.kdbx"));
250 assert!(is_sensitive_path("legacy.kdb"));
251 }
252
253 #[test]
254 fn allows_normal_files() {
255 assert!(!is_sensitive_path("/usr/bin/ls"));
256 assert!(!is_sensitive_path("src/main.rs"));
257 assert!(!is_sensitive_path("package.json"));
258 assert!(!is_sensitive_path("README.md"));
259 assert!(!is_sensitive_path("Cargo.toml"));
260 }
261}