mockforge_bench/owasp_api/
config.rs

1//! OWASP API Security Top 10 Configuration
2//!
3//! This module defines the configuration for running OWASP API Top 10 security tests.
4
5use super::categories::{OwaspCategory, Severity};
6use serde::{Deserialize, Serialize};
7use std::collections::HashSet;
8use std::path::PathBuf;
9
10/// Configuration for OWASP API Security Top 10 testing
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct OwaspApiConfig {
13    /// Categories to test (empty = all categories)
14    #[serde(default)]
15    pub categories: HashSet<OwaspCategory>,
16
17    /// Authorization header name for auth bypass tests
18    #[serde(default = "default_auth_header")]
19    pub auth_header: String,
20
21    /// File containing admin/privileged paths to test
22    #[serde(default)]
23    pub admin_paths_file: Option<PathBuf>,
24
25    /// List of admin/privileged paths to test
26    #[serde(default)]
27    pub admin_paths: Vec<String>,
28
29    /// Fields containing resource IDs for BOLA testing
30    #[serde(default = "default_id_fields")]
31    pub id_fields: Vec<String>,
32
33    /// Valid authorization token for baseline requests
34    #[serde(default)]
35    pub valid_auth_token: Option<String>,
36
37    /// Alternative authorization tokens for testing (e.g., different user roles)
38    #[serde(default)]
39    pub alt_auth_tokens: Vec<AuthToken>,
40
41    /// Output report file path
42    #[serde(default = "default_report_path")]
43    pub report_path: PathBuf,
44
45    /// Report format (json, sarif)
46    #[serde(default)]
47    pub report_format: ReportFormat,
48
49    /// Minimum severity level to report
50    #[serde(default)]
51    pub min_severity: Severity,
52
53    /// Rate limiting configuration for API4 tests
54    #[serde(default)]
55    pub rate_limit_config: RateLimitConfig,
56
57    /// SSRF-specific configuration for API7 tests
58    #[serde(default)]
59    pub ssrf_config: SsrfConfig,
60
61    /// Discovery-specific configuration for API9 tests
62    #[serde(default)]
63    pub discovery_config: DiscoveryConfig,
64
65    /// Enable verbose output during testing
66    #[serde(default)]
67    pub verbose: bool,
68
69    /// Number of concurrent test requests
70    #[serde(default = "default_concurrency")]
71    pub concurrency: usize,
72
73    /// Request timeout in milliseconds
74    #[serde(default = "default_timeout")]
75    pub timeout_ms: u64,
76
77    /// Skip TLS certificate verification (for testing with self-signed certs)
78    #[serde(default)]
79    pub insecure: bool,
80
81    /// Number of iterations per VU (default: 1)
82    #[serde(default = "default_iterations")]
83    pub iterations: usize,
84
85    /// Base path to prepend to all API paths (e.g., "/api")
86    #[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    /// Create a new configuration with default settings
151    pub fn new() -> Self {
152        Self::default()
153    }
154
155    /// Get the categories to test (all if none specified)
156    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    /// Check if a specific category should be tested
165    pub fn should_test_category(&self, category: OwaspCategory) -> bool {
166        self.categories.is_empty() || self.categories.contains(&category)
167    }
168
169    /// Load admin paths from file if specified
170    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    /// Get all admin paths (from file and explicit list)
184    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        // Add default admin paths if none specified
188        if paths.is_empty() {
189            paths.extend(DEFAULT_ADMIN_PATHS.iter().copied());
190        }
191
192        paths
193    }
194
195    /// Builder method to set categories
196    pub fn with_categories(mut self, categories: impl IntoIterator<Item = OwaspCategory>) -> Self {
197        self.categories = categories.into_iter().collect();
198        self
199    }
200
201    /// Builder method to set auth header
202    pub fn with_auth_header(mut self, header: impl Into<String>) -> Self {
203        self.auth_header = header.into();
204        self
205    }
206
207    /// Builder method to set valid auth token
208    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    /// Builder method to add admin paths
214    pub fn with_admin_paths(mut self, paths: impl IntoIterator<Item = String>) -> Self {
215        self.admin_paths.extend(paths);
216        self
217    }
218
219    /// Builder method to set ID fields
220    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    /// Builder method to set report path
226    pub fn with_report_path(mut self, path: impl Into<PathBuf>) -> Self {
227        self.report_path = path.into();
228        self
229    }
230
231    /// Builder method to set report format
232    pub fn with_report_format(mut self, format: ReportFormat) -> Self {
233        self.report_format = format;
234        self
235    }
236
237    /// Builder method to set verbosity
238    pub fn with_verbose(mut self, verbose: bool) -> Self {
239        self.verbose = verbose;
240        self
241    }
242
243    /// Builder method to set insecure TLS mode
244    pub fn with_insecure(mut self, insecure: bool) -> Self {
245        self.insecure = insecure;
246        self
247    }
248
249    /// Builder method to set concurrency (number of VUs)
250    pub fn with_concurrency(mut self, concurrency: usize) -> Self {
251        self.concurrency = concurrency;
252        self
253    }
254
255    /// Builder method to set iterations per VU
256    pub fn with_iterations(mut self, iterations: usize) -> Self {
257        self.iterations = iterations;
258        self
259    }
260
261    /// Builder method to set base path for API endpoints
262    pub fn with_base_path(mut self, base_path: Option<String>) -> Self {
263        self.base_path = base_path;
264        self
265    }
266}
267
268/// Authentication token with optional metadata
269#[derive(Debug, Clone, Serialize, Deserialize)]
270pub struct AuthToken {
271    /// The token value (including type prefix like "Bearer ")
272    pub value: String,
273    /// Role or description of this token
274    #[serde(default)]
275    pub role: Option<String>,
276    /// User ID associated with this token
277    #[serde(default)]
278    pub user_id: Option<String>,
279}
280
281impl AuthToken {
282    /// Create a new auth token
283    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    /// Create with role information
292    pub fn with_role(mut self, role: impl Into<String>) -> Self {
293        self.role = Some(role.into());
294        self
295    }
296
297    /// Create with user ID
298    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/// Report output format
305#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
306#[serde(rename_all = "lowercase")]
307pub enum ReportFormat {
308    /// JSON format with detailed findings
309    #[default]
310    Json,
311    /// SARIF format for IDE/CI integration
312    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/// Configuration for rate limit testing (API4)
328#[derive(Debug, Clone, Serialize, Deserialize)]
329pub struct RateLimitConfig {
330    /// Number of rapid requests to send
331    #[serde(default = "default_burst_size")]
332    pub burst_size: usize,
333    /// Maximum pagination limit to test
334    #[serde(default = "default_max_limit")]
335    pub max_limit: usize,
336    /// Large payload size in bytes for resource exhaustion
337    #[serde(default = "default_large_payload_size")]
338    pub large_payload_size: usize,
339    /// Maximum nesting depth for JSON bodies
340    #[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 // 10MB
354}
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/// Configuration for SSRF testing (API7)
372#[derive(Debug, Clone, Serialize, Deserialize)]
373pub struct SsrfConfig {
374    /// Internal URLs to test for SSRF
375    #[serde(default = "default_internal_urls")]
376    pub internal_urls: Vec<String>,
377    /// Cloud metadata URLs to test
378    #[serde(default = "default_metadata_urls")]
379    pub metadata_urls: Vec<String>,
380    /// Additional URL fields to test beyond defaults
381    #[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        // AWS
402        "http://169.254.169.254/latest/meta-data/".to_string(),
403        "http://169.254.169.254/latest/user-data/".to_string(),
404        // GCP
405        "http://metadata.google.internal/computeMetadata/v1/".to_string(),
406        // Azure
407        "http://169.254.169.254/metadata/instance".to_string(),
408        // DigitalOcean
409        "http://169.254.169.254/metadata/v1/".to_string(),
410        // Alibaba Cloud
411        "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/// Configuration for endpoint discovery (API9)
437#[derive(Debug, Clone, Serialize, Deserialize)]
438pub struct DiscoveryConfig {
439    /// API versions to probe
440    #[serde(default = "default_api_versions")]
441    pub api_versions: Vec<String>,
442    /// Common debug/internal endpoints to discover
443    #[serde(default = "default_discovery_paths")]
444    pub discovery_paths: Vec<String>,
445    /// Check for deprecated endpoints
446    #[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
509/// Default admin paths to test for privilege escalation
510pub 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}