1use super::categories::{OwaspCategory, Severity};
6use serde::{Deserialize, Serialize};
7use std::collections::HashSet;
8use std::path::PathBuf;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct OwaspApiConfig {
13 #[serde(default)]
15 pub categories: HashSet<OwaspCategory>,
16
17 #[serde(default = "default_auth_header")]
19 pub auth_header: String,
20
21 #[serde(default)]
23 pub admin_paths_file: Option<PathBuf>,
24
25 #[serde(default)]
27 pub admin_paths: Vec<String>,
28
29 #[serde(default = "default_id_fields")]
31 pub id_fields: Vec<String>,
32
33 #[serde(default)]
35 pub valid_auth_token: Option<String>,
36
37 #[serde(default)]
39 pub alt_auth_tokens: Vec<AuthToken>,
40
41 #[serde(default = "default_report_path")]
43 pub report_path: PathBuf,
44
45 #[serde(default)]
47 pub report_format: ReportFormat,
48
49 #[serde(default)]
51 pub min_severity: Severity,
52
53 #[serde(default)]
55 pub rate_limit_config: RateLimitConfig,
56
57 #[serde(default)]
59 pub ssrf_config: SsrfConfig,
60
61 #[serde(default)]
63 pub discovery_config: DiscoveryConfig,
64
65 #[serde(default)]
67 pub verbose: bool,
68
69 #[serde(default = "default_concurrency")]
71 pub concurrency: usize,
72
73 #[serde(default = "default_timeout")]
75 pub timeout_ms: u64,
76
77 #[serde(default)]
79 pub insecure: bool,
80
81 #[serde(default = "default_iterations")]
83 pub iterations: usize,
84
85 #[serde(default)]
87 pub base_path: Option<String>,
88}
89
90fn default_auth_header() -> String {
91 "Authorization".to_string()
92}
93
94fn default_id_fields() -> Vec<String> {
95 vec![
96 "id".to_string(),
97 "uuid".to_string(),
98 "user_id".to_string(),
99 "userId".to_string(),
100 "account_id".to_string(),
101 "accountId".to_string(),
102 "resource_id".to_string(),
103 "resourceId".to_string(),
104 ]
105}
106
107fn default_report_path() -> PathBuf {
108 PathBuf::from("owasp-report.json")
109}
110
111fn default_concurrency() -> usize {
112 10
113}
114
115fn default_iterations() -> usize {
116 1
117}
118
119fn default_timeout() -> u64 {
120 30000
121}
122
123impl Default for OwaspApiConfig {
124 fn default() -> Self {
125 Self {
126 categories: HashSet::new(),
127 auth_header: default_auth_header(),
128 admin_paths_file: None,
129 admin_paths: Vec::new(),
130 id_fields: default_id_fields(),
131 valid_auth_token: None,
132 alt_auth_tokens: Vec::new(),
133 report_path: default_report_path(),
134 report_format: ReportFormat::default(),
135 min_severity: Severity::Low,
136 rate_limit_config: RateLimitConfig::default(),
137 ssrf_config: SsrfConfig::default(),
138 discovery_config: DiscoveryConfig::default(),
139 verbose: false,
140 concurrency: default_concurrency(),
141 timeout_ms: default_timeout(),
142 insecure: false,
143 iterations: default_iterations(),
144 base_path: None,
145 }
146 }
147}
148
149impl OwaspApiConfig {
150 pub fn new() -> Self {
152 Self::default()
153 }
154
155 pub fn categories_to_test(&self) -> Vec<OwaspCategory> {
157 if self.categories.is_empty() {
158 OwaspCategory::all()
159 } else {
160 self.categories.iter().copied().collect()
161 }
162 }
163
164 pub fn should_test_category(&self, category: OwaspCategory) -> bool {
166 self.categories.is_empty() || self.categories.contains(&category)
167 }
168
169 pub fn load_admin_paths(&mut self) -> Result<(), std::io::Error> {
171 if let Some(ref path) = self.admin_paths_file {
172 let content = std::fs::read_to_string(path)?;
173 for line in content.lines() {
174 let trimmed = line.trim();
175 if !trimmed.is_empty() && !trimmed.starts_with('#') {
176 self.admin_paths.push(trimmed.to_string());
177 }
178 }
179 }
180 Ok(())
181 }
182
183 pub fn all_admin_paths(&self) -> Vec<&str> {
185 let mut paths: Vec<&str> = self.admin_paths.iter().map(String::as_str).collect();
186
187 if paths.is_empty() {
189 paths.extend(DEFAULT_ADMIN_PATHS.iter().copied());
190 }
191
192 paths
193 }
194
195 pub fn with_categories(mut self, categories: impl IntoIterator<Item = OwaspCategory>) -> Self {
197 self.categories = categories.into_iter().collect();
198 self
199 }
200
201 pub fn with_auth_header(mut self, header: impl Into<String>) -> Self {
203 self.auth_header = header.into();
204 self
205 }
206
207 pub fn with_valid_auth_token(mut self, token: impl Into<String>) -> Self {
209 self.valid_auth_token = Some(token.into());
210 self
211 }
212
213 pub fn with_admin_paths(mut self, paths: impl IntoIterator<Item = String>) -> Self {
215 self.admin_paths.extend(paths);
216 self
217 }
218
219 pub fn with_id_fields(mut self, fields: impl IntoIterator<Item = String>) -> Self {
221 self.id_fields = fields.into_iter().collect();
222 self
223 }
224
225 pub fn with_report_path(mut self, path: impl Into<PathBuf>) -> Self {
227 self.report_path = path.into();
228 self
229 }
230
231 pub fn with_report_format(mut self, format: ReportFormat) -> Self {
233 self.report_format = format;
234 self
235 }
236
237 pub fn with_verbose(mut self, verbose: bool) -> Self {
239 self.verbose = verbose;
240 self
241 }
242
243 pub fn with_insecure(mut self, insecure: bool) -> Self {
245 self.insecure = insecure;
246 self
247 }
248
249 pub fn with_concurrency(mut self, concurrency: usize) -> Self {
251 self.concurrency = concurrency;
252 self
253 }
254
255 pub fn with_iterations(mut self, iterations: usize) -> Self {
257 self.iterations = iterations;
258 self
259 }
260
261 pub fn with_base_path(mut self, base_path: Option<String>) -> Self {
263 self.base_path = base_path;
264 self
265 }
266}
267
268#[derive(Debug, Clone, Serialize, Deserialize)]
270pub struct AuthToken {
271 pub value: String,
273 #[serde(default)]
275 pub role: Option<String>,
276 #[serde(default)]
278 pub user_id: Option<String>,
279}
280
281impl AuthToken {
282 pub fn new(value: impl Into<String>) -> Self {
284 Self {
285 value: value.into(),
286 role: None,
287 user_id: None,
288 }
289 }
290
291 pub fn with_role(mut self, role: impl Into<String>) -> Self {
293 self.role = Some(role.into());
294 self
295 }
296
297 pub fn with_user_id(mut self, user_id: impl Into<String>) -> Self {
299 self.user_id = Some(user_id.into());
300 self
301 }
302}
303
304#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
306#[serde(rename_all = "lowercase")]
307pub enum ReportFormat {
308 #[default]
310 Json,
311 Sarif,
313}
314
315impl std::str::FromStr for ReportFormat {
316 type Err = String;
317
318 fn from_str(s: &str) -> Result<Self, Self::Err> {
319 match s.to_lowercase().as_str() {
320 "json" => Ok(Self::Json),
321 "sarif" => Ok(Self::Sarif),
322 _ => Err(format!("Unknown report format: '{}'. Valid values: json, sarif", s)),
323 }
324 }
325}
326
327#[derive(Debug, Clone, Serialize, Deserialize)]
329pub struct RateLimitConfig {
330 #[serde(default = "default_burst_size")]
332 pub burst_size: usize,
333 #[serde(default = "default_max_limit")]
335 pub max_limit: usize,
336 #[serde(default = "default_large_payload_size")]
338 pub large_payload_size: usize,
339 #[serde(default = "default_max_nesting")]
341 pub max_nesting_depth: usize,
342}
343
344fn default_burst_size() -> usize {
345 100
346}
347
348fn default_max_limit() -> usize {
349 100000
350}
351
352fn default_large_payload_size() -> usize {
353 10 * 1024 * 1024 }
355
356fn default_max_nesting() -> usize {
357 100
358}
359
360impl Default for RateLimitConfig {
361 fn default() -> Self {
362 Self {
363 burst_size: default_burst_size(),
364 max_limit: default_max_limit(),
365 large_payload_size: default_large_payload_size(),
366 max_nesting_depth: default_max_nesting(),
367 }
368 }
369}
370
371#[derive(Debug, Clone, Serialize, Deserialize)]
373pub struct SsrfConfig {
374 #[serde(default = "default_internal_urls")]
376 pub internal_urls: Vec<String>,
377 #[serde(default = "default_metadata_urls")]
379 pub metadata_urls: Vec<String>,
380 #[serde(default)]
382 pub url_fields: Vec<String>,
383}
384
385fn default_internal_urls() -> Vec<String> {
386 vec![
387 "http://localhost/".to_string(),
388 "http://127.0.0.1/".to_string(),
389 "http://[::1]/".to_string(),
390 "http://0.0.0.0/".to_string(),
391 "http://localhost:8080/".to_string(),
392 "http://localhost:3000/".to_string(),
393 "http://localhost:9000/".to_string(),
394 "http://internal/".to_string(),
395 "http://backend/".to_string(),
396 ]
397}
398
399fn default_metadata_urls() -> Vec<String> {
400 vec![
401 "http://169.254.169.254/latest/meta-data/".to_string(),
403 "http://169.254.169.254/latest/user-data/".to_string(),
404 "http://metadata.google.internal/computeMetadata/v1/".to_string(),
406 "http://169.254.169.254/metadata/instance".to_string(),
408 "http://169.254.169.254/metadata/v1/".to_string(),
410 "http://100.100.100.200/latest/meta-data/".to_string(),
412 ]
413}
414
415impl Default for SsrfConfig {
416 fn default() -> Self {
417 Self {
418 internal_urls: default_internal_urls(),
419 metadata_urls: default_metadata_urls(),
420 url_fields: vec![
421 "url".to_string(),
422 "uri".to_string(),
423 "link".to_string(),
424 "href".to_string(),
425 "callback".to_string(),
426 "redirect".to_string(),
427 "return_url".to_string(),
428 "webhook".to_string(),
429 "image_url".to_string(),
430 "fetch_url".to_string(),
431 ],
432 }
433 }
434}
435
436#[derive(Debug, Clone, Serialize, Deserialize)]
438pub struct DiscoveryConfig {
439 #[serde(default = "default_api_versions")]
441 pub api_versions: Vec<String>,
442 #[serde(default = "default_discovery_paths")]
444 pub discovery_paths: Vec<String>,
445 #[serde(default = "default_true")]
447 pub check_deprecated: bool,
448}
449
450fn default_api_versions() -> Vec<String> {
451 vec![
452 "v1".to_string(),
453 "v2".to_string(),
454 "v3".to_string(),
455 "v4".to_string(),
456 "api/v1".to_string(),
457 "api/v2".to_string(),
458 "api/v3".to_string(),
459 ]
460}
461
462fn default_discovery_paths() -> Vec<String> {
463 vec![
464 "/swagger".to_string(),
465 "/swagger-ui".to_string(),
466 "/swagger.json".to_string(),
467 "/swagger.yaml".to_string(),
468 "/api-docs".to_string(),
469 "/openapi".to_string(),
470 "/openapi.json".to_string(),
471 "/openapi.yaml".to_string(),
472 "/graphql".to_string(),
473 "/graphiql".to_string(),
474 "/playground".to_string(),
475 "/debug".to_string(),
476 "/debug/".to_string(),
477 "/actuator".to_string(),
478 "/actuator/health".to_string(),
479 "/actuator/info".to_string(),
480 "/actuator/env".to_string(),
481 "/metrics".to_string(),
482 "/health".to_string(),
483 "/healthz".to_string(),
484 "/status".to_string(),
485 "/info".to_string(),
486 "/.env".to_string(),
487 "/config".to_string(),
488 "/admin".to_string(),
489 "/internal".to_string(),
490 "/test".to_string(),
491 "/dev".to_string(),
492 ]
493}
494
495fn default_true() -> bool {
496 true
497}
498
499impl Default for DiscoveryConfig {
500 fn default() -> Self {
501 Self {
502 api_versions: default_api_versions(),
503 discovery_paths: default_discovery_paths(),
504 check_deprecated: default_true(),
505 }
506 }
507}
508
509pub const DEFAULT_ADMIN_PATHS: &[&str] = &[
511 "/admin",
512 "/admin/",
513 "/admin/users",
514 "/admin/settings",
515 "/admin/config",
516 "/api/admin",
517 "/api/admin/",
518 "/api/admin/users",
519 "/api/v1/admin",
520 "/api/v2/admin",
521 "/management",
522 "/manage",
523 "/internal",
524 "/internal/",
525 "/system",
526 "/system/config",
527 "/settings",
528 "/config",
529 "/users/admin",
530];
531
532#[cfg(test)]
533mod tests {
534 use super::*;
535
536 #[test]
537 fn test_default_config() {
538 let config = OwaspApiConfig::default();
539 assert!(config.categories.is_empty());
540 assert_eq!(config.auth_header, "Authorization");
541 assert!(!config.id_fields.is_empty());
542 }
543
544 #[test]
545 fn test_categories_to_test() {
546 let config = OwaspApiConfig::default();
547 assert_eq!(config.categories_to_test().len(), 10);
548
549 let config = OwaspApiConfig::default()
550 .with_categories([OwaspCategory::Api1Bola, OwaspCategory::Api7Ssrf]);
551 assert_eq!(config.categories_to_test().len(), 2);
552 }
553
554 #[test]
555 fn test_should_test_category() {
556 let config = OwaspApiConfig::default();
557 assert!(config.should_test_category(OwaspCategory::Api1Bola));
558
559 let config = OwaspApiConfig::default().with_categories([OwaspCategory::Api1Bola]);
560 assert!(config.should_test_category(OwaspCategory::Api1Bola));
561 assert!(!config.should_test_category(OwaspCategory::Api2BrokenAuth));
562 }
563
564 #[test]
565 fn test_builder_pattern() {
566 let config = OwaspApiConfig::new()
567 .with_auth_header("X-Auth-Token")
568 .with_valid_auth_token("secret123")
569 .with_verbose(true);
570
571 assert_eq!(config.auth_header, "X-Auth-Token");
572 assert_eq!(config.valid_auth_token, Some("secret123".to_string()));
573 assert!(config.verbose);
574 }
575
576 #[test]
577 fn test_report_format_from_str() {
578 assert_eq!("json".parse::<ReportFormat>().unwrap(), ReportFormat::Json);
579 assert_eq!("sarif".parse::<ReportFormat>().unwrap(), ReportFormat::Sarif);
580 assert!("invalid".parse::<ReportFormat>().is_err());
581 }
582}