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