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