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
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    /// Create a new configuration with default settings
146    pub fn new() -> Self {
147        Self::default()
148    }
149
150    /// Get the categories to test (all if none specified)
151    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    /// Check if a specific category should be tested
160    pub fn should_test_category(&self, category: OwaspCategory) -> bool {
161        self.categories.is_empty() || self.categories.contains(&category)
162    }
163
164    /// Load admin paths from file if specified
165    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    /// Get all admin paths (from file and explicit list)
179    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        // Add default admin paths if none specified
183        if paths.is_empty() {
184            paths.extend(DEFAULT_ADMIN_PATHS.iter().copied());
185        }
186
187        paths
188    }
189
190    /// Builder method to set categories
191    pub fn with_categories(mut self, categories: impl IntoIterator<Item = OwaspCategory>) -> Self {
192        self.categories = categories.into_iter().collect();
193        self
194    }
195
196    /// Builder method to set auth header
197    pub fn with_auth_header(mut self, header: impl Into<String>) -> Self {
198        self.auth_header = header.into();
199        self
200    }
201
202    /// Builder method to set valid auth token
203    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    /// Builder method to add admin paths
209    pub fn with_admin_paths(mut self, paths: impl IntoIterator<Item = String>) -> Self {
210        self.admin_paths.extend(paths);
211        self
212    }
213
214    /// Builder method to set ID fields
215    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    /// Builder method to set report path
221    pub fn with_report_path(mut self, path: impl Into<PathBuf>) -> Self {
222        self.report_path = path.into();
223        self
224    }
225
226    /// Builder method to set report format
227    pub fn with_report_format(mut self, format: ReportFormat) -> Self {
228        self.report_format = format;
229        self
230    }
231
232    /// Builder method to set verbosity
233    pub fn with_verbose(mut self, verbose: bool) -> Self {
234        self.verbose = verbose;
235        self
236    }
237
238    /// Builder method to set insecure TLS mode
239    pub fn with_insecure(mut self, insecure: bool) -> Self {
240        self.insecure = insecure;
241        self
242    }
243
244    /// Builder method to set concurrency (number of VUs)
245    pub fn with_concurrency(mut self, concurrency: usize) -> Self {
246        self.concurrency = concurrency;
247        self
248    }
249
250    /// Builder method to set iterations per VU
251    pub fn with_iterations(mut self, iterations: usize) -> Self {
252        self.iterations = iterations;
253        self
254    }
255}
256
257/// Authentication token with optional metadata
258#[derive(Debug, Clone, Serialize, Deserialize)]
259pub struct AuthToken {
260    /// The token value (including type prefix like "Bearer ")
261    pub value: String,
262    /// Role or description of this token
263    #[serde(default)]
264    pub role: Option<String>,
265    /// User ID associated with this token
266    #[serde(default)]
267    pub user_id: Option<String>,
268}
269
270impl AuthToken {
271    /// Create a new auth token
272    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    /// Create with role information
281    pub fn with_role(mut self, role: impl Into<String>) -> Self {
282        self.role = Some(role.into());
283        self
284    }
285
286    /// Create with user ID
287    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/// Report output format
294#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
295#[serde(rename_all = "lowercase")]
296pub enum ReportFormat {
297    /// JSON format with detailed findings
298    #[default]
299    Json,
300    /// SARIF format for IDE/CI integration
301    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/// Configuration for rate limit testing (API4)
317#[derive(Debug, Clone, Serialize, Deserialize)]
318pub struct RateLimitConfig {
319    /// Number of rapid requests to send
320    #[serde(default = "default_burst_size")]
321    pub burst_size: usize,
322    /// Maximum pagination limit to test
323    #[serde(default = "default_max_limit")]
324    pub max_limit: usize,
325    /// Large payload size in bytes for resource exhaustion
326    #[serde(default = "default_large_payload_size")]
327    pub large_payload_size: usize,
328    /// Maximum nesting depth for JSON bodies
329    #[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 // 10MB
343}
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/// Configuration for SSRF testing (API7)
361#[derive(Debug, Clone, Serialize, Deserialize)]
362pub struct SsrfConfig {
363    /// Internal URLs to test for SSRF
364    #[serde(default = "default_internal_urls")]
365    pub internal_urls: Vec<String>,
366    /// Cloud metadata URLs to test
367    #[serde(default = "default_metadata_urls")]
368    pub metadata_urls: Vec<String>,
369    /// Additional URL fields to test beyond defaults
370    #[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        // AWS
391        "http://169.254.169.254/latest/meta-data/".to_string(),
392        "http://169.254.169.254/latest/user-data/".to_string(),
393        // GCP
394        "http://metadata.google.internal/computeMetadata/v1/".to_string(),
395        // Azure
396        "http://169.254.169.254/metadata/instance".to_string(),
397        // DigitalOcean
398        "http://169.254.169.254/metadata/v1/".to_string(),
399        // Alibaba Cloud
400        "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/// Configuration for endpoint discovery (API9)
426#[derive(Debug, Clone, Serialize, Deserialize)]
427pub struct DiscoveryConfig {
428    /// API versions to probe
429    #[serde(default = "default_api_versions")]
430    pub api_versions: Vec<String>,
431    /// Common debug/internal endpoints to discover
432    #[serde(default = "default_discovery_paths")]
433    pub discovery_paths: Vec<String>,
434    /// Check for deprecated endpoints
435    #[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
498/// Default admin paths to test for privilege escalation
499pub 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}