wm_dispatch/
speculative.rs1use regex::Regex;
13use std::sync::OnceLock;
14
15static SQL_INJECTION_RE: OnceLock<Regex> = OnceLock::new();
16static HARDCODED_SECRET_RE: OnceLock<Regex> = OnceLock::new();
17static DANGEROUS_EXEC_RE: OnceLock<Regex> = OnceLock::new();
18
19fn sql_injection_re() -> &'static Regex {
20 SQL_INJECTION_RE.get_or_init(|| Regex::new(r#"(?i)execute\(\s*f["']"#).unwrap())
21}
22
23fn hardcoded_secret_re() -> &'static Regex {
24 HARDCODED_SECRET_RE.get_or_init(|| {
25 Regex::new(r#"(?i)(api_key|secret|password|token)\s*=\s*["'][A-Za-z0-9\-_]{20,}["']"#)
26 .unwrap()
27 })
28}
29
30fn dangerous_exec_re() -> &'static Regex {
31 DANGEROUS_EXEC_RE.get_or_init(|| Regex::new(r"\b(exec|eval|system|popen)\s*\(").unwrap())
32}
33
34#[derive(Debug, Clone)]
36pub struct CheckResult {
37 pub name: &'static str,
39 pub passed: bool,
41 pub error: Option<String>,
43 pub issues: Vec<String>,
45}
46
47#[derive(Debug, Clone)]
49pub struct ValidationResult {
50 pub valid: bool,
52 pub checks: Vec<CheckResult>,
54 pub errors: Vec<String>,
56}
57
58impl ValidationResult {
59 #[must_use]
61 pub const fn passing() -> Self {
62 Self {
63 valid: true,
64 checks: Vec::new(),
65 errors: Vec::new(),
66 }
67 }
68}
69
70pub struct SpeculativeExecutor {
75 pub check_security: bool,
77 pub check_brackets: bool,
79}
80
81impl Default for SpeculativeExecutor {
82 fn default() -> Self {
83 Self {
84 check_security: true,
85 check_brackets: true,
86 }
87 }
88}
89
90impl SpeculativeExecutor {
91 #[must_use]
93 pub const fn new() -> Self {
94 Self {
95 check_security: true,
96 check_brackets: true,
97 }
98 }
99
100 #[must_use]
104 pub fn check_bracket_balance(code: &str) -> (bool, Option<String>) {
105 let mut paren = 0i32;
106 let mut bracket = 0i32;
107 let mut brace = 0i32;
108 let mut in_string = false;
109 let mut string_char = '\0';
110 let mut escaped = false;
111
112 for ch in code.chars() {
113 if escaped {
114 escaped = false;
115 continue;
116 }
117 if ch == '\\' && in_string {
118 escaped = true;
119 continue;
120 }
121 if in_string {
122 if ch == string_char {
123 in_string = false;
124 }
125 continue;
126 }
127 match ch {
128 '"' | '\'' => {
129 in_string = true;
130 string_char = ch;
131 }
132 '(' => paren += 1,
133 ')' => paren -= 1,
134 '[' => bracket += 1,
135 ']' => bracket -= 1,
136 '{' => brace += 1,
137 '}' => brace -= 1,
138 _ => {}
139 }
140 if paren < 0 || bracket < 0 || brace < 0 {
141 return (false, Some(format!("Unmatched closing delimiter at: {ch}")));
142 }
143 }
144
145 if paren != 0 {
146 return (
147 false,
148 Some(format!("Unbalanced parentheses: offset {paren}")),
149 );
150 }
151 if bracket != 0 {
152 return (
153 false,
154 Some(format!("Unbalanced brackets: offset {bracket}")),
155 );
156 }
157 if brace != 0 {
158 return (false, Some(format!("Unbalanced braces: offset {brace}")));
159 }
160
161 (true, None)
162 }
163
164 #[must_use]
168 pub fn check_security_heuristics(code: &str) -> (bool, Vec<String>) {
169 let mut issues = Vec::new();
170
171 if sql_injection_re().is_match(code) {
172 issues.push("Potential SQL Injection (f-string in execute)".to_string());
173 }
174
175 if hardcoded_secret_re().is_match(code) {
176 issues.push("Potential hardcoded secret".to_string());
177 }
178
179 if dangerous_exec_re().is_match(code) {
180 issues.push("Dangerous usage of exec/eval/system/popen".to_string());
181 }
182
183 (issues.is_empty(), issues)
184 }
185
186 #[must_use]
188 pub fn validate(&self, content: &str) -> ValidationResult {
189 let mut result = ValidationResult::passing();
190
191 if self.check_brackets {
192 let (balanced, err) = Self::check_bracket_balance(content);
193 let check = CheckResult {
194 name: "bracket_balance",
195 passed: balanced,
196 error: err.clone(),
197 issues: Vec::new(),
198 };
199 result.checks.push(check);
200 if !balanced {
201 result.valid = false;
202 if let Some(e) = err {
203 result.errors.push(e);
204 }
205 return result; }
207 }
208
209 if self.check_security {
210 let (clean, issues) = Self::check_security_heuristics(content);
211 let check = CheckResult {
212 name: "security",
213 passed: clean,
214 error: None,
215 issues: issues.clone(),
216 };
217 result.checks.push(check);
218 if !clean {
219 result.valid = false;
220 result.errors.extend(issues);
221 }
222 }
223
224 result
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231
232 #[test]
233 fn bracket_balance_ok() {
234 let (ok, err) = SpeculativeExecutor::check_bracket_balance("fn main() { let x = [1, 2]; }");
235 assert!(ok);
236 assert!(err.is_none());
237 }
238
239 #[test]
240 fn bracket_balance_unmatched() {
241 let (ok, err) = SpeculativeExecutor::check_bracket_balance("fn main() {");
242 assert!(!ok);
243 assert!(err.is_some());
244 }
245
246 #[test]
247 fn bracket_balance_ignores_strings() {
248 let (ok, _) =
249 SpeculativeExecutor::check_bracket_balance("let s = \"(unmatched in string\";");
250 assert!(ok);
251 }
252
253 #[test]
254 fn bracket_balance_ignores_escapes() {
255 let (ok, _) = SpeculativeExecutor::check_bracket_balance("let s = \"\\\"escaped\\\"\";");
256 assert!(ok);
257 }
258
259 #[test]
260 fn security_clean_code() {
261 let (clean, issues) = SpeculativeExecutor::check_security_heuristics("let x = 1 + 2;");
262 assert!(clean);
263 assert!(issues.is_empty());
264 }
265
266 #[test]
267 fn security_detects_sql_injection() {
268 let (clean, issues) = SpeculativeExecutor::check_security_heuristics(
269 "cursor.execute(f\"SELECT * FROM {table}\")",
270 );
271 assert!(!clean);
272 assert!(issues.iter().any(|i| i.contains("SQL Injection")));
273 }
274
275 #[test]
276 fn security_detects_hardcoded_secret() {
277 let (clean, issues) = SpeculativeExecutor::check_security_heuristics(
278 "api_key = \"abcdefghijklmnopqrstuvwxyz123456\"",
279 );
280 assert!(!clean);
281 assert!(issues.iter().any(|i| i.contains("secret")));
282 }
283
284 #[test]
285 fn security_detects_dangerous_exec() {
286 let (clean, issues) = SpeculativeExecutor::check_security_heuristics("eval(user_input)");
287 assert!(!clean);
288 assert!(issues.iter().any(|i| i.contains("exec/eval")));
289 }
290
291 #[test]
292 fn validate_passes_clean_code() {
293 let executor = SpeculativeExecutor::default();
294 let result = executor.validate("fn add(a: i32, b: i32) -> i32 { a + b }");
295 assert!(result.valid);
296 assert!(result.errors.is_empty());
297 }
298
299 #[test]
300 fn validate_fails_on_unbalanced() {
301 let executor = SpeculativeExecutor::default();
302 let result = executor.validate("fn add(a: i32 {");
303 assert!(!result.valid);
304 assert!(!result.errors.is_empty());
305 }
306
307 #[test]
308 fn validate_fails_on_security() {
309 let executor = SpeculativeExecutor::default();
310 let result = executor.validate("eval(\"dangerous code\")");
311 assert!(!result.valid);
312 assert!(result.errors.iter().any(|e| e.contains("exec/eval")));
313 }
314
315 #[test]
316 fn validate_fail_fast_on_brackets() {
317 let executor = SpeculativeExecutor::default();
318 let result = executor.validate("fn { eval(\"bad\")");
319 assert!(!result.valid);
321 assert_eq!(result.checks.len(), 1);
323 assert_eq!(result.checks[0].name, "bracket_balance");
324 }
325
326 #[test]
327 fn validate_passes_with_security_disabled() {
328 let mut executor = SpeculativeExecutor::new();
329 executor.check_security = false;
330 let result = executor.validate("let x = 1;");
331 assert!(result.valid);
332 assert_eq!(result.checks.len(), 1);
334 }
335}