1use chrono::{DateTime, Utc};
7use log::{debug, info, warn};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11use crate::{VeracodeClient, VeracodeError};
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct SecurityPolicy {
16 pub guid: String,
18 pub name: String,
20 pub description: Option<String>,
22 #[serde(rename = "type")]
24 pub policy_type: String,
25 pub version: u32,
27 pub created: Option<DateTime<Utc>>,
29 pub modified_by: Option<String>,
31 pub organization_id: Option<u64>,
33 pub category: String,
35 pub vendor_policy: bool,
37 pub scan_frequency_rules: Vec<ScanFrequencyRule>,
39 pub finding_rules: Vec<FindingRule>,
41 pub custom_severities: Vec<serde_json::Value>,
43 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 pub score_grace_period: u32,
52 pub sca_blacklist_grace_period: u32,
54 pub sca_grace_periods: Option<serde_json::Value>,
56 pub evaluation_date: Option<DateTime<Utc>>,
58 pub evaluation_date_type: Option<String>,
60 pub capabilities: Vec<String>,
62 #[serde(rename = "_links")]
64 pub links: Option<serde_json::Value>,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
69#[serde(rename_all = "PascalCase")]
70pub enum PolicyComplianceStatus {
71 Passed,
73 #[serde(rename = "Conditional Pass")]
75 ConditionalPass,
76 #[serde(rename = "Did Not Pass")]
78 DidNotPass,
79 #[serde(rename = "Not Assessed")]
81 NotAssessed,
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct PolicyRule {
87 pub id: String,
89 pub name: String,
91 pub description: Option<String>,
93 pub rule_type: String,
95 pub criteria: Option<serde_json::Value>,
97 pub enabled: bool,
99 pub severity: Option<String>,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct PolicyThresholds {
106 pub very_high: Option<u32>,
108 pub high: Option<u32>,
110 pub medium: Option<u32>,
112 pub low: Option<u32>,
114 pub very_low: Option<u32>,
116 pub score_threshold: Option<f64>,
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct PolicyScanRequest {
123 pub application_guid: String,
125 pub policy_guid: String,
127 pub scan_type: ScanType,
129 pub sandbox_guid: Option<String>,
131 pub config: Option<PolicyScanConfig>,
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
137#[serde(rename_all = "lowercase")]
138pub enum ScanType {
139 Static,
141 Dynamic,
143 Sca,
145 Manual,
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct PolicyScanConfig {
152 pub auto_submit: Option<bool>,
154 pub timeout_minutes: Option<u32>,
156 pub include_third_party: Option<bool>,
158 pub modules: Option<Vec<String>>,
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct PolicyScanResult {
165 pub scan_id: u64,
167 pub application_guid: String,
169 pub policy_guid: String,
171 pub status: ScanStatus,
173 pub scan_type: ScanType,
175 pub started: DateTime<Utc>,
177 pub completed: Option<DateTime<Utc>>,
179 pub compliance_result: Option<PolicyComplianceResult>,
181 pub findings_summary: Option<FindingsSummary>,
183 pub results_url: Option<String>,
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize)]
189#[serde(rename_all = "UPPERCASE")]
190pub enum ScanStatus {
191 Queued,
193 Running,
195 Completed,
197 Failed,
199 Cancelled,
201 Timeout,
203}
204
205#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct PolicyComplianceResult {
208 pub status: PolicyComplianceStatus,
210 pub score: Option<f64>,
212 pub passed: bool,
214 pub breakdown: Option<ComplianceBreakdown>,
216 pub violations: Option<Vec<PolicyViolation>>,
218 pub summary: Option<String>,
220}
221
222#[derive(Debug, Clone, Serialize, Deserialize)]
224pub struct ComplianceBreakdown {
225 pub very_high: u32,
227 pub high: u32,
229 pub medium: u32,
231 pub low: u32,
233 pub very_low: u32,
235 pub total: u32,
237}
238
239#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct PolicyViolation {
242 pub violation_type: String,
244 pub severity: String,
246 pub description: String,
248 pub count: u32,
250 pub threshold_exceeded: Option<u32>,
252 pub actual_value: Option<u32>,
254}
255
256#[derive(Debug, Clone, Serialize, Deserialize)]
258pub struct FindingsSummary {
259 pub total: u32,
261 pub open: u32,
263 pub fixed: u32,
265 pub by_severity: HashMap<String, u32>,
267 pub by_category: Option<HashMap<String, u32>>,
269}
270
271#[derive(Debug, Clone, Serialize, Deserialize)]
273pub struct SummaryReport {
274 pub app_id: u64,
276 pub app_name: String,
278 pub build_id: u64,
280 pub policy_compliance_status: String,
282 pub policy_name: String,
284 pub policy_version: u32,
286 pub policy_rules_status: String,
288 pub grace_period_expired: bool,
290 pub scan_overdue: String,
292 pub is_latest_build: bool,
294 pub sandbox_name: Option<String>,
296 pub sandbox_id: Option<u64>,
298 pub generation_date: String,
300 pub last_update_time: String,
302 #[serde(rename = "static-analysis")]
304 pub static_analysis: Option<StaticAnalysisSummary>,
305 #[serde(rename = "flaw-status")]
307 pub flaw_status: Option<FlawStatusSummary>,
308 pub software_composition_analysis: Option<ScaSummary>,
310 pub severity: Option<Vec<SeverityLevel>>,
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize)]
316pub struct StaticAnalysisSummary {
317 pub rating: Option<String>,
319 pub score: Option<u32>,
321 pub mitigated_rating: Option<String>,
323 pub mitigated_score: Option<u32>,
325 pub analysis_size_bytes: Option<u64>,
327 pub engine_version: Option<String>,
329 pub published_date: Option<String>,
331 pub version: Option<String>,
333}
334
335#[derive(Debug, Clone, Serialize, Deserialize)]
337pub struct FlawStatusSummary {
338 pub new: u32,
340 pub reopen: u32,
342 pub open: u32,
344 pub fixed: u32,
346 pub total: u32,
348 pub not_mitigated: u32,
350}
351
352#[derive(Debug, Clone, Serialize, Deserialize)]
354pub struct ScaSummary {
355 pub third_party_components: u32,
357 pub violate_policy: bool,
359 pub components_violated_policy: u32,
361}
362
363#[derive(Debug, Clone, Serialize, Deserialize)]
365pub struct SeverityLevel {
366 pub level: u32,
368 pub category: Vec<CategorySummary>,
370}
371
372#[derive(Debug, Clone, Serialize, Deserialize)]
374pub struct CategorySummary {
375 pub categoryname: String,
377 pub severity: String,
379 pub count: u32,
381}
382
383#[derive(Debug, Clone, Serialize, Deserialize)]
385pub struct ScanFrequencyRule {
386 pub scan_type: String,
388 pub frequency: String,
390}
391
392#[derive(Debug, Clone, Serialize, Deserialize)]
394pub struct FindingRule {
395 #[serde(rename = "type")]
397 pub rule_type: String,
398 pub scan_type: Vec<String>,
400 pub value: String,
402 pub advanced_options: Option<serde_json::Value>,
404}
405
406#[derive(Debug, Clone, Serialize, Deserialize)]
408pub struct FindingRuleAdvancedOptions {
409 pub override_severity: Option<bool>,
411 pub build_action: Option<String>,
413 pub component_dependency: Option<String>,
415 pub vulnerable_methods: Option<String>,
417 pub selected_licenses: Option<Vec<String>>,
419 pub override_severity_level: Option<String>,
421 pub allowed_nonoss_licenses: Option<bool>,
423 pub allowed_unrecognized_licenses: Option<bool>,
425 pub all_licenses_must_meet_requirement: Option<bool>,
427 pub is_blocklist: Option<bool>,
429}
430
431#[derive(Debug, Clone, Default)]
433pub struct PolicyListParams {
434 pub name: Option<String>,
436 pub policy_type: Option<String>,
438 pub is_active: Option<bool>,
440 pub default_only: Option<bool>,
442 pub page: Option<u32>,
444 pub size: Option<u32>,
446}
447
448impl PolicyListParams {
449 #[must_use]
451 pub fn to_query_params(&self) -> Vec<(String, String)> {
452 Vec::from(self) }
454}
455
456impl 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())); }
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)); }
491 if let Some(policy_type) = query.policy_type {
492 params.push(("type".to_string(), policy_type)); }
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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
523pub struct PolicyEmbedded {
524 #[serde(rename = "policy_versions")]
525 pub policy_versions: Vec<SecurityPolicy>,
526}
527
528#[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#[derive(Debug, Clone, PartialEq, Eq)]
539pub enum ApiSource {
540 SummaryReport,
542 BuildInfo,
544}
545
546#[derive(Debug)]
548pub enum PolicyError {
549 Api(VeracodeError),
551 NotFound,
553 InvalidConfig(String),
555 ScanFailed(String),
557 EvaluationError(String),
559 PermissionDenied,
561 Unauthorized,
563 InternalServerError,
565 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
609pub struct PolicyApi<'a> {
611 client: &'a VeracodeClient,
612}
613
614impl<'a> PolicyApi<'a> {
615 #[must_use]
617 pub fn new(client: &'a VeracodeClient) -> Self {
618 Self { client }
619 }
620
621 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 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 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 policies
720 .into_iter()
721 .find(|p| p.policy_type == "CUSTOMER" && p.organization_id.is_some())
722 .ok_or(PolicyError::NotFound)
723 }
724
725 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 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, 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 let status = build_info
816 .policy_compliance_status
817 .as_deref()
818 .unwrap_or("Not Assessed");
819
820 if status != "Not Assessed" && status != "Calculating..." {
822 return Ok(Cow::Owned(status.to_string()));
823 }
824
825 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 info!(
836 "Policy evaluation not yet assessed, retrying in {retry_delay_seconds} seconds... (attempt {attempts}/{max_retries})"
837 );
838
839 sleep(Duration::from_secs(retry_delay_seconds)).await;
841 }
842 }
843
844 #[must_use]
854 pub fn should_break_build(status: &str) -> bool {
855 status == "Did Not Pass"
856 }
857
858 #[must_use]
868 pub fn get_exit_code_for_status(status: &str) -> i32 {
869 if Self::should_break_build(status) {
870 4 } else {
872 0 }
874 }
875
876 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 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 #[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 !enable_break_build {
997 return Ok((summary_report, None));
998 }
999
1000 let status = summary_report.policy_compliance_status.clone();
1002
1003 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 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 info!(
1020 "Policy evaluation not yet ready (status: '{status}'), retrying in {retry_delay_seconds} seconds... (attempt {attempts}/{max_retries})"
1021 );
1022
1023 sleep(Duration::from_secs(retry_delay_seconds)).await;
1025 }
1026 }
1027
1028 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 let status = &summary_report.policy_compliance_status;
1064
1065 if !status.is_empty() && status != "Not Assessed" {
1067 return Ok(Cow::Owned(status.clone()));
1068 }
1069
1070 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 info!(
1081 "Policy evaluation not yet ready (status: '{status}'), retrying in {retry_delay_seconds} seconds... (attempt {attempts}/{max_retries})"
1082 );
1083
1084 sleep(Duration::from_secs(retry_delay_seconds)).await;
1086 }
1087 }
1088
1089 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 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 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 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 #[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 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 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 pub async fn get_active_policies(&self) -> Result<Vec<SecurityPolicy>, PolicyError> {
1306 let policies = self.list_policies(None).await?;
1309 Ok(policies) }
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 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 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 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 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 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 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 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 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 let ready_statuses = vec!["Passed", "Did Not Pass", "Conditional Pass"];
1502 let not_ready_statuses = vec!["", "Not Assessed"];
1503
1504 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 for status in ¬_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 let compliance_status = Cow::Borrowed("Passed");
1532 assert_eq!(compliance_status.as_ref(), "Passed");
1533
1534 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 let debug_enabled = true;
1543 let debug_disabled = false;
1544
1545 assert!(debug_enabled);
1546 assert!(!debug_disabled);
1547
1548 if debug_enabled {
1551 }
1553
1554 if !debug_disabled {
1555 }
1557 }
1558
1559 #[test]
1560 fn test_break_build_flag_logic() {
1561 let break_build_enabled = true;
1563 let break_build_disabled = false;
1564
1565 if break_build_enabled {
1567 let compliance_returned = true;
1569 assert!(compliance_returned);
1570 }
1571
1572 if !break_build_disabled {
1574 let compliance_returned = false;
1576 assert!(!compliance_returned);
1577 }
1578 }
1579}