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/// Represents a security policy in the Veracode platform
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct SecurityPolicy {
16    /// Globally unique identifier for the policy
17    pub guid: String,
18    /// Policy name
19    pub name: String,
20    /// Policy description
21    pub description: Option<String>,
22    /// Policy type (CUSTOMER, BUILTIN, STANDARD)
23    #[serde(rename = "type")]
24    pub policy_type: String,
25    /// Policy version number
26    pub version: u32,
27    /// When the policy was created
28    pub created: Option<DateTime<Utc>>,
29    /// Who modified the policy last
30    pub modified_by: Option<String>,
31    /// Organization ID this policy belongs to
32    pub organization_id: Option<u64>,
33    /// Policy category (APPLICATION, etc.)
34    pub category: String,
35    /// Whether this is a vendor policy
36    pub vendor_policy: bool,
37    /// Scan frequency rules
38    pub scan_frequency_rules: Vec<ScanFrequencyRule>,
39    /// Finding rules for the policy
40    pub finding_rules: Vec<FindingRule>,
41    /// Custom severities defined for this policy
42    pub custom_severities: Vec<serde_json::Value>,
43    /// Grace periods for different severity levels
44    pub sev5_grace_period: u32,
45    pub sev4_grace_period: u32,
46    pub sev3_grace_period: u32,
47    pub sev2_grace_period: u32,
48    pub sev1_grace_period: u32,
49    pub sev0_grace_period: u32,
50    /// Score grace period
51    pub score_grace_period: u32,
52    /// SCA blacklist grace period
53    pub sca_blacklist_grace_period: u32,
54    /// SCA grace periods (nullable)
55    pub sca_grace_periods: Option<serde_json::Value>,
56    /// Evaluation date
57    pub evaluation_date: Option<DateTime<Utc>>,
58    /// Evaluation date type
59    pub evaluation_date_type: Option<String>,
60    /// Policy capabilities
61    pub capabilities: Vec<String>,
62    /// Links for API navigation
63    #[serde(rename = "_links")]
64    pub links: Option<serde_json::Value>,
65}
66
67/// Policy compliance status (XML API values from getbuildinfo.do)
68#[derive(Debug, Clone, Serialize, Deserialize)]
69#[serde(rename_all = "PascalCase")]
70pub enum PolicyComplianceStatus {
71    /// Application passes all policy requirements
72    Passed,
73    /// Application passes with conditional requirements  
74    #[serde(rename = "Conditional Pass")]
75    ConditionalPass,
76    /// Application fails policy requirements (triggers build break)
77    #[serde(rename = "Did Not Pass")]
78    DidNotPass,
79    /// Policy compliance status has not been assessed
80    #[serde(rename = "Not Assessed")]
81    NotAssessed,
82}
83
84/// Individual policy rule
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct PolicyRule {
87    /// Rule identifier
88    pub id: String,
89    /// Rule name
90    pub name: String,
91    /// Rule description
92    pub description: Option<String>,
93    /// Rule type (e.g., severity, category)
94    pub rule_type: String,
95    /// Rule criteria
96    pub criteria: Option<serde_json::Value>,
97    /// Whether the rule is enabled
98    pub enabled: bool,
99    /// Rule severity level
100    pub severity: Option<String>,
101}
102
103/// Policy compliance thresholds
104#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct PolicyThresholds {
106    /// Maximum allowed Very High severity flaws
107    pub very_high: Option<u32>,
108    /// Maximum allowed High severity flaws
109    pub high: Option<u32>,
110    /// Maximum allowed Medium severity flaws
111    pub medium: Option<u32>,
112    /// Maximum allowed Low severity flaws
113    pub low: Option<u32>,
114    /// Maximum allowed Very Low severity flaws
115    pub very_low: Option<u32>,
116    /// Overall score threshold
117    pub score_threshold: Option<f64>,
118}
119
120/// Policy scan request
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct PolicyScanRequest {
123    /// Application GUID to scan
124    pub application_guid: String,
125    /// Policy GUID to apply
126    pub policy_guid: String,
127    /// Scan type (static, dynamic, sca)
128    pub scan_type: ScanType,
129    /// Optional sandbox GUID for sandbox scans
130    pub sandbox_guid: Option<String>,
131    /// Scan configuration
132    pub config: Option<PolicyScanConfig>,
133}
134
135/// Types of scans for policy evaluation
136#[derive(Debug, Clone, Serialize, Deserialize)]
137#[serde(rename_all = "lowercase")]
138pub enum ScanType {
139    /// Static Application Security Testing
140    Static,
141    /// Dynamic Application Security Testing
142    Dynamic,
143    /// Software Composition Analysis
144    Sca,
145    /// Manual penetration testing
146    Manual,
147}
148
149/// Configuration for policy scans
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct PolicyScanConfig {
152    /// Whether to auto-submit the scan
153    pub auto_submit: Option<bool>,
154    /// Scan timeout in minutes
155    pub timeout_minutes: Option<u32>,
156    /// Include third-party components
157    pub include_third_party: Option<bool>,
158    /// Scan modules to include
159    pub modules: Option<Vec<String>>,
160}
161
162/// Policy scan result
163#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct PolicyScanResult {
165    /// Scan identifier
166    pub scan_id: u64,
167    /// Application GUID
168    pub application_guid: String,
169    /// Policy GUID used for evaluation
170    pub policy_guid: String,
171    /// Scan status
172    pub status: ScanStatus,
173    /// Scan type
174    pub scan_type: ScanType,
175    /// When the scan was initiated
176    pub started: DateTime<Utc>,
177    /// When the scan completed
178    pub completed: Option<DateTime<Utc>>,
179    /// Policy compliance result
180    pub compliance_result: Option<PolicyComplianceResult>,
181    /// Findings summary
182    pub findings_summary: Option<FindingsSummary>,
183    /// URL to detailed results
184    pub results_url: Option<String>,
185}
186
187/// Status of a policy scan
188#[derive(Debug, Clone, Serialize, Deserialize)]
189#[serde(rename_all = "UPPERCASE")]
190pub enum ScanStatus {
191    /// Scan is queued for processing
192    Queued,
193    /// Scan is currently running
194    Running,
195    /// Scan completed successfully
196    Completed,
197    /// Scan failed
198    Failed,
199    /// Scan was cancelled
200    Cancelled,
201    /// Scan timed out
202    Timeout,
203}
204
205/// Policy compliance evaluation result
206#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct PolicyComplianceResult {
208    /// Overall compliance status
209    pub status: PolicyComplianceStatus,
210    /// Compliance score (0-100)
211    pub score: Option<f64>,
212    /// Whether scan passed policy requirements
213    pub passed: bool,
214    /// Detailed compliance breakdown
215    pub breakdown: Option<ComplianceBreakdown>,
216    /// Policy violations found
217    pub violations: Option<Vec<PolicyViolation>>,
218    /// Compliance summary message
219    pub summary: Option<String>,
220}
221
222/// Detailed compliance breakdown by severity
223#[derive(Debug, Clone, Serialize, Deserialize)]
224pub struct ComplianceBreakdown {
225    /// Very High severity findings count
226    pub very_high: u32,
227    /// High severity findings count
228    pub high: u32,
229    /// Medium severity findings count
230    pub medium: u32,
231    /// Low severity findings count
232    pub low: u32,
233    /// Very Low severity findings count
234    pub very_low: u32,
235    /// Total findings count
236    pub total: u32,
237}
238
239/// Policy violation details
240#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct PolicyViolation {
242    /// Violation type
243    pub violation_type: String,
244    /// Severity of the violation
245    pub severity: String,
246    /// Description of the violation
247    pub description: String,
248    /// Count of this violation type
249    pub count: u32,
250    /// Threshold that was exceeded
251    pub threshold_exceeded: Option<u32>,
252    /// Actual value that caused the violation
253    pub actual_value: Option<u32>,
254}
255
256/// Summary of findings from a policy scan
257#[derive(Debug, Clone, Serialize, Deserialize)]
258pub struct FindingsSummary {
259    /// Total number of findings
260    pub total: u32,
261    /// Number of open findings
262    pub open: u32,
263    /// Number of fixed findings
264    pub fixed: u32,
265    /// Number of findings by severity
266    pub by_severity: HashMap<String, u32>,
267    /// Number of findings by category
268    pub by_category: Option<HashMap<String, u32>>,
269}
270
271///
272/// # Errors
273///
274/// Returns an error if the API request fails, the resource is not found,
275/// or authentication/authorization fails.
276/// Summary report data structure matching Veracode `summary_report` API response
277#[derive(Debug, Clone, Serialize, Deserialize)]
278pub struct SummaryReport {
279    /// Application ID
280    pub app_id: u64,
281    /// Application name
282    pub app_name: String,
283    /// Build ID
284    pub build_id: u64,
285    /// Policy compliance status (e.g., "Did Not Pass", "Passed", "Conditional Pass")
286    pub policy_compliance_status: String,
287    /// Policy name
288    pub policy_name: String,
289    /// Policy version
290    pub policy_version: u32,
291    /// Whether the policy rules status passed
292    pub policy_rules_status: String,
293    /// Whether grace period expired
294    pub grace_period_expired: bool,
295    /// Whether scan is overdue
296    pub scan_overdue: String,
297    /// Whether this is the latest build
298    pub is_latest_build: bool,
299    /// Sandbox name (optional)
300    pub sandbox_name: Option<String>,
301    /// Sandbox ID (optional)
302    pub sandbox_id: Option<u64>,
303    /// Generation date
304    pub generation_date: String,
305    /// Last update time
306    pub last_update_time: String,
307    /// Static analysis summary
308    #[serde(rename = "static-analysis")]
309    pub static_analysis: Option<StaticAnalysisSummary>,
310    /// Flaw status summary
311    #[serde(rename = "flaw-status")]
312    pub flaw_status: Option<FlawStatusSummary>,
313    /// Software composition analysis summary
314    pub software_composition_analysis: Option<ScaSummary>,
315    /// Severity breakdown
316    pub severity: Option<Vec<SeverityLevel>>,
317}
318
319/// Static analysis summary from summary report
320#[derive(Debug, Clone, Serialize, Deserialize)]
321pub struct StaticAnalysisSummary {
322    /// Rating (e.g., "A", "B", "C")
323    pub rating: Option<String>,
324    /// Score (0-100)
325    pub score: Option<u32>,
326    /// Mitigated rating
327    pub mitigated_rating: Option<String>,
328    /// Mitigated score
329    pub mitigated_score: Option<u32>,
330    /// Analysis size in bytes
331    pub analysis_size_bytes: Option<u64>,
332    /// Engine version
333    pub engine_version: Option<String>,
334    /// Published date
335    pub published_date: Option<String>,
336    /// Version/build identifier
337    pub version: Option<String>,
338}
339
340/// Flaw status summary from summary report
341#[derive(Debug, Clone, Serialize, Deserialize)]
342pub struct FlawStatusSummary {
343    /// New flaws
344    pub new: u32,
345    /// Reopened flaws
346    pub reopen: u32,
347    /// Open flaws
348    pub open: u32,
349    /// Fixed flaws
350    pub fixed: u32,
351    /// Total flaws
352    pub total: u32,
353    /// Not mitigated flaws
354    pub not_mitigated: u32,
355}
356
357/// Software Composition Analysis summary
358#[derive(Debug, Clone, Serialize, Deserialize)]
359pub struct ScaSummary {
360    /// Third party components count
361    pub third_party_components: u32,
362    /// Whether violates policy
363    pub violate_policy: bool,
364    /// Components that violated policy
365    pub components_violated_policy: u32,
366}
367
368/// Severity level breakdown from summary report
369#[derive(Debug, Clone, Serialize, Deserialize)]
370pub struct SeverityLevel {
371    /// Severity level (0-5)
372    pub level: u32,
373    /// Categories for this severity level
374    pub category: Vec<CategorySummary>,
375}
376
377/// Category summary within severity level
378#[derive(Debug, Clone, Serialize, Deserialize)]
379pub struct CategorySummary {
380    /// Category name
381    pub categoryname: String,
382    /// Severity name
383    pub severity: String,
384    /// Count of flaws in this category
385    pub count: u32,
386}
387
388/// Scan frequency rule for policies
389#[derive(Debug, Clone, Serialize, Deserialize)]
390pub struct ScanFrequencyRule {
391    /// Type of scan this rule applies to
392    pub scan_type: String,
393    /// How frequently scans should be performed
394    pub frequency: String,
395}
396
397/// Finding rule for policies
398#[derive(Debug, Clone, Serialize, Deserialize)]
399pub struct FindingRule {
400    /// Type of finding rule
401    #[serde(rename = "type")]
402    pub rule_type: String,
403    /// Scan types this rule applies to
404    pub scan_type: Vec<String>,
405    /// Rule value/threshold
406    pub value: String,
407    /// Advanced options for the rule
408    pub advanced_options: Option<serde_json::Value>,
409}
410
411/// Advanced options for finding rules
412#[derive(Debug, Clone, Serialize, Deserialize)]
413pub struct FindingRuleAdvancedOptions {
414    /// Override severity
415    pub override_severity: Option<bool>,
416    /// Build action (WARNING, ERROR, etc.)
417    pub build_action: Option<String>,
418    /// Component dependency type
419    pub component_dependency: Option<String>,
420    /// Vulnerable methods setting
421    pub vulnerable_methods: Option<String>,
422    /// Selected licenses
423    pub selected_licenses: Option<Vec<String>>,
424    /// Override severity level
425    pub override_severity_level: Option<String>,
426    /// Whether to allow non-OSS licenses
427    pub allowed_nonoss_licenses: Option<bool>,
428    /// Whether to allow unrecognized licenses
429    pub allowed_unrecognized_licenses: Option<bool>,
430    /// Whether all licenses must meet requirement
431    pub all_licenses_must_meet_requirement: Option<bool>,
432    /// Whether this is a blocklist
433    pub is_blocklist: Option<bool>,
434}
435
436/// Query parameters for listing policies
437#[derive(Debug, Clone, Default)]
438pub struct PolicyListParams {
439    /// Filter by policy name
440    pub name: Option<String>,
441    /// Filter by policy type
442    pub policy_type: Option<String>,
443    /// Filter by active status
444    pub is_active: Option<bool>,
445    /// Include only default policies
446    pub default_only: Option<bool>,
447    /// Page number for pagination
448    pub page: Option<u32>,
449    /// Number of items per page
450    pub size: Option<u32>,
451}
452
453impl PolicyListParams {
454    /// Convert to query parameters for HTTP requests
455    #[must_use]
456    pub fn to_query_params(&self) -> Vec<(String, String)> {
457        Vec::from(self) // Delegate to trait
458    }
459}
460
461// Trait implementations for memory optimization
462impl From<&PolicyListParams> for Vec<(String, String)> {
463    fn from(query: &PolicyListParams) -> Self {
464        let mut params = Vec::new();
465
466        if let Some(ref name) = query.name {
467            params.push(("name".to_string(), name.clone())); // Still clone for borrowing
468        }
469        if let Some(ref policy_type) = query.policy_type {
470            params.push(("type".to_string(), policy_type.clone()));
471        }
472        if let Some(is_active) = query.is_active {
473            params.push(("active".to_string(), is_active.to_string()));
474        }
475        if let Some(default_only) = query.default_only {
476            params.push(("default".to_string(), default_only.to_string()));
477        }
478        if let Some(page) = query.page {
479            params.push(("page".to_string(), page.to_string()));
480        }
481        if let Some(size) = query.size {
482            params.push(("size".to_string(), size.to_string()));
483        }
484
485        params
486    }
487}
488
489impl From<PolicyListParams> for Vec<(String, String)> {
490    fn from(query: PolicyListParams) -> Self {
491        let mut params = Vec::new();
492
493        if let Some(name) = query.name {
494            params.push(("name".to_string(), name)); // MOVE - no clone!
495        }
496        if let Some(policy_type) = query.policy_type {
497            params.push(("type".to_string(), policy_type)); // MOVE - no clone!
498        }
499        if let Some(is_active) = query.is_active {
500            params.push(("active".to_string(), is_active.to_string()));
501        }
502        if let Some(default_only) = query.default_only {
503            params.push(("default".to_string(), default_only.to_string()));
504        }
505        if let Some(page) = query.page {
506            params.push(("page".to_string(), page.to_string()));
507        }
508        if let Some(size) = query.size {
509            params.push(("size".to_string(), size.to_string()));
510        }
511
512        params
513    }
514}
515
516/// Response wrapper for policy list operations
517#[derive(Debug, Clone, Serialize, Deserialize)]
518pub struct PolicyListResponse {
519    #[serde(rename = "_embedded")]
520    pub embedded: Option<PolicyEmbedded>,
521    pub page: Option<PageInfo>,
522    #[serde(rename = "_links")]
523    pub links: Option<serde_json::Value>,
524}
525
526/// Embedded policies in the list response
527#[derive(Debug, Clone, Serialize, Deserialize)]
528pub struct PolicyEmbedded {
529    #[serde(rename = "policy_versions")]
530    pub policy_versions: Vec<SecurityPolicy>,
531}
532
533/// Page information for paginated responses
534#[derive(Debug, Clone, Serialize, Deserialize)]
535pub struct PageInfo {
536    pub size: u32,
537    pub number: u32,
538    pub total_elements: u32,
539    pub total_pages: u32,
540}
541
542/// Indicates which API was used to retrieve policy compliance status
543#[derive(Debug, Clone, PartialEq, Eq)]
544pub enum ApiSource {
545    /// Policy status retrieved from summary report API (preferred)
546    SummaryReport,
547    /// Policy status retrieved from getbuildinfo.do XML API (fallback)
548    BuildInfo,
549}
550
551/// Policy-specific error types
552#[derive(Debug)]
553#[must_use = "Need to handle all error enum types."]
554pub enum PolicyError {
555    /// Veracode API error
556    Api(VeracodeError),
557    /// Policy not found (404)
558    NotFound,
559    /// Invalid policy configuration (400)
560    InvalidConfig(String),
561    /// Policy scan failed
562    ScanFailed(String),
563    /// Policy evaluation error
564    EvaluationError(String),
565    /// Insufficient permissions (403)
566    PermissionDenied,
567    /// Authentication required (401)
568    Unauthorized,
569    /// Internal server error (500)
570    InternalServerError,
571    /// Policy compliance check timeout
572    Timeout,
573}
574
575impl std::fmt::Display for PolicyError {
576    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
577        match self {
578            PolicyError::Api(err) => write!(f, "API error: {err}"),
579            PolicyError::NotFound => write!(f, "Policy not found"),
580            PolicyError::InvalidConfig(msg) => write!(f, "Invalid policy configuration: {msg}"),
581            PolicyError::ScanFailed(msg) => write!(f, "Policy scan failed: {msg}"),
582            PolicyError::EvaluationError(msg) => write!(f, "Policy evaluation error: {msg}"),
583            PolicyError::PermissionDenied => {
584                write!(f, "Insufficient permissions for policy operation")
585            }
586            PolicyError::Unauthorized => {
587                write!(f, "Authentication required - invalid API credentials")
588            }
589            PolicyError::InternalServerError => write!(f, "Internal server error occurred"),
590            PolicyError::Timeout => write!(f, "Policy operation timed out"),
591        }
592    }
593}
594
595impl std::error::Error for PolicyError {}
596
597impl From<VeracodeError> for PolicyError {
598    fn from(err: VeracodeError) -> Self {
599        PolicyError::Api(err)
600    }
601}
602
603impl From<reqwest::Error> for PolicyError {
604    fn from(err: reqwest::Error) -> Self {
605        PolicyError::Api(VeracodeError::Http(err))
606    }
607}
608
609impl From<serde_json::Error> for PolicyError {
610    fn from(err: serde_json::Error) -> Self {
611        PolicyError::Api(VeracodeError::Serialization(err))
612    }
613}
614
615/// Veracode Policy API operations
616pub struct PolicyApi<'a> {
617    client: &'a VeracodeClient,
618}
619
620impl<'a> PolicyApi<'a> {
621    ///
622    /// # Errors
623    ///
624    /// Returns an error if the API request fails, the resource is not found,
625    /// or authentication/authorization fails.
626    /// Create a new `PolicyApi` instance
627    #[must_use]
628    pub fn new(client: &'a VeracodeClient) -> Self {
629        Self { client }
630    }
631
632    /// List all available security policies
633    ///
634    /// # Arguments
635    ///
636    /// * `params` - Optional query parameters for filtering
637    ///
638    /// # Returns
639    ///
640    /// A `Result` containing a list of policies or an error.
641    ///
642    /// # Errors
643    ///
644    /// Returns an error if the API request fails, the policy is invalid,
645    /// or authentication/authorization fails.
646    pub async fn list_policies(
647        &self,
648        params: Option<PolicyListParams>,
649    ) -> Result<Vec<SecurityPolicy>, PolicyError> {
650        let endpoint = "/appsec/v1/policies";
651
652        let query_params = params.as_ref().map(Vec::from);
653
654        let response = self.client.get(endpoint, query_params.as_deref()).await?;
655
656        let status = response.status().as_u16();
657        match status {
658            200 => {
659                let policy_response: PolicyListResponse = response.json().await?;
660                let policies = policy_response
661                    .embedded
662                    .map(|e| e.policy_versions)
663                    .unwrap_or_default();
664
665                Ok(policies)
666            }
667            400 => {
668                let error_text = response.text().await.unwrap_or_default();
669                Err(PolicyError::InvalidConfig(error_text))
670            }
671            401 => Err(PolicyError::Unauthorized),
672            403 => Err(PolicyError::PermissionDenied),
673            404 => Err(PolicyError::NotFound),
674            500 => Err(PolicyError::InternalServerError),
675            _ => {
676                let error_text = response.text().await.unwrap_or_default();
677                Err(PolicyError::Api(VeracodeError::InvalidResponse(format!(
678                    "HTTP {status}: {error_text}"
679                ))))
680            }
681        }
682    }
683
684    /// Get a specific policy by GUID
685    ///
686    /// # Arguments
687    ///
688    /// * `policy_guid` - The GUID of the policy
689    ///
690    /// # Returns
691    ///
692    /// A `Result` containing the policy or an error.
693    ///
694    /// # Errors
695    ///
696    /// Returns an error if the API request fails, the policy is invalid,
697    /// or authentication/authorization fails.
698    pub async fn get_policy(&self, policy_guid: &str) -> Result<SecurityPolicy, PolicyError> {
699        let endpoint = format!("/appsec/v1/policies/{policy_guid}");
700
701        let response = self.client.get(&endpoint, None).await?;
702
703        let status = response.status().as_u16();
704        match status {
705            200 => {
706                let policy: SecurityPolicy = response.json().await?;
707                Ok(policy)
708            }
709            400 => {
710                let error_text = response.text().await.unwrap_or_default();
711                Err(PolicyError::InvalidConfig(error_text))
712            }
713            401 => Err(PolicyError::Unauthorized),
714            403 => Err(PolicyError::PermissionDenied),
715            404 => Err(PolicyError::NotFound),
716            500 => Err(PolicyError::InternalServerError),
717            _ => {
718                let error_text = response.text().await.unwrap_or_default();
719                Err(PolicyError::Api(VeracodeError::InvalidResponse(format!(
720                    "HTTP {status}: {error_text}"
721                ))))
722            }
723        }
724    }
725
726    /// Get the default policy for the organization
727    ///
728    /// # Returns
729    ///
730    /// A `Result` containing the default policy or an error.
731    ///
732    /// # Errors
733    ///
734    /// Returns an error if the API request fails, the policy is invalid,
735    /// or authentication/authorization fails.
736    pub async fn get_default_policy(&self) -> Result<SecurityPolicy, PolicyError> {
737        let params = PolicyListParams {
738            default_only: Some(true),
739            ..Default::default()
740        };
741
742        let policies = self.list_policies(Some(params)).await?;
743        // Note: Default policy identification may need to be handled differently
744        // based on the actual API response structure
745        policies
746            .into_iter()
747            .find(|p| p.policy_type == "CUSTOMER" && p.organization_id.is_some())
748            .ok_or(PolicyError::NotFound)
749    }
750
751    /// Evaluates policy compliance for an application or sandbox using XML API
752    ///
753    /// This uses the /api/5.0/getbuildinfo.do endpoint which is the only working
754    /// policy compliance endpoint as the REST API compliance endpoints return 404.
755    ///
756    /// # Arguments
757    ///
758    /// * `app_id` - The numeric ID of the application
759    /// * `sandbox_id` - Optional numeric ID of the sandbox to evaluate
760    ///
761    /// # Returns
762    ///
763    /// A `Result` containing the policy compliance status string or an error.
764    ///
765    /// # Errors
766    ///
767    /// Returns an error if the API request fails, the policy is invalid,
768    /// or authentication/authorization fails.
769    pub async fn evaluate_policy_compliance_via_buildinfo(
770        &self,
771        app_id: &str,
772        build_id: Option<&str>,
773        sandbox_id: Option<&str>,
774    ) -> Result<std::borrow::Cow<'static, str>, PolicyError> {
775        self.evaluate_policy_compliance_via_buildinfo_with_retry(
776            app_id, build_id, sandbox_id, 30, 10,
777        )
778        .await
779    }
780
781    /// Evaluates policy compliance with retry logic for when assessment is not yet complete
782    ///
783    /// This function will retry the policy evaluation check when the status is "Not Assessed"
784    /// until either the assessment completes or the maximum retry attempts are reached.
785    ///
786    /// # Arguments
787    ///
788    /// * `app_id` - The numeric ID of the application
789    /// * `build_id` - Optional build ID to check. If None, checks the latest build
790    /// * `sandbox_id` - Optional numeric ID of the sandbox to evaluate
791    /// * `max_retries` - Maximum number of retry attempts (default: 30)
792    /// * `retry_delay_seconds` - Delay between retries in seconds (default: 10)
793    ///
794    /// # Returns
795    ///
796    /// A `Result` containing the policy compliance status string or an error.
797    ///
798    /// # Errors
799    ///
800    /// Returns an error if the API request fails, the policy is invalid,
801    /// or authentication/authorization fails.
802    pub async fn evaluate_policy_compliance_via_buildinfo_with_retry(
803        &self,
804        app_id: &str,
805        build_id: Option<&str>,
806        sandbox_id: Option<&str>,
807        max_retries: u32,
808        retry_delay_seconds: u64,
809    ) -> Result<std::borrow::Cow<'static, str>, PolicyError> {
810        use crate::build::{BuildError, GetBuildInfoRequest};
811        use std::borrow::Cow;
812        use tokio::time::{Duration, sleep};
813
814        let build_request = GetBuildInfoRequest {
815            app_id: app_id.to_string(),
816            build_id: build_id.map(str::to_string), // ← Use the parameter
817            sandbox_id: sandbox_id.map(str::to_string),
818        };
819
820        let mut attempts: u32 = 0;
821        loop {
822            let build_info = self
823                .client
824                .build_api()?
825                .get_build_info(&build_request)
826                .await
827                .map_err(|e| match e {
828                    BuildError::BuildNotFound => PolicyError::Api(crate::VeracodeError::InvalidResponse(
829                        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.")
830                    )),
831                    BuildError::ApplicationNotFound => PolicyError::Api(crate::VeracodeError::InvalidResponse(
832                        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.")
833                    )),
834                    BuildError::SandboxNotFound => PolicyError::Api(crate::VeracodeError::InvalidResponse(
835                        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"))
836                    )),
837                    BuildError::Api(api_err) => PolicyError::Api(api_err),
838                    BuildError::InvalidParameter(msg)
839                    | BuildError::CreationFailed(msg)
840                    | BuildError::UpdateFailed(msg)
841                    | BuildError::DeletionFailed(msg)
842                    | BuildError::XmlParsingError(msg) => {
843                        PolicyError::Api(crate::VeracodeError::InvalidResponse(msg))
844                    }
845                    BuildError::Unauthorized | BuildError::PermissionDenied => PolicyError::Api(
846                        crate::VeracodeError::Authentication("Build API access denied".to_string()),
847                    ),
848                    BuildError::BuildInProgress => {
849                        PolicyError::Api(crate::VeracodeError::InvalidResponse(
850                            "Build is currently in progress".to_string(),
851                        ))
852                    }
853                })?;
854
855            // Get the policy compliance status
856            let status = build_info
857                .policy_compliance_status
858                .as_deref()
859                .unwrap_or("Not Assessed");
860
861            // If status is ready (not in-progress), return the result
862            if status != "Not Assessed" && status != "Calculating..." {
863                return Ok(Cow::Owned(status.to_string()));
864            }
865
866            // If we've reached max retries, return "Not Assessed"
867            attempts = attempts.saturating_add(1);
868            if attempts >= max_retries {
869                warn!(
870                    "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"
871                );
872                return Ok(Cow::Borrowed("Not Assessed"));
873            }
874
875            // Log retry attempt
876            info!(
877                "Policy evaluation not yet assessed, retrying in {retry_delay_seconds} seconds... (attempt {attempts}/{max_retries})"
878            );
879
880            // Wait before retrying
881            sleep(Duration::from_secs(retry_delay_seconds)).await;
882        }
883    }
884
885    /// Determines if build should break based on policy compliance status
886    ///
887    /// # Arguments
888    ///
889    /// * `status` - The policy compliance status string from XML API
890    ///
891    /// # Returns
892    ///
893    /// `true` if build should break, `false` otherwise
894    #[must_use]
895    pub fn should_break_build(status: &str) -> bool {
896        status == "Did Not Pass"
897    }
898
899    /// Gets the appropriate exit code for CI/CD systems based on policy compliance
900    ///
901    /// # Arguments
902    ///
903    /// * `status` - The policy compliance status string from XML API
904    ///
905    /// # Returns
906    ///
907    /// Exit code: 0 for success, 4 for policy failure (build break)
908    #[must_use]
909    pub fn get_exit_code_for_status(status: &str) -> i32 {
910        if Self::should_break_build(status) {
911            4 // DID_NOT_PASSED_POLICY - matches Java wrapper
912        } else {
913            0 // SUCCESS
914        }
915    }
916
917    /// Get summary report for an application build using the REST API
918    ///
919    ///
920    /// # Errors
921    ///
922    /// Returns an error if the API request fails, the resource is not found,
923    /// or authentication/authorization fails.
924    /// This uses the `/appsec/v2/applications/{app_guid}/summary_report` endpoint
925    /// to get policy compliance status and scan results.
926    ///
927    /// # Arguments
928    ///
929    /// * `app_guid` - The GUID of the application
930    /// * `build_id` - The build ID (GUID) to get summary for
931    /// * `sandbox_guid` - Optional sandbox GUID for sandbox scans
932    ///
933    /// # Returns
934    ///
935    /// A `Result` containing the summary report or an error.
936    ///
937    /// # Errors
938    ///
939    /// Returns an error if the API request fails, the policy is invalid,
940    /// or authentication/authorization fails.
941    pub async fn get_summary_report(
942        &self,
943        app_guid: &str,
944        build_id: Option<&str>,
945        sandbox_guid: Option<&str>,
946    ) -> Result<SummaryReport, PolicyError> {
947        let endpoint = format!("/appsec/v2/applications/{app_guid}/summary_report");
948
949        // Build query parameters
950        let mut query_params = Vec::new();
951        if let Some(build_id) = build_id {
952            query_params.push(("build_id".to_string(), build_id.to_string()));
953        }
954        if let Some(sandbox_guid) = sandbox_guid {
955            query_params.push(("context".to_string(), sandbox_guid.to_string()));
956        }
957
958        let response = self.client.get(&endpoint, Some(&query_params)).await?;
959
960        let status = response.status().as_u16();
961        match status {
962            200 => {
963                let summary_report: SummaryReport = response.json().await?;
964                Ok(summary_report)
965            }
966            400 => {
967                let error_text = response.text().await.unwrap_or_default();
968                Err(PolicyError::InvalidConfig(error_text))
969            }
970            401 => Err(PolicyError::Unauthorized),
971            403 => Err(PolicyError::PermissionDenied),
972            404 => Err(PolicyError::NotFound),
973            500 => Err(PolicyError::InternalServerError),
974            _ => {
975                let error_text = response.text().await.unwrap_or_default();
976                Err(PolicyError::Api(VeracodeError::InvalidResponse(format!(
977                    "HTTP {status}: {error_text}"
978                ))))
979            }
980        }
981    }
982
983    /// Gets summary report with retry logic and returns both the full report and compliance status
984    ///
985    /// This function combines the functionality of both `get_summary_report` and
986    /// `evaluate_policy_compliance_via_summary_report_with_retry` to avoid redundant API calls.
987    /// It will retry until the policy compliance status is ready (not "Not Assessed").
988    ///
989    /// # Arguments
990    ///
991    /// * `app_guid` - The GUID of the application
992    /// * `build_id` - The build ID to check compliance for
993    /// * `sandbox_guid` - Optional sandbox GUID for sandbox scans
994    /// * `max_retries` - Maximum number of retry attempts
995    /// * `retry_delay_seconds` - Delay between retries in seconds
996    /// * `debug` - Enable debug logging
997    ///
998    /// # Returns
999    ///
1000    ///
1001    /// # Errors
1002    ///
1003    /// Returns an error if the API request fails, the policy is invalid,
1004    /// or authentication/authorization fails.
1005    /// A `Result` containing a tuple of (`SummaryReport`, Option<`compliance_status`>) or an error.
1006    ///
1007    /// # Errors
1008    ///
1009    /// Returns an error if the API request fails, the policy is invalid,
1010    /// or authentication/authorization fails.
1011    /// The `compliance_status` is Some(status) if `break_build` evaluation is needed, None otherwise.
1012    #[allow(clippy::too_many_arguments)]
1013    ///
1014    /// # Errors
1015    ///
1016    /// Returns an error if the API request fails, the policy is invalid,
1017    /// or authentication/authorization fails.
1018    pub async fn get_summary_report_with_policy_retry(
1019        &self,
1020        app_guid: &str,
1021        build_id: Option<&str>,
1022        sandbox_guid: Option<&str>,
1023        max_retries: u32,
1024        retry_delay_seconds: u64,
1025        enable_break_build: bool,
1026    ) -> Result<(SummaryReport, Option<std::borrow::Cow<'static, str>>), PolicyError> {
1027        use std::borrow::Cow;
1028        use tokio::time::{Duration, sleep};
1029
1030        if enable_break_build && build_id.is_none() {
1031            return Err(PolicyError::InvalidConfig(
1032                "Build ID is required for break build policy evaluation".to_string(),
1033            ));
1034        }
1035
1036        let mut attempts: u32 = 0;
1037        loop {
1038            if attempts == 0 && enable_break_build {
1039                debug!("Checking policy compliance status with retry logic...");
1040            } else if attempts == 0 {
1041                debug!("Getting summary report...");
1042            }
1043
1044            let summary_report = match self
1045                .get_summary_report(app_guid, build_id, sandbox_guid)
1046                .await
1047            {
1048                Ok(report) => report,
1049                Err(PolicyError::InternalServerError) if attempts < 3 => {
1050                    warn!(
1051                        "Summary report API failed with server error (attempt {}/3), retrying in 5 seconds...",
1052                        attempts.saturating_add(1)
1053                    );
1054                    sleep(Duration::from_secs(5)).await;
1055                    attempts = attempts.saturating_add(1);
1056                    continue;
1057                }
1058                Err(e) => return Err(e),
1059            };
1060
1061            // If break_build is not enabled, return immediately with the report
1062            if !enable_break_build {
1063                return Ok((summary_report, None));
1064            }
1065
1066            // For `break_build` evaluation, check if policy compliance status is ready
1067            let status = summary_report.policy_compliance_status.clone();
1068
1069            // If status is ready (not empty and not "Not Assessed"), return both report and status
1070            if !status.is_empty() && status != "Not Assessed" {
1071                debug!("Policy compliance status ready: {status}");
1072                return Ok((summary_report, Some(Cow::Owned(status))));
1073            }
1074
1075            // If we've reached max retries, return current results
1076            attempts = attempts.saturating_add(1);
1077            if attempts >= max_retries {
1078                warn!(
1079                    "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"
1080                );
1081                return Ok((summary_report, Some(Cow::Owned(status))));
1082            }
1083
1084            // Log retry attempt
1085            info!(
1086                "Policy evaluation not yet ready (status: '{status}'), retrying in {retry_delay_seconds} seconds... (attempt {attempts}/{max_retries})"
1087            );
1088
1089            // Wait before retrying
1090            sleep(Duration::from_secs(retry_delay_seconds)).await;
1091        }
1092    }
1093
1094    /// Evaluates policy compliance using the summary report API with retry logic
1095    ///
1096    ///
1097    /// # Errors
1098    ///
1099    /// Returns an error if the API request fails, the policy is invalid,
1100    /// or authentication/authorization fails.
1101    /// This function uses the `summary_report` endpoint instead of the buildinfo XML API
1102    /// and will retry when results are not ready yet.
1103    ///
1104    /// # Arguments
1105    ///
1106    /// * `app_guid` - The GUID of the application  
1107    /// * `build_id` - The build ID (GUID) to check compliance for
1108    /// * `sandbox_guid` - Optional sandbox GUID for sandbox scans
1109    /// * `max_retries` - Maximum number of retry attempts (default: 30)
1110    /// * `retry_delay_seconds` - Delay between retries in seconds (default: 10)
1111    ///
1112    /// # Returns
1113    ///
1114    /// A `Result` containing the policy compliance status string or an error.
1115    ///
1116    /// # Errors
1117    ///
1118    /// Returns an error if the API request fails, the policy is invalid,
1119    /// or authentication/authorization fails.
1120    pub async fn evaluate_policy_compliance_via_summary_report_with_retry(
1121        &self,
1122        app_guid: &str,
1123        build_id: &str,
1124        sandbox_guid: Option<&str>,
1125        max_retries: u32,
1126        retry_delay_seconds: u64,
1127    ) -> Result<std::borrow::Cow<'static, str>, PolicyError> {
1128        use std::borrow::Cow;
1129        use tokio::time::{Duration, sleep};
1130
1131        let mut attempts: u32 = 0;
1132        loop {
1133            let summary_report = self
1134                .get_summary_report(app_guid, Some(build_id), sandbox_guid)
1135                .await?;
1136
1137            // Check if results are ready - look for "Results Ready" or completed status
1138            // The summary report should have policy_compliance_status populated when ready
1139            let status = &summary_report.policy_compliance_status;
1140
1141            // If status is not empty and not "Not Assessed", return the result
1142            if !status.is_empty() && status != "Not Assessed" {
1143                return Ok(Cow::Owned(status.clone()));
1144            }
1145
1146            // If we've reached max retries, return current status
1147            attempts = attempts.saturating_add(1);
1148            if attempts >= max_retries {
1149                warn!(
1150                    "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"
1151                );
1152                return Ok(Cow::Owned(status.clone()));
1153            }
1154
1155            // Log retry attempt
1156            info!(
1157                "Policy evaluation not yet ready (status: '{status}'), retrying in {retry_delay_seconds} seconds... (attempt {attempts}/{max_retries})"
1158            );
1159
1160            // Wait before retrying
1161            sleep(Duration::from_secs(retry_delay_seconds)).await;
1162        }
1163    }
1164
1165    /// Evaluates policy compliance using the summary report API (single attempt)
1166    ///
1167    /// This is a convenience method that calls the retry version with default parameters.
1168    ///
1169    /// # Arguments
1170    ///
1171    /// * `app_guid` - The GUID of the application  
1172    /// * `build_id` - The build ID (GUID) to check compliance for
1173    /// * `sandbox_guid` - Optional sandbox GUID for sandbox scans
1174    ///
1175    /// # Returns
1176    ///
1177    /// A `Result` containing the policy compliance status string or an error.
1178    ///
1179    /// # Errors
1180    ///
1181    /// Returns an error if the API request fails, the policy is invalid,
1182    /// or authentication/authorization fails.
1183    pub async fn evaluate_policy_compliance_via_summary_report(
1184        &self,
1185        app_guid: &str,
1186        build_id: &str,
1187        sandbox_guid: Option<&str>,
1188    ) -> Result<std::borrow::Cow<'static, str>, PolicyError> {
1189        self.evaluate_policy_compliance_via_summary_report_with_retry(
1190            app_guid,
1191            build_id,
1192            sandbox_guid,
1193            30,
1194            10,
1195        )
1196        .await
1197    }
1198
1199    /// Initiate a policy scan for an application
1200    ///
1201    /// # Arguments
1202    ///
1203    /// * `request` - The policy scan request
1204    ///
1205    /// # Returns
1206    ///
1207    /// A `Result` containing the scan result or an error.
1208    ///
1209    /// # Errors
1210    ///
1211    /// Returns an error if the API request fails, the policy is invalid,
1212    /// or authentication/authorization fails.
1213    pub async fn initiate_policy_scan(
1214        &self,
1215        request: PolicyScanRequest,
1216    ) -> Result<PolicyScanResult, PolicyError> {
1217        let endpoint = "/appsec/v1/policy-scans";
1218
1219        let response = self.client.post(endpoint, Some(&request)).await?;
1220
1221        let status = response.status().as_u16();
1222        match status {
1223            200 | 201 => {
1224                let scan_result: PolicyScanResult = response.json().await?;
1225                Ok(scan_result)
1226            }
1227            400 => {
1228                let error_text = response.text().await.unwrap_or_default();
1229                Err(PolicyError::InvalidConfig(error_text))
1230            }
1231            404 => Err(PolicyError::NotFound),
1232            _ => {
1233                let error_text = response.text().await.unwrap_or_default();
1234                Err(PolicyError::Api(VeracodeError::InvalidResponse(format!(
1235                    "HTTP {status}: {error_text}"
1236                ))))
1237            }
1238        }
1239    }
1240
1241    /// Get policy scan status and results
1242    ///
1243    /// # Arguments
1244    ///
1245    /// * `scan_id` - The ID of the policy scan
1246    ///
1247    /// # Returns
1248    ///
1249    /// A `Result` containing the scan result or an error.
1250    ///
1251    /// # Errors
1252    ///
1253    /// Returns an error if the API request fails, the policy is invalid,
1254    /// or authentication/authorization fails.
1255    pub async fn get_policy_scan_result(
1256        &self,
1257        scan_id: u64,
1258    ) -> Result<PolicyScanResult, PolicyError> {
1259        let endpoint = format!("/appsec/v1/policy-scans/{scan_id}");
1260
1261        let response = self.client.get(&endpoint, None).await?;
1262
1263        let status = response.status().as_u16();
1264        match status {
1265            200 => {
1266                let scan_result: PolicyScanResult = response.json().await?;
1267                Ok(scan_result)
1268            }
1269            404 => Err(PolicyError::NotFound),
1270            _ => {
1271                let error_text = response.text().await.unwrap_or_default();
1272                Err(PolicyError::Api(VeracodeError::InvalidResponse(format!(
1273                    "HTTP {status}: {error_text}"
1274                ))))
1275            }
1276        }
1277    }
1278
1279    /// Check if a policy scan is complete
1280    ///
1281    /// # Arguments
1282    ///
1283    /// * `scan_id` - The ID of the policy scan
1284    ///
1285    /// # Returns
1286    ///
1287    /// A `Result` containing a boolean indicating completion status.
1288    ///
1289    /// # Errors
1290    ///
1291    /// Returns an error if the API request fails, the policy is invalid,
1292    /// or authentication/authorization fails.
1293    pub async fn is_policy_scan_complete(&self, scan_id: u64) -> Result<bool, PolicyError> {
1294        let scan_result = self.get_policy_scan_result(scan_id).await?;
1295        Ok(matches!(
1296            scan_result.status,
1297            ScanStatus::Completed | ScanStatus::Failed | ScanStatus::Cancelled
1298        ))
1299    }
1300
1301    /// Gets policy compliance status with automatic fallback from summary report to buildinfo
1302    ///
1303    /// This method first tries the summary report API for full functionality. If access is denied
1304    /// (401/403), it automatically falls back to the getbuildinfo.do XML API for policy compliance
1305    /// status only. This provides the best user experience while maintaining compatibility.
1306    ///
1307    /// # Arguments
1308    ///
1309    /// * `app_guid` - Application GUID (for REST API)
1310    /// * `app_id` - Application numeric ID (for XML API fallback)
1311    /// * `build_id` - Optional build ID
1312    /// * `sandbox_guid` - Optional sandbox GUID (for REST API)
1313    /// * `sandbox_id` - Optional sandbox numeric ID (for XML API fallback)
1314    /// * `max_retries` - Maximum number of retry attempts
1315    /// * `retry_delay_seconds` - Delay between retries in seconds
1316    /// * `enable_break_build` - Whether to enable break build evaluation
1317    /// * `force_buildinfo_api` - Skip summary report and use buildinfo directly
1318    ///
1319    /// # Returns
1320    ///
1321    /// A tuple containing:
1322    ///
1323    /// # Errors
1324    ///
1325    /// Returns an error if the API request fails, the policy is invalid,
1326    /// or authentication/authorization fails.
1327    /// - Optional `SummaryReport` (None if fallback was used)
1328    /// - Policy compliance status string
1329    ///
1330    /// # Errors
1331    ///
1332    /// Returns an error if the API request fails, the policy is invalid,
1333    /// or authentication/authorization fails.
1334    /// - `ApiSource` indicating which API was used
1335    #[allow(clippy::too_many_arguments)]
1336    ///
1337    /// # Errors
1338    ///
1339    /// Returns an error if the API request fails, the policy is invalid,
1340    /// or authentication/authorization fails.
1341    pub async fn get_policy_status_with_fallback(
1342        &self,
1343        app_guid: &str,
1344        app_id: &str,
1345        build_id: Option<&str>,
1346        sandbox_guid: Option<&str>,
1347        sandbox_id: Option<&str>,
1348        max_retries: u32,
1349        retry_delay_seconds: u64,
1350        enable_break_build: bool,
1351        force_buildinfo_api: bool,
1352    ) -> Result<(Option<SummaryReport>, String, ApiSource), PolicyError> {
1353        if force_buildinfo_api {
1354            // DIRECT PATH: Skip summary report, use getbuildinfo.do directly
1355            debug!("Using getbuildinfo.do API directly (forced via configuration)");
1356            let status = self
1357                .evaluate_policy_compliance_via_buildinfo_with_retry(
1358                    app_id,
1359                    build_id,
1360                    sandbox_id,
1361                    max_retries,
1362                    retry_delay_seconds,
1363                )
1364                .await?;
1365            return Ok((None, status.to_string(), ApiSource::BuildInfo));
1366        }
1367
1368        // FALLBACK PATH: Try summary report first, fallback to getbuildinfo.do
1369        match self
1370            .get_summary_report_with_policy_retry(
1371                app_guid,
1372                build_id,
1373                sandbox_guid,
1374                max_retries,
1375                retry_delay_seconds,
1376                enable_break_build,
1377            )
1378            .await
1379        {
1380            Ok((summary_report, compliance_status)) => {
1381                debug!("Used summary report API successfully");
1382                let status = compliance_status
1383                    .map(|s| s.to_string())
1384                    .unwrap_or_else(|| summary_report.policy_compliance_status.clone());
1385                Ok((Some(summary_report), status, ApiSource::SummaryReport))
1386            }
1387            Err(
1388                ref e @ (PolicyError::Unauthorized
1389                | PolicyError::PermissionDenied
1390                | PolicyError::InternalServerError),
1391            ) => {
1392                match *e {
1393                    PolicyError::InternalServerError => info!(
1394                        "Summary report API server error, falling back to getbuildinfo.do API"
1395                    ),
1396                    PolicyError::Unauthorized | PolicyError::PermissionDenied => {
1397                        info!("Summary report access denied, falling back to getbuildinfo.do API")
1398                    }
1399                    PolicyError::Api(_)
1400                    | PolicyError::NotFound
1401                    | PolicyError::InvalidConfig(_)
1402                    | PolicyError::ScanFailed(_)
1403                    | PolicyError::EvaluationError(_)
1404                    | PolicyError::Timeout => {}
1405                }
1406                let status = self
1407                    .evaluate_policy_compliance_via_buildinfo_with_retry(
1408                        app_id,
1409                        build_id,
1410                        sandbox_id,
1411                        max_retries,
1412                        retry_delay_seconds,
1413                    )
1414                    .await?;
1415                Ok((None, status.to_string(), ApiSource::BuildInfo))
1416            }
1417            Err(e) => Err(e),
1418        }
1419    }
1420
1421    /// Get active policies for the organization
1422    ///
1423    /// # Returns
1424    ///
1425    /// A `Result` containing a list of active policies or an error.
1426    ///
1427    /// # Errors
1428    ///
1429    /// Returns an error if the API request fails, the policy is invalid,
1430    /// or authentication/authorization fails.
1431    pub async fn get_active_policies(&self) -> Result<Vec<SecurityPolicy>, PolicyError> {
1432        // Note: The active/inactive concept may need to be handled differently
1433        // based on the actual API response structure
1434        let policies = self.list_policies(None).await?;
1435        Ok(policies) // Return all policies for now
1436    }
1437}
1438
1439#[cfg(test)]
1440#[allow(clippy::expect_used)]
1441mod tests {
1442    use super::*;
1443
1444    #[test]
1445    fn test_policy_list_params_to_query() {
1446        let params = PolicyListParams {
1447            name: Some("test-policy".to_string()),
1448            is_active: Some(true),
1449            page: Some(1),
1450            size: Some(10),
1451            ..Default::default()
1452        };
1453
1454        let query_params: Vec<_> = params.into();
1455        assert_eq!(query_params.len(), 4);
1456        assert!(query_params.contains(&("name".to_string(), "test-policy".to_string())));
1457        assert!(query_params.contains(&("active".to_string(), "true".to_string())));
1458        assert!(query_params.contains(&("page".to_string(), "1".to_string())));
1459        assert!(query_params.contains(&("size".to_string(), "10".to_string())));
1460    }
1461
1462    #[test]
1463    fn test_policy_error_display() {
1464        let error = PolicyError::NotFound;
1465        assert_eq!(error.to_string(), "Policy not found");
1466
1467        let error = PolicyError::InvalidConfig("test".to_string());
1468        assert_eq!(error.to_string(), "Invalid policy configuration: test");
1469
1470        let error = PolicyError::Timeout;
1471        assert_eq!(error.to_string(), "Policy operation timed out");
1472    }
1473
1474    #[test]
1475    fn test_scan_type_serialization() {
1476        let scan_type = ScanType::Static;
1477        let json = serde_json::to_string(&scan_type).expect("should serialize to json");
1478        assert_eq!(json, "\"static\"");
1479
1480        let deserialized: ScanType = serde_json::from_str(&json).expect("should deserialize json");
1481        assert!(matches!(deserialized, ScanType::Static));
1482    }
1483
1484    #[test]
1485    fn test_policy_compliance_status_serialization() {
1486        let status = PolicyComplianceStatus::Passed;
1487        let json = serde_json::to_string(&status).expect("should serialize to json");
1488        assert_eq!(json, "\"Passed\"");
1489
1490        let deserialized: PolicyComplianceStatus =
1491            serde_json::from_str(&json).expect("should deserialize json");
1492        assert!(matches!(deserialized, PolicyComplianceStatus::Passed));
1493
1494        // Test the special case statuses with spaces
1495        let conditional_pass = PolicyComplianceStatus::ConditionalPass;
1496        let json = serde_json::to_string(&conditional_pass).expect("should serialize to json");
1497        assert_eq!(json, "\"Conditional Pass\"");
1498
1499        let did_not_pass = PolicyComplianceStatus::DidNotPass;
1500        let json = serde_json::to_string(&did_not_pass).expect("should serialize to json");
1501        assert_eq!(json, "\"Did Not Pass\"");
1502    }
1503
1504    #[test]
1505    fn test_break_build_logic() {
1506        assert!(PolicyApi::should_break_build("Did Not Pass"));
1507        assert!(!PolicyApi::should_break_build("Passed"));
1508        assert!(!PolicyApi::should_break_build("Conditional Pass"));
1509        // "Not Assessed" should not break build as the retry logic should handle waiting
1510        // for policy evaluation to complete before reaching this point
1511        assert!(!PolicyApi::should_break_build("Not Assessed"));
1512
1513        assert_eq!(PolicyApi::get_exit_code_for_status("Did Not Pass"), 4);
1514        assert_eq!(PolicyApi::get_exit_code_for_status("Passed"), 0);
1515        assert_eq!(PolicyApi::get_exit_code_for_status("Conditional Pass"), 0);
1516        // "Not Assessed" returns 0 because it should only reach here after retry logic
1517        // has exhausted attempts, indicating a configuration or timing issue rather than policy failure
1518        assert_eq!(PolicyApi::get_exit_code_for_status("Not Assessed"), 0);
1519    }
1520
1521    #[test]
1522    fn test_summary_report_serialization() {
1523        let summary_json = r#"{
1524            "app_id": 2676517,
1525            "app_name": "Verascan Java Test",
1526            "build_id": 54209787,
1527            "policy_compliance_status": "Did Not Pass",
1528            "policy_name": "SecureCode Policy",
1529            "policy_version": 1,
1530            "policy_rules_status": "Did Not Pass",
1531            "grace_period_expired": false,
1532            "scan_overdue": "false",
1533            "is_latest_build": false,
1534            "generation_date": "2025-08-05 10:14:45 UTC",
1535            "last_update_time": "2025-08-05 10:00:51 UTC"
1536        }"#;
1537
1538        let summary: Result<SummaryReport, _> = serde_json::from_str(summary_json);
1539        assert!(summary.is_ok());
1540
1541        let summary = summary.expect("should have summary");
1542        assert_eq!(summary.policy_compliance_status, "Did Not Pass");
1543        assert_eq!(summary.app_name, "Verascan Java Test");
1544        assert_eq!(summary.build_id, 54209787);
1545        assert!(PolicyApi::should_break_build(
1546            &summary.policy_compliance_status
1547        ));
1548    }
1549
1550    #[test]
1551    fn test_export_json_structure() {
1552        // Test the JSON structure that would be exported
1553        let summary_report = SummaryReport {
1554            app_id: 2676517,
1555            app_name: "Test App".to_string(),
1556            build_id: 54209787,
1557            policy_compliance_status: "Passed".to_string(),
1558            policy_name: "Test Policy".to_string(),
1559            policy_version: 1,
1560            policy_rules_status: "Passed".to_string(),
1561            grace_period_expired: false,
1562            scan_overdue: "false".to_string(),
1563            is_latest_build: true,
1564            sandbox_name: Some("test-sandbox".to_string()),
1565            sandbox_id: Some(123456),
1566            generation_date: "2025-08-05 10:14:45 UTC".to_string(),
1567            last_update_time: "2025-08-05 10:00:51 UTC".to_string(),
1568            static_analysis: None,
1569            flaw_status: None,
1570            software_composition_analysis: None,
1571            severity: None,
1572        };
1573
1574        let export_json = serde_json::json!({
1575            "summary_report": summary_report,
1576            "export_metadata": {
1577                "exported_at": "2025-08-05T10:14:45Z",
1578                "tool": "verascan",
1579                "export_type": "summary_report",
1580                "scan_configuration": {
1581                    "autoscan": true,
1582                    "scan_all_nonfatal_top_level_modules": true,
1583                    "include_new_modules": true
1584                }
1585            }
1586        });
1587
1588        // Verify JSON structure
1589        assert!(
1590            export_json
1591                .get("summary_report")
1592                .and_then(|s| s.get("app_name"))
1593                .map(|v| v.is_string())
1594                .unwrap_or(false)
1595        );
1596        assert!(
1597            export_json
1598                .get("summary_report")
1599                .and_then(|s| s.get("policy_compliance_status"))
1600                .map(|v| v.is_string())
1601                .unwrap_or(false)
1602        );
1603        assert!(
1604            export_json
1605                .get("export_metadata")
1606                .and_then(|e| e.get("export_type"))
1607                .map(|v| v.is_string())
1608                .unwrap_or(false)
1609        );
1610        assert_eq!(
1611            export_json
1612                .get("export_metadata")
1613                .and_then(|e| e.get("export_type"))
1614                .and_then(|v| v.as_str())
1615                .expect("should have export_type"),
1616            "summary_report"
1617        );
1618
1619        // Verify the summary report can be serialized and deserialized
1620        let json_string =
1621            serde_json::to_string_pretty(&export_json).expect("should serialize to json");
1622        assert!(json_string.contains("summary_report"));
1623        assert!(json_string.contains("export_metadata"));
1624    }
1625
1626    #[test]
1627    fn test_get_summary_report_with_policy_retry_parameters() {
1628        // Unit tests for the new combined method parameter validation and logic
1629
1630        // Test parameter type validation
1631        let app_guid = "test-app-guid";
1632        let build_id = Some("test-build-id");
1633        let sandbox_guid: Option<&str> = None;
1634        let max_retries = 30u32;
1635        let retry_delay_seconds = 10u64;
1636        let debug = false;
1637        let enable_break_build = true;
1638
1639        // Verify parameter types are correct
1640        assert_eq!(app_guid, "test-app-guid");
1641        assert_eq!(build_id, Some("test-build-id"));
1642        assert_eq!(sandbox_guid, None);
1643        assert_eq!(max_retries, 30);
1644        assert_eq!(retry_delay_seconds, 10);
1645        assert!(!debug);
1646        assert!(enable_break_build);
1647    }
1648
1649    #[test]
1650    fn test_policy_status_ready_logic() {
1651        // Test the logic for determining when policy status is ready
1652        let ready_statuses = vec!["Passed", "Did Not Pass", "Conditional Pass"];
1653        let not_ready_statuses = vec!["", "Not Assessed"];
1654
1655        // Test ready statuses (should not trigger retry)
1656        for status in &ready_statuses {
1657            assert!(
1658                !status.is_empty(),
1659                "Ready status should not be empty: {status}"
1660            );
1661            assert_ne!(
1662                *status, "Not Assessed",
1663                "Ready status should not be 'Not Assessed': {status}"
1664            );
1665        }
1666
1667        // Test not ready statuses (should trigger retry)
1668        for status in &not_ready_statuses {
1669            let is_not_ready = status.is_empty() || *status == "Not Assessed";
1670            assert!(is_not_ready, "Status should trigger retry: '{status}'");
1671        }
1672    }
1673
1674    #[test]
1675    fn test_combined_method_return_types() {
1676        use std::borrow::Cow;
1677
1678        // Test the return type structure of the new combined method
1679        // This verifies the tuple structure is correct
1680
1681        // Test Some compliance status
1682        let compliance_status = Cow::Borrowed("Passed");
1683        assert_eq!(compliance_status.as_ref(), "Passed");
1684
1685        // Test None compliance status (when break_build is disabled)
1686        let compliance_status: Option<Cow<'static, str>> = None;
1687        assert!(compliance_status.is_none());
1688    }
1689
1690    #[test]
1691    fn test_debug_logging_parameters() {
1692        // Test debug parameter handling
1693        let debug_enabled = true;
1694        let debug_disabled = false;
1695
1696        assert!(debug_enabled);
1697        assert!(!debug_disabled);
1698
1699        // Test debug messages would be printed when debug=true
1700        // (Actual output testing would require integration tests)
1701        if debug_enabled {
1702            // Debug messages would be printed - this is just a placeholder
1703        }
1704
1705        if !debug_disabled {
1706            // Debug messages would be printed - this is just a placeholder
1707        }
1708    }
1709
1710    #[test]
1711    fn test_break_build_flag_logic() {
1712        // Test the enable_break_build flag logic
1713        let break_build_enabled = true;
1714        let break_build_disabled = false;
1715
1716        // When break_build is enabled, compliance_status should be Some(_)
1717        if break_build_enabled {
1718            // Would return (summary_report, Some(compliance_status))
1719            let compliance_returned = true;
1720            assert!(compliance_returned);
1721        }
1722
1723        // When break_build is disabled, compliance_status should be None
1724        if !break_build_disabled {
1725            // Would return (summary_report, None)
1726            let compliance_returned = false;
1727            assert!(!compliance_returned);
1728        }
1729    }
1730}