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