Skip to main content

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)) if e.name().as_ref() == b"error" => {
1420                    in_error_tag = false;
1421                }
1422                Ok(Event::Eof) => break,
1423                Err(e) => {
1424                    error!("Error parsing XML: {e}");
1425                    break;
1426                }
1427                _ => {}
1428            }
1429            buf.clear();
1430        }
1431
1432        // If an error was found in the XML, return it
1433        if let Some(error_msg) = current_error {
1434            return Err(ScanError::UploadFailed(error_msg));
1435        }
1436
1437        let filename = Path::new(file_path)
1438            .file_name()
1439            .and_then(|f| f.to_str())
1440            .unwrap_or("file")
1441            .to_string();
1442
1443        Ok(UploadedFile {
1444            file_id: file_id.unwrap_or_else(|| format!("file_{}", chrono::Utc::now().timestamp())),
1445            file_name: filename,
1446            file_size: tokio::fs::metadata(file_path)
1447                .await
1448                .map(|m| m.len())
1449                .unwrap_or(0),
1450            uploaded: Utc::now(),
1451            file_status,
1452            md5: None,
1453        })
1454    }
1455
1456    ///
1457    /// # Errors
1458    ///
1459    /// Returns an error if the API request fails, the scan operation fails,
1460    /// or authentication/authorization fails.
1461    /// Validate scan response for basic success without parsing `build_id`
1462    fn validate_scan_response(&self, xml: &str) -> Result<(), ScanError> {
1463        // Check for basic error conditions in the response
1464        if xml.contains("<error>") {
1465            // Extract error message if present
1466            let mut reader = Reader::from_str(xml);
1467            reader.config_mut().trim_text(true);
1468
1469            let mut buf = Vec::new();
1470            let mut in_error = false;
1471            let mut error_message = String::new();
1472
1473            loop {
1474                match reader.read_event_into(&mut buf) {
1475                    Ok(Event::Start(ref e)) if e.name().as_ref() == b"error" => {
1476                        in_error = true;
1477                    }
1478                    Ok(Event::Text(ref e)) if in_error => {
1479                        error_message.push_str(&String::from_utf8_lossy(e));
1480                    }
1481                    Ok(Event::End(ref e)) if e.name().as_ref() == b"error" => {
1482                        break;
1483                    }
1484                    Ok(Event::Eof) => break,
1485                    Err(e) => {
1486                        return Err(ScanError::ScanFailed(format!("XML parsing error: {e}")));
1487                    }
1488                    _ => {}
1489                }
1490                buf.clear();
1491            }
1492
1493            if !error_message.is_empty() {
1494                return Err(ScanError::ScanFailed(error_message));
1495            }
1496            return Err(ScanError::ScanFailed(
1497                "Unknown error in scan response".to_string(),
1498            ));
1499        }
1500
1501        // Check for successful response indicators
1502        if xml.contains("<buildinfo") || xml.contains("<build") {
1503            Ok(())
1504        } else {
1505            Err(ScanError::ScanFailed(
1506                "Invalid scan response format".to_string(),
1507            ))
1508        }
1509    }
1510
1511    /// Helper function to parse module attributes from XML element
1512    fn parse_module_from_attributes<'a>(
1513        &self,
1514        attributes: impl Iterator<
1515            Item = Result<
1516                quick_xml::events::attributes::Attribute<'a>,
1517                quick_xml::events::attributes::AttrError,
1518            >,
1519        >,
1520        has_fatal_errors: &mut bool,
1521    ) -> ScanModule {
1522        let mut module = ScanModule {
1523            id: String::new(),
1524            name: String::new(),
1525            module_type: String::new(),
1526            is_fatal: false,
1527            selected: false,
1528            size: None,
1529            platform: None,
1530        };
1531
1532        for attr in attributes.flatten() {
1533            match attr.key.as_ref() {
1534                b"id" => module.id = attr_to_string(&attr.value),
1535                b"name" => module.name = attr_to_string(&attr.value),
1536                b"type" => module.module_type = attr_to_string(&attr.value),
1537                b"isfatal" => module.is_fatal = attr.value.as_ref() == b"true",
1538                b"selected" => module.selected = attr.value.as_ref() == b"true",
1539                b"has_fatal_errors" if attr.value.as_ref() == b"true" => {
1540                    *has_fatal_errors = true;
1541                }
1542                b"size" => {
1543                    if let Ok(size_str) = String::from_utf8(attr.value.to_vec()) {
1544                        module.size = size_str.parse().ok();
1545                    }
1546                }
1547                b"platform" => module.platform = Some(attr_to_string(&attr.value)),
1548                _ => {}
1549            }
1550        }
1551
1552        module
1553    }
1554
1555    fn parse_prescan_results(
1556        &self,
1557        xml: &str,
1558        app_id: &str,
1559        sandbox_id: Option<&str>,
1560    ) -> Result<PreScanResults, ScanError> {
1561        // Check if response contains an error element (prescan not ready yet)
1562        if xml.contains("<error>") && xml.contains("Prescan results not available") {
1563            // Return a special status indicating prescan is still in progress
1564            return Ok(PreScanResults {
1565                build_id: String::new(),
1566                app_id: app_id.to_string(),
1567                sandbox_id: sandbox_id.map(|s| s.to_string()),
1568                status: "Pre-Scan Submitted".to_string(), // Indicates still in progress
1569                modules: Vec::new(),
1570                messages: Vec::new(),
1571            });
1572        }
1573
1574        let mut reader = Reader::from_str(xml);
1575        reader.config_mut().trim_text(true);
1576
1577        let mut buf = Vec::new();
1578        let mut build_id = None;
1579        let mut modules = Vec::new();
1580        let messages = Vec::new();
1581        let mut has_prescan_results = false;
1582        let mut has_fatal_errors = false;
1583
1584        loop {
1585            match reader.read_event_into(&mut buf) {
1586                Ok(Event::Start(ref e)) => {
1587                    match e.name().as_ref() {
1588                        b"prescanresults" => {
1589                            has_prescan_results = true;
1590                            // Extract build_id from prescanresults attributes if present
1591                            for attr in e.attributes().flatten() {
1592                                if attr.key.as_ref() == b"build_id" {
1593                                    build_id = Some(attr_to_string(&attr.value));
1594                                }
1595                            }
1596                        }
1597                        b"module" => {
1598                            let module = self.parse_module_from_attributes(
1599                                e.attributes(),
1600                                &mut has_fatal_errors,
1601                            );
1602                            modules.push(module);
1603                        }
1604                        _ => {}
1605                    }
1606                }
1607                Ok(Event::Empty(ref e))
1608                    // Handle self-closing module tags like <module ... />
1609                    if e.name().as_ref() == b"module" => {
1610                    let module = self
1611                        .parse_module_from_attributes(e.attributes(), &mut has_fatal_errors);
1612                    modules.push(module);
1613                }
1614                Ok(Event::Eof) => break,
1615                Err(e) => {
1616                    error!("Error parsing XML: {e}");
1617                    break;
1618                }
1619                _ => {}
1620            }
1621            buf.clear();
1622        }
1623
1624        // Determine prescan status based on the parsed results
1625        let status = if !has_prescan_results {
1626            "Unknown".to_string()
1627        } else if modules.is_empty() {
1628            // No modules found - this could indicate prescan failed or is still processing
1629            "Pre-Scan Failed".to_string()
1630        } else if has_fatal_errors {
1631            // Modules found but some have fatal errors
1632            "Pre-Scan Failed".to_string()
1633        } else {
1634            // Modules found with no fatal errors - prescan succeeded
1635            "Pre-Scan Success".to_string()
1636        };
1637
1638        Ok(PreScanResults {
1639            build_id: build_id.unwrap_or_else(|| "unknown".to_string()),
1640            app_id: app_id.to_string(),
1641            sandbox_id: sandbox_id.map(|s| s.to_string()),
1642            status,
1643            modules,
1644            messages,
1645        })
1646    }
1647
1648    /// Helper function to parse file attributes from XML element
1649    fn parse_file_from_attributes<'a>(
1650        &self,
1651        attributes: impl Iterator<
1652            Item = Result<
1653                quick_xml::events::attributes::Attribute<'a>,
1654                quick_xml::events::attributes::AttrError,
1655            >,
1656        >,
1657    ) -> UploadedFile {
1658        let mut file = UploadedFile {
1659            file_id: String::new(),
1660            file_name: String::new(),
1661            file_size: 0,
1662            uploaded: Utc::now(),
1663            file_status: FileStatus::PendingUpload, // Default to PendingUpload for unknown status
1664            md5: None,
1665        };
1666
1667        for attr in attributes.flatten() {
1668            match attr.key.as_ref() {
1669                b"file_id" => file.file_id = attr_to_string(&attr.value),
1670                b"file_name" => file.file_name = attr_to_string(&attr.value),
1671                b"file_size" => {
1672                    if let Ok(size_str) = String::from_utf8(attr.value.to_vec()) {
1673                        file.file_size = size_str.parse().unwrap_or(0);
1674                    }
1675                }
1676                b"file_status" => {
1677                    let status_str = attr_to_string(&attr.value);
1678                    // Parse status, fallback to PendingUpload if unknown
1679                    file.file_status = status_str.parse().unwrap_or_else(|e| {
1680                        error!("Unknown file status '{}': {}", status_str, e);
1681                        FileStatus::PendingUpload
1682                    });
1683                }
1684                b"md5" | b"file_md5" => {
1685                    file.md5 = Some(String::from_utf8_lossy(&attr.value).to_string())
1686                }
1687                _ => {}
1688            }
1689        }
1690
1691        file
1692    }
1693
1694    fn parse_file_list(&self, xml: &str) -> Result<Vec<UploadedFile>, ScanError> {
1695        let mut reader = Reader::from_str(xml);
1696        reader.config_mut().trim_text(true);
1697
1698        let mut buf = Vec::new();
1699        let mut files = Vec::new();
1700        let mut current_error: Option<String> = None;
1701        let mut in_error_tag = false;
1702
1703        loop {
1704            match reader.read_event_into(&mut buf) {
1705                Ok(Event::Start(ref e)) => {
1706                    if e.name().as_ref() == b"file" {
1707                        let file = self.parse_file_from_attributes(e.attributes());
1708                        files.push(file);
1709                    } else if e.name().as_ref() == b"error" {
1710                        in_error_tag = true;
1711                    }
1712                }
1713                Ok(Event::Empty(ref e))
1714                    // Handle self-closing file tags like <file ... />
1715                    if e.name().as_ref() == b"file" => {
1716                    let file = self.parse_file_from_attributes(e.attributes());
1717                    files.push(file);
1718                }
1719                Ok(Event::Text(ref e))
1720                    if in_error_tag => {
1721                    current_error = Some(String::from_utf8_lossy(e).to_string());
1722                }
1723                Ok(Event::End(ref e))
1724                    if e.name().as_ref() == b"error" => {
1725                    in_error_tag = false;
1726                }
1727                Ok(Event::Eof) => break,
1728                Err(e) => {
1729                    error!("Error parsing XML: {e}");
1730                    break;
1731                }
1732                _ => {}
1733            }
1734            buf.clear();
1735        }
1736
1737        // If an error was found in the XML, return it
1738        if let Some(error_msg) = current_error {
1739            return Err(ScanError::UploadFailed(error_msg));
1740        }
1741
1742        Ok(files)
1743    }
1744
1745    fn parse_build_info(
1746        &self,
1747        xml: &str,
1748        app_id: &str,
1749        sandbox_id: Option<&str>,
1750    ) -> Result<ScanInfo, ScanError> {
1751        let mut reader = Reader::from_str(xml);
1752        reader.config_mut().trim_text(true);
1753
1754        let mut buf = Vec::new();
1755        let mut scan_info = ScanInfo {
1756            build_id: String::new(),
1757            app_id: app_id.to_string(),
1758            sandbox_id: sandbox_id.map(|s| s.to_string()),
1759            status: "Unknown".to_string(),
1760            scan_type: "Static".to_string(),
1761            analysis_unit_id: None,
1762            scan_progress_percentage: None,
1763            scan_start: None,
1764            scan_complete: None,
1765            total_lines_of_code: None,
1766        };
1767
1768        let mut inside_build = false;
1769
1770        loop {
1771            match reader.read_event_into(&mut buf) {
1772                Ok(Event::Start(ref e)) => {
1773                    match e.name().as_ref() {
1774                        b"buildinfo" => {
1775                            // Parse buildinfo attributes
1776                            for attr in e.attributes().flatten() {
1777                                match attr.key.as_ref() {
1778                                    b"build_id" => scan_info.build_id = attr_to_string(&attr.value),
1779                                    b"analysis_unit"
1780                                        // Fallback status from buildinfo (older API format)
1781                                        if scan_info.status == "Unknown" => {
1782                                        scan_info.status = attr_to_string(&attr.value);
1783                                    }
1784                                    b"analysis_unit_id" => {
1785                                        scan_info.analysis_unit_id =
1786                                            Some(attr_to_string(&attr.value))
1787                                    }
1788                                    b"scan_progress_percentage" => {
1789                                        if let Ok(progress_str) =
1790                                            String::from_utf8(attr.value.to_vec())
1791                                        {
1792                                            scan_info.scan_progress_percentage =
1793                                                progress_str.parse().ok();
1794                                        }
1795                                    }
1796                                    b"total_lines_of_code" => {
1797                                        if let Ok(lines_str) =
1798                                            String::from_utf8(attr.value.to_vec())
1799                                        {
1800                                            scan_info.total_lines_of_code = lines_str.parse().ok();
1801                                        }
1802                                    }
1803                                    _ => {}
1804                                }
1805                            }
1806                        }
1807                        b"build" => {
1808                            inside_build = true;
1809                        }
1810                        b"analysis_unit" => {
1811                            // Parse analysis_unit attributes (primary status source)
1812                            for attr in e.attributes().flatten() {
1813                                match attr.key.as_ref() {
1814                                    b"status" => {
1815                                        // Primary status source from analysis_unit
1816                                        scan_info.status = attr_to_string(&attr.value);
1817                                    }
1818                                    b"analysis_type" => {
1819                                        scan_info.scan_type = attr_to_string(&attr.value);
1820                                    }
1821                                    _ => {}
1822                                }
1823                            }
1824                        }
1825                        _ => {}
1826                    }
1827                }
1828                Ok(Event::End(ref e))
1829                    if e.name().as_ref() == b"build" => {
1830                    inside_build = false;
1831                }
1832                Ok(Event::Empty(ref e))
1833                    // Handle self-closing elements like <analysis_unit ... />
1834                    if e.name().as_ref() == b"analysis_unit" && inside_build => {
1835                    for attr in e.attributes().flatten() {
1836                        match attr.key.as_ref() {
1837                            b"status" => {
1838                                scan_info.status = attr_to_string(&attr.value);
1839                            }
1840                            b"analysis_type" => {
1841                                scan_info.scan_type = attr_to_string(&attr.value);
1842                            }
1843                            _ => {}
1844                        }
1845                    }
1846                }
1847                Ok(Event::Eof) => break,
1848                Err(e) => {
1849                    error!("Error parsing XML: {e}");
1850                    break;
1851                }
1852                _ => {}
1853            }
1854            buf.clear();
1855        }
1856
1857        Ok(scan_info)
1858    }
1859}
1860
1861/// Convenience methods for common scan operations
1862impl ScanApi {
1863    /// Upload a file to a sandbox with simple parameters
1864    ///
1865    /// # Arguments
1866    ///
1867    /// * `app_id` - The application ID
1868    /// * `file_path` - Path to the file to upload
1869    /// * `sandbox_id` - The sandbox ID
1870    ///
1871    /// # Returns
1872    ///
1873    /// A `Result` containing the uploaded file information or an error.
1874    ///
1875    /// # Errors
1876    ///
1877    /// Returns an error if the API request fails, the scan operation fails,
1878    /// or authentication/authorization fails.
1879    pub async fn upload_file_to_sandbox(
1880        &self,
1881        app_id: &str,
1882        file_path: &str,
1883        sandbox_id: &str,
1884    ) -> Result<UploadedFile, ScanError> {
1885        let request = UploadFileRequest {
1886            app_id: app_id.to_string(),
1887            file_path: file_path.to_string(),
1888            save_as: None,
1889            sandbox_id: Some(sandbox_id.to_string()),
1890        };
1891
1892        self.upload_file(&request).await
1893    }
1894
1895    /// Upload a file to an application (non-sandbox)
1896    ///
1897    /// # Arguments
1898    ///
1899    /// * `app_id` - The application ID
1900    /// * `file_path` - Path to the file to upload
1901    ///
1902    /// # Returns
1903    ///
1904    /// A `Result` containing the uploaded file information or an error.
1905    ///
1906    /// # Errors
1907    ///
1908    /// Returns an error if the API request fails, the scan operation fails,
1909    /// or authentication/authorization fails.
1910    pub async fn upload_file_to_app(
1911        &self,
1912        app_id: &str,
1913        file_path: &str,
1914    ) -> Result<UploadedFile, ScanError> {
1915        let request = UploadFileRequest {
1916            app_id: app_id.to_string(),
1917            file_path: file_path.to_string(),
1918            save_as: None,
1919            sandbox_id: None,
1920        };
1921
1922        self.upload_file(&request).await
1923    }
1924
1925    /// Upload a large file to a sandbox using uploadlargefile.do
1926    ///
1927    /// # Arguments
1928    ///
1929    /// * `app_id` - The application ID
1930    /// * `file_path` - Path to the file to upload
1931    /// * `sandbox_id` - The sandbox ID
1932    /// * `filename` - Optional filename for flaw matching
1933    ///
1934    /// # Returns
1935    ///
1936    /// A `Result` containing the uploaded file information or an error.
1937    ///
1938    /// # Errors
1939    ///
1940    /// Returns an error if the API request fails, the scan operation fails,
1941    /// or authentication/authorization fails.
1942    pub async fn upload_large_file_to_sandbox(
1943        &self,
1944        app_id: &str,
1945        file_path: &str,
1946        sandbox_id: &str,
1947        filename: Option<&str>,
1948    ) -> Result<UploadedFile, ScanError> {
1949        let request = UploadLargeFileRequest {
1950            app_id: app_id.to_string(),
1951            file_path: file_path.to_string(),
1952            filename: filename.map(|s| s.to_string()),
1953            sandbox_id: Some(sandbox_id.to_string()),
1954        };
1955
1956        self.upload_large_file(request).await
1957    }
1958
1959    /// Upload a large file to an application using uploadlargefile.do
1960    ///
1961    /// # Arguments
1962    ///
1963    /// * `app_id` - The application ID
1964    /// * `file_path` - Path to the file to upload
1965    /// * `filename` - Optional filename for flaw matching
1966    ///
1967    /// # Returns
1968    ///
1969    /// A `Result` containing the uploaded file information or an error.
1970    ///
1971    /// # Errors
1972    ///
1973    /// Returns an error if the API request fails, the scan operation fails,
1974    /// or authentication/authorization fails.
1975    pub async fn upload_large_file_to_app(
1976        &self,
1977        app_id: &str,
1978        file_path: &str,
1979        filename: Option<&str>,
1980    ) -> Result<UploadedFile, ScanError> {
1981        let request = UploadLargeFileRequest {
1982            app_id: app_id.to_string(),
1983            file_path: file_path.to_string(),
1984            filename: filename.map(|s| s.to_string()),
1985            sandbox_id: None,
1986        };
1987
1988        self.upload_large_file(request).await
1989    }
1990
1991    /// Upload a large file with progress tracking to a sandbox
1992    ///
1993    /// # Arguments
1994    ///
1995    /// * `app_id` - The application ID
1996    /// * `file_path` - Path to the file to upload
1997    /// * `sandbox_id` - The sandbox ID
1998    /// * `filename` - Optional filename for flaw matching
1999    /// * `progress_callback` - Callback for progress updates
2000    ///
2001    /// # Returns
2002    ///
2003    /// A `Result` containing the uploaded file information or an error.
2004    ///
2005    /// # Errors
2006    ///
2007    /// Returns an error if the API request fails, the scan operation fails,
2008    /// or authentication/authorization fails.
2009    pub async fn upload_large_file_to_sandbox_with_progress<F>(
2010        &self,
2011        app_id: &str,
2012        file_path: &str,
2013        sandbox_id: &str,
2014        filename: Option<&str>,
2015        progress_callback: F,
2016    ) -> Result<UploadedFile, ScanError>
2017    where
2018        F: Fn(u64, u64, f64) + Send + Sync,
2019    {
2020        let request = UploadLargeFileRequest {
2021            app_id: app_id.to_string(),
2022            file_path: file_path.to_string(),
2023            filename: filename.map(|s| s.to_string()),
2024            sandbox_id: Some(sandbox_id.to_string()),
2025        };
2026
2027        self.upload_large_file_with_progress(request, progress_callback)
2028            .await
2029    }
2030
2031    /// Begin a simple pre-scan for a sandbox
2032    ///
2033    /// # Arguments
2034    ///
2035    /// * `app_id` - The application ID
2036    /// * `sandbox_id` - The sandbox ID
2037    ///
2038    /// # Returns
2039    ///
2040    /// A `Result` containing the build ID or an error.
2041    ///
2042    /// # Errors
2043    ///
2044    /// Returns an error if the API request fails, the scan operation fails,
2045    /// or authentication/authorization fails.
2046    pub async fn begin_sandbox_prescan(
2047        &self,
2048        app_id: &str,
2049        sandbox_id: &str,
2050    ) -> Result<(), ScanError> {
2051        let request = BeginPreScanRequest {
2052            app_id: app_id.to_string(),
2053            sandbox_id: Some(sandbox_id.to_string()),
2054            auto_scan: Some(true),
2055            scan_all_nonfatal_top_level_modules: Some(true),
2056            include_new_modules: Some(true),
2057        };
2058
2059        self.begin_prescan(&request).await
2060    }
2061
2062    /// Begin a simple scan for a sandbox with all modules
2063    ///
2064    /// # Arguments
2065    ///
2066    /// * `app_id` - The application ID
2067    /// * `sandbox_id` - The sandbox ID
2068    ///
2069    /// # Returns
2070    ///
2071    /// A `Result` containing the build ID or an error.
2072    ///
2073    /// # Errors
2074    ///
2075    /// Returns an error if the API request fails, the scan operation fails,
2076    /// or authentication/authorization fails.
2077    pub async fn begin_sandbox_scan_all_modules(
2078        &self,
2079        app_id: &str,
2080        sandbox_id: &str,
2081    ) -> Result<(), ScanError> {
2082        let request = BeginScanRequest {
2083            app_id: app_id.to_string(),
2084            sandbox_id: Some(sandbox_id.to_string()),
2085            modules: None,
2086            scan_all_top_level_modules: Some(true),
2087            scan_all_nonfatal_top_level_modules: Some(true),
2088            scan_previously_selected_modules: None,
2089        };
2090
2091        self.begin_scan(&request).await
2092    }
2093
2094    /// Complete workflow: upload file, pre-scan, and begin scan for sandbox
2095    ///
2096    /// # Arguments
2097    ///
2098    /// * `app_id` - The application ID
2099    /// * `sandbox_id` - The sandbox ID
2100    /// * `file_path` - Path to the file to upload
2101    ///
2102    /// # Returns
2103    ///
2104    /// A `Result` containing the scan build ID or an error.
2105    ///
2106    /// # Errors
2107    ///
2108    /// Returns an error if the API request fails, the scan operation fails,
2109    /// or authentication/authorization fails.
2110    pub async fn upload_and_scan_sandbox(
2111        &self,
2112        app_id: &str,
2113        sandbox_id: &str,
2114        file_path: &str,
2115    ) -> Result<String, ScanError> {
2116        // Step 1: Upload file
2117        info!("Uploading file to sandbox...");
2118        let _uploaded_file = self
2119            .upload_file_to_sandbox(app_id, file_path, sandbox_id)
2120            .await?;
2121
2122        // Step 2: Begin pre-scan
2123        info!("Beginning pre-scan...");
2124        self.begin_sandbox_prescan(app_id, sandbox_id).await?;
2125
2126        // Step 3: Wait a moment for pre-scan to complete (in production, poll for status)
2127        tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
2128
2129        // Step 4: Begin scan
2130        info!("Beginning scan...");
2131        self.begin_sandbox_scan_all_modules(app_id, sandbox_id)
2132            .await?;
2133
2134        // For now, return a placeholder build ID since we don't parse it from responses anymore
2135        // In a real implementation, this would need to come from ensure_build_exists or similar
2136        // This is a limitation of this convenience method - it should be deprecated in favor
2137        // of the proper workflow that tracks build IDs
2138        Ok("build_id_not_available".to_string())
2139    }
2140
2141    /// Delete a build from a sandbox
2142    ///
2143    /// # Arguments
2144    ///
2145    /// * `app_id` - The application ID
2146    /// * `build_id` - The build ID to delete
2147    /// * `sandbox_id` - The sandbox ID
2148    ///
2149    /// # Returns
2150    ///
2151    /// A `Result` indicating success or an error.
2152    ///
2153    /// # Errors
2154    ///
2155    /// Returns an error if the API request fails, the scan operation fails,
2156    /// or authentication/authorization fails.
2157    pub async fn delete_sandbox_build(
2158        &self,
2159        app_id: &str,
2160        build_id: &str,
2161        sandbox_id: &str,
2162    ) -> Result<(), ScanError> {
2163        self.delete_build(app_id, build_id, Some(sandbox_id)).await
2164    }
2165
2166    /// Delete all builds from a sandbox
2167    ///
2168    /// # Arguments
2169    ///
2170    /// * `app_id` - The application ID
2171    /// * `sandbox_id` - The sandbox ID
2172    ///
2173    /// # Returns
2174    ///
2175    /// A `Result` indicating success or an error.
2176    ///
2177    /// # Errors
2178    ///
2179    /// Returns an error if the API request fails, the scan operation fails,
2180    /// or authentication/authorization fails.
2181    pub async fn delete_all_sandbox_builds(
2182        &self,
2183        app_id: &str,
2184        sandbox_id: &str,
2185    ) -> Result<(), ScanError> {
2186        self.delete_all_builds(app_id, Some(sandbox_id)).await
2187    }
2188
2189    /// Delete a build from an application (non-sandbox)
2190    ///
2191    /// # Arguments
2192    ///
2193    /// * `app_id` - The application ID
2194    /// * `build_id` - The build ID to delete
2195    ///
2196    /// # Returns
2197    ///
2198    /// A `Result` indicating success or an error.
2199    ///
2200    /// # Errors
2201    ///
2202    /// Returns an error if the API request fails, the scan operation fails,
2203    /// or authentication/authorization fails.
2204    pub async fn delete_app_build(&self, app_id: &str, build_id: &str) -> Result<(), ScanError> {
2205        self.delete_build(app_id, build_id, None).await
2206    }
2207
2208    /// Delete all builds from an application (non-sandbox)
2209    ///
2210    /// # Arguments
2211    ///
2212    /// * `app_id` - The application ID
2213    ///
2214    /// # Returns
2215    ///
2216    /// A `Result` indicating success or an error.
2217    ///
2218    /// # Errors
2219    ///
2220    /// Returns an error if the API request fails, the scan operation fails,
2221    /// or authentication/authorization fails.
2222    pub async fn delete_all_app_builds(&self, app_id: &str) -> Result<(), ScanError> {
2223        self.delete_all_builds(app_id, None).await
2224    }
2225}
2226
2227#[cfg(test)]
2228mod tests {
2229    use super::*;
2230    use crate::VeracodeConfig;
2231    use proptest::prelude::*;
2232
2233    #[test]
2234    fn test_upload_file_request() {
2235        let request = UploadFileRequest {
2236            app_id: "123".to_string(),
2237            file_path: "/path/to/file.jar".to_string(),
2238            save_as: Some("app.jar".to_string()),
2239            sandbox_id: Some("456".to_string()),
2240        };
2241
2242        assert_eq!(request.app_id, "123");
2243        assert_eq!(request.sandbox_id, Some("456".to_string()));
2244    }
2245
2246    #[test]
2247    fn test_begin_prescan_request() {
2248        let request = BeginPreScanRequest {
2249            app_id: "123".to_string(),
2250            sandbox_id: Some("456".to_string()),
2251            auto_scan: Some(true),
2252            scan_all_nonfatal_top_level_modules: Some(true),
2253            include_new_modules: Some(false),
2254        };
2255
2256        assert_eq!(request.app_id, "123");
2257        assert_eq!(request.auto_scan, Some(true));
2258    }
2259
2260    #[test]
2261    fn test_scan_error_display() {
2262        let error = ScanError::FileNotFound("test.jar".to_string());
2263        assert_eq!(error.to_string(), "File not found: test.jar");
2264
2265        let error = ScanError::UploadFailed("Network error".to_string());
2266        assert_eq!(error.to_string(), "Network error");
2267
2268        let error = ScanError::Unauthorized;
2269        assert_eq!(error.to_string(), "Unauthorized access");
2270
2271        let error = ScanError::BuildNotFound;
2272        assert_eq!(error.to_string(), "Build not found");
2273    }
2274
2275    #[test]
2276    fn test_delete_build_request_structure() {
2277        // Test that the delete build methods have correct structure
2278        // This is a compile-time test to ensure methods exist with correct signatures
2279
2280        use crate::{VeracodeClient, VeracodeConfig};
2281
2282        async fn _test_delete_methods() -> Result<(), Box<dyn std::error::Error>> {
2283            let config = VeracodeConfig::new("test", "test");
2284            let client = VeracodeClient::new(config)?;
2285            let api = client.scan_api()?;
2286
2287            // These calls won't actually execute due to test environment,
2288            // but they validate the method signatures exist
2289            let _: Result<(), _> = api
2290                .delete_build("app_id", "build_id", Some("sandbox_id"))
2291                .await;
2292            let _: Result<(), _> = api.delete_all_builds("app_id", Some("sandbox_id")).await;
2293            let _: Result<(), _> = api
2294                .delete_sandbox_build("app_id", "build_id", "sandbox_id")
2295                .await;
2296            let _: Result<(), _> = api.delete_all_sandbox_builds("app_id", "sandbox_id").await;
2297
2298            Ok(())
2299        }
2300
2301        // If this compiles, the methods have correct signatures
2302        // Test passes if no panic occurs
2303    }
2304
2305    #[test]
2306    fn test_upload_large_file_request() {
2307        let request = UploadLargeFileRequest {
2308            app_id: "123".to_string(),
2309            file_path: "/path/to/large_file.jar".to_string(),
2310            filename: Some("custom_name.jar".to_string()),
2311            sandbox_id: Some("456".to_string()),
2312        };
2313
2314        assert_eq!(request.app_id, "123");
2315        assert_eq!(request.filename, Some("custom_name.jar".to_string()));
2316        assert_eq!(request.sandbox_id, Some("456".to_string()));
2317    }
2318
2319    #[test]
2320    fn test_upload_progress() {
2321        let progress = UploadProgress {
2322            bytes_uploaded: 1024,
2323            total_bytes: 2048,
2324            percentage: 50.0,
2325        };
2326
2327        assert_eq!(progress.bytes_uploaded, 1024);
2328        assert_eq!(progress.total_bytes, 2048);
2329        assert_eq!(progress.percentage, 50.0);
2330    }
2331
2332    #[test]
2333    fn test_large_file_scan_error_display() {
2334        let error = ScanError::FileTooLarge("File exceeds 2GB".to_string());
2335        assert_eq!(error.to_string(), "File too large: File exceeds 2GB");
2336
2337        let error = ScanError::UploadInProgress;
2338        assert_eq!(error.to_string(), "Upload or prescan already in progress");
2339
2340        let error = ScanError::ScanInProgress;
2341        assert_eq!(error.to_string(), "Scan in progress, cannot upload");
2342
2343        let error = ScanError::ChunkedUploadFailed("Network error".to_string());
2344        assert_eq!(error.to_string(), "Chunked upload failed: Network error");
2345    }
2346
2347    #[test]
2348    fn test_validate_filename_path_traversal() {
2349        // Valid filenames should pass
2350        assert!(ScanApi::validate_filename("valid_file.jar").is_ok());
2351        assert!(ScanApi::validate_filename("my-app.war").is_ok());
2352        assert!(ScanApi::validate_filename("file123.zip").is_ok());
2353
2354        // Path traversal sequences should fail
2355        assert!(ScanApi::validate_filename("../etc/passwd").is_err());
2356        assert!(ScanApi::validate_filename("test/../file.jar").is_err());
2357        assert!(ScanApi::validate_filename("test/file.jar").is_err());
2358        assert!(ScanApi::validate_filename("test\\file.jar").is_err());
2359        assert!(ScanApi::validate_filename("..\\windows\\system32").is_err());
2360
2361        // Control characters should fail
2362        assert!(ScanApi::validate_filename("test\x00file.jar").is_err());
2363        assert!(ScanApi::validate_filename("test\nfile.jar").is_err());
2364        assert!(ScanApi::validate_filename("test\rfile.jar").is_err());
2365        assert!(ScanApi::validate_filename("test\x1Ffile.jar").is_err());
2366    }
2367
2368    #[tokio::test]
2369    async fn test_large_file_upload_method_signatures() {
2370        async fn _test_large_file_methods() -> Result<(), Box<dyn std::error::Error>> {
2371            let config = VeracodeConfig::new("test", "test");
2372            let client = VeracodeClient::new(config)?;
2373            let api = client.scan_api()?;
2374
2375            // Test that the method signatures exist and compile
2376            let request = UploadLargeFileRequest {
2377                app_id: "123".to_string(),
2378                file_path: "/nonexistent/file.jar".to_string(),
2379                filename: None,
2380                sandbox_id: Some("456".to_string()),
2381            };
2382
2383            // These calls won't actually execute due to test environment,
2384            // but they validate the method signatures exist
2385            let _: Result<UploadedFile, _> = api.upload_large_file(request.clone()).await;
2386            let _: Result<UploadedFile, _> = api
2387                .upload_large_file_to_sandbox("123", "/path", "456", None)
2388                .await;
2389            let _: Result<UploadedFile, _> =
2390                api.upload_large_file_to_app("123", "/path", None).await;
2391
2392            // Test progress callback signature
2393            let progress_callback = |bytes_uploaded: u64, total_bytes: u64, percentage: f64| {
2394                debug!("Upload progress: {bytes_uploaded}/{total_bytes} ({percentage:.1}%)");
2395            };
2396            let _: Result<UploadedFile, _> = api
2397                .upload_large_file_with_progress(request, progress_callback)
2398                .await;
2399
2400            Ok(())
2401        }
2402
2403        // If this compiles, the methods have correct signatures
2404        // Test passes if no panic occurs
2405    }
2406
2407    // =========================================================================
2408    // PROPERTY-BASED SECURITY TESTS (Proptest)
2409    // =========================================================================
2410
2411    mod proptest_security {
2412        use super::*;
2413
2414        // Strategy to generate potentially malicious filenames
2415        fn malicious_filename_strategy() -> impl Strategy<Value = String> {
2416            prop_oneof![
2417                // Path traversal attempts
2418                Just("../etc/passwd".to_string()),
2419                Just("..\\windows\\system32".to_string()),
2420                Just("test/../../../secret".to_string()),
2421                Just("./../../admin".to_string()),
2422                // Embedded path separators
2423                Just("dir/file.jar".to_string()),
2424                Just("dir\\file.exe".to_string()),
2425                // Control characters
2426                Just("test\x00file.jar".to_string()),
2427                Just("test\nfile.jar".to_string()),
2428                Just("test\rfile.jar".to_string()),
2429                Just("test\x1Ffile.jar".to_string()),
2430                // URL encoding attempts
2431                Just("..%2F..%2Fetc%2Fpasswd".to_string()),
2432                Just("..%5C..%5Cwindows".to_string()),
2433                // Unicode normalization attacks
2434                Just("..%c0%af..%c0%afetc%c0%afpasswd".to_string()),
2435                // Double encoding
2436                Just("..%252F..%252Fetc".to_string()),
2437                // Mixed separators
2438                Just("..\\/../admin".to_string()),
2439                // Long path traversal
2440                Just("../".repeat(20)),
2441                // Null bytes in various positions
2442                Just("\x00file.jar".to_string()),
2443                Just("file.jar\x00.exe".to_string()),
2444                // More traversal attempts
2445                Just("..".to_string()),
2446                Just("../../".to_string()),
2447                Just("/etc/passwd".to_string()),
2448                Just("\\windows\\system32".to_string()),
2449            ]
2450        }
2451
2452        // Strategy for valid filenames
2453        fn valid_filename_strategy() -> impl Strategy<Value = String> {
2454            "[a-zA-Z0-9_-]{1,200}\\.(jar|war|zip|ear|class)".prop_map(|s| s)
2455        }
2456
2457        proptest! {
2458            #![proptest_config(ProptestConfig {
2459                cases: if cfg!(miri) { 5 } else { 1000 },
2460                failure_persistence: None,
2461                .. ProptestConfig::default()
2462            })]
2463
2464            /// Property: validate_filename must reject all path traversal attempts
2465            #[test]
2466            fn prop_validate_filename_rejects_path_traversal(
2467                filename in malicious_filename_strategy()
2468            ) {
2469                // All malicious filenames should be rejected
2470                let result = ScanApi::validate_filename(&filename);
2471                prop_assert!(result.is_err(), "Should reject malicious filename: {}", filename);
2472            }
2473
2474            /// Property: validate_filename accepts valid filenames
2475            #[test]
2476            fn prop_validate_filename_accepts_valid(
2477                filename in valid_filename_strategy()
2478            ) {
2479                let result = ScanApi::validate_filename(&filename);
2480                prop_assert!(result.is_ok(), "Should accept valid filename: {}", filename);
2481            }
2482
2483            /// Property: Empty filename is always rejected
2484            #[test]
2485            fn prop_validate_filename_rejects_empty(_n in 0..100u32) {
2486                let result = ScanApi::validate_filename("");
2487                prop_assert!(result.is_err(), "Empty filename should be rejected");
2488            }
2489
2490            /// Property: Filenames exceeding max length are rejected
2491            #[test]
2492            fn prop_validate_filename_rejects_too_long(extra_len in 1..100usize) {
2493                let long_filename = "a".repeat(256_usize.saturating_add(extra_len));
2494                let result = ScanApi::validate_filename(&long_filename);
2495                prop_assert!(result.is_err(), "Filename longer than 255 chars should be rejected");
2496            }
2497
2498            /// Property: Filenames with ".." anywhere are rejected
2499            #[test]
2500            fn prop_validate_filename_rejects_double_dot(
2501                prefix in "[a-zA-Z0-9]{0,10}",
2502                suffix in "[a-zA-Z0-9]{0,10}"
2503            ) {
2504                let filename = format!("{}..{}", prefix, suffix);
2505                let result = ScanApi::validate_filename(&filename);
2506                prop_assert!(result.is_err(), "Filename with '..' should be rejected: {}", filename);
2507            }
2508
2509            /// Property: Filenames with "/" are rejected
2510            #[test]
2511            fn prop_validate_filename_rejects_forward_slash(
2512                prefix in "[a-zA-Z0-9]{1,10}",
2513                suffix in "[a-zA-Z0-9]{1,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_backslash(
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 control characters are rejected
2532            #[test]
2533            fn prop_validate_filename_rejects_control_chars(
2534                prefix in "[a-zA-Z0-9]{0,10}",
2535                control_char in 0x00u8..0x20u8,
2536                suffix in "[a-zA-Z0-9]{0,10}"
2537            ) {
2538                let filename = format!("{}{}{}", prefix, control_char as char, suffix);
2539                let result = ScanApi::validate_filename(&filename);
2540                prop_assert!(result.is_err(), "Filename with control char should be rejected");
2541            }
2542        }
2543
2544        proptest! {
2545            #![proptest_config(ProptestConfig {
2546                cases: if cfg!(miri) { 5 } else { 500 },
2547                failure_persistence: None,
2548                .. ProptestConfig::default()
2549            })]
2550
2551            /// Property: attr_to_string handles all valid UTF-8
2552            #[test]
2553            fn prop_attr_to_string_valid_utf8(s in ".*") {
2554                let bytes = s.as_bytes();
2555                let result = attr_to_string(bytes);
2556                prop_assert_eq!(&result, &s, "attr_to_string should preserve valid UTF-8");
2557            }
2558
2559            /// Property: attr_to_string handles invalid UTF-8 gracefully
2560            #[test]
2561            fn prop_attr_to_string_invalid_utf8(bytes in prop::collection::vec(any::<u8>(), 0..100)) {
2562                // Should not panic on invalid UTF-8
2563                let _result = attr_to_string(&bytes);
2564                // Result should always be a valid Rust string (String type guarantees valid UTF-8)
2565                // The function may use replacement characters for invalid sequences
2566                // Just verify the function doesn't panic - the String type itself guarantees validity
2567                prop_assert!(true, "Function should not panic on invalid UTF-8");
2568            }
2569
2570            /// Property: File size validation for 2GB limit
2571            #[test]
2572            fn prop_file_size_validation(size in 0u64..5_000_000_000u64) {
2573                const MAX_SIZE: u64 = 2 * 1024 * 1024 * 1024; // 2GB
2574                let exceeds_limit = size > MAX_SIZE;
2575
2576                // Verify our logic matches the actual threshold
2577                if exceeds_limit {
2578                    prop_assert!(size > MAX_SIZE, "Size should exceed 2GB limit");
2579                } else {
2580                    prop_assert!(size <= MAX_SIZE, "Size should be within 2GB limit");
2581                }
2582            }
2583
2584            /// Property: UploadProgress percentage calculation is consistent
2585            #[test]
2586            fn prop_upload_progress_percentage(
2587                bytes_uploaded in 0u64..1_000_000u64,
2588                total_bytes in 1u64..1_000_000u64
2589            ) {
2590                // Ensure bytes_uploaded <= total_bytes
2591                let bytes_uploaded = bytes_uploaded.min(total_bytes);
2592
2593                #[allow(clippy::cast_precision_loss)]
2594                let percentage = (bytes_uploaded as f64 / total_bytes as f64) * 100.0;
2595
2596                prop_assert!((0.0..=100.0).contains(&percentage),
2597                    "Percentage should be in range [0, 100], got {}", percentage);
2598
2599                if bytes_uploaded == 0 {
2600                    prop_assert!(percentage == 0.0, "0 bytes should be 0%");
2601                }
2602                if bytes_uploaded == total_bytes {
2603                    prop_assert!(percentage == 100.0, "Full upload should be 100%");
2604                }
2605            }
2606
2607            /// Property: app_id and sandbox_id never contain path separators
2608            #[test]
2609            fn prop_request_ids_no_path_separators(
2610                app_id in "[a-zA-Z0-9-]{1,50}",
2611                sandbox_id in "[a-zA-Z0-9-]{1,50}"
2612            ) {
2613                // Verify IDs don't contain dangerous characters
2614                prop_assert!(!app_id.contains('/') && !app_id.contains('\\'));
2615                prop_assert!(!sandbox_id.contains('/') && !sandbox_id.contains('\\'));
2616                prop_assert!(!app_id.contains("..") && !sandbox_id.contains(".."));
2617            }
2618
2619            /// Property: Build ID parsing from XML should never panic
2620            #[test]
2621            fn prop_build_id_parsing_safe(build_id_value in ".*") {
2622                // Simulate XML attribute value
2623                let _xml = format!(r#"<buildinfo build_id="{}" />"#, build_id_value);
2624
2625                // Parsing should not panic even with malicious input
2626                // (We can't test the actual parser here without creating a full ScanApi instance,
2627                // but we can verify the string operations are safe)
2628                let _escaped = build_id_value.replace('&', "&amp;")
2629                    .replace('<', "&lt;")
2630                    .replace('>', "&gt;");
2631
2632                prop_assert!(true, "String escaping should not panic");
2633            }
2634
2635            /// Property: File path edge cases
2636            #[test]
2637            fn prop_file_path_edge_cases(
2638                path_segments in prop::collection::vec("[a-zA-Z0-9_-]{1,20}", 1..5)
2639            ) {
2640                let path = path_segments.join("/");
2641
2642                // Path should not contain ".."
2643                prop_assert!(!path.contains(".."), "Generated path should not contain '..'");
2644
2645                // Path should be constructible
2646                let _path_obj = std::path::Path::new(&path);
2647                prop_assert!(true, "Path construction should not panic");
2648            }
2649        }
2650
2651        proptest! {
2652            #![proptest_config(ProptestConfig {
2653                cases: if cfg!(miri) { 5 } else { 500 },
2654                failure_persistence: None,
2655                .. ProptestConfig::default()
2656            })]
2657
2658            /// Property: XML parsing robustness - should handle various attribute values
2659            #[test]
2660            fn prop_xml_attribute_robustness(
2661                file_id in "[a-zA-Z0-9_-]{1,50}",
2662                file_name in "[a-zA-Z0-9._-]{1,100}",
2663                file_size in 0u64..10_000_000u64
2664            ) {
2665                // Build a simple XML response
2666                let xml = format!(
2667                    r#"<filelist><file file_id="{}" file_name="{}" file_size="{}" /></filelist>"#,
2668                    file_id, file_name, file_size
2669                );
2670
2671                // Verify XML is well-formed (basic sanity check)
2672                prop_assert!(xml.contains(&file_id));
2673                prop_assert!(xml.contains(&file_name));
2674            }
2675
2676            /// Property: Status string validation
2677            #[test]
2678            fn prop_status_validation(status in "[A-Za-z ]{1,50}") {
2679                // Status strings should not contain control characters
2680                prop_assert!(!status.chars().any(|c| c.is_control()));
2681            }
2682
2683            /// Property: Module ID validation
2684            #[test]
2685            fn prop_module_id_validation(
2686                module_id in "[a-zA-Z0-9_-]{1,100}"
2687            ) {
2688                // Module IDs should be alphanumeric with dashes/underscores
2689                prop_assert!(module_id.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '-'));
2690            }
2691        }
2692    }
2693
2694    // =========================================================================
2695    // EDGE CASE AND BOUNDARY TESTS
2696    // =========================================================================
2697
2698    mod boundary_tests {
2699        use super::*;
2700
2701        #[test]
2702        fn test_file_size_exactly_2gb() {
2703            const TWO_GB: u64 = 2 * 1024 * 1024 * 1024;
2704            // File exactly at 2GB should not be rejected (boundary)
2705            assert_eq!(TWO_GB, 2_147_483_648);
2706        }
2707
2708        #[test]
2709        fn test_file_size_just_over_2gb() {
2710            const JUST_OVER: u64 = 2 * 1024 * 1024 * 1024 + 1;
2711            const TWO_GB_LIMIT: u64 = 2_147_483_648;
2712            // File just over 2GB should be rejected
2713            assert_eq!(JUST_OVER, TWO_GB_LIMIT + 1);
2714        }
2715
2716        #[test]
2717        fn test_filename_max_length_boundary() {
2718            // Filename exactly at 255 chars should pass
2719            let max_len_filename = "a".repeat(255);
2720            assert!(ScanApi::validate_filename(&max_len_filename).is_ok());
2721
2722            // Filename at 256 chars should fail
2723            let over_max_filename = "a".repeat(256);
2724            assert!(ScanApi::validate_filename(&over_max_filename).is_err());
2725        }
2726
2727        #[test]
2728        fn test_validate_filename_unicode_normalization() {
2729            // Test that Unicode normalization doesn't bypass validation
2730            // U+002E is ".", U+2024 is "ONE DOT LEADER"
2731            let tricky = ".\u{2024}./file.jar";
2732            // Should still be rejected if it contains path separators
2733            if tricky.contains('/') || tricky.contains('\\') || tricky.contains("..") {
2734                assert!(ScanApi::validate_filename(tricky).is_err());
2735            }
2736        }
2737
2738        #[test]
2739        fn test_validate_filename_homoglyph_attacks() {
2740            // Test homoglyph attacks (characters that look similar)
2741            // Cyrillic 'а' (U+0430) vs Latin 'a' (U+0061)
2742            // Full-width solidus (U+FF0F) vs regular slash (U+002F)
2743            let homoglyph_slash = "test\u{FF0F}file.jar";
2744
2745            // The validation should be strict enough to catch these
2746            // (depending on whether the homoglyph is normalized to a path separator)
2747            let result = ScanApi::validate_filename(homoglyph_slash);
2748            // At minimum, it should not panic
2749            assert!(result.is_ok() || result.is_err());
2750        }
2751
2752        #[test]
2753        fn test_attr_to_string_empty() {
2754            let result = attr_to_string(b"");
2755            assert_eq!(result, "");
2756        }
2757
2758        #[test]
2759        fn test_attr_to_string_ascii() {
2760            let result = attr_to_string(b"test123");
2761            assert_eq!(result, "test123");
2762        }
2763
2764        #[test]
2765        fn test_attr_to_string_utf8() {
2766            let result = attr_to_string("hello 世界".as_bytes());
2767            assert_eq!(result, "hello 世界");
2768        }
2769
2770        #[test]
2771        fn test_attr_to_string_invalid_utf8() {
2772            // Invalid UTF-8 sequence
2773            let invalid = &[0xFF, 0xFE, 0xFD];
2774            let result = attr_to_string(invalid);
2775            // Should contain replacement characters, not panic
2776            assert!(result.contains('\u{FFFD}'));
2777        }
2778
2779        #[test]
2780        fn test_upload_progress_zero_bytes() {
2781            let progress = UploadProgress {
2782                bytes_uploaded: 0,
2783                total_bytes: 1000,
2784                percentage: 0.0,
2785            };
2786            assert_eq!(progress.percentage, 0.0);
2787        }
2788
2789        #[test]
2790        fn test_upload_progress_complete() {
2791            let progress = UploadProgress {
2792                bytes_uploaded: 1000,
2793                total_bytes: 1000,
2794                percentage: 100.0,
2795            };
2796            assert_eq!(progress.percentage, 100.0);
2797        }
2798
2799        #[test]
2800        fn test_scan_error_display_all_variants() {
2801            // Ensure all error variants have valid Display implementations
2802            let errors = vec![
2803                ScanError::FileNotFound("test.jar".to_string()),
2804                ScanError::InvalidFileFormat("bad format".to_string()),
2805                ScanError::UploadFailed("network".to_string()),
2806                ScanError::ScanFailed("failed".to_string()),
2807                ScanError::PreScanFailed("prescan".to_string()),
2808                ScanError::BuildNotFound,
2809                ScanError::ApplicationNotFound,
2810                ScanError::SandboxNotFound,
2811                ScanError::Unauthorized,
2812                ScanError::PermissionDenied,
2813                ScanError::InvalidParameter("param".to_string()),
2814                ScanError::FileTooLarge("too big".to_string()),
2815                ScanError::UploadInProgress,
2816                ScanError::ScanInProgress,
2817                ScanError::BuildCreationFailed("failed".to_string()),
2818                ScanError::ChunkedUploadFailed("chunked".to_string()),
2819            ];
2820
2821            for error in errors {
2822                let display = error.to_string();
2823                assert!(!display.is_empty(), "Error display should not be empty");
2824                assert!(
2825                    !display.contains("Error"),
2826                    "Should have custom message, got: {}",
2827                    display
2828                );
2829            }
2830        }
2831    }
2832
2833    // =========================================================================
2834    // ERROR HANDLING TESTS
2835    // =========================================================================
2836
2837    mod error_handling_tests {
2838        use super::*;
2839
2840        #[test]
2841        fn test_scan_error_from_veracode_error() {
2842            let ve = VeracodeError::InvalidResponse("test".to_string());
2843            let se: ScanError = ve.into();
2844            assert!(matches!(se, ScanError::Api(_)));
2845        }
2846
2847        #[test]
2848        fn test_scan_error_from_io_error() {
2849            let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
2850            let se: ScanError = io_err.into();
2851            assert!(matches!(se, ScanError::FileNotFound(_)));
2852        }
2853
2854        #[test]
2855        fn test_scan_error_must_use() {
2856            // Verify #[must_use] attribute is present on ScanError enum
2857            // This is a compile-time check - if it compiles, the attribute is there
2858            fn _check_must_use() -> ScanError {
2859                ScanError::BuildNotFound
2860            }
2861        }
2862    }
2863}