1use serde::{Deserialize, Serialize};
8use chrono::{DateTime, Utc};
9use std::path::Path;
10use quick_xml::Reader;
11use quick_xml::events::Event;
12
13use crate::{VeracodeClient, VeracodeError};
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct UploadedFile {
18 pub file_id: String,
20 pub file_name: String,
22 pub file_size: u64,
24 pub uploaded: DateTime<Utc>,
26 pub file_status: String,
28 pub md5: Option<String>,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct PreScanResults {
35 pub build_id: String,
37 pub app_id: String,
39 pub sandbox_id: Option<String>,
41 pub status: String,
43 pub modules: Vec<ScanModule>,
45 pub messages: Vec<PreScanMessage>,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct ScanModule {
52 pub id: String,
54 pub name: String,
56 pub module_type: String,
58 pub is_fatal: bool,
60 pub selected: bool,
62 pub size: Option<u64>,
64 pub platform: Option<String>,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct PreScanMessage {
71 pub severity: String,
73 pub text: String,
75 pub module_name: Option<String>,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct ScanInfo {
82 pub build_id: String,
84 pub app_id: String,
86 pub sandbox_id: Option<String>,
88 pub status: String,
90 pub scan_type: String,
92 pub analysis_unit_id: Option<String>,
94 pub scan_progress_percentage: Option<u32>,
96 pub scan_start: Option<DateTime<Utc>>,
98 pub scan_complete: Option<DateTime<Utc>>,
100 pub total_lines_of_code: Option<u64>,
102}
103
104#[derive(Debug, Clone)]
106pub struct UploadFileRequest {
107 pub app_id: String,
109 pub file_path: String,
111 pub save_as: Option<String>,
113 pub sandbox_id: Option<String>,
115}
116
117#[derive(Debug, Clone)]
119pub struct UploadLargeFileRequest {
120 pub app_id: String,
122 pub file_path: String,
124 pub filename: Option<String>,
126 pub sandbox_id: Option<String>,
128}
129
130#[derive(Debug, Clone)]
132pub struct UploadProgress {
133 pub bytes_uploaded: u64,
135 pub total_bytes: u64,
137 pub percentage: f64,
139}
140
141pub trait UploadProgressCallback: Send + Sync {
143 fn on_progress(&self, progress: UploadProgress);
145 fn on_completed(&self);
147 fn on_error(&self, error: &str);
149}
150
151#[derive(Debug, Clone)]
153pub struct BeginPreScanRequest {
154 pub app_id: String,
156 pub sandbox_id: Option<String>,
158 pub auto_scan: Option<bool>,
160 pub scan_all_nonfatal_top_level_modules: Option<bool>,
162 pub include_new_modules: Option<bool>,
164}
165
166#[derive(Debug, Clone)]
168pub struct BeginScanRequest {
169 pub app_id: String,
171 pub sandbox_id: Option<String>,
173 pub modules: Option<String>,
175 pub scan_all_top_level_modules: Option<bool>,
177 pub scan_all_nonfatal_top_level_modules: Option<bool>,
179 pub scan_previously_selected_modules: Option<bool>,
181}
182
183#[derive(Debug)]
185pub enum ScanError {
186 Api(VeracodeError),
188 FileNotFound(String),
190 InvalidFileFormat(String),
192 UploadFailed(String),
194 ScanFailed(String),
196 PreScanFailed(String),
198 BuildNotFound,
200 ApplicationNotFound,
202 SandboxNotFound,
204 Unauthorized,
206 PermissionDenied,
208 InvalidParameter(String),
210 FileTooLarge(String),
212 UploadInProgress,
214 ScanInProgress,
216 BuildCreationFailed(String),
218 ChunkedUploadFailed(String),
220}
221
222impl std::fmt::Display for ScanError {
223 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
224 match self {
225 ScanError::Api(err) => write!(f, "API error: {err}"),
226 ScanError::FileNotFound(path) => write!(f, "File not found: {path}"),
227 ScanError::InvalidFileFormat(msg) => write!(f, "Invalid file format: {msg}"),
228 ScanError::UploadFailed(msg) => write!(f, "Upload failed: {msg}"),
229 ScanError::ScanFailed(msg) => write!(f, "Scan failed: {msg}"),
230 ScanError::PreScanFailed(msg) => write!(f, "Pre-scan failed: {msg}"),
231 ScanError::BuildNotFound => write!(f, "Build not found"),
232 ScanError::ApplicationNotFound => write!(f, "Application not found"),
233 ScanError::SandboxNotFound => write!(f, "Sandbox not found"),
234 ScanError::Unauthorized => write!(f, "Unauthorized access"),
235 ScanError::PermissionDenied => write!(f, "Permission denied"),
236 ScanError::InvalidParameter(msg) => write!(f, "Invalid parameter: {msg}"),
237 ScanError::FileTooLarge(msg) => write!(f, "File too large: {msg}"),
238 ScanError::UploadInProgress => write!(f, "Upload or prescan already in progress"),
239 ScanError::ScanInProgress => write!(f, "Scan in progress, cannot upload"),
240 ScanError::BuildCreationFailed(msg) => write!(f, "Build creation failed: {msg}"),
241 ScanError::ChunkedUploadFailed(msg) => write!(f, "Chunked upload failed: {msg}"),
242 }
243 }
244}
245
246impl std::error::Error for ScanError {}
247
248impl From<VeracodeError> for ScanError {
249 fn from(err: VeracodeError) -> Self {
250 ScanError::Api(err)
251 }
252}
253
254impl From<reqwest::Error> for ScanError {
255 fn from(err: reqwest::Error) -> Self {
256 ScanError::Api(VeracodeError::Http(err))
257 }
258}
259
260impl From<serde_json::Error> for ScanError {
261 fn from(err: serde_json::Error) -> Self {
262 ScanError::Api(VeracodeError::Serialization(err))
263 }
264}
265
266impl From<std::io::Error> for ScanError {
267 fn from(err: std::io::Error) -> Self {
268 ScanError::FileNotFound(err.to_string())
269 }
270}
271
272pub struct ScanApi {
274 client: VeracodeClient,
275}
276
277impl ScanApi {
278 pub fn new(client: VeracodeClient) -> Self {
280 Self { client }
281 }
282
283 pub async fn upload_file(&self, request: UploadFileRequest) -> Result<UploadedFile, ScanError> {
293 if !Path::new(&request.file_path).exists() {
295 return Err(ScanError::FileNotFound(request.file_path));
296 }
297
298 let endpoint = "api/5.0/uploadfile.do";
299
300 let mut query_params = Vec::new();
302 query_params.push(("app_id", request.app_id.as_str()));
303
304 if let Some(sandbox_id) = &request.sandbox_id {
305 query_params.push(("sandbox_id", sandbox_id.as_str()));
306 }
307
308 if let Some(save_as) = &request.save_as {
309 query_params.push(("save_as", save_as.as_str()));
310 }
311
312 let file_data = std::fs::read(&request.file_path)?;
314
315 let filename = Path::new(&request.file_path)
317 .file_name()
318 .and_then(|f| f.to_str())
319 .unwrap_or("file");
320
321 let response = self.client.upload_file_with_query_params(endpoint, &query_params, "file", filename, file_data).await?;
322
323 let status = response.status().as_u16();
324 match status {
325 200 => {
326 let response_text = response.text().await?;
327 self.parse_upload_response(&response_text, &request.file_path)
328 }
329 400 => {
330 let error_text = response.text().await.unwrap_or_default();
331 Err(ScanError::InvalidParameter(error_text))
332 }
333 401 => Err(ScanError::Unauthorized),
334 403 => Err(ScanError::PermissionDenied),
335 404 => {
336 if request.sandbox_id.is_some() {
337 Err(ScanError::SandboxNotFound)
338 } else {
339 Err(ScanError::ApplicationNotFound)
340 }
341 }
342 _ => {
343 let error_text = response.text().await.unwrap_or_default();
344 Err(ScanError::UploadFailed(format!("HTTP {status}: {error_text}")))
345 }
346 }
347 }
348
349 pub async fn upload_large_file(&self, request: UploadLargeFileRequest) -> Result<UploadedFile, ScanError> {
363 if !Path::new(&request.file_path).exists() {
365 return Err(ScanError::FileNotFound(request.file_path));
366 }
367
368 let file_metadata = std::fs::metadata(&request.file_path)?;
370 let file_size = file_metadata.len();
371 const MAX_FILE_SIZE: u64 = 2 * 1024 * 1024 * 1024; if file_size > MAX_FILE_SIZE {
374 return Err(ScanError::FileTooLarge(
375 format!("File size {} bytes exceeds 2GB limit", file_size)
376 ));
377 }
378
379 let endpoint = "uploadlargefile.do"; let mut query_params = Vec::new();
383 query_params.push(("app_id", request.app_id.as_str()));
384
385 if let Some(sandbox_id) = &request.sandbox_id {
386 query_params.push(("sandbox_id", sandbox_id.as_str()));
387 }
388
389 if let Some(filename) = &request.filename {
390 query_params.push(("filename", filename.as_str()));
391 }
392
393 let file_data = std::fs::read(&request.file_path)?;
395
396 let response = self.client.upload_file_binary(
397 endpoint,
398 &query_params,
399 file_data,
400 "binary/octet-stream"
401 ).await?;
402
403 let status = response.status().as_u16();
404 match status {
405 200 => {
406 let response_text = response.text().await?;
407 self.parse_upload_response(&response_text, &request.file_path)
408 }
409 400 => {
410 let error_text = response.text().await.unwrap_or_default();
411 if error_text.contains("upload or prescan in progress") {
412 Err(ScanError::UploadInProgress)
413 } else if error_text.contains("scan in progress") {
414 Err(ScanError::ScanInProgress)
415 } else {
416 Err(ScanError::InvalidParameter(error_text))
417 }
418 }
419 401 => Err(ScanError::Unauthorized),
420 403 => Err(ScanError::PermissionDenied),
421 404 => {
422 if request.sandbox_id.is_some() {
423 Err(ScanError::SandboxNotFound)
424 } else {
425 Err(ScanError::ApplicationNotFound)
426 }
427 }
428 413 => Err(ScanError::FileTooLarge("File size exceeds server limits".to_string())),
429 _ => {
430 let error_text = response.text().await.unwrap_or_default();
431 Err(ScanError::UploadFailed(format!("HTTP {status}: {error_text}")))
432 }
433 }
434 }
435
436 pub async fn upload_large_file_with_progress<F>(
450 &self,
451 request: UploadLargeFileRequest,
452 progress_callback: F,
453 ) -> Result<UploadedFile, ScanError>
454 where
455 F: Fn(u64, u64, f64) + Send + Sync,
456 {
457 if !Path::new(&request.file_path).exists() {
459 return Err(ScanError::FileNotFound(request.file_path));
460 }
461
462 let file_metadata = std::fs::metadata(&request.file_path)?;
464 let file_size = file_metadata.len();
465 const MAX_FILE_SIZE: u64 = 2 * 1024 * 1024 * 1024; if file_size > MAX_FILE_SIZE {
468 return Err(ScanError::FileTooLarge(
469 format!("File size {} bytes exceeds 2GB limit", file_size)
470 ));
471 }
472
473 let endpoint = "uploadlargefile.do";
474
475 let mut query_params = Vec::new();
477 query_params.push(("app_id", request.app_id.as_str()));
478
479 if let Some(sandbox_id) = &request.sandbox_id {
480 query_params.push(("sandbox_id", sandbox_id.as_str()));
481 }
482
483 if let Some(filename) = &request.filename {
484 query_params.push(("filename", filename.as_str()));
485 }
486
487 let response = self.client.upload_large_file_chunked(
488 endpoint,
489 &query_params,
490 &request.file_path,
491 Some("binary/octet-stream"),
492 Some(progress_callback),
493 ).await?;
494
495 let status = response.status().as_u16();
496 match status {
497 200 => {
498 let response_text = response.text().await?;
499 self.parse_upload_response(&response_text, &request.file_path)
500 }
501 400 => {
502 let error_text = response.text().await.unwrap_or_default();
503 if error_text.contains("upload or prescan in progress") {
504 Err(ScanError::UploadInProgress)
505 } else if error_text.contains("scan in progress") {
506 Err(ScanError::ScanInProgress)
507 } else {
508 Err(ScanError::InvalidParameter(error_text))
509 }
510 }
511 401 => Err(ScanError::Unauthorized),
512 403 => Err(ScanError::PermissionDenied),
513 404 => {
514 if request.sandbox_id.is_some() {
515 Err(ScanError::SandboxNotFound)
516 } else {
517 Err(ScanError::ApplicationNotFound)
518 }
519 }
520 413 => Err(ScanError::FileTooLarge("File size exceeds server limits".to_string())),
521 _ => {
522 let error_text = response.text().await.unwrap_or_default();
523 Err(ScanError::ChunkedUploadFailed(format!("HTTP {status}: {error_text}")))
524 }
525 }
526 }
527
528 pub async fn upload_file_smart(&self, request: UploadFileRequest) -> Result<UploadedFile, ScanError> {
541 if !Path::new(&request.file_path).exists() {
543 return Err(ScanError::FileNotFound(request.file_path));
544 }
545
546 let file_metadata = std::fs::metadata(&request.file_path)?;
548 let file_size = file_metadata.len();
549
550 const LARGE_FILE_THRESHOLD: u64 = 100 * 1024 * 1024; if file_size > LARGE_FILE_THRESHOLD {
554 let large_request = UploadLargeFileRequest {
556 app_id: request.app_id.clone(),
557 file_path: request.file_path.clone(),
558 filename: request.save_as.clone(),
559 sandbox_id: request.sandbox_id.clone(),
560 };
561
562 match self.upload_large_file(large_request).await {
564 Ok(result) => Ok(result),
565 Err(ScanError::Api(_)) => {
566 self.upload_file(request).await
568 }
569 Err(e) => Err(e),
570 }
571 } else {
572 self.upload_file(request).await
574 }
575 }
576
577 pub async fn begin_prescan(&self, request: BeginPreScanRequest) -> Result<String, ScanError> {
587 let endpoint = "api/5.0/beginprescan.do";
588
589 let mut query_params = Vec::new();
591 query_params.push(("app_id", request.app_id.as_str()));
592
593 if let Some(sandbox_id) = &request.sandbox_id {
594 query_params.push(("sandbox_id", sandbox_id.as_str()));
595 }
596
597 if let Some(auto_scan) = request.auto_scan {
598 query_params.push(("auto_scan", if auto_scan { "true" } else { "false" }));
599 }
600
601 if let Some(scan_all) = request.scan_all_nonfatal_top_level_modules {
602 query_params.push(("scan_all_nonfatal_top_level_modules", if scan_all { "true" } else { "false" }));
603 }
604
605 if let Some(include_new) = request.include_new_modules {
606 query_params.push(("include_new_modules", if include_new { "true" } else { "false" }));
607 }
608
609 let response = self.client.get_with_params(endpoint, &query_params).await?;
610
611 let status = response.status().as_u16();
612 match status {
613 200 => {
614 let response_text = response.text().await?;
615 self.parse_build_id_response(&response_text)
616 }
617 400 => {
618 let error_text = response.text().await.unwrap_or_default();
619 Err(ScanError::InvalidParameter(error_text))
620 }
621 401 => Err(ScanError::Unauthorized),
622 403 => Err(ScanError::PermissionDenied),
623 404 => {
624 if request.sandbox_id.is_some() {
625 Err(ScanError::SandboxNotFound)
626 } else {
627 Err(ScanError::ApplicationNotFound)
628 }
629 }
630 _ => {
631 let error_text = response.text().await.unwrap_or_default();
632 Err(ScanError::PreScanFailed(format!("HTTP {status}: {error_text}")))
633 }
634 }
635 }
636
637 pub async fn get_prescan_results(
649 &self,
650 app_id: &str,
651 sandbox_id: Option<&str>,
652 build_id: Option<&str>,
653 ) -> Result<PreScanResults, ScanError> {
654 let endpoint = "api/5.0/getprescanresults.do";
655
656 let mut params = Vec::new();
657 params.push(("app_id", app_id));
658
659 if let Some(sandbox_id) = sandbox_id {
660 params.push(("sandbox_id", sandbox_id));
661 }
662
663 if let Some(build_id) = build_id {
664 params.push(("build_id", build_id));
665 }
666
667 let response = self.client.get_with_params(endpoint, ¶ms).await?;
668
669 let status = response.status().as_u16();
670 match status {
671 200 => {
672 let response_text = response.text().await?;
673 self.parse_prescan_results(&response_text, app_id, sandbox_id)
674 }
675 401 => Err(ScanError::Unauthorized),
676 403 => Err(ScanError::PermissionDenied),
677 404 => {
678 if sandbox_id.is_some() {
679 Err(ScanError::SandboxNotFound)
680 } else {
681 Err(ScanError::ApplicationNotFound)
682 }
683 }
684 _ => {
685 let error_text = response.text().await.unwrap_or_default();
686 Err(ScanError::PreScanFailed(format!("HTTP {status}: {error_text}")))
687 }
688 }
689 }
690
691 pub async fn begin_scan(&self, request: BeginScanRequest) -> Result<String, ScanError> {
701 let endpoint = "api/5.0/beginscan.do";
702
703 let mut query_params = Vec::new();
705 query_params.push(("app_id", request.app_id.as_str()));
706
707 if let Some(sandbox_id) = &request.sandbox_id {
708 query_params.push(("sandbox_id", sandbox_id.as_str()));
709 }
710
711 if let Some(modules) = &request.modules {
712 query_params.push(("modules", modules.as_str()));
713 }
714
715 if let Some(scan_all) = request.scan_all_top_level_modules {
716 query_params.push(("scan_all_top_level_modules", if scan_all { "true" } else { "false" }));
717 }
718
719 if let Some(scan_all_nonfatal) = request.scan_all_nonfatal_top_level_modules {
720 query_params.push(("scan_all_nonfatal_top_level_modules", if scan_all_nonfatal { "true" } else { "false" }));
721 }
722
723 if let Some(scan_previous) = request.scan_previously_selected_modules {
724 query_params.push(("scan_previously_selected_modules", if scan_previous { "true" } else { "false" }));
725 }
726
727 let response = self.client.get_with_params(endpoint, &query_params).await?;
728
729 let status = response.status().as_u16();
730 match status {
731 200 => {
732 let response_text = response.text().await?;
733 self.parse_build_id_response(&response_text)
734 }
735 400 => {
736 let error_text = response.text().await.unwrap_or_default();
737 Err(ScanError::InvalidParameter(error_text))
738 }
739 401 => Err(ScanError::Unauthorized),
740 403 => Err(ScanError::PermissionDenied),
741 404 => {
742 if request.sandbox_id.is_some() {
743 Err(ScanError::SandboxNotFound)
744 } else {
745 Err(ScanError::ApplicationNotFound)
746 }
747 }
748 _ => {
749 let error_text = response.text().await.unwrap_or_default();
750 Err(ScanError::ScanFailed(format!("HTTP {status}: {error_text}")))
751 }
752 }
753 }
754
755 pub async fn get_file_list(
767 &self,
768 app_id: &str,
769 sandbox_id: Option<&str>,
770 build_id: Option<&str>,
771 ) -> Result<Vec<UploadedFile>, ScanError> {
772 let endpoint = "api/5.0/getfilelist.do";
773
774 let mut params = Vec::new();
775 params.push(("app_id", app_id));
776
777 if let Some(sandbox_id) = sandbox_id {
778 params.push(("sandbox_id", sandbox_id));
779 }
780
781 if let Some(build_id) = build_id {
782 params.push(("build_id", build_id));
783 }
784
785 let response = self.client.get_with_params(endpoint, ¶ms).await?;
786
787 let status = response.status().as_u16();
788 match status {
789 200 => {
790 let response_text = response.text().await?;
791 self.parse_file_list(&response_text)
792 }
793 401 => Err(ScanError::Unauthorized),
794 403 => Err(ScanError::PermissionDenied),
795 404 => {
796 if sandbox_id.is_some() {
797 Err(ScanError::SandboxNotFound)
798 } else {
799 Err(ScanError::ApplicationNotFound)
800 }
801 }
802 _ => {
803 let error_text = response.text().await.unwrap_or_default();
804 Err(ScanError::Api(VeracodeError::InvalidResponse(
805 format!("HTTP {status}: {error_text}")
806 )))
807 }
808 }
809 }
810
811 pub async fn remove_file(
823 &self,
824 app_id: &str,
825 file_id: &str,
826 sandbox_id: Option<&str>,
827 ) -> Result<(), ScanError> {
828 let endpoint = "api/5.0/removefile.do";
829
830 let mut query_params = Vec::new();
832 query_params.push(("app_id", app_id));
833 query_params.push(("file_id", file_id));
834
835 if let Some(sandbox_id) = sandbox_id {
836 query_params.push(("sandbox_id", sandbox_id));
837 }
838
839 let response = self.client.get_with_params(endpoint, &query_params).await?;
840
841 let status = response.status().as_u16();
842 match status {
843 200 => Ok(()),
844 400 => {
845 let error_text = response.text().await.unwrap_or_default();
846 Err(ScanError::InvalidParameter(error_text))
847 }
848 401 => Err(ScanError::Unauthorized),
849 403 => Err(ScanError::PermissionDenied),
850 404 => Err(ScanError::FileNotFound(file_id.to_string())),
851 _ => {
852 let error_text = response.text().await.unwrap_or_default();
853 Err(ScanError::Api(VeracodeError::InvalidResponse(
854 format!("HTTP {status}: {error_text}")
855 )))
856 }
857 }
858 }
859
860 pub async fn delete_build(
874 &self,
875 app_id: &str,
876 build_id: &str,
877 sandbox_id: Option<&str>,
878 ) -> Result<(), ScanError> {
879 let endpoint = "api/5.0/deletebuild.do";
880
881 let mut query_params = Vec::new();
883 query_params.push(("app_id", app_id));
884 query_params.push(("build_id", build_id));
885
886 if let Some(sandbox_id) = sandbox_id {
887 query_params.push(("sandbox_id", sandbox_id));
888 }
889
890 let response = self.client.get_with_params(endpoint, &query_params).await?;
891
892 let status = response.status().as_u16();
893 match status {
894 200 => Ok(()),
895 400 => {
896 let error_text = response.text().await.unwrap_or_default();
897 Err(ScanError::InvalidParameter(error_text))
898 }
899 401 => Err(ScanError::Unauthorized),
900 403 => Err(ScanError::PermissionDenied),
901 404 => Err(ScanError::BuildNotFound),
902 _ => {
903 let error_text = response.text().await.unwrap_or_default();
904 Err(ScanError::Api(VeracodeError::InvalidResponse(
905 format!("HTTP {status}: {error_text}")
906 )))
907 }
908 }
909 }
910
911 pub async fn delete_all_builds(
925 &self,
926 app_id: &str,
927 sandbox_id: Option<&str>,
928 ) -> Result<(), ScanError> {
929 let build_info = self.get_build_info(app_id, None, sandbox_id).await?;
931
932 if !build_info.build_id.is_empty() && build_info.build_id != "unknown" {
933 println!(" 🗑️ Deleting build: {}", build_info.build_id);
934 self.delete_build(app_id, &build_info.build_id, sandbox_id).await?;
935 }
936
937 Ok(())
938 }
939
940 pub async fn get_build_info(
952 &self,
953 app_id: &str,
954 build_id: Option<&str>,
955 sandbox_id: Option<&str>,
956 ) -> Result<ScanInfo, ScanError> {
957 let endpoint = "api/5.0/getbuildinfo.do";
958
959 let mut params = Vec::new();
960 params.push(("app_id", app_id));
961
962 if let Some(build_id) = build_id {
963 params.push(("build_id", build_id));
964 }
965
966 if let Some(sandbox_id) = sandbox_id {
967 params.push(("sandbox_id", sandbox_id));
968 }
969
970 let response = self.client.get_with_params(endpoint, ¶ms).await?;
971
972 let status = response.status().as_u16();
973 match status {
974 200 => {
975 let response_text = response.text().await?;
976 self.parse_build_info(&response_text, app_id, sandbox_id)
977 }
978 401 => Err(ScanError::Unauthorized),
979 403 => Err(ScanError::PermissionDenied),
980 404 => Err(ScanError::BuildNotFound),
981 _ => {
982 let error_text = response.text().await.unwrap_or_default();
983 Err(ScanError::Api(VeracodeError::InvalidResponse(
984 format!("HTTP {status}: {error_text}")
985 )))
986 }
987 }
988 }
989
990 fn parse_upload_response(&self, xml: &str, file_path: &str) -> Result<UploadedFile, ScanError> {
993 let mut reader = Reader::from_str(xml);
994 reader.config_mut().trim_text(true);
995
996 let mut buf = Vec::new();
997 let mut file_id = None;
998 let mut file_status = "Unknown".to_string();
999 let mut _md5: Option<String> = None;
1000
1001 loop {
1002 match reader.read_event_into(&mut buf) {
1003 Ok(Event::Start(ref e)) => {
1004 if e.name().as_ref() == b"file" {
1005 for attr in e.attributes() {
1007 if let Ok(attr) = attr {
1008 if attr.key.as_ref() == b"file_id" {
1009 file_id = Some(String::from_utf8_lossy(&attr.value).to_string());
1010 }
1011 }
1012 }
1013 }
1014 }
1015 Ok(Event::Text(e)) => {
1016 let text = std::str::from_utf8(&e).unwrap_or_default();
1017 if text.contains("successfully uploaded") {
1019 file_status = "Uploaded".to_string();
1020 } else if text.contains("error") || text.contains("failed") {
1021 file_status = "Failed".to_string();
1022 }
1023 }
1024 Ok(Event::Eof) => break,
1025 Err(e) => {
1026 eprintln!("Error parsing XML: {e}");
1027 break;
1028 }
1029 _ => {}
1030 }
1031 buf.clear();
1032 }
1033
1034 let filename = Path::new(file_path)
1035 .file_name()
1036 .and_then(|f| f.to_str())
1037 .unwrap_or("file")
1038 .to_string();
1039
1040 Ok(UploadedFile {
1041 file_id: file_id.unwrap_or_else(|| format!("file_{}", chrono::Utc::now().timestamp())),
1042 file_name: filename,
1043 file_size: std::fs::metadata(file_path).map(|m| m.len()).unwrap_or(0),
1044 uploaded: Utc::now(),
1045 file_status,
1046 md5: None,
1047 })
1048 }
1049
1050 fn parse_build_id_response(&self, xml: &str) -> Result<String, ScanError> {
1051 let mut reader = Reader::from_str(xml);
1052 reader.config_mut().trim_text(true);
1053
1054 let mut buf = Vec::new();
1055 let mut build_id = None;
1056
1057 loop {
1058 match reader.read_event_into(&mut buf) {
1059 Ok(Event::Start(ref e)) => {
1060 match e.name().as_ref() {
1061 b"buildinfo" | b"build" => {
1062 for attr in e.attributes() {
1064 if let Ok(attr) = attr {
1065 if attr.key.as_ref() == b"build_id" {
1066 build_id = Some(String::from_utf8_lossy(&attr.value).to_string());
1067 }
1068 }
1069 }
1070 }
1071 _ => {}
1072 }
1073 }
1074 Ok(Event::Eof) => break,
1075 Err(e) => {
1076 eprintln!("Error parsing XML: {e}");
1077 break;
1078 }
1079 _ => {}
1080 }
1081 buf.clear();
1082 }
1083
1084 build_id.ok_or_else(|| ScanError::PreScanFailed("No build_id found in response".to_string()))
1085 }
1086
1087 fn parse_prescan_results(&self, xml: &str, app_id: &str, sandbox_id: Option<&str>) -> Result<PreScanResults, ScanError> {
1088 let mut reader = Reader::from_str(xml);
1089 reader.config_mut().trim_text(true);
1090
1091 let mut buf = Vec::new();
1092 let mut build_id = None;
1093 let mut status = "Unknown".to_string();
1094 let mut modules = Vec::new();
1095 let messages = Vec::new();
1096
1097 loop {
1098 match reader.read_event_into(&mut buf) {
1099 Ok(Event::Start(ref e)) => {
1100 match e.name().as_ref() {
1101 b"buildinfo" => {
1102 for attr in e.attributes() {
1103 if let Ok(attr) = attr {
1104 match attr.key.as_ref() {
1105 b"build_id" => {
1106 build_id = Some(String::from_utf8_lossy(&attr.value).to_string());
1107 }
1108 b"analysis_unit" => {
1109 status = String::from_utf8_lossy(&attr.value).to_string();
1110 }
1111 _ => {}
1112 }
1113 }
1114 }
1115 }
1116 b"module" => {
1117 let mut module = ScanModule {
1118 id: String::new(),
1119 name: String::new(),
1120 module_type: String::new(),
1121 is_fatal: false,
1122 selected: false,
1123 size: None,
1124 platform: None,
1125 };
1126
1127 for attr in e.attributes() {
1128 if let Ok(attr) = attr {
1129 match attr.key.as_ref() {
1130 b"id" => module.id = String::from_utf8_lossy(&attr.value).to_string(),
1131 b"name" => module.name = String::from_utf8_lossy(&attr.value).to_string(),
1132 b"type" => module.module_type = String::from_utf8_lossy(&attr.value).to_string(),
1133 b"isfatal" => module.is_fatal = attr.value.as_ref() == b"true",
1134 b"selected" => module.selected = attr.value.as_ref() == b"true",
1135 b"size" => {
1136 if let Ok(size_str) = String::from_utf8(attr.value.to_vec()) {
1137 module.size = size_str.parse().ok();
1138 }
1139 }
1140 b"platform" => module.platform = Some(String::from_utf8_lossy(&attr.value).to_string()),
1141 _ => {}
1142 }
1143 }
1144 }
1145 modules.push(module);
1146 }
1147 _ => {}
1148 }
1149 }
1150 Ok(Event::Eof) => break,
1151 Err(e) => {
1152 eprintln!("Error parsing XML: {e}");
1153 break;
1154 }
1155 _ => {}
1156 }
1157 buf.clear();
1158 }
1159
1160 Ok(PreScanResults {
1161 build_id: build_id.unwrap_or_else(|| "unknown".to_string()),
1162 app_id: app_id.to_string(),
1163 sandbox_id: sandbox_id.map(|s| s.to_string()),
1164 status,
1165 modules,
1166 messages,
1167 })
1168 }
1169
1170 fn parse_file_list(&self, xml: &str) -> Result<Vec<UploadedFile>, ScanError> {
1171 let mut reader = Reader::from_str(xml);
1172 reader.config_mut().trim_text(true);
1173
1174 let mut buf = Vec::new();
1175 let mut files = Vec::new();
1176
1177 loop {
1178 match reader.read_event_into(&mut buf) {
1179 Ok(Event::Start(ref e)) => {
1180 if e.name().as_ref() == b"file" {
1181 let mut file = UploadedFile {
1182 file_id: String::new(),
1183 file_name: String::new(),
1184 file_size: 0,
1185 uploaded: Utc::now(),
1186 file_status: "Unknown".to_string(),
1187 md5: None,
1188 };
1189
1190 for attr in e.attributes() {
1191 if let Ok(attr) = attr {
1192 match attr.key.as_ref() {
1193 b"file_id" => file.file_id = String::from_utf8_lossy(&attr.value).to_string(),
1194 b"file_name" => file.file_name = String::from_utf8_lossy(&attr.value).to_string(),
1195 b"file_size" => {
1196 if let Ok(size_str) = String::from_utf8(attr.value.to_vec()) {
1197 file.file_size = size_str.parse().unwrap_or(0);
1198 }
1199 }
1200 b"file_status" => file.file_status = String::from_utf8_lossy(&attr.value).to_string(),
1201 b"md5" => file.md5 = Some(String::from_utf8_lossy(&attr.value).to_string()),
1202 _ => {}
1203 }
1204 }
1205 }
1206 files.push(file);
1207 }
1208 }
1209 Ok(Event::Eof) => break,
1210 Err(e) => {
1211 eprintln!("Error parsing XML: {e}");
1212 break;
1213 }
1214 _ => {}
1215 }
1216 buf.clear();
1217 }
1218
1219 Ok(files)
1220 }
1221
1222 fn parse_build_info(&self, xml: &str, app_id: &str, sandbox_id: Option<&str>) -> Result<ScanInfo, ScanError> {
1223 let mut reader = Reader::from_str(xml);
1224 reader.config_mut().trim_text(true);
1225
1226 let mut buf = Vec::new();
1227 let mut scan_info = ScanInfo {
1228 build_id: String::new(),
1229 app_id: app_id.to_string(),
1230 sandbox_id: sandbox_id.map(|s| s.to_string()),
1231 status: "Unknown".to_string(),
1232 scan_type: "Static".to_string(),
1233 analysis_unit_id: None,
1234 scan_progress_percentage: None,
1235 scan_start: None,
1236 scan_complete: None,
1237 total_lines_of_code: None,
1238 };
1239
1240 loop {
1241 match reader.read_event_into(&mut buf) {
1242 Ok(Event::Start(ref e)) => {
1243 if e.name().as_ref() == b"buildinfo" {
1244 for attr in e.attributes() {
1245 if let Ok(attr) = attr {
1246 match attr.key.as_ref() {
1247 b"build_id" => scan_info.build_id = String::from_utf8_lossy(&attr.value).to_string(),
1248 b"analysis_unit" => scan_info.status = String::from_utf8_lossy(&attr.value).to_string(),
1249 b"analysis_unit_id" => scan_info.analysis_unit_id = Some(String::from_utf8_lossy(&attr.value).to_string()),
1250 b"scan_progress_percentage" => {
1251 if let Ok(progress_str) = String::from_utf8(attr.value.to_vec()) {
1252 scan_info.scan_progress_percentage = progress_str.parse().ok();
1253 }
1254 }
1255 b"total_lines_of_code" => {
1256 if let Ok(lines_str) = String::from_utf8(attr.value.to_vec()) {
1257 scan_info.total_lines_of_code = lines_str.parse().ok();
1258 }
1259 }
1260 _ => {}
1261 }
1262 }
1263 }
1264 }
1265 }
1266 Ok(Event::Eof) => break,
1267 Err(e) => {
1268 eprintln!("Error parsing XML: {e}");
1269 break;
1270 }
1271 _ => {}
1272 }
1273 buf.clear();
1274 }
1275
1276 Ok(scan_info)
1277 }
1278}
1279
1280impl ScanApi {
1282 pub async fn upload_file_to_sandbox(
1294 &self,
1295 app_id: &str,
1296 file_path: &str,
1297 sandbox_id: &str,
1298 ) -> Result<UploadedFile, ScanError> {
1299 let request = UploadFileRequest {
1300 app_id: app_id.to_string(),
1301 file_path: file_path.to_string(),
1302 save_as: None,
1303 sandbox_id: Some(sandbox_id.to_string()),
1304 };
1305
1306 self.upload_file(request).await
1307 }
1308
1309 pub async fn upload_file_to_app(
1320 &self,
1321 app_id: &str,
1322 file_path: &str,
1323 ) -> Result<UploadedFile, ScanError> {
1324 let request = UploadFileRequest {
1325 app_id: app_id.to_string(),
1326 file_path: file_path.to_string(),
1327 save_as: None,
1328 sandbox_id: None,
1329 };
1330
1331 self.upload_file(request).await
1332 }
1333
1334 pub async fn upload_large_file_to_sandbox(
1347 &self,
1348 app_id: &str,
1349 file_path: &str,
1350 sandbox_id: &str,
1351 filename: Option<&str>,
1352 ) -> Result<UploadedFile, ScanError> {
1353 let request = UploadLargeFileRequest {
1354 app_id: app_id.to_string(),
1355 file_path: file_path.to_string(),
1356 filename: filename.map(|s| s.to_string()),
1357 sandbox_id: Some(sandbox_id.to_string()),
1358 };
1359
1360 self.upload_large_file(request).await
1361 }
1362
1363 pub async fn upload_large_file_to_app(
1375 &self,
1376 app_id: &str,
1377 file_path: &str,
1378 filename: Option<&str>,
1379 ) -> Result<UploadedFile, ScanError> {
1380 let request = UploadLargeFileRequest {
1381 app_id: app_id.to_string(),
1382 file_path: file_path.to_string(),
1383 filename: filename.map(|s| s.to_string()),
1384 sandbox_id: None,
1385 };
1386
1387 self.upload_large_file(request).await
1388 }
1389
1390 pub async fn upload_large_file_to_sandbox_with_progress<F>(
1404 &self,
1405 app_id: &str,
1406 file_path: &str,
1407 sandbox_id: &str,
1408 filename: Option<&str>,
1409 progress_callback: F,
1410 ) -> Result<UploadedFile, ScanError>
1411 where
1412 F: Fn(u64, u64, f64) + Send + Sync,
1413 {
1414 let request = UploadLargeFileRequest {
1415 app_id: app_id.to_string(),
1416 file_path: file_path.to_string(),
1417 filename: filename.map(|s| s.to_string()),
1418 sandbox_id: Some(sandbox_id.to_string()),
1419 };
1420
1421 self.upload_large_file_with_progress(request, progress_callback).await
1422 }
1423
1424 pub async fn begin_sandbox_prescan(
1435 &self,
1436 app_id: &str,
1437 sandbox_id: &str,
1438 ) -> Result<String, ScanError> {
1439 let request = BeginPreScanRequest {
1440 app_id: app_id.to_string(),
1441 sandbox_id: Some(sandbox_id.to_string()),
1442 auto_scan: Some(true),
1443 scan_all_nonfatal_top_level_modules: Some(true),
1444 include_new_modules: Some(true),
1445 };
1446
1447 self.begin_prescan(request).await
1448 }
1449
1450 pub async fn begin_sandbox_scan_all_modules(
1461 &self,
1462 app_id: &str,
1463 sandbox_id: &str,
1464 ) -> Result<String, ScanError> {
1465 let request = BeginScanRequest {
1466 app_id: app_id.to_string(),
1467 sandbox_id: Some(sandbox_id.to_string()),
1468 modules: None,
1469 scan_all_top_level_modules: Some(true),
1470 scan_all_nonfatal_top_level_modules: Some(true),
1471 scan_previously_selected_modules: None,
1472 };
1473
1474 self.begin_scan(request).await
1475 }
1476
1477 pub async fn upload_and_scan_sandbox(
1489 &self,
1490 app_id: &str,
1491 sandbox_id: &str,
1492 file_path: &str,
1493 ) -> Result<String, ScanError> {
1494 println!("📤 Uploading file to sandbox...");
1496 self.upload_file_to_sandbox(app_id, file_path, sandbox_id).await?;
1497
1498 println!("🔍 Beginning pre-scan...");
1500 let _build_id = self.begin_sandbox_prescan(app_id, sandbox_id).await?;
1501
1502 tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
1504
1505 println!("🚀 Beginning scan...");
1507 let scan_build_id = self.begin_sandbox_scan_all_modules(app_id, sandbox_id).await?;
1508
1509 Ok(scan_build_id)
1510 }
1511
1512 pub async fn delete_sandbox_build(
1524 &self,
1525 app_id: &str,
1526 build_id: &str,
1527 sandbox_id: &str,
1528 ) -> Result<(), ScanError> {
1529 self.delete_build(app_id, build_id, Some(sandbox_id)).await
1530 }
1531
1532 pub async fn delete_all_sandbox_builds(
1543 &self,
1544 app_id: &str,
1545 sandbox_id: &str,
1546 ) -> Result<(), ScanError> {
1547 self.delete_all_builds(app_id, Some(sandbox_id)).await
1548 }
1549
1550 pub async fn delete_app_build(
1561 &self,
1562 app_id: &str,
1563 build_id: &str,
1564 ) -> Result<(), ScanError> {
1565 self.delete_build(app_id, build_id, None).await
1566 }
1567
1568 pub async fn delete_all_app_builds(
1578 &self,
1579 app_id: &str,
1580 ) -> Result<(), ScanError> {
1581 self.delete_all_builds(app_id, None).await
1582 }
1583}
1584
1585#[cfg(test)]
1586mod tests {
1587 use super::*;
1588 use crate::VeracodeConfig;
1589
1590 #[test]
1591 fn test_upload_file_request() {
1592 let request = UploadFileRequest {
1593 app_id: "123".to_string(),
1594 file_path: "/path/to/file.jar".to_string(),
1595 save_as: Some("app.jar".to_string()),
1596 sandbox_id: Some("456".to_string()),
1597 };
1598
1599 assert_eq!(request.app_id, "123");
1600 assert_eq!(request.sandbox_id, Some("456".to_string()));
1601 }
1602
1603 #[test]
1604 fn test_begin_prescan_request() {
1605 let request = BeginPreScanRequest {
1606 app_id: "123".to_string(),
1607 sandbox_id: Some("456".to_string()),
1608 auto_scan: Some(true),
1609 scan_all_nonfatal_top_level_modules: Some(true),
1610 include_new_modules: Some(false),
1611 };
1612
1613 assert_eq!(request.app_id, "123");
1614 assert_eq!(request.auto_scan, Some(true));
1615 }
1616
1617 #[test]
1618 fn test_scan_error_display() {
1619 let error = ScanError::FileNotFound("test.jar".to_string());
1620 assert_eq!(error.to_string(), "File not found: test.jar");
1621
1622 let error = ScanError::UploadFailed("Network error".to_string());
1623 assert_eq!(error.to_string(), "Upload failed: Network error");
1624
1625 let error = ScanError::Unauthorized;
1626 assert_eq!(error.to_string(), "Unauthorized access");
1627
1628 let error = ScanError::BuildNotFound;
1629 assert_eq!(error.to_string(), "Build not found");
1630 }
1631
1632 #[test]
1633 fn test_delete_build_request_structure() {
1634 use crate::{VeracodeConfig, VeracodeClient};
1638
1639 async fn _test_delete_methods() -> Result<(), Box<dyn std::error::Error>> {
1640 let config = VeracodeConfig::new("test".to_string(), "test".to_string());
1641 let client = VeracodeClient::new(config)?;
1642 let api = client.scan_api();
1643
1644 let _: Result<(), _> = api.delete_build("app_id", "build_id", Some("sandbox_id")).await;
1647 let _: Result<(), _> = api.delete_all_builds("app_id", Some("sandbox_id")).await;
1648 let _: Result<(), _> = api.delete_sandbox_build("app_id", "build_id", "sandbox_id").await;
1649 let _: Result<(), _> = api.delete_all_sandbox_builds("app_id", "sandbox_id").await;
1650
1651 Ok(())
1652 }
1653
1654 assert!(true);
1656 }
1657
1658 #[test]
1659 fn test_upload_large_file_request() {
1660 let request = UploadLargeFileRequest {
1661 app_id: "123".to_string(),
1662 file_path: "/path/to/large_file.jar".to_string(),
1663 filename: Some("custom_name.jar".to_string()),
1664 sandbox_id: Some("456".to_string()),
1665 };
1666
1667 assert_eq!(request.app_id, "123");
1668 assert_eq!(request.filename, Some("custom_name.jar".to_string()));
1669 assert_eq!(request.sandbox_id, Some("456".to_string()));
1670 }
1671
1672 #[test]
1673 fn test_upload_progress() {
1674 let progress = UploadProgress {
1675 bytes_uploaded: 1024,
1676 total_bytes: 2048,
1677 percentage: 50.0,
1678 };
1679
1680 assert_eq!(progress.bytes_uploaded, 1024);
1681 assert_eq!(progress.total_bytes, 2048);
1682 assert_eq!(progress.percentage, 50.0);
1683 }
1684
1685 #[test]
1686 fn test_large_file_scan_error_display() {
1687 let error = ScanError::FileTooLarge("File exceeds 2GB".to_string());
1688 assert_eq!(error.to_string(), "File too large: File exceeds 2GB");
1689
1690 let error = ScanError::UploadInProgress;
1691 assert_eq!(error.to_string(), "Upload or prescan already in progress");
1692
1693 let error = ScanError::ScanInProgress;
1694 assert_eq!(error.to_string(), "Scan in progress, cannot upload");
1695
1696 let error = ScanError::ChunkedUploadFailed("Network error".to_string());
1697 assert_eq!(error.to_string(), "Chunked upload failed: Network error");
1698 }
1699
1700 #[tokio::test]
1701 async fn test_large_file_upload_method_signatures() {
1702 async fn _test_large_file_methods() -> Result<(), Box<dyn std::error::Error>> {
1703 let config = VeracodeConfig::new("test".to_string(), "test".to_string());
1704 let client = VeracodeClient::new(config)?;
1705 let api = client.scan_api();
1706
1707 let request = UploadLargeFileRequest {
1709 app_id: "123".to_string(),
1710 file_path: "/nonexistent/file.jar".to_string(),
1711 filename: None,
1712 sandbox_id: Some("456".to_string()),
1713 };
1714
1715 let _: Result<UploadedFile, _> = api.upload_large_file(request.clone()).await;
1718 let _: Result<UploadedFile, _> = api.upload_large_file_to_sandbox("123", "/path", "456", None).await;
1719 let _: Result<UploadedFile, _> = api.upload_large_file_to_app("123", "/path", None).await;
1720
1721 let progress_callback = |bytes_uploaded: u64, total_bytes: u64, percentage: f64| {
1723 println!("Progress: {}/{} ({:.1}%)", bytes_uploaded, total_bytes, percentage);
1724 };
1725 let _: Result<UploadedFile, _> = api.upload_large_file_with_progress(request, progress_callback).await;
1726
1727 Ok(())
1728 }
1729
1730 assert!(true);
1732 }
1733}