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    build::{Build, BuildError},
10    sandbox::{Sandbox, SandboxError},
11    scan::ScanError,
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(
205        &self,
206        config: WorkflowConfig,
207    ) -> WorkflowResult<WorkflowResultData> {
208        println!("๐Ÿš€ Starting complete Veracode XML API workflow");
209        println!("   Application: {}", config.app_name);
210        println!("   Sandbox: {}", config.sandbox_name);
211        println!("   Files to upload: {}", config.file_paths.len());
212
213        // Step 1: Check for Application existence, create if not exist
214        println!("\n๐Ÿ“ฑ Step 1: Checking application existence...");
215        let (application, app_created) =
216            match self.client.get_application_by_name(&config.app_name).await {
217                Ok(Some(app)) => {
218                    println!(
219                        "   โœ… Application '{}' found (GUID: {})",
220                        config.app_name, app.guid
221                    );
222                    (app, false)
223                }
224                Ok(None) => {
225                    println!(
226                        "   โž• Application '{}' not found, creating...",
227                        config.app_name
228                    );
229                    match self
230                        .client
231                        .create_application_if_not_exists(
232                            &config.app_name,
233                            config.business_criticality,
234                            config.app_description.clone(),
235                            None, // No teams specified
236                        )
237                        .await
238                    {
239                        Ok(app) => {
240                            println!(
241                                "   โœ… Application '{}' created successfully (GUID: {})",
242                                config.app_name, app.guid
243                            );
244                            (app, true)
245                        }
246                        Err(VeracodeError::InvalidResponse(msg))
247                            if msg.contains("403") || msg.contains("401") =>
248                        {
249                            return Err(WorkflowError::AccessDenied(format!(
250                                "Access denied creating application '{}': {}",
251                                config.app_name, msg
252                            )));
253                        }
254                        Err(e) => return Err(WorkflowError::Api(e)),
255                    }
256                }
257                Err(VeracodeError::InvalidResponse(msg))
258                    if msg.contains("403") || msg.contains("401") =>
259                {
260                    return Err(WorkflowError::AccessDenied(format!(
261                        "Access denied checking application '{}': {}",
262                        config.app_name, msg
263                    )));
264                }
265                Err(e) => return Err(WorkflowError::Api(e)),
266            };
267
268        // Get numeric app_id for XML API
269        let app_id = self.client.get_app_id_from_guid(&application.guid).await?;
270        println!("   ๐Ÿ“Š Application ID for XML API: {app_id}");
271
272        // Step 2: Check sandbox exists, if not create
273        println!("\n๐Ÿงช Step 2: Checking sandbox existence...");
274        let sandbox_api = self.client.sandbox_api();
275        let (sandbox, sandbox_created) = match sandbox_api
276            .get_sandbox_by_name(&application.guid, &config.sandbox_name)
277            .await
278        {
279            Ok(Some(sandbox)) => {
280                println!(
281                    "   โœ… Sandbox '{}' found (GUID: {})",
282                    config.sandbox_name, sandbox.guid
283                );
284                (sandbox, false)
285            }
286            Ok(None) => {
287                println!(
288                    "   โž• Sandbox '{}' not found, creating...",
289                    config.sandbox_name
290                );
291                match sandbox_api
292                    .create_sandbox_if_not_exists(
293                        &application.guid,
294                        &config.sandbox_name,
295                        config.sandbox_description.clone(),
296                    )
297                    .await
298                {
299                    Ok(sandbox) => {
300                        println!(
301                            "   โœ… Sandbox '{}' created successfully (GUID: {})",
302                            config.sandbox_name, sandbox.guid
303                        );
304                        (sandbox, true)
305                    }
306                    Err(SandboxError::Api(VeracodeError::InvalidResponse(msg)))
307                        if msg.contains("403") || msg.contains("401") =>
308                    {
309                        return Err(WorkflowError::AccessDenied(format!(
310                            "Access denied creating sandbox '{}': {}",
311                            config.sandbox_name, msg
312                        )));
313                    }
314                    Err(e) => return Err(WorkflowError::Sandbox(e)),
315                }
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 checking sandbox '{}': {}",
322                    config.sandbox_name, msg
323                )));
324            }
325            Err(e) => return Err(WorkflowError::Sandbox(e)),
326        };
327
328        // Get numeric sandbox_id for XML API
329        let sandbox_id = sandbox_api
330            .get_sandbox_id_from_guid(&application.guid, &sandbox.guid)
331            .await?;
332        println!("   ๐Ÿ“Š Sandbox ID for XML API: {sandbox_id}");
333
334        // Step 3: Upload multiple files to sandbox
335        println!("\n๐Ÿ“ค Step 3: Uploading files to sandbox...");
336        let scan_api = self.client.scan_api();
337        let mut files_uploaded = 0;
338
339        for file_path in &config.file_paths {
340            println!("   ๐Ÿ“ Uploading file: {file_path}");
341            match scan_api
342                .upload_file_to_sandbox(&app_id, file_path, &sandbox_id)
343                .await
344            {
345                Ok(uploaded_file) => {
346                    println!(
347                        "   โœ… File uploaded successfully: {} (ID: {})",
348                        uploaded_file.file_name, uploaded_file.file_id
349                    );
350                    files_uploaded += 1;
351                }
352                Err(ScanError::FileNotFound(_)) => {
353                    return Err(WorkflowError::NotFound(format!(
354                        "File not found: {file_path}"
355                    )));
356                }
357                Err(ScanError::Unauthorized) => {
358                    return Err(WorkflowError::AccessDenied(format!(
359                        "Access denied uploading file: {file_path}"
360                    )));
361                }
362                Err(ScanError::PermissionDenied) => {
363                    return Err(WorkflowError::AccessDenied(format!(
364                        "Permission denied uploading file: {file_path}"
365                    )));
366                }
367                Err(e) => return Err(WorkflowError::Scan(e)),
368            }
369        }
370
371        println!("   ๐Ÿ“Š Total files uploaded: {files_uploaded}");
372
373        // Step 4: Start prescan with available options
374        let build_id = if config.auto_scan {
375            println!("\n๐Ÿ” Step 4: Starting prescan and scan...");
376            match scan_api
377                .upload_and_scan_sandbox(&app_id, &sandbox_id, &config.file_paths[0])
378                .await
379            {
380                Ok(build_id) => {
381                    println!("   โœ… Scan started successfully with build ID: {build_id}");
382                    Some(build_id)
383                }
384                Err(ScanError::Unauthorized) => {
385                    return Err(WorkflowError::AccessDenied(
386                        "Access denied starting scan".to_string(),
387                    ));
388                }
389                Err(ScanError::PermissionDenied) => {
390                    return Err(WorkflowError::AccessDenied(
391                        "Permission denied starting scan".to_string(),
392                    ));
393                }
394                Err(e) => {
395                    println!("   โš ๏ธ  Warning: Could not start scan automatically: {e}");
396                    println!(
397                        "   ๐Ÿ’ก You may need to start the scan manually from the Veracode platform"
398                    );
399                    None
400                }
401            }
402        } else {
403            println!("\nโญ๏ธ  Step 4: Skipping automatic scan (auto_scan = false)");
404            None
405        };
406
407        let result = WorkflowResultData {
408            application,
409            sandbox,
410            app_id,
411            sandbox_id,
412            build_id: build_id.clone(),
413            app_created,
414            sandbox_created,
415            files_uploaded,
416        };
417
418        println!("\nโœ… Workflow completed successfully!");
419        println!("   ๐Ÿ“Š Summary:");
420        println!(
421            "   - Application: {} (created: {})",
422            config.app_name, app_created
423        );
424        println!(
425            "   - Sandbox: {} (created: {})",
426            config.sandbox_name, sandbox_created
427        );
428        println!("   - Files uploaded: {files_uploaded}");
429        if let Some(build_id) = &build_id {
430            println!(
431                "   - Scan started: {} (build ID: {})",
432                config.auto_scan, build_id
433            );
434        } else {
435            println!("   - Scan started: {}", config.auto_scan);
436        }
437
438        Ok(result)
439    }
440
441    /// Execute a simplified workflow with just application and sandbox creation
442    ///
443    /// This method implements a subset of the full workflow for cases where
444    /// you only need to ensure the application and sandbox exist.
445    ///
446    /// # Arguments
447    ///
448    /// * `app_name` - Application name
449    /// * `sandbox_name` - Sandbox name
450    /// * `business_criticality` - Business criticality for new applications
451    ///
452    /// # Returns
453    ///
454    /// A `Result` containing application and sandbox information.
455    pub async fn ensure_app_and_sandbox(
456        &self,
457        app_name: &str,
458        sandbox_name: &str,
459        business_criticality: BusinessCriticality,
460    ) -> WorkflowResult<(Application, Sandbox, String, String)> {
461        let config = WorkflowConfig::new(app_name.to_string(), sandbox_name.to_string())
462            .with_business_criticality(business_criticality)
463            .with_auto_scan(false);
464
465        let result = self.execute_complete_workflow(config).await?;
466        Ok((
467            result.application,
468            result.sandbox,
469            result.app_id,
470            result.sandbox_id,
471        ))
472    }
473
474    /// Get application by name with helpful error messages
475    ///
476    /// # Arguments
477    ///
478    /// * `app_name` - Application name to search for
479    ///
480    /// # Returns
481    ///
482    /// A `Result` containing the application or an error.
483    pub async fn get_application_by_name(&self, app_name: &str) -> WorkflowResult<Application> {
484        match self.client.get_application_by_name(app_name).await? {
485            Some(app) => Ok(app),
486            None => Err(WorkflowError::NotFound(format!(
487                "Application '{app_name}' not found"
488            ))),
489        }
490    }
491
492    /// Get sandbox by name with helpful error messages
493    ///
494    /// # Arguments
495    ///
496    /// * `app_guid` - Application GUID
497    /// * `sandbox_name` - Sandbox name to search for
498    ///
499    /// # Returns
500    ///
501    /// A `Result` containing the sandbox or an error.
502    pub async fn get_sandbox_by_name(
503        &self,
504        app_guid: &str,
505        sandbox_name: &str,
506    ) -> WorkflowResult<Sandbox> {
507        let sandbox_api = self.client.sandbox_api();
508        match sandbox_api
509            .get_sandbox_by_name(app_guid, sandbox_name)
510            .await?
511        {
512            Some(sandbox) => Ok(sandbox),
513            None => Err(WorkflowError::NotFound(format!(
514                "Sandbox '{sandbox_name}' not found"
515            ))),
516        }
517    }
518
519    /// Delete all builds from a sandbox
520    ///
521    /// This removes all uploaded files and scan data from the sandbox.
522    ///
523    /// # Arguments
524    ///
525    /// * `app_name` - Application name
526    /// * `sandbox_name` - Sandbox name
527    ///
528    /// # Returns
529    ///
530    /// A `Result` indicating success or an error.
531    pub async fn delete_sandbox_builds(
532        &self,
533        app_name: &str,
534        sandbox_name: &str,
535    ) -> WorkflowResult<()> {
536        println!("๐Ÿ—‘๏ธ  Deleting builds from sandbox '{sandbox_name}'...");
537
538        // Get application and sandbox
539        let app = self.get_application_by_name(app_name).await?;
540        let sandbox = self.get_sandbox_by_name(&app.guid, sandbox_name).await?;
541
542        // Get IDs for XML API
543        let app_id = self.client.get_app_id_from_guid(&app.guid).await?;
544        let sandbox_api = self.client.sandbox_api();
545        let sandbox_id = sandbox_api
546            .get_sandbox_id_from_guid(&app.guid, &sandbox.guid)
547            .await?;
548
549        // Delete all builds using XML API
550        let scan_api = self.client.scan_api();
551        match scan_api
552            .delete_all_sandbox_builds(&app_id, &sandbox_id)
553            .await
554        {
555            Ok(_) => {
556                println!("   โœ… Successfully deleted all builds from sandbox '{sandbox_name}'");
557                Ok(())
558            }
559            Err(ScanError::Unauthorized) => Err(WorkflowError::AccessDenied(
560                "Access denied deleting sandbox builds".to_string(),
561            )),
562            Err(ScanError::PermissionDenied) => Err(WorkflowError::AccessDenied(
563                "Permission denied deleting sandbox builds".to_string(),
564            )),
565            Err(ScanError::BuildNotFound) => {
566                println!("   โ„น๏ธ  No builds found to delete in sandbox '{sandbox_name}'");
567                Ok(())
568            }
569            Err(e) => Err(WorkflowError::Scan(e)),
570        }
571    }
572
573    /// Delete a sandbox
574    ///
575    /// This removes the sandbox and all its associated data.
576    ///
577    /// # Arguments
578    ///
579    /// * `app_name` - Application name
580    /// * `sandbox_name` - Sandbox name
581    ///
582    /// # Returns
583    ///
584    /// A `Result` indicating success or an error.
585    pub async fn delete_sandbox(&self, app_name: &str, sandbox_name: &str) -> WorkflowResult<()> {
586        println!("๐Ÿ—‘๏ธ  Deleting sandbox '{sandbox_name}'...");
587
588        // Get application and sandbox
589        let app = self.get_application_by_name(app_name).await?;
590        let sandbox = self.get_sandbox_by_name(&app.guid, sandbox_name).await?;
591
592        // First delete all builds
593        let _ = self.delete_sandbox_builds(app_name, sandbox_name).await;
594
595        // Delete the sandbox using REST API
596        let sandbox_api = self.client.sandbox_api();
597        match sandbox_api.delete_sandbox(&app.guid, &sandbox.guid).await {
598            Ok(_) => {
599                println!("   โœ… Successfully deleted sandbox '{sandbox_name}'");
600                Ok(())
601            }
602            Err(SandboxError::Api(VeracodeError::InvalidResponse(msg)))
603                if msg.contains("403") || msg.contains("401") =>
604            {
605                Err(WorkflowError::AccessDenied(format!(
606                    "Access denied deleting sandbox '{sandbox_name}': {msg}"
607                )))
608            }
609            Err(SandboxError::NotFound) => {
610                println!(
611                    "   โ„น๏ธ  Sandbox '{sandbox_name}' not found (may have been already deleted)"
612                );
613                Ok(())
614            }
615            Err(e) => Err(WorkflowError::Sandbox(e)),
616        }
617    }
618
619    /// Delete an application
620    ///
621    /// This removes the application and all its associated data including all sandboxes.
622    /// Use with extreme caution as this is irreversible.
623    ///
624    /// # Arguments
625    ///
626    /// * `app_name` - Application name
627    ///
628    /// # Returns
629    ///
630    /// A `Result` indicating success or an error.
631    pub async fn delete_application(&self, app_name: &str) -> WorkflowResult<()> {
632        println!("๐Ÿ—‘๏ธ  Deleting application '{app_name}'...");
633
634        // Get application
635        let app = self.get_application_by_name(app_name).await?;
636
637        // First, delete all sandboxes
638        let sandbox_api = self.client.sandbox_api();
639        match sandbox_api.list_sandboxes(&app.guid, None).await {
640            Ok(sandboxes) => {
641                for sandbox in sandboxes {
642                    println!("   ๐Ÿ—‘๏ธ  Deleting sandbox: {}", sandbox.name);
643                    let _ = self.delete_sandbox(app_name, &sandbox.name).await;
644                }
645            }
646            Err(e) => {
647                println!("   โš ๏ธ  Warning: Could not list sandboxes for cleanup: {e}");
648            }
649        }
650
651        // Delete main application builds
652        let app_id = self.client.get_app_id_from_guid(&app.guid).await?;
653        let scan_api = self.client.scan_api();
654        match scan_api.delete_all_app_builds(&app_id).await {
655            Ok(_) => println!("   โœ… Deleted all application builds"),
656            Err(e) => println!("   โš ๏ธ  Warning: Could not delete application builds: {e}"),
657        }
658
659        // Delete the application using REST API
660        match self.client.delete_application(&app.guid).await {
661            Ok(_) => {
662                println!("   โœ… Successfully deleted application '{app_name}'");
663                Ok(())
664            }
665            Err(VeracodeError::InvalidResponse(msg))
666                if msg.contains("403") || msg.contains("401") =>
667            {
668                Err(WorkflowError::AccessDenied(format!(
669                    "Access denied deleting application '{app_name}': {msg}"
670                )))
671            }
672            Err(VeracodeError::NotFound(_)) => {
673                println!(
674                    "   โ„น๏ธ  Application '{app_name}' not found (may have been already deleted)"
675                );
676                Ok(())
677            }
678            Err(e) => Err(WorkflowError::Api(e)),
679        }
680    }
681
682    /// Complete cleanup workflow
683    ///
684    /// This method performs a complete cleanup of an application and all its resources:
685    /// 1. Delete all builds from all sandboxes
686    /// 2. Delete all sandboxes  
687    /// 3. Delete all application builds
688    /// 4. Delete the application
689    ///
690    /// # Arguments
691    ///
692    /// * `app_name` - Application name to clean up
693    ///
694    /// # Returns
695    ///
696    /// A `Result` indicating success or an error.
697    pub async fn complete_cleanup(&self, app_name: &str) -> WorkflowResult<()> {
698        println!("๐Ÿงน Starting complete cleanup for application '{app_name}'");
699        println!("   โš ๏ธ  WARNING: This will delete ALL data associated with this application");
700        println!("   This includes all sandboxes, builds, and scan results");
701
702        match self.delete_application(app_name).await {
703            Ok(_) => {
704                println!("โœ… Complete cleanup finished successfully");
705                Ok(())
706            }
707            Err(WorkflowError::NotFound(_)) => {
708                println!("โ„น๏ธ  Application '{app_name}' not found - nothing to clean up");
709                Ok(())
710            }
711            Err(e) => {
712                println!("โŒ Cleanup encountered errors: {e}");
713                Err(e)
714            }
715        }
716    }
717
718    /// Ensure a build exists for an application or sandbox
719    ///
720    /// This method checks if a build exists and creates one if it doesn't.
721    /// This is required for uploadlargefile.do operations.
722    ///
723    /// # Arguments
724    ///
725    /// * `app_id` - Application ID (numeric)
726    /// * `sandbox_id` - Optional sandbox ID (numeric)
727    /// * `version` - Optional build version
728    ///
729    /// # Returns
730    ///
731    /// A `Result` containing the build information or an error.
732    pub async fn ensure_build_exists(
733        &self,
734        app_id: &str,
735        sandbox_id: Option<&str>,
736        version: Option<&str>,
737    ) -> WorkflowResult<Build> {
738        self.ensure_build_exists_with_policy(app_id, sandbox_id, version, 1)
739            .await
740    }
741
742    /// Ensure a build exists for the application/sandbox with configurable deletion policy
743    ///
744    /// This method checks if a build already exists and handles it according to the deletion policy:
745    /// - Policy 0: Never delete builds, fail if build exists
746    /// - Policy 1: Delete only "safe" builds (incomplete, failed, cancelled states)
747    /// - Policy 2: Delete any build except "Results Ready"
748    ///
749    /// # Arguments
750    ///
751    /// * `app_id` - Application ID (numeric)
752    /// * `sandbox_id` - Optional sandbox ID (numeric)
753    /// * `version` - Optional build version
754    /// * `deletion_policy` - Build deletion policy level (0, 1, or 2)
755    ///
756    /// # Returns
757    ///
758    /// A `Result` containing the build information or an error.
759    pub async fn ensure_build_exists_with_policy(
760        &self,
761        app_id: &str,
762        sandbox_id: Option<&str>,
763        version: Option<&str>,
764        deletion_policy: u8,
765    ) -> WorkflowResult<Build> {
766        println!("๐Ÿ” Checking if build exists (deletion policy: {deletion_policy})...");
767
768        let build_api = self.client.build_api();
769
770        // Try to get existing build info
771        let get_request = crate::build::GetBuildInfoRequest {
772            app_id: app_id.to_string(),
773            build_id: None, // Get most recent
774            sandbox_id: sandbox_id.map(|s| s.to_string()),
775        };
776
777        match build_api.get_build_info(get_request).await {
778            Ok(build) => {
779                println!("   ๐Ÿ“‹ Build already exists: {}", build.build_id);
780                if let Some(build_version) = &build.version {
781                    println!("      Existing Version: {build_version}");
782                }
783
784                // Parse build status from attributes
785                let build_status_str = build
786                    .attributes
787                    .get("status")
788                    .or_else(|| build.attributes.get("analysis_status"))
789                    .or_else(|| build.attributes.get("scan_status"))
790                    .map(|s| s.as_str())
791                    .unwrap_or("Unknown");
792
793                let build_status = crate::build::BuildStatus::from_string(build_status_str);
794                println!("      Build Status: {build_status}");
795
796                // Check deletion policy
797                if deletion_policy == 0 {
798                    return Err(WorkflowError::Workflow(format!(
799                        "Build {} already exists and deletion policy is set to 'Never delete' (0). Cannot proceed with upload.",
800                        build.build_id
801                    )));
802                }
803
804                // Special handling for "Results Ready" builds - create new build to preserve results
805                if build_status == crate::build::BuildStatus::ResultsReady {
806                    println!(
807                        "   ๐Ÿ“‹ Build has 'Results Ready' status - creating new build to preserve existing results"
808                    );
809                    self.create_build_for_upload(app_id, sandbox_id, version)
810                        .await
811                }
812                // Check if build is safe to delete according to policy
813                else if build_status.is_safe_to_delete(deletion_policy) {
814                    println!(
815                        "   ๐Ÿ—‘๏ธ  Build is safe to delete according to policy {deletion_policy}. Deleting..."
816                    );
817
818                    // Delete the existing build
819                    let delete_request = crate::build::DeleteBuildRequest {
820                        app_id: app_id.to_string(),
821                        sandbox_id: sandbox_id.map(|s| s.to_string()),
822                    };
823
824                    match build_api.delete_build(delete_request).await {
825                        Ok(_) => {
826                            println!("   โœ… Existing build deleted successfully");
827                        }
828                        Err(e) => {
829                            return Err(WorkflowError::Build(e));
830                        }
831                    }
832
833                    // Wait for build deletion to be fully processed by Veracode API
834                    println!("   โณ Waiting for build deletion to be fully processed...");
835                    self.wait_for_build_deletion(app_id, sandbox_id).await?;
836
837                    // Create new build
838                    println!("   โž• Creating new build...");
839                    self.create_build_for_upload(app_id, sandbox_id, version)
840                        .await
841                } else {
842                    return Err(WorkflowError::Workflow(format!(
843                        "Build {} has status '{}' which is not safe to delete with policy {} (0=Never, 1=Safe only, 2=Except Results Ready). Cannot proceed with upload.",
844                        build.build_id, build_status, deletion_policy
845                    )));
846                }
847            }
848            Err(crate::build::BuildError::BuildNotFound) => {
849                println!("   โž• No build found, creating new build...");
850                self.create_build_for_upload(app_id, sandbox_id, version)
851                    .await
852            }
853            Err(e) => {
854                println!("   โš ๏ธ  Error checking build existence: {e}");
855                // Try to create a build anyway
856                println!("   โž• Attempting to create new build...");
857                self.create_build_for_upload(app_id, sandbox_id, version)
858                    .await
859            }
860        }
861    }
862
863    /// Create a build for file upload operations
864    ///
865    /// # Arguments
866    ///
867    /// * `app_id` - Application ID (numeric)
868    /// * `sandbox_id` - Optional sandbox ID (numeric)
869    /// * `version` - Optional build version
870    ///
871    /// # Returns
872    ///
873    /// A `Result` containing the created build information or an error.
874    async fn create_build_for_upload(
875        &self,
876        app_id: &str,
877        sandbox_id: Option<&str>,
878        version: Option<&str>,
879    ) -> WorkflowResult<Build> {
880        let build_api = self.client.build_api();
881
882        let build_version = version.map(|v| v.to_string()).unwrap_or_else(|| {
883            // Generate a version based on timestamp if none provided
884            let timestamp = std::time::SystemTime::now()
885                .duration_since(std::time::UNIX_EPOCH)
886                .unwrap()
887                .as_secs();
888            format!("build-{timestamp}")
889        });
890
891        let create_request = crate::build::CreateBuildRequest {
892            app_id: app_id.to_string(),
893            version: Some(build_version.clone()),
894            lifecycle_stage: Some(crate::build::default_lifecycle_stage().to_string()),
895            launch_date: None,
896            sandbox_id: sandbox_id.map(|s| s.to_string()),
897        };
898
899        match build_api.create_build(create_request).await {
900            Ok(build) => {
901                println!("   โœ… Build created successfully: {}", build.build_id);
902                println!("      Version: {build_version}");
903                if sandbox_id.is_some() {
904                    println!("      Type: Sandbox build");
905                } else {
906                    println!("      Type: Application build");
907                }
908                Ok(build)
909            }
910            Err(e) => {
911                println!("   โŒ Build creation failed: {e}");
912                Err(WorkflowError::Build(e))
913            }
914        }
915    }
916
917    /// Wait for build deletion to be fully processed by the Veracode API
918    ///
919    /// This method waits up to 15 seconds (5 attempts ร— 3 seconds) for the build
920    /// to be completely removed from the Veracode system before allowing recreation.
921    ///
922    /// # Arguments
923    ///
924    /// * `app_id` - Application ID (numeric)
925    /// * `sandbox_id` - Optional sandbox ID (numeric)
926    ///
927    /// # Returns
928    ///
929    /// A `Result` indicating success or timeout error.
930    async fn wait_for_build_deletion(
931        &self,
932        app_id: &str,
933        sandbox_id: Option<&str>,
934    ) -> WorkflowResult<()> {
935        let build_api = self.client.build_api();
936        let max_attempts = 5;
937        let delay_seconds = 3;
938
939        // Keep this outside the loop to avoid repeated Duration creation
940        let sleep_duration = tokio::time::Duration::from_secs(delay_seconds);
941
942        for attempt in 1..=max_attempts {
943            // Wait 3 seconds before checking
944            tokio::time::sleep(sleep_duration).await;
945
946            // Recreate request each time - cheaper than cloning
947            let get_request = crate::build::GetBuildInfoRequest {
948                app_id: app_id.to_string(),
949                build_id: None,
950                sandbox_id: sandbox_id.map(|s| s.to_string()),
951            };
952
953            match build_api.get_build_info(get_request).await {
954                Ok(_build) => {
955                    // Build still exists, continue waiting
956                    if attempt < max_attempts {
957                        println!(
958                            "      โณ Build still exists, waiting {delay_seconds} more seconds... (attempt {attempt}/{max_attempts})"
959                        );
960                    } else {
961                        println!(
962                            "      โš ๏ธ  Build still exists after {max_attempts} attempts, proceeding anyway"
963                        );
964                    }
965                }
966                Err(crate::build::BuildError::BuildNotFound) => {
967                    // Build is gone, we can proceed
968                    println!(
969                        "      โœ… Build deletion confirmed (attempt {attempt}/{max_attempts})"
970                    );
971                    return Ok(());
972                }
973                Err(e) => {
974                    // Other error, might be temporary API issue, continue waiting
975                    println!("      โš ๏ธ  Error checking build status: {e} (attempt {attempt})");
976                }
977            }
978        }
979
980        // Even if build still exists after max attempts, continue with creation
981        // The create operation might still succeed or provide a clearer error
982        Ok(())
983    }
984
985    /// Upload a large file with automatic build management
986    ///
987    /// This method ensures a build exists before attempting to use uploadlargefile.do.
988    /// If no build exists, it creates one automatically.
989    ///
990    /// # Arguments
991    ///
992    /// * `app_id` - Application ID (numeric)
993    /// * `sandbox_id` - Optional sandbox ID (numeric)
994    /// * `file_path` - Path to the file to upload
995    /// * `filename` - Optional custom filename for flaw matching
996    /// * `version` - Optional build version (auto-generated if not provided)
997    ///
998    /// # Returns
999    ///
1000    /// A `Result` containing the uploaded file information or an error.
1001    pub async fn upload_large_file_with_build_management(
1002        &self,
1003        app_id: &str,
1004        sandbox_id: Option<&str>,
1005        file_path: &str,
1006        filename: Option<&str>,
1007        version: Option<&str>,
1008    ) -> WorkflowResult<crate::scan::UploadedFile> {
1009        println!("๐Ÿš€ Starting large file upload with build management");
1010        println!("   File: {file_path}");
1011        if let Some(sandbox_id) = sandbox_id {
1012            println!("   Target: Sandbox {sandbox_id}");
1013        } else {
1014            println!("   Target: Application {app_id}");
1015        }
1016
1017        // Step 1: Ensure build exists
1018        let _build = self
1019            .ensure_build_exists(app_id, sandbox_id, version)
1020            .await?;
1021
1022        // Step 2: Upload file using large file API
1023        println!("\n๐Ÿ“ค Uploading file using uploadlargefile.do...");
1024        let scan_api = self.client.scan_api();
1025
1026        let upload_request = crate::scan::UploadLargeFileRequest {
1027            app_id: app_id.to_string(),
1028            file_path: file_path.to_string(),
1029            filename: filename.map(|s| s.to_string()),
1030            sandbox_id: sandbox_id.map(|s| s.to_string()),
1031        };
1032
1033        match scan_api.upload_large_file(upload_request).await {
1034            Ok(uploaded_file) => {
1035                println!("   โœ… Large file uploaded successfully:");
1036                println!("      File ID: {}", uploaded_file.file_id);
1037                println!("      File Name: {}", uploaded_file.file_name);
1038                println!("      Size: {} bytes", uploaded_file.file_size);
1039                Ok(uploaded_file)
1040            }
1041            Err(e) => {
1042                println!("   โŒ Large file upload failed: {e}");
1043                Err(WorkflowError::Scan(e))
1044            }
1045        }
1046    }
1047
1048    /// Upload a large file with progress tracking and build management
1049    ///
1050    /// This method ensures a build exists before attempting to use uploadlargefile.do
1051    /// and provides progress tracking capabilities.
1052    ///
1053    /// # Arguments
1054    ///
1055    /// * `app_id` - Application ID (numeric)
1056    /// * `sandbox_id` - Optional sandbox ID (numeric)
1057    /// * `file_path` - Path to the file to upload
1058    /// * `filename` - Optional custom filename for flaw matching
1059    /// * `version` - Optional build version (auto-generated if not provided)
1060    /// * `progress_callback` - Callback function for progress updates
1061    ///
1062    /// # Returns
1063    ///
1064    /// A `Result` containing the uploaded file information or an error.
1065    pub async fn upload_large_file_with_progress_and_build_management<F>(
1066        &self,
1067        app_id: &str,
1068        sandbox_id: Option<&str>,
1069        file_path: &str,
1070        filename: Option<&str>,
1071        version: Option<&str>,
1072        progress_callback: F,
1073    ) -> WorkflowResult<crate::scan::UploadedFile>
1074    where
1075        F: Fn(u64, u64, f64) + Send + Sync,
1076    {
1077        println!("๐Ÿš€ Starting large file upload with progress tracking and build management");
1078        println!("   File: {file_path}");
1079
1080        // Step 1: Ensure build exists
1081        let _build = self
1082            .ensure_build_exists(app_id, sandbox_id, version)
1083            .await?;
1084
1085        // Step 2: Upload file with progress tracking
1086        println!("\n๐Ÿ“ค Uploading file with progress tracking...");
1087        let scan_api = self.client.scan_api();
1088
1089        let upload_request = crate::scan::UploadLargeFileRequest {
1090            app_id: app_id.to_string(),
1091            file_path: file_path.to_string(),
1092            filename: filename.map(|s| s.to_string()),
1093            sandbox_id: sandbox_id.map(|s| s.to_string()),
1094        };
1095
1096        match scan_api
1097            .upload_large_file_with_progress(upload_request, progress_callback)
1098            .await
1099        {
1100            Ok(uploaded_file) => {
1101                println!("   โœ… Large file uploaded successfully with progress tracking");
1102                Ok(uploaded_file)
1103            }
1104            Err(e) => {
1105                println!("   โŒ Large file upload with progress failed: {e}");
1106                Err(WorkflowError::Scan(e))
1107            }
1108        }
1109    }
1110
1111    /// Complete file upload workflow with intelligent endpoint selection and build management
1112    ///
1113    /// This method automatically chooses between uploadfile.do and uploadlargefile.do
1114    /// based on file size and ensures builds exist when needed.
1115    ///
1116    /// # Arguments
1117    ///
1118    /// * `app_id` - Application ID (numeric)
1119    /// * `sandbox_id` - Optional sandbox ID (numeric)
1120    /// * `file_path` - Path to the file to upload
1121    /// * `filename` - Optional custom filename
1122    /// * `version` - Optional build version
1123    ///
1124    /// # Returns
1125    ///
1126    /// A `Result` containing the uploaded file information or an error.
1127    pub async fn upload_file_with_smart_build_management(
1128        &self,
1129        app_id: &str,
1130        sandbox_id: Option<&str>,
1131        file_path: &str,
1132        filename: Option<&str>,
1133        version: Option<&str>,
1134    ) -> WorkflowResult<crate::scan::UploadedFile> {
1135        // Check file size to determine upload strategy
1136        let file_metadata = std::fs::metadata(file_path)
1137            .map_err(|e| WorkflowError::Workflow(format!("Cannot access file {file_path}: {e}")))?;
1138
1139        let file_size = file_metadata.len();
1140        const LARGE_FILE_THRESHOLD: u64 = 100 * 1024 * 1024; // 100MB
1141
1142        println!("๐Ÿ” File size: {file_size} bytes");
1143
1144        if file_size > LARGE_FILE_THRESHOLD {
1145            println!("๐Ÿ“ฆ Using large file upload (uploadlargefile.do) with build management");
1146            self.upload_large_file_with_build_management(
1147                app_id, sandbox_id, file_path, filename, version,
1148            )
1149            .await
1150        } else {
1151            println!("๐Ÿ“ฆ Using standard file upload (uploadfile.do)");
1152            let scan_api = self.client.scan_api();
1153
1154            let upload_request = crate::scan::UploadFileRequest {
1155                app_id: app_id.to_string(),
1156                file_path: file_path.to_string(),
1157                save_as: filename.map(|s| s.to_string()),
1158                sandbox_id: sandbox_id.map(|s| s.to_string()),
1159            };
1160
1161            match scan_api.upload_file(upload_request).await {
1162                Ok(uploaded_file) => {
1163                    println!("   โœ… File uploaded successfully via uploadfile.do");
1164                    Ok(uploaded_file)
1165                }
1166                Err(e) => {
1167                    println!("   โŒ Standard upload failed: {e}");
1168                    Err(WorkflowError::Scan(e))
1169                }
1170            }
1171        }
1172    }
1173
1174    /// Get or create a build for upload operations
1175    ///
1176    /// This is a convenience method that handles the build dependency for upload operations.
1177    ///
1178    /// # Arguments
1179    ///
1180    /// * `app_id` - Application ID (numeric)
1181    /// * `sandbox_id` - Optional sandbox ID (numeric)
1182    /// * `version` - Optional build version
1183    ///
1184    /// # Returns
1185    ///
1186    /// A `Result` containing the build information or an error.
1187    pub async fn get_or_create_build(
1188        &self,
1189        app_id: &str,
1190        sandbox_id: Option<&str>,
1191        version: Option<&str>,
1192    ) -> WorkflowResult<Build> {
1193        self.ensure_build_exists(app_id, sandbox_id, version).await
1194    }
1195}
1196
1197#[cfg(test)]
1198mod tests {
1199    use super::*;
1200
1201    #[test]
1202    fn test_workflow_config_builder() {
1203        let config = WorkflowConfig::new("MyApp".to_string(), "MySandbox".to_string())
1204            .with_business_criticality(BusinessCriticality::High)
1205            .with_app_description("Test application".to_string())
1206            .with_file("test.jar".to_string())
1207            .with_auto_scan(false);
1208
1209        assert_eq!(config.app_name, "MyApp");
1210        assert_eq!(config.sandbox_name, "MySandbox");
1211        assert_eq!(
1212            config.business_criticality as i32,
1213            BusinessCriticality::High as i32
1214        );
1215        assert_eq!(config.app_description, Some("Test application".to_string()));
1216        assert_eq!(config.file_paths, vec!["test.jar"]);
1217        assert!(!config.auto_scan);
1218    }
1219
1220    #[test]
1221    fn test_workflow_error_display() {
1222        let error = WorkflowError::NotFound("Application not found".to_string());
1223        assert_eq!(error.to_string(), "Not found: Application not found");
1224
1225        let error = WorkflowError::AccessDenied("Permission denied".to_string());
1226        assert_eq!(error.to_string(), "Access denied: Permission denied");
1227
1228        let error = WorkflowError::Workflow("Custom error".to_string());
1229        assert_eq!(error.to_string(), "Workflow error: Custom error");
1230    }
1231}