1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3use chrono::{DateTime, Utc};
4
5use crate::{VeracodeClient, VeracodeError};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct ApiErrorResponse {
10 #[serde(rename = "_embedded")]
11 pub embedded: Option<ApiErrorEmbedded>,
12 pub fallback_type: Option<String>,
13 pub full_type: Option<String>,
14}
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct ApiErrorEmbedded {
19 pub api_errors: Vec<ApiError>,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct ApiError {
25 pub id: String,
26 pub code: String,
27 pub title: String,
28 pub status: String,
29 pub source: Option<ApiErrorSource>,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct ApiErrorSource {
35 pub pointer: String,
36 pub parameter: String,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct Sandbox {
42 pub id: Option<u64>,
43 pub guid: String,
44 pub name: String,
45 pub description: Option<String>,
46 pub created: DateTime<Utc>,
47 pub modified: DateTime<Utc>,
48 pub auto_recreate: bool,
49 pub custom_fields: Option<HashMap<String, String>>,
50 pub owner: Option<String>,
51 pub owner_username: Option<String>,
52 pub organization_id: Option<u64>,
53 pub application_guid: Option<String>,
54 pub team_identifiers: Option<Vec<String>>,
55 pub scan_url: Option<String>,
56 pub last_scan_date: Option<DateTime<Utc>>,
57 pub status: Option<String>,
58 #[serde(rename = "_links")]
59 pub links: Option<serde_json::Value>,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct CreateSandboxRequest {
65 pub name: String,
66 pub description: Option<String>,
67 pub auto_recreate: Option<bool>,
68 pub custom_fields: Option<HashMap<String, String>>,
69 pub team_identifiers: Option<Vec<String>>,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct UpdateSandboxRequest {
75 pub name: Option<String>,
76 pub description: Option<String>,
77 pub auto_recreate: Option<bool>,
78 pub custom_fields: Option<HashMap<String, String>>,
79 pub team_identifiers: Option<Vec<String>>,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct SandboxListResponse {
85 #[serde(rename = "_embedded")]
86 pub embedded: Option<SandboxEmbedded>,
87 pub page: Option<PageInfo>,
88 pub total: Option<u64>,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct SandboxEmbedded {
94 pub sandboxes: Vec<Sandbox>,
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct PageInfo {
100 pub size: u64,
101 pub number: u64,
102 pub total_elements: u64,
103 pub total_pages: u64,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct SandboxScan {
109 pub scan_id: u64,
110 pub scan_type: String,
111 pub status: String,
112 pub created: DateTime<Utc>,
113 pub modified: DateTime<Utc>,
114 pub scan_url: Option<String>,
115 pub results_ready: bool,
116 pub engine_version: Option<String>,
117}
118
119#[derive(Debug, Clone, Default)]
121pub struct SandboxListParams {
122 pub name: Option<String>,
123 pub owner: Option<String>,
124 pub team: Option<String>,
125 pub page: Option<u64>,
126 pub size: Option<u64>,
127 pub modified_after: Option<DateTime<Utc>>,
128 pub modified_before: Option<DateTime<Utc>>,
129}
130
131impl SandboxListParams {
132 pub fn to_query_params(&self) -> Vec<(String, String)> {
134 let mut params = Vec::new();
135
136 if let Some(name) = &self.name {
137 params.push(("name".to_string(), name.clone()));
138 }
139 if let Some(owner) = &self.owner {
140 params.push(("owner".to_string(), owner.clone()));
141 }
142 if let Some(team) = &self.team {
143 params.push(("team".to_string(), team.clone()));
144 }
145 if let Some(page) = self.page {
146 params.push(("page".to_string(), page.to_string()));
147 }
148 if let Some(size) = self.size {
149 params.push(("size".to_string(), size.to_string()));
150 }
151 if let Some(modified_after) = self.modified_after {
152 params.push(("modified_after".to_string(), modified_after.to_rfc3339()));
153 }
154 if let Some(modified_before) = self.modified_before {
155 params.push(("modified_before".to_string(), modified_before.to_rfc3339()));
156 }
157
158 params
159 }
160}
161
162#[derive(Debug)]
164pub enum SandboxError {
165 Api(VeracodeError),
167 NotFound,
169 InvalidInput(String),
171 LimitExceeded,
173 OperationNotAllowed(String),
175 AlreadyExists(String),
177}
178
179impl std::fmt::Display for SandboxError {
180 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
181 match self {
182 SandboxError::Api(err) => write!(f, "API error: {err}"),
183 SandboxError::NotFound => write!(f, "Sandbox not found"),
184 SandboxError::InvalidInput(msg) => write!(f, "Invalid input: {msg}"),
185 SandboxError::LimitExceeded => write!(f, "Maximum number of sandboxes reached"),
186 SandboxError::OperationNotAllowed(msg) => write!(f, "Operation not allowed: {msg}"),
187 SandboxError::AlreadyExists(msg) => write!(f, "Sandbox already exists: {msg}"),
188 }
189 }
190}
191
192impl std::error::Error for SandboxError {}
193
194impl From<VeracodeError> for SandboxError {
195 fn from(err: VeracodeError) -> Self {
196 SandboxError::Api(err)
197 }
198}
199
200impl From<reqwest::Error> for SandboxError {
201 fn from(err: reqwest::Error) -> Self {
202 SandboxError::Api(VeracodeError::Http(err))
203 }
204}
205
206impl From<serde_json::Error> for SandboxError {
207 fn from(err: serde_json::Error) -> Self {
208 SandboxError::Api(VeracodeError::Serialization(err))
209 }
210}
211
212pub struct SandboxApi<'a> {
214 client: &'a VeracodeClient,
215}
216
217impl<'a> SandboxApi<'a> {
218 pub fn new(client: &'a VeracodeClient) -> Self {
220 Self { client }
221 }
222
223 pub async fn list_sandboxes(
234 &self,
235 application_guid: &str,
236 params: Option<SandboxListParams>,
237 ) -> Result<Vec<Sandbox>, SandboxError> {
238 let endpoint = format!("/appsec/v1/applications/{application_guid}/sandboxes");
239
240 let query_params = params.map(|p| p.to_query_params());
241
242 let response = self.client.get(&endpoint, query_params.as_deref()).await?;
243
244 let status = response.status().as_u16();
245 match status {
246 200 => {
247 let sandbox_response: SandboxListResponse = response.json().await?;
248 Ok(sandbox_response.embedded
249 .map(|e| e.sandboxes)
250 .unwrap_or_default())
251 }
252 404 => Err(SandboxError::NotFound),
253 _ => {
254 let error_text = response.text().await.unwrap_or_default();
255 Err(SandboxError::Api(VeracodeError::InvalidResponse(
256 format!("HTTP {status}: {error_text}")
257 )))
258 }
259 }
260 }
261
262 pub async fn get_sandbox(
273 &self,
274 application_guid: &str,
275 sandbox_guid: &str,
276 ) -> Result<Sandbox, SandboxError> {
277 let endpoint = format!(
278 "/appsec/v1/applications/{application_guid}/sandboxes/{sandbox_guid}"
279 );
280
281 let response = self.client.get(&endpoint, None).await?;
282
283 let status = response.status().as_u16();
284 match status {
285 200 => {
286 let sandbox: Sandbox = response.json().await?;
287 Ok(sandbox)
288 }
289 404 => Err(SandboxError::NotFound),
290 _ => {
291 let error_text = response.text().await.unwrap_or_default();
292 Err(SandboxError::Api(VeracodeError::InvalidResponse(
293 format!("HTTP {status}: {error_text}")
294 )))
295 }
296 }
297 }
298
299 pub async fn create_sandbox(
310 &self,
311 application_guid: &str,
312 request: CreateSandboxRequest,
313 ) -> Result<Sandbox, SandboxError> {
314 Self::validate_create_request(&request)?;
316
317 let endpoint = format!("/appsec/v1/applications/{application_guid}/sandboxes");
318
319 let response = self.client.post(&endpoint, Some(&request)).await?;
320
321 let status = response.status().as_u16();
322 match status {
323 200 | 201 => {
324 let sandbox: Sandbox = response.json().await?;
325 Ok(sandbox)
326 }
327 400 => {
328 let error_text = response.text().await.unwrap_or_default();
329
330 if let Ok(error_response) = serde_json::from_str::<ApiErrorResponse>(&error_text) {
332 if let Some(embedded) = error_response.embedded {
333 for api_error in embedded.api_errors {
334 if api_error.title.contains("already exists") {
335 return Err(SandboxError::AlreadyExists(api_error.title));
336 }
337 if api_error.title.contains("limit") || api_error.title.contains("maximum") {
338 return Err(SandboxError::LimitExceeded);
339 }
340 if api_error.title.contains("Json Parse Error") || api_error.title.contains("Cannot deserialize") {
341 return Err(SandboxError::InvalidInput(format!("JSON parsing error: {}", api_error.title)));
342 }
343 }
344 }
345 }
346
347 if error_text.contains("limit") || error_text.contains("maximum") {
349 Err(SandboxError::LimitExceeded)
350 } else if error_text.contains("already exists") {
351 Err(SandboxError::AlreadyExists(error_text))
352 } else {
353 Err(SandboxError::InvalidInput(error_text))
354 }
355 }
356 404 => Err(SandboxError::NotFound),
357 _ => {
358 let error_text = response.text().await.unwrap_or_default();
359 Err(SandboxError::Api(VeracodeError::InvalidResponse(
360 format!("HTTP {status}: {error_text}")
361 )))
362 }
363 }
364 }
365
366 pub async fn update_sandbox(
378 &self,
379 application_guid: &str,
380 sandbox_guid: &str,
381 request: UpdateSandboxRequest,
382 ) -> Result<Sandbox, SandboxError> {
383 Self::validate_update_request(&request)?;
385
386 let endpoint = format!(
387 "/appsec/v1/applications/{application_guid}/sandboxes/{sandbox_guid}"
388 );
389
390 let response = self.client.put(&endpoint, Some(&request)).await?;
391
392 let status = response.status().as_u16();
393 match status {
394 200 => {
395 let sandbox: Sandbox = response.json().await?;
396 Ok(sandbox)
397 }
398 400 => {
399 let error_text = response.text().await.unwrap_or_default();
400 Err(SandboxError::InvalidInput(error_text))
401 }
402 404 => Err(SandboxError::NotFound),
403 _ => {
404 let error_text = response.text().await.unwrap_or_default();
405 Err(SandboxError::Api(VeracodeError::InvalidResponse(
406 format!("HTTP {status}: {error_text}")
407 )))
408 }
409 }
410 }
411
412 pub async fn delete_sandbox(
423 &self,
424 application_guid: &str,
425 sandbox_guid: &str,
426 ) -> Result<(), SandboxError> {
427 let endpoint = format!(
428 "/appsec/v1/applications/{application_guid}/sandboxes/{sandbox_guid}"
429 );
430
431 let response = self.client.delete(&endpoint).await?;
432
433 let status = response.status().as_u16();
434 match status {
435 204 => Ok(()),
436 404 => Err(SandboxError::NotFound),
437 409 => {
438 let error_text = response.text().await.unwrap_or_default();
439 Err(SandboxError::OperationNotAllowed(error_text))
440 }
441 _ => {
442 let error_text = response.text().await.unwrap_or_default();
443 Err(SandboxError::Api(VeracodeError::InvalidResponse(
444 format!("HTTP {status}: {error_text}")
445 )))
446 }
447 }
448 }
449
450 pub async fn promote_sandbox_scan(
462 &self,
463 application_guid: &str,
464 sandbox_guid: &str,
465 delete_on_promote: bool,
466 ) -> Result<(), SandboxError> {
467 let endpoint = if delete_on_promote {
468 format!(
469 "/appsec/v1/applications/{application_guid}/sandboxes/{sandbox_guid}/promote?delete_on_promote=true"
470 )
471 } else {
472 format!(
473 "/appsec/v1/applications/{application_guid}/sandboxes/{sandbox_guid}/promote"
474 )
475 };
476
477 let response = self.client.post(&endpoint, None::<&()>).await?;
478
479 let status = response.status().as_u16();
480 match status {
481 200 | 204 => Ok(()),
482 404 => Err(SandboxError::NotFound),
483 409 => {
484 let error_text = response.text().await.unwrap_or_default();
485 Err(SandboxError::OperationNotAllowed(error_text))
486 }
487 _ => {
488 let error_text = response.text().await.unwrap_or_default();
489 Err(SandboxError::Api(VeracodeError::InvalidResponse(
490 format!("HTTP {status}: {error_text}")
491 )))
492 }
493 }
494 }
495
496 pub async fn get_sandbox_scans(
507 &self,
508 application_guid: &str,
509 sandbox_guid: &str,
510 ) -> Result<Vec<SandboxScan>, SandboxError> {
511 let endpoint = format!(
512 "/appsec/v1/applications/{application_guid}/sandboxes/{sandbox_guid}/scans"
513 );
514
515 let response = self.client.get(&endpoint, None).await?;
516
517 let status = response.status().as_u16();
518 match status {
519 200 => {
520 let scans: Vec<SandboxScan> = response.json().await?;
521 Ok(scans)
522 }
523 404 => Err(SandboxError::NotFound),
524 _ => {
525 let error_text = response.text().await.unwrap_or_default();
526 Err(SandboxError::Api(VeracodeError::InvalidResponse(
527 format!("HTTP {status}: {error_text}")
528 )))
529 }
530 }
531 }
532
533 pub async fn sandbox_exists(
544 &self,
545 application_guid: &str,
546 sandbox_guid: &str,
547 ) -> Result<bool, SandboxError> {
548 match self.get_sandbox(application_guid, sandbox_guid).await {
549 Ok(_) => Ok(true),
550 Err(SandboxError::NotFound) => Ok(false),
551 Err(e) => Err(e),
552 }
553 }
554
555 pub async fn get_sandbox_by_name(
566 &self,
567 application_guid: &str,
568 name: &str,
569 ) -> Result<Option<Sandbox>, SandboxError> {
570 let params = SandboxListParams {
571 name: Some(name.to_string()),
572 ..Default::default()
573 };
574
575 let sandboxes = self.list_sandboxes(application_guid, Some(params)).await?;
576 Ok(sandboxes.into_iter().find(|s| s.name == name))
577 }
578
579 fn validate_create_request(request: &CreateSandboxRequest) -> Result<(), SandboxError> {
581 if request.name.is_empty() {
582 return Err(SandboxError::InvalidInput("Sandbox name cannot be empty".to_string()));
583 }
584 if request.name.len() > 256 {
585 return Err(SandboxError::InvalidInput("Sandbox name too long (max 256 characters)".to_string()));
586 }
587
588 if request.name.contains(['<', '>', '"', '&', '\'']) {
590 return Err(SandboxError::InvalidInput("Sandbox name contains invalid characters".to_string()));
591 }
592
593 Ok(())
594 }
595
596 fn validate_update_request(request: &UpdateSandboxRequest) -> Result<(), SandboxError> {
598 if let Some(name) = &request.name {
599 if name.is_empty() {
600 return Err(SandboxError::InvalidInput("Sandbox name cannot be empty".to_string()));
601 }
602 if name.len() > 256 {
603 return Err(SandboxError::InvalidInput("Sandbox name too long (max 256 characters)".to_string()));
604 }
605
606 if name.contains(['<', '>', '"', '&', '\'']) {
608 return Err(SandboxError::InvalidInput("Sandbox name contains invalid characters".to_string()));
609 }
610 }
611
612 Ok(())
613 }
614}
615
616impl<'a> SandboxApi<'a> {
618 pub async fn create_simple_sandbox(
629 &self,
630 application_guid: &str,
631 name: &str,
632 ) -> Result<Sandbox, SandboxError> {
633 let request = CreateSandboxRequest {
634 name: name.to_string(),
635 description: None,
636 auto_recreate: None,
637 custom_fields: None,
638 team_identifiers: None,
639 };
640
641 self.create_sandbox(application_guid, request).await
642 }
643
644 pub async fn create_auto_recreate_sandbox(
656 &self,
657 application_guid: &str,
658 name: &str,
659 description: Option<String>,
660 ) -> Result<Sandbox, SandboxError> {
661 let request = CreateSandboxRequest {
662 name: name.to_string(),
663 description,
664 auto_recreate: Some(true),
665 custom_fields: None,
666 team_identifiers: None,
667 };
668
669 self.create_sandbox(application_guid, request).await
670 }
671
672 pub async fn update_sandbox_name(
684 &self,
685 application_guid: &str,
686 sandbox_guid: &str,
687 new_name: &str,
688 ) -> Result<Sandbox, SandboxError> {
689 let request = UpdateSandboxRequest {
690 name: Some(new_name.to_string()),
691 description: None,
692 auto_recreate: None,
693 custom_fields: None,
694 team_identifiers: None,
695 };
696
697 self.update_sandbox(application_guid, sandbox_guid, request).await
698 }
699
700 pub async fn count_sandboxes(&self, application_guid: &str) -> Result<usize, SandboxError> {
710 let sandboxes = self.list_sandboxes(application_guid, None).await?;
711 Ok(sandboxes.len())
712 }
713
714 pub async fn get_sandbox_id_from_guid(
727 &self,
728 application_guid: &str,
729 sandbox_guid: &str,
730 ) -> Result<String, SandboxError> {
731 let sandbox = self.get_sandbox(application_guid, sandbox_guid).await?;
732 match sandbox.id {
733 Some(id) => Ok(id.to_string()),
734 None => Err(SandboxError::InvalidInput("Sandbox has no numeric ID".to_string())),
735 }
736 }
737
738 pub async fn create_sandbox_if_not_exists(
753 &self,
754 application_guid: &str,
755 name: &str,
756 description: Option<String>,
757 ) -> Result<Sandbox, SandboxError> {
758 if let Some(existing_sandbox) = self.get_sandbox_by_name(application_guid, name).await? {
760 return Ok(existing_sandbox);
761 }
762
763 let create_request = CreateSandboxRequest {
765 name: name.to_string(),
766 description,
767 auto_recreate: Some(true), custom_fields: None,
769 team_identifiers: None,
770 };
771
772 self.create_sandbox(application_guid, create_request).await
773 }
774}
775
776#[cfg(test)]
777mod tests {
778 use super::*;
779
780 #[test]
781 fn test_validate_create_request() {
782 let valid_request = CreateSandboxRequest {
784 name: "valid-sandbox".to_string(),
785 description: None,
786 auto_recreate: None,
787 custom_fields: None,
788 team_identifiers: None,
789 };
790 assert!(SandboxApi::validate_create_request(&valid_request).is_ok());
791
792 let empty_name_request = CreateSandboxRequest {
794 name: String::new(),
795 description: None,
796 auto_recreate: None,
797 custom_fields: None,
798 team_identifiers: None,
799 };
800 assert!(SandboxApi::validate_create_request(&empty_name_request).is_err());
801
802 let long_name_request = CreateSandboxRequest {
804 name: "x".repeat(300),
805 description: None,
806 auto_recreate: None,
807 custom_fields: None,
808 team_identifiers: None,
809 };
810 assert!(SandboxApi::validate_create_request(&long_name_request).is_err());
811
812 let invalid_char_request = CreateSandboxRequest {
814 name: "invalid<name>".to_string(),
815 description: None,
816 auto_recreate: None,
817 custom_fields: None,
818 team_identifiers: None,
819 };
820 assert!(SandboxApi::validate_create_request(&invalid_char_request).is_err());
821 }
822
823 #[test]
824 fn test_sandbox_list_params_to_query() {
825 let params = SandboxListParams {
826 name: Some("test".to_string()),
827 page: Some(1),
828 size: Some(10),
829 ..Default::default()
830 };
831
832 let query_params = params.to_query_params();
833 assert_eq!(query_params.len(), 3);
834 assert!(query_params.contains(&("name".to_string(), "test".to_string())));
835 assert!(query_params.contains(&("page".to_string(), "1".to_string())));
836 assert!(query_params.contains(&("size".to_string(), "10".to_string())));
837 }
838
839 #[test]
840 fn test_sandbox_error_display() {
841 let error = SandboxError::NotFound;
842 assert_eq!(error.to_string(), "Sandbox not found");
843
844 let error = SandboxError::InvalidInput("test".to_string());
845 assert_eq!(error.to_string(), "Invalid input: test");
846
847 let error = SandboxError::LimitExceeded;
848 assert_eq!(error.to_string(), "Maximum number of sandboxes reached");
849 }
850}