veracode_platform/
policy.rs

1//! Policy API module for Veracode Platform
2//!
3//! This module provides functionality for managing security policies, policy compliance,
4//! and policy scan operations within the Veracode platform.
5
6use chrono::{DateTime, Utc};
7use log::{debug, info, warn};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11use crate::{VeracodeClient, VeracodeError};
12
13/// Maximum allowed retry delay in seconds (5 minutes)
14/// This prevents `DoS` scenarios where users could specify very long delays
15/// (e.g., 30 retries × 3600s = 30 hours)
16const MAX_RETRY_DELAY_SECONDS: u64 = 300;
17
18/// Input validation helpers to prevent path injection attacks
19mod validation {
20    /// Validates that a string is a valid UUID/GUID format
21    /// Format: 8-4-4-4-12 hexadecimal characters (with optional hyphens)
22    pub fn validate_guid(guid: &str) -> Result<(), String> {
23        // Allow both hyphenated and non-hyphenated UUIDs
24        let cleaned = guid.replace('-', "");
25
26        // Check length (32 hex chars for UUID)
27        if cleaned.len() != 32 {
28            return Err(format!(
29                "Invalid GUID format: expected 32 hex characters, got {}",
30                cleaned.len()
31            ));
32        }
33
34        // Check for valid hex characters only
35        if !cleaned.chars().all(|c| c.is_ascii_hexdigit()) {
36            return Err("Invalid GUID format: contains non-hexadecimal characters".to_string());
37        }
38
39        // Check for path traversal attempts
40        if guid.contains("..") || guid.contains('/') || guid.contains('\\') {
41            return Err("Invalid GUID format: contains path traversal characters".to_string());
42        }
43
44        // Check for URL parameter injection
45        if guid.contains('?') || guid.contains('&') || guid.contains('#') {
46            return Err("Invalid GUID format: contains URL parameter characters".to_string());
47        }
48
49        Ok(())
50    }
51
52    /// Validates that a string contains only safe identifier characters
53    /// Allows: alphanumeric, hyphens, underscores
54    pub fn validate_identifier(id: &str) -> Result<(), String> {
55        if id.is_empty() {
56            return Err("Identifier cannot be empty".to_string());
57        }
58
59        // Check for path traversal
60        if id.contains("..") || id.contains('/') || id.contains('\\') {
61            return Err("Invalid identifier: contains path traversal characters".to_string());
62        }
63
64        // Check for URL parameter injection
65        if id.contains('?') || id.contains('&') || id.contains('#') {
66            return Err("Invalid identifier: contains URL parameter characters".to_string());
67        }
68
69        // Allow alphanumeric, hyphens, and underscores only
70        if !id
71            .chars()
72            .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
73        {
74            return Err("Invalid identifier: contains unsafe characters".to_string());
75        }
76
77        Ok(())
78    }
79}
80
81/// Represents a security policy in the Veracode platform
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct SecurityPolicy {
84    /// Globally unique identifier for the policy
85    pub guid: String,
86    /// Policy name
87    pub name: String,
88    /// Policy description
89    pub description: Option<String>,
90    /// Policy type (CUSTOMER, BUILTIN, STANDARD)
91    #[serde(rename = "type")]
92    pub policy_type: String,
93    /// Policy version number
94    pub version: u32,
95    /// When the policy was created
96    pub created: Option<DateTime<Utc>>,
97    /// Who modified the policy last
98    pub modified_by: Option<String>,
99    /// Organization ID this policy belongs to
100    pub organization_id: Option<u64>,
101    /// Policy category (APPLICATION, etc.)
102    pub category: String,
103    /// Whether this is a vendor policy
104    pub vendor_policy: bool,
105    /// Scan frequency rules
106    pub scan_frequency_rules: Vec<ScanFrequencyRule>,
107    /// Finding rules for the policy
108    pub finding_rules: Vec<FindingRule>,
109    /// Custom severities defined for this policy
110    pub custom_severities: Vec<serde_json::Value>,
111    /// Grace periods for different severity levels
112    pub sev5_grace_period: u32,
113    pub sev4_grace_period: u32,
114    pub sev3_grace_period: u32,
115    pub sev2_grace_period: u32,
116    pub sev1_grace_period: u32,
117    pub sev0_grace_period: u32,
118    /// Score grace period
119    pub score_grace_period: u32,
120    /// SCA blacklist grace period
121    pub sca_blacklist_grace_period: u32,
122    /// SCA grace periods (nullable)
123    pub sca_grace_periods: Option<serde_json::Value>,
124    /// Evaluation date
125    pub evaluation_date: Option<DateTime<Utc>>,
126    /// Evaluation date type
127    pub evaluation_date_type: Option<String>,
128    /// Policy capabilities
129    pub capabilities: Vec<String>,
130    /// Links for API navigation
131    #[serde(rename = "_links")]
132    pub links: Option<serde_json::Value>,
133}
134
135/// Policy compliance status (XML API values from getbuildinfo.do)
136#[derive(Debug, Clone, Serialize, Deserialize)]
137#[serde(rename_all = "PascalCase")]
138pub enum PolicyComplianceStatus {
139    /// Application passes all policy requirements
140    Passed,
141    /// Application passes with conditional requirements  
142    #[serde(rename = "Conditional Pass")]
143    ConditionalPass,
144    /// Application fails policy requirements (triggers build break)
145    #[serde(rename = "Did Not Pass")]
146    DidNotPass,
147    /// Policy compliance status has not been assessed
148    #[serde(rename = "Not Assessed")]
149    NotAssessed,
150}
151
152/// Individual policy rule
153#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct PolicyRule {
155    /// Rule identifier
156    pub id: String,
157    /// Rule name
158    pub name: String,
159    /// Rule description
160    pub description: Option<String>,
161    /// Rule type (e.g., severity, category)
162    pub rule_type: String,
163    /// Rule criteria
164    pub criteria: Option<serde_json::Value>,
165    /// Whether the rule is enabled
166    pub enabled: bool,
167    /// Rule severity level
168    pub severity: Option<String>,
169}
170
171/// Policy compliance thresholds
172#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct PolicyThresholds {
174    /// Maximum allowed Very High severity flaws
175    pub very_high: Option<u32>,
176    /// Maximum allowed High severity flaws
177    pub high: Option<u32>,
178    /// Maximum allowed Medium severity flaws
179    pub medium: Option<u32>,
180    /// Maximum allowed Low severity flaws
181    pub low: Option<u32>,
182    /// Maximum allowed Very Low severity flaws
183    pub very_low: Option<u32>,
184    /// Overall score threshold
185    pub score_threshold: Option<f64>,
186}
187
188/// Policy scan request
189#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct PolicyScanRequest {
191    /// Application GUID to scan
192    pub application_guid: String,
193    /// Policy GUID to apply
194    pub policy_guid: String,
195    /// Scan type (static, dynamic, sca)
196    pub scan_type: ScanType,
197    /// Optional sandbox GUID for sandbox scans
198    pub sandbox_guid: Option<String>,
199    /// Scan configuration
200    pub config: Option<PolicyScanConfig>,
201}
202
203/// Types of scans for policy evaluation
204#[derive(Debug, Clone, Serialize, Deserialize)]
205#[serde(rename_all = "lowercase")]
206pub enum ScanType {
207    /// Static Application Security Testing
208    Static,
209    /// Dynamic Application Security Testing
210    Dynamic,
211    /// Software Composition Analysis
212    Sca,
213    /// Manual penetration testing
214    Manual,
215}
216
217/// Configuration for policy scans
218#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct PolicyScanConfig {
220    /// Whether to auto-submit the scan
221    pub auto_submit: Option<bool>,
222    /// Scan timeout in minutes
223    pub timeout_minutes: Option<u32>,
224    /// Include third-party components
225    pub include_third_party: Option<bool>,
226    /// Scan modules to include
227    pub modules: Option<Vec<String>>,
228}
229
230/// Policy scan result
231#[derive(Debug, Clone, Serialize, Deserialize)]
232pub struct PolicyScanResult {
233    /// Scan identifier
234    pub scan_id: u64,
235    /// Application GUID
236    pub application_guid: String,
237    /// Policy GUID used for evaluation
238    pub policy_guid: String,
239    /// Scan status
240    pub status: ScanStatus,
241    /// Scan type
242    pub scan_type: ScanType,
243    /// When the scan was initiated
244    pub started: DateTime<Utc>,
245    /// When the scan completed
246    pub completed: Option<DateTime<Utc>>,
247    /// Policy compliance result
248    pub compliance_result: Option<PolicyComplianceResult>,
249    /// Findings summary
250    pub findings_summary: Option<FindingsSummary>,
251    /// URL to detailed results
252    pub results_url: Option<String>,
253}
254
255/// Status of a policy scan
256#[derive(Debug, Clone, Serialize, Deserialize)]
257#[serde(rename_all = "UPPERCASE")]
258pub enum ScanStatus {
259    /// Scan is queued for processing
260    Queued,
261    /// Scan is currently running
262    Running,
263    /// Scan completed successfully
264    Completed,
265    /// Scan failed
266    Failed,
267    /// Scan was cancelled
268    Cancelled,
269    /// Scan timed out
270    Timeout,
271}
272
273/// Policy compliance evaluation result
274#[derive(Debug, Clone, Serialize, Deserialize)]
275pub struct PolicyComplianceResult {
276    /// Overall compliance status
277    pub status: PolicyComplianceStatus,
278    /// Compliance score (0-100)
279    pub score: Option<f64>,
280    /// Whether scan passed policy requirements
281    pub passed: bool,
282    /// Detailed compliance breakdown
283    pub breakdown: Option<ComplianceBreakdown>,
284    /// Policy violations found
285    pub violations: Option<Vec<PolicyViolation>>,
286    /// Compliance summary message
287    pub summary: Option<String>,
288}
289
290/// Detailed compliance breakdown by severity
291#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct ComplianceBreakdown {
293    /// Very High severity findings count
294    pub very_high: u32,
295    /// High severity findings count
296    pub high: u32,
297    /// Medium severity findings count
298    pub medium: u32,
299    /// Low severity findings count
300    pub low: u32,
301    /// Very Low severity findings count
302    pub very_low: u32,
303    /// Total findings count
304    pub total: u32,
305}
306
307/// Policy violation details
308#[derive(Debug, Clone, Serialize, Deserialize)]
309pub struct PolicyViolation {
310    /// Violation type
311    pub violation_type: String,
312    /// Severity of the violation
313    pub severity: String,
314    /// Description of the violation
315    pub description: String,
316    /// Count of this violation type
317    pub count: u32,
318    /// Threshold that was exceeded
319    pub threshold_exceeded: Option<u32>,
320    /// Actual value that caused the violation
321    pub actual_value: Option<u32>,
322}
323
324/// Summary of findings from a policy scan
325#[derive(Debug, Clone, Serialize, Deserialize)]
326pub struct FindingsSummary {
327    /// Total number of findings
328    pub total: u32,
329    /// Number of open findings
330    pub open: u32,
331    /// Number of fixed findings
332    pub fixed: u32,
333    /// Number of findings by severity
334    pub by_severity: HashMap<String, u32>,
335    /// Number of findings by category
336    pub by_category: Option<HashMap<String, u32>>,
337}
338
339///
340/// # Errors
341///
342/// Returns an error if the API request fails, the resource is not found,
343/// or authentication/authorization fails.
344/// Summary report data structure matching Veracode `summary_report` API response
345#[derive(Debug, Clone, Serialize, Deserialize)]
346pub struct SummaryReport {
347    /// Application ID
348    pub app_id: u64,
349    /// Application name
350    pub app_name: String,
351    /// Build ID
352    pub build_id: u64,
353    /// Policy compliance status (e.g., "Did Not Pass", "Passed", "Conditional Pass")
354    pub policy_compliance_status: String,
355    /// Policy name
356    pub policy_name: String,
357    /// Policy version
358    pub policy_version: u32,
359    /// Whether the policy rules status passed
360    pub policy_rules_status: String,
361    /// Whether grace period expired
362    pub grace_period_expired: bool,
363    /// Whether scan is overdue
364    pub scan_overdue: String,
365    /// Whether this is the latest build
366    pub is_latest_build: bool,
367    /// Sandbox name (optional)
368    pub sandbox_name: Option<String>,
369    /// Sandbox ID (optional)
370    pub sandbox_id: Option<u64>,
371    /// Generation date
372    pub generation_date: String,
373    /// Last update time
374    pub last_update_time: String,
375    /// Static analysis summary
376    #[serde(rename = "static-analysis")]
377    pub static_analysis: Option<StaticAnalysisSummary>,
378    /// Flaw status summary
379    #[serde(rename = "flaw-status")]
380    pub flaw_status: Option<FlawStatusSummary>,
381    /// Software composition analysis summary
382    pub software_composition_analysis: Option<ScaSummary>,
383    /// Severity breakdown
384    pub severity: Option<Vec<SeverityLevel>>,
385}
386
387/// Static analysis summary from summary report
388#[derive(Debug, Clone, Serialize, Deserialize)]
389pub struct StaticAnalysisSummary {
390    /// Rating (e.g., "A", "B", "C")
391    pub rating: Option<String>,
392    /// Score (0-100)
393    pub score: Option<u32>,
394    /// Mitigated rating
395    pub mitigated_rating: Option<String>,
396    /// Mitigated score
397    pub mitigated_score: Option<u32>,
398    /// Analysis size in bytes
399    pub analysis_size_bytes: Option<u64>,
400    /// Engine version
401    pub engine_version: Option<String>,
402    /// Published date
403    pub published_date: Option<String>,
404    /// Version/build identifier
405    pub version: Option<String>,
406}
407
408/// Flaw status summary from summary report
409#[derive(Debug, Clone, Serialize, Deserialize)]
410pub struct FlawStatusSummary {
411    /// New flaws
412    pub new: u32,
413    /// Reopened flaws
414    pub reopen: u32,
415    /// Open flaws
416    pub open: u32,
417    /// Fixed flaws
418    pub fixed: u32,
419    /// Total flaws
420    pub total: u32,
421    /// Not mitigated flaws
422    pub not_mitigated: u32,
423}
424
425/// Software Composition Analysis summary
426#[derive(Debug, Clone, Serialize, Deserialize)]
427pub struct ScaSummary {
428    /// Third party components count
429    pub third_party_components: u32,
430    /// Whether violates policy
431    pub violate_policy: bool,
432    /// Components that violated policy
433    pub components_violated_policy: u32,
434}
435
436/// Severity level breakdown from summary report
437#[derive(Debug, Clone, Serialize, Deserialize)]
438pub struct SeverityLevel {
439    /// Severity level (0-5)
440    pub level: u32,
441    /// Categories for this severity level
442    pub category: Vec<CategorySummary>,
443}
444
445/// Category summary within severity level
446#[derive(Debug, Clone, Serialize, Deserialize)]
447pub struct CategorySummary {
448    /// Category name
449    pub categoryname: String,
450    /// Severity name
451    pub severity: String,
452    /// Count of flaws in this category
453    pub count: u32,
454}
455
456/// Scan frequency rule for policies
457#[derive(Debug, Clone, Serialize, Deserialize)]
458pub struct ScanFrequencyRule {
459    /// Type of scan this rule applies to
460    pub scan_type: String,
461    /// How frequently scans should be performed
462    pub frequency: String,
463}
464
465/// Finding rule for policies
466#[derive(Debug, Clone, Serialize, Deserialize)]
467pub struct FindingRule {
468    /// Type of finding rule
469    #[serde(rename = "type")]
470    pub rule_type: String,
471    /// Scan types this rule applies to
472    pub scan_type: Vec<String>,
473    /// Rule value/threshold
474    pub value: String,
475    /// Advanced options for the rule
476    pub advanced_options: Option<serde_json::Value>,
477}
478
479/// Advanced options for finding rules
480#[derive(Debug, Clone, Serialize, Deserialize)]
481pub struct FindingRuleAdvancedOptions {
482    /// Override severity
483    pub override_severity: Option<bool>,
484    /// Build action (WARNING, ERROR, etc.)
485    pub build_action: Option<String>,
486    /// Component dependency type
487    pub component_dependency: Option<String>,
488    /// Vulnerable methods setting
489    pub vulnerable_methods: Option<String>,
490    /// Selected licenses
491    pub selected_licenses: Option<Vec<String>>,
492    /// Override severity level
493    pub override_severity_level: Option<String>,
494    /// Whether to allow non-OSS licenses
495    pub allowed_nonoss_licenses: Option<bool>,
496    /// Whether to allow unrecognized licenses
497    pub allowed_unrecognized_licenses: Option<bool>,
498    /// Whether all licenses must meet requirement
499    pub all_licenses_must_meet_requirement: Option<bool>,
500    /// Whether this is a blocklist
501    pub is_blocklist: Option<bool>,
502}
503
504/// Query parameters for listing policies
505#[derive(Debug, Clone, Default)]
506pub struct PolicyListParams {
507    /// Filter by policy name
508    pub name: Option<String>,
509    /// Filter by policy type
510    pub policy_type: Option<String>,
511    /// Filter by active status
512    pub is_active: Option<bool>,
513    /// Include only default policies
514    pub default_only: Option<bool>,
515    /// Page number for pagination
516    pub page: Option<u32>,
517    /// Number of items per page
518    pub size: Option<u32>,
519}
520
521impl PolicyListParams {
522    /// Convert to query parameters for HTTP requests
523    #[must_use]
524    pub fn to_query_params(&self) -> Vec<(String, String)> {
525        Vec::from(self) // Delegate to trait
526    }
527}
528
529// Trait implementations for memory optimization
530impl From<&PolicyListParams> for Vec<(String, String)> {
531    fn from(query: &PolicyListParams) -> Self {
532        let mut params = Vec::new();
533
534        if let Some(ref name) = query.name {
535            params.push(("name".to_string(), name.clone())); // Still clone for borrowing
536        }
537        if let Some(ref policy_type) = query.policy_type {
538            params.push(("type".to_string(), policy_type.clone()));
539        }
540        if let Some(is_active) = query.is_active {
541            params.push(("active".to_string(), is_active.to_string()));
542        }
543        if let Some(default_only) = query.default_only {
544            params.push(("default".to_string(), default_only.to_string()));
545        }
546        if let Some(page) = query.page {
547            params.push(("page".to_string(), page.to_string()));
548        }
549        if let Some(size) = query.size {
550            params.push(("size".to_string(), size.to_string()));
551        }
552
553        params
554    }
555}
556
557impl From<PolicyListParams> for Vec<(String, String)> {
558    fn from(query: PolicyListParams) -> Self {
559        let mut params = Vec::new();
560
561        if let Some(name) = query.name {
562            params.push(("name".to_string(), name)); // MOVE - no clone!
563        }
564        if let Some(policy_type) = query.policy_type {
565            params.push(("type".to_string(), policy_type)); // MOVE - no clone!
566        }
567        if let Some(is_active) = query.is_active {
568            params.push(("active".to_string(), is_active.to_string()));
569        }
570        if let Some(default_only) = query.default_only {
571            params.push(("default".to_string(), default_only.to_string()));
572        }
573        if let Some(page) = query.page {
574            params.push(("page".to_string(), page.to_string()));
575        }
576        if let Some(size) = query.size {
577            params.push(("size".to_string(), size.to_string()));
578        }
579
580        params
581    }
582}
583
584/// Response wrapper for policy list operations
585#[derive(Debug, Clone, Serialize, Deserialize)]
586pub struct PolicyListResponse {
587    #[serde(rename = "_embedded")]
588    pub embedded: Option<PolicyEmbedded>,
589    pub page: Option<PageInfo>,
590    #[serde(rename = "_links")]
591    pub links: Option<serde_json::Value>,
592}
593
594/// Embedded policies in the list response
595#[derive(Debug, Clone, Serialize, Deserialize)]
596pub struct PolicyEmbedded {
597    #[serde(rename = "policy_versions")]
598    pub policy_versions: Vec<SecurityPolicy>,
599}
600
601/// Page information for paginated responses
602#[derive(Debug, Clone, Serialize, Deserialize)]
603pub struct PageInfo {
604    pub size: u32,
605    pub number: u32,
606    pub total_elements: u32,
607    pub total_pages: u32,
608}
609
610/// Indicates which API was used to retrieve policy compliance status
611#[derive(Debug, Clone, PartialEq, Eq)]
612pub enum ApiSource {
613    /// Policy status retrieved from summary report API (preferred)
614    SummaryReport,
615    /// Policy status retrieved from getbuildinfo.do XML API (fallback)
616    BuildInfo,
617}
618
619/// Policy-specific error types
620#[derive(Debug)]
621#[must_use = "Need to handle all error enum types."]
622pub enum PolicyError {
623    /// Veracode API error
624    Api(VeracodeError),
625    /// Policy not found (404)
626    NotFound,
627    /// Invalid policy configuration (400)
628    InvalidConfig(String),
629    /// Policy scan failed
630    ScanFailed(String),
631    /// Policy evaluation error
632    EvaluationError(String),
633    /// Insufficient permissions (403)
634    PermissionDenied,
635    /// Authentication required (401)
636    Unauthorized,
637    /// Internal server error (500)
638    InternalServerError,
639    /// Policy compliance check timeout
640    Timeout,
641}
642
643impl std::fmt::Display for PolicyError {
644    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
645        match self {
646            PolicyError::Api(err) => write!(f, "API error: {err}"),
647            PolicyError::NotFound => write!(f, "Policy not found"),
648            PolicyError::InvalidConfig(msg) => write!(f, "Invalid policy configuration: {msg}"),
649            PolicyError::ScanFailed(msg) => write!(f, "Policy scan failed: {msg}"),
650            PolicyError::EvaluationError(msg) => write!(f, "Policy evaluation error: {msg}"),
651            PolicyError::PermissionDenied => {
652                write!(f, "Insufficient permissions for policy operation")
653            }
654            PolicyError::Unauthorized => {
655                write!(f, "Authentication required - invalid API credentials")
656            }
657            PolicyError::InternalServerError => write!(f, "Internal server error occurred"),
658            PolicyError::Timeout => write!(f, "Policy operation timed out"),
659        }
660    }
661}
662
663impl std::error::Error for PolicyError {}
664
665impl From<VeracodeError> for PolicyError {
666    fn from(err: VeracodeError) -> Self {
667        PolicyError::Api(err)
668    }
669}
670
671impl From<reqwest::Error> for PolicyError {
672    fn from(err: reqwest::Error) -> Self {
673        PolicyError::Api(VeracodeError::Http(err))
674    }
675}
676
677impl From<serde_json::Error> for PolicyError {
678    fn from(err: serde_json::Error) -> Self {
679        PolicyError::Api(VeracodeError::Serialization(err))
680    }
681}
682
683/// Veracode Policy API operations
684pub struct PolicyApi<'a> {
685    client: &'a VeracodeClient,
686}
687
688impl<'a> PolicyApi<'a> {
689    ///
690    /// # Errors
691    ///
692    /// Returns an error if the API request fails, the resource is not found,
693    /// or authentication/authorization fails.
694    /// Create a new `PolicyApi` instance
695    #[must_use]
696    pub fn new(client: &'a VeracodeClient) -> Self {
697        Self { client }
698    }
699
700    /// List all available security policies
701    ///
702    /// # Arguments
703    ///
704    /// * `params` - Optional query parameters for filtering
705    ///
706    /// # Returns
707    ///
708    /// A `Result` containing a list of policies or an error.
709    ///
710    /// # Errors
711    ///
712    /// Returns an error if the API request fails, the policy is invalid,
713    /// or authentication/authorization fails.
714    pub async fn list_policies(
715        &self,
716        params: Option<PolicyListParams>,
717    ) -> Result<Vec<SecurityPolicy>, PolicyError> {
718        let endpoint = "/appsec/v1/policies";
719
720        let query_params = params.as_ref().map(Vec::from);
721
722        let response = self.client.get(endpoint, query_params.as_deref()).await?;
723
724        let status = response.status().as_u16();
725        match status {
726            200 => {
727                let policy_response: PolicyListResponse = response.json().await?;
728                let policies = policy_response
729                    .embedded
730                    .map(|e| e.policy_versions)
731                    .unwrap_or_default();
732
733                Ok(policies)
734            }
735            400 => {
736                let error_text = response.text().await.unwrap_or_default();
737                Err(PolicyError::InvalidConfig(error_text))
738            }
739            401 => Err(PolicyError::Unauthorized),
740            403 => Err(PolicyError::PermissionDenied),
741            404 => Err(PolicyError::NotFound),
742            500 => Err(PolicyError::InternalServerError),
743            _ => {
744                let error_text = response.text().await.unwrap_or_default();
745                Err(PolicyError::Api(VeracodeError::InvalidResponse(format!(
746                    "HTTP {status}: {error_text}"
747                ))))
748            }
749        }
750    }
751
752    /// Get a specific policy by GUID
753    ///
754    /// # Arguments
755    ///
756    /// * `policy_guid` - The GUID of the policy
757    ///
758    /// # Returns
759    ///
760    /// A `Result` containing the policy or an error.
761    ///
762    /// # Errors
763    ///
764    /// Returns an error if the API request fails, the policy is invalid,
765    /// or authentication/authorization fails.
766    pub async fn get_policy(&self, policy_guid: &str) -> Result<SecurityPolicy, PolicyError> {
767        // Validate GUID to prevent path injection
768        validation::validate_guid(policy_guid)
769            .map_err(|e| PolicyError::InvalidConfig(format!("Invalid policy GUID: {e}")))?;
770
771        let endpoint = format!("/appsec/v1/policies/{policy_guid}");
772
773        let response = self.client.get(&endpoint, None).await?;
774
775        let status = response.status().as_u16();
776        match status {
777            200 => {
778                let policy: SecurityPolicy = response.json().await?;
779                Ok(policy)
780            }
781            400 => {
782                let error_text = response.text().await.unwrap_or_default();
783                Err(PolicyError::InvalidConfig(error_text))
784            }
785            401 => Err(PolicyError::Unauthorized),
786            403 => Err(PolicyError::PermissionDenied),
787            404 => Err(PolicyError::NotFound),
788            500 => Err(PolicyError::InternalServerError),
789            _ => {
790                let error_text = response.text().await.unwrap_or_default();
791                Err(PolicyError::Api(VeracodeError::InvalidResponse(format!(
792                    "HTTP {status}: {error_text}"
793                ))))
794            }
795        }
796    }
797
798    /// Get the default policy for the organization
799    ///
800    /// # Returns
801    ///
802    /// A `Result` containing the default policy or an error.
803    ///
804    /// # Errors
805    ///
806    /// Returns an error if the API request fails, the policy is invalid,
807    /// or authentication/authorization fails.
808    pub async fn get_default_policy(&self) -> Result<SecurityPolicy, PolicyError> {
809        let params = PolicyListParams {
810            default_only: Some(true),
811            ..Default::default()
812        };
813
814        let policies = self.list_policies(Some(params)).await?;
815        // Note: Default policy identification may need to be handled differently
816        // based on the actual API response structure
817        policies
818            .into_iter()
819            .find(|p| p.policy_type == "CUSTOMER" && p.organization_id.is_some())
820            .ok_or(PolicyError::NotFound)
821    }
822
823    /// Evaluates policy compliance for an application or sandbox using XML API
824    ///
825    /// This uses the /api/5.0/getbuildinfo.do endpoint which is the only working
826    /// policy compliance endpoint as the REST API compliance endpoints return 404.
827    ///
828    /// # Arguments
829    ///
830    /// * `app_id` - The numeric ID of the application
831    /// * `sandbox_id` - Optional numeric ID of the sandbox to evaluate
832    ///
833    /// # Returns
834    ///
835    /// A `Result` containing the policy compliance status string or an error.
836    ///
837    /// # Errors
838    ///
839    /// Returns an error if the API request fails, the policy is invalid,
840    /// or authentication/authorization fails.
841    pub async fn evaluate_policy_compliance_via_buildinfo(
842        &self,
843        app_id: &str,
844        build_id: Option<&str>,
845        sandbox_id: Option<&str>,
846    ) -> Result<std::borrow::Cow<'static, str>, PolicyError> {
847        self.evaluate_policy_compliance_via_buildinfo_with_retry(
848            app_id, build_id, sandbox_id, 30, 10,
849        )
850        .await
851    }
852
853    /// Evaluates policy compliance with retry logic for when assessment is not yet complete
854    ///
855    /// This function will retry the policy evaluation check when the status is "Not Assessed"
856    /// until either the assessment completes or the maximum retry attempts are reached.
857    ///
858    /// # Arguments
859    ///
860    /// * `app_id` - The numeric ID of the application
861    /// * `build_id` - Optional build ID to check. If None, checks the latest build
862    /// * `sandbox_id` - Optional numeric ID of the sandbox to evaluate
863    /// * `max_retries` - Maximum number of retry attempts (default: 30)
864    /// * `retry_delay_seconds` - Delay between retries in seconds (default: 10)
865    ///
866    /// # Returns
867    ///
868    /// A `Result` containing the policy compliance status string or an error.
869    ///
870    /// # Errors
871    ///
872    /// Returns an error if the API request fails, the policy is invalid,
873    /// or authentication/authorization fails.
874    pub async fn evaluate_policy_compliance_via_buildinfo_with_retry(
875        &self,
876        app_id: &str,
877        build_id: Option<&str>,
878        sandbox_id: Option<&str>,
879        max_retries: u32,
880        retry_delay_seconds: u64,
881    ) -> Result<std::borrow::Cow<'static, str>, PolicyError> {
882        use crate::build::{BuildError, GetBuildInfoRequest};
883        use std::borrow::Cow;
884        use tokio::time::{Duration, sleep};
885
886        // Cap retry delay to prevent DoS scenarios
887        let retry_delay_seconds = retry_delay_seconds.min(MAX_RETRY_DELAY_SECONDS);
888        if retry_delay_seconds > MAX_RETRY_DELAY_SECONDS {
889            warn!(
890                "Retry delay capped at {} seconds (requested: {})",
891                MAX_RETRY_DELAY_SECONDS, retry_delay_seconds
892            );
893        }
894
895        let build_request = GetBuildInfoRequest {
896            app_id: app_id.to_string(),
897            build_id: build_id.map(str::to_string), // ← Use the parameter
898            sandbox_id: sandbox_id.map(str::to_string),
899        };
900
901        let mut attempts: u32 = 0;
902        loop {
903            let build_info = self
904                .client
905                .build_api()?
906                .get_build_info(&build_request)
907                .await
908                .map_err(|e| match e {
909                    BuildError::BuildNotFound => PolicyError::Api(crate::VeracodeError::InvalidResponse(
910                        format!("Build not found for application ID {app_id}. This may indicate: no builds exist for this application, the build ID is invalid, or the application has no completed scans. Cannot retrieve policy status without a valid build.")
911                    )),
912                    BuildError::ApplicationNotFound => PolicyError::Api(crate::VeracodeError::InvalidResponse(
913                        format!("Application not found with ID {app_id}. This may indicate: incorrect application ID, insufficient permissions, or the application doesn't exist in your organization. Please verify the application ID and your API credentials.")
914                    )),
915                    BuildError::SandboxNotFound => PolicyError::Api(crate::VeracodeError::InvalidResponse(
916                        format!("Sandbox not found with ID {}. This may indicate: incorrect sandbox ID, insufficient permissions, or the sandbox doesn't exist for this application.", sandbox_id.unwrap_or("unknown"))
917                    )),
918                    BuildError::Api(api_err) => PolicyError::Api(api_err),
919                    BuildError::InvalidParameter(msg)
920                    | BuildError::CreationFailed(msg)
921                    | BuildError::UpdateFailed(msg)
922                    | BuildError::DeletionFailed(msg)
923                    | BuildError::XmlParsingError(msg) => {
924                        PolicyError::Api(crate::VeracodeError::InvalidResponse(msg))
925                    }
926                    BuildError::Unauthorized | BuildError::PermissionDenied => PolicyError::Api(
927                        crate::VeracodeError::Authentication("Build API access denied".to_string()),
928                    ),
929                    BuildError::BuildInProgress => {
930                        PolicyError::Api(crate::VeracodeError::InvalidResponse(
931                            "Build is currently in progress".to_string(),
932                        ))
933                    }
934                })?;
935
936            // Get the policy compliance status
937            let status = build_info
938                .policy_compliance_status
939                .as_deref()
940                .unwrap_or("Not Assessed");
941
942            // If status is ready (not in-progress), return the result
943            if status != "Not Assessed" && status != "Calculating..." {
944                return Ok(Cow::Owned(status.to_string()));
945            }
946
947            // If we've reached max retries, return "Not Assessed"
948            attempts = attempts.saturating_add(1);
949            if attempts >= max_retries {
950                warn!(
951                    "Policy evaluation still not assessed after {max_retries} attempts. This may indicate: scan is still in progress, policy evaluation is taking longer than expected, or application may not have a policy assigned"
952                );
953                return Ok(Cow::Borrowed("Not Assessed"));
954            }
955
956            // Log retry attempt
957            info!(
958                "Policy evaluation not yet assessed, retrying in {retry_delay_seconds} seconds... (attempt {attempts}/{max_retries})"
959            );
960
961            // Wait before retrying
962            sleep(Duration::from_secs(retry_delay_seconds)).await;
963        }
964    }
965
966    /// Determines if build should break based on policy compliance status
967    ///
968    /// # Arguments
969    ///
970    /// * `status` - The policy compliance status string from XML API
971    ///
972    /// # Returns
973    ///
974    /// `true` if build should break, `false` otherwise
975    #[must_use]
976    pub fn should_break_build(status: &str) -> bool {
977        status == "Did Not Pass"
978    }
979
980    /// Gets the appropriate exit code for CI/CD systems based on policy compliance
981    ///
982    /// # Arguments
983    ///
984    /// * `status` - The policy compliance status string from XML API
985    ///
986    /// # Returns
987    ///
988    /// Exit code: 0 for success, 4 for policy failure (build break)
989    #[must_use]
990    pub fn get_exit_code_for_status(status: &str) -> i32 {
991        if Self::should_break_build(status) {
992            4 // DID_NOT_PASSED_POLICY - matches Java wrapper
993        } else {
994            0 // SUCCESS
995        }
996    }
997
998    /// Get summary report for an application build using the REST API
999    ///
1000    ///
1001    /// # Errors
1002    ///
1003    /// Returns an error if the API request fails, the resource is not found,
1004    /// or authentication/authorization fails.
1005    /// This uses the `/appsec/v2/applications/{app_guid}/summary_report` endpoint
1006    /// to get policy compliance status and scan results.
1007    ///
1008    /// # Arguments
1009    ///
1010    /// * `app_guid` - The GUID of the application
1011    /// * `build_id` - The build ID (GUID) to get summary for
1012    /// * `sandbox_guid` - Optional sandbox GUID for sandbox scans
1013    ///
1014    /// # Returns
1015    ///
1016    /// A `Result` containing the summary report or an error.
1017    ///
1018    /// # Errors
1019    ///
1020    /// Returns an error if the API request fails, the policy is invalid,
1021    /// or authentication/authorization fails.
1022    pub async fn get_summary_report(
1023        &self,
1024        app_guid: &str,
1025        build_id: Option<&str>,
1026        sandbox_guid: Option<&str>,
1027    ) -> Result<SummaryReport, PolicyError> {
1028        // Validate app_guid to prevent path injection
1029        validation::validate_guid(app_guid)
1030            .map_err(|e| PolicyError::InvalidConfig(format!("Invalid application GUID: {e}")))?;
1031
1032        // Validate optional identifiers
1033        if let Some(build_id) = build_id {
1034            validation::validate_identifier(build_id)
1035                .map_err(|e| PolicyError::InvalidConfig(format!("Invalid build ID: {e}")))?;
1036        }
1037        if let Some(sandbox_guid) = sandbox_guid {
1038            validation::validate_guid(sandbox_guid)
1039                .map_err(|e| PolicyError::InvalidConfig(format!("Invalid sandbox GUID: {e}")))?;
1040        }
1041
1042        let endpoint = format!("/appsec/v2/applications/{app_guid}/summary_report");
1043
1044        // Build query parameters
1045        let mut query_params = Vec::new();
1046        if let Some(build_id) = build_id {
1047            query_params.push(("build_id".to_string(), build_id.to_string()));
1048        }
1049        if let Some(sandbox_guid) = sandbox_guid {
1050            query_params.push(("context".to_string(), sandbox_guid.to_string()));
1051        }
1052
1053        let response = self.client.get(&endpoint, Some(&query_params)).await?;
1054
1055        let status = response.status().as_u16();
1056        match status {
1057            200 => {
1058                let summary_report: SummaryReport = response.json().await?;
1059                Ok(summary_report)
1060            }
1061            400 => {
1062                let error_text = response.text().await.unwrap_or_default();
1063                Err(PolicyError::InvalidConfig(error_text))
1064            }
1065            401 => Err(PolicyError::Unauthorized),
1066            403 => Err(PolicyError::PermissionDenied),
1067            404 => Err(PolicyError::NotFound),
1068            500 => Err(PolicyError::InternalServerError),
1069            _ => {
1070                let error_text = response.text().await.unwrap_or_default();
1071                Err(PolicyError::Api(VeracodeError::InvalidResponse(format!(
1072                    "HTTP {status}: {error_text}"
1073                ))))
1074            }
1075        }
1076    }
1077
1078    /// Gets summary report with retry logic and returns both the full report and compliance status
1079    ///
1080    /// This function combines the functionality of both `get_summary_report` and
1081    /// `evaluate_policy_compliance_via_summary_report_with_retry` to avoid redundant API calls.
1082    /// It will retry until the policy compliance status is ready (not "Not Assessed").
1083    ///
1084    /// # Arguments
1085    ///
1086    /// * `app_guid` - The GUID of the application
1087    /// * `build_id` - The build ID to check compliance for
1088    /// * `sandbox_guid` - Optional sandbox GUID for sandbox scans
1089    /// * `max_retries` - Maximum number of retry attempts
1090    /// * `retry_delay_seconds` - Delay between retries in seconds
1091    /// * `debug` - Enable debug logging
1092    ///
1093    /// # Returns
1094    ///
1095    ///
1096    /// # Errors
1097    ///
1098    /// Returns an error if the API request fails, the policy is invalid,
1099    /// or authentication/authorization fails.
1100    /// A `Result` containing a tuple of (`SummaryReport`, Option<`compliance_status`>) or an error.
1101    ///
1102    /// # Errors
1103    ///
1104    /// Returns an error if the API request fails, the policy is invalid,
1105    /// or authentication/authorization fails.
1106    /// The `compliance_status` is Some(status) if `break_build` evaluation is needed, None otherwise.
1107    #[allow(clippy::too_many_arguments)]
1108    ///
1109    /// # Errors
1110    ///
1111    /// Returns an error if the API request fails, the policy is invalid,
1112    /// or authentication/authorization fails.
1113    pub async fn get_summary_report_with_policy_retry(
1114        &self,
1115        app_guid: &str,
1116        build_id: Option<&str>,
1117        sandbox_guid: Option<&str>,
1118        max_retries: u32,
1119        retry_delay_seconds: u64,
1120        enable_break_build: bool,
1121    ) -> Result<(SummaryReport, Option<std::borrow::Cow<'static, str>>), PolicyError> {
1122        use std::borrow::Cow;
1123        use tokio::time::{Duration, sleep};
1124
1125        // Cap retry delay to prevent DoS scenarios
1126        let retry_delay_seconds = retry_delay_seconds.min(MAX_RETRY_DELAY_SECONDS);
1127        if retry_delay_seconds > MAX_RETRY_DELAY_SECONDS {
1128            warn!(
1129                "Retry delay capped at {} seconds (requested: {})",
1130                MAX_RETRY_DELAY_SECONDS, retry_delay_seconds
1131            );
1132        }
1133
1134        if enable_break_build && build_id.is_none() {
1135            return Err(PolicyError::InvalidConfig(
1136                "Build ID is required for break build policy evaluation".to_string(),
1137            ));
1138        }
1139
1140        let mut attempts: u32 = 0;
1141        loop {
1142            if attempts == 0 && enable_break_build {
1143                debug!("Checking policy compliance status with retry logic...");
1144            } else if attempts == 0 {
1145                debug!("Getting summary report...");
1146            }
1147
1148            let summary_report = match self
1149                .get_summary_report(app_guid, build_id, sandbox_guid)
1150                .await
1151            {
1152                Ok(report) => report,
1153                Err(PolicyError::InternalServerError) if attempts < 3 => {
1154                    warn!(
1155                        "Summary report API failed with server error (attempt {}/3), retrying in 5 seconds...",
1156                        attempts.saturating_add(1)
1157                    );
1158                    sleep(Duration::from_secs(5)).await;
1159                    attempts = attempts.saturating_add(1);
1160                    continue;
1161                }
1162                Err(e) => return Err(e),
1163            };
1164
1165            // If break_build is not enabled, return immediately with the report
1166            if !enable_break_build {
1167                return Ok((summary_report, None));
1168            }
1169
1170            // For `break_build` evaluation, check if policy compliance status is ready
1171            let status = summary_report.policy_compliance_status.clone();
1172
1173            // If status is ready (not empty and not "Not Assessed"), return both report and status
1174            if !status.is_empty() && status != "Not Assessed" {
1175                debug!("Policy compliance status ready: {status}");
1176                return Ok((summary_report, Some(Cow::Owned(status))));
1177            }
1178
1179            // If we've reached max retries, return current results
1180            attempts = attempts.saturating_add(1);
1181            if attempts >= max_retries {
1182                warn!(
1183                    "Policy evaluation still not ready after {max_retries} attempts. Status: {status}. This may indicate: scan is still in progress, policy evaluation is taking longer than expected, or build results are not yet available"
1184                );
1185                return Ok((summary_report, Some(Cow::Owned(status))));
1186            }
1187
1188            // Log retry attempt
1189            info!(
1190                "Policy evaluation not yet ready (status: '{status}'), retrying in {retry_delay_seconds} seconds... (attempt {attempts}/{max_retries})"
1191            );
1192
1193            // Wait before retrying
1194            sleep(Duration::from_secs(retry_delay_seconds)).await;
1195        }
1196    }
1197
1198    /// Evaluates policy compliance using the summary report API with retry logic
1199    ///
1200    ///
1201    /// # Errors
1202    ///
1203    /// Returns an error if the API request fails, the policy is invalid,
1204    /// or authentication/authorization fails.
1205    /// This function uses the `summary_report` endpoint instead of the buildinfo XML API
1206    /// and will retry when results are not ready yet.
1207    ///
1208    /// # Arguments
1209    ///
1210    /// * `app_guid` - The GUID of the application  
1211    /// * `build_id` - The build ID (GUID) to check compliance for
1212    /// * `sandbox_guid` - Optional sandbox GUID for sandbox scans
1213    /// * `max_retries` - Maximum number of retry attempts (default: 30)
1214    /// * `retry_delay_seconds` - Delay between retries in seconds (default: 10)
1215    ///
1216    /// # Returns
1217    ///
1218    /// A `Result` containing the policy compliance status string or an error.
1219    ///
1220    /// # Errors
1221    ///
1222    /// Returns an error if the API request fails, the policy is invalid,
1223    /// or authentication/authorization fails.
1224    pub async fn evaluate_policy_compliance_via_summary_report_with_retry(
1225        &self,
1226        app_guid: &str,
1227        build_id: &str,
1228        sandbox_guid: Option<&str>,
1229        max_retries: u32,
1230        retry_delay_seconds: u64,
1231    ) -> Result<std::borrow::Cow<'static, str>, PolicyError> {
1232        use std::borrow::Cow;
1233        use tokio::time::{Duration, sleep};
1234
1235        // Cap retry delay to prevent DoS scenarios
1236        let retry_delay_seconds = retry_delay_seconds.min(MAX_RETRY_DELAY_SECONDS);
1237        if retry_delay_seconds > MAX_RETRY_DELAY_SECONDS {
1238            warn!(
1239                "Retry delay capped at {} seconds (requested: {})",
1240                MAX_RETRY_DELAY_SECONDS, retry_delay_seconds
1241            );
1242        }
1243
1244        let mut attempts: u32 = 0;
1245        loop {
1246            let summary_report = self
1247                .get_summary_report(app_guid, Some(build_id), sandbox_guid)
1248                .await?;
1249
1250            // Check if results are ready - look for "Results Ready" or completed status
1251            // The summary report should have policy_compliance_status populated when ready
1252            let status = &summary_report.policy_compliance_status;
1253
1254            // If status is not empty and not "Not Assessed", return the result
1255            if !status.is_empty() && status != "Not Assessed" {
1256                return Ok(Cow::Owned(status.clone()));
1257            }
1258
1259            // If we've reached max retries, return current status
1260            attempts = attempts.saturating_add(1);
1261            if attempts >= max_retries {
1262                warn!(
1263                    "Policy evaluation still not ready after {max_retries} attempts. Status: {status}. This may indicate: scan is still in progress, policy evaluation is taking longer than expected, or build results are not yet available"
1264                );
1265                return Ok(Cow::Owned(status.clone()));
1266            }
1267
1268            // Log retry attempt
1269            info!(
1270                "Policy evaluation not yet ready (status: '{status}'), retrying in {retry_delay_seconds} seconds... (attempt {attempts}/{max_retries})"
1271            );
1272
1273            // Wait before retrying
1274            sleep(Duration::from_secs(retry_delay_seconds)).await;
1275        }
1276    }
1277
1278    /// Evaluates policy compliance using the summary report API (single attempt)
1279    ///
1280    /// This is a convenience method that calls the retry version with default parameters.
1281    ///
1282    /// # Arguments
1283    ///
1284    /// * `app_guid` - The GUID of the application  
1285    /// * `build_id` - The build ID (GUID) to check compliance for
1286    /// * `sandbox_guid` - Optional sandbox GUID for sandbox scans
1287    ///
1288    /// # Returns
1289    ///
1290    /// A `Result` containing the policy compliance status string or an error.
1291    ///
1292    /// # Errors
1293    ///
1294    /// Returns an error if the API request fails, the policy is invalid,
1295    /// or authentication/authorization fails.
1296    pub async fn evaluate_policy_compliance_via_summary_report(
1297        &self,
1298        app_guid: &str,
1299        build_id: &str,
1300        sandbox_guid: Option<&str>,
1301    ) -> Result<std::borrow::Cow<'static, str>, PolicyError> {
1302        self.evaluate_policy_compliance_via_summary_report_with_retry(
1303            app_guid,
1304            build_id,
1305            sandbox_guid,
1306            30,
1307            10,
1308        )
1309        .await
1310    }
1311
1312    /// Initiate a policy scan for an application
1313    ///
1314    /// # Arguments
1315    ///
1316    /// * `request` - The policy scan request
1317    ///
1318    /// # Returns
1319    ///
1320    /// A `Result` containing the scan result or an error.
1321    ///
1322    /// # Errors
1323    ///
1324    /// Returns an error if the API request fails, the policy is invalid,
1325    /// or authentication/authorization fails.
1326    pub async fn initiate_policy_scan(
1327        &self,
1328        request: PolicyScanRequest,
1329    ) -> Result<PolicyScanResult, PolicyError> {
1330        // Validate application_guid to prevent injection
1331        validation::validate_guid(&request.application_guid)
1332            .map_err(|e| PolicyError::InvalidConfig(format!("Invalid application GUID: {e}")))?;
1333
1334        // Validate policy_guid to prevent injection
1335        validation::validate_guid(&request.policy_guid)
1336            .map_err(|e| PolicyError::InvalidConfig(format!("Invalid policy GUID: {e}")))?;
1337
1338        // Validate optional sandbox_guid to prevent injection
1339        if let Some(ref sandbox_guid) = request.sandbox_guid {
1340            validation::validate_guid(sandbox_guid)
1341                .map_err(|e| PolicyError::InvalidConfig(format!("Invalid sandbox GUID: {e}")))?;
1342        }
1343
1344        let endpoint = "/appsec/v1/policy-scans";
1345
1346        let response = self.client.post(endpoint, Some(&request)).await?;
1347
1348        let status = response.status().as_u16();
1349        match status {
1350            200 | 201 => {
1351                let scan_result: PolicyScanResult = response.json().await?;
1352                Ok(scan_result)
1353            }
1354            400 => {
1355                let error_text = response.text().await.unwrap_or_default();
1356                Err(PolicyError::InvalidConfig(error_text))
1357            }
1358            404 => Err(PolicyError::NotFound),
1359            _ => {
1360                let error_text = response.text().await.unwrap_or_default();
1361                Err(PolicyError::Api(VeracodeError::InvalidResponse(format!(
1362                    "HTTP {status}: {error_text}"
1363                ))))
1364            }
1365        }
1366    }
1367
1368    /// Get policy scan status and results
1369    ///
1370    /// # Arguments
1371    ///
1372    /// * `scan_id` - The ID of the policy scan
1373    ///
1374    /// # Returns
1375    ///
1376    /// A `Result` containing the scan result or an error.
1377    ///
1378    /// # Errors
1379    ///
1380    /// Returns an error if the API request fails, the policy is invalid,
1381    /// or authentication/authorization fails.
1382    pub async fn get_policy_scan_result(
1383        &self,
1384        scan_id: u64,
1385    ) -> Result<PolicyScanResult, PolicyError> {
1386        let endpoint = format!("/appsec/v1/policy-scans/{scan_id}");
1387
1388        let response = self.client.get(&endpoint, None).await?;
1389
1390        let status = response.status().as_u16();
1391        match status {
1392            200 => {
1393                let scan_result: PolicyScanResult = response.json().await?;
1394                Ok(scan_result)
1395            }
1396            404 => Err(PolicyError::NotFound),
1397            _ => {
1398                let error_text = response.text().await.unwrap_or_default();
1399                Err(PolicyError::Api(VeracodeError::InvalidResponse(format!(
1400                    "HTTP {status}: {error_text}"
1401                ))))
1402            }
1403        }
1404    }
1405
1406    /// Check if a policy scan is complete
1407    ///
1408    /// # Arguments
1409    ///
1410    /// * `scan_id` - The ID of the policy scan
1411    ///
1412    /// # Returns
1413    ///
1414    /// A `Result` containing a boolean indicating completion status.
1415    ///
1416    /// # Errors
1417    ///
1418    /// Returns an error if the API request fails, the policy is invalid,
1419    /// or authentication/authorization fails.
1420    pub async fn is_policy_scan_complete(&self, scan_id: u64) -> Result<bool, PolicyError> {
1421        let scan_result = self.get_policy_scan_result(scan_id).await?;
1422        Ok(matches!(
1423            scan_result.status,
1424            ScanStatus::Completed | ScanStatus::Failed | ScanStatus::Cancelled
1425        ))
1426    }
1427
1428    /// Gets policy compliance status with automatic fallback from summary report to buildinfo
1429    ///
1430    /// This method first tries the summary report API for full functionality. If access is denied
1431    /// (401/403), it automatically falls back to the getbuildinfo.do XML API for policy compliance
1432    /// status only. This provides the best user experience while maintaining compatibility.
1433    ///
1434    /// # Arguments
1435    ///
1436    /// * `app_guid` - Application GUID (for REST API)
1437    /// * `app_id` - Application numeric ID (for XML API fallback)
1438    /// * `build_id` - Optional build ID
1439    /// * `sandbox_guid` - Optional sandbox GUID (for REST API)
1440    /// * `sandbox_id` - Optional sandbox numeric ID (for XML API fallback)
1441    /// * `max_retries` - Maximum number of retry attempts
1442    /// * `retry_delay_seconds` - Delay between retries in seconds
1443    /// * `enable_break_build` - Whether to enable break build evaluation
1444    /// * `force_buildinfo_api` - Skip summary report and use buildinfo directly
1445    ///
1446    /// # Returns
1447    ///
1448    /// A tuple containing:
1449    ///
1450    /// # Errors
1451    ///
1452    /// Returns an error if the API request fails, the policy is invalid,
1453    /// or authentication/authorization fails.
1454    /// - Optional `SummaryReport` (None if fallback was used)
1455    /// - Policy compliance status string
1456    ///
1457    /// # Errors
1458    ///
1459    /// Returns an error if the API request fails, the policy is invalid,
1460    /// or authentication/authorization fails.
1461    /// - `ApiSource` indicating which API was used
1462    #[allow(clippy::too_many_arguments)]
1463    ///
1464    /// # Errors
1465    ///
1466    /// Returns an error if the API request fails, the policy is invalid,
1467    /// or authentication/authorization fails.
1468    pub async fn get_policy_status_with_fallback(
1469        &self,
1470        app_guid: &str,
1471        app_id: &str,
1472        build_id: Option<&str>,
1473        sandbox_guid: Option<&str>,
1474        sandbox_id: Option<&str>,
1475        max_retries: u32,
1476        retry_delay_seconds: u64,
1477        enable_break_build: bool,
1478        force_buildinfo_api: bool,
1479    ) -> Result<(Option<SummaryReport>, String, ApiSource), PolicyError> {
1480        // Cap retry delay to prevent DoS scenarios
1481        let retry_delay_seconds = retry_delay_seconds.min(MAX_RETRY_DELAY_SECONDS);
1482        if retry_delay_seconds > MAX_RETRY_DELAY_SECONDS {
1483            warn!(
1484                "Retry delay capped at {} seconds (requested: {})",
1485                MAX_RETRY_DELAY_SECONDS, retry_delay_seconds
1486            );
1487        }
1488
1489        if force_buildinfo_api {
1490            // DIRECT PATH: Skip summary report, use getbuildinfo.do directly
1491            debug!("Using getbuildinfo.do API directly (forced via configuration)");
1492            let status = self
1493                .evaluate_policy_compliance_via_buildinfo_with_retry(
1494                    app_id,
1495                    build_id,
1496                    sandbox_id,
1497                    max_retries,
1498                    retry_delay_seconds,
1499                )
1500                .await?;
1501            return Ok((None, status.to_string(), ApiSource::BuildInfo));
1502        }
1503
1504        // FALLBACK PATH: Try summary report first, fallback to getbuildinfo.do
1505        match self
1506            .get_summary_report_with_policy_retry(
1507                app_guid,
1508                build_id,
1509                sandbox_guid,
1510                max_retries,
1511                retry_delay_seconds,
1512                enable_break_build,
1513            )
1514            .await
1515        {
1516            Ok((summary_report, compliance_status)) => {
1517                debug!("Used summary report API successfully");
1518                let status = compliance_status
1519                    .map(|s| s.to_string())
1520                    .unwrap_or_else(|| summary_report.policy_compliance_status.clone());
1521                Ok((Some(summary_report), status, ApiSource::SummaryReport))
1522            }
1523            Err(
1524                ref e @ (PolicyError::Unauthorized
1525                | PolicyError::PermissionDenied
1526                | PolicyError::InternalServerError),
1527            ) => {
1528                match *e {
1529                    PolicyError::InternalServerError => info!(
1530                        "Summary report API server error, falling back to getbuildinfo.do API"
1531                    ),
1532                    PolicyError::Unauthorized | PolicyError::PermissionDenied => {
1533                        info!("Summary report access denied, falling back to getbuildinfo.do API")
1534                    }
1535                    PolicyError::Api(_)
1536                    | PolicyError::NotFound
1537                    | PolicyError::InvalidConfig(_)
1538                    | PolicyError::ScanFailed(_)
1539                    | PolicyError::EvaluationError(_)
1540                    | PolicyError::Timeout => {}
1541                }
1542                let status = self
1543                    .evaluate_policy_compliance_via_buildinfo_with_retry(
1544                        app_id,
1545                        build_id,
1546                        sandbox_id,
1547                        max_retries,
1548                        retry_delay_seconds,
1549                    )
1550                    .await?;
1551                Ok((None, status.to_string(), ApiSource::BuildInfo))
1552            }
1553            Err(e) => Err(e),
1554        }
1555    }
1556
1557    /// Get active policies for the organization
1558    ///
1559    /// # Returns
1560    ///
1561    /// A `Result` containing a list of active policies or an error.
1562    ///
1563    /// # Errors
1564    ///
1565    /// Returns an error if the API request fails, the policy is invalid,
1566    /// or authentication/authorization fails.
1567    pub async fn get_active_policies(&self) -> Result<Vec<SecurityPolicy>, PolicyError> {
1568        // Note: The active/inactive concept may need to be handled differently
1569        // based on the actual API response structure
1570        let policies = self.list_policies(None).await?;
1571        Ok(policies) // Return all policies for now
1572    }
1573}
1574
1575#[cfg(test)]
1576#[allow(clippy::expect_used)]
1577mod tests {
1578    use super::*;
1579
1580    #[test]
1581    fn test_policy_list_params_to_query() {
1582        let params = PolicyListParams {
1583            name: Some("test-policy".to_string()),
1584            is_active: Some(true),
1585            page: Some(1),
1586            size: Some(10),
1587            ..Default::default()
1588        };
1589
1590        let query_params: Vec<_> = params.into();
1591        assert_eq!(query_params.len(), 4);
1592        assert!(query_params.contains(&("name".to_string(), "test-policy".to_string())));
1593        assert!(query_params.contains(&("active".to_string(), "true".to_string())));
1594        assert!(query_params.contains(&("page".to_string(), "1".to_string())));
1595        assert!(query_params.contains(&("size".to_string(), "10".to_string())));
1596    }
1597
1598    #[test]
1599    fn test_policy_error_display() {
1600        let error = PolicyError::NotFound;
1601        assert_eq!(error.to_string(), "Policy not found");
1602
1603        let error = PolicyError::InvalidConfig("test".to_string());
1604        assert_eq!(error.to_string(), "Invalid policy configuration: test");
1605
1606        let error = PolicyError::Timeout;
1607        assert_eq!(error.to_string(), "Policy operation timed out");
1608    }
1609
1610    #[test]
1611    fn test_scan_type_serialization() {
1612        let scan_type = ScanType::Static;
1613        let json = serde_json::to_string(&scan_type).expect("should serialize to json");
1614        assert_eq!(json, "\"static\"");
1615
1616        let deserialized: ScanType = serde_json::from_str(&json).expect("should deserialize json");
1617        assert!(matches!(deserialized, ScanType::Static));
1618    }
1619
1620    #[test]
1621    fn test_policy_compliance_status_serialization() {
1622        let status = PolicyComplianceStatus::Passed;
1623        let json = serde_json::to_string(&status).expect("should serialize to json");
1624        assert_eq!(json, "\"Passed\"");
1625
1626        let deserialized: PolicyComplianceStatus =
1627            serde_json::from_str(&json).expect("should deserialize json");
1628        assert!(matches!(deserialized, PolicyComplianceStatus::Passed));
1629
1630        // Test the special case statuses with spaces
1631        let conditional_pass = PolicyComplianceStatus::ConditionalPass;
1632        let json = serde_json::to_string(&conditional_pass).expect("should serialize to json");
1633        assert_eq!(json, "\"Conditional Pass\"");
1634
1635        let did_not_pass = PolicyComplianceStatus::DidNotPass;
1636        let json = serde_json::to_string(&did_not_pass).expect("should serialize to json");
1637        assert_eq!(json, "\"Did Not Pass\"");
1638    }
1639
1640    #[test]
1641    fn test_break_build_logic() {
1642        assert!(PolicyApi::should_break_build("Did Not Pass"));
1643        assert!(!PolicyApi::should_break_build("Passed"));
1644        assert!(!PolicyApi::should_break_build("Conditional Pass"));
1645        // "Not Assessed" should not break build as the retry logic should handle waiting
1646        // for policy evaluation to complete before reaching this point
1647        assert!(!PolicyApi::should_break_build("Not Assessed"));
1648
1649        assert_eq!(PolicyApi::get_exit_code_for_status("Did Not Pass"), 4);
1650        assert_eq!(PolicyApi::get_exit_code_for_status("Passed"), 0);
1651        assert_eq!(PolicyApi::get_exit_code_for_status("Conditional Pass"), 0);
1652        // "Not Assessed" returns 0 because it should only reach here after retry logic
1653        // has exhausted attempts, indicating a configuration or timing issue rather than policy failure
1654        assert_eq!(PolicyApi::get_exit_code_for_status("Not Assessed"), 0);
1655    }
1656
1657    #[test]
1658    fn test_summary_report_serialization() {
1659        let summary_json = r#"{
1660            "app_id": 2676517,
1661            "app_name": "Verascan Java Test",
1662            "build_id": 54209787,
1663            "policy_compliance_status": "Did Not Pass",
1664            "policy_name": "SecureCode Policy",
1665            "policy_version": 1,
1666            "policy_rules_status": "Did Not Pass",
1667            "grace_period_expired": false,
1668            "scan_overdue": "false",
1669            "is_latest_build": false,
1670            "generation_date": "2025-08-05 10:14:45 UTC",
1671            "last_update_time": "2025-08-05 10:00:51 UTC"
1672        }"#;
1673
1674        let summary: Result<SummaryReport, _> = serde_json::from_str(summary_json);
1675        assert!(summary.is_ok());
1676
1677        let summary = summary.expect("should have summary");
1678        assert_eq!(summary.policy_compliance_status, "Did Not Pass");
1679        assert_eq!(summary.app_name, "Verascan Java Test");
1680        assert_eq!(summary.build_id, 54209787);
1681        assert!(PolicyApi::should_break_build(
1682            &summary.policy_compliance_status
1683        ));
1684    }
1685
1686    #[test]
1687    fn test_export_json_structure() {
1688        // Test the JSON structure that would be exported
1689        let summary_report = SummaryReport {
1690            app_id: 2676517,
1691            app_name: "Test App".to_string(),
1692            build_id: 54209787,
1693            policy_compliance_status: "Passed".to_string(),
1694            policy_name: "Test Policy".to_string(),
1695            policy_version: 1,
1696            policy_rules_status: "Passed".to_string(),
1697            grace_period_expired: false,
1698            scan_overdue: "false".to_string(),
1699            is_latest_build: true,
1700            sandbox_name: Some("test-sandbox".to_string()),
1701            sandbox_id: Some(123456),
1702            generation_date: "2025-08-05 10:14:45 UTC".to_string(),
1703            last_update_time: "2025-08-05 10:00:51 UTC".to_string(),
1704            static_analysis: None,
1705            flaw_status: None,
1706            software_composition_analysis: None,
1707            severity: None,
1708        };
1709
1710        let export_json = serde_json::json!({
1711            "summary_report": summary_report,
1712            "export_metadata": {
1713                "exported_at": "2025-08-05T10:14:45Z",
1714                "tool": "verascan",
1715                "export_type": "summary_report",
1716                "scan_configuration": {
1717                    "autoscan": true,
1718                    "scan_all_nonfatal_top_level_modules": true,
1719                    "include_new_modules": true
1720                }
1721            }
1722        });
1723
1724        // Verify JSON structure
1725        assert!(
1726            export_json
1727                .get("summary_report")
1728                .and_then(|s| s.get("app_name"))
1729                .map(|v| v.is_string())
1730                .unwrap_or(false)
1731        );
1732        assert!(
1733            export_json
1734                .get("summary_report")
1735                .and_then(|s| s.get("policy_compliance_status"))
1736                .map(|v| v.is_string())
1737                .unwrap_or(false)
1738        );
1739        assert!(
1740            export_json
1741                .get("export_metadata")
1742                .and_then(|e| e.get("export_type"))
1743                .map(|v| v.is_string())
1744                .unwrap_or(false)
1745        );
1746        assert_eq!(
1747            export_json
1748                .get("export_metadata")
1749                .and_then(|e| e.get("export_type"))
1750                .and_then(|v| v.as_str())
1751                .expect("should have export_type"),
1752            "summary_report"
1753        );
1754
1755        // Verify the summary report can be serialized and deserialized
1756        let json_string =
1757            serde_json::to_string_pretty(&export_json).expect("should serialize to json");
1758        assert!(json_string.contains("summary_report"));
1759        assert!(json_string.contains("export_metadata"));
1760    }
1761
1762    #[test]
1763    fn test_get_summary_report_with_policy_retry_parameters() {
1764        // Unit tests for the new combined method parameter validation and logic
1765
1766        // Test parameter type validation
1767        let app_guid = "test-app-guid";
1768        let build_id = Some("test-build-id");
1769        let sandbox_guid: Option<&str> = None;
1770        let max_retries = 30u32;
1771        let retry_delay_seconds = 10u64;
1772        let debug = false;
1773        let enable_break_build = true;
1774
1775        // Verify parameter types are correct
1776        assert_eq!(app_guid, "test-app-guid");
1777        assert_eq!(build_id, Some("test-build-id"));
1778        assert_eq!(sandbox_guid, None);
1779        assert_eq!(max_retries, 30);
1780        assert_eq!(retry_delay_seconds, 10);
1781        assert!(!debug);
1782        assert!(enable_break_build);
1783    }
1784
1785    #[test]
1786    fn test_policy_status_ready_logic() {
1787        // Test the logic for determining when policy status is ready
1788        let ready_statuses = vec!["Passed", "Did Not Pass", "Conditional Pass"];
1789        let not_ready_statuses = vec!["", "Not Assessed"];
1790
1791        // Test ready statuses (should not trigger retry)
1792        for status in &ready_statuses {
1793            assert!(
1794                !status.is_empty(),
1795                "Ready status should not be empty: {status}"
1796            );
1797            assert_ne!(
1798                *status, "Not Assessed",
1799                "Ready status should not be 'Not Assessed': {status}"
1800            );
1801        }
1802
1803        // Test not ready statuses (should trigger retry)
1804        for status in &not_ready_statuses {
1805            let is_not_ready = status.is_empty() || *status == "Not Assessed";
1806            assert!(is_not_ready, "Status should trigger retry: '{status}'");
1807        }
1808    }
1809
1810    #[test]
1811    fn test_combined_method_return_types() {
1812        use std::borrow::Cow;
1813
1814        // Test the return type structure of the new combined method
1815        // This verifies the tuple structure is correct
1816
1817        // Test Some compliance status
1818        let compliance_status = Cow::Borrowed("Passed");
1819        assert_eq!(compliance_status.as_ref(), "Passed");
1820
1821        // Test None compliance status (when break_build is disabled)
1822        let compliance_status: Option<Cow<'static, str>> = None;
1823        assert!(compliance_status.is_none());
1824    }
1825
1826    #[test]
1827    fn test_debug_logging_parameters() {
1828        // Test debug parameter handling
1829        let debug_enabled = true;
1830        let debug_disabled = false;
1831
1832        assert!(debug_enabled);
1833        assert!(!debug_disabled);
1834
1835        // Test debug messages would be printed when debug=true
1836        // (Actual output testing would require integration tests)
1837        if debug_enabled {
1838            // Debug messages would be printed - this is just a placeholder
1839        }
1840
1841        if !debug_disabled {
1842            // Debug messages would be printed - this is just a placeholder
1843        }
1844    }
1845
1846    #[test]
1847    fn test_break_build_flag_logic() {
1848        // Test the enable_break_build flag logic
1849        let break_build_enabled = true;
1850        let break_build_disabled = false;
1851
1852        // When break_build is enabled, compliance_status should be Some(_)
1853        if break_build_enabled {
1854            // Would return (summary_report, Some(compliance_status))
1855            let compliance_returned = true;
1856            assert!(compliance_returned);
1857        }
1858
1859        // When break_build is disabled, compliance_status should be None
1860        if !break_build_disabled {
1861            // Would return (summary_report, None)
1862            let compliance_returned = false;
1863            assert!(!compliance_returned);
1864        }
1865    }
1866}
1867
1868/// Security-focused property tests for input validation
1869#[cfg(test)]
1870#[allow(clippy::expect_used)]
1871mod validation_proptests {
1872    use super::validation::*;
1873    use proptest::prelude::*;
1874
1875    // Strategy for valid GUIDs (32 hex chars with optional hyphens)
1876    fn valid_guid_strategy() -> impl Strategy<Value = String> {
1877        prop_oneof![
1878            // Standard UUID format (8-4-4-4-12)
1879            prop::string::string_regex(
1880                "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"
1881            )
1882            .expect("valid regex for UUID"),
1883            // No hyphens (32 hex chars)
1884            prop::string::string_regex("[0-9a-fA-F]{32}").expect("valid regex for hex string"),
1885        ]
1886    }
1887
1888    // Strategy for invalid GUIDs (injection attacks, path traversal, malformed)
1889    fn invalid_guid_strategy() -> impl Strategy<Value = String> {
1890        prop_oneof![
1891            // Path traversal attempts
1892            Just("../../../etc/passwd".to_string()),
1893            Just("..\\..\\windows\\system32".to_string()),
1894            prop::string::string_regex("[0-9a-f]{8}\\.\\./{0,20}[0-9a-f]{8}")
1895                .expect("valid regex for path traversal with guid"),
1896            // URL parameter injection
1897            Just("abc123?param=value".to_string()),
1898            Just("abc123&admin=true".to_string()),
1899            Just("abc123#fragment".to_string()),
1900            // Non-hex characters
1901            prop::string::string_regex("[0-9a-zA-Z!@#$%^&*()]{32}")
1902                .expect("valid regex for non-hex chars"),
1903            // Wrong length
1904            prop::string::string_regex("[0-9a-f]{1,31}").expect("valid regex for too short"),
1905            prop::string::string_regex("[0-9a-f]{33,100}").expect("valid regex for too long"),
1906            // SQL injection attempts
1907            Just("abc123'; DROP TABLE users; --".to_string()),
1908            // Command injection
1909            Just("abc123; rm -rf /".to_string()),
1910            Just("abc123 | cat /etc/passwd".to_string()),
1911            // Null byte injection
1912            Just("abc123\0malicious".to_string()),
1913        ]
1914    }
1915
1916    // Strategy for valid identifiers (alphanumeric, hyphens, underscores)
1917    fn valid_identifier_strategy() -> impl Strategy<Value = String> {
1918        prop::string::string_regex("[a-zA-Z0-9_-]{1,256}").expect("valid regex for identifier")
1919    }
1920
1921    // Strategy for invalid identifiers
1922    fn invalid_identifier_strategy() -> impl Strategy<Value = String> {
1923        prop_oneof![
1924            // Empty string
1925            Just("".to_string()),
1926            // Path traversal
1927            Just("../etc/passwd".to_string()),
1928            Just("..\\windows\\system32".to_string()),
1929            // URL injection
1930            Just("test?param=value".to_string()),
1931            Just("test&admin=true".to_string()),
1932            Just("test#fragment".to_string()),
1933            // Special characters
1934            prop::string::string_regex(
1935                "[a-zA-Z0-9]{1,10}[@#$%^&*()+=\\[\\]{}|;:'\"<>,./\\\\?]+[a-zA-Z0-9]{0,10}"
1936            )
1937            .expect("valid regex for special chars"),
1938            // SQL injection
1939            Just("test'; DROP TABLE users; --".to_string()),
1940            // Command injection
1941            Just("test; rm -rf /".to_string()),
1942            // Unicode control characters
1943            Just("test\u{0000}injection".to_string()),
1944            Just("test\u{001F}control".to_string()),
1945        ]
1946    }
1947
1948    proptest! {
1949        #![proptest_config(ProptestConfig {
1950            cases: if cfg!(miri) { 5 } else { 1000 },
1951            failure_persistence: None,
1952            .. ProptestConfig::default()
1953        })]
1954
1955        #[test]
1956        fn proptest_valid_guids_accepted(guid in valid_guid_strategy()) {
1957            prop_assert!(validate_guid(&guid).is_ok(),
1958                "Valid GUID rejected: {}", guid);
1959        }
1960
1961        #[test]
1962        fn proptest_invalid_guids_rejected(guid in invalid_guid_strategy()) {
1963            prop_assert!(validate_guid(&guid).is_err(),
1964                "Invalid GUID accepted: {}", guid);
1965        }
1966
1967        #[test]
1968        fn proptest_guid_no_path_traversal(
1969            prefix in prop::string::string_regex("[0-9a-f]{8}").expect("valid regex for guid prefix")
1970        ) {
1971            let with_traversal = format!("{}/../../../etc/passwd", prefix);
1972            prop_assert!(validate_guid(&with_traversal).is_err(),
1973                "Path traversal GUID accepted: {}", with_traversal);
1974
1975            let with_backslash = format!("{}\\..\\windows", prefix);
1976            prop_assert!(validate_guid(&with_backslash).is_err(),
1977                "Backslash traversal GUID accepted: {}", with_backslash);
1978        }
1979
1980        #[test]
1981        fn proptest_guid_no_url_injection(
1982            prefix in prop::string::string_regex("[0-9a-f]{16}").expect("valid regex for guid prefix")
1983        ) {
1984            let with_query = format!("{}?admin=true", prefix);
1985            prop_assert!(validate_guid(&with_query).is_err(),
1986                "URL query injection accepted: {}", with_query);
1987
1988            let with_ampersand = format!("{}&param=value", prefix);
1989            prop_assert!(validate_guid(&with_ampersand).is_err(),
1990                "URL parameter injection accepted: {}", with_ampersand);
1991
1992            let with_fragment = format!("{}#section", prefix);
1993            prop_assert!(validate_guid(&with_fragment).is_err(),
1994                "URL fragment injection accepted: {}", with_fragment);
1995        }
1996
1997        #[test]
1998        fn proptest_valid_identifiers_accepted(id in valid_identifier_strategy()) {
1999            prop_assert!(validate_identifier(&id).is_ok(),
2000                "Valid identifier rejected: {}", id);
2001        }
2002
2003        #[test]
2004        fn proptest_invalid_identifiers_rejected(id in invalid_identifier_strategy()) {
2005            prop_assert!(validate_identifier(&id).is_err(),
2006                "Invalid identifier accepted: {}", id);
2007        }
2008
2009        #[test]
2010        fn proptest_identifier_no_path_traversal(
2011            base in prop::string::string_regex("[a-zA-Z0-9]{5,10}").expect("valid regex for base id")
2012        ) {
2013            let with_dots = format!("{}/../test", base);
2014            prop_assert!(validate_identifier(&with_dots).is_err(),
2015                "Path traversal identifier accepted: {}", with_dots);
2016
2017            let with_slashes = format!("{}/etc/passwd", base);
2018            prop_assert!(validate_identifier(&with_slashes).is_err(),
2019                "Forward slash identifier accepted: {}", with_slashes);
2020        }
2021
2022        #[test]
2023        fn proptest_identifier_no_url_injection(
2024            base in prop::string::string_regex("[a-zA-Z0-9_-]{5,20}").expect("valid regex for base id")
2025        ) {
2026            let with_query = format!("{}?param=value", base);
2027            prop_assert!(validate_identifier(&with_query).is_err(),
2028                "URL query injection in identifier accepted: {}", with_query);
2029
2030            let with_ampersand = format!("{}&admin=true", base);
2031            prop_assert!(validate_identifier(&with_ampersand).is_err(),
2032                "Ampersand injection in identifier accepted: {}", with_ampersand);
2033        }
2034
2035        #[test]
2036        fn proptest_identifier_no_special_chars(
2037            alphanumeric in prop::string::string_regex("[a-zA-Z0-9]{3,10}").expect("valid regex for alphanumeric"),
2038            special_char in "[!@#$%^&*()+=\\[\\]{}|;:'\"<>,./\\\\?]"
2039        ) {
2040            let with_special = format!("{}{}{}", alphanumeric, special_char, alphanumeric);
2041            prop_assert!(validate_identifier(&with_special).is_err(),
2042                "Identifier with special char accepted: {}", with_special);
2043        }
2044    }
2045}
2046
2047/// Security-focused property tests for query parameter handling
2048#[cfg(test)]
2049#[allow(clippy::expect_used)]
2050mod query_param_proptests {
2051    use super::*;
2052    use proptest::prelude::*;
2053
2054    proptest! {
2055        #![proptest_config(ProptestConfig {
2056            cases: if cfg!(miri) { 5 } else { 1000 },
2057            failure_persistence: None,
2058            .. ProptestConfig::default()
2059        })]
2060
2061        #[test]
2062        fn proptest_policy_list_params_no_duplicate_keys(
2063            name in prop::option::of(prop::string::string_regex("[a-zA-Z0-9 _-]{1,50}").expect("valid regex for policy name")),
2064            policy_type in prop::option::of(prop::string::string_regex("[A-Z]{1,20}").expect("valid regex for policy type")),
2065            is_active in prop::option::of(any::<bool>()),
2066            default_only in prop::option::of(any::<bool>()),
2067            page in prop::option::of(0u32..10000u32),
2068            size in prop::option::of(1u32..1000u32)
2069        ) {
2070            let params = PolicyListParams {
2071                name,
2072                policy_type,
2073                is_active,
2074                default_only,
2075                page,
2076                size,
2077            };
2078
2079            let query_params = params.to_query_params();
2080
2081            // Verify no duplicate keys
2082            let mut seen_keys = std::collections::HashSet::new();
2083            for (key, _) in query_params.iter() {
2084                prop_assert!(!seen_keys.contains(key),
2085                    "Duplicate query parameter key: {}", key);
2086                seen_keys.insert(key.clone());
2087            }
2088        }
2089
2090        #[test]
2091        fn proptest_policy_list_params_valid_values(
2092            page in 0u32..10000u32,
2093            size in 1u32..1000u32
2094        ) {
2095            let params = PolicyListParams {
2096                name: None,
2097                policy_type: None,
2098                is_active: Some(true),
2099                default_only: Some(false),
2100                page: Some(page),
2101                size: Some(size),
2102            };
2103
2104            let query_params = params.to_query_params();
2105
2106            // Find page and size params
2107            let page_param = query_params.iter().find(|(k, _)| k == "page");
2108            let size_param = query_params.iter().find(|(k, _)| k == "size");
2109
2110            if let Some((_, page_value)) = page_param {
2111                prop_assert!(page_value.parse::<u32>().is_ok(),
2112                    "Invalid page value: {}", page_value);
2113            }
2114
2115            if let Some((_, size_value)) = size_param {
2116                prop_assert!(size_value.parse::<u32>().is_ok(),
2117                    "Invalid size value: {}", size_value);
2118            }
2119        }
2120
2121        #[test]
2122        fn proptest_policy_list_params_string_sanitization(
2123            name in prop::string::string_regex("[a-zA-Z0-9 &=;?#]{1,100}").expect("valid regex for name with special chars")
2124        ) {
2125            let params = PolicyListParams {
2126                name: Some(name.clone()),
2127                policy_type: None,
2128                is_active: None,
2129                default_only: None,
2130                page: None,
2131                size: None,
2132            };
2133
2134            let query_params = params.to_query_params();
2135
2136            // Find the name parameter
2137            let name_param = query_params.iter().find(|(k, _)| k == "name");
2138
2139            if let Some((_, value)) = name_param {
2140                // The value should be the input (encoding happens at HTTP layer)
2141                prop_assert_eq!(value, &name);
2142            }
2143        }
2144    }
2145}
2146
2147/// Security-focused property tests for integer operations
2148#[cfg(test)]
2149#[allow(clippy::expect_used)]
2150mod integer_safety_proptests {
2151    use super::*;
2152    use proptest::prelude::*;
2153
2154    proptest! {
2155        #![proptest_config(ProptestConfig {
2156            cases: if cfg!(miri) { 5 } else { 1000 },
2157            failure_persistence: None,
2158            .. ProptestConfig::default()
2159        })]
2160
2161        #[test]
2162        fn proptest_retry_delay_capped(delay in 0u64..u64::MAX) {
2163            let capped = delay.min(MAX_RETRY_DELAY_SECONDS);
2164            prop_assert!(capped <= MAX_RETRY_DELAY_SECONDS,
2165                "Retry delay not properly capped: {}", capped);
2166            prop_assert!(capped <= 300,
2167                "Retry delay exceeds 5 minutes: {}", capped);
2168        }
2169
2170        #[test]
2171        fn proptest_retry_attempts_no_overflow(attempts in 0u32..u32::MAX - 1) {
2172            let incremented = attempts.saturating_add(1);
2173            prop_assert!(incremented >= attempts,
2174                "Retry counter overflowed: {} + 1 = {}", attempts, incremented);
2175            // Note: incremented is always <= u32::MAX since it's a u32
2176        }
2177
2178        #[test]
2179        fn proptest_max_retries_reasonable(max_retries in 0u32..1000u32) {
2180            // Verify max_retries used in comparisons doesn't overflow
2181            let test_attempts = max_retries.saturating_add(1);
2182            prop_assert!(test_attempts > max_retries || max_retries == u32::MAX,
2183                "Max retries comparison could overflow");
2184        }
2185
2186        #[test]
2187        fn proptest_retry_delay_multiplication_safe(
2188            retries in 0u32..100u32,
2189            delay in 0u64..MAX_RETRY_DELAY_SECONDS
2190        ) {
2191            // Verify that retry delay calculations don't overflow
2192            let total_delay = (retries as u64).saturating_mul(delay);
2193            // Total delay should be reasonable (less than 1 day)
2194            prop_assert!(total_delay <= 86400,
2195                "Total delay unreasonably large: {} seconds", total_delay);
2196        }
2197    }
2198}
2199
2200/// Security-focused property tests for string handling
2201#[cfg(test)]
2202#[allow(clippy::expect_used)]
2203mod string_safety_proptests {
2204    use super::*;
2205    use proptest::prelude::*;
2206
2207    proptest! {
2208        #![proptest_config(ProptestConfig {
2209            cases: if cfg!(miri) { 5 } else { 1000 },
2210            failure_persistence: None,
2211            .. ProptestConfig::default()
2212        })]
2213
2214        #[test]
2215        fn proptest_policy_status_string_utf8_safe(
2216            status in prop::string::string_regex("[ -~]{1,50}").expect("valid regex for ASCII status")
2217        ) {
2218            // All status strings should be valid UTF-8
2219            prop_assert!(status.is_ascii() || status.chars().all(|c| !c.is_control()),
2220                "Status string contains control characters");
2221        }
2222
2223        #[test]
2224        fn proptest_guid_formatting_safe(
2225            guid in prop::string::string_regex("[0-9a-f]{32}").expect("valid regex for guid")
2226        ) {
2227            let endpoint = format!("/appsec/v1/policies/{}", guid);
2228
2229            // Verify no format string injection
2230            prop_assert!(!endpoint.contains("{}"),
2231                "Format string injection in endpoint: {}", endpoint);
2232            prop_assert!(!endpoint.contains("%s"),
2233                "Printf-style injection in endpoint: {}", endpoint);
2234
2235            // Verify proper structure
2236            prop_assert!(endpoint.starts_with("/appsec/v1/policies/"),
2237                "Malformed endpoint structure: {}", endpoint);
2238            #[allow(clippy::arithmetic_side_effects)]
2239            {
2240                prop_assert_eq!(endpoint.len(), 20 + guid.len(),
2241                    "Unexpected endpoint length");
2242            }
2243        }
2244
2245        #[test]
2246        fn proptest_error_message_no_injection(
2247            user_input in prop::string::string_regex("[ -~]{1,100}").expect("valid regex for user input")
2248        ) {
2249            let error_msg = format!("Invalid GUID: {}", user_input);
2250
2251            // Error messages should be safe from injection
2252            prop_assert!(error_msg.starts_with("Invalid GUID: "),
2253                "Error message structure corrupted");
2254            prop_assert!(!error_msg.contains('\0'),
2255                "Null byte in error message");
2256            prop_assert!(error_msg.len() >= 14,
2257                "Error message unexpectedly short");
2258        }
2259
2260        #[test]
2261        fn proptest_compliance_status_values_safe(
2262            status in prop_oneof![
2263                Just("Passed".to_string()),
2264                Just("Did Not Pass".to_string()),
2265                Just("Conditional Pass".to_string()),
2266                Just("Not Assessed".to_string()),
2267                Just("Calculating...".to_string()),
2268            ]
2269        ) {
2270            // All status values should be alphanumeric or spaces
2271            prop_assert!(status.chars().all(|c| c.is_alphanumeric() || c.is_whitespace() || c == '.'),
2272                "Status contains unexpected characters: {}", status);
2273
2274            // Should not break build logic
2275            let should_break = PolicyApi::should_break_build(&status);
2276            if status == "Did Not Pass" {
2277                prop_assert!(should_break, "Did Not Pass should break build");
2278            } else {
2279                prop_assert!(!should_break, "{} should not break build", status);
2280            }
2281        }
2282    }
2283}
2284
2285/// Security-focused property tests for URL/endpoint construction
2286#[cfg(test)]
2287#[allow(clippy::expect_used)]
2288mod endpoint_safety_proptests {
2289    use proptest::prelude::*;
2290
2291    proptest! {
2292        #![proptest_config(ProptestConfig {
2293            cases: if cfg!(miri) { 5 } else { 1000 },
2294            failure_persistence: None,
2295            .. ProptestConfig::default()
2296        })]
2297
2298        #[test]
2299        fn proptest_scan_id_endpoint_no_injection(scan_id in 0u64..u64::MAX) {
2300            let endpoint = format!("/appsec/v1/policy-scans/{}", scan_id);
2301
2302            // Verify no injection
2303            prop_assert!(endpoint.starts_with("/appsec/v1/policy-scans/"),
2304                "Endpoint prefix corrupted: {}", endpoint);
2305            prop_assert!(!endpoint.contains(".."),
2306                "Path traversal in endpoint: {}", endpoint);
2307            prop_assert!(!endpoint.contains("//"),
2308                "Double slash in endpoint: {}", endpoint);
2309
2310            // Verify scan_id is properly formatted as number
2311            let scan_id_str = endpoint.get(24..).unwrap_or("");
2312            prop_assert!(scan_id_str.parse::<u64>().is_ok(),
2313                "Invalid scan_id in endpoint: {}", scan_id_str);
2314        }
2315
2316        #[test]
2317        fn proptest_app_guid_endpoint_validated(
2318            guid_part in prop::string::string_regex("[0-9a-f]{32}").expect("valid regex for guid")
2319        ) {
2320            // Simulate what happens after validation
2321            let endpoint = format!("/appsec/v2/applications/{}/summary_report", guid_part);
2322
2323            // Verify structure
2324            prop_assert!(endpoint.starts_with("/appsec/v2/applications/"),
2325                "Invalid endpoint prefix");
2326            prop_assert!(endpoint.ends_with("/summary_report"),
2327                "Invalid endpoint suffix");
2328            prop_assert!(!endpoint.contains(".."),
2329                "Path traversal in endpoint");
2330        }
2331
2332        #[test]
2333        fn proptest_query_string_no_injection(
2334            build_id in prop::string::string_regex("[a-zA-Z0-9_-]{1,64}").expect("valid regex for build id"),
2335            sandbox_guid in prop::string::string_regex("[0-9a-f]{32}").expect("valid regex for sandbox guid")
2336        ) {
2337            // Simulate query parameter construction
2338            let query_params = [
2339                ("build_id".to_string(), build_id.clone()),
2340                ("context".to_string(), sandbox_guid.clone())
2341            ];
2342
2343            // Verify no duplicate keys
2344            let keys: Vec<_> = query_params.iter().map(|(k, _)| k).collect();
2345            prop_assert_eq!(keys.len(), 2, "Wrong number of query params");
2346            prop_assert_eq!(keys.first().map(|s| s.as_str()), Some("build_id"), "Wrong first key");
2347            prop_assert_eq!(keys.get(1).map(|s| s.as_str()), Some("context"), "Wrong second key");
2348
2349            // Verify values are not corrupted
2350            if let Some((_, val)) = query_params.first() {
2351                prop_assert_eq!(val, &build_id, "build_id value corrupted");
2352            }
2353            if let Some((_, val)) = query_params.get(1) {
2354                prop_assert_eq!(val, &sandbox_guid, "sandbox_guid value corrupted");
2355            }
2356        }
2357    }
2358}
2359
2360/// Memory safety tests for data structure operations
2361#[cfg(test)]
2362#[allow(clippy::expect_used)]
2363mod memory_safety_proptests {
2364    use super::*;
2365    use proptest::prelude::*;
2366
2367    proptest! {
2368        #![proptest_config(ProptestConfig {
2369            cases: if cfg!(miri) { 5 } else { 1000 },
2370            failure_persistence: None,
2371            .. ProptestConfig::default()
2372        })]
2373
2374        #[test]
2375        fn proptest_policy_list_params_from_owned(
2376            name in prop::option::of(prop::string::string_regex("[a-zA-Z0-9_-]{1,50}").expect("valid regex for name")),
2377            page in prop::option::of(0u32..1000u32),
2378            size in prop::option::of(1u32..100u32)
2379        ) {
2380            let params = PolicyListParams {
2381                name: name.clone(),
2382                policy_type: None,
2383                is_active: None,
2384                default_only: None,
2385                page,
2386                size,
2387            };
2388
2389            // Convert owned to query params (should move, not clone)
2390            let query_params: Vec<(String, String)> = params.into();
2391
2392            // Verify structure is intact after move
2393            prop_assert!(query_params.len() <= 6, "Too many query params");
2394
2395            // If name was Some, it should be in params
2396            if name.is_some() {
2397                let has_name = query_params.iter().any(|(k, _)| k == "name");
2398                prop_assert!(has_name, "Name parameter lost after move");
2399            }
2400        }
2401
2402        #[test]
2403        fn proptest_policy_list_params_from_ref(
2404            name in prop::option::of(prop::string::string_regex("[a-zA-Z0-9_-]{1,50}").expect("valid regex for name")),
2405            page in prop::option::of(0u32..1000u32)
2406        ) {
2407            let params = PolicyListParams {
2408                name: name.clone(),
2409                policy_type: None,
2410                is_active: None,
2411                default_only: None,
2412                page,
2413                size: None,
2414            };
2415
2416            // Convert by reference (should clone)
2417            let query_params: Vec<(String, String)> = Vec::from(&params);
2418
2419            // Original should still be valid
2420            let query_params2 = params.to_query_params();
2421
2422            // Both should be equal
2423            prop_assert_eq!(query_params, query_params2,
2424                "Reference conversion differs from method call");
2425        }
2426
2427        #[test]
2428        fn proptest_vec_allocation_reasonable(
2429            param_count in 1usize..10usize
2430        ) {
2431            let mut params = Vec::new();
2432
2433            for i in 0..param_count {
2434                params.push((format!("key{}", i), format!("value{}", i)));
2435            }
2436
2437            // Verify no excessive allocations
2438            prop_assert_eq!(params.len(), param_count,
2439                "Parameter count mismatch");
2440            prop_assert!(params.capacity() >= param_count,
2441                "Insufficient capacity");
2442            // Capacity should be reasonable (not wildly over-allocated)
2443            prop_assert!(params.capacity() < param_count.saturating_mul(10),
2444                "Excessive capacity: {} for {} items", params.capacity(), param_count);
2445        }
2446    }
2447}