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, )
247 .await
248 {
249 Ok(app) => {
250 info!(
251 " โ
Application '{}' created successfully (GUID: {})",
252 config.app_name, app.guid
253 );
254 (app, true)
255 }
256 Err(VeracodeError::InvalidResponse(msg))
257 if msg.contains("403") || msg.contains("401") =>
258 {
259 return Err(WorkflowError::AccessDenied(format!(
260 "Access denied creating application '{}': {}",
261 config.app_name, msg
262 )));
263 }
264 Err(e) => return Err(WorkflowError::Api(e)),
265 }
266 }
267 Err(VeracodeError::InvalidResponse(msg))
268 if msg.contains("403") || msg.contains("401") =>
269 {
270 return Err(WorkflowError::AccessDenied(format!(
271 "Access denied checking application '{}': {}",
272 config.app_name, msg
273 )));
274 }
275 Err(e) => return Err(WorkflowError::Api(e)),
276 };
277
278 let app_id = self.client.get_app_id_from_guid(&application.guid).await?;
280 info!(" ๐ Application ID for XML API: {app_id}");
281
282 info!("\n๐งช Step 2: Checking sandbox existence...");
284 let sandbox_api = self.client.sandbox_api();
285 let (sandbox, sandbox_created) = match sandbox_api
286 .get_sandbox_by_name(&application.guid, &config.sandbox_name)
287 .await
288 {
289 Ok(Some(sandbox)) => {
290 info!(
291 " โ
Sandbox '{}' found (GUID: {})",
292 config.sandbox_name, sandbox.guid
293 );
294 (sandbox, false)
295 }
296 Ok(None) => {
297 info!(
298 " โ Sandbox '{}' not found, creating...",
299 config.sandbox_name
300 );
301 match sandbox_api
302 .create_sandbox_if_not_exists(
303 &application.guid,
304 &config.sandbox_name,
305 config.sandbox_description,
306 )
307 .await
308 {
309 Ok(sandbox) => {
310 info!(
311 " โ
Sandbox '{}' created successfully (GUID: {})",
312 config.sandbox_name, sandbox.guid
313 );
314 (sandbox, true)
315 }
316 Err(SandboxError::Api(VeracodeError::InvalidResponse(msg)))
317 if msg.contains("403") || msg.contains("401") =>
318 {
319 return Err(WorkflowError::AccessDenied(format!(
320 "Access denied creating sandbox '{}': {}",
321 config.sandbox_name, msg
322 )));
323 }
324 Err(e) => return Err(WorkflowError::Sandbox(e)),
325 }
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 checking sandbox '{}': {}",
332 config.sandbox_name, msg
333 )));
334 }
335 Err(e) => return Err(WorkflowError::Sandbox(e)),
336 };
337
338 let sandbox_id = sandbox_api
340 .get_sandbox_id_from_guid(&application.guid, &sandbox.guid)
341 .await?;
342 info!(" ๐ Sandbox ID for XML API: {sandbox_id}");
343
344 info!("\n๐ค Step 3: Uploading files to sandbox...");
346 let scan_api = self.client.scan_api();
347 let mut files_uploaded = 0;
348
349 for file_path in &config.file_paths {
350 info!(" ๐ Uploading file: {file_path}");
351 match scan_api
352 .upload_file_to_sandbox(&app_id, file_path, &sandbox_id)
353 .await
354 {
355 Ok(uploaded_file) => {
356 info!(
357 " โ
File uploaded successfully: {} (ID: {})",
358 uploaded_file.file_name, uploaded_file.file_id
359 );
360 files_uploaded += 1;
361 }
362 Err(ScanError::FileNotFound(_)) => {
363 return Err(WorkflowError::NotFound(format!(
364 "File not found: {file_path}"
365 )));
366 }
367 Err(ScanError::Unauthorized) => {
368 return Err(WorkflowError::AccessDenied(format!(
369 "Access denied uploading file: {file_path}"
370 )));
371 }
372 Err(ScanError::PermissionDenied) => {
373 return Err(WorkflowError::AccessDenied(format!(
374 "Permission denied uploading file: {file_path}"
375 )));
376 }
377 Err(e) => return Err(WorkflowError::Scan(e)),
378 }
379 }
380
381 info!(" ๐ Total files uploaded: {files_uploaded}");
382
383 let build_id = if config.auto_scan {
385 info!("\n๐ Step 4: Starting prescan and scan...");
386 match scan_api
387 .upload_and_scan_sandbox(&app_id, &sandbox_id, &config.file_paths[0])
388 .await
389 {
390 Ok(build_id) => {
391 info!(" โ
Scan started successfully with build ID: {build_id}");
392 Some(build_id)
393 }
394 Err(ScanError::Unauthorized) => {
395 return Err(WorkflowError::AccessDenied(
396 "Access denied starting scan".to_string(),
397 ));
398 }
399 Err(ScanError::PermissionDenied) => {
400 return Err(WorkflowError::AccessDenied(
401 "Permission denied starting scan".to_string(),
402 ));
403 }
404 Err(e) => {
405 info!(" โ ๏ธ Warning: Could not start scan automatically: {e}");
406 info!(
407 " ๐ก You may need to start the scan manually from the Veracode platform"
408 );
409 None
410 }
411 }
412 } else {
413 info!("\nโญ๏ธ Step 4: Skipping automatic scan (auto_scan = false)");
414 None
415 };
416
417 info!("\nโ
Workflow completed successfully!");
418 info!(" ๐ Summary:");
419 info!(
420 " - Application: {} (created: {})",
421 config.app_name, app_created
422 );
423 info!(
424 " - Sandbox: {} (created: {})",
425 config.sandbox_name, sandbox_created
426 );
427 info!(" - Files uploaded: {files_uploaded}");
428 if let Some(ref build_id_ref) = build_id {
429 info!(
430 " - Scan started: {} (build ID: {})",
431 config.auto_scan, build_id_ref
432 );
433 } else {
434 info!(" - Scan started: {}", config.auto_scan);
435 }
436
437 let result = WorkflowResultData {
438 application,
439 sandbox,
440 app_id,
441 sandbox_id,
442 build_id,
443 app_created,
444 sandbox_created,
445 files_uploaded,
446 };
447
448 Ok(result)
449 }
450
451 pub async fn ensure_app_and_sandbox(
466 &self,
467 app_name: &str,
468 sandbox_name: &str,
469 business_criticality: BusinessCriticality,
470 ) -> WorkflowResult<(Application, Sandbox, String, String)> {
471 let config = WorkflowConfig::new(app_name.to_string(), sandbox_name.to_string())
472 .with_business_criticality(business_criticality)
473 .with_auto_scan(false);
474
475 let result = self.execute_complete_workflow(config).await?;
476 Ok((
477 result.application,
478 result.sandbox,
479 result.app_id,
480 result.sandbox_id,
481 ))
482 }
483
484 pub async fn get_application_by_name(&self, app_name: &str) -> WorkflowResult<Application> {
494 match self.client.get_application_by_name(app_name).await? {
495 Some(app) => Ok(app),
496 None => Err(WorkflowError::NotFound(format!(
497 "Application '{app_name}' not found"
498 ))),
499 }
500 }
501
502 pub async fn get_sandbox_by_name(
513 &self,
514 app_guid: &str,
515 sandbox_name: &str,
516 ) -> WorkflowResult<Sandbox> {
517 let sandbox_api = self.client.sandbox_api();
518 match sandbox_api
519 .get_sandbox_by_name(app_guid, sandbox_name)
520 .await?
521 {
522 Some(sandbox) => Ok(sandbox),
523 None => Err(WorkflowError::NotFound(format!(
524 "Sandbox '{sandbox_name}' not found"
525 ))),
526 }
527 }
528
529 pub async fn delete_sandbox_builds(
542 &self,
543 app_name: &str,
544 sandbox_name: &str,
545 ) -> WorkflowResult<()> {
546 info!("๐๏ธ Deleting builds from sandbox '{sandbox_name}'...");
547
548 let app = self.get_application_by_name(app_name).await?;
550 let sandbox = self.get_sandbox_by_name(&app.guid, sandbox_name).await?;
551
552 let app_id = self.client.get_app_id_from_guid(&app.guid).await?;
554 let sandbox_api = self.client.sandbox_api();
555 let sandbox_id = sandbox_api
556 .get_sandbox_id_from_guid(&app.guid, &sandbox.guid)
557 .await?;
558
559 let scan_api = self.client.scan_api();
561 match scan_api
562 .delete_all_sandbox_builds(&app_id, &sandbox_id)
563 .await
564 {
565 Ok(_) => {
566 info!(" โ
Successfully deleted all builds from sandbox '{sandbox_name}'");
567 Ok(())
568 }
569 Err(ScanError::Unauthorized) => Err(WorkflowError::AccessDenied(
570 "Access denied deleting sandbox builds".to_string(),
571 )),
572 Err(ScanError::PermissionDenied) => Err(WorkflowError::AccessDenied(
573 "Permission denied deleting sandbox builds".to_string(),
574 )),
575 Err(ScanError::BuildNotFound) => {
576 info!(" โน๏ธ No builds found to delete in sandbox '{sandbox_name}'");
577 Ok(())
578 }
579 Err(e) => Err(WorkflowError::Scan(e)),
580 }
581 }
582
583 pub async fn delete_sandbox(&self, app_name: &str, sandbox_name: &str) -> WorkflowResult<()> {
596 info!("๐๏ธ Deleting sandbox '{sandbox_name}'...");
597
598 let app = self.get_application_by_name(app_name).await?;
600 let sandbox = self.get_sandbox_by_name(&app.guid, sandbox_name).await?;
601
602 let _ = self.delete_sandbox_builds(app_name, sandbox_name).await;
604
605 let sandbox_api = self.client.sandbox_api();
607 match sandbox_api.delete_sandbox(&app.guid, &sandbox.guid).await {
608 Ok(_) => {
609 info!(" โ
Successfully deleted sandbox '{sandbox_name}'");
610 Ok(())
611 }
612 Err(SandboxError::Api(VeracodeError::InvalidResponse(msg)))
613 if msg.contains("403") || msg.contains("401") =>
614 {
615 Err(WorkflowError::AccessDenied(format!(
616 "Access denied deleting sandbox '{sandbox_name}': {msg}"
617 )))
618 }
619 Err(SandboxError::NotFound) => {
620 info!(" โน๏ธ Sandbox '{sandbox_name}' not found (may have been already deleted)");
621 Ok(())
622 }
623 Err(e) => Err(WorkflowError::Sandbox(e)),
624 }
625 }
626
627 pub async fn delete_application(&self, app_name: &str) -> WorkflowResult<()> {
640 info!("๐๏ธ Deleting application '{app_name}'...");
641
642 let app = self.get_application_by_name(app_name).await?;
644
645 let sandbox_api = self.client.sandbox_api();
647 match sandbox_api.list_sandboxes(&app.guid, None).await {
648 Ok(sandboxes) => {
649 for sandbox in sandboxes {
650 info!(" ๐๏ธ Deleting sandbox: {}", sandbox.name);
651 let _ = self.delete_sandbox(app_name, &sandbox.name).await;
652 }
653 }
654 Err(e) => {
655 info!(" โ ๏ธ Warning: Could not list sandboxes for cleanup: {e}");
656 }
657 }
658
659 let app_id = self.client.get_app_id_from_guid(&app.guid).await?;
661 let scan_api = self.client.scan_api();
662 match scan_api.delete_all_app_builds(&app_id).await {
663 Ok(_) => info!(" โ
Deleted all application builds"),
664 Err(e) => info!(" โ ๏ธ Warning: Could not delete application builds: {e}"),
665 }
666
667 match self.client.delete_application(&app.guid).await {
669 Ok(_) => {
670 info!(" โ
Successfully deleted application '{app_name}'");
671 Ok(())
672 }
673 Err(VeracodeError::InvalidResponse(msg))
674 if msg.contains("403") || msg.contains("401") =>
675 {
676 Err(WorkflowError::AccessDenied(format!(
677 "Access denied deleting application '{app_name}': {msg}"
678 )))
679 }
680 Err(VeracodeError::NotFound(_)) => {
681 info!(" โน๏ธ Application '{app_name}' not found (may have been already deleted)");
682 Ok(())
683 }
684 Err(e) => Err(WorkflowError::Api(e)),
685 }
686 }
687
688 pub async fn complete_cleanup(&self, app_name: &str) -> WorkflowResult<()> {
704 info!("๐งน Starting complete cleanup for application '{app_name}'");
705 info!(" โ ๏ธ WARNING: This will delete ALL data associated with this application");
706 info!(" This includes all sandboxes, builds, and scan results");
707
708 match self.delete_application(app_name).await {
709 Ok(_) => {
710 info!("โ
Complete cleanup finished successfully");
711 Ok(())
712 }
713 Err(WorkflowError::NotFound(_)) => {
714 info!("โน๏ธ Application '{app_name}' not found - nothing to clean up");
715 Ok(())
716 }
717 Err(e) => {
718 info!("โ Cleanup encountered errors: {e}");
719 Err(e)
720 }
721 }
722 }
723
724 pub async fn ensure_build_exists(
739 &self,
740 app_id: &str,
741 sandbox_id: Option<&str>,
742 version: Option<&str>,
743 ) -> WorkflowResult<Build> {
744 self.ensure_build_exists_with_policy(app_id, sandbox_id, version, 1)
745 .await
746 }
747
748 pub async fn ensure_build_exists_with_policy(
766 &self,
767 app_id: &str,
768 sandbox_id: Option<&str>,
769 version: Option<&str>,
770 deletion_policy: u8,
771 ) -> WorkflowResult<Build> {
772 info!("๐ Checking if build exists (deletion policy: {deletion_policy})...");
773
774 let build_api = self.client.build_api();
775
776 match build_api
778 .get_build_info(&crate::build::GetBuildInfoRequest {
779 app_id: app_id.to_string(),
780 build_id: None, sandbox_id: sandbox_id.map(|s| s.to_string()),
782 })
783 .await
784 {
785 Ok(build) => {
786 debug!(" ๐ Build already exists: {}", build.build_id);
787 if let Some(build_version) = &build.version {
788 debug!(" Existing Version: {build_version}");
789 }
790
791 let build_status_str = build
793 .attributes
794 .get("status")
795 .or_else(|| build.attributes.get("analysis_status"))
796 .or_else(|| build.attributes.get("scan_status"))
797 .map(|s| s.as_str())
798 .unwrap_or("Unknown");
799
800 let build_status = crate::build::BuildStatus::from_string(build_status_str);
801 debug!(" Build Status: {build_status}");
802
803 if deletion_policy == 0 {
805 return Err(WorkflowError::Workflow(format!(
806 "Build {} already exists and deletion policy is set to 'Never delete' (0). Cannot proceed with upload.",
807 build.build_id
808 )));
809 }
810
811 if build_status == crate::build::BuildStatus::ResultsReady {
813 debug!(
814 " ๐ Build has 'Results Ready' status - creating new build to preserve existing results"
815 );
816 self.create_build_for_upload(app_id, sandbox_id, version)
817 .await
818 }
819 else if build_status.is_safe_to_delete(deletion_policy) {
821 info!(
822 " ๐๏ธ Build is safe to delete according to policy {deletion_policy}. Deleting..."
823 );
824
825 match build_api
827 .delete_build(&crate::build::DeleteBuildRequest {
828 app_id: app_id.to_string(),
829 sandbox_id: sandbox_id.map(|s| s.to_string()),
830 })
831 .await
832 {
833 Ok(_) => {
834 info!(" โ
Existing build deleted successfully");
835 }
836 Err(e) => {
837 return Err(WorkflowError::Build(e));
838 }
839 }
840
841 info!(" โณ Waiting for build deletion to be fully processed...");
843 self.wait_for_build_deletion(app_id, sandbox_id).await?;
844
845 info!(" โ Creating new build...");
847 self.create_build_for_upload(app_id, sandbox_id, version)
848 .await
849 } else {
850 Err(WorkflowError::Workflow(format!(
851 "Build {} has status '{}' which is not safe to delete with policy {} (0=Never, 1=Safe only, 2=Except Results Ready). Cannot proceed with upload.",
852 build.build_id, build_status, deletion_policy
853 )))
854 }
855 }
856 Err(crate::build::BuildError::BuildNotFound) => {
857 info!(" โ No build found, creating new build...");
858 self.create_build_for_upload(app_id, sandbox_id, version)
859 .await
860 }
861 Err(e) => {
862 info!(" โ ๏ธ Error checking build existence: {e}");
863 info!(" โ Attempting to create new build...");
865 self.create_build_for_upload(app_id, sandbox_id, version)
866 .await
867 }
868 }
869 }
870
871 async fn create_build_for_upload(
883 &self,
884 app_id: &str,
885 sandbox_id: Option<&str>,
886 version: Option<&str>,
887 ) -> WorkflowResult<Build> {
888 let build_api = self.client.build_api();
889
890 let build_version = if let Some(v) = version {
891 v.to_string()
892 } else {
893 let timestamp = std::time::SystemTime::now()
895 .duration_since(std::time::UNIX_EPOCH)
896 .map_err(|e| WorkflowError::Workflow(format!("System time error: {e}")))?
897 .as_secs();
898 format!("build-{timestamp}")
899 };
900
901 match build_api
902 .create_build(&crate::build::CreateBuildRequest {
903 app_id: app_id.to_string(),
904 version: Some(build_version.clone()),
905 lifecycle_stage: Some(crate::build::default_lifecycle_stage().to_string()),
906 launch_date: None,
907 sandbox_id: sandbox_id.map(|s| s.to_string()),
908 })
909 .await
910 {
911 Ok(build) => {
912 info!(" โ
Build created successfully: {}", build.build_id);
913 info!(" Version: {build_version}");
914 if sandbox_id.is_some() {
915 info!(" Type: Sandbox build");
916 } else {
917 info!(" Type: Application build");
918 }
919 Ok(build)
920 }
921 Err(e) => {
922 info!(" โ Build creation failed: {e}");
923 Err(WorkflowError::Build(e))
924 }
925 }
926 }
927
928 async fn wait_for_build_deletion(
942 &self,
943 app_id: &str,
944 sandbox_id: Option<&str>,
945 ) -> WorkflowResult<()> {
946 let build_api = self.client.build_api();
947 let max_attempts = 5;
948 let delay_seconds = 3;
949
950 let sleep_duration = tokio::time::Duration::from_secs(delay_seconds);
952
953 for attempt in 1..=max_attempts {
954 tokio::time::sleep(sleep_duration).await;
956
957 match build_api
959 .get_build_info(&crate::build::GetBuildInfoRequest {
960 app_id: app_id.to_string(),
961 build_id: None,
962 sandbox_id: sandbox_id.map(|s| s.to_string()),
963 })
964 .await
965 {
966 Ok(_build) => {
967 if attempt < max_attempts {
969 info!(
970 " โณ Build still exists, waiting {delay_seconds} more seconds... (attempt {attempt}/{max_attempts})"
971 );
972 } else {
973 info!(
974 " โ ๏ธ Build still exists after {max_attempts} attempts, proceeding anyway"
975 );
976 }
977 }
978 Err(crate::build::BuildError::BuildNotFound) => {
979 info!(" โ
Build deletion confirmed (attempt {attempt}/{max_attempts})");
981 return Ok(());
982 }
983 Err(e) => {
984 info!(" โ ๏ธ Error checking build status: {e} (attempt {attempt})");
986 }
987 }
988 }
989
990 Ok(())
993 }
994
995 pub async fn upload_large_file_with_build_management(
1012 &self,
1013 app_id: &str,
1014 sandbox_id: Option<&str>,
1015 file_path: &str,
1016 filename: Option<&str>,
1017 version: Option<&str>,
1018 ) -> WorkflowResult<crate::scan::UploadedFile> {
1019 info!("๐ Starting large file upload with build management");
1020 info!(" File: {file_path}");
1021 if let Some(sandbox_id) = sandbox_id {
1022 info!(" Target: Sandbox {sandbox_id}");
1023 } else {
1024 info!(" Target: Application {app_id}");
1025 }
1026
1027 let _build = self
1029 .ensure_build_exists(app_id, sandbox_id, version)
1030 .await?;
1031
1032 info!("\n๐ค Uploading file using uploadlargefile.do...");
1034 let scan_api = self.client.scan_api();
1035
1036 match scan_api
1037 .upload_large_file(crate::scan::UploadLargeFileRequest {
1038 app_id: app_id.to_string(),
1039 file_path: file_path.to_string(),
1040 filename: filename.map(|s| s.to_string()),
1041 sandbox_id: sandbox_id.map(|s| s.to_string()),
1042 })
1043 .await
1044 {
1045 Ok(uploaded_file) => {
1046 info!(" โ
Large file uploaded successfully:");
1047 info!(" File ID: {}", uploaded_file.file_id);
1048 info!(" File Name: {}", uploaded_file.file_name);
1049 info!(" Size: {} bytes", uploaded_file.file_size);
1050 Ok(uploaded_file)
1051 }
1052 Err(e) => {
1053 info!(" โ Large file upload failed: {e}");
1054 Err(WorkflowError::Scan(e))
1055 }
1056 }
1057 }
1058
1059 pub async fn upload_large_file_with_progress_and_build_management<F>(
1077 &self,
1078 app_id: &str,
1079 sandbox_id: Option<&str>,
1080 file_path: &str,
1081 filename: Option<&str>,
1082 version: Option<&str>,
1083 progress_callback: F,
1084 ) -> WorkflowResult<crate::scan::UploadedFile>
1085 where
1086 F: Fn(u64, u64, f64) + Send + Sync,
1087 {
1088 info!("๐ Starting large file upload with progress tracking and build management");
1089 info!(" File: {file_path}");
1090
1091 let _build = self
1093 .ensure_build_exists(app_id, sandbox_id, version)
1094 .await?;
1095
1096 info!("\n๐ค Uploading file with progress tracking...");
1098 let scan_api = self.client.scan_api();
1099
1100 match scan_api
1101 .upload_large_file_with_progress(
1102 crate::scan::UploadLargeFileRequest {
1103 app_id: app_id.to_string(),
1104 file_path: file_path.to_string(),
1105 filename: filename.map(|s| s.to_string()),
1106 sandbox_id: sandbox_id.map(|s| s.to_string()),
1107 },
1108 progress_callback,
1109 )
1110 .await
1111 {
1112 Ok(uploaded_file) => {
1113 info!(" โ
Large file uploaded successfully with progress tracking");
1114 Ok(uploaded_file)
1115 }
1116 Err(e) => {
1117 info!(" โ Large file upload with progress failed: {e}");
1118 Err(WorkflowError::Scan(e))
1119 }
1120 }
1121 }
1122
1123 pub async fn upload_file_with_smart_build_management(
1140 &self,
1141 app_id: &str,
1142 sandbox_id: Option<&str>,
1143 file_path: &str,
1144 filename: Option<&str>,
1145 version: Option<&str>,
1146 ) -> WorkflowResult<crate::scan::UploadedFile> {
1147 let file_metadata = tokio::fs::metadata(file_path)
1149 .await
1150 .map_err(|e| WorkflowError::Workflow(format!("Cannot access file {file_path}: {e}")))?;
1151
1152 let file_size = file_metadata.len();
1153 const LARGE_FILE_THRESHOLD: u64 = 100 * 1024 * 1024; info!("๐ File size: {file_size} bytes");
1156
1157 if file_size > LARGE_FILE_THRESHOLD {
1158 info!("๐ฆ Using large file upload (uploadlargefile.do) with build management");
1159 self.upload_large_file_with_build_management(
1160 app_id, sandbox_id, file_path, filename, version,
1161 )
1162 .await
1163 } else {
1164 info!("๐ฆ Using standard file upload (uploadfile.do)");
1165 let scan_api = self.client.scan_api();
1166
1167 match scan_api
1168 .upload_file(&crate::scan::UploadFileRequest {
1169 app_id: app_id.to_string(),
1170 file_path: file_path.to_string(),
1171 save_as: filename.map(|s| s.to_string()),
1172 sandbox_id: sandbox_id.map(|s| s.to_string()),
1173 })
1174 .await
1175 {
1176 Ok(uploaded_file) => {
1177 info!(" โ
File uploaded successfully via uploadfile.do");
1178 Ok(uploaded_file)
1179 }
1180 Err(e) => {
1181 info!(" โ Standard upload failed: {e}");
1182 Err(WorkflowError::Scan(e))
1183 }
1184 }
1185 }
1186 }
1187
1188 pub async fn get_or_create_build(
1202 &self,
1203 app_id: &str,
1204 sandbox_id: Option<&str>,
1205 version: Option<&str>,
1206 ) -> WorkflowResult<Build> {
1207 self.ensure_build_exists(app_id, sandbox_id, version).await
1208 }
1209}
1210
1211#[cfg(test)]
1212mod tests {
1213 use super::*;
1214
1215 #[test]
1216 fn test_workflow_config_builder() {
1217 let config = WorkflowConfig::new("MyApp".to_string(), "MySandbox".to_string())
1218 .with_business_criticality(BusinessCriticality::High)
1219 .with_app_description("Test application".to_string())
1220 .with_file("test.jar".to_string())
1221 .with_auto_scan(false);
1222
1223 assert_eq!(config.app_name, "MyApp");
1224 assert_eq!(config.sandbox_name, "MySandbox");
1225 assert_eq!(
1226 config.business_criticality as i32,
1227 BusinessCriticality::High as i32
1228 );
1229 assert_eq!(config.app_description, Some("Test application".to_string()));
1230 assert_eq!(config.file_paths, vec!["test.jar"]);
1231 assert!(!config.auto_scan);
1232 }
1233
1234 #[test]
1235 fn test_workflow_error_display() {
1236 let error = WorkflowError::NotFound("Application not found".to_string());
1237 assert_eq!(error.to_string(), "Not found: Application not found");
1238
1239 let error = WorkflowError::AccessDenied("Permission denied".to_string());
1240 assert_eq!(error.to_string(), "Access denied: Permission denied");
1241
1242 let error = WorkflowError::Workflow("Custom error".to_string());
1243 assert_eq!(error.to_string(), "Workflow error: Custom error");
1244 }
1245}