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        sandbox_id: Option<&str>,
773    ) -> Result<std::borrow::Cow<'static, str>, PolicyError> {
774        self.evaluate_policy_compliance_via_buildinfo_with_retry(app_id, sandbox_id, 30, 10)
775            .await
776    }
777
778    /// Evaluates policy compliance with retry logic for when assessment is not yet complete
779    ///
780    /// This function will retry the policy evaluation check when the status is "Not Assessed"
781    /// until either the assessment completes or the maximum retry attempts are reached.
782    ///
783    /// # Arguments
784    ///
785    /// * `app_id` - The numeric ID of the application
786    /// * `sandbox_id` - Optional numeric ID of the sandbox to evaluate
787    /// * `max_retries` - Maximum number of retry attempts (default: 30)
788    /// * `retry_delay_seconds` - Delay between retries in seconds (default: 10)
789    ///
790    /// # Returns
791    ///
792    /// A `Result` containing the policy compliance status string or an error.
793    ///
794    /// # Errors
795    ///
796    /// Returns an error if the API request fails, the policy is invalid,
797    /// or authentication/authorization fails.
798    pub async fn evaluate_policy_compliance_via_buildinfo_with_retry(
799        &self,
800        app_id: &str,
801        sandbox_id: Option<&str>,
802        max_retries: u32,
803        retry_delay_seconds: u64,
804    ) -> Result<std::borrow::Cow<'static, str>, PolicyError> {
805        use crate::build::{BuildError, GetBuildInfoRequest};
806        use std::borrow::Cow;
807        use tokio::time::{Duration, sleep};
808
809        let build_request = GetBuildInfoRequest {
810            app_id: app_id.to_string(),
811            build_id: None, // Get latest build
812            sandbox_id: sandbox_id.map(str::to_string),
813        };
814
815        let mut attempts: u32 = 0;
816        loop {
817            let build_info = self
818                .client
819                .build_api()?
820                .get_build_info(&build_request)
821                .await
822                .map_err(|e| match e {
823                    BuildError::BuildNotFound => PolicyError::Api(crate::VeracodeError::InvalidResponse(
824                        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.")
825                    )),
826                    BuildError::ApplicationNotFound => PolicyError::Api(crate::VeracodeError::InvalidResponse(
827                        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.")
828                    )),
829                    BuildError::SandboxNotFound => PolicyError::Api(crate::VeracodeError::InvalidResponse(
830                        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"))
831                    )),
832                    BuildError::Api(api_err) => PolicyError::Api(api_err),
833                    BuildError::InvalidParameter(msg)
834                    | BuildError::CreationFailed(msg)
835                    | BuildError::UpdateFailed(msg)
836                    | BuildError::DeletionFailed(msg)
837                    | BuildError::XmlParsingError(msg) => {
838                        PolicyError::Api(crate::VeracodeError::InvalidResponse(msg))
839                    }
840                    BuildError::Unauthorized | BuildError::PermissionDenied => PolicyError::Api(
841                        crate::VeracodeError::Authentication("Build API access denied".to_string()),
842                    ),
843                    BuildError::BuildInProgress => {
844                        PolicyError::Api(crate::VeracodeError::InvalidResponse(
845                            "Build is currently in progress".to_string(),
846                        ))
847                    }
848                })?;
849
850            // Get the policy compliance status
851            let status = build_info
852                .policy_compliance_status
853                .as_deref()
854                .unwrap_or("Not Assessed");
855
856            // If status is ready (not in-progress), return the result
857            if status != "Not Assessed" && status != "Calculating..." {
858                return Ok(Cow::Owned(status.to_string()));
859            }
860
861            // If we've reached max retries, return "Not Assessed"
862            attempts = attempts.saturating_add(1);
863            if attempts >= max_retries {
864                warn!(
865                    "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"
866                );
867                return Ok(Cow::Borrowed("Not Assessed"));
868            }
869
870            // Log retry attempt
871            info!(
872                "Policy evaluation not yet assessed, retrying in {retry_delay_seconds} seconds... (attempt {attempts}/{max_retries})"
873            );
874
875            // Wait before retrying
876            sleep(Duration::from_secs(retry_delay_seconds)).await;
877        }
878    }
879
880    /// Determines if build should break based on policy compliance status
881    ///
882    /// # Arguments
883    ///
884    /// * `status` - The policy compliance status string from XML API
885    ///
886    /// # Returns
887    ///
888    /// `true` if build should break, `false` otherwise
889    #[must_use]
890    pub fn should_break_build(status: &str) -> bool {
891        status == "Did Not Pass"
892    }
893
894    /// Gets the appropriate exit code for CI/CD systems based on policy compliance
895    ///
896    /// # Arguments
897    ///
898    /// * `status` - The policy compliance status string from XML API
899    ///
900    /// # Returns
901    ///
902    /// Exit code: 0 for success, 4 for policy failure (build break)
903    #[must_use]
904    pub fn get_exit_code_for_status(status: &str) -> i32 {
905        if Self::should_break_build(status) {
906            4 // DID_NOT_PASSED_POLICY - matches Java wrapper
907        } else {
908            0 // SUCCESS
909        }
910    }
911
912    /// Get summary report for an application build using the REST API
913    ///
914    ///
915    /// # Errors
916    ///
917    /// Returns an error if the API request fails, the resource is not found,
918    /// or authentication/authorization fails.
919    /// This uses the `/appsec/v2/applications/{app_guid}/summary_report` endpoint
920    /// to get policy compliance status and scan results.
921    ///
922    /// # Arguments
923    ///
924    /// * `app_guid` - The GUID of the application
925    /// * `build_id` - The build ID (GUID) to get summary for
926    /// * `sandbox_guid` - Optional sandbox GUID for sandbox scans
927    ///
928    /// # Returns
929    ///
930    /// A `Result` containing the summary report or an error.
931    ///
932    /// # Errors
933    ///
934    /// Returns an error if the API request fails, the policy is invalid,
935    /// or authentication/authorization fails.
936    pub async fn get_summary_report(
937        &self,
938        app_guid: &str,
939        build_id: Option<&str>,
940        sandbox_guid: Option<&str>,
941    ) -> Result<SummaryReport, PolicyError> {
942        let endpoint = format!("/appsec/v2/applications/{app_guid}/summary_report");
943
944        // Build query parameters
945        let mut query_params = Vec::new();
946        if let Some(build_id) = build_id {
947            query_params.push(("build_id".to_string(), build_id.to_string()));
948        }
949        if let Some(sandbox_guid) = sandbox_guid {
950            query_params.push(("context".to_string(), sandbox_guid.to_string()));
951        }
952
953        let response = self.client.get(&endpoint, Some(&query_params)).await?;
954
955        let status = response.status().as_u16();
956        match status {
957            200 => {
958                let summary_report: SummaryReport = response.json().await?;
959                Ok(summary_report)
960            }
961            400 => {
962                let error_text = response.text().await.unwrap_or_default();
963                Err(PolicyError::InvalidConfig(error_text))
964            }
965            401 => Err(PolicyError::Unauthorized),
966            403 => Err(PolicyError::PermissionDenied),
967            404 => Err(PolicyError::NotFound),
968            500 => Err(PolicyError::InternalServerError),
969            _ => {
970                let error_text = response.text().await.unwrap_or_default();
971                Err(PolicyError::Api(VeracodeError::InvalidResponse(format!(
972                    "HTTP {status}: {error_text}"
973                ))))
974            }
975        }
976    }
977
978    /// Gets summary report with retry logic and returns both the full report and compliance status
979    ///
980    /// This function combines the functionality of both `get_summary_report` and
981    /// `evaluate_policy_compliance_via_summary_report_with_retry` to avoid redundant API calls.
982    /// It will retry until the policy compliance status is ready (not "Not Assessed").
983    ///
984    /// # Arguments
985    ///
986    /// * `app_guid` - The GUID of the application
987    /// * `build_id` - The build ID to check compliance for
988    /// * `sandbox_guid` - Optional sandbox GUID for sandbox scans
989    /// * `max_retries` - Maximum number of retry attempts
990    /// * `retry_delay_seconds` - Delay between retries in seconds
991    /// * `debug` - Enable debug logging
992    ///
993    /// # Returns
994    ///
995    ///
996    /// # Errors
997    ///
998    /// Returns an error if the API request fails, the policy is invalid,
999    /// or authentication/authorization fails.
1000    /// A `Result` containing a tuple of (`SummaryReport`, Option<`compliance_status`>) or an error.
1001    ///
1002    /// # Errors
1003    ///
1004    /// Returns an error if the API request fails, the policy is invalid,
1005    /// or authentication/authorization fails.
1006    /// The `compliance_status` is Some(status) if `break_build` evaluation is needed, None otherwise.
1007    #[allow(clippy::too_many_arguments)]
1008    ///
1009    /// # Errors
1010    ///
1011    /// Returns an error if the API request fails, the policy is invalid,
1012    /// or authentication/authorization fails.
1013    pub async fn get_summary_report_with_policy_retry(
1014        &self,
1015        app_guid: &str,
1016        build_id: Option<&str>,
1017        sandbox_guid: Option<&str>,
1018        max_retries: u32,
1019        retry_delay_seconds: u64,
1020        enable_break_build: bool,
1021    ) -> Result<(SummaryReport, Option<std::borrow::Cow<'static, str>>), PolicyError> {
1022        use std::borrow::Cow;
1023        use tokio::time::{Duration, sleep};
1024
1025        if enable_break_build && build_id.is_none() {
1026            return Err(PolicyError::InvalidConfig(
1027                "Build ID is required for break build policy evaluation".to_string(),
1028            ));
1029        }
1030
1031        let mut attempts: u32 = 0;
1032        loop {
1033            if attempts == 0 && enable_break_build {
1034                debug!("Checking policy compliance status with retry logic...");
1035            } else if attempts == 0 {
1036                debug!("Getting summary report...");
1037            }
1038
1039            let summary_report = match self
1040                .get_summary_report(app_guid, build_id, sandbox_guid)
1041                .await
1042            {
1043                Ok(report) => report,
1044                Err(PolicyError::InternalServerError) if attempts < 3 => {
1045                    warn!(
1046                        "Summary report API failed with server error (attempt {}/3), retrying in 5 seconds...",
1047                        attempts.saturating_add(1)
1048                    );
1049                    sleep(Duration::from_secs(5)).await;
1050                    attempts = attempts.saturating_add(1);
1051                    continue;
1052                }
1053                Err(e) => return Err(e),
1054            };
1055
1056            // If break_build is not enabled, return immediately with the report
1057            if !enable_break_build {
1058                return Ok((summary_report, None));
1059            }
1060
1061            // For `break_build` evaluation, check if policy compliance status is ready
1062            let status = summary_report.policy_compliance_status.clone();
1063
1064            // If status is ready (not empty and not "Not Assessed"), return both report and status
1065            if !status.is_empty() && status != "Not Assessed" {
1066                debug!("Policy compliance status ready: {status}");
1067                return Ok((summary_report, Some(Cow::Owned(status))));
1068            }
1069
1070            // If we've reached max retries, return current results
1071            attempts = attempts.saturating_add(1);
1072            if attempts >= max_retries {
1073                warn!(
1074                    "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"
1075                );
1076                return Ok((summary_report, Some(Cow::Owned(status))));
1077            }
1078
1079            // Log retry attempt
1080            info!(
1081                "Policy evaluation not yet ready (status: '{status}'), retrying in {retry_delay_seconds} seconds... (attempt {attempts}/{max_retries})"
1082            );
1083
1084            // Wait before retrying
1085            sleep(Duration::from_secs(retry_delay_seconds)).await;
1086        }
1087    }
1088
1089    /// Evaluates policy compliance using the summary report API with retry logic
1090    ///
1091    ///
1092    /// # Errors
1093    ///
1094    /// Returns an error if the API request fails, the policy is invalid,
1095    /// or authentication/authorization fails.
1096    /// This function uses the `summary_report` endpoint instead of the buildinfo XML API
1097    /// and will retry when results are not ready yet.
1098    ///
1099    /// # Arguments
1100    ///
1101    /// * `app_guid` - The GUID of the application  
1102    /// * `build_id` - The build ID (GUID) to check compliance for
1103    /// * `sandbox_guid` - Optional sandbox GUID for sandbox scans
1104    /// * `max_retries` - Maximum number of retry attempts (default: 30)
1105    /// * `retry_delay_seconds` - Delay between retries in seconds (default: 10)
1106    ///
1107    /// # Returns
1108    ///
1109    /// A `Result` containing the policy compliance status string or an error.
1110    ///
1111    /// # Errors
1112    ///
1113    /// Returns an error if the API request fails, the policy is invalid,
1114    /// or authentication/authorization fails.
1115    pub async fn evaluate_policy_compliance_via_summary_report_with_retry(
1116        &self,
1117        app_guid: &str,
1118        build_id: &str,
1119        sandbox_guid: Option<&str>,
1120        max_retries: u32,
1121        retry_delay_seconds: u64,
1122    ) -> Result<std::borrow::Cow<'static, str>, PolicyError> {
1123        use std::borrow::Cow;
1124        use tokio::time::{Duration, sleep};
1125
1126        let mut attempts: u32 = 0;
1127        loop {
1128            let summary_report = self
1129                .get_summary_report(app_guid, Some(build_id), sandbox_guid)
1130                .await?;
1131
1132            // Check if results are ready - look for "Results Ready" or completed status
1133            // The summary report should have policy_compliance_status populated when ready
1134            let status = &summary_report.policy_compliance_status;
1135
1136            // If status is not empty and not "Not Assessed", return the result
1137            if !status.is_empty() && status != "Not Assessed" {
1138                return Ok(Cow::Owned(status.clone()));
1139            }
1140
1141            // If we've reached max retries, return current status
1142            attempts = attempts.saturating_add(1);
1143            if attempts >= max_retries {
1144                warn!(
1145                    "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"
1146                );
1147                return Ok(Cow::Owned(status.clone()));
1148            }
1149
1150            // Log retry attempt
1151            info!(
1152                "Policy evaluation not yet ready (status: '{status}'), retrying in {retry_delay_seconds} seconds... (attempt {attempts}/{max_retries})"
1153            );
1154
1155            // Wait before retrying
1156            sleep(Duration::from_secs(retry_delay_seconds)).await;
1157        }
1158    }
1159
1160    /// Evaluates policy compliance using the summary report API (single attempt)
1161    ///
1162    /// This is a convenience method that calls the retry version with default parameters.
1163    ///
1164    /// # Arguments
1165    ///
1166    /// * `app_guid` - The GUID of the application  
1167    /// * `build_id` - The build ID (GUID) to check compliance for
1168    /// * `sandbox_guid` - Optional sandbox GUID for sandbox scans
1169    ///
1170    /// # Returns
1171    ///
1172    /// A `Result` containing the policy compliance status string or an error.
1173    ///
1174    /// # Errors
1175    ///
1176    /// Returns an error if the API request fails, the policy is invalid,
1177    /// or authentication/authorization fails.
1178    pub async fn evaluate_policy_compliance_via_summary_report(
1179        &self,
1180        app_guid: &str,
1181        build_id: &str,
1182        sandbox_guid: Option<&str>,
1183    ) -> Result<std::borrow::Cow<'static, str>, PolicyError> {
1184        self.evaluate_policy_compliance_via_summary_report_with_retry(
1185            app_guid,
1186            build_id,
1187            sandbox_guid,
1188            30,
1189            10,
1190        )
1191        .await
1192    }
1193
1194    /// Initiate a policy scan for an application
1195    ///
1196    /// # Arguments
1197    ///
1198    /// * `request` - The policy scan request
1199    ///
1200    /// # Returns
1201    ///
1202    /// A `Result` containing the scan result or an error.
1203    ///
1204    /// # Errors
1205    ///
1206    /// Returns an error if the API request fails, the policy is invalid,
1207    /// or authentication/authorization fails.
1208    pub async fn initiate_policy_scan(
1209        &self,
1210        request: PolicyScanRequest,
1211    ) -> Result<PolicyScanResult, PolicyError> {
1212        let endpoint = "/appsec/v1/policy-scans";
1213
1214        let response = self.client.post(endpoint, Some(&request)).await?;
1215
1216        let status = response.status().as_u16();
1217        match status {
1218            200 | 201 => {
1219                let scan_result: PolicyScanResult = response.json().await?;
1220                Ok(scan_result)
1221            }
1222            400 => {
1223                let error_text = response.text().await.unwrap_or_default();
1224                Err(PolicyError::InvalidConfig(error_text))
1225            }
1226            404 => Err(PolicyError::NotFound),
1227            _ => {
1228                let error_text = response.text().await.unwrap_or_default();
1229                Err(PolicyError::Api(VeracodeError::InvalidResponse(format!(
1230                    "HTTP {status}: {error_text}"
1231                ))))
1232            }
1233        }
1234    }
1235
1236    /// Get policy scan status and results
1237    ///
1238    /// # Arguments
1239    ///
1240    /// * `scan_id` - The ID of the policy scan
1241    ///
1242    /// # Returns
1243    ///
1244    /// A `Result` containing the scan result or an error.
1245    ///
1246    /// # Errors
1247    ///
1248    /// Returns an error if the API request fails, the policy is invalid,
1249    /// or authentication/authorization fails.
1250    pub async fn get_policy_scan_result(
1251        &self,
1252        scan_id: u64,
1253    ) -> Result<PolicyScanResult, PolicyError> {
1254        let endpoint = format!("/appsec/v1/policy-scans/{scan_id}");
1255
1256        let response = self.client.get(&endpoint, None).await?;
1257
1258        let status = response.status().as_u16();
1259        match status {
1260            200 => {
1261                let scan_result: PolicyScanResult = response.json().await?;
1262                Ok(scan_result)
1263            }
1264            404 => Err(PolicyError::NotFound),
1265            _ => {
1266                let error_text = response.text().await.unwrap_or_default();
1267                Err(PolicyError::Api(VeracodeError::InvalidResponse(format!(
1268                    "HTTP {status}: {error_text}"
1269                ))))
1270            }
1271        }
1272    }
1273
1274    /// Check if a policy scan is complete
1275    ///
1276    /// # Arguments
1277    ///
1278    /// * `scan_id` - The ID of the policy scan
1279    ///
1280    /// # Returns
1281    ///
1282    /// A `Result` containing a boolean indicating completion status.
1283    ///
1284    /// # Errors
1285    ///
1286    /// Returns an error if the API request fails, the policy is invalid,
1287    /// or authentication/authorization fails.
1288    pub async fn is_policy_scan_complete(&self, scan_id: u64) -> Result<bool, PolicyError> {
1289        let scan_result = self.get_policy_scan_result(scan_id).await?;
1290        Ok(matches!(
1291            scan_result.status,
1292            ScanStatus::Completed | ScanStatus::Failed | ScanStatus::Cancelled
1293        ))
1294    }
1295
1296    /// Gets policy compliance status with automatic fallback from summary report to buildinfo
1297    ///
1298    /// This method first tries the summary report API for full functionality. If access is denied
1299    /// (401/403), it automatically falls back to the getbuildinfo.do XML API for policy compliance
1300    /// status only. This provides the best user experience while maintaining compatibility.
1301    ///
1302    /// # Arguments
1303    ///
1304    /// * `app_guid` - Application GUID (for REST API)
1305    /// * `app_id` - Application numeric ID (for XML API fallback)
1306    /// * `build_id` - Optional build ID
1307    /// * `sandbox_guid` - Optional sandbox GUID (for REST API)
1308    /// * `sandbox_id` - Optional sandbox numeric ID (for XML API fallback)
1309    /// * `max_retries` - Maximum number of retry attempts
1310    /// * `retry_delay_seconds` - Delay between retries in seconds
1311    /// * `enable_break_build` - Whether to enable break build evaluation
1312    /// * `force_buildinfo_api` - Skip summary report and use buildinfo directly
1313    ///
1314    /// # Returns
1315    ///
1316    /// A tuple containing:
1317    ///
1318    /// # Errors
1319    ///
1320    /// Returns an error if the API request fails, the policy is invalid,
1321    /// or authentication/authorization fails.
1322    /// - Optional `SummaryReport` (None if fallback was used)
1323    /// - Policy compliance status string
1324    ///
1325    /// # Errors
1326    ///
1327    /// Returns an error if the API request fails, the policy is invalid,
1328    /// or authentication/authorization fails.
1329    /// - `ApiSource` indicating which API was used
1330    #[allow(clippy::too_many_arguments)]
1331    ///
1332    /// # Errors
1333    ///
1334    /// Returns an error if the API request fails, the policy is invalid,
1335    /// or authentication/authorization fails.
1336    pub async fn get_policy_status_with_fallback(
1337        &self,
1338        app_guid: &str,
1339        app_id: &str,
1340        build_id: Option<&str>,
1341        sandbox_guid: Option<&str>,
1342        sandbox_id: Option<&str>,
1343        max_retries: u32,
1344        retry_delay_seconds: u64,
1345        enable_break_build: bool,
1346        force_buildinfo_api: bool,
1347    ) -> Result<(Option<SummaryReport>, String, ApiSource), PolicyError> {
1348        if force_buildinfo_api {
1349            // DIRECT PATH: Skip summary report, use getbuildinfo.do directly
1350            debug!("Using getbuildinfo.do API directly (forced via configuration)");
1351            let status = self
1352                .evaluate_policy_compliance_via_buildinfo_with_retry(
1353                    app_id,
1354                    sandbox_id,
1355                    max_retries,
1356                    retry_delay_seconds,
1357                )
1358                .await?;
1359            return Ok((None, status.to_string(), ApiSource::BuildInfo));
1360        }
1361
1362        // FALLBACK PATH: Try summary report first, fallback to getbuildinfo.do
1363        match self
1364            .get_summary_report_with_policy_retry(
1365                app_guid,
1366                build_id,
1367                sandbox_guid,
1368                max_retries,
1369                retry_delay_seconds,
1370                enable_break_build,
1371            )
1372            .await
1373        {
1374            Ok((summary_report, compliance_status)) => {
1375                debug!("Used summary report API successfully");
1376                let status = compliance_status
1377                    .map(|s| s.to_string())
1378                    .unwrap_or_else(|| summary_report.policy_compliance_status.clone());
1379                Ok((Some(summary_report), status, ApiSource::SummaryReport))
1380            }
1381            Err(
1382                ref e @ (PolicyError::Unauthorized
1383                | PolicyError::PermissionDenied
1384                | PolicyError::InternalServerError),
1385            ) => {
1386                match *e {
1387                    PolicyError::InternalServerError => info!(
1388                        "Summary report API server error, falling back to getbuildinfo.do API"
1389                    ),
1390                    PolicyError::Unauthorized | PolicyError::PermissionDenied => {
1391                        info!("Summary report access denied, falling back to getbuildinfo.do API")
1392                    }
1393                    PolicyError::Api(_)
1394                    | PolicyError::NotFound
1395                    | PolicyError::InvalidConfig(_)
1396                    | PolicyError::ScanFailed(_)
1397                    | PolicyError::EvaluationError(_)
1398                    | PolicyError::Timeout => {}
1399                }
1400                let status = self
1401                    .evaluate_policy_compliance_via_buildinfo_with_retry(
1402                        app_id,
1403                        sandbox_id,
1404                        max_retries,
1405                        retry_delay_seconds,
1406                    )
1407                    .await?;
1408                Ok((None, status.to_string(), ApiSource::BuildInfo))
1409            }
1410            Err(e) => Err(e),
1411        }
1412    }
1413
1414    /// Get active policies for the organization
1415    ///
1416    /// # Returns
1417    ///
1418    /// A `Result` containing a list of active policies or an error.
1419    ///
1420    /// # Errors
1421    ///
1422    /// Returns an error if the API request fails, the policy is invalid,
1423    /// or authentication/authorization fails.
1424    pub async fn get_active_policies(&self) -> Result<Vec<SecurityPolicy>, PolicyError> {
1425        // Note: The active/inactive concept may need to be handled differently
1426        // based on the actual API response structure
1427        let policies = self.list_policies(None).await?;
1428        Ok(policies) // Return all policies for now
1429    }
1430}
1431
1432#[cfg(test)]
1433#[allow(clippy::expect_used)]
1434mod tests {
1435    use super::*;
1436
1437    #[test]
1438    fn test_policy_list_params_to_query() {
1439        let params = PolicyListParams {
1440            name: Some("test-policy".to_string()),
1441            is_active: Some(true),
1442            page: Some(1),
1443            size: Some(10),
1444            ..Default::default()
1445        };
1446
1447        let query_params: Vec<_> = params.into();
1448        assert_eq!(query_params.len(), 4);
1449        assert!(query_params.contains(&("name".to_string(), "test-policy".to_string())));
1450        assert!(query_params.contains(&("active".to_string(), "true".to_string())));
1451        assert!(query_params.contains(&("page".to_string(), "1".to_string())));
1452        assert!(query_params.contains(&("size".to_string(), "10".to_string())));
1453    }
1454
1455    #[test]
1456    fn test_policy_error_display() {
1457        let error = PolicyError::NotFound;
1458        assert_eq!(error.to_string(), "Policy not found");
1459
1460        let error = PolicyError::InvalidConfig("test".to_string());
1461        assert_eq!(error.to_string(), "Invalid policy configuration: test");
1462
1463        let error = PolicyError::Timeout;
1464        assert_eq!(error.to_string(), "Policy operation timed out");
1465    }
1466
1467    #[test]
1468    fn test_scan_type_serialization() {
1469        let scan_type = ScanType::Static;
1470        let json = serde_json::to_string(&scan_type).expect("should serialize to json");
1471        assert_eq!(json, "\"static\"");
1472
1473        let deserialized: ScanType = serde_json::from_str(&json).expect("should deserialize json");
1474        assert!(matches!(deserialized, ScanType::Static));
1475    }
1476
1477    #[test]
1478    fn test_policy_compliance_status_serialization() {
1479        let status = PolicyComplianceStatus::Passed;
1480        let json = serde_json::to_string(&status).expect("should serialize to json");
1481        assert_eq!(json, "\"Passed\"");
1482
1483        let deserialized: PolicyComplianceStatus =
1484            serde_json::from_str(&json).expect("should deserialize json");
1485        assert!(matches!(deserialized, PolicyComplianceStatus::Passed));
1486
1487        // Test the special case statuses with spaces
1488        let conditional_pass = PolicyComplianceStatus::ConditionalPass;
1489        let json = serde_json::to_string(&conditional_pass).expect("should serialize to json");
1490        assert_eq!(json, "\"Conditional Pass\"");
1491
1492        let did_not_pass = PolicyComplianceStatus::DidNotPass;
1493        let json = serde_json::to_string(&did_not_pass).expect("should serialize to json");
1494        assert_eq!(json, "\"Did Not Pass\"");
1495    }
1496
1497    #[test]
1498    fn test_break_build_logic() {
1499        assert!(PolicyApi::should_break_build("Did Not Pass"));
1500        assert!(!PolicyApi::should_break_build("Passed"));
1501        assert!(!PolicyApi::should_break_build("Conditional Pass"));
1502        // "Not Assessed" should not break build as the retry logic should handle waiting
1503        // for policy evaluation to complete before reaching this point
1504        assert!(!PolicyApi::should_break_build("Not Assessed"));
1505
1506        assert_eq!(PolicyApi::get_exit_code_for_status("Did Not Pass"), 4);
1507        assert_eq!(PolicyApi::get_exit_code_for_status("Passed"), 0);
1508        assert_eq!(PolicyApi::get_exit_code_for_status("Conditional Pass"), 0);
1509        // "Not Assessed" returns 0 because it should only reach here after retry logic
1510        // has exhausted attempts, indicating a configuration or timing issue rather than policy failure
1511        assert_eq!(PolicyApi::get_exit_code_for_status("Not Assessed"), 0);
1512    }
1513
1514    #[test]
1515    fn test_summary_report_serialization() {
1516        let summary_json = r#"{
1517            "app_id": 2676517,
1518            "app_name": "Verascan Java Test",
1519            "build_id": 54209787,
1520            "policy_compliance_status": "Did Not Pass",
1521            "policy_name": "SecureCode Policy",
1522            "policy_version": 1,
1523            "policy_rules_status": "Did Not Pass",
1524            "grace_period_expired": false,
1525            "scan_overdue": "false",
1526            "is_latest_build": false,
1527            "generation_date": "2025-08-05 10:14:45 UTC",
1528            "last_update_time": "2025-08-05 10:00:51 UTC"
1529        }"#;
1530
1531        let summary: Result<SummaryReport, _> = serde_json::from_str(summary_json);
1532        assert!(summary.is_ok());
1533
1534        let summary = summary.expect("should have summary");
1535        assert_eq!(summary.policy_compliance_status, "Did Not Pass");
1536        assert_eq!(summary.app_name, "Verascan Java Test");
1537        assert_eq!(summary.build_id, 54209787);
1538        assert!(PolicyApi::should_break_build(
1539            &summary.policy_compliance_status
1540        ));
1541    }
1542
1543    #[test]
1544    fn test_export_json_structure() {
1545        // Test the JSON structure that would be exported
1546        let summary_report = SummaryReport {
1547            app_id: 2676517,
1548            app_name: "Test App".to_string(),
1549            build_id: 54209787,
1550            policy_compliance_status: "Passed".to_string(),
1551            policy_name: "Test Policy".to_string(),
1552            policy_version: 1,
1553            policy_rules_status: "Passed".to_string(),
1554            grace_period_expired: false,
1555            scan_overdue: "false".to_string(),
1556            is_latest_build: true,
1557            sandbox_name: Some("test-sandbox".to_string()),
1558            sandbox_id: Some(123456),
1559            generation_date: "2025-08-05 10:14:45 UTC".to_string(),
1560            last_update_time: "2025-08-05 10:00:51 UTC".to_string(),
1561            static_analysis: None,
1562            flaw_status: None,
1563            software_composition_analysis: None,
1564            severity: None,
1565        };
1566
1567        let export_json = serde_json::json!({
1568            "summary_report": summary_report,
1569            "export_metadata": {
1570                "exported_at": "2025-08-05T10:14:45Z",
1571                "tool": "verascan",
1572                "export_type": "summary_report",
1573                "scan_configuration": {
1574                    "autoscan": true,
1575                    "scan_all_nonfatal_top_level_modules": true,
1576                    "include_new_modules": true
1577                }
1578            }
1579        });
1580
1581        // Verify JSON structure
1582        assert!(
1583            export_json
1584                .get("summary_report")
1585                .and_then(|s| s.get("app_name"))
1586                .map(|v| v.is_string())
1587                .unwrap_or(false)
1588        );
1589        assert!(
1590            export_json
1591                .get("summary_report")
1592                .and_then(|s| s.get("policy_compliance_status"))
1593                .map(|v| v.is_string())
1594                .unwrap_or(false)
1595        );
1596        assert!(
1597            export_json
1598                .get("export_metadata")
1599                .and_then(|e| e.get("export_type"))
1600                .map(|v| v.is_string())
1601                .unwrap_or(false)
1602        );
1603        assert_eq!(
1604            export_json
1605                .get("export_metadata")
1606                .and_then(|e| e.get("export_type"))
1607                .and_then(|v| v.as_str())
1608                .expect("should have export_type"),
1609            "summary_report"
1610        );
1611
1612        // Verify the summary report can be serialized and deserialized
1613        let json_string =
1614            serde_json::to_string_pretty(&export_json).expect("should serialize to json");
1615        assert!(json_string.contains("summary_report"));
1616        assert!(json_string.contains("export_metadata"));
1617    }
1618
1619    #[test]
1620    fn test_get_summary_report_with_policy_retry_parameters() {
1621        // Unit tests for the new combined method parameter validation and logic
1622
1623        // Test parameter type validation
1624        let app_guid = "test-app-guid";
1625        let build_id = Some("test-build-id");
1626        let sandbox_guid: Option<&str> = None;
1627        let max_retries = 30u32;
1628        let retry_delay_seconds = 10u64;
1629        let debug = false;
1630        let enable_break_build = true;
1631
1632        // Verify parameter types are correct
1633        assert_eq!(app_guid, "test-app-guid");
1634        assert_eq!(build_id, Some("test-build-id"));
1635        assert_eq!(sandbox_guid, None);
1636        assert_eq!(max_retries, 30);
1637        assert_eq!(retry_delay_seconds, 10);
1638        assert!(!debug);
1639        assert!(enable_break_build);
1640    }
1641
1642    #[test]
1643    fn test_policy_status_ready_logic() {
1644        // Test the logic for determining when policy status is ready
1645        let ready_statuses = vec!["Passed", "Did Not Pass", "Conditional Pass"];
1646        let not_ready_statuses = vec!["", "Not Assessed"];
1647
1648        // Test ready statuses (should not trigger retry)
1649        for status in &ready_statuses {
1650            assert!(
1651                !status.is_empty(),
1652                "Ready status should not be empty: {status}"
1653            );
1654            assert_ne!(
1655                *status, "Not Assessed",
1656                "Ready status should not be 'Not Assessed': {status}"
1657            );
1658        }
1659
1660        // Test not ready statuses (should trigger retry)
1661        for status in &not_ready_statuses {
1662            let is_not_ready = status.is_empty() || *status == "Not Assessed";
1663            assert!(is_not_ready, "Status should trigger retry: '{status}'");
1664        }
1665    }
1666
1667    #[test]
1668    fn test_combined_method_return_types() {
1669        use std::borrow::Cow;
1670
1671        // Test the return type structure of the new combined method
1672        // This verifies the tuple structure is correct
1673
1674        // Test Some compliance status
1675        let compliance_status = Cow::Borrowed("Passed");
1676        assert_eq!(compliance_status.as_ref(), "Passed");
1677
1678        // Test None compliance status (when break_build is disabled)
1679        let compliance_status: Option<Cow<'static, str>> = None;
1680        assert!(compliance_status.is_none());
1681    }
1682
1683    #[test]
1684    fn test_debug_logging_parameters() {
1685        // Test debug parameter handling
1686        let debug_enabled = true;
1687        let debug_disabled = false;
1688
1689        assert!(debug_enabled);
1690        assert!(!debug_disabled);
1691
1692        // Test debug messages would be printed when debug=true
1693        // (Actual output testing would require integration tests)
1694        if debug_enabled {
1695            // Debug messages would be printed - this is just a placeholder
1696        }
1697
1698        if !debug_disabled {
1699            // Debug messages would be printed - this is just a placeholder
1700        }
1701    }
1702
1703    #[test]
1704    fn test_break_build_flag_logic() {
1705        // Test the enable_break_build flag logic
1706        let break_build_enabled = true;
1707        let break_build_disabled = false;
1708
1709        // When break_build is enabled, compliance_status should be Some(_)
1710        if break_build_enabled {
1711            // Would return (summary_report, Some(compliance_status))
1712            let compliance_returned = true;
1713            assert!(compliance_returned);
1714        }
1715
1716        // When break_build is disabled, compliance_status should be None
1717        if !break_build_disabled {
1718            // Would return (summary_report, None)
1719            let compliance_returned = false;
1720            assert!(!compliance_returned);
1721        }
1722    }
1723}