1use crate::{
7 VeracodeClient, VeracodeError,
8 app::{Application, BusinessCriticality},
9 build::{Build, BuildError},
10 sandbox::{Sandbox, SandboxError},
11 scan::ScanError,
12};
13use log::{debug, info};
14
15pub struct VeracodeWorkflow {
17 client: VeracodeClient,
18}
19
20pub type WorkflowResult<T> = Result<T, WorkflowError>;
22
23#[derive(Debug)]
25pub enum WorkflowError {
26 Api(VeracodeError),
28 Sandbox(SandboxError),
30 Scan(ScanError),
32 Build(BuildError),
34 Workflow(String),
36 AccessDenied(String),
38 NotFound(String),
40}
41
42impl std::fmt::Display for WorkflowError {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 match self {
45 WorkflowError::Api(err) => write!(f, "API error: {err}"),
46 WorkflowError::Sandbox(err) => write!(f, "Sandbox error: {err}"),
47 WorkflowError::Scan(err) => write!(f, "Scan error: {err}"),
48 WorkflowError::Build(err) => write!(f, "Build error: {err}"),
49 WorkflowError::Workflow(msg) => write!(f, "Workflow error: {msg}"),
50 WorkflowError::AccessDenied(msg) => write!(f, "Access denied: {msg}"),
51 WorkflowError::NotFound(msg) => write!(f, "Not found: {msg}"),
52 }
53 }
54}
55
56impl std::error::Error for WorkflowError {}
57
58impl From<VeracodeError> for WorkflowError {
59 fn from(err: VeracodeError) -> Self {
60 WorkflowError::Api(err)
61 }
62}
63
64impl From<SandboxError> for WorkflowError {
65 fn from(err: SandboxError) -> Self {
66 WorkflowError::Sandbox(err)
67 }
68}
69
70impl From<ScanError> for WorkflowError {
71 fn from(err: ScanError) -> Self {
72 WorkflowError::Scan(err)
73 }
74}
75
76impl From<BuildError> for WorkflowError {
77 fn from(err: BuildError) -> Self {
78 WorkflowError::Build(err)
79 }
80}
81
82#[derive(Debug, Clone)]
84pub struct WorkflowConfig {
85 pub app_name: String,
87 pub sandbox_name: String,
89 pub business_criticality: BusinessCriticality,
91 pub app_description: Option<String>,
93 pub sandbox_description: Option<String>,
95 pub file_paths: Vec<String>,
97 pub auto_scan: bool,
99 pub scan_all_modules: bool,
101}
102
103impl WorkflowConfig {
104 #[must_use]
106 pub fn new(app_name: String, sandbox_name: String) -> Self {
107 Self {
108 app_name,
109 sandbox_name,
110 business_criticality: BusinessCriticality::Medium,
111 app_description: None,
112 sandbox_description: None,
113 file_paths: Vec::new(),
114 auto_scan: true,
115 scan_all_modules: true,
116 }
117 }
118
119 #[must_use]
121 pub fn with_business_criticality(mut self, criticality: BusinessCriticality) -> Self {
122 self.business_criticality = criticality;
123 self
124 }
125
126 #[must_use]
128 pub fn with_app_description(mut self, description: String) -> Self {
129 self.app_description = Some(description);
130 self
131 }
132
133 #[must_use]
135 pub fn with_sandbox_description(mut self, description: String) -> Self {
136 self.sandbox_description = Some(description);
137 self
138 }
139
140 #[must_use]
142 pub fn with_file(mut self, file_path: String) -> Self {
143 self.file_paths.push(file_path);
144 self
145 }
146
147 #[must_use]
149 pub fn with_files(mut self, file_paths: Vec<String>) -> Self {
150 self.file_paths.extend(file_paths);
151 self
152 }
153
154 #[must_use]
156 pub fn with_auto_scan(mut self, auto_scan: bool) -> Self {
157 self.auto_scan = auto_scan;
158 self
159 }
160
161 #[must_use]
163 pub fn with_scan_all_modules(mut self, scan_all: bool) -> Self {
164 self.scan_all_modules = scan_all;
165 self
166 }
167}
168
169#[derive(Debug, Clone)]
171pub struct WorkflowResultData {
172 pub application: Application,
174 pub sandbox: Sandbox,
176 pub app_id: String,
178 pub sandbox_id: String,
180 pub build_id: Option<String>,
182 pub app_created: bool,
184 pub sandbox_created: bool,
186 pub files_uploaded: usize,
188}
189
190impl VeracodeWorkflow {
191 #[must_use]
193 pub fn new(client: VeracodeClient) -> Self {
194 Self { client }
195 }
196
197 pub async fn execute_complete_workflow(
215 &self,
216 config: WorkflowConfig,
217 ) -> WorkflowResult<WorkflowResultData> {
218 info!("๐ Starting complete Veracode XML API workflow");
219 info!(" Application: {}", config.app_name);
220 info!(" Sandbox: {}", config.sandbox_name);
221 info!(" Files to upload: {}", config.file_paths.len());
222
223 info!("\n๐ฑ Step 1: Checking application existence...");
225 let (application, app_created) =
226 match self.client.get_application_by_name(&config.app_name).await {
227 Ok(Some(app)) => {
228 info!(
229 " โ
Application '{}' found (GUID: {})",
230 config.app_name, app.guid
231 );
232 (app, false)
233 }
234 Ok(None) => {
235 info!(
236 " โ Application '{}' not found, creating...",
237 config.app_name
238 );
239 match self
240 .client
241 .create_application_if_not_exists(
242 &config.app_name,
243 config.business_criticality,
244 config.app_description,
245 None, None, )
248 .await
249 {
250 Ok(app) => {
251 info!(
252 " โ
Application '{}' created successfully (GUID: {})",
253 config.app_name, app.guid
254 );
255 (app, true)
256 }
257 Err(VeracodeError::InvalidResponse(msg))
258 if msg.contains("403") || msg.contains("401") =>
259 {
260 return Err(WorkflowError::AccessDenied(format!(
261 "Access denied creating application '{}': {}",
262 config.app_name, msg
263 )));
264 }
265 Err(e) => return Err(WorkflowError::Api(e)),
266 }
267 }
268 Err(VeracodeError::InvalidResponse(msg))
269 if msg.contains("403") || msg.contains("401") =>
270 {
271 return Err(WorkflowError::AccessDenied(format!(
272 "Access denied checking application '{}': {}",
273 config.app_name, msg
274 )));
275 }
276 Err(e) => return Err(WorkflowError::Api(e)),
277 };
278
279 let app_id = self.client.get_app_id_from_guid(&application.guid).await?;
281 info!(" ๐ Application ID for XML API: {app_id}");
282
283 info!("\n๐งช Step 2: Checking sandbox existence...");
285 let sandbox_api = self.client.sandbox_api();
286 let (sandbox, sandbox_created) = match sandbox_api
287 .get_sandbox_by_name(&application.guid, &config.sandbox_name)
288 .await
289 {
290 Ok(Some(sandbox)) => {
291 info!(
292 " โ
Sandbox '{}' found (GUID: {})",
293 config.sandbox_name, sandbox.guid
294 );
295 (sandbox, false)
296 }
297 Ok(None) => {
298 info!(
299 " โ Sandbox '{}' not found, creating...",
300 config.sandbox_name
301 );
302 match sandbox_api
303 .create_sandbox_if_not_exists(
304 &application.guid,
305 &config.sandbox_name,
306 config.sandbox_description,
307 )
308 .await
309 {
310 Ok(sandbox) => {
311 info!(
312 " โ
Sandbox '{}' created successfully (GUID: {})",
313 config.sandbox_name, sandbox.guid
314 );
315 (sandbox, true)
316 }
317 Err(SandboxError::Api(VeracodeError::InvalidResponse(msg)))
318 if msg.contains("403") || msg.contains("401") =>
319 {
320 return Err(WorkflowError::AccessDenied(format!(
321 "Access denied creating sandbox '{}': {}",
322 config.sandbox_name, msg
323 )));
324 }
325 Err(e) => return Err(WorkflowError::Sandbox(e)),
326 }
327 }
328 Err(SandboxError::Api(VeracodeError::InvalidResponse(msg)))
329 if msg.contains("403") || msg.contains("401") =>
330 {
331 return Err(WorkflowError::AccessDenied(format!(
332 "Access denied checking sandbox '{}': {}",
333 config.sandbox_name, msg
334 )));
335 }
336 Err(e) => return Err(WorkflowError::Sandbox(e)),
337 };
338
339 let sandbox_id = sandbox_api
341 .get_sandbox_id_from_guid(&application.guid, &sandbox.guid)
342 .await?;
343 info!(" ๐ Sandbox ID for XML API: {sandbox_id}");
344
345 info!("\n๐ค Step 3: Uploading files to sandbox...");
347 let scan_api = self.client.scan_api();
348 let mut files_uploaded = 0;
349
350 for file_path in &config.file_paths {
351 info!(" ๐ Uploading file: {file_path}");
352 match scan_api
353 .upload_file_to_sandbox(&app_id, file_path, &sandbox_id)
354 .await
355 {
356 Ok(uploaded_file) => {
357 info!(
358 " โ
File uploaded successfully: {} (ID: {})",
359 uploaded_file.file_name, uploaded_file.file_id
360 );
361 files_uploaded += 1;
362 }
363 Err(ScanError::FileNotFound(_)) => {
364 return Err(WorkflowError::NotFound(format!(
365 "File not found: {file_path}"
366 )));
367 }
368 Err(ScanError::Unauthorized) => {
369 return Err(WorkflowError::AccessDenied(format!(
370 "Access denied uploading file: {file_path}"
371 )));
372 }
373 Err(ScanError::PermissionDenied) => {
374 return Err(WorkflowError::AccessDenied(format!(
375 "Permission denied uploading file: {file_path}"
376 )));
377 }
378 Err(e) => return Err(WorkflowError::Scan(e)),
379 }
380 }
381
382 info!(" ๐ Total files uploaded: {files_uploaded}");
383
384 let build_id = if config.auto_scan {
386 info!("\n๐ Step 4: Starting prescan and scan...");
387 match scan_api
388 .upload_and_scan_sandbox(&app_id, &sandbox_id, &config.file_paths[0])
389 .await
390 {
391 Ok(build_id) => {
392 info!(" โ
Scan started successfully with build ID: {build_id}");
393 Some(build_id)
394 }
395 Err(ScanError::Unauthorized) => {
396 return Err(WorkflowError::AccessDenied(
397 "Access denied starting scan".to_string(),
398 ));
399 }
400 Err(ScanError::PermissionDenied) => {
401 return Err(WorkflowError::AccessDenied(
402 "Permission denied starting scan".to_string(),
403 ));
404 }
405 Err(e) => {
406 info!(" โ ๏ธ Warning: Could not start scan automatically: {e}");
407 info!(
408 " ๐ก You may need to start the scan manually from the Veracode platform"
409 );
410 None
411 }
412 }
413 } else {
414 info!("\nโญ๏ธ Step 4: Skipping automatic scan (auto_scan = false)");
415 None
416 };
417
418 info!("\nโ
Workflow completed successfully!");
419 info!(" ๐ Summary:");
420 info!(
421 " - Application: {} (created: {})",
422 config.app_name, app_created
423 );
424 info!(
425 " - Sandbox: {} (created: {})",
426 config.sandbox_name, sandbox_created
427 );
428 info!(" - Files uploaded: {files_uploaded}");
429 if let Some(ref build_id_ref) = build_id {
430 info!(
431 " - Scan started: {} (build ID: {})",
432 config.auto_scan, build_id_ref
433 );
434 } else {
435 info!(" - Scan started: {}", config.auto_scan);
436 }
437
438 let result = WorkflowResultData {
439 application,
440 sandbox,
441 app_id,
442 sandbox_id,
443 build_id,
444 app_created,
445 sandbox_created,
446 files_uploaded,
447 };
448
449 Ok(result)
450 }
451
452 pub async fn ensure_app_and_sandbox(
467 &self,
468 app_name: &str,
469 sandbox_name: &str,
470 business_criticality: BusinessCriticality,
471 ) -> WorkflowResult<(Application, Sandbox, String, String)> {
472 let config = WorkflowConfig::new(app_name.to_string(), sandbox_name.to_string())
473 .with_business_criticality(business_criticality)
474 .with_auto_scan(false);
475
476 let result = self.execute_complete_workflow(config).await?;
477 Ok((
478 result.application,
479 result.sandbox,
480 result.app_id,
481 result.sandbox_id,
482 ))
483 }
484
485 pub async fn get_application_by_name(&self, app_name: &str) -> WorkflowResult<Application> {
495 match self.client.get_application_by_name(app_name).await? {
496 Some(app) => Ok(app),
497 None => Err(WorkflowError::NotFound(format!(
498 "Application '{app_name}' not found"
499 ))),
500 }
501 }
502
503 pub async fn get_sandbox_by_name(
514 &self,
515 app_guid: &str,
516 sandbox_name: &str,
517 ) -> WorkflowResult<Sandbox> {
518 let sandbox_api = self.client.sandbox_api();
519 match sandbox_api
520 .get_sandbox_by_name(app_guid, sandbox_name)
521 .await?
522 {
523 Some(sandbox) => Ok(sandbox),
524 None => Err(WorkflowError::NotFound(format!(
525 "Sandbox '{sandbox_name}' not found"
526 ))),
527 }
528 }
529
530 pub async fn delete_sandbox_builds(
543 &self,
544 app_name: &str,
545 sandbox_name: &str,
546 ) -> WorkflowResult<()> {
547 info!("๐๏ธ Deleting builds from sandbox '{sandbox_name}'...");
548
549 let app = self.get_application_by_name(app_name).await?;
551 let sandbox = self.get_sandbox_by_name(&app.guid, sandbox_name).await?;
552
553 let app_id = self.client.get_app_id_from_guid(&app.guid).await?;
555 let sandbox_api = self.client.sandbox_api();
556 let sandbox_id = sandbox_api
557 .get_sandbox_id_from_guid(&app.guid, &sandbox.guid)
558 .await?;
559
560 let scan_api = self.client.scan_api();
562 match scan_api
563 .delete_all_sandbox_builds(&app_id, &sandbox_id)
564 .await
565 {
566 Ok(_) => {
567 info!(" โ
Successfully deleted all builds from sandbox '{sandbox_name}'");
568 Ok(())
569 }
570 Err(ScanError::Unauthorized) => Err(WorkflowError::AccessDenied(
571 "Access denied deleting sandbox builds".to_string(),
572 )),
573 Err(ScanError::PermissionDenied) => Err(WorkflowError::AccessDenied(
574 "Permission denied deleting sandbox builds".to_string(),
575 )),
576 Err(ScanError::BuildNotFound) => {
577 info!(" โน๏ธ No builds found to delete in sandbox '{sandbox_name}'");
578 Ok(())
579 }
580 Err(e) => Err(WorkflowError::Scan(e)),
581 }
582 }
583
584 pub async fn delete_sandbox(&self, app_name: &str, sandbox_name: &str) -> WorkflowResult<()> {
597 info!("๐๏ธ Deleting sandbox '{sandbox_name}'...");
598
599 let app = self.get_application_by_name(app_name).await?;
601 let sandbox = self.get_sandbox_by_name(&app.guid, sandbox_name).await?;
602
603 let _ = self.delete_sandbox_builds(app_name, sandbox_name).await;
605
606 let sandbox_api = self.client.sandbox_api();
608 match sandbox_api.delete_sandbox(&app.guid, &sandbox.guid).await {
609 Ok(_) => {
610 info!(" โ
Successfully deleted sandbox '{sandbox_name}'");
611 Ok(())
612 }
613 Err(SandboxError::Api(VeracodeError::InvalidResponse(msg)))
614 if msg.contains("403") || msg.contains("401") =>
615 {
616 Err(WorkflowError::AccessDenied(format!(
617 "Access denied deleting sandbox '{sandbox_name}': {msg}"
618 )))
619 }
620 Err(SandboxError::NotFound) => {
621 info!(" โน๏ธ Sandbox '{sandbox_name}' not found (may have been already deleted)");
622 Ok(())
623 }
624 Err(e) => Err(WorkflowError::Sandbox(e)),
625 }
626 }
627
628 pub async fn delete_application(&self, app_name: &str) -> WorkflowResult<()> {
641 info!("๐๏ธ Deleting application '{app_name}'...");
642
643 let app = self.get_application_by_name(app_name).await?;
645
646 let sandbox_api = self.client.sandbox_api();
648 match sandbox_api.list_sandboxes(&app.guid, None).await {
649 Ok(sandboxes) => {
650 for sandbox in sandboxes {
651 info!(" ๐๏ธ Deleting sandbox: {}", sandbox.name);
652 let _ = self.delete_sandbox(app_name, &sandbox.name).await;
653 }
654 }
655 Err(e) => {
656 info!(" โ ๏ธ Warning: Could not list sandboxes for cleanup: {e}");
657 }
658 }
659
660 let app_id = self.client.get_app_id_from_guid(&app.guid).await?;
662 let scan_api = self.client.scan_api();
663 match scan_api.delete_all_app_builds(&app_id).await {
664 Ok(_) => info!(" โ
Deleted all application builds"),
665 Err(e) => info!(" โ ๏ธ Warning: Could not delete application builds: {e}"),
666 }
667
668 match self.client.delete_application(&app.guid).await {
670 Ok(_) => {
671 info!(" โ
Successfully deleted application '{app_name}'");
672 Ok(())
673 }
674 Err(VeracodeError::InvalidResponse(msg))
675 if msg.contains("403") || msg.contains("401") =>
676 {
677 Err(WorkflowError::AccessDenied(format!(
678 "Access denied deleting application '{app_name}': {msg}"
679 )))
680 }
681 Err(VeracodeError::NotFound(_)) => {
682 info!(" โน๏ธ Application '{app_name}' not found (may have been already deleted)");
683 Ok(())
684 }
685 Err(e) => Err(WorkflowError::Api(e)),
686 }
687 }
688
689 pub async fn complete_cleanup(&self, app_name: &str) -> WorkflowResult<()> {
705 info!("๐งน Starting complete cleanup for application '{app_name}'");
706 info!(" โ ๏ธ WARNING: This will delete ALL data associated with this application");
707 info!(" This includes all sandboxes, builds, and scan results");
708
709 match self.delete_application(app_name).await {
710 Ok(_) => {
711 info!("โ
Complete cleanup finished successfully");
712 Ok(())
713 }
714 Err(WorkflowError::NotFound(_)) => {
715 info!("โน๏ธ Application '{app_name}' not found - nothing to clean up");
716 Ok(())
717 }
718 Err(e) => {
719 info!("โ Cleanup encountered errors: {e}");
720 Err(e)
721 }
722 }
723 }
724
725 pub async fn ensure_build_exists(
740 &self,
741 app_id: &str,
742 sandbox_id: Option<&str>,
743 version: Option<&str>,
744 ) -> WorkflowResult<Build> {
745 self.ensure_build_exists_with_policy(app_id, sandbox_id, version, 1)
746 .await
747 }
748
749 pub async fn ensure_build_exists_with_policy(
767 &self,
768 app_id: &str,
769 sandbox_id: Option<&str>,
770 version: Option<&str>,
771 deletion_policy: u8,
772 ) -> WorkflowResult<Build> {
773 info!("๐ Checking if build exists (deletion policy: {deletion_policy})...");
774
775 let build_api = self.client.build_api();
776
777 match build_api
779 .get_build_info(&crate::build::GetBuildInfoRequest {
780 app_id: app_id.to_string(),
781 build_id: None, sandbox_id: sandbox_id.map(|s| s.to_string()),
783 })
784 .await
785 {
786 Ok(build) => {
787 debug!(" ๐ Build already exists: {}", build.build_id);
788 if let Some(build_version) = &build.version {
789 debug!(" Existing Version: {build_version}");
790 }
791
792 let build_status_str = build
794 .attributes
795 .get("status")
796 .or_else(|| build.attributes.get("analysis_status"))
797 .or_else(|| build.attributes.get("scan_status"))
798 .map(|s| s.as_str())
799 .unwrap_or("Unknown");
800
801 let build_status = crate::build::BuildStatus::from_string(build_status_str);
802 debug!(" Build Status: {build_status}");
803
804 if deletion_policy == 0 {
806 return Err(WorkflowError::Workflow(format!(
807 "Build {} already exists and deletion policy is set to 'Never delete' (0). Cannot proceed with upload.",
808 build.build_id
809 )));
810 }
811
812 if build_status == crate::build::BuildStatus::ResultsReady {
814 debug!(
815 " ๐ Build has 'Results Ready' status - creating new build to preserve existing results"
816 );
817 self.create_build_for_upload(app_id, sandbox_id, version)
818 .await
819 }
820 else if build_status.is_safe_to_delete(deletion_policy) {
822 info!(
823 " ๐๏ธ Build is safe to delete according to policy {deletion_policy}. Deleting..."
824 );
825
826 match build_api
828 .delete_build(&crate::build::DeleteBuildRequest {
829 app_id: app_id.to_string(),
830 sandbox_id: sandbox_id.map(|s| s.to_string()),
831 })
832 .await
833 {
834 Ok(_) => {
835 info!(" โ
Existing build deleted successfully");
836 }
837 Err(e) => {
838 return Err(WorkflowError::Build(e));
839 }
840 }
841
842 info!(" โณ Waiting for build deletion to be fully processed...");
844 self.wait_for_build_deletion(app_id, sandbox_id).await?;
845
846 info!(" โ Creating new build...");
848 self.create_build_for_upload(app_id, sandbox_id, version)
849 .await
850 } else {
851 Err(WorkflowError::Workflow(format!(
852 "Build {} has status '{}' which is not safe to delete with policy {} (0=Never, 1=Safe only, 2=Except Results Ready). Cannot proceed with upload.",
853 build.build_id, build_status, deletion_policy
854 )))
855 }
856 }
857 Err(crate::build::BuildError::BuildNotFound) => {
858 info!(" โ No build found, creating new build...");
859 self.create_build_for_upload(app_id, sandbox_id, version)
860 .await
861 }
862 Err(e) => {
863 info!(" โ ๏ธ Error checking build existence: {e}");
864 info!(" โ Attempting to create new build...");
866 self.create_build_for_upload(app_id, sandbox_id, version)
867 .await
868 }
869 }
870 }
871
872 async fn create_build_for_upload(
884 &self,
885 app_id: &str,
886 sandbox_id: Option<&str>,
887 version: Option<&str>,
888 ) -> WorkflowResult<Build> {
889 let build_api = self.client.build_api();
890
891 let build_version = if let Some(v) = version {
892 v.to_string()
893 } else {
894 let timestamp = std::time::SystemTime::now()
896 .duration_since(std::time::UNIX_EPOCH)
897 .map_err(|e| WorkflowError::Workflow(format!("System time error: {e}")))?
898 .as_secs();
899 format!("build-{timestamp}")
900 };
901
902 match build_api
903 .create_build(&crate::build::CreateBuildRequest {
904 app_id: app_id.to_string(),
905 version: Some(build_version.clone()),
906 lifecycle_stage: Some(crate::build::default_lifecycle_stage().to_string()),
907 launch_date: None,
908 sandbox_id: sandbox_id.map(|s| s.to_string()),
909 })
910 .await
911 {
912 Ok(build) => {
913 info!(" โ
Build created successfully: {}", build.build_id);
914 info!(" Version: {build_version}");
915 if sandbox_id.is_some() {
916 info!(" Type: Sandbox build");
917 } else {
918 info!(" Type: Application build");
919 }
920 Ok(build)
921 }
922 Err(e) => {
923 info!(" โ Build creation failed: {e}");
924 Err(WorkflowError::Build(e))
925 }
926 }
927 }
928
929 async fn wait_for_build_deletion(
943 &self,
944 app_id: &str,
945 sandbox_id: Option<&str>,
946 ) -> WorkflowResult<()> {
947 let build_api = self.client.build_api();
948 let max_attempts = 5;
949 let delay_seconds = 3;
950
951 let sleep_duration = tokio::time::Duration::from_secs(delay_seconds);
953
954 for attempt in 1..=max_attempts {
955 tokio::time::sleep(sleep_duration).await;
957
958 match build_api
960 .get_build_info(&crate::build::GetBuildInfoRequest {
961 app_id: app_id.to_string(),
962 build_id: None,
963 sandbox_id: sandbox_id.map(|s| s.to_string()),
964 })
965 .await
966 {
967 Ok(_build) => {
968 if attempt < max_attempts {
970 info!(
971 " โณ Build still exists, waiting {delay_seconds} more seconds... (attempt {attempt}/{max_attempts})"
972 );
973 } else {
974 info!(
975 " โ ๏ธ Build still exists after {max_attempts} attempts, proceeding anyway"
976 );
977 }
978 }
979 Err(crate::build::BuildError::BuildNotFound) => {
980 info!(" โ
Build deletion confirmed (attempt {attempt}/{max_attempts})");
982 return Ok(());
983 }
984 Err(e) => {
985 info!(" โ ๏ธ Error checking build status: {e} (attempt {attempt})");
987 }
988 }
989 }
990
991 Ok(())
994 }
995
996 pub async fn upload_large_file_with_build_management(
1013 &self,
1014 app_id: &str,
1015 sandbox_id: Option<&str>,
1016 file_path: &str,
1017 filename: Option<&str>,
1018 version: Option<&str>,
1019 ) -> WorkflowResult<crate::scan::UploadedFile> {
1020 info!("๐ Starting large file upload with build management");
1021 info!(" File: {file_path}");
1022 if let Some(sandbox_id) = sandbox_id {
1023 info!(" Target: Sandbox {sandbox_id}");
1024 } else {
1025 info!(" Target: Application {app_id}");
1026 }
1027
1028 let _build = self
1030 .ensure_build_exists(app_id, sandbox_id, version)
1031 .await?;
1032
1033 info!("\n๐ค Uploading file using uploadlargefile.do...");
1035 let scan_api = self.client.scan_api();
1036
1037 match scan_api
1038 .upload_large_file(crate::scan::UploadLargeFileRequest {
1039 app_id: app_id.to_string(),
1040 file_path: file_path.to_string(),
1041 filename: filename.map(|s| s.to_string()),
1042 sandbox_id: sandbox_id.map(|s| s.to_string()),
1043 })
1044 .await
1045 {
1046 Ok(uploaded_file) => {
1047 info!(" โ
Large file uploaded successfully:");
1048 info!(" File ID: {}", uploaded_file.file_id);
1049 info!(" File Name: {}", uploaded_file.file_name);
1050 info!(" Size: {} bytes", uploaded_file.file_size);
1051 Ok(uploaded_file)
1052 }
1053 Err(e) => {
1054 info!(" โ Large file upload failed: {e}");
1055 Err(WorkflowError::Scan(e))
1056 }
1057 }
1058 }
1059
1060 pub async fn upload_large_file_with_progress_and_build_management<F>(
1078 &self,
1079 app_id: &str,
1080 sandbox_id: Option<&str>,
1081 file_path: &str,
1082 filename: Option<&str>,
1083 version: Option<&str>,
1084 progress_callback: F,
1085 ) -> WorkflowResult<crate::scan::UploadedFile>
1086 where
1087 F: Fn(u64, u64, f64) + Send + Sync,
1088 {
1089 info!("๐ Starting large file upload with progress tracking and build management");
1090 info!(" File: {file_path}");
1091
1092 let _build = self
1094 .ensure_build_exists(app_id, sandbox_id, version)
1095 .await?;
1096
1097 info!("\n๐ค Uploading file with progress tracking...");
1099 let scan_api = self.client.scan_api();
1100
1101 match scan_api
1102 .upload_large_file_with_progress(
1103 crate::scan::UploadLargeFileRequest {
1104 app_id: app_id.to_string(),
1105 file_path: file_path.to_string(),
1106 filename: filename.map(|s| s.to_string()),
1107 sandbox_id: sandbox_id.map(|s| s.to_string()),
1108 },
1109 progress_callback,
1110 )
1111 .await
1112 {
1113 Ok(uploaded_file) => {
1114 info!(" โ
Large file uploaded successfully with progress tracking");
1115 Ok(uploaded_file)
1116 }
1117 Err(e) => {
1118 info!(" โ Large file upload with progress failed: {e}");
1119 Err(WorkflowError::Scan(e))
1120 }
1121 }
1122 }
1123
1124 pub async fn upload_file_with_smart_build_management(
1141 &self,
1142 app_id: &str,
1143 sandbox_id: Option<&str>,
1144 file_path: &str,
1145 filename: Option<&str>,
1146 version: Option<&str>,
1147 ) -> WorkflowResult<crate::scan::UploadedFile> {
1148 let file_metadata = tokio::fs::metadata(file_path)
1150 .await
1151 .map_err(|e| WorkflowError::Workflow(format!("Cannot access file {file_path}: {e}")))?;
1152
1153 let file_size = file_metadata.len();
1154 const LARGE_FILE_THRESHOLD: u64 = 100 * 1024 * 1024; info!("๐ File size: {file_size} bytes");
1157
1158 if file_size > LARGE_FILE_THRESHOLD {
1159 info!("๐ฆ Using large file upload (uploadlargefile.do) with build management");
1160 self.upload_large_file_with_build_management(
1161 app_id, sandbox_id, file_path, filename, version,
1162 )
1163 .await
1164 } else {
1165 info!("๐ฆ Using standard file upload (uploadfile.do)");
1166 let scan_api = self.client.scan_api();
1167
1168 match scan_api
1169 .upload_file(&crate::scan::UploadFileRequest {
1170 app_id: app_id.to_string(),
1171 file_path: file_path.to_string(),
1172 save_as: filename.map(|s| s.to_string()),
1173 sandbox_id: sandbox_id.map(|s| s.to_string()),
1174 })
1175 .await
1176 {
1177 Ok(uploaded_file) => {
1178 info!(" โ
File uploaded successfully via uploadfile.do");
1179 Ok(uploaded_file)
1180 }
1181 Err(e) => {
1182 info!(" โ Standard upload failed: {e}");
1183 Err(WorkflowError::Scan(e))
1184 }
1185 }
1186 }
1187 }
1188
1189 pub async fn get_or_create_build(
1203 &self,
1204 app_id: &str,
1205 sandbox_id: Option<&str>,
1206 version: Option<&str>,
1207 ) -> WorkflowResult<Build> {
1208 self.ensure_build_exists(app_id, sandbox_id, version).await
1209 }
1210}
1211
1212#[cfg(test)]
1213mod tests {
1214 use super::*;
1215
1216 #[test]
1217 fn test_workflow_config_builder() {
1218 let config = WorkflowConfig::new("MyApp".to_string(), "MySandbox".to_string())
1219 .with_business_criticality(BusinessCriticality::High)
1220 .with_app_description("Test application".to_string())
1221 .with_file("test.jar".to_string())
1222 .with_auto_scan(false);
1223
1224 assert_eq!(config.app_name, "MyApp");
1225 assert_eq!(config.sandbox_name, "MySandbox");
1226 assert_eq!(
1227 config.business_criticality as i32,
1228 BusinessCriticality::High as i32
1229 );
1230 assert_eq!(config.app_description, Some("Test application".to_string()));
1231 assert_eq!(config.file_paths, vec!["test.jar"]);
1232 assert!(!config.auto_scan);
1233 }
1234
1235 #[test]
1236 fn test_workflow_error_display() {
1237 let error = WorkflowError::NotFound("Application not found".to_string());
1238 assert_eq!(error.to_string(), "Not found: Application not found");
1239
1240 let error = WorkflowError::AccessDenied("Permission denied".to_string());
1241 assert_eq!(error.to_string(), "Access denied: Permission denied");
1242
1243 let error = WorkflowError::Workflow("Custom error".to_string());
1244 assert_eq!(error.to_string(), "Workflow error: Custom error");
1245 }
1246}