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, PartialEq, Eq, Serialize, Deserialize)]
29pub enum FileStatus {
30 #[serde(rename = "Pending Upload")]
32 PendingUpload,
33 #[serde(rename = "Uploading")]
35 Uploading,
36 #[serde(rename = "Purged")]
38 Purged,
39 #[serde(rename = "Uploaded")]
41 Uploaded,
42 #[serde(rename = "Missing")]
44 Missing,
45 #[serde(rename = "Partial")]
47 Partial,
48 #[serde(rename = "Invalid Checksum")]
50 InvalidChecksum,
51 #[serde(rename = "Invalid Archive")]
53 InvalidArchive,
54 #[serde(rename = "Archive File Within Another Archive")]
56 ArchiveWithinArchive,
57 #[serde(rename = "Archive File with Unsupported Compression")]
59 UnsupportedCompression,
60 #[serde(rename = "Archive File is Password Protected")]
62 PasswordProtected,
63}
64
65impl FileStatus {
66 #[must_use]
68 pub fn is_uploaded(&self) -> bool {
69 matches!(self, FileStatus::Uploaded)
70 }
71
72 #[must_use]
74 pub fn is_error(&self) -> bool {
75 matches!(
76 self,
77 FileStatus::InvalidChecksum
78 | FileStatus::InvalidArchive
79 | FileStatus::ArchiveWithinArchive
80 | FileStatus::UnsupportedCompression
81 | FileStatus::PasswordProtected
82 | FileStatus::Missing
83 | FileStatus::Purged
84 )
85 }
86
87 #[must_use]
89 pub fn is_in_progress(&self) -> bool {
90 matches!(
91 self,
92 FileStatus::PendingUpload | FileStatus::Uploading | FileStatus::Partial
93 )
94 }
95
96 #[must_use]
98 pub fn description(&self) -> &'static str {
99 match self {
100 FileStatus::PendingUpload => "File is pending upload",
101 FileStatus::Uploading => "File is currently being uploaded",
102 FileStatus::Purged => "File has been purged from the platform",
103 FileStatus::Uploaded => "File successfully uploaded and ready for scanning",
104 FileStatus::Missing => "File is missing from the build",
105 FileStatus::Partial => "File upload was only partially completed",
106 FileStatus::InvalidChecksum => "File MD5 checksum validation failed",
107 FileStatus::InvalidArchive => "File is not a valid archive format",
108 FileStatus::ArchiveWithinArchive => "Archive contains nested archives (not allowed)",
109 FileStatus::UnsupportedCompression => "Archive uses unsupported compression algorithm",
110 FileStatus::PasswordProtected => {
111 "Archive is password protected and cannot be processed"
112 }
113 }
114 }
115}
116
117impl std::str::FromStr for FileStatus {
118 type Err = ScanError;
119
120 fn from_str(s: &str) -> Result<Self, Self::Err> {
134 match s {
135 "Pending Upload" => Ok(FileStatus::PendingUpload),
136 "Uploading" => Ok(FileStatus::Uploading),
137 "Purged" => Ok(FileStatus::Purged),
138 "Uploaded" => Ok(FileStatus::Uploaded),
139 "Missing" => Ok(FileStatus::Missing),
140 "Partial" => Ok(FileStatus::Partial),
141 "Invalid Checksum" => Ok(FileStatus::InvalidChecksum),
142 "Invalid Archive" => Ok(FileStatus::InvalidArchive),
143 "Archive File Within Another Archive" => Ok(FileStatus::ArchiveWithinArchive),
144 "Archive File with Unsupported Compression" => Ok(FileStatus::UnsupportedCompression),
145 "Archive File is Password Protected" => Ok(FileStatus::PasswordProtected),
146 _ => Err(ScanError::InvalidParameter(format!(
147 "Unknown file status: {}",
148 s
149 ))),
150 }
151 }
152}
153
154impl std::fmt::Display for FileStatus {
155 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156 let s = match self {
157 FileStatus::PendingUpload => "Pending Upload",
158 FileStatus::Uploading => "Uploading",
159 FileStatus::Purged => "Purged",
160 FileStatus::Uploaded => "Uploaded",
161 FileStatus::Missing => "Missing",
162 FileStatus::Partial => "Partial",
163 FileStatus::InvalidChecksum => "Invalid Checksum",
164 FileStatus::InvalidArchive => "Invalid Archive",
165 FileStatus::ArchiveWithinArchive => "Archive File Within Another Archive",
166 FileStatus::UnsupportedCompression => "Archive File with Unsupported Compression",
167 FileStatus::PasswordProtected => "Archive File is Password Protected",
168 };
169 write!(f, "{}", s)
170 }
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct UploadedFile {
176 pub file_id: String,
178 pub file_name: String,
180 pub file_size: u64,
182 pub uploaded: DateTime<Utc>,
184 pub file_status: FileStatus,
186 pub md5: Option<String>,
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct PreScanResults {
193 pub build_id: String,
195 pub app_id: String,
197 pub sandbox_id: Option<String>,
199 pub status: String,
201 pub modules: Vec<ScanModule>,
203 pub messages: Vec<PreScanMessage>,
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize)]
209pub struct ScanModule {
210 pub id: String,
212 pub name: String,
214 pub module_type: String,
216 pub is_fatal: bool,
218 pub selected: bool,
220 pub size: Option<u64>,
222 pub platform: Option<String>,
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize)]
228pub struct PreScanMessage {
229 pub severity: String,
231 pub text: String,
233 pub module_name: Option<String>,
235}
236
237#[derive(Debug, Clone, Serialize, Deserialize)]
239pub struct ScanInfo {
240 pub build_id: String,
242 pub app_id: String,
244 pub sandbox_id: Option<String>,
246 pub status: String,
248 pub scan_type: String,
250 pub analysis_unit_id: Option<String>,
252 pub scan_progress_percentage: Option<u32>,
254 pub scan_start: Option<DateTime<Utc>>,
256 pub scan_complete: Option<DateTime<Utc>>,
258 pub total_lines_of_code: Option<u64>,
260}
261
262#[derive(Debug, Clone)]
264pub struct UploadFileRequest {
265 pub app_id: String,
267 pub file_path: String,
269 pub save_as: Option<String>,
271 pub sandbox_id: Option<String>,
273}
274
275#[derive(Debug, Clone)]
277pub struct UploadLargeFileRequest {
278 pub app_id: String,
280 pub file_path: String,
282 pub filename: Option<String>,
284 pub sandbox_id: Option<String>,
286}
287
288#[derive(Debug, Clone)]
290pub struct UploadProgress {
291 pub bytes_uploaded: u64,
293 pub total_bytes: u64,
295 pub percentage: f64,
297}
298
299pub trait UploadProgressCallback: Send + Sync {
301 fn on_progress(&self, progress: UploadProgress);
303 fn on_completed(&self);
305 fn on_error(&self, error: &str);
307}
308
309#[derive(Debug, Clone)]
311pub struct BeginPreScanRequest {
312 pub app_id: String,
314 pub sandbox_id: Option<String>,
316 pub auto_scan: Option<bool>,
318 pub scan_all_nonfatal_top_level_modules: Option<bool>,
320 pub include_new_modules: Option<bool>,
322}
323
324#[derive(Debug, Clone)]
326pub struct BeginScanRequest {
327 pub app_id: String,
329 pub sandbox_id: Option<String>,
331 pub modules: Option<String>,
333 pub scan_all_top_level_modules: Option<bool>,
335 pub scan_all_nonfatal_top_level_modules: Option<bool>,
337 pub scan_previously_selected_modules: Option<bool>,
339}
340
341impl From<&UploadFileRequest> for UploadLargeFileRequest {
343 fn from(request: &UploadFileRequest) -> Self {
344 UploadLargeFileRequest {
345 app_id: request.app_id.clone(),
346 file_path: request.file_path.clone(),
347 filename: request.save_as.clone(),
348 sandbox_id: request.sandbox_id.clone(),
349 }
350 }
351}
352
353#[derive(Debug)]
355#[must_use = "Need to handle all error enum types."]
356pub enum ScanError {
357 Api(VeracodeError),
359 FileNotFound(String),
361 InvalidFileFormat(String),
363 UploadFailed(String),
365 ScanFailed(String),
367 PreScanFailed(String),
369 BuildNotFound,
371 ApplicationNotFound,
373 SandboxNotFound,
375 Unauthorized,
377 PermissionDenied,
379 InvalidParameter(String),
381 FileTooLarge(String),
383 UploadInProgress,
385 ScanInProgress,
387 UploadTimeout(String),
389 BuildCreationFailed(String),
391 ChunkedUploadFailed(String),
393}
394
395impl std::fmt::Display for ScanError {
396 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
397 match self {
398 ScanError::Api(err) => write!(f, "API error: {err}"),
399 ScanError::FileNotFound(path) => write!(f, "File not found: {path}"),
400 ScanError::InvalidFileFormat(msg) => write!(f, "Invalid file format: {msg}"),
401 ScanError::UploadFailed(msg) => write!(f, "{msg}"),
402 ScanError::ScanFailed(msg) => write!(f, "Scan failed: {msg}"),
403 ScanError::PreScanFailed(msg) => write!(f, "Pre-scan failed: {msg}"),
404 ScanError::BuildNotFound => write!(f, "Build not found"),
405 ScanError::ApplicationNotFound => write!(f, "Application not found"),
406 ScanError::SandboxNotFound => write!(f, "Sandbox not found"),
407 ScanError::Unauthorized => write!(f, "Unauthorized access"),
408 ScanError::PermissionDenied => write!(f, "Permission denied"),
409 ScanError::InvalidParameter(msg) => write!(f, "Invalid parameter: {msg}"),
410 ScanError::FileTooLarge(msg) => write!(f, "File too large: {msg}"),
411 ScanError::UploadInProgress => write!(f, "Upload or prescan already in progress"),
412 ScanError::ScanInProgress => write!(f, "Scan in progress, cannot upload"),
413 ScanError::UploadTimeout(msg) => write!(f, "Upload timeout: {msg}"),
414 ScanError::BuildCreationFailed(msg) => write!(f, "Build creation failed: {msg}"),
415 ScanError::ChunkedUploadFailed(msg) => write!(f, "Chunked upload failed: {msg}"),
416 }
417 }
418}
419
420impl std::error::Error for ScanError {}
421
422impl From<VeracodeError> for ScanError {
423 fn from(err: VeracodeError) -> Self {
424 ScanError::Api(err)
425 }
426}
427
428impl From<reqwest::Error> for ScanError {
429 fn from(err: reqwest::Error) -> Self {
430 ScanError::Api(VeracodeError::Http(err))
431 }
432}
433
434impl From<serde_json::Error> for ScanError {
435 fn from(err: serde_json::Error) -> Self {
436 ScanError::Api(VeracodeError::Serialization(err))
437 }
438}
439
440impl From<std::io::Error> for ScanError {
441 fn from(err: std::io::Error) -> Self {
442 ScanError::FileNotFound(err.to_string())
443 }
444}
445
446pub struct ScanApi {
448 client: VeracodeClient,
449}
450
451impl ScanApi {
452 #[must_use]
459 pub fn new(client: VeracodeClient) -> Self {
460 Self { client }
461 }
462
463 fn validate_filename(filename: &str) -> Result<(), ScanError> {
465 validate_url_segment(filename, 255)
467 .map_err(|e| ScanError::InvalidParameter(format!("Invalid filename: {}", e)))?;
468 Ok(())
469 }
470
471 pub async fn upload_file(
486 &self,
487 request: &UploadFileRequest,
488 ) -> Result<UploadedFile, ScanError> {
489 if let Some(save_as) = &request.save_as {
491 Self::validate_filename(save_as)?;
492 }
493
494 let endpoint = "/api/5.0/uploadfile.do";
495
496 let mut query_params = Vec::new();
498 query_params.push(("app_id", request.app_id.as_str()));
499
500 if let Some(sandbox_id) = &request.sandbox_id {
501 query_params.push(("sandbox_id", sandbox_id.as_str()));
502 }
503
504 if let Some(save_as) = &request.save_as {
505 query_params.push(("save_as", save_as.as_str()));
506 }
507
508 let file_data = tokio::fs::read(&request.file_path).await.map_err(|e| {
510 if e.kind() == std::io::ErrorKind::NotFound {
511 ScanError::FileNotFound(request.file_path.clone())
512 } else {
513 ScanError::from(e)
514 }
515 })?;
516
517 let filename = Path::new(&request.file_path)
519 .file_name()
520 .and_then(|f| f.to_str())
521 .unwrap_or("file");
522
523 let response = self
524 .client
525 .upload_file_with_query_params(endpoint, &query_params, "file", filename, file_data)
526 .await?;
527
528 let status = response.status().as_u16();
529 match status {
530 200 => {
531 let response_text = response.text().await?;
532 self.parse_upload_response(&response_text, &request.file_path)
533 .await
534 }
535 400 => {
536 let error_text = response.text().await.unwrap_or_default();
537 Err(ScanError::InvalidParameter(error_text))
538 }
539 401 => Err(ScanError::Unauthorized),
540 403 => Err(ScanError::PermissionDenied),
541 404 => {
542 if request.sandbox_id.is_some() {
543 Err(ScanError::SandboxNotFound)
544 } else {
545 Err(ScanError::ApplicationNotFound)
546 }
547 }
548 _ => {
549 let error_text = response.text().await.unwrap_or_default();
550 Err(ScanError::UploadFailed(format!(
551 "HTTP {status}: {error_text}"
552 )))
553 }
554 }
555 }
556
557 pub async fn upload_large_file(
576 &self,
577 request: UploadLargeFileRequest,
578 ) -> Result<UploadedFile, ScanError> {
579 if let Some(filename) = &request.filename {
581 Self::validate_filename(filename)?;
582 }
583
584 let file_metadata = tokio::fs::metadata(&request.file_path).await.map_err(|e| {
587 if e.kind() == std::io::ErrorKind::NotFound {
588 ScanError::FileNotFound(request.file_path.clone())
589 } else {
590 ScanError::from(e)
591 }
592 })?;
593 let file_size = file_metadata.len();
594 const MAX_FILE_SIZE: u64 = 2 * 1024 * 1024 * 1024; if file_size > MAX_FILE_SIZE {
597 return Err(ScanError::FileTooLarge(format!(
598 "File size {file_size} bytes exceeds 2GB limit"
599 )));
600 }
601
602 let endpoint = "/api/5.0/uploadlargefile.do";
603
604 let mut query_params = Vec::new();
606 query_params.push(("app_id", request.app_id.as_str()));
607
608 if let Some(sandbox_id) = &request.sandbox_id {
609 query_params.push(("sandbox_id", sandbox_id.as_str()));
610 }
611
612 if let Some(filename) = &request.filename {
613 query_params.push(("filename", filename.as_str()));
614 }
615
616 let response = self
618 .client
619 .upload_file_streaming(
620 endpoint,
621 &query_params,
622 &request.file_path,
623 file_size,
624 "binary/octet-stream",
625 )
626 .await?;
627
628 let status = response.status().as_u16();
629 match status {
630 200 => {
631 info!("File upload completed (HTTP 200), parsing response...");
633
634 let response_text = response.text().await?;
635
636 let files = self.parse_file_list(&response_text)?;
638
639 let filename = request.filename.as_ref().cloned().unwrap_or_else(|| {
641 Path::new(&request.file_path)
642 .file_name()
643 .and_then(|f| f.to_str())
644 .unwrap_or("file")
645 .to_string()
646 });
647
648 files
650 .into_iter()
651 .find(|f| f.file_name == filename)
652 .ok_or_else(|| {
653 ScanError::UploadFailed(format!(
654 "File '{}' not found in upload response",
655 filename
656 ))
657 })
658 }
659 400 => {
660 let error_text = response.text().await.unwrap_or_default();
661 if error_text.contains("upload or prescan in progress") {
662 Err(ScanError::UploadInProgress)
663 } else if error_text.contains("scan in progress") {
664 Err(ScanError::ScanInProgress)
665 } else {
666 Err(ScanError::InvalidParameter(error_text))
667 }
668 }
669 401 => Err(ScanError::Unauthorized),
670 403 => Err(ScanError::PermissionDenied),
671 404 => {
672 if request.sandbox_id.is_some() {
673 Err(ScanError::SandboxNotFound)
674 } else {
675 Err(ScanError::ApplicationNotFound)
676 }
677 }
678 413 => Err(ScanError::FileTooLarge(
679 "File size exceeds server limits".to_string(),
680 )),
681 _ => {
682 let error_text = response.text().await.unwrap_or_default();
683 Err(ScanError::UploadFailed(format!(
684 "HTTP {status}: {error_text}"
685 )))
686 }
687 }
688 }
689
690 pub async fn upload_large_file_with_progress<F>(
719 &self,
720 request: UploadLargeFileRequest,
721 progress_callback: F,
722 ) -> Result<UploadedFile, ScanError>
723 where
724 F: Fn(u64, u64, f64) + Send + Sync,
725 {
726 if let Some(filename) = &request.filename {
728 Self::validate_filename(filename)?;
729 }
730
731 let file_metadata = tokio::fs::metadata(&request.file_path).await.map_err(|e| {
734 if e.kind() == std::io::ErrorKind::NotFound {
735 ScanError::FileNotFound(request.file_path.clone())
736 } else {
737 ScanError::from(e)
738 }
739 })?;
740 let file_size = file_metadata.len();
741 const MAX_FILE_SIZE: u64 = 2 * 1024 * 1024 * 1024; if file_size > MAX_FILE_SIZE {
744 return Err(ScanError::FileTooLarge(format!(
745 "File size {file_size} bytes exceeds 2GB limit"
746 )));
747 }
748
749 let endpoint = "/api/5.0/uploadlargefile.do";
750
751 let mut query_params = Vec::new();
753 query_params.push(("app_id", request.app_id.as_str()));
754
755 if let Some(sandbox_id) = &request.sandbox_id {
756 query_params.push(("sandbox_id", sandbox_id.as_str()));
757 }
758
759 if let Some(filename) = &request.filename {
760 query_params.push(("filename", filename.as_str()));
761 }
762
763 let response = self
764 .client
765 .upload_large_file_chunked(
766 endpoint,
767 &query_params,
768 &request.file_path,
769 Some("binary/octet-stream"),
770 Some(progress_callback),
771 )
772 .await?;
773
774 let status = response.status().as_u16();
775 match status {
776 200 => {
777 info!("File upload completed (HTTP 200), parsing response...");
779
780 let response_text = response.text().await?;
781
782 let files = self.parse_file_list(&response_text)?;
784
785 let filename = request.filename.as_ref().cloned().unwrap_or_else(|| {
787 Path::new(&request.file_path)
788 .file_name()
789 .and_then(|f| f.to_str())
790 .unwrap_or("file")
791 .to_string()
792 });
793
794 files
796 .into_iter()
797 .find(|f| f.file_name == filename)
798 .ok_or_else(|| {
799 ScanError::UploadFailed(format!(
800 "File '{}' not found in upload response",
801 filename
802 ))
803 })
804 }
805 400 => {
806 let error_text = response.text().await.unwrap_or_default();
807 if error_text.contains("upload or prescan in progress") {
808 Err(ScanError::UploadInProgress)
809 } else if error_text.contains("scan in progress") {
810 Err(ScanError::ScanInProgress)
811 } else {
812 Err(ScanError::InvalidParameter(error_text))
813 }
814 }
815 401 => Err(ScanError::Unauthorized),
816 403 => Err(ScanError::PermissionDenied),
817 404 => {
818 if request.sandbox_id.is_some() {
819 Err(ScanError::SandboxNotFound)
820 } else {
821 Err(ScanError::ApplicationNotFound)
822 }
823 }
824 413 => Err(ScanError::FileTooLarge(
825 "File size exceeds server limits".to_string(),
826 )),
827 _ => {
828 let error_text = response.text().await.unwrap_or_default();
829 Err(ScanError::ChunkedUploadFailed(format!(
830 "HTTP {status}: {error_text}"
831 )))
832 }
833 }
834 }
835
836 pub async fn upload_file_smart(
854 &self,
855 request: &UploadFileRequest,
856 ) -> Result<UploadedFile, ScanError> {
857 let file_metadata = tokio::fs::metadata(&request.file_path).await.map_err(|e| {
860 if e.kind() == std::io::ErrorKind::NotFound {
861 ScanError::FileNotFound(request.file_path.clone())
862 } else {
863 ScanError::from(e)
864 }
865 })?;
866 let file_size = file_metadata.len();
867
868 const LARGE_FILE_THRESHOLD: u64 = 100 * 1024 * 1024; if file_size > LARGE_FILE_THRESHOLD {
872 let large_request = UploadLargeFileRequest::from(request);
874
875 match self.upload_large_file(large_request).await {
877 Ok(result) => Ok(result),
878 Err(ScanError::Api(_)) => {
879 self.upload_file(request).await
881 }
882 Err(e) => Err(e),
883 }
884 } else {
885 self.upload_file(request).await
887 }
888 }
889
890 pub async fn begin_prescan(&self, request: &BeginPreScanRequest) -> Result<(), ScanError> {
905 let endpoint = "/api/5.0/beginprescan.do";
906
907 let mut query_params = Vec::new();
909 query_params.push(("app_id", request.app_id.as_str()));
910
911 if let Some(sandbox_id) = &request.sandbox_id {
912 query_params.push(("sandbox_id", sandbox_id.as_str()));
913 }
914
915 if let Some(auto_scan) = request.auto_scan {
916 query_params.push(("auto_scan", if auto_scan { "true" } else { "false" }));
917 }
918
919 if let Some(scan_all) = request.scan_all_nonfatal_top_level_modules {
920 query_params.push((
921 "scan_all_nonfatal_top_level_modules",
922 if scan_all { "true" } else { "false" },
923 ));
924 }
925
926 if let Some(include_new) = request.include_new_modules {
927 query_params.push((
928 "include_new_modules",
929 if include_new { "true" } else { "false" },
930 ));
931 }
932
933 let response = self.client.get_with_params(endpoint, &query_params).await?;
934
935 let status = response.status().as_u16();
936 match status {
937 200 => {
938 let response_text = response.text().await?;
939 self.validate_scan_response(&response_text)?;
942 Ok(())
943 }
944 400 => {
945 let error_text = response.text().await.unwrap_or_default();
946 Err(ScanError::InvalidParameter(error_text))
947 }
948 401 => Err(ScanError::Unauthorized),
949 403 => Err(ScanError::PermissionDenied),
950 404 => {
951 if request.sandbox_id.is_some() {
952 Err(ScanError::SandboxNotFound)
953 } else {
954 Err(ScanError::ApplicationNotFound)
955 }
956 }
957 _ => {
958 let error_text = response.text().await.unwrap_or_default();
959 Err(ScanError::PreScanFailed(format!(
960 "HTTP {status}: {error_text}"
961 )))
962 }
963 }
964 }
965
966 pub async fn get_prescan_results(
983 &self,
984 app_id: &str,
985 sandbox_id: Option<&str>,
986 build_id: Option<&str>,
987 ) -> Result<PreScanResults, ScanError> {
988 let endpoint = "/api/5.0/getprescanresults.do";
989
990 let mut params = Vec::new();
991 params.push(("app_id", app_id));
992
993 if let Some(sandbox_id) = sandbox_id {
994 params.push(("sandbox_id", sandbox_id));
995 }
996
997 if let Some(build_id) = build_id {
998 params.push(("build_id", build_id));
999 }
1000
1001 let response = self.client.get_with_params(endpoint, ¶ms).await?;
1002
1003 let status = response.status().as_u16();
1004 match status {
1005 200 => {
1006 let response_text = response.text().await?;
1007 self.parse_prescan_results(&response_text, app_id, sandbox_id)
1008 }
1009 401 => Err(ScanError::Unauthorized),
1010 403 => Err(ScanError::PermissionDenied),
1011 404 => {
1012 if sandbox_id.is_some() {
1013 Err(ScanError::SandboxNotFound)
1014 } else {
1015 Err(ScanError::ApplicationNotFound)
1016 }
1017 }
1018 _ => {
1019 let error_text = response.text().await.unwrap_or_default();
1020 Err(ScanError::PreScanFailed(format!(
1021 "HTTP {status}: {error_text}"
1022 )))
1023 }
1024 }
1025 }
1026
1027 pub async fn begin_scan(&self, request: &BeginScanRequest) -> Result<(), ScanError> {
1042 let endpoint = "/api/5.0/beginscan.do";
1043
1044 let mut query_params = Vec::new();
1046 query_params.push(("app_id", request.app_id.as_str()));
1047
1048 if let Some(sandbox_id) = &request.sandbox_id {
1049 query_params.push(("sandbox_id", sandbox_id.as_str()));
1050 }
1051
1052 if let Some(modules) = &request.modules {
1053 query_params.push(("modules", modules.as_str()));
1054 }
1055
1056 if let Some(scan_all) = request.scan_all_top_level_modules {
1057 query_params.push((
1058 "scan_all_top_level_modules",
1059 if scan_all { "true" } else { "false" },
1060 ));
1061 }
1062
1063 if let Some(scan_all_nonfatal) = request.scan_all_nonfatal_top_level_modules {
1064 query_params.push((
1065 "scan_all_nonfatal_top_level_modules",
1066 if scan_all_nonfatal { "true" } else { "false" },
1067 ));
1068 }
1069
1070 if let Some(scan_previous) = request.scan_previously_selected_modules {
1071 query_params.push((
1072 "scan_previously_selected_modules",
1073 if scan_previous { "true" } else { "false" },
1074 ));
1075 }
1076
1077 let response = self.client.get_with_params(endpoint, &query_params).await?;
1078
1079 let status = response.status().as_u16();
1080 match status {
1081 200 => {
1082 let response_text = response.text().await?;
1083 self.validate_scan_response(&response_text)?;
1086 Ok(())
1087 }
1088 400 => {
1089 let error_text = response.text().await.unwrap_or_default();
1090 Err(ScanError::InvalidParameter(error_text))
1091 }
1092 401 => Err(ScanError::Unauthorized),
1093 403 => Err(ScanError::PermissionDenied),
1094 404 => {
1095 if request.sandbox_id.is_some() {
1096 Err(ScanError::SandboxNotFound)
1097 } else {
1098 Err(ScanError::ApplicationNotFound)
1099 }
1100 }
1101 _ => {
1102 let error_text = response.text().await.unwrap_or_default();
1103 Err(ScanError::ScanFailed(format!(
1104 "HTTP {status}: {error_text}"
1105 )))
1106 }
1107 }
1108 }
1109
1110 pub async fn get_file_list(
1127 &self,
1128 app_id: &str,
1129 sandbox_id: Option<&str>,
1130 build_id: Option<&str>,
1131 ) -> Result<Vec<UploadedFile>, ScanError> {
1132 let endpoint = "/api/5.0/getfilelist.do";
1133
1134 let mut params = Vec::new();
1135 params.push(("app_id", app_id));
1136
1137 if let Some(sandbox_id) = sandbox_id {
1138 params.push(("sandbox_id", sandbox_id));
1139 }
1140
1141 if let Some(build_id) = build_id {
1142 params.push(("build_id", build_id));
1143 }
1144
1145 let response = self.client.get_with_params(endpoint, ¶ms).await?;
1146
1147 let status = response.status().as_u16();
1148 match status {
1149 200 => {
1150 let response_text = response.text().await?;
1151 self.parse_file_list(&response_text)
1152 }
1153 401 => Err(ScanError::Unauthorized),
1154 403 => Err(ScanError::PermissionDenied),
1155 404 => {
1156 if sandbox_id.is_some() {
1157 Err(ScanError::SandboxNotFound)
1158 } else {
1159 Err(ScanError::ApplicationNotFound)
1160 }
1161 }
1162 _ => {
1163 let error_text = response.text().await.unwrap_or_default();
1164 Err(ScanError::Api(VeracodeError::InvalidResponse(format!(
1165 "HTTP {status}: {error_text}"
1166 ))))
1167 }
1168 }
1169 }
1170
1171 pub async fn remove_file(
1188 &self,
1189 app_id: &str,
1190 file_id: &str,
1191 sandbox_id: Option<&str>,
1192 ) -> Result<(), ScanError> {
1193 let endpoint = "/api/5.0/removefile.do";
1194
1195 let mut query_params = Vec::new();
1197 query_params.push(("app_id", app_id));
1198 query_params.push(("file_id", file_id));
1199
1200 if let Some(sandbox_id) = sandbox_id {
1201 query_params.push(("sandbox_id", sandbox_id));
1202 }
1203
1204 let response = self.client.get_with_params(endpoint, &query_params).await?;
1205
1206 let status = response.status().as_u16();
1207 match status {
1208 200 => Ok(()),
1209 400 => {
1210 let error_text = response.text().await.unwrap_or_default();
1211 Err(ScanError::InvalidParameter(error_text))
1212 }
1213 401 => Err(ScanError::Unauthorized),
1214 403 => Err(ScanError::PermissionDenied),
1215 404 => Err(ScanError::FileNotFound(file_id.to_string())),
1216 _ => {
1217 let error_text = response.text().await.unwrap_or_default();
1218 Err(ScanError::Api(VeracodeError::InvalidResponse(format!(
1219 "HTTP {status}: {error_text}"
1220 ))))
1221 }
1222 }
1223 }
1224
1225 pub async fn delete_build(
1244 &self,
1245 app_id: &str,
1246 build_id: &str,
1247 sandbox_id: Option<&str>,
1248 ) -> Result<(), ScanError> {
1249 let endpoint = "/api/5.0/deletebuild.do";
1250
1251 let mut query_params = Vec::new();
1253 query_params.push(("app_id", app_id));
1254 query_params.push(("build_id", build_id));
1255
1256 if let Some(sandbox_id) = sandbox_id {
1257 query_params.push(("sandbox_id", sandbox_id));
1258 }
1259
1260 let response = self.client.get_with_params(endpoint, &query_params).await?;
1261
1262 let status = response.status().as_u16();
1263 match status {
1264 200 => Ok(()),
1265 400 => {
1266 let error_text = response.text().await.unwrap_or_default();
1267 Err(ScanError::InvalidParameter(error_text))
1268 }
1269 401 => Err(ScanError::Unauthorized),
1270 403 => Err(ScanError::PermissionDenied),
1271 404 => Err(ScanError::BuildNotFound),
1272 _ => {
1273 let error_text = response.text().await.unwrap_or_default();
1274 Err(ScanError::Api(VeracodeError::InvalidResponse(format!(
1275 "HTTP {status}: {error_text}"
1276 ))))
1277 }
1278 }
1279 }
1280
1281 pub async fn delete_all_builds(
1300 &self,
1301 app_id: &str,
1302 sandbox_id: Option<&str>,
1303 ) -> Result<(), ScanError> {
1304 let build_info = self.get_build_info(app_id, None, sandbox_id).await?;
1306
1307 if !build_info.build_id.is_empty() && build_info.build_id != "unknown" {
1308 info!("Deleting build: {}", build_info.build_id);
1309 self.delete_build(app_id, &build_info.build_id, sandbox_id)
1310 .await?;
1311 }
1312
1313 Ok(())
1314 }
1315
1316 pub async fn get_build_info(
1333 &self,
1334 app_id: &str,
1335 build_id: Option<&str>,
1336 sandbox_id: Option<&str>,
1337 ) -> Result<ScanInfo, ScanError> {
1338 let endpoint = "/api/5.0/getbuildinfo.do";
1339
1340 let mut params = Vec::new();
1341 params.push(("app_id", app_id));
1342
1343 if let Some(build_id) = build_id {
1344 params.push(("build_id", build_id));
1345 }
1346
1347 if let Some(sandbox_id) = sandbox_id {
1348 params.push(("sandbox_id", sandbox_id));
1349 }
1350
1351 let response = self.client.get_with_params(endpoint, ¶ms).await?;
1352
1353 let status = response.status().as_u16();
1354 match status {
1355 200 => {
1356 let response_text = response.text().await?;
1357 self.parse_build_info(&response_text, app_id, sandbox_id)
1358 }
1359 401 => Err(ScanError::Unauthorized),
1360 403 => Err(ScanError::PermissionDenied),
1361 404 => Err(ScanError::BuildNotFound),
1362 _ => {
1363 let error_text = response.text().await.unwrap_or_default();
1364 Err(ScanError::Api(VeracodeError::InvalidResponse(format!(
1365 "HTTP {status}: {error_text}"
1366 ))))
1367 }
1368 }
1369 }
1370
1371 async fn parse_upload_response(
1374 &self,
1375 xml: &str,
1376 file_path: &str,
1377 ) -> Result<UploadedFile, ScanError> {
1378 let mut reader = Reader::from_str(xml);
1379 reader.config_mut().trim_text(true);
1380
1381 let mut buf = Vec::new();
1382 let mut file_id = None;
1383 let mut file_status = FileStatus::PendingUpload;
1384 let mut _md5: Option<String> = None;
1385 let mut current_error: Option<String> = None;
1386 let mut in_error_tag = false;
1387
1388 loop {
1389 match reader.read_event_into(&mut buf) {
1390 Ok(Event::Start(ref e)) => {
1391 if e.name().as_ref() == b"file" {
1392 for attr in e.attributes().flatten() {
1394 match attr.key.as_ref() {
1395 b"file_id" => file_id = Some(attr_to_string(&attr.value)),
1396 b"file_status" => {
1397 let status_str = attr_to_string(&attr.value);
1398 file_status =
1399 status_str.parse().unwrap_or(FileStatus::PendingUpload);
1400 }
1401 _ => {}
1402 }
1403 }
1404 } else if e.name().as_ref() == b"error" {
1405 in_error_tag = true;
1406 }
1407 }
1408 Ok(Event::Text(e)) => {
1409 if in_error_tag {
1410 current_error = Some(String::from_utf8_lossy(&e).to_string());
1411 } else {
1412 let text = std::str::from_utf8(&e).unwrap_or_default();
1413 if text.contains("successfully uploaded") {
1415 file_status = FileStatus::Uploaded;
1416 }
1417 }
1418 }
1419 Ok(Event::End(ref e)) => {
1420 if e.name().as_ref() == b"error" {
1421 in_error_tag = false;
1422 }
1423 }
1424 Ok(Event::Eof) => break,
1425 Err(e) => {
1426 error!("Error parsing XML: {e}");
1427 break;
1428 }
1429 _ => {}
1430 }
1431 buf.clear();
1432 }
1433
1434 if let Some(error_msg) = current_error {
1436 return Err(ScanError::UploadFailed(error_msg));
1437 }
1438
1439 let filename = Path::new(file_path)
1440 .file_name()
1441 .and_then(|f| f.to_str())
1442 .unwrap_or("file")
1443 .to_string();
1444
1445 Ok(UploadedFile {
1446 file_id: file_id.unwrap_or_else(|| format!("file_{}", chrono::Utc::now().timestamp())),
1447 file_name: filename,
1448 file_size: tokio::fs::metadata(file_path)
1449 .await
1450 .map(|m| m.len())
1451 .unwrap_or(0),
1452 uploaded: Utc::now(),
1453 file_status,
1454 md5: None,
1455 })
1456 }
1457
1458 fn validate_scan_response(&self, xml: &str) -> Result<(), ScanError> {
1465 if xml.contains("<error>") {
1467 let mut reader = Reader::from_str(xml);
1469 reader.config_mut().trim_text(true);
1470
1471 let mut buf = Vec::new();
1472 let mut in_error = false;
1473 let mut error_message = String::new();
1474
1475 loop {
1476 match reader.read_event_into(&mut buf) {
1477 Ok(Event::Start(ref e)) if e.name().as_ref() == b"error" => {
1478 in_error = true;
1479 }
1480 Ok(Event::Text(ref e)) if in_error => {
1481 error_message.push_str(&String::from_utf8_lossy(e));
1482 }
1483 Ok(Event::End(ref e)) if e.name().as_ref() == b"error" => {
1484 break;
1485 }
1486 Ok(Event::Eof) => break,
1487 Err(e) => {
1488 return Err(ScanError::ScanFailed(format!("XML parsing error: {e}")));
1489 }
1490 _ => {}
1491 }
1492 buf.clear();
1493 }
1494
1495 if !error_message.is_empty() {
1496 return Err(ScanError::ScanFailed(error_message));
1497 }
1498 return Err(ScanError::ScanFailed(
1499 "Unknown error in scan response".to_string(),
1500 ));
1501 }
1502
1503 if xml.contains("<buildinfo") || xml.contains("<build") {
1505 Ok(())
1506 } else {
1507 Err(ScanError::ScanFailed(
1508 "Invalid scan response format".to_string(),
1509 ))
1510 }
1511 }
1512
1513 fn parse_module_from_attributes<'a>(
1515 &self,
1516 attributes: impl Iterator<
1517 Item = Result<
1518 quick_xml::events::attributes::Attribute<'a>,
1519 quick_xml::events::attributes::AttrError,
1520 >,
1521 >,
1522 has_fatal_errors: &mut bool,
1523 ) -> ScanModule {
1524 let mut module = ScanModule {
1525 id: String::new(),
1526 name: String::new(),
1527 module_type: String::new(),
1528 is_fatal: false,
1529 selected: false,
1530 size: None,
1531 platform: None,
1532 };
1533
1534 for attr in attributes.flatten() {
1535 match attr.key.as_ref() {
1536 b"id" => module.id = attr_to_string(&attr.value),
1537 b"name" => module.name = attr_to_string(&attr.value),
1538 b"type" => module.module_type = attr_to_string(&attr.value),
1539 b"isfatal" => module.is_fatal = attr.value.as_ref() == b"true",
1540 b"selected" => module.selected = attr.value.as_ref() == b"true",
1541 b"has_fatal_errors" => {
1542 if attr.value.as_ref() == b"true" {
1543 *has_fatal_errors = true;
1544 }
1545 }
1546 b"size" => {
1547 if let Ok(size_str) = String::from_utf8(attr.value.to_vec()) {
1548 module.size = size_str.parse().ok();
1549 }
1550 }
1551 b"platform" => module.platform = Some(attr_to_string(&attr.value)),
1552 _ => {}
1553 }
1554 }
1555
1556 module
1557 }
1558
1559 fn parse_prescan_results(
1560 &self,
1561 xml: &str,
1562 app_id: &str,
1563 sandbox_id: Option<&str>,
1564 ) -> Result<PreScanResults, ScanError> {
1565 if xml.contains("<error>") && xml.contains("Prescan results not available") {
1567 return Ok(PreScanResults {
1569 build_id: String::new(),
1570 app_id: app_id.to_string(),
1571 sandbox_id: sandbox_id.map(|s| s.to_string()),
1572 status: "Pre-Scan Submitted".to_string(), modules: Vec::new(),
1574 messages: Vec::new(),
1575 });
1576 }
1577
1578 let mut reader = Reader::from_str(xml);
1579 reader.config_mut().trim_text(true);
1580
1581 let mut buf = Vec::new();
1582 let mut build_id = None;
1583 let mut modules = Vec::new();
1584 let messages = Vec::new();
1585 let mut has_prescan_results = false;
1586 let mut has_fatal_errors = false;
1587
1588 loop {
1589 match reader.read_event_into(&mut buf) {
1590 Ok(Event::Start(ref e)) => {
1591 match e.name().as_ref() {
1592 b"prescanresults" => {
1593 has_prescan_results = true;
1594 for attr in e.attributes().flatten() {
1596 if attr.key.as_ref() == b"build_id" {
1597 build_id = Some(attr_to_string(&attr.value));
1598 }
1599 }
1600 }
1601 b"module" => {
1602 let module = self.parse_module_from_attributes(
1603 e.attributes(),
1604 &mut has_fatal_errors,
1605 );
1606 modules.push(module);
1607 }
1608 _ => {}
1609 }
1610 }
1611 Ok(Event::Empty(ref e)) => {
1612 if e.name().as_ref() == b"module" {
1614 let module = self
1615 .parse_module_from_attributes(e.attributes(), &mut has_fatal_errors);
1616 modules.push(module);
1617 }
1618 }
1619 Ok(Event::Eof) => break,
1620 Err(e) => {
1621 error!("Error parsing XML: {e}");
1622 break;
1623 }
1624 _ => {}
1625 }
1626 buf.clear();
1627 }
1628
1629 let status = if !has_prescan_results {
1631 "Unknown".to_string()
1632 } else if modules.is_empty() {
1633 "Pre-Scan Failed".to_string()
1635 } else if has_fatal_errors {
1636 "Pre-Scan Failed".to_string()
1638 } else {
1639 "Pre-Scan Success".to_string()
1641 };
1642
1643 Ok(PreScanResults {
1644 build_id: build_id.unwrap_or_else(|| "unknown".to_string()),
1645 app_id: app_id.to_string(),
1646 sandbox_id: sandbox_id.map(|s| s.to_string()),
1647 status,
1648 modules,
1649 messages,
1650 })
1651 }
1652
1653 fn parse_file_from_attributes<'a>(
1655 &self,
1656 attributes: impl Iterator<
1657 Item = Result<
1658 quick_xml::events::attributes::Attribute<'a>,
1659 quick_xml::events::attributes::AttrError,
1660 >,
1661 >,
1662 ) -> UploadedFile {
1663 let mut file = UploadedFile {
1664 file_id: String::new(),
1665 file_name: String::new(),
1666 file_size: 0,
1667 uploaded: Utc::now(),
1668 file_status: FileStatus::PendingUpload, md5: None,
1670 };
1671
1672 for attr in attributes.flatten() {
1673 match attr.key.as_ref() {
1674 b"file_id" => file.file_id = attr_to_string(&attr.value),
1675 b"file_name" => file.file_name = attr_to_string(&attr.value),
1676 b"file_size" => {
1677 if let Ok(size_str) = String::from_utf8(attr.value.to_vec()) {
1678 file.file_size = size_str.parse().unwrap_or(0);
1679 }
1680 }
1681 b"file_status" => {
1682 let status_str = attr_to_string(&attr.value);
1683 file.file_status = status_str.parse().unwrap_or_else(|e| {
1685 error!("Unknown file status '{}': {}", status_str, e);
1686 FileStatus::PendingUpload
1687 });
1688 }
1689 b"md5" | b"file_md5" => {
1690 file.md5 = Some(String::from_utf8_lossy(&attr.value).to_string())
1691 }
1692 _ => {}
1693 }
1694 }
1695
1696 file
1697 }
1698
1699 fn parse_file_list(&self, xml: &str) -> Result<Vec<UploadedFile>, ScanError> {
1700 let mut reader = Reader::from_str(xml);
1701 reader.config_mut().trim_text(true);
1702
1703 let mut buf = Vec::new();
1704 let mut files = Vec::new();
1705 let mut current_error: Option<String> = None;
1706 let mut in_error_tag = false;
1707
1708 loop {
1709 match reader.read_event_into(&mut buf) {
1710 Ok(Event::Start(ref e)) => {
1711 if e.name().as_ref() == b"file" {
1712 let file = self.parse_file_from_attributes(e.attributes());
1713 files.push(file);
1714 } else if e.name().as_ref() == b"error" {
1715 in_error_tag = true;
1716 }
1717 }
1718 Ok(Event::Empty(ref e)) => {
1719 if e.name().as_ref() == b"file" {
1721 let file = self.parse_file_from_attributes(e.attributes());
1722 files.push(file);
1723 }
1724 }
1725 Ok(Event::Text(ref e)) => {
1726 if in_error_tag {
1727 current_error = Some(String::from_utf8_lossy(e).to_string());
1728 }
1729 }
1730 Ok(Event::End(ref e)) => {
1731 if e.name().as_ref() == b"error" {
1732 in_error_tag = false;
1733 }
1734 }
1735 Ok(Event::Eof) => break,
1736 Err(e) => {
1737 error!("Error parsing XML: {e}");
1738 break;
1739 }
1740 _ => {}
1741 }
1742 buf.clear();
1743 }
1744
1745 if let Some(error_msg) = current_error {
1747 return Err(ScanError::UploadFailed(error_msg));
1748 }
1749
1750 Ok(files)
1751 }
1752
1753 fn parse_build_info(
1754 &self,
1755 xml: &str,
1756 app_id: &str,
1757 sandbox_id: Option<&str>,
1758 ) -> Result<ScanInfo, ScanError> {
1759 let mut reader = Reader::from_str(xml);
1760 reader.config_mut().trim_text(true);
1761
1762 let mut buf = Vec::new();
1763 let mut scan_info = ScanInfo {
1764 build_id: String::new(),
1765 app_id: app_id.to_string(),
1766 sandbox_id: sandbox_id.map(|s| s.to_string()),
1767 status: "Unknown".to_string(),
1768 scan_type: "Static".to_string(),
1769 analysis_unit_id: None,
1770 scan_progress_percentage: None,
1771 scan_start: None,
1772 scan_complete: None,
1773 total_lines_of_code: None,
1774 };
1775
1776 let mut inside_build = false;
1777
1778 loop {
1779 match reader.read_event_into(&mut buf) {
1780 Ok(Event::Start(ref e)) => {
1781 match e.name().as_ref() {
1782 b"buildinfo" => {
1783 for attr in e.attributes().flatten() {
1785 match attr.key.as_ref() {
1786 b"build_id" => scan_info.build_id = attr_to_string(&attr.value),
1787 b"analysis_unit" => {
1788 if scan_info.status == "Unknown" {
1790 scan_info.status = attr_to_string(&attr.value);
1791 }
1792 }
1793 b"analysis_unit_id" => {
1794 scan_info.analysis_unit_id =
1795 Some(attr_to_string(&attr.value))
1796 }
1797 b"scan_progress_percentage" => {
1798 if let Ok(progress_str) =
1799 String::from_utf8(attr.value.to_vec())
1800 {
1801 scan_info.scan_progress_percentage =
1802 progress_str.parse().ok();
1803 }
1804 }
1805 b"total_lines_of_code" => {
1806 if let Ok(lines_str) =
1807 String::from_utf8(attr.value.to_vec())
1808 {
1809 scan_info.total_lines_of_code = lines_str.parse().ok();
1810 }
1811 }
1812 _ => {}
1813 }
1814 }
1815 }
1816 b"build" => {
1817 inside_build = true;
1818 }
1819 b"analysis_unit" => {
1820 for attr in e.attributes().flatten() {
1822 match attr.key.as_ref() {
1823 b"status" => {
1824 scan_info.status = attr_to_string(&attr.value);
1826 }
1827 b"analysis_type" => {
1828 scan_info.scan_type = attr_to_string(&attr.value);
1829 }
1830 _ => {}
1831 }
1832 }
1833 }
1834 _ => {}
1835 }
1836 }
1837 Ok(Event::End(ref e)) => {
1838 if e.name().as_ref() == b"build" {
1839 inside_build = false;
1840 }
1841 }
1842 Ok(Event::Empty(ref e)) => {
1843 if e.name().as_ref() == b"analysis_unit" && inside_build {
1845 for attr in e.attributes().flatten() {
1846 match attr.key.as_ref() {
1847 b"status" => {
1848 scan_info.status = attr_to_string(&attr.value);
1849 }
1850 b"analysis_type" => {
1851 scan_info.scan_type = attr_to_string(&attr.value);
1852 }
1853 _ => {}
1854 }
1855 }
1856 }
1857 }
1858 Ok(Event::Eof) => break,
1859 Err(e) => {
1860 error!("Error parsing XML: {e}");
1861 break;
1862 }
1863 _ => {}
1864 }
1865 buf.clear();
1866 }
1867
1868 Ok(scan_info)
1869 }
1870}
1871
1872impl ScanApi {
1874 pub async fn upload_file_to_sandbox(
1891 &self,
1892 app_id: &str,
1893 file_path: &str,
1894 sandbox_id: &str,
1895 ) -> Result<UploadedFile, ScanError> {
1896 let request = UploadFileRequest {
1897 app_id: app_id.to_string(),
1898 file_path: file_path.to_string(),
1899 save_as: None,
1900 sandbox_id: Some(sandbox_id.to_string()),
1901 };
1902
1903 self.upload_file(&request).await
1904 }
1905
1906 pub async fn upload_file_to_app(
1922 &self,
1923 app_id: &str,
1924 file_path: &str,
1925 ) -> Result<UploadedFile, ScanError> {
1926 let request = UploadFileRequest {
1927 app_id: app_id.to_string(),
1928 file_path: file_path.to_string(),
1929 save_as: None,
1930 sandbox_id: None,
1931 };
1932
1933 self.upload_file(&request).await
1934 }
1935
1936 pub async fn upload_large_file_to_sandbox(
1954 &self,
1955 app_id: &str,
1956 file_path: &str,
1957 sandbox_id: &str,
1958 filename: Option<&str>,
1959 ) -> Result<UploadedFile, ScanError> {
1960 let request = UploadLargeFileRequest {
1961 app_id: app_id.to_string(),
1962 file_path: file_path.to_string(),
1963 filename: filename.map(|s| s.to_string()),
1964 sandbox_id: Some(sandbox_id.to_string()),
1965 };
1966
1967 self.upload_large_file(request).await
1968 }
1969
1970 pub async fn upload_large_file_to_app(
1987 &self,
1988 app_id: &str,
1989 file_path: &str,
1990 filename: Option<&str>,
1991 ) -> Result<UploadedFile, ScanError> {
1992 let request = UploadLargeFileRequest {
1993 app_id: app_id.to_string(),
1994 file_path: file_path.to_string(),
1995 filename: filename.map(|s| s.to_string()),
1996 sandbox_id: None,
1997 };
1998
1999 self.upload_large_file(request).await
2000 }
2001
2002 pub async fn upload_large_file_to_sandbox_with_progress<F>(
2021 &self,
2022 app_id: &str,
2023 file_path: &str,
2024 sandbox_id: &str,
2025 filename: Option<&str>,
2026 progress_callback: F,
2027 ) -> Result<UploadedFile, ScanError>
2028 where
2029 F: Fn(u64, u64, f64) + Send + Sync,
2030 {
2031 let request = UploadLargeFileRequest {
2032 app_id: app_id.to_string(),
2033 file_path: file_path.to_string(),
2034 filename: filename.map(|s| s.to_string()),
2035 sandbox_id: Some(sandbox_id.to_string()),
2036 };
2037
2038 self.upload_large_file_with_progress(request, progress_callback)
2039 .await
2040 }
2041
2042 pub async fn begin_sandbox_prescan(
2058 &self,
2059 app_id: &str,
2060 sandbox_id: &str,
2061 ) -> Result<(), ScanError> {
2062 let request = BeginPreScanRequest {
2063 app_id: app_id.to_string(),
2064 sandbox_id: Some(sandbox_id.to_string()),
2065 auto_scan: Some(true),
2066 scan_all_nonfatal_top_level_modules: Some(true),
2067 include_new_modules: Some(true),
2068 };
2069
2070 self.begin_prescan(&request).await
2071 }
2072
2073 pub async fn begin_sandbox_scan_all_modules(
2089 &self,
2090 app_id: &str,
2091 sandbox_id: &str,
2092 ) -> Result<(), ScanError> {
2093 let request = BeginScanRequest {
2094 app_id: app_id.to_string(),
2095 sandbox_id: Some(sandbox_id.to_string()),
2096 modules: None,
2097 scan_all_top_level_modules: Some(true),
2098 scan_all_nonfatal_top_level_modules: Some(true),
2099 scan_previously_selected_modules: None,
2100 };
2101
2102 self.begin_scan(&request).await
2103 }
2104
2105 pub async fn upload_and_scan_sandbox(
2122 &self,
2123 app_id: &str,
2124 sandbox_id: &str,
2125 file_path: &str,
2126 ) -> Result<String, ScanError> {
2127 info!("Uploading file to sandbox...");
2129 let _uploaded_file = self
2130 .upload_file_to_sandbox(app_id, file_path, sandbox_id)
2131 .await?;
2132
2133 info!("Beginning pre-scan...");
2135 self.begin_sandbox_prescan(app_id, sandbox_id).await?;
2136
2137 tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
2139
2140 info!("Beginning scan...");
2142 self.begin_sandbox_scan_all_modules(app_id, sandbox_id)
2143 .await?;
2144
2145 Ok("build_id_not_available".to_string())
2150 }
2151
2152 pub async fn delete_sandbox_build(
2169 &self,
2170 app_id: &str,
2171 build_id: &str,
2172 sandbox_id: &str,
2173 ) -> Result<(), ScanError> {
2174 self.delete_build(app_id, build_id, Some(sandbox_id)).await
2175 }
2176
2177 pub async fn delete_all_sandbox_builds(
2193 &self,
2194 app_id: &str,
2195 sandbox_id: &str,
2196 ) -> Result<(), ScanError> {
2197 self.delete_all_builds(app_id, Some(sandbox_id)).await
2198 }
2199
2200 pub async fn delete_app_build(&self, app_id: &str, build_id: &str) -> Result<(), ScanError> {
2216 self.delete_build(app_id, build_id, None).await
2217 }
2218
2219 pub async fn delete_all_app_builds(&self, app_id: &str) -> Result<(), ScanError> {
2234 self.delete_all_builds(app_id, None).await
2235 }
2236}
2237
2238#[cfg(test)]
2239mod tests {
2240 use super::*;
2241 use crate::VeracodeConfig;
2242 use proptest::prelude::*;
2243
2244 #[test]
2245 fn test_upload_file_request() {
2246 let request = UploadFileRequest {
2247 app_id: "123".to_string(),
2248 file_path: "/path/to/file.jar".to_string(),
2249 save_as: Some("app.jar".to_string()),
2250 sandbox_id: Some("456".to_string()),
2251 };
2252
2253 assert_eq!(request.app_id, "123");
2254 assert_eq!(request.sandbox_id, Some("456".to_string()));
2255 }
2256
2257 #[test]
2258 fn test_begin_prescan_request() {
2259 let request = BeginPreScanRequest {
2260 app_id: "123".to_string(),
2261 sandbox_id: Some("456".to_string()),
2262 auto_scan: Some(true),
2263 scan_all_nonfatal_top_level_modules: Some(true),
2264 include_new_modules: Some(false),
2265 };
2266
2267 assert_eq!(request.app_id, "123");
2268 assert_eq!(request.auto_scan, Some(true));
2269 }
2270
2271 #[test]
2272 fn test_scan_error_display() {
2273 let error = ScanError::FileNotFound("test.jar".to_string());
2274 assert_eq!(error.to_string(), "File not found: test.jar");
2275
2276 let error = ScanError::UploadFailed("Network error".to_string());
2277 assert_eq!(error.to_string(), "Network error");
2278
2279 let error = ScanError::Unauthorized;
2280 assert_eq!(error.to_string(), "Unauthorized access");
2281
2282 let error = ScanError::BuildNotFound;
2283 assert_eq!(error.to_string(), "Build not found");
2284 }
2285
2286 #[test]
2287 fn test_delete_build_request_structure() {
2288 use crate::{VeracodeClient, VeracodeConfig};
2292
2293 async fn _test_delete_methods() -> Result<(), Box<dyn std::error::Error>> {
2294 let config = VeracodeConfig::new("test", "test");
2295 let client = VeracodeClient::new(config)?;
2296 let api = client.scan_api()?;
2297
2298 let _: Result<(), _> = api
2301 .delete_build("app_id", "build_id", Some("sandbox_id"))
2302 .await;
2303 let _: Result<(), _> = api.delete_all_builds("app_id", Some("sandbox_id")).await;
2304 let _: Result<(), _> = api
2305 .delete_sandbox_build("app_id", "build_id", "sandbox_id")
2306 .await;
2307 let _: Result<(), _> = api.delete_all_sandbox_builds("app_id", "sandbox_id").await;
2308
2309 Ok(())
2310 }
2311
2312 }
2315
2316 #[test]
2317 fn test_upload_large_file_request() {
2318 let request = UploadLargeFileRequest {
2319 app_id: "123".to_string(),
2320 file_path: "/path/to/large_file.jar".to_string(),
2321 filename: Some("custom_name.jar".to_string()),
2322 sandbox_id: Some("456".to_string()),
2323 };
2324
2325 assert_eq!(request.app_id, "123");
2326 assert_eq!(request.filename, Some("custom_name.jar".to_string()));
2327 assert_eq!(request.sandbox_id, Some("456".to_string()));
2328 }
2329
2330 #[test]
2331 fn test_upload_progress() {
2332 let progress = UploadProgress {
2333 bytes_uploaded: 1024,
2334 total_bytes: 2048,
2335 percentage: 50.0,
2336 };
2337
2338 assert_eq!(progress.bytes_uploaded, 1024);
2339 assert_eq!(progress.total_bytes, 2048);
2340 assert_eq!(progress.percentage, 50.0);
2341 }
2342
2343 #[test]
2344 fn test_large_file_scan_error_display() {
2345 let error = ScanError::FileTooLarge("File exceeds 2GB".to_string());
2346 assert_eq!(error.to_string(), "File too large: File exceeds 2GB");
2347
2348 let error = ScanError::UploadInProgress;
2349 assert_eq!(error.to_string(), "Upload or prescan already in progress");
2350
2351 let error = ScanError::ScanInProgress;
2352 assert_eq!(error.to_string(), "Scan in progress, cannot upload");
2353
2354 let error = ScanError::ChunkedUploadFailed("Network error".to_string());
2355 assert_eq!(error.to_string(), "Chunked upload failed: Network error");
2356 }
2357
2358 #[test]
2359 fn test_validate_filename_path_traversal() {
2360 assert!(ScanApi::validate_filename("valid_file.jar").is_ok());
2362 assert!(ScanApi::validate_filename("my-app.war").is_ok());
2363 assert!(ScanApi::validate_filename("file123.zip").is_ok());
2364
2365 assert!(ScanApi::validate_filename("../etc/passwd").is_err());
2367 assert!(ScanApi::validate_filename("test/../file.jar").is_err());
2368 assert!(ScanApi::validate_filename("test/file.jar").is_err());
2369 assert!(ScanApi::validate_filename("test\\file.jar").is_err());
2370 assert!(ScanApi::validate_filename("..\\windows\\system32").is_err());
2371
2372 assert!(ScanApi::validate_filename("test\x00file.jar").is_err());
2374 assert!(ScanApi::validate_filename("test\nfile.jar").is_err());
2375 assert!(ScanApi::validate_filename("test\rfile.jar").is_err());
2376 assert!(ScanApi::validate_filename("test\x1Ffile.jar").is_err());
2377 }
2378
2379 #[tokio::test]
2380 async fn test_large_file_upload_method_signatures() {
2381 async fn _test_large_file_methods() -> Result<(), Box<dyn std::error::Error>> {
2382 let config = VeracodeConfig::new("test", "test");
2383 let client = VeracodeClient::new(config)?;
2384 let api = client.scan_api()?;
2385
2386 let request = UploadLargeFileRequest {
2388 app_id: "123".to_string(),
2389 file_path: "/nonexistent/file.jar".to_string(),
2390 filename: None,
2391 sandbox_id: Some("456".to_string()),
2392 };
2393
2394 let _: Result<UploadedFile, _> = api.upload_large_file(request.clone()).await;
2397 let _: Result<UploadedFile, _> = api
2398 .upload_large_file_to_sandbox("123", "/path", "456", None)
2399 .await;
2400 let _: Result<UploadedFile, _> =
2401 api.upload_large_file_to_app("123", "/path", None).await;
2402
2403 let progress_callback = |bytes_uploaded: u64, total_bytes: u64, percentage: f64| {
2405 debug!("Upload progress: {bytes_uploaded}/{total_bytes} ({percentage:.1}%)");
2406 };
2407 let _: Result<UploadedFile, _> = api
2408 .upload_large_file_with_progress(request, progress_callback)
2409 .await;
2410
2411 Ok(())
2412 }
2413
2414 }
2417
2418 mod proptest_security {
2423 use super::*;
2424
2425 fn malicious_filename_strategy() -> impl Strategy<Value = String> {
2427 prop_oneof![
2428 Just("../etc/passwd".to_string()),
2430 Just("..\\windows\\system32".to_string()),
2431 Just("test/../../../secret".to_string()),
2432 Just("./../../admin".to_string()),
2433 Just("dir/file.jar".to_string()),
2435 Just("dir\\file.exe".to_string()),
2436 Just("test\x00file.jar".to_string()),
2438 Just("test\nfile.jar".to_string()),
2439 Just("test\rfile.jar".to_string()),
2440 Just("test\x1Ffile.jar".to_string()),
2441 Just("..%2F..%2Fetc%2Fpasswd".to_string()),
2443 Just("..%5C..%5Cwindows".to_string()),
2444 Just("..%c0%af..%c0%afetc%c0%afpasswd".to_string()),
2446 Just("..%252F..%252Fetc".to_string()),
2448 Just("..\\/../admin".to_string()),
2450 Just("../".repeat(20)),
2452 Just("\x00file.jar".to_string()),
2454 Just("file.jar\x00.exe".to_string()),
2455 Just("..".to_string()),
2457 Just("../../".to_string()),
2458 Just("/etc/passwd".to_string()),
2459 Just("\\windows\\system32".to_string()),
2460 ]
2461 }
2462
2463 fn valid_filename_strategy() -> impl Strategy<Value = String> {
2465 "[a-zA-Z0-9_-]{1,200}\\.(jar|war|zip|ear|class)".prop_map(|s| s)
2466 }
2467
2468 proptest! {
2469 #![proptest_config(ProptestConfig {
2470 cases: if cfg!(miri) { 5 } else { 1000 },
2471 failure_persistence: None,
2472 .. ProptestConfig::default()
2473 })]
2474
2475 #[test]
2477 fn prop_validate_filename_rejects_path_traversal(
2478 filename in malicious_filename_strategy()
2479 ) {
2480 let result = ScanApi::validate_filename(&filename);
2482 prop_assert!(result.is_err(), "Should reject malicious filename: {}", filename);
2483 }
2484
2485 #[test]
2487 fn prop_validate_filename_accepts_valid(
2488 filename in valid_filename_strategy()
2489 ) {
2490 let result = ScanApi::validate_filename(&filename);
2491 prop_assert!(result.is_ok(), "Should accept valid filename: {}", filename);
2492 }
2493
2494 #[test]
2496 fn prop_validate_filename_rejects_empty(_n in 0..100u32) {
2497 let result = ScanApi::validate_filename("");
2498 prop_assert!(result.is_err(), "Empty filename should be rejected");
2499 }
2500
2501 #[test]
2503 fn prop_validate_filename_rejects_too_long(extra_len in 1..100usize) {
2504 let long_filename = "a".repeat(256_usize.saturating_add(extra_len));
2505 let result = ScanApi::validate_filename(&long_filename);
2506 prop_assert!(result.is_err(), "Filename longer than 255 chars should be rejected");
2507 }
2508
2509 #[test]
2511 fn prop_validate_filename_rejects_double_dot(
2512 prefix in "[a-zA-Z0-9]{0,10}",
2513 suffix in "[a-zA-Z0-9]{0,10}"
2514 ) {
2515 let filename = format!("{}..{}", prefix, suffix);
2516 let result = ScanApi::validate_filename(&filename);
2517 prop_assert!(result.is_err(), "Filename with '..' should be rejected: {}", filename);
2518 }
2519
2520 #[test]
2522 fn prop_validate_filename_rejects_forward_slash(
2523 prefix in "[a-zA-Z0-9]{1,10}",
2524 suffix in "[a-zA-Z0-9]{1,10}"
2525 ) {
2526 let filename = format!("{}/{}", prefix, suffix);
2527 let result = ScanApi::validate_filename(&filename);
2528 prop_assert!(result.is_err(), "Filename with '/' should be rejected: {}", filename);
2529 }
2530
2531 #[test]
2533 fn prop_validate_filename_rejects_backslash(
2534 prefix in "[a-zA-Z0-9]{1,10}",
2535 suffix in "[a-zA-Z0-9]{1,10}"
2536 ) {
2537 let filename = format!("{}\\{}", prefix, suffix);
2538 let result = ScanApi::validate_filename(&filename);
2539 prop_assert!(result.is_err(), "Filename with '\\' should be rejected: {}", filename);
2540 }
2541
2542 #[test]
2544 fn prop_validate_filename_rejects_control_chars(
2545 prefix in "[a-zA-Z0-9]{0,10}",
2546 control_char in 0x00u8..0x20u8,
2547 suffix in "[a-zA-Z0-9]{0,10}"
2548 ) {
2549 let filename = format!("{}{}{}", prefix, control_char as char, suffix);
2550 let result = ScanApi::validate_filename(&filename);
2551 prop_assert!(result.is_err(), "Filename with control char should be rejected");
2552 }
2553 }
2554
2555 proptest! {
2556 #![proptest_config(ProptestConfig {
2557 cases: if cfg!(miri) { 5 } else { 500 },
2558 failure_persistence: None,
2559 .. ProptestConfig::default()
2560 })]
2561
2562 #[test]
2564 fn prop_attr_to_string_valid_utf8(s in ".*") {
2565 let bytes = s.as_bytes();
2566 let result = attr_to_string(bytes);
2567 prop_assert_eq!(&result, &s, "attr_to_string should preserve valid UTF-8");
2568 }
2569
2570 #[test]
2572 fn prop_attr_to_string_invalid_utf8(bytes in prop::collection::vec(any::<u8>(), 0..100)) {
2573 let _result = attr_to_string(&bytes);
2575 prop_assert!(true, "Function should not panic on invalid UTF-8");
2579 }
2580
2581 #[test]
2583 fn prop_file_size_validation(size in 0u64..5_000_000_000u64) {
2584 const MAX_SIZE: u64 = 2 * 1024 * 1024 * 1024; let exceeds_limit = size > MAX_SIZE;
2586
2587 if exceeds_limit {
2589 prop_assert!(size > MAX_SIZE, "Size should exceed 2GB limit");
2590 } else {
2591 prop_assert!(size <= MAX_SIZE, "Size should be within 2GB limit");
2592 }
2593 }
2594
2595 #[test]
2597 fn prop_upload_progress_percentage(
2598 bytes_uploaded in 0u64..1_000_000u64,
2599 total_bytes in 1u64..1_000_000u64
2600 ) {
2601 let bytes_uploaded = bytes_uploaded.min(total_bytes);
2603
2604 #[allow(clippy::cast_precision_loss)]
2605 let percentage = (bytes_uploaded as f64 / total_bytes as f64) * 100.0;
2606
2607 prop_assert!((0.0..=100.0).contains(&percentage),
2608 "Percentage should be in range [0, 100], got {}", percentage);
2609
2610 if bytes_uploaded == 0 {
2611 prop_assert!(percentage == 0.0, "0 bytes should be 0%");
2612 }
2613 if bytes_uploaded == total_bytes {
2614 prop_assert!(percentage == 100.0, "Full upload should be 100%");
2615 }
2616 }
2617
2618 #[test]
2620 fn prop_request_ids_no_path_separators(
2621 app_id in "[a-zA-Z0-9-]{1,50}",
2622 sandbox_id in "[a-zA-Z0-9-]{1,50}"
2623 ) {
2624 prop_assert!(!app_id.contains('/') && !app_id.contains('\\'));
2626 prop_assert!(!sandbox_id.contains('/') && !sandbox_id.contains('\\'));
2627 prop_assert!(!app_id.contains("..") && !sandbox_id.contains(".."));
2628 }
2629
2630 #[test]
2632 fn prop_build_id_parsing_safe(build_id_value in ".*") {
2633 let _xml = format!(r#"<buildinfo build_id="{}" />"#, build_id_value);
2635
2636 let _escaped = build_id_value.replace('&', "&")
2640 .replace('<', "<")
2641 .replace('>', ">");
2642
2643 prop_assert!(true, "String escaping should not panic");
2644 }
2645
2646 #[test]
2648 fn prop_file_path_edge_cases(
2649 path_segments in prop::collection::vec("[a-zA-Z0-9_-]{1,20}", 1..5)
2650 ) {
2651 let path = path_segments.join("/");
2652
2653 prop_assert!(!path.contains(".."), "Generated path should not contain '..'");
2655
2656 let _path_obj = std::path::Path::new(&path);
2658 prop_assert!(true, "Path construction should not panic");
2659 }
2660 }
2661
2662 proptest! {
2663 #![proptest_config(ProptestConfig {
2664 cases: if cfg!(miri) { 5 } else { 500 },
2665 failure_persistence: None,
2666 .. ProptestConfig::default()
2667 })]
2668
2669 #[test]
2671 fn prop_xml_attribute_robustness(
2672 file_id in "[a-zA-Z0-9_-]{1,50}",
2673 file_name in "[a-zA-Z0-9._-]{1,100}",
2674 file_size in 0u64..10_000_000u64
2675 ) {
2676 let xml = format!(
2678 r#"<filelist><file file_id="{}" file_name="{}" file_size="{}" /></filelist>"#,
2679 file_id, file_name, file_size
2680 );
2681
2682 prop_assert!(xml.contains(&file_id));
2684 prop_assert!(xml.contains(&file_name));
2685 }
2686
2687 #[test]
2689 fn prop_status_validation(status in "[A-Za-z ]{1,50}") {
2690 prop_assert!(!status.chars().any(|c| c.is_control()));
2692 }
2693
2694 #[test]
2696 fn prop_module_id_validation(
2697 module_id in "[a-zA-Z0-9_-]{1,100}"
2698 ) {
2699 prop_assert!(module_id.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '-'));
2701 }
2702 }
2703 }
2704
2705 mod boundary_tests {
2710 use super::*;
2711
2712 #[test]
2713 fn test_file_size_exactly_2gb() {
2714 const TWO_GB: u64 = 2 * 1024 * 1024 * 1024;
2715 assert_eq!(TWO_GB, 2_147_483_648);
2717 }
2718
2719 #[test]
2720 fn test_file_size_just_over_2gb() {
2721 const JUST_OVER: u64 = 2 * 1024 * 1024 * 1024 + 1;
2722 const TWO_GB_LIMIT: u64 = 2_147_483_648;
2723 assert_eq!(JUST_OVER, TWO_GB_LIMIT + 1);
2725 }
2726
2727 #[test]
2728 fn test_filename_max_length_boundary() {
2729 let max_len_filename = "a".repeat(255);
2731 assert!(ScanApi::validate_filename(&max_len_filename).is_ok());
2732
2733 let over_max_filename = "a".repeat(256);
2735 assert!(ScanApi::validate_filename(&over_max_filename).is_err());
2736 }
2737
2738 #[test]
2739 fn test_validate_filename_unicode_normalization() {
2740 let tricky = ".\u{2024}./file.jar";
2743 if tricky.contains('/') || tricky.contains('\\') || tricky.contains("..") {
2745 assert!(ScanApi::validate_filename(tricky).is_err());
2746 }
2747 }
2748
2749 #[test]
2750 fn test_validate_filename_homoglyph_attacks() {
2751 let homoglyph_slash = "test\u{FF0F}file.jar";
2755
2756 let result = ScanApi::validate_filename(homoglyph_slash);
2759 assert!(result.is_ok() || result.is_err());
2761 }
2762
2763 #[test]
2764 fn test_attr_to_string_empty() {
2765 let result = attr_to_string(b"");
2766 assert_eq!(result, "");
2767 }
2768
2769 #[test]
2770 fn test_attr_to_string_ascii() {
2771 let result = attr_to_string(b"test123");
2772 assert_eq!(result, "test123");
2773 }
2774
2775 #[test]
2776 fn test_attr_to_string_utf8() {
2777 let result = attr_to_string("hello 世界".as_bytes());
2778 assert_eq!(result, "hello 世界");
2779 }
2780
2781 #[test]
2782 fn test_attr_to_string_invalid_utf8() {
2783 let invalid = &[0xFF, 0xFE, 0xFD];
2785 let result = attr_to_string(invalid);
2786 assert!(result.contains('\u{FFFD}'));
2788 }
2789
2790 #[test]
2791 fn test_upload_progress_zero_bytes() {
2792 let progress = UploadProgress {
2793 bytes_uploaded: 0,
2794 total_bytes: 1000,
2795 percentage: 0.0,
2796 };
2797 assert_eq!(progress.percentage, 0.0);
2798 }
2799
2800 #[test]
2801 fn test_upload_progress_complete() {
2802 let progress = UploadProgress {
2803 bytes_uploaded: 1000,
2804 total_bytes: 1000,
2805 percentage: 100.0,
2806 };
2807 assert_eq!(progress.percentage, 100.0);
2808 }
2809
2810 #[test]
2811 fn test_scan_error_display_all_variants() {
2812 let errors = vec![
2814 ScanError::FileNotFound("test.jar".to_string()),
2815 ScanError::InvalidFileFormat("bad format".to_string()),
2816 ScanError::UploadFailed("network".to_string()),
2817 ScanError::ScanFailed("failed".to_string()),
2818 ScanError::PreScanFailed("prescan".to_string()),
2819 ScanError::BuildNotFound,
2820 ScanError::ApplicationNotFound,
2821 ScanError::SandboxNotFound,
2822 ScanError::Unauthorized,
2823 ScanError::PermissionDenied,
2824 ScanError::InvalidParameter("param".to_string()),
2825 ScanError::FileTooLarge("too big".to_string()),
2826 ScanError::UploadInProgress,
2827 ScanError::ScanInProgress,
2828 ScanError::BuildCreationFailed("failed".to_string()),
2829 ScanError::ChunkedUploadFailed("chunked".to_string()),
2830 ];
2831
2832 for error in errors {
2833 let display = error.to_string();
2834 assert!(!display.is_empty(), "Error display should not be empty");
2835 assert!(
2836 !display.contains("Error"),
2837 "Should have custom message, got: {}",
2838 display
2839 );
2840 }
2841 }
2842 }
2843
2844 mod error_handling_tests {
2849 use super::*;
2850
2851 #[test]
2852 fn test_scan_error_from_veracode_error() {
2853 let ve = VeracodeError::InvalidResponse("test".to_string());
2854 let se: ScanError = ve.into();
2855 assert!(matches!(se, ScanError::Api(_)));
2856 }
2857
2858 #[test]
2859 fn test_scan_error_from_io_error() {
2860 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
2861 let se: ScanError = io_err.into();
2862 assert!(matches!(se, ScanError::FileNotFound(_)));
2863 }
2864
2865 #[test]
2866 fn test_scan_error_must_use() {
2867 fn _check_must_use() -> ScanError {
2870 ScanError::BuildNotFound
2871 }
2872 }
2873 }
2874}