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};
13use log::{debug, info};
14
15/// High-level workflow operations for Veracode platform
16pub struct VeracodeWorkflow {
17    client: VeracodeClient,
18}
19
20/// Result type for workflow operations
21pub type WorkflowResult<T> = Result<T, WorkflowError>;
22
23/// Errors that can occur during workflow operations
24#[derive(Debug)]
25pub enum WorkflowError {
26    /// Veracode API error
27    Api(VeracodeError),
28    /// Sandbox operation error
29    Sandbox(SandboxError),
30    /// Scan operation error
31    Scan(ScanError),
32    /// Build operation error
33    Build(BuildError),
34    /// Workflow-specific error
35    Workflow(String),
36    /// Access denied
37    AccessDenied(String),
38    /// Resource not found
39    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/// Configuration for the complete XML API workflow
83#[derive(Debug, Clone)]
84pub struct WorkflowConfig {
85    /// Application name
86    pub app_name: String,
87    /// Sandbox name
88    pub sandbox_name: String,
89    /// Business criticality for new applications
90    pub business_criticality: BusinessCriticality,
91    /// Application description (optional)
92    pub app_description: Option<String>,
93    /// Sandbox description (optional)
94    pub sandbox_description: Option<String>,
95    /// Files to upload
96    pub file_paths: Vec<String>,
97    /// Whether to start scan automatically after upload
98    pub auto_scan: bool,
99    /// Whether to scan all modules
100    pub scan_all_modules: bool,
101}
102
103impl WorkflowConfig {
104    /// Create a new workflow configuration
105    #[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    /// Set business criticality
120    #[must_use]
121    pub fn with_business_criticality(mut self, criticality: BusinessCriticality) -> Self {
122        self.business_criticality = criticality;
123        self
124    }
125
126    /// Set application description
127    #[must_use]
128    pub fn with_app_description(mut self, description: String) -> Self {
129        self.app_description = Some(description);
130        self
131    }
132
133    /// Set sandbox description
134    #[must_use]
135    pub fn with_sandbox_description(mut self, description: String) -> Self {
136        self.sandbox_description = Some(description);
137        self
138    }
139
140    /// Add file to upload
141    #[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    /// Add multiple files to upload
148    #[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    /// Set auto-scan behavior
155    #[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    /// Set scan all modules behavior
162    #[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/// Result of the complete workflow
170#[derive(Debug, Clone)]
171pub struct WorkflowResultData {
172    /// Application information
173    pub application: Application,
174    /// Sandbox information
175    pub sandbox: Sandbox,
176    /// Numeric application ID for XML API
177    pub app_id: String,
178    /// Numeric sandbox ID for XML API
179    pub sandbox_id: String,
180    /// Build ID from scan initiation (if scan was started)
181    pub build_id: Option<String>,
182    /// Whether the application was newly created
183    pub app_created: bool,
184    /// Whether the sandbox was newly created
185    pub sandbox_created: bool,
186    /// Number of files uploaded
187    pub files_uploaded: usize,
188}
189
190impl VeracodeWorkflow {
191    /// Create a new workflow instance
192    #[must_use]
193    pub fn new(client: VeracodeClient) -> Self {
194        Self { client }
195    }
196
197    /// Execute the complete XML API workflow
198    ///
199    /// This method implements the full workflow:
200    /// 1. Check for application existence, create if not exist
201    /// 2. Handle access denied scenarios
202    /// 3. Check sandbox exists, if not create
203    /// 4. Handle access denied scenarios  
204    /// 5. Upload multiple files to sandbox
205    /// 6. Start prescan with available options
206    ///
207    /// # Arguments
208    ///
209    /// * `config` - Workflow configuration
210    ///
211    /// # Returns
212    ///
213    /// A `Result` containing the workflow result or an error.
214    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        // Step 1: Check for Application existence, create if not exist
224        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, // No teams specified
246                        )
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        // Get numeric app_id for XML API
279        let app_id = self.client.get_app_id_from_guid(&application.guid).await?;
280        info!("   ๐Ÿ“Š Application ID for XML API: {app_id}");
281
282        // Step 2: Check sandbox exists, if not create
283        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        // Get numeric sandbox_id for XML API
339        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        // Step 3: Upload multiple files to sandbox
345        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        // Step 4: Start prescan with available options
384        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    /// Execute a simplified workflow with just application and sandbox creation
452    ///
453    /// This method implements a subset of the full workflow for cases where
454    /// you only need to ensure the application and sandbox exist.
455    ///
456    /// # Arguments
457    ///
458    /// * `app_name` - Application name
459    /// * `sandbox_name` - Sandbox name
460    /// * `business_criticality` - Business criticality for new applications
461    ///
462    /// # Returns
463    ///
464    /// A `Result` containing application and sandbox information.
465    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    /// Get application by name with helpful error messages
485    ///
486    /// # Arguments
487    ///
488    /// * `app_name` - Application name to search for
489    ///
490    /// # Returns
491    ///
492    /// A `Result` containing the application or an error.
493    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    /// Get sandbox by name with helpful error messages
503    ///
504    /// # Arguments
505    ///
506    /// * `app_guid` - Application GUID
507    /// * `sandbox_name` - Sandbox name to search for
508    ///
509    /// # Returns
510    ///
511    /// A `Result` containing the sandbox or an error.
512    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    /// Delete all builds from a sandbox
530    ///
531    /// This removes all uploaded files and scan data from the sandbox.
532    ///
533    /// # Arguments
534    ///
535    /// * `app_name` - Application name
536    /// * `sandbox_name` - Sandbox name
537    ///
538    /// # Returns
539    ///
540    /// A `Result` indicating success or an error.
541    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        // Get application and sandbox
549        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        // Get IDs for XML API
553        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        // Delete all builds using XML API
560        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    /// Delete a sandbox
584    ///
585    /// This removes the sandbox and all its associated data.
586    ///
587    /// # Arguments
588    ///
589    /// * `app_name` - Application name
590    /// * `sandbox_name` - Sandbox name
591    ///
592    /// # Returns
593    ///
594    /// A `Result` indicating success or an error.
595    pub async fn delete_sandbox(&self, app_name: &str, sandbox_name: &str) -> WorkflowResult<()> {
596        info!("๐Ÿ—‘๏ธ  Deleting sandbox '{sandbox_name}'...");
597
598        // Get application and sandbox
599        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        // First delete all builds
603        let _ = self.delete_sandbox_builds(app_name, sandbox_name).await;
604
605        // Delete the sandbox using REST API
606        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    /// Delete an application
628    ///
629    /// This removes the application and all its associated data including all sandboxes.
630    /// Use with extreme caution as this is irreversible.
631    ///
632    /// # Arguments
633    ///
634    /// * `app_name` - Application name
635    ///
636    /// # Returns
637    ///
638    /// A `Result` indicating success or an error.
639    pub async fn delete_application(&self, app_name: &str) -> WorkflowResult<()> {
640        info!("๐Ÿ—‘๏ธ  Deleting application '{app_name}'...");
641
642        // Get application
643        let app = self.get_application_by_name(app_name).await?;
644
645        // First, delete all sandboxes
646        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        // Delete main application builds
660        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        // Delete the application using REST API
668        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    /// Complete cleanup workflow
689    ///
690    /// This method performs a complete cleanup of an application and all its resources:
691    /// 1. Delete all builds from all sandboxes
692    /// 2. Delete all sandboxes  
693    /// 3. Delete all application builds
694    /// 4. Delete the application
695    ///
696    /// # Arguments
697    ///
698    /// * `app_name` - Application name to clean up
699    ///
700    /// # Returns
701    ///
702    /// A `Result` indicating success or an error.
703    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    /// Ensure a build exists for an application or sandbox
725    ///
726    /// This method checks if a build exists and creates one if it doesn't.
727    /// This is required for uploadlargefile.do operations.
728    ///
729    /// # Arguments
730    ///
731    /// * `app_id` - Application ID (numeric)
732    /// * `sandbox_id` - Optional sandbox ID (numeric)
733    /// * `version` - Optional build version
734    ///
735    /// # Returns
736    ///
737    /// A `Result` containing the build information or an error.
738    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    /// Ensure a build exists for the application/sandbox with configurable deletion policy
749    ///
750    /// This method checks if a build already exists and handles it according to the deletion policy:
751    /// - Policy 0: Never delete builds, fail if build exists
752    /// - Policy 1: Delete only "safe" builds (incomplete, failed, cancelled states)
753    /// - Policy 2: Delete any build except "Results Ready"
754    ///
755    /// # Arguments
756    ///
757    /// * `app_id` - Application ID (numeric)
758    /// * `sandbox_id` - Optional sandbox ID (numeric)
759    /// * `version` - Optional build version
760    /// * `deletion_policy` - Build deletion policy level (0, 1, or 2)
761    ///
762    /// # Returns
763    ///
764    /// A `Result` containing the build information or an error.
765    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        // Try to get existing build info
777        match build_api
778            .get_build_info(&crate::build::GetBuildInfoRequest {
779                app_id: app_id.to_string(),
780                build_id: None, // Get most recent
781                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                // Parse build status from attributes
792                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                // Check deletion policy
804                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                // Special handling for "Results Ready" builds - create new build to preserve results
812                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                // Check if build is safe to delete according to policy
820                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                    // Delete the existing build
826                    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                    // Wait for build deletion to be fully processed by Veracode API
842                    info!("   โณ Waiting for build deletion to be fully processed...");
843                    self.wait_for_build_deletion(app_id, sandbox_id).await?;
844
845                    // Create new build
846                    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                // Try to create a build anyway
864                info!("   โž• Attempting to create new build...");
865                self.create_build_for_upload(app_id, sandbox_id, version)
866                    .await
867            }
868        }
869    }
870
871    /// Create a build for file upload operations
872    ///
873    /// # Arguments
874    ///
875    /// * `app_id` - Application ID (numeric)
876    /// * `sandbox_id` - Optional sandbox ID (numeric)
877    /// * `version` - Optional build version
878    ///
879    /// # Returns
880    ///
881    /// A `Result` containing the created build information or an error.
882    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            // Generate a version based on timestamp if none provided
894            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    /// Wait for build deletion to be fully processed by the Veracode API
929    ///
930    /// This method waits up to 15 seconds (5 attempts ร— 3 seconds) for the build
931    /// to be completely removed from the Veracode system before allowing recreation.
932    ///
933    /// # Arguments
934    ///
935    /// * `app_id` - Application ID (numeric)
936    /// * `sandbox_id` - Optional sandbox ID (numeric)
937    ///
938    /// # Returns
939    ///
940    /// A `Result` indicating success or timeout error.
941    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        // Keep this outside the loop to avoid repeated Duration creation
951        let sleep_duration = tokio::time::Duration::from_secs(delay_seconds);
952
953        for attempt in 1..=max_attempts {
954            // Wait 3 seconds before checking
955            tokio::time::sleep(sleep_duration).await;
956
957            // Check build status directly without intermediate variable
958            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                    // Build still exists, continue waiting
968                    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                    // Build is gone, we can proceed
980                    info!("      โœ… Build deletion confirmed (attempt {attempt}/{max_attempts})");
981                    return Ok(());
982                }
983                Err(e) => {
984                    // Other error, might be temporary API issue, continue waiting
985                    info!("      โš ๏ธ  Error checking build status: {e} (attempt {attempt})");
986                }
987            }
988        }
989
990        // Even if build still exists after max attempts, continue with creation
991        // The create operation might still succeed or provide a clearer error
992        Ok(())
993    }
994
995    /// Upload a large file with automatic build management
996    ///
997    /// This method ensures a build exists before attempting to use uploadlargefile.do.
998    /// If no build exists, it creates one automatically.
999    ///
1000    /// # Arguments
1001    ///
1002    /// * `app_id` - Application ID (numeric)
1003    /// * `sandbox_id` - Optional sandbox ID (numeric)
1004    /// * `file_path` - Path to the file to upload
1005    /// * `filename` - Optional custom filename for flaw matching
1006    /// * `version` - Optional build version (auto-generated if not provided)
1007    ///
1008    /// # Returns
1009    ///
1010    /// A `Result` containing the uploaded file information or an error.
1011    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        // Step 1: Ensure build exists
1028        let _build = self
1029            .ensure_build_exists(app_id, sandbox_id, version)
1030            .await?;
1031
1032        // Step 2: Upload file using large file API
1033        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    /// Upload a large file with progress tracking and build management
1060    ///
1061    /// This method ensures a build exists before attempting to use uploadlargefile.do
1062    /// and provides progress tracking capabilities.
1063    ///
1064    /// # Arguments
1065    ///
1066    /// * `app_id` - Application ID (numeric)
1067    /// * `sandbox_id` - Optional sandbox ID (numeric)
1068    /// * `file_path` - Path to the file to upload
1069    /// * `filename` - Optional custom filename for flaw matching
1070    /// * `version` - Optional build version (auto-generated if not provided)
1071    /// * `progress_callback` - Callback function for progress updates
1072    ///
1073    /// # Returns
1074    ///
1075    /// A `Result` containing the uploaded file information or an error.
1076    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        // Step 1: Ensure build exists
1092        let _build = self
1093            .ensure_build_exists(app_id, sandbox_id, version)
1094            .await?;
1095
1096        // Step 2: Upload file with progress tracking
1097        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    /// Complete file upload workflow with intelligent endpoint selection and build management
1124    ///
1125    /// This method automatically chooses between uploadfile.do and uploadlargefile.do
1126    /// based on file size and ensures builds exist when needed.
1127    ///
1128    /// # Arguments
1129    ///
1130    /// * `app_id` - Application ID (numeric)
1131    /// * `sandbox_id` - Optional sandbox ID (numeric)
1132    /// * `file_path` - Path to the file to upload
1133    /// * `filename` - Optional custom filename
1134    /// * `version` - Optional build version
1135    ///
1136    /// # Returns
1137    ///
1138    /// A `Result` containing the uploaded file information or an error.
1139    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        // Check file size to determine upload strategy
1148        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; // 100MB
1154
1155        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    /// Get or create a build for upload operations
1189    ///
1190    /// This is a convenience method that handles the build dependency for upload operations.
1191    ///
1192    /// # Arguments
1193    ///
1194    /// * `app_id` - Application ID (numeric)
1195    /// * `sandbox_id` - Optional sandbox ID (numeric)
1196    /// * `version` - Optional build version
1197    ///
1198    /// # Returns
1199    ///
1200    /// A `Result` containing the build information or an error.
1201    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}