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    /// Get an applications API instance.
449    /// Uses REST API (api.veracode.*).
450    #[must_use]
451    pub fn applications_api(&self) -> &Self {
452        self
453    }
454
455    /// Get a sandbox API instance.
456    /// Uses REST API (api.veracode.*).
457    #[must_use]
458    pub fn sandbox_api(&self) -> SandboxApi<'_> {
459        SandboxApi::new(self)
460    }
461
462    /// Get an identity API instance.
463    /// Uses REST API (api.veracode.*).
464    #[must_use]
465    pub fn identity_api(&self) -> IdentityApi<'_> {
466        IdentityApi::new(self)
467    }
468
469    /// Get a pipeline scan API instance.
470    /// Uses REST API (api.veracode.*).
471    #[must_use]
472    pub fn pipeline_api(&self) -> PipelineApi {
473        PipelineApi::new(self.clone())
474    }
475
476    /// Get a policy API instance.
477    /// Uses REST API (api.veracode.*).
478    #[must_use]
479    pub fn policy_api(&self) -> PolicyApi<'_> {
480        PolicyApi::new(self)
481    }
482
483    /// Get a findings API instance.
484    /// Uses REST API (api.veracode.*).
485    #[must_use]
486    pub fn findings_api(&self) -> FindingsApi {
487        FindingsApi::new(self.clone())
488    }
489
490    /// Get a reporting API instance.
491    /// Uses REST API (api.veracode.*).
492    #[must_use]
493    pub fn reporting_api(&self) -> reporting::ReportingApi {
494        reporting::ReportingApi::new(self.clone())
495    }
496
497    /// Get a scan API instance.
498    /// Uses XML API (analysiscenter.veracode.*) for both sandbox and application scans.
499    ///
500    /// # Errors
501    ///
502    /// Returns an error if the XML client cannot be created.
503    pub fn scan_api(&self) -> Result<ScanApi, VeracodeError> {
504        Ok(ScanApi::new(self.new_xml_variant()))
505    }
506
507    /// Get a build API instance.
508    /// Uses XML API (analysiscenter.veracode.*) for build management operations.
509    ///
510    /// # Errors
511    ///
512    /// Returns an error if the XML client cannot be created.
513    pub fn build_api(&self) -> Result<build::BuildApi, VeracodeError> {
514        Ok(build::BuildApi::new(self.new_xml_variant()))
515    }
516
517    /// Get a workflow helper instance.
518    /// Provides high-level operations that combine multiple API calls.
519    #[must_use]
520    pub fn workflow(&self) -> workflow::VeracodeWorkflow {
521        workflow::VeracodeWorkflow::new(self.clone())
522    }
523}
524
525impl fmt::Display for VeracodeError {
526    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
527        match self {
528            VeracodeError::Http(e) => write!(f, "HTTP error: {e}"),
529            VeracodeError::Serialization(e) => write!(f, "Serialization error: {e}"),
530            VeracodeError::Authentication(e) => write!(f, "Authentication error: {e}"),
531            VeracodeError::InvalidResponse(e) => write!(f, "Invalid response: {e}"),
532            VeracodeError::HttpStatus {
533                status_code,
534                url,
535                message,
536            } => write!(f, "HTTP {status_code} error at {url}: {message}"),
537            VeracodeError::InvalidConfig(e) => write!(f, "Invalid configuration: {e}"),
538            VeracodeError::NotFound(e) => write!(f, "Item not found: {e}"),
539            VeracodeError::RetryExhausted(e) => write!(f, "Retry attempts exhausted: {e}"),
540            VeracodeError::RateLimited {
541                retry_after_seconds,
542                message,
543            } => match retry_after_seconds {
544                Some(seconds) => {
545                    write!(f, "Rate limit exceeded: {message} (retry after {seconds}s)")
546                }
547                None => write!(f, "Rate limit exceeded: {message}"),
548            },
549            VeracodeError::Validation(e) => write!(f, "Validation error: {e}"),
550        }
551    }
552}
553
554impl std::error::Error for VeracodeError {}
555
556impl From<ReqwestError> for VeracodeError {
557    fn from(error: ReqwestError) -> Self {
558        VeracodeError::Http(error)
559    }
560}
561
562impl From<serde_json::Error> for VeracodeError {
563    fn from(error: serde_json::Error) -> Self {
564        VeracodeError::Serialization(error)
565    }
566}
567
568impl From<validation::ValidationError> for VeracodeError {
569    fn from(error: validation::ValidationError) -> Self {
570        VeracodeError::Validation(error)
571    }
572}
573
574/// ARC-based credential storage for thread-safe access via memory pointers
575///
576/// This struct provides secure credential storage with the following protections:
577/// - Fields are private to prevent direct access
578/// - `SecretString` provides memory protection and debug redaction
579/// - ARC allows safe sharing across threads
580/// - Access is only possible through controlled expose_* methods
581#[derive(Clone)]
582pub struct VeracodeCredentials {
583    /// API ID stored in ARC for shared access - PRIVATE for security
584    api_id: Arc<SecretString>,
585    /// API key stored in ARC for shared access - PRIVATE for security  
586    api_key: Arc<SecretString>,
587}
588
589impl VeracodeCredentials {
590    /// Create new ARC-based credentials
591    #[must_use]
592    pub fn new(api_id: String, api_key: String) -> Self {
593        Self {
594            api_id: Arc::new(SecretString::new(api_id.into())),
595            api_key: Arc::new(SecretString::new(api_key.into())),
596        }
597    }
598
599    /// Get API ID via memory pointer (ARC) - USE WITH CAUTION
600    ///
601    /// # Security Warning
602    /// This returns an `Arc<SecretString>` which allows the caller to call `expose_secret()`.
603    /// Only use this method when you need to share credentials across thread boundaries.
604    /// For authentication, prefer using `expose_api_id()` directly.
605    #[must_use]
606    pub fn api_id_ptr(&self) -> Arc<SecretString> {
607        Arc::clone(&self.api_id)
608    }
609
610    /// Get API key via memory pointer (ARC) - USE WITH CAUTION
611    ///
612    /// # Security Warning
613    /// This returns an `Arc<SecretString>` which allows the caller to call `expose_secret()`.
614    /// Only use this method when you need to share credentials across thread boundaries.
615    /// For authentication, prefer using `expose_api_key()` directly.
616    #[must_use]
617    pub fn api_key_ptr(&self) -> Arc<SecretString> {
618        Arc::clone(&self.api_key)
619    }
620
621    /// Access API ID securely (temporary access for authentication)
622    ///
623    /// This is the preferred method for accessing the API ID during authentication.
624    /// The returned reference is only valid for the lifetime of this call.
625    #[must_use]
626    pub fn expose_api_id(&self) -> &str {
627        self.api_id.expose_secret()
628    }
629
630    /// Access API key securely (temporary access for authentication)
631    ///
632    /// This is the preferred method for accessing the API key during authentication.
633    /// The returned reference is only valid for the lifetime of this call.
634    #[must_use]
635    pub fn expose_api_key(&self) -> &str {
636        self.api_key.expose_secret()
637    }
638}
639
640impl std::fmt::Debug for VeracodeCredentials {
641    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
642        f.debug_struct("VeracodeCredentials")
643            .field("api_id", &"[REDACTED]")
644            .field("api_key", &"[REDACTED]")
645            .finish()
646    }
647}
648
649/// Configuration for the Veracode API client.
650///
651/// This struct contains all the necessary configuration for connecting to
652/// the Veracode APIs, including authentication credentials and regional settings.
653/// It automatically manages both REST API (api.veracode.*) and XML API
654/// (analysiscenter.veracode.*) endpoints based on the selected region.
655#[derive(Clone)]
656pub struct VeracodeConfig {
657    /// ARC-based credentials for thread-safe access
658    pub credentials: VeracodeCredentials,
659    /// Base URL for the current client instance
660    pub base_url: String,
661    /// REST API base URL (api.veracode.*)
662    pub rest_base_url: String,
663    /// XML API base URL (analysiscenter.veracode.*)
664    pub xml_base_url: String,
665    /// Veracode region for your account
666    pub region: VeracodeRegion,
667    /// Whether to validate TLS certificates (default: true)
668    pub validate_certificates: bool,
669    /// Retry configuration for HTTP requests
670    pub retry_config: RetryConfig,
671    /// HTTP connection timeout in seconds (default: 30)
672    pub connect_timeout: u64,
673    /// HTTP request timeout in seconds (default: 300)
674    pub request_timeout: u64,
675    /// HTTP/HTTPS proxy URL (optional)
676    pub proxy_url: Option<String>,
677    /// Proxy authentication username (optional)
678    pub proxy_username: Option<SecretString>,
679    /// Proxy authentication password (optional)
680    pub proxy_password: Option<SecretString>,
681}
682
683/// Custom Debug implementation for `VeracodeConfig` that redacts sensitive information
684impl std::fmt::Debug for VeracodeConfig {
685    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
686        // Redact proxy URL if it contains credentials
687        let proxy_url_redacted = self.proxy_url.as_ref().map(|url| {
688            if url.contains('@') {
689                // URL contains credentials, redact them
690                if let Some(at_pos) = url.rfind('@') {
691                    if let Some(proto_end) = url.find("://") {
692                        // Use safe string slicing methods that respect UTF-8 boundaries
693                        let protocol = url.get(..proto_end).unwrap_or("");
694                        let host_part = url.get(at_pos.saturating_add(1)..).unwrap_or("");
695                        format!("{}://[REDACTED]@{}", protocol, host_part)
696                    } else {
697                        "[REDACTED]".to_string()
698                    }
699                } else {
700                    "[REDACTED]".to_string()
701                }
702            } else {
703                url.clone()
704            }
705        });
706
707        f.debug_struct("VeracodeConfig")
708            .field("credentials", &self.credentials)
709            .field("base_url", &self.base_url)
710            .field("rest_base_url", &self.rest_base_url)
711            .field("xml_base_url", &self.xml_base_url)
712            .field("region", &self.region)
713            .field("validate_certificates", &self.validate_certificates)
714            .field("retry_config", &self.retry_config)
715            .field("connect_timeout", &self.connect_timeout)
716            .field("request_timeout", &self.request_timeout)
717            .field("proxy_url", &proxy_url_redacted)
718            .field(
719                "proxy_username",
720                &self.proxy_username.as_ref().map(|_| "[REDACTED]"),
721            )
722            .field(
723                "proxy_password",
724                &self.proxy_password.as_ref().map(|_| "[REDACTED]"),
725            )
726            .finish()
727    }
728}
729
730// URL constants for different regions
731const COMMERCIAL_REST_URL: &str = "https://api.veracode.com";
732const COMMERCIAL_XML_URL: &str = "https://analysiscenter.veracode.com";
733const EUROPEAN_REST_URL: &str = "https://api.veracode.eu";
734const EUROPEAN_XML_URL: &str = "https://analysiscenter.veracode.eu";
735const FEDERAL_REST_URL: &str = "https://api.veracode.us";
736const FEDERAL_XML_URL: &str = "https://analysiscenter.veracode.us";
737
738/// Veracode regions for API access.
739///
740/// Different regions use different API endpoints. Choose the region
741/// that matches your Veracode account configuration.
742#[derive(Debug, Clone, Copy, PartialEq)]
743pub enum VeracodeRegion {
744    /// Commercial region (default) - api.veracode.com
745    Commercial,
746    /// European region - api.veracode.eu
747    European,
748    /// US Federal region - api.veracode.us
749    Federal,
750}
751
752impl VeracodeConfig {
753    /// Create a new configuration for the Commercial region.
754    ///
755    /// This creates a configuration that supports both REST API (api.veracode.*)
756    /// and XML API (analysiscenter.veracode.*) endpoints. The `base_url` defaults
757    /// to REST API for most modules, while sandbox scan operations automatically
758    /// use the XML API endpoint.
759    ///
760    /// # Arguments
761    ///
762    /// * `api_id` - Your Veracode API ID
763    /// * `api_key` - Your Veracode API key
764    ///
765    /// # Returns
766    ///
767    /// A new `VeracodeConfig` instance configured for the Commercial region.
768    #[must_use]
769    pub fn new(api_id: &str, api_key: &str) -> Self {
770        let credentials = VeracodeCredentials::new(api_id.to_string(), api_key.to_string());
771        Self {
772            credentials,
773            base_url: COMMERCIAL_REST_URL.to_string(),
774            rest_base_url: COMMERCIAL_REST_URL.to_string(),
775            xml_base_url: COMMERCIAL_XML_URL.to_string(),
776            region: VeracodeRegion::Commercial,
777            validate_certificates: true, // Default to secure
778            retry_config: RetryConfig::default(),
779            connect_timeout: 30,  // Default: 30 seconds
780            request_timeout: 300, // Default: 5 minutes
781            proxy_url: None,
782            proxy_username: None,
783            proxy_password: None,
784        }
785    }
786
787    /// Create a new configuration with ARC-based credentials
788    ///
789    /// This method allows direct use of ARC pointers for credential sharing
790    /// across threads and components.
791    #[must_use]
792    pub fn from_arc_credentials(api_id: Arc<SecretString>, api_key: Arc<SecretString>) -> Self {
793        let credentials = VeracodeCredentials { api_id, api_key };
794
795        Self {
796            credentials,
797            base_url: COMMERCIAL_REST_URL.to_string(),
798            rest_base_url: COMMERCIAL_REST_URL.to_string(),
799            xml_base_url: COMMERCIAL_XML_URL.to_string(),
800            region: VeracodeRegion::Commercial,
801            validate_certificates: true,
802            retry_config: RetryConfig::default(),
803            connect_timeout: 30,
804            request_timeout: 300,
805            proxy_url: None,
806            proxy_username: None,
807            proxy_password: None,
808        }
809    }
810
811    /// Set the region for this configuration.
812    ///
813    /// This will automatically update both REST and XML API URLs to match the region.
814    /// All modules will use the appropriate regional endpoint for their API type.
815    ///
816    /// # Arguments
817    ///
818    /// * `region` - The Veracode region to use
819    ///
820    /// # Returns
821    ///
822    /// The updated configuration instance (for method chaining).
823    #[must_use]
824    pub fn with_region(mut self, region: VeracodeRegion) -> Self {
825        let (rest_url, xml_url) = match region {
826            VeracodeRegion::Commercial => (COMMERCIAL_REST_URL, COMMERCIAL_XML_URL),
827            VeracodeRegion::European => (EUROPEAN_REST_URL, EUROPEAN_XML_URL),
828            VeracodeRegion::Federal => (FEDERAL_REST_URL, FEDERAL_XML_URL),
829        };
830
831        self.region = region;
832        self.rest_base_url = rest_url.to_string();
833        self.xml_base_url = xml_url.to_string();
834        self.base_url = self.rest_base_url.clone(); // Default to REST
835        self
836    }
837
838    /// Disable certificate validation for development environments.
839    ///
840    /// WARNING: This should only be used in development environments with
841    /// self-signed certificates. Never use this in production.
842    ///
843    /// # Returns
844    ///
845    /// The updated configuration instance (for method chaining).
846    #[must_use]
847    pub fn with_certificate_validation_disabled(mut self) -> Self {
848        self.validate_certificates = false;
849        self
850    }
851
852    /// Set a custom retry configuration.
853    ///
854    /// This allows you to customize the retry behavior for HTTP requests,
855    /// including the number of attempts, delays, and backoff strategy.
856    ///
857    /// # Arguments
858    ///
859    /// * `retry_config` - The retry configuration to use
860    ///
861    /// # Returns
862    ///
863    /// The updated configuration instance (for method chaining).
864    #[must_use]
865    pub fn with_retry_config(mut self, retry_config: RetryConfig) -> Self {
866        self.retry_config = retry_config;
867        self
868    }
869
870    /// Disable retries for HTTP requests.
871    ///
872    /// This will set the retry configuration to perform no retries on failed requests.
873    /// Useful for scenarios where you want to handle errors immediately without any delays.
874    ///
875    /// # Returns
876    ///
877    /// The updated configuration instance (for method chaining).
878    #[must_use]
879    pub fn with_retries_disabled(mut self) -> Self {
880        self.retry_config = RetryConfig::new().with_max_attempts(0);
881        self
882    }
883
884    /// Set the HTTP connection timeout.
885    ///
886    /// This controls how long to wait for a connection to be established.
887    ///
888    /// # Arguments
889    ///
890    /// * `timeout_seconds` - Connection timeout in seconds
891    ///
892    /// # Returns
893    ///
894    /// The updated configuration instance (for method chaining).
895    #[must_use]
896    pub fn with_connect_timeout(mut self, timeout_seconds: u64) -> Self {
897        self.connect_timeout = timeout_seconds;
898        self
899    }
900
901    /// Set the HTTP request timeout.
902    ///
903    /// This controls the total time allowed for a request to complete,
904    /// including connection establishment, request transmission, and response reception.
905    ///
906    /// # Arguments
907    ///
908    /// * `timeout_seconds` - Request timeout in seconds
909    ///
910    /// # Returns
911    ///
912    /// The updated configuration instance (for method chaining).
913    #[must_use]
914    pub fn with_request_timeout(mut self, timeout_seconds: u64) -> Self {
915        self.request_timeout = timeout_seconds;
916        self
917    }
918
919    /// Set both connection and request timeouts.
920    ///
921    /// This is a convenience method to set both timeout values at once.
922    ///
923    /// # Arguments
924    ///
925    /// * `connect_timeout_seconds` - Connection timeout in seconds
926    /// * `request_timeout_seconds` - Request timeout in seconds
927    ///
928    /// # Returns
929    ///
930    /// The updated configuration instance (for method chaining).
931    #[must_use]
932    pub fn with_timeouts(
933        mut self,
934        connect_timeout_seconds: u64,
935        request_timeout_seconds: u64,
936    ) -> Self {
937        self.connect_timeout = connect_timeout_seconds;
938        self.request_timeout = request_timeout_seconds;
939        self
940    }
941
942    /// Get ARC pointer to API ID for sharing across threads
943    #[must_use]
944    pub fn api_id_arc(&self) -> Arc<SecretString> {
945        self.credentials.api_id_ptr()
946    }
947
948    /// Get ARC pointer to API key for sharing across threads
949    #[must_use]
950    pub fn api_key_arc(&self) -> Arc<SecretString> {
951        self.credentials.api_key_ptr()
952    }
953
954    /// Set the HTTP/HTTPS proxy URL.
955    ///
956    /// Configures an HTTP or HTTPS proxy for all requests. The proxy URL should include
957    /// the protocol (http:// or https://). Credentials can be embedded in the URL or
958    /// set separately using `with_proxy_auth()`.
959    ///
960    /// # Arguments
961    ///
962    /// * `proxy_url` - The proxy URL (e.g., "<http://proxy.example.com:8080>")
963    ///
964    /// # Returns
965    ///
966    /// The updated configuration instance (for method chaining).
967    ///
968    /// # Examples
969    ///
970    /// ```no_run
971    /// use veracode_platform::VeracodeConfig;
972    ///
973    /// // Without authentication
974    /// let config = VeracodeConfig::new("api_id", "api_key")
975    ///     .with_proxy("http://proxy.example.com:8080");
976    ///
977    /// // With embedded credentials (less secure)
978    /// let config = VeracodeConfig::new("api_id", "api_key")
979    ///     .with_proxy("http://user:pass@proxy.example.com:8080");
980    /// ```
981    #[must_use]
982    pub fn with_proxy(mut self, proxy_url: impl Into<String>) -> Self {
983        self.proxy_url = Some(proxy_url.into());
984        self
985    }
986
987    /// Set proxy authentication credentials.
988    ///
989    /// Configures username and password for proxy authentication using HTTP Basic Auth.
990    /// This is more secure than embedding credentials in the proxy URL as the credentials
991    /// are stored using `SecretString` and properly redacted in debug output.
992    ///
993    /// # Arguments
994    ///
995    /// * `username` - The proxy username
996    /// * `password` - The proxy password
997    ///
998    /// # Returns
999    ///
1000    /// The updated configuration instance (for method chaining).
1001    ///
1002    /// # Examples
1003    ///
1004    /// ```no_run
1005    /// use veracode_platform::VeracodeConfig;
1006    ///
1007    /// let config = VeracodeConfig::new("api_id", "api_key")
1008    ///     .with_proxy("http://proxy.example.com:8080")
1009    ///     .with_proxy_auth("username", "password");
1010    /// ```
1011    #[must_use]
1012    pub fn with_proxy_auth(
1013        mut self,
1014        username: impl Into<String>,
1015        password: impl Into<String>,
1016    ) -> Self {
1017        self.proxy_username = Some(SecretString::new(username.into().into()));
1018        self.proxy_password = Some(SecretString::new(password.into().into()));
1019        self
1020    }
1021}
1022
1023#[cfg(test)]
1024#[allow(clippy::expect_used)]
1025mod tests {
1026    use super::*;
1027
1028    #[test]
1029    fn test_config_creation() {
1030        let config = VeracodeConfig::new("test_api_id", "test_api_key");
1031
1032        assert_eq!(config.credentials.expose_api_id(), "test_api_id");
1033        assert_eq!(config.credentials.expose_api_key(), "test_api_key");
1034        assert_eq!(config.base_url, "https://api.veracode.com");
1035        assert_eq!(config.rest_base_url, "https://api.veracode.com");
1036        assert_eq!(config.xml_base_url, "https://analysiscenter.veracode.com");
1037        assert_eq!(config.region, VeracodeRegion::Commercial);
1038        assert!(config.validate_certificates); // Default is secure
1039        assert_eq!(config.retry_config.max_attempts, 5); // Default retry config
1040    }
1041
1042    #[test]
1043    fn test_european_region_config() {
1044        let config = VeracodeConfig::new("test_api_id", "test_api_key")
1045            .with_region(VeracodeRegion::European);
1046
1047        assert_eq!(config.base_url, "https://api.veracode.eu");
1048        assert_eq!(config.rest_base_url, "https://api.veracode.eu");
1049        assert_eq!(config.xml_base_url, "https://analysiscenter.veracode.eu");
1050        assert_eq!(config.region, VeracodeRegion::European);
1051    }
1052
1053    #[test]
1054    fn test_federal_region_config() {
1055        let config =
1056            VeracodeConfig::new("test_api_id", "test_api_key").with_region(VeracodeRegion::Federal);
1057
1058        assert_eq!(config.base_url, "https://api.veracode.us");
1059        assert_eq!(config.rest_base_url, "https://api.veracode.us");
1060        assert_eq!(config.xml_base_url, "https://analysiscenter.veracode.us");
1061        assert_eq!(config.region, VeracodeRegion::Federal);
1062    }
1063
1064    #[test]
1065    fn test_certificate_validation_disabled() {
1066        let config = VeracodeConfig::new("test_api_id", "test_api_key")
1067            .with_certificate_validation_disabled();
1068
1069        assert!(!config.validate_certificates);
1070    }
1071
1072    #[test]
1073    fn test_veracode_credentials_debug_redaction() {
1074        let credentials = VeracodeCredentials::new(
1075            "test_api_id_123".to_string(),
1076            "test_api_key_456".to_string(),
1077        );
1078        let debug_output = format!("{credentials:?}");
1079
1080        // Should show structure but redact actual values
1081        assert!(debug_output.contains("VeracodeCredentials"));
1082        assert!(debug_output.contains("[REDACTED]"));
1083
1084        // Should not contain actual credential values
1085        assert!(!debug_output.contains("test_api_id_123"));
1086        assert!(!debug_output.contains("test_api_key_456"));
1087    }
1088
1089    #[test]
1090    fn test_veracode_config_debug_redaction() {
1091        let config = VeracodeConfig::new("test_api_id_123", "test_api_key_456");
1092        let debug_output = format!("{config:?}");
1093
1094        // Should show structure but redact actual values
1095        assert!(debug_output.contains("VeracodeConfig"));
1096        assert!(debug_output.contains("credentials"));
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_credentials_access_methods() {
1106        let credentials = VeracodeCredentials::new(
1107            "test_api_id_123".to_string(),
1108            "test_api_key_456".to_string(),
1109        );
1110
1111        // Test expose methods
1112        assert_eq!(credentials.expose_api_id(), "test_api_id_123");
1113        assert_eq!(credentials.expose_api_key(), "test_api_key_456");
1114    }
1115
1116    #[test]
1117    fn test_veracode_credentials_arc_pointers() {
1118        let credentials = VeracodeCredentials::new(
1119            "test_api_id_123".to_string(),
1120            "test_api_key_456".to_string(),
1121        );
1122
1123        // Test ARC pointer methods
1124        let api_id_arc = credentials.api_id_ptr();
1125        let api_key_arc = credentials.api_key_ptr();
1126
1127        // Should be able to access through ARC
1128        assert_eq!(api_id_arc.expose_secret(), "test_api_id_123");
1129        assert_eq!(api_key_arc.expose_secret(), "test_api_key_456");
1130    }
1131
1132    #[test]
1133    fn test_veracode_credentials_clone() {
1134        let credentials = VeracodeCredentials::new(
1135            "test_api_id_123".to_string(),
1136            "test_api_key_456".to_string(),
1137        );
1138        let cloned_credentials = credentials.clone();
1139
1140        // Both should have the same values
1141        assert_eq!(
1142            credentials.expose_api_id(),
1143            cloned_credentials.expose_api_id()
1144        );
1145        assert_eq!(
1146            credentials.expose_api_key(),
1147            cloned_credentials.expose_api_key()
1148        );
1149    }
1150
1151    #[test]
1152    fn test_config_with_arc_credentials() {
1153        use secrecy::SecretString;
1154        use std::sync::Arc;
1155
1156        let api_id_arc = Arc::new(SecretString::new("test_api_id".into()));
1157        let api_key_arc = Arc::new(SecretString::new("test_api_key".into()));
1158
1159        let config = VeracodeConfig::from_arc_credentials(api_id_arc, api_key_arc);
1160
1161        assert_eq!(config.credentials.expose_api_id(), "test_api_id");
1162        assert_eq!(config.credentials.expose_api_key(), "test_api_key");
1163        assert_eq!(config.region, VeracodeRegion::Commercial);
1164    }
1165
1166    #[test]
1167    fn test_error_display() {
1168        let error = VeracodeError::Authentication("Invalid API key".to_string());
1169        assert_eq!(format!("{error}"), "Authentication error: Invalid API key");
1170    }
1171
1172    #[test]
1173    fn test_error_from_reqwest() {
1174        // Test that we can convert from reqwest errors
1175        // Note: We can't easily create a reqwest::Error for testing,
1176        // so we'll just verify the From trait implementation exists
1177        // by checking that it compiles
1178        fn _test_conversion(error: reqwest::Error) -> VeracodeError {
1179            VeracodeError::from(error)
1180        }
1181
1182        // If this compiles, the From trait is implemented correctly
1183        // Test passes if no panic occurs
1184    }
1185
1186    #[test]
1187    fn test_retry_config_default() {
1188        let config = RetryConfig::default();
1189        assert_eq!(config.max_attempts, 5);
1190        assert_eq!(config.initial_delay_ms, 1000);
1191        assert_eq!(config.max_delay_ms, 30000);
1192        assert_eq!(config.backoff_multiplier, 2.0);
1193        assert_eq!(config.max_total_delay_ms, 300000);
1194        assert!(config.jitter_enabled); // Jitter should be enabled by default
1195    }
1196
1197    #[test]
1198    fn test_retry_config_builder() {
1199        let config = RetryConfig::new()
1200            .with_max_attempts(5)
1201            .with_initial_delay(500)
1202            .with_max_delay(60000)
1203            .with_backoff_multiplier(1.5)
1204            .with_max_total_delay(600000);
1205
1206        assert_eq!(config.max_attempts, 5);
1207        assert_eq!(config.initial_delay_ms, 500);
1208        assert_eq!(config.max_delay_ms, 60000);
1209        assert_eq!(config.backoff_multiplier, 1.5);
1210        assert_eq!(config.max_total_delay_ms, 600000);
1211    }
1212
1213    #[test]
1214    fn test_retry_config_calculate_delay() {
1215        let config = RetryConfig::new()
1216            .with_initial_delay(1000)
1217            .with_backoff_multiplier(2.0)
1218            .with_max_delay(10000)
1219            .with_jitter_disabled(); // Disable jitter for predictable testing
1220
1221        // Test exponential backoff calculation
1222        assert_eq!(config.calculate_delay(0).as_millis(), 0); // No delay for attempt 0
1223        assert_eq!(config.calculate_delay(1).as_millis(), 1000); // First retry: 1000ms
1224        assert_eq!(config.calculate_delay(2).as_millis(), 2000); // Second retry: 2000ms
1225        assert_eq!(config.calculate_delay(3).as_millis(), 4000); // Third retry: 4000ms
1226        assert_eq!(config.calculate_delay(4).as_millis(), 8000); // Fourth retry: 8000ms
1227        assert_eq!(config.calculate_delay(5).as_millis(), 10000); // Fifth retry: capped at max_delay
1228    }
1229
1230    #[test]
1231    fn test_retry_config_is_retryable_error() {
1232        let config = RetryConfig::new();
1233
1234        // Test retryable errors
1235        assert!(
1236            config.is_retryable_error(&VeracodeError::InvalidResponse("temp error".to_string()))
1237        );
1238
1239        // Test non-retryable errors
1240        assert!(!config.is_retryable_error(&VeracodeError::Authentication("bad auth".to_string())));
1241        assert!(!config.is_retryable_error(&VeracodeError::Serialization(
1242            serde_json::from_str::<i32>("invalid").expect_err("should fail to deserialize")
1243        )));
1244        assert!(
1245            !config.is_retryable_error(&VeracodeError::InvalidConfig("bad config".to_string()))
1246        );
1247        assert!(!config.is_retryable_error(&VeracodeError::NotFound("not found".to_string())));
1248        assert!(
1249            !config.is_retryable_error(&VeracodeError::RetryExhausted("exhausted".to_string()))
1250        );
1251    }
1252
1253    #[test]
1254    fn test_veracode_config_with_retry_config() {
1255        let retry_config = RetryConfig::new().with_max_attempts(5);
1256        let config =
1257            VeracodeConfig::new("test_api_id", "test_api_key").with_retry_config(retry_config);
1258
1259        assert_eq!(config.retry_config.max_attempts, 5);
1260    }
1261
1262    #[test]
1263    fn test_veracode_config_with_retries_disabled() {
1264        let config = VeracodeConfig::new("test_api_id", "test_api_key").with_retries_disabled();
1265
1266        assert_eq!(config.retry_config.max_attempts, 0);
1267    }
1268
1269    #[test]
1270    fn test_timeout_configuration() {
1271        let config = VeracodeConfig::new("test_api_id", "test_api_key");
1272
1273        // Test default values
1274        assert_eq!(config.connect_timeout, 30);
1275        assert_eq!(config.request_timeout, 300);
1276    }
1277
1278    #[test]
1279    fn test_with_connect_timeout() {
1280        let config = VeracodeConfig::new("test_api_id", "test_api_key").with_connect_timeout(60);
1281
1282        assert_eq!(config.connect_timeout, 60);
1283        assert_eq!(config.request_timeout, 300); // Should remain default
1284    }
1285
1286    #[test]
1287    fn test_with_request_timeout() {
1288        let config = VeracodeConfig::new("test_api_id", "test_api_key").with_request_timeout(600);
1289
1290        assert_eq!(config.connect_timeout, 30); // Should remain default
1291        assert_eq!(config.request_timeout, 600);
1292    }
1293
1294    #[test]
1295    fn test_with_timeouts() {
1296        let config = VeracodeConfig::new("test_api_id", "test_api_key").with_timeouts(120, 1800);
1297
1298        assert_eq!(config.connect_timeout, 120);
1299        assert_eq!(config.request_timeout, 1800);
1300    }
1301
1302    #[test]
1303    fn test_timeout_configuration_chaining() {
1304        let config = VeracodeConfig::new("test_api_id", "test_api_key")
1305            .with_region(VeracodeRegion::European)
1306            .with_connect_timeout(45)
1307            .with_request_timeout(900)
1308            .with_retries_disabled();
1309
1310        assert_eq!(config.region, VeracodeRegion::European);
1311        assert_eq!(config.connect_timeout, 45);
1312        assert_eq!(config.request_timeout, 900);
1313        assert_eq!(config.retry_config.max_attempts, 0);
1314    }
1315
1316    #[test]
1317    fn test_retry_exhausted_error_display() {
1318        let error = VeracodeError::RetryExhausted("Failed after 3 attempts".to_string());
1319        assert_eq!(
1320            format!("{error}"),
1321            "Retry attempts exhausted: Failed after 3 attempts"
1322        );
1323    }
1324
1325    #[test]
1326    fn test_rate_limited_error_display_with_retry_after() {
1327        let error = VeracodeError::RateLimited {
1328            retry_after_seconds: Some(60),
1329            message: "Too Many Requests".to_string(),
1330        };
1331        assert_eq!(
1332            format!("{error}"),
1333            "Rate limit exceeded: Too Many Requests (retry after 60s)"
1334        );
1335    }
1336
1337    #[test]
1338    fn test_rate_limited_error_display_without_retry_after() {
1339        let error = VeracodeError::RateLimited {
1340            retry_after_seconds: None,
1341            message: "Too Many Requests".to_string(),
1342        };
1343        assert_eq!(format!("{error}"), "Rate limit exceeded: Too Many Requests");
1344    }
1345
1346    #[test]
1347    fn test_rate_limited_error_is_retryable() {
1348        let config = RetryConfig::new();
1349        let error = VeracodeError::RateLimited {
1350            retry_after_seconds: Some(60),
1351            message: "Rate limit exceeded".to_string(),
1352        };
1353        assert!(config.is_retryable_error(&error));
1354    }
1355
1356    #[test]
1357    fn test_calculate_rate_limit_delay_with_retry_after() {
1358        let config = RetryConfig::new();
1359        let delay = config.calculate_rate_limit_delay(Some(30));
1360        assert_eq!(delay.as_secs(), 30);
1361    }
1362
1363    #[test]
1364    #[cfg_attr(miri, ignore)] // Miri doesn't support SystemTime::now() in isolation
1365    fn test_calculate_rate_limit_delay_without_retry_after() {
1366        let config = RetryConfig::new();
1367        let delay = config.calculate_rate_limit_delay(None);
1368
1369        // Should be somewhere between buffer (5s) and 60 + buffer (65s)
1370        // depending on current second within the minute
1371        assert!(delay.as_secs() >= 5);
1372        assert!(delay.as_secs() <= 65);
1373    }
1374
1375    #[test]
1376    fn test_rate_limit_config_defaults() {
1377        let config = RetryConfig::default();
1378        assert_eq!(config.rate_limit_buffer_seconds, 5);
1379        assert_eq!(config.rate_limit_max_attempts, 1);
1380    }
1381
1382    #[test]
1383    fn test_rate_limit_config_builders() {
1384        let config = RetryConfig::new()
1385            .with_rate_limit_buffer(10)
1386            .with_rate_limit_max_attempts(2);
1387
1388        assert_eq!(config.rate_limit_buffer_seconds, 10);
1389        assert_eq!(config.rate_limit_max_attempts, 2);
1390    }
1391
1392    #[test]
1393    #[cfg_attr(miri, ignore)] // Miri doesn't support SystemTime::now() in isolation
1394    fn test_rate_limit_delay_uses_buffer() {
1395        let config = RetryConfig::new().with_rate_limit_buffer(15);
1396        let delay = config.calculate_rate_limit_delay(None);
1397
1398        // The delay should include our custom 15s buffer
1399        assert!(delay.as_secs() >= 15);
1400        assert!(delay.as_secs() <= 75); // 60 + 15
1401    }
1402
1403    #[test]
1404    fn test_jitter_disabled() {
1405        let config = RetryConfig::new().with_jitter_disabled();
1406        assert!(!config.jitter_enabled);
1407
1408        // With jitter disabled, delays should be consistent
1409        let delay1 = config.calculate_delay(2);
1410        let delay2 = config.calculate_delay(2);
1411        assert_eq!(delay1, delay2);
1412    }
1413
1414    #[test]
1415    fn test_jitter_enabled() {
1416        let config = RetryConfig::new(); // Jitter enabled by default
1417        assert!(config.jitter_enabled);
1418
1419        // With jitter enabled, delays may vary (though they might occasionally be the same)
1420        let base_delay = config.initial_delay_ms;
1421        let delay = config.calculate_delay(1);
1422
1423        // The delay should be within the expected range (ยฑ25% jitter)
1424        #[allow(
1425            clippy::cast_possible_truncation,
1426            clippy::cast_sign_loss,
1427            clippy::cast_precision_loss
1428        )]
1429        let min_expected = (base_delay as f64 * 0.75) as u64;
1430        #[allow(
1431            clippy::cast_possible_truncation,
1432            clippy::cast_sign_loss,
1433            clippy::cast_precision_loss
1434        )]
1435        let max_expected = (base_delay as f64 * 1.25) as u64;
1436
1437        assert!(delay.as_millis() >= min_expected as u128);
1438        assert!(delay.as_millis() <= max_expected as u128);
1439    }
1440}
1441
1442// ============================================================================
1443// SECURITY TESTS - Property-Based Testing with Proptest
1444// ============================================================================
1445// These tests verify security properties under arbitrary inputs to catch
1446// edge cases that could lead to vulnerabilities.
1447
1448#[cfg(test)]
1449mod security_tests {
1450    use super::*;
1451    use proptest::prelude::*;
1452
1453    // ========================================================================
1454    // Test 1: RetryConfig::calculate_delay() - Integer Overflow Safety
1455    // ========================================================================
1456    // Security concern: Arithmetic overflow could cause:
1457    // - Incorrect delays leading to thundering herd problems
1458    // - Infinite loops if delay wraps to 0
1459    // - DoS if delay becomes excessively large
1460
1461    proptest! {
1462        #![proptest_config(ProptestConfig {
1463            cases: if cfg!(miri) { 5 } else { 1000 },
1464            failure_persistence: None,
1465            .. ProptestConfig::default()
1466        })]
1467
1468        /// Property: calculate_delay() must never panic or overflow for any input
1469        #[test]
1470        fn test_calculate_delay_no_overflow(
1471            attempt in 0u32..1000u32,
1472            initial_delay in 1u64..100_000u64,
1473            multiplier in 1.0f64..10.0f64,
1474            max_delay in 1u64..1_000_000u64,
1475        ) {
1476            let config = RetryConfig::new()
1477                .with_initial_delay(initial_delay)
1478                .with_backoff_multiplier(multiplier)
1479                .with_max_delay(max_delay)
1480                .with_jitter_disabled(); // Disable jitter for deterministic testing
1481
1482            // Should never panic
1483            let delay = config.calculate_delay(attempt);
1484
1485            // Delay should always be capped at max_delay
1486            assert!(delay.as_millis() <= max_delay as u128);
1487
1488            // Delay should be valid (Duration is always non-negative by type invariant)
1489            // This is a semantic assertion that the calculation produces reasonable results
1490            assert!(delay <= Duration::from_millis(max_delay));
1491        }
1492
1493        /// Property: Extreme multipliers should still produce safe delays
1494        #[test]
1495        fn test_calculate_delay_extreme_multipliers(
1496            attempt in 0u32..100u32,
1497            multiplier in 1.0f64..1000.0f64,
1498        ) {
1499            let config = RetryConfig::new()
1500                .with_initial_delay(1000)
1501                .with_backoff_multiplier(multiplier)
1502                .with_max_delay(60_000)
1503                .with_jitter_disabled();
1504
1505            // Should never panic even with extreme multipliers
1506            let delay = config.calculate_delay(attempt);
1507
1508            // Should be properly capped
1509            assert!(delay.as_millis() <= 60_000);
1510        }
1511    }
1512
1513    // ========================================================================
1514    // Test 2: RetryConfig::calculate_rate_limit_delay() - Time Safety
1515    // ========================================================================
1516    // Security concern: Time calculations could:
1517    // - Overflow when adding buffer seconds
1518    // - Result in negative durations
1519    // - Cause integer truncation issues
1520
1521    proptest! {
1522        #![proptest_config(ProptestConfig {
1523            cases: if cfg!(miri) { 5 } else { 1000 },
1524            failure_persistence: None,
1525            .. ProptestConfig::default()
1526        })]
1527
1528        /// Property: Rate limit delay with retry_after must never panic
1529        #[test]
1530        fn test_rate_limit_delay_with_retry_after(
1531            retry_after_seconds in 0u64..100_000u64,
1532        ) {
1533            let config = RetryConfig::new();
1534
1535            // Should never panic
1536            let delay = config.calculate_rate_limit_delay(Some(retry_after_seconds));
1537
1538            // Should match the requested delay
1539            assert_eq!(delay.as_secs(), retry_after_seconds);
1540        }
1541
1542        /// Property: Buffer addition should never overflow
1543        #[test]
1544        fn test_rate_limit_delay_buffer_no_overflow(
1545            buffer_seconds in 0u64..10_000u64,
1546        ) {
1547            let config = RetryConfig::new()
1548                .with_rate_limit_buffer(buffer_seconds);
1549
1550            // Should never panic even with large buffers
1551            let delay = config.calculate_rate_limit_delay(None);
1552
1553            // Delay should be at least the buffer
1554            assert!(delay.as_secs() >= buffer_seconds);
1555
1556            // Delay should not exceed minute window + buffer
1557            assert!(delay.as_secs() <= 60_u64.saturating_add(buffer_seconds));
1558        }
1559    }
1560
1561    // ========================================================================
1562    // Test 3: VeracodeCredentials - Memory Safety & Thread Safety
1563    // ========================================================================
1564    // Security concern: Credentials use Arc<SecretString> which could:
1565    // - Leak across thread boundaries if not properly managed
1566    // - Expose secrets in debug output
1567    // - Allow use-after-free if Arc is mishandled
1568
1569    proptest! {
1570        #![proptest_config(ProptestConfig {
1571            cases: if cfg!(miri) { 5 } else { 500 },
1572            failure_persistence: None,
1573            .. ProptestConfig::default()
1574        })]
1575
1576        /// Property: Credentials debug output must NEVER expose actual values
1577        #[test]
1578        fn test_credentials_debug_never_exposes_secrets(
1579            api_id in "[a-zA-Z0-9]{10,256}",
1580            api_key in "[a-zA-Z0-9]{10,256}",
1581        ) {
1582            let credentials = VeracodeCredentials::new(api_id.clone(), api_key.clone());
1583            let debug_output = format!("{:?}", credentials);
1584
1585            // Must contain the struct name
1586            assert!(debug_output.contains("VeracodeCredentials"));
1587
1588            // Must contain redaction marker
1589            assert!(debug_output.contains("[REDACTED]"));
1590
1591            // CRITICAL: Must NOT contain actual secrets
1592            assert!(!debug_output.contains(&api_id));
1593            assert!(!debug_output.contains(&api_key));
1594        }
1595
1596        /// Property: Arc cloning preserves values across copies
1597        #[test]
1598        fn test_credentials_arc_cloning_preserves_values(
1599            api_id in "[a-zA-Z0-9]{10,100}",
1600            api_key in "[a-zA-Z0-9]{10,100}",
1601        ) {
1602            let credentials = VeracodeCredentials::new(api_id.clone(), api_key.clone());
1603
1604            // Clone the Arc pointers
1605            let api_id_arc1 = credentials.api_id_ptr();
1606            let api_id_arc2 = credentials.api_id_ptr();
1607            let api_key_arc1 = credentials.api_key_ptr();
1608            let api_key_arc2 = credentials.api_key_ptr();
1609
1610            // All should expose the same values
1611            assert_eq!(api_id_arc1.expose_secret(), &api_id);
1612            assert_eq!(api_id_arc2.expose_secret(), &api_id);
1613            assert_eq!(api_key_arc1.expose_secret(), &api_key);
1614            assert_eq!(api_key_arc2.expose_secret(), &api_key);
1615        }
1616
1617        /// Property: Expose methods must return correct values
1618        #[test]
1619        fn test_credentials_expose_methods_correctness(
1620            api_id in "[a-zA-Z0-9]{10,256}",
1621            api_key in "[a-zA-Z0-9]{10,256}",
1622        ) {
1623            let credentials = VeracodeCredentials::new(api_id.clone(), api_key.clone());
1624
1625            // Expose methods should return exact values
1626            assert_eq!(credentials.expose_api_id(), api_id);
1627            assert_eq!(credentials.expose_api_key(), api_key);
1628        }
1629    }
1630
1631    // ========================================================================
1632    // Test 4: VeracodeConfig - Proxy URL Redaction Safety
1633    // ========================================================================
1634    // Security concern: Proxy URL redaction (lines 674-692) uses string slicing
1635    // which could panic on UTF-8 boundaries and leak credentials in debug output
1636
1637    proptest! {
1638        #![proptest_config(ProptestConfig {
1639            cases: if cfg!(miri) { 5 } else { 500 },
1640            failure_persistence: None,
1641            .. ProptestConfig::default()
1642        })]
1643
1644        /// Property: Debug output must redact proxy credentials
1645        #[test]
1646        fn test_config_debug_redacts_proxy_credentials(
1647            protocol in "(http|https)",
1648            // Use longer patterns to avoid accidental substring matches with port/host
1649            username in "[a-zA-Z]{15,30}",
1650            password in "[a-zA-Z]{15,30}",
1651            host in "proxy\\d{1,3}\\.example\\.com",
1652        ) {
1653            let port = 8080u16;
1654            let proxy_url = format!("{}://{}:{}@{}:{}", protocol, username, password, host, port);
1655
1656            let config = VeracodeConfig::new("api_id", "api_key")
1657                .with_proxy(&proxy_url);
1658
1659            let debug_output = format!("{:?}", config);
1660
1661            // Must contain redaction
1662            assert!(debug_output.contains("[REDACTED]"));
1663
1664            // CRITICAL: Must NOT expose credentials (full username/password strings)
1665            // Note: We test that the FULL credential string is not present, avoiding
1666            // false positives from substring matches with numeric port numbers
1667            assert!(!debug_output.contains(&username));
1668            assert!(!debug_output.contains(&password));
1669
1670            // Should still show host information
1671            assert!(debug_output.contains(&host));
1672        }
1673
1674        /// Property: Debug redaction must handle UTF-8 safely
1675        #[test]
1676        fn test_config_debug_handles_utf8_safely(
1677            // Use simpler ASCII-only strings to avoid proptest UTF-8 generation issues
1678            protocol in "(http|https)",
1679            creds in "[a-zA-Z0-9]{1,30}",
1680            host in "[a-z]{3,15}\\.[a-z]{2,5}",
1681        ) {
1682            // Create URL with credentials
1683            let proxy_url = format!("{}://{}@{}", protocol, creds, host);
1684
1685            let config = VeracodeConfig::new("test", "test")
1686                .with_proxy(&proxy_url);
1687
1688            // Should never panic on UTF-8 boundaries
1689            let debug_output = format!("{:?}", config);
1690
1691            // Basic sanity check
1692            assert!(debug_output.contains("VeracodeConfig"));
1693        }
1694
1695        /// Property: Proxy auth credentials must be redacted in debug output
1696        #[test]
1697        fn test_config_debug_redacts_proxy_auth(
1698            proxy_username in "[a-zA-Z0-9]{10,50}",
1699            proxy_password in "[a-zA-Z0-9]{10,50}",
1700        ) {
1701            let config = VeracodeConfig::new("api_id", "api_key")
1702                .with_proxy("http://proxy.example.com:8080")
1703                .with_proxy_auth(proxy_username.clone(), proxy_password.clone());
1704
1705            let debug_output = format!("{:?}", config);
1706
1707            // Must redact proxy credentials
1708            assert!(debug_output.contains("proxy_username"));
1709            assert!(debug_output.contains("proxy_password"));
1710            assert!(debug_output.contains("[REDACTED]"));
1711
1712            // CRITICAL: Must NOT expose actual proxy credentials
1713            assert!(!debug_output.contains(&proxy_username));
1714            assert!(!debug_output.contains(&proxy_password));
1715        }
1716    }
1717
1718    // ========================================================================
1719    // Test 5: VeracodeConfig - Regional URL Construction Safety
1720    // ========================================================================
1721    // Security concern: URL construction could result in:
1722    // - Malformed URLs leading to requests to wrong endpoints
1723    // - Credential leakage to unintended servers
1724
1725    proptest! {
1726        #![proptest_config(ProptestConfig {
1727            cases: if cfg!(miri) { 5 } else { 100 },
1728            failure_persistence: None,
1729            .. ProptestConfig::default()
1730        })]
1731
1732        /// Property: Region configuration must produce valid, expected URLs
1733        #[test]
1734        fn test_config_region_urls_are_valid(
1735            region in prop::sample::select(vec![
1736                VeracodeRegion::Commercial,
1737                VeracodeRegion::European,
1738                VeracodeRegion::Federal,
1739            ])
1740        ) {
1741            let config = VeracodeConfig::new("test", "test")
1742                .with_region(region);
1743
1744            // Verify URLs are properly formed
1745            match region {
1746                VeracodeRegion::Commercial => {
1747                    assert_eq!(config.rest_base_url, "https://api.veracode.com");
1748                    assert_eq!(config.xml_base_url, "https://analysiscenter.veracode.com");
1749                    assert_eq!(config.base_url, config.rest_base_url);
1750                }
1751                VeracodeRegion::European => {
1752                    assert_eq!(config.rest_base_url, "https://api.veracode.eu");
1753                    assert_eq!(config.xml_base_url, "https://analysiscenter.veracode.eu");
1754                    assert_eq!(config.base_url, config.rest_base_url);
1755                }
1756                VeracodeRegion::Federal => {
1757                    assert_eq!(config.rest_base_url, "https://api.veracode.us");
1758                    assert_eq!(config.xml_base_url, "https://analysiscenter.veracode.us");
1759                    assert_eq!(config.base_url, config.rest_base_url);
1760                }
1761            }
1762
1763            // All URLs must use HTTPS for security
1764            assert!(config.rest_base_url.starts_with("https://"));
1765            assert!(config.xml_base_url.starts_with("https://"));
1766            assert!(config.base_url.starts_with("https://"));
1767        }
1768    }
1769
1770    // ========================================================================
1771    // Test 6: RetryConfig - Timeout Configuration Safety
1772    // ========================================================================
1773    // Security concern: Invalid timeout values could:
1774    // - Cause integer overflow in duration calculations
1775    // - Lead to indefinite blocking (DoS)
1776    // - Allow timing attacks
1777
1778    proptest! {
1779        #![proptest_config(ProptestConfig {
1780            cases: if cfg!(miri) { 5 } else { 500 },
1781            failure_persistence: None,
1782            .. ProptestConfig::default()
1783        })]
1784
1785        /// Property: Timeout values must be safely handled without overflow
1786        #[test]
1787        fn test_config_timeout_no_overflow(
1788            connect_timeout in 1u64..100_000u64,
1789            request_timeout in 1u64..100_000u64,
1790        ) {
1791            let config = VeracodeConfig::new("test", "test")
1792                .with_timeouts(connect_timeout, request_timeout);
1793
1794            // Should store values correctly
1795            assert_eq!(config.connect_timeout, connect_timeout);
1796            assert_eq!(config.request_timeout, request_timeout);
1797
1798            // Values should remain positive (no wrapping)
1799            assert!(config.connect_timeout > 0);
1800            assert!(config.request_timeout > 0);
1801        }
1802
1803        /// Property: Individual timeout setters work correctly
1804        #[test]
1805        fn test_config_individual_timeout_setters(
1806            connect_timeout in 1u64..50_000u64,
1807            request_timeout in 1u64..50_000u64,
1808        ) {
1809            let config1 = VeracodeConfig::new("test", "test")
1810                .with_connect_timeout(connect_timeout);
1811            assert_eq!(config1.connect_timeout, connect_timeout);
1812            assert_eq!(config1.request_timeout, 300); // Default
1813
1814            let config2 = VeracodeConfig::new("test", "test")
1815                .with_request_timeout(request_timeout);
1816            assert_eq!(config2.connect_timeout, 30); // Default
1817            assert_eq!(config2.request_timeout, request_timeout);
1818        }
1819    }
1820
1821    // ========================================================================
1822    // Test 7: Error Display - Information Disclosure Prevention
1823    // ========================================================================
1824    // Security concern: Error messages could leak:
1825    // - Internal system details
1826    // - API credentials
1827    // - Network topology information
1828
1829    proptest! {
1830        #![proptest_config(ProptestConfig {
1831            cases: if cfg!(miri) { 5 } else { 500 },
1832            failure_persistence: None,
1833            .. ProptestConfig::default()
1834        })]
1835
1836        /// Property: Error Display should not expose sensitive patterns
1837        #[test]
1838        fn test_error_display_safe_messages(
1839            message in "[a-zA-Z0-9 ]{1,100}",
1840        ) {
1841            let errors = vec![
1842                VeracodeError::Authentication(message.clone()),
1843                VeracodeError::InvalidResponse(message.clone()),
1844                VeracodeError::InvalidConfig(message.clone()),
1845                VeracodeError::NotFound(message.clone()),
1846                VeracodeError::RetryExhausted(message.clone()),
1847            ];
1848
1849            for error in errors {
1850                let display = format!("{}", error);
1851
1852                // Should contain the error type
1853                assert!(!display.is_empty());
1854
1855                // Should contain the message we provided
1856                assert!(display.contains(&message));
1857            }
1858        }
1859
1860        /// Property: RateLimited error display handles retry_after safely
1861        #[test]
1862        fn test_rate_limited_error_display_safe(
1863            retry_after in prop::option::of(0u64..10_000u64),
1864            message in "[a-zA-Z0-9 ]{1,100}",
1865        ) {
1866            let error = VeracodeError::RateLimited {
1867                retry_after_seconds: retry_after,
1868                message: message.clone(),
1869            };
1870
1871            let display = format!("{}", error);
1872
1873            // Should contain core message
1874            assert!(display.contains(&message));
1875            assert!(display.contains("Rate limit exceeded"));
1876
1877            // If retry_after is present, should be in output
1878            if let Some(seconds) = retry_after {
1879                assert!(display.contains(&seconds.to_string()));
1880            }
1881        }
1882    }
1883}
1884
1885// ============================================================================
1886// KANI FORMAL VERIFICATION PROOFS
1887// ============================================================================
1888// These proofs use bounded model checking to formally verify security
1889// properties for critical arithmetic and memory operations.
1890
1891#[cfg(kani)]
1892mod kani_proofs {
1893    use super::*;
1894
1895    // ========================================================================
1896    // Proof 1: RetryConfig::calculate_delay() - Arithmetic Safety
1897    // ========================================================================
1898    // This proof formally verifies that the exponential backoff calculation
1899    // never overflows, never panics, and always produces values within bounds.
1900
1901    #[kani::proof]
1902    #[kani::unwind(10)] // Limit loop iterations for verification
1903    fn verify_calculate_delay_arithmetic_safety() {
1904        // Create arbitrary inputs
1905        let initial_delay: u64 = kani::any();
1906        let max_delay: u64 = kani::any();
1907        let multiplier: f64 = kani::any();
1908        let attempt: u32 = kani::any();
1909
1910        // Constrain inputs to realistic ranges
1911        kani::assume(initial_delay > 0);
1912        kani::assume(initial_delay <= 100_000);
1913        kani::assume(max_delay > 0);
1914        kani::assume(max_delay >= initial_delay);
1915        kani::assume(max_delay <= 1_000_000);
1916        kani::assume(multiplier >= 1.0);
1917        kani::assume(multiplier <= 10.0);
1918        kani::assume(multiplier.is_finite());
1919        kani::assume(attempt < 20); // Reasonable attempt limit
1920
1921        let config = RetryConfig::new()
1922            .with_initial_delay(initial_delay)
1923            .with_backoff_multiplier(multiplier)
1924            .with_max_delay(max_delay)
1925            .with_jitter_disabled();
1926
1927        // Calculate delay - should never panic
1928        let delay = config.calculate_delay(attempt);
1929
1930        // PROPERTY 1: Delay must never exceed max_delay
1931        assert!(delay.as_millis() <= max_delay as u128);
1932
1933        // PROPERTY 2: Delay must be non-negative (always true for Duration)
1934        assert!(delay.as_secs() < u64::MAX);
1935
1936        // PROPERTY 3: For attempt 0, delay should be 0
1937        if attempt == 0 {
1938            assert_eq!(delay.as_millis(), 0);
1939        }
1940    }
1941
1942    // ========================================================================
1943    // Proof 2: RetryConfig::calculate_rate_limit_delay() - Time Arithmetic
1944    // ========================================================================
1945    // This proof verifies that rate limit delay calculations handle all
1946    // possible inputs without overflow or panic.
1947    // Note: This proof tests the arithmetic logic only, avoiding SystemTime::now()
1948    // which calls unsupported FFI function clock_gettime.
1949
1950    #[kani::proof]
1951    fn verify_rate_limit_delay_safety() {
1952        let buffer_seconds: u64 = kani::any();
1953        let retry_after_seconds: Option<u64> = kani::any();
1954
1955        // Constrain to realistic values
1956        kani::assume(buffer_seconds <= 10_000);
1957        if let Some(secs) = retry_after_seconds {
1958            kani::assume(secs <= 100_000);
1959        }
1960
1961        // TEST 1: Verify arithmetic when retry_after is provided (direct Duration creation)
1962        if let Some(expected_secs) = retry_after_seconds {
1963            let delay = Duration::from_secs(expected_secs);
1964            // PROPERTY 1: Delay should match the provided value exactly
1965            assert_eq!(delay.as_secs(), expected_secs);
1966        }
1967
1968        // TEST 2: Verify arithmetic for minute window calculation (without calling SystemTime)
1969        // Simulate what calculate_rate_limit_delay does internally
1970        let current_second: u64 = kani::any();
1971        kani::assume(current_second < 60);
1972
1973        // This is the core arithmetic from calculate_rate_limit_delay
1974        let seconds_until_next_minute = 60_u64.saturating_sub(current_second);
1975        let total_delay = seconds_until_next_minute.saturating_add(buffer_seconds);
1976
1977        // PROPERTY 2: Delay should never be less than buffer
1978        assert!(total_delay >= buffer_seconds);
1979
1980        // PROPERTY 3: Delay should never exceed 60 seconds + buffer
1981        assert!(total_delay <= 60 + buffer_seconds);
1982
1983        // PROPERTY 4: saturating_sub and saturating_add never panic
1984        // (Proven by successful execution of the operations above)
1985    }
1986
1987    // ========================================================================
1988    // Proof 3: VeracodeCredentials - Memory Safety of Arc Operations
1989    // ========================================================================
1990    // This proof verifies that Arc cloning and access operations are memory-safe
1991    // and don't introduce use-after-free or double-free bugs.
1992    // Note: Increased unwind limit to accommodate string comparison loops in memcmp.
1993
1994    #[kani::proof]
1995    #[kani::unwind(50)]
1996    fn verify_credentials_arc_memory_safety() {
1997        // Create test credentials with fixed strings to avoid excessive state space exploration
1998        let api_id = "test_api_id".to_string();
1999        let api_key = "test_key123".to_string();
2000
2001        let credentials = VeracodeCredentials::new(api_id.clone(), api_key.clone());
2002
2003        // PROPERTY 1: Arc pointers from multiple calls should point to the same allocation
2004        let api_id_arc1 = credentials.api_id_ptr();
2005        let api_id_arc2 = credentials.api_id_ptr();
2006
2007        // Verify Arc pointers reference the same memory location
2008        assert_eq!(
2009            Arc::as_ptr(&api_id_arc1) as *const (),
2010            Arc::as_ptr(&api_id_arc2) as *const ()
2011        );
2012
2013        // PROPERTY 2: Dereferencing Arc pointers should yield same values
2014        assert_eq!(api_id_arc1.expose_secret(), api_id_arc2.expose_secret());
2015
2016        // PROPERTY 3: Expose methods should return original values
2017        assert_eq!(credentials.expose_api_id(), &api_id);
2018        assert_eq!(credentials.expose_api_key(), &api_key);
2019
2020        // PROPERTY 4: Cloning credentials should preserve values and maintain memory safety
2021        let cloned = credentials.clone();
2022        assert_eq!(cloned.expose_api_id(), credentials.expose_api_id());
2023        assert_eq!(cloned.expose_api_key(), credentials.expose_api_key());
2024
2025        // PROPERTY 5: Multiple clones should not cause memory corruption
2026        let cloned2 = cloned.clone();
2027        let _ = cloned2.expose_api_id();
2028        let _ = cloned2.expose_api_key();
2029
2030        // If we reach here without panic or memory error, Arc operations are safe
2031    }
2032
2033    // ========================================================================
2034    // Proof 4: VeracodeConfig - URL String Slicing Safety
2035    // ========================================================================
2036    // This proof verifies that the proxy URL redaction logic handles
2037    // string slicing safely without panicking on UTF-8 boundaries.
2038    // Optimized to avoid expensive format! operations.
2039
2040    #[kani::proof]
2041    fn verify_proxy_url_redaction_safety() {
2042        // Test only the specific string operation: finding '@' and slicing
2043        let has_at_sign: bool = kani::any();
2044
2045        // Use minimal test strings to reduce state space
2046        let url = if has_at_sign {
2047            "u:p@h".to_string()
2048        } else {
2049            "http://h".to_string()
2050        };
2051
2052        // PROPERTY 1: Creating config with proxy should never panic
2053        let config = VeracodeConfig::new("t", "k").with_proxy(&url);
2054
2055        // PROPERTY 2: Config should be created successfully
2056        assert!(config.proxy_url.is_some());
2057
2058        // PROPERTY 3: The redaction logic (tested indirectly via storage)
2059        // If we reach here without panic, string slicing is safe
2060        let _ = config.proxy_url;
2061    }
2062
2063    // ========================================================================
2064    // Proof 5: VeracodeConfig - Region URL Construction Correctness
2065    // ========================================================================
2066    // This proof verifies that regional URL construction produces valid,
2067    // expected URLs for all region variants.
2068    // Optimized to avoid expensive string equality comparisons.
2069
2070    #[kani::proof]
2071    #[kani::unwind(10)] // Limit loop unwinding to reduce complexity
2072    fn verify_region_url_construction() {
2073        // ULTRA-SIMPLIFIED PROOF: Test only one region to avoid OOM
2074        // This proof verifies that URL construction doesn't panic and produces valid output
2075        // Testing all 3 regions causes CBMC to run out of memory due to string operations
2076
2077        // Test: Commercial region - verify basic invariants
2078        let config = VeracodeConfig::new("a", "b").with_region(VeracodeRegion::Commercial);
2079
2080        // PROPERTY 1: URLs are non-empty (most critical safety property)
2081        assert!(!config.rest_base_url.is_empty());
2082        assert!(!config.xml_base_url.is_empty());
2083
2084        // PROPERTY 2: URLs are bounded in size (prevent unbounded growth)
2085        assert!(config.rest_base_url.len() < 100);
2086        assert!(config.xml_base_url.len() < 100);
2087
2088        // PROPERTY 3: Region is set correctly
2089        assert!(matches!(config.region, VeracodeRegion::Commercial));
2090    }
2091
2092    // ========================================================================
2093    // Proof 6: VeracodeConfig - European Region URL Construction
2094    // ========================================================================
2095    // Separate proof for European region to maintain verification performance
2096
2097    #[kani::proof]
2098    #[kani::unwind(10)]
2099    fn verify_european_region_url_construction() {
2100        let config = VeracodeConfig::new("a", "b").with_region(VeracodeRegion::European);
2101
2102        // PROPERTY 1: URLs are non-empty
2103        assert!(!config.rest_base_url.is_empty());
2104        assert!(!config.xml_base_url.is_empty());
2105
2106        // PROPERTY 2: URLs are bounded in size
2107        assert!(config.rest_base_url.len() < 100);
2108        assert!(config.xml_base_url.len() < 100);
2109
2110        // PROPERTY 3: Region is set correctly
2111        assert!(matches!(config.region, VeracodeRegion::European));
2112    }
2113
2114    // ========================================================================
2115    // Proof 7: VeracodeConfig - Federal Region URL Construction
2116    // ========================================================================
2117    // Separate proof for Federal region to maintain verification performance
2118
2119    #[kani::proof]
2120    #[kani::unwind(10)]
2121    fn verify_federal_region_url_construction() {
2122        let config = VeracodeConfig::new("a", "b").with_region(VeracodeRegion::Federal);
2123
2124        // PROPERTY 1: URLs are non-empty
2125        assert!(!config.rest_base_url.is_empty());
2126        assert!(!config.xml_base_url.is_empty());
2127
2128        // PROPERTY 2: URLs are bounded in size
2129        assert!(config.rest_base_url.len() < 100);
2130        assert!(config.xml_base_url.len() < 100);
2131
2132        // PROPERTY 3: Region is set correctly
2133        assert!(matches!(config.region, VeracodeRegion::Federal));
2134    }
2135}