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
103 "*.min.js".to_string(),
105 "*.min.css".to_string(),
106 "*.bundle.js".to_string(),
107 "*.bundle.css".to_string(),
108 "*.chunk.js".to_string(),
109 "*.vendor.js".to_string(),
110 "*.map".to_string(),
111
112 "*.lock".to_string(),
114 "*.lockb".to_string(),
115 "yarn.lock".to_string(),
116 "package-lock.json".to_string(),
117 "pnpm-lock.yaml".to_string(),
118 "bun.lockb".to_string(),
119 "cargo.lock".to_string(),
120 "go.sum".to_string(),
121 "poetry.lock".to_string(),
122 "composer.lock".to_string(),
123 "gemfile.lock".to_string(),
124
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
146 "*_sample.*".to_string(),
148 "*example*".to_string(),
149 "*test*".to_string(),
150 "*spec*".to_string(),
151 "*mock*".to_string(),
152 "*fixture*".to_string(),
153 "test/*".to_string(),
154 "tests/*".to_string(),
155 "__test__/*".to_string(),
156 "__tests__/*".to_string(),
157 "spec/*".to_string(),
158 "specs/*".to_string(),
159
160 "*.md".to_string(),
162 "*.txt".to_string(),
163 "*.rst".to_string(),
164 "docs/*".to_string(),
165 "documentation/*".to_string(),
166
167 ".vscode/*".to_string(),
169 ".idea/*".to_string(),
170 ".vs/*".to_string(),
171 "*.swp".to_string(),
172 "*.swo".to_string(),
173 ".DS_Store".to_string(),
174 "Thumbs.db".to_string(),
175
176 "*.d.ts".to_string(),
178 "*.generated.*".to_string(),
179 "*.auto.*".to_string(),
180
181 ".angular/*".to_string(),
183 ".svelte-kit/*".to_string(),
184 "storybook-static/*".to_string(),
185 ],
186 include_patterns: vec![], skip_gitignored_files: true,
190 downgrade_gitignored_severity: false,
191 check_git_history: false, check_env_files: true,
195 warn_on_public_env_vars: true,
196 sensitive_env_keywords: vec![
197 "SECRET".to_string(),
198 "KEY".to_string(),
199 "TOKEN".to_string(),
200 "PASSWORD".to_string(),
201 "PASS".to_string(),
202 "AUTH".to_string(),
203 "API".to_string(),
204 "PRIVATE".to_string(),
205 "CREDENTIAL".to_string(),
206 "CERT".to_string(),
207 "SSL".to_string(),
208 "TLS".to_string(),
209 "OAUTH".to_string(),
210 "CLIENT_SECRET".to_string(),
211 "ACCESS_TOKEN".to_string(),
212 "REFRESH_TOKEN".to_string(),
213 "DATABASE_URL".to_string(),
214 "DB_PASS".to_string(),
215 "STRIPE_SECRET".to_string(),
216 "AWS_SECRET".to_string(),
217 "FIREBASE_PRIVATE".to_string(),
218 ],
219
220 check_package_json: true,
222 check_node_modules: false, framework_env_prefixes: vec![
224 "REACT_APP_".to_string(),
225 "NEXT_PUBLIC_".to_string(),
226 "VITE_".to_string(),
227 "VUE_APP_".to_string(),
228 "EXPO_PUBLIC_".to_string(),
229 "NUXT_PUBLIC_".to_string(),
230 "GATSBY_".to_string(),
231 "STORYBOOK_".to_string(),
232 ],
233
234 max_findings_per_file: Some(50), deduplicate_findings: true,
237 group_by_severity: true,
238
239 max_file_size_mb: Some(10), parallel_analysis: true,
242 analysis_timeout_seconds: Some(300), }
244 }
245}
246
247impl SecurityAnalysisConfig {
248 pub fn for_javascript() -> Self {
250 let mut config = Self::default();
251 config.javascript_enabled = true;
252 config.python_enabled = false;
253 config.rust_enabled = false;
254 config.check_package_json = true;
255 config.frameworks_to_check = vec![
256 "React".to_string(),
257 "Vue".to_string(),
258 "Angular".to_string(),
259 "Next.js".to_string(),
260 "Vite".to_string(),
261 "Express".to_string(),
262 "Svelte".to_string(),
263 "Nuxt".to_string(),
264 ];
265 config
266 }
267
268 pub fn for_python() -> Self {
270 let mut config = Self::default();
271 config.javascript_enabled = false;
272 config.python_enabled = true;
273 config.rust_enabled = false;
274 config.check_package_json = false;
275 config.frameworks_to_check = vec![
276 "Django".to_string(),
277 "Flask".to_string(),
278 "FastAPI".to_string(),
279 "Tornado".to_string(),
280 ];
281 config
282 }
283
284 pub fn high_security() -> Self {
286 let mut config = Self::default();
287 config.include_low_severity = true;
288 config.include_info_level = true;
289 config.skip_gitignored_files = false; config.check_git_history = true;
291 config.warn_on_public_env_vars = true;
292 config.max_findings_per_file = None; config
294 }
295
296 pub fn fast_ci() -> Self {
298 let mut config = Self::default();
299 config.include_low_severity = false;
300 config.include_info_level = false;
301 config.check_compliance = false;
302 config.check_git_history = false;
303 config.parallel_analysis = true;
304 config.max_findings_per_file = Some(20); config.analysis_timeout_seconds = Some(120); config
307 }
308
309 pub fn should_analyze_file(&self, file_path: &std::path::Path) -> bool {
311 let file_path_str = file_path.to_string_lossy();
312 let file_name = file_path.file_name()
313 .and_then(|n| n.to_str())
314 .unwrap_or("");
315
316 for pattern in &self.ignore_patterns {
318 if self.matches_pattern(pattern, &file_path_str, file_name) {
319 return false;
320 }
321 }
322
323 if !self.include_patterns.is_empty() {
325 return self.include_patterns.iter().any(|pattern| {
326 self.matches_pattern(pattern, &file_path_str, file_name)
327 });
328 }
329
330 true
331 }
332
333 fn matches_pattern(&self, pattern: &str, file_path: &str, file_name: &str) -> bool {
335 if pattern.contains('*') {
336 glob::Pattern::new(pattern)
338 .map(|p| p.matches(file_path) || p.matches(file_name))
339 .unwrap_or(false)
340 } else {
341 file_path.contains(pattern) || file_name.contains(pattern)
343 }
344 }
345
346 pub fn is_sensitive_env_var(&self, var_name: &str) -> bool {
348 let var_upper = var_name.to_uppercase();
349 self.sensitive_env_keywords.iter()
350 .any(|keyword| var_upper.contains(keyword))
351 }
352
353 pub fn is_public_env_var(&self, var_name: &str) -> bool {
355 self.framework_env_prefixes.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}