gatekpr_patterns/
security.rs1use crate::registry::PatternRegistry;
6use once_cell::sync::Lazy;
7
8pub static SECURITY_PATTERNS: Lazy<PatternRegistry> = Lazy::new(|| {
10 let mut registry = PatternRegistry::new();
11
12 registry.register("eval", r"(?i)\beval\s*\(").unwrap();
14
15 registry
16 .register("inner_html", r"(?i)\.innerHTML\s*=")
17 .unwrap();
18
19 registry
20 .register("dangerous_inner_html", r"(?i)dangerouslySetInnerHTML")
21 .unwrap();
22
23 registry
24 .register("exec", r"(?i)(child_process\.exec|shell_exec|exec\s*\()")
25 .unwrap();
26
27 registry
28 .register(
29 "sql_concat",
30 r#"(?i)(\+\s*['"].*SELECT|SELECT.*\+\s*['"]|['"].*\+.*WHERE)"#,
31 )
32 .unwrap();
33
34 registry
36 .register("shopify_access_token", r"shpat_[a-fA-F0-9]{32}")
37 .unwrap();
38
39 registry
40 .register("shopify_secret", r"shpss_[a-fA-F0-9]{32}")
41 .unwrap();
42
43 registry
44 .register("stripe_live_key", r"sk_live_[a-zA-Z0-9]{24,}")
45 .unwrap();
46
47 registry
48 .register("stripe_test_key", r"sk_test_[a-zA-Z0-9]{24,}")
49 .unwrap();
50
51 registry
52 .register("aws_access_key", r"AKIA[0-9A-Z]{16}")
53 .unwrap();
54
55 registry
56 .register(
57 "aws_secret_key",
58 r#"(?i)aws.{0,20}['"][0-9a-zA-Z/+]{40}['"]"#,
59 )
60 .unwrap();
61
62 registry
63 .register("private_key_rsa", r"-----BEGIN RSA PRIVATE KEY-----")
64 .unwrap();
65
66 registry
67 .register("private_key_generic", r"-----BEGIN PRIVATE KEY-----")
68 .unwrap();
69
70 registry
71 .register(
72 "generic_api_key",
73 r#"(?i)(api[_-]?key|apikey)\s*[:=]\s*['"][a-zA-Z0-9_-]{20,}['"]"#,
74 )
75 .unwrap();
76
77 registry
78 .register(
79 "generic_secret",
80 r#"(?i)(secret|password|passwd|pwd)\s*[:=]\s*['"][^'"]{8,}['"]"#,
81 )
82 .unwrap();
83
84 registry
86 .register("http_url", r#"['"]http://[^'"]+['"]"#)
87 .unwrap();
88
89 registry
91 .register(
92 "localhost_http",
93 r#"['"]http://(localhost|127\.0\.0\.1)[^'"]*['"]"#,
94 )
95 .unwrap();
96
97 registry
99 .register(
100 "command_injection",
101 r"(?i)(os\.system|subprocess\.call|subprocess\.run|Runtime\.exec)",
102 )
103 .unwrap();
104
105 registry
107 .register("document_write", r"(?i)document\.write\s*\(")
108 .unwrap();
109
110 registry
111 .register("outerhtml", r"(?i)\.outerHTML\s*=")
112 .unwrap();
113
114 registry
115});
116
117pub const DANGEROUS_FUNCTION_KEYS: &[&str] = &[
119 "eval",
120 "inner_html",
121 "dangerous_inner_html",
122 "exec",
123 "sql_concat",
124 "command_injection",
125 "document_write",
126 "outerhtml",
127];
128
129pub const HARDCODED_SECRET_KEYS: &[&str] = &[
131 "shopify_access_token",
132 "shopify_secret",
133 "stripe_live_key",
134 "stripe_test_key",
135 "aws_access_key",
136 "aws_secret_key",
137 "private_key_rsa",
138 "private_key_generic",
139 "generic_api_key",
140 "generic_secret",
141];
142
143pub const HTTP_URL_KEYS: &[&str] = &["http_url", "localhost_http"];
145
146pub fn find_security_issues(text: &str) -> SecurityIssues {
148 let mut issues = SecurityIssues::default();
149
150 for key in DANGEROUS_FUNCTION_KEYS {
152 if SECURITY_PATTERNS.is_match(key, text) {
153 issues.dangerous_functions.push(key.to_string());
154 }
155 }
156
157 for key in HARDCODED_SECRET_KEYS {
159 let matches = SECURITY_PATTERNS.find_all(key, text);
160 if !matches.is_empty() {
161 issues.hardcoded_secrets.push(SecretMatch {
162 pattern: key.to_string(),
163 count: matches.len(),
164 });
165 }
166 }
167
168 let http_matches = SECURITY_PATTERNS.find_all("http_url", text);
170 let localhost_matches = SECURITY_PATTERNS.find_all("localhost_http", text);
171
172 for http_match in &http_matches {
174 let is_localhost = localhost_matches
175 .iter()
176 .any(|lm| lm.start == http_match.start && lm.end == http_match.end);
177 if !is_localhost {
178 issues.insecure_http = true;
179 break;
180 }
181 }
182
183 issues
184}
185
186#[derive(Debug, Clone, Default)]
188pub struct SecurityIssues {
189 pub dangerous_functions: Vec<String>,
191 pub hardcoded_secrets: Vec<SecretMatch>,
193 pub insecure_http: bool,
195}
196
197#[derive(Debug, Clone)]
199pub struct SecretMatch {
200 pub pattern: String,
202 pub count: usize,
204}
205
206impl SecurityIssues {
207 pub fn has_issues(&self) -> bool {
209 !self.dangerous_functions.is_empty()
210 || !self.hardcoded_secrets.is_empty()
211 || self.insecure_http
212 }
213
214 pub fn total_count(&self) -> usize {
216 self.dangerous_functions.len()
217 + self
218 .hardcoded_secrets
219 .iter()
220 .map(|s| s.count)
221 .sum::<usize>()
222 + if self.insecure_http { 1 } else { 0 }
223 }
224
225 pub fn severity(&self) -> &'static str {
227 if !self.hardcoded_secrets.is_empty() {
228 "critical"
229 } else if !self.dangerous_functions.is_empty() {
230 "high"
231 } else if self.insecure_http {
232 "medium"
233 } else {
234 "none"
235 }
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242
243 #[test]
244 fn test_eval_detection() {
245 let code = "const result = eval(userInput);";
246 let issues = find_security_issues(code);
247 assert!(issues.dangerous_functions.contains(&"eval".to_string()));
248 }
249
250 #[test]
251 fn test_inner_html_detection() {
252 let code = r#"element.innerHTML = userInput;"#;
253 let issues = find_security_issues(code);
254 assert!(issues
255 .dangerous_functions
256 .contains(&"inner_html".to_string()));
257 }
258
259 #[test]
260 fn test_shopify_token_detection() {
261 let code = r#"const token = "shpat_1234567890abcdef1234567890abcdef";"#;
262 let issues = find_security_issues(code);
263 assert!(!issues.hardcoded_secrets.is_empty());
264 assert_eq!(issues.severity(), "critical");
265 }
266
267 #[test]
268 fn test_stripe_key_detection() {
269 let code = r#"const key = "sk_live_1234567890abcdefghijklmnop";"#;
270 let issues = find_security_issues(code);
271 assert!(!issues.hardcoded_secrets.is_empty());
272 }
273
274 #[test]
275 fn test_no_issues() {
276 let code = r#"
277 const data = sanitize(userInput);
278 element.textContent = data;
279 "#;
280 let issues = find_security_issues(code);
281 assert!(!issues.has_issues());
282 }
283
284 #[test]
285 fn test_insecure_http() {
286 let code = r#"fetch("http://api.example.com/data")"#;
287 let issues = find_security_issues(code);
288 assert!(issues.insecure_http);
289 }
290
291 #[test]
292 fn test_localhost_http_allowed() {
293 let code = r#"fetch("http://localhost:3000/api")"#;
294 let issues = find_security_issues(code);
295 assert!(!issues.insecure_http);
296 }
297}