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