1use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct SecurityAnalysisConfig {
10 pub include_low_severity: bool,
12 pub include_info_level: bool,
13
14 pub check_secrets: bool,
16 pub check_code_patterns: bool,
17 pub check_infrastructure: bool,
18 pub check_compliance: bool,
19
20 pub javascript_enabled: bool,
22 pub python_enabled: bool,
23 pub rust_enabled: bool,
24
25 pub frameworks_to_check: Vec<String>,
27
28 pub ignore_patterns: Vec<String>,
30 pub include_patterns: Vec<String>,
31
32 pub skip_gitignored_files: bool,
34 pub downgrade_gitignored_severity: bool,
35 pub check_git_history: bool,
36
37 pub check_env_files: bool,
39 pub warn_on_public_env_vars: bool,
40 pub sensitive_env_keywords: Vec<String>,
41
42 pub check_package_json: bool,
44 pub check_node_modules: bool,
45 pub framework_env_prefixes: Vec<String>,
46
47 pub max_findings_per_file: Option<usize>,
49 pub deduplicate_findings: bool,
50 pub group_by_severity: bool,
51
52 pub max_file_size_mb: Option<usize>,
54 pub parallel_analysis: bool,
55 pub analysis_timeout_seconds: Option<u64>,
56}
57
58impl Default for SecurityAnalysisConfig {
59 fn default() -> Self {
60 Self {
61 include_low_severity: false,
63 include_info_level: false,
64
65 check_secrets: true,
67 check_code_patterns: true,
68 check_infrastructure: true,
69 check_compliance: false, javascript_enabled: true,
73 python_enabled: true,
74 rust_enabled: true,
75
76 frameworks_to_check: vec![
78 "React".to_string(),
79 "Vue".to_string(),
80 "Angular".to_string(),
81 "Next.js".to_string(),
82 "Vite".to_string(),
83 "Express".to_string(),
84 "Django".to_string(),
85 "Spring Boot".to_string(),
86 ],
87
88 ignore_patterns: vec![
90 "node_modules".to_string(),
92 ".git".to_string(),
93 "target".to_string(),
94 "build".to_string(),
95 ".next".to_string(),
96 "coverage".to_string(),
97 "dist".to_string(),
98 ".nuxt".to_string(),
99 ".output".to_string(),
100 ".vercel".to_string(),
101 ".netlify".to_string(),
102 "venv/".to_string(),
104 ".venv/".to_string(),
105 "*.min.js".to_string(),
107 "*.min.css".to_string(),
108 "*.bundle.js".to_string(),
109 "*.bundle.css".to_string(),
110 "*.chunk.js".to_string(),
111 "*.vendor.js".to_string(),
112 "*.map".to_string(),
113 "*.lock".to_string(),
115 "*.lockb".to_string(),
116 "yarn.lock".to_string(),
117 "package-lock.json".to_string(),
118 "pnpm-lock.yaml".to_string(),
119 "bun.lockb".to_string(),
120 "cargo.lock".to_string(),
121 "go.sum".to_string(),
122 "poetry.lock".to_string(),
123 "composer.lock".to_string(),
124 "gemfile.lock".to_string(),
125 "*.jpg".to_string(),
127 "*.jpeg".to_string(),
128 "*.png".to_string(),
129 "*.gif".to_string(),
130 "*.bmp".to_string(),
131 "*.svg".to_string(),
132 "*.ico".to_string(),
133 "*.webp".to_string(),
134 "*.tiff".to_string(),
135 "*.mp3".to_string(),
136 "*.mp4".to_string(),
137 "*.avi".to_string(),
138 "*.mov".to_string(),
139 "*.pdf".to_string(),
140 "*.ttf".to_string(),
141 "*.otf".to_string(),
142 "*.woff".to_string(),
143 "*.woff2".to_string(),
144 "*.eot".to_string(),
145 "*.wt".to_string(),
147 "*.cer".to_string(),
148 "*.jks".to_string(),
149 "*_sample.*".to_string(),
151 "*example*".to_string(),
152 "*test*".to_string(),
153 "*spec*".to_string(),
154 "*mock*".to_string(),
155 "*fixture*".to_string(),
156 "test/*".to_string(),
157 "tests/*".to_string(),
158 "__test__/*".to_string(),
159 "__tests__/*".to_string(),
160 "spec/*".to_string(),
161 "specs/*".to_string(),
162 "*.md".to_string(),
164 "*.txt".to_string(),
165 "*.rst".to_string(),
166 "docs/*".to_string(),
167 "documentation/*".to_string(),
168 ".vscode/*".to_string(),
170 ".idea/*".to_string(),
171 ".vs/*".to_string(),
172 "*.swp".to_string(),
173 "*.swo".to_string(),
174 ".DS_Store".to_string(),
175 "Thumbs.db".to_string(),
176 "*.d.ts".to_string(),
178 "*.generated.*".to_string(),
179 "*.auto.*".to_string(),
180 ".angular/*".to_string(),
182 ".svelte-kit/*".to_string(),
183 "storybook-static/*".to_string(),
184 ],
185 include_patterns: vec![], skip_gitignored_files: true,
189 downgrade_gitignored_severity: false,
190 check_git_history: false, check_env_files: true,
194 warn_on_public_env_vars: true,
195 sensitive_env_keywords: vec![
196 "SECRET".to_string(),
197 "KEY".to_string(),
198 "TOKEN".to_string(),
199 "PASSWORD".to_string(),
200 "PASS".to_string(),
201 "AUTH".to_string(),
202 "API".to_string(),
203 "PRIVATE".to_string(),
204 "CREDENTIAL".to_string(),
205 "CERT".to_string(),
206 "SSL".to_string(),
207 "TLS".to_string(),
208 "OAUTH".to_string(),
209 "CLIENT_SECRET".to_string(),
210 "ACCESS_TOKEN".to_string(),
211 "REFRESH_TOKEN".to_string(),
212 "DATABASE_URL".to_string(),
213 "DB_PASS".to_string(),
214 "STRIPE_SECRET".to_string(),
215 "AWS_SECRET".to_string(),
216 "FIREBASE_PRIVATE".to_string(),
217 ],
218
219 check_package_json: true,
221 check_node_modules: false, framework_env_prefixes: vec![
223 "REACT_APP_".to_string(),
224 "NEXT_PUBLIC_".to_string(),
225 "VITE_".to_string(),
226 "VUE_APP_".to_string(),
227 "EXPO_PUBLIC_".to_string(),
228 "NUXT_PUBLIC_".to_string(),
229 "GATSBY_".to_string(),
230 "STORYBOOK_".to_string(),
231 ],
232
233 max_findings_per_file: Some(50), deduplicate_findings: true,
236 group_by_severity: true,
237
238 max_file_size_mb: Some(10), parallel_analysis: true,
241 analysis_timeout_seconds: Some(300), }
243 }
244}
245
246impl SecurityAnalysisConfig {
247 pub fn for_javascript() -> Self {
249 let mut config = Self::default();
250 config.javascript_enabled = true;
251 config.python_enabled = false;
252 config.rust_enabled = false;
253 config.check_package_json = true;
254 config.frameworks_to_check = vec![
255 "React".to_string(),
256 "Vue".to_string(),
257 "Angular".to_string(),
258 "Next.js".to_string(),
259 "Vite".to_string(),
260 "Express".to_string(),
261 "Svelte".to_string(),
262 "Nuxt".to_string(),
263 ];
264 config
265 }
266
267 pub fn for_python() -> Self {
269 let mut config = Self::default();
270 config.javascript_enabled = false;
271 config.python_enabled = true;
272 config.rust_enabled = false;
273 config.check_package_json = false;
274 config.frameworks_to_check = vec![
275 "Django".to_string(),
276 "Flask".to_string(),
277 "FastAPI".to_string(),
278 "Tornado".to_string(),
279 ];
280 config
281 }
282
283 pub fn high_security() -> Self {
285 let mut config = Self::default();
286 config.include_low_severity = true;
287 config.include_info_level = true;
288 config.skip_gitignored_files = false; config.check_git_history = true;
290 config.warn_on_public_env_vars = true;
291 config.max_findings_per_file = None; config
293 }
294
295 pub fn fast_ci() -> Self {
297 let mut config = Self::default();
298 config.include_low_severity = false;
299 config.include_info_level = false;
300 config.check_compliance = false;
301 config.check_git_history = false;
302 config.parallel_analysis = true;
303 config.max_findings_per_file = Some(20); config.analysis_timeout_seconds = Some(120); config
306 }
307
308 pub fn should_analyze_file(&self, file_path: &std::path::Path) -> bool {
310 let file_path_str = file_path.to_string_lossy();
311 let file_name = file_path.file_name().and_then(|n| n.to_str()).unwrap_or("");
312
313 for pattern in &self.ignore_patterns {
315 if self.matches_pattern(pattern, &file_path_str, file_name) {
316 return false;
317 }
318 }
319
320 if !self.include_patterns.is_empty() {
322 return self
323 .include_patterns
324 .iter()
325 .any(|pattern| self.matches_pattern(pattern, &file_path_str, file_name));
326 }
327
328 true
329 }
330
331 fn matches_pattern(&self, pattern: &str, file_path: &str, file_name: &str) -> bool {
333 if pattern.contains('*') {
334 glob::Pattern::new(pattern)
336 .map(|p| p.matches(file_path) || p.matches(file_name))
337 .unwrap_or(false)
338 } else {
339 file_path.contains(pattern) || file_name.contains(pattern)
341 }
342 }
343
344 pub fn is_sensitive_env_var(&self, var_name: &str) -> bool {
346 let var_upper = var_name.to_uppercase();
347 self.sensitive_env_keywords
348 .iter()
349 .any(|keyword| var_upper.contains(keyword))
350 }
351
352 pub fn is_public_env_var(&self, var_name: &str) -> bool {
354 self.framework_env_prefixes
355 .iter()
356 .any(|prefix| var_name.starts_with(prefix))
357 }
358
359 pub fn max_file_size_bytes(&self) -> Option<usize> {
361 self.max_file_size_mb.map(|mb| mb * 1024 * 1024)
362 }
363}
364
365#[derive(Debug, Clone, Copy)]
367pub enum SecurityConfigPreset {
368 Default,
370 JavaScript,
372 Python,
374 HighSecurity,
376 FastCI,
378}
379
380impl SecurityConfigPreset {
381 pub fn to_config(self) -> SecurityAnalysisConfig {
382 match self {
383 Self::Default => SecurityAnalysisConfig::default(),
384 Self::JavaScript => SecurityAnalysisConfig::for_javascript(),
385 Self::Python => SecurityAnalysisConfig::for_python(),
386 Self::HighSecurity => SecurityAnalysisConfig::high_security(),
387 Self::FastCI => SecurityAnalysisConfig::fast_ci(),
388 }
389 }
390}
391
392impl From<SecurityConfigPreset> for SecurityAnalysisConfig {
393 fn from(preset: SecurityConfigPreset) -> Self {
394 preset.to_config()
395 }
396}