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