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 build_id: Option<&str>,
773 sandbox_id: Option<&str>,
774 ) -> Result<std::borrow::Cow<'static, str>, PolicyError> {
775 self.evaluate_policy_compliance_via_buildinfo_with_retry(
776 app_id, build_id, sandbox_id, 30, 10,
777 )
778 .await
779 }
780
781 pub async fn evaluate_policy_compliance_via_buildinfo_with_retry(
803 &self,
804 app_id: &str,
805 build_id: Option<&str>,
806 sandbox_id: Option<&str>,
807 max_retries: u32,
808 retry_delay_seconds: u64,
809 ) -> Result<std::borrow::Cow<'static, str>, PolicyError> {
810 use crate::build::{BuildError, GetBuildInfoRequest};
811 use std::borrow::Cow;
812 use tokio::time::{Duration, sleep};
813
814 let build_request = GetBuildInfoRequest {
815 app_id: app_id.to_string(),
816 build_id: build_id.map(str::to_string), sandbox_id: sandbox_id.map(str::to_string),
818 };
819
820 let mut attempts: u32 = 0;
821 loop {
822 let build_info = self
823 .client
824 .build_api()?
825 .get_build_info(&build_request)
826 .await
827 .map_err(|e| match e {
828 BuildError::BuildNotFound => PolicyError::Api(crate::VeracodeError::InvalidResponse(
829 format!("Build not found for application ID {app_id}. This may indicate: no builds exist for this application, the build ID is invalid, or the application has no completed scans. Cannot retrieve policy status without a valid build.")
830 )),
831 BuildError::ApplicationNotFound => PolicyError::Api(crate::VeracodeError::InvalidResponse(
832 format!("Application not found with ID {app_id}. This may indicate: incorrect application ID, insufficient permissions, or the application doesn't exist in your organization. Please verify the application ID and your API credentials.")
833 )),
834 BuildError::SandboxNotFound => PolicyError::Api(crate::VeracodeError::InvalidResponse(
835 format!("Sandbox not found with ID {}. This may indicate: incorrect sandbox ID, insufficient permissions, or the sandbox doesn't exist for this application.", sandbox_id.unwrap_or("unknown"))
836 )),
837 BuildError::Api(api_err) => PolicyError::Api(api_err),
838 BuildError::InvalidParameter(msg)
839 | BuildError::CreationFailed(msg)
840 | BuildError::UpdateFailed(msg)
841 | BuildError::DeletionFailed(msg)
842 | BuildError::XmlParsingError(msg) => {
843 PolicyError::Api(crate::VeracodeError::InvalidResponse(msg))
844 }
845 BuildError::Unauthorized | BuildError::PermissionDenied => PolicyError::Api(
846 crate::VeracodeError::Authentication("Build API access denied".to_string()),
847 ),
848 BuildError::BuildInProgress => {
849 PolicyError::Api(crate::VeracodeError::InvalidResponse(
850 "Build is currently in progress".to_string(),
851 ))
852 }
853 })?;
854
855 let status = build_info
857 .policy_compliance_status
858 .as_deref()
859 .unwrap_or("Not Assessed");
860
861 if status != "Not Assessed" && status != "Calculating..." {
863 return Ok(Cow::Owned(status.to_string()));
864 }
865
866 attempts = attempts.saturating_add(1);
868 if attempts >= max_retries {
869 warn!(
870 "Policy evaluation still not assessed after {max_retries} attempts. This may indicate: scan is still in progress, policy evaluation is taking longer than expected, or application may not have a policy assigned"
871 );
872 return Ok(Cow::Borrowed("Not Assessed"));
873 }
874
875 info!(
877 "Policy evaluation not yet assessed, retrying in {retry_delay_seconds} seconds... (attempt {attempts}/{max_retries})"
878 );
879
880 sleep(Duration::from_secs(retry_delay_seconds)).await;
882 }
883 }
884
885 #[must_use]
895 pub fn should_break_build(status: &str) -> bool {
896 status == "Did Not Pass"
897 }
898
899 #[must_use]
909 pub fn get_exit_code_for_status(status: &str) -> i32 {
910 if Self::should_break_build(status) {
911 4 } else {
913 0 }
915 }
916
917 pub async fn get_summary_report(
942 &self,
943 app_guid: &str,
944 build_id: Option<&str>,
945 sandbox_guid: Option<&str>,
946 ) -> Result<SummaryReport, PolicyError> {
947 let endpoint = format!("/appsec/v2/applications/{app_guid}/summary_report");
948
949 let mut query_params = Vec::new();
951 if let Some(build_id) = build_id {
952 query_params.push(("build_id".to_string(), build_id.to_string()));
953 }
954 if let Some(sandbox_guid) = sandbox_guid {
955 query_params.push(("context".to_string(), sandbox_guid.to_string()));
956 }
957
958 let response = self.client.get(&endpoint, Some(&query_params)).await?;
959
960 let status = response.status().as_u16();
961 match status {
962 200 => {
963 let summary_report: SummaryReport = response.json().await?;
964 Ok(summary_report)
965 }
966 400 => {
967 let error_text = response.text().await.unwrap_or_default();
968 Err(PolicyError::InvalidConfig(error_text))
969 }
970 401 => Err(PolicyError::Unauthorized),
971 403 => Err(PolicyError::PermissionDenied),
972 404 => Err(PolicyError::NotFound),
973 500 => Err(PolicyError::InternalServerError),
974 _ => {
975 let error_text = response.text().await.unwrap_or_default();
976 Err(PolicyError::Api(VeracodeError::InvalidResponse(format!(
977 "HTTP {status}: {error_text}"
978 ))))
979 }
980 }
981 }
982
983 #[allow(clippy::too_many_arguments)]
1013 pub async fn get_summary_report_with_policy_retry(
1019 &self,
1020 app_guid: &str,
1021 build_id: Option<&str>,
1022 sandbox_guid: Option<&str>,
1023 max_retries: u32,
1024 retry_delay_seconds: u64,
1025 enable_break_build: bool,
1026 ) -> Result<(SummaryReport, Option<std::borrow::Cow<'static, str>>), PolicyError> {
1027 use std::borrow::Cow;
1028 use tokio::time::{Duration, sleep};
1029
1030 if enable_break_build && build_id.is_none() {
1031 return Err(PolicyError::InvalidConfig(
1032 "Build ID is required for break build policy evaluation".to_string(),
1033 ));
1034 }
1035
1036 let mut attempts: u32 = 0;
1037 loop {
1038 if attempts == 0 && enable_break_build {
1039 debug!("Checking policy compliance status with retry logic...");
1040 } else if attempts == 0 {
1041 debug!("Getting summary report...");
1042 }
1043
1044 let summary_report = match self
1045 .get_summary_report(app_guid, build_id, sandbox_guid)
1046 .await
1047 {
1048 Ok(report) => report,
1049 Err(PolicyError::InternalServerError) if attempts < 3 => {
1050 warn!(
1051 "Summary report API failed with server error (attempt {}/3), retrying in 5 seconds...",
1052 attempts.saturating_add(1)
1053 );
1054 sleep(Duration::from_secs(5)).await;
1055 attempts = attempts.saturating_add(1);
1056 continue;
1057 }
1058 Err(e) => return Err(e),
1059 };
1060
1061 if !enable_break_build {
1063 return Ok((summary_report, None));
1064 }
1065
1066 let status = summary_report.policy_compliance_status.clone();
1068
1069 if !status.is_empty() && status != "Not Assessed" {
1071 debug!("Policy compliance status ready: {status}");
1072 return Ok((summary_report, Some(Cow::Owned(status))));
1073 }
1074
1075 attempts = attempts.saturating_add(1);
1077 if attempts >= max_retries {
1078 warn!(
1079 "Policy evaluation still not ready after {max_retries} attempts. Status: {status}. This may indicate: scan is still in progress, policy evaluation is taking longer than expected, or build results are not yet available"
1080 );
1081 return Ok((summary_report, Some(Cow::Owned(status))));
1082 }
1083
1084 info!(
1086 "Policy evaluation not yet ready (status: '{status}'), retrying in {retry_delay_seconds} seconds... (attempt {attempts}/{max_retries})"
1087 );
1088
1089 sleep(Duration::from_secs(retry_delay_seconds)).await;
1091 }
1092 }
1093
1094 pub async fn evaluate_policy_compliance_via_summary_report_with_retry(
1121 &self,
1122 app_guid: &str,
1123 build_id: &str,
1124 sandbox_guid: Option<&str>,
1125 max_retries: u32,
1126 retry_delay_seconds: u64,
1127 ) -> Result<std::borrow::Cow<'static, str>, PolicyError> {
1128 use std::borrow::Cow;
1129 use tokio::time::{Duration, sleep};
1130
1131 let mut attempts: u32 = 0;
1132 loop {
1133 let summary_report = self
1134 .get_summary_report(app_guid, Some(build_id), sandbox_guid)
1135 .await?;
1136
1137 let status = &summary_report.policy_compliance_status;
1140
1141 if !status.is_empty() && status != "Not Assessed" {
1143 return Ok(Cow::Owned(status.clone()));
1144 }
1145
1146 attempts = attempts.saturating_add(1);
1148 if attempts >= max_retries {
1149 warn!(
1150 "Policy evaluation still not ready after {max_retries} attempts. Status: {status}. This may indicate: scan is still in progress, policy evaluation is taking longer than expected, or build results are not yet available"
1151 );
1152 return Ok(Cow::Owned(status.clone()));
1153 }
1154
1155 info!(
1157 "Policy evaluation not yet ready (status: '{status}'), retrying in {retry_delay_seconds} seconds... (attempt {attempts}/{max_retries})"
1158 );
1159
1160 sleep(Duration::from_secs(retry_delay_seconds)).await;
1162 }
1163 }
1164
1165 pub async fn evaluate_policy_compliance_via_summary_report(
1184 &self,
1185 app_guid: &str,
1186 build_id: &str,
1187 sandbox_guid: Option<&str>,
1188 ) -> Result<std::borrow::Cow<'static, str>, PolicyError> {
1189 self.evaluate_policy_compliance_via_summary_report_with_retry(
1190 app_guid,
1191 build_id,
1192 sandbox_guid,
1193 30,
1194 10,
1195 )
1196 .await
1197 }
1198
1199 pub async fn initiate_policy_scan(
1214 &self,
1215 request: PolicyScanRequest,
1216 ) -> Result<PolicyScanResult, PolicyError> {
1217 let endpoint = "/appsec/v1/policy-scans";
1218
1219 let response = self.client.post(endpoint, Some(&request)).await?;
1220
1221 let status = response.status().as_u16();
1222 match status {
1223 200 | 201 => {
1224 let scan_result: PolicyScanResult = response.json().await?;
1225 Ok(scan_result)
1226 }
1227 400 => {
1228 let error_text = response.text().await.unwrap_or_default();
1229 Err(PolicyError::InvalidConfig(error_text))
1230 }
1231 404 => Err(PolicyError::NotFound),
1232 _ => {
1233 let error_text = response.text().await.unwrap_or_default();
1234 Err(PolicyError::Api(VeracodeError::InvalidResponse(format!(
1235 "HTTP {status}: {error_text}"
1236 ))))
1237 }
1238 }
1239 }
1240
1241 pub async fn get_policy_scan_result(
1256 &self,
1257 scan_id: u64,
1258 ) -> Result<PolicyScanResult, PolicyError> {
1259 let endpoint = format!("/appsec/v1/policy-scans/{scan_id}");
1260
1261 let response = self.client.get(&endpoint, None).await?;
1262
1263 let status = response.status().as_u16();
1264 match status {
1265 200 => {
1266 let scan_result: PolicyScanResult = response.json().await?;
1267 Ok(scan_result)
1268 }
1269 404 => Err(PolicyError::NotFound),
1270 _ => {
1271 let error_text = response.text().await.unwrap_or_default();
1272 Err(PolicyError::Api(VeracodeError::InvalidResponse(format!(
1273 "HTTP {status}: {error_text}"
1274 ))))
1275 }
1276 }
1277 }
1278
1279 pub async fn is_policy_scan_complete(&self, scan_id: u64) -> Result<bool, PolicyError> {
1294 let scan_result = self.get_policy_scan_result(scan_id).await?;
1295 Ok(matches!(
1296 scan_result.status,
1297 ScanStatus::Completed | ScanStatus::Failed | ScanStatus::Cancelled
1298 ))
1299 }
1300
1301 #[allow(clippy::too_many_arguments)]
1336 pub async fn get_policy_status_with_fallback(
1342 &self,
1343 app_guid: &str,
1344 app_id: &str,
1345 build_id: Option<&str>,
1346 sandbox_guid: Option<&str>,
1347 sandbox_id: Option<&str>,
1348 max_retries: u32,
1349 retry_delay_seconds: u64,
1350 enable_break_build: bool,
1351 force_buildinfo_api: bool,
1352 ) -> Result<(Option<SummaryReport>, String, ApiSource), PolicyError> {
1353 if force_buildinfo_api {
1354 debug!("Using getbuildinfo.do API directly (forced via configuration)");
1356 let status = self
1357 .evaluate_policy_compliance_via_buildinfo_with_retry(
1358 app_id,
1359 build_id,
1360 sandbox_id,
1361 max_retries,
1362 retry_delay_seconds,
1363 )
1364 .await?;
1365 return Ok((None, status.to_string(), ApiSource::BuildInfo));
1366 }
1367
1368 match self
1370 .get_summary_report_with_policy_retry(
1371 app_guid,
1372 build_id,
1373 sandbox_guid,
1374 max_retries,
1375 retry_delay_seconds,
1376 enable_break_build,
1377 )
1378 .await
1379 {
1380 Ok((summary_report, compliance_status)) => {
1381 debug!("Used summary report API successfully");
1382 let status = compliance_status
1383 .map(|s| s.to_string())
1384 .unwrap_or_else(|| summary_report.policy_compliance_status.clone());
1385 Ok((Some(summary_report), status, ApiSource::SummaryReport))
1386 }
1387 Err(
1388 ref e @ (PolicyError::Unauthorized
1389 | PolicyError::PermissionDenied
1390 | PolicyError::InternalServerError),
1391 ) => {
1392 match *e {
1393 PolicyError::InternalServerError => info!(
1394 "Summary report API server error, falling back to getbuildinfo.do API"
1395 ),
1396 PolicyError::Unauthorized | PolicyError::PermissionDenied => {
1397 info!("Summary report access denied, falling back to getbuildinfo.do API")
1398 }
1399 PolicyError::Api(_)
1400 | PolicyError::NotFound
1401 | PolicyError::InvalidConfig(_)
1402 | PolicyError::ScanFailed(_)
1403 | PolicyError::EvaluationError(_)
1404 | PolicyError::Timeout => {}
1405 }
1406 let status = self
1407 .evaluate_policy_compliance_via_buildinfo_with_retry(
1408 app_id,
1409 build_id,
1410 sandbox_id,
1411 max_retries,
1412 retry_delay_seconds,
1413 )
1414 .await?;
1415 Ok((None, status.to_string(), ApiSource::BuildInfo))
1416 }
1417 Err(e) => Err(e),
1418 }
1419 }
1420
1421 pub async fn get_active_policies(&self) -> Result<Vec<SecurityPolicy>, PolicyError> {
1432 let policies = self.list_policies(None).await?;
1435 Ok(policies) }
1437}
1438
1439#[cfg(test)]
1440#[allow(clippy::expect_used)]
1441mod tests {
1442 use super::*;
1443
1444 #[test]
1445 fn test_policy_list_params_to_query() {
1446 let params = PolicyListParams {
1447 name: Some("test-policy".to_string()),
1448 is_active: Some(true),
1449 page: Some(1),
1450 size: Some(10),
1451 ..Default::default()
1452 };
1453
1454 let query_params: Vec<_> = params.into();
1455 assert_eq!(query_params.len(), 4);
1456 assert!(query_params.contains(&("name".to_string(), "test-policy".to_string())));
1457 assert!(query_params.contains(&("active".to_string(), "true".to_string())));
1458 assert!(query_params.contains(&("page".to_string(), "1".to_string())));
1459 assert!(query_params.contains(&("size".to_string(), "10".to_string())));
1460 }
1461
1462 #[test]
1463 fn test_policy_error_display() {
1464 let error = PolicyError::NotFound;
1465 assert_eq!(error.to_string(), "Policy not found");
1466
1467 let error = PolicyError::InvalidConfig("test".to_string());
1468 assert_eq!(error.to_string(), "Invalid policy configuration: test");
1469
1470 let error = PolicyError::Timeout;
1471 assert_eq!(error.to_string(), "Policy operation timed out");
1472 }
1473
1474 #[test]
1475 fn test_scan_type_serialization() {
1476 let scan_type = ScanType::Static;
1477 let json = serde_json::to_string(&scan_type).expect("should serialize to json");
1478 assert_eq!(json, "\"static\"");
1479
1480 let deserialized: ScanType = serde_json::from_str(&json).expect("should deserialize json");
1481 assert!(matches!(deserialized, ScanType::Static));
1482 }
1483
1484 #[test]
1485 fn test_policy_compliance_status_serialization() {
1486 let status = PolicyComplianceStatus::Passed;
1487 let json = serde_json::to_string(&status).expect("should serialize to json");
1488 assert_eq!(json, "\"Passed\"");
1489
1490 let deserialized: PolicyComplianceStatus =
1491 serde_json::from_str(&json).expect("should deserialize json");
1492 assert!(matches!(deserialized, PolicyComplianceStatus::Passed));
1493
1494 let conditional_pass = PolicyComplianceStatus::ConditionalPass;
1496 let json = serde_json::to_string(&conditional_pass).expect("should serialize to json");
1497 assert_eq!(json, "\"Conditional Pass\"");
1498
1499 let did_not_pass = PolicyComplianceStatus::DidNotPass;
1500 let json = serde_json::to_string(&did_not_pass).expect("should serialize to json");
1501 assert_eq!(json, "\"Did Not Pass\"");
1502 }
1503
1504 #[test]
1505 fn test_break_build_logic() {
1506 assert!(PolicyApi::should_break_build("Did Not Pass"));
1507 assert!(!PolicyApi::should_break_build("Passed"));
1508 assert!(!PolicyApi::should_break_build("Conditional Pass"));
1509 assert!(!PolicyApi::should_break_build("Not Assessed"));
1512
1513 assert_eq!(PolicyApi::get_exit_code_for_status("Did Not Pass"), 4);
1514 assert_eq!(PolicyApi::get_exit_code_for_status("Passed"), 0);
1515 assert_eq!(PolicyApi::get_exit_code_for_status("Conditional Pass"), 0);
1516 assert_eq!(PolicyApi::get_exit_code_for_status("Not Assessed"), 0);
1519 }
1520
1521 #[test]
1522 fn test_summary_report_serialization() {
1523 let summary_json = r#"{
1524 "app_id": 2676517,
1525 "app_name": "Verascan Java Test",
1526 "build_id": 54209787,
1527 "policy_compliance_status": "Did Not Pass",
1528 "policy_name": "SecureCode Policy",
1529 "policy_version": 1,
1530 "policy_rules_status": "Did Not Pass",
1531 "grace_period_expired": false,
1532 "scan_overdue": "false",
1533 "is_latest_build": false,
1534 "generation_date": "2025-08-05 10:14:45 UTC",
1535 "last_update_time": "2025-08-05 10:00:51 UTC"
1536 }"#;
1537
1538 let summary: Result<SummaryReport, _> = serde_json::from_str(summary_json);
1539 assert!(summary.is_ok());
1540
1541 let summary = summary.expect("should have summary");
1542 assert_eq!(summary.policy_compliance_status, "Did Not Pass");
1543 assert_eq!(summary.app_name, "Verascan Java Test");
1544 assert_eq!(summary.build_id, 54209787);
1545 assert!(PolicyApi::should_break_build(
1546 &summary.policy_compliance_status
1547 ));
1548 }
1549
1550 #[test]
1551 fn test_export_json_structure() {
1552 let summary_report = SummaryReport {
1554 app_id: 2676517,
1555 app_name: "Test App".to_string(),
1556 build_id: 54209787,
1557 policy_compliance_status: "Passed".to_string(),
1558 policy_name: "Test Policy".to_string(),
1559 policy_version: 1,
1560 policy_rules_status: "Passed".to_string(),
1561 grace_period_expired: false,
1562 scan_overdue: "false".to_string(),
1563 is_latest_build: true,
1564 sandbox_name: Some("test-sandbox".to_string()),
1565 sandbox_id: Some(123456),
1566 generation_date: "2025-08-05 10:14:45 UTC".to_string(),
1567 last_update_time: "2025-08-05 10:00:51 UTC".to_string(),
1568 static_analysis: None,
1569 flaw_status: None,
1570 software_composition_analysis: None,
1571 severity: None,
1572 };
1573
1574 let export_json = serde_json::json!({
1575 "summary_report": summary_report,
1576 "export_metadata": {
1577 "exported_at": "2025-08-05T10:14:45Z",
1578 "tool": "verascan",
1579 "export_type": "summary_report",
1580 "scan_configuration": {
1581 "autoscan": true,
1582 "scan_all_nonfatal_top_level_modules": true,
1583 "include_new_modules": true
1584 }
1585 }
1586 });
1587
1588 assert!(
1590 export_json
1591 .get("summary_report")
1592 .and_then(|s| s.get("app_name"))
1593 .map(|v| v.is_string())
1594 .unwrap_or(false)
1595 );
1596 assert!(
1597 export_json
1598 .get("summary_report")
1599 .and_then(|s| s.get("policy_compliance_status"))
1600 .map(|v| v.is_string())
1601 .unwrap_or(false)
1602 );
1603 assert!(
1604 export_json
1605 .get("export_metadata")
1606 .and_then(|e| e.get("export_type"))
1607 .map(|v| v.is_string())
1608 .unwrap_or(false)
1609 );
1610 assert_eq!(
1611 export_json
1612 .get("export_metadata")
1613 .and_then(|e| e.get("export_type"))
1614 .and_then(|v| v.as_str())
1615 .expect("should have export_type"),
1616 "summary_report"
1617 );
1618
1619 let json_string =
1621 serde_json::to_string_pretty(&export_json).expect("should serialize to json");
1622 assert!(json_string.contains("summary_report"));
1623 assert!(json_string.contains("export_metadata"));
1624 }
1625
1626 #[test]
1627 fn test_get_summary_report_with_policy_retry_parameters() {
1628 let app_guid = "test-app-guid";
1632 let build_id = Some("test-build-id");
1633 let sandbox_guid: Option<&str> = None;
1634 let max_retries = 30u32;
1635 let retry_delay_seconds = 10u64;
1636 let debug = false;
1637 let enable_break_build = true;
1638
1639 assert_eq!(app_guid, "test-app-guid");
1641 assert_eq!(build_id, Some("test-build-id"));
1642 assert_eq!(sandbox_guid, None);
1643 assert_eq!(max_retries, 30);
1644 assert_eq!(retry_delay_seconds, 10);
1645 assert!(!debug);
1646 assert!(enable_break_build);
1647 }
1648
1649 #[test]
1650 fn test_policy_status_ready_logic() {
1651 let ready_statuses = vec!["Passed", "Did Not Pass", "Conditional Pass"];
1653 let not_ready_statuses = vec!["", "Not Assessed"];
1654
1655 for status in &ready_statuses {
1657 assert!(
1658 !status.is_empty(),
1659 "Ready status should not be empty: {status}"
1660 );
1661 assert_ne!(
1662 *status, "Not Assessed",
1663 "Ready status should not be 'Not Assessed': {status}"
1664 );
1665 }
1666
1667 for status in ¬_ready_statuses {
1669 let is_not_ready = status.is_empty() || *status == "Not Assessed";
1670 assert!(is_not_ready, "Status should trigger retry: '{status}'");
1671 }
1672 }
1673
1674 #[test]
1675 fn test_combined_method_return_types() {
1676 use std::borrow::Cow;
1677
1678 let compliance_status = Cow::Borrowed("Passed");
1683 assert_eq!(compliance_status.as_ref(), "Passed");
1684
1685 let compliance_status: Option<Cow<'static, str>> = None;
1687 assert!(compliance_status.is_none());
1688 }
1689
1690 #[test]
1691 fn test_debug_logging_parameters() {
1692 let debug_enabled = true;
1694 let debug_disabled = false;
1695
1696 assert!(debug_enabled);
1697 assert!(!debug_disabled);
1698
1699 if debug_enabled {
1702 }
1704
1705 if !debug_disabled {
1706 }
1708 }
1709
1710 #[test]
1711 fn test_break_build_flag_logic() {
1712 let break_build_enabled = true;
1714 let break_build_disabled = false;
1715
1716 if break_build_enabled {
1718 let compliance_returned = true;
1720 assert!(compliance_returned);
1721 }
1722
1723 if !break_build_disabled {
1725 let compliance_returned = false;
1727 assert!(!compliance_returned);
1728 }
1729 }
1730}