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
1301 #[test]
1302 fn test_workflow_config_builder() {
1303 let config = WorkflowConfig::new("MyApp".to_string(), "MySandbox".to_string())
1304 .with_business_criticality(BusinessCriticality::High)
1305 .with_app_description("Test application".to_string())
1306 .with_file("test.jar".to_string())
1307 .with_auto_scan(false);
1308
1309 assert_eq!(config.app_name, "MyApp");
1310 assert_eq!(config.sandbox_name, "MySandbox");
1311 assert_eq!(
1312 config.business_criticality as i32,
1313 BusinessCriticality::High as i32
1314 );
1315 assert_eq!(config.app_description, Some("Test application".to_string()));
1316 assert_eq!(config.file_paths, vec!["test.jar"]);
1317 assert!(!config.auto_scan);
1318 }
1319
1320 #[test]
1321 fn test_workflow_error_display() {
1322 let error = WorkflowError::NotFound("Application not found".to_string());
1323 assert_eq!(error.to_string(), "Not found: Application not found");
1324
1325 let error = WorkflowError::AccessDenied("Permission denied".to_string());
1326 assert_eq!(error.to_string(), "Access denied: Permission denied");
1327
1328 let error = WorkflowError::Workflow("Custom error".to_string());
1329 assert_eq!(error.to_string(), "Workflow error: Custom error");
1330 }
1331}