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