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/// Summary report data structure matching Veracode summary_report API response
272#[derive(Debug, Clone, Serialize, Deserialize)]
273pub struct SummaryReport {
274    /// Application ID
275    pub app_id: u64,
276    /// Application name
277    pub app_name: String,
278    /// Build ID
279    pub build_id: u64,
280    /// Policy compliance status (e.g., "Did Not Pass", "Passed", "Conditional Pass")
281    pub policy_compliance_status: String,
282    /// Policy name
283    pub policy_name: String,
284    /// Policy version
285    pub policy_version: u32,
286    /// Whether the policy rules status passed
287    pub policy_rules_status: String,
288    /// Whether grace period expired
289    pub grace_period_expired: bool,
290    /// Whether scan is overdue
291    pub scan_overdue: String,
292    /// Whether this is the latest build
293    pub is_latest_build: bool,
294    /// Sandbox name (optional)
295    pub sandbox_name: Option<String>,
296    /// Sandbox ID (optional)
297    pub sandbox_id: Option<u64>,
298    /// Generation date
299    pub generation_date: String,
300    /// Last update time
301    pub last_update_time: String,
302    /// Static analysis summary
303    #[serde(rename = "static-analysis")]
304    pub static_analysis: Option<StaticAnalysisSummary>,
305    /// Flaw status summary
306    #[serde(rename = "flaw-status")]
307    pub flaw_status: Option<FlawStatusSummary>,
308    /// Software composition analysis summary
309    pub software_composition_analysis: Option<ScaSummary>,
310    /// Severity breakdown
311    pub severity: Option<Vec<SeverityLevel>>,
312}
313
314/// Static analysis summary from summary report
315#[derive(Debug, Clone, Serialize, Deserialize)]
316pub struct StaticAnalysisSummary {
317    /// Rating (e.g., "A", "B", "C")
318    pub rating: Option<String>,
319    /// Score (0-100)
320    pub score: Option<u32>,
321    /// Mitigated rating
322    pub mitigated_rating: Option<String>,
323    /// Mitigated score
324    pub mitigated_score: Option<u32>,
325    /// Analysis size in bytes
326    pub analysis_size_bytes: Option<u64>,
327    /// Engine version
328    pub engine_version: Option<String>,
329    /// Published date
330    pub published_date: Option<String>,
331    /// Version/build identifier
332    pub version: Option<String>,
333}
334
335/// Flaw status summary from summary report
336#[derive(Debug, Clone, Serialize, Deserialize)]
337pub struct FlawStatusSummary {
338    /// New flaws
339    pub new: u32,
340    /// Reopened flaws
341    pub reopen: u32,
342    /// Open flaws
343    pub open: u32,
344    /// Fixed flaws
345    pub fixed: u32,
346    /// Total flaws
347    pub total: u32,
348    /// Not mitigated flaws
349    pub not_mitigated: u32,
350}
351
352/// Software Composition Analysis summary
353#[derive(Debug, Clone, Serialize, Deserialize)]
354pub struct ScaSummary {
355    /// Third party components count
356    pub third_party_components: u32,
357    /// Whether violates policy
358    pub violate_policy: bool,
359    /// Components that violated policy
360    pub components_violated_policy: u32,
361}
362
363/// Severity level breakdown from summary report
364#[derive(Debug, Clone, Serialize, Deserialize)]
365pub struct SeverityLevel {
366    /// Severity level (0-5)
367    pub level: u32,
368    /// Categories for this severity level
369    pub category: Vec<CategorySummary>,
370}
371
372/// Category summary within severity level
373#[derive(Debug, Clone, Serialize, Deserialize)]
374pub struct CategorySummary {
375    /// Category name
376    pub categoryname: String,
377    /// Severity name
378    pub severity: String,
379    /// Count of flaws in this category
380    pub count: u32,
381}
382
383/// Scan frequency rule for policies
384#[derive(Debug, Clone, Serialize, Deserialize)]
385pub struct ScanFrequencyRule {
386    /// Type of scan this rule applies to
387    pub scan_type: String,
388    /// How frequently scans should be performed
389    pub frequency: String,
390}
391
392/// Finding rule for policies
393#[derive(Debug, Clone, Serialize, Deserialize)]
394pub struct FindingRule {
395    /// Type of finding rule
396    #[serde(rename = "type")]
397    pub rule_type: String,
398    /// Scan types this rule applies to
399    pub scan_type: Vec<String>,
400    /// Rule value/threshold
401    pub value: String,
402    /// Advanced options for the rule
403    pub advanced_options: Option<serde_json::Value>,
404}
405
406/// Advanced options for finding rules
407#[derive(Debug, Clone, Serialize, Deserialize)]
408pub struct FindingRuleAdvancedOptions {
409    /// Override severity
410    pub override_severity: Option<bool>,
411    /// Build action (WARNING, ERROR, etc.)
412    pub build_action: Option<String>,
413    /// Component dependency type
414    pub component_dependency: Option<String>,
415    /// Vulnerable methods setting
416    pub vulnerable_methods: Option<String>,
417    /// Selected licenses
418    pub selected_licenses: Option<Vec<String>>,
419    /// Override severity level
420    pub override_severity_level: Option<String>,
421    /// Whether to allow non-OSS licenses
422    pub allowed_nonoss_licenses: Option<bool>,
423    /// Whether to allow unrecognized licenses
424    pub allowed_unrecognized_licenses: Option<bool>,
425    /// Whether all licenses must meet requirement
426    pub all_licenses_must_meet_requirement: Option<bool>,
427    /// Whether this is a blocklist
428    pub is_blocklist: Option<bool>,
429}
430
431/// Query parameters for listing policies
432#[derive(Debug, Clone, Default)]
433pub struct PolicyListParams {
434    /// Filter by policy name
435    pub name: Option<String>,
436    /// Filter by policy type
437    pub policy_type: Option<String>,
438    /// Filter by active status
439    pub is_active: Option<bool>,
440    /// Include only default policies
441    pub default_only: Option<bool>,
442    /// Page number for pagination
443    pub page: Option<u32>,
444    /// Number of items per page
445    pub size: Option<u32>,
446}
447
448impl PolicyListParams {
449    /// Convert to query parameters for HTTP requests
450    #[must_use]
451    pub fn to_query_params(&self) -> Vec<(String, String)> {
452        Vec::from(self) // Delegate to trait
453    }
454}
455
456// Trait implementations for memory optimization
457impl From<&PolicyListParams> for Vec<(String, String)> {
458    fn from(query: &PolicyListParams) -> Self {
459        let mut params = Vec::new();
460
461        if let Some(ref name) = query.name {
462            params.push(("name".to_string(), name.clone())); // Still clone for borrowing
463        }
464        if let Some(ref policy_type) = query.policy_type {
465            params.push(("type".to_string(), policy_type.clone()));
466        }
467        if let Some(is_active) = query.is_active {
468            params.push(("active".to_string(), is_active.to_string()));
469        }
470        if let Some(default_only) = query.default_only {
471            params.push(("default".to_string(), default_only.to_string()));
472        }
473        if let Some(page) = query.page {
474            params.push(("page".to_string(), page.to_string()));
475        }
476        if let Some(size) = query.size {
477            params.push(("size".to_string(), size.to_string()));
478        }
479
480        params
481    }
482}
483
484impl From<PolicyListParams> for Vec<(String, String)> {
485    fn from(query: PolicyListParams) -> Self {
486        let mut params = Vec::new();
487
488        if let Some(name) = query.name {
489            params.push(("name".to_string(), name)); // MOVE - no clone!
490        }
491        if let Some(policy_type) = query.policy_type {
492            params.push(("type".to_string(), policy_type)); // MOVE - no clone!
493        }
494        if let Some(is_active) = query.is_active {
495            params.push(("active".to_string(), is_active.to_string()));
496        }
497        if let Some(default_only) = query.default_only {
498            params.push(("default".to_string(), default_only.to_string()));
499        }
500        if let Some(page) = query.page {
501            params.push(("page".to_string(), page.to_string()));
502        }
503        if let Some(size) = query.size {
504            params.push(("size".to_string(), size.to_string()));
505        }
506
507        params
508    }
509}
510
511/// Response wrapper for policy list operations
512#[derive(Debug, Clone, Serialize, Deserialize)]
513pub struct PolicyListResponse {
514    #[serde(rename = "_embedded")]
515    pub embedded: Option<PolicyEmbedded>,
516    pub page: Option<PageInfo>,
517    #[serde(rename = "_links")]
518    pub links: Option<serde_json::Value>,
519}
520
521/// Embedded policies in the list response
522#[derive(Debug, Clone, Serialize, Deserialize)]
523pub struct PolicyEmbedded {
524    #[serde(rename = "policy_versions")]
525    pub policy_versions: Vec<SecurityPolicy>,
526}
527
528/// Page information for paginated responses
529#[derive(Debug, Clone, Serialize, Deserialize)]
530pub struct PageInfo {
531    pub size: u32,
532    pub number: u32,
533    pub total_elements: u32,
534    pub total_pages: u32,
535}
536
537/// Indicates which API was used to retrieve policy compliance status
538#[derive(Debug, Clone, PartialEq, Eq)]
539pub enum ApiSource {
540    /// Policy status retrieved from summary report API (preferred)
541    SummaryReport,
542    /// Policy status retrieved from getbuildinfo.do XML API (fallback)
543    BuildInfo,
544}
545
546/// Policy-specific error types
547#[derive(Debug)]
548pub enum PolicyError {
549    /// Veracode API error
550    Api(VeracodeError),
551    /// Policy not found (404)
552    NotFound,
553    /// Invalid policy configuration (400)
554    InvalidConfig(String),
555    /// Policy scan failed
556    ScanFailed(String),
557    /// Policy evaluation error
558    EvaluationError(String),
559    /// Insufficient permissions (403)
560    PermissionDenied,
561    /// Authentication required (401)
562    Unauthorized,
563    /// Internal server error (500)
564    InternalServerError,
565    /// Policy compliance check timeout
566    Timeout,
567}
568
569impl std::fmt::Display for PolicyError {
570    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
571        match self {
572            PolicyError::Api(err) => write!(f, "API error: {err}"),
573            PolicyError::NotFound => write!(f, "Policy not found"),
574            PolicyError::InvalidConfig(msg) => write!(f, "Invalid policy configuration: {msg}"),
575            PolicyError::ScanFailed(msg) => write!(f, "Policy scan failed: {msg}"),
576            PolicyError::EvaluationError(msg) => write!(f, "Policy evaluation error: {msg}"),
577            PolicyError::PermissionDenied => {
578                write!(f, "Insufficient permissions for policy operation")
579            }
580            PolicyError::Unauthorized => {
581                write!(f, "Authentication required - invalid API credentials")
582            }
583            PolicyError::InternalServerError => write!(f, "Internal server error occurred"),
584            PolicyError::Timeout => write!(f, "Policy operation timed out"),
585        }
586    }
587}
588
589impl std::error::Error for PolicyError {}
590
591impl From<VeracodeError> for PolicyError {
592    fn from(err: VeracodeError) -> Self {
593        PolicyError::Api(err)
594    }
595}
596
597impl From<reqwest::Error> for PolicyError {
598    fn from(err: reqwest::Error) -> Self {
599        PolicyError::Api(VeracodeError::Http(err))
600    }
601}
602
603impl From<serde_json::Error> for PolicyError {
604    fn from(err: serde_json::Error) -> Self {
605        PolicyError::Api(VeracodeError::Serialization(err))
606    }
607}
608
609/// Veracode Policy API operations
610pub struct PolicyApi<'a> {
611    client: &'a VeracodeClient,
612}
613
614impl<'a> PolicyApi<'a> {
615    /// Create a new PolicyApi instance
616    #[must_use]
617    pub fn new(client: &'a VeracodeClient) -> Self {
618        Self { client }
619    }
620
621    /// List all available security policies
622    ///
623    /// # Arguments
624    ///
625    /// * `params` - Optional query parameters for filtering
626    ///
627    /// # Returns
628    ///
629    /// A `Result` containing a list of policies or an error.
630    pub async fn list_policies(
631        &self,
632        params: Option<PolicyListParams>,
633    ) -> Result<Vec<SecurityPolicy>, PolicyError> {
634        let endpoint = "/appsec/v1/policies";
635
636        let query_params = params.as_ref().map(Vec::from);
637
638        let response = self.client.get(endpoint, query_params.as_deref()).await?;
639
640        let status = response.status().as_u16();
641        match status {
642            200 => {
643                let policy_response: PolicyListResponse = response.json().await?;
644                let policies = policy_response
645                    .embedded
646                    .map(|e| e.policy_versions)
647                    .unwrap_or_default();
648
649                Ok(policies)
650            }
651            400 => {
652                let error_text = response.text().await.unwrap_or_default();
653                Err(PolicyError::InvalidConfig(error_text))
654            }
655            401 => Err(PolicyError::Unauthorized),
656            403 => Err(PolicyError::PermissionDenied),
657            404 => Err(PolicyError::NotFound),
658            500 => Err(PolicyError::InternalServerError),
659            _ => {
660                let error_text = response.text().await.unwrap_or_default();
661                Err(PolicyError::Api(VeracodeError::InvalidResponse(format!(
662                    "HTTP {status}: {error_text}"
663                ))))
664            }
665        }
666    }
667
668    /// Get a specific policy by GUID
669    ///
670    /// # Arguments
671    ///
672    /// * `policy_guid` - The GUID of the policy
673    ///
674    /// # Returns
675    ///
676    /// A `Result` containing the policy or an error.
677    pub async fn get_policy(&self, policy_guid: &str) -> Result<SecurityPolicy, PolicyError> {
678        let endpoint = format!("/appsec/v1/policies/{policy_guid}");
679
680        let response = self.client.get(&endpoint, None).await?;
681
682        let status = response.status().as_u16();
683        match status {
684            200 => {
685                let policy: SecurityPolicy = response.json().await?;
686                Ok(policy)
687            }
688            400 => {
689                let error_text = response.text().await.unwrap_or_default();
690                Err(PolicyError::InvalidConfig(error_text))
691            }
692            401 => Err(PolicyError::Unauthorized),
693            403 => Err(PolicyError::PermissionDenied),
694            404 => Err(PolicyError::NotFound),
695            500 => Err(PolicyError::InternalServerError),
696            _ => {
697                let error_text = response.text().await.unwrap_or_default();
698                Err(PolicyError::Api(VeracodeError::InvalidResponse(format!(
699                    "HTTP {status}: {error_text}"
700                ))))
701            }
702        }
703    }
704
705    /// Get the default policy for the organization
706    ///
707    /// # Returns
708    ///
709    /// A `Result` containing the default policy or an error.
710    pub async fn get_default_policy(&self) -> Result<SecurityPolicy, PolicyError> {
711        let params = PolicyListParams {
712            default_only: Some(true),
713            ..Default::default()
714        };
715
716        let policies = self.list_policies(Some(params)).await?;
717        // Note: Default policy identification may need to be handled differently
718        // based on the actual API response structure
719        policies
720            .into_iter()
721            .find(|p| p.policy_type == "CUSTOMER" && p.organization_id.is_some())
722            .ok_or(PolicyError::NotFound)
723    }
724
725    /// Evaluates policy compliance for an application or sandbox using XML API
726    ///
727    /// This uses the /api/5.0/getbuildinfo.do endpoint which is the only working
728    /// policy compliance endpoint as the REST API compliance endpoints return 404.
729    ///
730    /// # Arguments
731    ///
732    /// * `app_id` - The numeric ID of the application
733    /// * `sandbox_id` - Optional numeric ID of the sandbox to evaluate
734    ///
735    /// # Returns
736    ///
737    /// A `Result` containing the policy compliance status string or an error.
738    pub async fn evaluate_policy_compliance_via_buildinfo(
739        &self,
740        app_id: &str,
741        sandbox_id: Option<&str>,
742    ) -> Result<std::borrow::Cow<'static, str>, PolicyError> {
743        self.evaluate_policy_compliance_via_buildinfo_with_retry(app_id, sandbox_id, 30, 10)
744            .await
745    }
746
747    /// Evaluates policy compliance with retry logic for when assessment is not yet complete
748    ///
749    /// This function will retry the policy evaluation check when the status is "Not Assessed"
750    /// until either the assessment completes or the maximum retry attempts are reached.
751    ///
752    /// # Arguments
753    ///
754    /// * `app_id` - The numeric ID of the application
755    /// * `sandbox_id` - Optional numeric ID of the sandbox to evaluate
756    /// * `max_retries` - Maximum number of retry attempts (default: 30)
757    /// * `retry_delay_seconds` - Delay between retries in seconds (default: 10)
758    ///
759    /// # Returns
760    ///
761    /// A `Result` containing the policy compliance status string or an error.
762    pub async fn evaluate_policy_compliance_via_buildinfo_with_retry(
763        &self,
764        app_id: &str,
765        sandbox_id: Option<&str>,
766        max_retries: u32,
767        retry_delay_seconds: u64,
768    ) -> Result<std::borrow::Cow<'static, str>, PolicyError> {
769        use crate::build::{BuildError, GetBuildInfoRequest};
770        use std::borrow::Cow;
771        use tokio::time::{Duration, sleep};
772
773        let build_request = GetBuildInfoRequest {
774            app_id: app_id.to_string(),
775            build_id: None, // Get latest build
776            sandbox_id: sandbox_id.map(str::to_string),
777        };
778
779        let mut attempts = 0;
780        loop {
781            let build_info = self
782                .client
783                .build_api()
784                .get_build_info(&build_request)
785                .await
786                .map_err(|e| match e {
787                    BuildError::BuildNotFound => PolicyError::Api(crate::VeracodeError::InvalidResponse(
788                        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.")
789                    )),
790                    BuildError::ApplicationNotFound => PolicyError::Api(crate::VeracodeError::InvalidResponse(
791                        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.")
792                    )),
793                    BuildError::SandboxNotFound => PolicyError::Api(crate::VeracodeError::InvalidResponse(
794                        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"))
795                    )),
796                    BuildError::Api(api_err) => PolicyError::Api(api_err),
797                    BuildError::InvalidParameter(msg)
798                    | BuildError::CreationFailed(msg)
799                    | BuildError::UpdateFailed(msg)
800                    | BuildError::DeletionFailed(msg)
801                    | BuildError::XmlParsingError(msg) => {
802                        PolicyError::Api(crate::VeracodeError::InvalidResponse(msg))
803                    }
804                    BuildError::Unauthorized | BuildError::PermissionDenied => PolicyError::Api(
805                        crate::VeracodeError::Authentication("Build API access denied".to_string()),
806                    ),
807                    BuildError::BuildInProgress => {
808                        PolicyError::Api(crate::VeracodeError::InvalidResponse(
809                            "Build is currently in progress".to_string(),
810                        ))
811                    }
812                })?;
813
814            // Get the policy compliance status
815            let status = build_info
816                .policy_compliance_status
817                .as_deref()
818                .unwrap_or("Not Assessed");
819
820            // If status is ready (not in-progress), return the result
821            if status != "Not Assessed" && status != "Calculating..." {
822                return Ok(Cow::Owned(status.to_string()));
823            }
824
825            // If we've reached max retries, return "Not Assessed"
826            attempts += 1;
827            if attempts >= max_retries {
828                warn!(
829                    "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"
830                );
831                return Ok(Cow::Borrowed("Not Assessed"));
832            }
833
834            // Log retry attempt
835            info!(
836                "Policy evaluation not yet assessed, retrying in {retry_delay_seconds} seconds... (attempt {attempts}/{max_retries})"
837            );
838
839            // Wait before retrying
840            sleep(Duration::from_secs(retry_delay_seconds)).await;
841        }
842    }
843
844    /// Determines if build should break based on policy compliance status
845    ///
846    /// # Arguments
847    ///
848    /// * `status` - The policy compliance status string from XML API
849    ///
850    /// # Returns
851    ///
852    /// `true` if build should break, `false` otherwise
853    #[must_use]
854    pub fn should_break_build(status: &str) -> bool {
855        status == "Did Not Pass"
856    }
857
858    /// Gets the appropriate exit code for CI/CD systems based on policy compliance
859    ///
860    /// # Arguments
861    ///
862    /// * `status` - The policy compliance status string from XML API
863    ///
864    /// # Returns
865    ///
866    /// Exit code: 0 for success, 4 for policy failure (build break)
867    #[must_use]
868    pub fn get_exit_code_for_status(status: &str) -> i32 {
869        if Self::should_break_build(status) {
870            4 // DID_NOT_PASSED_POLICY - matches Java wrapper
871        } else {
872            0 // SUCCESS
873        }
874    }
875
876    /// Get summary report for an application build using the REST API
877    ///
878    /// This uses the /appsec/v2/applications/{app_guid}/summary_report endpoint
879    /// to get policy compliance status and scan results.
880    ///
881    /// # Arguments
882    ///
883    /// * `app_guid` - The GUID of the application
884    /// * `build_id` - The build ID (GUID) to get summary for
885    /// * `sandbox_guid` - Optional sandbox GUID for sandbox scans
886    ///
887    /// # Returns
888    ///
889    /// A `Result` containing the summary report or an error.
890    pub async fn get_summary_report(
891        &self,
892        app_guid: &str,
893        build_id: Option<&str>,
894        sandbox_guid: Option<&str>,
895    ) -> Result<SummaryReport, PolicyError> {
896        let endpoint = format!("/appsec/v2/applications/{app_guid}/summary_report");
897
898        // Build query parameters
899        let mut query_params = Vec::new();
900        if let Some(build_id) = build_id {
901            query_params.push(("build_id".to_string(), build_id.to_string()));
902        }
903        if let Some(sandbox_guid) = sandbox_guid {
904            query_params.push(("context".to_string(), sandbox_guid.to_string()));
905        }
906
907        let response = self.client.get(&endpoint, Some(&query_params)).await?;
908
909        let status = response.status().as_u16();
910        match status {
911            200 => {
912                let summary_report: SummaryReport = response.json().await?;
913                Ok(summary_report)
914            }
915            400 => {
916                let error_text = response.text().await.unwrap_or_default();
917                Err(PolicyError::InvalidConfig(error_text))
918            }
919            401 => Err(PolicyError::Unauthorized),
920            403 => Err(PolicyError::PermissionDenied),
921            404 => Err(PolicyError::NotFound),
922            500 => Err(PolicyError::InternalServerError),
923            _ => {
924                let error_text = response.text().await.unwrap_or_default();
925                Err(PolicyError::Api(VeracodeError::InvalidResponse(format!(
926                    "HTTP {status}: {error_text}"
927                ))))
928            }
929        }
930    }
931
932    /// Gets summary report with retry logic and returns both the full report and compliance status
933    ///
934    /// This function combines the functionality of both `get_summary_report` and
935    /// `evaluate_policy_compliance_via_summary_report_with_retry` to avoid redundant API calls.
936    /// It will retry until the policy compliance status is ready (not "Not Assessed").
937    ///
938    /// # Arguments
939    ///
940    /// * `app_guid` - The GUID of the application
941    /// * `build_id` - The build ID to check compliance for
942    /// * `sandbox_guid` - Optional sandbox GUID for sandbox scans
943    /// * `max_retries` - Maximum number of retry attempts
944    /// * `retry_delay_seconds` - Delay between retries in seconds
945    /// * `debug` - Enable debug logging
946    ///
947    /// # Returns
948    ///
949    /// A `Result` containing a tuple of (SummaryReport, Option<compliance_status>) or an error.
950    /// The compliance_status is Some(status) if break_build evaluation is needed, None otherwise.
951    #[allow(clippy::too_many_arguments)]
952    pub async fn get_summary_report_with_policy_retry(
953        &self,
954        app_guid: &str,
955        build_id: Option<&str>,
956        sandbox_guid: Option<&str>,
957        max_retries: u32,
958        retry_delay_seconds: u64,
959        enable_break_build: bool,
960    ) -> Result<(SummaryReport, Option<std::borrow::Cow<'static, str>>), PolicyError> {
961        use std::borrow::Cow;
962        use tokio::time::{Duration, sleep};
963
964        if enable_break_build && build_id.is_none() {
965            return Err(PolicyError::InvalidConfig(
966                "Build ID is required for break build policy evaluation".to_string(),
967            ));
968        }
969
970        let mut attempts = 0;
971        loop {
972            if attempts == 0 && enable_break_build {
973                debug!("Checking policy compliance status with retry logic...");
974            } else if attempts == 0 {
975                debug!("Getting summary report...");
976            }
977
978            let summary_report = match self
979                .get_summary_report(app_guid, build_id, sandbox_guid)
980                .await
981            {
982                Ok(report) => report,
983                Err(PolicyError::InternalServerError) if attempts < 3 => {
984                    warn!(
985                        "Summary report API failed with server error (attempt {}/3), retrying in 5 seconds...",
986                        attempts + 1
987                    );
988                    sleep(Duration::from_secs(5)).await;
989                    attempts += 1;
990                    continue;
991                }
992                Err(e) => return Err(e),
993            };
994
995            // If break_build is not enabled, return immediately with the report
996            if !enable_break_build {
997                return Ok((summary_report, None));
998            }
999
1000            // For break_build evaluation, check if policy compliance status is ready
1001            let status = summary_report.policy_compliance_status.clone();
1002
1003            // If status is ready (not empty and not "Not Assessed"), return both report and status
1004            if !status.is_empty() && status != "Not Assessed" {
1005                debug!("Policy compliance status ready: {status}");
1006                return Ok((summary_report, Some(Cow::Owned(status))));
1007            }
1008
1009            // If we've reached max retries, return current results
1010            attempts += 1;
1011            if attempts >= max_retries {
1012                warn!(
1013                    "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"
1014                );
1015                return Ok((summary_report, Some(Cow::Owned(status))));
1016            }
1017
1018            // Log retry attempt
1019            info!(
1020                "Policy evaluation not yet ready (status: '{status}'), retrying in {retry_delay_seconds} seconds... (attempt {attempts}/{max_retries})"
1021            );
1022
1023            // Wait before retrying
1024            sleep(Duration::from_secs(retry_delay_seconds)).await;
1025        }
1026    }
1027
1028    /// Evaluates policy compliance using the summary report API with retry logic
1029    ///
1030    /// This function uses the summary_report endpoint instead of the buildinfo XML API
1031    /// and will retry when results are not ready yet.
1032    ///
1033    /// # Arguments
1034    ///
1035    /// * `app_guid` - The GUID of the application  
1036    /// * `build_id` - The build ID (GUID) to check compliance for
1037    /// * `sandbox_guid` - Optional sandbox GUID for sandbox scans
1038    /// * `max_retries` - Maximum number of retry attempts (default: 30)
1039    /// * `retry_delay_seconds` - Delay between retries in seconds (default: 10)
1040    ///
1041    /// # Returns
1042    ///
1043    /// A `Result` containing the policy compliance status string or an error.
1044    pub async fn evaluate_policy_compliance_via_summary_report_with_retry(
1045        &self,
1046        app_guid: &str,
1047        build_id: &str,
1048        sandbox_guid: Option<&str>,
1049        max_retries: u32,
1050        retry_delay_seconds: u64,
1051    ) -> Result<std::borrow::Cow<'static, str>, PolicyError> {
1052        use std::borrow::Cow;
1053        use tokio::time::{Duration, sleep};
1054
1055        let mut attempts = 0;
1056        loop {
1057            let summary_report = self
1058                .get_summary_report(app_guid, Some(build_id), sandbox_guid)
1059                .await?;
1060
1061            // Check if results are ready - look for "Results Ready" or completed status
1062            // The summary report should have policy_compliance_status populated when ready
1063            let status = &summary_report.policy_compliance_status;
1064
1065            // If status is not empty and not "Not Assessed", return the result
1066            if !status.is_empty() && status != "Not Assessed" {
1067                return Ok(Cow::Owned(status.clone()));
1068            }
1069
1070            // If we've reached max retries, return current status
1071            attempts += 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(Cow::Owned(status.clone()));
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 (single attempt)
1090    ///
1091    /// This is a convenience method that calls the retry version with default parameters.
1092    ///
1093    /// # Arguments
1094    ///
1095    /// * `app_guid` - The GUID of the application  
1096    /// * `build_id` - The build ID (GUID) to check compliance for
1097    /// * `sandbox_guid` - Optional sandbox GUID for sandbox scans
1098    ///
1099    /// # Returns
1100    ///
1101    /// A `Result` containing the policy compliance status string or an error.
1102    pub async fn evaluate_policy_compliance_via_summary_report(
1103        &self,
1104        app_guid: &str,
1105        build_id: &str,
1106        sandbox_guid: Option<&str>,
1107    ) -> Result<std::borrow::Cow<'static, str>, PolicyError> {
1108        self.evaluate_policy_compliance_via_summary_report_with_retry(
1109            app_guid,
1110            build_id,
1111            sandbox_guid,
1112            30,
1113            10,
1114        )
1115        .await
1116    }
1117
1118    /// Initiate a policy scan for an application
1119    ///
1120    /// # Arguments
1121    ///
1122    /// * `request` - The policy scan request
1123    ///
1124    /// # Returns
1125    ///
1126    /// A `Result` containing the scan result or an error.
1127    pub async fn initiate_policy_scan(
1128        &self,
1129        request: PolicyScanRequest,
1130    ) -> Result<PolicyScanResult, PolicyError> {
1131        let endpoint = "/appsec/v1/policy-scans";
1132
1133        let response = self.client.post(endpoint, Some(&request)).await?;
1134
1135        let status = response.status().as_u16();
1136        match status {
1137            200 | 201 => {
1138                let scan_result: PolicyScanResult = response.json().await?;
1139                Ok(scan_result)
1140            }
1141            400 => {
1142                let error_text = response.text().await.unwrap_or_default();
1143                Err(PolicyError::InvalidConfig(error_text))
1144            }
1145            404 => Err(PolicyError::NotFound),
1146            _ => {
1147                let error_text = response.text().await.unwrap_or_default();
1148                Err(PolicyError::Api(VeracodeError::InvalidResponse(format!(
1149                    "HTTP {status}: {error_text}"
1150                ))))
1151            }
1152        }
1153    }
1154
1155    /// Get policy scan status and results
1156    ///
1157    /// # Arguments
1158    ///
1159    /// * `scan_id` - The ID of the policy scan
1160    ///
1161    /// # Returns
1162    ///
1163    /// A `Result` containing the scan result or an error.
1164    pub async fn get_policy_scan_result(
1165        &self,
1166        scan_id: u64,
1167    ) -> Result<PolicyScanResult, PolicyError> {
1168        let endpoint = format!("/appsec/v1/policy-scans/{scan_id}");
1169
1170        let response = self.client.get(&endpoint, None).await?;
1171
1172        let status = response.status().as_u16();
1173        match status {
1174            200 => {
1175                let scan_result: PolicyScanResult = response.json().await?;
1176                Ok(scan_result)
1177            }
1178            404 => Err(PolicyError::NotFound),
1179            _ => {
1180                let error_text = response.text().await.unwrap_or_default();
1181                Err(PolicyError::Api(VeracodeError::InvalidResponse(format!(
1182                    "HTTP {status}: {error_text}"
1183                ))))
1184            }
1185        }
1186    }
1187
1188    /// Check if a policy scan is complete
1189    ///
1190    /// # Arguments
1191    ///
1192    /// * `scan_id` - The ID of the policy scan
1193    ///
1194    /// # Returns
1195    ///
1196    /// A `Result` containing a boolean indicating completion status.
1197    pub async fn is_policy_scan_complete(&self, scan_id: u64) -> Result<bool, PolicyError> {
1198        let scan_result = self.get_policy_scan_result(scan_id).await?;
1199        Ok(matches!(
1200            scan_result.status,
1201            ScanStatus::Completed | ScanStatus::Failed | ScanStatus::Cancelled
1202        ))
1203    }
1204
1205    /// Gets policy compliance status with automatic fallback from summary report to buildinfo
1206    ///
1207    /// This method first tries the summary report API for full functionality. If access is denied
1208    /// (401/403), it automatically falls back to the getbuildinfo.do XML API for policy compliance
1209    /// status only. This provides the best user experience while maintaining compatibility.
1210    ///
1211    /// # Arguments
1212    ///
1213    /// * `app_guid` - Application GUID (for REST API)
1214    /// * `app_id` - Application numeric ID (for XML API fallback)
1215    /// * `build_id` - Optional build ID
1216    /// * `sandbox_guid` - Optional sandbox GUID (for REST API)
1217    /// * `sandbox_id` - Optional sandbox numeric ID (for XML API fallback)
1218    /// * `max_retries` - Maximum number of retry attempts
1219    /// * `retry_delay_seconds` - Delay between retries in seconds
1220    /// * `enable_break_build` - Whether to enable break build evaluation
1221    /// * `force_buildinfo_api` - Skip summary report and use buildinfo directly
1222    ///
1223    /// # Returns
1224    ///
1225    /// A tuple containing:
1226    /// - Optional SummaryReport (None if fallback was used)
1227    /// - Policy compliance status string
1228    /// - ApiSource indicating which API was used
1229    #[allow(clippy::too_many_arguments)]
1230    pub async fn get_policy_status_with_fallback(
1231        &self,
1232        app_guid: &str,
1233        app_id: &str,
1234        build_id: Option<&str>,
1235        sandbox_guid: Option<&str>,
1236        sandbox_id: Option<&str>,
1237        max_retries: u32,
1238        retry_delay_seconds: u64,
1239        enable_break_build: bool,
1240        force_buildinfo_api: bool,
1241    ) -> Result<(Option<SummaryReport>, String, ApiSource), PolicyError> {
1242        if force_buildinfo_api {
1243            // DIRECT PATH: Skip summary report, use getbuildinfo.do directly
1244            debug!("Using getbuildinfo.do API directly (forced via configuration)");
1245            let status = self
1246                .evaluate_policy_compliance_via_buildinfo_with_retry(
1247                    app_id,
1248                    sandbox_id,
1249                    max_retries,
1250                    retry_delay_seconds,
1251                )
1252                .await?;
1253            return Ok((None, status.to_string(), ApiSource::BuildInfo));
1254        }
1255
1256        // FALLBACK PATH: Try summary report first, fallback to getbuildinfo.do
1257        match self
1258            .get_summary_report_with_policy_retry(
1259                app_guid,
1260                build_id,
1261                sandbox_guid,
1262                max_retries,
1263                retry_delay_seconds,
1264                enable_break_build,
1265            )
1266            .await
1267        {
1268            Ok((summary_report, compliance_status)) => {
1269                debug!("Used summary report API successfully");
1270                let status = compliance_status
1271                    .map(|s| s.to_string())
1272                    .unwrap_or_else(|| summary_report.policy_compliance_status.clone());
1273                Ok((Some(summary_report), status, ApiSource::SummaryReport))
1274            }
1275            Err(
1276                ref e @ (PolicyError::Unauthorized
1277                | PolicyError::PermissionDenied
1278                | PolicyError::InternalServerError),
1279            ) => {
1280                match e {
1281                    PolicyError::InternalServerError => info!(
1282                        "Summary report API server error, falling back to getbuildinfo.do API"
1283                    ),
1284                    _ => info!("Summary report access denied, falling back to getbuildinfo.do API"),
1285                }
1286                let status = self
1287                    .evaluate_policy_compliance_via_buildinfo_with_retry(
1288                        app_id,
1289                        sandbox_id,
1290                        max_retries,
1291                        retry_delay_seconds,
1292                    )
1293                    .await?;
1294                Ok((None, status.to_string(), ApiSource::BuildInfo))
1295            }
1296            Err(e) => Err(e),
1297        }
1298    }
1299
1300    /// Get active policies for the organization
1301    ///
1302    /// # Returns
1303    ///
1304    /// A `Result` containing a list of active policies or an error.
1305    pub async fn get_active_policies(&self) -> Result<Vec<SecurityPolicy>, PolicyError> {
1306        // Note: The active/inactive concept may need to be handled differently
1307        // based on the actual API response structure
1308        let policies = self.list_policies(None).await?;
1309        Ok(policies) // Return all policies for now
1310    }
1311}
1312
1313#[cfg(test)]
1314mod tests {
1315    use super::*;
1316
1317    #[test]
1318    fn test_policy_list_params_to_query() {
1319        let params = PolicyListParams {
1320            name: Some("test-policy".to_string()),
1321            is_active: Some(true),
1322            page: Some(1),
1323            size: Some(10),
1324            ..Default::default()
1325        };
1326
1327        let query_params: Vec<_> = params.into();
1328        assert_eq!(query_params.len(), 4);
1329        assert!(query_params.contains(&("name".to_string(), "test-policy".to_string())));
1330        assert!(query_params.contains(&("active".to_string(), "true".to_string())));
1331        assert!(query_params.contains(&("page".to_string(), "1".to_string())));
1332        assert!(query_params.contains(&("size".to_string(), "10".to_string())));
1333    }
1334
1335    #[test]
1336    fn test_policy_error_display() {
1337        let error = PolicyError::NotFound;
1338        assert_eq!(error.to_string(), "Policy not found");
1339
1340        let error = PolicyError::InvalidConfig("test".to_string());
1341        assert_eq!(error.to_string(), "Invalid policy configuration: test");
1342
1343        let error = PolicyError::Timeout;
1344        assert_eq!(error.to_string(), "Policy operation timed out");
1345    }
1346
1347    #[test]
1348    fn test_scan_type_serialization() {
1349        let scan_type = ScanType::Static;
1350        let json = serde_json::to_string(&scan_type).unwrap();
1351        assert_eq!(json, "\"static\"");
1352
1353        let deserialized: ScanType = serde_json::from_str(&json).unwrap();
1354        assert!(matches!(deserialized, ScanType::Static));
1355    }
1356
1357    #[test]
1358    fn test_policy_compliance_status_serialization() {
1359        let status = PolicyComplianceStatus::Passed;
1360        let json = serde_json::to_string(&status).unwrap();
1361        assert_eq!(json, "\"Passed\"");
1362
1363        let deserialized: PolicyComplianceStatus = serde_json::from_str(&json).unwrap();
1364        assert!(matches!(deserialized, PolicyComplianceStatus::Passed));
1365
1366        // Test the special case statuses with spaces
1367        let conditional_pass = PolicyComplianceStatus::ConditionalPass;
1368        let json = serde_json::to_string(&conditional_pass).unwrap();
1369        assert_eq!(json, "\"Conditional Pass\"");
1370
1371        let did_not_pass = PolicyComplianceStatus::DidNotPass;
1372        let json = serde_json::to_string(&did_not_pass).unwrap();
1373        assert_eq!(json, "\"Did Not Pass\"");
1374    }
1375
1376    #[test]
1377    fn test_break_build_logic() {
1378        assert!(PolicyApi::should_break_build("Did Not Pass"));
1379        assert!(!PolicyApi::should_break_build("Passed"));
1380        assert!(!PolicyApi::should_break_build("Conditional Pass"));
1381        // "Not Assessed" should not break build as the retry logic should handle waiting
1382        // for policy evaluation to complete before reaching this point
1383        assert!(!PolicyApi::should_break_build("Not Assessed"));
1384
1385        assert_eq!(PolicyApi::get_exit_code_for_status("Did Not Pass"), 4);
1386        assert_eq!(PolicyApi::get_exit_code_for_status("Passed"), 0);
1387        assert_eq!(PolicyApi::get_exit_code_for_status("Conditional Pass"), 0);
1388        // "Not Assessed" returns 0 because it should only reach here after retry logic
1389        // has exhausted attempts, indicating a configuration or timing issue rather than policy failure
1390        assert_eq!(PolicyApi::get_exit_code_for_status("Not Assessed"), 0);
1391    }
1392
1393    #[test]
1394    fn test_summary_report_serialization() {
1395        let summary_json = r#"{
1396            "app_id": 2676517,
1397            "app_name": "Verascan Java Test",
1398            "build_id": 54209787,
1399            "policy_compliance_status": "Did Not Pass",
1400            "policy_name": "SecureCode Policy",
1401            "policy_version": 1,
1402            "policy_rules_status": "Did Not Pass",
1403            "grace_period_expired": false,
1404            "scan_overdue": "false",
1405            "is_latest_build": false,
1406            "generation_date": "2025-08-05 10:14:45 UTC",
1407            "last_update_time": "2025-08-05 10:00:51 UTC"
1408        }"#;
1409
1410        let summary: Result<SummaryReport, _> = serde_json::from_str(summary_json);
1411        assert!(summary.is_ok());
1412
1413        let summary = summary.unwrap();
1414        assert_eq!(summary.policy_compliance_status, "Did Not Pass");
1415        assert_eq!(summary.app_name, "Verascan Java Test");
1416        assert_eq!(summary.build_id, 54209787);
1417        assert!(PolicyApi::should_break_build(
1418            &summary.policy_compliance_status
1419        ));
1420    }
1421
1422    #[test]
1423    fn test_export_json_structure() {
1424        // Test the JSON structure that would be exported
1425        let summary_report = SummaryReport {
1426            app_id: 2676517,
1427            app_name: "Test App".to_string(),
1428            build_id: 54209787,
1429            policy_compliance_status: "Passed".to_string(),
1430            policy_name: "Test Policy".to_string(),
1431            policy_version: 1,
1432            policy_rules_status: "Passed".to_string(),
1433            grace_period_expired: false,
1434            scan_overdue: "false".to_string(),
1435            is_latest_build: true,
1436            sandbox_name: Some("test-sandbox".to_string()),
1437            sandbox_id: Some(123456),
1438            generation_date: "2025-08-05 10:14:45 UTC".to_string(),
1439            last_update_time: "2025-08-05 10:00:51 UTC".to_string(),
1440            static_analysis: None,
1441            flaw_status: None,
1442            software_composition_analysis: None,
1443            severity: None,
1444        };
1445
1446        let export_json = serde_json::json!({
1447            "summary_report": summary_report,
1448            "export_metadata": {
1449                "exported_at": "2025-08-05T10:14:45Z",
1450                "tool": "verascan",
1451                "export_type": "summary_report",
1452                "scan_configuration": {
1453                    "autoscan": true,
1454                    "scan_all_nonfatal_top_level_modules": true,
1455                    "include_new_modules": true
1456                }
1457            }
1458        });
1459
1460        // Verify JSON structure
1461        assert!(export_json["summary_report"]["app_name"].is_string());
1462        assert!(export_json["summary_report"]["policy_compliance_status"].is_string());
1463        assert!(export_json["export_metadata"]["export_type"].is_string());
1464        assert_eq!(
1465            export_json["export_metadata"]["export_type"],
1466            "summary_report"
1467        );
1468
1469        // Verify the summary report can be serialized and deserialized
1470        let json_string = serde_json::to_string_pretty(&export_json).unwrap();
1471        assert!(json_string.contains("summary_report"));
1472        assert!(json_string.contains("export_metadata"));
1473    }
1474
1475    #[test]
1476    fn test_get_summary_report_with_policy_retry_parameters() {
1477        // Unit tests for the new combined method parameter validation and logic
1478
1479        // Test parameter type validation
1480        let app_guid = "test-app-guid";
1481        let build_id = Some("test-build-id");
1482        let sandbox_guid: Option<&str> = None;
1483        let max_retries = 30u32;
1484        let retry_delay_seconds = 10u64;
1485        let debug = false;
1486        let enable_break_build = true;
1487
1488        // Verify parameter types are correct
1489        assert_eq!(app_guid, "test-app-guid");
1490        assert_eq!(build_id, Some("test-build-id"));
1491        assert_eq!(sandbox_guid, None);
1492        assert_eq!(max_retries, 30);
1493        assert_eq!(retry_delay_seconds, 10);
1494        assert!(!debug);
1495        assert!(enable_break_build);
1496    }
1497
1498    #[test]
1499    fn test_policy_status_ready_logic() {
1500        // Test the logic for determining when policy status is ready
1501        let ready_statuses = vec!["Passed", "Did Not Pass", "Conditional Pass"];
1502        let not_ready_statuses = vec!["", "Not Assessed"];
1503
1504        // Test ready statuses (should not trigger retry)
1505        for status in &ready_statuses {
1506            assert!(
1507                !status.is_empty(),
1508                "Ready status should not be empty: {status}"
1509            );
1510            assert_ne!(
1511                *status, "Not Assessed",
1512                "Ready status should not be 'Not Assessed': {status}"
1513            );
1514        }
1515
1516        // Test not ready statuses (should trigger retry)
1517        for status in &not_ready_statuses {
1518            let is_not_ready = status.is_empty() || *status == "Not Assessed";
1519            assert!(is_not_ready, "Status should trigger retry: '{status}'");
1520        }
1521    }
1522
1523    #[test]
1524    fn test_combined_method_return_types() {
1525        use std::borrow::Cow;
1526
1527        // Test the return type structure of the new combined method
1528        // This verifies the tuple structure is correct
1529
1530        // Test Some compliance status
1531        let compliance_status = Cow::Borrowed("Passed");
1532        assert_eq!(compliance_status.as_ref(), "Passed");
1533
1534        // Test None compliance status (when break_build is disabled)
1535        let compliance_status: Option<Cow<'static, str>> = None;
1536        assert!(compliance_status.is_none());
1537    }
1538
1539    #[test]
1540    fn test_debug_logging_parameters() {
1541        // Test debug parameter handling
1542        let debug_enabled = true;
1543        let debug_disabled = false;
1544
1545        assert!(debug_enabled);
1546        assert!(!debug_disabled);
1547
1548        // Test debug messages would be printed when debug=true
1549        // (Actual output testing would require integration tests)
1550        if debug_enabled {
1551            // Debug messages would be printed - this is just a placeholder
1552        }
1553
1554        if !debug_disabled {
1555            // Debug messages would be printed - this is just a placeholder
1556        }
1557    }
1558
1559    #[test]
1560    fn test_break_build_flag_logic() {
1561        // Test the enable_break_build flag logic
1562        let break_build_enabled = true;
1563        let break_build_disabled = false;
1564
1565        // When break_build is enabled, compliance_status should be Some(_)
1566        if break_build_enabled {
1567            // Would return (summary_report, Some(compliance_status))
1568            let compliance_returned = true;
1569            assert!(compliance_returned);
1570        }
1571
1572        // When break_build is disabled, compliance_status should be None
1573        if !break_build_disabled {
1574            // Would return (summary_report, None)
1575            let compliance_returned = false;
1576            assert!(!compliance_returned);
1577        }
1578    }
1579}