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::validation::validate_url_segment;
16use crate::{VeracodeClient, VeracodeError};
17
18fn attr_to_string(value: &[u8]) -> String {
21 String::from_utf8_lossy(value).into_owned()
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct UploadedFile {
27 pub file_id: String,
29 pub file_name: String,
31 pub file_size: u64,
33 pub uploaded: DateTime<Utc>,
35 pub file_status: String,
37 pub md5: Option<String>,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct PreScanResults {
44 pub build_id: String,
46 pub app_id: String,
48 pub sandbox_id: Option<String>,
50 pub status: String,
52 pub modules: Vec<ScanModule>,
54 pub messages: Vec<PreScanMessage>,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct ScanModule {
61 pub id: String,
63 pub name: String,
65 pub module_type: String,
67 pub is_fatal: bool,
69 pub selected: bool,
71 pub size: Option<u64>,
73 pub platform: Option<String>,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct PreScanMessage {
80 pub severity: String,
82 pub text: String,
84 pub module_name: Option<String>,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct ScanInfo {
91 pub build_id: String,
93 pub app_id: String,
95 pub sandbox_id: Option<String>,
97 pub status: String,
99 pub scan_type: String,
101 pub analysis_unit_id: Option<String>,
103 pub scan_progress_percentage: Option<u32>,
105 pub scan_start: Option<DateTime<Utc>>,
107 pub scan_complete: Option<DateTime<Utc>>,
109 pub total_lines_of_code: Option<u64>,
111}
112
113#[derive(Debug, Clone)]
115pub struct UploadFileRequest {
116 pub app_id: String,
118 pub file_path: String,
120 pub save_as: Option<String>,
122 pub sandbox_id: Option<String>,
124}
125
126#[derive(Debug, Clone)]
128pub struct UploadLargeFileRequest {
129 pub app_id: String,
131 pub file_path: String,
133 pub filename: Option<String>,
135 pub sandbox_id: Option<String>,
137}
138
139#[derive(Debug, Clone)]
141pub struct UploadProgress {
142 pub bytes_uploaded: u64,
144 pub total_bytes: u64,
146 pub percentage: f64,
148}
149
150pub trait UploadProgressCallback: Send + Sync {
152 fn on_progress(&self, progress: UploadProgress);
154 fn on_completed(&self);
156 fn on_error(&self, error: &str);
158}
159
160#[derive(Debug, Clone)]
162pub struct BeginPreScanRequest {
163 pub app_id: String,
165 pub sandbox_id: Option<String>,
167 pub auto_scan: Option<bool>,
169 pub scan_all_nonfatal_top_level_modules: Option<bool>,
171 pub include_new_modules: Option<bool>,
173}
174
175#[derive(Debug, Clone)]
177pub struct BeginScanRequest {
178 pub app_id: String,
180 pub sandbox_id: Option<String>,
182 pub modules: Option<String>,
184 pub scan_all_top_level_modules: Option<bool>,
186 pub scan_all_nonfatal_top_level_modules: Option<bool>,
188 pub scan_previously_selected_modules: Option<bool>,
190}
191
192impl From<&UploadFileRequest> for UploadLargeFileRequest {
194 fn from(request: &UploadFileRequest) -> Self {
195 UploadLargeFileRequest {
196 app_id: request.app_id.clone(),
197 file_path: request.file_path.clone(),
198 filename: request.save_as.clone(),
199 sandbox_id: request.sandbox_id.clone(),
200 }
201 }
202}
203
204#[derive(Debug)]
206#[must_use = "Need to handle all error enum types."]
207pub enum ScanError {
208 Api(VeracodeError),
210 FileNotFound(String),
212 InvalidFileFormat(String),
214 UploadFailed(String),
216 ScanFailed(String),
218 PreScanFailed(String),
220 BuildNotFound,
222 ApplicationNotFound,
224 SandboxNotFound,
226 Unauthorized,
228 PermissionDenied,
230 InvalidParameter(String),
232 FileTooLarge(String),
234 UploadInProgress,
236 ScanInProgress,
238 BuildCreationFailed(String),
240 ChunkedUploadFailed(String),
242}
243
244impl std::fmt::Display for ScanError {
245 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
246 match self {
247 ScanError::Api(err) => write!(f, "API error: {err}"),
248 ScanError::FileNotFound(path) => write!(f, "File not found: {path}"),
249 ScanError::InvalidFileFormat(msg) => write!(f, "Invalid file format: {msg}"),
250 ScanError::UploadFailed(msg) => write!(f, "Upload failed: {msg}"),
251 ScanError::ScanFailed(msg) => write!(f, "Scan failed: {msg}"),
252 ScanError::PreScanFailed(msg) => write!(f, "Pre-scan failed: {msg}"),
253 ScanError::BuildNotFound => write!(f, "Build not found"),
254 ScanError::ApplicationNotFound => write!(f, "Application not found"),
255 ScanError::SandboxNotFound => write!(f, "Sandbox not found"),
256 ScanError::Unauthorized => write!(f, "Unauthorized access"),
257 ScanError::PermissionDenied => write!(f, "Permission denied"),
258 ScanError::InvalidParameter(msg) => write!(f, "Invalid parameter: {msg}"),
259 ScanError::FileTooLarge(msg) => write!(f, "File too large: {msg}"),
260 ScanError::UploadInProgress => write!(f, "Upload or prescan already in progress"),
261 ScanError::ScanInProgress => write!(f, "Scan in progress, cannot upload"),
262 ScanError::BuildCreationFailed(msg) => write!(f, "Build creation failed: {msg}"),
263 ScanError::ChunkedUploadFailed(msg) => write!(f, "Chunked upload failed: {msg}"),
264 }
265 }
266}
267
268impl std::error::Error for ScanError {}
269
270impl From<VeracodeError> for ScanError {
271 fn from(err: VeracodeError) -> Self {
272 ScanError::Api(err)
273 }
274}
275
276impl From<reqwest::Error> for ScanError {
277 fn from(err: reqwest::Error) -> Self {
278 ScanError::Api(VeracodeError::Http(err))
279 }
280}
281
282impl From<serde_json::Error> for ScanError {
283 fn from(err: serde_json::Error) -> Self {
284 ScanError::Api(VeracodeError::Serialization(err))
285 }
286}
287
288impl From<std::io::Error> for ScanError {
289 fn from(err: std::io::Error) -> Self {
290 ScanError::FileNotFound(err.to_string())
291 }
292}
293
294pub struct ScanApi {
296 client: VeracodeClient,
297}
298
299impl ScanApi {
300 #[must_use]
307 pub fn new(client: VeracodeClient) -> Self {
308 Self { client }
309 }
310
311 fn validate_filename(filename: &str) -> Result<(), ScanError> {
313 validate_url_segment(filename, 255)
315 .map_err(|e| ScanError::InvalidParameter(format!("Invalid filename: {}", e)))?;
316 Ok(())
317 }
318
319 pub async fn upload_file(
334 &self,
335 request: &UploadFileRequest,
336 ) -> Result<UploadedFile, ScanError> {
337 if let Some(save_as) = &request.save_as {
339 Self::validate_filename(save_as)?;
340 }
341
342 let endpoint = "/api/5.0/uploadfile.do";
343
344 let mut query_params = Vec::new();
346 query_params.push(("app_id", request.app_id.as_str()));
347
348 if let Some(sandbox_id) = &request.sandbox_id {
349 query_params.push(("sandbox_id", sandbox_id.as_str()));
350 }
351
352 if let Some(save_as) = &request.save_as {
353 query_params.push(("save_as", save_as.as_str()));
354 }
355
356 let file_data = tokio::fs::read(&request.file_path).await.map_err(|e| {
358 if e.kind() == std::io::ErrorKind::NotFound {
359 ScanError::FileNotFound(request.file_path.clone())
360 } else {
361 ScanError::from(e)
362 }
363 })?;
364
365 let filename = Path::new(&request.file_path)
367 .file_name()
368 .and_then(|f| f.to_str())
369 .unwrap_or("file");
370
371 let response = self
372 .client
373 .upload_file_with_query_params(endpoint, &query_params, "file", filename, file_data)
374 .await?;
375
376 let status = response.status().as_u16();
377 match status {
378 200 => {
379 let response_text = response.text().await?;
380 self.parse_upload_response(&response_text, &request.file_path)
381 .await
382 }
383 400 => {
384 let error_text = response.text().await.unwrap_or_default();
385 Err(ScanError::InvalidParameter(error_text))
386 }
387 401 => Err(ScanError::Unauthorized),
388 403 => Err(ScanError::PermissionDenied),
389 404 => {
390 if request.sandbox_id.is_some() {
391 Err(ScanError::SandboxNotFound)
392 } else {
393 Err(ScanError::ApplicationNotFound)
394 }
395 }
396 _ => {
397 let error_text = response.text().await.unwrap_or_default();
398 Err(ScanError::UploadFailed(format!(
399 "HTTP {status}: {error_text}"
400 )))
401 }
402 }
403 }
404
405 pub async fn upload_large_file(
424 &self,
425 request: UploadLargeFileRequest,
426 ) -> Result<UploadedFile, ScanError> {
427 if let Some(filename) = &request.filename {
429 Self::validate_filename(filename)?;
430 }
431
432 let file_metadata = tokio::fs::metadata(&request.file_path).await.map_err(|e| {
435 if e.kind() == std::io::ErrorKind::NotFound {
436 ScanError::FileNotFound(request.file_path.clone())
437 } else {
438 ScanError::from(e)
439 }
440 })?;
441 let file_size = file_metadata.len();
442 const MAX_FILE_SIZE: u64 = 2 * 1024 * 1024 * 1024; if file_size > MAX_FILE_SIZE {
445 return Err(ScanError::FileTooLarge(format!(
446 "File size {file_size} bytes exceeds 2GB limit"
447 )));
448 }
449
450 let endpoint = "uploadlargefile.do"; let mut query_params = Vec::new();
454 query_params.push(("app_id", request.app_id.as_str()));
455
456 if let Some(sandbox_id) = &request.sandbox_id {
457 query_params.push(("sandbox_id", sandbox_id.as_str()));
458 }
459
460 if let Some(filename) = &request.filename {
461 query_params.push(("filename", filename.as_str()));
462 }
463
464 let file_data = tokio::fs::read(&request.file_path).await.map_err(|e| {
466 if e.kind() == std::io::ErrorKind::NotFound {
467 ScanError::FileNotFound(request.file_path.clone())
468 } else {
469 ScanError::from(e)
470 }
471 })?;
472
473 let response = self
474 .client
475 .upload_file_binary(endpoint, &query_params, file_data, "binary/octet-stream")
476 .await?;
477
478 let status = response.status().as_u16();
479 match status {
480 200 => {
481 let response_text = response.text().await?;
482 self.parse_upload_response(&response_text, &request.file_path)
483 .await
484 }
485 400 => {
486 let error_text = response.text().await.unwrap_or_default();
487 if error_text.contains("upload or prescan in progress") {
488 Err(ScanError::UploadInProgress)
489 } else if error_text.contains("scan in progress") {
490 Err(ScanError::ScanInProgress)
491 } else {
492 Err(ScanError::InvalidParameter(error_text))
493 }
494 }
495 401 => Err(ScanError::Unauthorized),
496 403 => Err(ScanError::PermissionDenied),
497 404 => {
498 if request.sandbox_id.is_some() {
499 Err(ScanError::SandboxNotFound)
500 } else {
501 Err(ScanError::ApplicationNotFound)
502 }
503 }
504 413 => Err(ScanError::FileTooLarge(
505 "File size exceeds server limits".to_string(),
506 )),
507 _ => {
508 let error_text = response.text().await.unwrap_or_default();
509 Err(ScanError::UploadFailed(format!(
510 "HTTP {status}: {error_text}"
511 )))
512 }
513 }
514 }
515
516 pub async fn upload_large_file_with_progress<F>(
545 &self,
546 request: UploadLargeFileRequest,
547 progress_callback: F,
548 ) -> Result<UploadedFile, ScanError>
549 where
550 F: Fn(u64, u64, f64) + Send + Sync,
551 {
552 if let Some(filename) = &request.filename {
554 Self::validate_filename(filename)?;
555 }
556
557 let file_metadata = tokio::fs::metadata(&request.file_path).await.map_err(|e| {
560 if e.kind() == std::io::ErrorKind::NotFound {
561 ScanError::FileNotFound(request.file_path.clone())
562 } else {
563 ScanError::from(e)
564 }
565 })?;
566 let file_size = file_metadata.len();
567 const MAX_FILE_SIZE: u64 = 2 * 1024 * 1024 * 1024; if file_size > MAX_FILE_SIZE {
570 return Err(ScanError::FileTooLarge(format!(
571 "File size {file_size} bytes exceeds 2GB limit"
572 )));
573 }
574
575 let endpoint = "uploadlargefile.do";
576
577 let mut query_params = Vec::new();
579 query_params.push(("app_id", request.app_id.as_str()));
580
581 if let Some(sandbox_id) = &request.sandbox_id {
582 query_params.push(("sandbox_id", sandbox_id.as_str()));
583 }
584
585 if let Some(filename) = &request.filename {
586 query_params.push(("filename", filename.as_str()));
587 }
588
589 let response = self
590 .client
591 .upload_large_file_chunked(
592 endpoint,
593 &query_params,
594 &request.file_path,
595 Some("binary/octet-stream"),
596 Some(progress_callback),
597 )
598 .await?;
599
600 let status = response.status().as_u16();
601 match status {
602 200 => {
603 let response_text = response.text().await?;
604 self.parse_upload_response(&response_text, &request.file_path)
605 .await
606 }
607 400 => {
608 let error_text = response.text().await.unwrap_or_default();
609 if error_text.contains("upload or prescan in progress") {
610 Err(ScanError::UploadInProgress)
611 } else if error_text.contains("scan in progress") {
612 Err(ScanError::ScanInProgress)
613 } else {
614 Err(ScanError::InvalidParameter(error_text))
615 }
616 }
617 401 => Err(ScanError::Unauthorized),
618 403 => Err(ScanError::PermissionDenied),
619 404 => {
620 if request.sandbox_id.is_some() {
621 Err(ScanError::SandboxNotFound)
622 } else {
623 Err(ScanError::ApplicationNotFound)
624 }
625 }
626 413 => Err(ScanError::FileTooLarge(
627 "File size exceeds server limits".to_string(),
628 )),
629 _ => {
630 let error_text = response.text().await.unwrap_or_default();
631 Err(ScanError::ChunkedUploadFailed(format!(
632 "HTTP {status}: {error_text}"
633 )))
634 }
635 }
636 }
637
638 pub async fn upload_file_smart(
656 &self,
657 request: &UploadFileRequest,
658 ) -> Result<UploadedFile, ScanError> {
659 let file_metadata = tokio::fs::metadata(&request.file_path).await.map_err(|e| {
662 if e.kind() == std::io::ErrorKind::NotFound {
663 ScanError::FileNotFound(request.file_path.clone())
664 } else {
665 ScanError::from(e)
666 }
667 })?;
668 let file_size = file_metadata.len();
669
670 const LARGE_FILE_THRESHOLD: u64 = 100 * 1024 * 1024; if file_size > LARGE_FILE_THRESHOLD {
674 let large_request = UploadLargeFileRequest::from(request);
676
677 match self.upload_large_file(large_request).await {
679 Ok(result) => Ok(result),
680 Err(ScanError::Api(_)) => {
681 self.upload_file(request).await
683 }
684 Err(e) => Err(e),
685 }
686 } else {
687 self.upload_file(request).await
689 }
690 }
691
692 pub async fn begin_prescan(&self, request: &BeginPreScanRequest) -> Result<(), ScanError> {
707 let endpoint = "/api/5.0/beginprescan.do";
708
709 let mut query_params = Vec::new();
711 query_params.push(("app_id", request.app_id.as_str()));
712
713 if let Some(sandbox_id) = &request.sandbox_id {
714 query_params.push(("sandbox_id", sandbox_id.as_str()));
715 }
716
717 if let Some(auto_scan) = request.auto_scan {
718 query_params.push(("auto_scan", if auto_scan { "true" } else { "false" }));
719 }
720
721 if let Some(scan_all) = request.scan_all_nonfatal_top_level_modules {
722 query_params.push((
723 "scan_all_nonfatal_top_level_modules",
724 if scan_all { "true" } else { "false" },
725 ));
726 }
727
728 if let Some(include_new) = request.include_new_modules {
729 query_params.push((
730 "include_new_modules",
731 if include_new { "true" } else { "false" },
732 ));
733 }
734
735 let response = self.client.get_with_params(endpoint, &query_params).await?;
736
737 let status = response.status().as_u16();
738 match status {
739 200 => {
740 let response_text = response.text().await?;
741 self.validate_scan_response(&response_text)?;
744 Ok(())
745 }
746 400 => {
747 let error_text = response.text().await.unwrap_or_default();
748 Err(ScanError::InvalidParameter(error_text))
749 }
750 401 => Err(ScanError::Unauthorized),
751 403 => Err(ScanError::PermissionDenied),
752 404 => {
753 if request.sandbox_id.is_some() {
754 Err(ScanError::SandboxNotFound)
755 } else {
756 Err(ScanError::ApplicationNotFound)
757 }
758 }
759 _ => {
760 let error_text = response.text().await.unwrap_or_default();
761 Err(ScanError::PreScanFailed(format!(
762 "HTTP {status}: {error_text}"
763 )))
764 }
765 }
766 }
767
768 pub async fn get_prescan_results(
785 &self,
786 app_id: &str,
787 sandbox_id: Option<&str>,
788 build_id: Option<&str>,
789 ) -> Result<PreScanResults, ScanError> {
790 let endpoint = "/api/5.0/getprescanresults.do";
791
792 let mut params = Vec::new();
793 params.push(("app_id", app_id));
794
795 if let Some(sandbox_id) = sandbox_id {
796 params.push(("sandbox_id", sandbox_id));
797 }
798
799 if let Some(build_id) = build_id {
800 params.push(("build_id", build_id));
801 }
802
803 let response = self.client.get_with_params(endpoint, ¶ms).await?;
804
805 let status = response.status().as_u16();
806 match status {
807 200 => {
808 let response_text = response.text().await?;
809 self.parse_prescan_results(&response_text, app_id, sandbox_id)
810 }
811 401 => Err(ScanError::Unauthorized),
812 403 => Err(ScanError::PermissionDenied),
813 404 => {
814 if sandbox_id.is_some() {
815 Err(ScanError::SandboxNotFound)
816 } else {
817 Err(ScanError::ApplicationNotFound)
818 }
819 }
820 _ => {
821 let error_text = response.text().await.unwrap_or_default();
822 Err(ScanError::PreScanFailed(format!(
823 "HTTP {status}: {error_text}"
824 )))
825 }
826 }
827 }
828
829 pub async fn begin_scan(&self, request: &BeginScanRequest) -> Result<(), ScanError> {
844 let endpoint = "/api/5.0/beginscan.do";
845
846 let mut query_params = Vec::new();
848 query_params.push(("app_id", request.app_id.as_str()));
849
850 if let Some(sandbox_id) = &request.sandbox_id {
851 query_params.push(("sandbox_id", sandbox_id.as_str()));
852 }
853
854 if let Some(modules) = &request.modules {
855 query_params.push(("modules", modules.as_str()));
856 }
857
858 if let Some(scan_all) = request.scan_all_top_level_modules {
859 query_params.push((
860 "scan_all_top_level_modules",
861 if scan_all { "true" } else { "false" },
862 ));
863 }
864
865 if let Some(scan_all_nonfatal) = request.scan_all_nonfatal_top_level_modules {
866 query_params.push((
867 "scan_all_nonfatal_top_level_modules",
868 if scan_all_nonfatal { "true" } else { "false" },
869 ));
870 }
871
872 if let Some(scan_previous) = request.scan_previously_selected_modules {
873 query_params.push((
874 "scan_previously_selected_modules",
875 if scan_previous { "true" } else { "false" },
876 ));
877 }
878
879 let response = self.client.get_with_params(endpoint, &query_params).await?;
880
881 let status = response.status().as_u16();
882 match status {
883 200 => {
884 let response_text = response.text().await?;
885 self.validate_scan_response(&response_text)?;
888 Ok(())
889 }
890 400 => {
891 let error_text = response.text().await.unwrap_or_default();
892 Err(ScanError::InvalidParameter(error_text))
893 }
894 401 => Err(ScanError::Unauthorized),
895 403 => Err(ScanError::PermissionDenied),
896 404 => {
897 if request.sandbox_id.is_some() {
898 Err(ScanError::SandboxNotFound)
899 } else {
900 Err(ScanError::ApplicationNotFound)
901 }
902 }
903 _ => {
904 let error_text = response.text().await.unwrap_or_default();
905 Err(ScanError::ScanFailed(format!(
906 "HTTP {status}: {error_text}"
907 )))
908 }
909 }
910 }
911
912 pub async fn get_file_list(
929 &self,
930 app_id: &str,
931 sandbox_id: Option<&str>,
932 build_id: Option<&str>,
933 ) -> Result<Vec<UploadedFile>, ScanError> {
934 let endpoint = "/api/5.0/getfilelist.do";
935
936 let mut params = Vec::new();
937 params.push(("app_id", app_id));
938
939 if let Some(sandbox_id) = sandbox_id {
940 params.push(("sandbox_id", sandbox_id));
941 }
942
943 if let Some(build_id) = build_id {
944 params.push(("build_id", build_id));
945 }
946
947 let response = self.client.get_with_params(endpoint, ¶ms).await?;
948
949 let status = response.status().as_u16();
950 match status {
951 200 => {
952 let response_text = response.text().await?;
953 self.parse_file_list(&response_text)
954 }
955 401 => Err(ScanError::Unauthorized),
956 403 => Err(ScanError::PermissionDenied),
957 404 => {
958 if sandbox_id.is_some() {
959 Err(ScanError::SandboxNotFound)
960 } else {
961 Err(ScanError::ApplicationNotFound)
962 }
963 }
964 _ => {
965 let error_text = response.text().await.unwrap_or_default();
966 Err(ScanError::Api(VeracodeError::InvalidResponse(format!(
967 "HTTP {status}: {error_text}"
968 ))))
969 }
970 }
971 }
972
973 pub async fn remove_file(
990 &self,
991 app_id: &str,
992 file_id: &str,
993 sandbox_id: Option<&str>,
994 ) -> Result<(), ScanError> {
995 let endpoint = "/api/5.0/removefile.do";
996
997 let mut query_params = Vec::new();
999 query_params.push(("app_id", app_id));
1000 query_params.push(("file_id", file_id));
1001
1002 if let Some(sandbox_id) = sandbox_id {
1003 query_params.push(("sandbox_id", sandbox_id));
1004 }
1005
1006 let response = self.client.get_with_params(endpoint, &query_params).await?;
1007
1008 let status = response.status().as_u16();
1009 match status {
1010 200 => Ok(()),
1011 400 => {
1012 let error_text = response.text().await.unwrap_or_default();
1013 Err(ScanError::InvalidParameter(error_text))
1014 }
1015 401 => Err(ScanError::Unauthorized),
1016 403 => Err(ScanError::PermissionDenied),
1017 404 => Err(ScanError::FileNotFound(file_id.to_string())),
1018 _ => {
1019 let error_text = response.text().await.unwrap_or_default();
1020 Err(ScanError::Api(VeracodeError::InvalidResponse(format!(
1021 "HTTP {status}: {error_text}"
1022 ))))
1023 }
1024 }
1025 }
1026
1027 pub async fn delete_build(
1046 &self,
1047 app_id: &str,
1048 build_id: &str,
1049 sandbox_id: Option<&str>,
1050 ) -> Result<(), ScanError> {
1051 let endpoint = "/api/5.0/deletebuild.do";
1052
1053 let mut query_params = Vec::new();
1055 query_params.push(("app_id", app_id));
1056 query_params.push(("build_id", build_id));
1057
1058 if let Some(sandbox_id) = sandbox_id {
1059 query_params.push(("sandbox_id", sandbox_id));
1060 }
1061
1062 let response = self.client.get_with_params(endpoint, &query_params).await?;
1063
1064 let status = response.status().as_u16();
1065 match status {
1066 200 => Ok(()),
1067 400 => {
1068 let error_text = response.text().await.unwrap_or_default();
1069 Err(ScanError::InvalidParameter(error_text))
1070 }
1071 401 => Err(ScanError::Unauthorized),
1072 403 => Err(ScanError::PermissionDenied),
1073 404 => Err(ScanError::BuildNotFound),
1074 _ => {
1075 let error_text = response.text().await.unwrap_or_default();
1076 Err(ScanError::Api(VeracodeError::InvalidResponse(format!(
1077 "HTTP {status}: {error_text}"
1078 ))))
1079 }
1080 }
1081 }
1082
1083 pub async fn delete_all_builds(
1102 &self,
1103 app_id: &str,
1104 sandbox_id: Option<&str>,
1105 ) -> Result<(), ScanError> {
1106 let build_info = self.get_build_info(app_id, None, sandbox_id).await?;
1108
1109 if !build_info.build_id.is_empty() && build_info.build_id != "unknown" {
1110 info!("Deleting build: {}", build_info.build_id);
1111 self.delete_build(app_id, &build_info.build_id, sandbox_id)
1112 .await?;
1113 }
1114
1115 Ok(())
1116 }
1117
1118 pub async fn get_build_info(
1135 &self,
1136 app_id: &str,
1137 build_id: Option<&str>,
1138 sandbox_id: Option<&str>,
1139 ) -> Result<ScanInfo, ScanError> {
1140 let endpoint = "/api/5.0/getbuildinfo.do";
1141
1142 let mut params = Vec::new();
1143 params.push(("app_id", app_id));
1144
1145 if let Some(build_id) = build_id {
1146 params.push(("build_id", build_id));
1147 }
1148
1149 if let Some(sandbox_id) = sandbox_id {
1150 params.push(("sandbox_id", sandbox_id));
1151 }
1152
1153 let response = self.client.get_with_params(endpoint, ¶ms).await?;
1154
1155 let status = response.status().as_u16();
1156 match status {
1157 200 => {
1158 let response_text = response.text().await?;
1159 self.parse_build_info(&response_text, app_id, sandbox_id)
1160 }
1161 401 => Err(ScanError::Unauthorized),
1162 403 => Err(ScanError::PermissionDenied),
1163 404 => Err(ScanError::BuildNotFound),
1164 _ => {
1165 let error_text = response.text().await.unwrap_or_default();
1166 Err(ScanError::Api(VeracodeError::InvalidResponse(format!(
1167 "HTTP {status}: {error_text}"
1168 ))))
1169 }
1170 }
1171 }
1172
1173 async fn parse_upload_response(
1176 &self,
1177 xml: &str,
1178 file_path: &str,
1179 ) -> Result<UploadedFile, ScanError> {
1180 let mut reader = Reader::from_str(xml);
1181 reader.config_mut().trim_text(true);
1182
1183 let mut buf = Vec::new();
1184 let mut file_id = None;
1185 let mut file_status = "Unknown".to_string();
1186 let mut _md5: Option<String> = None;
1187
1188 loop {
1189 match reader.read_event_into(&mut buf) {
1190 Ok(Event::Start(ref e)) => {
1191 if e.name().as_ref() == b"file" {
1192 for attr in e.attributes().flatten() {
1194 if attr.key.as_ref() == b"file_id" {
1195 file_id = Some(attr_to_string(&attr.value));
1196 }
1197 }
1198 }
1199 }
1200 Ok(Event::Text(e)) => {
1201 let text = std::str::from_utf8(&e).unwrap_or_default();
1202 if text.contains("successfully uploaded") {
1204 file_status = "Uploaded".to_string();
1205 } else if text.contains("error") || text.contains("failed") {
1206 file_status = "Failed".to_string();
1207 }
1208 }
1209 Ok(Event::Eof) => break,
1210 Err(e) => {
1211 error!("Error parsing XML: {e}");
1212 break;
1213 }
1214 _ => {}
1215 }
1216 buf.clear();
1217 }
1218
1219 let filename = Path::new(file_path)
1220 .file_name()
1221 .and_then(|f| f.to_str())
1222 .unwrap_or("file")
1223 .to_string();
1224
1225 Ok(UploadedFile {
1226 file_id: file_id.unwrap_or_else(|| format!("file_{}", chrono::Utc::now().timestamp())),
1227 file_name: filename,
1228 file_size: tokio::fs::metadata(file_path)
1229 .await
1230 .map(|m| m.len())
1231 .unwrap_or(0),
1232 uploaded: Utc::now(),
1233 file_status,
1234 md5: None,
1235 })
1236 }
1237
1238 fn validate_scan_response(&self, xml: &str) -> Result<(), ScanError> {
1245 if xml.contains("<error>") {
1247 let mut reader = Reader::from_str(xml);
1249 reader.config_mut().trim_text(true);
1250
1251 let mut buf = Vec::new();
1252 let mut in_error = false;
1253 let mut error_message = String::new();
1254
1255 loop {
1256 match reader.read_event_into(&mut buf) {
1257 Ok(Event::Start(ref e)) if e.name().as_ref() == b"error" => {
1258 in_error = true;
1259 }
1260 Ok(Event::Text(ref e)) if in_error => {
1261 error_message.push_str(&String::from_utf8_lossy(e));
1262 }
1263 Ok(Event::End(ref e)) if e.name().as_ref() == b"error" => {
1264 break;
1265 }
1266 Ok(Event::Eof) => break,
1267 Err(e) => {
1268 return Err(ScanError::ScanFailed(format!("XML parsing error: {e}")));
1269 }
1270 _ => {}
1271 }
1272 buf.clear();
1273 }
1274
1275 if !error_message.is_empty() {
1276 return Err(ScanError::ScanFailed(error_message));
1277 }
1278 return Err(ScanError::ScanFailed(
1279 "Unknown error in scan response".to_string(),
1280 ));
1281 }
1282
1283 if xml.contains("<buildinfo") || xml.contains("<build") {
1285 Ok(())
1286 } else {
1287 Err(ScanError::ScanFailed(
1288 "Invalid scan response format".to_string(),
1289 ))
1290 }
1291 }
1292
1293 fn parse_module_from_attributes<'a>(
1295 &self,
1296 attributes: impl Iterator<
1297 Item = Result<
1298 quick_xml::events::attributes::Attribute<'a>,
1299 quick_xml::events::attributes::AttrError,
1300 >,
1301 >,
1302 has_fatal_errors: &mut bool,
1303 ) -> ScanModule {
1304 let mut module = ScanModule {
1305 id: String::new(),
1306 name: String::new(),
1307 module_type: String::new(),
1308 is_fatal: false,
1309 selected: false,
1310 size: None,
1311 platform: None,
1312 };
1313
1314 for attr in attributes.flatten() {
1315 match attr.key.as_ref() {
1316 b"id" => module.id = attr_to_string(&attr.value),
1317 b"name" => module.name = attr_to_string(&attr.value),
1318 b"type" => module.module_type = attr_to_string(&attr.value),
1319 b"isfatal" => module.is_fatal = attr.value.as_ref() == b"true",
1320 b"selected" => module.selected = attr.value.as_ref() == b"true",
1321 b"has_fatal_errors" => {
1322 if attr.value.as_ref() == b"true" {
1323 *has_fatal_errors = true;
1324 }
1325 }
1326 b"size" => {
1327 if let Ok(size_str) = String::from_utf8(attr.value.to_vec()) {
1328 module.size = size_str.parse().ok();
1329 }
1330 }
1331 b"platform" => module.platform = Some(attr_to_string(&attr.value)),
1332 _ => {}
1333 }
1334 }
1335
1336 module
1337 }
1338
1339 fn parse_prescan_results(
1340 &self,
1341 xml: &str,
1342 app_id: &str,
1343 sandbox_id: Option<&str>,
1344 ) -> Result<PreScanResults, ScanError> {
1345 if xml.contains("<error>") && xml.contains("Prescan results not available") {
1347 return Ok(PreScanResults {
1349 build_id: String::new(),
1350 app_id: app_id.to_string(),
1351 sandbox_id: sandbox_id.map(|s| s.to_string()),
1352 status: "Pre-Scan Submitted".to_string(), modules: Vec::new(),
1354 messages: Vec::new(),
1355 });
1356 }
1357
1358 let mut reader = Reader::from_str(xml);
1359 reader.config_mut().trim_text(true);
1360
1361 let mut buf = Vec::new();
1362 let mut build_id = None;
1363 let mut modules = Vec::new();
1364 let messages = Vec::new();
1365 let mut has_prescan_results = false;
1366 let mut has_fatal_errors = false;
1367
1368 loop {
1369 match reader.read_event_into(&mut buf) {
1370 Ok(Event::Start(ref e)) => {
1371 match e.name().as_ref() {
1372 b"prescanresults" => {
1373 has_prescan_results = true;
1374 for attr in e.attributes().flatten() {
1376 if attr.key.as_ref() == b"build_id" {
1377 build_id = Some(attr_to_string(&attr.value));
1378 }
1379 }
1380 }
1381 b"module" => {
1382 let module = self.parse_module_from_attributes(
1383 e.attributes(),
1384 &mut has_fatal_errors,
1385 );
1386 modules.push(module);
1387 }
1388 _ => {}
1389 }
1390 }
1391 Ok(Event::Empty(ref e)) => {
1392 if e.name().as_ref() == b"module" {
1394 let module = self
1395 .parse_module_from_attributes(e.attributes(), &mut has_fatal_errors);
1396 modules.push(module);
1397 }
1398 }
1399 Ok(Event::Eof) => break,
1400 Err(e) => {
1401 error!("Error parsing XML: {e}");
1402 break;
1403 }
1404 _ => {}
1405 }
1406 buf.clear();
1407 }
1408
1409 let status = if !has_prescan_results {
1411 "Unknown".to_string()
1412 } else if modules.is_empty() {
1413 "Pre-Scan Failed".to_string()
1415 } else if has_fatal_errors {
1416 "Pre-Scan Failed".to_string()
1418 } else {
1419 "Pre-Scan Success".to_string()
1421 };
1422
1423 Ok(PreScanResults {
1424 build_id: build_id.unwrap_or_else(|| "unknown".to_string()),
1425 app_id: app_id.to_string(),
1426 sandbox_id: sandbox_id.map(|s| s.to_string()),
1427 status,
1428 modules,
1429 messages,
1430 })
1431 }
1432
1433 fn parse_file_from_attributes<'a>(
1435 &self,
1436 attributes: impl Iterator<
1437 Item = Result<
1438 quick_xml::events::attributes::Attribute<'a>,
1439 quick_xml::events::attributes::AttrError,
1440 >,
1441 >,
1442 ) -> UploadedFile {
1443 let mut file = UploadedFile {
1444 file_id: String::new(),
1445 file_name: String::new(),
1446 file_size: 0,
1447 uploaded: Utc::now(),
1448 file_status: "Unknown".to_string(),
1449 md5: None,
1450 };
1451
1452 for attr in attributes.flatten() {
1453 match attr.key.as_ref() {
1454 b"file_id" => file.file_id = attr_to_string(&attr.value),
1455 b"file_name" => file.file_name = attr_to_string(&attr.value),
1456 b"file_size" => {
1457 if let Ok(size_str) = String::from_utf8(attr.value.to_vec()) {
1458 file.file_size = size_str.parse().unwrap_or(0);
1459 }
1460 }
1461 b"file_status" => file.file_status = attr_to_string(&attr.value),
1462 b"md5" => file.md5 = Some(String::from_utf8_lossy(&attr.value).to_string()),
1463 _ => {}
1464 }
1465 }
1466
1467 file
1468 }
1469
1470 fn parse_file_list(&self, xml: &str) -> Result<Vec<UploadedFile>, ScanError> {
1471 let mut reader = Reader::from_str(xml);
1472 reader.config_mut().trim_text(true);
1473
1474 let mut buf = Vec::new();
1475 let mut files = Vec::new();
1476
1477 loop {
1478 match reader.read_event_into(&mut buf) {
1479 Ok(Event::Start(ref e)) => {
1480 if e.name().as_ref() == b"file" {
1481 let file = self.parse_file_from_attributes(e.attributes());
1482 files.push(file);
1483 }
1484 }
1485 Ok(Event::Empty(ref e)) => {
1486 if e.name().as_ref() == b"file" {
1488 let file = self.parse_file_from_attributes(e.attributes());
1489 files.push(file);
1490 }
1491 }
1492 Ok(Event::Eof) => break,
1493 Err(e) => {
1494 error!("Error parsing XML: {e}");
1495 break;
1496 }
1497 _ => {}
1498 }
1499 buf.clear();
1500 }
1501
1502 Ok(files)
1503 }
1504
1505 fn parse_build_info(
1506 &self,
1507 xml: &str,
1508 app_id: &str,
1509 sandbox_id: Option<&str>,
1510 ) -> Result<ScanInfo, ScanError> {
1511 let mut reader = Reader::from_str(xml);
1512 reader.config_mut().trim_text(true);
1513
1514 let mut buf = Vec::new();
1515 let mut scan_info = ScanInfo {
1516 build_id: String::new(),
1517 app_id: app_id.to_string(),
1518 sandbox_id: sandbox_id.map(|s| s.to_string()),
1519 status: "Unknown".to_string(),
1520 scan_type: "Static".to_string(),
1521 analysis_unit_id: None,
1522 scan_progress_percentage: None,
1523 scan_start: None,
1524 scan_complete: None,
1525 total_lines_of_code: None,
1526 };
1527
1528 let mut inside_build = false;
1529
1530 loop {
1531 match reader.read_event_into(&mut buf) {
1532 Ok(Event::Start(ref e)) => {
1533 match e.name().as_ref() {
1534 b"buildinfo" => {
1535 for attr in e.attributes().flatten() {
1537 match attr.key.as_ref() {
1538 b"build_id" => scan_info.build_id = attr_to_string(&attr.value),
1539 b"analysis_unit" => {
1540 if scan_info.status == "Unknown" {
1542 scan_info.status = attr_to_string(&attr.value);
1543 }
1544 }
1545 b"analysis_unit_id" => {
1546 scan_info.analysis_unit_id =
1547 Some(attr_to_string(&attr.value))
1548 }
1549 b"scan_progress_percentage" => {
1550 if let Ok(progress_str) =
1551 String::from_utf8(attr.value.to_vec())
1552 {
1553 scan_info.scan_progress_percentage =
1554 progress_str.parse().ok();
1555 }
1556 }
1557 b"total_lines_of_code" => {
1558 if let Ok(lines_str) =
1559 String::from_utf8(attr.value.to_vec())
1560 {
1561 scan_info.total_lines_of_code = lines_str.parse().ok();
1562 }
1563 }
1564 _ => {}
1565 }
1566 }
1567 }
1568 b"build" => {
1569 inside_build = true;
1570 }
1571 b"analysis_unit" => {
1572 for attr in e.attributes().flatten() {
1574 match attr.key.as_ref() {
1575 b"status" => {
1576 scan_info.status = attr_to_string(&attr.value);
1578 }
1579 b"analysis_type" => {
1580 scan_info.scan_type = attr_to_string(&attr.value);
1581 }
1582 _ => {}
1583 }
1584 }
1585 }
1586 _ => {}
1587 }
1588 }
1589 Ok(Event::End(ref e)) => {
1590 if e.name().as_ref() == b"build" {
1591 inside_build = false;
1592 }
1593 }
1594 Ok(Event::Empty(ref e)) => {
1595 if e.name().as_ref() == b"analysis_unit" && inside_build {
1597 for attr in e.attributes().flatten() {
1598 match attr.key.as_ref() {
1599 b"status" => {
1600 scan_info.status = attr_to_string(&attr.value);
1601 }
1602 b"analysis_type" => {
1603 scan_info.scan_type = attr_to_string(&attr.value);
1604 }
1605 _ => {}
1606 }
1607 }
1608 }
1609 }
1610 Ok(Event::Eof) => break,
1611 Err(e) => {
1612 error!("Error parsing XML: {e}");
1613 break;
1614 }
1615 _ => {}
1616 }
1617 buf.clear();
1618 }
1619
1620 Ok(scan_info)
1621 }
1622}
1623
1624impl ScanApi {
1626 pub async fn upload_file_to_sandbox(
1643 &self,
1644 app_id: &str,
1645 file_path: &str,
1646 sandbox_id: &str,
1647 ) -> Result<UploadedFile, ScanError> {
1648 let request = UploadFileRequest {
1649 app_id: app_id.to_string(),
1650 file_path: file_path.to_string(),
1651 save_as: None,
1652 sandbox_id: Some(sandbox_id.to_string()),
1653 };
1654
1655 self.upload_file(&request).await
1656 }
1657
1658 pub async fn upload_file_to_app(
1674 &self,
1675 app_id: &str,
1676 file_path: &str,
1677 ) -> Result<UploadedFile, ScanError> {
1678 let request = UploadFileRequest {
1679 app_id: app_id.to_string(),
1680 file_path: file_path.to_string(),
1681 save_as: None,
1682 sandbox_id: None,
1683 };
1684
1685 self.upload_file(&request).await
1686 }
1687
1688 pub async fn upload_large_file_to_sandbox(
1706 &self,
1707 app_id: &str,
1708 file_path: &str,
1709 sandbox_id: &str,
1710 filename: Option<&str>,
1711 ) -> Result<UploadedFile, ScanError> {
1712 let request = UploadLargeFileRequest {
1713 app_id: app_id.to_string(),
1714 file_path: file_path.to_string(),
1715 filename: filename.map(|s| s.to_string()),
1716 sandbox_id: Some(sandbox_id.to_string()),
1717 };
1718
1719 self.upload_large_file(request).await
1720 }
1721
1722 pub async fn upload_large_file_to_app(
1739 &self,
1740 app_id: &str,
1741 file_path: &str,
1742 filename: Option<&str>,
1743 ) -> Result<UploadedFile, ScanError> {
1744 let request = UploadLargeFileRequest {
1745 app_id: app_id.to_string(),
1746 file_path: file_path.to_string(),
1747 filename: filename.map(|s| s.to_string()),
1748 sandbox_id: None,
1749 };
1750
1751 self.upload_large_file(request).await
1752 }
1753
1754 pub async fn upload_large_file_to_sandbox_with_progress<F>(
1773 &self,
1774 app_id: &str,
1775 file_path: &str,
1776 sandbox_id: &str,
1777 filename: Option<&str>,
1778 progress_callback: F,
1779 ) -> Result<UploadedFile, ScanError>
1780 where
1781 F: Fn(u64, u64, f64) + Send + Sync,
1782 {
1783 let request = UploadLargeFileRequest {
1784 app_id: app_id.to_string(),
1785 file_path: file_path.to_string(),
1786 filename: filename.map(|s| s.to_string()),
1787 sandbox_id: Some(sandbox_id.to_string()),
1788 };
1789
1790 self.upload_large_file_with_progress(request, progress_callback)
1791 .await
1792 }
1793
1794 pub async fn begin_sandbox_prescan(
1810 &self,
1811 app_id: &str,
1812 sandbox_id: &str,
1813 ) -> Result<(), ScanError> {
1814 let request = BeginPreScanRequest {
1815 app_id: app_id.to_string(),
1816 sandbox_id: Some(sandbox_id.to_string()),
1817 auto_scan: Some(true),
1818 scan_all_nonfatal_top_level_modules: Some(true),
1819 include_new_modules: Some(true),
1820 };
1821
1822 self.begin_prescan(&request).await
1823 }
1824
1825 pub async fn begin_sandbox_scan_all_modules(
1841 &self,
1842 app_id: &str,
1843 sandbox_id: &str,
1844 ) -> Result<(), ScanError> {
1845 let request = BeginScanRequest {
1846 app_id: app_id.to_string(),
1847 sandbox_id: Some(sandbox_id.to_string()),
1848 modules: None,
1849 scan_all_top_level_modules: Some(true),
1850 scan_all_nonfatal_top_level_modules: Some(true),
1851 scan_previously_selected_modules: None,
1852 };
1853
1854 self.begin_scan(&request).await
1855 }
1856
1857 pub async fn upload_and_scan_sandbox(
1874 &self,
1875 app_id: &str,
1876 sandbox_id: &str,
1877 file_path: &str,
1878 ) -> Result<String, ScanError> {
1879 info!("Uploading file to sandbox...");
1881 let _uploaded_file = self
1882 .upload_file_to_sandbox(app_id, file_path, sandbox_id)
1883 .await?;
1884
1885 info!("Beginning pre-scan...");
1887 self.begin_sandbox_prescan(app_id, sandbox_id).await?;
1888
1889 tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
1891
1892 info!("Beginning scan...");
1894 self.begin_sandbox_scan_all_modules(app_id, sandbox_id)
1895 .await?;
1896
1897 Ok("build_id_not_available".to_string())
1902 }
1903
1904 pub async fn delete_sandbox_build(
1921 &self,
1922 app_id: &str,
1923 build_id: &str,
1924 sandbox_id: &str,
1925 ) -> Result<(), ScanError> {
1926 self.delete_build(app_id, build_id, Some(sandbox_id)).await
1927 }
1928
1929 pub async fn delete_all_sandbox_builds(
1945 &self,
1946 app_id: &str,
1947 sandbox_id: &str,
1948 ) -> Result<(), ScanError> {
1949 self.delete_all_builds(app_id, Some(sandbox_id)).await
1950 }
1951
1952 pub async fn delete_app_build(&self, app_id: &str, build_id: &str) -> Result<(), ScanError> {
1968 self.delete_build(app_id, build_id, None).await
1969 }
1970
1971 pub async fn delete_all_app_builds(&self, app_id: &str) -> Result<(), ScanError> {
1986 self.delete_all_builds(app_id, None).await
1987 }
1988}
1989
1990#[cfg(test)]
1991mod tests {
1992 use super::*;
1993 use crate::VeracodeConfig;
1994 use proptest::prelude::*;
1995
1996 #[test]
1997 fn test_upload_file_request() {
1998 let request = UploadFileRequest {
1999 app_id: "123".to_string(),
2000 file_path: "/path/to/file.jar".to_string(),
2001 save_as: Some("app.jar".to_string()),
2002 sandbox_id: Some("456".to_string()),
2003 };
2004
2005 assert_eq!(request.app_id, "123");
2006 assert_eq!(request.sandbox_id, Some("456".to_string()));
2007 }
2008
2009 #[test]
2010 fn test_begin_prescan_request() {
2011 let request = BeginPreScanRequest {
2012 app_id: "123".to_string(),
2013 sandbox_id: Some("456".to_string()),
2014 auto_scan: Some(true),
2015 scan_all_nonfatal_top_level_modules: Some(true),
2016 include_new_modules: Some(false),
2017 };
2018
2019 assert_eq!(request.app_id, "123");
2020 assert_eq!(request.auto_scan, Some(true));
2021 }
2022
2023 #[test]
2024 fn test_scan_error_display() {
2025 let error = ScanError::FileNotFound("test.jar".to_string());
2026 assert_eq!(error.to_string(), "File not found: test.jar");
2027
2028 let error = ScanError::UploadFailed("Network error".to_string());
2029 assert_eq!(error.to_string(), "Upload failed: Network error");
2030
2031 let error = ScanError::Unauthorized;
2032 assert_eq!(error.to_string(), "Unauthorized access");
2033
2034 let error = ScanError::BuildNotFound;
2035 assert_eq!(error.to_string(), "Build not found");
2036 }
2037
2038 #[test]
2039 fn test_delete_build_request_structure() {
2040 use crate::{VeracodeClient, VeracodeConfig};
2044
2045 async fn _test_delete_methods() -> Result<(), Box<dyn std::error::Error>> {
2046 let config = VeracodeConfig::new("test", "test");
2047 let client = VeracodeClient::new(config)?;
2048 let api = client.scan_api()?;
2049
2050 let _: Result<(), _> = api
2053 .delete_build("app_id", "build_id", Some("sandbox_id"))
2054 .await;
2055 let _: Result<(), _> = api.delete_all_builds("app_id", Some("sandbox_id")).await;
2056 let _: Result<(), _> = api
2057 .delete_sandbox_build("app_id", "build_id", "sandbox_id")
2058 .await;
2059 let _: Result<(), _> = api.delete_all_sandbox_builds("app_id", "sandbox_id").await;
2060
2061 Ok(())
2062 }
2063
2064 }
2067
2068 #[test]
2069 fn test_upload_large_file_request() {
2070 let request = UploadLargeFileRequest {
2071 app_id: "123".to_string(),
2072 file_path: "/path/to/large_file.jar".to_string(),
2073 filename: Some("custom_name.jar".to_string()),
2074 sandbox_id: Some("456".to_string()),
2075 };
2076
2077 assert_eq!(request.app_id, "123");
2078 assert_eq!(request.filename, Some("custom_name.jar".to_string()));
2079 assert_eq!(request.sandbox_id, Some("456".to_string()));
2080 }
2081
2082 #[test]
2083 fn test_upload_progress() {
2084 let progress = UploadProgress {
2085 bytes_uploaded: 1024,
2086 total_bytes: 2048,
2087 percentage: 50.0,
2088 };
2089
2090 assert_eq!(progress.bytes_uploaded, 1024);
2091 assert_eq!(progress.total_bytes, 2048);
2092 assert_eq!(progress.percentage, 50.0);
2093 }
2094
2095 #[test]
2096 fn test_large_file_scan_error_display() {
2097 let error = ScanError::FileTooLarge("File exceeds 2GB".to_string());
2098 assert_eq!(error.to_string(), "File too large: File exceeds 2GB");
2099
2100 let error = ScanError::UploadInProgress;
2101 assert_eq!(error.to_string(), "Upload or prescan already in progress");
2102
2103 let error = ScanError::ScanInProgress;
2104 assert_eq!(error.to_string(), "Scan in progress, cannot upload");
2105
2106 let error = ScanError::ChunkedUploadFailed("Network error".to_string());
2107 assert_eq!(error.to_string(), "Chunked upload failed: Network error");
2108 }
2109
2110 #[test]
2111 fn test_validate_filename_path_traversal() {
2112 assert!(ScanApi::validate_filename("valid_file.jar").is_ok());
2114 assert!(ScanApi::validate_filename("my-app.war").is_ok());
2115 assert!(ScanApi::validate_filename("file123.zip").is_ok());
2116
2117 assert!(ScanApi::validate_filename("../etc/passwd").is_err());
2119 assert!(ScanApi::validate_filename("test/../file.jar").is_err());
2120 assert!(ScanApi::validate_filename("test/file.jar").is_err());
2121 assert!(ScanApi::validate_filename("test\\file.jar").is_err());
2122 assert!(ScanApi::validate_filename("..\\windows\\system32").is_err());
2123
2124 assert!(ScanApi::validate_filename("test\x00file.jar").is_err());
2126 assert!(ScanApi::validate_filename("test\nfile.jar").is_err());
2127 assert!(ScanApi::validate_filename("test\rfile.jar").is_err());
2128 assert!(ScanApi::validate_filename("test\x1Ffile.jar").is_err());
2129 }
2130
2131 #[tokio::test]
2132 async fn test_large_file_upload_method_signatures() {
2133 async fn _test_large_file_methods() -> Result<(), Box<dyn std::error::Error>> {
2134 let config = VeracodeConfig::new("test", "test");
2135 let client = VeracodeClient::new(config)?;
2136 let api = client.scan_api()?;
2137
2138 let request = UploadLargeFileRequest {
2140 app_id: "123".to_string(),
2141 file_path: "/nonexistent/file.jar".to_string(),
2142 filename: None,
2143 sandbox_id: Some("456".to_string()),
2144 };
2145
2146 let _: Result<UploadedFile, _> = api.upload_large_file(request.clone()).await;
2149 let _: Result<UploadedFile, _> = api
2150 .upload_large_file_to_sandbox("123", "/path", "456", None)
2151 .await;
2152 let _: Result<UploadedFile, _> =
2153 api.upload_large_file_to_app("123", "/path", None).await;
2154
2155 let progress_callback = |bytes_uploaded: u64, total_bytes: u64, percentage: f64| {
2157 debug!("Upload progress: {bytes_uploaded}/{total_bytes} ({percentage:.1}%)");
2158 };
2159 let _: Result<UploadedFile, _> = api
2160 .upload_large_file_with_progress(request, progress_callback)
2161 .await;
2162
2163 Ok(())
2164 }
2165
2166 }
2169
2170 mod proptest_security {
2175 use super::*;
2176
2177 fn malicious_filename_strategy() -> impl Strategy<Value = String> {
2179 prop_oneof![
2180 Just("../etc/passwd".to_string()),
2182 Just("..\\windows\\system32".to_string()),
2183 Just("test/../../../secret".to_string()),
2184 Just("./../../admin".to_string()),
2185 Just("dir/file.jar".to_string()),
2187 Just("dir\\file.exe".to_string()),
2188 Just("test\x00file.jar".to_string()),
2190 Just("test\nfile.jar".to_string()),
2191 Just("test\rfile.jar".to_string()),
2192 Just("test\x1Ffile.jar".to_string()),
2193 Just("..%2F..%2Fetc%2Fpasswd".to_string()),
2195 Just("..%5C..%5Cwindows".to_string()),
2196 Just("..%c0%af..%c0%afetc%c0%afpasswd".to_string()),
2198 Just("..%252F..%252Fetc".to_string()),
2200 Just("..\\/../admin".to_string()),
2202 Just("../".repeat(20)),
2204 Just("\x00file.jar".to_string()),
2206 Just("file.jar\x00.exe".to_string()),
2207 Just("..".to_string()),
2209 Just("../../".to_string()),
2210 Just("/etc/passwd".to_string()),
2211 Just("\\windows\\system32".to_string()),
2212 ]
2213 }
2214
2215 fn valid_filename_strategy() -> impl Strategy<Value = String> {
2217 "[a-zA-Z0-9_-]{1,200}\\.(jar|war|zip|ear|class)".prop_map(|s| s)
2218 }
2219
2220 proptest! {
2221 #![proptest_config(ProptestConfig {
2222 cases: if cfg!(miri) { 5 } else { 1000 },
2223 failure_persistence: None,
2224 .. ProptestConfig::default()
2225 })]
2226
2227 #[test]
2229 fn prop_validate_filename_rejects_path_traversal(
2230 filename in malicious_filename_strategy()
2231 ) {
2232 let result = ScanApi::validate_filename(&filename);
2234 prop_assert!(result.is_err(), "Should reject malicious filename: {}", filename);
2235 }
2236
2237 #[test]
2239 fn prop_validate_filename_accepts_valid(
2240 filename in valid_filename_strategy()
2241 ) {
2242 let result = ScanApi::validate_filename(&filename);
2243 prop_assert!(result.is_ok(), "Should accept valid filename: {}", filename);
2244 }
2245
2246 #[test]
2248 fn prop_validate_filename_rejects_empty(_n in 0..100u32) {
2249 let result = ScanApi::validate_filename("");
2250 prop_assert!(result.is_err(), "Empty filename should be rejected");
2251 }
2252
2253 #[test]
2255 fn prop_validate_filename_rejects_too_long(extra_len in 1..100usize) {
2256 let long_filename = "a".repeat(256_usize.saturating_add(extra_len));
2257 let result = ScanApi::validate_filename(&long_filename);
2258 prop_assert!(result.is_err(), "Filename longer than 255 chars should be rejected");
2259 }
2260
2261 #[test]
2263 fn prop_validate_filename_rejects_double_dot(
2264 prefix in "[a-zA-Z0-9]{0,10}",
2265 suffix in "[a-zA-Z0-9]{0,10}"
2266 ) {
2267 let filename = format!("{}..{}", prefix, suffix);
2268 let result = ScanApi::validate_filename(&filename);
2269 prop_assert!(result.is_err(), "Filename with '..' should be rejected: {}", filename);
2270 }
2271
2272 #[test]
2274 fn prop_validate_filename_rejects_forward_slash(
2275 prefix in "[a-zA-Z0-9]{1,10}",
2276 suffix in "[a-zA-Z0-9]{1,10}"
2277 ) {
2278 let filename = format!("{}/{}", prefix, suffix);
2279 let result = ScanApi::validate_filename(&filename);
2280 prop_assert!(result.is_err(), "Filename with '/' should be rejected: {}", filename);
2281 }
2282
2283 #[test]
2285 fn prop_validate_filename_rejects_backslash(
2286 prefix in "[a-zA-Z0-9]{1,10}",
2287 suffix in "[a-zA-Z0-9]{1,10}"
2288 ) {
2289 let filename = format!("{}\\{}", prefix, suffix);
2290 let result = ScanApi::validate_filename(&filename);
2291 prop_assert!(result.is_err(), "Filename with '\\' should be rejected: {}", filename);
2292 }
2293
2294 #[test]
2296 fn prop_validate_filename_rejects_control_chars(
2297 prefix in "[a-zA-Z0-9]{0,10}",
2298 control_char in 0x00u8..0x20u8,
2299 suffix in "[a-zA-Z0-9]{0,10}"
2300 ) {
2301 let filename = format!("{}{}{}", prefix, control_char as char, suffix);
2302 let result = ScanApi::validate_filename(&filename);
2303 prop_assert!(result.is_err(), "Filename with control char should be rejected");
2304 }
2305 }
2306
2307 proptest! {
2308 #![proptest_config(ProptestConfig {
2309 cases: if cfg!(miri) { 5 } else { 500 },
2310 failure_persistence: None,
2311 .. ProptestConfig::default()
2312 })]
2313
2314 #[test]
2316 fn prop_attr_to_string_valid_utf8(s in ".*") {
2317 let bytes = s.as_bytes();
2318 let result = attr_to_string(bytes);
2319 prop_assert_eq!(&result, &s, "attr_to_string should preserve valid UTF-8");
2320 }
2321
2322 #[test]
2324 fn prop_attr_to_string_invalid_utf8(bytes in prop::collection::vec(any::<u8>(), 0..100)) {
2325 let _result = attr_to_string(&bytes);
2327 prop_assert!(true, "Function should not panic on invalid UTF-8");
2331 }
2332
2333 #[test]
2335 fn prop_file_size_validation(size in 0u64..5_000_000_000u64) {
2336 const MAX_SIZE: u64 = 2 * 1024 * 1024 * 1024; let exceeds_limit = size > MAX_SIZE;
2338
2339 if exceeds_limit {
2341 prop_assert!(size > MAX_SIZE, "Size should exceed 2GB limit");
2342 } else {
2343 prop_assert!(size <= MAX_SIZE, "Size should be within 2GB limit");
2344 }
2345 }
2346
2347 #[test]
2349 fn prop_upload_progress_percentage(
2350 bytes_uploaded in 0u64..1_000_000u64,
2351 total_bytes in 1u64..1_000_000u64
2352 ) {
2353 let bytes_uploaded = bytes_uploaded.min(total_bytes);
2355
2356 #[allow(clippy::cast_precision_loss)]
2357 let percentage = (bytes_uploaded as f64 / total_bytes as f64) * 100.0;
2358
2359 prop_assert!((0.0..=100.0).contains(&percentage),
2360 "Percentage should be in range [0, 100], got {}", percentage);
2361
2362 if bytes_uploaded == 0 {
2363 prop_assert!(percentage == 0.0, "0 bytes should be 0%");
2364 }
2365 if bytes_uploaded == total_bytes {
2366 prop_assert!(percentage == 100.0, "Full upload should be 100%");
2367 }
2368 }
2369
2370 #[test]
2372 fn prop_request_ids_no_path_separators(
2373 app_id in "[a-zA-Z0-9-]{1,50}",
2374 sandbox_id in "[a-zA-Z0-9-]{1,50}"
2375 ) {
2376 prop_assert!(!app_id.contains('/') && !app_id.contains('\\'));
2378 prop_assert!(!sandbox_id.contains('/') && !sandbox_id.contains('\\'));
2379 prop_assert!(!app_id.contains("..") && !sandbox_id.contains(".."));
2380 }
2381
2382 #[test]
2384 fn prop_build_id_parsing_safe(build_id_value in ".*") {
2385 let _xml = format!(r#"<buildinfo build_id="{}" />"#, build_id_value);
2387
2388 let _escaped = build_id_value.replace('&', "&")
2392 .replace('<', "<")
2393 .replace('>', ">");
2394
2395 prop_assert!(true, "String escaping should not panic");
2396 }
2397
2398 #[test]
2400 fn prop_file_path_edge_cases(
2401 path_segments in prop::collection::vec("[a-zA-Z0-9_-]{1,20}", 1..5)
2402 ) {
2403 let path = path_segments.join("/");
2404
2405 prop_assert!(!path.contains(".."), "Generated path should not contain '..'");
2407
2408 let _path_obj = std::path::Path::new(&path);
2410 prop_assert!(true, "Path construction should not panic");
2411 }
2412 }
2413
2414 proptest! {
2415 #![proptest_config(ProptestConfig {
2416 cases: if cfg!(miri) { 5 } else { 500 },
2417 failure_persistence: None,
2418 .. ProptestConfig::default()
2419 })]
2420
2421 #[test]
2423 fn prop_xml_attribute_robustness(
2424 file_id in "[a-zA-Z0-9_-]{1,50}",
2425 file_name in "[a-zA-Z0-9._-]{1,100}",
2426 file_size in 0u64..10_000_000u64
2427 ) {
2428 let xml = format!(
2430 r#"<filelist><file file_id="{}" file_name="{}" file_size="{}" /></filelist>"#,
2431 file_id, file_name, file_size
2432 );
2433
2434 prop_assert!(xml.contains(&file_id));
2436 prop_assert!(xml.contains(&file_name));
2437 }
2438
2439 #[test]
2441 fn prop_status_validation(status in "[A-Za-z ]{1,50}") {
2442 prop_assert!(!status.chars().any(|c| c.is_control()));
2444 }
2445
2446 #[test]
2448 fn prop_module_id_validation(
2449 module_id in "[a-zA-Z0-9_-]{1,100}"
2450 ) {
2451 prop_assert!(module_id.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '-'));
2453 }
2454 }
2455 }
2456
2457 mod boundary_tests {
2462 use super::*;
2463
2464 #[test]
2465 fn test_file_size_exactly_2gb() {
2466 const TWO_GB: u64 = 2 * 1024 * 1024 * 1024;
2467 assert_eq!(TWO_GB, 2_147_483_648);
2469 }
2470
2471 #[test]
2472 fn test_file_size_just_over_2gb() {
2473 const JUST_OVER: u64 = 2 * 1024 * 1024 * 1024 + 1;
2474 const TWO_GB_LIMIT: u64 = 2_147_483_648;
2475 assert_eq!(JUST_OVER, TWO_GB_LIMIT + 1);
2477 }
2478
2479 #[test]
2480 fn test_filename_max_length_boundary() {
2481 let max_len_filename = "a".repeat(255);
2483 assert!(ScanApi::validate_filename(&max_len_filename).is_ok());
2484
2485 let over_max_filename = "a".repeat(256);
2487 assert!(ScanApi::validate_filename(&over_max_filename).is_err());
2488 }
2489
2490 #[test]
2491 fn test_validate_filename_unicode_normalization() {
2492 let tricky = ".\u{2024}./file.jar";
2495 if tricky.contains('/') || tricky.contains('\\') || tricky.contains("..") {
2497 assert!(ScanApi::validate_filename(tricky).is_err());
2498 }
2499 }
2500
2501 #[test]
2502 fn test_validate_filename_homoglyph_attacks() {
2503 let homoglyph_slash = "test\u{FF0F}file.jar";
2507
2508 let result = ScanApi::validate_filename(homoglyph_slash);
2511 assert!(result.is_ok() || result.is_err());
2513 }
2514
2515 #[test]
2516 fn test_attr_to_string_empty() {
2517 let result = attr_to_string(b"");
2518 assert_eq!(result, "");
2519 }
2520
2521 #[test]
2522 fn test_attr_to_string_ascii() {
2523 let result = attr_to_string(b"test123");
2524 assert_eq!(result, "test123");
2525 }
2526
2527 #[test]
2528 fn test_attr_to_string_utf8() {
2529 let result = attr_to_string("hello 世界".as_bytes());
2530 assert_eq!(result, "hello 世界");
2531 }
2532
2533 #[test]
2534 fn test_attr_to_string_invalid_utf8() {
2535 let invalid = &[0xFF, 0xFE, 0xFD];
2537 let result = attr_to_string(invalid);
2538 assert!(result.contains('\u{FFFD}'));
2540 }
2541
2542 #[test]
2543 fn test_upload_progress_zero_bytes() {
2544 let progress = UploadProgress {
2545 bytes_uploaded: 0,
2546 total_bytes: 1000,
2547 percentage: 0.0,
2548 };
2549 assert_eq!(progress.percentage, 0.0);
2550 }
2551
2552 #[test]
2553 fn test_upload_progress_complete() {
2554 let progress = UploadProgress {
2555 bytes_uploaded: 1000,
2556 total_bytes: 1000,
2557 percentage: 100.0,
2558 };
2559 assert_eq!(progress.percentage, 100.0);
2560 }
2561
2562 #[test]
2563 fn test_scan_error_display_all_variants() {
2564 let errors = vec![
2566 ScanError::FileNotFound("test.jar".to_string()),
2567 ScanError::InvalidFileFormat("bad format".to_string()),
2568 ScanError::UploadFailed("network".to_string()),
2569 ScanError::ScanFailed("failed".to_string()),
2570 ScanError::PreScanFailed("prescan".to_string()),
2571 ScanError::BuildNotFound,
2572 ScanError::ApplicationNotFound,
2573 ScanError::SandboxNotFound,
2574 ScanError::Unauthorized,
2575 ScanError::PermissionDenied,
2576 ScanError::InvalidParameter("param".to_string()),
2577 ScanError::FileTooLarge("too big".to_string()),
2578 ScanError::UploadInProgress,
2579 ScanError::ScanInProgress,
2580 ScanError::BuildCreationFailed("failed".to_string()),
2581 ScanError::ChunkedUploadFailed("chunked".to_string()),
2582 ];
2583
2584 for error in errors {
2585 let display = error.to_string();
2586 assert!(!display.is_empty(), "Error display should not be empty");
2587 assert!(
2588 !display.contains("Error"),
2589 "Should have custom message, got: {}",
2590 display
2591 );
2592 }
2593 }
2594 }
2595
2596 mod error_handling_tests {
2601 use super::*;
2602
2603 #[test]
2604 fn test_scan_error_from_veracode_error() {
2605 let ve = VeracodeError::InvalidResponse("test".to_string());
2606 let se: ScanError = ve.into();
2607 assert!(matches!(se, ScanError::Api(_)));
2608 }
2609
2610 #[test]
2611 fn test_scan_error_from_io_error() {
2612 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
2613 let se: ScanError = io_err.into();
2614 assert!(matches!(se, ScanError::FileNotFound(_)));
2615 }
2616
2617 #[test]
2618 fn test_scan_error_must_use() {
2619 fn _check_must_use() -> ScanError {
2622 ScanError::BuildNotFound
2623 }
2624 }
2625 }
2626}