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};
8#[allow(unused_imports)] // debug is used in tests
9use log::{debug, error, info};
10use quick_xml::Reader;
11use quick_xml::events::Event;
12use serde::{Deserialize, Serialize};
13use std::path::Path;
14
15use crate::validation::validate_url_segment;
16use crate::{VeracodeClient, VeracodeError};
17
18/// Helper function to efficiently convert XML attribute bytes to string
19/// Avoids unnecessary allocation when possible
20fn attr_to_string(value: &[u8]) -> String {
21    String::from_utf8_lossy(value).into_owned()
22}
23
24/// File upload status as defined in the Veracode filelist.xsd schema
25///
26/// This enum represents all possible states a file can be in during and after upload.
27/// Reference: <https://analysiscenter.veracode.com/resource/2.0/filelist.xsd>
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29pub enum FileStatus {
30    /// File is pending upload to the platform
31    #[serde(rename = "Pending Upload")]
32    PendingUpload,
33    /// File is currently being uploaded
34    #[serde(rename = "Uploading")]
35    Uploading,
36    /// File has been purged from the platform
37    #[serde(rename = "Purged")]
38    Purged,
39    /// File was successfully uploaded and is ready for scanning
40    #[serde(rename = "Uploaded")]
41    Uploaded,
42    /// File is missing from the build
43    #[serde(rename = "Missing")]
44    Missing,
45    /// File upload was only partially completed
46    #[serde(rename = "Partial")]
47    Partial,
48    /// File MD5 checksum validation failed
49    #[serde(rename = "Invalid Checksum")]
50    InvalidChecksum,
51    /// File is not a valid archive format
52    #[serde(rename = "Invalid Archive")]
53    InvalidArchive,
54    /// Archive contains nested archives (not allowed)
55    #[serde(rename = "Archive File Within Another Archive")]
56    ArchiveWithinArchive,
57    /// Archive uses unsupported compression algorithm
58    #[serde(rename = "Archive File with Unsupported Compression")]
59    UnsupportedCompression,
60    /// Archive is password protected and cannot be processed
61    #[serde(rename = "Archive File is Password Protected")]
62    PasswordProtected,
63}
64
65impl FileStatus {
66    /// Check if this status indicates a successful upload
67    #[must_use]
68    pub fn is_uploaded(&self) -> bool {
69        matches!(self, FileStatus::Uploaded)
70    }
71
72    /// Check if this status indicates an error state
73    #[must_use]
74    pub fn is_error(&self) -> bool {
75        matches!(
76            self,
77            FileStatus::InvalidChecksum
78                | FileStatus::InvalidArchive
79                | FileStatus::ArchiveWithinArchive
80                | FileStatus::UnsupportedCompression
81                | FileStatus::PasswordProtected
82                | FileStatus::Missing
83                | FileStatus::Purged
84        )
85    }
86
87    /// Check if this status indicates upload is still in progress
88    #[must_use]
89    pub fn is_in_progress(&self) -> bool {
90        matches!(
91            self,
92            FileStatus::PendingUpload | FileStatus::Uploading | FileStatus::Partial
93        )
94    }
95
96    /// Get a human-readable description of the status
97    #[must_use]
98    pub fn description(&self) -> &'static str {
99        match self {
100            FileStatus::PendingUpload => "File is pending upload",
101            FileStatus::Uploading => "File is currently being uploaded",
102            FileStatus::Purged => "File has been purged from the platform",
103            FileStatus::Uploaded => "File successfully uploaded and ready for scanning",
104            FileStatus::Missing => "File is missing from the build",
105            FileStatus::Partial => "File upload was only partially completed",
106            FileStatus::InvalidChecksum => "File MD5 checksum validation failed",
107            FileStatus::InvalidArchive => "File is not a valid archive format",
108            FileStatus::ArchiveWithinArchive => "Archive contains nested archives (not allowed)",
109            FileStatus::UnsupportedCompression => "Archive uses unsupported compression algorithm",
110            FileStatus::PasswordProtected => {
111                "Archive is password protected and cannot be processed"
112            }
113        }
114    }
115}
116
117impl std::str::FromStr for FileStatus {
118    type Err = ScanError;
119
120    /// Parse file status from XML string value
121    ///
122    /// # Arguments
123    ///
124    /// * `s` - The status string from the XML response
125    ///
126    /// # Returns
127    ///
128    /// The corresponding `FileStatus` enum value, or an error if the status is unknown
129    ///
130    /// # Errors
131    ///
132    /// Returns `ScanError::InvalidParameter` if the status string is not recognized
133    fn from_str(s: &str) -> Result<Self, Self::Err> {
134        match s {
135            "Pending Upload" => Ok(FileStatus::PendingUpload),
136            "Uploading" => Ok(FileStatus::Uploading),
137            "Purged" => Ok(FileStatus::Purged),
138            "Uploaded" => Ok(FileStatus::Uploaded),
139            "Missing" => Ok(FileStatus::Missing),
140            "Partial" => Ok(FileStatus::Partial),
141            "Invalid Checksum" => Ok(FileStatus::InvalidChecksum),
142            "Invalid Archive" => Ok(FileStatus::InvalidArchive),
143            "Archive File Within Another Archive" => Ok(FileStatus::ArchiveWithinArchive),
144            "Archive File with Unsupported Compression" => Ok(FileStatus::UnsupportedCompression),
145            "Archive File is Password Protected" => Ok(FileStatus::PasswordProtected),
146            _ => Err(ScanError::InvalidParameter(format!(
147                "Unknown file status: {}",
148                s
149            ))),
150        }
151    }
152}
153
154impl std::fmt::Display for FileStatus {
155    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156        let s = match self {
157            FileStatus::PendingUpload => "Pending Upload",
158            FileStatus::Uploading => "Uploading",
159            FileStatus::Purged => "Purged",
160            FileStatus::Uploaded => "Uploaded",
161            FileStatus::Missing => "Missing",
162            FileStatus::Partial => "Partial",
163            FileStatus::InvalidChecksum => "Invalid Checksum",
164            FileStatus::InvalidArchive => "Invalid Archive",
165            FileStatus::ArchiveWithinArchive => "Archive File Within Another Archive",
166            FileStatus::UnsupportedCompression => "Archive File with Unsupported Compression",
167            FileStatus::PasswordProtected => "Archive File is Password Protected",
168        };
169        write!(f, "{}", s)
170    }
171}
172
173/// Represents an uploaded file in a sandbox
174#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct UploadedFile {
176    /// File ID assigned by Veracode
177    pub file_id: String,
178    /// Original filename
179    pub file_name: String,
180    /// File size in bytes
181    pub file_size: u64,
182    /// Upload timestamp
183    pub uploaded: DateTime<Utc>,
184    /// File status (from Veracode filelist.xsd)
185    pub file_status: FileStatus,
186    /// MD5 hash of the file
187    pub md5: Option<String>,
188}
189
190/// Represents pre-scan results for a sandbox
191#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct PreScanResults {
193    /// Build ID for the pre-scan
194    pub build_id: String,
195    /// Application ID
196    pub app_id: String,
197    /// Sandbox ID
198    pub sandbox_id: Option<String>,
199    /// Pre-scan status
200    pub status: String,
201    /// Available modules for scanning
202    pub modules: Vec<ScanModule>,
203    /// Pre-scan errors or warnings
204    pub messages: Vec<PreScanMessage>,
205}
206
207/// Represents a module available for scanning
208#[derive(Debug, Clone, Serialize, Deserialize)]
209pub struct ScanModule {
210    /// Module ID
211    pub id: String,
212    /// Module name
213    pub name: String,
214    /// Module type
215    pub module_type: String,
216    /// Whether module is fatal (required for scan)
217    pub is_fatal: bool,
218    /// Whether module should be selected for scanning
219    pub selected: bool,
220    /// Module size
221    pub size: Option<u64>,
222    /// Module platform
223    pub platform: Option<String>,
224}
225
226/// Represents a pre-scan message
227#[derive(Debug, Clone, Serialize, Deserialize)]
228pub struct PreScanMessage {
229    /// Message severity
230    pub severity: String,
231    /// Message text
232    pub text: String,
233    /// Associated module (if any)
234    pub module_name: Option<String>,
235}
236
237/// Represents scan information
238#[derive(Debug, Clone, Serialize, Deserialize)]
239pub struct ScanInfo {
240    /// Build ID
241    pub build_id: String,
242    /// Application ID
243    pub app_id: String,
244    /// Sandbox ID
245    pub sandbox_id: Option<String>,
246    /// Scan status
247    pub status: String,
248    /// Scan type
249    pub scan_type: String,
250    /// Analysis unit ID
251    pub analysis_unit_id: Option<String>,
252    /// Scan completion percentage
253    pub scan_progress_percentage: Option<u32>,
254    /// Scan started timestamp
255    pub scan_start: Option<DateTime<Utc>>,
256    /// Scan completed timestamp
257    pub scan_complete: Option<DateTime<Utc>>,
258    /// Total lines of code
259    pub total_lines_of_code: Option<u64>,
260}
261
262/// Request for uploading a file
263#[derive(Debug, Clone)]
264pub struct UploadFileRequest {
265    /// Application ID
266    pub app_id: String,
267    /// File path to upload
268    pub file_path: String,
269    /// Name to save file as (optional)
270    pub save_as: Option<String>,
271    /// Sandbox ID (optional, for sandbox uploads)
272    pub sandbox_id: Option<String>,
273}
274
275/// Request for uploading a large file
276#[derive(Debug, Clone)]
277pub struct UploadLargeFileRequest {
278    /// Application ID
279    pub app_id: String,
280    /// File path to upload
281    pub file_path: String,
282    /// Name to save file as (optional, for flaw matching)
283    pub filename: Option<String>,
284    /// Sandbox ID (optional, for sandbox uploads)
285    pub sandbox_id: Option<String>,
286}
287
288/// Progress information for file uploads
289#[derive(Debug, Clone)]
290pub struct UploadProgress {
291    /// Bytes uploaded so far
292    pub bytes_uploaded: u64,
293    /// Total bytes to upload
294    pub total_bytes: u64,
295    /// Progress percentage (0-100)
296    pub percentage: f64,
297}
298
299/// Callback trait for upload progress tracking
300pub trait UploadProgressCallback: Send + Sync {
301    /// Called when upload progress changes
302    fn on_progress(&self, progress: UploadProgress);
303    /// Called when upload completes successfully
304    fn on_completed(&self);
305    /// Called when upload fails
306    fn on_error(&self, error: &str);
307}
308
309/// Request for beginning a pre-scan
310#[derive(Debug, Clone)]
311pub struct BeginPreScanRequest {
312    /// Application ID
313    pub app_id: String,
314    /// Sandbox ID (optional)
315    pub sandbox_id: Option<String>,
316    /// Auto-scan flag
317    pub auto_scan: Option<bool>,
318    /// Scan all non-fatal top level modules
319    pub scan_all_nonfatal_top_level_modules: Option<bool>,
320    /// Include new modules
321    pub include_new_modules: Option<bool>,
322}
323
324/// Request for beginning a scan
325#[derive(Debug, Clone)]
326pub struct BeginScanRequest {
327    /// Application ID
328    pub app_id: String,
329    /// Sandbox ID (optional)
330    pub sandbox_id: Option<String>,
331    /// Modules to scan (comma-separated module IDs)
332    pub modules: Option<String>,
333    /// Scan all top level modules
334    pub scan_all_top_level_modules: Option<bool>,
335    /// Scan all non-fatal top level modules
336    pub scan_all_nonfatal_top_level_modules: Option<bool>,
337    /// Scan previously selected modules
338    pub scan_previously_selected_modules: Option<bool>,
339}
340
341// Trait implementations for memory optimization
342impl From<&UploadFileRequest> for UploadLargeFileRequest {
343    fn from(request: &UploadFileRequest) -> Self {
344        UploadLargeFileRequest {
345            app_id: request.app_id.clone(),
346            file_path: request.file_path.clone(),
347            filename: request.save_as.clone(),
348            sandbox_id: request.sandbox_id.clone(),
349        }
350    }
351}
352
353/// Scan specific error types
354#[derive(Debug)]
355#[must_use = "Need to handle all error enum types."]
356pub enum ScanError {
357    /// Veracode API error
358    Api(VeracodeError),
359    /// File not found
360    FileNotFound(String),
361    /// Invalid file format
362    InvalidFileFormat(String),
363    /// Upload failed
364    UploadFailed(String),
365    /// Scan failed
366    ScanFailed(String),
367    /// Pre-scan failed
368    PreScanFailed(String),
369    /// Build not found
370    BuildNotFound,
371    /// Application not found
372    ApplicationNotFound,
373    /// Sandbox not found
374    SandboxNotFound,
375    /// Unauthorized access
376    Unauthorized,
377    /// Permission denied
378    PermissionDenied,
379    /// Invalid parameter
380    InvalidParameter(String),
381    /// File too large (exceeds 2GB limit)
382    FileTooLarge(String),
383    /// Upload or prescan already in progress
384    UploadInProgress,
385    /// Scan in progress, cannot upload
386    ScanInProgress,
387    /// Upload timeout waiting for file processing
388    UploadTimeout(String),
389    /// Build creation failed
390    BuildCreationFailed(String),
391    /// Chunked upload failed
392    ChunkedUploadFailed(String),
393}
394
395impl std::fmt::Display for ScanError {
396    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
397        match self {
398            ScanError::Api(err) => write!(f, "API error: {err}"),
399            ScanError::FileNotFound(path) => write!(f, "File not found: {path}"),
400            ScanError::InvalidFileFormat(msg) => write!(f, "Invalid file format: {msg}"),
401            ScanError::UploadFailed(msg) => write!(f, "{msg}"),
402            ScanError::ScanFailed(msg) => write!(f, "Scan failed: {msg}"),
403            ScanError::PreScanFailed(msg) => write!(f, "Pre-scan failed: {msg}"),
404            ScanError::BuildNotFound => write!(f, "Build not found"),
405            ScanError::ApplicationNotFound => write!(f, "Application not found"),
406            ScanError::SandboxNotFound => write!(f, "Sandbox not found"),
407            ScanError::Unauthorized => write!(f, "Unauthorized access"),
408            ScanError::PermissionDenied => write!(f, "Permission denied"),
409            ScanError::InvalidParameter(msg) => write!(f, "Invalid parameter: {msg}"),
410            ScanError::FileTooLarge(msg) => write!(f, "File too large: {msg}"),
411            ScanError::UploadInProgress => write!(f, "Upload or prescan already in progress"),
412            ScanError::ScanInProgress => write!(f, "Scan in progress, cannot upload"),
413            ScanError::UploadTimeout(msg) => write!(f, "Upload timeout: {msg}"),
414            ScanError::BuildCreationFailed(msg) => write!(f, "Build creation failed: {msg}"),
415            ScanError::ChunkedUploadFailed(msg) => write!(f, "Chunked upload failed: {msg}"),
416        }
417    }
418}
419
420impl std::error::Error for ScanError {}
421
422impl From<VeracodeError> for ScanError {
423    fn from(err: VeracodeError) -> Self {
424        ScanError::Api(err)
425    }
426}
427
428impl From<reqwest::Error> for ScanError {
429    fn from(err: reqwest::Error) -> Self {
430        ScanError::Api(VeracodeError::Http(err))
431    }
432}
433
434impl From<serde_json::Error> for ScanError {
435    fn from(err: serde_json::Error) -> Self {
436        ScanError::Api(VeracodeError::Serialization(err))
437    }
438}
439
440impl From<std::io::Error> for ScanError {
441    fn from(err: std::io::Error) -> Self {
442        ScanError::FileNotFound(err.to_string())
443    }
444}
445
446/// Veracode Scan API operations
447pub struct ScanApi {
448    client: VeracodeClient,
449}
450
451impl ScanApi {
452    ///
453    /// # Errors
454    ///
455    /// Returns an error if the API request fails, the resource is not found,
456    /// or authentication/authorization fails.
457    /// Create a new `ScanApi` instance
458    #[must_use]
459    pub fn new(client: VeracodeClient) -> Self {
460        Self { client }
461    }
462
463    /// Validate filename for path traversal sequences
464    fn validate_filename(filename: &str) -> Result<(), ScanError> {
465        // Use shared validation from validation.rs to prevent path traversal
466        validate_url_segment(filename, 255)
467            .map_err(|e| ScanError::InvalidParameter(format!("Invalid filename: {}", e)))?;
468        Ok(())
469    }
470
471    /// Upload a file to an application or sandbox
472    ///
473    /// # Arguments
474    ///
475    /// * `request` - The upload file request
476    ///
477    /// # Returns
478    ///
479    /// A `Result` containing the uploaded file information or an error.
480    ///
481    /// # Errors
482    ///
483    /// Returns an error if the API request fails, the scan operation fails,
484    /// or authentication/authorization fails.
485    pub async fn upload_file(
486        &self,
487        request: &UploadFileRequest,
488    ) -> Result<UploadedFile, ScanError> {
489        // Validate save_as parameter for path traversal
490        if let Some(save_as) = &request.save_as {
491            Self::validate_filename(save_as)?;
492        }
493
494        let endpoint = "/api/5.0/uploadfile.do";
495
496        // Build query parameters like Java implementation
497        let mut query_params = Vec::new();
498        query_params.push(("app_id", request.app_id.as_str()));
499
500        if let Some(sandbox_id) = &request.sandbox_id {
501            query_params.push(("sandbox_id", sandbox_id.as_str()));
502        }
503
504        if let Some(save_as) = &request.save_as {
505            query_params.push(("save_as", save_as.as_str()));
506        }
507
508        // Read file data - this will return an error if the file doesn't exist
509        let file_data = tokio::fs::read(&request.file_path).await.map_err(|e| {
510            if e.kind() == std::io::ErrorKind::NotFound {
511                ScanError::FileNotFound(request.file_path.clone())
512            } else {
513                ScanError::from(e)
514            }
515        })?;
516
517        // Get filename from path
518        let filename = Path::new(&request.file_path)
519            .file_name()
520            .and_then(|f| f.to_str())
521            .unwrap_or("file");
522
523        let response = self
524            .client
525            .upload_file_with_query_params(endpoint, &query_params, "file", filename, file_data)
526            .await?;
527
528        let status = response.status().as_u16();
529        match status {
530            200 => {
531                let response_text = response.text().await?;
532                self.parse_upload_response(&response_text, &request.file_path)
533                    .await
534            }
535            400 => {
536                let error_text = response.text().await.unwrap_or_default();
537                Err(ScanError::InvalidParameter(error_text))
538            }
539            401 => Err(ScanError::Unauthorized),
540            403 => Err(ScanError::PermissionDenied),
541            404 => {
542                if request.sandbox_id.is_some() {
543                    Err(ScanError::SandboxNotFound)
544                } else {
545                    Err(ScanError::ApplicationNotFound)
546                }
547            }
548            _ => {
549                let error_text = response.text().await.unwrap_or_default();
550                Err(ScanError::UploadFailed(format!(
551                    "HTTP {status}: {error_text}"
552                )))
553            }
554        }
555    }
556
557    /// Upload a large file using the uploadlargefile.do endpoint
558    ///
559    /// This method uploads large files (up to 2GB) to an existing build.
560    /// Unlike uploadfile.do, this endpoint requires a build to exist before uploading.
561    /// It automatically creates a build if one doesn't exist.
562    ///
563    /// # Arguments
564    ///
565    /// * `request` - The upload large file request
566    ///
567    /// # Returns
568    ///
569    /// A `Result` containing the uploaded file information or an error.
570    ///
571    /// # Errors
572    ///
573    /// Returns an error if the API request fails, the scan operation fails,
574    /// or authentication/authorization fails.
575    pub async fn upload_large_file(
576        &self,
577        request: UploadLargeFileRequest,
578    ) -> Result<UploadedFile, ScanError> {
579        // Validate filename parameter for path traversal
580        if let Some(filename) = &request.filename {
581            Self::validate_filename(filename)?;
582        }
583
584        // Check file size (2GB limit for uploadlargefile.do)
585        // This will return an error if the file doesn't exist
586        let file_metadata = tokio::fs::metadata(&request.file_path).await.map_err(|e| {
587            if e.kind() == std::io::ErrorKind::NotFound {
588                ScanError::FileNotFound(request.file_path.clone())
589            } else {
590                ScanError::from(e)
591            }
592        })?;
593        let file_size = file_metadata.len();
594        const MAX_FILE_SIZE: u64 = 2 * 1024 * 1024 * 1024; // 2GB
595
596        if file_size > MAX_FILE_SIZE {
597            return Err(ScanError::FileTooLarge(format!(
598                "File size {file_size} bytes exceeds 2GB limit"
599            )));
600        }
601
602        let endpoint = "/api/5.0/uploadlargefile.do";
603
604        // Build query parameters
605        let mut query_params = Vec::new();
606        query_params.push(("app_id", request.app_id.as_str()));
607
608        if let Some(sandbox_id) = &request.sandbox_id {
609            query_params.push(("sandbox_id", sandbox_id.as_str()));
610        }
611
612        if let Some(filename) = &request.filename {
613            query_params.push(("filename", filename.as_str()));
614        }
615
616        // Use streaming upload for memory efficiency (avoids loading entire file into RAM)
617        let response = self
618            .client
619            .upload_file_streaming(
620                endpoint,
621                &query_params,
622                &request.file_path,
623                file_size,
624                "binary/octet-stream",
625            )
626            .await?;
627
628        let status = response.status().as_u16();
629        match status {
630            200 => {
631                // uploadlargefile.do returns 200 with XML containing the file list
632                info!("File upload completed (HTTP 200), parsing response...");
633
634                let response_text = response.text().await?;
635
636                // Parse the file list from the response
637                let files = self.parse_file_list(&response_text)?;
638
639                // Determine the filename that was uploaded
640                let filename = request.filename.as_ref().cloned().unwrap_or_else(|| {
641                    Path::new(&request.file_path)
642                        .file_name()
643                        .and_then(|f| f.to_str())
644                        .unwrap_or("file")
645                        .to_string()
646                });
647
648                // Find the uploaded file in the response
649                files
650                    .into_iter()
651                    .find(|f| f.file_name == filename)
652                    .ok_or_else(|| {
653                        ScanError::UploadFailed(format!(
654                            "File '{}' not found in upload response",
655                            filename
656                        ))
657                    })
658            }
659            400 => {
660                let error_text = response.text().await.unwrap_or_default();
661                if error_text.contains("upload or prescan in progress") {
662                    Err(ScanError::UploadInProgress)
663                } else if error_text.contains("scan in progress") {
664                    Err(ScanError::ScanInProgress)
665                } else {
666                    Err(ScanError::InvalidParameter(error_text))
667                }
668            }
669            401 => Err(ScanError::Unauthorized),
670            403 => Err(ScanError::PermissionDenied),
671            404 => {
672                if request.sandbox_id.is_some() {
673                    Err(ScanError::SandboxNotFound)
674                } else {
675                    Err(ScanError::ApplicationNotFound)
676                }
677            }
678            413 => Err(ScanError::FileTooLarge(
679                "File size exceeds server limits".to_string(),
680            )),
681            _ => {
682                let error_text = response.text().await.unwrap_or_default();
683                Err(ScanError::UploadFailed(format!(
684                    "HTTP {status}: {error_text}"
685                )))
686            }
687        }
688    }
689
690    /// Upload a large file with progress tracking
691    ///
692    ///
693    /// # Errors
694    ///
695    /// Returns an error if the API request fails, the scan operation fails,
696    /// or authentication/authorization fails.
697    /// This method provides the same functionality as `upload_large_file` but with
698    /// progress tracking capabilities through a callback function.
699    ///
700    /// # Arguments
701    ///
702    /// * `request` - The upload large file request
703    ///
704    /// # Errors
705    ///
706    /// Returns an error if the API request fails, the scan operation fails,
707    /// or authentication/authorization fails.
708    /// * `progress_callback` - Callback function for progress updates (`bytes_uploaded`, `total_bytes`, percentage)
709    ///
710    /// # Returns
711    ///
712    /// A `Result` containing the uploaded file information or an error.
713    ///
714    /// # Errors
715    ///
716    /// Returns an error if the API request fails, the scan operation fails,
717    /// or authentication/authorization fails.
718    pub async fn upload_large_file_with_progress<F>(
719        &self,
720        request: UploadLargeFileRequest,
721        progress_callback: F,
722    ) -> Result<UploadedFile, ScanError>
723    where
724        F: Fn(u64, u64, f64) + Send + Sync,
725    {
726        // Validate filename parameter for path traversal
727        if let Some(filename) = &request.filename {
728            Self::validate_filename(filename)?;
729        }
730
731        // Check file size (2GB limit)
732        // This will return an error if the file doesn't exist
733        let file_metadata = tokio::fs::metadata(&request.file_path).await.map_err(|e| {
734            if e.kind() == std::io::ErrorKind::NotFound {
735                ScanError::FileNotFound(request.file_path.clone())
736            } else {
737                ScanError::from(e)
738            }
739        })?;
740        let file_size = file_metadata.len();
741        const MAX_FILE_SIZE: u64 = 2 * 1024 * 1024 * 1024; // 2GB
742
743        if file_size > MAX_FILE_SIZE {
744            return Err(ScanError::FileTooLarge(format!(
745                "File size {file_size} bytes exceeds 2GB limit"
746            )));
747        }
748
749        let endpoint = "/api/5.0/uploadlargefile.do";
750
751        // Build query parameters
752        let mut query_params = Vec::new();
753        query_params.push(("app_id", request.app_id.as_str()));
754
755        if let Some(sandbox_id) = &request.sandbox_id {
756            query_params.push(("sandbox_id", sandbox_id.as_str()));
757        }
758
759        if let Some(filename) = &request.filename {
760            query_params.push(("filename", filename.as_str()));
761        }
762
763        let response = self
764            .client
765            .upload_large_file_chunked(
766                endpoint,
767                &query_params,
768                &request.file_path,
769                Some("binary/octet-stream"),
770                Some(progress_callback),
771            )
772            .await?;
773
774        let status = response.status().as_u16();
775        match status {
776            200 => {
777                // uploadlargefile.do returns 200 with XML containing the file list
778                info!("File upload completed (HTTP 200), parsing response...");
779
780                let response_text = response.text().await?;
781
782                // Parse the file list from the response
783                let files = self.parse_file_list(&response_text)?;
784
785                // Determine the filename that was uploaded
786                let filename = request.filename.as_ref().cloned().unwrap_or_else(|| {
787                    Path::new(&request.file_path)
788                        .file_name()
789                        .and_then(|f| f.to_str())
790                        .unwrap_or("file")
791                        .to_string()
792                });
793
794                // Find the uploaded file in the response
795                files
796                    .into_iter()
797                    .find(|f| f.file_name == filename)
798                    .ok_or_else(|| {
799                        ScanError::UploadFailed(format!(
800                            "File '{}' not found in upload response",
801                            filename
802                        ))
803                    })
804            }
805            400 => {
806                let error_text = response.text().await.unwrap_or_default();
807                if error_text.contains("upload or prescan in progress") {
808                    Err(ScanError::UploadInProgress)
809                } else if error_text.contains("scan in progress") {
810                    Err(ScanError::ScanInProgress)
811                } else {
812                    Err(ScanError::InvalidParameter(error_text))
813                }
814            }
815            401 => Err(ScanError::Unauthorized),
816            403 => Err(ScanError::PermissionDenied),
817            404 => {
818                if request.sandbox_id.is_some() {
819                    Err(ScanError::SandboxNotFound)
820                } else {
821                    Err(ScanError::ApplicationNotFound)
822                }
823            }
824            413 => Err(ScanError::FileTooLarge(
825                "File size exceeds server limits".to_string(),
826            )),
827            _ => {
828                let error_text = response.text().await.unwrap_or_default();
829                Err(ScanError::ChunkedUploadFailed(format!(
830                    "HTTP {status}: {error_text}"
831                )))
832            }
833        }
834    }
835
836    /// Intelligently choose between uploadfile.do and uploadlargefile.do
837    ///
838    /// This method automatically selects the appropriate upload endpoint based on
839    /// file size and other factors, similar to the Java API wrapper behavior.
840    ///
841    /// # Arguments
842    ///
843    /// * `request` - The upload file request (converted to appropriate format)
844    ///
845    /// # Returns
846    ///
847    /// A `Result` containing the uploaded file information or an error.
848    ///
849    /// # Errors
850    ///
851    /// Returns an error if the API request fails, the scan operation fails,
852    /// or authentication/authorization fails.
853    pub async fn upload_file_smart(
854        &self,
855        request: &UploadFileRequest,
856    ) -> Result<UploadedFile, ScanError> {
857        // Get file size to determine upload method
858        // This will return an error if the file doesn't exist
859        let file_metadata = tokio::fs::metadata(&request.file_path).await.map_err(|e| {
860            if e.kind() == std::io::ErrorKind::NotFound {
861                ScanError::FileNotFound(request.file_path.clone())
862            } else {
863                ScanError::from(e)
864            }
865        })?;
866        let file_size = file_metadata.len();
867
868        // Use large file upload for files over 100MB or when build might exist
869        const LARGE_FILE_THRESHOLD: u64 = 100 * 1024 * 1024; // 100MB
870
871        if file_size > LARGE_FILE_THRESHOLD {
872            // Convert to large file request format using From trait
873            let large_request = UploadLargeFileRequest::from(request);
874
875            // Try large file upload first, fall back to regular upload if needed
876            match self.upload_large_file(large_request).await {
877                Ok(result) => Ok(result),
878                Err(ScanError::Api(_)) => {
879                    // Fall back to regular upload if large file upload fails
880                    self.upload_file(request).await
881                }
882                Err(e) => Err(e),
883            }
884        } else {
885            // Use regular upload for smaller files
886            self.upload_file(request).await
887        }
888    }
889
890    /// Begin pre-scan for an application or sandbox
891    ///
892    /// # Arguments
893    ///
894    /// * `request` - The pre-scan request
895    ///
896    /// # Returns
897    ///
898    /// A `Result` indicating success or an error.
899    ///
900    /// # Errors
901    ///
902    /// Returns an error if the API request fails, the scan operation fails,
903    /// or authentication/authorization fails.
904    pub async fn begin_prescan(&self, request: &BeginPreScanRequest) -> Result<(), ScanError> {
905        let endpoint = "/api/5.0/beginprescan.do";
906
907        // Build query parameters like Java implementation
908        let mut query_params = Vec::new();
909        query_params.push(("app_id", request.app_id.as_str()));
910
911        if let Some(sandbox_id) = &request.sandbox_id {
912            query_params.push(("sandbox_id", sandbox_id.as_str()));
913        }
914
915        if let Some(auto_scan) = request.auto_scan {
916            query_params.push(("auto_scan", if auto_scan { "true" } else { "false" }));
917        }
918
919        if let Some(scan_all) = request.scan_all_nonfatal_top_level_modules {
920            query_params.push((
921                "scan_all_nonfatal_top_level_modules",
922                if scan_all { "true" } else { "false" },
923            ));
924        }
925
926        if let Some(include_new) = request.include_new_modules {
927            query_params.push((
928                "include_new_modules",
929                if include_new { "true" } else { "false" },
930            ));
931        }
932
933        let response = self.client.get_with_params(endpoint, &query_params).await?;
934
935        let status = response.status().as_u16();
936        match status {
937            200 => {
938                let response_text = response.text().await?;
939                // Just validate the response is successful, don't parse build_id
940                // since we already have it from ensure_build_exists
941                self.validate_scan_response(&response_text)?;
942                Ok(())
943            }
944            400 => {
945                let error_text = response.text().await.unwrap_or_default();
946                Err(ScanError::InvalidParameter(error_text))
947            }
948            401 => Err(ScanError::Unauthorized),
949            403 => Err(ScanError::PermissionDenied),
950            404 => {
951                if request.sandbox_id.is_some() {
952                    Err(ScanError::SandboxNotFound)
953                } else {
954                    Err(ScanError::ApplicationNotFound)
955                }
956            }
957            _ => {
958                let error_text = response.text().await.unwrap_or_default();
959                Err(ScanError::PreScanFailed(format!(
960                    "HTTP {status}: {error_text}"
961                )))
962            }
963        }
964    }
965
966    /// Get pre-scan results for an application or sandbox
967    ///
968    /// # Arguments
969    ///
970    /// * `app_id` - The application ID
971    /// * `sandbox_id` - The sandbox ID (optional)
972    /// * `build_id` - The build ID (optional)
973    ///
974    /// # Returns
975    ///
976    /// A `Result` containing the pre-scan results or an error.
977    ///
978    /// # Errors
979    ///
980    /// Returns an error if the API request fails, the scan operation fails,
981    /// or authentication/authorization fails.
982    pub async fn get_prescan_results(
983        &self,
984        app_id: &str,
985        sandbox_id: Option<&str>,
986        build_id: Option<&str>,
987    ) -> Result<PreScanResults, ScanError> {
988        let endpoint = "/api/5.0/getprescanresults.do";
989
990        let mut params = Vec::new();
991        params.push(("app_id", app_id));
992
993        if let Some(sandbox_id) = sandbox_id {
994            params.push(("sandbox_id", sandbox_id));
995        }
996
997        if let Some(build_id) = build_id {
998            params.push(("build_id", build_id));
999        }
1000
1001        let response = self.client.get_with_params(endpoint, &params).await?;
1002
1003        let status = response.status().as_u16();
1004        match status {
1005            200 => {
1006                let response_text = response.text().await?;
1007                self.parse_prescan_results(&response_text, app_id, sandbox_id)
1008            }
1009            401 => Err(ScanError::Unauthorized),
1010            403 => Err(ScanError::PermissionDenied),
1011            404 => {
1012                if sandbox_id.is_some() {
1013                    Err(ScanError::SandboxNotFound)
1014                } else {
1015                    Err(ScanError::ApplicationNotFound)
1016                }
1017            }
1018            _ => {
1019                let error_text = response.text().await.unwrap_or_default();
1020                Err(ScanError::PreScanFailed(format!(
1021                    "HTTP {status}: {error_text}"
1022                )))
1023            }
1024        }
1025    }
1026
1027    /// Begin scan for an application or sandbox
1028    ///
1029    /// # Arguments
1030    ///
1031    /// * `request` - The scan request
1032    ///
1033    /// # Returns
1034    ///
1035    /// A `Result` indicating success or an error.
1036    ///
1037    /// # Errors
1038    ///
1039    /// Returns an error if the API request fails, the scan operation fails,
1040    /// or authentication/authorization fails.
1041    pub async fn begin_scan(&self, request: &BeginScanRequest) -> Result<(), ScanError> {
1042        let endpoint = "/api/5.0/beginscan.do";
1043
1044        // Build query parameters like Java implementation
1045        let mut query_params = Vec::new();
1046        query_params.push(("app_id", request.app_id.as_str()));
1047
1048        if let Some(sandbox_id) = &request.sandbox_id {
1049            query_params.push(("sandbox_id", sandbox_id.as_str()));
1050        }
1051
1052        if let Some(modules) = &request.modules {
1053            query_params.push(("modules", modules.as_str()));
1054        }
1055
1056        if let Some(scan_all) = request.scan_all_top_level_modules {
1057            query_params.push((
1058                "scan_all_top_level_modules",
1059                if scan_all { "true" } else { "false" },
1060            ));
1061        }
1062
1063        if let Some(scan_all_nonfatal) = request.scan_all_nonfatal_top_level_modules {
1064            query_params.push((
1065                "scan_all_nonfatal_top_level_modules",
1066                if scan_all_nonfatal { "true" } else { "false" },
1067            ));
1068        }
1069
1070        if let Some(scan_previous) = request.scan_previously_selected_modules {
1071            query_params.push((
1072                "scan_previously_selected_modules",
1073                if scan_previous { "true" } else { "false" },
1074            ));
1075        }
1076
1077        let response = self.client.get_with_params(endpoint, &query_params).await?;
1078
1079        let status = response.status().as_u16();
1080        match status {
1081            200 => {
1082                let response_text = response.text().await?;
1083                // Just validate the response is successful, don't parse build_id
1084                // since we already have it from ensure_build_exists
1085                self.validate_scan_response(&response_text)?;
1086                Ok(())
1087            }
1088            400 => {
1089                let error_text = response.text().await.unwrap_or_default();
1090                Err(ScanError::InvalidParameter(error_text))
1091            }
1092            401 => Err(ScanError::Unauthorized),
1093            403 => Err(ScanError::PermissionDenied),
1094            404 => {
1095                if request.sandbox_id.is_some() {
1096                    Err(ScanError::SandboxNotFound)
1097                } else {
1098                    Err(ScanError::ApplicationNotFound)
1099                }
1100            }
1101            _ => {
1102                let error_text = response.text().await.unwrap_or_default();
1103                Err(ScanError::ScanFailed(format!(
1104                    "HTTP {status}: {error_text}"
1105                )))
1106            }
1107        }
1108    }
1109
1110    /// Get list of uploaded files for an application or sandbox
1111    ///
1112    /// # Arguments
1113    ///
1114    /// * `app_id` - The application ID
1115    /// * `sandbox_id` - The sandbox ID (optional)
1116    /// * `build_id` - The build ID (optional)
1117    ///
1118    /// # Returns
1119    ///
1120    /// A `Result` containing the list of uploaded files or an error.
1121    ///
1122    /// # Errors
1123    ///
1124    /// Returns an error if the API request fails, the scan operation fails,
1125    /// or authentication/authorization fails.
1126    pub async fn get_file_list(
1127        &self,
1128        app_id: &str,
1129        sandbox_id: Option<&str>,
1130        build_id: Option<&str>,
1131    ) -> Result<Vec<UploadedFile>, ScanError> {
1132        let endpoint = "/api/5.0/getfilelist.do";
1133
1134        let mut params = Vec::new();
1135        params.push(("app_id", app_id));
1136
1137        if let Some(sandbox_id) = sandbox_id {
1138            params.push(("sandbox_id", sandbox_id));
1139        }
1140
1141        if let Some(build_id) = build_id {
1142            params.push(("build_id", build_id));
1143        }
1144
1145        let response = self.client.get_with_params(endpoint, &params).await?;
1146
1147        let status = response.status().as_u16();
1148        match status {
1149            200 => {
1150                let response_text = response.text().await?;
1151                self.parse_file_list(&response_text)
1152            }
1153            401 => Err(ScanError::Unauthorized),
1154            403 => Err(ScanError::PermissionDenied),
1155            404 => {
1156                if sandbox_id.is_some() {
1157                    Err(ScanError::SandboxNotFound)
1158                } else {
1159                    Err(ScanError::ApplicationNotFound)
1160                }
1161            }
1162            _ => {
1163                let error_text = response.text().await.unwrap_or_default();
1164                Err(ScanError::Api(VeracodeError::InvalidResponse(format!(
1165                    "HTTP {status}: {error_text}"
1166                ))))
1167            }
1168        }
1169    }
1170
1171    /// Remove a file from an application or sandbox
1172    ///
1173    /// # Arguments
1174    ///
1175    /// * `app_id` - The application ID
1176    /// * `file_id` - The file ID to remove
1177    /// * `sandbox_id` - The sandbox ID (optional)
1178    ///
1179    /// # Returns
1180    ///
1181    /// A `Result` indicating success or an error.
1182    ///
1183    /// # Errors
1184    ///
1185    /// Returns an error if the API request fails, the scan operation fails,
1186    /// or authentication/authorization fails.
1187    pub async fn remove_file(
1188        &self,
1189        app_id: &str,
1190        file_id: &str,
1191        sandbox_id: Option<&str>,
1192    ) -> Result<(), ScanError> {
1193        let endpoint = "/api/5.0/removefile.do";
1194
1195        // Build query parameters like Java implementation
1196        let mut query_params = Vec::new();
1197        query_params.push(("app_id", app_id));
1198        query_params.push(("file_id", file_id));
1199
1200        if let Some(sandbox_id) = sandbox_id {
1201            query_params.push(("sandbox_id", sandbox_id));
1202        }
1203
1204        let response = self.client.get_with_params(endpoint, &query_params).await?;
1205
1206        let status = response.status().as_u16();
1207        match status {
1208            200 => Ok(()),
1209            400 => {
1210                let error_text = response.text().await.unwrap_or_default();
1211                Err(ScanError::InvalidParameter(error_text))
1212            }
1213            401 => Err(ScanError::Unauthorized),
1214            403 => Err(ScanError::PermissionDenied),
1215            404 => Err(ScanError::FileNotFound(file_id.to_string())),
1216            _ => {
1217                let error_text = response.text().await.unwrap_or_default();
1218                Err(ScanError::Api(VeracodeError::InvalidResponse(format!(
1219                    "HTTP {status}: {error_text}"
1220                ))))
1221            }
1222        }
1223    }
1224
1225    /// Delete a build from an application or sandbox
1226    ///
1227    /// This removes all uploaded files and scan data for a specific build.
1228    ///
1229    /// # Arguments
1230    ///
1231    /// * `app_id` - The application ID
1232    /// * `build_id` - The build ID to delete
1233    /// * `sandbox_id` - The sandbox ID (optional)
1234    ///
1235    /// # Returns
1236    ///
1237    /// A `Result` indicating success or an error.
1238    ///
1239    /// # Errors
1240    ///
1241    /// Returns an error if the API request fails, the scan operation fails,
1242    /// or authentication/authorization fails.
1243    pub async fn delete_build(
1244        &self,
1245        app_id: &str,
1246        build_id: &str,
1247        sandbox_id: Option<&str>,
1248    ) -> Result<(), ScanError> {
1249        let endpoint = "/api/5.0/deletebuild.do";
1250
1251        // Build query parameters like Java implementation
1252        let mut query_params = Vec::new();
1253        query_params.push(("app_id", app_id));
1254        query_params.push(("build_id", build_id));
1255
1256        if let Some(sandbox_id) = sandbox_id {
1257            query_params.push(("sandbox_id", sandbox_id));
1258        }
1259
1260        let response = self.client.get_with_params(endpoint, &query_params).await?;
1261
1262        let status = response.status().as_u16();
1263        match status {
1264            200 => Ok(()),
1265            400 => {
1266                let error_text = response.text().await.unwrap_or_default();
1267                Err(ScanError::InvalidParameter(error_text))
1268            }
1269            401 => Err(ScanError::Unauthorized),
1270            403 => Err(ScanError::PermissionDenied),
1271            404 => Err(ScanError::BuildNotFound),
1272            _ => {
1273                let error_text = response.text().await.unwrap_or_default();
1274                Err(ScanError::Api(VeracodeError::InvalidResponse(format!(
1275                    "HTTP {status}: {error_text}"
1276                ))))
1277            }
1278        }
1279    }
1280
1281    /// Delete all builds for an application or sandbox
1282    ///
1283    /// This removes all uploaded files and scan data for all builds.
1284    /// Use with caution as this is irreversible.
1285    ///
1286    /// # Arguments
1287    ///
1288    /// * `app_id` - The application ID
1289    /// * `sandbox_id` - The sandbox ID (optional)
1290    ///
1291    /// # Returns
1292    ///
1293    /// A `Result` indicating success or an error.
1294    ///
1295    /// # Errors
1296    ///
1297    /// Returns an error if the API request fails, the scan operation fails,
1298    /// or authentication/authorization fails.
1299    pub async fn delete_all_builds(
1300        &self,
1301        app_id: &str,
1302        sandbox_id: Option<&str>,
1303    ) -> Result<(), ScanError> {
1304        // First get list of builds
1305        let build_info = self.get_build_info(app_id, None, sandbox_id).await?;
1306
1307        if !build_info.build_id.is_empty() && build_info.build_id != "unknown" {
1308            info!("Deleting build: {}", build_info.build_id);
1309            self.delete_build(app_id, &build_info.build_id, sandbox_id)
1310                .await?;
1311        }
1312
1313        Ok(())
1314    }
1315
1316    /// Get build information for an application or sandbox
1317    ///
1318    /// # Arguments
1319    ///
1320    /// * `app_id` - The application ID
1321    /// * `build_id` - The build ID (optional)
1322    /// * `sandbox_id` - The sandbox ID (optional)
1323    ///
1324    /// # Returns
1325    ///
1326    /// A `Result` containing the scan information or an error.
1327    ///
1328    /// # Errors
1329    ///
1330    /// Returns an error if the API request fails, the scan operation fails,
1331    /// or authentication/authorization fails.
1332    pub async fn get_build_info(
1333        &self,
1334        app_id: &str,
1335        build_id: Option<&str>,
1336        sandbox_id: Option<&str>,
1337    ) -> Result<ScanInfo, ScanError> {
1338        let endpoint = "/api/5.0/getbuildinfo.do";
1339
1340        let mut params = Vec::new();
1341        params.push(("app_id", app_id));
1342
1343        if let Some(build_id) = build_id {
1344            params.push(("build_id", build_id));
1345        }
1346
1347        if let Some(sandbox_id) = sandbox_id {
1348            params.push(("sandbox_id", sandbox_id));
1349        }
1350
1351        let response = self.client.get_with_params(endpoint, &params).await?;
1352
1353        let status = response.status().as_u16();
1354        match status {
1355            200 => {
1356                let response_text = response.text().await?;
1357                self.parse_build_info(&response_text, app_id, sandbox_id)
1358            }
1359            401 => Err(ScanError::Unauthorized),
1360            403 => Err(ScanError::PermissionDenied),
1361            404 => Err(ScanError::BuildNotFound),
1362            _ => {
1363                let error_text = response.text().await.unwrap_or_default();
1364                Err(ScanError::Api(VeracodeError::InvalidResponse(format!(
1365                    "HTTP {status}: {error_text}"
1366                ))))
1367            }
1368        }
1369    }
1370
1371    // Helper methods for parsing XML responses (Veracode API returns XML)
1372
1373    async fn parse_upload_response(
1374        &self,
1375        xml: &str,
1376        file_path: &str,
1377    ) -> Result<UploadedFile, 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 file_id = None;
1383        let mut file_status = FileStatus::PendingUpload;
1384        let mut _md5: Option<String> = None;
1385        let mut current_error: Option<String> = None;
1386        let mut in_error_tag = false;
1387
1388        loop {
1389            match reader.read_event_into(&mut buf) {
1390                Ok(Event::Start(ref e)) => {
1391                    if e.name().as_ref() == b"file" {
1392                        // Extract file_id and file_status from attributes
1393                        for attr in e.attributes().flatten() {
1394                            match attr.key.as_ref() {
1395                                b"file_id" => file_id = Some(attr_to_string(&attr.value)),
1396                                b"file_status" => {
1397                                    let status_str = attr_to_string(&attr.value);
1398                                    file_status =
1399                                        status_str.parse().unwrap_or(FileStatus::PendingUpload);
1400                                }
1401                                _ => {}
1402                            }
1403                        }
1404                    } else if e.name().as_ref() == b"error" {
1405                        in_error_tag = true;
1406                    }
1407                }
1408                Ok(Event::Text(e)) => {
1409                    if in_error_tag {
1410                        current_error = Some(String::from_utf8_lossy(&e).to_string());
1411                    } else {
1412                        let text = std::str::from_utf8(&e).unwrap_or_default();
1413                        // Check for success/error messages in text content
1414                        if text.contains("successfully uploaded") {
1415                            file_status = FileStatus::Uploaded;
1416                        }
1417                    }
1418                }
1419                Ok(Event::End(ref e)) => {
1420                    if e.name().as_ref() == b"error" {
1421                        in_error_tag = false;
1422                    }
1423                }
1424                Ok(Event::Eof) => break,
1425                Err(e) => {
1426                    error!("Error parsing XML: {e}");
1427                    break;
1428                }
1429                _ => {}
1430            }
1431            buf.clear();
1432        }
1433
1434        // If an error was found in the XML, return it
1435        if let Some(error_msg) = current_error {
1436            return Err(ScanError::UploadFailed(error_msg));
1437        }
1438
1439        let filename = Path::new(file_path)
1440            .file_name()
1441            .and_then(|f| f.to_str())
1442            .unwrap_or("file")
1443            .to_string();
1444
1445        Ok(UploadedFile {
1446            file_id: file_id.unwrap_or_else(|| format!("file_{}", chrono::Utc::now().timestamp())),
1447            file_name: filename,
1448            file_size: tokio::fs::metadata(file_path)
1449                .await
1450                .map(|m| m.len())
1451                .unwrap_or(0),
1452            uploaded: Utc::now(),
1453            file_status,
1454            md5: None,
1455        })
1456    }
1457
1458    ///
1459    /// # Errors
1460    ///
1461    /// Returns an error if the API request fails, the scan operation fails,
1462    /// or authentication/authorization fails.
1463    /// Validate scan response for basic success without parsing `build_id`
1464    fn validate_scan_response(&self, xml: &str) -> Result<(), ScanError> {
1465        // Check for basic error conditions in the response
1466        if xml.contains("<error>") {
1467            // Extract error message if present
1468            let mut reader = Reader::from_str(xml);
1469            reader.config_mut().trim_text(true);
1470
1471            let mut buf = Vec::new();
1472            let mut in_error = false;
1473            let mut error_message = String::new();
1474
1475            loop {
1476                match reader.read_event_into(&mut buf) {
1477                    Ok(Event::Start(ref e)) if e.name().as_ref() == b"error" => {
1478                        in_error = true;
1479                    }
1480                    Ok(Event::Text(ref e)) if in_error => {
1481                        error_message.push_str(&String::from_utf8_lossy(e));
1482                    }
1483                    Ok(Event::End(ref e)) if e.name().as_ref() == b"error" => {
1484                        break;
1485                    }
1486                    Ok(Event::Eof) => break,
1487                    Err(e) => {
1488                        return Err(ScanError::ScanFailed(format!("XML parsing error: {e}")));
1489                    }
1490                    _ => {}
1491                }
1492                buf.clear();
1493            }
1494
1495            if !error_message.is_empty() {
1496                return Err(ScanError::ScanFailed(error_message));
1497            }
1498            return Err(ScanError::ScanFailed(
1499                "Unknown error in scan response".to_string(),
1500            ));
1501        }
1502
1503        // Check for successful response indicators
1504        if xml.contains("<buildinfo") || xml.contains("<build") {
1505            Ok(())
1506        } else {
1507            Err(ScanError::ScanFailed(
1508                "Invalid scan response format".to_string(),
1509            ))
1510        }
1511    }
1512
1513    /// Helper function to parse module attributes from XML element
1514    fn parse_module_from_attributes<'a>(
1515        &self,
1516        attributes: impl Iterator<
1517            Item = Result<
1518                quick_xml::events::attributes::Attribute<'a>,
1519                quick_xml::events::attributes::AttrError,
1520            >,
1521        >,
1522        has_fatal_errors: &mut bool,
1523    ) -> ScanModule {
1524        let mut module = ScanModule {
1525            id: String::new(),
1526            name: String::new(),
1527            module_type: String::new(),
1528            is_fatal: false,
1529            selected: false,
1530            size: None,
1531            platform: None,
1532        };
1533
1534        for attr in attributes.flatten() {
1535            match attr.key.as_ref() {
1536                b"id" => module.id = attr_to_string(&attr.value),
1537                b"name" => module.name = attr_to_string(&attr.value),
1538                b"type" => module.module_type = attr_to_string(&attr.value),
1539                b"isfatal" => module.is_fatal = attr.value.as_ref() == b"true",
1540                b"selected" => module.selected = attr.value.as_ref() == b"true",
1541                b"has_fatal_errors" => {
1542                    if attr.value.as_ref() == b"true" {
1543                        *has_fatal_errors = true;
1544                    }
1545                }
1546                b"size" => {
1547                    if let Ok(size_str) = String::from_utf8(attr.value.to_vec()) {
1548                        module.size = size_str.parse().ok();
1549                    }
1550                }
1551                b"platform" => module.platform = Some(attr_to_string(&attr.value)),
1552                _ => {}
1553            }
1554        }
1555
1556        module
1557    }
1558
1559    fn parse_prescan_results(
1560        &self,
1561        xml: &str,
1562        app_id: &str,
1563        sandbox_id: Option<&str>,
1564    ) -> Result<PreScanResults, ScanError> {
1565        // Check if response contains an error element (prescan not ready yet)
1566        if xml.contains("<error>") && xml.contains("Prescan results not available") {
1567            // Return a special status indicating prescan is still in progress
1568            return Ok(PreScanResults {
1569                build_id: String::new(),
1570                app_id: app_id.to_string(),
1571                sandbox_id: sandbox_id.map(|s| s.to_string()),
1572                status: "Pre-Scan Submitted".to_string(), // Indicates still in progress
1573                modules: Vec::new(),
1574                messages: Vec::new(),
1575            });
1576        }
1577
1578        let mut reader = Reader::from_str(xml);
1579        reader.config_mut().trim_text(true);
1580
1581        let mut buf = Vec::new();
1582        let mut build_id = None;
1583        let mut modules = Vec::new();
1584        let messages = Vec::new();
1585        let mut has_prescan_results = false;
1586        let mut has_fatal_errors = false;
1587
1588        loop {
1589            match reader.read_event_into(&mut buf) {
1590                Ok(Event::Start(ref e)) => {
1591                    match e.name().as_ref() {
1592                        b"prescanresults" => {
1593                            has_prescan_results = true;
1594                            // Extract build_id from prescanresults attributes if present
1595                            for attr in e.attributes().flatten() {
1596                                if attr.key.as_ref() == b"build_id" {
1597                                    build_id = Some(attr_to_string(&attr.value));
1598                                }
1599                            }
1600                        }
1601                        b"module" => {
1602                            let module = self.parse_module_from_attributes(
1603                                e.attributes(),
1604                                &mut has_fatal_errors,
1605                            );
1606                            modules.push(module);
1607                        }
1608                        _ => {}
1609                    }
1610                }
1611                Ok(Event::Empty(ref e)) => {
1612                    // Handle self-closing module tags like <module ... />
1613                    if e.name().as_ref() == b"module" {
1614                        let module = self
1615                            .parse_module_from_attributes(e.attributes(), &mut has_fatal_errors);
1616                        modules.push(module);
1617                    }
1618                }
1619                Ok(Event::Eof) => break,
1620                Err(e) => {
1621                    error!("Error parsing XML: {e}");
1622                    break;
1623                }
1624                _ => {}
1625            }
1626            buf.clear();
1627        }
1628
1629        // Determine prescan status based on the parsed results
1630        let status = if !has_prescan_results {
1631            "Unknown".to_string()
1632        } else if modules.is_empty() {
1633            // No modules found - this could indicate prescan failed or is still processing
1634            "Pre-Scan Failed".to_string()
1635        } else if has_fatal_errors {
1636            // Modules found but some have fatal errors
1637            "Pre-Scan Failed".to_string()
1638        } else {
1639            // Modules found with no fatal errors - prescan succeeded
1640            "Pre-Scan Success".to_string()
1641        };
1642
1643        Ok(PreScanResults {
1644            build_id: build_id.unwrap_or_else(|| "unknown".to_string()),
1645            app_id: app_id.to_string(),
1646            sandbox_id: sandbox_id.map(|s| s.to_string()),
1647            status,
1648            modules,
1649            messages,
1650        })
1651    }
1652
1653    /// Helper function to parse file attributes from XML element
1654    fn parse_file_from_attributes<'a>(
1655        &self,
1656        attributes: impl Iterator<
1657            Item = Result<
1658                quick_xml::events::attributes::Attribute<'a>,
1659                quick_xml::events::attributes::AttrError,
1660            >,
1661        >,
1662    ) -> UploadedFile {
1663        let mut file = UploadedFile {
1664            file_id: String::new(),
1665            file_name: String::new(),
1666            file_size: 0,
1667            uploaded: Utc::now(),
1668            file_status: FileStatus::PendingUpload, // Default to PendingUpload for unknown status
1669            md5: None,
1670        };
1671
1672        for attr in attributes.flatten() {
1673            match attr.key.as_ref() {
1674                b"file_id" => file.file_id = attr_to_string(&attr.value),
1675                b"file_name" => file.file_name = attr_to_string(&attr.value),
1676                b"file_size" => {
1677                    if let Ok(size_str) = String::from_utf8(attr.value.to_vec()) {
1678                        file.file_size = size_str.parse().unwrap_or(0);
1679                    }
1680                }
1681                b"file_status" => {
1682                    let status_str = attr_to_string(&attr.value);
1683                    // Parse status, fallback to PendingUpload if unknown
1684                    file.file_status = status_str.parse().unwrap_or_else(|e| {
1685                        error!("Unknown file status '{}': {}", status_str, e);
1686                        FileStatus::PendingUpload
1687                    });
1688                }
1689                b"md5" | b"file_md5" => {
1690                    file.md5 = Some(String::from_utf8_lossy(&attr.value).to_string())
1691                }
1692                _ => {}
1693            }
1694        }
1695
1696        file
1697    }
1698
1699    fn parse_file_list(&self, xml: &str) -> Result<Vec<UploadedFile>, ScanError> {
1700        let mut reader = Reader::from_str(xml);
1701        reader.config_mut().trim_text(true);
1702
1703        let mut buf = Vec::new();
1704        let mut files = Vec::new();
1705        let mut current_error: Option<String> = None;
1706        let mut in_error_tag = false;
1707
1708        loop {
1709            match reader.read_event_into(&mut buf) {
1710                Ok(Event::Start(ref e)) => {
1711                    if e.name().as_ref() == b"file" {
1712                        let file = self.parse_file_from_attributes(e.attributes());
1713                        files.push(file);
1714                    } else if e.name().as_ref() == b"error" {
1715                        in_error_tag = true;
1716                    }
1717                }
1718                Ok(Event::Empty(ref e)) => {
1719                    // Handle self-closing file tags like <file ... />
1720                    if e.name().as_ref() == b"file" {
1721                        let file = self.parse_file_from_attributes(e.attributes());
1722                        files.push(file);
1723                    }
1724                }
1725                Ok(Event::Text(ref e)) => {
1726                    if in_error_tag {
1727                        current_error = Some(String::from_utf8_lossy(e).to_string());
1728                    }
1729                }
1730                Ok(Event::End(ref e)) => {
1731                    if e.name().as_ref() == b"error" {
1732                        in_error_tag = false;
1733                    }
1734                }
1735                Ok(Event::Eof) => break,
1736                Err(e) => {
1737                    error!("Error parsing XML: {e}");
1738                    break;
1739                }
1740                _ => {}
1741            }
1742            buf.clear();
1743        }
1744
1745        // If an error was found in the XML, return it
1746        if let Some(error_msg) = current_error {
1747            return Err(ScanError::UploadFailed(error_msg));
1748        }
1749
1750        Ok(files)
1751    }
1752
1753    fn parse_build_info(
1754        &self,
1755        xml: &str,
1756        app_id: &str,
1757        sandbox_id: Option<&str>,
1758    ) -> Result<ScanInfo, ScanError> {
1759        let mut reader = Reader::from_str(xml);
1760        reader.config_mut().trim_text(true);
1761
1762        let mut buf = Vec::new();
1763        let mut scan_info = ScanInfo {
1764            build_id: String::new(),
1765            app_id: app_id.to_string(),
1766            sandbox_id: sandbox_id.map(|s| s.to_string()),
1767            status: "Unknown".to_string(),
1768            scan_type: "Static".to_string(),
1769            analysis_unit_id: None,
1770            scan_progress_percentage: None,
1771            scan_start: None,
1772            scan_complete: None,
1773            total_lines_of_code: None,
1774        };
1775
1776        let mut inside_build = false;
1777
1778        loop {
1779            match reader.read_event_into(&mut buf) {
1780                Ok(Event::Start(ref e)) => {
1781                    match e.name().as_ref() {
1782                        b"buildinfo" => {
1783                            // Parse buildinfo attributes
1784                            for attr in e.attributes().flatten() {
1785                                match attr.key.as_ref() {
1786                                    b"build_id" => scan_info.build_id = attr_to_string(&attr.value),
1787                                    b"analysis_unit" => {
1788                                        // Fallback status from buildinfo (older API format)
1789                                        if scan_info.status == "Unknown" {
1790                                            scan_info.status = attr_to_string(&attr.value);
1791                                        }
1792                                    }
1793                                    b"analysis_unit_id" => {
1794                                        scan_info.analysis_unit_id =
1795                                            Some(attr_to_string(&attr.value))
1796                                    }
1797                                    b"scan_progress_percentage" => {
1798                                        if let Ok(progress_str) =
1799                                            String::from_utf8(attr.value.to_vec())
1800                                        {
1801                                            scan_info.scan_progress_percentage =
1802                                                progress_str.parse().ok();
1803                                        }
1804                                    }
1805                                    b"total_lines_of_code" => {
1806                                        if let Ok(lines_str) =
1807                                            String::from_utf8(attr.value.to_vec())
1808                                        {
1809                                            scan_info.total_lines_of_code = lines_str.parse().ok();
1810                                        }
1811                                    }
1812                                    _ => {}
1813                                }
1814                            }
1815                        }
1816                        b"build" => {
1817                            inside_build = true;
1818                        }
1819                        b"analysis_unit" => {
1820                            // Parse analysis_unit attributes (primary status source)
1821                            for attr in e.attributes().flatten() {
1822                                match attr.key.as_ref() {
1823                                    b"status" => {
1824                                        // Primary status source from analysis_unit
1825                                        scan_info.status = attr_to_string(&attr.value);
1826                                    }
1827                                    b"analysis_type" => {
1828                                        scan_info.scan_type = attr_to_string(&attr.value);
1829                                    }
1830                                    _ => {}
1831                                }
1832                            }
1833                        }
1834                        _ => {}
1835                    }
1836                }
1837                Ok(Event::End(ref e)) => {
1838                    if e.name().as_ref() == b"build" {
1839                        inside_build = false;
1840                    }
1841                }
1842                Ok(Event::Empty(ref e)) => {
1843                    // Handle self-closing elements like <analysis_unit ... />
1844                    if e.name().as_ref() == b"analysis_unit" && inside_build {
1845                        for attr in e.attributes().flatten() {
1846                            match attr.key.as_ref() {
1847                                b"status" => {
1848                                    scan_info.status = attr_to_string(&attr.value);
1849                                }
1850                                b"analysis_type" => {
1851                                    scan_info.scan_type = attr_to_string(&attr.value);
1852                                }
1853                                _ => {}
1854                            }
1855                        }
1856                    }
1857                }
1858                Ok(Event::Eof) => break,
1859                Err(e) => {
1860                    error!("Error parsing XML: {e}");
1861                    break;
1862                }
1863                _ => {}
1864            }
1865            buf.clear();
1866        }
1867
1868        Ok(scan_info)
1869    }
1870}
1871
1872/// Convenience methods for common scan operations
1873impl ScanApi {
1874    /// Upload a file to a sandbox with simple parameters
1875    ///
1876    /// # Arguments
1877    ///
1878    /// * `app_id` - The application ID
1879    /// * `file_path` - Path to the file to upload
1880    /// * `sandbox_id` - The sandbox ID
1881    ///
1882    /// # Returns
1883    ///
1884    /// A `Result` containing the uploaded file information or an error.
1885    ///
1886    /// # Errors
1887    ///
1888    /// Returns an error if the API request fails, the scan operation fails,
1889    /// or authentication/authorization fails.
1890    pub async fn upload_file_to_sandbox(
1891        &self,
1892        app_id: &str,
1893        file_path: &str,
1894        sandbox_id: &str,
1895    ) -> Result<UploadedFile, ScanError> {
1896        let request = UploadFileRequest {
1897            app_id: app_id.to_string(),
1898            file_path: file_path.to_string(),
1899            save_as: None,
1900            sandbox_id: Some(sandbox_id.to_string()),
1901        };
1902
1903        self.upload_file(&request).await
1904    }
1905
1906    /// Upload a file to an application (non-sandbox)
1907    ///
1908    /// # Arguments
1909    ///
1910    /// * `app_id` - The application ID
1911    /// * `file_path` - Path to the file to upload
1912    ///
1913    /// # Returns
1914    ///
1915    /// A `Result` containing the uploaded file information or an error.
1916    ///
1917    /// # Errors
1918    ///
1919    /// Returns an error if the API request fails, the scan operation fails,
1920    /// or authentication/authorization fails.
1921    pub async fn upload_file_to_app(
1922        &self,
1923        app_id: &str,
1924        file_path: &str,
1925    ) -> Result<UploadedFile, ScanError> {
1926        let request = UploadFileRequest {
1927            app_id: app_id.to_string(),
1928            file_path: file_path.to_string(),
1929            save_as: None,
1930            sandbox_id: None,
1931        };
1932
1933        self.upload_file(&request).await
1934    }
1935
1936    /// Upload a large file to a sandbox using uploadlargefile.do
1937    ///
1938    /// # Arguments
1939    ///
1940    /// * `app_id` - The application ID
1941    /// * `file_path` - Path to the file to upload
1942    /// * `sandbox_id` - The sandbox ID
1943    /// * `filename` - Optional filename for flaw matching
1944    ///
1945    /// # Returns
1946    ///
1947    /// A `Result` containing the uploaded file information or an error.
1948    ///
1949    /// # Errors
1950    ///
1951    /// Returns an error if the API request fails, the scan operation fails,
1952    /// or authentication/authorization fails.
1953    pub async fn upload_large_file_to_sandbox(
1954        &self,
1955        app_id: &str,
1956        file_path: &str,
1957        sandbox_id: &str,
1958        filename: Option<&str>,
1959    ) -> Result<UploadedFile, ScanError> {
1960        let request = UploadLargeFileRequest {
1961            app_id: app_id.to_string(),
1962            file_path: file_path.to_string(),
1963            filename: filename.map(|s| s.to_string()),
1964            sandbox_id: Some(sandbox_id.to_string()),
1965        };
1966
1967        self.upload_large_file(request).await
1968    }
1969
1970    /// Upload a large file to an application using uploadlargefile.do
1971    ///
1972    /// # Arguments
1973    ///
1974    /// * `app_id` - The application ID
1975    /// * `file_path` - Path to the file to upload
1976    /// * `filename` - Optional filename for flaw matching
1977    ///
1978    /// # Returns
1979    ///
1980    /// A `Result` containing the uploaded file information or an error.
1981    ///
1982    /// # Errors
1983    ///
1984    /// Returns an error if the API request fails, the scan operation fails,
1985    /// or authentication/authorization fails.
1986    pub async fn upload_large_file_to_app(
1987        &self,
1988        app_id: &str,
1989        file_path: &str,
1990        filename: Option<&str>,
1991    ) -> Result<UploadedFile, ScanError> {
1992        let request = UploadLargeFileRequest {
1993            app_id: app_id.to_string(),
1994            file_path: file_path.to_string(),
1995            filename: filename.map(|s| s.to_string()),
1996            sandbox_id: None,
1997        };
1998
1999        self.upload_large_file(request).await
2000    }
2001
2002    /// Upload a large file with progress tracking to a sandbox
2003    ///
2004    /// # Arguments
2005    ///
2006    /// * `app_id` - The application ID
2007    /// * `file_path` - Path to the file to upload
2008    /// * `sandbox_id` - The sandbox ID
2009    /// * `filename` - Optional filename for flaw matching
2010    /// * `progress_callback` - Callback for progress updates
2011    ///
2012    /// # Returns
2013    ///
2014    /// A `Result` containing the uploaded file information or an error.
2015    ///
2016    /// # Errors
2017    ///
2018    /// Returns an error if the API request fails, the scan operation fails,
2019    /// or authentication/authorization fails.
2020    pub async fn upload_large_file_to_sandbox_with_progress<F>(
2021        &self,
2022        app_id: &str,
2023        file_path: &str,
2024        sandbox_id: &str,
2025        filename: Option<&str>,
2026        progress_callback: F,
2027    ) -> Result<UploadedFile, ScanError>
2028    where
2029        F: Fn(u64, u64, f64) + Send + Sync,
2030    {
2031        let request = UploadLargeFileRequest {
2032            app_id: app_id.to_string(),
2033            file_path: file_path.to_string(),
2034            filename: filename.map(|s| s.to_string()),
2035            sandbox_id: Some(sandbox_id.to_string()),
2036        };
2037
2038        self.upload_large_file_with_progress(request, progress_callback)
2039            .await
2040    }
2041
2042    /// Begin a simple pre-scan for a sandbox
2043    ///
2044    /// # Arguments
2045    ///
2046    /// * `app_id` - The application ID
2047    /// * `sandbox_id` - The sandbox ID
2048    ///
2049    /// # Returns
2050    ///
2051    /// A `Result` containing the build ID or an error.
2052    ///
2053    /// # Errors
2054    ///
2055    /// Returns an error if the API request fails, the scan operation fails,
2056    /// or authentication/authorization fails.
2057    pub async fn begin_sandbox_prescan(
2058        &self,
2059        app_id: &str,
2060        sandbox_id: &str,
2061    ) -> Result<(), ScanError> {
2062        let request = BeginPreScanRequest {
2063            app_id: app_id.to_string(),
2064            sandbox_id: Some(sandbox_id.to_string()),
2065            auto_scan: Some(true),
2066            scan_all_nonfatal_top_level_modules: Some(true),
2067            include_new_modules: Some(true),
2068        };
2069
2070        self.begin_prescan(&request).await
2071    }
2072
2073    /// Begin a simple scan for a sandbox with all modules
2074    ///
2075    /// # Arguments
2076    ///
2077    /// * `app_id` - The application ID
2078    /// * `sandbox_id` - The sandbox ID
2079    ///
2080    /// # Returns
2081    ///
2082    /// A `Result` containing the build ID or an error.
2083    ///
2084    /// # Errors
2085    ///
2086    /// Returns an error if the API request fails, the scan operation fails,
2087    /// or authentication/authorization fails.
2088    pub async fn begin_sandbox_scan_all_modules(
2089        &self,
2090        app_id: &str,
2091        sandbox_id: &str,
2092    ) -> Result<(), ScanError> {
2093        let request = BeginScanRequest {
2094            app_id: app_id.to_string(),
2095            sandbox_id: Some(sandbox_id.to_string()),
2096            modules: None,
2097            scan_all_top_level_modules: Some(true),
2098            scan_all_nonfatal_top_level_modules: Some(true),
2099            scan_previously_selected_modules: None,
2100        };
2101
2102        self.begin_scan(&request).await
2103    }
2104
2105    /// Complete workflow: upload file, pre-scan, and begin scan for sandbox
2106    ///
2107    /// # Arguments
2108    ///
2109    /// * `app_id` - The application ID
2110    /// * `sandbox_id` - The sandbox ID
2111    /// * `file_path` - Path to the file to upload
2112    ///
2113    /// # Returns
2114    ///
2115    /// A `Result` containing the scan build ID or an error.
2116    ///
2117    /// # Errors
2118    ///
2119    /// Returns an error if the API request fails, the scan operation fails,
2120    /// or authentication/authorization fails.
2121    pub async fn upload_and_scan_sandbox(
2122        &self,
2123        app_id: &str,
2124        sandbox_id: &str,
2125        file_path: &str,
2126    ) -> Result<String, ScanError> {
2127        // Step 1: Upload file
2128        info!("Uploading file to sandbox...");
2129        let _uploaded_file = self
2130            .upload_file_to_sandbox(app_id, file_path, sandbox_id)
2131            .await?;
2132
2133        // Step 2: Begin pre-scan
2134        info!("Beginning pre-scan...");
2135        self.begin_sandbox_prescan(app_id, sandbox_id).await?;
2136
2137        // Step 3: Wait a moment for pre-scan to complete (in production, poll for status)
2138        tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
2139
2140        // Step 4: Begin scan
2141        info!("Beginning scan...");
2142        self.begin_sandbox_scan_all_modules(app_id, sandbox_id)
2143            .await?;
2144
2145        // For now, return a placeholder build ID since we don't parse it from responses anymore
2146        // In a real implementation, this would need to come from ensure_build_exists or similar
2147        // This is a limitation of this convenience method - it should be deprecated in favor
2148        // of the proper workflow that tracks build IDs
2149        Ok("build_id_not_available".to_string())
2150    }
2151
2152    /// Delete a build from a sandbox
2153    ///
2154    /// # Arguments
2155    ///
2156    /// * `app_id` - The application ID
2157    /// * `build_id` - The build ID to delete
2158    /// * `sandbox_id` - The sandbox ID
2159    ///
2160    /// # Returns
2161    ///
2162    /// A `Result` indicating success or an error.
2163    ///
2164    /// # Errors
2165    ///
2166    /// Returns an error if the API request fails, the scan operation fails,
2167    /// or authentication/authorization fails.
2168    pub async fn delete_sandbox_build(
2169        &self,
2170        app_id: &str,
2171        build_id: &str,
2172        sandbox_id: &str,
2173    ) -> Result<(), ScanError> {
2174        self.delete_build(app_id, build_id, Some(sandbox_id)).await
2175    }
2176
2177    /// Delete all builds from a sandbox
2178    ///
2179    /// # Arguments
2180    ///
2181    /// * `app_id` - The application ID
2182    /// * `sandbox_id` - The sandbox ID
2183    ///
2184    /// # Returns
2185    ///
2186    /// A `Result` indicating success or an error.
2187    ///
2188    /// # Errors
2189    ///
2190    /// Returns an error if the API request fails, the scan operation fails,
2191    /// or authentication/authorization fails.
2192    pub async fn delete_all_sandbox_builds(
2193        &self,
2194        app_id: &str,
2195        sandbox_id: &str,
2196    ) -> Result<(), ScanError> {
2197        self.delete_all_builds(app_id, Some(sandbox_id)).await
2198    }
2199
2200    /// Delete a build from an application (non-sandbox)
2201    ///
2202    /// # Arguments
2203    ///
2204    /// * `app_id` - The application ID
2205    /// * `build_id` - The build ID to delete
2206    ///
2207    /// # Returns
2208    ///
2209    /// A `Result` indicating success or an error.
2210    ///
2211    /// # Errors
2212    ///
2213    /// Returns an error if the API request fails, the scan operation fails,
2214    /// or authentication/authorization fails.
2215    pub async fn delete_app_build(&self, app_id: &str, build_id: &str) -> Result<(), ScanError> {
2216        self.delete_build(app_id, build_id, None).await
2217    }
2218
2219    /// Delete all builds from an application (non-sandbox)
2220    ///
2221    /// # Arguments
2222    ///
2223    /// * `app_id` - The application ID
2224    ///
2225    /// # Returns
2226    ///
2227    /// A `Result` indicating success or an error.
2228    ///
2229    /// # Errors
2230    ///
2231    /// Returns an error if the API request fails, the scan operation fails,
2232    /// or authentication/authorization fails.
2233    pub async fn delete_all_app_builds(&self, app_id: &str) -> Result<(), ScanError> {
2234        self.delete_all_builds(app_id, None).await
2235    }
2236}
2237
2238#[cfg(test)]
2239mod tests {
2240    use super::*;
2241    use crate::VeracodeConfig;
2242    use proptest::prelude::*;
2243
2244    #[test]
2245    fn test_upload_file_request() {
2246        let request = UploadFileRequest {
2247            app_id: "123".to_string(),
2248            file_path: "/path/to/file.jar".to_string(),
2249            save_as: Some("app.jar".to_string()),
2250            sandbox_id: Some("456".to_string()),
2251        };
2252
2253        assert_eq!(request.app_id, "123");
2254        assert_eq!(request.sandbox_id, Some("456".to_string()));
2255    }
2256
2257    #[test]
2258    fn test_begin_prescan_request() {
2259        let request = BeginPreScanRequest {
2260            app_id: "123".to_string(),
2261            sandbox_id: Some("456".to_string()),
2262            auto_scan: Some(true),
2263            scan_all_nonfatal_top_level_modules: Some(true),
2264            include_new_modules: Some(false),
2265        };
2266
2267        assert_eq!(request.app_id, "123");
2268        assert_eq!(request.auto_scan, Some(true));
2269    }
2270
2271    #[test]
2272    fn test_scan_error_display() {
2273        let error = ScanError::FileNotFound("test.jar".to_string());
2274        assert_eq!(error.to_string(), "File not found: test.jar");
2275
2276        let error = ScanError::UploadFailed("Network error".to_string());
2277        assert_eq!(error.to_string(), "Network error");
2278
2279        let error = ScanError::Unauthorized;
2280        assert_eq!(error.to_string(), "Unauthorized access");
2281
2282        let error = ScanError::BuildNotFound;
2283        assert_eq!(error.to_string(), "Build not found");
2284    }
2285
2286    #[test]
2287    fn test_delete_build_request_structure() {
2288        // Test that the delete build methods have correct structure
2289        // This is a compile-time test to ensure methods exist with correct signatures
2290
2291        use crate::{VeracodeClient, VeracodeConfig};
2292
2293        async fn _test_delete_methods() -> Result<(), Box<dyn std::error::Error>> {
2294            let config = VeracodeConfig::new("test", "test");
2295            let client = VeracodeClient::new(config)?;
2296            let api = client.scan_api()?;
2297
2298            // These calls won't actually execute due to test environment,
2299            // but they validate the method signatures exist
2300            let _: Result<(), _> = api
2301                .delete_build("app_id", "build_id", Some("sandbox_id"))
2302                .await;
2303            let _: Result<(), _> = api.delete_all_builds("app_id", Some("sandbox_id")).await;
2304            let _: Result<(), _> = api
2305                .delete_sandbox_build("app_id", "build_id", "sandbox_id")
2306                .await;
2307            let _: Result<(), _> = api.delete_all_sandbox_builds("app_id", "sandbox_id").await;
2308
2309            Ok(())
2310        }
2311
2312        // If this compiles, the methods have correct signatures
2313        // Test passes if no panic occurs
2314    }
2315
2316    #[test]
2317    fn test_upload_large_file_request() {
2318        let request = UploadLargeFileRequest {
2319            app_id: "123".to_string(),
2320            file_path: "/path/to/large_file.jar".to_string(),
2321            filename: Some("custom_name.jar".to_string()),
2322            sandbox_id: Some("456".to_string()),
2323        };
2324
2325        assert_eq!(request.app_id, "123");
2326        assert_eq!(request.filename, Some("custom_name.jar".to_string()));
2327        assert_eq!(request.sandbox_id, Some("456".to_string()));
2328    }
2329
2330    #[test]
2331    fn test_upload_progress() {
2332        let progress = UploadProgress {
2333            bytes_uploaded: 1024,
2334            total_bytes: 2048,
2335            percentage: 50.0,
2336        };
2337
2338        assert_eq!(progress.bytes_uploaded, 1024);
2339        assert_eq!(progress.total_bytes, 2048);
2340        assert_eq!(progress.percentage, 50.0);
2341    }
2342
2343    #[test]
2344    fn test_large_file_scan_error_display() {
2345        let error = ScanError::FileTooLarge("File exceeds 2GB".to_string());
2346        assert_eq!(error.to_string(), "File too large: File exceeds 2GB");
2347
2348        let error = ScanError::UploadInProgress;
2349        assert_eq!(error.to_string(), "Upload or prescan already in progress");
2350
2351        let error = ScanError::ScanInProgress;
2352        assert_eq!(error.to_string(), "Scan in progress, cannot upload");
2353
2354        let error = ScanError::ChunkedUploadFailed("Network error".to_string());
2355        assert_eq!(error.to_string(), "Chunked upload failed: Network error");
2356    }
2357
2358    #[test]
2359    fn test_validate_filename_path_traversal() {
2360        // Valid filenames should pass
2361        assert!(ScanApi::validate_filename("valid_file.jar").is_ok());
2362        assert!(ScanApi::validate_filename("my-app.war").is_ok());
2363        assert!(ScanApi::validate_filename("file123.zip").is_ok());
2364
2365        // Path traversal sequences should fail
2366        assert!(ScanApi::validate_filename("../etc/passwd").is_err());
2367        assert!(ScanApi::validate_filename("test/../file.jar").is_err());
2368        assert!(ScanApi::validate_filename("test/file.jar").is_err());
2369        assert!(ScanApi::validate_filename("test\\file.jar").is_err());
2370        assert!(ScanApi::validate_filename("..\\windows\\system32").is_err());
2371
2372        // Control characters should fail
2373        assert!(ScanApi::validate_filename("test\x00file.jar").is_err());
2374        assert!(ScanApi::validate_filename("test\nfile.jar").is_err());
2375        assert!(ScanApi::validate_filename("test\rfile.jar").is_err());
2376        assert!(ScanApi::validate_filename("test\x1Ffile.jar").is_err());
2377    }
2378
2379    #[tokio::test]
2380    async fn test_large_file_upload_method_signatures() {
2381        async fn _test_large_file_methods() -> Result<(), Box<dyn std::error::Error>> {
2382            let config = VeracodeConfig::new("test", "test");
2383            let client = VeracodeClient::new(config)?;
2384            let api = client.scan_api()?;
2385
2386            // Test that the method signatures exist and compile
2387            let request = UploadLargeFileRequest {
2388                app_id: "123".to_string(),
2389                file_path: "/nonexistent/file.jar".to_string(),
2390                filename: None,
2391                sandbox_id: Some("456".to_string()),
2392            };
2393
2394            // These calls won't actually execute due to test environment,
2395            // but they validate the method signatures exist
2396            let _: Result<UploadedFile, _> = api.upload_large_file(request.clone()).await;
2397            let _: Result<UploadedFile, _> = api
2398                .upload_large_file_to_sandbox("123", "/path", "456", None)
2399                .await;
2400            let _: Result<UploadedFile, _> =
2401                api.upload_large_file_to_app("123", "/path", None).await;
2402
2403            // Test progress callback signature
2404            let progress_callback = |bytes_uploaded: u64, total_bytes: u64, percentage: f64| {
2405                debug!("Upload progress: {bytes_uploaded}/{total_bytes} ({percentage:.1}%)");
2406            };
2407            let _: Result<UploadedFile, _> = api
2408                .upload_large_file_with_progress(request, progress_callback)
2409                .await;
2410
2411            Ok(())
2412        }
2413
2414        // If this compiles, the methods have correct signatures
2415        // Test passes if no panic occurs
2416    }
2417
2418    // =========================================================================
2419    // PROPERTY-BASED SECURITY TESTS (Proptest)
2420    // =========================================================================
2421
2422    mod proptest_security {
2423        use super::*;
2424
2425        // Strategy to generate potentially malicious filenames
2426        fn malicious_filename_strategy() -> impl Strategy<Value = String> {
2427            prop_oneof![
2428                // Path traversal attempts
2429                Just("../etc/passwd".to_string()),
2430                Just("..\\windows\\system32".to_string()),
2431                Just("test/../../../secret".to_string()),
2432                Just("./../../admin".to_string()),
2433                // Embedded path separators
2434                Just("dir/file.jar".to_string()),
2435                Just("dir\\file.exe".to_string()),
2436                // Control characters
2437                Just("test\x00file.jar".to_string()),
2438                Just("test\nfile.jar".to_string()),
2439                Just("test\rfile.jar".to_string()),
2440                Just("test\x1Ffile.jar".to_string()),
2441                // URL encoding attempts
2442                Just("..%2F..%2Fetc%2Fpasswd".to_string()),
2443                Just("..%5C..%5Cwindows".to_string()),
2444                // Unicode normalization attacks
2445                Just("..%c0%af..%c0%afetc%c0%afpasswd".to_string()),
2446                // Double encoding
2447                Just("..%252F..%252Fetc".to_string()),
2448                // Mixed separators
2449                Just("..\\/../admin".to_string()),
2450                // Long path traversal
2451                Just("../".repeat(20)),
2452                // Null bytes in various positions
2453                Just("\x00file.jar".to_string()),
2454                Just("file.jar\x00.exe".to_string()),
2455                // More traversal attempts
2456                Just("..".to_string()),
2457                Just("../../".to_string()),
2458                Just("/etc/passwd".to_string()),
2459                Just("\\windows\\system32".to_string()),
2460            ]
2461        }
2462
2463        // Strategy for valid filenames
2464        fn valid_filename_strategy() -> impl Strategy<Value = String> {
2465            "[a-zA-Z0-9_-]{1,200}\\.(jar|war|zip|ear|class)".prop_map(|s| s)
2466        }
2467
2468        proptest! {
2469            #![proptest_config(ProptestConfig {
2470                cases: if cfg!(miri) { 5 } else { 1000 },
2471                failure_persistence: None,
2472                .. ProptestConfig::default()
2473            })]
2474
2475            /// Property: validate_filename must reject all path traversal attempts
2476            #[test]
2477            fn prop_validate_filename_rejects_path_traversal(
2478                filename in malicious_filename_strategy()
2479            ) {
2480                // All malicious filenames should be rejected
2481                let result = ScanApi::validate_filename(&filename);
2482                prop_assert!(result.is_err(), "Should reject malicious filename: {}", filename);
2483            }
2484
2485            /// Property: validate_filename accepts valid filenames
2486            #[test]
2487            fn prop_validate_filename_accepts_valid(
2488                filename in valid_filename_strategy()
2489            ) {
2490                let result = ScanApi::validate_filename(&filename);
2491                prop_assert!(result.is_ok(), "Should accept valid filename: {}", filename);
2492            }
2493
2494            /// Property: Empty filename is always rejected
2495            #[test]
2496            fn prop_validate_filename_rejects_empty(_n in 0..100u32) {
2497                let result = ScanApi::validate_filename("");
2498                prop_assert!(result.is_err(), "Empty filename should be rejected");
2499            }
2500
2501            /// Property: Filenames exceeding max length are rejected
2502            #[test]
2503            fn prop_validate_filename_rejects_too_long(extra_len in 1..100usize) {
2504                let long_filename = "a".repeat(256_usize.saturating_add(extra_len));
2505                let result = ScanApi::validate_filename(&long_filename);
2506                prop_assert!(result.is_err(), "Filename longer than 255 chars should be rejected");
2507            }
2508
2509            /// Property: Filenames with ".." anywhere are rejected
2510            #[test]
2511            fn prop_validate_filename_rejects_double_dot(
2512                prefix in "[a-zA-Z0-9]{0,10}",
2513                suffix in "[a-zA-Z0-9]{0,10}"
2514            ) {
2515                let filename = format!("{}..{}", prefix, suffix);
2516                let result = ScanApi::validate_filename(&filename);
2517                prop_assert!(result.is_err(), "Filename with '..' should be rejected: {}", filename);
2518            }
2519
2520            /// Property: Filenames with "/" are rejected
2521            #[test]
2522            fn prop_validate_filename_rejects_forward_slash(
2523                prefix in "[a-zA-Z0-9]{1,10}",
2524                suffix in "[a-zA-Z0-9]{1,10}"
2525            ) {
2526                let filename = format!("{}/{}", prefix, suffix);
2527                let result = ScanApi::validate_filename(&filename);
2528                prop_assert!(result.is_err(), "Filename with '/' should be rejected: {}", filename);
2529            }
2530
2531            /// Property: Filenames with "\\" are rejected
2532            #[test]
2533            fn prop_validate_filename_rejects_backslash(
2534                prefix in "[a-zA-Z0-9]{1,10}",
2535                suffix in "[a-zA-Z0-9]{1,10}"
2536            ) {
2537                let filename = format!("{}\\{}", prefix, suffix);
2538                let result = ScanApi::validate_filename(&filename);
2539                prop_assert!(result.is_err(), "Filename with '\\' should be rejected: {}", filename);
2540            }
2541
2542            /// Property: Filenames with control characters are rejected
2543            #[test]
2544            fn prop_validate_filename_rejects_control_chars(
2545                prefix in "[a-zA-Z0-9]{0,10}",
2546                control_char in 0x00u8..0x20u8,
2547                suffix in "[a-zA-Z0-9]{0,10}"
2548            ) {
2549                let filename = format!("{}{}{}", prefix, control_char as char, suffix);
2550                let result = ScanApi::validate_filename(&filename);
2551                prop_assert!(result.is_err(), "Filename with control char should be rejected");
2552            }
2553        }
2554
2555        proptest! {
2556            #![proptest_config(ProptestConfig {
2557                cases: if cfg!(miri) { 5 } else { 500 },
2558                failure_persistence: None,
2559                .. ProptestConfig::default()
2560            })]
2561
2562            /// Property: attr_to_string handles all valid UTF-8
2563            #[test]
2564            fn prop_attr_to_string_valid_utf8(s in ".*") {
2565                let bytes = s.as_bytes();
2566                let result = attr_to_string(bytes);
2567                prop_assert_eq!(&result, &s, "attr_to_string should preserve valid UTF-8");
2568            }
2569
2570            /// Property: attr_to_string handles invalid UTF-8 gracefully
2571            #[test]
2572            fn prop_attr_to_string_invalid_utf8(bytes in prop::collection::vec(any::<u8>(), 0..100)) {
2573                // Should not panic on invalid UTF-8
2574                let _result = attr_to_string(&bytes);
2575                // Result should always be a valid Rust string (String type guarantees valid UTF-8)
2576                // The function may use replacement characters for invalid sequences
2577                // Just verify the function doesn't panic - the String type itself guarantees validity
2578                prop_assert!(true, "Function should not panic on invalid UTF-8");
2579            }
2580
2581            /// Property: File size validation for 2GB limit
2582            #[test]
2583            fn prop_file_size_validation(size in 0u64..5_000_000_000u64) {
2584                const MAX_SIZE: u64 = 2 * 1024 * 1024 * 1024; // 2GB
2585                let exceeds_limit = size > MAX_SIZE;
2586
2587                // Verify our logic matches the actual threshold
2588                if exceeds_limit {
2589                    prop_assert!(size > MAX_SIZE, "Size should exceed 2GB limit");
2590                } else {
2591                    prop_assert!(size <= MAX_SIZE, "Size should be within 2GB limit");
2592                }
2593            }
2594
2595            /// Property: UploadProgress percentage calculation is consistent
2596            #[test]
2597            fn prop_upload_progress_percentage(
2598                bytes_uploaded in 0u64..1_000_000u64,
2599                total_bytes in 1u64..1_000_000u64
2600            ) {
2601                // Ensure bytes_uploaded <= total_bytes
2602                let bytes_uploaded = bytes_uploaded.min(total_bytes);
2603
2604                #[allow(clippy::cast_precision_loss)]
2605                let percentage = (bytes_uploaded as f64 / total_bytes as f64) * 100.0;
2606
2607                prop_assert!((0.0..=100.0).contains(&percentage),
2608                    "Percentage should be in range [0, 100], got {}", percentage);
2609
2610                if bytes_uploaded == 0 {
2611                    prop_assert!(percentage == 0.0, "0 bytes should be 0%");
2612                }
2613                if bytes_uploaded == total_bytes {
2614                    prop_assert!(percentage == 100.0, "Full upload should be 100%");
2615                }
2616            }
2617
2618            /// Property: app_id and sandbox_id never contain path separators
2619            #[test]
2620            fn prop_request_ids_no_path_separators(
2621                app_id in "[a-zA-Z0-9-]{1,50}",
2622                sandbox_id in "[a-zA-Z0-9-]{1,50}"
2623            ) {
2624                // Verify IDs don't contain dangerous characters
2625                prop_assert!(!app_id.contains('/') && !app_id.contains('\\'));
2626                prop_assert!(!sandbox_id.contains('/') && !sandbox_id.contains('\\'));
2627                prop_assert!(!app_id.contains("..") && !sandbox_id.contains(".."));
2628            }
2629
2630            /// Property: Build ID parsing from XML should never panic
2631            #[test]
2632            fn prop_build_id_parsing_safe(build_id_value in ".*") {
2633                // Simulate XML attribute value
2634                let _xml = format!(r#"<buildinfo build_id="{}" />"#, build_id_value);
2635
2636                // Parsing should not panic even with malicious input
2637                // (We can't test the actual parser here without creating a full ScanApi instance,
2638                // but we can verify the string operations are safe)
2639                let _escaped = build_id_value.replace('&', "&amp;")
2640                    .replace('<', "&lt;")
2641                    .replace('>', "&gt;");
2642
2643                prop_assert!(true, "String escaping should not panic");
2644            }
2645
2646            /// Property: File path edge cases
2647            #[test]
2648            fn prop_file_path_edge_cases(
2649                path_segments in prop::collection::vec("[a-zA-Z0-9_-]{1,20}", 1..5)
2650            ) {
2651                let path = path_segments.join("/");
2652
2653                // Path should not contain ".."
2654                prop_assert!(!path.contains(".."), "Generated path should not contain '..'");
2655
2656                // Path should be constructible
2657                let _path_obj = std::path::Path::new(&path);
2658                prop_assert!(true, "Path construction should not panic");
2659            }
2660        }
2661
2662        proptest! {
2663            #![proptest_config(ProptestConfig {
2664                cases: if cfg!(miri) { 5 } else { 500 },
2665                failure_persistence: None,
2666                .. ProptestConfig::default()
2667            })]
2668
2669            /// Property: XML parsing robustness - should handle various attribute values
2670            #[test]
2671            fn prop_xml_attribute_robustness(
2672                file_id in "[a-zA-Z0-9_-]{1,50}",
2673                file_name in "[a-zA-Z0-9._-]{1,100}",
2674                file_size in 0u64..10_000_000u64
2675            ) {
2676                // Build a simple XML response
2677                let xml = format!(
2678                    r#"<filelist><file file_id="{}" file_name="{}" file_size="{}" /></filelist>"#,
2679                    file_id, file_name, file_size
2680                );
2681
2682                // Verify XML is well-formed (basic sanity check)
2683                prop_assert!(xml.contains(&file_id));
2684                prop_assert!(xml.contains(&file_name));
2685            }
2686
2687            /// Property: Status string validation
2688            #[test]
2689            fn prop_status_validation(status in "[A-Za-z ]{1,50}") {
2690                // Status strings should not contain control characters
2691                prop_assert!(!status.chars().any(|c| c.is_control()));
2692            }
2693
2694            /// Property: Module ID validation
2695            #[test]
2696            fn prop_module_id_validation(
2697                module_id in "[a-zA-Z0-9_-]{1,100}"
2698            ) {
2699                // Module IDs should be alphanumeric with dashes/underscores
2700                prop_assert!(module_id.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '-'));
2701            }
2702        }
2703    }
2704
2705    // =========================================================================
2706    // EDGE CASE AND BOUNDARY TESTS
2707    // =========================================================================
2708
2709    mod boundary_tests {
2710        use super::*;
2711
2712        #[test]
2713        fn test_file_size_exactly_2gb() {
2714            const TWO_GB: u64 = 2 * 1024 * 1024 * 1024;
2715            // File exactly at 2GB should not be rejected (boundary)
2716            assert_eq!(TWO_GB, 2_147_483_648);
2717        }
2718
2719        #[test]
2720        fn test_file_size_just_over_2gb() {
2721            const JUST_OVER: u64 = 2 * 1024 * 1024 * 1024 + 1;
2722            const TWO_GB_LIMIT: u64 = 2_147_483_648;
2723            // File just over 2GB should be rejected
2724            assert_eq!(JUST_OVER, TWO_GB_LIMIT + 1);
2725        }
2726
2727        #[test]
2728        fn test_filename_max_length_boundary() {
2729            // Filename exactly at 255 chars should pass
2730            let max_len_filename = "a".repeat(255);
2731            assert!(ScanApi::validate_filename(&max_len_filename).is_ok());
2732
2733            // Filename at 256 chars should fail
2734            let over_max_filename = "a".repeat(256);
2735            assert!(ScanApi::validate_filename(&over_max_filename).is_err());
2736        }
2737
2738        #[test]
2739        fn test_validate_filename_unicode_normalization() {
2740            // Test that Unicode normalization doesn't bypass validation
2741            // U+002E is ".", U+2024 is "ONE DOT LEADER"
2742            let tricky = ".\u{2024}./file.jar";
2743            // Should still be rejected if it contains path separators
2744            if tricky.contains('/') || tricky.contains('\\') || tricky.contains("..") {
2745                assert!(ScanApi::validate_filename(tricky).is_err());
2746            }
2747        }
2748
2749        #[test]
2750        fn test_validate_filename_homoglyph_attacks() {
2751            // Test homoglyph attacks (characters that look similar)
2752            // Cyrillic 'а' (U+0430) vs Latin 'a' (U+0061)
2753            // Full-width solidus (U+FF0F) vs regular slash (U+002F)
2754            let homoglyph_slash = "test\u{FF0F}file.jar";
2755
2756            // The validation should be strict enough to catch these
2757            // (depending on whether the homoglyph is normalized to a path separator)
2758            let result = ScanApi::validate_filename(homoglyph_slash);
2759            // At minimum, it should not panic
2760            assert!(result.is_ok() || result.is_err());
2761        }
2762
2763        #[test]
2764        fn test_attr_to_string_empty() {
2765            let result = attr_to_string(b"");
2766            assert_eq!(result, "");
2767        }
2768
2769        #[test]
2770        fn test_attr_to_string_ascii() {
2771            let result = attr_to_string(b"test123");
2772            assert_eq!(result, "test123");
2773        }
2774
2775        #[test]
2776        fn test_attr_to_string_utf8() {
2777            let result = attr_to_string("hello 世界".as_bytes());
2778            assert_eq!(result, "hello 世界");
2779        }
2780
2781        #[test]
2782        fn test_attr_to_string_invalid_utf8() {
2783            // Invalid UTF-8 sequence
2784            let invalid = &[0xFF, 0xFE, 0xFD];
2785            let result = attr_to_string(invalid);
2786            // Should contain replacement characters, not panic
2787            assert!(result.contains('\u{FFFD}'));
2788        }
2789
2790        #[test]
2791        fn test_upload_progress_zero_bytes() {
2792            let progress = UploadProgress {
2793                bytes_uploaded: 0,
2794                total_bytes: 1000,
2795                percentage: 0.0,
2796            };
2797            assert_eq!(progress.percentage, 0.0);
2798        }
2799
2800        #[test]
2801        fn test_upload_progress_complete() {
2802            let progress = UploadProgress {
2803                bytes_uploaded: 1000,
2804                total_bytes: 1000,
2805                percentage: 100.0,
2806            };
2807            assert_eq!(progress.percentage, 100.0);
2808        }
2809
2810        #[test]
2811        fn test_scan_error_display_all_variants() {
2812            // Ensure all error variants have valid Display implementations
2813            let errors = vec![
2814                ScanError::FileNotFound("test.jar".to_string()),
2815                ScanError::InvalidFileFormat("bad format".to_string()),
2816                ScanError::UploadFailed("network".to_string()),
2817                ScanError::ScanFailed("failed".to_string()),
2818                ScanError::PreScanFailed("prescan".to_string()),
2819                ScanError::BuildNotFound,
2820                ScanError::ApplicationNotFound,
2821                ScanError::SandboxNotFound,
2822                ScanError::Unauthorized,
2823                ScanError::PermissionDenied,
2824                ScanError::InvalidParameter("param".to_string()),
2825                ScanError::FileTooLarge("too big".to_string()),
2826                ScanError::UploadInProgress,
2827                ScanError::ScanInProgress,
2828                ScanError::BuildCreationFailed("failed".to_string()),
2829                ScanError::ChunkedUploadFailed("chunked".to_string()),
2830            ];
2831
2832            for error in errors {
2833                let display = error.to_string();
2834                assert!(!display.is_empty(), "Error display should not be empty");
2835                assert!(
2836                    !display.contains("Error"),
2837                    "Should have custom message, got: {}",
2838                    display
2839                );
2840            }
2841        }
2842    }
2843
2844    // =========================================================================
2845    // ERROR HANDLING TESTS
2846    // =========================================================================
2847
2848    mod error_handling_tests {
2849        use super::*;
2850
2851        #[test]
2852        fn test_scan_error_from_veracode_error() {
2853            let ve = VeracodeError::InvalidResponse("test".to_string());
2854            let se: ScanError = ve.into();
2855            assert!(matches!(se, ScanError::Api(_)));
2856        }
2857
2858        #[test]
2859        fn test_scan_error_from_io_error() {
2860            let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
2861            let se: ScanError = io_err.into();
2862            assert!(matches!(se, ScanError::FileNotFound(_)));
2863        }
2864
2865        #[test]
2866        fn test_scan_error_must_use() {
2867            // Verify #[must_use] attribute is present on ScanError enum
2868            // This is a compile-time check - if it compiles, the attribute is there
2869            fn _check_must_use() -> ScanError {
2870                ScanError::BuildNotFound
2871            }
2872        }
2873    }
2874}