Skip to main content

oparry_validators/
security.rs

1//! Security validator - Security patterns and vulnerabilities
2
3use crate::Validator;
4use oparry_core::{Issue, IssueLevel, Result, ValidationResult};
5use oparry_parser::{ParsedCode, Language};
6use regex::Regex;
7use std::path::Path;
8
9/// Security validation configuration
10#[derive(Debug, Clone)]
11pub struct SecurityConfig {
12    /// Block dangerous innerHTML usage
13    pub block_innerhtml: bool,
14    /// Block eval usage
15    pub block_eval: bool,
16    /// Block dangerous APIs
17    pub block_dangerous_apis: bool,
18    /// Check for hardcoded secrets
19    pub check_secrets: bool,
20    /// Warn on unsafe DOM manipulation
21    pub warn_unsafe_dom: bool,
22}
23
24impl Default for SecurityConfig {
25    fn default() -> Self {
26        Self {
27            block_innerhtml: true,
28            block_eval: true,
29            block_dangerous_apis: true,
30            check_secrets: true,
31            warn_unsafe_dom: true,
32        }
33    }
34}
35
36/// Security validator
37pub struct SecurityValidator {
38    config: SecurityConfig,
39    innerhtml_regex: Regex,
40    eval_regex: Regex,
41    dangerous_dom_regex: Regex,
42    secret_pattern_regex: Regex,
43}
44
45impl SecurityValidator {
46    /// Create new security validator
47    pub fn new(config: SecurityConfig) -> Self {
48        Self {
49            config,
50            innerhtml_regex: Regex::new(r"dangerouslySetInnerHTML|innerHTML\s*=").unwrap(),
51            eval_regex: Regex::new(r#"\beval\s*\(|new\s+Function\s*\("#).unwrap(),
52            dangerous_dom_regex: Regex::new(r#"\b(document\.write|outerHTML\s*=)"#).unwrap(),
53            secret_pattern_regex: Regex::new(
54                r#"(?i)(api[_-]?key|secret|password|token)\s*[:=]\s*['"][\w-]{20,}"#
55            ).unwrap(),
56        }
57    }
58
59    /// Create with default config
60    pub fn default_config() -> Self {
61        Self::new(SecurityConfig::default())
62    }
63
64    /// Check for dangerous innerHTML usage
65    fn check_innerhtml(&self, source: &str, file: &str) -> Vec<Issue> {
66        let mut issues = Vec::new();
67        if !self.config.block_innerhtml {
68            return issues;
69        }
70
71        for (idx, line) in source.lines().enumerate() {
72            if self.innerhtml_regex.is_match(line) {
73                issues.push(Issue::error(
74                    "sec-dangerous-innerhtml",
75                    "Dangerous innerHTML usage detected - XSS vulnerability",
76                )
77                .with_file(file)
78                .with_line(idx + 1)
79                .with_suggestion("Use React text or DOMParser with sanitization"));
80            }
81        }
82        issues
83    }
84
85    /// Check for eval usage
86    fn check_eval(&self, source: &str, file: &str) -> Vec<Issue> {
87        let mut issues = Vec::new();
88        if !self.config.block_eval {
89            return issues;
90        }
91
92        for (idx, line) in source.lines().enumerate() {
93            if self.eval_regex.is_match(line) {
94                issues.push(Issue::error(
95                    "sec-eval-usage",
96                    "eval() or new Function() detected - code injection risk",
97                )
98                .with_file(file)
99                .with_line(idx + 1)
100                .with_suggestion("Never use eval - find safer alternative"));
101            }
102        }
103        issues
104    }
105
106    /// Check for hardcoded secrets
107    fn check_secrets(&self, source: &str, file: &str) -> Vec<Issue> {
108        let mut issues = Vec::new();
109        if !self.config.check_secrets {
110            return issues;
111        }
112
113        for (idx, line) in source.lines().enumerate() {
114            // Skip comment lines
115            let trimmed = line.trim();
116            if trimmed.starts_with("//") || trimmed.starts_with("#") {
117                continue;
118            }
119
120            if self.secret_pattern_regex.is_match(line) {
121                issues.push(Issue::error(
122                    "sec-hardcoded-secret",
123                    "Possible hardcoded secret detected",
124                )
125                .with_file(file)
126                .with_line(idx + 1)
127                .with_suggestion("Move secrets to environment variables"));
128            }
129        }
130        issues
131    }
132
133    /// Check for unsafe DOM manipulation
134    fn check_unsafe_dom(&self, source: &str, file: &str) -> Vec<Issue> {
135        let mut issues = Vec::new();
136        if !self.config.warn_unsafe_dom {
137            return issues;
138        }
139
140        for (idx, line) in source.lines().enumerate() {
141            if self.dangerous_dom_regex.is_match(line) {
142                issues.push(Issue::warning(
143                    "sec-unsafe-dom",
144                    "Unsafe DOM manipulation detected",
145                )
146                .with_file(file)
147                .with_line(idx + 1)
148                .with_suggestion("Use React state and refs instead"));
149            }
150        }
151        issues
152    }
153}
154
155impl Validator for SecurityValidator {
156    fn name(&self) -> &str {
157        "Security"
158    }
159
160    fn supports(&self, language: Language) -> bool {
161        language.is_javascript_variant()
162    }
163
164    fn validate_parsed(&self, code: &ParsedCode, file: &Path) -> Result<ValidationResult> {
165        let mut result = ValidationResult::new();
166        let source = code.source();
167        let file_str = file.to_string_lossy().to_string();
168
169        for issue in self.check_innerhtml(source, &file_str) {
170            result.add_issue(issue);
171        }
172        for issue in self.check_eval(source, &file_str) {
173            result.add_issue(issue);
174        }
175        for issue in self.check_secrets(source, &file_str) {
176            result.add_issue(issue);
177        }
178        for issue in self.check_unsafe_dom(source, &file_str) {
179            result.add_issue(issue);
180        }
181
182        Ok(result)
183    }
184
185    fn validate_raw(&self, source: &str, file: &Path) -> Result<ValidationResult> {
186        let parsed = ParsedCode::Generic(source.to_string());
187        self.validate_parsed(&parsed, file)
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    #[test]
196    fn test_security_validator_valid() {
197        let validator = SecurityValidator::default_config();
198        let code = r#"function Safe() { return <div>{content}</div>; }"#;
199        let result = validator.validate_raw(code, Path::new("Safe.tsx")).unwrap();
200        assert!(result.passed);
201    }
202
203    #[test]
204    fn test_sec_innerhtml() {
205        let validator = SecurityValidator::default_config();
206        let code = r#"<div dangerouslySetInnerHTML={{ __html: html }} />"#;
207        let result = validator.validate_raw(code, Path::new("Unsafe.tsx")).unwrap();
208        assert!(!result.passed);
209    }
210
211    #[test]
212    fn test_sec_eval() {
213        let validator = SecurityValidator::default_config();
214        let code = r#"eval(userInput)"#;
215        let result = validator.validate_raw(code, Path::new("Bad.ts")).unwrap();
216        assert!(!result.passed);
217    }
218
219    #[test]
220    fn test_sec_hardcoded_secret() {
221        let validator = SecurityValidator::default_config();
222        let code = r#"const apiKey = 'sk-1234567890abcdef1234567890abcdef'"#;
223        let result = validator.validate_raw(code, Path::new("config.ts")).unwrap();
224        assert!(!result.passed);
225    }
226
227    #[test]
228    fn test_security_validator_supports() {
229        let validator = SecurityValidator::default_config();
230        assert!(validator.supports(Language::JavaScript));
231        assert!(!validator.supports(Language::Rust));
232    }
233}