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    /// Helper function to parse module attributes from XML element
1257    fn parse_module_from_attributes<'a>(
1258        &self,
1259        attributes: impl Iterator<
1260            Item = Result<
1261                quick_xml::events::attributes::Attribute<'a>,
1262                quick_xml::events::attributes::AttrError,
1263            >,
1264        >,
1265        has_fatal_errors: &mut bool,
1266    ) -> ScanModule {
1267        let mut module = ScanModule {
1268            id: String::new(),
1269            name: String::new(),
1270            module_type: String::new(),
1271            is_fatal: false,
1272            selected: false,
1273            size: None,
1274            platform: None,
1275        };
1276
1277        for attr in attributes.flatten() {
1278            match attr.key.as_ref() {
1279                b"id" => module.id = attr_to_string(&attr.value),
1280                b"name" => module.name = attr_to_string(&attr.value),
1281                b"type" => module.module_type = attr_to_string(&attr.value),
1282                b"isfatal" => module.is_fatal = attr.value.as_ref() == b"true",
1283                b"selected" => module.selected = attr.value.as_ref() == b"true",
1284                b"has_fatal_errors" => {
1285                    if attr.value.as_ref() == b"true" {
1286                        *has_fatal_errors = true;
1287                    }
1288                }
1289                b"size" => {
1290                    if let Ok(size_str) = String::from_utf8(attr.value.to_vec()) {
1291                        module.size = size_str.parse().ok();
1292                    }
1293                }
1294                b"platform" => module.platform = Some(attr_to_string(&attr.value)),
1295                _ => {}
1296            }
1297        }
1298
1299        module
1300    }
1301
1302    fn parse_prescan_results(
1303        &self,
1304        xml: &str,
1305        app_id: &str,
1306        sandbox_id: Option<&str>,
1307    ) -> Result<PreScanResults, ScanError> {
1308        // Check if response contains an error element (prescan not ready yet)
1309        if xml.contains("<error>") && xml.contains("Prescan results not available") {
1310            // Return a special status indicating prescan is still in progress
1311            return Ok(PreScanResults {
1312                build_id: String::new(),
1313                app_id: app_id.to_string(),
1314                sandbox_id: sandbox_id.map(|s| s.to_string()),
1315                status: "Pre-Scan Submitted".to_string(), // Indicates still in progress
1316                modules: Vec::new(),
1317                messages: Vec::new(),
1318            });
1319        }
1320
1321        let mut reader = Reader::from_str(xml);
1322        reader.config_mut().trim_text(true);
1323
1324        let mut buf = Vec::new();
1325        let mut build_id = None;
1326        let mut modules = Vec::new();
1327        let messages = Vec::new();
1328        let mut has_prescan_results = false;
1329        let mut has_fatal_errors = false;
1330
1331        loop {
1332            match reader.read_event_into(&mut buf) {
1333                Ok(Event::Start(ref e)) => {
1334                    match e.name().as_ref() {
1335                        b"prescanresults" => {
1336                            has_prescan_results = true;
1337                            // Extract build_id from prescanresults attributes if present
1338                            for attr in e.attributes().flatten() {
1339                                if attr.key.as_ref() == b"build_id" {
1340                                    build_id = Some(attr_to_string(&attr.value));
1341                                }
1342                            }
1343                        }
1344                        b"module" => {
1345                            let module = self.parse_module_from_attributes(
1346                                e.attributes(),
1347                                &mut has_fatal_errors,
1348                            );
1349                            modules.push(module);
1350                        }
1351                        _ => {}
1352                    }
1353                }
1354                Ok(Event::Empty(ref e)) => {
1355                    // Handle self-closing module tags like <module ... />
1356                    if e.name().as_ref() == b"module" {
1357                        let module = self
1358                            .parse_module_from_attributes(e.attributes(), &mut has_fatal_errors);
1359                        modules.push(module);
1360                    }
1361                }
1362                Ok(Event::Eof) => break,
1363                Err(e) => {
1364                    error!("Error parsing XML: {e}");
1365                    break;
1366                }
1367                _ => {}
1368            }
1369            buf.clear();
1370        }
1371
1372        // Determine prescan status based on the parsed results
1373        let status = if !has_prescan_results {
1374            "Unknown".to_string()
1375        } else if modules.is_empty() {
1376            // No modules found - this could indicate prescan failed or is still processing
1377            "Pre-Scan Failed".to_string()
1378        } else if has_fatal_errors {
1379            // Modules found but some have fatal errors
1380            "Pre-Scan Failed".to_string()
1381        } else {
1382            // Modules found with no fatal errors - prescan succeeded
1383            "Pre-Scan Success".to_string()
1384        };
1385
1386        Ok(PreScanResults {
1387            build_id: build_id.unwrap_or_else(|| "unknown".to_string()),
1388            app_id: app_id.to_string(),
1389            sandbox_id: sandbox_id.map(|s| s.to_string()),
1390            status,
1391            modules,
1392            messages,
1393        })
1394    }
1395
1396    /// Helper function to parse file attributes from XML element
1397    fn parse_file_from_attributes<'a>(
1398        &self,
1399        attributes: impl Iterator<
1400            Item = Result<
1401                quick_xml::events::attributes::Attribute<'a>,
1402                quick_xml::events::attributes::AttrError,
1403            >,
1404        >,
1405    ) -> UploadedFile {
1406        let mut file = UploadedFile {
1407            file_id: String::new(),
1408            file_name: String::new(),
1409            file_size: 0,
1410            uploaded: Utc::now(),
1411            file_status: "Unknown".to_string(),
1412            md5: None,
1413        };
1414
1415        for attr in attributes.flatten() {
1416            match attr.key.as_ref() {
1417                b"file_id" => file.file_id = attr_to_string(&attr.value),
1418                b"file_name" => file.file_name = attr_to_string(&attr.value),
1419                b"file_size" => {
1420                    if let Ok(size_str) = String::from_utf8(attr.value.to_vec()) {
1421                        file.file_size = size_str.parse().unwrap_or(0);
1422                    }
1423                }
1424                b"file_status" => file.file_status = attr_to_string(&attr.value),
1425                b"md5" => file.md5 = Some(String::from_utf8_lossy(&attr.value).to_string()),
1426                _ => {}
1427            }
1428        }
1429
1430        file
1431    }
1432
1433    fn parse_file_list(&self, xml: &str) -> Result<Vec<UploadedFile>, ScanError> {
1434        let mut reader = Reader::from_str(xml);
1435        reader.config_mut().trim_text(true);
1436
1437        let mut buf = Vec::new();
1438        let mut files = Vec::new();
1439
1440        loop {
1441            match reader.read_event_into(&mut buf) {
1442                Ok(Event::Start(ref e)) => {
1443                    if e.name().as_ref() == b"file" {
1444                        let file = self.parse_file_from_attributes(e.attributes());
1445                        files.push(file);
1446                    }
1447                }
1448                Ok(Event::Empty(ref e)) => {
1449                    // Handle self-closing file tags like <file ... />
1450                    if e.name().as_ref() == b"file" {
1451                        let file = self.parse_file_from_attributes(e.attributes());
1452                        files.push(file);
1453                    }
1454                }
1455                Ok(Event::Eof) => break,
1456                Err(e) => {
1457                    error!("Error parsing XML: {e}");
1458                    break;
1459                }
1460                _ => {}
1461            }
1462            buf.clear();
1463        }
1464
1465        Ok(files)
1466    }
1467
1468    fn parse_build_info(
1469        &self,
1470        xml: &str,
1471        app_id: &str,
1472        sandbox_id: Option<&str>,
1473    ) -> Result<ScanInfo, ScanError> {
1474        let mut reader = Reader::from_str(xml);
1475        reader.config_mut().trim_text(true);
1476
1477        let mut buf = Vec::new();
1478        let mut scan_info = ScanInfo {
1479            build_id: String::new(),
1480            app_id: app_id.to_string(),
1481            sandbox_id: sandbox_id.map(|s| s.to_string()),
1482            status: "Unknown".to_string(),
1483            scan_type: "Static".to_string(),
1484            analysis_unit_id: None,
1485            scan_progress_percentage: None,
1486            scan_start: None,
1487            scan_complete: None,
1488            total_lines_of_code: None,
1489        };
1490
1491        let mut inside_build = false;
1492
1493        loop {
1494            match reader.read_event_into(&mut buf) {
1495                Ok(Event::Start(ref e)) => {
1496                    match e.name().as_ref() {
1497                        b"buildinfo" => {
1498                            // Parse buildinfo attributes
1499                            for attr in e.attributes().flatten() {
1500                                match attr.key.as_ref() {
1501                                    b"build_id" => scan_info.build_id = attr_to_string(&attr.value),
1502                                    b"analysis_unit" => {
1503                                        // Fallback status from buildinfo (older API format)
1504                                        if scan_info.status == "Unknown" {
1505                                            scan_info.status = attr_to_string(&attr.value);
1506                                        }
1507                                    }
1508                                    b"analysis_unit_id" => {
1509                                        scan_info.analysis_unit_id =
1510                                            Some(attr_to_string(&attr.value))
1511                                    }
1512                                    b"scan_progress_percentage" => {
1513                                        if let Ok(progress_str) =
1514                                            String::from_utf8(attr.value.to_vec())
1515                                        {
1516                                            scan_info.scan_progress_percentage =
1517                                                progress_str.parse().ok();
1518                                        }
1519                                    }
1520                                    b"total_lines_of_code" => {
1521                                        if let Ok(lines_str) =
1522                                            String::from_utf8(attr.value.to_vec())
1523                                        {
1524                                            scan_info.total_lines_of_code = lines_str.parse().ok();
1525                                        }
1526                                    }
1527                                    _ => {}
1528                                }
1529                            }
1530                        }
1531                        b"build" => {
1532                            inside_build = true;
1533                        }
1534                        b"analysis_unit" => {
1535                            // Parse analysis_unit attributes (primary status source)
1536                            for attr in e.attributes().flatten() {
1537                                match attr.key.as_ref() {
1538                                    b"status" => {
1539                                        // Primary status source from analysis_unit
1540                                        scan_info.status = attr_to_string(&attr.value);
1541                                    }
1542                                    b"analysis_type" => {
1543                                        scan_info.scan_type = attr_to_string(&attr.value);
1544                                    }
1545                                    _ => {}
1546                                }
1547                            }
1548                        }
1549                        _ => {}
1550                    }
1551                }
1552                Ok(Event::End(ref e)) => {
1553                    if e.name().as_ref() == b"build" {
1554                        inside_build = false;
1555                    }
1556                }
1557                Ok(Event::Empty(ref e)) => {
1558                    // Handle self-closing elements like <analysis_unit ... />
1559                    if e.name().as_ref() == b"analysis_unit" && inside_build {
1560                        for attr in e.attributes().flatten() {
1561                            match attr.key.as_ref() {
1562                                b"status" => {
1563                                    scan_info.status = attr_to_string(&attr.value);
1564                                }
1565                                b"analysis_type" => {
1566                                    scan_info.scan_type = attr_to_string(&attr.value);
1567                                }
1568                                _ => {}
1569                            }
1570                        }
1571                    }
1572                }
1573                Ok(Event::Eof) => break,
1574                Err(e) => {
1575                    error!("Error parsing XML: {e}");
1576                    break;
1577                }
1578                _ => {}
1579            }
1580            buf.clear();
1581        }
1582
1583        Ok(scan_info)
1584    }
1585}
1586
1587/// Convenience methods for common scan operations
1588impl ScanApi {
1589    /// Upload a file to a sandbox with simple parameters
1590    ///
1591    /// # Arguments
1592    ///
1593    /// * `app_id` - The application ID
1594    /// * `file_path` - Path to the file to upload
1595    /// * `sandbox_id` - The sandbox ID
1596    ///
1597    /// # Returns
1598    ///
1599    /// A `Result` containing the uploaded file information or an error.
1600    ///
1601    /// # Errors
1602    ///
1603    /// Returns an error if the API request fails, the scan operation fails,
1604    /// or authentication/authorization fails.
1605    pub async fn upload_file_to_sandbox(
1606        &self,
1607        app_id: &str,
1608        file_path: &str,
1609        sandbox_id: &str,
1610    ) -> Result<UploadedFile, ScanError> {
1611        let request = UploadFileRequest {
1612            app_id: app_id.to_string(),
1613            file_path: file_path.to_string(),
1614            save_as: None,
1615            sandbox_id: Some(sandbox_id.to_string()),
1616        };
1617
1618        self.upload_file(&request).await
1619    }
1620
1621    /// Upload a file to an application (non-sandbox)
1622    ///
1623    /// # Arguments
1624    ///
1625    /// * `app_id` - The application ID
1626    /// * `file_path` - Path to the file to upload
1627    ///
1628    /// # Returns
1629    ///
1630    /// A `Result` containing the uploaded file information or an error.
1631    ///
1632    /// # Errors
1633    ///
1634    /// Returns an error if the API request fails, the scan operation fails,
1635    /// or authentication/authorization fails.
1636    pub async fn upload_file_to_app(
1637        &self,
1638        app_id: &str,
1639        file_path: &str,
1640    ) -> Result<UploadedFile, ScanError> {
1641        let request = UploadFileRequest {
1642            app_id: app_id.to_string(),
1643            file_path: file_path.to_string(),
1644            save_as: None,
1645            sandbox_id: None,
1646        };
1647
1648        self.upload_file(&request).await
1649    }
1650
1651    /// Upload a large file to a sandbox using uploadlargefile.do
1652    ///
1653    /// # Arguments
1654    ///
1655    /// * `app_id` - The application ID
1656    /// * `file_path` - Path to the file to upload
1657    /// * `sandbox_id` - The sandbox ID
1658    /// * `filename` - Optional filename for flaw matching
1659    ///
1660    /// # Returns
1661    ///
1662    /// A `Result` containing the uploaded file information or an error.
1663    ///
1664    /// # Errors
1665    ///
1666    /// Returns an error if the API request fails, the scan operation fails,
1667    /// or authentication/authorization fails.
1668    pub async fn upload_large_file_to_sandbox(
1669        &self,
1670        app_id: &str,
1671        file_path: &str,
1672        sandbox_id: &str,
1673        filename: Option<&str>,
1674    ) -> Result<UploadedFile, ScanError> {
1675        let request = UploadLargeFileRequest {
1676            app_id: app_id.to_string(),
1677            file_path: file_path.to_string(),
1678            filename: filename.map(|s| s.to_string()),
1679            sandbox_id: Some(sandbox_id.to_string()),
1680        };
1681
1682        self.upload_large_file(request).await
1683    }
1684
1685    /// Upload a large file to an application using uploadlargefile.do
1686    ///
1687    /// # Arguments
1688    ///
1689    /// * `app_id` - The application ID
1690    /// * `file_path` - Path to the file to upload
1691    /// * `filename` - Optional filename for flaw matching
1692    ///
1693    /// # Returns
1694    ///
1695    /// A `Result` containing the uploaded file information or an error.
1696    ///
1697    /// # Errors
1698    ///
1699    /// Returns an error if the API request fails, the scan operation fails,
1700    /// or authentication/authorization fails.
1701    pub async fn upload_large_file_to_app(
1702        &self,
1703        app_id: &str,
1704        file_path: &str,
1705        filename: Option<&str>,
1706    ) -> Result<UploadedFile, ScanError> {
1707        let request = UploadLargeFileRequest {
1708            app_id: app_id.to_string(),
1709            file_path: file_path.to_string(),
1710            filename: filename.map(|s| s.to_string()),
1711            sandbox_id: None,
1712        };
1713
1714        self.upload_large_file(request).await
1715    }
1716
1717    /// Upload a large file with progress tracking to a sandbox
1718    ///
1719    /// # Arguments
1720    ///
1721    /// * `app_id` - The application ID
1722    /// * `file_path` - Path to the file to upload
1723    /// * `sandbox_id` - The sandbox ID
1724    /// * `filename` - Optional filename for flaw matching
1725    /// * `progress_callback` - Callback for progress updates
1726    ///
1727    /// # Returns
1728    ///
1729    /// A `Result` containing the uploaded file information or an error.
1730    ///
1731    /// # Errors
1732    ///
1733    /// Returns an error if the API request fails, the scan operation fails,
1734    /// or authentication/authorization fails.
1735    pub async fn upload_large_file_to_sandbox_with_progress<F>(
1736        &self,
1737        app_id: &str,
1738        file_path: &str,
1739        sandbox_id: &str,
1740        filename: Option<&str>,
1741        progress_callback: F,
1742    ) -> Result<UploadedFile, ScanError>
1743    where
1744        F: Fn(u64, u64, f64) + Send + Sync,
1745    {
1746        let request = UploadLargeFileRequest {
1747            app_id: app_id.to_string(),
1748            file_path: file_path.to_string(),
1749            filename: filename.map(|s| s.to_string()),
1750            sandbox_id: Some(sandbox_id.to_string()),
1751        };
1752
1753        self.upload_large_file_with_progress(request, progress_callback)
1754            .await
1755    }
1756
1757    /// Begin a simple pre-scan for a sandbox
1758    ///
1759    /// # Arguments
1760    ///
1761    /// * `app_id` - The application ID
1762    /// * `sandbox_id` - The sandbox ID
1763    ///
1764    /// # Returns
1765    ///
1766    /// A `Result` containing the build ID or an error.
1767    ///
1768    /// # Errors
1769    ///
1770    /// Returns an error if the API request fails, the scan operation fails,
1771    /// or authentication/authorization fails.
1772    pub async fn begin_sandbox_prescan(
1773        &self,
1774        app_id: &str,
1775        sandbox_id: &str,
1776    ) -> Result<(), ScanError> {
1777        let request = BeginPreScanRequest {
1778            app_id: app_id.to_string(),
1779            sandbox_id: Some(sandbox_id.to_string()),
1780            auto_scan: Some(true),
1781            scan_all_nonfatal_top_level_modules: Some(true),
1782            include_new_modules: Some(true),
1783        };
1784
1785        self.begin_prescan(&request).await
1786    }
1787
1788    /// Begin a simple scan for a sandbox with all modules
1789    ///
1790    /// # Arguments
1791    ///
1792    /// * `app_id` - The application ID
1793    /// * `sandbox_id` - The sandbox ID
1794    ///
1795    /// # Returns
1796    ///
1797    /// A `Result` containing the build ID or an error.
1798    ///
1799    /// # Errors
1800    ///
1801    /// Returns an error if the API request fails, the scan operation fails,
1802    /// or authentication/authorization fails.
1803    pub async fn begin_sandbox_scan_all_modules(
1804        &self,
1805        app_id: &str,
1806        sandbox_id: &str,
1807    ) -> Result<(), ScanError> {
1808        let request = BeginScanRequest {
1809            app_id: app_id.to_string(),
1810            sandbox_id: Some(sandbox_id.to_string()),
1811            modules: None,
1812            scan_all_top_level_modules: Some(true),
1813            scan_all_nonfatal_top_level_modules: Some(true),
1814            scan_previously_selected_modules: None,
1815        };
1816
1817        self.begin_scan(&request).await
1818    }
1819
1820    /// Complete workflow: upload file, pre-scan, and begin scan for sandbox
1821    ///
1822    /// # Arguments
1823    ///
1824    /// * `app_id` - The application ID
1825    /// * `sandbox_id` - The sandbox ID
1826    /// * `file_path` - Path to the file to upload
1827    ///
1828    /// # Returns
1829    ///
1830    /// A `Result` containing the scan build ID or an error.
1831    ///
1832    /// # Errors
1833    ///
1834    /// Returns an error if the API request fails, the scan operation fails,
1835    /// or authentication/authorization fails.
1836    pub async fn upload_and_scan_sandbox(
1837        &self,
1838        app_id: &str,
1839        sandbox_id: &str,
1840        file_path: &str,
1841    ) -> Result<String, ScanError> {
1842        // Step 1: Upload file
1843        info!("Uploading file to sandbox...");
1844        let _uploaded_file = self
1845            .upload_file_to_sandbox(app_id, file_path, sandbox_id)
1846            .await?;
1847
1848        // Step 2: Begin pre-scan
1849        info!("Beginning pre-scan...");
1850        self.begin_sandbox_prescan(app_id, sandbox_id).await?;
1851
1852        // Step 3: Wait a moment for pre-scan to complete (in production, poll for status)
1853        tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
1854
1855        // Step 4: Begin scan
1856        info!("Beginning scan...");
1857        self.begin_sandbox_scan_all_modules(app_id, sandbox_id)
1858            .await?;
1859
1860        // For now, return a placeholder build ID since we don't parse it from responses anymore
1861        // In a real implementation, this would need to come from ensure_build_exists or similar
1862        // This is a limitation of this convenience method - it should be deprecated in favor
1863        // of the proper workflow that tracks build IDs
1864        Ok("build_id_not_available".to_string())
1865    }
1866
1867    /// Delete a build from a sandbox
1868    ///
1869    /// # Arguments
1870    ///
1871    /// * `app_id` - The application ID
1872    /// * `build_id` - The build ID to delete
1873    /// * `sandbox_id` - The sandbox ID
1874    ///
1875    /// # Returns
1876    ///
1877    /// A `Result` indicating success or an error.
1878    ///
1879    /// # Errors
1880    ///
1881    /// Returns an error if the API request fails, the scan operation fails,
1882    /// or authentication/authorization fails.
1883    pub async fn delete_sandbox_build(
1884        &self,
1885        app_id: &str,
1886        build_id: &str,
1887        sandbox_id: &str,
1888    ) -> Result<(), ScanError> {
1889        self.delete_build(app_id, build_id, Some(sandbox_id)).await
1890    }
1891
1892    /// Delete all builds from a sandbox
1893    ///
1894    /// # Arguments
1895    ///
1896    /// * `app_id` - The application ID
1897    /// * `sandbox_id` - The sandbox ID
1898    ///
1899    /// # Returns
1900    ///
1901    /// A `Result` indicating success or an error.
1902    ///
1903    /// # Errors
1904    ///
1905    /// Returns an error if the API request fails, the scan operation fails,
1906    /// or authentication/authorization fails.
1907    pub async fn delete_all_sandbox_builds(
1908        &self,
1909        app_id: &str,
1910        sandbox_id: &str,
1911    ) -> Result<(), ScanError> {
1912        self.delete_all_builds(app_id, Some(sandbox_id)).await
1913    }
1914
1915    /// Delete a build from an application (non-sandbox)
1916    ///
1917    /// # Arguments
1918    ///
1919    /// * `app_id` - The application ID
1920    /// * `build_id` - The build ID to delete
1921    ///
1922    /// # Returns
1923    ///
1924    /// A `Result` indicating success or an error.
1925    ///
1926    /// # Errors
1927    ///
1928    /// Returns an error if the API request fails, the scan operation fails,
1929    /// or authentication/authorization fails.
1930    pub async fn delete_app_build(&self, app_id: &str, build_id: &str) -> Result<(), ScanError> {
1931        self.delete_build(app_id, build_id, None).await
1932    }
1933
1934    /// Delete all builds from an application (non-sandbox)
1935    ///
1936    /// # Arguments
1937    ///
1938    /// * `app_id` - The application ID
1939    ///
1940    /// # Returns
1941    ///
1942    /// A `Result` indicating success or an error.
1943    ///
1944    /// # Errors
1945    ///
1946    /// Returns an error if the API request fails, the scan operation fails,
1947    /// or authentication/authorization fails.
1948    pub async fn delete_all_app_builds(&self, app_id: &str) -> Result<(), ScanError> {
1949        self.delete_all_builds(app_id, None).await
1950    }
1951}
1952
1953#[cfg(test)]
1954mod tests {
1955    use super::*;
1956    use crate::VeracodeConfig;
1957
1958    #[test]
1959    fn test_upload_file_request() {
1960        let request = UploadFileRequest {
1961            app_id: "123".to_string(),
1962            file_path: "/path/to/file.jar".to_string(),
1963            save_as: Some("app.jar".to_string()),
1964            sandbox_id: Some("456".to_string()),
1965        };
1966
1967        assert_eq!(request.app_id, "123");
1968        assert_eq!(request.sandbox_id, Some("456".to_string()));
1969    }
1970
1971    #[test]
1972    fn test_begin_prescan_request() {
1973        let request = BeginPreScanRequest {
1974            app_id: "123".to_string(),
1975            sandbox_id: Some("456".to_string()),
1976            auto_scan: Some(true),
1977            scan_all_nonfatal_top_level_modules: Some(true),
1978            include_new_modules: Some(false),
1979        };
1980
1981        assert_eq!(request.app_id, "123");
1982        assert_eq!(request.auto_scan, Some(true));
1983    }
1984
1985    #[test]
1986    fn test_scan_error_display() {
1987        let error = ScanError::FileNotFound("test.jar".to_string());
1988        assert_eq!(error.to_string(), "File not found: test.jar");
1989
1990        let error = ScanError::UploadFailed("Network error".to_string());
1991        assert_eq!(error.to_string(), "Upload failed: Network error");
1992
1993        let error = ScanError::Unauthorized;
1994        assert_eq!(error.to_string(), "Unauthorized access");
1995
1996        let error = ScanError::BuildNotFound;
1997        assert_eq!(error.to_string(), "Build not found");
1998    }
1999
2000    #[test]
2001    fn test_delete_build_request_structure() {
2002        // Test that the delete build methods have correct structure
2003        // This is a compile-time test to ensure methods exist with correct signatures
2004
2005        use crate::{VeracodeClient, VeracodeConfig};
2006
2007        async fn _test_delete_methods() -> Result<(), Box<dyn std::error::Error>> {
2008            let config = VeracodeConfig::new("test", "test");
2009            let client = VeracodeClient::new(config)?;
2010            let api = client.scan_api()?;
2011
2012            // These calls won't actually execute due to test environment,
2013            // but they validate the method signatures exist
2014            let _: Result<(), _> = api
2015                .delete_build("app_id", "build_id", Some("sandbox_id"))
2016                .await;
2017            let _: Result<(), _> = api.delete_all_builds("app_id", Some("sandbox_id")).await;
2018            let _: Result<(), _> = api
2019                .delete_sandbox_build("app_id", "build_id", "sandbox_id")
2020                .await;
2021            let _: Result<(), _> = api.delete_all_sandbox_builds("app_id", "sandbox_id").await;
2022
2023            Ok(())
2024        }
2025
2026        // If this compiles, the methods have correct signatures
2027        // Test passes if no panic occurs
2028    }
2029
2030    #[test]
2031    fn test_upload_large_file_request() {
2032        let request = UploadLargeFileRequest {
2033            app_id: "123".to_string(),
2034            file_path: "/path/to/large_file.jar".to_string(),
2035            filename: Some("custom_name.jar".to_string()),
2036            sandbox_id: Some("456".to_string()),
2037        };
2038
2039        assert_eq!(request.app_id, "123");
2040        assert_eq!(request.filename, Some("custom_name.jar".to_string()));
2041        assert_eq!(request.sandbox_id, Some("456".to_string()));
2042    }
2043
2044    #[test]
2045    fn test_upload_progress() {
2046        let progress = UploadProgress {
2047            bytes_uploaded: 1024,
2048            total_bytes: 2048,
2049            percentage: 50.0,
2050        };
2051
2052        assert_eq!(progress.bytes_uploaded, 1024);
2053        assert_eq!(progress.total_bytes, 2048);
2054        assert_eq!(progress.percentage, 50.0);
2055    }
2056
2057    #[test]
2058    fn test_large_file_scan_error_display() {
2059        let error = ScanError::FileTooLarge("File exceeds 2GB".to_string());
2060        assert_eq!(error.to_string(), "File too large: File exceeds 2GB");
2061
2062        let error = ScanError::UploadInProgress;
2063        assert_eq!(error.to_string(), "Upload or prescan already in progress");
2064
2065        let error = ScanError::ScanInProgress;
2066        assert_eq!(error.to_string(), "Scan in progress, cannot upload");
2067
2068        let error = ScanError::ChunkedUploadFailed("Network error".to_string());
2069        assert_eq!(error.to_string(), "Chunked upload failed: Network error");
2070    }
2071
2072    #[tokio::test]
2073    async fn test_large_file_upload_method_signatures() {
2074        async fn _test_large_file_methods() -> Result<(), Box<dyn std::error::Error>> {
2075            let config = VeracodeConfig::new("test", "test");
2076            let client = VeracodeClient::new(config)?;
2077            let api = client.scan_api()?;
2078
2079            // Test that the method signatures exist and compile
2080            let request = UploadLargeFileRequest {
2081                app_id: "123".to_string(),
2082                file_path: "/nonexistent/file.jar".to_string(),
2083                filename: None,
2084                sandbox_id: Some("456".to_string()),
2085            };
2086
2087            // These calls won't actually execute due to test environment,
2088            // but they validate the method signatures exist
2089            let _: Result<UploadedFile, _> = api.upload_large_file(request.clone()).await;
2090            let _: Result<UploadedFile, _> = api
2091                .upload_large_file_to_sandbox("123", "/path", "456", None)
2092                .await;
2093            let _: Result<UploadedFile, _> =
2094                api.upload_large_file_to_app("123", "/path", None).await;
2095
2096            // Test progress callback signature
2097            let progress_callback = |bytes_uploaded: u64, total_bytes: u64, percentage: f64| {
2098                debug!("Upload progress: {bytes_uploaded}/{total_bytes} ({percentage:.1}%)");
2099            };
2100            let _: Result<UploadedFile, _> = api
2101                .upload_large_file_with_progress(request, progress_callback)
2102                .await;
2103
2104            Ok(())
2105        }
2106
2107        // If this compiles, the methods have correct signatures
2108        // Test passes if no panic occurs
2109    }
2110}