Skip to main content

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#![cfg_attr(test, allow(clippy::expect_used))]
7//!
8//! This library provides a safe and ergonomic interface to the Veracode platform,
9//! handling HMAC authentication, request/response serialization, and error handling.
10//!
11//! ## Features
12//!
13//! - ๐Ÿ” **HMAC Authentication** - Built-in support for Veracode API credentials
14//! - ๐ŸŒ **Multi-Regional Support** - Automatic endpoint routing for Commercial, European, and Federal regions
15//! - ๐Ÿ”„ **Smart API Routing** - Automatically uses REST or XML APIs based on the operation
16//! - ๐Ÿ“ฑ **Applications API** - Manage applications, builds, and scans (REST)
17//! - ๐Ÿ‘ค **Identity API** - User and team management (REST)
18//! - ๐Ÿ” **Pipeline Scan API** - Automated security scanning in CI/CD pipelines (REST)
19//! - ๐Ÿงช **Sandbox API** - Development sandbox management (REST)
20//! - ๐Ÿ“ค **Sandbox Scan API** - File upload and scan operations (XML)
21//! - ๐Ÿš€ **Async/Await** - Built on tokio for high-performance async operations
22//! - โšก **Type-Safe** - Full Rust type safety with serde serialization
23//! - ๐Ÿ“Š **Rich Data Types** - Comprehensive data structures for all API responses
24//!
25//! ## Quick Start
26//!
27//! ```no_run
28//! use veracode_platform::{VeracodeConfig, VeracodeClient, VeracodeRegion};
29//!
30//! #[tokio::main]
31//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
32//!     // Create configuration - automatically supports both API types
33//!     let config = VeracodeConfig::new(
34//!         "your_api_id",
35//!         "your_api_key",
36//!     ).with_region(VeracodeRegion::Commercial); // Optional: defaults to Commercial
37//!
38//!     let client = VeracodeClient::new(config)?;
39//!     
40//!     // REST API modules (use api.veracode.*)
41//!     let apps = client.get_all_applications().await?;
42//!     let pipeline = client.pipeline_api();
43//!     let identity = client.identity_api();
44//!     let sandbox = client.sandbox_api();  // REST API for sandbox management
45//!     let policy = client.policy_api();
46//!     
47//!     // XML API modules (automatically use analysiscenter.veracode.*)
48//!     let scan = client.scan_api(); // XML API for scanning
49//!     
50//!     Ok(())
51//! }
52//! ```
53//!
54//! ## Regional Support
55//!
56//! The library automatically handles regional endpoints for both API types:
57//!
58//! ```no_run
59//! use veracode_platform::{VeracodeConfig, VeracodeRegion};
60//!
61//! // European region
62//! let config = VeracodeConfig::new("api_id", "api_key")
63//!     .with_region(VeracodeRegion::European);
64//! // REST APIs will use: api.veracode.eu
65//! // XML APIs will use: analysiscenter.veracode.eu
66//!
67//! // US Federal region  
68//! let config = VeracodeConfig::new("api_id", "api_key")
69//!     .with_region(VeracodeRegion::Federal);
70//! // REST APIs will use: api.veracode.us
71//! // XML APIs will use: analysiscenter.veracode.us
72//! ```
73//!
74//! ## API Types
75//!
76//! Different Veracode modules use different API endpoints:
77//!
78//! - **REST API (api.veracode.*)**: Applications, Identity, Pipeline, Policy, Sandbox management
79//! - **XML API (analysiscenter.veracode.*)**: Sandbox scanning operations
80//!
81//! The client automatically routes each module to the correct API type based on the operation.
82//!
83//! ## Sandbox Operations
84//!
85//! Note that sandbox functionality is split across two modules:
86//!
87//! - **`sandbox_api()`** - Sandbox management (create, delete, list sandboxes) via REST API
88//! - **`scan_api()`** - File upload and scan operations via XML API
89//!
90//! This separation reflects the underlying Veracode API architecture where sandbox management
91//! uses the newer REST endpoints while scan operations use the legacy XML endpoints.
92
93pub mod app;
94pub mod build;
95pub mod client;
96pub mod findings;
97pub mod identity;
98pub mod json_validator;
99pub mod pipeline;
100pub mod policy;
101pub mod reporting;
102pub mod sandbox;
103pub mod scan;
104pub mod validation;
105pub mod workflow;
106
107use reqwest::Error as ReqwestError;
108use secrecy::{ExposeSecret, SecretString};
109use std::fmt;
110use std::sync::Arc;
111use std::time::Duration;
112
113// Re-export common types for convenience
114pub use app::{
115    Application, ApplicationQuery, ApplicationsResponse, CreateApplicationRequest,
116    UpdateApplicationRequest,
117};
118pub use build::{
119    Build, BuildApi, BuildError, BuildList, CreateBuildRequest, DeleteBuildRequest,
120    DeleteBuildResult, GetBuildInfoRequest, GetBuildListRequest, UpdateBuildRequest,
121};
122pub use client::VeracodeClient;
123pub use findings::{
124    CweInfo, FindingCategory, FindingDetails, FindingStatus, FindingsApi, FindingsError,
125    FindingsQuery, FindingsResponse, RestFinding,
126};
127pub use identity::{
128    ApiCredential, BusinessUnit, CreateApiCredentialRequest, CreateTeamRequest, CreateUserRequest,
129    IdentityApi, IdentityError, Role, Team, UpdateTeamRequest, UpdateUserRequest, User, UserQuery,
130    UserType,
131};
132pub use json_validator::{MAX_JSON_DEPTH, validate_json_depth};
133pub use pipeline::{
134    CreateScanRequest, DevStage, Finding, FindingsSummary, PipelineApi, PipelineError, Scan,
135    ScanConfig, ScanResults, ScanStage, ScanStatus, SecurityStandards, Severity,
136};
137pub use policy::{
138    ApiSource, PolicyApi, PolicyComplianceResult, PolicyComplianceStatus, PolicyError, PolicyRule,
139    PolicyScanRequest, PolicyScanResult, PolicyThresholds, ScanType, SecurityPolicy, SummaryReport,
140};
141pub use reporting::{AuditReportRequest, GenerateReportResponse, ReportingApi, ReportingError};
142pub use sandbox::{
143    ApiError, ApiErrorResponse, CreateSandboxRequest, Sandbox, SandboxApi, SandboxError,
144    SandboxListParams, SandboxScan, UpdateSandboxRequest,
145};
146pub use scan::{
147    BeginPreScanRequest, BeginScanRequest, PreScanMessage, PreScanResults, ScanApi, ScanError,
148    ScanInfo, ScanModule, UploadFileRequest, UploadLargeFileRequest, UploadProgress,
149    UploadProgressCallback, UploadedFile,
150};
151pub use validation::{
152    AppGuid, AppName, DEFAULT_PAGE_SIZE, Description, MAX_APP_NAME_LEN, MAX_DESCRIPTION_LEN,
153    MAX_GUID_LEN, MAX_PAGE_NUMBER, MAX_PAGE_SIZE, ValidationError, validate_url_segment,
154};
155pub use workflow::{VeracodeWorkflow, WorkflowConfig, WorkflowError, WorkflowResultData};
156/// Retry configuration for HTTP requests
157#[derive(Debug, Clone)]
158pub struct RetryConfig {
159    /// Maximum number of retry attempts (default: 5)
160    pub max_attempts: u32,
161    /// Initial delay between retries in milliseconds (default: 1000ms)
162    pub initial_delay_ms: u64,
163    /// Maximum delay between retries in milliseconds (default: 30000ms)
164    pub max_delay_ms: u64,
165    /// Exponential backoff multiplier (default: 2.0)
166    pub backoff_multiplier: f64,
167    /// Maximum total time to spend on retries in milliseconds (default: 300000ms = 5 minutes)
168    pub max_total_delay_ms: u64,
169    /// Buffer time in seconds to add when waiting for rate limit window reset (default: 5s)
170    pub rate_limit_buffer_seconds: u64,
171    /// Maximum number of retry attempts specifically for rate limit errors (default: 1)
172    pub rate_limit_max_attempts: u32,
173    /// Whether to enable jitter in retry delays (default: true)
174    pub jitter_enabled: bool,
175}
176
177impl Default for RetryConfig {
178    fn default() -> Self {
179        Self {
180            max_attempts: 5,
181            initial_delay_ms: 1000,
182            max_delay_ms: 30000,
183            backoff_multiplier: 2.0,
184            max_total_delay_ms: 300_000,  // 5 minutes
185            rate_limit_buffer_seconds: 5, // 5 second buffer for rate limit windows
186            rate_limit_max_attempts: 1,   // Only retry once for rate limits
187            jitter_enabled: true,         // Enable jitter by default
188        }
189    }
190}
191
192impl RetryConfig {
193    /// Create a new retry configuration with conservative defaults
194    #[must_use]
195    pub fn new() -> Self {
196        Self::default()
197    }
198
199    /// Set the maximum number of retry attempts
200    #[must_use]
201    pub fn with_max_attempts(mut self, max_attempts: u32) -> Self {
202        self.max_attempts = max_attempts;
203        self
204    }
205
206    /// Set the initial delay between retries
207    #[must_use]
208    pub fn with_initial_delay(mut self, delay_ms: u64) -> Self {
209        self.initial_delay_ms = delay_ms;
210        self
211    }
212
213    /// Set the initial delay between retries (alias for compatibility)
214    #[must_use]
215    pub fn with_initial_delay_millis(mut self, delay_ms: u64) -> Self {
216        self.initial_delay_ms = delay_ms;
217        self
218    }
219
220    /// Set the maximum delay between retries
221    #[must_use]
222    pub fn with_max_delay(mut self, delay_ms: u64) -> Self {
223        self.max_delay_ms = delay_ms;
224        self
225    }
226
227    /// Set the maximum delay between retries (alias for compatibility)
228    #[must_use]
229    pub fn with_max_delay_millis(mut self, delay_ms: u64) -> Self {
230        self.max_delay_ms = delay_ms;
231        self
232    }
233
234    /// Set the exponential backoff multiplier
235    #[must_use]
236    pub fn with_backoff_multiplier(mut self, multiplier: f64) -> Self {
237        self.backoff_multiplier = multiplier;
238        self
239    }
240
241    /// Set the exponential backoff multiplier (alias for compatibility)
242    #[must_use]
243    pub fn with_exponential_backoff(mut self, multiplier: f64) -> Self {
244        self.backoff_multiplier = multiplier;
245        self
246    }
247
248    /// Set the maximum total time to spend on retries
249    #[must_use]
250    pub fn with_max_total_delay(mut self, delay_ms: u64) -> Self {
251        self.max_total_delay_ms = delay_ms;
252        self
253    }
254
255    /// Set the buffer time to add when waiting for rate limit window reset
256    #[must_use]
257    pub fn with_rate_limit_buffer(mut self, buffer_seconds: u64) -> Self {
258        self.rate_limit_buffer_seconds = buffer_seconds;
259        self
260    }
261
262    /// Set the maximum number of retry attempts for rate limit errors
263    #[must_use]
264    pub fn with_rate_limit_max_attempts(mut self, max_attempts: u32) -> Self {
265        self.rate_limit_max_attempts = max_attempts;
266        self
267    }
268
269    /// Disable jitter in retry delays
270    ///
271    /// Jitter adds randomness to retry delays to prevent thundering herd problems.
272    /// Disabling jitter makes retry timing more predictable but may cause synchronized
273    /// retries from multiple clients.
274    #[must_use]
275    pub fn with_jitter_disabled(mut self) -> Self {
276        self.jitter_enabled = false;
277        self
278    }
279
280    /// Calculate the delay for a given attempt number using exponential backoff
281    #[must_use]
282    pub fn calculate_delay(&self, attempt: u32) -> Duration {
283        if attempt == 0 {
284            return Duration::from_millis(0);
285        }
286
287        #[allow(
288            clippy::cast_possible_truncation,
289            clippy::cast_sign_loss,
290            clippy::cast_precision_loss,
291            clippy::cast_possible_wrap
292        )]
293        let delay_ms = (self.initial_delay_ms as f64
294            * self
295                .backoff_multiplier
296                .powi(attempt.saturating_sub(1) as i32))
297        .round() as u64;
298
299        let mut capped_delay = delay_ms.min(self.max_delay_ms);
300
301        // Apply jitter if enabled (ยฑ25% randomization)
302        if self.jitter_enabled {
303            use rand::RngExt;
304            #[allow(
305                clippy::cast_possible_truncation,
306                clippy::cast_sign_loss,
307                clippy::cast_precision_loss
308            )]
309            let jitter_range = (capped_delay as f64 * 0.25).round() as u64;
310            let min_delay = capped_delay.saturating_sub(jitter_range);
311            let max_delay = capped_delay.saturating_add(jitter_range);
312            capped_delay = rand::rng().random_range(min_delay..=max_delay);
313        }
314
315        Duration::from_millis(capped_delay)
316    }
317
318    /// Calculate the delay for rate limit (429) errors
319    ///
320    /// For Veracode's 500 requests/minute rate limiting, this calculates the optimal
321    /// wait time based on the current time within the minute window or uses the
322    /// server's Retry-After header if provided.
323    #[must_use]
324    pub fn calculate_rate_limit_delay(&self, retry_after_seconds: Option<u64>) -> Duration {
325        if let Some(seconds) = retry_after_seconds {
326            // Use the server's suggested delay
327            Duration::from_secs(seconds)
328        } else {
329            // Fall back to minute window calculation for Veracode's 500/minute limit
330            let now = std::time::SystemTime::now()
331                .duration_since(std::time::UNIX_EPOCH)
332                .unwrap_or_default();
333
334            let current_second = now.as_secs() % 60;
335
336            // Wait until the next minute window + configurable buffer to ensure window has reset
337            let seconds_until_next_minute = 60_u64.saturating_sub(current_second);
338
339            Duration::from_secs(
340                seconds_until_next_minute.saturating_add(self.rate_limit_buffer_seconds),
341            )
342        }
343    }
344
345    /// Check if an error is retryable based on its type
346    #[must_use]
347    pub fn is_retryable_error(&self, error: &VeracodeError) -> bool {
348        match error {
349            VeracodeError::Http(reqwest_error) => {
350                // Retry on network errors, timeouts, and temporary server errors
351                if reqwest_error.is_timeout()
352                    || reqwest_error.is_connect()
353                    || reqwest_error.is_request()
354                {
355                    return true;
356                }
357
358                // Check for retryable HTTP status codes
359                if let Some(status) = reqwest_error.status() {
360                    match status.as_u16() {
361                        // 429 Too Many Requests
362                        429 => true,
363                        // 502 Bad Gateway, 503 Service Unavailable, 504 Gateway Timeout
364                        502..=504 => true,
365                        // Other server errors (5xx) - retry conservatively
366                        500..=599 => true,
367                        // Don't retry client errors (4xx) except 429
368                        _ => false,
369                    }
370                } else {
371                    // Network-level errors without status codes are typically retryable
372                    true
373                }
374            }
375            // Don't retry authentication, serialization, validation, or configuration errors
376            VeracodeError::Authentication(_)
377            | VeracodeError::Serialization(_)
378            | VeracodeError::Validation(_)
379            | VeracodeError::InvalidConfig(_) => false,
380            // InvalidResponse could be temporary (like malformed JSON due to network issues)
381            VeracodeError::InvalidResponse(_) => true,
382            // HttpStatus errors - check status code for retryability
383            VeracodeError::HttpStatus { status_code, .. } => match status_code {
384                // 429 Too Many Requests (handled separately by RateLimited)
385                429 => true,
386                // 502 Bad Gateway, 503 Service Unavailable, 504 Gateway Timeout
387                502..=504 => true,
388                // Other server errors (5xx) - retry conservatively
389                500..=599 => true,
390                // Don't retry client errors (4xx) including 401/403 auth errors
391                // Auth errors should be handled by credential refresh logic
392                400..=499 => false,
393                // Other status codes - don't retry
394                _ => false,
395            },
396            // NotFound is typically not retryable
397            VeracodeError::NotFound(_) => false,
398            // New retry-specific error is not retryable (avoid infinite loops)
399            VeracodeError::RetryExhausted(_) => false,
400            // Rate limited errors are retryable with special handling
401            VeracodeError::RateLimited { .. } => true,
402        }
403    }
404}
405
406/// Custom error type for Veracode API operations.
407///
408/// This enum represents all possible errors that can occur when interacting
409/// with the Veracode Applications API.
410#[derive(Debug)]
411#[must_use = "Need to handle all error enum types."]
412pub enum VeracodeError {
413    /// HTTP request failed
414    Http(ReqwestError),
415    /// JSON serialization/deserialization failed
416    Serialization(serde_json::Error),
417    /// Authentication error (invalid credentials, signature generation failure, etc.)
418    Authentication(String),
419    /// API returned an error response
420    InvalidResponse(String),
421    /// HTTP error with status code (for better error handling)
422    HttpStatus {
423        /// HTTP status code
424        status_code: u16,
425        /// URL that was requested
426        url: String,
427        /// Error message/response body
428        message: String,
429    },
430    /// Configuration is invalid
431    InvalidConfig(String),
432    /// When an item is not found
433    NotFound(String),
434    /// When all retry attempts have been exhausted
435    RetryExhausted(String),
436    /// Rate limit exceeded (HTTP 429) - includes server's suggested retry delay
437    RateLimited {
438        /// Number of seconds to wait before retrying (from Retry-After header)
439        retry_after_seconds: Option<u64>,
440        /// The original HTTP error response
441        message: String,
442    },
443    /// Input validation failed
444    Validation(validation::ValidationError),
445}
446
447impl VeracodeClient {
448    /// Create a specialized client for XML API operations.
449    ///
450    /// This internal method creates a client configured for the XML API
451    /// (analysiscenter.veracode.*) based on the current region settings.
452    /// Used exclusively for sandbox scan operations that require the XML API.
453    fn new_xml_client(config: VeracodeConfig) -> Result<Self, VeracodeError> {
454        let mut xml_config = config;
455        xml_config.base_url = xml_config.xml_base_url.clone();
456        Self::new(xml_config)
457    }
458
459    /// Get an applications API instance.
460    /// Uses REST API (api.veracode.*).
461    #[must_use]
462    pub fn applications_api(&self) -> &Self {
463        self
464    }
465
466    /// Get a sandbox API instance.
467    /// Uses REST API (api.veracode.*).
468    #[must_use]
469    pub fn sandbox_api(&self) -> SandboxApi<'_> {
470        SandboxApi::new(self)
471    }
472
473    /// Get an identity API instance.
474    /// Uses REST API (api.veracode.*).
475    #[must_use]
476    pub fn identity_api(&self) -> IdentityApi<'_> {
477        IdentityApi::new(self)
478    }
479
480    /// Get a pipeline scan API instance.
481    /// Uses REST API (api.veracode.*).
482    #[must_use]
483    pub fn pipeline_api(&self) -> PipelineApi {
484        PipelineApi::new(self.clone())
485    }
486
487    /// Get a policy API instance.
488    /// Uses REST API (api.veracode.*).
489    #[must_use]
490    pub fn policy_api(&self) -> PolicyApi<'_> {
491        PolicyApi::new(self)
492    }
493
494    /// Get a findings API instance.
495    /// Uses REST API (api.veracode.*).
496    #[must_use]
497    pub fn findings_api(&self) -> FindingsApi {
498        FindingsApi::new(self.clone())
499    }
500
501    /// Get a reporting API instance.
502    /// Uses REST API (api.veracode.*).
503    #[must_use]
504    pub fn reporting_api(&self) -> reporting::ReportingApi {
505        reporting::ReportingApi::new(self.clone())
506    }
507
508    /// Get a scan API instance.
509    /// Uses XML API (analysiscenter.veracode.*) for both sandbox and application scans.
510    ///
511    /// # Errors
512    ///
513    /// Returns an error if the XML client cannot be created.
514    pub fn scan_api(&self) -> Result<ScanApi, VeracodeError> {
515        // Create a specialized XML client for scan operations
516        let xml_client = Self::new_xml_client(self.config().clone())?;
517        Ok(ScanApi::new(xml_client))
518    }
519
520    /// Get a build API instance.
521    /// Uses XML API (analysiscenter.veracode.*) for build management operations.
522    ///
523    /// # Errors
524    ///
525    /// Returns an error if the XML client cannot be created.
526    pub fn build_api(&self) -> Result<build::BuildApi, VeracodeError> {
527        // Create a specialized XML client for build operations
528        let xml_client = Self::new_xml_client(self.config().clone())?;
529        Ok(build::BuildApi::new(xml_client))
530    }
531
532    /// Get a workflow helper instance.
533    /// Provides high-level operations that combine multiple API calls.
534    #[must_use]
535    pub fn workflow(&self) -> workflow::VeracodeWorkflow {
536        workflow::VeracodeWorkflow::new(self.clone())
537    }
538}
539
540impl fmt::Display for VeracodeError {
541    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
542        match self {
543            VeracodeError::Http(e) => write!(f, "HTTP error: {e}"),
544            VeracodeError::Serialization(e) => write!(f, "Serialization error: {e}"),
545            VeracodeError::Authentication(e) => write!(f, "Authentication error: {e}"),
546            VeracodeError::InvalidResponse(e) => write!(f, "Invalid response: {e}"),
547            VeracodeError::HttpStatus {
548                status_code,
549                url,
550                message,
551            } => write!(f, "HTTP {status_code} error at {url}: {message}"),
552            VeracodeError::InvalidConfig(e) => write!(f, "Invalid configuration: {e}"),
553            VeracodeError::NotFound(e) => write!(f, "Item not found: {e}"),
554            VeracodeError::RetryExhausted(e) => write!(f, "Retry attempts exhausted: {e}"),
555            VeracodeError::RateLimited {
556                retry_after_seconds,
557                message,
558            } => match retry_after_seconds {
559                Some(seconds) => {
560                    write!(f, "Rate limit exceeded: {message} (retry after {seconds}s)")
561                }
562                None => write!(f, "Rate limit exceeded: {message}"),
563            },
564            VeracodeError::Validation(e) => write!(f, "Validation error: {e}"),
565        }
566    }
567}
568
569impl std::error::Error for VeracodeError {}
570
571impl From<ReqwestError> for VeracodeError {
572    fn from(error: ReqwestError) -> Self {
573        VeracodeError::Http(error)
574    }
575}
576
577impl From<serde_json::Error> for VeracodeError {
578    fn from(error: serde_json::Error) -> Self {
579        VeracodeError::Serialization(error)
580    }
581}
582
583impl From<validation::ValidationError> for VeracodeError {
584    fn from(error: validation::ValidationError) -> Self {
585        VeracodeError::Validation(error)
586    }
587}
588
589/// ARC-based credential storage for thread-safe access via memory pointers
590///
591/// This struct provides secure credential storage with the following protections:
592/// - Fields are private to prevent direct access
593/// - `SecretString` provides memory protection and debug redaction
594/// - ARC allows safe sharing across threads
595/// - Access is only possible through controlled expose_* methods
596#[derive(Clone)]
597pub struct VeracodeCredentials {
598    /// API ID stored in ARC for shared access - PRIVATE for security
599    api_id: Arc<SecretString>,
600    /// API key stored in ARC for shared access - PRIVATE for security  
601    api_key: Arc<SecretString>,
602}
603
604impl VeracodeCredentials {
605    /// Create new ARC-based credentials
606    #[must_use]
607    pub fn new(api_id: String, api_key: String) -> Self {
608        Self {
609            api_id: Arc::new(SecretString::new(api_id.into())),
610            api_key: Arc::new(SecretString::new(api_key.into())),
611        }
612    }
613
614    /// Get API ID via memory pointer (ARC) - USE WITH CAUTION
615    ///
616    /// # Security Warning
617    /// This returns an `Arc<SecretString>` which allows the caller to call `expose_secret()`.
618    /// Only use this method when you need to share credentials across thread boundaries.
619    /// For authentication, prefer using `expose_api_id()` directly.
620    #[must_use]
621    pub fn api_id_ptr(&self) -> Arc<SecretString> {
622        Arc::clone(&self.api_id)
623    }
624
625    /// Get API key via memory pointer (ARC) - USE WITH CAUTION
626    ///
627    /// # Security Warning
628    /// This returns an `Arc<SecretString>` which allows the caller to call `expose_secret()`.
629    /// Only use this method when you need to share credentials across thread boundaries.
630    /// For authentication, prefer using `expose_api_key()` directly.
631    #[must_use]
632    pub fn api_key_ptr(&self) -> Arc<SecretString> {
633        Arc::clone(&self.api_key)
634    }
635
636    /// Access API ID securely (temporary access for authentication)
637    ///
638    /// This is the preferred method for accessing the API ID during authentication.
639    /// The returned reference is only valid for the lifetime of this call.
640    #[must_use]
641    pub fn expose_api_id(&self) -> &str {
642        self.api_id.expose_secret()
643    }
644
645    /// Access API key securely (temporary access for authentication)
646    ///
647    /// This is the preferred method for accessing the API key during authentication.
648    /// The returned reference is only valid for the lifetime of this call.
649    #[must_use]
650    pub fn expose_api_key(&self) -> &str {
651        self.api_key.expose_secret()
652    }
653}
654
655impl std::fmt::Debug for VeracodeCredentials {
656    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
657        f.debug_struct("VeracodeCredentials")
658            .field("api_id", &"[REDACTED]")
659            .field("api_key", &"[REDACTED]")
660            .finish()
661    }
662}
663
664/// Configuration for the Veracode API client.
665///
666/// This struct contains all the necessary configuration for connecting to
667/// the Veracode APIs, including authentication credentials and regional settings.
668/// It automatically manages both REST API (api.veracode.*) and XML API
669/// (analysiscenter.veracode.*) endpoints based on the selected region.
670#[derive(Clone)]
671pub struct VeracodeConfig {
672    /// ARC-based credentials for thread-safe access
673    pub credentials: VeracodeCredentials,
674    /// Base URL for the current client instance
675    pub base_url: String,
676    /// REST API base URL (api.veracode.*)
677    pub rest_base_url: String,
678    /// XML API base URL (analysiscenter.veracode.*)
679    pub xml_base_url: String,
680    /// Veracode region for your account
681    pub region: VeracodeRegion,
682    /// Whether to validate TLS certificates (default: true)
683    pub validate_certificates: bool,
684    /// Retry configuration for HTTP requests
685    pub retry_config: RetryConfig,
686    /// HTTP connection timeout in seconds (default: 30)
687    pub connect_timeout: u64,
688    /// HTTP request timeout in seconds (default: 300)
689    pub request_timeout: u64,
690    /// HTTP/HTTPS proxy URL (optional)
691    pub proxy_url: Option<String>,
692    /// Proxy authentication username (optional)
693    pub proxy_username: Option<SecretString>,
694    /// Proxy authentication password (optional)
695    pub proxy_password: Option<SecretString>,
696}
697
698/// Custom Debug implementation for `VeracodeConfig` that redacts sensitive information
699impl std::fmt::Debug for VeracodeConfig {
700    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
701        // Redact proxy URL if it contains credentials
702        let proxy_url_redacted = self.proxy_url.as_ref().map(|url| {
703            if url.contains('@') {
704                // URL contains credentials, redact them
705                if let Some(at_pos) = url.rfind('@') {
706                    if let Some(proto_end) = url.find("://") {
707                        // Use safe string slicing methods that respect UTF-8 boundaries
708                        let protocol = url.get(..proto_end).unwrap_or("");
709                        let host_part = url.get(at_pos.saturating_add(1)..).unwrap_or("");
710                        format!("{}://[REDACTED]@{}", protocol, host_part)
711                    } else {
712                        "[REDACTED]".to_string()
713                    }
714                } else {
715                    "[REDACTED]".to_string()
716                }
717            } else {
718                url.clone()
719            }
720        });
721
722        f.debug_struct("VeracodeConfig")
723            .field("credentials", &self.credentials)
724            .field("base_url", &self.base_url)
725            .field("rest_base_url", &self.rest_base_url)
726            .field("xml_base_url", &self.xml_base_url)
727            .field("region", &self.region)
728            .field("validate_certificates", &self.validate_certificates)
729            .field("retry_config", &self.retry_config)
730            .field("connect_timeout", &self.connect_timeout)
731            .field("request_timeout", &self.request_timeout)
732            .field("proxy_url", &proxy_url_redacted)
733            .field(
734                "proxy_username",
735                &self.proxy_username.as_ref().map(|_| "[REDACTED]"),
736            )
737            .field(
738                "proxy_password",
739                &self.proxy_password.as_ref().map(|_| "[REDACTED]"),
740            )
741            .finish()
742    }
743}
744
745// URL constants for different regions
746const COMMERCIAL_REST_URL: &str = "https://api.veracode.com";
747const COMMERCIAL_XML_URL: &str = "https://analysiscenter.veracode.com";
748const EUROPEAN_REST_URL: &str = "https://api.veracode.eu";
749const EUROPEAN_XML_URL: &str = "https://analysiscenter.veracode.eu";
750const FEDERAL_REST_URL: &str = "https://api.veracode.us";
751const FEDERAL_XML_URL: &str = "https://analysiscenter.veracode.us";
752
753/// Veracode regions for API access.
754///
755/// Different regions use different API endpoints. Choose the region
756/// that matches your Veracode account configuration.
757#[derive(Debug, Clone, Copy, PartialEq)]
758pub enum VeracodeRegion {
759    /// Commercial region (default) - api.veracode.com
760    Commercial,
761    /// European region - api.veracode.eu
762    European,
763    /// US Federal region - api.veracode.us
764    Federal,
765}
766
767impl VeracodeConfig {
768    /// Create a new configuration for the Commercial region.
769    ///
770    /// This creates a configuration that supports both REST API (api.veracode.*)
771    /// and XML API (analysiscenter.veracode.*) endpoints. The `base_url` defaults
772    /// to REST API for most modules, while sandbox scan operations automatically
773    /// use the XML API endpoint.
774    ///
775    /// # Arguments
776    ///
777    /// * `api_id` - Your Veracode API ID
778    /// * `api_key` - Your Veracode API key
779    ///
780    /// # Returns
781    ///
782    /// A new `VeracodeConfig` instance configured for the Commercial region.
783    #[must_use]
784    pub fn new(api_id: &str, api_key: &str) -> Self {
785        let credentials = VeracodeCredentials::new(api_id.to_string(), api_key.to_string());
786        Self {
787            credentials,
788            base_url: COMMERCIAL_REST_URL.to_string(),
789            rest_base_url: COMMERCIAL_REST_URL.to_string(),
790            xml_base_url: COMMERCIAL_XML_URL.to_string(),
791            region: VeracodeRegion::Commercial,
792            validate_certificates: true, // Default to secure
793            retry_config: RetryConfig::default(),
794            connect_timeout: 30,  // Default: 30 seconds
795            request_timeout: 300, // Default: 5 minutes
796            proxy_url: None,
797            proxy_username: None,
798            proxy_password: None,
799        }
800    }
801
802    /// Create a new configuration with ARC-based credentials
803    ///
804    /// This method allows direct use of ARC pointers for credential sharing
805    /// across threads and components.
806    #[must_use]
807    pub fn from_arc_credentials(api_id: Arc<SecretString>, api_key: Arc<SecretString>) -> Self {
808        let credentials = VeracodeCredentials { api_id, api_key };
809
810        Self {
811            credentials,
812            base_url: COMMERCIAL_REST_URL.to_string(),
813            rest_base_url: COMMERCIAL_REST_URL.to_string(),
814            xml_base_url: COMMERCIAL_XML_URL.to_string(),
815            region: VeracodeRegion::Commercial,
816            validate_certificates: true,
817            retry_config: RetryConfig::default(),
818            connect_timeout: 30,
819            request_timeout: 300,
820            proxy_url: None,
821            proxy_username: None,
822            proxy_password: None,
823        }
824    }
825
826    /// Set the region for this configuration.
827    ///
828    /// This will automatically update both REST and XML API URLs to match the region.
829    /// All modules will use the appropriate regional endpoint for their API type.
830    ///
831    /// # Arguments
832    ///
833    /// * `region` - The Veracode region to use
834    ///
835    /// # Returns
836    ///
837    /// The updated configuration instance (for method chaining).
838    #[must_use]
839    pub fn with_region(mut self, region: VeracodeRegion) -> Self {
840        let (rest_url, xml_url) = match region {
841            VeracodeRegion::Commercial => (COMMERCIAL_REST_URL, COMMERCIAL_XML_URL),
842            VeracodeRegion::European => (EUROPEAN_REST_URL, EUROPEAN_XML_URL),
843            VeracodeRegion::Federal => (FEDERAL_REST_URL, FEDERAL_XML_URL),
844        };
845
846        self.region = region;
847        self.rest_base_url = rest_url.to_string();
848        self.xml_base_url = xml_url.to_string();
849        self.base_url = self.rest_base_url.clone(); // Default to REST
850        self
851    }
852
853    /// Disable certificate validation for development environments.
854    ///
855    /// WARNING: This should only be used in development environments with
856    /// self-signed certificates. Never use this in production.
857    ///
858    /// # Returns
859    ///
860    /// The updated configuration instance (for method chaining).
861    #[must_use]
862    pub fn with_certificate_validation_disabled(mut self) -> Self {
863        self.validate_certificates = false;
864        self
865    }
866
867    /// Set a custom retry configuration.
868    ///
869    /// This allows you to customize the retry behavior for HTTP requests,
870    /// including the number of attempts, delays, and backoff strategy.
871    ///
872    /// # Arguments
873    ///
874    /// * `retry_config` - The retry configuration to use
875    ///
876    /// # Returns
877    ///
878    /// The updated configuration instance (for method chaining).
879    #[must_use]
880    pub fn with_retry_config(mut self, retry_config: RetryConfig) -> Self {
881        self.retry_config = retry_config;
882        self
883    }
884
885    /// Disable retries for HTTP requests.
886    ///
887    /// This will set the retry configuration to perform no retries on failed requests.
888    /// Useful for scenarios where you want to handle errors immediately without any delays.
889    ///
890    /// # Returns
891    ///
892    /// The updated configuration instance (for method chaining).
893    #[must_use]
894    pub fn with_retries_disabled(mut self) -> Self {
895        self.retry_config = RetryConfig::new().with_max_attempts(0);
896        self
897    }
898
899    /// Set the HTTP connection timeout.
900    ///
901    /// This controls how long to wait for a connection to be established.
902    ///
903    /// # Arguments
904    ///
905    /// * `timeout_seconds` - Connection timeout in seconds
906    ///
907    /// # Returns
908    ///
909    /// The updated configuration instance (for method chaining).
910    #[must_use]
911    pub fn with_connect_timeout(mut self, timeout_seconds: u64) -> Self {
912        self.connect_timeout = timeout_seconds;
913        self
914    }
915
916    /// Set the HTTP request timeout.
917    ///
918    /// This controls the total time allowed for a request to complete,
919    /// including connection establishment, request transmission, and response reception.
920    ///
921    /// # Arguments
922    ///
923    /// * `timeout_seconds` - Request timeout in seconds
924    ///
925    /// # Returns
926    ///
927    /// The updated configuration instance (for method chaining).
928    #[must_use]
929    pub fn with_request_timeout(mut self, timeout_seconds: u64) -> Self {
930        self.request_timeout = timeout_seconds;
931        self
932    }
933
934    /// Set both connection and request timeouts.
935    ///
936    /// This is a convenience method to set both timeout values at once.
937    ///
938    /// # Arguments
939    ///
940    /// * `connect_timeout_seconds` - Connection timeout in seconds
941    /// * `request_timeout_seconds` - Request timeout in seconds
942    ///
943    /// # Returns
944    ///
945    /// The updated configuration instance (for method chaining).
946    #[must_use]
947    pub fn with_timeouts(
948        mut self,
949        connect_timeout_seconds: u64,
950        request_timeout_seconds: u64,
951    ) -> Self {
952        self.connect_timeout = connect_timeout_seconds;
953        self.request_timeout = request_timeout_seconds;
954        self
955    }
956
957    /// Get ARC pointer to API ID for sharing across threads
958    #[must_use]
959    pub fn api_id_arc(&self) -> Arc<SecretString> {
960        self.credentials.api_id_ptr()
961    }
962
963    /// Get ARC pointer to API key for sharing across threads
964    #[must_use]
965    pub fn api_key_arc(&self) -> Arc<SecretString> {
966        self.credentials.api_key_ptr()
967    }
968
969    /// Set the HTTP/HTTPS proxy URL.
970    ///
971    /// Configures an HTTP or HTTPS proxy for all requests. The proxy URL should include
972    /// the protocol (http:// or https://). Credentials can be embedded in the URL or
973    /// set separately using `with_proxy_auth()`.
974    ///
975    /// # Arguments
976    ///
977    /// * `proxy_url` - The proxy URL (e.g., "<http://proxy.example.com:8080>")
978    ///
979    /// # Returns
980    ///
981    /// The updated configuration instance (for method chaining).
982    ///
983    /// # Examples
984    ///
985    /// ```no_run
986    /// use veracode_platform::VeracodeConfig;
987    ///
988    /// // Without authentication
989    /// let config = VeracodeConfig::new("api_id", "api_key")
990    ///     .with_proxy("http://proxy.example.com:8080");
991    ///
992    /// // With embedded credentials (less secure)
993    /// let config = VeracodeConfig::new("api_id", "api_key")
994    ///     .with_proxy("http://user:pass@proxy.example.com:8080");
995    /// ```
996    #[must_use]
997    pub fn with_proxy(mut self, proxy_url: impl Into<String>) -> Self {
998        self.proxy_url = Some(proxy_url.into());
999        self
1000    }
1001
1002    /// Set proxy authentication credentials.
1003    ///
1004    /// Configures username and password for proxy authentication using HTTP Basic Auth.
1005    /// This is more secure than embedding credentials in the proxy URL as the credentials
1006    /// are stored using `SecretString` and properly redacted in debug output.
1007    ///
1008    /// # Arguments
1009    ///
1010    /// * `username` - The proxy username
1011    /// * `password` - The proxy password
1012    ///
1013    /// # Returns
1014    ///
1015    /// The updated configuration instance (for method chaining).
1016    ///
1017    /// # Examples
1018    ///
1019    /// ```no_run
1020    /// use veracode_platform::VeracodeConfig;
1021    ///
1022    /// let config = VeracodeConfig::new("api_id", "api_key")
1023    ///     .with_proxy("http://proxy.example.com:8080")
1024    ///     .with_proxy_auth("username", "password");
1025    /// ```
1026    #[must_use]
1027    pub fn with_proxy_auth(
1028        mut self,
1029        username: impl Into<String>,
1030        password: impl Into<String>,
1031    ) -> Self {
1032        self.proxy_username = Some(SecretString::new(username.into().into()));
1033        self.proxy_password = Some(SecretString::new(password.into().into()));
1034        self
1035    }
1036}
1037
1038#[cfg(test)]
1039#[allow(clippy::expect_used)]
1040mod tests {
1041    use super::*;
1042
1043    #[test]
1044    fn test_config_creation() {
1045        let config = VeracodeConfig::new("test_api_id", "test_api_key");
1046
1047        assert_eq!(config.credentials.expose_api_id(), "test_api_id");
1048        assert_eq!(config.credentials.expose_api_key(), "test_api_key");
1049        assert_eq!(config.base_url, "https://api.veracode.com");
1050        assert_eq!(config.rest_base_url, "https://api.veracode.com");
1051        assert_eq!(config.xml_base_url, "https://analysiscenter.veracode.com");
1052        assert_eq!(config.region, VeracodeRegion::Commercial);
1053        assert!(config.validate_certificates); // Default is secure
1054        assert_eq!(config.retry_config.max_attempts, 5); // Default retry config
1055    }
1056
1057    #[test]
1058    fn test_european_region_config() {
1059        let config = VeracodeConfig::new("test_api_id", "test_api_key")
1060            .with_region(VeracodeRegion::European);
1061
1062        assert_eq!(config.base_url, "https://api.veracode.eu");
1063        assert_eq!(config.rest_base_url, "https://api.veracode.eu");
1064        assert_eq!(config.xml_base_url, "https://analysiscenter.veracode.eu");
1065        assert_eq!(config.region, VeracodeRegion::European);
1066    }
1067
1068    #[test]
1069    fn test_federal_region_config() {
1070        let config =
1071            VeracodeConfig::new("test_api_id", "test_api_key").with_region(VeracodeRegion::Federal);
1072
1073        assert_eq!(config.base_url, "https://api.veracode.us");
1074        assert_eq!(config.rest_base_url, "https://api.veracode.us");
1075        assert_eq!(config.xml_base_url, "https://analysiscenter.veracode.us");
1076        assert_eq!(config.region, VeracodeRegion::Federal);
1077    }
1078
1079    #[test]
1080    fn test_certificate_validation_disabled() {
1081        let config = VeracodeConfig::new("test_api_id", "test_api_key")
1082            .with_certificate_validation_disabled();
1083
1084        assert!(!config.validate_certificates);
1085    }
1086
1087    #[test]
1088    fn test_veracode_credentials_debug_redaction() {
1089        let credentials = VeracodeCredentials::new(
1090            "test_api_id_123".to_string(),
1091            "test_api_key_456".to_string(),
1092        );
1093        let debug_output = format!("{credentials:?}");
1094
1095        // Should show structure but redact actual values
1096        assert!(debug_output.contains("VeracodeCredentials"));
1097        assert!(debug_output.contains("[REDACTED]"));
1098
1099        // Should not contain actual credential values
1100        assert!(!debug_output.contains("test_api_id_123"));
1101        assert!(!debug_output.contains("test_api_key_456"));
1102    }
1103
1104    #[test]
1105    fn test_veracode_config_debug_redaction() {
1106        let config = VeracodeConfig::new("test_api_id_123", "test_api_key_456");
1107        let debug_output = format!("{config:?}");
1108
1109        // Should show structure but redact actual values
1110        assert!(debug_output.contains("VeracodeConfig"));
1111        assert!(debug_output.contains("credentials"));
1112        assert!(debug_output.contains("[REDACTED]"));
1113
1114        // Should not contain actual credential values
1115        assert!(!debug_output.contains("test_api_id_123"));
1116        assert!(!debug_output.contains("test_api_key_456"));
1117    }
1118
1119    #[test]
1120    fn test_veracode_credentials_access_methods() {
1121        let credentials = VeracodeCredentials::new(
1122            "test_api_id_123".to_string(),
1123            "test_api_key_456".to_string(),
1124        );
1125
1126        // Test expose methods
1127        assert_eq!(credentials.expose_api_id(), "test_api_id_123");
1128        assert_eq!(credentials.expose_api_key(), "test_api_key_456");
1129    }
1130
1131    #[test]
1132    fn test_veracode_credentials_arc_pointers() {
1133        let credentials = VeracodeCredentials::new(
1134            "test_api_id_123".to_string(),
1135            "test_api_key_456".to_string(),
1136        );
1137
1138        // Test ARC pointer methods
1139        let api_id_arc = credentials.api_id_ptr();
1140        let api_key_arc = credentials.api_key_ptr();
1141
1142        // Should be able to access through ARC
1143        assert_eq!(api_id_arc.expose_secret(), "test_api_id_123");
1144        assert_eq!(api_key_arc.expose_secret(), "test_api_key_456");
1145    }
1146
1147    #[test]
1148    fn test_veracode_credentials_clone() {
1149        let credentials = VeracodeCredentials::new(
1150            "test_api_id_123".to_string(),
1151            "test_api_key_456".to_string(),
1152        );
1153        let cloned_credentials = credentials.clone();
1154
1155        // Both should have the same values
1156        assert_eq!(
1157            credentials.expose_api_id(),
1158            cloned_credentials.expose_api_id()
1159        );
1160        assert_eq!(
1161            credentials.expose_api_key(),
1162            cloned_credentials.expose_api_key()
1163        );
1164    }
1165
1166    #[test]
1167    fn test_config_with_arc_credentials() {
1168        use secrecy::SecretString;
1169        use std::sync::Arc;
1170
1171        let api_id_arc = Arc::new(SecretString::new("test_api_id".into()));
1172        let api_key_arc = Arc::new(SecretString::new("test_api_key".into()));
1173
1174        let config = VeracodeConfig::from_arc_credentials(api_id_arc, api_key_arc);
1175
1176        assert_eq!(config.credentials.expose_api_id(), "test_api_id");
1177        assert_eq!(config.credentials.expose_api_key(), "test_api_key");
1178        assert_eq!(config.region, VeracodeRegion::Commercial);
1179    }
1180
1181    #[test]
1182    fn test_error_display() {
1183        let error = VeracodeError::Authentication("Invalid API key".to_string());
1184        assert_eq!(format!("{error}"), "Authentication error: Invalid API key");
1185    }
1186
1187    #[test]
1188    fn test_error_from_reqwest() {
1189        // Test that we can convert from reqwest errors
1190        // Note: We can't easily create a reqwest::Error for testing,
1191        // so we'll just verify the From trait implementation exists
1192        // by checking that it compiles
1193        fn _test_conversion(error: reqwest::Error) -> VeracodeError {
1194            VeracodeError::from(error)
1195        }
1196
1197        // If this compiles, the From trait is implemented correctly
1198        // Test passes if no panic occurs
1199    }
1200
1201    #[test]
1202    fn test_retry_config_default() {
1203        let config = RetryConfig::default();
1204        assert_eq!(config.max_attempts, 5);
1205        assert_eq!(config.initial_delay_ms, 1000);
1206        assert_eq!(config.max_delay_ms, 30000);
1207        assert_eq!(config.backoff_multiplier, 2.0);
1208        assert_eq!(config.max_total_delay_ms, 300000);
1209        assert!(config.jitter_enabled); // Jitter should be enabled by default
1210    }
1211
1212    #[test]
1213    fn test_retry_config_builder() {
1214        let config = RetryConfig::new()
1215            .with_max_attempts(5)
1216            .with_initial_delay(500)
1217            .with_max_delay(60000)
1218            .with_backoff_multiplier(1.5)
1219            .with_max_total_delay(600000);
1220
1221        assert_eq!(config.max_attempts, 5);
1222        assert_eq!(config.initial_delay_ms, 500);
1223        assert_eq!(config.max_delay_ms, 60000);
1224        assert_eq!(config.backoff_multiplier, 1.5);
1225        assert_eq!(config.max_total_delay_ms, 600000);
1226    }
1227
1228    #[test]
1229    fn test_retry_config_calculate_delay() {
1230        let config = RetryConfig::new()
1231            .with_initial_delay(1000)
1232            .with_backoff_multiplier(2.0)
1233            .with_max_delay(10000)
1234            .with_jitter_disabled(); // Disable jitter for predictable testing
1235
1236        // Test exponential backoff calculation
1237        assert_eq!(config.calculate_delay(0).as_millis(), 0); // No delay for attempt 0
1238        assert_eq!(config.calculate_delay(1).as_millis(), 1000); // First retry: 1000ms
1239        assert_eq!(config.calculate_delay(2).as_millis(), 2000); // Second retry: 2000ms
1240        assert_eq!(config.calculate_delay(3).as_millis(), 4000); // Third retry: 4000ms
1241        assert_eq!(config.calculate_delay(4).as_millis(), 8000); // Fourth retry: 8000ms
1242        assert_eq!(config.calculate_delay(5).as_millis(), 10000); // Fifth retry: capped at max_delay
1243    }
1244
1245    #[test]
1246    fn test_retry_config_is_retryable_error() {
1247        let config = RetryConfig::new();
1248
1249        // Test retryable errors
1250        assert!(
1251            config.is_retryable_error(&VeracodeError::InvalidResponse("temp error".to_string()))
1252        );
1253
1254        // Test non-retryable errors
1255        assert!(!config.is_retryable_error(&VeracodeError::Authentication("bad auth".to_string())));
1256        assert!(!config.is_retryable_error(&VeracodeError::Serialization(
1257            serde_json::from_str::<i32>("invalid").expect_err("should fail to deserialize")
1258        )));
1259        assert!(
1260            !config.is_retryable_error(&VeracodeError::InvalidConfig("bad config".to_string()))
1261        );
1262        assert!(!config.is_retryable_error(&VeracodeError::NotFound("not found".to_string())));
1263        assert!(
1264            !config.is_retryable_error(&VeracodeError::RetryExhausted("exhausted".to_string()))
1265        );
1266    }
1267
1268    #[test]
1269    fn test_veracode_config_with_retry_config() {
1270        let retry_config = RetryConfig::new().with_max_attempts(5);
1271        let config =
1272            VeracodeConfig::new("test_api_id", "test_api_key").with_retry_config(retry_config);
1273
1274        assert_eq!(config.retry_config.max_attempts, 5);
1275    }
1276
1277    #[test]
1278    fn test_veracode_config_with_retries_disabled() {
1279        let config = VeracodeConfig::new("test_api_id", "test_api_key").with_retries_disabled();
1280
1281        assert_eq!(config.retry_config.max_attempts, 0);
1282    }
1283
1284    #[test]
1285    fn test_timeout_configuration() {
1286        let config = VeracodeConfig::new("test_api_id", "test_api_key");
1287
1288        // Test default values
1289        assert_eq!(config.connect_timeout, 30);
1290        assert_eq!(config.request_timeout, 300);
1291    }
1292
1293    #[test]
1294    fn test_with_connect_timeout() {
1295        let config = VeracodeConfig::new("test_api_id", "test_api_key").with_connect_timeout(60);
1296
1297        assert_eq!(config.connect_timeout, 60);
1298        assert_eq!(config.request_timeout, 300); // Should remain default
1299    }
1300
1301    #[test]
1302    fn test_with_request_timeout() {
1303        let config = VeracodeConfig::new("test_api_id", "test_api_key").with_request_timeout(600);
1304
1305        assert_eq!(config.connect_timeout, 30); // Should remain default
1306        assert_eq!(config.request_timeout, 600);
1307    }
1308
1309    #[test]
1310    fn test_with_timeouts() {
1311        let config = VeracodeConfig::new("test_api_id", "test_api_key").with_timeouts(120, 1800);
1312
1313        assert_eq!(config.connect_timeout, 120);
1314        assert_eq!(config.request_timeout, 1800);
1315    }
1316
1317    #[test]
1318    fn test_timeout_configuration_chaining() {
1319        let config = VeracodeConfig::new("test_api_id", "test_api_key")
1320            .with_region(VeracodeRegion::European)
1321            .with_connect_timeout(45)
1322            .with_request_timeout(900)
1323            .with_retries_disabled();
1324
1325        assert_eq!(config.region, VeracodeRegion::European);
1326        assert_eq!(config.connect_timeout, 45);
1327        assert_eq!(config.request_timeout, 900);
1328        assert_eq!(config.retry_config.max_attempts, 0);
1329    }
1330
1331    #[test]
1332    fn test_retry_exhausted_error_display() {
1333        let error = VeracodeError::RetryExhausted("Failed after 3 attempts".to_string());
1334        assert_eq!(
1335            format!("{error}"),
1336            "Retry attempts exhausted: Failed after 3 attempts"
1337        );
1338    }
1339
1340    #[test]
1341    fn test_rate_limited_error_display_with_retry_after() {
1342        let error = VeracodeError::RateLimited {
1343            retry_after_seconds: Some(60),
1344            message: "Too Many Requests".to_string(),
1345        };
1346        assert_eq!(
1347            format!("{error}"),
1348            "Rate limit exceeded: Too Many Requests (retry after 60s)"
1349        );
1350    }
1351
1352    #[test]
1353    fn test_rate_limited_error_display_without_retry_after() {
1354        let error = VeracodeError::RateLimited {
1355            retry_after_seconds: None,
1356            message: "Too Many Requests".to_string(),
1357        };
1358        assert_eq!(format!("{error}"), "Rate limit exceeded: Too Many Requests");
1359    }
1360
1361    #[test]
1362    fn test_rate_limited_error_is_retryable() {
1363        let config = RetryConfig::new();
1364        let error = VeracodeError::RateLimited {
1365            retry_after_seconds: Some(60),
1366            message: "Rate limit exceeded".to_string(),
1367        };
1368        assert!(config.is_retryable_error(&error));
1369    }
1370
1371    #[test]
1372    fn test_calculate_rate_limit_delay_with_retry_after() {
1373        let config = RetryConfig::new();
1374        let delay = config.calculate_rate_limit_delay(Some(30));
1375        assert_eq!(delay.as_secs(), 30);
1376    }
1377
1378    #[test]
1379    #[cfg_attr(miri, ignore)] // Miri doesn't support SystemTime::now() in isolation
1380    fn test_calculate_rate_limit_delay_without_retry_after() {
1381        let config = RetryConfig::new();
1382        let delay = config.calculate_rate_limit_delay(None);
1383
1384        // Should be somewhere between buffer (5s) and 60 + buffer (65s)
1385        // depending on current second within the minute
1386        assert!(delay.as_secs() >= 5);
1387        assert!(delay.as_secs() <= 65);
1388    }
1389
1390    #[test]
1391    fn test_rate_limit_config_defaults() {
1392        let config = RetryConfig::default();
1393        assert_eq!(config.rate_limit_buffer_seconds, 5);
1394        assert_eq!(config.rate_limit_max_attempts, 1);
1395    }
1396
1397    #[test]
1398    fn test_rate_limit_config_builders() {
1399        let config = RetryConfig::new()
1400            .with_rate_limit_buffer(10)
1401            .with_rate_limit_max_attempts(2);
1402
1403        assert_eq!(config.rate_limit_buffer_seconds, 10);
1404        assert_eq!(config.rate_limit_max_attempts, 2);
1405    }
1406
1407    #[test]
1408    #[cfg_attr(miri, ignore)] // Miri doesn't support SystemTime::now() in isolation
1409    fn test_rate_limit_delay_uses_buffer() {
1410        let config = RetryConfig::new().with_rate_limit_buffer(15);
1411        let delay = config.calculate_rate_limit_delay(None);
1412
1413        // The delay should include our custom 15s buffer
1414        assert!(delay.as_secs() >= 15);
1415        assert!(delay.as_secs() <= 75); // 60 + 15
1416    }
1417
1418    #[test]
1419    fn test_jitter_disabled() {
1420        let config = RetryConfig::new().with_jitter_disabled();
1421        assert!(!config.jitter_enabled);
1422
1423        // With jitter disabled, delays should be consistent
1424        let delay1 = config.calculate_delay(2);
1425        let delay2 = config.calculate_delay(2);
1426        assert_eq!(delay1, delay2);
1427    }
1428
1429    #[test]
1430    fn test_jitter_enabled() {
1431        let config = RetryConfig::new(); // Jitter enabled by default
1432        assert!(config.jitter_enabled);
1433
1434        // With jitter enabled, delays may vary (though they might occasionally be the same)
1435        let base_delay = config.initial_delay_ms;
1436        let delay = config.calculate_delay(1);
1437
1438        // The delay should be within the expected range (ยฑ25% jitter)
1439        #[allow(
1440            clippy::cast_possible_truncation,
1441            clippy::cast_sign_loss,
1442            clippy::cast_precision_loss
1443        )]
1444        let min_expected = (base_delay as f64 * 0.75) as u64;
1445        #[allow(
1446            clippy::cast_possible_truncation,
1447            clippy::cast_sign_loss,
1448            clippy::cast_precision_loss
1449        )]
1450        let max_expected = (base_delay as f64 * 1.25) as u64;
1451
1452        assert!(delay.as_millis() >= min_expected as u128);
1453        assert!(delay.as_millis() <= max_expected as u128);
1454    }
1455}
1456
1457// ============================================================================
1458// SECURITY TESTS - Property-Based Testing with Proptest
1459// ============================================================================
1460// These tests verify security properties under arbitrary inputs to catch
1461// edge cases that could lead to vulnerabilities.
1462
1463#[cfg(test)]
1464mod security_tests {
1465    use super::*;
1466    use proptest::prelude::*;
1467
1468    // ========================================================================
1469    // Test 1: RetryConfig::calculate_delay() - Integer Overflow Safety
1470    // ========================================================================
1471    // Security concern: Arithmetic overflow could cause:
1472    // - Incorrect delays leading to thundering herd problems
1473    // - Infinite loops if delay wraps to 0
1474    // - DoS if delay becomes excessively large
1475
1476    proptest! {
1477        #![proptest_config(ProptestConfig {
1478            cases: if cfg!(miri) { 5 } else { 1000 },
1479            failure_persistence: None,
1480            .. ProptestConfig::default()
1481        })]
1482
1483        /// Property: calculate_delay() must never panic or overflow for any input
1484        #[test]
1485        fn test_calculate_delay_no_overflow(
1486            attempt in 0u32..1000u32,
1487            initial_delay in 1u64..100_000u64,
1488            multiplier in 1.0f64..10.0f64,
1489            max_delay in 1u64..1_000_000u64,
1490        ) {
1491            let config = RetryConfig::new()
1492                .with_initial_delay(initial_delay)
1493                .with_backoff_multiplier(multiplier)
1494                .with_max_delay(max_delay)
1495                .with_jitter_disabled(); // Disable jitter for deterministic testing
1496
1497            // Should never panic
1498            let delay = config.calculate_delay(attempt);
1499
1500            // Delay should always be capped at max_delay
1501            assert!(delay.as_millis() <= max_delay as u128);
1502
1503            // Delay should be valid (Duration is always non-negative by type invariant)
1504            // This is a semantic assertion that the calculation produces reasonable results
1505            assert!(delay <= Duration::from_millis(max_delay));
1506        }
1507
1508        /// Property: Extreme multipliers should still produce safe delays
1509        #[test]
1510        fn test_calculate_delay_extreme_multipliers(
1511            attempt in 0u32..100u32,
1512            multiplier in 1.0f64..1000.0f64,
1513        ) {
1514            let config = RetryConfig::new()
1515                .with_initial_delay(1000)
1516                .with_backoff_multiplier(multiplier)
1517                .with_max_delay(60_000)
1518                .with_jitter_disabled();
1519
1520            // Should never panic even with extreme multipliers
1521            let delay = config.calculate_delay(attempt);
1522
1523            // Should be properly capped
1524            assert!(delay.as_millis() <= 60_000);
1525        }
1526    }
1527
1528    // ========================================================================
1529    // Test 2: RetryConfig::calculate_rate_limit_delay() - Time Safety
1530    // ========================================================================
1531    // Security concern: Time calculations could:
1532    // - Overflow when adding buffer seconds
1533    // - Result in negative durations
1534    // - Cause integer truncation issues
1535
1536    proptest! {
1537        #![proptest_config(ProptestConfig {
1538            cases: if cfg!(miri) { 5 } else { 1000 },
1539            failure_persistence: None,
1540            .. ProptestConfig::default()
1541        })]
1542
1543        /// Property: Rate limit delay with retry_after must never panic
1544        #[test]
1545        fn test_rate_limit_delay_with_retry_after(
1546            retry_after_seconds in 0u64..100_000u64,
1547        ) {
1548            let config = RetryConfig::new();
1549
1550            // Should never panic
1551            let delay = config.calculate_rate_limit_delay(Some(retry_after_seconds));
1552
1553            // Should match the requested delay
1554            assert_eq!(delay.as_secs(), retry_after_seconds);
1555        }
1556
1557        /// Property: Buffer addition should never overflow
1558        #[test]
1559        fn test_rate_limit_delay_buffer_no_overflow(
1560            buffer_seconds in 0u64..10_000u64,
1561        ) {
1562            let config = RetryConfig::new()
1563                .with_rate_limit_buffer(buffer_seconds);
1564
1565            // Should never panic even with large buffers
1566            let delay = config.calculate_rate_limit_delay(None);
1567
1568            // Delay should be at least the buffer
1569            assert!(delay.as_secs() >= buffer_seconds);
1570
1571            // Delay should not exceed minute window + buffer
1572            assert!(delay.as_secs() <= 60_u64.saturating_add(buffer_seconds));
1573        }
1574    }
1575
1576    // ========================================================================
1577    // Test 3: VeracodeCredentials - Memory Safety & Thread Safety
1578    // ========================================================================
1579    // Security concern: Credentials use Arc<SecretString> which could:
1580    // - Leak across thread boundaries if not properly managed
1581    // - Expose secrets in debug output
1582    // - Allow use-after-free if Arc is mishandled
1583
1584    proptest! {
1585        #![proptest_config(ProptestConfig {
1586            cases: if cfg!(miri) { 5 } else { 500 },
1587            failure_persistence: None,
1588            .. ProptestConfig::default()
1589        })]
1590
1591        /// Property: Credentials debug output must NEVER expose actual values
1592        #[test]
1593        fn test_credentials_debug_never_exposes_secrets(
1594            api_id in "[a-zA-Z0-9]{10,256}",
1595            api_key in "[a-zA-Z0-9]{10,256}",
1596        ) {
1597            let credentials = VeracodeCredentials::new(api_id.clone(), api_key.clone());
1598            let debug_output = format!("{:?}", credentials);
1599
1600            // Must contain the struct name
1601            assert!(debug_output.contains("VeracodeCredentials"));
1602
1603            // Must contain redaction marker
1604            assert!(debug_output.contains("[REDACTED]"));
1605
1606            // CRITICAL: Must NOT contain actual secrets
1607            assert!(!debug_output.contains(&api_id));
1608            assert!(!debug_output.contains(&api_key));
1609        }
1610
1611        /// Property: Arc cloning preserves values across copies
1612        #[test]
1613        fn test_credentials_arc_cloning_preserves_values(
1614            api_id in "[a-zA-Z0-9]{10,100}",
1615            api_key in "[a-zA-Z0-9]{10,100}",
1616        ) {
1617            let credentials = VeracodeCredentials::new(api_id.clone(), api_key.clone());
1618
1619            // Clone the Arc pointers
1620            let api_id_arc1 = credentials.api_id_ptr();
1621            let api_id_arc2 = credentials.api_id_ptr();
1622            let api_key_arc1 = credentials.api_key_ptr();
1623            let api_key_arc2 = credentials.api_key_ptr();
1624
1625            // All should expose the same values
1626            assert_eq!(api_id_arc1.expose_secret(), &api_id);
1627            assert_eq!(api_id_arc2.expose_secret(), &api_id);
1628            assert_eq!(api_key_arc1.expose_secret(), &api_key);
1629            assert_eq!(api_key_arc2.expose_secret(), &api_key);
1630        }
1631
1632        /// Property: Expose methods must return correct values
1633        #[test]
1634        fn test_credentials_expose_methods_correctness(
1635            api_id in "[a-zA-Z0-9]{10,256}",
1636            api_key in "[a-zA-Z0-9]{10,256}",
1637        ) {
1638            let credentials = VeracodeCredentials::new(api_id.clone(), api_key.clone());
1639
1640            // Expose methods should return exact values
1641            assert_eq!(credentials.expose_api_id(), api_id);
1642            assert_eq!(credentials.expose_api_key(), api_key);
1643        }
1644    }
1645
1646    // ========================================================================
1647    // Test 4: VeracodeConfig - Proxy URL Redaction Safety
1648    // ========================================================================
1649    // Security concern: Proxy URL redaction (lines 674-692) uses string slicing
1650    // which could panic on UTF-8 boundaries and leak credentials in debug output
1651
1652    proptest! {
1653        #![proptest_config(ProptestConfig {
1654            cases: if cfg!(miri) { 5 } else { 500 },
1655            failure_persistence: None,
1656            .. ProptestConfig::default()
1657        })]
1658
1659        /// Property: Debug output must redact proxy credentials
1660        #[test]
1661        fn test_config_debug_redacts_proxy_credentials(
1662            protocol in "(http|https)",
1663            // Use longer patterns to avoid accidental substring matches with port/host
1664            username in "[a-zA-Z]{15,30}",
1665            password in "[a-zA-Z]{15,30}",
1666            host in "proxy\\d{1,3}\\.example\\.com",
1667        ) {
1668            let port = 8080u16;
1669            let proxy_url = format!("{}://{}:{}@{}:{}", protocol, username, password, host, port);
1670
1671            let config = VeracodeConfig::new("api_id", "api_key")
1672                .with_proxy(&proxy_url);
1673
1674            let debug_output = format!("{:?}", config);
1675
1676            // Must contain redaction
1677            assert!(debug_output.contains("[REDACTED]"));
1678
1679            // CRITICAL: Must NOT expose credentials (full username/password strings)
1680            // Note: We test that the FULL credential string is not present, avoiding
1681            // false positives from substring matches with numeric port numbers
1682            assert!(!debug_output.contains(&username));
1683            assert!(!debug_output.contains(&password));
1684
1685            // Should still show host information
1686            assert!(debug_output.contains(&host));
1687        }
1688
1689        /// Property: Debug redaction must handle UTF-8 safely
1690        #[test]
1691        fn test_config_debug_handles_utf8_safely(
1692            // Use simpler ASCII-only strings to avoid proptest UTF-8 generation issues
1693            protocol in "(http|https)",
1694            creds in "[a-zA-Z0-9]{1,30}",
1695            host in "[a-z]{3,15}\\.[a-z]{2,5}",
1696        ) {
1697            // Create URL with credentials
1698            let proxy_url = format!("{}://{}@{}", protocol, creds, host);
1699
1700            let config = VeracodeConfig::new("test", "test")
1701                .with_proxy(&proxy_url);
1702
1703            // Should never panic on UTF-8 boundaries
1704            let debug_output = format!("{:?}", config);
1705
1706            // Basic sanity check
1707            assert!(debug_output.contains("VeracodeConfig"));
1708        }
1709
1710        /// Property: Proxy auth credentials must be redacted in debug output
1711        #[test]
1712        fn test_config_debug_redacts_proxy_auth(
1713            proxy_username in "[a-zA-Z0-9]{10,50}",
1714            proxy_password in "[a-zA-Z0-9]{10,50}",
1715        ) {
1716            let config = VeracodeConfig::new("api_id", "api_key")
1717                .with_proxy("http://proxy.example.com:8080")
1718                .with_proxy_auth(proxy_username.clone(), proxy_password.clone());
1719
1720            let debug_output = format!("{:?}", config);
1721
1722            // Must redact proxy credentials
1723            assert!(debug_output.contains("proxy_username"));
1724            assert!(debug_output.contains("proxy_password"));
1725            assert!(debug_output.contains("[REDACTED]"));
1726
1727            // CRITICAL: Must NOT expose actual proxy credentials
1728            assert!(!debug_output.contains(&proxy_username));
1729            assert!(!debug_output.contains(&proxy_password));
1730        }
1731    }
1732
1733    // ========================================================================
1734    // Test 5: VeracodeConfig - Regional URL Construction Safety
1735    // ========================================================================
1736    // Security concern: URL construction could result in:
1737    // - Malformed URLs leading to requests to wrong endpoints
1738    // - Credential leakage to unintended servers
1739
1740    proptest! {
1741        #![proptest_config(ProptestConfig {
1742            cases: if cfg!(miri) { 5 } else { 100 },
1743            failure_persistence: None,
1744            .. ProptestConfig::default()
1745        })]
1746
1747        /// Property: Region configuration must produce valid, expected URLs
1748        #[test]
1749        fn test_config_region_urls_are_valid(
1750            region in prop::sample::select(vec![
1751                VeracodeRegion::Commercial,
1752                VeracodeRegion::European,
1753                VeracodeRegion::Federal,
1754            ])
1755        ) {
1756            let config = VeracodeConfig::new("test", "test")
1757                .with_region(region);
1758
1759            // Verify URLs are properly formed
1760            match region {
1761                VeracodeRegion::Commercial => {
1762                    assert_eq!(config.rest_base_url, "https://api.veracode.com");
1763                    assert_eq!(config.xml_base_url, "https://analysiscenter.veracode.com");
1764                    assert_eq!(config.base_url, config.rest_base_url);
1765                }
1766                VeracodeRegion::European => {
1767                    assert_eq!(config.rest_base_url, "https://api.veracode.eu");
1768                    assert_eq!(config.xml_base_url, "https://analysiscenter.veracode.eu");
1769                    assert_eq!(config.base_url, config.rest_base_url);
1770                }
1771                VeracodeRegion::Federal => {
1772                    assert_eq!(config.rest_base_url, "https://api.veracode.us");
1773                    assert_eq!(config.xml_base_url, "https://analysiscenter.veracode.us");
1774                    assert_eq!(config.base_url, config.rest_base_url);
1775                }
1776            }
1777
1778            // All URLs must use HTTPS for security
1779            assert!(config.rest_base_url.starts_with("https://"));
1780            assert!(config.xml_base_url.starts_with("https://"));
1781            assert!(config.base_url.starts_with("https://"));
1782        }
1783    }
1784
1785    // ========================================================================
1786    // Test 6: RetryConfig - Timeout Configuration Safety
1787    // ========================================================================
1788    // Security concern: Invalid timeout values could:
1789    // - Cause integer overflow in duration calculations
1790    // - Lead to indefinite blocking (DoS)
1791    // - Allow timing attacks
1792
1793    proptest! {
1794        #![proptest_config(ProptestConfig {
1795            cases: if cfg!(miri) { 5 } else { 500 },
1796            failure_persistence: None,
1797            .. ProptestConfig::default()
1798        })]
1799
1800        /// Property: Timeout values must be safely handled without overflow
1801        #[test]
1802        fn test_config_timeout_no_overflow(
1803            connect_timeout in 1u64..100_000u64,
1804            request_timeout in 1u64..100_000u64,
1805        ) {
1806            let config = VeracodeConfig::new("test", "test")
1807                .with_timeouts(connect_timeout, request_timeout);
1808
1809            // Should store values correctly
1810            assert_eq!(config.connect_timeout, connect_timeout);
1811            assert_eq!(config.request_timeout, request_timeout);
1812
1813            // Values should remain positive (no wrapping)
1814            assert!(config.connect_timeout > 0);
1815            assert!(config.request_timeout > 0);
1816        }
1817
1818        /// Property: Individual timeout setters work correctly
1819        #[test]
1820        fn test_config_individual_timeout_setters(
1821            connect_timeout in 1u64..50_000u64,
1822            request_timeout in 1u64..50_000u64,
1823        ) {
1824            let config1 = VeracodeConfig::new("test", "test")
1825                .with_connect_timeout(connect_timeout);
1826            assert_eq!(config1.connect_timeout, connect_timeout);
1827            assert_eq!(config1.request_timeout, 300); // Default
1828
1829            let config2 = VeracodeConfig::new("test", "test")
1830                .with_request_timeout(request_timeout);
1831            assert_eq!(config2.connect_timeout, 30); // Default
1832            assert_eq!(config2.request_timeout, request_timeout);
1833        }
1834    }
1835
1836    // ========================================================================
1837    // Test 7: Error Display - Information Disclosure Prevention
1838    // ========================================================================
1839    // Security concern: Error messages could leak:
1840    // - Internal system details
1841    // - API credentials
1842    // - Network topology information
1843
1844    proptest! {
1845        #![proptest_config(ProptestConfig {
1846            cases: if cfg!(miri) { 5 } else { 500 },
1847            failure_persistence: None,
1848            .. ProptestConfig::default()
1849        })]
1850
1851        /// Property: Error Display should not expose sensitive patterns
1852        #[test]
1853        fn test_error_display_safe_messages(
1854            message in "[a-zA-Z0-9 ]{1,100}",
1855        ) {
1856            let errors = vec![
1857                VeracodeError::Authentication(message.clone()),
1858                VeracodeError::InvalidResponse(message.clone()),
1859                VeracodeError::InvalidConfig(message.clone()),
1860                VeracodeError::NotFound(message.clone()),
1861                VeracodeError::RetryExhausted(message.clone()),
1862            ];
1863
1864            for error in errors {
1865                let display = format!("{}", error);
1866
1867                // Should contain the error type
1868                assert!(!display.is_empty());
1869
1870                // Should contain the message we provided
1871                assert!(display.contains(&message));
1872            }
1873        }
1874
1875        /// Property: RateLimited error display handles retry_after safely
1876        #[test]
1877        fn test_rate_limited_error_display_safe(
1878            retry_after in prop::option::of(0u64..10_000u64),
1879            message in "[a-zA-Z0-9 ]{1,100}",
1880        ) {
1881            let error = VeracodeError::RateLimited {
1882                retry_after_seconds: retry_after,
1883                message: message.clone(),
1884            };
1885
1886            let display = format!("{}", error);
1887
1888            // Should contain core message
1889            assert!(display.contains(&message));
1890            assert!(display.contains("Rate limit exceeded"));
1891
1892            // If retry_after is present, should be in output
1893            if let Some(seconds) = retry_after {
1894                assert!(display.contains(&seconds.to_string()));
1895            }
1896        }
1897    }
1898}
1899
1900// ============================================================================
1901// KANI FORMAL VERIFICATION PROOFS
1902// ============================================================================
1903// These proofs use bounded model checking to formally verify security
1904// properties for critical arithmetic and memory operations.
1905
1906#[cfg(kani)]
1907mod kani_proofs {
1908    use super::*;
1909
1910    // ========================================================================
1911    // Proof 1: RetryConfig::calculate_delay() - Arithmetic Safety
1912    // ========================================================================
1913    // This proof formally verifies that the exponential backoff calculation
1914    // never overflows, never panics, and always produces values within bounds.
1915
1916    #[kani::proof]
1917    #[kani::unwind(10)] // Limit loop iterations for verification
1918    fn verify_calculate_delay_arithmetic_safety() {
1919        // Create arbitrary inputs
1920        let initial_delay: u64 = kani::any();
1921        let max_delay: u64 = kani::any();
1922        let multiplier: f64 = kani::any();
1923        let attempt: u32 = kani::any();
1924
1925        // Constrain inputs to realistic ranges
1926        kani::assume(initial_delay > 0);
1927        kani::assume(initial_delay <= 100_000);
1928        kani::assume(max_delay > 0);
1929        kani::assume(max_delay >= initial_delay);
1930        kani::assume(max_delay <= 1_000_000);
1931        kani::assume(multiplier >= 1.0);
1932        kani::assume(multiplier <= 10.0);
1933        kani::assume(multiplier.is_finite());
1934        kani::assume(attempt < 20); // Reasonable attempt limit
1935
1936        let config = RetryConfig::new()
1937            .with_initial_delay(initial_delay)
1938            .with_backoff_multiplier(multiplier)
1939            .with_max_delay(max_delay)
1940            .with_jitter_disabled();
1941
1942        // Calculate delay - should never panic
1943        let delay = config.calculate_delay(attempt);
1944
1945        // PROPERTY 1: Delay must never exceed max_delay
1946        assert!(delay.as_millis() <= max_delay as u128);
1947
1948        // PROPERTY 2: Delay must be non-negative (always true for Duration)
1949        assert!(delay.as_secs() < u64::MAX);
1950
1951        // PROPERTY 3: For attempt 0, delay should be 0
1952        if attempt == 0 {
1953            assert_eq!(delay.as_millis(), 0);
1954        }
1955    }
1956
1957    // ========================================================================
1958    // Proof 2: RetryConfig::calculate_rate_limit_delay() - Time Arithmetic
1959    // ========================================================================
1960    // This proof verifies that rate limit delay calculations handle all
1961    // possible inputs without overflow or panic.
1962    // Note: This proof tests the arithmetic logic only, avoiding SystemTime::now()
1963    // which calls unsupported FFI function clock_gettime.
1964
1965    #[kani::proof]
1966    fn verify_rate_limit_delay_safety() {
1967        let buffer_seconds: u64 = kani::any();
1968        let retry_after_seconds: Option<u64> = kani::any();
1969
1970        // Constrain to realistic values
1971        kani::assume(buffer_seconds <= 10_000);
1972        if let Some(secs) = retry_after_seconds {
1973            kani::assume(secs <= 100_000);
1974        }
1975
1976        // TEST 1: Verify arithmetic when retry_after is provided (direct Duration creation)
1977        if let Some(expected_secs) = retry_after_seconds {
1978            let delay = Duration::from_secs(expected_secs);
1979            // PROPERTY 1: Delay should match the provided value exactly
1980            assert_eq!(delay.as_secs(), expected_secs);
1981        }
1982
1983        // TEST 2: Verify arithmetic for minute window calculation (without calling SystemTime)
1984        // Simulate what calculate_rate_limit_delay does internally
1985        let current_second: u64 = kani::any();
1986        kani::assume(current_second < 60);
1987
1988        // This is the core arithmetic from calculate_rate_limit_delay
1989        let seconds_until_next_minute = 60_u64.saturating_sub(current_second);
1990        let total_delay = seconds_until_next_minute.saturating_add(buffer_seconds);
1991
1992        // PROPERTY 2: Delay should never be less than buffer
1993        assert!(total_delay >= buffer_seconds);
1994
1995        // PROPERTY 3: Delay should never exceed 60 seconds + buffer
1996        assert!(total_delay <= 60 + buffer_seconds);
1997
1998        // PROPERTY 4: saturating_sub and saturating_add never panic
1999        // (Proven by successful execution of the operations above)
2000    }
2001
2002    // ========================================================================
2003    // Proof 3: VeracodeCredentials - Memory Safety of Arc Operations
2004    // ========================================================================
2005    // This proof verifies that Arc cloning and access operations are memory-safe
2006    // and don't introduce use-after-free or double-free bugs.
2007    // Note: Increased unwind limit to accommodate string comparison loops in memcmp.
2008
2009    #[kani::proof]
2010    #[kani::unwind(50)]
2011    fn verify_credentials_arc_memory_safety() {
2012        // Create test credentials with fixed strings to avoid excessive state space exploration
2013        let api_id = "test_api_id".to_string();
2014        let api_key = "test_key123".to_string();
2015
2016        let credentials = VeracodeCredentials::new(api_id.clone(), api_key.clone());
2017
2018        // PROPERTY 1: Arc pointers from multiple calls should point to the same allocation
2019        let api_id_arc1 = credentials.api_id_ptr();
2020        let api_id_arc2 = credentials.api_id_ptr();
2021
2022        // Verify Arc pointers reference the same memory location
2023        assert_eq!(
2024            Arc::as_ptr(&api_id_arc1) as *const (),
2025            Arc::as_ptr(&api_id_arc2) as *const ()
2026        );
2027
2028        // PROPERTY 2: Dereferencing Arc pointers should yield same values
2029        assert_eq!(api_id_arc1.expose_secret(), api_id_arc2.expose_secret());
2030
2031        // PROPERTY 3: Expose methods should return original values
2032        assert_eq!(credentials.expose_api_id(), &api_id);
2033        assert_eq!(credentials.expose_api_key(), &api_key);
2034
2035        // PROPERTY 4: Cloning credentials should preserve values and maintain memory safety
2036        let cloned = credentials.clone();
2037        assert_eq!(cloned.expose_api_id(), credentials.expose_api_id());
2038        assert_eq!(cloned.expose_api_key(), credentials.expose_api_key());
2039
2040        // PROPERTY 5: Multiple clones should not cause memory corruption
2041        let cloned2 = cloned.clone();
2042        let _ = cloned2.expose_api_id();
2043        let _ = cloned2.expose_api_key();
2044
2045        // If we reach here without panic or memory error, Arc operations are safe
2046    }
2047
2048    // ========================================================================
2049    // Proof 4: VeracodeConfig - URL String Slicing Safety
2050    // ========================================================================
2051    // This proof verifies that the proxy URL redaction logic handles
2052    // string slicing safely without panicking on UTF-8 boundaries.
2053    // Optimized to avoid expensive format! operations.
2054
2055    #[kani::proof]
2056    fn verify_proxy_url_redaction_safety() {
2057        // Test only the specific string operation: finding '@' and slicing
2058        let has_at_sign: bool = kani::any();
2059
2060        // Use minimal test strings to reduce state space
2061        let url = if has_at_sign {
2062            "u:p@h".to_string()
2063        } else {
2064            "http://h".to_string()
2065        };
2066
2067        // PROPERTY 1: Creating config with proxy should never panic
2068        let config = VeracodeConfig::new("t", "k").with_proxy(&url);
2069
2070        // PROPERTY 2: Config should be created successfully
2071        assert!(config.proxy_url.is_some());
2072
2073        // PROPERTY 3: The redaction logic (tested indirectly via storage)
2074        // If we reach here without panic, string slicing is safe
2075        let _ = config.proxy_url;
2076    }
2077
2078    // ========================================================================
2079    // Proof 5: VeracodeConfig - Region URL Construction Correctness
2080    // ========================================================================
2081    // This proof verifies that regional URL construction produces valid,
2082    // expected URLs for all region variants.
2083    // Optimized to avoid expensive string equality comparisons.
2084
2085    #[kani::proof]
2086    #[kani::unwind(10)] // Limit loop unwinding to reduce complexity
2087    fn verify_region_url_construction() {
2088        // ULTRA-SIMPLIFIED PROOF: Test only one region to avoid OOM
2089        // This proof verifies that URL construction doesn't panic and produces valid output
2090        // Testing all 3 regions causes CBMC to run out of memory due to string operations
2091
2092        // Test: Commercial region - verify basic invariants
2093        let config = VeracodeConfig::new("a", "b").with_region(VeracodeRegion::Commercial);
2094
2095        // PROPERTY 1: URLs are non-empty (most critical safety property)
2096        assert!(!config.rest_base_url.is_empty());
2097        assert!(!config.xml_base_url.is_empty());
2098
2099        // PROPERTY 2: URLs are bounded in size (prevent unbounded growth)
2100        assert!(config.rest_base_url.len() < 100);
2101        assert!(config.xml_base_url.len() < 100);
2102
2103        // PROPERTY 3: Region is set correctly
2104        assert!(matches!(config.region, VeracodeRegion::Commercial));
2105    }
2106
2107    // ========================================================================
2108    // Proof 6: VeracodeConfig - European Region URL Construction
2109    // ========================================================================
2110    // Separate proof for European region to maintain verification performance
2111
2112    #[kani::proof]
2113    #[kani::unwind(10)]
2114    fn verify_european_region_url_construction() {
2115        let config = VeracodeConfig::new("a", "b").with_region(VeracodeRegion::European);
2116
2117        // PROPERTY 1: URLs are non-empty
2118        assert!(!config.rest_base_url.is_empty());
2119        assert!(!config.xml_base_url.is_empty());
2120
2121        // PROPERTY 2: URLs are bounded in size
2122        assert!(config.rest_base_url.len() < 100);
2123        assert!(config.xml_base_url.len() < 100);
2124
2125        // PROPERTY 3: Region is set correctly
2126        assert!(matches!(config.region, VeracodeRegion::European));
2127    }
2128
2129    // ========================================================================
2130    // Proof 7: VeracodeConfig - Federal Region URL Construction
2131    // ========================================================================
2132    // Separate proof for Federal region to maintain verification performance
2133
2134    #[kani::proof]
2135    #[kani::unwind(10)]
2136    fn verify_federal_region_url_construction() {
2137        let config = VeracodeConfig::new("a", "b").with_region(VeracodeRegion::Federal);
2138
2139        // PROPERTY 1: URLs are non-empty
2140        assert!(!config.rest_base_url.is_empty());
2141        assert!(!config.xml_base_url.is_empty());
2142
2143        // PROPERTY 2: URLs are bounded in size
2144        assert!(config.rest_base_url.len() < 100);
2145        assert!(config.xml_base_url.len() < 100);
2146
2147        // PROPERTY 3: Region is set correctly
2148        assert!(matches!(config.region, VeracodeRegion::Federal));
2149    }
2150}