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