1use chrono::{DateTime, Utc};
8#[allow(unused_imports)] use 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
17fn attr_to_string(value: &[u8]) -> String {
20 String::from_utf8_lossy(value).into_owned()
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct UploadedFile {
26 pub file_id: String,
28 pub file_name: String,
30 pub file_size: u64,
32 pub uploaded: DateTime<Utc>,
34 pub file_status: String,
36 pub md5: Option<String>,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct PreScanResults {
43 pub build_id: String,
45 pub app_id: String,
47 pub sandbox_id: Option<String>,
49 pub status: String,
51 pub modules: Vec<ScanModule>,
53 pub messages: Vec<PreScanMessage>,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct ScanModule {
60 pub id: String,
62 pub name: String,
64 pub module_type: String,
66 pub is_fatal: bool,
68 pub selected: bool,
70 pub size: Option<u64>,
72 pub platform: Option<String>,
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct PreScanMessage {
79 pub severity: String,
81 pub text: String,
83 pub module_name: Option<String>,
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct ScanInfo {
90 pub build_id: String,
92 pub app_id: String,
94 pub sandbox_id: Option<String>,
96 pub status: String,
98 pub scan_type: String,
100 pub analysis_unit_id: Option<String>,
102 pub scan_progress_percentage: Option<u32>,
104 pub scan_start: Option<DateTime<Utc>>,
106 pub scan_complete: Option<DateTime<Utc>>,
108 pub total_lines_of_code: Option<u64>,
110}
111
112#[derive(Debug, Clone)]
114pub struct UploadFileRequest {
115 pub app_id: String,
117 pub file_path: String,
119 pub save_as: Option<String>,
121 pub sandbox_id: Option<String>,
123}
124
125#[derive(Debug, Clone)]
127pub struct UploadLargeFileRequest {
128 pub app_id: String,
130 pub file_path: String,
132 pub filename: Option<String>,
134 pub sandbox_id: Option<String>,
136}
137
138#[derive(Debug, Clone)]
140pub struct UploadProgress {
141 pub bytes_uploaded: u64,
143 pub total_bytes: u64,
145 pub percentage: f64,
147}
148
149pub trait UploadProgressCallback: Send + Sync {
151 fn on_progress(&self, progress: UploadProgress);
153 fn on_completed(&self);
155 fn on_error(&self, error: &str);
157}
158
159#[derive(Debug, Clone)]
161pub struct BeginPreScanRequest {
162 pub app_id: String,
164 pub sandbox_id: Option<String>,
166 pub auto_scan: Option<bool>,
168 pub scan_all_nonfatal_top_level_modules: Option<bool>,
170 pub include_new_modules: Option<bool>,
172}
173
174#[derive(Debug, Clone)]
176pub struct BeginScanRequest {
177 pub app_id: String,
179 pub sandbox_id: Option<String>,
181 pub modules: Option<String>,
183 pub scan_all_top_level_modules: Option<bool>,
185 pub scan_all_nonfatal_top_level_modules: Option<bool>,
187 pub scan_previously_selected_modules: Option<bool>,
189}
190
191impl 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#[derive(Debug)]
205pub enum ScanError {
206 Api(VeracodeError),
208 FileNotFound(String),
210 InvalidFileFormat(String),
212 UploadFailed(String),
214 ScanFailed(String),
216 PreScanFailed(String),
218 BuildNotFound,
220 ApplicationNotFound,
222 SandboxNotFound,
224 Unauthorized,
226 PermissionDenied,
228 InvalidParameter(String),
230 FileTooLarge(String),
232 UploadInProgress,
234 ScanInProgress,
236 BuildCreationFailed(String),
238 ChunkedUploadFailed(String),
240}
241
242impl std::fmt::Display for ScanError {
243 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
244 match self {
245 ScanError::Api(err) => write!(f, "API error: {err}"),
246 ScanError::FileNotFound(path) => write!(f, "File not found: {path}"),
247 ScanError::InvalidFileFormat(msg) => write!(f, "Invalid file format: {msg}"),
248 ScanError::UploadFailed(msg) => write!(f, "Upload failed: {msg}"),
249 ScanError::ScanFailed(msg) => write!(f, "Scan failed: {msg}"),
250 ScanError::PreScanFailed(msg) => write!(f, "Pre-scan failed: {msg}"),
251 ScanError::BuildNotFound => write!(f, "Build not found"),
252 ScanError::ApplicationNotFound => write!(f, "Application not found"),
253 ScanError::SandboxNotFound => write!(f, "Sandbox not found"),
254 ScanError::Unauthorized => write!(f, "Unauthorized access"),
255 ScanError::PermissionDenied => write!(f, "Permission denied"),
256 ScanError::InvalidParameter(msg) => write!(f, "Invalid parameter: {msg}"),
257 ScanError::FileTooLarge(msg) => write!(f, "File too large: {msg}"),
258 ScanError::UploadInProgress => write!(f, "Upload or prescan already in progress"),
259 ScanError::ScanInProgress => write!(f, "Scan in progress, cannot upload"),
260 ScanError::BuildCreationFailed(msg) => write!(f, "Build creation failed: {msg}"),
261 ScanError::ChunkedUploadFailed(msg) => write!(f, "Chunked upload failed: {msg}"),
262 }
263 }
264}
265
266impl std::error::Error for ScanError {}
267
268impl From<VeracodeError> for ScanError {
269 fn from(err: VeracodeError) -> Self {
270 ScanError::Api(err)
271 }
272}
273
274impl From<reqwest::Error> for ScanError {
275 fn from(err: reqwest::Error) -> Self {
276 ScanError::Api(VeracodeError::Http(err))
277 }
278}
279
280impl From<serde_json::Error> for ScanError {
281 fn from(err: serde_json::Error) -> Self {
282 ScanError::Api(VeracodeError::Serialization(err))
283 }
284}
285
286impl From<std::io::Error> for ScanError {
287 fn from(err: std::io::Error) -> Self {
288 ScanError::FileNotFound(err.to_string())
289 }
290}
291
292pub struct ScanApi {
294 client: VeracodeClient,
295}
296
297impl ScanApi {
298 #[must_use]
300 pub fn new(client: VeracodeClient) -> Self {
301 Self { client }
302 }
303
304 pub async fn upload_file(
314 &self,
315 request: &UploadFileRequest,
316 ) -> Result<UploadedFile, ScanError> {
317 if !Path::new(&request.file_path).exists() {
319 return Err(ScanError::FileNotFound(request.file_path.clone()));
320 }
321
322 let endpoint = "/api/5.0/uploadfile.do";
323
324 let mut query_params = Vec::new();
326 query_params.push(("app_id", request.app_id.as_str()));
327
328 if let Some(sandbox_id) = &request.sandbox_id {
329 query_params.push(("sandbox_id", sandbox_id.as_str()));
330 }
331
332 if let Some(save_as) = &request.save_as {
333 query_params.push(("save_as", save_as.as_str()));
334 }
335
336 let file_data = tokio::fs::read(&request.file_path).await?;
338
339 let filename = Path::new(&request.file_path)
341 .file_name()
342 .and_then(|f| f.to_str())
343 .unwrap_or("file");
344
345 let response = self
346 .client
347 .upload_file_with_query_params(endpoint, &query_params, "file", filename, file_data)
348 .await?;
349
350 let status = response.status().as_u16();
351 match status {
352 200 => {
353 let response_text = response.text().await?;
354 self.parse_upload_response(&response_text, &request.file_path)
355 .await
356 }
357 400 => {
358 let error_text = response.text().await.unwrap_or_default();
359 Err(ScanError::InvalidParameter(error_text))
360 }
361 401 => Err(ScanError::Unauthorized),
362 403 => Err(ScanError::PermissionDenied),
363 404 => {
364 if request.sandbox_id.is_some() {
365 Err(ScanError::SandboxNotFound)
366 } else {
367 Err(ScanError::ApplicationNotFound)
368 }
369 }
370 _ => {
371 let error_text = response.text().await.unwrap_or_default();
372 Err(ScanError::UploadFailed(format!(
373 "HTTP {status}: {error_text}"
374 )))
375 }
376 }
377 }
378
379 pub async fn upload_large_file(
393 &self,
394 request: UploadLargeFileRequest,
395 ) -> Result<UploadedFile, ScanError> {
396 if !Path::new(&request.file_path).exists() {
398 return Err(ScanError::FileNotFound(request.file_path));
399 }
400
401 let file_metadata = tokio::fs::metadata(&request.file_path).await?;
403 let file_size = file_metadata.len();
404 const MAX_FILE_SIZE: u64 = 2 * 1024 * 1024 * 1024; if file_size > MAX_FILE_SIZE {
407 return Err(ScanError::FileTooLarge(format!(
408 "File size {file_size} bytes exceeds 2GB limit"
409 )));
410 }
411
412 let endpoint = "uploadlargefile.do"; let mut query_params = Vec::new();
416 query_params.push(("app_id", request.app_id.as_str()));
417
418 if let Some(sandbox_id) = &request.sandbox_id {
419 query_params.push(("sandbox_id", sandbox_id.as_str()));
420 }
421
422 if let Some(filename) = &request.filename {
423 query_params.push(("filename", filename.as_str()));
424 }
425
426 let file_data = tokio::fs::read(&request.file_path).await?;
428
429 let response = self
430 .client
431 .upload_file_binary(endpoint, &query_params, file_data, "binary/octet-stream")
432 .await?;
433
434 let status = response.status().as_u16();
435 match status {
436 200 => {
437 let response_text = response.text().await?;
438 self.parse_upload_response(&response_text, &request.file_path)
439 .await
440 }
441 400 => {
442 let error_text = response.text().await.unwrap_or_default();
443 if error_text.contains("upload or prescan in progress") {
444 Err(ScanError::UploadInProgress)
445 } else if error_text.contains("scan in progress") {
446 Err(ScanError::ScanInProgress)
447 } else {
448 Err(ScanError::InvalidParameter(error_text))
449 }
450 }
451 401 => Err(ScanError::Unauthorized),
452 403 => Err(ScanError::PermissionDenied),
453 404 => {
454 if request.sandbox_id.is_some() {
455 Err(ScanError::SandboxNotFound)
456 } else {
457 Err(ScanError::ApplicationNotFound)
458 }
459 }
460 413 => Err(ScanError::FileTooLarge(
461 "File size exceeds server limits".to_string(),
462 )),
463 _ => {
464 let error_text = response.text().await.unwrap_or_default();
465 Err(ScanError::UploadFailed(format!(
466 "HTTP {status}: {error_text}"
467 )))
468 }
469 }
470 }
471
472 pub async fn upload_large_file_with_progress<F>(
486 &self,
487 request: UploadLargeFileRequest,
488 progress_callback: F,
489 ) -> Result<UploadedFile, ScanError>
490 where
491 F: Fn(u64, u64, f64) + Send + Sync,
492 {
493 if !Path::new(&request.file_path).exists() {
495 return Err(ScanError::FileNotFound(request.file_path));
496 }
497
498 let file_metadata = tokio::fs::metadata(&request.file_path).await?;
500 let file_size = file_metadata.len();
501 const MAX_FILE_SIZE: u64 = 2 * 1024 * 1024 * 1024; if file_size > MAX_FILE_SIZE {
504 return Err(ScanError::FileTooLarge(format!(
505 "File size {file_size} bytes exceeds 2GB limit"
506 )));
507 }
508
509 let endpoint = "uploadlargefile.do";
510
511 let mut query_params = Vec::new();
513 query_params.push(("app_id", request.app_id.as_str()));
514
515 if let Some(sandbox_id) = &request.sandbox_id {
516 query_params.push(("sandbox_id", sandbox_id.as_str()));
517 }
518
519 if let Some(filename) = &request.filename {
520 query_params.push(("filename", filename.as_str()));
521 }
522
523 let response = self
524 .client
525 .upload_large_file_chunked(
526 endpoint,
527 &query_params,
528 &request.file_path,
529 Some("binary/octet-stream"),
530 Some(progress_callback),
531 )
532 .await?;
533
534 let status = response.status().as_u16();
535 match status {
536 200 => {
537 let response_text = response.text().await?;
538 self.parse_upload_response(&response_text, &request.file_path)
539 .await
540 }
541 400 => {
542 let error_text = response.text().await.unwrap_or_default();
543 if error_text.contains("upload or prescan in progress") {
544 Err(ScanError::UploadInProgress)
545 } else if error_text.contains("scan in progress") {
546 Err(ScanError::ScanInProgress)
547 } else {
548 Err(ScanError::InvalidParameter(error_text))
549 }
550 }
551 401 => Err(ScanError::Unauthorized),
552 403 => Err(ScanError::PermissionDenied),
553 404 => {
554 if request.sandbox_id.is_some() {
555 Err(ScanError::SandboxNotFound)
556 } else {
557 Err(ScanError::ApplicationNotFound)
558 }
559 }
560 413 => Err(ScanError::FileTooLarge(
561 "File size exceeds server limits".to_string(),
562 )),
563 _ => {
564 let error_text = response.text().await.unwrap_or_default();
565 Err(ScanError::ChunkedUploadFailed(format!(
566 "HTTP {status}: {error_text}"
567 )))
568 }
569 }
570 }
571
572 pub async fn upload_file_smart(
585 &self,
586 request: &UploadFileRequest,
587 ) -> Result<UploadedFile, ScanError> {
588 if !Path::new(&request.file_path).exists() {
590 return Err(ScanError::FileNotFound(request.file_path.clone()));
591 }
592
593 let file_metadata = tokio::fs::metadata(&request.file_path).await?;
595 let file_size = file_metadata.len();
596
597 const LARGE_FILE_THRESHOLD: u64 = 100 * 1024 * 1024; if file_size > LARGE_FILE_THRESHOLD {
601 let large_request = UploadLargeFileRequest::from(request);
603
604 match self.upload_large_file(large_request).await {
606 Ok(result) => Ok(result),
607 Err(ScanError::Api(_)) => {
608 self.upload_file(request).await
610 }
611 Err(e) => Err(e),
612 }
613 } else {
614 self.upload_file(request).await
616 }
617 }
618
619 pub async fn begin_prescan(&self, request: &BeginPreScanRequest) -> Result<(), ScanError> {
629 let endpoint = "/api/5.0/beginprescan.do";
630
631 let mut query_params = Vec::new();
633 query_params.push(("app_id", request.app_id.as_str()));
634
635 if let Some(sandbox_id) = &request.sandbox_id {
636 query_params.push(("sandbox_id", sandbox_id.as_str()));
637 }
638
639 if let Some(auto_scan) = request.auto_scan {
640 query_params.push(("auto_scan", if auto_scan { "true" } else { "false" }));
641 }
642
643 if let Some(scan_all) = request.scan_all_nonfatal_top_level_modules {
644 query_params.push((
645 "scan_all_nonfatal_top_level_modules",
646 if scan_all { "true" } else { "false" },
647 ));
648 }
649
650 if let Some(include_new) = request.include_new_modules {
651 query_params.push((
652 "include_new_modules",
653 if include_new { "true" } else { "false" },
654 ));
655 }
656
657 let response = self.client.get_with_params(endpoint, &query_params).await?;
658
659 let status = response.status().as_u16();
660 match status {
661 200 => {
662 let response_text = response.text().await?;
663 self.validate_scan_response(&response_text)?;
666 Ok(())
667 }
668 400 => {
669 let error_text = response.text().await.unwrap_or_default();
670 Err(ScanError::InvalidParameter(error_text))
671 }
672 401 => Err(ScanError::Unauthorized),
673 403 => Err(ScanError::PermissionDenied),
674 404 => {
675 if request.sandbox_id.is_some() {
676 Err(ScanError::SandboxNotFound)
677 } else {
678 Err(ScanError::ApplicationNotFound)
679 }
680 }
681 _ => {
682 let error_text = response.text().await.unwrap_or_default();
683 Err(ScanError::PreScanFailed(format!(
684 "HTTP {status}: {error_text}"
685 )))
686 }
687 }
688 }
689
690 pub async fn get_prescan_results(
702 &self,
703 app_id: &str,
704 sandbox_id: Option<&str>,
705 build_id: Option<&str>,
706 ) -> Result<PreScanResults, ScanError> {
707 let endpoint = "/api/5.0/getprescanresults.do";
708
709 let mut params = Vec::new();
710 params.push(("app_id", app_id));
711
712 if let Some(sandbox_id) = sandbox_id {
713 params.push(("sandbox_id", sandbox_id));
714 }
715
716 if let Some(build_id) = build_id {
717 params.push(("build_id", build_id));
718 }
719
720 let response = self.client.get_with_params(endpoint, ¶ms).await?;
721
722 let status = response.status().as_u16();
723 match status {
724 200 => {
725 let response_text = response.text().await?;
726 self.parse_prescan_results(&response_text, app_id, sandbox_id)
727 }
728 401 => Err(ScanError::Unauthorized),
729 403 => Err(ScanError::PermissionDenied),
730 404 => {
731 if sandbox_id.is_some() {
732 Err(ScanError::SandboxNotFound)
733 } else {
734 Err(ScanError::ApplicationNotFound)
735 }
736 }
737 _ => {
738 let error_text = response.text().await.unwrap_or_default();
739 Err(ScanError::PreScanFailed(format!(
740 "HTTP {status}: {error_text}"
741 )))
742 }
743 }
744 }
745
746 pub async fn begin_scan(&self, request: &BeginScanRequest) -> Result<(), ScanError> {
756 let endpoint = "/api/5.0/beginscan.do";
757
758 let mut query_params = Vec::new();
760 query_params.push(("app_id", request.app_id.as_str()));
761
762 if let Some(sandbox_id) = &request.sandbox_id {
763 query_params.push(("sandbox_id", sandbox_id.as_str()));
764 }
765
766 if let Some(modules) = &request.modules {
767 query_params.push(("modules", modules.as_str()));
768 }
769
770 if let Some(scan_all) = request.scan_all_top_level_modules {
771 query_params.push((
772 "scan_all_top_level_modules",
773 if scan_all { "true" } else { "false" },
774 ));
775 }
776
777 if let Some(scan_all_nonfatal) = request.scan_all_nonfatal_top_level_modules {
778 query_params.push((
779 "scan_all_nonfatal_top_level_modules",
780 if scan_all_nonfatal { "true" } else { "false" },
781 ));
782 }
783
784 if let Some(scan_previous) = request.scan_previously_selected_modules {
785 query_params.push((
786 "scan_previously_selected_modules",
787 if scan_previous { "true" } else { "false" },
788 ));
789 }
790
791 let response = self.client.get_with_params(endpoint, &query_params).await?;
792
793 let status = response.status().as_u16();
794 match status {
795 200 => {
796 let response_text = response.text().await?;
797 self.validate_scan_response(&response_text)?;
800 Ok(())
801 }
802 400 => {
803 let error_text = response.text().await.unwrap_or_default();
804 Err(ScanError::InvalidParameter(error_text))
805 }
806 401 => Err(ScanError::Unauthorized),
807 403 => Err(ScanError::PermissionDenied),
808 404 => {
809 if request.sandbox_id.is_some() {
810 Err(ScanError::SandboxNotFound)
811 } else {
812 Err(ScanError::ApplicationNotFound)
813 }
814 }
815 _ => {
816 let error_text = response.text().await.unwrap_or_default();
817 Err(ScanError::ScanFailed(format!(
818 "HTTP {status}: {error_text}"
819 )))
820 }
821 }
822 }
823
824 pub async fn get_file_list(
836 &self,
837 app_id: &str,
838 sandbox_id: Option<&str>,
839 build_id: Option<&str>,
840 ) -> Result<Vec<UploadedFile>, ScanError> {
841 let endpoint = "/api/5.0/getfilelist.do";
842
843 let mut params = Vec::new();
844 params.push(("app_id", app_id));
845
846 if let Some(sandbox_id) = sandbox_id {
847 params.push(("sandbox_id", sandbox_id));
848 }
849
850 if let Some(build_id) = build_id {
851 params.push(("build_id", build_id));
852 }
853
854 let response = self.client.get_with_params(endpoint, ¶ms).await?;
855
856 let status = response.status().as_u16();
857 match status {
858 200 => {
859 let response_text = response.text().await?;
860 self.parse_file_list(&response_text)
861 }
862 401 => Err(ScanError::Unauthorized),
863 403 => Err(ScanError::PermissionDenied),
864 404 => {
865 if sandbox_id.is_some() {
866 Err(ScanError::SandboxNotFound)
867 } else {
868 Err(ScanError::ApplicationNotFound)
869 }
870 }
871 _ => {
872 let error_text = response.text().await.unwrap_or_default();
873 Err(ScanError::Api(VeracodeError::InvalidResponse(format!(
874 "HTTP {status}: {error_text}"
875 ))))
876 }
877 }
878 }
879
880 pub async fn remove_file(
892 &self,
893 app_id: &str,
894 file_id: &str,
895 sandbox_id: Option<&str>,
896 ) -> Result<(), ScanError> {
897 let endpoint = "/api/5.0/removefile.do";
898
899 let mut query_params = Vec::new();
901 query_params.push(("app_id", app_id));
902 query_params.push(("file_id", file_id));
903
904 if let Some(sandbox_id) = sandbox_id {
905 query_params.push(("sandbox_id", sandbox_id));
906 }
907
908 let response = self.client.get_with_params(endpoint, &query_params).await?;
909
910 let status = response.status().as_u16();
911 match status {
912 200 => Ok(()),
913 400 => {
914 let error_text = response.text().await.unwrap_or_default();
915 Err(ScanError::InvalidParameter(error_text))
916 }
917 401 => Err(ScanError::Unauthorized),
918 403 => Err(ScanError::PermissionDenied),
919 404 => Err(ScanError::FileNotFound(file_id.to_string())),
920 _ => {
921 let error_text = response.text().await.unwrap_or_default();
922 Err(ScanError::Api(VeracodeError::InvalidResponse(format!(
923 "HTTP {status}: {error_text}"
924 ))))
925 }
926 }
927 }
928
929 pub async fn delete_build(
943 &self,
944 app_id: &str,
945 build_id: &str,
946 sandbox_id: Option<&str>,
947 ) -> Result<(), ScanError> {
948 let endpoint = "/api/5.0/deletebuild.do";
949
950 let mut query_params = Vec::new();
952 query_params.push(("app_id", app_id));
953 query_params.push(("build_id", build_id));
954
955 if let Some(sandbox_id) = sandbox_id {
956 query_params.push(("sandbox_id", sandbox_id));
957 }
958
959 let response = self.client.get_with_params(endpoint, &query_params).await?;
960
961 let status = response.status().as_u16();
962 match status {
963 200 => Ok(()),
964 400 => {
965 let error_text = response.text().await.unwrap_or_default();
966 Err(ScanError::InvalidParameter(error_text))
967 }
968 401 => Err(ScanError::Unauthorized),
969 403 => Err(ScanError::PermissionDenied),
970 404 => Err(ScanError::BuildNotFound),
971 _ => {
972 let error_text = response.text().await.unwrap_or_default();
973 Err(ScanError::Api(VeracodeError::InvalidResponse(format!(
974 "HTTP {status}: {error_text}"
975 ))))
976 }
977 }
978 }
979
980 pub async fn delete_all_builds(
994 &self,
995 app_id: &str,
996 sandbox_id: Option<&str>,
997 ) -> Result<(), ScanError> {
998 let build_info = self.get_build_info(app_id, None, sandbox_id).await?;
1000
1001 if !build_info.build_id.is_empty() && build_info.build_id != "unknown" {
1002 info!("Deleting build: {}", build_info.build_id);
1003 self.delete_build(app_id, &build_info.build_id, sandbox_id)
1004 .await?;
1005 }
1006
1007 Ok(())
1008 }
1009
1010 pub async fn get_build_info(
1022 &self,
1023 app_id: &str,
1024 build_id: Option<&str>,
1025 sandbox_id: Option<&str>,
1026 ) -> Result<ScanInfo, ScanError> {
1027 let endpoint = "/api/5.0/getbuildinfo.do";
1028
1029 let mut params = Vec::new();
1030 params.push(("app_id", app_id));
1031
1032 if let Some(build_id) = build_id {
1033 params.push(("build_id", build_id));
1034 }
1035
1036 if let Some(sandbox_id) = sandbox_id {
1037 params.push(("sandbox_id", sandbox_id));
1038 }
1039
1040 let response = self.client.get_with_params(endpoint, ¶ms).await?;
1041
1042 let status = response.status().as_u16();
1043 match status {
1044 200 => {
1045 let response_text = response.text().await?;
1046 self.parse_build_info(&response_text, app_id, sandbox_id)
1047 }
1048 401 => Err(ScanError::Unauthorized),
1049 403 => Err(ScanError::PermissionDenied),
1050 404 => Err(ScanError::BuildNotFound),
1051 _ => {
1052 let error_text = response.text().await.unwrap_or_default();
1053 Err(ScanError::Api(VeracodeError::InvalidResponse(format!(
1054 "HTTP {status}: {error_text}"
1055 ))))
1056 }
1057 }
1058 }
1059
1060 async fn parse_upload_response(
1063 &self,
1064 xml: &str,
1065 file_path: &str,
1066 ) -> Result<UploadedFile, ScanError> {
1067 let mut reader = Reader::from_str(xml);
1068 reader.config_mut().trim_text(true);
1069
1070 let mut buf = Vec::new();
1071 let mut file_id = None;
1072 let mut file_status = "Unknown".to_string();
1073 let mut _md5: Option<String> = None;
1074
1075 loop {
1076 match reader.read_event_into(&mut buf) {
1077 Ok(Event::Start(ref e)) => {
1078 if e.name().as_ref() == b"file" {
1079 for attr in e.attributes().flatten() {
1081 if attr.key.as_ref() == b"file_id" {
1082 file_id = Some(attr_to_string(&attr.value));
1083 }
1084 }
1085 }
1086 }
1087 Ok(Event::Text(e)) => {
1088 let text = std::str::from_utf8(&e).unwrap_or_default();
1089 if text.contains("successfully uploaded") {
1091 file_status = "Uploaded".to_string();
1092 } else if text.contains("error") || text.contains("failed") {
1093 file_status = "Failed".to_string();
1094 }
1095 }
1096 Ok(Event::Eof) => break,
1097 Err(e) => {
1098 error!("Error parsing XML: {e}");
1099 break;
1100 }
1101 _ => {}
1102 }
1103 buf.clear();
1104 }
1105
1106 let filename = Path::new(file_path)
1107 .file_name()
1108 .and_then(|f| f.to_str())
1109 .unwrap_or("file")
1110 .to_string();
1111
1112 Ok(UploadedFile {
1113 file_id: file_id.unwrap_or_else(|| format!("file_{}", chrono::Utc::now().timestamp())),
1114 file_name: filename,
1115 file_size: tokio::fs::metadata(file_path)
1116 .await
1117 .map(|m| m.len())
1118 .unwrap_or(0),
1119 uploaded: Utc::now(),
1120 file_status,
1121 md5: None,
1122 })
1123 }
1124
1125 fn validate_scan_response(&self, xml: &str) -> Result<(), ScanError> {
1127 if xml.contains("<error>") {
1129 let mut reader = Reader::from_str(xml);
1131 reader.config_mut().trim_text(true);
1132
1133 let mut buf = Vec::new();
1134 let mut in_error = false;
1135 let mut error_message = String::new();
1136
1137 loop {
1138 match reader.read_event_into(&mut buf) {
1139 Ok(Event::Start(ref e)) if e.name().as_ref() == b"error" => {
1140 in_error = true;
1141 }
1142 Ok(Event::Text(ref e)) if in_error => {
1143 error_message.push_str(&String::from_utf8_lossy(e));
1144 }
1145 Ok(Event::End(ref e)) if e.name().as_ref() == b"error" => {
1146 break;
1147 }
1148 Ok(Event::Eof) => break,
1149 Err(e) => {
1150 return Err(ScanError::ScanFailed(format!("XML parsing error: {e}")));
1151 }
1152 _ => {}
1153 }
1154 buf.clear();
1155 }
1156
1157 if !error_message.is_empty() {
1158 return Err(ScanError::ScanFailed(error_message));
1159 }
1160 return Err(ScanError::ScanFailed(
1161 "Unknown error in scan response".to_string(),
1162 ));
1163 }
1164
1165 if xml.contains("<buildinfo") || xml.contains("<build") {
1167 Ok(())
1168 } else {
1169 Err(ScanError::ScanFailed(
1170 "Invalid scan response format".to_string(),
1171 ))
1172 }
1173 }
1174
1175 fn parse_prescan_results(
1176 &self,
1177 xml: &str,
1178 app_id: &str,
1179 sandbox_id: Option<&str>,
1180 ) -> Result<PreScanResults, ScanError> {
1181 if xml.contains("<error>") && xml.contains("Prescan results not available") {
1183 return Ok(PreScanResults {
1185 build_id: String::new(),
1186 app_id: app_id.to_string(),
1187 sandbox_id: sandbox_id.map(|s| s.to_string()),
1188 status: "Pre-Scan Submitted".to_string(), modules: Vec::new(),
1190 messages: Vec::new(),
1191 });
1192 }
1193
1194 let mut reader = Reader::from_str(xml);
1195 reader.config_mut().trim_text(true);
1196
1197 let mut buf = Vec::new();
1198 let mut build_id = None;
1199 let mut modules = Vec::new();
1200 let messages = Vec::new();
1201 let mut has_prescan_results = false;
1202 let mut has_fatal_errors = false;
1203
1204 loop {
1205 match reader.read_event_into(&mut buf) {
1206 Ok(Event::Start(ref e)) => {
1207 match e.name().as_ref() {
1208 b"prescanresults" => {
1209 has_prescan_results = true;
1210 for attr in e.attributes().flatten() {
1212 if attr.key.as_ref() == b"build_id" {
1213 build_id = Some(attr_to_string(&attr.value));
1214 }
1215 }
1216 }
1217 b"module" => {
1218 let mut module = ScanModule {
1219 id: String::new(),
1220 name: String::new(),
1221 module_type: String::new(),
1222 is_fatal: false,
1223 selected: false,
1224 size: None,
1225 platform: None,
1226 };
1227
1228 for attr in e.attributes().flatten() {
1229 match attr.key.as_ref() {
1230 b"id" => module.id = attr_to_string(&attr.value),
1231 b"name" => module.name = attr_to_string(&attr.value),
1232 b"type" => module.module_type = attr_to_string(&attr.value),
1233 b"isfatal" => module.is_fatal = attr.value.as_ref() == b"true",
1234 b"selected" => module.selected = attr.value.as_ref() == b"true",
1235 b"has_fatal_errors" => {
1236 if attr.value.as_ref() == b"true" {
1237 has_fatal_errors = true;
1238 }
1239 }
1240 b"size" => {
1241 if let Ok(size_str) = String::from_utf8(attr.value.to_vec())
1242 {
1243 module.size = size_str.parse().ok();
1244 }
1245 }
1246 b"platform" => {
1247 module.platform = Some(attr_to_string(&attr.value))
1248 }
1249 _ => {}
1250 }
1251 }
1252 modules.push(module);
1253 }
1254 _ => {}
1255 }
1256 }
1257 Ok(Event::Eof) => break,
1258 Err(e) => {
1259 error!("Error parsing XML: {e}");
1260 break;
1261 }
1262 _ => {}
1263 }
1264 buf.clear();
1265 }
1266
1267 let status = if !has_prescan_results {
1269 "Unknown".to_string()
1270 } else if modules.is_empty() {
1271 "Pre-Scan Failed".to_string()
1273 } else if has_fatal_errors {
1274 "Pre-Scan Failed".to_string()
1276 } else {
1277 "Pre-Scan Success".to_string()
1279 };
1280
1281 Ok(PreScanResults {
1282 build_id: build_id.unwrap_or_else(|| "unknown".to_string()),
1283 app_id: app_id.to_string(),
1284 sandbox_id: sandbox_id.map(|s| s.to_string()),
1285 status,
1286 modules,
1287 messages,
1288 })
1289 }
1290
1291 fn parse_file_list(&self, xml: &str) -> Result<Vec<UploadedFile>, ScanError> {
1292 let mut reader = Reader::from_str(xml);
1293 reader.config_mut().trim_text(true);
1294
1295 let mut buf = Vec::new();
1296 let mut files = Vec::new();
1297
1298 loop {
1299 match reader.read_event_into(&mut buf) {
1300 Ok(Event::Start(ref e)) => {
1301 if e.name().as_ref() == b"file" {
1302 let mut file = UploadedFile {
1303 file_id: String::new(),
1304 file_name: String::new(),
1305 file_size: 0,
1306 uploaded: Utc::now(),
1307 file_status: "Unknown".to_string(),
1308 md5: None,
1309 };
1310
1311 for attr in e.attributes().flatten() {
1312 match attr.key.as_ref() {
1313 b"file_id" => file.file_id = attr_to_string(&attr.value),
1314 b"file_name" => file.file_name = attr_to_string(&attr.value),
1315 b"file_size" => {
1316 if let Ok(size_str) = String::from_utf8(attr.value.to_vec()) {
1317 file.file_size = size_str.parse().unwrap_or(0);
1318 }
1319 }
1320 b"file_status" => file.file_status = attr_to_string(&attr.value),
1321 b"md5" => {
1322 file.md5 =
1323 Some(String::from_utf8_lossy(&attr.value).to_string())
1324 }
1325 _ => {}
1326 }
1327 }
1328 files.push(file);
1329 }
1330 }
1331 Ok(Event::Eof) => break,
1332 Err(e) => {
1333 error!("Error parsing XML: {e}");
1334 break;
1335 }
1336 _ => {}
1337 }
1338 buf.clear();
1339 }
1340
1341 Ok(files)
1342 }
1343
1344 fn parse_build_info(
1345 &self,
1346 xml: &str,
1347 app_id: &str,
1348 sandbox_id: Option<&str>,
1349 ) -> Result<ScanInfo, ScanError> {
1350 let mut reader = Reader::from_str(xml);
1351 reader.config_mut().trim_text(true);
1352
1353 let mut buf = Vec::new();
1354 let mut scan_info = ScanInfo {
1355 build_id: String::new(),
1356 app_id: app_id.to_string(),
1357 sandbox_id: sandbox_id.map(|s| s.to_string()),
1358 status: "Unknown".to_string(),
1359 scan_type: "Static".to_string(),
1360 analysis_unit_id: None,
1361 scan_progress_percentage: None,
1362 scan_start: None,
1363 scan_complete: None,
1364 total_lines_of_code: None,
1365 };
1366
1367 let mut inside_build = false;
1368
1369 loop {
1370 match reader.read_event_into(&mut buf) {
1371 Ok(Event::Start(ref e)) => {
1372 match e.name().as_ref() {
1373 b"buildinfo" => {
1374 for attr in e.attributes().flatten() {
1376 match attr.key.as_ref() {
1377 b"build_id" => scan_info.build_id = attr_to_string(&attr.value),
1378 b"analysis_unit" => {
1379 if scan_info.status == "Unknown" {
1381 scan_info.status = attr_to_string(&attr.value);
1382 }
1383 }
1384 b"analysis_unit_id" => {
1385 scan_info.analysis_unit_id =
1386 Some(attr_to_string(&attr.value))
1387 }
1388 b"scan_progress_percentage" => {
1389 if let Ok(progress_str) =
1390 String::from_utf8(attr.value.to_vec())
1391 {
1392 scan_info.scan_progress_percentage =
1393 progress_str.parse().ok();
1394 }
1395 }
1396 b"total_lines_of_code" => {
1397 if let Ok(lines_str) =
1398 String::from_utf8(attr.value.to_vec())
1399 {
1400 scan_info.total_lines_of_code = lines_str.parse().ok();
1401 }
1402 }
1403 _ => {}
1404 }
1405 }
1406 }
1407 b"build" => {
1408 inside_build = true;
1409 }
1410 b"analysis_unit" => {
1411 for attr in e.attributes().flatten() {
1413 match attr.key.as_ref() {
1414 b"status" => {
1415 scan_info.status = attr_to_string(&attr.value);
1417 }
1418 b"analysis_type" => {
1419 scan_info.scan_type = attr_to_string(&attr.value);
1420 }
1421 _ => {}
1422 }
1423 }
1424 }
1425 _ => {}
1426 }
1427 }
1428 Ok(Event::End(ref e)) => {
1429 if e.name().as_ref() == b"build" {
1430 inside_build = false;
1431 }
1432 }
1433 Ok(Event::Empty(ref e)) => {
1434 if e.name().as_ref() == b"analysis_unit" && inside_build {
1436 for attr in e.attributes().flatten() {
1437 match attr.key.as_ref() {
1438 b"status" => {
1439 scan_info.status = attr_to_string(&attr.value);
1440 }
1441 b"analysis_type" => {
1442 scan_info.scan_type = attr_to_string(&attr.value);
1443 }
1444 _ => {}
1445 }
1446 }
1447 }
1448 }
1449 Ok(Event::Eof) => break,
1450 Err(e) => {
1451 error!("Error parsing XML: {e}");
1452 break;
1453 }
1454 _ => {}
1455 }
1456 buf.clear();
1457 }
1458
1459 Ok(scan_info)
1460 }
1461}
1462
1463impl ScanApi {
1465 pub async fn upload_file_to_sandbox(
1477 &self,
1478 app_id: &str,
1479 file_path: &str,
1480 sandbox_id: &str,
1481 ) -> Result<UploadedFile, ScanError> {
1482 let request = UploadFileRequest {
1483 app_id: app_id.to_string(),
1484 file_path: file_path.to_string(),
1485 save_as: None,
1486 sandbox_id: Some(sandbox_id.to_string()),
1487 };
1488
1489 self.upload_file(&request).await
1490 }
1491
1492 pub async fn upload_file_to_app(
1503 &self,
1504 app_id: &str,
1505 file_path: &str,
1506 ) -> Result<UploadedFile, ScanError> {
1507 let request = UploadFileRequest {
1508 app_id: app_id.to_string(),
1509 file_path: file_path.to_string(),
1510 save_as: None,
1511 sandbox_id: None,
1512 };
1513
1514 self.upload_file(&request).await
1515 }
1516
1517 pub async fn upload_large_file_to_sandbox(
1530 &self,
1531 app_id: &str,
1532 file_path: &str,
1533 sandbox_id: &str,
1534 filename: Option<&str>,
1535 ) -> Result<UploadedFile, ScanError> {
1536 let request = UploadLargeFileRequest {
1537 app_id: app_id.to_string(),
1538 file_path: file_path.to_string(),
1539 filename: filename.map(|s| s.to_string()),
1540 sandbox_id: Some(sandbox_id.to_string()),
1541 };
1542
1543 self.upload_large_file(request).await
1544 }
1545
1546 pub async fn upload_large_file_to_app(
1558 &self,
1559 app_id: &str,
1560 file_path: &str,
1561 filename: Option<&str>,
1562 ) -> Result<UploadedFile, ScanError> {
1563 let request = UploadLargeFileRequest {
1564 app_id: app_id.to_string(),
1565 file_path: file_path.to_string(),
1566 filename: filename.map(|s| s.to_string()),
1567 sandbox_id: None,
1568 };
1569
1570 self.upload_large_file(request).await
1571 }
1572
1573 pub async fn upload_large_file_to_sandbox_with_progress<F>(
1587 &self,
1588 app_id: &str,
1589 file_path: &str,
1590 sandbox_id: &str,
1591 filename: Option<&str>,
1592 progress_callback: F,
1593 ) -> Result<UploadedFile, ScanError>
1594 where
1595 F: Fn(u64, u64, f64) + Send + Sync,
1596 {
1597 let request = UploadLargeFileRequest {
1598 app_id: app_id.to_string(),
1599 file_path: file_path.to_string(),
1600 filename: filename.map(|s| s.to_string()),
1601 sandbox_id: Some(sandbox_id.to_string()),
1602 };
1603
1604 self.upload_large_file_with_progress(request, progress_callback)
1605 .await
1606 }
1607
1608 pub async fn begin_sandbox_prescan(
1619 &self,
1620 app_id: &str,
1621 sandbox_id: &str,
1622 ) -> Result<(), ScanError> {
1623 let request = BeginPreScanRequest {
1624 app_id: app_id.to_string(),
1625 sandbox_id: Some(sandbox_id.to_string()),
1626 auto_scan: Some(true),
1627 scan_all_nonfatal_top_level_modules: Some(true),
1628 include_new_modules: Some(true),
1629 };
1630
1631 self.begin_prescan(&request).await
1632 }
1633
1634 pub async fn begin_sandbox_scan_all_modules(
1645 &self,
1646 app_id: &str,
1647 sandbox_id: &str,
1648 ) -> Result<(), ScanError> {
1649 let request = BeginScanRequest {
1650 app_id: app_id.to_string(),
1651 sandbox_id: Some(sandbox_id.to_string()),
1652 modules: None,
1653 scan_all_top_level_modules: Some(true),
1654 scan_all_nonfatal_top_level_modules: Some(true),
1655 scan_previously_selected_modules: None,
1656 };
1657
1658 self.begin_scan(&request).await
1659 }
1660
1661 pub async fn upload_and_scan_sandbox(
1673 &self,
1674 app_id: &str,
1675 sandbox_id: &str,
1676 file_path: &str,
1677 ) -> Result<String, ScanError> {
1678 info!("Uploading file to sandbox...");
1680 let _uploaded_file = self
1681 .upload_file_to_sandbox(app_id, file_path, sandbox_id)
1682 .await?;
1683
1684 info!("Beginning pre-scan...");
1686 self.begin_sandbox_prescan(app_id, sandbox_id).await?;
1687
1688 tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
1690
1691 info!("Beginning scan...");
1693 self.begin_sandbox_scan_all_modules(app_id, sandbox_id)
1694 .await?;
1695
1696 Ok("build_id_not_available".to_string())
1701 }
1702
1703 pub async fn delete_sandbox_build(
1715 &self,
1716 app_id: &str,
1717 build_id: &str,
1718 sandbox_id: &str,
1719 ) -> Result<(), ScanError> {
1720 self.delete_build(app_id, build_id, Some(sandbox_id)).await
1721 }
1722
1723 pub async fn delete_all_sandbox_builds(
1734 &self,
1735 app_id: &str,
1736 sandbox_id: &str,
1737 ) -> Result<(), ScanError> {
1738 self.delete_all_builds(app_id, Some(sandbox_id)).await
1739 }
1740
1741 pub async fn delete_app_build(&self, app_id: &str, build_id: &str) -> Result<(), ScanError> {
1752 self.delete_build(app_id, build_id, None).await
1753 }
1754
1755 pub async fn delete_all_app_builds(&self, app_id: &str) -> Result<(), ScanError> {
1765 self.delete_all_builds(app_id, None).await
1766 }
1767}
1768
1769#[cfg(test)]
1770mod tests {
1771 use super::*;
1772 use crate::VeracodeConfig;
1773
1774 #[test]
1775 fn test_upload_file_request() {
1776 let request = UploadFileRequest {
1777 app_id: "123".to_string(),
1778 file_path: "/path/to/file.jar".to_string(),
1779 save_as: Some("app.jar".to_string()),
1780 sandbox_id: Some("456".to_string()),
1781 };
1782
1783 assert_eq!(request.app_id, "123");
1784 assert_eq!(request.sandbox_id, Some("456".to_string()));
1785 }
1786
1787 #[test]
1788 fn test_begin_prescan_request() {
1789 let request = BeginPreScanRequest {
1790 app_id: "123".to_string(),
1791 sandbox_id: Some("456".to_string()),
1792 auto_scan: Some(true),
1793 scan_all_nonfatal_top_level_modules: Some(true),
1794 include_new_modules: Some(false),
1795 };
1796
1797 assert_eq!(request.app_id, "123");
1798 assert_eq!(request.auto_scan, Some(true));
1799 }
1800
1801 #[test]
1802 fn test_scan_error_display() {
1803 let error = ScanError::FileNotFound("test.jar".to_string());
1804 assert_eq!(error.to_string(), "File not found: test.jar");
1805
1806 let error = ScanError::UploadFailed("Network error".to_string());
1807 assert_eq!(error.to_string(), "Upload failed: Network error");
1808
1809 let error = ScanError::Unauthorized;
1810 assert_eq!(error.to_string(), "Unauthorized access");
1811
1812 let error = ScanError::BuildNotFound;
1813 assert_eq!(error.to_string(), "Build not found");
1814 }
1815
1816 #[test]
1817 fn test_delete_build_request_structure() {
1818 use crate::{VeracodeClient, VeracodeConfig};
1822
1823 async fn _test_delete_methods() -> Result<(), Box<dyn std::error::Error>> {
1824 let config = VeracodeConfig::new("test", "test");
1825 let client = VeracodeClient::new(config)?;
1826 let api = client.scan_api();
1827
1828 let _: Result<(), _> = api
1831 .delete_build("app_id", "build_id", Some("sandbox_id"))
1832 .await;
1833 let _: Result<(), _> = api.delete_all_builds("app_id", Some("sandbox_id")).await;
1834 let _: Result<(), _> = api
1835 .delete_sandbox_build("app_id", "build_id", "sandbox_id")
1836 .await;
1837 let _: Result<(), _> = api.delete_all_sandbox_builds("app_id", "sandbox_id").await;
1838
1839 Ok(())
1840 }
1841
1842 }
1845
1846 #[test]
1847 fn test_upload_large_file_request() {
1848 let request = UploadLargeFileRequest {
1849 app_id: "123".to_string(),
1850 file_path: "/path/to/large_file.jar".to_string(),
1851 filename: Some("custom_name.jar".to_string()),
1852 sandbox_id: Some("456".to_string()),
1853 };
1854
1855 assert_eq!(request.app_id, "123");
1856 assert_eq!(request.filename, Some("custom_name.jar".to_string()));
1857 assert_eq!(request.sandbox_id, Some("456".to_string()));
1858 }
1859
1860 #[test]
1861 fn test_upload_progress() {
1862 let progress = UploadProgress {
1863 bytes_uploaded: 1024,
1864 total_bytes: 2048,
1865 percentage: 50.0,
1866 };
1867
1868 assert_eq!(progress.bytes_uploaded, 1024);
1869 assert_eq!(progress.total_bytes, 2048);
1870 assert_eq!(progress.percentage, 50.0);
1871 }
1872
1873 #[test]
1874 fn test_large_file_scan_error_display() {
1875 let error = ScanError::FileTooLarge("File exceeds 2GB".to_string());
1876 assert_eq!(error.to_string(), "File too large: File exceeds 2GB");
1877
1878 let error = ScanError::UploadInProgress;
1879 assert_eq!(error.to_string(), "Upload or prescan already in progress");
1880
1881 let error = ScanError::ScanInProgress;
1882 assert_eq!(error.to_string(), "Scan in progress, cannot upload");
1883
1884 let error = ScanError::ChunkedUploadFailed("Network error".to_string());
1885 assert_eq!(error.to_string(), "Chunked upload failed: Network error");
1886 }
1887
1888 #[tokio::test]
1889 async fn test_large_file_upload_method_signatures() {
1890 async fn _test_large_file_methods() -> Result<(), Box<dyn std::error::Error>> {
1891 let config = VeracodeConfig::new("test", "test");
1892 let client = VeracodeClient::new(config)?;
1893 let api = client.scan_api();
1894
1895 let request = UploadLargeFileRequest {
1897 app_id: "123".to_string(),
1898 file_path: "/nonexistent/file.jar".to_string(),
1899 filename: None,
1900 sandbox_id: Some("456".to_string()),
1901 };
1902
1903 let _: Result<UploadedFile, _> = api.upload_large_file(request.clone()).await;
1906 let _: Result<UploadedFile, _> = api
1907 .upload_large_file_to_sandbox("123", "/path", "456", None)
1908 .await;
1909 let _: Result<UploadedFile, _> =
1910 api.upload_large_file_to_app("123", "/path", None).await;
1911
1912 let progress_callback = |bytes_uploaded: u64, total_bytes: u64, percentage: f64| {
1914 debug!("Upload progress: {bytes_uploaded}/{total_bytes} ({percentage:.1}%)");
1915 };
1916 let _: Result<UploadedFile, _> = api
1917 .upload_large_file_with_progress(request, progress_callback)
1918 .await;
1919
1920 Ok(())
1921 }
1922
1923 }
1926}