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