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)) if e.name().as_ref() == b"error" => {
1420 in_error_tag = false;
1421 }
1422 Ok(Event::Eof) => break,
1423 Err(e) => {
1424 error!("Error parsing XML: {e}");
1425 break;
1426 }
1427 _ => {}
1428 }
1429 buf.clear();
1430 }
1431
1432 if let Some(error_msg) = current_error {
1434 return Err(ScanError::UploadFailed(error_msg));
1435 }
1436
1437 let filename = Path::new(file_path)
1438 .file_name()
1439 .and_then(|f| f.to_str())
1440 .unwrap_or("file")
1441 .to_string();
1442
1443 Ok(UploadedFile {
1444 file_id: file_id.unwrap_or_else(|| format!("file_{}", chrono::Utc::now().timestamp())),
1445 file_name: filename,
1446 file_size: tokio::fs::metadata(file_path)
1447 .await
1448 .map(|m| m.len())
1449 .unwrap_or(0),
1450 uploaded: Utc::now(),
1451 file_status,
1452 md5: None,
1453 })
1454 }
1455
1456 fn validate_scan_response(&self, xml: &str) -> Result<(), ScanError> {
1463 if xml.contains("<error>") {
1465 let mut reader = Reader::from_str(xml);
1467 reader.config_mut().trim_text(true);
1468
1469 let mut buf = Vec::new();
1470 let mut in_error = false;
1471 let mut error_message = String::new();
1472
1473 loop {
1474 match reader.read_event_into(&mut buf) {
1475 Ok(Event::Start(ref e)) if e.name().as_ref() == b"error" => {
1476 in_error = true;
1477 }
1478 Ok(Event::Text(ref e)) if in_error => {
1479 error_message.push_str(&String::from_utf8_lossy(e));
1480 }
1481 Ok(Event::End(ref e)) if e.name().as_ref() == b"error" => {
1482 break;
1483 }
1484 Ok(Event::Eof) => break,
1485 Err(e) => {
1486 return Err(ScanError::ScanFailed(format!("XML parsing error: {e}")));
1487 }
1488 _ => {}
1489 }
1490 buf.clear();
1491 }
1492
1493 if !error_message.is_empty() {
1494 return Err(ScanError::ScanFailed(error_message));
1495 }
1496 return Err(ScanError::ScanFailed(
1497 "Unknown error in scan response".to_string(),
1498 ));
1499 }
1500
1501 if xml.contains("<buildinfo") || xml.contains("<build") {
1503 Ok(())
1504 } else {
1505 Err(ScanError::ScanFailed(
1506 "Invalid scan response format".to_string(),
1507 ))
1508 }
1509 }
1510
1511 fn parse_module_from_attributes<'a>(
1513 &self,
1514 attributes: impl Iterator<
1515 Item = Result<
1516 quick_xml::events::attributes::Attribute<'a>,
1517 quick_xml::events::attributes::AttrError,
1518 >,
1519 >,
1520 has_fatal_errors: &mut bool,
1521 ) -> ScanModule {
1522 let mut module = ScanModule {
1523 id: String::new(),
1524 name: String::new(),
1525 module_type: String::new(),
1526 is_fatal: false,
1527 selected: false,
1528 size: None,
1529 platform: None,
1530 };
1531
1532 for attr in attributes.flatten() {
1533 match attr.key.as_ref() {
1534 b"id" => module.id = attr_to_string(&attr.value),
1535 b"name" => module.name = attr_to_string(&attr.value),
1536 b"type" => module.module_type = attr_to_string(&attr.value),
1537 b"isfatal" => module.is_fatal = attr.value.as_ref() == b"true",
1538 b"selected" => module.selected = attr.value.as_ref() == b"true",
1539 b"has_fatal_errors" if attr.value.as_ref() == b"true" => {
1540 *has_fatal_errors = true;
1541 }
1542 b"size" => {
1543 if let Ok(size_str) = String::from_utf8(attr.value.to_vec()) {
1544 module.size = size_str.parse().ok();
1545 }
1546 }
1547 b"platform" => module.platform = Some(attr_to_string(&attr.value)),
1548 _ => {}
1549 }
1550 }
1551
1552 module
1553 }
1554
1555 fn parse_prescan_results(
1556 &self,
1557 xml: &str,
1558 app_id: &str,
1559 sandbox_id: Option<&str>,
1560 ) -> Result<PreScanResults, ScanError> {
1561 if xml.contains("<error>") && xml.contains("Prescan results not available") {
1563 return Ok(PreScanResults {
1565 build_id: String::new(),
1566 app_id: app_id.to_string(),
1567 sandbox_id: sandbox_id.map(|s| s.to_string()),
1568 status: "Pre-Scan Submitted".to_string(), modules: Vec::new(),
1570 messages: Vec::new(),
1571 });
1572 }
1573
1574 let mut reader = Reader::from_str(xml);
1575 reader.config_mut().trim_text(true);
1576
1577 let mut buf = Vec::new();
1578 let mut build_id = None;
1579 let mut modules = Vec::new();
1580 let messages = Vec::new();
1581 let mut has_prescan_results = false;
1582 let mut has_fatal_errors = false;
1583
1584 loop {
1585 match reader.read_event_into(&mut buf) {
1586 Ok(Event::Start(ref e)) => {
1587 match e.name().as_ref() {
1588 b"prescanresults" => {
1589 has_prescan_results = true;
1590 for attr in e.attributes().flatten() {
1592 if attr.key.as_ref() == b"build_id" {
1593 build_id = Some(attr_to_string(&attr.value));
1594 }
1595 }
1596 }
1597 b"module" => {
1598 let module = self.parse_module_from_attributes(
1599 e.attributes(),
1600 &mut has_fatal_errors,
1601 );
1602 modules.push(module);
1603 }
1604 _ => {}
1605 }
1606 }
1607 Ok(Event::Empty(ref e))
1608 if e.name().as_ref() == b"module" => {
1610 let module = self
1611 .parse_module_from_attributes(e.attributes(), &mut has_fatal_errors);
1612 modules.push(module);
1613 }
1614 Ok(Event::Eof) => break,
1615 Err(e) => {
1616 error!("Error parsing XML: {e}");
1617 break;
1618 }
1619 _ => {}
1620 }
1621 buf.clear();
1622 }
1623
1624 let status = if !has_prescan_results {
1626 "Unknown".to_string()
1627 } else if modules.is_empty() {
1628 "Pre-Scan Failed".to_string()
1630 } else if has_fatal_errors {
1631 "Pre-Scan Failed".to_string()
1633 } else {
1634 "Pre-Scan Success".to_string()
1636 };
1637
1638 Ok(PreScanResults {
1639 build_id: build_id.unwrap_or_else(|| "unknown".to_string()),
1640 app_id: app_id.to_string(),
1641 sandbox_id: sandbox_id.map(|s| s.to_string()),
1642 status,
1643 modules,
1644 messages,
1645 })
1646 }
1647
1648 fn parse_file_from_attributes<'a>(
1650 &self,
1651 attributes: impl Iterator<
1652 Item = Result<
1653 quick_xml::events::attributes::Attribute<'a>,
1654 quick_xml::events::attributes::AttrError,
1655 >,
1656 >,
1657 ) -> UploadedFile {
1658 let mut file = UploadedFile {
1659 file_id: String::new(),
1660 file_name: String::new(),
1661 file_size: 0,
1662 uploaded: Utc::now(),
1663 file_status: FileStatus::PendingUpload, md5: None,
1665 };
1666
1667 for attr in attributes.flatten() {
1668 match attr.key.as_ref() {
1669 b"file_id" => file.file_id = attr_to_string(&attr.value),
1670 b"file_name" => file.file_name = attr_to_string(&attr.value),
1671 b"file_size" => {
1672 if let Ok(size_str) = String::from_utf8(attr.value.to_vec()) {
1673 file.file_size = size_str.parse().unwrap_or(0);
1674 }
1675 }
1676 b"file_status" => {
1677 let status_str = attr_to_string(&attr.value);
1678 file.file_status = status_str.parse().unwrap_or_else(|e| {
1680 error!("Unknown file status '{}': {}", status_str, e);
1681 FileStatus::PendingUpload
1682 });
1683 }
1684 b"md5" | b"file_md5" => {
1685 file.md5 = Some(String::from_utf8_lossy(&attr.value).to_string())
1686 }
1687 _ => {}
1688 }
1689 }
1690
1691 file
1692 }
1693
1694 fn parse_file_list(&self, xml: &str) -> Result<Vec<UploadedFile>, ScanError> {
1695 let mut reader = Reader::from_str(xml);
1696 reader.config_mut().trim_text(true);
1697
1698 let mut buf = Vec::new();
1699 let mut files = Vec::new();
1700 let mut current_error: Option<String> = None;
1701 let mut in_error_tag = false;
1702
1703 loop {
1704 match reader.read_event_into(&mut buf) {
1705 Ok(Event::Start(ref e)) => {
1706 if e.name().as_ref() == b"file" {
1707 let file = self.parse_file_from_attributes(e.attributes());
1708 files.push(file);
1709 } else if e.name().as_ref() == b"error" {
1710 in_error_tag = true;
1711 }
1712 }
1713 Ok(Event::Empty(ref e))
1714 if e.name().as_ref() == b"file" => {
1716 let file = self.parse_file_from_attributes(e.attributes());
1717 files.push(file);
1718 }
1719 Ok(Event::Text(ref e))
1720 if in_error_tag => {
1721 current_error = Some(String::from_utf8_lossy(e).to_string());
1722 }
1723 Ok(Event::End(ref e))
1724 if e.name().as_ref() == b"error" => {
1725 in_error_tag = false;
1726 }
1727 Ok(Event::Eof) => break,
1728 Err(e) => {
1729 error!("Error parsing XML: {e}");
1730 break;
1731 }
1732 _ => {}
1733 }
1734 buf.clear();
1735 }
1736
1737 if let Some(error_msg) = current_error {
1739 return Err(ScanError::UploadFailed(error_msg));
1740 }
1741
1742 Ok(files)
1743 }
1744
1745 fn parse_build_info(
1746 &self,
1747 xml: &str,
1748 app_id: &str,
1749 sandbox_id: Option<&str>,
1750 ) -> Result<ScanInfo, ScanError> {
1751 let mut reader = Reader::from_str(xml);
1752 reader.config_mut().trim_text(true);
1753
1754 let mut buf = Vec::new();
1755 let mut scan_info = ScanInfo {
1756 build_id: String::new(),
1757 app_id: app_id.to_string(),
1758 sandbox_id: sandbox_id.map(|s| s.to_string()),
1759 status: "Unknown".to_string(),
1760 scan_type: "Static".to_string(),
1761 analysis_unit_id: None,
1762 scan_progress_percentage: None,
1763 scan_start: None,
1764 scan_complete: None,
1765 total_lines_of_code: None,
1766 };
1767
1768 let mut inside_build = false;
1769
1770 loop {
1771 match reader.read_event_into(&mut buf) {
1772 Ok(Event::Start(ref e)) => {
1773 match e.name().as_ref() {
1774 b"buildinfo" => {
1775 for attr in e.attributes().flatten() {
1777 match attr.key.as_ref() {
1778 b"build_id" => scan_info.build_id = attr_to_string(&attr.value),
1779 b"analysis_unit"
1780 if scan_info.status == "Unknown" => {
1782 scan_info.status = attr_to_string(&attr.value);
1783 }
1784 b"analysis_unit_id" => {
1785 scan_info.analysis_unit_id =
1786 Some(attr_to_string(&attr.value))
1787 }
1788 b"scan_progress_percentage" => {
1789 if let Ok(progress_str) =
1790 String::from_utf8(attr.value.to_vec())
1791 {
1792 scan_info.scan_progress_percentage =
1793 progress_str.parse().ok();
1794 }
1795 }
1796 b"total_lines_of_code" => {
1797 if let Ok(lines_str) =
1798 String::from_utf8(attr.value.to_vec())
1799 {
1800 scan_info.total_lines_of_code = lines_str.parse().ok();
1801 }
1802 }
1803 _ => {}
1804 }
1805 }
1806 }
1807 b"build" => {
1808 inside_build = true;
1809 }
1810 b"analysis_unit" => {
1811 for attr in e.attributes().flatten() {
1813 match attr.key.as_ref() {
1814 b"status" => {
1815 scan_info.status = attr_to_string(&attr.value);
1817 }
1818 b"analysis_type" => {
1819 scan_info.scan_type = attr_to_string(&attr.value);
1820 }
1821 _ => {}
1822 }
1823 }
1824 }
1825 _ => {}
1826 }
1827 }
1828 Ok(Event::End(ref e))
1829 if e.name().as_ref() == b"build" => {
1830 inside_build = false;
1831 }
1832 Ok(Event::Empty(ref e))
1833 if e.name().as_ref() == b"analysis_unit" && inside_build => {
1835 for attr in e.attributes().flatten() {
1836 match attr.key.as_ref() {
1837 b"status" => {
1838 scan_info.status = attr_to_string(&attr.value);
1839 }
1840 b"analysis_type" => {
1841 scan_info.scan_type = attr_to_string(&attr.value);
1842 }
1843 _ => {}
1844 }
1845 }
1846 }
1847 Ok(Event::Eof) => break,
1848 Err(e) => {
1849 error!("Error parsing XML: {e}");
1850 break;
1851 }
1852 _ => {}
1853 }
1854 buf.clear();
1855 }
1856
1857 Ok(scan_info)
1858 }
1859}
1860
1861impl ScanApi {
1863 pub async fn upload_file_to_sandbox(
1880 &self,
1881 app_id: &str,
1882 file_path: &str,
1883 sandbox_id: &str,
1884 ) -> Result<UploadedFile, ScanError> {
1885 let request = UploadFileRequest {
1886 app_id: app_id.to_string(),
1887 file_path: file_path.to_string(),
1888 save_as: None,
1889 sandbox_id: Some(sandbox_id.to_string()),
1890 };
1891
1892 self.upload_file(&request).await
1893 }
1894
1895 pub async fn upload_file_to_app(
1911 &self,
1912 app_id: &str,
1913 file_path: &str,
1914 ) -> Result<UploadedFile, ScanError> {
1915 let request = UploadFileRequest {
1916 app_id: app_id.to_string(),
1917 file_path: file_path.to_string(),
1918 save_as: None,
1919 sandbox_id: None,
1920 };
1921
1922 self.upload_file(&request).await
1923 }
1924
1925 pub async fn upload_large_file_to_sandbox(
1943 &self,
1944 app_id: &str,
1945 file_path: &str,
1946 sandbox_id: &str,
1947 filename: Option<&str>,
1948 ) -> Result<UploadedFile, ScanError> {
1949 let request = UploadLargeFileRequest {
1950 app_id: app_id.to_string(),
1951 file_path: file_path.to_string(),
1952 filename: filename.map(|s| s.to_string()),
1953 sandbox_id: Some(sandbox_id.to_string()),
1954 };
1955
1956 self.upload_large_file(request).await
1957 }
1958
1959 pub async fn upload_large_file_to_app(
1976 &self,
1977 app_id: &str,
1978 file_path: &str,
1979 filename: Option<&str>,
1980 ) -> Result<UploadedFile, ScanError> {
1981 let request = UploadLargeFileRequest {
1982 app_id: app_id.to_string(),
1983 file_path: file_path.to_string(),
1984 filename: filename.map(|s| s.to_string()),
1985 sandbox_id: None,
1986 };
1987
1988 self.upload_large_file(request).await
1989 }
1990
1991 pub async fn upload_large_file_to_sandbox_with_progress<F>(
2010 &self,
2011 app_id: &str,
2012 file_path: &str,
2013 sandbox_id: &str,
2014 filename: Option<&str>,
2015 progress_callback: F,
2016 ) -> Result<UploadedFile, ScanError>
2017 where
2018 F: Fn(u64, u64, f64) + Send + Sync,
2019 {
2020 let request = UploadLargeFileRequest {
2021 app_id: app_id.to_string(),
2022 file_path: file_path.to_string(),
2023 filename: filename.map(|s| s.to_string()),
2024 sandbox_id: Some(sandbox_id.to_string()),
2025 };
2026
2027 self.upload_large_file_with_progress(request, progress_callback)
2028 .await
2029 }
2030
2031 pub async fn begin_sandbox_prescan(
2047 &self,
2048 app_id: &str,
2049 sandbox_id: &str,
2050 ) -> Result<(), ScanError> {
2051 let request = BeginPreScanRequest {
2052 app_id: app_id.to_string(),
2053 sandbox_id: Some(sandbox_id.to_string()),
2054 auto_scan: Some(true),
2055 scan_all_nonfatal_top_level_modules: Some(true),
2056 include_new_modules: Some(true),
2057 };
2058
2059 self.begin_prescan(&request).await
2060 }
2061
2062 pub async fn begin_sandbox_scan_all_modules(
2078 &self,
2079 app_id: &str,
2080 sandbox_id: &str,
2081 ) -> Result<(), ScanError> {
2082 let request = BeginScanRequest {
2083 app_id: app_id.to_string(),
2084 sandbox_id: Some(sandbox_id.to_string()),
2085 modules: None,
2086 scan_all_top_level_modules: Some(true),
2087 scan_all_nonfatal_top_level_modules: Some(true),
2088 scan_previously_selected_modules: None,
2089 };
2090
2091 self.begin_scan(&request).await
2092 }
2093
2094 pub async fn upload_and_scan_sandbox(
2111 &self,
2112 app_id: &str,
2113 sandbox_id: &str,
2114 file_path: &str,
2115 ) -> Result<String, ScanError> {
2116 info!("Uploading file to sandbox...");
2118 let _uploaded_file = self
2119 .upload_file_to_sandbox(app_id, file_path, sandbox_id)
2120 .await?;
2121
2122 info!("Beginning pre-scan...");
2124 self.begin_sandbox_prescan(app_id, sandbox_id).await?;
2125
2126 tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
2128
2129 info!("Beginning scan...");
2131 self.begin_sandbox_scan_all_modules(app_id, sandbox_id)
2132 .await?;
2133
2134 Ok("build_id_not_available".to_string())
2139 }
2140
2141 pub async fn delete_sandbox_build(
2158 &self,
2159 app_id: &str,
2160 build_id: &str,
2161 sandbox_id: &str,
2162 ) -> Result<(), ScanError> {
2163 self.delete_build(app_id, build_id, Some(sandbox_id)).await
2164 }
2165
2166 pub async fn delete_all_sandbox_builds(
2182 &self,
2183 app_id: &str,
2184 sandbox_id: &str,
2185 ) -> Result<(), ScanError> {
2186 self.delete_all_builds(app_id, Some(sandbox_id)).await
2187 }
2188
2189 pub async fn delete_app_build(&self, app_id: &str, build_id: &str) -> Result<(), ScanError> {
2205 self.delete_build(app_id, build_id, None).await
2206 }
2207
2208 pub async fn delete_all_app_builds(&self, app_id: &str) -> Result<(), ScanError> {
2223 self.delete_all_builds(app_id, None).await
2224 }
2225}
2226
2227#[cfg(test)]
2228mod tests {
2229 use super::*;
2230 use crate::VeracodeConfig;
2231 use proptest::prelude::*;
2232
2233 #[test]
2234 fn test_upload_file_request() {
2235 let request = UploadFileRequest {
2236 app_id: "123".to_string(),
2237 file_path: "/path/to/file.jar".to_string(),
2238 save_as: Some("app.jar".to_string()),
2239 sandbox_id: Some("456".to_string()),
2240 };
2241
2242 assert_eq!(request.app_id, "123");
2243 assert_eq!(request.sandbox_id, Some("456".to_string()));
2244 }
2245
2246 #[test]
2247 fn test_begin_prescan_request() {
2248 let request = BeginPreScanRequest {
2249 app_id: "123".to_string(),
2250 sandbox_id: Some("456".to_string()),
2251 auto_scan: Some(true),
2252 scan_all_nonfatal_top_level_modules: Some(true),
2253 include_new_modules: Some(false),
2254 };
2255
2256 assert_eq!(request.app_id, "123");
2257 assert_eq!(request.auto_scan, Some(true));
2258 }
2259
2260 #[test]
2261 fn test_scan_error_display() {
2262 let error = ScanError::FileNotFound("test.jar".to_string());
2263 assert_eq!(error.to_string(), "File not found: test.jar");
2264
2265 let error = ScanError::UploadFailed("Network error".to_string());
2266 assert_eq!(error.to_string(), "Network error");
2267
2268 let error = ScanError::Unauthorized;
2269 assert_eq!(error.to_string(), "Unauthorized access");
2270
2271 let error = ScanError::BuildNotFound;
2272 assert_eq!(error.to_string(), "Build not found");
2273 }
2274
2275 #[test]
2276 fn test_delete_build_request_structure() {
2277 use crate::{VeracodeClient, VeracodeConfig};
2281
2282 async fn _test_delete_methods() -> Result<(), Box<dyn std::error::Error>> {
2283 let config = VeracodeConfig::new("test", "test");
2284 let client = VeracodeClient::new(config)?;
2285 let api = client.scan_api()?;
2286
2287 let _: Result<(), _> = api
2290 .delete_build("app_id", "build_id", Some("sandbox_id"))
2291 .await;
2292 let _: Result<(), _> = api.delete_all_builds("app_id", Some("sandbox_id")).await;
2293 let _: Result<(), _> = api
2294 .delete_sandbox_build("app_id", "build_id", "sandbox_id")
2295 .await;
2296 let _: Result<(), _> = api.delete_all_sandbox_builds("app_id", "sandbox_id").await;
2297
2298 Ok(())
2299 }
2300
2301 }
2304
2305 #[test]
2306 fn test_upload_large_file_request() {
2307 let request = UploadLargeFileRequest {
2308 app_id: "123".to_string(),
2309 file_path: "/path/to/large_file.jar".to_string(),
2310 filename: Some("custom_name.jar".to_string()),
2311 sandbox_id: Some("456".to_string()),
2312 };
2313
2314 assert_eq!(request.app_id, "123");
2315 assert_eq!(request.filename, Some("custom_name.jar".to_string()));
2316 assert_eq!(request.sandbox_id, Some("456".to_string()));
2317 }
2318
2319 #[test]
2320 fn test_upload_progress() {
2321 let progress = UploadProgress {
2322 bytes_uploaded: 1024,
2323 total_bytes: 2048,
2324 percentage: 50.0,
2325 };
2326
2327 assert_eq!(progress.bytes_uploaded, 1024);
2328 assert_eq!(progress.total_bytes, 2048);
2329 assert_eq!(progress.percentage, 50.0);
2330 }
2331
2332 #[test]
2333 fn test_large_file_scan_error_display() {
2334 let error = ScanError::FileTooLarge("File exceeds 2GB".to_string());
2335 assert_eq!(error.to_string(), "File too large: File exceeds 2GB");
2336
2337 let error = ScanError::UploadInProgress;
2338 assert_eq!(error.to_string(), "Upload or prescan already in progress");
2339
2340 let error = ScanError::ScanInProgress;
2341 assert_eq!(error.to_string(), "Scan in progress, cannot upload");
2342
2343 let error = ScanError::ChunkedUploadFailed("Network error".to_string());
2344 assert_eq!(error.to_string(), "Chunked upload failed: Network error");
2345 }
2346
2347 #[test]
2348 fn test_validate_filename_path_traversal() {
2349 assert!(ScanApi::validate_filename("valid_file.jar").is_ok());
2351 assert!(ScanApi::validate_filename("my-app.war").is_ok());
2352 assert!(ScanApi::validate_filename("file123.zip").is_ok());
2353
2354 assert!(ScanApi::validate_filename("../etc/passwd").is_err());
2356 assert!(ScanApi::validate_filename("test/../file.jar").is_err());
2357 assert!(ScanApi::validate_filename("test/file.jar").is_err());
2358 assert!(ScanApi::validate_filename("test\\file.jar").is_err());
2359 assert!(ScanApi::validate_filename("..\\windows\\system32").is_err());
2360
2361 assert!(ScanApi::validate_filename("test\x00file.jar").is_err());
2363 assert!(ScanApi::validate_filename("test\nfile.jar").is_err());
2364 assert!(ScanApi::validate_filename("test\rfile.jar").is_err());
2365 assert!(ScanApi::validate_filename("test\x1Ffile.jar").is_err());
2366 }
2367
2368 #[tokio::test]
2369 async fn test_large_file_upload_method_signatures() {
2370 async fn _test_large_file_methods() -> Result<(), Box<dyn std::error::Error>> {
2371 let config = VeracodeConfig::new("test", "test");
2372 let client = VeracodeClient::new(config)?;
2373 let api = client.scan_api()?;
2374
2375 let request = UploadLargeFileRequest {
2377 app_id: "123".to_string(),
2378 file_path: "/nonexistent/file.jar".to_string(),
2379 filename: None,
2380 sandbox_id: Some("456".to_string()),
2381 };
2382
2383 let _: Result<UploadedFile, _> = api.upload_large_file(request.clone()).await;
2386 let _: Result<UploadedFile, _> = api
2387 .upload_large_file_to_sandbox("123", "/path", "456", None)
2388 .await;
2389 let _: Result<UploadedFile, _> =
2390 api.upload_large_file_to_app("123", "/path", None).await;
2391
2392 let progress_callback = |bytes_uploaded: u64, total_bytes: u64, percentage: f64| {
2394 debug!("Upload progress: {bytes_uploaded}/{total_bytes} ({percentage:.1}%)");
2395 };
2396 let _: Result<UploadedFile, _> = api
2397 .upload_large_file_with_progress(request, progress_callback)
2398 .await;
2399
2400 Ok(())
2401 }
2402
2403 }
2406
2407 mod proptest_security {
2412 use super::*;
2413
2414 fn malicious_filename_strategy() -> impl Strategy<Value = String> {
2416 prop_oneof![
2417 Just("../etc/passwd".to_string()),
2419 Just("..\\windows\\system32".to_string()),
2420 Just("test/../../../secret".to_string()),
2421 Just("./../../admin".to_string()),
2422 Just("dir/file.jar".to_string()),
2424 Just("dir\\file.exe".to_string()),
2425 Just("test\x00file.jar".to_string()),
2427 Just("test\nfile.jar".to_string()),
2428 Just("test\rfile.jar".to_string()),
2429 Just("test\x1Ffile.jar".to_string()),
2430 Just("..%2F..%2Fetc%2Fpasswd".to_string()),
2432 Just("..%5C..%5Cwindows".to_string()),
2433 Just("..%c0%af..%c0%afetc%c0%afpasswd".to_string()),
2435 Just("..%252F..%252Fetc".to_string()),
2437 Just("..\\/../admin".to_string()),
2439 Just("../".repeat(20)),
2441 Just("\x00file.jar".to_string()),
2443 Just("file.jar\x00.exe".to_string()),
2444 Just("..".to_string()),
2446 Just("../../".to_string()),
2447 Just("/etc/passwd".to_string()),
2448 Just("\\windows\\system32".to_string()),
2449 ]
2450 }
2451
2452 fn valid_filename_strategy() -> impl Strategy<Value = String> {
2454 "[a-zA-Z0-9_-]{1,200}\\.(jar|war|zip|ear|class)".prop_map(|s| s)
2455 }
2456
2457 proptest! {
2458 #![proptest_config(ProptestConfig {
2459 cases: if cfg!(miri) { 5 } else { 1000 },
2460 failure_persistence: None,
2461 .. ProptestConfig::default()
2462 })]
2463
2464 #[test]
2466 fn prop_validate_filename_rejects_path_traversal(
2467 filename in malicious_filename_strategy()
2468 ) {
2469 let result = ScanApi::validate_filename(&filename);
2471 prop_assert!(result.is_err(), "Should reject malicious filename: {}", filename);
2472 }
2473
2474 #[test]
2476 fn prop_validate_filename_accepts_valid(
2477 filename in valid_filename_strategy()
2478 ) {
2479 let result = ScanApi::validate_filename(&filename);
2480 prop_assert!(result.is_ok(), "Should accept valid filename: {}", filename);
2481 }
2482
2483 #[test]
2485 fn prop_validate_filename_rejects_empty(_n in 0..100u32) {
2486 let result = ScanApi::validate_filename("");
2487 prop_assert!(result.is_err(), "Empty filename should be rejected");
2488 }
2489
2490 #[test]
2492 fn prop_validate_filename_rejects_too_long(extra_len in 1..100usize) {
2493 let long_filename = "a".repeat(256_usize.saturating_add(extra_len));
2494 let result = ScanApi::validate_filename(&long_filename);
2495 prop_assert!(result.is_err(), "Filename longer than 255 chars should be rejected");
2496 }
2497
2498 #[test]
2500 fn prop_validate_filename_rejects_double_dot(
2501 prefix in "[a-zA-Z0-9]{0,10}",
2502 suffix in "[a-zA-Z0-9]{0,10}"
2503 ) {
2504 let filename = format!("{}..{}", prefix, suffix);
2505 let result = ScanApi::validate_filename(&filename);
2506 prop_assert!(result.is_err(), "Filename with '..' should be rejected: {}", filename);
2507 }
2508
2509 #[test]
2511 fn prop_validate_filename_rejects_forward_slash(
2512 prefix in "[a-zA-Z0-9]{1,10}",
2513 suffix in "[a-zA-Z0-9]{1,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_backslash(
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_control_chars(
2534 prefix in "[a-zA-Z0-9]{0,10}",
2535 control_char in 0x00u8..0x20u8,
2536 suffix in "[a-zA-Z0-9]{0,10}"
2537 ) {
2538 let filename = format!("{}{}{}", prefix, control_char as char, suffix);
2539 let result = ScanApi::validate_filename(&filename);
2540 prop_assert!(result.is_err(), "Filename with control char should be rejected");
2541 }
2542 }
2543
2544 proptest! {
2545 #![proptest_config(ProptestConfig {
2546 cases: if cfg!(miri) { 5 } else { 500 },
2547 failure_persistence: None,
2548 .. ProptestConfig::default()
2549 })]
2550
2551 #[test]
2553 fn prop_attr_to_string_valid_utf8(s in ".*") {
2554 let bytes = s.as_bytes();
2555 let result = attr_to_string(bytes);
2556 prop_assert_eq!(&result, &s, "attr_to_string should preserve valid UTF-8");
2557 }
2558
2559 #[test]
2561 fn prop_attr_to_string_invalid_utf8(bytes in prop::collection::vec(any::<u8>(), 0..100)) {
2562 let _result = attr_to_string(&bytes);
2564 prop_assert!(true, "Function should not panic on invalid UTF-8");
2568 }
2569
2570 #[test]
2572 fn prop_file_size_validation(size in 0u64..5_000_000_000u64) {
2573 const MAX_SIZE: u64 = 2 * 1024 * 1024 * 1024; let exceeds_limit = size > MAX_SIZE;
2575
2576 if exceeds_limit {
2578 prop_assert!(size > MAX_SIZE, "Size should exceed 2GB limit");
2579 } else {
2580 prop_assert!(size <= MAX_SIZE, "Size should be within 2GB limit");
2581 }
2582 }
2583
2584 #[test]
2586 fn prop_upload_progress_percentage(
2587 bytes_uploaded in 0u64..1_000_000u64,
2588 total_bytes in 1u64..1_000_000u64
2589 ) {
2590 let bytes_uploaded = bytes_uploaded.min(total_bytes);
2592
2593 #[allow(clippy::cast_precision_loss)]
2594 let percentage = (bytes_uploaded as f64 / total_bytes as f64) * 100.0;
2595
2596 prop_assert!((0.0..=100.0).contains(&percentage),
2597 "Percentage should be in range [0, 100], got {}", percentage);
2598
2599 if bytes_uploaded == 0 {
2600 prop_assert!(percentage == 0.0, "0 bytes should be 0%");
2601 }
2602 if bytes_uploaded == total_bytes {
2603 prop_assert!(percentage == 100.0, "Full upload should be 100%");
2604 }
2605 }
2606
2607 #[test]
2609 fn prop_request_ids_no_path_separators(
2610 app_id in "[a-zA-Z0-9-]{1,50}",
2611 sandbox_id in "[a-zA-Z0-9-]{1,50}"
2612 ) {
2613 prop_assert!(!app_id.contains('/') && !app_id.contains('\\'));
2615 prop_assert!(!sandbox_id.contains('/') && !sandbox_id.contains('\\'));
2616 prop_assert!(!app_id.contains("..") && !sandbox_id.contains(".."));
2617 }
2618
2619 #[test]
2621 fn prop_build_id_parsing_safe(build_id_value in ".*") {
2622 let _xml = format!(r#"<buildinfo build_id="{}" />"#, build_id_value);
2624
2625 let _escaped = build_id_value.replace('&', "&")
2629 .replace('<', "<")
2630 .replace('>', ">");
2631
2632 prop_assert!(true, "String escaping should not panic");
2633 }
2634
2635 #[test]
2637 fn prop_file_path_edge_cases(
2638 path_segments in prop::collection::vec("[a-zA-Z0-9_-]{1,20}", 1..5)
2639 ) {
2640 let path = path_segments.join("/");
2641
2642 prop_assert!(!path.contains(".."), "Generated path should not contain '..'");
2644
2645 let _path_obj = std::path::Path::new(&path);
2647 prop_assert!(true, "Path construction should not panic");
2648 }
2649 }
2650
2651 proptest! {
2652 #![proptest_config(ProptestConfig {
2653 cases: if cfg!(miri) { 5 } else { 500 },
2654 failure_persistence: None,
2655 .. ProptestConfig::default()
2656 })]
2657
2658 #[test]
2660 fn prop_xml_attribute_robustness(
2661 file_id in "[a-zA-Z0-9_-]{1,50}",
2662 file_name in "[a-zA-Z0-9._-]{1,100}",
2663 file_size in 0u64..10_000_000u64
2664 ) {
2665 let xml = format!(
2667 r#"<filelist><file file_id="{}" file_name="{}" file_size="{}" /></filelist>"#,
2668 file_id, file_name, file_size
2669 );
2670
2671 prop_assert!(xml.contains(&file_id));
2673 prop_assert!(xml.contains(&file_name));
2674 }
2675
2676 #[test]
2678 fn prop_status_validation(status in "[A-Za-z ]{1,50}") {
2679 prop_assert!(!status.chars().any(|c| c.is_control()));
2681 }
2682
2683 #[test]
2685 fn prop_module_id_validation(
2686 module_id in "[a-zA-Z0-9_-]{1,100}"
2687 ) {
2688 prop_assert!(module_id.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '-'));
2690 }
2691 }
2692 }
2693
2694 mod boundary_tests {
2699 use super::*;
2700
2701 #[test]
2702 fn test_file_size_exactly_2gb() {
2703 const TWO_GB: u64 = 2 * 1024 * 1024 * 1024;
2704 assert_eq!(TWO_GB, 2_147_483_648);
2706 }
2707
2708 #[test]
2709 fn test_file_size_just_over_2gb() {
2710 const JUST_OVER: u64 = 2 * 1024 * 1024 * 1024 + 1;
2711 const TWO_GB_LIMIT: u64 = 2_147_483_648;
2712 assert_eq!(JUST_OVER, TWO_GB_LIMIT + 1);
2714 }
2715
2716 #[test]
2717 fn test_filename_max_length_boundary() {
2718 let max_len_filename = "a".repeat(255);
2720 assert!(ScanApi::validate_filename(&max_len_filename).is_ok());
2721
2722 let over_max_filename = "a".repeat(256);
2724 assert!(ScanApi::validate_filename(&over_max_filename).is_err());
2725 }
2726
2727 #[test]
2728 fn test_validate_filename_unicode_normalization() {
2729 let tricky = ".\u{2024}./file.jar";
2732 if tricky.contains('/') || tricky.contains('\\') || tricky.contains("..") {
2734 assert!(ScanApi::validate_filename(tricky).is_err());
2735 }
2736 }
2737
2738 #[test]
2739 fn test_validate_filename_homoglyph_attacks() {
2740 let homoglyph_slash = "test\u{FF0F}file.jar";
2744
2745 let result = ScanApi::validate_filename(homoglyph_slash);
2748 assert!(result.is_ok() || result.is_err());
2750 }
2751
2752 #[test]
2753 fn test_attr_to_string_empty() {
2754 let result = attr_to_string(b"");
2755 assert_eq!(result, "");
2756 }
2757
2758 #[test]
2759 fn test_attr_to_string_ascii() {
2760 let result = attr_to_string(b"test123");
2761 assert_eq!(result, "test123");
2762 }
2763
2764 #[test]
2765 fn test_attr_to_string_utf8() {
2766 let result = attr_to_string("hello 世界".as_bytes());
2767 assert_eq!(result, "hello 世界");
2768 }
2769
2770 #[test]
2771 fn test_attr_to_string_invalid_utf8() {
2772 let invalid = &[0xFF, 0xFE, 0xFD];
2774 let result = attr_to_string(invalid);
2775 assert!(result.contains('\u{FFFD}'));
2777 }
2778
2779 #[test]
2780 fn test_upload_progress_zero_bytes() {
2781 let progress = UploadProgress {
2782 bytes_uploaded: 0,
2783 total_bytes: 1000,
2784 percentage: 0.0,
2785 };
2786 assert_eq!(progress.percentage, 0.0);
2787 }
2788
2789 #[test]
2790 fn test_upload_progress_complete() {
2791 let progress = UploadProgress {
2792 bytes_uploaded: 1000,
2793 total_bytes: 1000,
2794 percentage: 100.0,
2795 };
2796 assert_eq!(progress.percentage, 100.0);
2797 }
2798
2799 #[test]
2800 fn test_scan_error_display_all_variants() {
2801 let errors = vec![
2803 ScanError::FileNotFound("test.jar".to_string()),
2804 ScanError::InvalidFileFormat("bad format".to_string()),
2805 ScanError::UploadFailed("network".to_string()),
2806 ScanError::ScanFailed("failed".to_string()),
2807 ScanError::PreScanFailed("prescan".to_string()),
2808 ScanError::BuildNotFound,
2809 ScanError::ApplicationNotFound,
2810 ScanError::SandboxNotFound,
2811 ScanError::Unauthorized,
2812 ScanError::PermissionDenied,
2813 ScanError::InvalidParameter("param".to_string()),
2814 ScanError::FileTooLarge("too big".to_string()),
2815 ScanError::UploadInProgress,
2816 ScanError::ScanInProgress,
2817 ScanError::BuildCreationFailed("failed".to_string()),
2818 ScanError::ChunkedUploadFailed("chunked".to_string()),
2819 ];
2820
2821 for error in errors {
2822 let display = error.to_string();
2823 assert!(!display.is_empty(), "Error display should not be empty");
2824 assert!(
2825 !display.contains("Error"),
2826 "Should have custom message, got: {}",
2827 display
2828 );
2829 }
2830 }
2831 }
2832
2833 mod error_handling_tests {
2838 use super::*;
2839
2840 #[test]
2841 fn test_scan_error_from_veracode_error() {
2842 let ve = VeracodeError::InvalidResponse("test".to_string());
2843 let se: ScanError = ve.into();
2844 assert!(matches!(se, ScanError::Api(_)));
2845 }
2846
2847 #[test]
2848 fn test_scan_error_from_io_error() {
2849 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
2850 let se: ScanError = io_err.into();
2851 assert!(matches!(se, ScanError::FileNotFound(_)));
2852 }
2853
2854 #[test]
2855 fn test_scan_error_must_use() {
2856 fn _check_must_use() -> ScanError {
2859 ScanError::BuildNotFound
2860 }
2861 }
2862 }
2863}