veracode_platform/
workflow.rs

1//! High-level workflow helpers for common Veracode operations.
2//!
3//! This module provides convenience functions that combine multiple API operations
4//! to implement common workflows like the complete application/sandbox lifecycle.
5
6use crate::{
7    VeracodeClient, VeracodeError, 
8    app::{Application, BusinessCriticality},
9    sandbox::{Sandbox, SandboxError},
10    scan::{ScanError},
11    build::{Build, BuildError}
12};
13
14/// High-level workflow operations for Veracode platform
15pub struct VeracodeWorkflow {
16    client: VeracodeClient,
17}
18
19/// Result type for workflow operations
20pub type WorkflowResult<T> = Result<T, WorkflowError>;
21
22/// Errors that can occur during workflow operations
23#[derive(Debug)]
24pub enum WorkflowError {
25    /// Veracode API error
26    Api(VeracodeError),
27    /// Sandbox operation error
28    Sandbox(SandboxError),
29    /// Scan operation error
30    Scan(ScanError),
31    /// Build operation error
32    Build(BuildError),
33    /// Workflow-specific error
34    Workflow(String),
35    /// Access denied
36    AccessDenied(String),
37    /// Resource not found
38    NotFound(String),
39}
40
41impl std::fmt::Display for WorkflowError {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        match self {
44            WorkflowError::Api(err) => write!(f, "API error: {err}"),
45            WorkflowError::Sandbox(err) => write!(f, "Sandbox error: {err}"),
46            WorkflowError::Scan(err) => write!(f, "Scan error: {err}"),
47            WorkflowError::Build(err) => write!(f, "Build error: {err}"),
48            WorkflowError::Workflow(msg) => write!(f, "Workflow error: {msg}"),
49            WorkflowError::AccessDenied(msg) => write!(f, "Access denied: {msg}"),
50            WorkflowError::NotFound(msg) => write!(f, "Not found: {msg}"),
51        }
52    }
53}
54
55impl std::error::Error for WorkflowError {}
56
57impl From<VeracodeError> for WorkflowError {
58    fn from(err: VeracodeError) -> Self {
59        WorkflowError::Api(err)
60    }
61}
62
63impl From<SandboxError> for WorkflowError {
64    fn from(err: SandboxError) -> Self {
65        WorkflowError::Sandbox(err)
66    }
67}
68
69impl From<ScanError> for WorkflowError {
70    fn from(err: ScanError) -> Self {
71        WorkflowError::Scan(err)
72    }
73}
74
75impl From<BuildError> for WorkflowError {
76    fn from(err: BuildError) -> Self {
77        WorkflowError::Build(err)
78    }
79}
80
81/// Configuration for the complete XML API workflow
82#[derive(Debug, Clone)]
83pub struct WorkflowConfig {
84    /// Application name
85    pub app_name: String,
86    /// Sandbox name
87    pub sandbox_name: String,
88    /// Business criticality for new applications
89    pub business_criticality: BusinessCriticality,
90    /// Application description (optional)
91    pub app_description: Option<String>,
92    /// Sandbox description (optional)
93    pub sandbox_description: Option<String>,
94    /// Files to upload
95    pub file_paths: Vec<String>,
96    /// Whether to start scan automatically after upload
97    pub auto_scan: bool,
98    /// Whether to scan all modules
99    pub scan_all_modules: bool,
100}
101
102impl WorkflowConfig {
103    /// Create a new workflow configuration
104    pub fn new(app_name: String, sandbox_name: String) -> Self {
105        Self {
106            app_name,
107            sandbox_name,
108            business_criticality: BusinessCriticality::Medium,
109            app_description: None,
110            sandbox_description: None,
111            file_paths: Vec::new(),
112            auto_scan: true,
113            scan_all_modules: true,
114        }
115    }
116
117    /// Set business criticality
118    pub fn with_business_criticality(mut self, criticality: BusinessCriticality) -> Self {
119        self.business_criticality = criticality;
120        self
121    }
122
123    /// Set application description
124    pub fn with_app_description(mut self, description: String) -> Self {
125        self.app_description = Some(description);
126        self
127    }
128
129    /// Set sandbox description
130    pub fn with_sandbox_description(mut self, description: String) -> Self {
131        self.sandbox_description = Some(description);
132        self
133    }
134
135    /// Add file to upload
136    pub fn with_file(mut self, file_path: String) -> Self {
137        self.file_paths.push(file_path);
138        self
139    }
140
141    /// Add multiple files to upload
142    pub fn with_files(mut self, file_paths: Vec<String>) -> Self {
143        self.file_paths.extend(file_paths);
144        self
145    }
146
147    /// Set auto-scan behavior
148    pub fn with_auto_scan(mut self, auto_scan: bool) -> Self {
149        self.auto_scan = auto_scan;
150        self
151    }
152
153    /// Set scan all modules behavior
154    pub fn with_scan_all_modules(mut self, scan_all: bool) -> Self {
155        self.scan_all_modules = scan_all;
156        self
157    }
158}
159
160/// Result of the complete workflow
161#[derive(Debug, Clone)]
162pub struct WorkflowResultData {
163    /// Application information
164    pub application: Application,
165    /// Sandbox information
166    pub sandbox: Sandbox,
167    /// Numeric application ID for XML API
168    pub app_id: String,
169    /// Numeric sandbox ID for XML API
170    pub sandbox_id: String,
171    /// Build ID from scan initiation (if scan was started)
172    pub build_id: Option<String>,
173    /// Whether the application was newly created
174    pub app_created: bool,
175    /// Whether the sandbox was newly created
176    pub sandbox_created: bool,
177    /// Number of files uploaded
178    pub files_uploaded: usize,
179}
180
181impl VeracodeWorkflow {
182    /// Create a new workflow instance
183    pub fn new(client: VeracodeClient) -> Self {
184        Self { client }
185    }
186
187    /// Execute the complete XML API workflow
188    ///
189    /// This method implements the full workflow:
190    /// 1. Check for application existence, create if not exist
191    /// 2. Handle access denied scenarios
192    /// 3. Check sandbox exists, if not create
193    /// 4. Handle access denied scenarios  
194    /// 5. Upload multiple files to sandbox
195    /// 6. Start prescan with available options
196    ///
197    /// # Arguments
198    ///
199    /// * `config` - Workflow configuration
200    ///
201    /// # Returns
202    ///
203    /// A `Result` containing the workflow result or an error.
204    pub async fn execute_complete_workflow(&self, config: WorkflowConfig) -> WorkflowResult<WorkflowResultData> {
205        println!("๐Ÿš€ Starting complete Veracode XML API workflow");
206        println!("   Application: {}", config.app_name);
207        println!("   Sandbox: {}", config.sandbox_name);
208        println!("   Files to upload: {}", config.file_paths.len());
209
210        // Step 1: Check for Application existence, create if not exist
211        println!("\n๐Ÿ“ฑ Step 1: Checking application existence...");
212        let (application, app_created) = match self.client.get_application_by_name(&config.app_name).await {
213            Ok(Some(app)) => {
214                println!("   โœ… Application '{}' found (GUID: {})", config.app_name, app.guid);
215                (app, false)
216            }
217            Ok(None) => {
218                println!("   โž• Application '{}' not found, creating...", config.app_name);
219                match self.client.create_application_if_not_exists(
220                    &config.app_name,
221                    config.business_criticality,
222                    config.app_description.clone(),
223                ).await {
224                    Ok(app) => {
225                        println!("   โœ… Application '{}' created successfully (GUID: {})", config.app_name, app.guid);
226                        (app, true)
227                    }
228                    Err(VeracodeError::InvalidResponse(msg)) if msg.contains("403") || msg.contains("401") => {
229                        return Err(WorkflowError::AccessDenied(format!(
230                            "Access denied creating application '{}': {}", config.app_name, msg
231                        )));
232                    }
233                    Err(e) => return Err(WorkflowError::Api(e)),
234                }
235            }
236            Err(VeracodeError::InvalidResponse(msg)) if msg.contains("403") || msg.contains("401") => {
237                return Err(WorkflowError::AccessDenied(format!(
238                    "Access denied checking application '{}': {}", config.app_name, msg
239                )));
240            }
241            Err(e) => return Err(WorkflowError::Api(e)),
242        };
243
244        // Get numeric app_id for XML API
245        let app_id = self.client.get_app_id_from_guid(&application.guid).await?;
246        println!("   ๐Ÿ“Š Application ID for XML API: {app_id}");
247
248        // Step 2: Check sandbox exists, if not create
249        println!("\n๐Ÿงช Step 2: Checking sandbox existence...");
250        let sandbox_api = self.client.sandbox_api();
251        let (sandbox, sandbox_created) = match sandbox_api.get_sandbox_by_name(&application.guid, &config.sandbox_name).await {
252            Ok(Some(sandbox)) => {
253                println!("   โœ… Sandbox '{}' found (GUID: {})", config.sandbox_name, sandbox.guid);
254                (sandbox, false)
255            }
256            Ok(None) => {
257                println!("   โž• Sandbox '{}' not found, creating...", config.sandbox_name);
258                match sandbox_api.create_sandbox_if_not_exists(
259                    &application.guid,
260                    &config.sandbox_name,
261                    config.sandbox_description.clone(),
262                ).await {
263                    Ok(sandbox) => {
264                        println!("   โœ… Sandbox '{}' created successfully (GUID: {})", config.sandbox_name, sandbox.guid);
265                        (sandbox, true)
266                    }
267                    Err(SandboxError::Api(VeracodeError::InvalidResponse(msg))) if msg.contains("403") || msg.contains("401") => {
268                        return Err(WorkflowError::AccessDenied(format!(
269                            "Access denied creating sandbox '{}': {}", config.sandbox_name, msg
270                        )));
271                    }
272                    Err(e) => return Err(WorkflowError::Sandbox(e)),
273                }
274            }
275            Err(SandboxError::Api(VeracodeError::InvalidResponse(msg))) if msg.contains("403") || msg.contains("401") => {
276                return Err(WorkflowError::AccessDenied(format!(
277                    "Access denied checking sandbox '{}': {}", config.sandbox_name, msg
278                )));
279            }
280            Err(e) => return Err(WorkflowError::Sandbox(e)),
281        };
282
283        // Get numeric sandbox_id for XML API
284        let sandbox_id = sandbox_api.get_sandbox_id_from_guid(&application.guid, &sandbox.guid).await?;
285        println!("   ๐Ÿ“Š Sandbox ID for XML API: {sandbox_id}");
286
287        // Step 3: Upload multiple files to sandbox
288        println!("\n๐Ÿ“ค Step 3: Uploading files to sandbox...");
289        let scan_api = self.client.scan_api();
290        let mut files_uploaded = 0;
291
292        for file_path in &config.file_paths {
293            println!("   ๐Ÿ“ Uploading file: {file_path}");
294            match scan_api.upload_file_to_sandbox(&app_id, file_path, &sandbox_id).await {
295                Ok(uploaded_file) => {
296                    println!("   โœ… File uploaded successfully: {} (ID: {})", uploaded_file.file_name, uploaded_file.file_id);
297                    files_uploaded += 1;
298                }
299                Err(ScanError::FileNotFound(_)) => {
300                    return Err(WorkflowError::NotFound(format!("File not found: {file_path}")));
301                }
302                Err(ScanError::Unauthorized) => {
303                    return Err(WorkflowError::AccessDenied(format!("Access denied uploading file: {file_path}")));
304                }
305                Err(ScanError::PermissionDenied) => {
306                    return Err(WorkflowError::AccessDenied(format!("Permission denied uploading file: {file_path}")));
307                }
308                Err(e) => return Err(WorkflowError::Scan(e)),
309            }
310        }
311
312        println!("   ๐Ÿ“Š Total files uploaded: {files_uploaded}");
313
314        // Step 4: Start prescan with available options
315        let build_id = if config.auto_scan {
316            println!("\n๐Ÿ” Step 4: Starting prescan and scan...");
317            match scan_api.upload_and_scan_sandbox(&app_id, &sandbox_id, &config.file_paths[0]).await {
318                Ok(build_id) => {
319                    println!("   โœ… Scan started successfully with build ID: {build_id}");
320                    Some(build_id)
321                }
322                Err(ScanError::Unauthorized) => {
323                    return Err(WorkflowError::AccessDenied("Access denied starting scan".to_string()));
324                }
325                Err(ScanError::PermissionDenied) => {
326                    return Err(WorkflowError::AccessDenied("Permission denied starting scan".to_string()));
327                }
328                Err(e) => {
329                    println!("   โš ๏ธ  Warning: Could not start scan automatically: {e}");
330                    println!("   ๐Ÿ’ก You may need to start the scan manually from the Veracode platform");
331                    None
332                }
333            }
334        } else {
335            println!("\nโญ๏ธ  Step 4: Skipping automatic scan (auto_scan = false)");
336            None
337        };
338
339        let result = WorkflowResultData {
340            application,
341            sandbox,
342            app_id,
343            sandbox_id,
344            build_id: build_id.clone(),
345            app_created,
346            sandbox_created,
347            files_uploaded,
348        };
349
350        println!("\nโœ… Workflow completed successfully!");
351        println!("   ๐Ÿ“Š Summary:");
352        println!("   - Application: {} (created: {})", config.app_name, app_created);
353        println!("   - Sandbox: {} (created: {})", config.sandbox_name, sandbox_created);
354        println!("   - Files uploaded: {files_uploaded}");
355        if let Some(build_id) = &build_id {
356            println!("   - Scan started: {} (build ID: {})", config.auto_scan, build_id);
357        } else {
358            println!("   - Scan started: {}", config.auto_scan);
359        }
360
361        Ok(result)
362    }
363
364    /// Execute a simplified workflow with just application and sandbox creation
365    ///
366    /// This method implements a subset of the full workflow for cases where
367    /// you only need to ensure the application and sandbox exist.
368    ///
369    /// # Arguments
370    ///
371    /// * `app_name` - Application name
372    /// * `sandbox_name` - Sandbox name
373    /// * `business_criticality` - Business criticality for new applications
374    ///
375    /// # Returns
376    ///
377    /// A `Result` containing application and sandbox information.
378    pub async fn ensure_app_and_sandbox(
379        &self,
380        app_name: &str,
381        sandbox_name: &str,
382        business_criticality: BusinessCriticality,
383    ) -> WorkflowResult<(Application, Sandbox, String, String)> {
384        let config = WorkflowConfig::new(app_name.to_string(), sandbox_name.to_string())
385            .with_business_criticality(business_criticality)
386            .with_auto_scan(false);
387
388        let result = self.execute_complete_workflow(config).await?;
389        Ok((result.application, result.sandbox, result.app_id, result.sandbox_id))
390    }
391
392    /// Get application by name with helpful error messages
393    ///
394    /// # Arguments
395    ///
396    /// * `app_name` - Application name to search for
397    ///
398    /// # Returns
399    ///
400    /// A `Result` containing the application or an error.
401    pub async fn get_application_by_name(&self, app_name: &str) -> WorkflowResult<Application> {
402        match self.client.get_application_by_name(app_name).await? {
403            Some(app) => Ok(app),
404            None => Err(WorkflowError::NotFound(format!("Application '{app_name}' not found"))),
405        }
406    }
407
408    /// Get sandbox by name with helpful error messages
409    ///
410    /// # Arguments
411    ///
412    /// * `app_guid` - Application GUID
413    /// * `sandbox_name` - Sandbox name to search for
414    ///
415    /// # Returns
416    ///
417    /// A `Result` containing the sandbox or an error.
418    pub async fn get_sandbox_by_name(&self, app_guid: &str, sandbox_name: &str) -> WorkflowResult<Sandbox> {
419        let sandbox_api = self.client.sandbox_api();
420        match sandbox_api.get_sandbox_by_name(app_guid, sandbox_name).await? {
421            Some(sandbox) => Ok(sandbox),
422            None => Err(WorkflowError::NotFound(format!("Sandbox '{sandbox_name}' not found"))),
423        }
424    }
425
426    /// Delete all builds from a sandbox
427    ///
428    /// This removes all uploaded files and scan data from the sandbox.
429    ///
430    /// # Arguments
431    ///
432    /// * `app_name` - Application name
433    /// * `sandbox_name` - Sandbox name
434    ///
435    /// # Returns
436    ///
437    /// A `Result` indicating success or an error.
438    pub async fn delete_sandbox_builds(
439        &self,
440        app_name: &str,
441        sandbox_name: &str,
442    ) -> WorkflowResult<()> {
443        println!("๐Ÿ—‘๏ธ  Deleting builds from sandbox '{sandbox_name}'...");
444
445        // Get application and sandbox
446        let app = self.get_application_by_name(app_name).await?;
447        let sandbox = self.get_sandbox_by_name(&app.guid, sandbox_name).await?;
448
449        // Get IDs for XML API
450        let app_id = self.client.get_app_id_from_guid(&app.guid).await?;
451        let sandbox_api = self.client.sandbox_api();
452        let sandbox_id = sandbox_api.get_sandbox_id_from_guid(&app.guid, &sandbox.guid).await?;
453
454        // Delete all builds using XML API
455        let scan_api = self.client.scan_api();
456        match scan_api.delete_all_sandbox_builds(&app_id, &sandbox_id).await {
457            Ok(_) => {
458                println!("   โœ… Successfully deleted all builds from sandbox '{sandbox_name}'");
459                Ok(())
460            }
461            Err(ScanError::Unauthorized) => {
462                Err(WorkflowError::AccessDenied("Access denied deleting sandbox builds".to_string()))
463            }
464            Err(ScanError::PermissionDenied) => {
465                Err(WorkflowError::AccessDenied("Permission denied deleting sandbox builds".to_string()))
466            }
467            Err(ScanError::BuildNotFound) => {
468                println!("   โ„น๏ธ  No builds found to delete in sandbox '{sandbox_name}'");
469                Ok(())
470            }
471            Err(e) => Err(WorkflowError::Scan(e)),
472        }
473    }
474
475    /// Delete a sandbox
476    ///
477    /// This removes the sandbox and all its associated data.
478    ///
479    /// # Arguments
480    ///
481    /// * `app_name` - Application name
482    /// * `sandbox_name` - Sandbox name
483    ///
484    /// # Returns
485    ///
486    /// A `Result` indicating success or an error.
487    pub async fn delete_sandbox(
488        &self,
489        app_name: &str,
490        sandbox_name: &str,
491    ) -> WorkflowResult<()> {
492        println!("๐Ÿ—‘๏ธ  Deleting sandbox '{sandbox_name}'...");
493
494        // Get application and sandbox
495        let app = self.get_application_by_name(app_name).await?;
496        let sandbox = self.get_sandbox_by_name(&app.guid, sandbox_name).await?;
497
498        // First delete all builds
499        let _ = self.delete_sandbox_builds(app_name, sandbox_name).await;
500
501        // Delete the sandbox using REST API
502        let sandbox_api = self.client.sandbox_api();
503        match sandbox_api.delete_sandbox(&app.guid, &sandbox.guid).await {
504            Ok(_) => {
505                println!("   โœ… Successfully deleted sandbox '{sandbox_name}'");
506                Ok(())
507            }
508            Err(SandboxError::Api(VeracodeError::InvalidResponse(msg))) if msg.contains("403") || msg.contains("401") => {
509                Err(WorkflowError::AccessDenied(format!("Access denied deleting sandbox '{sandbox_name}': {msg}")))
510            }
511            Err(SandboxError::NotFound) => {
512                println!("   โ„น๏ธ  Sandbox '{sandbox_name}' not found (may have been already deleted)");
513                Ok(())
514            }
515            Err(e) => Err(WorkflowError::Sandbox(e)),
516        }
517    }
518
519    /// Delete an application
520    ///
521    /// This removes the application and all its associated data including all sandboxes.
522    /// Use with extreme caution as this is irreversible.
523    ///
524    /// # Arguments
525    ///
526    /// * `app_name` - Application name
527    ///
528    /// # Returns
529    ///
530    /// A `Result` indicating success or an error.
531    pub async fn delete_application(
532        &self,
533        app_name: &str,
534    ) -> WorkflowResult<()> {
535        println!("๐Ÿ—‘๏ธ  Deleting application '{app_name}'...");
536
537        // Get application
538        let app = self.get_application_by_name(app_name).await?;
539
540        // First, delete all sandboxes
541        let sandbox_api = self.client.sandbox_api();
542        match sandbox_api.list_sandboxes(&app.guid, None).await {
543            Ok(sandboxes) => {
544                for sandbox in sandboxes {
545                    println!("   ๐Ÿ—‘๏ธ  Deleting sandbox: {}", sandbox.name);
546                    let _ = self.delete_sandbox(app_name, &sandbox.name).await;
547                }
548            }
549            Err(e) => {
550                println!("   โš ๏ธ  Warning: Could not list sandboxes for cleanup: {e}");
551            }
552        }
553
554        // Delete main application builds
555        let app_id = self.client.get_app_id_from_guid(&app.guid).await?;
556        let scan_api = self.client.scan_api();
557        match scan_api.delete_all_app_builds(&app_id).await {
558            Ok(_) => println!("   โœ… Deleted all application builds"),
559            Err(e) => println!("   โš ๏ธ  Warning: Could not delete application builds: {e}"),
560        }
561
562        // Delete the application using REST API
563        match self.client.delete_application(&app.guid).await {
564            Ok(_) => {
565                println!("   โœ… Successfully deleted application '{app_name}'");
566                Ok(())
567            }
568            Err(VeracodeError::InvalidResponse(msg)) if msg.contains("403") || msg.contains("401") => {
569                Err(WorkflowError::AccessDenied(format!("Access denied deleting application '{app_name}': {msg}")))
570            }
571            Err(VeracodeError::NotFound(_)) => {
572                println!("   โ„น๏ธ  Application '{app_name}' not found (may have been already deleted)");
573                Ok(())
574            }
575            Err(e) => Err(WorkflowError::Api(e)),
576        }
577    }
578
579    /// Complete cleanup workflow
580    ///
581    /// This method performs a complete cleanup of an application and all its resources:
582    /// 1. Delete all builds from all sandboxes
583    /// 2. Delete all sandboxes  
584    /// 3. Delete all application builds
585    /// 4. Delete the application
586    ///
587    /// # Arguments
588    ///
589    /// * `app_name` - Application name to clean up
590    ///
591    /// # Returns
592    ///
593    /// A `Result` indicating success or an error.
594    pub async fn complete_cleanup(
595        &self,
596        app_name: &str,
597    ) -> WorkflowResult<()> {
598        println!("๐Ÿงน Starting complete cleanup for application '{app_name}'");
599        println!("   โš ๏ธ  WARNING: This will delete ALL data associated with this application");
600        println!("   This includes all sandboxes, builds, and scan results");
601
602        match self.delete_application(app_name).await {
603            Ok(_) => {
604                println!("โœ… Complete cleanup finished successfully");
605                Ok(())
606            }
607            Err(WorkflowError::NotFound(_)) => {
608                println!("โ„น๏ธ  Application '{app_name}' not found - nothing to clean up");
609                Ok(())
610            }
611            Err(e) => {
612                println!("โŒ Cleanup encountered errors: {e}");
613                Err(e)
614            }
615        }
616    }
617
618    /// Ensure a build exists for an application or sandbox
619    ///
620    /// This method checks if a build exists and creates one if it doesn't.
621    /// This is required for uploadlargefile.do operations.
622    ///
623    /// # Arguments
624    ///
625    /// * `app_id` - Application ID (numeric)
626    /// * `sandbox_id` - Optional sandbox ID (numeric)
627    /// * `version` - Optional build version
628    ///
629    /// # Returns
630    ///
631    /// A `Result` containing the build information or an error.
632    pub async fn ensure_build_exists(
633        &self,
634        app_id: &str,
635        sandbox_id: Option<&str>,
636        version: Option<&str>,
637    ) -> WorkflowResult<Build> {
638        println!("๐Ÿ” Checking if build exists...");
639        
640        let build_api = self.client.build_api();
641        
642        // Try to get existing build info
643        let get_request = crate::build::GetBuildInfoRequest {
644            app_id: app_id.to_string(),
645            build_id: None, // Get most recent
646            sandbox_id: sandbox_id.map(|s| s.to_string()),
647        };
648
649        match build_api.get_build_info(get_request).await {
650            Ok(build) => {
651                println!("   โœ… Build already exists: {}", build.build_id);
652                if let Some(build_version) = &build.version {
653                    println!("      Version: {}", build_version);
654                }
655                Ok(build)
656            }
657            Err(crate::build::BuildError::BuildNotFound) => {
658                println!("   โž• No build found, creating new build...");
659                self.create_build_for_upload(app_id, sandbox_id, version).await
660            }
661            Err(e) => {
662                println!("   โš ๏ธ  Error checking build existence: {e}");
663                // Try to create a build anyway
664                println!("   โž• Attempting to create new build...");
665                self.create_build_for_upload(app_id, sandbox_id, version).await
666            }
667        }
668    }
669
670    /// Create a build for file upload operations
671    ///
672    /// # Arguments
673    ///
674    /// * `app_id` - Application ID (numeric)
675    /// * `sandbox_id` - Optional sandbox ID (numeric)
676    /// * `version` - Optional build version
677    ///
678    /// # Returns
679    ///
680    /// A `Result` containing the created build information or an error.
681    async fn create_build_for_upload(
682        &self,
683        app_id: &str,
684        sandbox_id: Option<&str>,
685        version: Option<&str>,
686    ) -> WorkflowResult<Build> {
687        let build_api = self.client.build_api();
688        
689        let build_version = version
690            .map(|v| v.to_string())
691            .unwrap_or_else(|| {
692                // Generate a version based on timestamp if none provided
693                let timestamp = std::time::SystemTime::now()
694                    .duration_since(std::time::UNIX_EPOCH)
695                    .unwrap()
696                    .as_secs();
697                format!("build-{}", timestamp)
698            });
699
700        let create_request = crate::build::CreateBuildRequest {
701            app_id: app_id.to_string(),
702            version: Some(build_version.clone()),
703            lifecycle_stage: Some("Development".to_string()),
704            launch_date: None,
705            sandbox_id: sandbox_id.map(|s| s.to_string()),
706        };
707
708        match build_api.create_build(create_request).await {
709            Ok(build) => {
710                println!("   โœ… Build created successfully: {}", build.build_id);
711                println!("      Version: {}", build_version);
712                if sandbox_id.is_some() {
713                    println!("      Type: Sandbox build");
714                } else {
715                    println!("      Type: Application build");
716                }
717                Ok(build)
718            }
719            Err(e) => {
720                println!("   โŒ Build creation failed: {e}");
721                Err(WorkflowError::Build(e))
722            }
723        }
724    }
725
726    /// Upload a large file with automatic build management
727    ///
728    /// This method ensures a build exists before attempting to use uploadlargefile.do.
729    /// If no build exists, it creates one automatically.
730    ///
731    /// # Arguments
732    ///
733    /// * `app_id` - Application ID (numeric)
734    /// * `sandbox_id` - Optional sandbox ID (numeric)
735    /// * `file_path` - Path to the file to upload
736    /// * `filename` - Optional custom filename for flaw matching
737    /// * `version` - Optional build version (auto-generated if not provided)
738    ///
739    /// # Returns
740    ///
741    /// A `Result` containing the uploaded file information or an error.
742    pub async fn upload_large_file_with_build_management(
743        &self,
744        app_id: &str,
745        sandbox_id: Option<&str>,
746        file_path: &str,
747        filename: Option<&str>,
748        version: Option<&str>,
749    ) -> WorkflowResult<crate::scan::UploadedFile> {
750        println!("๐Ÿš€ Starting large file upload with build management");
751        println!("   File: {}", file_path);
752        if let Some(sandbox_id) = sandbox_id {
753            println!("   Target: Sandbox {}", sandbox_id);
754        } else {
755            println!("   Target: Application {}", app_id);
756        }
757
758        // Step 1: Ensure build exists
759        let _build = self.ensure_build_exists(app_id, sandbox_id, version).await?;
760
761        // Step 2: Upload file using large file API
762        println!("\n๐Ÿ“ค Uploading file using uploadlargefile.do...");
763        let scan_api = self.client.scan_api();
764        
765        let upload_request = crate::scan::UploadLargeFileRequest {
766            app_id: app_id.to_string(),
767            file_path: file_path.to_string(),
768            filename: filename.map(|s| s.to_string()),
769            sandbox_id: sandbox_id.map(|s| s.to_string()),
770        };
771
772        match scan_api.upload_large_file(upload_request).await {
773            Ok(uploaded_file) => {
774                println!("   โœ… Large file uploaded successfully:");
775                println!("      File ID: {}", uploaded_file.file_id);
776                println!("      File Name: {}", uploaded_file.file_name);
777                println!("      Size: {} bytes", uploaded_file.file_size);
778                Ok(uploaded_file)
779            }
780            Err(e) => {
781                println!("   โŒ Large file upload failed: {e}");
782                Err(WorkflowError::Scan(e))
783            }
784        }
785    }
786
787    /// Upload a large file with progress tracking and build management
788    ///
789    /// This method ensures a build exists before attempting to use uploadlargefile.do
790    /// and provides progress tracking capabilities.
791    ///
792    /// # Arguments
793    ///
794    /// * `app_id` - Application ID (numeric)
795    /// * `sandbox_id` - Optional sandbox ID (numeric)
796    /// * `file_path` - Path to the file to upload
797    /// * `filename` - Optional custom filename for flaw matching
798    /// * `version` - Optional build version (auto-generated if not provided)
799    /// * `progress_callback` - Callback function for progress updates
800    ///
801    /// # Returns
802    ///
803    /// A `Result` containing the uploaded file information or an error.
804    pub async fn upload_large_file_with_progress_and_build_management<F>(
805        &self,
806        app_id: &str,
807        sandbox_id: Option<&str>,
808        file_path: &str,
809        filename: Option<&str>,
810        version: Option<&str>,
811        progress_callback: F,
812    ) -> WorkflowResult<crate::scan::UploadedFile>
813    where
814        F: Fn(u64, u64, f64) + Send + Sync,
815    {
816        println!("๐Ÿš€ Starting large file upload with progress tracking and build management");
817        println!("   File: {}", file_path);
818
819        // Step 1: Ensure build exists
820        let _build = self.ensure_build_exists(app_id, sandbox_id, version).await?;
821
822        // Step 2: Upload file with progress tracking
823        println!("\n๐Ÿ“ค Uploading file with progress tracking...");
824        let scan_api = self.client.scan_api();
825        
826        let upload_request = crate::scan::UploadLargeFileRequest {
827            app_id: app_id.to_string(),
828            file_path: file_path.to_string(),
829            filename: filename.map(|s| s.to_string()),
830            sandbox_id: sandbox_id.map(|s| s.to_string()),
831        };
832
833        match scan_api.upload_large_file_with_progress(upload_request, progress_callback).await {
834            Ok(uploaded_file) => {
835                println!("   โœ… Large file uploaded successfully with progress tracking");
836                Ok(uploaded_file)
837            }
838            Err(e) => {
839                println!("   โŒ Large file upload with progress failed: {e}");
840                Err(WorkflowError::Scan(e))
841            }
842        }
843    }
844
845    /// Complete file upload workflow with intelligent endpoint selection and build management
846    ///
847    /// This method automatically chooses between uploadfile.do and uploadlargefile.do
848    /// based on file size and ensures builds exist when needed.
849    ///
850    /// # Arguments
851    ///
852    /// * `app_id` - Application ID (numeric)
853    /// * `sandbox_id` - Optional sandbox ID (numeric)
854    /// * `file_path` - Path to the file to upload
855    /// * `filename` - Optional custom filename
856    /// * `version` - Optional build version
857    ///
858    /// # Returns
859    ///
860    /// A `Result` containing the uploaded file information or an error.
861    pub async fn upload_file_with_smart_build_management(
862        &self,
863        app_id: &str,
864        sandbox_id: Option<&str>,
865        file_path: &str,
866        filename: Option<&str>,
867        version: Option<&str>,
868    ) -> WorkflowResult<crate::scan::UploadedFile> {
869        // Check file size to determine upload strategy
870        let file_metadata = std::fs::metadata(file_path)
871            .map_err(|e| WorkflowError::Workflow(format!("Cannot access file {}: {}", file_path, e)))?;
872        
873        let file_size = file_metadata.len();
874        const LARGE_FILE_THRESHOLD: u64 = 100 * 1024 * 1024; // 100MB
875        
876        println!("๐Ÿ” File size: {} bytes", file_size);
877        
878        if file_size > LARGE_FILE_THRESHOLD {
879            println!("๐Ÿ“ฆ Using large file upload (uploadlargefile.do) with build management");
880            self.upload_large_file_with_build_management(
881                app_id,
882                sandbox_id,
883                file_path,
884                filename,
885                version,
886            ).await
887        } else {
888            println!("๐Ÿ“ฆ Using standard file upload (uploadfile.do)");
889            let scan_api = self.client.scan_api();
890            
891            let upload_request = crate::scan::UploadFileRequest {
892                app_id: app_id.to_string(),
893                file_path: file_path.to_string(),
894                save_as: filename.map(|s| s.to_string()),
895                sandbox_id: sandbox_id.map(|s| s.to_string()),
896            };
897
898            match scan_api.upload_file(upload_request).await {
899                Ok(uploaded_file) => {
900                    println!("   โœ… File uploaded successfully via uploadfile.do");
901                    Ok(uploaded_file)
902                }
903                Err(e) => {
904                    println!("   โŒ Standard upload failed: {e}");
905                    Err(WorkflowError::Scan(e))
906                }
907            }
908        }
909    }
910
911    /// Get or create a build for upload operations
912    ///
913    /// This is a convenience method that handles the build dependency for upload operations.
914    ///
915    /// # Arguments
916    ///
917    /// * `app_id` - Application ID (numeric)
918    /// * `sandbox_id` - Optional sandbox ID (numeric)
919    /// * `version` - Optional build version
920    ///
921    /// # Returns
922    ///
923    /// A `Result` containing the build information or an error.
924    pub async fn get_or_create_build(
925        &self,
926        app_id: &str,
927        sandbox_id: Option<&str>,
928        version: Option<&str>,
929    ) -> WorkflowResult<Build> {
930        self.ensure_build_exists(app_id, sandbox_id, version).await
931    }
932}
933
934#[cfg(test)]
935mod tests {
936    use super::*;
937
938    #[test]
939    fn test_workflow_config_builder() {
940        let config = WorkflowConfig::new("MyApp".to_string(), "MySandbox".to_string())
941            .with_business_criticality(BusinessCriticality::High)
942            .with_app_description("Test application".to_string())
943            .with_file("test.jar".to_string())
944            .with_auto_scan(false);
945
946        assert_eq!(config.app_name, "MyApp");
947        assert_eq!(config.sandbox_name, "MySandbox");
948        assert_eq!(config.business_criticality as i32, BusinessCriticality::High as i32);
949        assert_eq!(config.app_description, Some("Test application".to_string()));
950        assert_eq!(config.file_paths, vec!["test.jar"]);
951        assert!(!config.auto_scan);
952    }
953
954    #[test]
955    fn test_workflow_error_display() {
956        let error = WorkflowError::NotFound("Application not found".to_string());
957        assert_eq!(error.to_string(), "Not found: Application not found");
958
959        let error = WorkflowError::AccessDenied("Permission denied".to_string());
960        assert_eq!(error.to_string(), "Access denied: Permission denied");
961
962        let error = WorkflowError::Workflow("Custom error".to_string());
963        assert_eq!(error.to_string(), "Workflow error: Custom error");
964    }
965}