veracode_platform/
scan.rs

1//! Scan API functionality for Veracode platform.
2//!
3//! This module provides functionality to interact with the Veracode Scan APIs,
4//! allowing you to upload files, initiate scans, and monitor scan progress for both
5//! application-level and sandbox scans. This implementation mirrors the Java API wrapper functionality.
6
7use chrono::{DateTime, Utc};
8use quick_xml::Reader;
9use quick_xml::events::Event;
10use serde::{Deserialize, Serialize};
11use std::path::Path;
12
13use crate::{VeracodeClient, VeracodeError};
14
15/// Represents an uploaded file in a sandbox
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct UploadedFile {
18    /// File ID assigned by Veracode
19    pub file_id: String,
20    /// Original filename
21    pub file_name: String,
22    /// File size in bytes
23    pub file_size: u64,
24    /// Upload timestamp
25    pub uploaded: DateTime<Utc>,
26    /// File status
27    pub file_status: String,
28    /// MD5 hash of the file
29    pub md5: Option<String>,
30}
31
32/// Represents pre-scan results for a sandbox
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct PreScanResults {
35    /// Build ID for the pre-scan
36    pub build_id: String,
37    /// Application ID
38    pub app_id: String,
39    /// Sandbox ID
40    pub sandbox_id: Option<String>,
41    /// Pre-scan status
42    pub status: String,
43    /// Available modules for scanning
44    pub modules: Vec<ScanModule>,
45    /// Pre-scan errors or warnings
46    pub messages: Vec<PreScanMessage>,
47}
48
49/// Represents a module available for scanning
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct ScanModule {
52    /// Module ID
53    pub id: String,
54    /// Module name
55    pub name: String,
56    /// Module type
57    pub module_type: String,
58    /// Whether module is fatal (required for scan)
59    pub is_fatal: bool,
60    /// Whether module should be selected for scanning
61    pub selected: bool,
62    /// Module size
63    pub size: Option<u64>,
64    /// Module platform
65    pub platform: Option<String>,
66}
67
68/// Represents a pre-scan message
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct PreScanMessage {
71    /// Message severity
72    pub severity: String,
73    /// Message text
74    pub text: String,
75    /// Associated module (if any)
76    pub module_name: Option<String>,
77}
78
79/// Represents scan information
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct ScanInfo {
82    /// Build ID
83    pub build_id: String,
84    /// Application ID
85    pub app_id: String,
86    /// Sandbox ID
87    pub sandbox_id: Option<String>,
88    /// Scan status
89    pub status: String,
90    /// Scan type
91    pub scan_type: String,
92    /// Analysis unit ID
93    pub analysis_unit_id: Option<String>,
94    /// Scan completion percentage
95    pub scan_progress_percentage: Option<u32>,
96    /// Scan started timestamp
97    pub scan_start: Option<DateTime<Utc>>,
98    /// Scan completed timestamp
99    pub scan_complete: Option<DateTime<Utc>>,
100    /// Total lines of code
101    pub total_lines_of_code: Option<u64>,
102}
103
104/// Request for uploading a file
105#[derive(Debug, Clone)]
106pub struct UploadFileRequest {
107    /// Application ID
108    pub app_id: String,
109    /// File path to upload
110    pub file_path: String,
111    /// Name to save file as (optional)
112    pub save_as: Option<String>,
113    /// Sandbox ID (optional, for sandbox uploads)
114    pub sandbox_id: Option<String>,
115}
116
117/// Request for uploading a large file
118#[derive(Debug, Clone)]
119pub struct UploadLargeFileRequest {
120    /// Application ID
121    pub app_id: String,
122    /// File path to upload
123    pub file_path: String,
124    /// Name to save file as (optional, for flaw matching)
125    pub filename: Option<String>,
126    /// Sandbox ID (optional, for sandbox uploads)
127    pub sandbox_id: Option<String>,
128}
129
130/// Progress information for file uploads
131#[derive(Debug, Clone)]
132pub struct UploadProgress {
133    /// Bytes uploaded so far
134    pub bytes_uploaded: u64,
135    /// Total bytes to upload
136    pub total_bytes: u64,
137    /// Progress percentage (0-100)
138    pub percentage: f64,
139}
140
141/// Callback trait for upload progress tracking
142pub trait UploadProgressCallback: Send + Sync {
143    /// Called when upload progress changes
144    fn on_progress(&self, progress: UploadProgress);
145    /// Called when upload completes successfully
146    fn on_completed(&self);
147    /// Called when upload fails
148    fn on_error(&self, error: &str);
149}
150
151/// Request for beginning a pre-scan
152#[derive(Debug, Clone)]
153pub struct BeginPreScanRequest {
154    /// Application ID
155    pub app_id: String,
156    /// Sandbox ID (optional)
157    pub sandbox_id: Option<String>,
158    /// Auto-scan flag
159    pub auto_scan: Option<bool>,
160    /// Scan all non-fatal top level modules
161    pub scan_all_nonfatal_top_level_modules: Option<bool>,
162    /// Include new modules
163    pub include_new_modules: Option<bool>,
164}
165
166/// Request for beginning a scan
167#[derive(Debug, Clone)]
168pub struct BeginScanRequest {
169    /// Application ID
170    pub app_id: String,
171    /// Sandbox ID (optional)
172    pub sandbox_id: Option<String>,
173    /// Modules to scan (comma-separated module IDs)
174    pub modules: Option<String>,
175    /// Scan all top level modules
176    pub scan_all_top_level_modules: Option<bool>,
177    /// Scan all non-fatal top level modules
178    pub scan_all_nonfatal_top_level_modules: Option<bool>,
179    /// Scan previously selected modules
180    pub scan_previously_selected_modules: Option<bool>,
181}
182
183/// Scan specific error types
184#[derive(Debug)]
185pub enum ScanError {
186    /// Veracode API error
187    Api(VeracodeError),
188    /// File not found
189    FileNotFound(String),
190    /// Invalid file format
191    InvalidFileFormat(String),
192    /// Upload failed
193    UploadFailed(String),
194    /// Scan failed
195    ScanFailed(String),
196    /// Pre-scan failed
197    PreScanFailed(String),
198    /// Build not found
199    BuildNotFound,
200    /// Application not found
201    ApplicationNotFound,
202    /// Sandbox not found
203    SandboxNotFound,
204    /// Unauthorized access
205    Unauthorized,
206    /// Permission denied
207    PermissionDenied,
208    /// Invalid parameter
209    InvalidParameter(String),
210    /// File too large (exceeds 2GB limit)
211    FileTooLarge(String),
212    /// Upload or prescan already in progress
213    UploadInProgress,
214    /// Scan in progress, cannot upload
215    ScanInProgress,
216    /// Build creation failed
217    BuildCreationFailed(String),
218    /// Chunked upload failed
219    ChunkedUploadFailed(String),
220}
221
222impl std::fmt::Display for ScanError {
223    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
224        match self {
225            ScanError::Api(err) => write!(f, "API error: {err}"),
226            ScanError::FileNotFound(path) => write!(f, "File not found: {path}"),
227            ScanError::InvalidFileFormat(msg) => write!(f, "Invalid file format: {msg}"),
228            ScanError::UploadFailed(msg) => write!(f, "Upload failed: {msg}"),
229            ScanError::ScanFailed(msg) => write!(f, "Scan failed: {msg}"),
230            ScanError::PreScanFailed(msg) => write!(f, "Pre-scan failed: {msg}"),
231            ScanError::BuildNotFound => write!(f, "Build not found"),
232            ScanError::ApplicationNotFound => write!(f, "Application not found"),
233            ScanError::SandboxNotFound => write!(f, "Sandbox not found"),
234            ScanError::Unauthorized => write!(f, "Unauthorized access"),
235            ScanError::PermissionDenied => write!(f, "Permission denied"),
236            ScanError::InvalidParameter(msg) => write!(f, "Invalid parameter: {msg}"),
237            ScanError::FileTooLarge(msg) => write!(f, "File too large: {msg}"),
238            ScanError::UploadInProgress => write!(f, "Upload or prescan already in progress"),
239            ScanError::ScanInProgress => write!(f, "Scan in progress, cannot upload"),
240            ScanError::BuildCreationFailed(msg) => write!(f, "Build creation failed: {msg}"),
241            ScanError::ChunkedUploadFailed(msg) => write!(f, "Chunked upload failed: {msg}"),
242        }
243    }
244}
245
246impl std::error::Error for ScanError {}
247
248impl From<VeracodeError> for ScanError {
249    fn from(err: VeracodeError) -> Self {
250        ScanError::Api(err)
251    }
252}
253
254impl From<reqwest::Error> for ScanError {
255    fn from(err: reqwest::Error) -> Self {
256        ScanError::Api(VeracodeError::Http(err))
257    }
258}
259
260impl From<serde_json::Error> for ScanError {
261    fn from(err: serde_json::Error) -> Self {
262        ScanError::Api(VeracodeError::Serialization(err))
263    }
264}
265
266impl From<std::io::Error> for ScanError {
267    fn from(err: std::io::Error) -> Self {
268        ScanError::FileNotFound(err.to_string())
269    }
270}
271
272/// Veracode Scan API operations
273pub struct ScanApi {
274    client: VeracodeClient,
275}
276
277impl ScanApi {
278    /// Create a new ScanApi instance
279    pub fn new(client: VeracodeClient) -> Self {
280        Self { client }
281    }
282
283    /// Upload a file to an application or sandbox
284    ///
285    /// # Arguments
286    ///
287    /// * `request` - The upload file request
288    ///
289    /// # Returns
290    ///
291    /// A `Result` containing the uploaded file information or an error.
292    pub async fn upload_file(&self, request: UploadFileRequest) -> Result<UploadedFile, ScanError> {
293        // Validate file exists
294        if !Path::new(&request.file_path).exists() {
295            return Err(ScanError::FileNotFound(request.file_path));
296        }
297
298        let endpoint = "/api/5.0/uploadfile.do";
299
300        // Build query parameters like Java implementation
301        let mut query_params = Vec::new();
302        query_params.push(("app_id", request.app_id.as_str()));
303
304        if let Some(sandbox_id) = &request.sandbox_id {
305            query_params.push(("sandbox_id", sandbox_id.as_str()));
306        }
307
308        if let Some(save_as) = &request.save_as {
309            query_params.push(("save_as", save_as.as_str()));
310        }
311
312        // Read file data
313        let file_data = std::fs::read(&request.file_path)?;
314
315        // Get filename from path
316        let filename = Path::new(&request.file_path)
317            .file_name()
318            .and_then(|f| f.to_str())
319            .unwrap_or("file");
320
321        let response = self
322            .client
323            .upload_file_with_query_params(endpoint, &query_params, "file", filename, file_data)
324            .await?;
325
326        let status = response.status().as_u16();
327        match status {
328            200 => {
329                let response_text = response.text().await?;
330                self.parse_upload_response(&response_text, &request.file_path)
331            }
332            400 => {
333                let error_text = response.text().await.unwrap_or_default();
334                Err(ScanError::InvalidParameter(error_text))
335            }
336            401 => Err(ScanError::Unauthorized),
337            403 => Err(ScanError::PermissionDenied),
338            404 => {
339                if request.sandbox_id.is_some() {
340                    Err(ScanError::SandboxNotFound)
341                } else {
342                    Err(ScanError::ApplicationNotFound)
343                }
344            }
345            _ => {
346                let error_text = response.text().await.unwrap_or_default();
347                Err(ScanError::UploadFailed(format!(
348                    "HTTP {status}: {error_text}"
349                )))
350            }
351        }
352    }
353
354    /// Upload a large file using the uploadlargefile.do endpoint
355    ///
356    /// This method uploads large files (up to 2GB) to an existing build.
357    /// Unlike uploadfile.do, this endpoint requires a build to exist before uploading.
358    /// It automatically creates a build if one doesn't exist.
359    ///
360    /// # Arguments
361    ///
362    /// * `request` - The upload large file request
363    ///
364    /// # Returns
365    ///
366    /// A `Result` containing the uploaded file information or an error.
367    pub async fn upload_large_file(
368        &self,
369        request: UploadLargeFileRequest,
370    ) -> Result<UploadedFile, ScanError> {
371        // Validate file exists
372        if !Path::new(&request.file_path).exists() {
373            return Err(ScanError::FileNotFound(request.file_path));
374        }
375
376        // Check file size (2GB limit for uploadlargefile.do)
377        let file_metadata = std::fs::metadata(&request.file_path)?;
378        let file_size = file_metadata.len();
379        const MAX_FILE_SIZE: u64 = 2 * 1024 * 1024 * 1024; // 2GB
380
381        if file_size > MAX_FILE_SIZE {
382            return Err(ScanError::FileTooLarge(format!(
383                "File size {file_size} bytes exceeds 2GB limit"
384            )));
385        }
386
387        let endpoint = "uploadlargefile.do"; // No version prefix for large file upload
388
389        // Build query parameters
390        let mut query_params = Vec::new();
391        query_params.push(("app_id", request.app_id.as_str()));
392
393        if let Some(sandbox_id) = &request.sandbox_id {
394            query_params.push(("sandbox_id", sandbox_id.as_str()));
395        }
396
397        if let Some(filename) = &request.filename {
398            query_params.push(("filename", filename.as_str()));
399        }
400
401        // Read file data
402        let file_data = std::fs::read(&request.file_path)?;
403
404        let response = self
405            .client
406            .upload_file_binary(endpoint, &query_params, file_data, "binary/octet-stream")
407            .await?;
408
409        let status = response.status().as_u16();
410        match status {
411            200 => {
412                let response_text = response.text().await?;
413                self.parse_upload_response(&response_text, &request.file_path)
414            }
415            400 => {
416                let error_text = response.text().await.unwrap_or_default();
417                if error_text.contains("upload or prescan in progress") {
418                    Err(ScanError::UploadInProgress)
419                } else if error_text.contains("scan in progress") {
420                    Err(ScanError::ScanInProgress)
421                } else {
422                    Err(ScanError::InvalidParameter(error_text))
423                }
424            }
425            401 => Err(ScanError::Unauthorized),
426            403 => Err(ScanError::PermissionDenied),
427            404 => {
428                if request.sandbox_id.is_some() {
429                    Err(ScanError::SandboxNotFound)
430                } else {
431                    Err(ScanError::ApplicationNotFound)
432                }
433            }
434            413 => Err(ScanError::FileTooLarge(
435                "File size exceeds server limits".to_string(),
436            )),
437            _ => {
438                let error_text = response.text().await.unwrap_or_default();
439                Err(ScanError::UploadFailed(format!(
440                    "HTTP {status}: {error_text}"
441                )))
442            }
443        }
444    }
445
446    /// Upload a large file with progress tracking
447    ///
448    /// This method provides the same functionality as upload_large_file but with
449    /// progress tracking capabilities through a callback function.
450    ///
451    /// # Arguments
452    ///
453    /// * `request` - The upload large file request
454    /// * `progress_callback` - Callback function for progress updates (bytes_uploaded, total_bytes, percentage)
455    ///
456    /// # Returns
457    ///
458    /// A `Result` containing the uploaded file information or an error.
459    pub async fn upload_large_file_with_progress<F>(
460        &self,
461        request: UploadLargeFileRequest,
462        progress_callback: F,
463    ) -> Result<UploadedFile, ScanError>
464    where
465        F: Fn(u64, u64, f64) + Send + Sync,
466    {
467        // Validate file exists
468        if !Path::new(&request.file_path).exists() {
469            return Err(ScanError::FileNotFound(request.file_path));
470        }
471
472        // Check file size (2GB limit)
473        let file_metadata = std::fs::metadata(&request.file_path)?;
474        let file_size = file_metadata.len();
475        const MAX_FILE_SIZE: u64 = 2 * 1024 * 1024 * 1024; // 2GB
476
477        if file_size > MAX_FILE_SIZE {
478            return Err(ScanError::FileTooLarge(format!(
479                "File size {file_size} bytes exceeds 2GB limit"
480            )));
481        }
482
483        let endpoint = "uploadlargefile.do";
484
485        // Build query parameters
486        let mut query_params = Vec::new();
487        query_params.push(("app_id", request.app_id.as_str()));
488
489        if let Some(sandbox_id) = &request.sandbox_id {
490            query_params.push(("sandbox_id", sandbox_id.as_str()));
491        }
492
493        if let Some(filename) = &request.filename {
494            query_params.push(("filename", filename.as_str()));
495        }
496
497        let response = self
498            .client
499            .upload_large_file_chunked(
500                endpoint,
501                &query_params,
502                &request.file_path,
503                Some("binary/octet-stream"),
504                Some(progress_callback),
505            )
506            .await?;
507
508        let status = response.status().as_u16();
509        match status {
510            200 => {
511                let response_text = response.text().await?;
512                self.parse_upload_response(&response_text, &request.file_path)
513            }
514            400 => {
515                let error_text = response.text().await.unwrap_or_default();
516                if error_text.contains("upload or prescan in progress") {
517                    Err(ScanError::UploadInProgress)
518                } else if error_text.contains("scan in progress") {
519                    Err(ScanError::ScanInProgress)
520                } else {
521                    Err(ScanError::InvalidParameter(error_text))
522                }
523            }
524            401 => Err(ScanError::Unauthorized),
525            403 => Err(ScanError::PermissionDenied),
526            404 => {
527                if request.sandbox_id.is_some() {
528                    Err(ScanError::SandboxNotFound)
529                } else {
530                    Err(ScanError::ApplicationNotFound)
531                }
532            }
533            413 => Err(ScanError::FileTooLarge(
534                "File size exceeds server limits".to_string(),
535            )),
536            _ => {
537                let error_text = response.text().await.unwrap_or_default();
538                Err(ScanError::ChunkedUploadFailed(format!(
539                    "HTTP {status}: {error_text}"
540                )))
541            }
542        }
543    }
544
545    /// Intelligently choose between uploadfile.do and uploadlargefile.do
546    ///
547    /// This method automatically selects the appropriate upload endpoint based on
548    /// file size and other factors, similar to the Java API wrapper behavior.
549    ///
550    /// # Arguments
551    ///
552    /// * `request` - The upload file request (converted to appropriate format)
553    ///
554    /// # Returns
555    ///
556    /// A `Result` containing the uploaded file information or an error.
557    pub async fn upload_file_smart(
558        &self,
559        request: UploadFileRequest,
560    ) -> Result<UploadedFile, ScanError> {
561        // Check if file exists
562        if !Path::new(&request.file_path).exists() {
563            return Err(ScanError::FileNotFound(request.file_path));
564        }
565
566        // Get file size to determine upload method
567        let file_metadata = std::fs::metadata(&request.file_path)?;
568        let file_size = file_metadata.len();
569
570        // Use large file upload for files over 100MB or when build might exist
571        const LARGE_FILE_THRESHOLD: u64 = 100 * 1024 * 1024; // 100MB
572
573        if file_size > LARGE_FILE_THRESHOLD {
574            // Convert to large file request format
575            let large_request = UploadLargeFileRequest {
576                app_id: request.app_id.clone(),
577                file_path: request.file_path.clone(),
578                filename: request.save_as.clone(),
579                sandbox_id: request.sandbox_id.clone(),
580            };
581
582            // Try large file upload first, fall back to regular upload if needed
583            match self.upload_large_file(large_request).await {
584                Ok(result) => Ok(result),
585                Err(ScanError::Api(_)) => {
586                    // Fall back to regular upload if large file upload fails
587                    self.upload_file(request).await
588                }
589                Err(e) => Err(e),
590            }
591        } else {
592            // Use regular upload for smaller files
593            self.upload_file(request).await
594        }
595    }
596
597    /// Begin pre-scan for an application or sandbox
598    ///
599    /// # Arguments
600    ///
601    /// * `request` - The pre-scan request
602    ///
603    /// # Returns
604    ///
605    /// A `Result` indicating success or an error.
606    pub async fn begin_prescan(&self, request: BeginPreScanRequest) -> Result<(), ScanError> {
607        let endpoint = "/api/5.0/beginprescan.do";
608
609        // Build query parameters like Java implementation
610        let mut query_params = Vec::new();
611        query_params.push(("app_id", request.app_id.as_str()));
612
613        if let Some(sandbox_id) = &request.sandbox_id {
614            query_params.push(("sandbox_id", sandbox_id.as_str()));
615        }
616
617        if let Some(auto_scan) = request.auto_scan {
618            query_params.push(("auto_scan", if auto_scan { "true" } else { "false" }));
619        }
620
621        if let Some(scan_all) = request.scan_all_nonfatal_top_level_modules {
622            query_params.push((
623                "scan_all_nonfatal_top_level_modules",
624                if scan_all { "true" } else { "false" },
625            ));
626        }
627
628        if let Some(include_new) = request.include_new_modules {
629            query_params.push((
630                "include_new_modules",
631                if include_new { "true" } else { "false" },
632            ));
633        }
634
635        let response = self.client.get_with_params(endpoint, &query_params).await?;
636
637        let status = response.status().as_u16();
638        match status {
639            200 => {
640                let response_text = response.text().await?;
641                // Just validate the response is successful, don't parse build_id
642                // since we already have it from ensure_build_exists
643                self.validate_scan_response(&response_text)?;
644                Ok(())
645            }
646            400 => {
647                let error_text = response.text().await.unwrap_or_default();
648                Err(ScanError::InvalidParameter(error_text))
649            }
650            401 => Err(ScanError::Unauthorized),
651            403 => Err(ScanError::PermissionDenied),
652            404 => {
653                if request.sandbox_id.is_some() {
654                    Err(ScanError::SandboxNotFound)
655                } else {
656                    Err(ScanError::ApplicationNotFound)
657                }
658            }
659            _ => {
660                let error_text = response.text().await.unwrap_or_default();
661                Err(ScanError::PreScanFailed(format!(
662                    "HTTP {status}: {error_text}"
663                )))
664            }
665        }
666    }
667
668    /// Get pre-scan results for an application or sandbox
669    ///
670    /// # Arguments
671    ///
672    /// * `app_id` - The application ID
673    /// * `sandbox_id` - The sandbox ID (optional)
674    /// * `build_id` - The build ID (optional)
675    ///
676    /// # Returns
677    ///
678    /// A `Result` containing the pre-scan results or an error.
679    pub async fn get_prescan_results(
680        &self,
681        app_id: &str,
682        sandbox_id: Option<&str>,
683        build_id: Option<&str>,
684    ) -> Result<PreScanResults, ScanError> {
685        let endpoint = "/api/5.0/getprescanresults.do";
686
687        let mut params = Vec::new();
688        params.push(("app_id", app_id));
689
690        if let Some(sandbox_id) = sandbox_id {
691            params.push(("sandbox_id", sandbox_id));
692        }
693
694        if let Some(build_id) = build_id {
695            params.push(("build_id", build_id));
696        }
697
698        let response = self.client.get_with_params(endpoint, &params).await?;
699
700        let status = response.status().as_u16();
701        match status {
702            200 => {
703                let response_text = response.text().await?;
704                self.parse_prescan_results(&response_text, app_id, sandbox_id)
705            }
706            401 => Err(ScanError::Unauthorized),
707            403 => Err(ScanError::PermissionDenied),
708            404 => {
709                if sandbox_id.is_some() {
710                    Err(ScanError::SandboxNotFound)
711                } else {
712                    Err(ScanError::ApplicationNotFound)
713                }
714            }
715            _ => {
716                let error_text = response.text().await.unwrap_or_default();
717                Err(ScanError::PreScanFailed(format!(
718                    "HTTP {status}: {error_text}"
719                )))
720            }
721        }
722    }
723
724    /// Begin scan for an application or sandbox
725    ///
726    /// # Arguments
727    ///
728    /// * `request` - The scan request
729    ///
730    /// # Returns
731    ///
732    /// A `Result` indicating success or an error.
733    pub async fn begin_scan(&self, request: BeginScanRequest) -> Result<(), ScanError> {
734        let endpoint = "/api/5.0/beginscan.do";
735
736        // Build query parameters like Java implementation
737        let mut query_params = Vec::new();
738        query_params.push(("app_id", request.app_id.as_str()));
739
740        if let Some(sandbox_id) = &request.sandbox_id {
741            query_params.push(("sandbox_id", sandbox_id.as_str()));
742        }
743
744        if let Some(modules) = &request.modules {
745            query_params.push(("modules", modules.as_str()));
746        }
747
748        if let Some(scan_all) = request.scan_all_top_level_modules {
749            query_params.push((
750                "scan_all_top_level_modules",
751                if scan_all { "true" } else { "false" },
752            ));
753        }
754
755        if let Some(scan_all_nonfatal) = request.scan_all_nonfatal_top_level_modules {
756            query_params.push((
757                "scan_all_nonfatal_top_level_modules",
758                if scan_all_nonfatal { "true" } else { "false" },
759            ));
760        }
761
762        if let Some(scan_previous) = request.scan_previously_selected_modules {
763            query_params.push((
764                "scan_previously_selected_modules",
765                if scan_previous { "true" } else { "false" },
766            ));
767        }
768
769        let response = self.client.get_with_params(endpoint, &query_params).await?;
770
771        let status = response.status().as_u16();
772        match status {
773            200 => {
774                let response_text = response.text().await?;
775                // Just validate the response is successful, don't parse build_id
776                // since we already have it from ensure_build_exists
777                self.validate_scan_response(&response_text)?;
778                Ok(())
779            }
780            400 => {
781                let error_text = response.text().await.unwrap_or_default();
782                Err(ScanError::InvalidParameter(error_text))
783            }
784            401 => Err(ScanError::Unauthorized),
785            403 => Err(ScanError::PermissionDenied),
786            404 => {
787                if request.sandbox_id.is_some() {
788                    Err(ScanError::SandboxNotFound)
789                } else {
790                    Err(ScanError::ApplicationNotFound)
791                }
792            }
793            _ => {
794                let error_text = response.text().await.unwrap_or_default();
795                Err(ScanError::ScanFailed(format!(
796                    "HTTP {status}: {error_text}"
797                )))
798            }
799        }
800    }
801
802    /// Get list of uploaded files for an application or sandbox
803    ///
804    /// # Arguments
805    ///
806    /// * `app_id` - The application ID
807    /// * `sandbox_id` - The sandbox ID (optional)
808    /// * `build_id` - The build ID (optional)
809    ///
810    /// # Returns
811    ///
812    /// A `Result` containing the list of uploaded files or an error.
813    pub async fn get_file_list(
814        &self,
815        app_id: &str,
816        sandbox_id: Option<&str>,
817        build_id: Option<&str>,
818    ) -> Result<Vec<UploadedFile>, ScanError> {
819        let endpoint = "/api/5.0/getfilelist.do";
820
821        let mut params = Vec::new();
822        params.push(("app_id", app_id));
823
824        if let Some(sandbox_id) = sandbox_id {
825            params.push(("sandbox_id", sandbox_id));
826        }
827
828        if let Some(build_id) = build_id {
829            params.push(("build_id", build_id));
830        }
831
832        let response = self.client.get_with_params(endpoint, &params).await?;
833
834        let status = response.status().as_u16();
835        match status {
836            200 => {
837                let response_text = response.text().await?;
838                self.parse_file_list(&response_text)
839            }
840            401 => Err(ScanError::Unauthorized),
841            403 => Err(ScanError::PermissionDenied),
842            404 => {
843                if sandbox_id.is_some() {
844                    Err(ScanError::SandboxNotFound)
845                } else {
846                    Err(ScanError::ApplicationNotFound)
847                }
848            }
849            _ => {
850                let error_text = response.text().await.unwrap_or_default();
851                Err(ScanError::Api(VeracodeError::InvalidResponse(format!(
852                    "HTTP {status}: {error_text}"
853                ))))
854            }
855        }
856    }
857
858    /// Remove a file from an application or sandbox
859    ///
860    /// # Arguments
861    ///
862    /// * `app_id` - The application ID
863    /// * `file_id` - The file ID to remove
864    /// * `sandbox_id` - The sandbox ID (optional)
865    ///
866    /// # Returns
867    ///
868    /// A `Result` indicating success or an error.
869    pub async fn remove_file(
870        &self,
871        app_id: &str,
872        file_id: &str,
873        sandbox_id: Option<&str>,
874    ) -> Result<(), ScanError> {
875        let endpoint = "/api/5.0/removefile.do";
876
877        // Build query parameters like Java implementation
878        let mut query_params = Vec::new();
879        query_params.push(("app_id", app_id));
880        query_params.push(("file_id", file_id));
881
882        if let Some(sandbox_id) = sandbox_id {
883            query_params.push(("sandbox_id", sandbox_id));
884        }
885
886        let response = self.client.get_with_params(endpoint, &query_params).await?;
887
888        let status = response.status().as_u16();
889        match status {
890            200 => Ok(()),
891            400 => {
892                let error_text = response.text().await.unwrap_or_default();
893                Err(ScanError::InvalidParameter(error_text))
894            }
895            401 => Err(ScanError::Unauthorized),
896            403 => Err(ScanError::PermissionDenied),
897            404 => Err(ScanError::FileNotFound(file_id.to_string())),
898            _ => {
899                let error_text = response.text().await.unwrap_or_default();
900                Err(ScanError::Api(VeracodeError::InvalidResponse(format!(
901                    "HTTP {status}: {error_text}"
902                ))))
903            }
904        }
905    }
906
907    /// Delete a build from an application or sandbox
908    ///
909    /// This removes all uploaded files and scan data for a specific build.
910    ///
911    /// # Arguments
912    ///
913    /// * `app_id` - The application ID
914    /// * `build_id` - The build ID to delete
915    /// * `sandbox_id` - The sandbox ID (optional)
916    ///
917    /// # Returns
918    ///
919    /// A `Result` indicating success or an error.
920    pub async fn delete_build(
921        &self,
922        app_id: &str,
923        build_id: &str,
924        sandbox_id: Option<&str>,
925    ) -> Result<(), ScanError> {
926        let endpoint = "/api/5.0/deletebuild.do";
927
928        // Build query parameters like Java implementation
929        let mut query_params = Vec::new();
930        query_params.push(("app_id", app_id));
931        query_params.push(("build_id", build_id));
932
933        if let Some(sandbox_id) = sandbox_id {
934            query_params.push(("sandbox_id", sandbox_id));
935        }
936
937        let response = self.client.get_with_params(endpoint, &query_params).await?;
938
939        let status = response.status().as_u16();
940        match status {
941            200 => Ok(()),
942            400 => {
943                let error_text = response.text().await.unwrap_or_default();
944                Err(ScanError::InvalidParameter(error_text))
945            }
946            401 => Err(ScanError::Unauthorized),
947            403 => Err(ScanError::PermissionDenied),
948            404 => Err(ScanError::BuildNotFound),
949            _ => {
950                let error_text = response.text().await.unwrap_or_default();
951                Err(ScanError::Api(VeracodeError::InvalidResponse(format!(
952                    "HTTP {status}: {error_text}"
953                ))))
954            }
955        }
956    }
957
958    /// Delete all builds for an application or sandbox
959    ///
960    /// This removes all uploaded files and scan data for all builds.
961    /// Use with caution as this is irreversible.
962    ///
963    /// # Arguments
964    ///
965    /// * `app_id` - The application ID
966    /// * `sandbox_id` - The sandbox ID (optional)
967    ///
968    /// # Returns
969    ///
970    /// A `Result` indicating success or an error.
971    pub async fn delete_all_builds(
972        &self,
973        app_id: &str,
974        sandbox_id: Option<&str>,
975    ) -> Result<(), ScanError> {
976        // First get list of builds
977        let build_info = self.get_build_info(app_id, None, sandbox_id).await?;
978
979        if !build_info.build_id.is_empty() && build_info.build_id != "unknown" {
980            println!("   🗑️  Deleting build: {}", build_info.build_id);
981            self.delete_build(app_id, &build_info.build_id, sandbox_id)
982                .await?;
983        }
984
985        Ok(())
986    }
987
988    /// Get build information for an application or sandbox
989    ///
990    /// # Arguments
991    ///
992    /// * `app_id` - The application ID
993    /// * `build_id` - The build ID (optional)
994    /// * `sandbox_id` - The sandbox ID (optional)
995    ///
996    /// # Returns
997    ///
998    /// A `Result` containing the scan information or an error.
999    pub async fn get_build_info(
1000        &self,
1001        app_id: &str,
1002        build_id: Option<&str>,
1003        sandbox_id: Option<&str>,
1004    ) -> Result<ScanInfo, ScanError> {
1005        let endpoint = "/api/5.0/getbuildinfo.do";
1006
1007        let mut params = Vec::new();
1008        params.push(("app_id", app_id));
1009
1010        if let Some(build_id) = build_id {
1011            params.push(("build_id", build_id));
1012        }
1013
1014        if let Some(sandbox_id) = sandbox_id {
1015            params.push(("sandbox_id", sandbox_id));
1016        }
1017
1018        let response = self.client.get_with_params(endpoint, &params).await?;
1019
1020        let status = response.status().as_u16();
1021        match status {
1022            200 => {
1023                let response_text = response.text().await?;
1024                self.parse_build_info(&response_text, app_id, sandbox_id)
1025            }
1026            401 => Err(ScanError::Unauthorized),
1027            403 => Err(ScanError::PermissionDenied),
1028            404 => Err(ScanError::BuildNotFound),
1029            _ => {
1030                let error_text = response.text().await.unwrap_or_default();
1031                Err(ScanError::Api(VeracodeError::InvalidResponse(format!(
1032                    "HTTP {status}: {error_text}"
1033                ))))
1034            }
1035        }
1036    }
1037
1038    // Helper methods for parsing XML responses (Veracode API returns XML)
1039
1040    fn parse_upload_response(&self, xml: &str, file_path: &str) -> Result<UploadedFile, ScanError> {
1041        let mut reader = Reader::from_str(xml);
1042        reader.config_mut().trim_text(true);
1043
1044        let mut buf = Vec::new();
1045        let mut file_id = None;
1046        let mut file_status = "Unknown".to_string();
1047        let mut _md5: Option<String> = None;
1048
1049        loop {
1050            match reader.read_event_into(&mut buf) {
1051                Ok(Event::Start(ref e)) => {
1052                    if e.name().as_ref() == b"file" {
1053                        // Extract file_id from attributes
1054                        for attr in e.attributes().flatten() {
1055                            if attr.key.as_ref() == b"file_id" {
1056                                file_id = Some(String::from_utf8_lossy(&attr.value).to_string());
1057                            }
1058                        }
1059                    }
1060                }
1061                Ok(Event::Text(e)) => {
1062                    let text = std::str::from_utf8(&e).unwrap_or_default();
1063                    // Check for success/error messages
1064                    if text.contains("successfully uploaded") {
1065                        file_status = "Uploaded".to_string();
1066                    } else if text.contains("error") || text.contains("failed") {
1067                        file_status = "Failed".to_string();
1068                    }
1069                }
1070                Ok(Event::Eof) => break,
1071                Err(e) => {
1072                    eprintln!("Error parsing XML: {e}");
1073                    break;
1074                }
1075                _ => {}
1076            }
1077            buf.clear();
1078        }
1079
1080        let filename = Path::new(file_path)
1081            .file_name()
1082            .and_then(|f| f.to_str())
1083            .unwrap_or("file")
1084            .to_string();
1085
1086        Ok(UploadedFile {
1087            file_id: file_id.unwrap_or_else(|| format!("file_{}", chrono::Utc::now().timestamp())),
1088            file_name: filename,
1089            file_size: std::fs::metadata(file_path).map(|m| m.len()).unwrap_or(0),
1090            uploaded: Utc::now(),
1091            file_status,
1092            md5: None,
1093        })
1094    }
1095
1096    #[allow(dead_code)]
1097    fn parse_build_id_response(&self, xml: &str) -> Result<String, ScanError> {
1098        let mut reader = Reader::from_str(xml);
1099        reader.config_mut().trim_text(true);
1100
1101        let mut buf = Vec::new();
1102        let mut build_id = None;
1103
1104        loop {
1105            match reader.read_event_into(&mut buf) {
1106                Ok(Event::Start(ref e)) => {
1107                    match e.name().as_ref() {
1108                        b"buildinfo" | b"build" => {
1109                            // Extract build_id from attributes
1110                            for attr in e.attributes().flatten() {
1111                                if attr.key.as_ref() == b"build_id" {
1112                                    build_id =
1113                                        Some(String::from_utf8_lossy(&attr.value).to_string());
1114                                }
1115                            }
1116                        }
1117                        _ => {}
1118                    }
1119                }
1120                Ok(Event::Eof) => break,
1121                Err(e) => {
1122                    eprintln!("Error parsing XML: {e}");
1123                    break;
1124                }
1125                _ => {}
1126            }
1127            buf.clear();
1128        }
1129
1130        build_id
1131            .ok_or_else(|| ScanError::PreScanFailed("No build_id found in response".to_string()))
1132    }
1133
1134    /// Validate scan response for basic success without parsing build_id
1135    fn validate_scan_response(&self, xml: &str) -> Result<(), ScanError> {
1136        // Check for basic error conditions in the response
1137        if xml.contains("<error>") {
1138            // Extract error message if present
1139            let mut reader = Reader::from_str(xml);
1140            reader.config_mut().trim_text(true);
1141
1142            let mut buf = Vec::new();
1143            let mut in_error = false;
1144            let mut error_message = String::new();
1145
1146            loop {
1147                match reader.read_event_into(&mut buf) {
1148                    Ok(Event::Start(ref e)) if e.name().as_ref() == b"error" => {
1149                        in_error = true;
1150                    }
1151                    Ok(Event::Text(ref e)) if in_error => {
1152                        error_message.push_str(&String::from_utf8_lossy(e));
1153                    }
1154                    Ok(Event::End(ref e)) if e.name().as_ref() == b"error" => {
1155                        break;
1156                    }
1157                    Ok(Event::Eof) => break,
1158                    Err(e) => {
1159                        return Err(ScanError::ScanFailed(format!("XML parsing error: {e}")));
1160                    }
1161                    _ => {}
1162                }
1163                buf.clear();
1164            }
1165
1166            if !error_message.is_empty() {
1167                return Err(ScanError::ScanFailed(error_message));
1168            } else {
1169                return Err(ScanError::ScanFailed(
1170                    "Unknown error in scan response".to_string(),
1171                ));
1172            }
1173        }
1174
1175        // Check for successful response indicators
1176        if xml.contains("<buildinfo") || xml.contains("<build") {
1177            Ok(())
1178        } else {
1179            Err(ScanError::ScanFailed(
1180                "Invalid scan response format".to_string(),
1181            ))
1182        }
1183    }
1184
1185    fn parse_prescan_results(
1186        &self,
1187        xml: &str,
1188        app_id: &str,
1189        sandbox_id: Option<&str>,
1190    ) -> Result<PreScanResults, ScanError> {
1191        // Check if response contains an error element (prescan not ready yet)
1192        if xml.contains("<error>") && xml.contains("Prescan results not available") {
1193            // Return a special status indicating prescan is still in progress
1194            return Ok(PreScanResults {
1195                build_id: String::new(),
1196                app_id: app_id.to_string(),
1197                sandbox_id: sandbox_id.map(|s| s.to_string()),
1198                status: "Pre-Scan Submitted".to_string(), // Indicates still in progress
1199                modules: Vec::new(),
1200                messages: Vec::new(),
1201            });
1202        }
1203
1204        let mut reader = Reader::from_str(xml);
1205        reader.config_mut().trim_text(true);
1206
1207        let mut buf = Vec::new();
1208        let mut build_id = None;
1209        let mut modules = Vec::new();
1210        let messages = Vec::new();
1211        let mut has_prescan_results = false;
1212        let mut has_fatal_errors = false;
1213
1214        loop {
1215            match reader.read_event_into(&mut buf) {
1216                Ok(Event::Start(ref e)) => {
1217                    match e.name().as_ref() {
1218                        b"prescanresults" => {
1219                            has_prescan_results = true;
1220                            // Extract build_id from prescanresults attributes if present
1221                            for attr in e.attributes().flatten() {
1222                                if attr.key.as_ref() == b"build_id" {
1223                                    build_id =
1224                                        Some(String::from_utf8_lossy(&attr.value).to_string());
1225                                }
1226                            }
1227                        }
1228                        b"module" => {
1229                            let mut module = ScanModule {
1230                                id: String::new(),
1231                                name: String::new(),
1232                                module_type: String::new(),
1233                                is_fatal: false,
1234                                selected: false,
1235                                size: None,
1236                                platform: None,
1237                            };
1238
1239                            for attr in e.attributes().flatten() {
1240                                match attr.key.as_ref() {
1241                                    b"id" => {
1242                                        module.id = String::from_utf8_lossy(&attr.value).to_string()
1243                                    }
1244                                    b"name" => {
1245                                        module.name =
1246                                            String::from_utf8_lossy(&attr.value).to_string()
1247                                    }
1248                                    b"type" => {
1249                                        module.module_type =
1250                                            String::from_utf8_lossy(&attr.value).to_string()
1251                                    }
1252                                    b"isfatal" => module.is_fatal = attr.value.as_ref() == b"true",
1253                                    b"selected" => module.selected = attr.value.as_ref() == b"true",
1254                                    b"has_fatal_errors" => {
1255                                        if attr.value.as_ref() == b"true" {
1256                                            has_fatal_errors = true;
1257                                        }
1258                                    }
1259                                    b"size" => {
1260                                        if let Ok(size_str) = String::from_utf8(attr.value.to_vec())
1261                                        {
1262                                            module.size = size_str.parse().ok();
1263                                        }
1264                                    }
1265                                    b"platform" => {
1266                                        module.platform =
1267                                            Some(String::from_utf8_lossy(&attr.value).to_string())
1268                                    }
1269                                    _ => {}
1270                                }
1271                            }
1272                            modules.push(module);
1273                        }
1274                        _ => {}
1275                    }
1276                }
1277                Ok(Event::Eof) => break,
1278                Err(e) => {
1279                    eprintln!("Error parsing XML: {e}");
1280                    break;
1281                }
1282                _ => {}
1283            }
1284            buf.clear();
1285        }
1286
1287        // Determine prescan status based on the parsed results
1288        let status = if !has_prescan_results {
1289            "Unknown".to_string()
1290        } else if modules.is_empty() {
1291            // No modules found - this could indicate prescan failed or is still processing
1292            "Pre-Scan Failed".to_string()
1293        } else if has_fatal_errors {
1294            // Modules found but some have fatal errors
1295            "Pre-Scan Failed".to_string()
1296        } else {
1297            // Modules found with no fatal errors - prescan succeeded
1298            "Pre-Scan Success".to_string()
1299        };
1300
1301        Ok(PreScanResults {
1302            build_id: build_id.unwrap_or_else(|| "unknown".to_string()),
1303            app_id: app_id.to_string(),
1304            sandbox_id: sandbox_id.map(|s| s.to_string()),
1305            status,
1306            modules,
1307            messages,
1308        })
1309    }
1310
1311    fn parse_file_list(&self, xml: &str) -> Result<Vec<UploadedFile>, ScanError> {
1312        let mut reader = Reader::from_str(xml);
1313        reader.config_mut().trim_text(true);
1314
1315        let mut buf = Vec::new();
1316        let mut files = Vec::new();
1317
1318        loop {
1319            match reader.read_event_into(&mut buf) {
1320                Ok(Event::Start(ref e)) => {
1321                    if e.name().as_ref() == b"file" {
1322                        let mut file = UploadedFile {
1323                            file_id: String::new(),
1324                            file_name: String::new(),
1325                            file_size: 0,
1326                            uploaded: Utc::now(),
1327                            file_status: "Unknown".to_string(),
1328                            md5: None,
1329                        };
1330
1331                        for attr in e.attributes().flatten() {
1332                            match attr.key.as_ref() {
1333                                b"file_id" => {
1334                                    file.file_id = String::from_utf8_lossy(&attr.value).to_string()
1335                                }
1336                                b"file_name" => {
1337                                    file.file_name =
1338                                        String::from_utf8_lossy(&attr.value).to_string()
1339                                }
1340                                b"file_size" => {
1341                                    if let Ok(size_str) = String::from_utf8(attr.value.to_vec()) {
1342                                        file.file_size = size_str.parse().unwrap_or(0);
1343                                    }
1344                                }
1345                                b"file_status" => {
1346                                    file.file_status =
1347                                        String::from_utf8_lossy(&attr.value).to_string()
1348                                }
1349                                b"md5" => {
1350                                    file.md5 =
1351                                        Some(String::from_utf8_lossy(&attr.value).to_string())
1352                                }
1353                                _ => {}
1354                            }
1355                        }
1356                        files.push(file);
1357                    }
1358                }
1359                Ok(Event::Eof) => break,
1360                Err(e) => {
1361                    eprintln!("Error parsing XML: {e}");
1362                    break;
1363                }
1364                _ => {}
1365            }
1366            buf.clear();
1367        }
1368
1369        Ok(files)
1370    }
1371
1372    fn parse_build_info(
1373        &self,
1374        xml: &str,
1375        app_id: &str,
1376        sandbox_id: Option<&str>,
1377    ) -> Result<ScanInfo, ScanError> {
1378        let mut reader = Reader::from_str(xml);
1379        reader.config_mut().trim_text(true);
1380
1381        let mut buf = Vec::new();
1382        let mut scan_info = ScanInfo {
1383            build_id: String::new(),
1384            app_id: app_id.to_string(),
1385            sandbox_id: sandbox_id.map(|s| s.to_string()),
1386            status: "Unknown".to_string(),
1387            scan_type: "Static".to_string(),
1388            analysis_unit_id: None,
1389            scan_progress_percentage: None,
1390            scan_start: None,
1391            scan_complete: None,
1392            total_lines_of_code: None,
1393        };
1394
1395        let mut inside_build = false;
1396
1397        loop {
1398            match reader.read_event_into(&mut buf) {
1399                Ok(Event::Start(ref e)) => {
1400                    match e.name().as_ref() {
1401                        b"buildinfo" => {
1402                            // Parse buildinfo attributes
1403                            for attr in e.attributes().flatten() {
1404                                match attr.key.as_ref() {
1405                                    b"build_id" => {
1406                                        scan_info.build_id =
1407                                            String::from_utf8_lossy(&attr.value).to_string()
1408                                    }
1409                                    b"analysis_unit" => {
1410                                        // Fallback status from buildinfo (older API format)
1411                                        if scan_info.status == "Unknown" {
1412                                            scan_info.status =
1413                                                String::from_utf8_lossy(&attr.value).to_string();
1414                                        }
1415                                    }
1416                                    b"analysis_unit_id" => {
1417                                        scan_info.analysis_unit_id =
1418                                            Some(String::from_utf8_lossy(&attr.value).to_string())
1419                                    }
1420                                    b"scan_progress_percentage" => {
1421                                        if let Ok(progress_str) =
1422                                            String::from_utf8(attr.value.to_vec())
1423                                        {
1424                                            scan_info.scan_progress_percentage =
1425                                                progress_str.parse().ok();
1426                                        }
1427                                    }
1428                                    b"total_lines_of_code" => {
1429                                        if let Ok(lines_str) =
1430                                            String::from_utf8(attr.value.to_vec())
1431                                        {
1432                                            scan_info.total_lines_of_code = lines_str.parse().ok();
1433                                        }
1434                                    }
1435                                    _ => {}
1436                                }
1437                            }
1438                        }
1439                        b"build" => {
1440                            inside_build = true;
1441                        }
1442                        b"analysis_unit" => {
1443                            // Parse analysis_unit attributes (primary status source)
1444                            for attr in e.attributes().flatten() {
1445                                match attr.key.as_ref() {
1446                                    b"status" => {
1447                                        // Primary status source from analysis_unit
1448                                        scan_info.status =
1449                                            String::from_utf8_lossy(&attr.value).to_string();
1450                                    }
1451                                    b"analysis_type" => {
1452                                        scan_info.scan_type =
1453                                            String::from_utf8_lossy(&attr.value).to_string();
1454                                    }
1455                                    _ => {}
1456                                }
1457                            }
1458                        }
1459                        _ => {}
1460                    }
1461                }
1462                Ok(Event::End(ref e)) => {
1463                    if e.name().as_ref() == b"build" {
1464                        inside_build = false;
1465                    }
1466                }
1467                Ok(Event::Empty(ref e)) => {
1468                    // Handle self-closing elements like <analysis_unit ... />
1469                    if e.name().as_ref() == b"analysis_unit" && inside_build {
1470                        for attr in e.attributes().flatten() {
1471                            match attr.key.as_ref() {
1472                                b"status" => {
1473                                    scan_info.status =
1474                                        String::from_utf8_lossy(&attr.value).to_string();
1475                                }
1476                                b"analysis_type" => {
1477                                    scan_info.scan_type =
1478                                        String::from_utf8_lossy(&attr.value).to_string();
1479                                }
1480                                _ => {}
1481                            }
1482                        }
1483                    }
1484                }
1485                Ok(Event::Eof) => break,
1486                Err(e) => {
1487                    eprintln!("Error parsing XML: {e}");
1488                    break;
1489                }
1490                _ => {}
1491            }
1492            buf.clear();
1493        }
1494
1495        Ok(scan_info)
1496    }
1497}
1498
1499/// Convenience methods for common scan operations
1500impl ScanApi {
1501    /// Upload a file to a sandbox with simple parameters
1502    ///
1503    /// # Arguments
1504    ///
1505    /// * `app_id` - The application ID
1506    /// * `file_path` - Path to the file to upload
1507    /// * `sandbox_id` - The sandbox ID
1508    ///
1509    /// # Returns
1510    ///
1511    /// A `Result` containing the uploaded file information or an error.
1512    pub async fn upload_file_to_sandbox(
1513        &self,
1514        app_id: &str,
1515        file_path: &str,
1516        sandbox_id: &str,
1517    ) -> Result<UploadedFile, ScanError> {
1518        let request = UploadFileRequest {
1519            app_id: app_id.to_string(),
1520            file_path: file_path.to_string(),
1521            save_as: None,
1522            sandbox_id: Some(sandbox_id.to_string()),
1523        };
1524
1525        self.upload_file(request).await
1526    }
1527
1528    /// Upload a file to an application (non-sandbox)
1529    ///
1530    /// # Arguments
1531    ///
1532    /// * `app_id` - The application ID
1533    /// * `file_path` - Path to the file to upload
1534    ///
1535    /// # Returns
1536    ///
1537    /// A `Result` containing the uploaded file information or an error.
1538    pub async fn upload_file_to_app(
1539        &self,
1540        app_id: &str,
1541        file_path: &str,
1542    ) -> Result<UploadedFile, ScanError> {
1543        let request = UploadFileRequest {
1544            app_id: app_id.to_string(),
1545            file_path: file_path.to_string(),
1546            save_as: None,
1547            sandbox_id: None,
1548        };
1549
1550        self.upload_file(request).await
1551    }
1552
1553    /// Upload a large file to a sandbox using uploadlargefile.do
1554    ///
1555    /// # Arguments
1556    ///
1557    /// * `app_id` - The application ID
1558    /// * `file_path` - Path to the file to upload
1559    /// * `sandbox_id` - The sandbox ID
1560    /// * `filename` - Optional filename for flaw matching
1561    ///
1562    /// # Returns
1563    ///
1564    /// A `Result` containing the uploaded file information or an error.
1565    pub async fn upload_large_file_to_sandbox(
1566        &self,
1567        app_id: &str,
1568        file_path: &str,
1569        sandbox_id: &str,
1570        filename: Option<&str>,
1571    ) -> Result<UploadedFile, ScanError> {
1572        let request = UploadLargeFileRequest {
1573            app_id: app_id.to_string(),
1574            file_path: file_path.to_string(),
1575            filename: filename.map(|s| s.to_string()),
1576            sandbox_id: Some(sandbox_id.to_string()),
1577        };
1578
1579        self.upload_large_file(request).await
1580    }
1581
1582    /// Upload a large file to an application using uploadlargefile.do
1583    ///
1584    /// # Arguments
1585    ///
1586    /// * `app_id` - The application ID
1587    /// * `file_path` - Path to the file to upload
1588    /// * `filename` - Optional filename for flaw matching
1589    ///
1590    /// # Returns
1591    ///
1592    /// A `Result` containing the uploaded file information or an error.
1593    pub async fn upload_large_file_to_app(
1594        &self,
1595        app_id: &str,
1596        file_path: &str,
1597        filename: Option<&str>,
1598    ) -> Result<UploadedFile, ScanError> {
1599        let request = UploadLargeFileRequest {
1600            app_id: app_id.to_string(),
1601            file_path: file_path.to_string(),
1602            filename: filename.map(|s| s.to_string()),
1603            sandbox_id: None,
1604        };
1605
1606        self.upload_large_file(request).await
1607    }
1608
1609    /// Upload a large file with progress tracking to a sandbox
1610    ///
1611    /// # Arguments
1612    ///
1613    /// * `app_id` - The application ID
1614    /// * `file_path` - Path to the file to upload
1615    /// * `sandbox_id` - The sandbox ID
1616    /// * `filename` - Optional filename for flaw matching
1617    /// * `progress_callback` - Callback for progress updates
1618    ///
1619    /// # Returns
1620    ///
1621    /// A `Result` containing the uploaded file information or an error.
1622    pub async fn upload_large_file_to_sandbox_with_progress<F>(
1623        &self,
1624        app_id: &str,
1625        file_path: &str,
1626        sandbox_id: &str,
1627        filename: Option<&str>,
1628        progress_callback: F,
1629    ) -> Result<UploadedFile, ScanError>
1630    where
1631        F: Fn(u64, u64, f64) + Send + Sync,
1632    {
1633        let request = UploadLargeFileRequest {
1634            app_id: app_id.to_string(),
1635            file_path: file_path.to_string(),
1636            filename: filename.map(|s| s.to_string()),
1637            sandbox_id: Some(sandbox_id.to_string()),
1638        };
1639
1640        self.upload_large_file_with_progress(request, progress_callback)
1641            .await
1642    }
1643
1644    /// Begin a simple pre-scan for a sandbox
1645    ///
1646    /// # Arguments
1647    ///
1648    /// * `app_id` - The application ID
1649    /// * `sandbox_id` - The sandbox ID
1650    ///
1651    /// # Returns
1652    ///
1653    /// A `Result` containing the build ID or an error.
1654    pub async fn begin_sandbox_prescan(
1655        &self,
1656        app_id: &str,
1657        sandbox_id: &str,
1658    ) -> Result<(), ScanError> {
1659        let request = BeginPreScanRequest {
1660            app_id: app_id.to_string(),
1661            sandbox_id: Some(sandbox_id.to_string()),
1662            auto_scan: Some(true),
1663            scan_all_nonfatal_top_level_modules: Some(true),
1664            include_new_modules: Some(true),
1665        };
1666
1667        self.begin_prescan(request).await
1668    }
1669
1670    /// Begin a simple scan for a sandbox with all modules
1671    ///
1672    /// # Arguments
1673    ///
1674    /// * `app_id` - The application ID
1675    /// * `sandbox_id` - The sandbox ID
1676    ///
1677    /// # Returns
1678    ///
1679    /// A `Result` containing the build ID or an error.
1680    pub async fn begin_sandbox_scan_all_modules(
1681        &self,
1682        app_id: &str,
1683        sandbox_id: &str,
1684    ) -> Result<(), ScanError> {
1685        let request = BeginScanRequest {
1686            app_id: app_id.to_string(),
1687            sandbox_id: Some(sandbox_id.to_string()),
1688            modules: None,
1689            scan_all_top_level_modules: Some(true),
1690            scan_all_nonfatal_top_level_modules: Some(true),
1691            scan_previously_selected_modules: None,
1692        };
1693
1694        self.begin_scan(request).await
1695    }
1696
1697    /// Complete workflow: upload file, pre-scan, and begin scan for sandbox
1698    ///
1699    /// # Arguments
1700    ///
1701    /// * `app_id` - The application ID
1702    /// * `sandbox_id` - The sandbox ID
1703    /// * `file_path` - Path to the file to upload
1704    ///
1705    /// # Returns
1706    ///
1707    /// A `Result` containing the scan build ID or an error.
1708    pub async fn upload_and_scan_sandbox(
1709        &self,
1710        app_id: &str,
1711        sandbox_id: &str,
1712        file_path: &str,
1713    ) -> Result<String, ScanError> {
1714        // Step 1: Upload file
1715        println!("📤 Uploading file to sandbox...");
1716        let _uploaded_file = self
1717            .upload_file_to_sandbox(app_id, file_path, sandbox_id)
1718            .await?;
1719
1720        // Step 2: Begin pre-scan
1721        println!("🔍 Beginning pre-scan...");
1722        self.begin_sandbox_prescan(app_id, sandbox_id).await?;
1723
1724        // Step 3: Wait a moment for pre-scan to complete (in production, poll for status)
1725        tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
1726
1727        // Step 4: Begin scan
1728        println!("🚀 Beginning scan...");
1729        self.begin_sandbox_scan_all_modules(app_id, sandbox_id)
1730            .await?;
1731
1732        // For now, return a placeholder build ID since we don't parse it from responses anymore
1733        // In a real implementation, this would need to come from ensure_build_exists or similar
1734        // This is a limitation of this convenience method - it should be deprecated in favor
1735        // of the proper workflow that tracks build IDs
1736        Ok("build_id_not_available".to_string())
1737    }
1738
1739    /// Delete a build from a sandbox
1740    ///
1741    /// # Arguments
1742    ///
1743    /// * `app_id` - The application ID
1744    /// * `build_id` - The build ID to delete
1745    /// * `sandbox_id` - The sandbox ID
1746    ///
1747    /// # Returns
1748    ///
1749    /// A `Result` indicating success or an error.
1750    pub async fn delete_sandbox_build(
1751        &self,
1752        app_id: &str,
1753        build_id: &str,
1754        sandbox_id: &str,
1755    ) -> Result<(), ScanError> {
1756        self.delete_build(app_id, build_id, Some(sandbox_id)).await
1757    }
1758
1759    /// Delete all builds from a sandbox
1760    ///
1761    /// # Arguments
1762    ///
1763    /// * `app_id` - The application ID
1764    /// * `sandbox_id` - The sandbox ID
1765    ///
1766    /// # Returns
1767    ///
1768    /// A `Result` indicating success or an error.
1769    pub async fn delete_all_sandbox_builds(
1770        &self,
1771        app_id: &str,
1772        sandbox_id: &str,
1773    ) -> Result<(), ScanError> {
1774        self.delete_all_builds(app_id, Some(sandbox_id)).await
1775    }
1776
1777    /// Delete a build from an application (non-sandbox)
1778    ///
1779    /// # Arguments
1780    ///
1781    /// * `app_id` - The application ID
1782    /// * `build_id` - The build ID to delete
1783    ///
1784    /// # Returns
1785    ///
1786    /// A `Result` indicating success or an error.
1787    pub async fn delete_app_build(&self, app_id: &str, build_id: &str) -> Result<(), ScanError> {
1788        self.delete_build(app_id, build_id, None).await
1789    }
1790
1791    /// Delete all builds from an application (non-sandbox)
1792    ///
1793    /// # Arguments
1794    ///
1795    /// * `app_id` - The application ID
1796    ///
1797    /// # Returns
1798    ///
1799    /// A `Result` indicating success or an error.
1800    pub async fn delete_all_app_builds(&self, app_id: &str) -> Result<(), ScanError> {
1801        self.delete_all_builds(app_id, None).await
1802    }
1803}
1804
1805#[cfg(test)]
1806mod tests {
1807    use super::*;
1808    use crate::VeracodeConfig;
1809
1810    #[test]
1811    fn test_upload_file_request() {
1812        let request = UploadFileRequest {
1813            app_id: "123".to_string(),
1814            file_path: "/path/to/file.jar".to_string(),
1815            save_as: Some("app.jar".to_string()),
1816            sandbox_id: Some("456".to_string()),
1817        };
1818
1819        assert_eq!(request.app_id, "123");
1820        assert_eq!(request.sandbox_id, Some("456".to_string()));
1821    }
1822
1823    #[test]
1824    fn test_begin_prescan_request() {
1825        let request = BeginPreScanRequest {
1826            app_id: "123".to_string(),
1827            sandbox_id: Some("456".to_string()),
1828            auto_scan: Some(true),
1829            scan_all_nonfatal_top_level_modules: Some(true),
1830            include_new_modules: Some(false),
1831        };
1832
1833        assert_eq!(request.app_id, "123");
1834        assert_eq!(request.auto_scan, Some(true));
1835    }
1836
1837    #[test]
1838    fn test_scan_error_display() {
1839        let error = ScanError::FileNotFound("test.jar".to_string());
1840        assert_eq!(error.to_string(), "File not found: test.jar");
1841
1842        let error = ScanError::UploadFailed("Network error".to_string());
1843        assert_eq!(error.to_string(), "Upload failed: Network error");
1844
1845        let error = ScanError::Unauthorized;
1846        assert_eq!(error.to_string(), "Unauthorized access");
1847
1848        let error = ScanError::BuildNotFound;
1849        assert_eq!(error.to_string(), "Build not found");
1850    }
1851
1852    #[test]
1853    fn test_delete_build_request_structure() {
1854        // Test that the delete build methods have correct structure
1855        // This is a compile-time test to ensure methods exist with correct signatures
1856
1857        use crate::{VeracodeClient, VeracodeConfig};
1858
1859        async fn _test_delete_methods() -> Result<(), Box<dyn std::error::Error>> {
1860            let config = VeracodeConfig::new("test".to_string(), "test".to_string());
1861            let client = VeracodeClient::new(config)?;
1862            let api = client.scan_api();
1863
1864            // These calls won't actually execute due to test environment,
1865            // but they validate the method signatures exist
1866            let _: Result<(), _> = api
1867                .delete_build("app_id", "build_id", Some("sandbox_id"))
1868                .await;
1869            let _: Result<(), _> = api.delete_all_builds("app_id", Some("sandbox_id")).await;
1870            let _: Result<(), _> = api
1871                .delete_sandbox_build("app_id", "build_id", "sandbox_id")
1872                .await;
1873            let _: Result<(), _> = api.delete_all_sandbox_builds("app_id", "sandbox_id").await;
1874
1875            Ok(())
1876        }
1877
1878        // If this compiles, the methods have correct signatures
1879        // Test passes if no panic occurs
1880    }
1881
1882    #[test]
1883    fn test_upload_large_file_request() {
1884        let request = UploadLargeFileRequest {
1885            app_id: "123".to_string(),
1886            file_path: "/path/to/large_file.jar".to_string(),
1887            filename: Some("custom_name.jar".to_string()),
1888            sandbox_id: Some("456".to_string()),
1889        };
1890
1891        assert_eq!(request.app_id, "123");
1892        assert_eq!(request.filename, Some("custom_name.jar".to_string()));
1893        assert_eq!(request.sandbox_id, Some("456".to_string()));
1894    }
1895
1896    #[test]
1897    fn test_upload_progress() {
1898        let progress = UploadProgress {
1899            bytes_uploaded: 1024,
1900            total_bytes: 2048,
1901            percentage: 50.0,
1902        };
1903
1904        assert_eq!(progress.bytes_uploaded, 1024);
1905        assert_eq!(progress.total_bytes, 2048);
1906        assert_eq!(progress.percentage, 50.0);
1907    }
1908
1909    #[test]
1910    fn test_large_file_scan_error_display() {
1911        let error = ScanError::FileTooLarge("File exceeds 2GB".to_string());
1912        assert_eq!(error.to_string(), "File too large: File exceeds 2GB");
1913
1914        let error = ScanError::UploadInProgress;
1915        assert_eq!(error.to_string(), "Upload or prescan already in progress");
1916
1917        let error = ScanError::ScanInProgress;
1918        assert_eq!(error.to_string(), "Scan in progress, cannot upload");
1919
1920        let error = ScanError::ChunkedUploadFailed("Network error".to_string());
1921        assert_eq!(error.to_string(), "Chunked upload failed: Network error");
1922    }
1923
1924    #[tokio::test]
1925    async fn test_large_file_upload_method_signatures() {
1926        async fn _test_large_file_methods() -> Result<(), Box<dyn std::error::Error>> {
1927            let config = VeracodeConfig::new("test".to_string(), "test".to_string());
1928            let client = VeracodeClient::new(config)?;
1929            let api = client.scan_api();
1930
1931            // Test that the method signatures exist and compile
1932            let request = UploadLargeFileRequest {
1933                app_id: "123".to_string(),
1934                file_path: "/nonexistent/file.jar".to_string(),
1935                filename: None,
1936                sandbox_id: Some("456".to_string()),
1937            };
1938
1939            // These calls won't actually execute due to test environment,
1940            // but they validate the method signatures exist
1941            let _: Result<UploadedFile, _> = api.upload_large_file(request.clone()).await;
1942            let _: Result<UploadedFile, _> = api
1943                .upload_large_file_to_sandbox("123", "/path", "456", None)
1944                .await;
1945            let _: Result<UploadedFile, _> =
1946                api.upload_large_file_to_app("123", "/path", None).await;
1947
1948            // Test progress callback signature
1949            let progress_callback = |bytes_uploaded: u64, total_bytes: u64, percentage: f64| {
1950                println!("Progress: {bytes_uploaded}/{total_bytes} ({percentage:.1}%)");
1951            };
1952            let _: Result<UploadedFile, _> = api
1953                .upload_large_file_with_progress(request, progress_callback)
1954                .await;
1955
1956            Ok(())
1957        }
1958
1959        // If this compiles, the methods have correct signatures
1960        // Test passes if no panic occurs
1961    }
1962}