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)]
205#[must_use = "Need to handle all error enum types."]
206pub enum ScanError {
207 Api(VeracodeError),
209 FileNotFound(String),
211 InvalidFileFormat(String),
213 UploadFailed(String),
215 ScanFailed(String),
217 PreScanFailed(String),
219 BuildNotFound,
221 ApplicationNotFound,
223 SandboxNotFound,
225 Unauthorized,
227 PermissionDenied,
229 InvalidParameter(String),
231 FileTooLarge(String),
233 UploadInProgress,
235 ScanInProgress,
237 BuildCreationFailed(String),
239 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
293pub struct ScanApi {
295 client: VeracodeClient,
296}
297
298impl ScanApi {
299 #[must_use]
306 pub fn new(client: VeracodeClient) -> Self {
307 Self { client }
308 }
309
310 pub async fn upload_file(
325 &self,
326 request: &UploadFileRequest,
327 ) -> Result<UploadedFile, ScanError> {
328 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 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 let file_data = tokio::fs::read(&request.file_path).await?;
349
350 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 pub async fn upload_large_file(
409 &self,
410 request: UploadLargeFileRequest,
411 ) -> Result<UploadedFile, ScanError> {
412 if !Path::new(&request.file_path).exists() {
414 return Err(ScanError::FileNotFound(request.file_path));
415 }
416
417 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; 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"; 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 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 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 if !Path::new(&request.file_path).exists() {
526 return Err(ScanError::FileNotFound(request.file_path));
527 }
528
529 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; 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 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 pub async fn upload_file_smart(
621 &self,
622 request: &UploadFileRequest,
623 ) -> Result<UploadedFile, ScanError> {
624 if !Path::new(&request.file_path).exists() {
626 return Err(ScanError::FileNotFound(request.file_path.clone()));
627 }
628
629 let file_metadata = tokio::fs::metadata(&request.file_path).await?;
631 let file_size = file_metadata.len();
632
633 const LARGE_FILE_THRESHOLD: u64 = 100 * 1024 * 1024; if file_size > LARGE_FILE_THRESHOLD {
637 let large_request = UploadLargeFileRequest::from(request);
639
640 match self.upload_large_file(large_request).await {
642 Ok(result) => Ok(result),
643 Err(ScanError::Api(_)) => {
644 self.upload_file(request).await
646 }
647 Err(e) => Err(e),
648 }
649 } else {
650 self.upload_file(request).await
652 }
653 }
654
655 pub async fn begin_prescan(&self, request: &BeginPreScanRequest) -> Result<(), ScanError> {
670 let endpoint = "/api/5.0/beginprescan.do";
671
672 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 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 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, ¶ms).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 pub async fn begin_scan(&self, request: &BeginScanRequest) -> Result<(), ScanError> {
807 let endpoint = "/api/5.0/beginscan.do";
808
809 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 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 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, ¶ms).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 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 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 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 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 pub async fn delete_all_builds(
1065 &self,
1066 app_id: &str,
1067 sandbox_id: Option<&str>,
1068 ) -> Result<(), ScanError> {
1069 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 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, ¶ms).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 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 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 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 fn validate_scan_response(&self, xml: &str) -> Result<(), ScanError> {
1208 if xml.contains("<error>") {
1210 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 if xml.contains("<buildinfo") || xml.contains("<build") {
1248 Ok(())
1249 } else {
1250 Err(ScanError::ScanFailed(
1251 "Invalid scan response format".to_string(),
1252 ))
1253 }
1254 }
1255
1256 fn parse_prescan_results(
1257 &self,
1258 xml: &str,
1259 app_id: &str,
1260 sandbox_id: Option<&str>,
1261 ) -> Result<PreScanResults, ScanError> {
1262 if xml.contains("<error>") && xml.contains("Prescan results not available") {
1264 return Ok(PreScanResults {
1266 build_id: String::new(),
1267 app_id: app_id.to_string(),
1268 sandbox_id: sandbox_id.map(|s| s.to_string()),
1269 status: "Pre-Scan Submitted".to_string(), modules: Vec::new(),
1271 messages: Vec::new(),
1272 });
1273 }
1274
1275 let mut reader = Reader::from_str(xml);
1276 reader.config_mut().trim_text(true);
1277
1278 let mut buf = Vec::new();
1279 let mut build_id = None;
1280 let mut modules = Vec::new();
1281 let messages = Vec::new();
1282 let mut has_prescan_results = false;
1283 let mut has_fatal_errors = false;
1284
1285 loop {
1286 match reader.read_event_into(&mut buf) {
1287 Ok(Event::Start(ref e)) => {
1288 match e.name().as_ref() {
1289 b"prescanresults" => {
1290 has_prescan_results = true;
1291 for attr in e.attributes().flatten() {
1293 if attr.key.as_ref() == b"build_id" {
1294 build_id = Some(attr_to_string(&attr.value));
1295 }
1296 }
1297 }
1298 b"module" => {
1299 let mut module = ScanModule {
1300 id: String::new(),
1301 name: String::new(),
1302 module_type: String::new(),
1303 is_fatal: false,
1304 selected: false,
1305 size: None,
1306 platform: None,
1307 };
1308
1309 for attr in e.attributes().flatten() {
1310 match attr.key.as_ref() {
1311 b"id" => module.id = attr_to_string(&attr.value),
1312 b"name" => module.name = attr_to_string(&attr.value),
1313 b"type" => module.module_type = attr_to_string(&attr.value),
1314 b"isfatal" => module.is_fatal = attr.value.as_ref() == b"true",
1315 b"selected" => module.selected = attr.value.as_ref() == b"true",
1316 b"has_fatal_errors" => {
1317 if attr.value.as_ref() == b"true" {
1318 has_fatal_errors = true;
1319 }
1320 }
1321 b"size" => {
1322 if let Ok(size_str) = String::from_utf8(attr.value.to_vec())
1323 {
1324 module.size = size_str.parse().ok();
1325 }
1326 }
1327 b"platform" => {
1328 module.platform = Some(attr_to_string(&attr.value))
1329 }
1330 _ => {}
1331 }
1332 }
1333 modules.push(module);
1334 }
1335 _ => {}
1336 }
1337 }
1338 Ok(Event::Eof) => break,
1339 Err(e) => {
1340 error!("Error parsing XML: {e}");
1341 break;
1342 }
1343 _ => {}
1344 }
1345 buf.clear();
1346 }
1347
1348 let status = if !has_prescan_results {
1350 "Unknown".to_string()
1351 } else if modules.is_empty() {
1352 "Pre-Scan Failed".to_string()
1354 } else if has_fatal_errors {
1355 "Pre-Scan Failed".to_string()
1357 } else {
1358 "Pre-Scan Success".to_string()
1360 };
1361
1362 Ok(PreScanResults {
1363 build_id: build_id.unwrap_or_else(|| "unknown".to_string()),
1364 app_id: app_id.to_string(),
1365 sandbox_id: sandbox_id.map(|s| s.to_string()),
1366 status,
1367 modules,
1368 messages,
1369 })
1370 }
1371
1372 fn parse_file_list(&self, xml: &str) -> Result<Vec<UploadedFile>, ScanError> {
1373 let mut reader = Reader::from_str(xml);
1374 reader.config_mut().trim_text(true);
1375
1376 let mut buf = Vec::new();
1377 let mut files = Vec::new();
1378
1379 loop {
1380 match reader.read_event_into(&mut buf) {
1381 Ok(Event::Start(ref e)) => {
1382 if e.name().as_ref() == b"file" {
1383 let mut file = UploadedFile {
1384 file_id: String::new(),
1385 file_name: String::new(),
1386 file_size: 0,
1387 uploaded: Utc::now(),
1388 file_status: "Unknown".to_string(),
1389 md5: None,
1390 };
1391
1392 for attr in e.attributes().flatten() {
1393 match attr.key.as_ref() {
1394 b"file_id" => file.file_id = attr_to_string(&attr.value),
1395 b"file_name" => file.file_name = attr_to_string(&attr.value),
1396 b"file_size" => {
1397 if let Ok(size_str) = String::from_utf8(attr.value.to_vec()) {
1398 file.file_size = size_str.parse().unwrap_or(0);
1399 }
1400 }
1401 b"file_status" => file.file_status = attr_to_string(&attr.value),
1402 b"md5" => {
1403 file.md5 =
1404 Some(String::from_utf8_lossy(&attr.value).to_string())
1405 }
1406 _ => {}
1407 }
1408 }
1409 files.push(file);
1410 }
1411 }
1412 Ok(Event::Eof) => break,
1413 Err(e) => {
1414 error!("Error parsing XML: {e}");
1415 break;
1416 }
1417 _ => {}
1418 }
1419 buf.clear();
1420 }
1421
1422 Ok(files)
1423 }
1424
1425 fn parse_build_info(
1426 &self,
1427 xml: &str,
1428 app_id: &str,
1429 sandbox_id: Option<&str>,
1430 ) -> Result<ScanInfo, ScanError> {
1431 let mut reader = Reader::from_str(xml);
1432 reader.config_mut().trim_text(true);
1433
1434 let mut buf = Vec::new();
1435 let mut scan_info = ScanInfo {
1436 build_id: String::new(),
1437 app_id: app_id.to_string(),
1438 sandbox_id: sandbox_id.map(|s| s.to_string()),
1439 status: "Unknown".to_string(),
1440 scan_type: "Static".to_string(),
1441 analysis_unit_id: None,
1442 scan_progress_percentage: None,
1443 scan_start: None,
1444 scan_complete: None,
1445 total_lines_of_code: None,
1446 };
1447
1448 let mut inside_build = false;
1449
1450 loop {
1451 match reader.read_event_into(&mut buf) {
1452 Ok(Event::Start(ref e)) => {
1453 match e.name().as_ref() {
1454 b"buildinfo" => {
1455 for attr in e.attributes().flatten() {
1457 match attr.key.as_ref() {
1458 b"build_id" => scan_info.build_id = attr_to_string(&attr.value),
1459 b"analysis_unit" => {
1460 if scan_info.status == "Unknown" {
1462 scan_info.status = attr_to_string(&attr.value);
1463 }
1464 }
1465 b"analysis_unit_id" => {
1466 scan_info.analysis_unit_id =
1467 Some(attr_to_string(&attr.value))
1468 }
1469 b"scan_progress_percentage" => {
1470 if let Ok(progress_str) =
1471 String::from_utf8(attr.value.to_vec())
1472 {
1473 scan_info.scan_progress_percentage =
1474 progress_str.parse().ok();
1475 }
1476 }
1477 b"total_lines_of_code" => {
1478 if let Ok(lines_str) =
1479 String::from_utf8(attr.value.to_vec())
1480 {
1481 scan_info.total_lines_of_code = lines_str.parse().ok();
1482 }
1483 }
1484 _ => {}
1485 }
1486 }
1487 }
1488 b"build" => {
1489 inside_build = true;
1490 }
1491 b"analysis_unit" => {
1492 for attr in e.attributes().flatten() {
1494 match attr.key.as_ref() {
1495 b"status" => {
1496 scan_info.status = attr_to_string(&attr.value);
1498 }
1499 b"analysis_type" => {
1500 scan_info.scan_type = attr_to_string(&attr.value);
1501 }
1502 _ => {}
1503 }
1504 }
1505 }
1506 _ => {}
1507 }
1508 }
1509 Ok(Event::End(ref e)) => {
1510 if e.name().as_ref() == b"build" {
1511 inside_build = false;
1512 }
1513 }
1514 Ok(Event::Empty(ref e)) => {
1515 if e.name().as_ref() == b"analysis_unit" && inside_build {
1517 for attr in e.attributes().flatten() {
1518 match attr.key.as_ref() {
1519 b"status" => {
1520 scan_info.status = attr_to_string(&attr.value);
1521 }
1522 b"analysis_type" => {
1523 scan_info.scan_type = attr_to_string(&attr.value);
1524 }
1525 _ => {}
1526 }
1527 }
1528 }
1529 }
1530 Ok(Event::Eof) => break,
1531 Err(e) => {
1532 error!("Error parsing XML: {e}");
1533 break;
1534 }
1535 _ => {}
1536 }
1537 buf.clear();
1538 }
1539
1540 Ok(scan_info)
1541 }
1542}
1543
1544impl ScanApi {
1546 pub async fn upload_file_to_sandbox(
1563 &self,
1564 app_id: &str,
1565 file_path: &str,
1566 sandbox_id: &str,
1567 ) -> Result<UploadedFile, ScanError> {
1568 let request = UploadFileRequest {
1569 app_id: app_id.to_string(),
1570 file_path: file_path.to_string(),
1571 save_as: None,
1572 sandbox_id: Some(sandbox_id.to_string()),
1573 };
1574
1575 self.upload_file(&request).await
1576 }
1577
1578 pub async fn upload_file_to_app(
1594 &self,
1595 app_id: &str,
1596 file_path: &str,
1597 ) -> Result<UploadedFile, ScanError> {
1598 let request = UploadFileRequest {
1599 app_id: app_id.to_string(),
1600 file_path: file_path.to_string(),
1601 save_as: None,
1602 sandbox_id: None,
1603 };
1604
1605 self.upload_file(&request).await
1606 }
1607
1608 pub async fn upload_large_file_to_sandbox(
1626 &self,
1627 app_id: &str,
1628 file_path: &str,
1629 sandbox_id: &str,
1630 filename: Option<&str>,
1631 ) -> Result<UploadedFile, ScanError> {
1632 let request = UploadLargeFileRequest {
1633 app_id: app_id.to_string(),
1634 file_path: file_path.to_string(),
1635 filename: filename.map(|s| s.to_string()),
1636 sandbox_id: Some(sandbox_id.to_string()),
1637 };
1638
1639 self.upload_large_file(request).await
1640 }
1641
1642 pub async fn upload_large_file_to_app(
1659 &self,
1660 app_id: &str,
1661 file_path: &str,
1662 filename: Option<&str>,
1663 ) -> Result<UploadedFile, ScanError> {
1664 let request = UploadLargeFileRequest {
1665 app_id: app_id.to_string(),
1666 file_path: file_path.to_string(),
1667 filename: filename.map(|s| s.to_string()),
1668 sandbox_id: None,
1669 };
1670
1671 self.upload_large_file(request).await
1672 }
1673
1674 pub async fn upload_large_file_to_sandbox_with_progress<F>(
1693 &self,
1694 app_id: &str,
1695 file_path: &str,
1696 sandbox_id: &str,
1697 filename: Option<&str>,
1698 progress_callback: F,
1699 ) -> Result<UploadedFile, ScanError>
1700 where
1701 F: Fn(u64, u64, f64) + Send + Sync,
1702 {
1703 let request = UploadLargeFileRequest {
1704 app_id: app_id.to_string(),
1705 file_path: file_path.to_string(),
1706 filename: filename.map(|s| s.to_string()),
1707 sandbox_id: Some(sandbox_id.to_string()),
1708 };
1709
1710 self.upload_large_file_with_progress(request, progress_callback)
1711 .await
1712 }
1713
1714 pub async fn begin_sandbox_prescan(
1730 &self,
1731 app_id: &str,
1732 sandbox_id: &str,
1733 ) -> Result<(), ScanError> {
1734 let request = BeginPreScanRequest {
1735 app_id: app_id.to_string(),
1736 sandbox_id: Some(sandbox_id.to_string()),
1737 auto_scan: Some(true),
1738 scan_all_nonfatal_top_level_modules: Some(true),
1739 include_new_modules: Some(true),
1740 };
1741
1742 self.begin_prescan(&request).await
1743 }
1744
1745 pub async fn begin_sandbox_scan_all_modules(
1761 &self,
1762 app_id: &str,
1763 sandbox_id: &str,
1764 ) -> Result<(), ScanError> {
1765 let request = BeginScanRequest {
1766 app_id: app_id.to_string(),
1767 sandbox_id: Some(sandbox_id.to_string()),
1768 modules: None,
1769 scan_all_top_level_modules: Some(true),
1770 scan_all_nonfatal_top_level_modules: Some(true),
1771 scan_previously_selected_modules: None,
1772 };
1773
1774 self.begin_scan(&request).await
1775 }
1776
1777 pub async fn upload_and_scan_sandbox(
1794 &self,
1795 app_id: &str,
1796 sandbox_id: &str,
1797 file_path: &str,
1798 ) -> Result<String, ScanError> {
1799 info!("Uploading file to sandbox...");
1801 let _uploaded_file = self
1802 .upload_file_to_sandbox(app_id, file_path, sandbox_id)
1803 .await?;
1804
1805 info!("Beginning pre-scan...");
1807 self.begin_sandbox_prescan(app_id, sandbox_id).await?;
1808
1809 tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
1811
1812 info!("Beginning scan...");
1814 self.begin_sandbox_scan_all_modules(app_id, sandbox_id)
1815 .await?;
1816
1817 Ok("build_id_not_available".to_string())
1822 }
1823
1824 pub async fn delete_sandbox_build(
1841 &self,
1842 app_id: &str,
1843 build_id: &str,
1844 sandbox_id: &str,
1845 ) -> Result<(), ScanError> {
1846 self.delete_build(app_id, build_id, Some(sandbox_id)).await
1847 }
1848
1849 pub async fn delete_all_sandbox_builds(
1865 &self,
1866 app_id: &str,
1867 sandbox_id: &str,
1868 ) -> Result<(), ScanError> {
1869 self.delete_all_builds(app_id, Some(sandbox_id)).await
1870 }
1871
1872 pub async fn delete_app_build(&self, app_id: &str, build_id: &str) -> Result<(), ScanError> {
1888 self.delete_build(app_id, build_id, None).await
1889 }
1890
1891 pub async fn delete_all_app_builds(&self, app_id: &str) -> Result<(), ScanError> {
1906 self.delete_all_builds(app_id, None).await
1907 }
1908}
1909
1910#[cfg(test)]
1911mod tests {
1912 use super::*;
1913 use crate::VeracodeConfig;
1914
1915 #[test]
1916 fn test_upload_file_request() {
1917 let request = UploadFileRequest {
1918 app_id: "123".to_string(),
1919 file_path: "/path/to/file.jar".to_string(),
1920 save_as: Some("app.jar".to_string()),
1921 sandbox_id: Some("456".to_string()),
1922 };
1923
1924 assert_eq!(request.app_id, "123");
1925 assert_eq!(request.sandbox_id, Some("456".to_string()));
1926 }
1927
1928 #[test]
1929 fn test_begin_prescan_request() {
1930 let request = BeginPreScanRequest {
1931 app_id: "123".to_string(),
1932 sandbox_id: Some("456".to_string()),
1933 auto_scan: Some(true),
1934 scan_all_nonfatal_top_level_modules: Some(true),
1935 include_new_modules: Some(false),
1936 };
1937
1938 assert_eq!(request.app_id, "123");
1939 assert_eq!(request.auto_scan, Some(true));
1940 }
1941
1942 #[test]
1943 fn test_scan_error_display() {
1944 let error = ScanError::FileNotFound("test.jar".to_string());
1945 assert_eq!(error.to_string(), "File not found: test.jar");
1946
1947 let error = ScanError::UploadFailed("Network error".to_string());
1948 assert_eq!(error.to_string(), "Upload failed: Network error");
1949
1950 let error = ScanError::Unauthorized;
1951 assert_eq!(error.to_string(), "Unauthorized access");
1952
1953 let error = ScanError::BuildNotFound;
1954 assert_eq!(error.to_string(), "Build not found");
1955 }
1956
1957 #[test]
1958 fn test_delete_build_request_structure() {
1959 use crate::{VeracodeClient, VeracodeConfig};
1963
1964 async fn _test_delete_methods() -> Result<(), Box<dyn std::error::Error>> {
1965 let config = VeracodeConfig::new("test", "test");
1966 let client = VeracodeClient::new(config)?;
1967 let api = client.scan_api()?;
1968
1969 let _: Result<(), _> = api
1972 .delete_build("app_id", "build_id", Some("sandbox_id"))
1973 .await;
1974 let _: Result<(), _> = api.delete_all_builds("app_id", Some("sandbox_id")).await;
1975 let _: Result<(), _> = api
1976 .delete_sandbox_build("app_id", "build_id", "sandbox_id")
1977 .await;
1978 let _: Result<(), _> = api.delete_all_sandbox_builds("app_id", "sandbox_id").await;
1979
1980 Ok(())
1981 }
1982
1983 }
1986
1987 #[test]
1988 fn test_upload_large_file_request() {
1989 let request = UploadLargeFileRequest {
1990 app_id: "123".to_string(),
1991 file_path: "/path/to/large_file.jar".to_string(),
1992 filename: Some("custom_name.jar".to_string()),
1993 sandbox_id: Some("456".to_string()),
1994 };
1995
1996 assert_eq!(request.app_id, "123");
1997 assert_eq!(request.filename, Some("custom_name.jar".to_string()));
1998 assert_eq!(request.sandbox_id, Some("456".to_string()));
1999 }
2000
2001 #[test]
2002 fn test_upload_progress() {
2003 let progress = UploadProgress {
2004 bytes_uploaded: 1024,
2005 total_bytes: 2048,
2006 percentage: 50.0,
2007 };
2008
2009 assert_eq!(progress.bytes_uploaded, 1024);
2010 assert_eq!(progress.total_bytes, 2048);
2011 assert_eq!(progress.percentage, 50.0);
2012 }
2013
2014 #[test]
2015 fn test_large_file_scan_error_display() {
2016 let error = ScanError::FileTooLarge("File exceeds 2GB".to_string());
2017 assert_eq!(error.to_string(), "File too large: File exceeds 2GB");
2018
2019 let error = ScanError::UploadInProgress;
2020 assert_eq!(error.to_string(), "Upload or prescan already in progress");
2021
2022 let error = ScanError::ScanInProgress;
2023 assert_eq!(error.to_string(), "Scan in progress, cannot upload");
2024
2025 let error = ScanError::ChunkedUploadFailed("Network error".to_string());
2026 assert_eq!(error.to_string(), "Chunked upload failed: Network error");
2027 }
2028
2029 #[tokio::test]
2030 async fn test_large_file_upload_method_signatures() {
2031 async fn _test_large_file_methods() -> Result<(), Box<dyn std::error::Error>> {
2032 let config = VeracodeConfig::new("test", "test");
2033 let client = VeracodeClient::new(config)?;
2034 let api = client.scan_api()?;
2035
2036 let request = UploadLargeFileRequest {
2038 app_id: "123".to_string(),
2039 file_path: "/nonexistent/file.jar".to_string(),
2040 filename: None,
2041 sandbox_id: Some("456".to_string()),
2042 };
2043
2044 let _: Result<UploadedFile, _> = api.upload_large_file(request.clone()).await;
2047 let _: Result<UploadedFile, _> = api
2048 .upload_large_file_to_sandbox("123", "/path", "456", None)
2049 .await;
2050 let _: Result<UploadedFile, _> =
2051 api.upload_large_file_to_app("123", "/path", None).await;
2052
2053 let progress_callback = |bytes_uploaded: u64, total_bytes: u64, percentage: f64| {
2055 debug!("Upload progress: {bytes_uploaded}/{total_bytes} ({percentage:.1}%)");
2056 };
2057 let _: Result<UploadedFile, _> = api
2058 .upload_large_file_with_progress(request, progress_callback)
2059 .await;
2060
2061 Ok(())
2062 }
2063
2064 }
2067}