1use chrono::{DateTime, Utc};
7use log::{debug, info, warn};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11use crate::{VeracodeClient, VeracodeError};
12
13const MAX_RETRY_DELAY_SECONDS: u64 = 300;
17
18mod validation {
20 pub fn validate_guid(guid: &str) -> Result<(), String> {
23 let cleaned = guid.replace('-', "");
25
26 if cleaned.len() != 32 {
28 return Err(format!(
29 "Invalid GUID format: expected 32 hex characters, got {}",
30 cleaned.len()
31 ));
32 }
33
34 if !cleaned.chars().all(|c| c.is_ascii_hexdigit()) {
36 return Err("Invalid GUID format: contains non-hexadecimal characters".to_string());
37 }
38
39 if guid.contains("..") || guid.contains('/') || guid.contains('\\') {
41 return Err("Invalid GUID format: contains path traversal characters".to_string());
42 }
43
44 if guid.contains('?') || guid.contains('&') || guid.contains('#') {
46 return Err("Invalid GUID format: contains URL parameter characters".to_string());
47 }
48
49 Ok(())
50 }
51
52 pub fn validate_identifier(id: &str) -> Result<(), String> {
55 if id.is_empty() {
56 return Err("Identifier cannot be empty".to_string());
57 }
58
59 if id.contains("..") || id.contains('/') || id.contains('\\') {
61 return Err("Invalid identifier: contains path traversal characters".to_string());
62 }
63
64 if id.contains('?') || id.contains('&') || id.contains('#') {
66 return Err("Invalid identifier: contains URL parameter characters".to_string());
67 }
68
69 if !id
71 .chars()
72 .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
73 {
74 return Err("Invalid identifier: contains unsafe characters".to_string());
75 }
76
77 Ok(())
78 }
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct SecurityPolicy {
84 pub guid: String,
86 pub name: String,
88 pub description: Option<String>,
90 #[serde(rename = "type")]
92 pub policy_type: String,
93 pub version: u32,
95 pub created: Option<DateTime<Utc>>,
97 pub modified_by: Option<String>,
99 pub organization_id: Option<u64>,
101 pub category: String,
103 pub vendor_policy: bool,
105 pub scan_frequency_rules: Vec<ScanFrequencyRule>,
107 pub finding_rules: Vec<FindingRule>,
109 pub custom_severities: Vec<serde_json::Value>,
111 pub sev5_grace_period: u32,
113 pub sev4_grace_period: u32,
114 pub sev3_grace_period: u32,
115 pub sev2_grace_period: u32,
116 pub sev1_grace_period: u32,
117 pub sev0_grace_period: u32,
118 pub score_grace_period: u32,
120 pub sca_blacklist_grace_period: u32,
122 pub sca_grace_periods: Option<serde_json::Value>,
124 pub evaluation_date: Option<DateTime<Utc>>,
126 pub evaluation_date_type: Option<String>,
128 pub capabilities: Vec<String>,
130 #[serde(rename = "_links")]
132 pub links: Option<serde_json::Value>,
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
137#[serde(rename_all = "PascalCase")]
138pub enum PolicyComplianceStatus {
139 Passed,
141 #[serde(rename = "Conditional Pass")]
143 ConditionalPass,
144 #[serde(rename = "Did Not Pass")]
146 DidNotPass,
147 #[serde(rename = "Not Assessed")]
149 NotAssessed,
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct PolicyRule {
155 pub id: String,
157 pub name: String,
159 pub description: Option<String>,
161 pub rule_type: String,
163 pub criteria: Option<serde_json::Value>,
165 pub enabled: bool,
167 pub severity: Option<String>,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct PolicyThresholds {
174 pub very_high: Option<u32>,
176 pub high: Option<u32>,
178 pub medium: Option<u32>,
180 pub low: Option<u32>,
182 pub very_low: Option<u32>,
184 pub score_threshold: Option<f64>,
186}
187
188#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct PolicyScanRequest {
191 pub application_guid: String,
193 pub policy_guid: String,
195 pub scan_type: ScanType,
197 pub sandbox_guid: Option<String>,
199 pub config: Option<PolicyScanConfig>,
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize)]
205#[serde(rename_all = "lowercase")]
206pub enum ScanType {
207 Static,
209 Dynamic,
211 Sca,
213 Manual,
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct PolicyScanConfig {
220 pub auto_submit: Option<bool>,
222 pub timeout_minutes: Option<u32>,
224 pub include_third_party: Option<bool>,
226 pub modules: Option<Vec<String>>,
228}
229
230#[derive(Debug, Clone, Serialize, Deserialize)]
232pub struct PolicyScanResult {
233 pub scan_id: u64,
235 pub application_guid: String,
237 pub policy_guid: String,
239 pub status: ScanStatus,
241 pub scan_type: ScanType,
243 pub started: DateTime<Utc>,
245 pub completed: Option<DateTime<Utc>>,
247 pub compliance_result: Option<PolicyComplianceResult>,
249 pub findings_summary: Option<FindingsSummary>,
251 pub results_url: Option<String>,
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize)]
257#[serde(rename_all = "UPPERCASE")]
258pub enum ScanStatus {
259 Queued,
261 Running,
263 Completed,
265 Failed,
267 Cancelled,
269 Timeout,
271}
272
273#[derive(Debug, Clone, Serialize, Deserialize)]
275pub struct PolicyComplianceResult {
276 pub status: PolicyComplianceStatus,
278 pub score: Option<f64>,
280 pub passed: bool,
282 pub breakdown: Option<ComplianceBreakdown>,
284 pub violations: Option<Vec<PolicyViolation>>,
286 pub summary: Option<String>,
288}
289
290#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct ComplianceBreakdown {
293 pub very_high: u32,
295 pub high: u32,
297 pub medium: u32,
299 pub low: u32,
301 pub very_low: u32,
303 pub total: u32,
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize)]
309pub struct PolicyViolation {
310 pub violation_type: String,
312 pub severity: String,
314 pub description: String,
316 pub count: u32,
318 pub threshold_exceeded: Option<u32>,
320 pub actual_value: Option<u32>,
322}
323
324#[derive(Debug, Clone, Serialize, Deserialize)]
326pub struct FindingsSummary {
327 pub total: u32,
329 pub open: u32,
331 pub fixed: u32,
333 pub by_severity: HashMap<String, u32>,
335 pub by_category: Option<HashMap<String, u32>>,
337}
338
339#[derive(Debug, Clone, Serialize, Deserialize)]
346pub struct SummaryReport {
347 pub app_id: u64,
349 pub app_name: String,
351 pub build_id: u64,
353 pub policy_compliance_status: String,
355 pub policy_name: String,
357 pub policy_version: u32,
359 pub policy_rules_status: String,
361 pub grace_period_expired: bool,
363 pub scan_overdue: String,
365 pub is_latest_build: bool,
367 pub sandbox_name: Option<String>,
369 pub sandbox_id: Option<u64>,
371 pub generation_date: String,
373 pub last_update_time: String,
375 #[serde(rename = "static-analysis")]
377 pub static_analysis: Option<StaticAnalysisSummary>,
378 #[serde(rename = "flaw-status")]
380 pub flaw_status: Option<FlawStatusSummary>,
381 pub software_composition_analysis: Option<ScaSummary>,
383 pub severity: Option<Vec<SeverityLevel>>,
385}
386
387#[derive(Debug, Clone, Serialize, Deserialize)]
389pub struct StaticAnalysisSummary {
390 pub rating: Option<String>,
392 pub score: Option<u32>,
394 pub mitigated_rating: Option<String>,
396 pub mitigated_score: Option<u32>,
398 pub analysis_size_bytes: Option<u64>,
400 pub engine_version: Option<String>,
402 pub published_date: Option<String>,
404 pub version: Option<String>,
406}
407
408#[derive(Debug, Clone, Serialize, Deserialize)]
410pub struct FlawStatusSummary {
411 pub new: u32,
413 pub reopen: u32,
415 pub open: u32,
417 pub fixed: u32,
419 pub total: u32,
421 pub not_mitigated: u32,
423}
424
425#[derive(Debug, Clone, Serialize, Deserialize)]
427pub struct ScaSummary {
428 pub third_party_components: u32,
430 pub violate_policy: bool,
432 pub components_violated_policy: u32,
434}
435
436#[derive(Debug, Clone, Serialize, Deserialize)]
438pub struct SeverityLevel {
439 pub level: u32,
441 pub category: Vec<CategorySummary>,
443}
444
445#[derive(Debug, Clone, Serialize, Deserialize)]
447pub struct CategorySummary {
448 pub categoryname: String,
450 pub severity: String,
452 pub count: u32,
454}
455
456#[derive(Debug, Clone, Serialize, Deserialize)]
458pub struct ScanFrequencyRule {
459 pub scan_type: String,
461 pub frequency: String,
463}
464
465#[derive(Debug, Clone, Serialize, Deserialize)]
467pub struct FindingRule {
468 #[serde(rename = "type")]
470 pub rule_type: String,
471 pub scan_type: Vec<String>,
473 pub value: String,
475 pub advanced_options: Option<serde_json::Value>,
477}
478
479#[derive(Debug, Clone, Serialize, Deserialize)]
481pub struct FindingRuleAdvancedOptions {
482 pub override_severity: Option<bool>,
484 pub build_action: Option<String>,
486 pub component_dependency: Option<String>,
488 pub vulnerable_methods: Option<String>,
490 pub selected_licenses: Option<Vec<String>>,
492 pub override_severity_level: Option<String>,
494 pub allowed_nonoss_licenses: Option<bool>,
496 pub allowed_unrecognized_licenses: Option<bool>,
498 pub all_licenses_must_meet_requirement: Option<bool>,
500 pub is_blocklist: Option<bool>,
502}
503
504#[derive(Debug, Clone, Default)]
506pub struct PolicyListParams {
507 pub name: Option<String>,
509 pub policy_type: Option<String>,
511 pub is_active: Option<bool>,
513 pub default_only: Option<bool>,
515 pub page: Option<u32>,
517 pub size: Option<u32>,
519}
520
521impl PolicyListParams {
522 #[must_use]
524 pub fn to_query_params(&self) -> Vec<(String, String)> {
525 Vec::from(self) }
527}
528
529impl From<&PolicyListParams> for Vec<(String, String)> {
531 fn from(query: &PolicyListParams) -> Self {
532 let mut params = Vec::new();
533
534 if let Some(ref name) = query.name {
535 params.push(("name".to_string(), name.clone())); }
537 if let Some(ref policy_type) = query.policy_type {
538 params.push(("type".to_string(), policy_type.clone()));
539 }
540 if let Some(is_active) = query.is_active {
541 params.push(("active".to_string(), is_active.to_string()));
542 }
543 if let Some(default_only) = query.default_only {
544 params.push(("default".to_string(), default_only.to_string()));
545 }
546 if let Some(page) = query.page {
547 params.push(("page".to_string(), page.to_string()));
548 }
549 if let Some(size) = query.size {
550 params.push(("size".to_string(), size.to_string()));
551 }
552
553 params
554 }
555}
556
557impl From<PolicyListParams> for Vec<(String, String)> {
558 fn from(query: PolicyListParams) -> Self {
559 let mut params = Vec::new();
560
561 if let Some(name) = query.name {
562 params.push(("name".to_string(), name)); }
564 if let Some(policy_type) = query.policy_type {
565 params.push(("type".to_string(), policy_type)); }
567 if let Some(is_active) = query.is_active {
568 params.push(("active".to_string(), is_active.to_string()));
569 }
570 if let Some(default_only) = query.default_only {
571 params.push(("default".to_string(), default_only.to_string()));
572 }
573 if let Some(page) = query.page {
574 params.push(("page".to_string(), page.to_string()));
575 }
576 if let Some(size) = query.size {
577 params.push(("size".to_string(), size.to_string()));
578 }
579
580 params
581 }
582}
583
584#[derive(Debug, Clone, Serialize, Deserialize)]
586pub struct PolicyListResponse {
587 #[serde(rename = "_embedded")]
588 pub embedded: Option<PolicyEmbedded>,
589 pub page: Option<PageInfo>,
590 #[serde(rename = "_links")]
591 pub links: Option<serde_json::Value>,
592}
593
594#[derive(Debug, Clone, Serialize, Deserialize)]
596pub struct PolicyEmbedded {
597 #[serde(rename = "policy_versions")]
598 pub policy_versions: Vec<SecurityPolicy>,
599}
600
601#[derive(Debug, Clone, Serialize, Deserialize)]
603pub struct PageInfo {
604 pub size: u32,
605 pub number: u32,
606 pub total_elements: u32,
607 pub total_pages: u32,
608}
609
610#[derive(Debug, Clone, PartialEq, Eq)]
612pub enum ApiSource {
613 SummaryReport,
615 BuildInfo,
617}
618
619#[derive(Debug)]
621#[must_use = "Need to handle all error enum types."]
622pub enum PolicyError {
623 Api(VeracodeError),
625 NotFound,
627 InvalidConfig(String),
629 ScanFailed(String),
631 EvaluationError(String),
633 PermissionDenied,
635 Unauthorized,
637 InternalServerError,
639 Timeout,
641}
642
643impl std::fmt::Display for PolicyError {
644 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
645 match self {
646 PolicyError::Api(err) => write!(f, "API error: {err}"),
647 PolicyError::NotFound => write!(f, "Policy not found"),
648 PolicyError::InvalidConfig(msg) => write!(f, "Invalid policy configuration: {msg}"),
649 PolicyError::ScanFailed(msg) => write!(f, "Policy scan failed: {msg}"),
650 PolicyError::EvaluationError(msg) => write!(f, "Policy evaluation error: {msg}"),
651 PolicyError::PermissionDenied => {
652 write!(f, "Insufficient permissions for policy operation")
653 }
654 PolicyError::Unauthorized => {
655 write!(f, "Authentication required - invalid API credentials")
656 }
657 PolicyError::InternalServerError => write!(f, "Internal server error occurred"),
658 PolicyError::Timeout => write!(f, "Policy operation timed out"),
659 }
660 }
661}
662
663impl std::error::Error for PolicyError {}
664
665impl From<VeracodeError> for PolicyError {
666 fn from(err: VeracodeError) -> Self {
667 PolicyError::Api(err)
668 }
669}
670
671impl From<reqwest::Error> for PolicyError {
672 fn from(err: reqwest::Error) -> Self {
673 PolicyError::Api(VeracodeError::Http(err))
674 }
675}
676
677impl From<serde_json::Error> for PolicyError {
678 fn from(err: serde_json::Error) -> Self {
679 PolicyError::Api(VeracodeError::Serialization(err))
680 }
681}
682
683pub struct PolicyApi<'a> {
685 client: &'a VeracodeClient,
686}
687
688impl<'a> PolicyApi<'a> {
689 #[must_use]
696 pub fn new(client: &'a VeracodeClient) -> Self {
697 Self { client }
698 }
699
700 pub async fn list_policies(
715 &self,
716 params: Option<PolicyListParams>,
717 ) -> Result<Vec<SecurityPolicy>, PolicyError> {
718 let endpoint = "/appsec/v1/policies";
719
720 let query_params = params.as_ref().map(Vec::from);
721
722 let response = self.client.get(endpoint, query_params.as_deref()).await?;
723
724 let status = response.status().as_u16();
725 match status {
726 200 => {
727 let policy_response: PolicyListResponse = response.json().await?;
728 let policies = policy_response
729 .embedded
730 .map(|e| e.policy_versions)
731 .unwrap_or_default();
732
733 Ok(policies)
734 }
735 400 => {
736 let error_text = response.text().await.unwrap_or_default();
737 Err(PolicyError::InvalidConfig(error_text))
738 }
739 401 => Err(PolicyError::Unauthorized),
740 403 => Err(PolicyError::PermissionDenied),
741 404 => Err(PolicyError::NotFound),
742 500 => Err(PolicyError::InternalServerError),
743 _ => {
744 let error_text = response.text().await.unwrap_or_default();
745 Err(PolicyError::Api(VeracodeError::InvalidResponse(format!(
746 "HTTP {status}: {error_text}"
747 ))))
748 }
749 }
750 }
751
752 pub async fn get_policy(&self, policy_guid: &str) -> Result<SecurityPolicy, PolicyError> {
767 validation::validate_guid(policy_guid)
769 .map_err(|e| PolicyError::InvalidConfig(format!("Invalid policy GUID: {e}")))?;
770
771 let endpoint = format!("/appsec/v1/policies/{policy_guid}");
772
773 let response = self.client.get(&endpoint, None).await?;
774
775 let status = response.status().as_u16();
776 match status {
777 200 => {
778 let policy: SecurityPolicy = response.json().await?;
779 Ok(policy)
780 }
781 400 => {
782 let error_text = response.text().await.unwrap_or_default();
783 Err(PolicyError::InvalidConfig(error_text))
784 }
785 401 => Err(PolicyError::Unauthorized),
786 403 => Err(PolicyError::PermissionDenied),
787 404 => Err(PolicyError::NotFound),
788 500 => Err(PolicyError::InternalServerError),
789 _ => {
790 let error_text = response.text().await.unwrap_or_default();
791 Err(PolicyError::Api(VeracodeError::InvalidResponse(format!(
792 "HTTP {status}: {error_text}"
793 ))))
794 }
795 }
796 }
797
798 pub async fn get_default_policy(&self) -> Result<SecurityPolicy, PolicyError> {
809 let params = PolicyListParams {
810 default_only: Some(true),
811 ..Default::default()
812 };
813
814 let policies = self.list_policies(Some(params)).await?;
815 policies
818 .into_iter()
819 .find(|p| p.policy_type == "CUSTOMER" && p.organization_id.is_some())
820 .ok_or(PolicyError::NotFound)
821 }
822
823 pub async fn evaluate_policy_compliance_via_buildinfo(
842 &self,
843 app_id: &str,
844 build_id: Option<&str>,
845 sandbox_id: Option<&str>,
846 ) -> Result<std::borrow::Cow<'static, str>, PolicyError> {
847 self.evaluate_policy_compliance_via_buildinfo_with_retry(
848 app_id, build_id, sandbox_id, 30, 10,
849 )
850 .await
851 }
852
853 pub async fn evaluate_policy_compliance_via_buildinfo_with_retry(
875 &self,
876 app_id: &str,
877 build_id: Option<&str>,
878 sandbox_id: Option<&str>,
879 max_retries: u32,
880 retry_delay_seconds: u64,
881 ) -> Result<std::borrow::Cow<'static, str>, PolicyError> {
882 use crate::build::{BuildError, GetBuildInfoRequest};
883 use std::borrow::Cow;
884 use tokio::time::{Duration, sleep};
885
886 let retry_delay_seconds = retry_delay_seconds.min(MAX_RETRY_DELAY_SECONDS);
888 if retry_delay_seconds > MAX_RETRY_DELAY_SECONDS {
889 warn!(
890 "Retry delay capped at {} seconds (requested: {})",
891 MAX_RETRY_DELAY_SECONDS, retry_delay_seconds
892 );
893 }
894
895 let build_request = GetBuildInfoRequest {
896 app_id: app_id.to_string(),
897 build_id: build_id.map(str::to_string), sandbox_id: sandbox_id.map(str::to_string),
899 };
900
901 let mut attempts: u32 = 0;
902 loop {
903 let build_info = self
904 .client
905 .build_api()?
906 .get_build_info(&build_request)
907 .await
908 .map_err(|e| match e {
909 BuildError::BuildNotFound => PolicyError::Api(crate::VeracodeError::InvalidResponse(
910 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.")
911 )),
912 BuildError::ApplicationNotFound => PolicyError::Api(crate::VeracodeError::InvalidResponse(
913 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.")
914 )),
915 BuildError::SandboxNotFound => PolicyError::Api(crate::VeracodeError::InvalidResponse(
916 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"))
917 )),
918 BuildError::Api(api_err) => PolicyError::Api(api_err),
919 BuildError::InvalidParameter(msg)
920 | BuildError::CreationFailed(msg)
921 | BuildError::UpdateFailed(msg)
922 | BuildError::DeletionFailed(msg)
923 | BuildError::XmlParsingError(msg) => {
924 PolicyError::Api(crate::VeracodeError::InvalidResponse(msg))
925 }
926 BuildError::Unauthorized | BuildError::PermissionDenied => PolicyError::Api(
927 crate::VeracodeError::Authentication("Build API access denied".to_string()),
928 ),
929 BuildError::BuildInProgress => {
930 PolicyError::Api(crate::VeracodeError::InvalidResponse(
931 "Build is currently in progress".to_string(),
932 ))
933 }
934 })?;
935
936 let status = build_info
938 .policy_compliance_status
939 .as_deref()
940 .unwrap_or("Not Assessed");
941
942 if status != "Not Assessed" && status != "Calculating..." {
944 return Ok(Cow::Owned(status.to_string()));
945 }
946
947 attempts = attempts.saturating_add(1);
949 if attempts >= max_retries {
950 warn!(
951 "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"
952 );
953 return Ok(Cow::Borrowed("Not Assessed"));
954 }
955
956 info!(
958 "Policy evaluation not yet assessed, retrying in {retry_delay_seconds} seconds... (attempt {attempts}/{max_retries})"
959 );
960
961 sleep(Duration::from_secs(retry_delay_seconds)).await;
963 }
964 }
965
966 #[must_use]
976 pub fn should_break_build(status: &str) -> bool {
977 status == "Did Not Pass"
978 }
979
980 #[must_use]
990 pub fn get_exit_code_for_status(status: &str) -> i32 {
991 if Self::should_break_build(status) {
992 4 } else {
994 0 }
996 }
997
998 pub async fn get_summary_report(
1023 &self,
1024 app_guid: &str,
1025 build_id: Option<&str>,
1026 sandbox_guid: Option<&str>,
1027 ) -> Result<SummaryReport, PolicyError> {
1028 validation::validate_guid(app_guid)
1030 .map_err(|e| PolicyError::InvalidConfig(format!("Invalid application GUID: {e}")))?;
1031
1032 if let Some(build_id) = build_id {
1034 validation::validate_identifier(build_id)
1035 .map_err(|e| PolicyError::InvalidConfig(format!("Invalid build ID: {e}")))?;
1036 }
1037 if let Some(sandbox_guid) = sandbox_guid {
1038 validation::validate_guid(sandbox_guid)
1039 .map_err(|e| PolicyError::InvalidConfig(format!("Invalid sandbox GUID: {e}")))?;
1040 }
1041
1042 let endpoint = format!("/appsec/v2/applications/{app_guid}/summary_report");
1043
1044 let mut query_params = Vec::new();
1046 if let Some(build_id) = build_id {
1047 query_params.push(("build_id".to_string(), build_id.to_string()));
1048 }
1049 if let Some(sandbox_guid) = sandbox_guid {
1050 query_params.push(("context".to_string(), sandbox_guid.to_string()));
1051 }
1052
1053 let response = self.client.get(&endpoint, Some(&query_params)).await?;
1054
1055 let status = response.status().as_u16();
1056 match status {
1057 200 => {
1058 let summary_report: SummaryReport = response.json().await?;
1059 Ok(summary_report)
1060 }
1061 400 => {
1062 let error_text = response.text().await.unwrap_or_default();
1063 Err(PolicyError::InvalidConfig(error_text))
1064 }
1065 401 => Err(PolicyError::Unauthorized),
1066 403 => Err(PolicyError::PermissionDenied),
1067 404 => Err(PolicyError::NotFound),
1068 500 => Err(PolicyError::InternalServerError),
1069 _ => {
1070 let error_text = response.text().await.unwrap_or_default();
1071 Err(PolicyError::Api(VeracodeError::InvalidResponse(format!(
1072 "HTTP {status}: {error_text}"
1073 ))))
1074 }
1075 }
1076 }
1077
1078 #[allow(clippy::too_many_arguments)]
1108 pub async fn get_summary_report_with_policy_retry(
1114 &self,
1115 app_guid: &str,
1116 build_id: Option<&str>,
1117 sandbox_guid: Option<&str>,
1118 max_retries: u32,
1119 retry_delay_seconds: u64,
1120 enable_break_build: bool,
1121 ) -> Result<(SummaryReport, Option<std::borrow::Cow<'static, str>>), PolicyError> {
1122 use std::borrow::Cow;
1123 use tokio::time::{Duration, sleep};
1124
1125 let retry_delay_seconds = retry_delay_seconds.min(MAX_RETRY_DELAY_SECONDS);
1127 if retry_delay_seconds > MAX_RETRY_DELAY_SECONDS {
1128 warn!(
1129 "Retry delay capped at {} seconds (requested: {})",
1130 MAX_RETRY_DELAY_SECONDS, retry_delay_seconds
1131 );
1132 }
1133
1134 if enable_break_build && build_id.is_none() {
1135 return Err(PolicyError::InvalidConfig(
1136 "Build ID is required for break build policy evaluation".to_string(),
1137 ));
1138 }
1139
1140 let mut attempts: u32 = 0;
1141 loop {
1142 if attempts == 0 && enable_break_build {
1143 debug!("Checking policy compliance status with retry logic...");
1144 } else if attempts == 0 {
1145 debug!("Getting summary report...");
1146 }
1147
1148 let summary_report = match self
1149 .get_summary_report(app_guid, build_id, sandbox_guid)
1150 .await
1151 {
1152 Ok(report) => report,
1153 Err(PolicyError::InternalServerError) if attempts < 3 => {
1154 warn!(
1155 "Summary report API failed with server error (attempt {}/3), retrying in 5 seconds...",
1156 attempts.saturating_add(1)
1157 );
1158 sleep(Duration::from_secs(5)).await;
1159 attempts = attempts.saturating_add(1);
1160 continue;
1161 }
1162 Err(e) => return Err(e),
1163 };
1164
1165 if !enable_break_build {
1167 return Ok((summary_report, None));
1168 }
1169
1170 let status = summary_report.policy_compliance_status.clone();
1172
1173 if !status.is_empty() && status != "Not Assessed" {
1175 debug!("Policy compliance status ready: {status}");
1176 return Ok((summary_report, Some(Cow::Owned(status))));
1177 }
1178
1179 attempts = attempts.saturating_add(1);
1181 if attempts >= max_retries {
1182 warn!(
1183 "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"
1184 );
1185 return Ok((summary_report, Some(Cow::Owned(status))));
1186 }
1187
1188 info!(
1190 "Policy evaluation not yet ready (status: '{status}'), retrying in {retry_delay_seconds} seconds... (attempt {attempts}/{max_retries})"
1191 );
1192
1193 sleep(Duration::from_secs(retry_delay_seconds)).await;
1195 }
1196 }
1197
1198 pub async fn evaluate_policy_compliance_via_summary_report_with_retry(
1225 &self,
1226 app_guid: &str,
1227 build_id: &str,
1228 sandbox_guid: Option<&str>,
1229 max_retries: u32,
1230 retry_delay_seconds: u64,
1231 ) -> Result<std::borrow::Cow<'static, str>, PolicyError> {
1232 use std::borrow::Cow;
1233 use tokio::time::{Duration, sleep};
1234
1235 let retry_delay_seconds = retry_delay_seconds.min(MAX_RETRY_DELAY_SECONDS);
1237 if retry_delay_seconds > MAX_RETRY_DELAY_SECONDS {
1238 warn!(
1239 "Retry delay capped at {} seconds (requested: {})",
1240 MAX_RETRY_DELAY_SECONDS, retry_delay_seconds
1241 );
1242 }
1243
1244 let mut attempts: u32 = 0;
1245 loop {
1246 let summary_report = self
1247 .get_summary_report(app_guid, Some(build_id), sandbox_guid)
1248 .await?;
1249
1250 let status = &summary_report.policy_compliance_status;
1253
1254 if !status.is_empty() && status != "Not Assessed" {
1256 return Ok(Cow::Owned(status.clone()));
1257 }
1258
1259 attempts = attempts.saturating_add(1);
1261 if attempts >= max_retries {
1262 warn!(
1263 "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"
1264 );
1265 return Ok(Cow::Owned(status.clone()));
1266 }
1267
1268 info!(
1270 "Policy evaluation not yet ready (status: '{status}'), retrying in {retry_delay_seconds} seconds... (attempt {attempts}/{max_retries})"
1271 );
1272
1273 sleep(Duration::from_secs(retry_delay_seconds)).await;
1275 }
1276 }
1277
1278 pub async fn evaluate_policy_compliance_via_summary_report(
1297 &self,
1298 app_guid: &str,
1299 build_id: &str,
1300 sandbox_guid: Option<&str>,
1301 ) -> Result<std::borrow::Cow<'static, str>, PolicyError> {
1302 self.evaluate_policy_compliance_via_summary_report_with_retry(
1303 app_guid,
1304 build_id,
1305 sandbox_guid,
1306 30,
1307 10,
1308 )
1309 .await
1310 }
1311
1312 pub async fn initiate_policy_scan(
1327 &self,
1328 request: PolicyScanRequest,
1329 ) -> Result<PolicyScanResult, PolicyError> {
1330 validation::validate_guid(&request.application_guid)
1332 .map_err(|e| PolicyError::InvalidConfig(format!("Invalid application GUID: {e}")))?;
1333
1334 validation::validate_guid(&request.policy_guid)
1336 .map_err(|e| PolicyError::InvalidConfig(format!("Invalid policy GUID: {e}")))?;
1337
1338 if let Some(ref sandbox_guid) = request.sandbox_guid {
1340 validation::validate_guid(sandbox_guid)
1341 .map_err(|e| PolicyError::InvalidConfig(format!("Invalid sandbox GUID: {e}")))?;
1342 }
1343
1344 let endpoint = "/appsec/v1/policy-scans";
1345
1346 let response = self.client.post(endpoint, Some(&request)).await?;
1347
1348 let status = response.status().as_u16();
1349 match status {
1350 200 | 201 => {
1351 let scan_result: PolicyScanResult = response.json().await?;
1352 Ok(scan_result)
1353 }
1354 400 => {
1355 let error_text = response.text().await.unwrap_or_default();
1356 Err(PolicyError::InvalidConfig(error_text))
1357 }
1358 404 => Err(PolicyError::NotFound),
1359 _ => {
1360 let error_text = response.text().await.unwrap_or_default();
1361 Err(PolicyError::Api(VeracodeError::InvalidResponse(format!(
1362 "HTTP {status}: {error_text}"
1363 ))))
1364 }
1365 }
1366 }
1367
1368 pub async fn get_policy_scan_result(
1383 &self,
1384 scan_id: u64,
1385 ) -> Result<PolicyScanResult, PolicyError> {
1386 let endpoint = format!("/appsec/v1/policy-scans/{scan_id}");
1387
1388 let response = self.client.get(&endpoint, None).await?;
1389
1390 let status = response.status().as_u16();
1391 match status {
1392 200 => {
1393 let scan_result: PolicyScanResult = response.json().await?;
1394 Ok(scan_result)
1395 }
1396 404 => Err(PolicyError::NotFound),
1397 _ => {
1398 let error_text = response.text().await.unwrap_or_default();
1399 Err(PolicyError::Api(VeracodeError::InvalidResponse(format!(
1400 "HTTP {status}: {error_text}"
1401 ))))
1402 }
1403 }
1404 }
1405
1406 pub async fn is_policy_scan_complete(&self, scan_id: u64) -> Result<bool, PolicyError> {
1421 let scan_result = self.get_policy_scan_result(scan_id).await?;
1422 Ok(matches!(
1423 scan_result.status,
1424 ScanStatus::Completed | ScanStatus::Failed | ScanStatus::Cancelled
1425 ))
1426 }
1427
1428 #[allow(clippy::too_many_arguments)]
1463 pub async fn get_policy_status_with_fallback(
1469 &self,
1470 app_guid: &str,
1471 app_id: &str,
1472 build_id: Option<&str>,
1473 sandbox_guid: Option<&str>,
1474 sandbox_id: Option<&str>,
1475 max_retries: u32,
1476 retry_delay_seconds: u64,
1477 enable_break_build: bool,
1478 force_buildinfo_api: bool,
1479 ) -> Result<(Option<SummaryReport>, String, ApiSource), PolicyError> {
1480 let retry_delay_seconds = retry_delay_seconds.min(MAX_RETRY_DELAY_SECONDS);
1482 if retry_delay_seconds > MAX_RETRY_DELAY_SECONDS {
1483 warn!(
1484 "Retry delay capped at {} seconds (requested: {})",
1485 MAX_RETRY_DELAY_SECONDS, retry_delay_seconds
1486 );
1487 }
1488
1489 if force_buildinfo_api {
1490 debug!("Using getbuildinfo.do API directly (forced via configuration)");
1492 let status = self
1493 .evaluate_policy_compliance_via_buildinfo_with_retry(
1494 app_id,
1495 build_id,
1496 sandbox_id,
1497 max_retries,
1498 retry_delay_seconds,
1499 )
1500 .await?;
1501 return Ok((None, status.to_string(), ApiSource::BuildInfo));
1502 }
1503
1504 match self
1506 .get_summary_report_with_policy_retry(
1507 app_guid,
1508 build_id,
1509 sandbox_guid,
1510 max_retries,
1511 retry_delay_seconds,
1512 enable_break_build,
1513 )
1514 .await
1515 {
1516 Ok((summary_report, compliance_status)) => {
1517 debug!("Used summary report API successfully");
1518 let status = compliance_status
1519 .map(|s| s.to_string())
1520 .unwrap_or_else(|| summary_report.policy_compliance_status.clone());
1521 Ok((Some(summary_report), status, ApiSource::SummaryReport))
1522 }
1523 Err(
1524 ref e @ (PolicyError::Unauthorized
1525 | PolicyError::PermissionDenied
1526 | PolicyError::InternalServerError),
1527 ) => {
1528 match *e {
1529 PolicyError::InternalServerError => info!(
1530 "Summary report API server error, falling back to getbuildinfo.do API"
1531 ),
1532 PolicyError::Unauthorized | PolicyError::PermissionDenied => {
1533 info!("Summary report access denied, falling back to getbuildinfo.do API")
1534 }
1535 PolicyError::Api(_)
1536 | PolicyError::NotFound
1537 | PolicyError::InvalidConfig(_)
1538 | PolicyError::ScanFailed(_)
1539 | PolicyError::EvaluationError(_)
1540 | PolicyError::Timeout => {}
1541 }
1542 let status = self
1543 .evaluate_policy_compliance_via_buildinfo_with_retry(
1544 app_id,
1545 build_id,
1546 sandbox_id,
1547 max_retries,
1548 retry_delay_seconds,
1549 )
1550 .await?;
1551 Ok((None, status.to_string(), ApiSource::BuildInfo))
1552 }
1553 Err(e) => Err(e),
1554 }
1555 }
1556
1557 pub async fn get_active_policies(&self) -> Result<Vec<SecurityPolicy>, PolicyError> {
1568 let policies = self.list_policies(None).await?;
1571 Ok(policies) }
1573}
1574
1575#[cfg(test)]
1576#[allow(clippy::expect_used)]
1577mod tests {
1578 use super::*;
1579
1580 #[test]
1581 fn test_policy_list_params_to_query() {
1582 let params = PolicyListParams {
1583 name: Some("test-policy".to_string()),
1584 is_active: Some(true),
1585 page: Some(1),
1586 size: Some(10),
1587 ..Default::default()
1588 };
1589
1590 let query_params: Vec<_> = params.into();
1591 assert_eq!(query_params.len(), 4);
1592 assert!(query_params.contains(&("name".to_string(), "test-policy".to_string())));
1593 assert!(query_params.contains(&("active".to_string(), "true".to_string())));
1594 assert!(query_params.contains(&("page".to_string(), "1".to_string())));
1595 assert!(query_params.contains(&("size".to_string(), "10".to_string())));
1596 }
1597
1598 #[test]
1599 fn test_policy_error_display() {
1600 let error = PolicyError::NotFound;
1601 assert_eq!(error.to_string(), "Policy not found");
1602
1603 let error = PolicyError::InvalidConfig("test".to_string());
1604 assert_eq!(error.to_string(), "Invalid policy configuration: test");
1605
1606 let error = PolicyError::Timeout;
1607 assert_eq!(error.to_string(), "Policy operation timed out");
1608 }
1609
1610 #[test]
1611 fn test_scan_type_serialization() {
1612 let scan_type = ScanType::Static;
1613 let json = serde_json::to_string(&scan_type).expect("should serialize to json");
1614 assert_eq!(json, "\"static\"");
1615
1616 let deserialized: ScanType = serde_json::from_str(&json).expect("should deserialize json");
1617 assert!(matches!(deserialized, ScanType::Static));
1618 }
1619
1620 #[test]
1621 fn test_policy_compliance_status_serialization() {
1622 let status = PolicyComplianceStatus::Passed;
1623 let json = serde_json::to_string(&status).expect("should serialize to json");
1624 assert_eq!(json, "\"Passed\"");
1625
1626 let deserialized: PolicyComplianceStatus =
1627 serde_json::from_str(&json).expect("should deserialize json");
1628 assert!(matches!(deserialized, PolicyComplianceStatus::Passed));
1629
1630 let conditional_pass = PolicyComplianceStatus::ConditionalPass;
1632 let json = serde_json::to_string(&conditional_pass).expect("should serialize to json");
1633 assert_eq!(json, "\"Conditional Pass\"");
1634
1635 let did_not_pass = PolicyComplianceStatus::DidNotPass;
1636 let json = serde_json::to_string(&did_not_pass).expect("should serialize to json");
1637 assert_eq!(json, "\"Did Not Pass\"");
1638 }
1639
1640 #[test]
1641 fn test_break_build_logic() {
1642 assert!(PolicyApi::should_break_build("Did Not Pass"));
1643 assert!(!PolicyApi::should_break_build("Passed"));
1644 assert!(!PolicyApi::should_break_build("Conditional Pass"));
1645 assert!(!PolicyApi::should_break_build("Not Assessed"));
1648
1649 assert_eq!(PolicyApi::get_exit_code_for_status("Did Not Pass"), 4);
1650 assert_eq!(PolicyApi::get_exit_code_for_status("Passed"), 0);
1651 assert_eq!(PolicyApi::get_exit_code_for_status("Conditional Pass"), 0);
1652 assert_eq!(PolicyApi::get_exit_code_for_status("Not Assessed"), 0);
1655 }
1656
1657 #[test]
1658 fn test_summary_report_serialization() {
1659 let summary_json = r#"{
1660 "app_id": 2676517,
1661 "app_name": "Verascan Java Test",
1662 "build_id": 54209787,
1663 "policy_compliance_status": "Did Not Pass",
1664 "policy_name": "SecureCode Policy",
1665 "policy_version": 1,
1666 "policy_rules_status": "Did Not Pass",
1667 "grace_period_expired": false,
1668 "scan_overdue": "false",
1669 "is_latest_build": false,
1670 "generation_date": "2025-08-05 10:14:45 UTC",
1671 "last_update_time": "2025-08-05 10:00:51 UTC"
1672 }"#;
1673
1674 let summary: Result<SummaryReport, _> = serde_json::from_str(summary_json);
1675 assert!(summary.is_ok());
1676
1677 let summary = summary.expect("should have summary");
1678 assert_eq!(summary.policy_compliance_status, "Did Not Pass");
1679 assert_eq!(summary.app_name, "Verascan Java Test");
1680 assert_eq!(summary.build_id, 54209787);
1681 assert!(PolicyApi::should_break_build(
1682 &summary.policy_compliance_status
1683 ));
1684 }
1685
1686 #[test]
1687 fn test_export_json_structure() {
1688 let summary_report = SummaryReport {
1690 app_id: 2676517,
1691 app_name: "Test App".to_string(),
1692 build_id: 54209787,
1693 policy_compliance_status: "Passed".to_string(),
1694 policy_name: "Test Policy".to_string(),
1695 policy_version: 1,
1696 policy_rules_status: "Passed".to_string(),
1697 grace_period_expired: false,
1698 scan_overdue: "false".to_string(),
1699 is_latest_build: true,
1700 sandbox_name: Some("test-sandbox".to_string()),
1701 sandbox_id: Some(123456),
1702 generation_date: "2025-08-05 10:14:45 UTC".to_string(),
1703 last_update_time: "2025-08-05 10:00:51 UTC".to_string(),
1704 static_analysis: None,
1705 flaw_status: None,
1706 software_composition_analysis: None,
1707 severity: None,
1708 };
1709
1710 let export_json = serde_json::json!({
1711 "summary_report": summary_report,
1712 "export_metadata": {
1713 "exported_at": "2025-08-05T10:14:45Z",
1714 "tool": "verascan",
1715 "export_type": "summary_report",
1716 "scan_configuration": {
1717 "autoscan": true,
1718 "scan_all_nonfatal_top_level_modules": true,
1719 "include_new_modules": true
1720 }
1721 }
1722 });
1723
1724 assert!(
1726 export_json
1727 .get("summary_report")
1728 .and_then(|s| s.get("app_name"))
1729 .map(|v| v.is_string())
1730 .unwrap_or(false)
1731 );
1732 assert!(
1733 export_json
1734 .get("summary_report")
1735 .and_then(|s| s.get("policy_compliance_status"))
1736 .map(|v| v.is_string())
1737 .unwrap_or(false)
1738 );
1739 assert!(
1740 export_json
1741 .get("export_metadata")
1742 .and_then(|e| e.get("export_type"))
1743 .map(|v| v.is_string())
1744 .unwrap_or(false)
1745 );
1746 assert_eq!(
1747 export_json
1748 .get("export_metadata")
1749 .and_then(|e| e.get("export_type"))
1750 .and_then(|v| v.as_str())
1751 .expect("should have export_type"),
1752 "summary_report"
1753 );
1754
1755 let json_string =
1757 serde_json::to_string_pretty(&export_json).expect("should serialize to json");
1758 assert!(json_string.contains("summary_report"));
1759 assert!(json_string.contains("export_metadata"));
1760 }
1761
1762 #[test]
1763 fn test_get_summary_report_with_policy_retry_parameters() {
1764 let app_guid = "test-app-guid";
1768 let build_id = Some("test-build-id");
1769 let sandbox_guid: Option<&str> = None;
1770 let max_retries = 30u32;
1771 let retry_delay_seconds = 10u64;
1772 let debug = false;
1773 let enable_break_build = true;
1774
1775 assert_eq!(app_guid, "test-app-guid");
1777 assert_eq!(build_id, Some("test-build-id"));
1778 assert_eq!(sandbox_guid, None);
1779 assert_eq!(max_retries, 30);
1780 assert_eq!(retry_delay_seconds, 10);
1781 assert!(!debug);
1782 assert!(enable_break_build);
1783 }
1784
1785 #[test]
1786 fn test_policy_status_ready_logic() {
1787 let ready_statuses = vec!["Passed", "Did Not Pass", "Conditional Pass"];
1789 let not_ready_statuses = vec!["", "Not Assessed"];
1790
1791 for status in &ready_statuses {
1793 assert!(
1794 !status.is_empty(),
1795 "Ready status should not be empty: {status}"
1796 );
1797 assert_ne!(
1798 *status, "Not Assessed",
1799 "Ready status should not be 'Not Assessed': {status}"
1800 );
1801 }
1802
1803 for status in ¬_ready_statuses {
1805 let is_not_ready = status.is_empty() || *status == "Not Assessed";
1806 assert!(is_not_ready, "Status should trigger retry: '{status}'");
1807 }
1808 }
1809
1810 #[test]
1811 fn test_combined_method_return_types() {
1812 use std::borrow::Cow;
1813
1814 let compliance_status = Cow::Borrowed("Passed");
1819 assert_eq!(compliance_status.as_ref(), "Passed");
1820
1821 let compliance_status: Option<Cow<'static, str>> = None;
1823 assert!(compliance_status.is_none());
1824 }
1825
1826 #[test]
1827 fn test_debug_logging_parameters() {
1828 let debug_enabled = true;
1830 let debug_disabled = false;
1831
1832 assert!(debug_enabled);
1833 assert!(!debug_disabled);
1834
1835 if debug_enabled {
1838 }
1840
1841 if !debug_disabled {
1842 }
1844 }
1845
1846 #[test]
1847 fn test_break_build_flag_logic() {
1848 let break_build_enabled = true;
1850 let break_build_disabled = false;
1851
1852 if break_build_enabled {
1854 let compliance_returned = true;
1856 assert!(compliance_returned);
1857 }
1858
1859 if !break_build_disabled {
1861 let compliance_returned = false;
1863 assert!(!compliance_returned);
1864 }
1865 }
1866}
1867
1868#[cfg(test)]
1870#[allow(clippy::expect_used)]
1871mod validation_proptests {
1872 use super::validation::*;
1873 use proptest::prelude::*;
1874
1875 fn valid_guid_strategy() -> impl Strategy<Value = String> {
1877 prop_oneof![
1878 prop::string::string_regex(
1880 "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"
1881 )
1882 .expect("valid regex for UUID"),
1883 prop::string::string_regex("[0-9a-fA-F]{32}").expect("valid regex for hex string"),
1885 ]
1886 }
1887
1888 fn invalid_guid_strategy() -> impl Strategy<Value = String> {
1890 prop_oneof![
1891 Just("../../../etc/passwd".to_string()),
1893 Just("..\\..\\windows\\system32".to_string()),
1894 prop::string::string_regex("[0-9a-f]{8}\\.\\./{0,20}[0-9a-f]{8}")
1895 .expect("valid regex for path traversal with guid"),
1896 Just("abc123?param=value".to_string()),
1898 Just("abc123&admin=true".to_string()),
1899 Just("abc123#fragment".to_string()),
1900 prop::string::string_regex("[0-9a-zA-Z!@#$%^&*()]{32}")
1902 .expect("valid regex for non-hex chars"),
1903 prop::string::string_regex("[0-9a-f]{1,31}").expect("valid regex for too short"),
1905 prop::string::string_regex("[0-9a-f]{33,100}").expect("valid regex for too long"),
1906 Just("abc123'; DROP TABLE users; --".to_string()),
1908 Just("abc123; rm -rf /".to_string()),
1910 Just("abc123 | cat /etc/passwd".to_string()),
1911 Just("abc123\0malicious".to_string()),
1913 ]
1914 }
1915
1916 fn valid_identifier_strategy() -> impl Strategy<Value = String> {
1918 prop::string::string_regex("[a-zA-Z0-9_-]{1,256}").expect("valid regex for identifier")
1919 }
1920
1921 fn invalid_identifier_strategy() -> impl Strategy<Value = String> {
1923 prop_oneof![
1924 Just("".to_string()),
1926 Just("../etc/passwd".to_string()),
1928 Just("..\\windows\\system32".to_string()),
1929 Just("test?param=value".to_string()),
1931 Just("test&admin=true".to_string()),
1932 Just("test#fragment".to_string()),
1933 prop::string::string_regex(
1935 "[a-zA-Z0-9]{1,10}[@#$%^&*()+=\\[\\]{}|;:'\"<>,./\\\\?]+[a-zA-Z0-9]{0,10}"
1936 )
1937 .expect("valid regex for special chars"),
1938 Just("test'; DROP TABLE users; --".to_string()),
1940 Just("test; rm -rf /".to_string()),
1942 Just("test\u{0000}injection".to_string()),
1944 Just("test\u{001F}control".to_string()),
1945 ]
1946 }
1947
1948 proptest! {
1949 #![proptest_config(ProptestConfig {
1950 cases: if cfg!(miri) { 5 } else { 1000 },
1951 failure_persistence: None,
1952 .. ProptestConfig::default()
1953 })]
1954
1955 #[test]
1956 fn proptest_valid_guids_accepted(guid in valid_guid_strategy()) {
1957 prop_assert!(validate_guid(&guid).is_ok(),
1958 "Valid GUID rejected: {}", guid);
1959 }
1960
1961 #[test]
1962 fn proptest_invalid_guids_rejected(guid in invalid_guid_strategy()) {
1963 prop_assert!(validate_guid(&guid).is_err(),
1964 "Invalid GUID accepted: {}", guid);
1965 }
1966
1967 #[test]
1968 fn proptest_guid_no_path_traversal(
1969 prefix in prop::string::string_regex("[0-9a-f]{8}").expect("valid regex for guid prefix")
1970 ) {
1971 let with_traversal = format!("{}/../../../etc/passwd", prefix);
1972 prop_assert!(validate_guid(&with_traversal).is_err(),
1973 "Path traversal GUID accepted: {}", with_traversal);
1974
1975 let with_backslash = format!("{}\\..\\windows", prefix);
1976 prop_assert!(validate_guid(&with_backslash).is_err(),
1977 "Backslash traversal GUID accepted: {}", with_backslash);
1978 }
1979
1980 #[test]
1981 fn proptest_guid_no_url_injection(
1982 prefix in prop::string::string_regex("[0-9a-f]{16}").expect("valid regex for guid prefix")
1983 ) {
1984 let with_query = format!("{}?admin=true", prefix);
1985 prop_assert!(validate_guid(&with_query).is_err(),
1986 "URL query injection accepted: {}", with_query);
1987
1988 let with_ampersand = format!("{}¶m=value", prefix);
1989 prop_assert!(validate_guid(&with_ampersand).is_err(),
1990 "URL parameter injection accepted: {}", with_ampersand);
1991
1992 let with_fragment = format!("{}#section", prefix);
1993 prop_assert!(validate_guid(&with_fragment).is_err(),
1994 "URL fragment injection accepted: {}", with_fragment);
1995 }
1996
1997 #[test]
1998 fn proptest_valid_identifiers_accepted(id in valid_identifier_strategy()) {
1999 prop_assert!(validate_identifier(&id).is_ok(),
2000 "Valid identifier rejected: {}", id);
2001 }
2002
2003 #[test]
2004 fn proptest_invalid_identifiers_rejected(id in invalid_identifier_strategy()) {
2005 prop_assert!(validate_identifier(&id).is_err(),
2006 "Invalid identifier accepted: {}", id);
2007 }
2008
2009 #[test]
2010 fn proptest_identifier_no_path_traversal(
2011 base in prop::string::string_regex("[a-zA-Z0-9]{5,10}").expect("valid regex for base id")
2012 ) {
2013 let with_dots = format!("{}/../test", base);
2014 prop_assert!(validate_identifier(&with_dots).is_err(),
2015 "Path traversal identifier accepted: {}", with_dots);
2016
2017 let with_slashes = format!("{}/etc/passwd", base);
2018 prop_assert!(validate_identifier(&with_slashes).is_err(),
2019 "Forward slash identifier accepted: {}", with_slashes);
2020 }
2021
2022 #[test]
2023 fn proptest_identifier_no_url_injection(
2024 base in prop::string::string_regex("[a-zA-Z0-9_-]{5,20}").expect("valid regex for base id")
2025 ) {
2026 let with_query = format!("{}?param=value", base);
2027 prop_assert!(validate_identifier(&with_query).is_err(),
2028 "URL query injection in identifier accepted: {}", with_query);
2029
2030 let with_ampersand = format!("{}&admin=true", base);
2031 prop_assert!(validate_identifier(&with_ampersand).is_err(),
2032 "Ampersand injection in identifier accepted: {}", with_ampersand);
2033 }
2034
2035 #[test]
2036 fn proptest_identifier_no_special_chars(
2037 alphanumeric in prop::string::string_regex("[a-zA-Z0-9]{3,10}").expect("valid regex for alphanumeric"),
2038 special_char in "[!@#$%^&*()+=\\[\\]{}|;:'\"<>,./\\\\?]"
2039 ) {
2040 let with_special = format!("{}{}{}", alphanumeric, special_char, alphanumeric);
2041 prop_assert!(validate_identifier(&with_special).is_err(),
2042 "Identifier with special char accepted: {}", with_special);
2043 }
2044 }
2045}
2046
2047#[cfg(test)]
2049#[allow(clippy::expect_used)]
2050mod query_param_proptests {
2051 use super::*;
2052 use proptest::prelude::*;
2053
2054 proptest! {
2055 #![proptest_config(ProptestConfig {
2056 cases: if cfg!(miri) { 5 } else { 1000 },
2057 failure_persistence: None,
2058 .. ProptestConfig::default()
2059 })]
2060
2061 #[test]
2062 fn proptest_policy_list_params_no_duplicate_keys(
2063 name in prop::option::of(prop::string::string_regex("[a-zA-Z0-9 _-]{1,50}").expect("valid regex for policy name")),
2064 policy_type in prop::option::of(prop::string::string_regex("[A-Z]{1,20}").expect("valid regex for policy type")),
2065 is_active in prop::option::of(any::<bool>()),
2066 default_only in prop::option::of(any::<bool>()),
2067 page in prop::option::of(0u32..10000u32),
2068 size in prop::option::of(1u32..1000u32)
2069 ) {
2070 let params = PolicyListParams {
2071 name,
2072 policy_type,
2073 is_active,
2074 default_only,
2075 page,
2076 size,
2077 };
2078
2079 let query_params = params.to_query_params();
2080
2081 let mut seen_keys = std::collections::HashSet::new();
2083 for (key, _) in query_params.iter() {
2084 prop_assert!(!seen_keys.contains(key),
2085 "Duplicate query parameter key: {}", key);
2086 seen_keys.insert(key.clone());
2087 }
2088 }
2089
2090 #[test]
2091 fn proptest_policy_list_params_valid_values(
2092 page in 0u32..10000u32,
2093 size in 1u32..1000u32
2094 ) {
2095 let params = PolicyListParams {
2096 name: None,
2097 policy_type: None,
2098 is_active: Some(true),
2099 default_only: Some(false),
2100 page: Some(page),
2101 size: Some(size),
2102 };
2103
2104 let query_params = params.to_query_params();
2105
2106 let page_param = query_params.iter().find(|(k, _)| k == "page");
2108 let size_param = query_params.iter().find(|(k, _)| k == "size");
2109
2110 if let Some((_, page_value)) = page_param {
2111 prop_assert!(page_value.parse::<u32>().is_ok(),
2112 "Invalid page value: {}", page_value);
2113 }
2114
2115 if let Some((_, size_value)) = size_param {
2116 prop_assert!(size_value.parse::<u32>().is_ok(),
2117 "Invalid size value: {}", size_value);
2118 }
2119 }
2120
2121 #[test]
2122 fn proptest_policy_list_params_string_sanitization(
2123 name in prop::string::string_regex("[a-zA-Z0-9 &=;?#]{1,100}").expect("valid regex for name with special chars")
2124 ) {
2125 let params = PolicyListParams {
2126 name: Some(name.clone()),
2127 policy_type: None,
2128 is_active: None,
2129 default_only: None,
2130 page: None,
2131 size: None,
2132 };
2133
2134 let query_params = params.to_query_params();
2135
2136 let name_param = query_params.iter().find(|(k, _)| k == "name");
2138
2139 if let Some((_, value)) = name_param {
2140 prop_assert_eq!(value, &name);
2142 }
2143 }
2144 }
2145}
2146
2147#[cfg(test)]
2149#[allow(clippy::expect_used)]
2150mod integer_safety_proptests {
2151 use super::*;
2152 use proptest::prelude::*;
2153
2154 proptest! {
2155 #![proptest_config(ProptestConfig {
2156 cases: if cfg!(miri) { 5 } else { 1000 },
2157 failure_persistence: None,
2158 .. ProptestConfig::default()
2159 })]
2160
2161 #[test]
2162 fn proptest_retry_delay_capped(delay in 0u64..u64::MAX) {
2163 let capped = delay.min(MAX_RETRY_DELAY_SECONDS);
2164 prop_assert!(capped <= MAX_RETRY_DELAY_SECONDS,
2165 "Retry delay not properly capped: {}", capped);
2166 prop_assert!(capped <= 300,
2167 "Retry delay exceeds 5 minutes: {}", capped);
2168 }
2169
2170 #[test]
2171 fn proptest_retry_attempts_no_overflow(attempts in 0u32..u32::MAX - 1) {
2172 let incremented = attempts.saturating_add(1);
2173 prop_assert!(incremented >= attempts,
2174 "Retry counter overflowed: {} + 1 = {}", attempts, incremented);
2175 }
2177
2178 #[test]
2179 fn proptest_max_retries_reasonable(max_retries in 0u32..1000u32) {
2180 let test_attempts = max_retries.saturating_add(1);
2182 prop_assert!(test_attempts > max_retries || max_retries == u32::MAX,
2183 "Max retries comparison could overflow");
2184 }
2185
2186 #[test]
2187 fn proptest_retry_delay_multiplication_safe(
2188 retries in 0u32..100u32,
2189 delay in 0u64..MAX_RETRY_DELAY_SECONDS
2190 ) {
2191 let total_delay = (retries as u64).saturating_mul(delay);
2193 prop_assert!(total_delay <= 86400,
2195 "Total delay unreasonably large: {} seconds", total_delay);
2196 }
2197 }
2198}
2199
2200#[cfg(test)]
2202#[allow(clippy::expect_used)]
2203mod string_safety_proptests {
2204 use super::*;
2205 use proptest::prelude::*;
2206
2207 proptest! {
2208 #![proptest_config(ProptestConfig {
2209 cases: if cfg!(miri) { 5 } else { 1000 },
2210 failure_persistence: None,
2211 .. ProptestConfig::default()
2212 })]
2213
2214 #[test]
2215 fn proptest_policy_status_string_utf8_safe(
2216 status in prop::string::string_regex("[ -~]{1,50}").expect("valid regex for ASCII status")
2217 ) {
2218 prop_assert!(status.is_ascii() || status.chars().all(|c| !c.is_control()),
2220 "Status string contains control characters");
2221 }
2222
2223 #[test]
2224 fn proptest_guid_formatting_safe(
2225 guid in prop::string::string_regex("[0-9a-f]{32}").expect("valid regex for guid")
2226 ) {
2227 let endpoint = format!("/appsec/v1/policies/{}", guid);
2228
2229 prop_assert!(!endpoint.contains("{}"),
2231 "Format string injection in endpoint: {}", endpoint);
2232 prop_assert!(!endpoint.contains("%s"),
2233 "Printf-style injection in endpoint: {}", endpoint);
2234
2235 prop_assert!(endpoint.starts_with("/appsec/v1/policies/"),
2237 "Malformed endpoint structure: {}", endpoint);
2238 #[allow(clippy::arithmetic_side_effects)]
2239 {
2240 prop_assert_eq!(endpoint.len(), 20 + guid.len(),
2241 "Unexpected endpoint length");
2242 }
2243 }
2244
2245 #[test]
2246 fn proptest_error_message_no_injection(
2247 user_input in prop::string::string_regex("[ -~]{1,100}").expect("valid regex for user input")
2248 ) {
2249 let error_msg = format!("Invalid GUID: {}", user_input);
2250
2251 prop_assert!(error_msg.starts_with("Invalid GUID: "),
2253 "Error message structure corrupted");
2254 prop_assert!(!error_msg.contains('\0'),
2255 "Null byte in error message");
2256 prop_assert!(error_msg.len() >= 14,
2257 "Error message unexpectedly short");
2258 }
2259
2260 #[test]
2261 fn proptest_compliance_status_values_safe(
2262 status in prop_oneof![
2263 Just("Passed".to_string()),
2264 Just("Did Not Pass".to_string()),
2265 Just("Conditional Pass".to_string()),
2266 Just("Not Assessed".to_string()),
2267 Just("Calculating...".to_string()),
2268 ]
2269 ) {
2270 prop_assert!(status.chars().all(|c| c.is_alphanumeric() || c.is_whitespace() || c == '.'),
2272 "Status contains unexpected characters: {}", status);
2273
2274 let should_break = PolicyApi::should_break_build(&status);
2276 if status == "Did Not Pass" {
2277 prop_assert!(should_break, "Did Not Pass should break build");
2278 } else {
2279 prop_assert!(!should_break, "{} should not break build", status);
2280 }
2281 }
2282 }
2283}
2284
2285#[cfg(test)]
2287#[allow(clippy::expect_used)]
2288mod endpoint_safety_proptests {
2289 use proptest::prelude::*;
2290
2291 proptest! {
2292 #![proptest_config(ProptestConfig {
2293 cases: if cfg!(miri) { 5 } else { 1000 },
2294 failure_persistence: None,
2295 .. ProptestConfig::default()
2296 })]
2297
2298 #[test]
2299 fn proptest_scan_id_endpoint_no_injection(scan_id in 0u64..u64::MAX) {
2300 let endpoint = format!("/appsec/v1/policy-scans/{}", scan_id);
2301
2302 prop_assert!(endpoint.starts_with("/appsec/v1/policy-scans/"),
2304 "Endpoint prefix corrupted: {}", endpoint);
2305 prop_assert!(!endpoint.contains(".."),
2306 "Path traversal in endpoint: {}", endpoint);
2307 prop_assert!(!endpoint.contains("//"),
2308 "Double slash in endpoint: {}", endpoint);
2309
2310 let scan_id_str = endpoint.get(24..).unwrap_or("");
2312 prop_assert!(scan_id_str.parse::<u64>().is_ok(),
2313 "Invalid scan_id in endpoint: {}", scan_id_str);
2314 }
2315
2316 #[test]
2317 fn proptest_app_guid_endpoint_validated(
2318 guid_part in prop::string::string_regex("[0-9a-f]{32}").expect("valid regex for guid")
2319 ) {
2320 let endpoint = format!("/appsec/v2/applications/{}/summary_report", guid_part);
2322
2323 prop_assert!(endpoint.starts_with("/appsec/v2/applications/"),
2325 "Invalid endpoint prefix");
2326 prop_assert!(endpoint.ends_with("/summary_report"),
2327 "Invalid endpoint suffix");
2328 prop_assert!(!endpoint.contains(".."),
2329 "Path traversal in endpoint");
2330 }
2331
2332 #[test]
2333 fn proptest_query_string_no_injection(
2334 build_id in prop::string::string_regex("[a-zA-Z0-9_-]{1,64}").expect("valid regex for build id"),
2335 sandbox_guid in prop::string::string_regex("[0-9a-f]{32}").expect("valid regex for sandbox guid")
2336 ) {
2337 let query_params = [
2339 ("build_id".to_string(), build_id.clone()),
2340 ("context".to_string(), sandbox_guid.clone())
2341 ];
2342
2343 let keys: Vec<_> = query_params.iter().map(|(k, _)| k).collect();
2345 prop_assert_eq!(keys.len(), 2, "Wrong number of query params");
2346 prop_assert_eq!(keys.first().map(|s| s.as_str()), Some("build_id"), "Wrong first key");
2347 prop_assert_eq!(keys.get(1).map(|s| s.as_str()), Some("context"), "Wrong second key");
2348
2349 if let Some((_, val)) = query_params.first() {
2351 prop_assert_eq!(val, &build_id, "build_id value corrupted");
2352 }
2353 if let Some((_, val)) = query_params.get(1) {
2354 prop_assert_eq!(val, &sandbox_guid, "sandbox_guid value corrupted");
2355 }
2356 }
2357 }
2358}
2359
2360#[cfg(test)]
2362#[allow(clippy::expect_used)]
2363mod memory_safety_proptests {
2364 use super::*;
2365 use proptest::prelude::*;
2366
2367 proptest! {
2368 #![proptest_config(ProptestConfig {
2369 cases: if cfg!(miri) { 5 } else { 1000 },
2370 failure_persistence: None,
2371 .. ProptestConfig::default()
2372 })]
2373
2374 #[test]
2375 fn proptest_policy_list_params_from_owned(
2376 name in prop::option::of(prop::string::string_regex("[a-zA-Z0-9_-]{1,50}").expect("valid regex for name")),
2377 page in prop::option::of(0u32..1000u32),
2378 size in prop::option::of(1u32..100u32)
2379 ) {
2380 let params = PolicyListParams {
2381 name: name.clone(),
2382 policy_type: None,
2383 is_active: None,
2384 default_only: None,
2385 page,
2386 size,
2387 };
2388
2389 let query_params: Vec<(String, String)> = params.into();
2391
2392 prop_assert!(query_params.len() <= 6, "Too many query params");
2394
2395 if name.is_some() {
2397 let has_name = query_params.iter().any(|(k, _)| k == "name");
2398 prop_assert!(has_name, "Name parameter lost after move");
2399 }
2400 }
2401
2402 #[test]
2403 fn proptest_policy_list_params_from_ref(
2404 name in prop::option::of(prop::string::string_regex("[a-zA-Z0-9_-]{1,50}").expect("valid regex for name")),
2405 page in prop::option::of(0u32..1000u32)
2406 ) {
2407 let params = PolicyListParams {
2408 name: name.clone(),
2409 policy_type: None,
2410 is_active: None,
2411 default_only: None,
2412 page,
2413 size: None,
2414 };
2415
2416 let query_params: Vec<(String, String)> = Vec::from(¶ms);
2418
2419 let query_params2 = params.to_query_params();
2421
2422 prop_assert_eq!(query_params, query_params2,
2424 "Reference conversion differs from method call");
2425 }
2426
2427 #[test]
2428 fn proptest_vec_allocation_reasonable(
2429 param_count in 1usize..10usize
2430 ) {
2431 let mut params = Vec::new();
2432
2433 for i in 0..param_count {
2434 params.push((format!("key{}", i), format!("value{}", i)));
2435 }
2436
2437 prop_assert_eq!(params.len(), param_count,
2439 "Parameter count mismatch");
2440 prop_assert!(params.capacity() >= param_count,
2441 "Insufficient capacity");
2442 prop_assert!(params.capacity() < param_count.saturating_mul(10),
2444 "Excessive capacity: {} for {} items", params.capacity(), param_count);
2445 }
2446 }
2447}