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)]
278pub struct SummaryReport {
279 pub app_id: u64,
281 pub app_name: String,
283 pub build_id: u64,
285 pub policy_compliance_status: String,
287 pub policy_name: String,
289 pub policy_version: u32,
291 pub policy_rules_status: String,
293 pub grace_period_expired: bool,
295 pub scan_overdue: String,
297 pub is_latest_build: bool,
299 pub sandbox_name: Option<String>,
301 pub sandbox_id: Option<u64>,
303 pub generation_date: String,
305 pub last_update_time: String,
307 #[serde(rename = "static-analysis")]
309 pub static_analysis: Option<StaticAnalysisSummary>,
310 #[serde(rename = "flaw-status")]
312 pub flaw_status: Option<FlawStatusSummary>,
313 pub software_composition_analysis: Option<ScaSummary>,
315 pub severity: Option<Vec<SeverityLevel>>,
317}
318
319#[derive(Debug, Clone, Serialize, Deserialize)]
321pub struct StaticAnalysisSummary {
322 pub rating: Option<String>,
324 pub score: Option<u32>,
326 pub mitigated_rating: Option<String>,
328 pub mitigated_score: Option<u32>,
330 pub analysis_size_bytes: Option<u64>,
332 pub engine_version: Option<String>,
334 pub published_date: Option<String>,
336 pub version: Option<String>,
338}
339
340#[derive(Debug, Clone, Serialize, Deserialize)]
342pub struct FlawStatusSummary {
343 pub new: u32,
345 pub reopen: u32,
347 pub open: u32,
349 pub fixed: u32,
351 pub total: u32,
353 pub not_mitigated: u32,
355}
356
357#[derive(Debug, Clone, Serialize, Deserialize)]
359pub struct ScaSummary {
360 pub third_party_components: u32,
362 pub violate_policy: bool,
364 pub components_violated_policy: u32,
366}
367
368#[derive(Debug, Clone, Serialize, Deserialize)]
370pub struct SeverityLevel {
371 pub level: u32,
373 pub category: Vec<CategorySummary>,
375}
376
377#[derive(Debug, Clone, Serialize, Deserialize)]
379pub struct CategorySummary {
380 pub categoryname: String,
382 pub severity: String,
384 pub count: u32,
386}
387
388#[derive(Debug, Clone, Serialize, Deserialize)]
390pub struct ScanFrequencyRule {
391 pub scan_type: String,
393 pub frequency: String,
395}
396
397#[derive(Debug, Clone, Serialize, Deserialize)]
399pub struct FindingRule {
400 #[serde(rename = "type")]
402 pub rule_type: String,
403 pub scan_type: Vec<String>,
405 pub value: String,
407 pub advanced_options: Option<serde_json::Value>,
409}
410
411#[derive(Debug, Clone, Serialize, Deserialize)]
413pub struct FindingRuleAdvancedOptions {
414 pub override_severity: Option<bool>,
416 pub build_action: Option<String>,
418 pub component_dependency: Option<String>,
420 pub vulnerable_methods: Option<String>,
422 pub selected_licenses: Option<Vec<String>>,
424 pub override_severity_level: Option<String>,
426 pub allowed_nonoss_licenses: Option<bool>,
428 pub allowed_unrecognized_licenses: Option<bool>,
430 pub all_licenses_must_meet_requirement: Option<bool>,
432 pub is_blocklist: Option<bool>,
434}
435
436#[derive(Debug, Clone, Default)]
438pub struct PolicyListParams {
439 pub name: Option<String>,
441 pub policy_type: Option<String>,
443 pub is_active: Option<bool>,
445 pub default_only: Option<bool>,
447 pub page: Option<u32>,
449 pub size: Option<u32>,
451}
452
453impl PolicyListParams {
454 #[must_use]
456 pub fn to_query_params(&self) -> Vec<(String, String)> {
457 Vec::from(self) }
459}
460
461impl From<&PolicyListParams> for Vec<(String, String)> {
463 fn from(query: &PolicyListParams) -> Self {
464 let mut params = Vec::new();
465
466 if let Some(ref name) = query.name {
467 params.push(("name".to_string(), name.clone())); }
469 if let Some(ref policy_type) = query.policy_type {
470 params.push(("type".to_string(), policy_type.clone()));
471 }
472 if let Some(is_active) = query.is_active {
473 params.push(("active".to_string(), is_active.to_string()));
474 }
475 if let Some(default_only) = query.default_only {
476 params.push(("default".to_string(), default_only.to_string()));
477 }
478 if let Some(page) = query.page {
479 params.push(("page".to_string(), page.to_string()));
480 }
481 if let Some(size) = query.size {
482 params.push(("size".to_string(), size.to_string()));
483 }
484
485 params
486 }
487}
488
489impl From<PolicyListParams> for Vec<(String, String)> {
490 fn from(query: PolicyListParams) -> Self {
491 let mut params = Vec::new();
492
493 if let Some(name) = query.name {
494 params.push(("name".to_string(), name)); }
496 if let Some(policy_type) = query.policy_type {
497 params.push(("type".to_string(), policy_type)); }
499 if let Some(is_active) = query.is_active {
500 params.push(("active".to_string(), is_active.to_string()));
501 }
502 if let Some(default_only) = query.default_only {
503 params.push(("default".to_string(), default_only.to_string()));
504 }
505 if let Some(page) = query.page {
506 params.push(("page".to_string(), page.to_string()));
507 }
508 if let Some(size) = query.size {
509 params.push(("size".to_string(), size.to_string()));
510 }
511
512 params
513 }
514}
515
516#[derive(Debug, Clone, Serialize, Deserialize)]
518pub struct PolicyListResponse {
519 #[serde(rename = "_embedded")]
520 pub embedded: Option<PolicyEmbedded>,
521 pub page: Option<PageInfo>,
522 #[serde(rename = "_links")]
523 pub links: Option<serde_json::Value>,
524}
525
526#[derive(Debug, Clone, Serialize, Deserialize)]
528pub struct PolicyEmbedded {
529 #[serde(rename = "policy_versions")]
530 pub policy_versions: Vec<SecurityPolicy>,
531}
532
533#[derive(Debug, Clone, Serialize, Deserialize)]
535pub struct PageInfo {
536 pub size: u32,
537 pub number: u32,
538 pub total_elements: u32,
539 pub total_pages: u32,
540}
541
542#[derive(Debug, Clone, PartialEq, Eq)]
544pub enum ApiSource {
545 SummaryReport,
547 BuildInfo,
549}
550
551#[derive(Debug)]
553#[must_use = "Need to handle all error enum types."]
554pub enum PolicyError {
555 Api(VeracodeError),
557 NotFound,
559 InvalidConfig(String),
561 ScanFailed(String),
563 EvaluationError(String),
565 PermissionDenied,
567 Unauthorized,
569 InternalServerError,
571 Timeout,
573}
574
575impl std::fmt::Display for PolicyError {
576 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
577 match self {
578 PolicyError::Api(err) => write!(f, "API error: {err}"),
579 PolicyError::NotFound => write!(f, "Policy not found"),
580 PolicyError::InvalidConfig(msg) => write!(f, "Invalid policy configuration: {msg}"),
581 PolicyError::ScanFailed(msg) => write!(f, "Policy scan failed: {msg}"),
582 PolicyError::EvaluationError(msg) => write!(f, "Policy evaluation error: {msg}"),
583 PolicyError::PermissionDenied => {
584 write!(f, "Insufficient permissions for policy operation")
585 }
586 PolicyError::Unauthorized => {
587 write!(f, "Authentication required - invalid API credentials")
588 }
589 PolicyError::InternalServerError => write!(f, "Internal server error occurred"),
590 PolicyError::Timeout => write!(f, "Policy operation timed out"),
591 }
592 }
593}
594
595impl std::error::Error for PolicyError {}
596
597impl From<VeracodeError> for PolicyError {
598 fn from(err: VeracodeError) -> Self {
599 PolicyError::Api(err)
600 }
601}
602
603impl From<reqwest::Error> for PolicyError {
604 fn from(err: reqwest::Error) -> Self {
605 PolicyError::Api(VeracodeError::Http(err))
606 }
607}
608
609impl From<serde_json::Error> for PolicyError {
610 fn from(err: serde_json::Error) -> Self {
611 PolicyError::Api(VeracodeError::Serialization(err))
612 }
613}
614
615pub struct PolicyApi<'a> {
617 client: &'a VeracodeClient,
618}
619
620impl<'a> PolicyApi<'a> {
621 #[must_use]
628 pub fn new(client: &'a VeracodeClient) -> Self {
629 Self { client }
630 }
631
632 pub async fn list_policies(
647 &self,
648 params: Option<PolicyListParams>,
649 ) -> Result<Vec<SecurityPolicy>, PolicyError> {
650 let endpoint = "/appsec/v1/policies";
651
652 let query_params = params.as_ref().map(Vec::from);
653
654 let response = self.client.get(endpoint, query_params.as_deref()).await?;
655
656 let status = response.status().as_u16();
657 match status {
658 200 => {
659 let policy_response: PolicyListResponse = response.json().await?;
660 let policies = policy_response
661 .embedded
662 .map(|e| e.policy_versions)
663 .unwrap_or_default();
664
665 Ok(policies)
666 }
667 400 => {
668 let error_text = response.text().await.unwrap_or_default();
669 Err(PolicyError::InvalidConfig(error_text))
670 }
671 401 => Err(PolicyError::Unauthorized),
672 403 => Err(PolicyError::PermissionDenied),
673 404 => Err(PolicyError::NotFound),
674 500 => Err(PolicyError::InternalServerError),
675 _ => {
676 let error_text = response.text().await.unwrap_or_default();
677 Err(PolicyError::Api(VeracodeError::InvalidResponse(format!(
678 "HTTP {status}: {error_text}"
679 ))))
680 }
681 }
682 }
683
684 pub async fn get_policy(&self, policy_guid: &str) -> Result<SecurityPolicy, PolicyError> {
699 let endpoint = format!("/appsec/v1/policies/{policy_guid}");
700
701 let response = self.client.get(&endpoint, None).await?;
702
703 let status = response.status().as_u16();
704 match status {
705 200 => {
706 let policy: SecurityPolicy = response.json().await?;
707 Ok(policy)
708 }
709 400 => {
710 let error_text = response.text().await.unwrap_or_default();
711 Err(PolicyError::InvalidConfig(error_text))
712 }
713 401 => Err(PolicyError::Unauthorized),
714 403 => Err(PolicyError::PermissionDenied),
715 404 => Err(PolicyError::NotFound),
716 500 => Err(PolicyError::InternalServerError),
717 _ => {
718 let error_text = response.text().await.unwrap_or_default();
719 Err(PolicyError::Api(VeracodeError::InvalidResponse(format!(
720 "HTTP {status}: {error_text}"
721 ))))
722 }
723 }
724 }
725
726 pub async fn get_default_policy(&self) -> Result<SecurityPolicy, PolicyError> {
737 let params = PolicyListParams {
738 default_only: Some(true),
739 ..Default::default()
740 };
741
742 let policies = self.list_policies(Some(params)).await?;
743 policies
746 .into_iter()
747 .find(|p| p.policy_type == "CUSTOMER" && p.organization_id.is_some())
748 .ok_or(PolicyError::NotFound)
749 }
750
751 pub async fn evaluate_policy_compliance_via_buildinfo(
770 &self,
771 app_id: &str,
772 sandbox_id: Option<&str>,
773 ) -> Result<std::borrow::Cow<'static, str>, PolicyError> {
774 self.evaluate_policy_compliance_via_buildinfo_with_retry(app_id, sandbox_id, 30, 10)
775 .await
776 }
777
778 pub async fn evaluate_policy_compliance_via_buildinfo_with_retry(
799 &self,
800 app_id: &str,
801 sandbox_id: Option<&str>,
802 max_retries: u32,
803 retry_delay_seconds: u64,
804 ) -> Result<std::borrow::Cow<'static, str>, PolicyError> {
805 use crate::build::{BuildError, GetBuildInfoRequest};
806 use std::borrow::Cow;
807 use tokio::time::{Duration, sleep};
808
809 let build_request = GetBuildInfoRequest {
810 app_id: app_id.to_string(),
811 build_id: None, sandbox_id: sandbox_id.map(str::to_string),
813 };
814
815 let mut attempts: u32 = 0;
816 loop {
817 let build_info = self
818 .client
819 .build_api()?
820 .get_build_info(&build_request)
821 .await
822 .map_err(|e| match e {
823 BuildError::BuildNotFound => PolicyError::Api(crate::VeracodeError::InvalidResponse(
824 format!("Build not found for application ID {app_id}. This may indicate: no builds exist for this application, the build ID is invalid, or the application has no completed scans. Cannot retrieve policy status without a valid build.")
825 )),
826 BuildError::ApplicationNotFound => PolicyError::Api(crate::VeracodeError::InvalidResponse(
827 format!("Application not found with ID {app_id}. This may indicate: incorrect application ID, insufficient permissions, or the application doesn't exist in your organization. Please verify the application ID and your API credentials.")
828 )),
829 BuildError::SandboxNotFound => PolicyError::Api(crate::VeracodeError::InvalidResponse(
830 format!("Sandbox not found with ID {}. This may indicate: incorrect sandbox ID, insufficient permissions, or the sandbox doesn't exist for this application.", sandbox_id.unwrap_or("unknown"))
831 )),
832 BuildError::Api(api_err) => PolicyError::Api(api_err),
833 BuildError::InvalidParameter(msg)
834 | BuildError::CreationFailed(msg)
835 | BuildError::UpdateFailed(msg)
836 | BuildError::DeletionFailed(msg)
837 | BuildError::XmlParsingError(msg) => {
838 PolicyError::Api(crate::VeracodeError::InvalidResponse(msg))
839 }
840 BuildError::Unauthorized | BuildError::PermissionDenied => PolicyError::Api(
841 crate::VeracodeError::Authentication("Build API access denied".to_string()),
842 ),
843 BuildError::BuildInProgress => {
844 PolicyError::Api(crate::VeracodeError::InvalidResponse(
845 "Build is currently in progress".to_string(),
846 ))
847 }
848 })?;
849
850 let status = build_info
852 .policy_compliance_status
853 .as_deref()
854 .unwrap_or("Not Assessed");
855
856 if status != "Not Assessed" && status != "Calculating..." {
858 return Ok(Cow::Owned(status.to_string()));
859 }
860
861 attempts = attempts.saturating_add(1);
863 if attempts >= max_retries {
864 warn!(
865 "Policy evaluation still not assessed after {max_retries} attempts. This may indicate: scan is still in progress, policy evaluation is taking longer than expected, or application may not have a policy assigned"
866 );
867 return Ok(Cow::Borrowed("Not Assessed"));
868 }
869
870 info!(
872 "Policy evaluation not yet assessed, retrying in {retry_delay_seconds} seconds... (attempt {attempts}/{max_retries})"
873 );
874
875 sleep(Duration::from_secs(retry_delay_seconds)).await;
877 }
878 }
879
880 #[must_use]
890 pub fn should_break_build(status: &str) -> bool {
891 status == "Did Not Pass"
892 }
893
894 #[must_use]
904 pub fn get_exit_code_for_status(status: &str) -> i32 {
905 if Self::should_break_build(status) {
906 4 } else {
908 0 }
910 }
911
912 pub async fn get_summary_report(
937 &self,
938 app_guid: &str,
939 build_id: Option<&str>,
940 sandbox_guid: Option<&str>,
941 ) -> Result<SummaryReport, PolicyError> {
942 let endpoint = format!("/appsec/v2/applications/{app_guid}/summary_report");
943
944 let mut query_params = Vec::new();
946 if let Some(build_id) = build_id {
947 query_params.push(("build_id".to_string(), build_id.to_string()));
948 }
949 if let Some(sandbox_guid) = sandbox_guid {
950 query_params.push(("context".to_string(), sandbox_guid.to_string()));
951 }
952
953 let response = self.client.get(&endpoint, Some(&query_params)).await?;
954
955 let status = response.status().as_u16();
956 match status {
957 200 => {
958 let summary_report: SummaryReport = response.json().await?;
959 Ok(summary_report)
960 }
961 400 => {
962 let error_text = response.text().await.unwrap_or_default();
963 Err(PolicyError::InvalidConfig(error_text))
964 }
965 401 => Err(PolicyError::Unauthorized),
966 403 => Err(PolicyError::PermissionDenied),
967 404 => Err(PolicyError::NotFound),
968 500 => Err(PolicyError::InternalServerError),
969 _ => {
970 let error_text = response.text().await.unwrap_or_default();
971 Err(PolicyError::Api(VeracodeError::InvalidResponse(format!(
972 "HTTP {status}: {error_text}"
973 ))))
974 }
975 }
976 }
977
978 #[allow(clippy::too_many_arguments)]
1008 pub async fn get_summary_report_with_policy_retry(
1014 &self,
1015 app_guid: &str,
1016 build_id: Option<&str>,
1017 sandbox_guid: Option<&str>,
1018 max_retries: u32,
1019 retry_delay_seconds: u64,
1020 enable_break_build: bool,
1021 ) -> Result<(SummaryReport, Option<std::borrow::Cow<'static, str>>), PolicyError> {
1022 use std::borrow::Cow;
1023 use tokio::time::{Duration, sleep};
1024
1025 if enable_break_build && build_id.is_none() {
1026 return Err(PolicyError::InvalidConfig(
1027 "Build ID is required for break build policy evaluation".to_string(),
1028 ));
1029 }
1030
1031 let mut attempts: u32 = 0;
1032 loop {
1033 if attempts == 0 && enable_break_build {
1034 debug!("Checking policy compliance status with retry logic...");
1035 } else if attempts == 0 {
1036 debug!("Getting summary report...");
1037 }
1038
1039 let summary_report = match self
1040 .get_summary_report(app_guid, build_id, sandbox_guid)
1041 .await
1042 {
1043 Ok(report) => report,
1044 Err(PolicyError::InternalServerError) if attempts < 3 => {
1045 warn!(
1046 "Summary report API failed with server error (attempt {}/3), retrying in 5 seconds...",
1047 attempts.saturating_add(1)
1048 );
1049 sleep(Duration::from_secs(5)).await;
1050 attempts = attempts.saturating_add(1);
1051 continue;
1052 }
1053 Err(e) => return Err(e),
1054 };
1055
1056 if !enable_break_build {
1058 return Ok((summary_report, None));
1059 }
1060
1061 let status = summary_report.policy_compliance_status.clone();
1063
1064 if !status.is_empty() && status != "Not Assessed" {
1066 debug!("Policy compliance status ready: {status}");
1067 return Ok((summary_report, Some(Cow::Owned(status))));
1068 }
1069
1070 attempts = attempts.saturating_add(1);
1072 if attempts >= max_retries {
1073 warn!(
1074 "Policy evaluation still not ready after {max_retries} attempts. Status: {status}. This may indicate: scan is still in progress, policy evaluation is taking longer than expected, or build results are not yet available"
1075 );
1076 return Ok((summary_report, Some(Cow::Owned(status))));
1077 }
1078
1079 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_with_retry(
1116 &self,
1117 app_guid: &str,
1118 build_id: &str,
1119 sandbox_guid: Option<&str>,
1120 max_retries: u32,
1121 retry_delay_seconds: u64,
1122 ) -> Result<std::borrow::Cow<'static, str>, PolicyError> {
1123 use std::borrow::Cow;
1124 use tokio::time::{Duration, sleep};
1125
1126 let mut attempts: u32 = 0;
1127 loop {
1128 let summary_report = self
1129 .get_summary_report(app_guid, Some(build_id), sandbox_guid)
1130 .await?;
1131
1132 let status = &summary_report.policy_compliance_status;
1135
1136 if !status.is_empty() && status != "Not Assessed" {
1138 return Ok(Cow::Owned(status.clone()));
1139 }
1140
1141 attempts = attempts.saturating_add(1);
1143 if attempts >= max_retries {
1144 warn!(
1145 "Policy evaluation still not ready after {max_retries} attempts. Status: {status}. This may indicate: scan is still in progress, policy evaluation is taking longer than expected, or build results are not yet available"
1146 );
1147 return Ok(Cow::Owned(status.clone()));
1148 }
1149
1150 info!(
1152 "Policy evaluation not yet ready (status: '{status}'), retrying in {retry_delay_seconds} seconds... (attempt {attempts}/{max_retries})"
1153 );
1154
1155 sleep(Duration::from_secs(retry_delay_seconds)).await;
1157 }
1158 }
1159
1160 pub async fn evaluate_policy_compliance_via_summary_report(
1179 &self,
1180 app_guid: &str,
1181 build_id: &str,
1182 sandbox_guid: Option<&str>,
1183 ) -> Result<std::borrow::Cow<'static, str>, PolicyError> {
1184 self.evaluate_policy_compliance_via_summary_report_with_retry(
1185 app_guid,
1186 build_id,
1187 sandbox_guid,
1188 30,
1189 10,
1190 )
1191 .await
1192 }
1193
1194 pub async fn initiate_policy_scan(
1209 &self,
1210 request: PolicyScanRequest,
1211 ) -> Result<PolicyScanResult, PolicyError> {
1212 let endpoint = "/appsec/v1/policy-scans";
1213
1214 let response = self.client.post(endpoint, Some(&request)).await?;
1215
1216 let status = response.status().as_u16();
1217 match status {
1218 200 | 201 => {
1219 let scan_result: PolicyScanResult = response.json().await?;
1220 Ok(scan_result)
1221 }
1222 400 => {
1223 let error_text = response.text().await.unwrap_or_default();
1224 Err(PolicyError::InvalidConfig(error_text))
1225 }
1226 404 => Err(PolicyError::NotFound),
1227 _ => {
1228 let error_text = response.text().await.unwrap_or_default();
1229 Err(PolicyError::Api(VeracodeError::InvalidResponse(format!(
1230 "HTTP {status}: {error_text}"
1231 ))))
1232 }
1233 }
1234 }
1235
1236 pub async fn get_policy_scan_result(
1251 &self,
1252 scan_id: u64,
1253 ) -> Result<PolicyScanResult, PolicyError> {
1254 let endpoint = format!("/appsec/v1/policy-scans/{scan_id}");
1255
1256 let response = self.client.get(&endpoint, None).await?;
1257
1258 let status = response.status().as_u16();
1259 match status {
1260 200 => {
1261 let scan_result: PolicyScanResult = response.json().await?;
1262 Ok(scan_result)
1263 }
1264 404 => Err(PolicyError::NotFound),
1265 _ => {
1266 let error_text = response.text().await.unwrap_or_default();
1267 Err(PolicyError::Api(VeracodeError::InvalidResponse(format!(
1268 "HTTP {status}: {error_text}"
1269 ))))
1270 }
1271 }
1272 }
1273
1274 pub async fn is_policy_scan_complete(&self, scan_id: u64) -> Result<bool, PolicyError> {
1289 let scan_result = self.get_policy_scan_result(scan_id).await?;
1290 Ok(matches!(
1291 scan_result.status,
1292 ScanStatus::Completed | ScanStatus::Failed | ScanStatus::Cancelled
1293 ))
1294 }
1295
1296 #[allow(clippy::too_many_arguments)]
1331 pub async fn get_policy_status_with_fallback(
1337 &self,
1338 app_guid: &str,
1339 app_id: &str,
1340 build_id: Option<&str>,
1341 sandbox_guid: Option<&str>,
1342 sandbox_id: Option<&str>,
1343 max_retries: u32,
1344 retry_delay_seconds: u64,
1345 enable_break_build: bool,
1346 force_buildinfo_api: bool,
1347 ) -> Result<(Option<SummaryReport>, String, ApiSource), PolicyError> {
1348 if force_buildinfo_api {
1349 debug!("Using getbuildinfo.do API directly (forced via configuration)");
1351 let status = self
1352 .evaluate_policy_compliance_via_buildinfo_with_retry(
1353 app_id,
1354 sandbox_id,
1355 max_retries,
1356 retry_delay_seconds,
1357 )
1358 .await?;
1359 return Ok((None, status.to_string(), ApiSource::BuildInfo));
1360 }
1361
1362 match self
1364 .get_summary_report_with_policy_retry(
1365 app_guid,
1366 build_id,
1367 sandbox_guid,
1368 max_retries,
1369 retry_delay_seconds,
1370 enable_break_build,
1371 )
1372 .await
1373 {
1374 Ok((summary_report, compliance_status)) => {
1375 debug!("Used summary report API successfully");
1376 let status = compliance_status
1377 .map(|s| s.to_string())
1378 .unwrap_or_else(|| summary_report.policy_compliance_status.clone());
1379 Ok((Some(summary_report), status, ApiSource::SummaryReport))
1380 }
1381 Err(
1382 ref e @ (PolicyError::Unauthorized
1383 | PolicyError::PermissionDenied
1384 | PolicyError::InternalServerError),
1385 ) => {
1386 match *e {
1387 PolicyError::InternalServerError => info!(
1388 "Summary report API server error, falling back to getbuildinfo.do API"
1389 ),
1390 PolicyError::Unauthorized | PolicyError::PermissionDenied => {
1391 info!("Summary report access denied, falling back to getbuildinfo.do API")
1392 }
1393 PolicyError::Api(_)
1394 | PolicyError::NotFound
1395 | PolicyError::InvalidConfig(_)
1396 | PolicyError::ScanFailed(_)
1397 | PolicyError::EvaluationError(_)
1398 | PolicyError::Timeout => {}
1399 }
1400 let status = self
1401 .evaluate_policy_compliance_via_buildinfo_with_retry(
1402 app_id,
1403 sandbox_id,
1404 max_retries,
1405 retry_delay_seconds,
1406 )
1407 .await?;
1408 Ok((None, status.to_string(), ApiSource::BuildInfo))
1409 }
1410 Err(e) => Err(e),
1411 }
1412 }
1413
1414 pub async fn get_active_policies(&self) -> Result<Vec<SecurityPolicy>, PolicyError> {
1425 let policies = self.list_policies(None).await?;
1428 Ok(policies) }
1430}
1431
1432#[cfg(test)]
1433#[allow(clippy::expect_used)]
1434mod tests {
1435 use super::*;
1436
1437 #[test]
1438 fn test_policy_list_params_to_query() {
1439 let params = PolicyListParams {
1440 name: Some("test-policy".to_string()),
1441 is_active: Some(true),
1442 page: Some(1),
1443 size: Some(10),
1444 ..Default::default()
1445 };
1446
1447 let query_params: Vec<_> = params.into();
1448 assert_eq!(query_params.len(), 4);
1449 assert!(query_params.contains(&("name".to_string(), "test-policy".to_string())));
1450 assert!(query_params.contains(&("active".to_string(), "true".to_string())));
1451 assert!(query_params.contains(&("page".to_string(), "1".to_string())));
1452 assert!(query_params.contains(&("size".to_string(), "10".to_string())));
1453 }
1454
1455 #[test]
1456 fn test_policy_error_display() {
1457 let error = PolicyError::NotFound;
1458 assert_eq!(error.to_string(), "Policy not found");
1459
1460 let error = PolicyError::InvalidConfig("test".to_string());
1461 assert_eq!(error.to_string(), "Invalid policy configuration: test");
1462
1463 let error = PolicyError::Timeout;
1464 assert_eq!(error.to_string(), "Policy operation timed out");
1465 }
1466
1467 #[test]
1468 fn test_scan_type_serialization() {
1469 let scan_type = ScanType::Static;
1470 let json = serde_json::to_string(&scan_type).expect("should serialize to json");
1471 assert_eq!(json, "\"static\"");
1472
1473 let deserialized: ScanType = serde_json::from_str(&json).expect("should deserialize json");
1474 assert!(matches!(deserialized, ScanType::Static));
1475 }
1476
1477 #[test]
1478 fn test_policy_compliance_status_serialization() {
1479 let status = PolicyComplianceStatus::Passed;
1480 let json = serde_json::to_string(&status).expect("should serialize to json");
1481 assert_eq!(json, "\"Passed\"");
1482
1483 let deserialized: PolicyComplianceStatus =
1484 serde_json::from_str(&json).expect("should deserialize json");
1485 assert!(matches!(deserialized, PolicyComplianceStatus::Passed));
1486
1487 let conditional_pass = PolicyComplianceStatus::ConditionalPass;
1489 let json = serde_json::to_string(&conditional_pass).expect("should serialize to json");
1490 assert_eq!(json, "\"Conditional Pass\"");
1491
1492 let did_not_pass = PolicyComplianceStatus::DidNotPass;
1493 let json = serde_json::to_string(&did_not_pass).expect("should serialize to json");
1494 assert_eq!(json, "\"Did Not Pass\"");
1495 }
1496
1497 #[test]
1498 fn test_break_build_logic() {
1499 assert!(PolicyApi::should_break_build("Did Not Pass"));
1500 assert!(!PolicyApi::should_break_build("Passed"));
1501 assert!(!PolicyApi::should_break_build("Conditional Pass"));
1502 assert!(!PolicyApi::should_break_build("Not Assessed"));
1505
1506 assert_eq!(PolicyApi::get_exit_code_for_status("Did Not Pass"), 4);
1507 assert_eq!(PolicyApi::get_exit_code_for_status("Passed"), 0);
1508 assert_eq!(PolicyApi::get_exit_code_for_status("Conditional Pass"), 0);
1509 assert_eq!(PolicyApi::get_exit_code_for_status("Not Assessed"), 0);
1512 }
1513
1514 #[test]
1515 fn test_summary_report_serialization() {
1516 let summary_json = r#"{
1517 "app_id": 2676517,
1518 "app_name": "Verascan Java Test",
1519 "build_id": 54209787,
1520 "policy_compliance_status": "Did Not Pass",
1521 "policy_name": "SecureCode Policy",
1522 "policy_version": 1,
1523 "policy_rules_status": "Did Not Pass",
1524 "grace_period_expired": false,
1525 "scan_overdue": "false",
1526 "is_latest_build": false,
1527 "generation_date": "2025-08-05 10:14:45 UTC",
1528 "last_update_time": "2025-08-05 10:00:51 UTC"
1529 }"#;
1530
1531 let summary: Result<SummaryReport, _> = serde_json::from_str(summary_json);
1532 assert!(summary.is_ok());
1533
1534 let summary = summary.expect("should have summary");
1535 assert_eq!(summary.policy_compliance_status, "Did Not Pass");
1536 assert_eq!(summary.app_name, "Verascan Java Test");
1537 assert_eq!(summary.build_id, 54209787);
1538 assert!(PolicyApi::should_break_build(
1539 &summary.policy_compliance_status
1540 ));
1541 }
1542
1543 #[test]
1544 fn test_export_json_structure() {
1545 let summary_report = SummaryReport {
1547 app_id: 2676517,
1548 app_name: "Test App".to_string(),
1549 build_id: 54209787,
1550 policy_compliance_status: "Passed".to_string(),
1551 policy_name: "Test Policy".to_string(),
1552 policy_version: 1,
1553 policy_rules_status: "Passed".to_string(),
1554 grace_period_expired: false,
1555 scan_overdue: "false".to_string(),
1556 is_latest_build: true,
1557 sandbox_name: Some("test-sandbox".to_string()),
1558 sandbox_id: Some(123456),
1559 generation_date: "2025-08-05 10:14:45 UTC".to_string(),
1560 last_update_time: "2025-08-05 10:00:51 UTC".to_string(),
1561 static_analysis: None,
1562 flaw_status: None,
1563 software_composition_analysis: None,
1564 severity: None,
1565 };
1566
1567 let export_json = serde_json::json!({
1568 "summary_report": summary_report,
1569 "export_metadata": {
1570 "exported_at": "2025-08-05T10:14:45Z",
1571 "tool": "verascan",
1572 "export_type": "summary_report",
1573 "scan_configuration": {
1574 "autoscan": true,
1575 "scan_all_nonfatal_top_level_modules": true,
1576 "include_new_modules": true
1577 }
1578 }
1579 });
1580
1581 assert!(
1583 export_json
1584 .get("summary_report")
1585 .and_then(|s| s.get("app_name"))
1586 .map(|v| v.is_string())
1587 .unwrap_or(false)
1588 );
1589 assert!(
1590 export_json
1591 .get("summary_report")
1592 .and_then(|s| s.get("policy_compliance_status"))
1593 .map(|v| v.is_string())
1594 .unwrap_or(false)
1595 );
1596 assert!(
1597 export_json
1598 .get("export_metadata")
1599 .and_then(|e| e.get("export_type"))
1600 .map(|v| v.is_string())
1601 .unwrap_or(false)
1602 );
1603 assert_eq!(
1604 export_json
1605 .get("export_metadata")
1606 .and_then(|e| e.get("export_type"))
1607 .and_then(|v| v.as_str())
1608 .expect("should have export_type"),
1609 "summary_report"
1610 );
1611
1612 let json_string =
1614 serde_json::to_string_pretty(&export_json).expect("should serialize to json");
1615 assert!(json_string.contains("summary_report"));
1616 assert!(json_string.contains("export_metadata"));
1617 }
1618
1619 #[test]
1620 fn test_get_summary_report_with_policy_retry_parameters() {
1621 let app_guid = "test-app-guid";
1625 let build_id = Some("test-build-id");
1626 let sandbox_guid: Option<&str> = None;
1627 let max_retries = 30u32;
1628 let retry_delay_seconds = 10u64;
1629 let debug = false;
1630 let enable_break_build = true;
1631
1632 assert_eq!(app_guid, "test-app-guid");
1634 assert_eq!(build_id, Some("test-build-id"));
1635 assert_eq!(sandbox_guid, None);
1636 assert_eq!(max_retries, 30);
1637 assert_eq!(retry_delay_seconds, 10);
1638 assert!(!debug);
1639 assert!(enable_break_build);
1640 }
1641
1642 #[test]
1643 fn test_policy_status_ready_logic() {
1644 let ready_statuses = vec!["Passed", "Did Not Pass", "Conditional Pass"];
1646 let not_ready_statuses = vec!["", "Not Assessed"];
1647
1648 for status in &ready_statuses {
1650 assert!(
1651 !status.is_empty(),
1652 "Ready status should not be empty: {status}"
1653 );
1654 assert_ne!(
1655 *status, "Not Assessed",
1656 "Ready status should not be 'Not Assessed': {status}"
1657 );
1658 }
1659
1660 for status in ¬_ready_statuses {
1662 let is_not_ready = status.is_empty() || *status == "Not Assessed";
1663 assert!(is_not_ready, "Status should trigger retry: '{status}'");
1664 }
1665 }
1666
1667 #[test]
1668 fn test_combined_method_return_types() {
1669 use std::borrow::Cow;
1670
1671 let compliance_status = Cow::Borrowed("Passed");
1676 assert_eq!(compliance_status.as_ref(), "Passed");
1677
1678 let compliance_status: Option<Cow<'static, str>> = None;
1680 assert!(compliance_status.is_none());
1681 }
1682
1683 #[test]
1684 fn test_debug_logging_parameters() {
1685 let debug_enabled = true;
1687 let debug_disabled = false;
1688
1689 assert!(debug_enabled);
1690 assert!(!debug_disabled);
1691
1692 if debug_enabled {
1695 }
1697
1698 if !debug_disabled {
1699 }
1701 }
1702
1703 #[test]
1704 fn test_break_build_flag_logic() {
1705 let break_build_enabled = true;
1707 let break_build_disabled = false;
1708
1709 if break_build_enabled {
1711 let compliance_returned = true;
1713 assert!(compliance_returned);
1714 }
1715
1716 if !break_build_disabled {
1718 let compliance_returned = false;
1720 assert!(!compliance_returned);
1721 }
1722 }
1723}