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