veracode_platform/
sandbox.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5use crate::json_validator::{MAX_JSON_DEPTH, validate_json_depth};
6use crate::validation::validate_url_segment;
7use crate::{VeracodeClient, VeracodeError};
8
9/// Maximum page size for pagination to prevent memory exhaustion attacks
10const MAX_PAGE_SIZE: u64 = 500;
11
12/// API error response structure
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct ApiErrorResponse {
15    #[serde(rename = "_embedded")]
16    pub embedded: Option<ApiErrorEmbedded>,
17    pub fallback_type: Option<String>,
18    pub full_type: Option<String>,
19}
20
21/// Embedded API errors
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct ApiErrorEmbedded {
24    pub api_errors: Vec<ApiError>,
25}
26
27/// Individual API error
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct ApiError {
30    pub id: String,
31    pub code: String,
32    pub title: String,
33    pub status: String,
34    pub source: Option<ApiErrorSource>,
35}
36
37/// API error source information
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct ApiErrorSource {
40    pub pointer: String,
41    pub parameter: String,
42}
43
44/// Represents a Veracode development sandbox
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct Sandbox {
47    pub id: Option<u64>,
48    pub guid: String,
49    pub name: String,
50    pub description: Option<String>,
51    pub created: DateTime<Utc>,
52    pub modified: DateTime<Utc>,
53    pub auto_recreate: bool,
54    pub custom_fields: Option<HashMap<String, String>>,
55    pub owner: Option<String>,
56    pub owner_username: Option<String>,
57    pub organization_id: Option<u64>,
58    pub application_guid: Option<String>,
59    pub team_identifiers: Option<Vec<String>>,
60    pub scan_url: Option<String>,
61    pub last_scan_date: Option<DateTime<Utc>>,
62    pub status: Option<String>,
63    #[serde(rename = "_links")]
64    pub links: Option<serde_json::Value>,
65}
66
67/// Request payload for creating a new sandbox
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct CreateSandboxRequest {
70    pub name: String,
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub description: Option<String>,
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub auto_recreate: Option<bool>,
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub custom_fields: Option<HashMap<String, String>>,
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub team_identifiers: Option<Vec<String>>,
79}
80
81/// Request payload for updating an existing sandbox
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct UpdateSandboxRequest {
84    pub name: Option<String>,
85    pub description: Option<String>,
86    pub auto_recreate: Option<bool>,
87    pub custom_fields: Option<HashMap<String, String>>,
88    pub team_identifiers: Option<Vec<String>>,
89}
90
91/// Response wrapper for sandbox list operations
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct SandboxListResponse {
94    #[serde(rename = "_embedded")]
95    pub embedded: Option<SandboxEmbedded>,
96    pub page: Option<PageInfo>,
97    pub total: Option<u64>,
98}
99
100/// Embedded sandboxes in the list response
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct SandboxEmbedded {
103    pub sandboxes: Vec<Sandbox>,
104}
105
106/// Page information for paginated responses
107#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct PageInfo {
109    pub size: u64,
110    pub number: u64,
111    pub total_elements: u64,
112    pub total_pages: u64,
113}
114
115/// Represents a scan within a sandbox
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct SandboxScan {
118    pub scan_id: u64,
119    pub scan_type: String,
120    pub status: String,
121    pub created: DateTime<Utc>,
122    pub modified: DateTime<Utc>,
123    pub scan_url: Option<String>,
124    pub results_ready: bool,
125    pub engine_version: Option<String>,
126}
127
128/// Query parameters for listing sandboxes
129#[derive(Debug, Clone, Default)]
130pub struct SandboxListParams {
131    pub name: Option<String>,
132    pub owner: Option<String>,
133    pub team: Option<String>,
134    pub page: Option<u64>,
135    pub size: Option<u64>,
136    pub modified_after: Option<DateTime<Utc>>,
137    pub modified_before: Option<DateTime<Utc>>,
138}
139
140impl SandboxListParams {
141    /// Convert to query parameters for HTTP requests
142    #[must_use]
143    pub fn to_query_params(&self) -> Vec<(String, String)> {
144        Vec::from(self) // Delegate to trait
145    }
146}
147
148// Trait implementations for memory optimization
149impl From<&SandboxListParams> for Vec<(String, String)> {
150    fn from(query: &SandboxListParams) -> Self {
151        let mut params = Vec::new();
152
153        if let Some(ref name) = query.name {
154            params.push(("name".to_string(), name.clone())); // Still clone for borrowing
155        }
156        if let Some(ref owner) = query.owner {
157            params.push(("owner".to_string(), owner.clone()));
158        }
159        if let Some(ref team) = query.team {
160            params.push(("team".to_string(), team.clone()));
161        }
162        if let Some(page) = query.page {
163            params.push(("page".to_string(), page.to_string()));
164        }
165        if let Some(size) = query.size {
166            // Cap page size at MAX_PAGE_SIZE to prevent memory exhaustion
167            let safe_size = size.min(MAX_PAGE_SIZE);
168            params.push(("size".to_string(), safe_size.to_string()));
169        }
170        if let Some(modified_after) = query.modified_after {
171            params.push(("modified_after".to_string(), modified_after.to_rfc3339()));
172        }
173        if let Some(modified_before) = query.modified_before {
174            params.push(("modified_before".to_string(), modified_before.to_rfc3339()));
175        }
176
177        params
178    }
179}
180
181impl From<SandboxListParams> for Vec<(String, String)> {
182    fn from(query: SandboxListParams) -> Self {
183        let mut params = Vec::new();
184
185        if let Some(name) = query.name {
186            params.push(("name".to_string(), name)); // MOVE - no clone!
187        }
188        if let Some(owner) = query.owner {
189            params.push(("owner".to_string(), owner)); // MOVE - no clone!
190        }
191        if let Some(team) = query.team {
192            params.push(("team".to_string(), team)); // MOVE - no clone!
193        }
194        if let Some(page) = query.page {
195            params.push(("page".to_string(), page.to_string()));
196        }
197        if let Some(size) = query.size {
198            // Cap page size at MAX_PAGE_SIZE to prevent memory exhaustion
199            let safe_size = size.min(MAX_PAGE_SIZE);
200            params.push(("size".to_string(), safe_size.to_string()));
201        }
202        if let Some(modified_after) = query.modified_after {
203            params.push(("modified_after".to_string(), modified_after.to_rfc3339()));
204        }
205        if let Some(modified_before) = query.modified_before {
206            params.push(("modified_before".to_string(), modified_before.to_rfc3339()));
207        }
208
209        params
210    }
211}
212
213///
214/// # Errors
215///
216/// Returns an error if the API request fails, the resource is not found,
217/// or authentication/authorization fails.
218/// Sandbox-specific error types that extend the base `VeracodeError`
219#[derive(Debug)]
220#[must_use = "Need to handle all error enum types."]
221pub enum SandboxError {
222    /// Veracode API error
223    Api(VeracodeError),
224    /// Sandbox not found
225    NotFound,
226    /// Invalid sandbox name or configuration
227    InvalidInput(String),
228    /// Maximum number of sandboxes reached
229    LimitExceeded,
230    /// Sandbox operation not allowed
231    OperationNotAllowed(String),
232    /// Sandbox already exists
233    AlreadyExists(String),
234}
235
236impl std::fmt::Display for SandboxError {
237    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
238        match self {
239            SandboxError::Api(err) => write!(f, "API error: {err}"),
240            SandboxError::NotFound => write!(f, "Sandbox not found"),
241            SandboxError::InvalidInput(msg) => write!(f, "Invalid input: {msg}"),
242            SandboxError::LimitExceeded => write!(f, "Maximum number of sandboxes reached"),
243            SandboxError::OperationNotAllowed(msg) => write!(f, "Operation not allowed: {msg}"),
244            SandboxError::AlreadyExists(msg) => write!(f, "Sandbox already exists: {msg}"),
245        }
246    }
247}
248
249impl std::error::Error for SandboxError {}
250
251impl From<VeracodeError> for SandboxError {
252    fn from(err: VeracodeError) -> Self {
253        SandboxError::Api(err)
254    }
255}
256
257impl From<reqwest::Error> for SandboxError {
258    fn from(err: reqwest::Error) -> Self {
259        SandboxError::Api(VeracodeError::Http(err))
260    }
261}
262
263impl From<serde_json::Error> for SandboxError {
264    fn from(err: serde_json::Error) -> Self {
265        SandboxError::Api(VeracodeError::Serialization(err))
266    }
267}
268
269/// Veracode Sandbox API operations
270pub struct SandboxApi<'a> {
271    client: &'a VeracodeClient,
272}
273
274impl<'a> SandboxApi<'a> {
275    ///
276    /// # Errors
277    ///
278    /// Returns an error if the API request fails, the resource is not found,
279    /// or authentication/authorization fails.
280    /// Create a new `SandboxApi` instance
281    #[must_use]
282    pub fn new(client: &'a VeracodeClient) -> Self {
283        Self { client }
284    }
285
286    /// List all sandboxes for a given application
287    ///
288    /// # Arguments
289    ///
290    /// * `application_guid` - The GUID of the application
291    /// * `params` - Optional query parameters for filtering
292    ///
293    /// # Returns
294    ///
295    /// A `Result` containing a list of sandboxes or an error.
296    ///
297    /// # Errors
298    ///
299    /// Returns an error if the API request fails, the sandbox is not found,
300    /// or authentication/authorization fails.
301    pub async fn list_sandboxes(
302        &self,
303        application_guid: &str,
304        params: Option<SandboxListParams>,
305    ) -> Result<Vec<Sandbox>, SandboxError> {
306        let endpoint = format!("/appsec/v1/applications/{application_guid}/sandboxes");
307
308        let query_params = params.as_ref().map(Vec::from);
309
310        let response = self.client.get(&endpoint, query_params.as_deref()).await?;
311
312        let status = response.status().as_u16();
313        match status {
314            200 => {
315                let response_text = response.text().await?;
316
317                // Validate JSON depth before parsing to prevent DoS attacks
318                if validate_json_depth(&response_text, MAX_JSON_DEPTH).is_err() {
319                    return Err(SandboxError::Api(VeracodeError::InvalidResponse(
320                        "JSON validation failed on response".to_string(),
321                    )));
322                }
323
324                let sandbox_response: SandboxListResponse = serde_json::from_str(&response_text)?;
325                Ok(sandbox_response
326                    .embedded
327                    .map(|e| e.sandboxes)
328                    .unwrap_or_default())
329            }
330            404 => Err(SandboxError::NotFound),
331            _ => {
332                let error_text = response.text().await.unwrap_or_default();
333                Err(SandboxError::Api(VeracodeError::InvalidResponse(format!(
334                    "HTTP {status}: {error_text}"
335                ))))
336            }
337        }
338    }
339
340    /// Get a specific sandbox by GUID
341    ///
342    /// # Arguments
343    ///
344    /// * `application_guid` - The GUID of the application
345    /// * `sandbox_guid` - The GUID of the sandbox
346    ///
347    /// # Returns
348    ///
349    /// A `Result` containing the sandbox or an error.
350    ///
351    /// # Errors
352    ///
353    /// Returns an error if the API request fails, the sandbox is not found,
354    /// or authentication/authorization fails.
355    pub async fn get_sandbox(
356        &self,
357        application_guid: &str,
358        sandbox_guid: &str,
359    ) -> Result<Sandbox, SandboxError> {
360        let endpoint =
361            format!("/appsec/v1/applications/{application_guid}/sandboxes/{sandbox_guid}");
362
363        let response = self.client.get(&endpoint, None).await?;
364
365        let status = response.status().as_u16();
366        match status {
367            200 => {
368                let response_text = response.text().await?;
369
370                // Validate JSON depth before parsing to prevent DoS attacks
371                if validate_json_depth(&response_text, MAX_JSON_DEPTH).is_err() {
372                    return Err(SandboxError::Api(VeracodeError::InvalidResponse(
373                        "JSON validation failed on response".to_string(),
374                    )));
375                }
376
377                let sandbox: Sandbox = serde_json::from_str(&response_text)?;
378                Ok(sandbox)
379            }
380            404 => Err(SandboxError::NotFound),
381            _ => {
382                let error_text = response.text().await.unwrap_or_default();
383                Err(SandboxError::Api(VeracodeError::InvalidResponse(format!(
384                    "HTTP {status}: {error_text}"
385                ))))
386            }
387        }
388    }
389
390    /// Create a new sandbox
391    ///
392    /// # Arguments
393    ///
394    /// * `application_guid` - The GUID of the application
395    /// * `request` - The sandbox creation request
396    ///
397    /// # Returns
398    ///
399    /// A `Result` containing the created sandbox or an error.
400    ///
401    /// # Errors
402    ///
403    /// Returns an error if the API request fails, the sandbox is not found,
404    /// or authentication/authorization fails.
405    pub async fn create_sandbox(
406        &self,
407        application_guid: &str,
408        request: CreateSandboxRequest,
409    ) -> Result<Sandbox, SandboxError> {
410        // Validate the request
411        Self::validate_create_request(&request)?;
412
413        let endpoint = format!("/appsec/v1/applications/{application_guid}/sandboxes");
414
415        let response = self.client.post(&endpoint, Some(&request)).await?;
416
417        let status = response.status().as_u16();
418        match status {
419            200 | 201 => {
420                let sandbox: Sandbox = response.json().await?;
421                Ok(sandbox)
422            }
423            400 => {
424                let error_text = response.text().await.unwrap_or_default();
425
426                // Validate JSON depth before parsing to prevent DoS attacks
427                if validate_json_depth(&error_text, MAX_JSON_DEPTH).is_err() {
428                    return Err(SandboxError::Api(VeracodeError::InvalidResponse(
429                        "JSON validation failed on error response".to_string(),
430                    )));
431                }
432
433                // Try to parse the structured error response
434                if let Ok(error_response) = serde_json::from_str::<ApiErrorResponse>(&error_text)
435                    && let Some(embedded) = error_response.embedded
436                {
437                    for api_error in embedded.api_errors {
438                        if api_error.title.contains("already exists") {
439                            return Err(SandboxError::AlreadyExists(api_error.title));
440                        }
441                        if api_error.title.contains("limit") || api_error.title.contains("maximum")
442                        {
443                            return Err(SandboxError::LimitExceeded);
444                        }
445                        if api_error.title.contains("Json Parse Error")
446                            || api_error.title.contains("Cannot deserialize")
447                        {
448                            return Err(SandboxError::InvalidInput(format!(
449                                "JSON parsing error: {}",
450                                api_error.title
451                            )));
452                        }
453                    }
454                }
455
456                // Fallback to string matching for backwards compatibility
457                if error_text.contains("limit") || error_text.contains("maximum") {
458                    Err(SandboxError::LimitExceeded)
459                } else if error_text.contains("already exists") {
460                    Err(SandboxError::AlreadyExists(error_text))
461                } else {
462                    Err(SandboxError::InvalidInput(error_text))
463                }
464            }
465            404 => Err(SandboxError::NotFound),
466            _ => {
467                let error_text = response.text().await.unwrap_or_default();
468                Err(SandboxError::Api(VeracodeError::InvalidResponse(format!(
469                    "HTTP {status}: {error_text}"
470                ))))
471            }
472        }
473    }
474
475    /// Update an existing sandbox
476    ///
477    /// # Arguments
478    ///
479    /// * `application_guid` - The GUID of the application
480    /// * `sandbox_guid` - The GUID of the sandbox to update
481    /// * `request` - The sandbox update request
482    ///
483    /// # Returns
484    ///
485    /// A `Result` containing the updated sandbox or an error.
486    ///
487    /// # Errors
488    ///
489    /// Returns an error if the API request fails, the sandbox is not found,
490    /// or authentication/authorization fails.
491    pub async fn update_sandbox(
492        &self,
493        application_guid: &str,
494        sandbox_guid: &str,
495        request: UpdateSandboxRequest,
496    ) -> Result<Sandbox, SandboxError> {
497        // Validate the request
498        Self::validate_update_request(&request)?;
499
500        let endpoint =
501            format!("/appsec/v1/applications/{application_guid}/sandboxes/{sandbox_guid}");
502
503        let response = self.client.put(&endpoint, Some(&request)).await?;
504
505        let status = response.status().as_u16();
506        match status {
507            200 => {
508                let response_text = response.text().await?;
509
510                // Validate JSON depth before parsing to prevent DoS attacks
511                if validate_json_depth(&response_text, MAX_JSON_DEPTH).is_err() {
512                    return Err(SandboxError::Api(VeracodeError::InvalidResponse(
513                        "JSON validation failed on response".to_string(),
514                    )));
515                }
516
517                let sandbox: Sandbox = serde_json::from_str(&response_text)?;
518                Ok(sandbox)
519            }
520            400 => {
521                let error_text = response.text().await.unwrap_or_default();
522                Err(SandboxError::InvalidInput(error_text))
523            }
524            404 => Err(SandboxError::NotFound),
525            _ => {
526                let error_text = response.text().await.unwrap_or_default();
527                Err(SandboxError::Api(VeracodeError::InvalidResponse(format!(
528                    "HTTP {status}: {error_text}"
529                ))))
530            }
531        }
532    }
533
534    /// Delete a sandbox
535    ///
536    /// # Arguments
537    ///
538    /// * `application_guid` - The GUID of the application
539    /// * `sandbox_guid` - The GUID of the sandbox to delete
540    ///
541    /// # Returns
542    ///
543    /// A `Result` indicating success or failure.
544    ///
545    /// # Errors
546    ///
547    /// Returns an error if the API request fails, the sandbox is not found,
548    /// or authentication/authorization fails.
549    pub async fn delete_sandbox(
550        &self,
551        application_guid: &str,
552        sandbox_guid: &str,
553    ) -> Result<(), SandboxError> {
554        let endpoint =
555            format!("/appsec/v1/applications/{application_guid}/sandboxes/{sandbox_guid}");
556
557        let response = self.client.delete(&endpoint).await?;
558
559        let status = response.status().as_u16();
560        match status {
561            204 => Ok(()),
562            404 => Err(SandboxError::NotFound),
563            409 => {
564                let error_text = response.text().await.unwrap_or_default();
565                Err(SandboxError::OperationNotAllowed(error_text))
566            }
567            _ => {
568                let error_text = response.text().await.unwrap_or_default();
569                Err(SandboxError::Api(VeracodeError::InvalidResponse(format!(
570                    "HTTP {status}: {error_text}"
571                ))))
572            }
573        }
574    }
575
576    /// Promote a sandbox scan to the policy sandbox
577    ///
578    /// # Arguments
579    ///
580    /// * `application_guid` - The GUID of the application
581    /// * `sandbox_guid` - The GUID of the sandbox to promote
582    /// * `delete_on_promote` - Whether to delete the sandbox after promotion
583    ///
584    /// # Returns
585    ///
586    /// A `Result` indicating success or failure.
587    ///
588    /// # Errors
589    ///
590    /// Returns an error if the API request fails, the sandbox is not found,
591    /// or authentication/authorization fails.
592    pub async fn promote_sandbox_scan(
593        &self,
594        application_guid: &str,
595        sandbox_guid: &str,
596        delete_on_promote: bool,
597    ) -> Result<(), SandboxError> {
598        let endpoint = if delete_on_promote {
599            format!(
600                "/appsec/v1/applications/{application_guid}/sandboxes/{sandbox_guid}/promote?delete_on_promote=true"
601            )
602        } else {
603            format!("/appsec/v1/applications/{application_guid}/sandboxes/{sandbox_guid}/promote")
604        };
605
606        let response = self.client.post(&endpoint, None::<&()>).await?;
607
608        let status = response.status().as_u16();
609        match status {
610            200 | 204 => Ok(()),
611            404 => Err(SandboxError::NotFound),
612            409 => {
613                let error_text = response.text().await.unwrap_or_default();
614                Err(SandboxError::OperationNotAllowed(error_text))
615            }
616            _ => {
617                let error_text = response.text().await.unwrap_or_default();
618                Err(SandboxError::Api(VeracodeError::InvalidResponse(format!(
619                    "HTTP {status}: {error_text}"
620                ))))
621            }
622        }
623    }
624
625    /// Get sandbox scan information
626    ///
627    /// # Arguments
628    ///
629    /// * `application_guid` - The GUID of the application
630    /// * `sandbox_guid` - The GUID of the sandbox
631    ///
632    /// # Returns
633    ///
634    /// A `Result` containing a list of scans or an error.
635    ///
636    /// # Errors
637    ///
638    /// Returns an error if the API request fails, the sandbox is not found,
639    /// or authentication/authorization fails.
640    pub async fn get_sandbox_scans(
641        &self,
642        application_guid: &str,
643        sandbox_guid: &str,
644    ) -> Result<Vec<SandboxScan>, SandboxError> {
645        let endpoint =
646            format!("/appsec/v1/applications/{application_guid}/sandboxes/{sandbox_guid}/scans");
647
648        let response = self.client.get(&endpoint, None).await?;
649
650        let status = response.status().as_u16();
651        match status {
652            200 => {
653                let response_text = response.text().await?;
654
655                // Validate JSON depth before parsing to prevent DoS attacks
656                if validate_json_depth(&response_text, MAX_JSON_DEPTH).is_err() {
657                    return Err(SandboxError::Api(VeracodeError::InvalidResponse(
658                        "JSON validation failed on response".to_string(),
659                    )));
660                }
661
662                let scans: Vec<SandboxScan> = serde_json::from_str(&response_text)?;
663                Ok(scans)
664            }
665            404 => Err(SandboxError::NotFound),
666            _ => {
667                let error_text = response.text().await.unwrap_or_default();
668                Err(SandboxError::Api(VeracodeError::InvalidResponse(format!(
669                    "HTTP {status}: {error_text}"
670                ))))
671            }
672        }
673    }
674
675    /// Check if a sandbox exists
676    ///
677    /// # Arguments
678    ///
679    /// * `application_guid` - The GUID of the application
680    /// * `sandbox_guid` - The GUID of the sandbox
681    ///
682    /// # Returns
683    ///
684    /// A `Result` containing a boolean indicating if the sandbox exists.
685    ///
686    /// # Errors
687    ///
688    /// Returns an error if the API request fails, the sandbox is not found,
689    /// or authentication/authorization fails.
690    pub async fn sandbox_exists(
691        &self,
692        application_guid: &str,
693        sandbox_guid: &str,
694    ) -> Result<bool, SandboxError> {
695        match self.get_sandbox(application_guid, sandbox_guid).await {
696            Ok(_) => Ok(true),
697            Err(SandboxError::NotFound) => Ok(false),
698            Err(e) => Err(e),
699        }
700    }
701
702    /// Get sandbox by name
703    ///
704    /// # Arguments
705    ///
706    /// * `application_guid` - The GUID of the application
707    /// * `name` - The name of the sandbox to find
708    ///
709    /// # Returns
710    ///
711    /// A `Result` containing the sandbox if found, or None if not found.
712    ///
713    /// # Errors
714    ///
715    /// Returns an error if the API request fails, the sandbox is not found,
716    /// or authentication/authorization fails.
717    pub async fn get_sandbox_by_name(
718        &self,
719        application_guid: &str,
720        name: &str,
721    ) -> Result<Option<Sandbox>, SandboxError> {
722        let params = SandboxListParams {
723            name: Some(name.to_string()),
724            ..Default::default()
725        };
726
727        let sandboxes = self.list_sandboxes(application_guid, Some(params)).await?;
728        Ok(sandboxes.into_iter().find(|s| s.name == name))
729    }
730
731    /// Validate sandbox name
732    fn validate_name(name: &str) -> Result<(), SandboxError> {
733        if name.is_empty() {
734            return Err(SandboxError::InvalidInput(
735                "Sandbox name cannot be empty".to_string(),
736            ));
737        }
738        if name.len() > 256 {
739            return Err(SandboxError::InvalidInput(
740                "Sandbox name too long (max 256 characters)".to_string(),
741            ));
742        }
743
744        // Check for invalid characters in name
745        if name.contains(['<', '>', '"', '&', '\'']) {
746            return Err(SandboxError::InvalidInput(
747                "Sandbox name contains invalid characters".to_string(),
748            ));
749        }
750
751        // Use shared validation from validation.rs to prevent path traversal and control characters
752        validate_url_segment(name, 256)
753            .map_err(|e| SandboxError::InvalidInput(format!("Invalid sandbox name: {}", e)))?;
754
755        Ok(())
756    }
757
758    /// Validate sandbox creation request
759    fn validate_create_request(request: &CreateSandboxRequest) -> Result<(), SandboxError> {
760        Self::validate_name(&request.name)?;
761
762        // Validate custom fields if present
763        if let Some(ref custom_fields) = request.custom_fields {
764            Self::validate_custom_fields(custom_fields)?;
765        }
766
767        // Validate team identifiers if present
768        if let Some(ref team_ids) = request.team_identifiers {
769            Self::validate_team_identifiers(team_ids)?;
770        }
771
772        Ok(())
773    }
774
775    /// Validate sandbox update request
776    fn validate_update_request(request: &UpdateSandboxRequest) -> Result<(), SandboxError> {
777        if let Some(name) = &request.name {
778            Self::validate_name(name)?;
779        }
780
781        // Validate custom fields if present
782        if let Some(ref custom_fields) = request.custom_fields {
783            Self::validate_custom_fields(custom_fields)?;
784        }
785
786        // Validate team identifiers if present
787        if let Some(ref team_ids) = request.team_identifiers {
788            Self::validate_team_identifiers(team_ids)?;
789        }
790
791        Ok(())
792    }
793
794    /// Validate custom fields
795    fn validate_custom_fields(custom_fields: &HashMap<String, String>) -> Result<(), SandboxError> {
796        const MAX_CUSTOM_FIELDS: usize = 50;
797        const MAX_KEY_LENGTH: usize = 128;
798        const MAX_VALUE_LENGTH: usize = 1024;
799
800        if custom_fields.len() > MAX_CUSTOM_FIELDS {
801            return Err(SandboxError::InvalidInput(format!(
802                "Too many custom fields (max {MAX_CUSTOM_FIELDS})"
803            )));
804        }
805
806        for (key, value) in custom_fields {
807            // Validate key
808            if key.is_empty() {
809                return Err(SandboxError::InvalidInput(
810                    "Custom field key cannot be empty".to_string(),
811                ));
812            }
813            if key.len() > MAX_KEY_LENGTH {
814                return Err(SandboxError::InvalidInput(format!(
815                    "Custom field key too long (max {MAX_KEY_LENGTH} characters)"
816                )));
817            }
818            if key.chars().any(|c| c.is_control()) {
819                return Err(SandboxError::InvalidInput(
820                    "Custom field key contains control characters".to_string(),
821                ));
822            }
823            if key.contains(['<', '>', '"', '&', '\'', '/', '\\']) {
824                return Err(SandboxError::InvalidInput(
825                    "Custom field key contains invalid characters".to_string(),
826                ));
827            }
828
829            // Validate value
830            if value.len() > MAX_VALUE_LENGTH {
831                return Err(SandboxError::InvalidInput(format!(
832                    "Custom field value too long (max {MAX_VALUE_LENGTH} characters)"
833                )));
834            }
835            if value
836                .chars()
837                .any(|c| c.is_control() && c != '\n' && c != '\r' && c != '\t')
838            {
839                return Err(SandboxError::InvalidInput(
840                    "Custom field value contains invalid control characters".to_string(),
841                ));
842            }
843        }
844
845        Ok(())
846    }
847
848    /// Validate team identifiers
849    fn validate_team_identifiers(team_ids: &[String]) -> Result<(), SandboxError> {
850        const MAX_TEAM_IDS: usize = 100;
851        const MAX_TEAM_ID_LENGTH: usize = 128;
852
853        if team_ids.len() > MAX_TEAM_IDS {
854            return Err(SandboxError::InvalidInput(format!(
855                "Too many team identifiers (max {MAX_TEAM_IDS})"
856            )));
857        }
858
859        for team_id in team_ids {
860            if team_id.is_empty() {
861                return Err(SandboxError::InvalidInput(
862                    "Team identifier cannot be empty".to_string(),
863                ));
864            }
865            if team_id.len() > MAX_TEAM_ID_LENGTH {
866                return Err(SandboxError::InvalidInput(format!(
867                    "Team identifier too long (max {MAX_TEAM_ID_LENGTH} characters)"
868                )));
869            }
870            if team_id.chars().any(|c| c.is_control()) {
871                return Err(SandboxError::InvalidInput(
872                    "Team identifier contains control characters".to_string(),
873                ));
874            }
875            if team_id.contains(['<', '>', '"', '&', '\'', '/', '\\']) {
876                return Err(SandboxError::InvalidInput(
877                    "Team identifier contains invalid characters".to_string(),
878                ));
879            }
880        }
881
882        Ok(())
883    }
884}
885
886/// Convenience methods for common sandbox operations
887impl<'a> SandboxApi<'a> {
888    /// Create a simple sandbox with just a name
889    ///
890    /// # Arguments
891    ///
892    /// * `application_guid` - The GUID of the application
893    /// * `name` - The name of the sandbox
894    ///
895    /// # Returns
896    ///
897    /// A `Result` containing the created sandbox or an error.
898    ///
899    /// # Errors
900    ///
901    /// Returns an error if the API request fails, the sandbox is not found,
902    /// or authentication/authorization fails.
903    pub async fn create_simple_sandbox(
904        &self,
905        application_guid: &str,
906        name: &str,
907    ) -> Result<Sandbox, SandboxError> {
908        let request = CreateSandboxRequest {
909            name: name.to_string(),
910            description: None,
911            auto_recreate: None,
912            custom_fields: None,
913            team_identifiers: None,
914        };
915
916        self.create_sandbox(application_guid, request).await
917    }
918
919    /// Create a sandbox with auto-recreate enabled
920    ///
921    /// # Arguments
922    ///
923    /// * `application_guid` - The GUID of the application
924    /// * `name` - The name of the sandbox
925    /// * `description` - Optional description
926    ///
927    /// # Returns
928    ///
929    /// A `Result` containing the created sandbox or an error.
930    ///
931    /// # Errors
932    ///
933    /// Returns an error if the API request fails, the sandbox is not found,
934    /// or authentication/authorization fails.
935    pub async fn create_auto_recreate_sandbox(
936        &self,
937        application_guid: &str,
938        name: &str,
939        description: Option<String>,
940    ) -> Result<Sandbox, SandboxError> {
941        let request = CreateSandboxRequest {
942            name: name.to_string(),
943            description,
944            auto_recreate: Some(true),
945            custom_fields: None,
946            team_identifiers: None,
947        };
948
949        self.create_sandbox(application_guid, request).await
950    }
951
952    /// Update sandbox name
953    ///
954    /// # Arguments
955    ///
956    /// * `application_guid` - The GUID of the application
957    /// * `sandbox_guid` - The GUID of the sandbox
958    /// * `new_name` - The new name for the sandbox
959    ///
960    /// # Returns
961    ///
962    /// A `Result` containing the updated sandbox or an error.
963    ///
964    /// # Errors
965    ///
966    /// Returns an error if the API request fails, the sandbox is not found,
967    /// or authentication/authorization fails.
968    pub async fn update_sandbox_name(
969        &self,
970        application_guid: &str,
971        sandbox_guid: &str,
972        new_name: &str,
973    ) -> Result<Sandbox, SandboxError> {
974        let request = UpdateSandboxRequest {
975            name: Some(new_name.to_string()),
976            description: None,
977            auto_recreate: None,
978            custom_fields: None,
979            team_identifiers: None,
980        };
981
982        self.update_sandbox(application_guid, sandbox_guid, request)
983            .await
984    }
985
986    /// Count sandboxes for an application
987    ///
988    /// # Arguments
989    ///
990    /// * `application_guid` - The GUID of the application
991    ///
992    /// # Returns
993    ///
994    /// A `Result` containing the count of sandboxes or an error.
995    ///
996    /// # Errors
997    ///
998    /// Returns an error if the API request fails, the sandbox is not found,
999    /// or authentication/authorization fails.
1000    pub async fn count_sandboxes(&self, application_guid: &str) -> Result<u64, SandboxError> {
1001        let endpoint = format!("/appsec/v1/applications/{application_guid}/sandboxes");
1002
1003        // Request with size=1 to minimize data transfer - we only need the count
1004        let query_params = vec![("size".to_string(), "1".to_string())];
1005
1006        let response = self.client.get(&endpoint, Some(&query_params)).await?;
1007
1008        let status = response.status().as_u16();
1009        match status {
1010            200 => {
1011                let response_text = response.text().await?;
1012
1013                // Validate JSON depth before parsing to prevent DoS attacks
1014                if validate_json_depth(&response_text, MAX_JSON_DEPTH).is_err() {
1015                    return Err(SandboxError::Api(VeracodeError::InvalidResponse(
1016                        "JSON validation failed on response".to_string(),
1017                    )));
1018                }
1019
1020                let sandbox_response: SandboxListResponse = serde_json::from_str(&response_text)?;
1021                // Use the total from page info if available, otherwise fall back to counting embedded items
1022                Ok(sandbox_response
1023                    .page
1024                    .map(|p| p.total_elements)
1025                    .or(sandbox_response.total)
1026                    .unwrap_or(0))
1027            }
1028            404 => Ok(0), // Application not found or has no sandboxes
1029            _ => {
1030                let error_text = response.text().await.unwrap_or_default();
1031                Err(SandboxError::Api(VeracodeError::InvalidResponse(format!(
1032                    "HTTP {status}: {error_text}"
1033                ))))
1034            }
1035        }
1036    }
1037
1038    ///
1039    /// # Errors
1040    ///
1041    /// Returns an error if the API request fails, the sandbox is not found,
1042    /// or authentication/authorization fails.
1043    /// Get numeric `sandbox_id` from sandbox GUID.
1044    ///
1045    /// This is needed for XML API operations that require numeric IDs.
1046    ///
1047    /// # Arguments
1048    ///
1049    /// * `application_guid` - The GUID of the application
1050    /// * `sandbox_guid` - The sandbox GUID
1051    ///
1052    /// # Returns
1053    ///
1054    ///
1055    /// # Errors
1056    ///
1057    /// Returns an error if the API request fails, the sandbox is not found,
1058    /// or authentication/authorization fails.
1059    /// A `Result` containing the numeric `sandbox_id` as a string.
1060    ///
1061    /// # Errors
1062    ///
1063    /// Returns an error if the API request fails, the sandbox is not found,
1064    /// or authentication/authorization fails.
1065    pub async fn get_sandbox_id_from_guid(
1066        &self,
1067        application_guid: &str,
1068        sandbox_guid: &str,
1069    ) -> Result<String, SandboxError> {
1070        let sandbox = self.get_sandbox(application_guid, sandbox_guid).await?;
1071        match sandbox.id {
1072            Some(id) => Ok(id.to_string()),
1073            None => Err(SandboxError::InvalidInput(
1074                "Sandbox has no numeric ID".to_string(),
1075            )),
1076        }
1077    }
1078
1079    /// Create sandbox if it doesn't exist, or return existing sandbox.
1080    ///
1081    /// This method implements the "try-create-or-get" pattern which is safe
1082    /// against TOCTOU race conditions in concurrent environments.
1083    ///
1084    /// # Arguments
1085    ///
1086    /// * `application_guid` - The GUID of the application
1087    /// * `name` - The name of the sandbox
1088    /// * `description` - Optional description for new sandboxes
1089    ///
1090    /// # Returns
1091    ///
1092    /// A `Result` containing the sandbox (existing or newly created).
1093    ///
1094    /// # Errors
1095    ///
1096    /// Returns an error if the API request fails or authentication/authorization fails.
1097    /// Does not return an error if the sandbox already exists.
1098    pub async fn create_sandbox_if_not_exists(
1099        &self,
1100        application_guid: &str,
1101        name: &str,
1102        description: Option<String>,
1103    ) -> Result<Sandbox, SandboxError> {
1104        // Try to create the sandbox first (optimistic approach)
1105        let create_request = CreateSandboxRequest {
1106            name: name.to_string(),
1107            description: description.clone(),
1108            auto_recreate: Some(true), // Enable auto-recreate by default for CI/CD
1109            custom_fields: None,
1110            team_identifiers: None,
1111        };
1112
1113        match self.create_sandbox(application_guid, create_request).await {
1114            Ok(sandbox) => Ok(sandbox),
1115            Err(SandboxError::AlreadyExists(_)) => {
1116                // Sandbox was created concurrently, fetch and return it
1117                self.get_sandbox_by_name(application_guid, name)
1118                    .await?
1119                    .ok_or_else(|| {
1120                        SandboxError::Api(VeracodeError::InvalidResponse(
1121                            "Sandbox exists but cannot be retrieved".to_string(),
1122                        ))
1123                    })
1124            }
1125            Err(e) => Err(e),
1126        }
1127    }
1128}
1129
1130#[cfg(test)]
1131mod tests {
1132    use super::*;
1133    use proptest::prelude::*;
1134
1135    #[test]
1136    fn test_validate_create_request() {
1137        // Valid request
1138        let valid_request = CreateSandboxRequest {
1139            name: "valid-sandbox".to_string(),
1140            description: None,
1141            auto_recreate: None,
1142            custom_fields: None,
1143            team_identifiers: None,
1144        };
1145        assert!(SandboxApi::validate_create_request(&valid_request).is_ok());
1146
1147        // Empty name
1148        let empty_name_request = CreateSandboxRequest {
1149            name: String::new(),
1150            description: None,
1151            auto_recreate: None,
1152            custom_fields: None,
1153            team_identifiers: None,
1154        };
1155        assert!(SandboxApi::validate_create_request(&empty_name_request).is_err());
1156
1157        // Long name
1158        let long_name_request = CreateSandboxRequest {
1159            name: "x".repeat(300),
1160            description: None,
1161            auto_recreate: None,
1162            custom_fields: None,
1163            team_identifiers: None,
1164        };
1165        assert!(SandboxApi::validate_create_request(&long_name_request).is_err());
1166
1167        // Invalid characters
1168        let invalid_char_request = CreateSandboxRequest {
1169            name: "invalid<name>".to_string(),
1170            description: None,
1171            auto_recreate: None,
1172            custom_fields: None,
1173            team_identifiers: None,
1174        };
1175        assert!(SandboxApi::validate_create_request(&invalid_char_request).is_err());
1176    }
1177
1178    #[test]
1179    fn test_sandbox_list_params_to_query() {
1180        let params = SandboxListParams {
1181            name: Some("test".to_string()),
1182            page: Some(1),
1183            size: Some(10),
1184            ..Default::default()
1185        };
1186
1187        let query_params: Vec<_> = params.into();
1188        assert_eq!(query_params.len(), 3);
1189        assert!(query_params.contains(&("name".to_string(), "test".to_string())));
1190        assert!(query_params.contains(&("page".to_string(), "1".to_string())));
1191        assert!(query_params.contains(&("size".to_string(), "10".to_string())));
1192    }
1193
1194    #[test]
1195    fn test_sandbox_list_params_page_size_limit() {
1196        // Test that excessive page sizes are capped at MAX_PAGE_SIZE
1197        let params = SandboxListParams {
1198            size: Some(999999999),
1199            ..Default::default()
1200        };
1201
1202        let query_params: Vec<_> = params.into();
1203        assert_eq!(query_params.len(), 1);
1204        assert!(query_params.contains(&("size".to_string(), MAX_PAGE_SIZE.to_string())));
1205
1206        // Test with u64::MAX
1207        let params = SandboxListParams {
1208            size: Some(u64::MAX),
1209            ..Default::default()
1210        };
1211
1212        let query_params: Vec<_> = params.into();
1213        assert_eq!(query_params.len(), 1);
1214        assert!(query_params.contains(&("size".to_string(), MAX_PAGE_SIZE.to_string())));
1215
1216        // Test that reasonable sizes are not modified
1217        let params = SandboxListParams {
1218            size: Some(100),
1219            ..Default::default()
1220        };
1221
1222        let query_params: Vec<_> = params.into();
1223        assert_eq!(query_params.len(), 1);
1224        assert!(query_params.contains(&("size".to_string(), "100".to_string())));
1225    }
1226
1227    #[test]
1228    fn test_sandbox_error_display() {
1229        let error = SandboxError::NotFound;
1230        assert_eq!(error.to_string(), "Sandbox not found");
1231
1232        let error = SandboxError::InvalidInput("test".to_string());
1233        assert_eq!(error.to_string(), "Invalid input: test");
1234
1235        let error = SandboxError::LimitExceeded;
1236        assert_eq!(error.to_string(), "Maximum number of sandboxes reached");
1237    }
1238
1239    #[test]
1240    fn test_validate_name_control_characters() {
1241        // Test control characters
1242        assert!(SandboxApi::validate_name("test\x00name").is_err());
1243        assert!(SandboxApi::validate_name("test\nname").is_err());
1244        assert!(SandboxApi::validate_name("test\rname").is_err());
1245        assert!(SandboxApi::validate_name("test\x1Fname").is_err());
1246    }
1247
1248    #[test]
1249    fn test_validate_name_path_traversal() {
1250        // Test path traversal sequences
1251        assert!(SandboxApi::validate_name("../etc/passwd").is_err());
1252        assert!(SandboxApi::validate_name("test/../name").is_err());
1253        assert!(SandboxApi::validate_name("test/name").is_err());
1254        assert!(SandboxApi::validate_name("test\\name").is_err());
1255    }
1256
1257    #[test]
1258    fn test_validate_custom_fields() {
1259        use std::collections::HashMap;
1260
1261        // Valid custom fields
1262        let mut valid_fields = HashMap::new();
1263        valid_fields.insert("key1".to_string(), "value1".to_string());
1264        assert!(SandboxApi::validate_custom_fields(&valid_fields).is_ok());
1265
1266        // Empty key
1267        let mut empty_key = HashMap::new();
1268        empty_key.insert("".to_string(), "value".to_string());
1269        assert!(SandboxApi::validate_custom_fields(&empty_key).is_err());
1270
1271        // Key too long
1272        let mut long_key = HashMap::new();
1273        long_key.insert("x".repeat(200), "value".to_string());
1274        assert!(SandboxApi::validate_custom_fields(&long_key).is_err());
1275
1276        // Value too long
1277        let mut long_value = HashMap::new();
1278        long_value.insert("key".to_string(), "x".repeat(2000));
1279        assert!(SandboxApi::validate_custom_fields(&long_value).is_err());
1280
1281        // Control characters in key
1282        let mut control_key = HashMap::new();
1283        control_key.insert("test\x00key".to_string(), "value".to_string());
1284        assert!(SandboxApi::validate_custom_fields(&control_key).is_err());
1285
1286        // Invalid characters in key
1287        let mut invalid_key = HashMap::new();
1288        invalid_key.insert("test<key>".to_string(), "value".to_string());
1289        assert!(SandboxApi::validate_custom_fields(&invalid_key).is_err());
1290
1291        // Too many fields
1292        let mut too_many = HashMap::new();
1293        for i in 0..100 {
1294            too_many.insert(format!("key{i}"), format!("value{i}"));
1295        }
1296        assert!(SandboxApi::validate_custom_fields(&too_many).is_err());
1297    }
1298
1299    #[test]
1300    fn test_validate_team_identifiers() {
1301        // Valid team identifiers
1302        assert!(
1303            SandboxApi::validate_team_identifiers(&["team1".to_string(), "team2".to_string()])
1304                .is_ok()
1305        );
1306
1307        // Empty team identifier
1308        assert!(SandboxApi::validate_team_identifiers(&["".to_string()]).is_err());
1309
1310        // Team identifier too long
1311        assert!(SandboxApi::validate_team_identifiers(&["x".repeat(200)]).is_err());
1312
1313        // Control characters
1314        assert!(SandboxApi::validate_team_identifiers(&["team\x00id".to_string()]).is_err());
1315
1316        // Invalid characters
1317        assert!(SandboxApi::validate_team_identifiers(&["team<id>".to_string()]).is_err());
1318
1319        // Too many team identifiers
1320        let too_many: Vec<String> = (0..150).map(|i| format!("team{i}")).collect();
1321        assert!(SandboxApi::validate_team_identifiers(&too_many).is_err());
1322    }
1323
1324    // ========================================================================
1325    // PROPTEST SECURITY TESTS
1326    // ========================================================================
1327    //
1328    // These property-based tests verify security properties across all validation
1329    // functions using randomly generated inputs. They are Miri-optimized to run
1330    // 10 cases under Miri (for memory safety checking) and 1000 cases normally.
1331    //
1332    // SECURITY PROPERTIES TESTED:
1333    // 1. Input validation rejects control characters (prevents injection)
1334    // 2. Path traversal sequences are rejected (prevents directory traversal)
1335    // 3. Length bounds are enforced (prevents DoS via memory exhaustion)
1336    // 4. Dangerous characters are rejected (prevents XSS/injection)
1337    // 5. Empty/invalid inputs are rejected (prevents logic errors)
1338    // 6. Page size limits are enforced (prevents DoS via unbounded queries)
1339    //
1340    // TESTING STRATEGY:
1341    // - Tier 1: Property-based testing with proptest (all validation functions)
1342    // - No unsafe code → Miri verification not needed
1343    // - No cryptographic/concurrent code → Kani formal verification not needed
1344    //
1345    // WHY KANI IS NOT USED:
1346    // These are simple input validation functions with straightforward logic.
1347    // Kani would provide formal proofs but at high computational cost (minutes
1348    // to hours per proof). Proptest provides excellent coverage for validation
1349    // logic at much lower cost (seconds). Kani is reserved for cryptographic,
1350    // concurrent, or mission-critical state machines where formal proofs add
1351    // significant value.
1352
1353    mod proptest_security_tests {
1354        use super::*;
1355
1356        // Property: validate_name should reject any string with control characters
1357        proptest! {
1358            #![proptest_config(ProptestConfig {
1359                cases: if cfg!(miri) { 5 } else { 1000 },
1360                failure_persistence: None,
1361                .. ProptestConfig::default()
1362            })]
1363
1364            #[test]
1365            fn prop_validate_name_rejects_control_chars(
1366                prefix in "[a-zA-Z0-9_-]{0,10}",
1367                control_char in prop::char::range('\x00', '\x1F'),
1368                suffix in "[a-zA-Z0-9_-]{0,10}",
1369            ) {
1370                let name = format!("{}{}{}", prefix, control_char, suffix);
1371                let result = SandboxApi::validate_name(&name);
1372                prop_assert!(result.is_err(), "Should reject control character: {:?}", control_char);
1373            }
1374        }
1375
1376        // Property: validate_name should reject path traversal sequences
1377        proptest! {
1378            #![proptest_config(ProptestConfig {
1379                cases: if cfg!(miri) { 5 } else { 1000 },
1380                failure_persistence: None,
1381                .. ProptestConfig::default()
1382            })]
1383
1384            #[test]
1385            fn prop_validate_name_rejects_path_traversal(
1386                prefix in "[a-zA-Z0-9_-]{0,20}",
1387                suffix in "[a-zA-Z0-9_-]{0,20}",
1388            ) {
1389                let test_cases = vec![
1390                    format!("{}/../{}", prefix, suffix),
1391                    format!("{}//{}", prefix, suffix),
1392                    format!("{}\\{}", prefix, suffix),
1393                    format!("{}..", suffix),
1394                ];
1395
1396                for name in test_cases {
1397                    let result = SandboxApi::validate_name(&name);
1398                    prop_assert!(result.is_err(), "Should reject path traversal in: {}", name);
1399                }
1400            }
1401        }
1402
1403        // Property: validate_name should reject names exceeding max length
1404        proptest! {
1405            #![proptest_config(ProptestConfig {
1406                cases: if cfg!(miri) { 5 } else { 1000 },
1407                failure_persistence: None,
1408                .. ProptestConfig::default()
1409            })]
1410
1411            #[test]
1412            #[allow(clippy::arithmetic_side_effects)]
1413            fn prop_validate_name_rejects_too_long(
1414                extra_len in 1usize..=100usize,
1415            ) {
1416                let name = "a".repeat(256 + extra_len);
1417                let result = SandboxApi::validate_name(&name);
1418                prop_assert!(result.is_err(), "Should reject name of length {}", name.len());
1419            }
1420        }
1421
1422        // Property: validate_name should accept valid names
1423        proptest! {
1424            #![proptest_config(ProptestConfig {
1425                cases: if cfg!(miri) { 5 } else { 1000 },
1426                failure_persistence: None,
1427                .. ProptestConfig::default()
1428            })]
1429
1430            #[test]
1431            fn prop_validate_name_accepts_valid_names(
1432                name in "[a-zA-Z0-9_-]{1,256}",
1433            ) {
1434                // Filter out any accidentally generated invalid patterns
1435                prop_assume!(!name.contains(".."));
1436                prop_assume!(!name.contains('/'));
1437                prop_assume!(!name.contains('\\'));
1438
1439                let result = SandboxApi::validate_name(&name);
1440                prop_assert!(result.is_ok(), "Should accept valid name: {}", name);
1441            }
1442        }
1443
1444        // Property: validate_name should reject dangerous characters
1445        proptest! {
1446            #![proptest_config(ProptestConfig {
1447                cases: if cfg!(miri) { 5 } else { 500 },
1448                failure_persistence: None,
1449                .. ProptestConfig::default()
1450            })]
1451
1452            #[test]
1453            fn prop_validate_name_rejects_dangerous_chars(
1454                prefix in "[a-zA-Z0-9]{0,20}",
1455                dangerous in prop::sample::select(vec!['<', '>', '"', '&', '\'']),
1456                suffix in "[a-zA-Z0-9]{0,20}",
1457            ) {
1458                let name = format!("{}{}{}", prefix, dangerous, suffix);
1459                let result = SandboxApi::validate_name(&name);
1460                prop_assert!(result.is_err(), "Should reject dangerous character: {}", dangerous);
1461            }
1462        }
1463
1464        // Property: validate_custom_fields should reject empty keys
1465        proptest! {
1466            #![proptest_config(ProptestConfig {
1467                cases: if cfg!(miri) { 5 } else { 1000 },
1468                failure_persistence: None,
1469                .. ProptestConfig::default()
1470            })]
1471
1472            #[test]
1473            fn prop_validate_custom_fields_rejects_empty_key(
1474                value in "[a-zA-Z0-9]{1,100}",
1475            ) {
1476                let mut fields = HashMap::new();
1477                fields.insert("".to_string(), value);
1478                let result = SandboxApi::validate_custom_fields(&fields);
1479                prop_assert!(result.is_err(), "Should reject empty key");
1480            }
1481        }
1482
1483        // Property: validate_custom_fields should reject keys exceeding max length
1484        proptest! {
1485            #![proptest_config(ProptestConfig {
1486                cases: if cfg!(miri) { 5 } else { 1000 },
1487                failure_persistence: None,
1488                .. ProptestConfig::default()
1489            })]
1490
1491            #[test]
1492            fn prop_validate_custom_fields_rejects_long_key(
1493                extra_len in 1usize..=50usize,
1494            ) {
1495                let mut fields = HashMap::new();
1496                let long_key = "a".repeat(128_usize.saturating_add(extra_len));
1497                fields.insert(long_key.clone(), "value".to_string());
1498                let result = SandboxApi::validate_custom_fields(&fields);
1499                prop_assert!(result.is_err(), "Should reject key of length {}", long_key.len());
1500            }
1501        }
1502
1503        // Property: validate_custom_fields should reject values exceeding max length
1504        proptest! {
1505            #![proptest_config(ProptestConfig {
1506                cases: if cfg!(miri) { 5 } else { 1000 },
1507                failure_persistence: None,
1508                .. ProptestConfig::default()
1509            })]
1510
1511            #[test]
1512            fn prop_validate_custom_fields_rejects_long_value(
1513                extra_len in 1usize..=100usize,
1514            ) {
1515                let mut fields = HashMap::new();
1516                let long_value = "a".repeat(1024_usize.saturating_add(extra_len));
1517                fields.insert("key".to_string(), long_value.clone());
1518                let result = SandboxApi::validate_custom_fields(&fields);
1519                prop_assert!(result.is_err(), "Should reject value of length {}", long_value.len());
1520            }
1521        }
1522
1523        // Property: validate_custom_fields should reject too many fields
1524        proptest! {
1525            #![proptest_config(ProptestConfig {
1526                cases: if cfg!(miri) { 5 } else { 100 },
1527                failure_persistence: None,
1528                .. ProptestConfig::default()
1529            })]
1530
1531            #[test]
1532            fn prop_validate_custom_fields_rejects_too_many(
1533                extra_count in 1usize..=20usize,
1534            ) {
1535                let mut fields = HashMap::new();
1536                let total = 50_usize.saturating_add(extra_count);
1537                for i in 0..total {
1538                    fields.insert(format!("key{}", i), format!("value{}", i));
1539                }
1540                let result = SandboxApi::validate_custom_fields(&fields);
1541                prop_assert!(result.is_err(), "Should reject {} fields", total);
1542            }
1543        }
1544
1545        // Property: validate_custom_fields should reject keys with control characters
1546        proptest! {
1547            #![proptest_config(ProptestConfig {
1548                cases: if cfg!(miri) { 5 } else { 1000 },
1549                failure_persistence: None,
1550                .. ProptestConfig::default()
1551            })]
1552
1553            #[test]
1554            fn prop_validate_custom_fields_rejects_control_in_key(
1555                prefix in "[a-zA-Z]{1,10}",
1556                control_char in prop::char::range('\x00', '\x1F'),
1557                suffix in "[a-zA-Z]{1,10}",
1558            ) {
1559                let mut fields = HashMap::new();
1560                let key = format!("{}{}{}", prefix, control_char, suffix);
1561                fields.insert(key.clone(), "value".to_string());
1562                let result = SandboxApi::validate_custom_fields(&fields);
1563                prop_assert!(result.is_err(), "Should reject key with control character: {:?}", control_char);
1564            }
1565        }
1566
1567        // Property: validate_custom_fields should reject dangerous characters in keys
1568        proptest! {
1569            #![proptest_config(ProptestConfig {
1570                cases: if cfg!(miri) { 5 } else { 500 },
1571                failure_persistence: None,
1572                .. ProptestConfig::default()
1573            })]
1574
1575            #[test]
1576            fn prop_validate_custom_fields_rejects_dangerous_in_key(
1577                prefix in "[a-zA-Z]{1,10}",
1578                dangerous in prop::sample::select(vec!['<', '>', '"', '&', '\'', '/', '\\']),
1579                suffix in "[a-zA-Z]{1,10}",
1580            ) {
1581                let mut fields = HashMap::new();
1582                let key = format!("{}{}{}", prefix, dangerous, suffix);
1583                fields.insert(key.clone(), "value".to_string());
1584                let result = SandboxApi::validate_custom_fields(&fields);
1585                prop_assert!(result.is_err(), "Should reject key with dangerous character: {}", dangerous);
1586            }
1587        }
1588
1589        // Property: validate_team_identifiers should reject empty identifiers
1590        proptest! {
1591            #![proptest_config(ProptestConfig {
1592                cases: if cfg!(miri) { 5 } else { 1000 },
1593                failure_persistence: None,
1594                .. ProptestConfig::default()
1595            })]
1596
1597            #[test]
1598            fn prop_validate_team_identifiers_rejects_empty(
1599                prefix_count in 0usize..=5usize,
1600                suffix_count in 0usize..=5usize,
1601            ) {
1602                let mut team_ids = Vec::new();
1603                for i in 0..prefix_count {
1604                    team_ids.push(format!("team{}", i));
1605                }
1606                team_ids.push("".to_string());
1607                for i in 0..suffix_count {
1608                    team_ids.push(format!("team{}", i.saturating_add(prefix_count)));
1609                }
1610                let result = SandboxApi::validate_team_identifiers(&team_ids);
1611                prop_assert!(result.is_err(), "Should reject empty team identifier");
1612            }
1613        }
1614
1615        // Property: validate_team_identifiers should reject identifiers exceeding max length
1616        proptest! {
1617            #![proptest_config(ProptestConfig {
1618                cases: if cfg!(miri) { 5 } else { 1000 },
1619                failure_persistence: None,
1620                .. ProptestConfig::default()
1621            })]
1622
1623            #[test]
1624            fn prop_validate_team_identifiers_rejects_too_long(
1625                extra_len in 1usize..=50usize,
1626            ) {
1627                let long_id = "a".repeat(128_usize.saturating_add(extra_len));
1628                let result = SandboxApi::validate_team_identifiers(std::slice::from_ref(&long_id));
1629                prop_assert!(result.is_err(), "Should reject identifier of length {}", long_id.len());
1630            }
1631        }
1632
1633        // Property: validate_team_identifiers should reject too many identifiers
1634        proptest! {
1635            #![proptest_config(ProptestConfig {
1636                cases: if cfg!(miri) { 5 } else { 100 },
1637                failure_persistence: None,
1638                .. ProptestConfig::default()
1639            })]
1640
1641            #[test]
1642            fn prop_validate_team_identifiers_rejects_too_many(
1643                extra_count in 1usize..=20usize,
1644            ) {
1645                let total = 100_usize.saturating_add(extra_count);
1646                let team_ids: Vec<String> = (0..total).map(|i| format!("team{}", i)).collect();
1647                let result = SandboxApi::validate_team_identifiers(&team_ids);
1648                prop_assert!(result.is_err(), "Should reject {} identifiers", total);
1649            }
1650        }
1651
1652        // Property: validate_team_identifiers should reject control characters
1653        proptest! {
1654            #![proptest_config(ProptestConfig {
1655                cases: if cfg!(miri) { 5 } else { 1000 },
1656                failure_persistence: None,
1657                .. ProptestConfig::default()
1658            })]
1659
1660            #[test]
1661            fn prop_validate_team_identifiers_rejects_control(
1662                prefix in "[a-zA-Z]{1,10}",
1663                control_char in prop::char::range('\x00', '\x1F'),
1664                suffix in "[a-zA-Z]{1,10}",
1665            ) {
1666                let team_id = format!("{}{}{}", prefix, control_char, suffix);
1667                let result = SandboxApi::validate_team_identifiers(std::slice::from_ref(&team_id));
1668                prop_assert!(result.is_err(), "Should reject identifier with control character: {:?}", control_char);
1669            }
1670        }
1671
1672        // Property: validate_team_identifiers should reject dangerous characters
1673        proptest! {
1674            #![proptest_config(ProptestConfig {
1675                cases: if cfg!(miri) { 5 } else { 500 },
1676                failure_persistence: None,
1677                .. ProptestConfig::default()
1678            })]
1679
1680            #[test]
1681            fn prop_validate_team_identifiers_rejects_dangerous(
1682                prefix in "[a-zA-Z]{1,10}",
1683                dangerous in prop::sample::select(vec!['<', '>', '"', '&', '\'', '/', '\\']),
1684                suffix in "[a-zA-Z]{1,10}",
1685            ) {
1686                let team_id = format!("{}{}{}", prefix, dangerous, suffix);
1687                let result = SandboxApi::validate_team_identifiers(std::slice::from_ref(&team_id));
1688                prop_assert!(result.is_err(), "Should reject identifier with dangerous character: {}", dangerous);
1689            }
1690        }
1691
1692        // Property: SandboxListParams page size capping
1693        proptest! {
1694            #![proptest_config(ProptestConfig {
1695                cases: if cfg!(miri) { 5 } else { 1000 },
1696                failure_persistence: None,
1697                .. ProptestConfig::default()
1698            })]
1699
1700            #[test]
1701            fn prop_sandbox_list_params_caps_page_size(
1702                excessive_size in (MAX_PAGE_SIZE + 1)..=u64::MAX,
1703            ) {
1704                let params = SandboxListParams {
1705                    size: Some(excessive_size),
1706                    ..Default::default()
1707                };
1708                let query_params: Vec<_> = params.into();
1709
1710                // Find the size parameter
1711                let size_param = query_params.iter().find(|(k, _)| k == "size");
1712                prop_assert!(size_param.is_some(), "Should have size parameter");
1713
1714                let (_, size_value) = size_param.expect("Size parameter should exist");
1715                let parsed_size: u64 = size_value.parse().expect("Size value should be parseable");
1716                prop_assert_eq!(parsed_size, MAX_PAGE_SIZE, "Should cap size to MAX_PAGE_SIZE");
1717            }
1718        }
1719
1720        // Property: SandboxListParams preserves reasonable page sizes
1721        proptest! {
1722            #![proptest_config(ProptestConfig {
1723                cases: if cfg!(miri) { 5 } else { 1000 },
1724                failure_persistence: None,
1725                .. ProptestConfig::default()
1726            })]
1727
1728            #[test]
1729            fn prop_sandbox_list_params_preserves_reasonable_size(
1730                reasonable_size in 1u64..=MAX_PAGE_SIZE,
1731            ) {
1732                let params = SandboxListParams {
1733                    size: Some(reasonable_size),
1734                    ..Default::default()
1735                };
1736                let query_params: Vec<_> = params.into();
1737
1738                let size_param = query_params.iter().find(|(k, _)| k == "size");
1739                prop_assert!(size_param.is_some(), "Should have size parameter");
1740
1741                let (_, size_value) = size_param.expect("Size parameter should exist");
1742                let parsed_size: u64 = size_value.parse().expect("Size value should be parseable");
1743                prop_assert_eq!(parsed_size, reasonable_size, "Should preserve reasonable size");
1744            }
1745        }
1746
1747        // Property: validate_create_request should accept valid requests
1748        proptest! {
1749            #![proptest_config(ProptestConfig {
1750                cases: if cfg!(miri) { 5 } else { 500 },
1751                failure_persistence: None,
1752                .. ProptestConfig::default()
1753            })]
1754
1755            #[test]
1756            fn prop_validate_create_request_accepts_valid(
1757                name in "[a-zA-Z0-9_-]{1,256}",
1758            ) {
1759                prop_assume!(!name.contains(".."));
1760                prop_assume!(!name.contains('/'));
1761                prop_assume!(!name.contains('\\'));
1762
1763                let request = CreateSandboxRequest {
1764                    name,
1765                    description: None,
1766                    auto_recreate: None,
1767                    custom_fields: None,
1768                    team_identifiers: None,
1769                };
1770                let result = SandboxApi::validate_create_request(&request);
1771                prop_assert!(result.is_ok(), "Should accept valid create request");
1772            }
1773        }
1774
1775        // Property: validate_update_request should accept valid requests
1776        proptest! {
1777            #![proptest_config(ProptestConfig {
1778                cases: if cfg!(miri) { 5 } else { 500 },
1779                failure_persistence: None,
1780                .. ProptestConfig::default()
1781            })]
1782
1783            #[test]
1784            fn prop_validate_update_request_accepts_valid(
1785                name in "[a-zA-Z0-9_-]{1,256}",
1786            ) {
1787                prop_assume!(!name.contains(".."));
1788                prop_assume!(!name.contains('/'));
1789                prop_assume!(!name.contains('\\'));
1790
1791                let request = UpdateSandboxRequest {
1792                    name: Some(name),
1793                    description: None,
1794                    auto_recreate: None,
1795                    custom_fields: None,
1796                    team_identifiers: None,
1797                };
1798                let result = SandboxApi::validate_update_request(&request);
1799                prop_assert!(result.is_ok(), "Should accept valid update request");
1800            }
1801        }
1802    }
1803}