#![cfg_attr(test, allow(clippy::expect_used))]
pub mod app;
pub mod build;
pub mod client;
pub mod findings;
pub mod identity;
pub mod json_validator;
pub mod pipeline;
pub mod policy;
pub mod reporting;
pub mod sandbox;
pub mod scan;
pub mod validation;
pub mod workflow;
use reqwest::Error as ReqwestError;
use secrecy::{ExposeSecret, SecretString};
use std::fmt;
use std::sync::Arc;
use std::time::Duration;
pub use app::{
Application, ApplicationQuery, ApplicationsResponse, CreateApplicationRequest,
UpdateApplicationRequest,
};
pub use build::{
Build, BuildApi, BuildError, BuildList, CreateBuildRequest, DeleteBuildRequest,
DeleteBuildResult, GetBuildInfoRequest, GetBuildListRequest, UpdateBuildRequest,
};
pub use client::VeracodeClient;
pub use findings::{
CweInfo, FindingCategory, FindingDetails, FindingStatus, FindingsApi, FindingsError,
FindingsQuery, FindingsResponse, RestFinding,
};
pub use identity::{
ApiCredential, BusinessUnit, CreateApiCredentialRequest, CreateTeamRequest, CreateUserRequest,
IdentityApi, IdentityError, Role, Team, UpdateTeamRequest, UpdateUserRequest, User, UserQuery,
UserType,
};
pub use json_validator::{MAX_JSON_DEPTH, validate_json_depth};
pub use pipeline::{
CreateScanRequest, DevStage, Finding, FindingsSummary, PipelineApi, PipelineError, Scan,
ScanConfig, ScanResults, ScanStage, ScanStatus, SecurityStandards, Severity,
};
pub use policy::{
ApiSource, PolicyApi, PolicyComplianceResult, PolicyComplianceStatus, PolicyError, PolicyRule,
PolicyScanRequest, PolicyScanResult, PolicyThresholds, ScanType, SecurityPolicy, SummaryReport,
};
pub use reporting::{AuditReportRequest, GenerateReportResponse, ReportingApi, ReportingError};
pub use sandbox::{
ApiError, ApiErrorResponse, CreateSandboxRequest, Sandbox, SandboxApi, SandboxError,
SandboxListParams, SandboxScan, UpdateSandboxRequest,
};
pub use scan::{
BeginPreScanRequest, BeginScanRequest, PreScanMessage, PreScanResults, ScanApi, ScanError,
ScanInfo, ScanModule, UploadFileRequest, UploadLargeFileRequest, UploadProgress,
UploadProgressCallback, UploadedFile,
};
pub use validation::{
AppGuid, AppName, DEFAULT_PAGE_SIZE, Description, MAX_APP_NAME_LEN, MAX_DESCRIPTION_LEN,
MAX_GUID_LEN, MAX_PAGE_NUMBER, MAX_PAGE_SIZE, ValidationError, validate_url_segment,
};
pub use workflow::{VeracodeWorkflow, WorkflowConfig, WorkflowError, WorkflowResultData};
#[derive(Debug, Clone)]
pub struct RetryConfig {
pub max_attempts: u32,
pub initial_delay_ms: u64,
pub max_delay_ms: u64,
pub backoff_multiplier: f64,
pub max_total_delay_ms: u64,
pub rate_limit_buffer_seconds: u64,
pub rate_limit_max_attempts: u32,
pub jitter_enabled: bool,
}
impl Default for RetryConfig {
fn default() -> Self {
Self {
max_attempts: 5,
initial_delay_ms: 1000,
max_delay_ms: 30000,
backoff_multiplier: 2.0,
max_total_delay_ms: 300_000, rate_limit_buffer_seconds: 5, rate_limit_max_attempts: 1, jitter_enabled: true, }
}
}
impl RetryConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_max_attempts(mut self, max_attempts: u32) -> Self {
self.max_attempts = max_attempts;
self
}
#[must_use]
pub fn with_initial_delay(mut self, delay_ms: u64) -> Self {
self.initial_delay_ms = delay_ms;
self
}
#[must_use]
pub fn with_initial_delay_millis(mut self, delay_ms: u64) -> Self {
self.initial_delay_ms = delay_ms;
self
}
#[must_use]
pub fn with_max_delay(mut self, delay_ms: u64) -> Self {
self.max_delay_ms = delay_ms;
self
}
#[must_use]
pub fn with_max_delay_millis(mut self, delay_ms: u64) -> Self {
self.max_delay_ms = delay_ms;
self
}
#[must_use]
pub fn with_backoff_multiplier(mut self, multiplier: f64) -> Self {
self.backoff_multiplier = multiplier;
self
}
#[must_use]
pub fn with_exponential_backoff(mut self, multiplier: f64) -> Self {
self.backoff_multiplier = multiplier;
self
}
#[must_use]
pub fn with_max_total_delay(mut self, delay_ms: u64) -> Self {
self.max_total_delay_ms = delay_ms;
self
}
#[must_use]
pub fn with_rate_limit_buffer(mut self, buffer_seconds: u64) -> Self {
self.rate_limit_buffer_seconds = buffer_seconds;
self
}
#[must_use]
pub fn with_rate_limit_max_attempts(mut self, max_attempts: u32) -> Self {
self.rate_limit_max_attempts = max_attempts;
self
}
#[must_use]
pub fn with_jitter_disabled(mut self) -> Self {
self.jitter_enabled = false;
self
}
#[must_use]
pub fn calculate_delay(&self, attempt: u32) -> Duration {
if attempt == 0 {
return Duration::from_millis(0);
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss,
clippy::cast_possible_wrap
)]
let delay_ms = (self.initial_delay_ms as f64
* self
.backoff_multiplier
.powi(attempt.saturating_sub(1) as i32))
.round() as u64;
let mut capped_delay = delay_ms.min(self.max_delay_ms);
if self.jitter_enabled {
use rand::RngExt;
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss
)]
let jitter_range = (capped_delay as f64 * 0.25).round() as u64;
let min_delay = capped_delay.saturating_sub(jitter_range);
let max_delay = capped_delay.saturating_add(jitter_range);
capped_delay = rand::rng().random_range(min_delay..=max_delay);
}
Duration::from_millis(capped_delay)
}
#[must_use]
pub fn calculate_rate_limit_delay(&self, retry_after_seconds: Option<u64>) -> Duration {
if let Some(seconds) = retry_after_seconds {
Duration::from_secs(seconds)
} else {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default();
let current_second = now.as_secs() % 60;
let seconds_until_next_minute = 60_u64.saturating_sub(current_second);
Duration::from_secs(
seconds_until_next_minute.saturating_add(self.rate_limit_buffer_seconds),
)
}
}
#[must_use]
pub fn is_retryable_error(&self, error: &VeracodeError) -> bool {
match error {
VeracodeError::Http(reqwest_error) => {
if reqwest_error.is_timeout()
|| reqwest_error.is_connect()
|| reqwest_error.is_request()
{
return true;
}
if let Some(status) = reqwest_error.status() {
match status.as_u16() {
429 => true,
502..=504 => true,
500..=599 => true,
_ => false,
}
} else {
true
}
}
VeracodeError::Authentication(_)
| VeracodeError::Serialization(_)
| VeracodeError::Validation(_)
| VeracodeError::InvalidConfig(_) => false,
VeracodeError::InvalidResponse(_) => true,
VeracodeError::HttpStatus { status_code, .. } => match status_code {
429 => true,
502..=504 => true,
500..=599 => true,
400..=499 => false,
_ => false,
},
VeracodeError::NotFound(_) => false,
VeracodeError::RetryExhausted(_) => false,
VeracodeError::RateLimited { .. } => true,
}
}
}
#[derive(Debug)]
#[must_use = "Need to handle all error enum types."]
pub enum VeracodeError {
Http(ReqwestError),
Serialization(serde_json::Error),
Authentication(String),
InvalidResponse(String),
HttpStatus {
status_code: u16,
url: String,
message: String,
},
InvalidConfig(String),
NotFound(String),
RetryExhausted(String),
RateLimited {
retry_after_seconds: Option<u64>,
message: String,
},
Validation(validation::ValidationError),
}
impl VeracodeClient {
#[must_use]
pub fn applications_api(&self) -> &Self {
self
}
#[must_use]
pub fn sandbox_api(&self) -> SandboxApi<'_> {
SandboxApi::new(self)
}
#[must_use]
pub fn identity_api(&self) -> IdentityApi<'_> {
IdentityApi::new(self)
}
#[must_use]
pub fn pipeline_api(&self) -> PipelineApi {
PipelineApi::new(self.clone())
}
#[must_use]
pub fn policy_api(&self) -> PolicyApi<'_> {
PolicyApi::new(self)
}
#[must_use]
pub fn findings_api(&self) -> FindingsApi {
FindingsApi::new(self.clone())
}
#[must_use]
pub fn reporting_api(&self) -> reporting::ReportingApi {
reporting::ReportingApi::new(self.clone())
}
pub fn scan_api(&self) -> Result<ScanApi, VeracodeError> {
Ok(ScanApi::new(self.new_xml_variant()))
}
pub fn build_api(&self) -> Result<build::BuildApi, VeracodeError> {
Ok(build::BuildApi::new(self.new_xml_variant()))
}
#[must_use]
pub fn workflow(&self) -> workflow::VeracodeWorkflow {
workflow::VeracodeWorkflow::new(self.clone())
}
}
impl fmt::Display for VeracodeError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
VeracodeError::Http(e) => write!(f, "HTTP error: {e}"),
VeracodeError::Serialization(e) => write!(f, "Serialization error: {e}"),
VeracodeError::Authentication(e) => write!(f, "Authentication error: {e}"),
VeracodeError::InvalidResponse(e) => write!(f, "Invalid response: {e}"),
VeracodeError::HttpStatus {
status_code,
url,
message,
} => write!(f, "HTTP {status_code} error at {url}: {message}"),
VeracodeError::InvalidConfig(e) => write!(f, "Invalid configuration: {e}"),
VeracodeError::NotFound(e) => write!(f, "Item not found: {e}"),
VeracodeError::RetryExhausted(e) => write!(f, "Retry attempts exhausted: {e}"),
VeracodeError::RateLimited {
retry_after_seconds,
message,
} => match retry_after_seconds {
Some(seconds) => {
write!(f, "Rate limit exceeded: {message} (retry after {seconds}s)")
}
None => write!(f, "Rate limit exceeded: {message}"),
},
VeracodeError::Validation(e) => write!(f, "Validation error: {e}"),
}
}
}
impl std::error::Error for VeracodeError {}
impl From<ReqwestError> for VeracodeError {
fn from(error: ReqwestError) -> Self {
VeracodeError::Http(error)
}
}
impl From<serde_json::Error> for VeracodeError {
fn from(error: serde_json::Error) -> Self {
VeracodeError::Serialization(error)
}
}
impl From<validation::ValidationError> for VeracodeError {
fn from(error: validation::ValidationError) -> Self {
VeracodeError::Validation(error)
}
}
#[derive(Clone)]
pub struct VeracodeCredentials {
api_id: Arc<SecretString>,
api_key: Arc<SecretString>,
}
impl VeracodeCredentials {
#[must_use]
pub fn new(api_id: String, api_key: String) -> Self {
Self {
api_id: Arc::new(SecretString::new(api_id.into())),
api_key: Arc::new(SecretString::new(api_key.into())),
}
}
#[must_use]
pub fn api_id_ptr(&self) -> Arc<SecretString> {
Arc::clone(&self.api_id)
}
#[must_use]
pub fn api_key_ptr(&self) -> Arc<SecretString> {
Arc::clone(&self.api_key)
}
#[must_use]
pub fn expose_api_id(&self) -> &str {
self.api_id.expose_secret()
}
#[must_use]
pub fn expose_api_key(&self) -> &str {
self.api_key.expose_secret()
}
}
impl std::fmt::Debug for VeracodeCredentials {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("VeracodeCredentials")
.field("api_id", &"[REDACTED]")
.field("api_key", &"[REDACTED]")
.finish()
}
}
#[derive(Clone)]
pub struct VeracodeConfig {
pub credentials: VeracodeCredentials,
pub base_url: String,
pub rest_base_url: String,
pub xml_base_url: String,
pub region: VeracodeRegion,
pub validate_certificates: bool,
pub retry_config: RetryConfig,
pub connect_timeout: u64,
pub request_timeout: u64,
pub proxy_url: Option<String>,
pub proxy_username: Option<SecretString>,
pub proxy_password: Option<SecretString>,
}
impl std::fmt::Debug for VeracodeConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let proxy_url_redacted = self.proxy_url.as_ref().map(|url| {
if url.contains('@') {
if let Some(at_pos) = url.rfind('@') {
if let Some(proto_end) = url.find("://") {
let protocol = url.get(..proto_end).unwrap_or("");
let host_part = url.get(at_pos.saturating_add(1)..).unwrap_or("");
format!("{}://[REDACTED]@{}", protocol, host_part)
} else {
"[REDACTED]".to_string()
}
} else {
"[REDACTED]".to_string()
}
} else {
url.clone()
}
});
f.debug_struct("VeracodeConfig")
.field("credentials", &self.credentials)
.field("base_url", &self.base_url)
.field("rest_base_url", &self.rest_base_url)
.field("xml_base_url", &self.xml_base_url)
.field("region", &self.region)
.field("validate_certificates", &self.validate_certificates)
.field("retry_config", &self.retry_config)
.field("connect_timeout", &self.connect_timeout)
.field("request_timeout", &self.request_timeout)
.field("proxy_url", &proxy_url_redacted)
.field(
"proxy_username",
&self.proxy_username.as_ref().map(|_| "[REDACTED]"),
)
.field(
"proxy_password",
&self.proxy_password.as_ref().map(|_| "[REDACTED]"),
)
.finish()
}
}
const COMMERCIAL_REST_URL: &str = "https://api.veracode.com";
const COMMERCIAL_XML_URL: &str = "https://analysiscenter.veracode.com";
const EUROPEAN_REST_URL: &str = "https://api.veracode.eu";
const EUROPEAN_XML_URL: &str = "https://analysiscenter.veracode.eu";
const FEDERAL_REST_URL: &str = "https://api.veracode.us";
const FEDERAL_XML_URL: &str = "https://analysiscenter.veracode.us";
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum VeracodeRegion {
Commercial,
European,
Federal,
}
impl VeracodeConfig {
#[must_use]
pub fn new(api_id: &str, api_key: &str) -> Self {
let credentials = VeracodeCredentials::new(api_id.to_string(), api_key.to_string());
Self {
credentials,
base_url: COMMERCIAL_REST_URL.to_string(),
rest_base_url: COMMERCIAL_REST_URL.to_string(),
xml_base_url: COMMERCIAL_XML_URL.to_string(),
region: VeracodeRegion::Commercial,
validate_certificates: true, retry_config: RetryConfig::default(),
connect_timeout: 30, request_timeout: 300, proxy_url: None,
proxy_username: None,
proxy_password: None,
}
}
#[must_use]
pub fn from_arc_credentials(api_id: Arc<SecretString>, api_key: Arc<SecretString>) -> Self {
let credentials = VeracodeCredentials { api_id, api_key };
Self {
credentials,
base_url: COMMERCIAL_REST_URL.to_string(),
rest_base_url: COMMERCIAL_REST_URL.to_string(),
xml_base_url: COMMERCIAL_XML_URL.to_string(),
region: VeracodeRegion::Commercial,
validate_certificates: true,
retry_config: RetryConfig::default(),
connect_timeout: 30,
request_timeout: 300,
proxy_url: None,
proxy_username: None,
proxy_password: None,
}
}
#[must_use]
pub fn with_region(mut self, region: VeracodeRegion) -> Self {
let (rest_url, xml_url) = match region {
VeracodeRegion::Commercial => (COMMERCIAL_REST_URL, COMMERCIAL_XML_URL),
VeracodeRegion::European => (EUROPEAN_REST_URL, EUROPEAN_XML_URL),
VeracodeRegion::Federal => (FEDERAL_REST_URL, FEDERAL_XML_URL),
};
self.region = region;
self.rest_base_url = rest_url.to_string();
self.xml_base_url = xml_url.to_string();
self.base_url = self.rest_base_url.clone(); self
}
#[must_use]
pub fn with_certificate_validation_disabled(mut self) -> Self {
self.validate_certificates = false;
self
}
#[must_use]
pub fn with_retry_config(mut self, retry_config: RetryConfig) -> Self {
self.retry_config = retry_config;
self
}
#[must_use]
pub fn with_retries_disabled(mut self) -> Self {
self.retry_config = RetryConfig::new().with_max_attempts(0);
self
}
#[must_use]
pub fn with_connect_timeout(mut self, timeout_seconds: u64) -> Self {
self.connect_timeout = timeout_seconds;
self
}
#[must_use]
pub fn with_request_timeout(mut self, timeout_seconds: u64) -> Self {
self.request_timeout = timeout_seconds;
self
}
#[must_use]
pub fn with_timeouts(
mut self,
connect_timeout_seconds: u64,
request_timeout_seconds: u64,
) -> Self {
self.connect_timeout = connect_timeout_seconds;
self.request_timeout = request_timeout_seconds;
self
}
#[must_use]
pub fn api_id_arc(&self) -> Arc<SecretString> {
self.credentials.api_id_ptr()
}
#[must_use]
pub fn api_key_arc(&self) -> Arc<SecretString> {
self.credentials.api_key_ptr()
}
#[must_use]
pub fn with_proxy(mut self, proxy_url: impl Into<String>) -> Self {
self.proxy_url = Some(proxy_url.into());
self
}
#[must_use]
pub fn with_proxy_auth(
mut self,
username: impl Into<String>,
password: impl Into<String>,
) -> Self {
self.proxy_username = Some(SecretString::new(username.into().into()));
self.proxy_password = Some(SecretString::new(password.into().into()));
self
}
}
#[cfg(test)]
#[allow(clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn test_config_creation() {
let config = VeracodeConfig::new("test_api_id", "test_api_key");
assert_eq!(config.credentials.expose_api_id(), "test_api_id");
assert_eq!(config.credentials.expose_api_key(), "test_api_key");
assert_eq!(config.base_url, "https://api.veracode.com");
assert_eq!(config.rest_base_url, "https://api.veracode.com");
assert_eq!(config.xml_base_url, "https://analysiscenter.veracode.com");
assert_eq!(config.region, VeracodeRegion::Commercial);
assert!(config.validate_certificates); assert_eq!(config.retry_config.max_attempts, 5); }
#[test]
fn test_european_region_config() {
let config = VeracodeConfig::new("test_api_id", "test_api_key")
.with_region(VeracodeRegion::European);
assert_eq!(config.base_url, "https://api.veracode.eu");
assert_eq!(config.rest_base_url, "https://api.veracode.eu");
assert_eq!(config.xml_base_url, "https://analysiscenter.veracode.eu");
assert_eq!(config.region, VeracodeRegion::European);
}
#[test]
fn test_federal_region_config() {
let config =
VeracodeConfig::new("test_api_id", "test_api_key").with_region(VeracodeRegion::Federal);
assert_eq!(config.base_url, "https://api.veracode.us");
assert_eq!(config.rest_base_url, "https://api.veracode.us");
assert_eq!(config.xml_base_url, "https://analysiscenter.veracode.us");
assert_eq!(config.region, VeracodeRegion::Federal);
}
#[test]
fn test_certificate_validation_disabled() {
let config = VeracodeConfig::new("test_api_id", "test_api_key")
.with_certificate_validation_disabled();
assert!(!config.validate_certificates);
}
#[test]
fn test_veracode_credentials_debug_redaction() {
let credentials = VeracodeCredentials::new(
"test_api_id_123".to_string(),
"test_api_key_456".to_string(),
);
let debug_output = format!("{credentials:?}");
assert!(debug_output.contains("VeracodeCredentials"));
assert!(debug_output.contains("[REDACTED]"));
assert!(!debug_output.contains("test_api_id_123"));
assert!(!debug_output.contains("test_api_key_456"));
}
#[test]
fn test_veracode_config_debug_redaction() {
let config = VeracodeConfig::new("test_api_id_123", "test_api_key_456");
let debug_output = format!("{config:?}");
assert!(debug_output.contains("VeracodeConfig"));
assert!(debug_output.contains("credentials"));
assert!(debug_output.contains("[REDACTED]"));
assert!(!debug_output.contains("test_api_id_123"));
assert!(!debug_output.contains("test_api_key_456"));
}
#[test]
fn test_veracode_credentials_access_methods() {
let credentials = VeracodeCredentials::new(
"test_api_id_123".to_string(),
"test_api_key_456".to_string(),
);
assert_eq!(credentials.expose_api_id(), "test_api_id_123");
assert_eq!(credentials.expose_api_key(), "test_api_key_456");
}
#[test]
fn test_veracode_credentials_arc_pointers() {
let credentials = VeracodeCredentials::new(
"test_api_id_123".to_string(),
"test_api_key_456".to_string(),
);
let api_id_arc = credentials.api_id_ptr();
let api_key_arc = credentials.api_key_ptr();
assert_eq!(api_id_arc.expose_secret(), "test_api_id_123");
assert_eq!(api_key_arc.expose_secret(), "test_api_key_456");
}
#[test]
fn test_veracode_credentials_clone() {
let credentials = VeracodeCredentials::new(
"test_api_id_123".to_string(),
"test_api_key_456".to_string(),
);
let cloned_credentials = credentials.clone();
assert_eq!(
credentials.expose_api_id(),
cloned_credentials.expose_api_id()
);
assert_eq!(
credentials.expose_api_key(),
cloned_credentials.expose_api_key()
);
}
#[test]
fn test_config_with_arc_credentials() {
use secrecy::SecretString;
use std::sync::Arc;
let api_id_arc = Arc::new(SecretString::new("test_api_id".into()));
let api_key_arc = Arc::new(SecretString::new("test_api_key".into()));
let config = VeracodeConfig::from_arc_credentials(api_id_arc, api_key_arc);
assert_eq!(config.credentials.expose_api_id(), "test_api_id");
assert_eq!(config.credentials.expose_api_key(), "test_api_key");
assert_eq!(config.region, VeracodeRegion::Commercial);
}
#[test]
fn test_error_display() {
let error = VeracodeError::Authentication("Invalid API key".to_string());
assert_eq!(format!("{error}"), "Authentication error: Invalid API key");
}
#[test]
fn test_error_from_reqwest() {
fn _test_conversion(error: reqwest::Error) -> VeracodeError {
VeracodeError::from(error)
}
}
#[test]
fn test_retry_config_default() {
let config = RetryConfig::default();
assert_eq!(config.max_attempts, 5);
assert_eq!(config.initial_delay_ms, 1000);
assert_eq!(config.max_delay_ms, 30000);
assert_eq!(config.backoff_multiplier, 2.0);
assert_eq!(config.max_total_delay_ms, 300000);
assert!(config.jitter_enabled); }
#[test]
fn test_retry_config_builder() {
let config = RetryConfig::new()
.with_max_attempts(5)
.with_initial_delay(500)
.with_max_delay(60000)
.with_backoff_multiplier(1.5)
.with_max_total_delay(600000);
assert_eq!(config.max_attempts, 5);
assert_eq!(config.initial_delay_ms, 500);
assert_eq!(config.max_delay_ms, 60000);
assert_eq!(config.backoff_multiplier, 1.5);
assert_eq!(config.max_total_delay_ms, 600000);
}
#[test]
fn test_retry_config_calculate_delay() {
let config = RetryConfig::new()
.with_initial_delay(1000)
.with_backoff_multiplier(2.0)
.with_max_delay(10000)
.with_jitter_disabled();
assert_eq!(config.calculate_delay(0).as_millis(), 0); assert_eq!(config.calculate_delay(1).as_millis(), 1000); assert_eq!(config.calculate_delay(2).as_millis(), 2000); assert_eq!(config.calculate_delay(3).as_millis(), 4000); assert_eq!(config.calculate_delay(4).as_millis(), 8000); assert_eq!(config.calculate_delay(5).as_millis(), 10000); }
#[test]
fn test_retry_config_is_retryable_error() {
let config = RetryConfig::new();
assert!(
config.is_retryable_error(&VeracodeError::InvalidResponse("temp error".to_string()))
);
assert!(!config.is_retryable_error(&VeracodeError::Authentication("bad auth".to_string())));
assert!(!config.is_retryable_error(&VeracodeError::Serialization(
serde_json::from_str::<i32>("invalid").expect_err("should fail to deserialize")
)));
assert!(
!config.is_retryable_error(&VeracodeError::InvalidConfig("bad config".to_string()))
);
assert!(!config.is_retryable_error(&VeracodeError::NotFound("not found".to_string())));
assert!(
!config.is_retryable_error(&VeracodeError::RetryExhausted("exhausted".to_string()))
);
}
#[test]
fn test_veracode_config_with_retry_config() {
let retry_config = RetryConfig::new().with_max_attempts(5);
let config =
VeracodeConfig::new("test_api_id", "test_api_key").with_retry_config(retry_config);
assert_eq!(config.retry_config.max_attempts, 5);
}
#[test]
fn test_veracode_config_with_retries_disabled() {
let config = VeracodeConfig::new("test_api_id", "test_api_key").with_retries_disabled();
assert_eq!(config.retry_config.max_attempts, 0);
}
#[test]
fn test_timeout_configuration() {
let config = VeracodeConfig::new("test_api_id", "test_api_key");
assert_eq!(config.connect_timeout, 30);
assert_eq!(config.request_timeout, 300);
}
#[test]
fn test_with_connect_timeout() {
let config = VeracodeConfig::new("test_api_id", "test_api_key").with_connect_timeout(60);
assert_eq!(config.connect_timeout, 60);
assert_eq!(config.request_timeout, 300); }
#[test]
fn test_with_request_timeout() {
let config = VeracodeConfig::new("test_api_id", "test_api_key").with_request_timeout(600);
assert_eq!(config.connect_timeout, 30); assert_eq!(config.request_timeout, 600);
}
#[test]
fn test_with_timeouts() {
let config = VeracodeConfig::new("test_api_id", "test_api_key").with_timeouts(120, 1800);
assert_eq!(config.connect_timeout, 120);
assert_eq!(config.request_timeout, 1800);
}
#[test]
fn test_timeout_configuration_chaining() {
let config = VeracodeConfig::new("test_api_id", "test_api_key")
.with_region(VeracodeRegion::European)
.with_connect_timeout(45)
.with_request_timeout(900)
.with_retries_disabled();
assert_eq!(config.region, VeracodeRegion::European);
assert_eq!(config.connect_timeout, 45);
assert_eq!(config.request_timeout, 900);
assert_eq!(config.retry_config.max_attempts, 0);
}
#[test]
fn test_retry_exhausted_error_display() {
let error = VeracodeError::RetryExhausted("Failed after 3 attempts".to_string());
assert_eq!(
format!("{error}"),
"Retry attempts exhausted: Failed after 3 attempts"
);
}
#[test]
fn test_rate_limited_error_display_with_retry_after() {
let error = VeracodeError::RateLimited {
retry_after_seconds: Some(60),
message: "Too Many Requests".to_string(),
};
assert_eq!(
format!("{error}"),
"Rate limit exceeded: Too Many Requests (retry after 60s)"
);
}
#[test]
fn test_rate_limited_error_display_without_retry_after() {
let error = VeracodeError::RateLimited {
retry_after_seconds: None,
message: "Too Many Requests".to_string(),
};
assert_eq!(format!("{error}"), "Rate limit exceeded: Too Many Requests");
}
#[test]
fn test_rate_limited_error_is_retryable() {
let config = RetryConfig::new();
let error = VeracodeError::RateLimited {
retry_after_seconds: Some(60),
message: "Rate limit exceeded".to_string(),
};
assert!(config.is_retryable_error(&error));
}
#[test]
fn test_calculate_rate_limit_delay_with_retry_after() {
let config = RetryConfig::new();
let delay = config.calculate_rate_limit_delay(Some(30));
assert_eq!(delay.as_secs(), 30);
}
#[test]
#[cfg_attr(miri, ignore)] fn test_calculate_rate_limit_delay_without_retry_after() {
let config = RetryConfig::new();
let delay = config.calculate_rate_limit_delay(None);
assert!(delay.as_secs() >= 5);
assert!(delay.as_secs() <= 65);
}
#[test]
fn test_rate_limit_config_defaults() {
let config = RetryConfig::default();
assert_eq!(config.rate_limit_buffer_seconds, 5);
assert_eq!(config.rate_limit_max_attempts, 1);
}
#[test]
fn test_rate_limit_config_builders() {
let config = RetryConfig::new()
.with_rate_limit_buffer(10)
.with_rate_limit_max_attempts(2);
assert_eq!(config.rate_limit_buffer_seconds, 10);
assert_eq!(config.rate_limit_max_attempts, 2);
}
#[test]
#[cfg_attr(miri, ignore)] fn test_rate_limit_delay_uses_buffer() {
let config = RetryConfig::new().with_rate_limit_buffer(15);
let delay = config.calculate_rate_limit_delay(None);
assert!(delay.as_secs() >= 15);
assert!(delay.as_secs() <= 75); }
#[test]
fn test_jitter_disabled() {
let config = RetryConfig::new().with_jitter_disabled();
assert!(!config.jitter_enabled);
let delay1 = config.calculate_delay(2);
let delay2 = config.calculate_delay(2);
assert_eq!(delay1, delay2);
}
#[test]
fn test_jitter_enabled() {
let config = RetryConfig::new(); assert!(config.jitter_enabled);
let base_delay = config.initial_delay_ms;
let delay = config.calculate_delay(1);
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss
)]
let min_expected = (base_delay as f64 * 0.75) as u64;
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss
)]
let max_expected = (base_delay as f64 * 1.25) as u64;
assert!(delay.as_millis() >= min_expected as u128);
assert!(delay.as_millis() <= max_expected as u128);
}
}
#[cfg(test)]
mod security_tests {
use super::*;
use proptest::prelude::*;
proptest! {
#![proptest_config(ProptestConfig {
cases: if cfg!(miri) { 5 } else { 1000 },
failure_persistence: None,
.. ProptestConfig::default()
})]
#[test]
fn test_calculate_delay_no_overflow(
attempt in 0u32..1000u32,
initial_delay in 1u64..100_000u64,
multiplier in 1.0f64..10.0f64,
max_delay in 1u64..1_000_000u64,
) {
let config = RetryConfig::new()
.with_initial_delay(initial_delay)
.with_backoff_multiplier(multiplier)
.with_max_delay(max_delay)
.with_jitter_disabled();
let delay = config.calculate_delay(attempt);
assert!(delay.as_millis() <= max_delay as u128);
assert!(delay <= Duration::from_millis(max_delay));
}
#[test]
fn test_calculate_delay_extreme_multipliers(
attempt in 0u32..100u32,
multiplier in 1.0f64..1000.0f64,
) {
let config = RetryConfig::new()
.with_initial_delay(1000)
.with_backoff_multiplier(multiplier)
.with_max_delay(60_000)
.with_jitter_disabled();
let delay = config.calculate_delay(attempt);
assert!(delay.as_millis() <= 60_000);
}
}
proptest! {
#![proptest_config(ProptestConfig {
cases: if cfg!(miri) { 5 } else { 1000 },
failure_persistence: None,
.. ProptestConfig::default()
})]
#[test]
fn test_rate_limit_delay_with_retry_after(
retry_after_seconds in 0u64..100_000u64,
) {
let config = RetryConfig::new();
let delay = config.calculate_rate_limit_delay(Some(retry_after_seconds));
assert_eq!(delay.as_secs(), retry_after_seconds);
}
#[test]
fn test_rate_limit_delay_buffer_no_overflow(
buffer_seconds in 0u64..10_000u64,
) {
let config = RetryConfig::new()
.with_rate_limit_buffer(buffer_seconds);
let delay = config.calculate_rate_limit_delay(None);
assert!(delay.as_secs() >= buffer_seconds);
assert!(delay.as_secs() <= 60_u64.saturating_add(buffer_seconds));
}
}
proptest! {
#![proptest_config(ProptestConfig {
cases: if cfg!(miri) { 5 } else { 500 },
failure_persistence: None,
.. ProptestConfig::default()
})]
#[test]
fn test_credentials_debug_never_exposes_secrets(
api_id in "[a-zA-Z0-9]{10,256}",
api_key in "[a-zA-Z0-9]{10,256}",
) {
let credentials = VeracodeCredentials::new(api_id.clone(), api_key.clone());
let debug_output = format!("{:?}", credentials);
assert!(debug_output.contains("VeracodeCredentials"));
assert!(debug_output.contains("[REDACTED]"));
assert!(!debug_output.contains(&api_id));
assert!(!debug_output.contains(&api_key));
}
#[test]
fn test_credentials_arc_cloning_preserves_values(
api_id in "[a-zA-Z0-9]{10,100}",
api_key in "[a-zA-Z0-9]{10,100}",
) {
let credentials = VeracodeCredentials::new(api_id.clone(), api_key.clone());
let api_id_arc1 = credentials.api_id_ptr();
let api_id_arc2 = credentials.api_id_ptr();
let api_key_arc1 = credentials.api_key_ptr();
let api_key_arc2 = credentials.api_key_ptr();
assert_eq!(api_id_arc1.expose_secret(), &api_id);
assert_eq!(api_id_arc2.expose_secret(), &api_id);
assert_eq!(api_key_arc1.expose_secret(), &api_key);
assert_eq!(api_key_arc2.expose_secret(), &api_key);
}
#[test]
fn test_credentials_expose_methods_correctness(
api_id in "[a-zA-Z0-9]{10,256}",
api_key in "[a-zA-Z0-9]{10,256}",
) {
let credentials = VeracodeCredentials::new(api_id.clone(), api_key.clone());
assert_eq!(credentials.expose_api_id(), api_id);
assert_eq!(credentials.expose_api_key(), api_key);
}
}
proptest! {
#![proptest_config(ProptestConfig {
cases: if cfg!(miri) { 5 } else { 500 },
failure_persistence: None,
.. ProptestConfig::default()
})]
#[test]
fn test_config_debug_redacts_proxy_credentials(
protocol in "(http|https)",
username in "[a-zA-Z]{15,30}",
password in "[a-zA-Z]{15,30}",
host in "proxy\\d{1,3}\\.example\\.com",
) {
let port = 8080u16;
let proxy_url = format!("{}://{}:{}@{}:{}", protocol, username, password, host, port);
let config = VeracodeConfig::new("api_id", "api_key")
.with_proxy(&proxy_url);
let debug_output = format!("{:?}", config);
assert!(debug_output.contains("[REDACTED]"));
assert!(!debug_output.contains(&username));
assert!(!debug_output.contains(&password));
assert!(debug_output.contains(&host));
}
#[test]
fn test_config_debug_handles_utf8_safely(
protocol in "(http|https)",
creds in "[a-zA-Z0-9]{1,30}",
host in "[a-z]{3,15}\\.[a-z]{2,5}",
) {
let proxy_url = format!("{}://{}@{}", protocol, creds, host);
let config = VeracodeConfig::new("test", "test")
.with_proxy(&proxy_url);
let debug_output = format!("{:?}", config);
assert!(debug_output.contains("VeracodeConfig"));
}
#[test]
fn test_config_debug_redacts_proxy_auth(
proxy_username in "[a-zA-Z0-9]{10,50}",
proxy_password in "[a-zA-Z0-9]{10,50}",
) {
let config = VeracodeConfig::new("api_id", "api_key")
.with_proxy("http://proxy.example.com:8080")
.with_proxy_auth(proxy_username.clone(), proxy_password.clone());
let debug_output = format!("{:?}", config);
assert!(debug_output.contains("proxy_username"));
assert!(debug_output.contains("proxy_password"));
assert!(debug_output.contains("[REDACTED]"));
assert!(!debug_output.contains(&proxy_username));
assert!(!debug_output.contains(&proxy_password));
}
}
proptest! {
#![proptest_config(ProptestConfig {
cases: if cfg!(miri) { 5 } else { 100 },
failure_persistence: None,
.. ProptestConfig::default()
})]
#[test]
fn test_config_region_urls_are_valid(
region in prop::sample::select(vec![
VeracodeRegion::Commercial,
VeracodeRegion::European,
VeracodeRegion::Federal,
])
) {
let config = VeracodeConfig::new("test", "test")
.with_region(region);
match region {
VeracodeRegion::Commercial => {
assert_eq!(config.rest_base_url, "https://api.veracode.com");
assert_eq!(config.xml_base_url, "https://analysiscenter.veracode.com");
assert_eq!(config.base_url, config.rest_base_url);
}
VeracodeRegion::European => {
assert_eq!(config.rest_base_url, "https://api.veracode.eu");
assert_eq!(config.xml_base_url, "https://analysiscenter.veracode.eu");
assert_eq!(config.base_url, config.rest_base_url);
}
VeracodeRegion::Federal => {
assert_eq!(config.rest_base_url, "https://api.veracode.us");
assert_eq!(config.xml_base_url, "https://analysiscenter.veracode.us");
assert_eq!(config.base_url, config.rest_base_url);
}
}
assert!(config.rest_base_url.starts_with("https://"));
assert!(config.xml_base_url.starts_with("https://"));
assert!(config.base_url.starts_with("https://"));
}
}
proptest! {
#![proptest_config(ProptestConfig {
cases: if cfg!(miri) { 5 } else { 500 },
failure_persistence: None,
.. ProptestConfig::default()
})]
#[test]
fn test_config_timeout_no_overflow(
connect_timeout in 1u64..100_000u64,
request_timeout in 1u64..100_000u64,
) {
let config = VeracodeConfig::new("test", "test")
.with_timeouts(connect_timeout, request_timeout);
assert_eq!(config.connect_timeout, connect_timeout);
assert_eq!(config.request_timeout, request_timeout);
assert!(config.connect_timeout > 0);
assert!(config.request_timeout > 0);
}
#[test]
fn test_config_individual_timeout_setters(
connect_timeout in 1u64..50_000u64,
request_timeout in 1u64..50_000u64,
) {
let config1 = VeracodeConfig::new("test", "test")
.with_connect_timeout(connect_timeout);
assert_eq!(config1.connect_timeout, connect_timeout);
assert_eq!(config1.request_timeout, 300);
let config2 = VeracodeConfig::new("test", "test")
.with_request_timeout(request_timeout);
assert_eq!(config2.connect_timeout, 30); assert_eq!(config2.request_timeout, request_timeout);
}
}
proptest! {
#![proptest_config(ProptestConfig {
cases: if cfg!(miri) { 5 } else { 500 },
failure_persistence: None,
.. ProptestConfig::default()
})]
#[test]
fn test_error_display_safe_messages(
message in "[a-zA-Z0-9 ]{1,100}",
) {
let errors = vec![
VeracodeError::Authentication(message.clone()),
VeracodeError::InvalidResponse(message.clone()),
VeracodeError::InvalidConfig(message.clone()),
VeracodeError::NotFound(message.clone()),
VeracodeError::RetryExhausted(message.clone()),
];
for error in errors {
let display = format!("{}", error);
assert!(!display.is_empty());
assert!(display.contains(&message));
}
}
#[test]
fn test_rate_limited_error_display_safe(
retry_after in prop::option::of(0u64..10_000u64),
message in "[a-zA-Z0-9 ]{1,100}",
) {
let error = VeracodeError::RateLimited {
retry_after_seconds: retry_after,
message: message.clone(),
};
let display = format!("{}", error);
assert!(display.contains(&message));
assert!(display.contains("Rate limit exceeded"));
if let Some(seconds) = retry_after {
assert!(display.contains(&seconds.to_string()));
}
}
}
}
#[cfg(kani)]
mod kani_proofs {
use super::*;
#[kani::proof]
#[kani::unwind(10)] fn verify_calculate_delay_arithmetic_safety() {
let initial_delay: u64 = kani::any();
let max_delay: u64 = kani::any();
let multiplier: f64 = kani::any();
let attempt: u32 = kani::any();
kani::assume(initial_delay > 0);
kani::assume(initial_delay <= 100_000);
kani::assume(max_delay > 0);
kani::assume(max_delay >= initial_delay);
kani::assume(max_delay <= 1_000_000);
kani::assume(multiplier >= 1.0);
kani::assume(multiplier <= 10.0);
kani::assume(multiplier.is_finite());
kani::assume(attempt < 20);
let config = RetryConfig::new()
.with_initial_delay(initial_delay)
.with_backoff_multiplier(multiplier)
.with_max_delay(max_delay)
.with_jitter_disabled();
let delay = config.calculate_delay(attempt);
assert!(delay.as_millis() <= max_delay as u128);
assert!(delay.as_secs() < u64::MAX);
if attempt == 0 {
assert_eq!(delay.as_millis(), 0);
}
}
#[kani::proof]
fn verify_rate_limit_delay_safety() {
let buffer_seconds: u64 = kani::any();
let retry_after_seconds: Option<u64> = kani::any();
kani::assume(buffer_seconds <= 10_000);
if let Some(secs) = retry_after_seconds {
kani::assume(secs <= 100_000);
}
if let Some(expected_secs) = retry_after_seconds {
let delay = Duration::from_secs(expected_secs);
assert_eq!(delay.as_secs(), expected_secs);
}
let current_second: u64 = kani::any();
kani::assume(current_second < 60);
let seconds_until_next_minute = 60_u64.saturating_sub(current_second);
let total_delay = seconds_until_next_minute.saturating_add(buffer_seconds);
assert!(total_delay >= buffer_seconds);
assert!(total_delay <= 60 + buffer_seconds);
}
#[kani::proof]
#[kani::unwind(50)]
fn verify_credentials_arc_memory_safety() {
let api_id = "test_api_id".to_string();
let api_key = "test_key123".to_string();
let credentials = VeracodeCredentials::new(api_id.clone(), api_key.clone());
let api_id_arc1 = credentials.api_id_ptr();
let api_id_arc2 = credentials.api_id_ptr();
assert_eq!(
Arc::as_ptr(&api_id_arc1) as *const (),
Arc::as_ptr(&api_id_arc2) as *const ()
);
assert_eq!(api_id_arc1.expose_secret(), api_id_arc2.expose_secret());
assert_eq!(credentials.expose_api_id(), &api_id);
assert_eq!(credentials.expose_api_key(), &api_key);
let cloned = credentials.clone();
assert_eq!(cloned.expose_api_id(), credentials.expose_api_id());
assert_eq!(cloned.expose_api_key(), credentials.expose_api_key());
let cloned2 = cloned.clone();
let _ = cloned2.expose_api_id();
let _ = cloned2.expose_api_key();
}
#[kani::proof]
fn verify_proxy_url_redaction_safety() {
let has_at_sign: bool = kani::any();
let url = if has_at_sign {
"u:p@h".to_string()
} else {
"http://h".to_string()
};
let config = VeracodeConfig::new("t", "k").with_proxy(&url);
assert!(config.proxy_url.is_some());
let _ = config.proxy_url;
}
#[kani::proof]
#[kani::unwind(10)] fn verify_region_url_construction() {
let config = VeracodeConfig::new("a", "b").with_region(VeracodeRegion::Commercial);
assert!(!config.rest_base_url.is_empty());
assert!(!config.xml_base_url.is_empty());
assert!(config.rest_base_url.len() < 100);
assert!(config.xml_base_url.len() < 100);
assert!(matches!(config.region, VeracodeRegion::Commercial));
}
#[kani::proof]
#[kani::unwind(10)]
fn verify_european_region_url_construction() {
let config = VeracodeConfig::new("a", "b").with_region(VeracodeRegion::European);
assert!(!config.rest_base_url.is_empty());
assert!(!config.xml_base_url.is_empty());
assert!(config.rest_base_url.len() < 100);
assert!(config.xml_base_url.len() < 100);
assert!(matches!(config.region, VeracodeRegion::European));
}
#[kani::proof]
#[kani::unwind(10)]
fn verify_federal_region_url_construction() {
let config = VeracodeConfig::new("a", "b").with_region(VeracodeRegion::Federal);
assert!(!config.rest_base_url.is_empty());
assert!(!config.xml_base_url.is_empty());
assert!(config.rest_base_url.len() < 100);
assert!(config.xml_base_url.len() < 100);
assert!(matches!(config.region, VeracodeRegion::Federal));
}
}