veracode_platform/
lib.rs

1//! # Veracode API Client Library
2//!
3//! A comprehensive Rust client library for interacting with Veracode APIs including
4//! Applications, Identity, Pipeline Scan, and Sandbox APIs.
5//!
6//! This library provides a safe and ergonomic interface to the Veracode platform,
7//! handling HMAC authentication, request/response serialization, and error handling.
8//!
9//! ## Features
10//!
11//! - ๐Ÿ” **HMAC Authentication** - Built-in support for Veracode API credentials
12//! - ๐ŸŒ **Multi-Regional Support** - Automatic endpoint routing for Commercial, European, and Federal regions
13//! - ๐Ÿ”„ **Smart API Routing** - Automatically uses REST or XML APIs based on the operation
14//! - ๐Ÿ“ฑ **Applications API** - Manage applications, builds, and scans (REST)
15//! - ๐Ÿ‘ค **Identity API** - User and team management (REST)
16//! - ๐Ÿ” **Pipeline Scan API** - Automated security scanning in CI/CD pipelines (REST)
17//! - ๐Ÿงช **Sandbox API** - Development sandbox management (REST)
18//! - ๐Ÿ“ค **Sandbox Scan API** - File upload and scan operations (XML)
19//! - ๐Ÿš€ **Async/Await** - Built on tokio for high-performance async operations
20//! - โšก **Type-Safe** - Full Rust type safety with serde serialization
21//! - ๐Ÿ“Š **Rich Data Types** - Comprehensive data structures for all API responses
22//!
23//! ## Quick Start
24//!
25//! ```no_run
26//! use veracode_platform::{VeracodeConfig, VeracodeClient, VeracodeRegion};
27//!
28//! #[tokio::main]
29//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
30//!     // Create configuration - automatically supports both API types
31//!     let config = VeracodeConfig::new(
32//!         "your_api_id",
33//!         "your_api_key",
34//!     ).with_region(VeracodeRegion::Commercial); // Optional: defaults to Commercial
35//!
36//!     let client = VeracodeClient::new(config)?;
37//!     
38//!     // REST API modules (use api.veracode.*)
39//!     let apps = client.get_all_applications().await?;
40//!     let pipeline = client.pipeline_api();
41//!     let identity = client.identity_api();
42//!     let sandbox = client.sandbox_api();  // REST API for sandbox management
43//!     let policy = client.policy_api();
44//!     
45//!     // XML API modules (automatically use analysiscenter.veracode.*)
46//!     let scan = client.scan_api(); // XML API for scanning
47//!     
48//!     Ok(())
49//! }
50//! ```
51//!
52//! ## Regional Support
53//!
54//! The library automatically handles regional endpoints for both API types:
55//!
56//! ```no_run
57//! use veracode_platform::{VeracodeConfig, VeracodeRegion};
58//!
59//! // European region
60//! let config = VeracodeConfig::new("api_id", "api_key")
61//!     .with_region(VeracodeRegion::European);
62//! // REST APIs will use: api.veracode.eu
63//! // XML APIs will use: analysiscenter.veracode.eu
64//!
65//! // US Federal region  
66//! let config = VeracodeConfig::new("api_id", "api_key")
67//!     .with_region(VeracodeRegion::Federal);
68//! // REST APIs will use: api.veracode.us
69//! // XML APIs will use: analysiscenter.veracode.us
70//! ```
71//!
72//! ## API Types
73//!
74//! Different Veracode modules use different API endpoints:
75//!
76//! - **REST API (api.veracode.*)**: Applications, Identity, Pipeline, Policy, Sandbox management
77//! - **XML API (analysiscenter.veracode.*)**: Sandbox scanning operations
78//!
79//! The client automatically routes each module to the correct API type based on the operation.
80//!
81//! ## Sandbox Operations
82//!
83//! Note that sandbox functionality is split across two modules:
84//!
85//! - **`sandbox_api()`** - Sandbox management (create, delete, list sandboxes) via REST API
86//! - **`scan_api()`** - File upload and scan operations via XML API
87//!
88//! This separation reflects the underlying Veracode API architecture where sandbox management
89//! uses the newer REST endpoints while scan operations use the legacy XML endpoints.
90
91pub mod app;
92pub mod build;
93pub mod client;
94pub mod findings;
95pub mod identity;
96pub mod pipeline;
97pub mod policy;
98pub mod sandbox;
99pub mod scan;
100pub mod workflow;
101
102use reqwest::Error as ReqwestError;
103use secrecy::{ExposeSecret, SecretString};
104use std::fmt;
105use std::sync::Arc;
106use std::time::Duration;
107
108// Re-export common types for convenience
109pub use app::{
110    Application, ApplicationQuery, ApplicationsResponse, CreateApplicationRequest,
111    UpdateApplicationRequest,
112};
113pub use build::{
114    Build, BuildApi, BuildError, BuildList, CreateBuildRequest, DeleteBuildRequest,
115    DeleteBuildResult, GetBuildInfoRequest, GetBuildListRequest, UpdateBuildRequest,
116};
117pub use client::VeracodeClient;
118pub use findings::{
119    CweInfo, FindingCategory, FindingDetails, FindingStatus, FindingsApi, FindingsError,
120    FindingsQuery, FindingsResponse, RestFinding,
121};
122pub use identity::{
123    ApiCredential, BusinessUnit, CreateApiCredentialRequest, CreateTeamRequest, CreateUserRequest,
124    IdentityApi, IdentityError, Role, Team, UpdateTeamRequest, UpdateUserRequest, User, UserQuery,
125    UserType,
126};
127pub use pipeline::{
128    CreateScanRequest, DevStage, Finding, FindingsSummary, PipelineApi, PipelineError, Scan,
129    ScanConfig, ScanResults, ScanStage, ScanStatus, SecurityStandards, Severity,
130};
131pub use policy::{
132    ApiSource, PolicyApi, PolicyComplianceResult, PolicyComplianceStatus, PolicyError, PolicyRule,
133    PolicyScanRequest, PolicyScanResult, PolicyThresholds, ScanType, SecurityPolicy, SummaryReport,
134};
135pub use sandbox::{
136    ApiError, ApiErrorResponse, CreateSandboxRequest, Sandbox, SandboxApi, SandboxError,
137    SandboxListParams, SandboxScan, UpdateSandboxRequest,
138};
139pub use scan::{
140    BeginPreScanRequest, BeginScanRequest, PreScanMessage, PreScanResults, ScanApi, ScanError,
141    ScanInfo, ScanModule, UploadFileRequest, UploadLargeFileRequest, UploadProgress,
142    UploadProgressCallback, UploadedFile,
143};
144pub use workflow::{VeracodeWorkflow, WorkflowConfig, WorkflowError, WorkflowResultData};
145/// Retry configuration for HTTP requests
146#[derive(Debug, Clone)]
147pub struct RetryConfig {
148    /// Maximum number of retry attempts (default: 5)
149    pub max_attempts: u32,
150    /// Initial delay between retries in milliseconds (default: 1000ms)
151    pub initial_delay_ms: u64,
152    /// Maximum delay between retries in milliseconds (default: 30000ms)
153    pub max_delay_ms: u64,
154    /// Exponential backoff multiplier (default: 2.0)
155    pub backoff_multiplier: f64,
156    /// Maximum total time to spend on retries in milliseconds (default: 300000ms = 5 minutes)
157    pub max_total_delay_ms: u64,
158    /// Buffer time in seconds to add when waiting for rate limit window reset (default: 5s)
159    pub rate_limit_buffer_seconds: u64,
160    /// Maximum number of retry attempts specifically for rate limit errors (default: 1)
161    pub rate_limit_max_attempts: u32,
162    /// Whether to enable jitter in retry delays (default: true)
163    pub jitter_enabled: bool,
164}
165
166impl Default for RetryConfig {
167    fn default() -> Self {
168        Self {
169            max_attempts: 5,
170            initial_delay_ms: 1000,
171            max_delay_ms: 30000,
172            backoff_multiplier: 2.0,
173            max_total_delay_ms: 300_000,  // 5 minutes
174            rate_limit_buffer_seconds: 5, // 5 second buffer for rate limit windows
175            rate_limit_max_attempts: 1,   // Only retry once for rate limits
176            jitter_enabled: true,         // Enable jitter by default
177        }
178    }
179}
180
181impl RetryConfig {
182    /// Create a new retry configuration with conservative defaults
183    #[must_use]
184    pub fn new() -> Self {
185        Self::default()
186    }
187
188    /// Set the maximum number of retry attempts
189    #[must_use]
190    pub fn with_max_attempts(mut self, max_attempts: u32) -> Self {
191        self.max_attempts = max_attempts;
192        self
193    }
194
195    /// Set the initial delay between retries
196    #[must_use]
197    pub fn with_initial_delay(mut self, delay_ms: u64) -> Self {
198        self.initial_delay_ms = delay_ms;
199        self
200    }
201
202    /// Set the initial delay between retries (alias for compatibility)
203    #[must_use]
204    pub fn with_initial_delay_millis(mut self, delay_ms: u64) -> Self {
205        self.initial_delay_ms = delay_ms;
206        self
207    }
208
209    /// Set the maximum delay between retries
210    #[must_use]
211    pub fn with_max_delay(mut self, delay_ms: u64) -> Self {
212        self.max_delay_ms = delay_ms;
213        self
214    }
215
216    /// Set the maximum delay between retries (alias for compatibility)
217    #[must_use]
218    pub fn with_max_delay_millis(mut self, delay_ms: u64) -> Self {
219        self.max_delay_ms = delay_ms;
220        self
221    }
222
223    /// Set the exponential backoff multiplier
224    #[must_use]
225    pub fn with_backoff_multiplier(mut self, multiplier: f64) -> Self {
226        self.backoff_multiplier = multiplier;
227        self
228    }
229
230    /// Set the exponential backoff multiplier (alias for compatibility)
231    #[must_use]
232    pub fn with_exponential_backoff(mut self, multiplier: f64) -> Self {
233        self.backoff_multiplier = multiplier;
234        self
235    }
236
237    /// Set the maximum total time to spend on retries
238    #[must_use]
239    pub fn with_max_total_delay(mut self, delay_ms: u64) -> Self {
240        self.max_total_delay_ms = delay_ms;
241        self
242    }
243
244    /// Set the buffer time to add when waiting for rate limit window reset
245    #[must_use]
246    pub fn with_rate_limit_buffer(mut self, buffer_seconds: u64) -> Self {
247        self.rate_limit_buffer_seconds = buffer_seconds;
248        self
249    }
250
251    /// Set the maximum number of retry attempts for rate limit errors
252    #[must_use]
253    pub fn with_rate_limit_max_attempts(mut self, max_attempts: u32) -> Self {
254        self.rate_limit_max_attempts = max_attempts;
255        self
256    }
257
258    /// Disable jitter in retry delays
259    ///
260    /// Jitter adds randomness to retry delays to prevent thundering herd problems.
261    /// Disabling jitter makes retry timing more predictable but may cause synchronized
262    /// retries from multiple clients.
263    #[must_use]
264    pub fn with_jitter_disabled(mut self) -> Self {
265        self.jitter_enabled = false;
266        self
267    }
268
269    /// Calculate the delay for a given attempt number using exponential backoff
270    #[must_use]
271    pub fn calculate_delay(&self, attempt: u32) -> Duration {
272        if attempt == 0 {
273            return Duration::from_millis(0);
274        }
275
276        let delay_ms = (self.initial_delay_ms as f64
277            * self.backoff_multiplier.powi((attempt - 1) as i32)) as u64;
278
279        let mut capped_delay = delay_ms.min(self.max_delay_ms);
280
281        // Apply jitter if enabled (ยฑ25% randomization)
282        if self.jitter_enabled {
283            use rand::Rng;
284            let jitter_range = (capped_delay as f64 * 0.25) as u64;
285            let min_delay = capped_delay.saturating_sub(jitter_range);
286            let max_delay = capped_delay + jitter_range;
287            capped_delay = rand::rng().random_range(min_delay..=max_delay);
288        }
289
290        Duration::from_millis(capped_delay)
291    }
292
293    /// Calculate the delay for rate limit (429) errors
294    ///
295    /// For Veracode's 500 requests/minute rate limiting, this calculates the optimal
296    /// wait time based on the current time within the minute window or uses the
297    /// server's Retry-After header if provided.
298    #[must_use]
299    pub fn calculate_rate_limit_delay(&self, retry_after_seconds: Option<u64>) -> Duration {
300        if let Some(seconds) = retry_after_seconds {
301            // Use the server's suggested delay
302            Duration::from_secs(seconds)
303        } else {
304            // Fall back to minute window calculation for Veracode's 500/minute limit
305            let now = std::time::SystemTime::now()
306                .duration_since(std::time::UNIX_EPOCH)
307                .unwrap_or_default();
308
309            let current_second = now.as_secs() % 60;
310
311            // Wait until the next minute window + configurable buffer to ensure window has reset
312            let seconds_until_next_minute = 60 - current_second;
313
314            Duration::from_secs(seconds_until_next_minute + self.rate_limit_buffer_seconds)
315        }
316    }
317
318    /// Check if an error is retryable based on its type
319    #[must_use]
320    pub fn is_retryable_error(&self, error: &VeracodeError) -> bool {
321        match error {
322            VeracodeError::Http(reqwest_error) => {
323                // Retry on network errors, timeouts, and temporary server errors
324                if reqwest_error.is_timeout()
325                    || reqwest_error.is_connect()
326                    || reqwest_error.is_request()
327                {
328                    return true;
329                }
330
331                // Check for retryable HTTP status codes
332                if let Some(status) = reqwest_error.status() {
333                    match status.as_u16() {
334                        // 429 Too Many Requests
335                        429 => true,
336                        // 502 Bad Gateway, 503 Service Unavailable, 504 Gateway Timeout
337                        502..=504 => true,
338                        // Other server errors (5xx) - retry conservatively
339                        500..=599 => true,
340                        // Don't retry client errors (4xx) except 429
341                        _ => false,
342                    }
343                } else {
344                    // Network-level errors without status codes are typically retryable
345                    true
346                }
347            }
348            // Don't retry authentication, serialization, or configuration errors
349            VeracodeError::Authentication(_)
350            | VeracodeError::Serialization(_)
351            | VeracodeError::InvalidConfig(_) => false,
352            // InvalidResponse could be temporary (like malformed JSON due to network issues)
353            VeracodeError::InvalidResponse(_) => true,
354            // NotFound is typically not retryable
355            VeracodeError::NotFound(_) => false,
356            // New retry-specific error is not retryable (avoid infinite loops)
357            VeracodeError::RetryExhausted(_) => false,
358            // Rate limited errors are retryable with special handling
359            VeracodeError::RateLimited { .. } => true,
360        }
361    }
362}
363
364/// Custom error type for Veracode API operations.
365///
366/// This enum represents all possible errors that can occur when interacting
367/// with the Veracode Applications API.
368#[derive(Debug)]
369pub enum VeracodeError {
370    /// HTTP request failed
371    Http(ReqwestError),
372    /// JSON serialization/deserialization failed
373    Serialization(serde_json::Error),
374    /// Authentication error (invalid credentials, signature generation failure, etc.)
375    Authentication(String),
376    /// API returned an error response
377    InvalidResponse(String),
378    /// Configuration is invalid
379    InvalidConfig(String),
380    /// When an item is not found
381    NotFound(String),
382    /// When all retry attempts have been exhausted
383    RetryExhausted(String),
384    /// Rate limit exceeded (HTTP 429) - includes server's suggested retry delay
385    RateLimited {
386        /// Number of seconds to wait before retrying (from Retry-After header)
387        retry_after_seconds: Option<u64>,
388        /// The original HTTP error response
389        message: String,
390    },
391}
392
393impl VeracodeClient {
394    /// Create a specialized client for XML API operations.
395    ///
396    /// This internal method creates a client configured for the XML API
397    /// (analysiscenter.veracode.*) based on the current region settings.
398    /// Used exclusively for sandbox scan operations that require the XML API.
399    fn new_xml_client(config: VeracodeConfig) -> Result<Self, VeracodeError> {
400        let mut xml_config = config;
401        xml_config.base_url = xml_config.xml_base_url.clone();
402        Self::new(xml_config)
403    }
404
405    /// Get an applications API instance.
406    /// Uses REST API (api.veracode.*).
407    #[must_use]
408    pub fn applications_api(&self) -> &Self {
409        self
410    }
411
412    /// Get a sandbox API instance.
413    /// Uses REST API (api.veracode.*).
414    #[must_use]
415    pub fn sandbox_api(&self) -> SandboxApi<'_> {
416        SandboxApi::new(self)
417    }
418
419    /// Get an identity API instance.
420    /// Uses REST API (api.veracode.*).
421    #[must_use]
422    pub fn identity_api(&self) -> IdentityApi<'_> {
423        IdentityApi::new(self)
424    }
425
426    /// Get a pipeline scan API instance.
427    /// Uses REST API (api.veracode.*).
428    #[must_use]
429    pub fn pipeline_api(&self) -> PipelineApi {
430        PipelineApi::new(self.clone())
431    }
432
433    /// Get a policy API instance.
434    /// Uses REST API (api.veracode.*).
435    #[must_use]
436    pub fn policy_api(&self) -> PolicyApi<'_> {
437        PolicyApi::new(self)
438    }
439
440    /// Get a findings API instance.
441    /// Uses REST API (api.veracode.*).
442    #[must_use]
443    pub fn findings_api(&self) -> FindingsApi {
444        FindingsApi::new(self.clone())
445    }
446
447    /// Get a scan API instance.
448    /// Uses XML API (analysiscenter.veracode.*) for both sandbox and application scans.
449    #[must_use]
450    pub fn scan_api(&self) -> ScanApi {
451        // Create a specialized XML client for scan operations
452        let xml_client = Self::new_xml_client(self.config().clone())
453            .expect("XML client creation should not fail if main client was created successfully");
454        ScanApi::new(xml_client)
455    }
456
457    /// Get a build API instance.
458    /// Uses XML API (analysiscenter.veracode.*) for build management operations.
459    #[must_use]
460    pub fn build_api(&self) -> build::BuildApi {
461        // Create a specialized XML client for build operations
462        let xml_client = Self::new_xml_client(self.config().clone())
463            .expect("XML client creation should not fail if main client was created successfully");
464        build::BuildApi::new(xml_client)
465    }
466
467    /// Get a workflow helper instance.
468    /// Provides high-level operations that combine multiple API calls.
469    #[must_use]
470    pub fn workflow(&self) -> workflow::VeracodeWorkflow {
471        workflow::VeracodeWorkflow::new(self.clone())
472    }
473}
474
475impl fmt::Display for VeracodeError {
476    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
477        match self {
478            VeracodeError::Http(e) => write!(f, "HTTP error: {e}"),
479            VeracodeError::Serialization(e) => write!(f, "Serialization error: {e}"),
480            VeracodeError::Authentication(e) => write!(f, "Authentication error: {e}"),
481            VeracodeError::InvalidResponse(e) => write!(f, "Invalid response: {e}"),
482            VeracodeError::InvalidConfig(e) => write!(f, "Invalid configuration: {e}"),
483            VeracodeError::NotFound(e) => write!(f, "Item not found: {e}"),
484            VeracodeError::RetryExhausted(e) => write!(f, "Retry attempts exhausted: {e}"),
485            VeracodeError::RateLimited {
486                retry_after_seconds,
487                message,
488            } => match retry_after_seconds {
489                Some(seconds) => {
490                    write!(f, "Rate limit exceeded: {message} (retry after {seconds}s)")
491                }
492                None => write!(f, "Rate limit exceeded: {message}"),
493            },
494        }
495    }
496}
497
498impl std::error::Error for VeracodeError {}
499
500impl From<ReqwestError> for VeracodeError {
501    fn from(error: ReqwestError) -> Self {
502        VeracodeError::Http(error)
503    }
504}
505
506impl From<serde_json::Error> for VeracodeError {
507    fn from(error: serde_json::Error) -> Self {
508        VeracodeError::Serialization(error)
509    }
510}
511
512/// ARC-based credential storage for thread-safe access via memory pointers
513///
514/// This struct provides secure credential storage with the following protections:
515/// - Fields are private to prevent direct access
516/// - SecretString provides memory protection and debug redaction
517/// - ARC allows safe sharing across threads
518/// - Access is only possible through controlled expose_* methods
519#[derive(Clone)]
520pub struct VeracodeCredentials {
521    /// API ID stored in ARC for shared access - PRIVATE for security
522    api_id: Arc<SecretString>,
523    /// API key stored in ARC for shared access - PRIVATE for security  
524    api_key: Arc<SecretString>,
525}
526
527impl VeracodeCredentials {
528    /// Create new ARC-based credentials
529    #[must_use]
530    pub fn new(api_id: String, api_key: String) -> Self {
531        Self {
532            api_id: Arc::new(SecretString::new(api_id.into())),
533            api_key: Arc::new(SecretString::new(api_key.into())),
534        }
535    }
536
537    /// Get API ID via memory pointer (ARC) - USE WITH CAUTION
538    ///
539    /// # Security Warning
540    /// This returns an `Arc<SecretString>` which allows the caller to call expose_secret().
541    /// Only use this method when you need to share credentials across thread boundaries.
542    /// For authentication, prefer using expose_api_id() directly.
543    #[must_use]
544    pub fn api_id_ptr(&self) -> Arc<SecretString> {
545        Arc::clone(&self.api_id)
546    }
547
548    /// Get API key via memory pointer (ARC) - USE WITH CAUTION
549    ///
550    /// # Security Warning  
551    /// This returns an `Arc<SecretString>` which allows the caller to call expose_secret().
552    /// Only use this method when you need to share credentials across thread boundaries.
553    /// For authentication, prefer using expose_api_key() directly.
554    #[must_use]
555    pub fn api_key_ptr(&self) -> Arc<SecretString> {
556        Arc::clone(&self.api_key)
557    }
558
559    /// Access API ID securely (temporary access for authentication)
560    ///
561    /// This is the preferred method for accessing the API ID during authentication.
562    /// The returned reference is only valid for the lifetime of this call.
563    #[must_use]
564    pub fn expose_api_id(&self) -> &str {
565        self.api_id.expose_secret()
566    }
567
568    /// Access API key securely (temporary access for authentication)
569    ///
570    /// This is the preferred method for accessing the API key during authentication.
571    /// The returned reference is only valid for the lifetime of this call.
572    #[must_use]
573    pub fn expose_api_key(&self) -> &str {
574        self.api_key.expose_secret()
575    }
576}
577
578impl std::fmt::Debug for VeracodeCredentials {
579    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
580        f.debug_struct("VeracodeCredentials")
581            .field("api_id", &"[REDACTED]")
582            .field("api_key", &"[REDACTED]")
583            .finish()
584    }
585}
586
587/// Configuration for the Veracode API client.
588///
589/// This struct contains all the necessary configuration for connecting to
590/// the Veracode APIs, including authentication credentials and regional settings.
591/// It automatically manages both REST API (api.veracode.*) and XML API
592/// (analysiscenter.veracode.*) endpoints based on the selected region.
593#[derive(Clone)]
594pub struct VeracodeConfig {
595    /// ARC-based credentials for thread-safe access
596    pub credentials: VeracodeCredentials,
597    /// Base URL for the current client instance
598    pub base_url: String,
599    /// REST API base URL (api.veracode.*)
600    pub rest_base_url: String,
601    /// XML API base URL (analysiscenter.veracode.*)
602    pub xml_base_url: String,
603    /// Veracode region for your account
604    pub region: VeracodeRegion,
605    /// Whether to validate TLS certificates (default: true)
606    pub validate_certificates: bool,
607    /// Retry configuration for HTTP requests
608    pub retry_config: RetryConfig,
609    /// HTTP connection timeout in seconds (default: 30)
610    pub connect_timeout: u64,
611    /// HTTP request timeout in seconds (default: 300)
612    pub request_timeout: u64,
613}
614
615/// Custom Debug implementation for VeracodeConfig that redacts sensitive information
616impl std::fmt::Debug for VeracodeConfig {
617    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
618        f.debug_struct("VeracodeConfig")
619            .field("credentials", &self.credentials)
620            .field("base_url", &self.base_url)
621            .field("rest_base_url", &self.rest_base_url)
622            .field("xml_base_url", &self.xml_base_url)
623            .field("region", &self.region)
624            .field("validate_certificates", &self.validate_certificates)
625            .field("retry_config", &self.retry_config)
626            .field("connect_timeout", &self.connect_timeout)
627            .field("request_timeout", &self.request_timeout)
628            .finish()
629    }
630}
631
632// URL constants for different regions
633const COMMERCIAL_REST_URL: &str = "https://api.veracode.com";
634const COMMERCIAL_XML_URL: &str = "https://analysiscenter.veracode.com";
635const EUROPEAN_REST_URL: &str = "https://api.veracode.eu";
636const EUROPEAN_XML_URL: &str = "https://analysiscenter.veracode.eu";
637const FEDERAL_REST_URL: &str = "https://api.veracode.us";
638const FEDERAL_XML_URL: &str = "https://analysiscenter.veracode.us";
639
640/// Veracode regions for API access.
641///
642/// Different regions use different API endpoints. Choose the region
643/// that matches your Veracode account configuration.
644#[derive(Debug, Clone, Copy, PartialEq)]
645pub enum VeracodeRegion {
646    /// Commercial region (default) - api.veracode.com
647    Commercial,
648    /// European region - api.veracode.eu
649    European,
650    /// US Federal region - api.veracode.us
651    Federal,
652}
653
654impl VeracodeConfig {
655    /// Create a new configuration for the Commercial region.
656    ///
657    /// This creates a configuration that supports both REST API (api.veracode.*)
658    /// and XML API (analysiscenter.veracode.*) endpoints. The base_url defaults
659    /// to REST API for most modules, while sandbox scan operations automatically
660    /// use the XML API endpoint.
661    ///
662    /// # Arguments
663    ///
664    /// * `api_id` - Your Veracode API ID
665    /// * `api_key` - Your Veracode API key
666    ///
667    /// # Returns
668    ///
669    /// A new `VeracodeConfig` instance configured for the Commercial region.
670    #[must_use]
671    pub fn new(api_id: &str, api_key: &str) -> Self {
672        let credentials = VeracodeCredentials::new(api_id.to_string(), api_key.to_string());
673        Self {
674            credentials,
675            base_url: COMMERCIAL_REST_URL.to_string(),
676            rest_base_url: COMMERCIAL_REST_URL.to_string(),
677            xml_base_url: COMMERCIAL_XML_URL.to_string(),
678            region: VeracodeRegion::Commercial,
679            validate_certificates: true, // Default to secure
680            retry_config: RetryConfig::default(),
681            connect_timeout: 30,  // Default: 30 seconds
682            request_timeout: 300, // Default: 5 minutes
683        }
684    }
685
686    /// Create a new configuration with ARC-based credentials
687    ///
688    /// This method allows direct use of ARC pointers for credential sharing
689    /// across threads and components.
690    #[must_use]
691    pub fn from_arc_credentials(api_id: Arc<SecretString>, api_key: Arc<SecretString>) -> Self {
692        let credentials = VeracodeCredentials { api_id, api_key };
693
694        Self {
695            credentials,
696            base_url: COMMERCIAL_REST_URL.to_string(),
697            rest_base_url: COMMERCIAL_REST_URL.to_string(),
698            xml_base_url: COMMERCIAL_XML_URL.to_string(),
699            region: VeracodeRegion::Commercial,
700            validate_certificates: true,
701            retry_config: RetryConfig::default(),
702            connect_timeout: 30,
703            request_timeout: 300,
704        }
705    }
706
707    /// Set the region for this configuration.
708    ///
709    /// This will automatically update both REST and XML API URLs to match the region.
710    /// All modules will use the appropriate regional endpoint for their API type.
711    ///
712    /// # Arguments
713    ///
714    /// * `region` - The Veracode region to use
715    ///
716    /// # Returns
717    ///
718    /// The updated configuration instance (for method chaining).
719    #[must_use]
720    pub fn with_region(mut self, region: VeracodeRegion) -> Self {
721        let (rest_url, xml_url) = match region {
722            VeracodeRegion::Commercial => (COMMERCIAL_REST_URL, COMMERCIAL_XML_URL),
723            VeracodeRegion::European => (EUROPEAN_REST_URL, EUROPEAN_XML_URL),
724            VeracodeRegion::Federal => (FEDERAL_REST_URL, FEDERAL_XML_URL),
725        };
726
727        self.region = region;
728        self.rest_base_url = rest_url.to_string();
729        self.xml_base_url = xml_url.to_string();
730        self.base_url = self.rest_base_url.clone(); // Default to REST
731        self
732    }
733
734    /// Disable certificate validation for development environments.
735    ///
736    /// WARNING: This should only be used in development environments with
737    /// self-signed certificates. Never use this in production.
738    ///
739    /// # Returns
740    ///
741    /// The updated configuration instance (for method chaining).
742    #[must_use]
743    pub fn with_certificate_validation_disabled(mut self) -> Self {
744        self.validate_certificates = false;
745        self
746    }
747
748    /// Set a custom retry configuration.
749    ///
750    /// This allows you to customize the retry behavior for HTTP requests,
751    /// including the number of attempts, delays, and backoff strategy.
752    ///
753    /// # Arguments
754    ///
755    /// * `retry_config` - The retry configuration to use
756    ///
757    /// # Returns
758    ///
759    /// The updated configuration instance (for method chaining).
760    #[must_use]
761    pub fn with_retry_config(mut self, retry_config: RetryConfig) -> Self {
762        self.retry_config = retry_config;
763        self
764    }
765
766    /// Disable retries for HTTP requests.
767    ///
768    /// This will set the retry configuration to perform no retries on failed requests.
769    /// Useful for scenarios where you want to handle errors immediately without any delays.
770    ///
771    /// # Returns
772    ///
773    /// The updated configuration instance (for method chaining).
774    #[must_use]
775    pub fn with_retries_disabled(mut self) -> Self {
776        self.retry_config = RetryConfig::new().with_max_attempts(0);
777        self
778    }
779
780    /// Set the HTTP connection timeout.
781    ///
782    /// This controls how long to wait for a connection to be established.
783    ///
784    /// # Arguments
785    ///
786    /// * `timeout_seconds` - Connection timeout in seconds
787    ///
788    /// # Returns
789    ///
790    /// The updated configuration instance (for method chaining).
791    #[must_use]
792    pub fn with_connect_timeout(mut self, timeout_seconds: u64) -> Self {
793        self.connect_timeout = timeout_seconds;
794        self
795    }
796
797    /// Set the HTTP request timeout.
798    ///
799    /// This controls the total time allowed for a request to complete,
800    /// including connection establishment, request transmission, and response reception.
801    ///
802    /// # Arguments
803    ///
804    /// * `timeout_seconds` - Request timeout in seconds
805    ///
806    /// # Returns
807    ///
808    /// The updated configuration instance (for method chaining).
809    #[must_use]
810    pub fn with_request_timeout(mut self, timeout_seconds: u64) -> Self {
811        self.request_timeout = timeout_seconds;
812        self
813    }
814
815    /// Set both connection and request timeouts.
816    ///
817    /// This is a convenience method to set both timeout values at once.
818    ///
819    /// # Arguments
820    ///
821    /// * `connect_timeout_seconds` - Connection timeout in seconds
822    /// * `request_timeout_seconds` - Request timeout in seconds
823    ///
824    /// # Returns
825    ///
826    /// The updated configuration instance (for method chaining).
827    #[must_use]
828    pub fn with_timeouts(
829        mut self,
830        connect_timeout_seconds: u64,
831        request_timeout_seconds: u64,
832    ) -> Self {
833        self.connect_timeout = connect_timeout_seconds;
834        self.request_timeout = request_timeout_seconds;
835        self
836    }
837
838    /// Get ARC pointer to API ID for sharing across threads
839    #[must_use]
840    pub fn api_id_arc(&self) -> Arc<SecretString> {
841        self.credentials.api_id_ptr()
842    }
843
844    /// Get ARC pointer to API key for sharing across threads
845    #[must_use]
846    pub fn api_key_arc(&self) -> Arc<SecretString> {
847        self.credentials.api_key_ptr()
848    }
849}
850
851#[cfg(test)]
852mod tests {
853    use super::*;
854
855    #[test]
856    fn test_config_creation() {
857        let config = VeracodeConfig::new("test_api_id", "test_api_key");
858
859        assert_eq!(config.credentials.expose_api_id(), "test_api_id");
860        assert_eq!(config.credentials.expose_api_key(), "test_api_key");
861        assert_eq!(config.base_url, "https://api.veracode.com");
862        assert_eq!(config.rest_base_url, "https://api.veracode.com");
863        assert_eq!(config.xml_base_url, "https://analysiscenter.veracode.com");
864        assert_eq!(config.region, VeracodeRegion::Commercial);
865        assert!(config.validate_certificates); // Default is secure
866        assert_eq!(config.retry_config.max_attempts, 5); // Default retry config
867    }
868
869    #[test]
870    fn test_european_region_config() {
871        let config = VeracodeConfig::new("test_api_id", "test_api_key")
872            .with_region(VeracodeRegion::European);
873
874        assert_eq!(config.base_url, "https://api.veracode.eu");
875        assert_eq!(config.rest_base_url, "https://api.veracode.eu");
876        assert_eq!(config.xml_base_url, "https://analysiscenter.veracode.eu");
877        assert_eq!(config.region, VeracodeRegion::European);
878    }
879
880    #[test]
881    fn test_federal_region_config() {
882        let config =
883            VeracodeConfig::new("test_api_id", "test_api_key").with_region(VeracodeRegion::Federal);
884
885        assert_eq!(config.base_url, "https://api.veracode.us");
886        assert_eq!(config.rest_base_url, "https://api.veracode.us");
887        assert_eq!(config.xml_base_url, "https://analysiscenter.veracode.us");
888        assert_eq!(config.region, VeracodeRegion::Federal);
889    }
890
891    #[test]
892    fn test_certificate_validation_disabled() {
893        let config = VeracodeConfig::new("test_api_id", "test_api_key")
894            .with_certificate_validation_disabled();
895
896        assert!(!config.validate_certificates);
897    }
898
899    #[test]
900    fn test_veracode_credentials_debug_redaction() {
901        let credentials = VeracodeCredentials::new(
902            "test_api_id_123".to_string(),
903            "test_api_key_456".to_string(),
904        );
905        let debug_output = format!("{credentials:?}");
906
907        // Should show structure but redact actual values
908        assert!(debug_output.contains("VeracodeCredentials"));
909        assert!(debug_output.contains("[REDACTED]"));
910
911        // Should not contain actual credential values
912        assert!(!debug_output.contains("test_api_id_123"));
913        assert!(!debug_output.contains("test_api_key_456"));
914    }
915
916    #[test]
917    fn test_veracode_config_debug_redaction() {
918        let config = VeracodeConfig::new("test_api_id_123", "test_api_key_456");
919        let debug_output = format!("{config:?}");
920
921        // Should show structure but redact actual values
922        assert!(debug_output.contains("VeracodeConfig"));
923        assert!(debug_output.contains("credentials"));
924        assert!(debug_output.contains("[REDACTED]"));
925
926        // Should not contain actual credential values
927        assert!(!debug_output.contains("test_api_id_123"));
928        assert!(!debug_output.contains("test_api_key_456"));
929    }
930
931    #[test]
932    fn test_veracode_credentials_access_methods() {
933        let credentials = VeracodeCredentials::new(
934            "test_api_id_123".to_string(),
935            "test_api_key_456".to_string(),
936        );
937
938        // Test expose methods
939        assert_eq!(credentials.expose_api_id(), "test_api_id_123");
940        assert_eq!(credentials.expose_api_key(), "test_api_key_456");
941    }
942
943    #[test]
944    fn test_veracode_credentials_arc_pointers() {
945        let credentials = VeracodeCredentials::new(
946            "test_api_id_123".to_string(),
947            "test_api_key_456".to_string(),
948        );
949
950        // Test ARC pointer methods
951        let api_id_arc = credentials.api_id_ptr();
952        let api_key_arc = credentials.api_key_ptr();
953
954        // Should be able to access through ARC
955        assert_eq!(api_id_arc.expose_secret(), "test_api_id_123");
956        assert_eq!(api_key_arc.expose_secret(), "test_api_key_456");
957    }
958
959    #[test]
960    fn test_veracode_credentials_clone() {
961        let credentials = VeracodeCredentials::new(
962            "test_api_id_123".to_string(),
963            "test_api_key_456".to_string(),
964        );
965        let cloned_credentials = credentials.clone();
966
967        // Both should have the same values
968        assert_eq!(
969            credentials.expose_api_id(),
970            cloned_credentials.expose_api_id()
971        );
972        assert_eq!(
973            credentials.expose_api_key(),
974            cloned_credentials.expose_api_key()
975        );
976    }
977
978    #[test]
979    fn test_config_with_arc_credentials() {
980        use secrecy::SecretString;
981        use std::sync::Arc;
982
983        let api_id_arc = Arc::new(SecretString::new("test_api_id".into()));
984        let api_key_arc = Arc::new(SecretString::new("test_api_key".into()));
985
986        let config = VeracodeConfig::from_arc_credentials(api_id_arc, api_key_arc);
987
988        assert_eq!(config.credentials.expose_api_id(), "test_api_id");
989        assert_eq!(config.credentials.expose_api_key(), "test_api_key");
990        assert_eq!(config.region, VeracodeRegion::Commercial);
991    }
992
993    #[test]
994    fn test_error_display() {
995        let error = VeracodeError::Authentication("Invalid API key".to_string());
996        assert_eq!(format!("{error}"), "Authentication error: Invalid API key");
997    }
998
999    #[test]
1000    fn test_error_from_reqwest() {
1001        // Test that we can convert from reqwest errors
1002        // Note: We can't easily create a reqwest::Error for testing,
1003        // so we'll just verify the From trait implementation exists
1004        // by checking that it compiles
1005        fn _test_conversion(error: reqwest::Error) -> VeracodeError {
1006            VeracodeError::from(error)
1007        }
1008
1009        // If this compiles, the From trait is implemented correctly
1010        // Test passes if no panic occurs
1011    }
1012
1013    #[test]
1014    fn test_retry_config_default() {
1015        let config = RetryConfig::default();
1016        assert_eq!(config.max_attempts, 5);
1017        assert_eq!(config.initial_delay_ms, 1000);
1018        assert_eq!(config.max_delay_ms, 30000);
1019        assert_eq!(config.backoff_multiplier, 2.0);
1020        assert_eq!(config.max_total_delay_ms, 300000);
1021        assert!(config.jitter_enabled); // Jitter should be enabled by default
1022    }
1023
1024    #[test]
1025    fn test_retry_config_builder() {
1026        let config = RetryConfig::new()
1027            .with_max_attempts(5)
1028            .with_initial_delay(500)
1029            .with_max_delay(60000)
1030            .with_backoff_multiplier(1.5)
1031            .with_max_total_delay(600000);
1032
1033        assert_eq!(config.max_attempts, 5);
1034        assert_eq!(config.initial_delay_ms, 500);
1035        assert_eq!(config.max_delay_ms, 60000);
1036        assert_eq!(config.backoff_multiplier, 1.5);
1037        assert_eq!(config.max_total_delay_ms, 600000);
1038    }
1039
1040    #[test]
1041    fn test_retry_config_calculate_delay() {
1042        let config = RetryConfig::new()
1043            .with_initial_delay(1000)
1044            .with_backoff_multiplier(2.0)
1045            .with_max_delay(10000)
1046            .with_jitter_disabled(); // Disable jitter for predictable testing
1047
1048        // Test exponential backoff calculation
1049        assert_eq!(config.calculate_delay(0).as_millis(), 0); // No delay for attempt 0
1050        assert_eq!(config.calculate_delay(1).as_millis(), 1000); // First retry: 1000ms
1051        assert_eq!(config.calculate_delay(2).as_millis(), 2000); // Second retry: 2000ms
1052        assert_eq!(config.calculate_delay(3).as_millis(), 4000); // Third retry: 4000ms
1053        assert_eq!(config.calculate_delay(4).as_millis(), 8000); // Fourth retry: 8000ms
1054        assert_eq!(config.calculate_delay(5).as_millis(), 10000); // Fifth retry: capped at max_delay
1055    }
1056
1057    #[test]
1058    fn test_retry_config_is_retryable_error() {
1059        let config = RetryConfig::new();
1060
1061        // Test retryable errors
1062        assert!(
1063            config.is_retryable_error(&VeracodeError::InvalidResponse("temp error".to_string()))
1064        );
1065
1066        // Test non-retryable errors
1067        assert!(!config.is_retryable_error(&VeracodeError::Authentication("bad auth".to_string())));
1068        assert!(!config.is_retryable_error(&VeracodeError::Serialization(
1069            serde_json::from_str::<i32>("invalid").unwrap_err()
1070        )));
1071        assert!(
1072            !config.is_retryable_error(&VeracodeError::InvalidConfig("bad config".to_string()))
1073        );
1074        assert!(!config.is_retryable_error(&VeracodeError::NotFound("not found".to_string())));
1075        assert!(
1076            !config.is_retryable_error(&VeracodeError::RetryExhausted("exhausted".to_string()))
1077        );
1078    }
1079
1080    #[test]
1081    fn test_veracode_config_with_retry_config() {
1082        let retry_config = RetryConfig::new().with_max_attempts(5);
1083        let config =
1084            VeracodeConfig::new("test_api_id", "test_api_key").with_retry_config(retry_config);
1085
1086        assert_eq!(config.retry_config.max_attempts, 5);
1087    }
1088
1089    #[test]
1090    fn test_veracode_config_with_retries_disabled() {
1091        let config = VeracodeConfig::new("test_api_id", "test_api_key").with_retries_disabled();
1092
1093        assert_eq!(config.retry_config.max_attempts, 0);
1094    }
1095
1096    #[test]
1097    fn test_timeout_configuration() {
1098        let config = VeracodeConfig::new("test_api_id", "test_api_key");
1099
1100        // Test default values
1101        assert_eq!(config.connect_timeout, 30);
1102        assert_eq!(config.request_timeout, 300);
1103    }
1104
1105    #[test]
1106    fn test_with_connect_timeout() {
1107        let config = VeracodeConfig::new("test_api_id", "test_api_key").with_connect_timeout(60);
1108
1109        assert_eq!(config.connect_timeout, 60);
1110        assert_eq!(config.request_timeout, 300); // Should remain default
1111    }
1112
1113    #[test]
1114    fn test_with_request_timeout() {
1115        let config = VeracodeConfig::new("test_api_id", "test_api_key").with_request_timeout(600);
1116
1117        assert_eq!(config.connect_timeout, 30); // Should remain default
1118        assert_eq!(config.request_timeout, 600);
1119    }
1120
1121    #[test]
1122    fn test_with_timeouts() {
1123        let config = VeracodeConfig::new("test_api_id", "test_api_key").with_timeouts(120, 1800);
1124
1125        assert_eq!(config.connect_timeout, 120);
1126        assert_eq!(config.request_timeout, 1800);
1127    }
1128
1129    #[test]
1130    fn test_timeout_configuration_chaining() {
1131        let config = VeracodeConfig::new("test_api_id", "test_api_key")
1132            .with_region(VeracodeRegion::European)
1133            .with_connect_timeout(45)
1134            .with_request_timeout(900)
1135            .with_retries_disabled();
1136
1137        assert_eq!(config.region, VeracodeRegion::European);
1138        assert_eq!(config.connect_timeout, 45);
1139        assert_eq!(config.request_timeout, 900);
1140        assert_eq!(config.retry_config.max_attempts, 0);
1141    }
1142
1143    #[test]
1144    fn test_retry_exhausted_error_display() {
1145        let error = VeracodeError::RetryExhausted("Failed after 3 attempts".to_string());
1146        assert_eq!(
1147            format!("{error}"),
1148            "Retry attempts exhausted: Failed after 3 attempts"
1149        );
1150    }
1151
1152    #[test]
1153    fn test_rate_limited_error_display_with_retry_after() {
1154        let error = VeracodeError::RateLimited {
1155            retry_after_seconds: Some(60),
1156            message: "Too Many Requests".to_string(),
1157        };
1158        assert_eq!(
1159            format!("{error}"),
1160            "Rate limit exceeded: Too Many Requests (retry after 60s)"
1161        );
1162    }
1163
1164    #[test]
1165    fn test_rate_limited_error_display_without_retry_after() {
1166        let error = VeracodeError::RateLimited {
1167            retry_after_seconds: None,
1168            message: "Too Many Requests".to_string(),
1169        };
1170        assert_eq!(format!("{error}"), "Rate limit exceeded: Too Many Requests");
1171    }
1172
1173    #[test]
1174    fn test_rate_limited_error_is_retryable() {
1175        let config = RetryConfig::new();
1176        let error = VeracodeError::RateLimited {
1177            retry_after_seconds: Some(60),
1178            message: "Rate limit exceeded".to_string(),
1179        };
1180        assert!(config.is_retryable_error(&error));
1181    }
1182
1183    #[test]
1184    fn test_calculate_rate_limit_delay_with_retry_after() {
1185        let config = RetryConfig::new();
1186        let delay = config.calculate_rate_limit_delay(Some(30));
1187        assert_eq!(delay.as_secs(), 30);
1188    }
1189
1190    #[test]
1191    fn test_calculate_rate_limit_delay_without_retry_after() {
1192        let config = RetryConfig::new();
1193        let delay = config.calculate_rate_limit_delay(None);
1194
1195        // Should be somewhere between buffer (5s) and 60 + buffer (65s)
1196        // depending on current second within the minute
1197        assert!(delay.as_secs() >= 5);
1198        assert!(delay.as_secs() <= 65);
1199    }
1200
1201    #[test]
1202    fn test_rate_limit_config_defaults() {
1203        let config = RetryConfig::default();
1204        assert_eq!(config.rate_limit_buffer_seconds, 5);
1205        assert_eq!(config.rate_limit_max_attempts, 1);
1206    }
1207
1208    #[test]
1209    fn test_rate_limit_config_builders() {
1210        let config = RetryConfig::new()
1211            .with_rate_limit_buffer(10)
1212            .with_rate_limit_max_attempts(2);
1213
1214        assert_eq!(config.rate_limit_buffer_seconds, 10);
1215        assert_eq!(config.rate_limit_max_attempts, 2);
1216    }
1217
1218    #[test]
1219    fn test_rate_limit_delay_uses_buffer() {
1220        let config = RetryConfig::new().with_rate_limit_buffer(15);
1221        let delay = config.calculate_rate_limit_delay(None);
1222
1223        // The delay should include our custom 15s buffer
1224        assert!(delay.as_secs() >= 15);
1225        assert!(delay.as_secs() <= 75); // 60 + 15
1226    }
1227
1228    #[test]
1229    fn test_jitter_disabled() {
1230        let config = RetryConfig::new().with_jitter_disabled();
1231        assert!(!config.jitter_enabled);
1232
1233        // With jitter disabled, delays should be consistent
1234        let delay1 = config.calculate_delay(2);
1235        let delay2 = config.calculate_delay(2);
1236        assert_eq!(delay1, delay2);
1237    }
1238
1239    #[test]
1240    fn test_jitter_enabled() {
1241        let config = RetryConfig::new(); // Jitter enabled by default
1242        assert!(config.jitter_enabled);
1243
1244        // With jitter enabled, delays may vary (though they might occasionally be the same)
1245        let base_delay = config.initial_delay_ms;
1246        let delay = config.calculate_delay(1);
1247
1248        // The delay should be within the expected range (ยฑ25% jitter)
1249        let min_expected = (base_delay as f64 * 0.75) as u64;
1250        let max_expected = (base_delay as f64 * 1.25) as u64;
1251
1252        assert!(delay.as_millis() >= min_expected as u128);
1253        assert!(delay.as_millis() <= max_expected as u128);
1254    }
1255}