Skip to main content

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::{HashMap, 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    /// Custom headers to include in all requests (e.g., Cookie, X-Custom-Header)
90    #[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    /// Create a new configuration with default settings
156    pub fn new() -> Self {
157        Self::default()
158    }
159
160    /// Get the categories to test (all if none specified)
161    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    /// Check if a specific category should be tested
170    pub fn should_test_category(&self, category: OwaspCategory) -> bool {
171        self.categories.is_empty() || self.categories.contains(&category)
172    }
173
174    /// Load admin paths from file if specified
175    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    /// Get all admin paths (from file and explicit list)
189    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        // Add default admin paths if none specified
193        if paths.is_empty() {
194            paths.extend(DEFAULT_ADMIN_PATHS.iter().copied());
195        }
196
197        paths
198    }
199
200    /// Builder method to set categories
201    pub fn with_categories(mut self, categories: impl IntoIterator<Item = OwaspCategory>) -> Self {
202        self.categories = categories.into_iter().collect();
203        self
204    }
205
206    /// Builder method to set auth header
207    pub fn with_auth_header(mut self, header: impl Into<String>) -> Self {
208        self.auth_header = header.into();
209        self
210    }
211
212    /// Builder method to set valid auth token
213    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    /// Builder method to add admin paths
219    pub fn with_admin_paths(mut self, paths: impl IntoIterator<Item = String>) -> Self {
220        self.admin_paths.extend(paths);
221        self
222    }
223
224    /// Builder method to set ID fields
225    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    /// Builder method to set report path
231    pub fn with_report_path(mut self, path: impl Into<PathBuf>) -> Self {
232        self.report_path = path.into();
233        self
234    }
235
236    /// Builder method to set report format
237    pub fn with_report_format(mut self, format: ReportFormat) -> Self {
238        self.report_format = format;
239        self
240    }
241
242    /// Builder method to set verbosity
243    pub fn with_verbose(mut self, verbose: bool) -> Self {
244        self.verbose = verbose;
245        self
246    }
247
248    /// Builder method to set insecure TLS mode
249    pub fn with_insecure(mut self, insecure: bool) -> Self {
250        self.insecure = insecure;
251        self
252    }
253
254    /// Builder method to set concurrency (number of VUs)
255    pub fn with_concurrency(mut self, concurrency: usize) -> Self {
256        self.concurrency = concurrency;
257        self
258    }
259
260    /// Builder method to set iterations per VU
261    pub fn with_iterations(mut self, iterations: usize) -> Self {
262        self.iterations = iterations;
263        self
264    }
265
266    /// Builder method to set base path for API endpoints
267    pub fn with_base_path(mut self, base_path: Option<String>) -> Self {
268        self.base_path = base_path;
269        self
270    }
271
272    /// Builder method to set custom headers for all requests
273    pub fn with_custom_headers(mut self, headers: HashMap<String, String>) -> Self {
274        self.custom_headers = headers;
275        self
276    }
277}
278
279/// Authentication token with optional metadata
280#[derive(Debug, Clone, Serialize, Deserialize)]
281pub struct AuthToken {
282    /// The token value (including type prefix like "Bearer ")
283    pub value: String,
284    /// Role or description of this token
285    #[serde(default)]
286    pub role: Option<String>,
287    /// User ID associated with this token
288    #[serde(default)]
289    pub user_id: Option<String>,
290}
291
292impl AuthToken {
293    /// Create a new auth token
294    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    /// Create with role information
303    pub fn with_role(mut self, role: impl Into<String>) -> Self {
304        self.role = Some(role.into());
305        self
306    }
307
308    /// Create with user ID
309    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/// Report output format
316#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
317#[serde(rename_all = "lowercase")]
318pub enum ReportFormat {
319    /// JSON format with detailed findings
320    #[default]
321    Json,
322    /// SARIF format for IDE/CI integration
323    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/// Configuration for rate limit testing (API4)
339#[derive(Debug, Clone, Serialize, Deserialize)]
340pub struct RateLimitConfig {
341    /// Number of rapid requests to send
342    #[serde(default = "default_burst_size")]
343    pub burst_size: usize,
344    /// Maximum pagination limit to test
345    #[serde(default = "default_max_limit")]
346    pub max_limit: usize,
347    /// Large payload size in bytes for resource exhaustion
348    #[serde(default = "default_large_payload_size")]
349    pub large_payload_size: usize,
350    /// Maximum nesting depth for JSON bodies
351    #[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 // 10MB
365}
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/// Configuration for SSRF testing (API7)
383#[derive(Debug, Clone, Serialize, Deserialize)]
384pub struct SsrfConfig {
385    /// Internal URLs to test for SSRF
386    #[serde(default = "default_internal_urls")]
387    pub internal_urls: Vec<String>,
388    /// Cloud metadata URLs to test
389    #[serde(default = "default_metadata_urls")]
390    pub metadata_urls: Vec<String>,
391    /// Additional URL fields to test beyond defaults
392    #[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        // AWS
413        "http://169.254.169.254/latest/meta-data/".to_string(),
414        "http://169.254.169.254/latest/user-data/".to_string(),
415        // GCP
416        "http://metadata.google.internal/computeMetadata/v1/".to_string(),
417        // Azure
418        "http://169.254.169.254/metadata/instance".to_string(),
419        // DigitalOcean
420        "http://169.254.169.254/metadata/v1/".to_string(),
421        // Alibaba Cloud
422        "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/// Configuration for endpoint discovery (API9)
448#[derive(Debug, Clone, Serialize, Deserialize)]
449pub struct DiscoveryConfig {
450    /// API versions to probe
451    #[serde(default = "default_api_versions")]
452    pub api_versions: Vec<String>,
453    /// Common debug/internal endpoints to discover
454    #[serde(default = "default_discovery_paths")]
455    pub discovery_paths: Vec<String>,
456    /// Check for deprecated endpoints
457    #[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
520/// Default admin paths to test for privilege escalation
521pub 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}