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 Self {
250 javascript_enabled: true,
251 python_enabled: false,
252 rust_enabled: false,
253 check_package_json: true,
254 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 ..Self::default()
265 }
266 }
267
268 pub fn for_python() -> Self {
270 Self {
271 javascript_enabled: false,
272 python_enabled: true,
273 rust_enabled: false,
274 check_package_json: false,
275 frameworks_to_check: vec![
276 "Django".to_string(),
277 "Flask".to_string(),
278 "FastAPI".to_string(),
279 "Tornado".to_string(),
280 ],
281 ..Self::default()
282 }
283 }
284
285 pub fn high_security() -> Self {
287 Self {
288 include_low_severity: true,
289 include_info_level: true,
290 skip_gitignored_files: false, check_git_history: true,
292 warn_on_public_env_vars: true,
293 max_findings_per_file: None, ..Self::default()
295 }
296 }
297
298 pub fn fast_ci() -> Self {
300 Self {
301 include_low_severity: false,
302 include_info_level: false,
303 check_compliance: false,
304 check_git_history: false,
305 parallel_analysis: true,
306 max_findings_per_file: Some(20), analysis_timeout_seconds: Some(120), ..Self::default()
309 }
310 }
311
312 pub fn should_analyze_file(&self, file_path: &std::path::Path) -> bool {
314 let file_path_str = file_path.to_string_lossy();
315 let file_name = file_path.file_name().and_then(|n| n.to_str()).unwrap_or("");
316
317 for pattern in &self.ignore_patterns {
319 if self.matches_pattern(pattern, &file_path_str, file_name) {
320 return false;
321 }
322 }
323
324 if !self.include_patterns.is_empty() {
326 return self
327 .include_patterns
328 .iter()
329 .any(|pattern| self.matches_pattern(pattern, &file_path_str, file_name));
330 }
331
332 true
333 }
334
335 fn matches_pattern(&self, pattern: &str, file_path: &str, file_name: &str) -> bool {
337 if pattern.contains('*') {
338 glob::Pattern::new(pattern)
340 .map(|p| p.matches(file_path) || p.matches(file_name))
341 .unwrap_or(false)
342 } else {
343 file_path.contains(pattern) || file_name.contains(pattern)
345 }
346 }
347
348 pub fn is_sensitive_env_var(&self, var_name: &str) -> bool {
350 let var_upper = var_name.to_uppercase();
351 self.sensitive_env_keywords
352 .iter()
353 .any(|keyword| var_upper.contains(keyword))
354 }
355
356 pub fn is_public_env_var(&self, var_name: &str) -> bool {
358 self.framework_env_prefixes
359 .iter()
360 .any(|prefix| var_name.starts_with(prefix))
361 }
362
363 pub fn max_file_size_bytes(&self) -> Option<usize> {
365 self.max_file_size_mb.map(|mb| mb * 1024 * 1024)
366 }
367}
368
369#[derive(Debug, Clone, Copy)]
371pub enum SecurityConfigPreset {
372 Default,
374 JavaScript,
376 Python,
378 HighSecurity,
380 FastCI,
382}
383
384impl SecurityConfigPreset {
385 pub fn to_config(self) -> SecurityAnalysisConfig {
386 match self {
387 Self::Default => SecurityAnalysisConfig::default(),
388 Self::JavaScript => SecurityAnalysisConfig::for_javascript(),
389 Self::Python => SecurityAnalysisConfig::for_python(),
390 Self::HighSecurity => SecurityAnalysisConfig::high_security(),
391 Self::FastCI => SecurityAnalysisConfig::fast_ci(),
392 }
393 }
394}
395
396impl From<SecurityConfigPreset> for SecurityAnalysisConfig {
397 fn from(preset: SecurityConfigPreset) -> Self {
398 preset.to_config()
399 }
400}