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);
950 if attempts >= max_retries {
951 warn!(
952 "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"
953 );
954 return Err(PolicyError::Timeout);
955 }
956
957 info!(
959 "Policy evaluation not yet assessed, retrying in {retry_delay_seconds} seconds... (attempt {attempts}/{max_retries})"
960 );
961
962 sleep(Duration::from_secs(retry_delay_seconds)).await;
964 }
965 }
966
967 #[must_use]
977 pub fn should_break_build(status: &str) -> bool {
978 status == "Did Not Pass"
979 }
980
981 #[must_use]
991 pub fn get_exit_code_for_status(status: &str) -> i32 {
992 if Self::should_break_build(status) {
993 4 } else {
995 0 }
997 }
998
999 pub async fn get_summary_report(
1024 &self,
1025 app_guid: &str,
1026 build_id: Option<&str>,
1027 sandbox_guid: Option<&str>,
1028 ) -> Result<SummaryReport, PolicyError> {
1029 validation::validate_guid(app_guid)
1031 .map_err(|e| PolicyError::InvalidConfig(format!("Invalid application GUID: {e}")))?;
1032
1033 if let Some(build_id) = build_id {
1035 validation::validate_identifier(build_id)
1036 .map_err(|e| PolicyError::InvalidConfig(format!("Invalid build ID: {e}")))?;
1037 }
1038 if let Some(sandbox_guid) = sandbox_guid {
1039 validation::validate_guid(sandbox_guid)
1040 .map_err(|e| PolicyError::InvalidConfig(format!("Invalid sandbox GUID: {e}")))?;
1041 }
1042
1043 let endpoint = format!("/appsec/v2/applications/{app_guid}/summary_report");
1044
1045 let mut query_params = Vec::new();
1047 if let Some(build_id) = build_id {
1048 query_params.push(("build_id".to_string(), build_id.to_string()));
1049 }
1050 if let Some(sandbox_guid) = sandbox_guid {
1051 query_params.push(("context".to_string(), sandbox_guid.to_string()));
1052 }
1053
1054 let response = self.client.get(&endpoint, Some(&query_params)).await?;
1055
1056 let status = response.status().as_u16();
1057 match status {
1058 200 => {
1059 let summary_report: SummaryReport = response.json().await?;
1060 Ok(summary_report)
1061 }
1062 400 => {
1063 let error_text = response.text().await.unwrap_or_default();
1064 Err(PolicyError::InvalidConfig(error_text))
1065 }
1066 401 => Err(PolicyError::Unauthorized),
1067 403 => Err(PolicyError::PermissionDenied),
1068 404 => Err(PolicyError::NotFound),
1069 500 => Err(PolicyError::InternalServerError),
1070 _ => {
1071 let error_text = response.text().await.unwrap_or_default();
1072 Err(PolicyError::Api(VeracodeError::InvalidResponse(format!(
1073 "HTTP {status}: {error_text}"
1074 ))))
1075 }
1076 }
1077 }
1078
1079 #[allow(clippy::too_many_arguments)]
1109 pub async fn get_summary_report_with_policy_retry(
1115 &self,
1116 app_guid: &str,
1117 build_id: Option<&str>,
1118 sandbox_guid: Option<&str>,
1119 max_retries: u32,
1120 retry_delay_seconds: u64,
1121 enable_break_build: bool,
1122 ) -> Result<(SummaryReport, Option<std::borrow::Cow<'static, str>>), PolicyError> {
1123 use std::borrow::Cow;
1124 use tokio::time::{Duration, sleep};
1125
1126 let retry_delay_seconds = retry_delay_seconds.min(MAX_RETRY_DELAY_SECONDS);
1128 if retry_delay_seconds > MAX_RETRY_DELAY_SECONDS {
1129 warn!(
1130 "Retry delay capped at {} seconds (requested: {})",
1131 MAX_RETRY_DELAY_SECONDS, retry_delay_seconds
1132 );
1133 }
1134
1135 if enable_break_build && build_id.is_none() {
1136 return Err(PolicyError::InvalidConfig(
1137 "Build ID is required for break build policy evaluation".to_string(),
1138 ));
1139 }
1140
1141 let mut attempts: u32 = 0;
1142 loop {
1143 if attempts == 0 && enable_break_build {
1144 debug!("Checking policy compliance status with retry logic...");
1145 } else if attempts == 0 {
1146 debug!("Getting summary report...");
1147 }
1148
1149 let summary_report = match self
1150 .get_summary_report(app_guid, build_id, sandbox_guid)
1151 .await
1152 {
1153 Ok(report) => report,
1154 Err(PolicyError::InternalServerError) if attempts < 3 => {
1155 warn!(
1156 "Summary report API failed with server error (attempt {}/3), retrying in 5 seconds...",
1157 attempts.saturating_add(1)
1158 );
1159 sleep(Duration::from_secs(5)).await;
1160 attempts = attempts.saturating_add(1);
1161 continue;
1162 }
1163 Err(e) => return Err(e),
1164 };
1165
1166 if !enable_break_build {
1168 return Ok((summary_report, None));
1169 }
1170
1171 let status = summary_report.policy_compliance_status.clone();
1173
1174 if !status.is_empty() && status != "Not Assessed" {
1176 debug!("Policy compliance status ready: {status}");
1177 return Ok((summary_report, Some(Cow::Owned(status))));
1178 }
1179
1180 attempts = attempts.saturating_add(1);
1182 if attempts >= max_retries {
1183 warn!(
1184 "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"
1185 );
1186 return Ok((summary_report, Some(Cow::Owned(status))));
1187 }
1188
1189 info!(
1191 "Policy evaluation not yet ready (status: '{status}'), retrying in {retry_delay_seconds} seconds... (attempt {attempts}/{max_retries})"
1192 );
1193
1194 sleep(Duration::from_secs(retry_delay_seconds)).await;
1196 }
1197 }
1198
1199 pub async fn evaluate_policy_compliance_via_summary_report_with_retry(
1226 &self,
1227 app_guid: &str,
1228 build_id: &str,
1229 sandbox_guid: Option<&str>,
1230 max_retries: u32,
1231 retry_delay_seconds: u64,
1232 ) -> Result<std::borrow::Cow<'static, str>, PolicyError> {
1233 use std::borrow::Cow;
1234 use tokio::time::{Duration, sleep};
1235
1236 let retry_delay_seconds = retry_delay_seconds.min(MAX_RETRY_DELAY_SECONDS);
1238 if retry_delay_seconds > MAX_RETRY_DELAY_SECONDS {
1239 warn!(
1240 "Retry delay capped at {} seconds (requested: {})",
1241 MAX_RETRY_DELAY_SECONDS, retry_delay_seconds
1242 );
1243 }
1244
1245 let mut attempts: u32 = 0;
1246 loop {
1247 let summary_report = self
1248 .get_summary_report(app_guid, Some(build_id), sandbox_guid)
1249 .await?;
1250
1251 let status = &summary_report.policy_compliance_status;
1254
1255 if !status.is_empty() && status != "Not Assessed" {
1257 return Ok(Cow::Owned(status.clone()));
1258 }
1259
1260 attempts = attempts.saturating_add(1);
1262 if attempts >= max_retries {
1263 warn!(
1264 "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"
1265 );
1266 return Ok(Cow::Owned(status.clone()));
1267 }
1268
1269 info!(
1271 "Policy evaluation not yet ready (status: '{status}'), retrying in {retry_delay_seconds} seconds... (attempt {attempts}/{max_retries})"
1272 );
1273
1274 sleep(Duration::from_secs(retry_delay_seconds)).await;
1276 }
1277 }
1278
1279 pub async fn evaluate_policy_compliance_via_summary_report(
1298 &self,
1299 app_guid: &str,
1300 build_id: &str,
1301 sandbox_guid: Option<&str>,
1302 ) -> Result<std::borrow::Cow<'static, str>, PolicyError> {
1303 self.evaluate_policy_compliance_via_summary_report_with_retry(
1304 app_guid,
1305 build_id,
1306 sandbox_guid,
1307 30,
1308 10,
1309 )
1310 .await
1311 }
1312
1313 pub async fn initiate_policy_scan(
1328 &self,
1329 request: PolicyScanRequest,
1330 ) -> Result<PolicyScanResult, PolicyError> {
1331 validation::validate_guid(&request.application_guid)
1333 .map_err(|e| PolicyError::InvalidConfig(format!("Invalid application GUID: {e}")))?;
1334
1335 validation::validate_guid(&request.policy_guid)
1337 .map_err(|e| PolicyError::InvalidConfig(format!("Invalid policy GUID: {e}")))?;
1338
1339 if let Some(ref sandbox_guid) = request.sandbox_guid {
1341 validation::validate_guid(sandbox_guid)
1342 .map_err(|e| PolicyError::InvalidConfig(format!("Invalid sandbox GUID: {e}")))?;
1343 }
1344
1345 let endpoint = "/appsec/v1/policy-scans";
1346
1347 let response = self.client.post(endpoint, Some(&request)).await?;
1348
1349 let status = response.status().as_u16();
1350 match status {
1351 200 | 201 => {
1352 let scan_result: PolicyScanResult = response.json().await?;
1353 Ok(scan_result)
1354 }
1355 400 => {
1356 let error_text = response.text().await.unwrap_or_default();
1357 Err(PolicyError::InvalidConfig(error_text))
1358 }
1359 404 => Err(PolicyError::NotFound),
1360 _ => {
1361 let error_text = response.text().await.unwrap_or_default();
1362 Err(PolicyError::Api(VeracodeError::InvalidResponse(format!(
1363 "HTTP {status}: {error_text}"
1364 ))))
1365 }
1366 }
1367 }
1368
1369 pub async fn get_policy_scan_result(
1384 &self,
1385 scan_id: u64,
1386 ) -> Result<PolicyScanResult, PolicyError> {
1387 let endpoint = format!("/appsec/v1/policy-scans/{scan_id}");
1388
1389 let response = self.client.get(&endpoint, None).await?;
1390
1391 let status = response.status().as_u16();
1392 match status {
1393 200 => {
1394 let scan_result: PolicyScanResult = response.json().await?;
1395 Ok(scan_result)
1396 }
1397 404 => Err(PolicyError::NotFound),
1398 _ => {
1399 let error_text = response.text().await.unwrap_or_default();
1400 Err(PolicyError::Api(VeracodeError::InvalidResponse(format!(
1401 "HTTP {status}: {error_text}"
1402 ))))
1403 }
1404 }
1405 }
1406
1407 pub async fn is_policy_scan_complete(&self, scan_id: u64) -> Result<bool, PolicyError> {
1422 let scan_result = self.get_policy_scan_result(scan_id).await?;
1423 Ok(matches!(
1424 scan_result.status,
1425 ScanStatus::Completed | ScanStatus::Failed | ScanStatus::Cancelled
1426 ))
1427 }
1428
1429 #[allow(clippy::too_many_arguments)]
1464 pub async fn get_policy_status_with_fallback(
1470 &self,
1471 app_guid: &str,
1472 app_id: &str,
1473 build_id: Option<&str>,
1474 sandbox_guid: Option<&str>,
1475 sandbox_id: Option<&str>,
1476 max_retries: u32,
1477 retry_delay_seconds: u64,
1478 enable_break_build: bool,
1479 force_buildinfo_api: bool,
1480 ) -> Result<(Option<SummaryReport>, String, ApiSource), PolicyError> {
1481 let retry_delay_seconds = retry_delay_seconds.min(MAX_RETRY_DELAY_SECONDS);
1483 if retry_delay_seconds > MAX_RETRY_DELAY_SECONDS {
1484 warn!(
1485 "Retry delay capped at {} seconds (requested: {})",
1486 MAX_RETRY_DELAY_SECONDS, retry_delay_seconds
1487 );
1488 }
1489
1490 if force_buildinfo_api {
1491 debug!("Using getbuildinfo.do API directly (forced via configuration)");
1493 let status = self
1494 .evaluate_policy_compliance_via_buildinfo_with_retry(
1495 app_id,
1496 build_id,
1497 sandbox_id,
1498 max_retries,
1499 retry_delay_seconds,
1500 )
1501 .await?;
1502 return Ok((None, status.to_string(), ApiSource::BuildInfo));
1503 }
1504
1505 match self
1507 .get_summary_report_with_policy_retry(
1508 app_guid,
1509 build_id,
1510 sandbox_guid,
1511 max_retries,
1512 retry_delay_seconds,
1513 enable_break_build,
1514 )
1515 .await
1516 {
1517 Ok((summary_report, compliance_status)) => {
1518 debug!("Used summary report API successfully");
1519 let status = compliance_status
1520 .map(|s| s.to_string())
1521 .unwrap_or_else(|| summary_report.policy_compliance_status.clone());
1522 Ok((Some(summary_report), status, ApiSource::SummaryReport))
1523 }
1524 Err(
1525 ref e @ (PolicyError::Unauthorized
1526 | PolicyError::PermissionDenied
1527 | PolicyError::InternalServerError),
1528 ) => {
1529 match *e {
1530 PolicyError::InternalServerError => info!(
1531 "Summary report API server error, falling back to getbuildinfo.do API"
1532 ),
1533 PolicyError::Unauthorized | PolicyError::PermissionDenied => {
1534 info!("Summary report access denied, falling back to getbuildinfo.do API")
1535 }
1536 PolicyError::Api(_)
1537 | PolicyError::NotFound
1538 | PolicyError::InvalidConfig(_)
1539 | PolicyError::ScanFailed(_)
1540 | PolicyError::EvaluationError(_)
1541 | PolicyError::Timeout => {}
1542 }
1543 let status = self
1544 .evaluate_policy_compliance_via_buildinfo_with_retry(
1545 app_id,
1546 build_id,
1547 sandbox_id,
1548 max_retries,
1549 retry_delay_seconds,
1550 )
1551 .await?;
1552 Ok((None, status.to_string(), ApiSource::BuildInfo))
1553 }
1554 Err(e) => Err(e),
1555 }
1556 }
1557
1558 pub async fn get_active_policies(&self) -> Result<Vec<SecurityPolicy>, PolicyError> {
1569 let policies = self.list_policies(None).await?;
1572 Ok(policies) }
1574}
1575
1576#[cfg(test)]
1577#[allow(clippy::expect_used)]
1578mod tests {
1579 use super::*;
1580
1581 #[test]
1582 fn test_policy_list_params_to_query() {
1583 let params = PolicyListParams {
1584 name: Some("test-policy".to_string()),
1585 is_active: Some(true),
1586 page: Some(1),
1587 size: Some(10),
1588 ..Default::default()
1589 };
1590
1591 let query_params: Vec<_> = params.into();
1592 assert_eq!(query_params.len(), 4);
1593 assert!(query_params.contains(&("name".to_string(), "test-policy".to_string())));
1594 assert!(query_params.contains(&("active".to_string(), "true".to_string())));
1595 assert!(query_params.contains(&("page".to_string(), "1".to_string())));
1596 assert!(query_params.contains(&("size".to_string(), "10".to_string())));
1597 }
1598
1599 #[test]
1600 fn test_policy_error_display() {
1601 let error = PolicyError::NotFound;
1602 assert_eq!(error.to_string(), "Policy not found");
1603
1604 let error = PolicyError::InvalidConfig("test".to_string());
1605 assert_eq!(error.to_string(), "Invalid policy configuration: test");
1606
1607 let error = PolicyError::Timeout;
1608 assert_eq!(error.to_string(), "Policy operation timed out");
1609 }
1610
1611 #[test]
1612 fn test_scan_type_serialization() {
1613 let scan_type = ScanType::Static;
1614 let json = serde_json::to_string(&scan_type).expect("should serialize to json");
1615 assert_eq!(json, "\"static\"");
1616
1617 let deserialized: ScanType = serde_json::from_str(&json).expect("should deserialize json");
1618 assert!(matches!(deserialized, ScanType::Static));
1619 }
1620
1621 #[test]
1622 fn test_policy_compliance_status_serialization() {
1623 let status = PolicyComplianceStatus::Passed;
1624 let json = serde_json::to_string(&status).expect("should serialize to json");
1625 assert_eq!(json, "\"Passed\"");
1626
1627 let deserialized: PolicyComplianceStatus =
1628 serde_json::from_str(&json).expect("should deserialize json");
1629 assert!(matches!(deserialized, PolicyComplianceStatus::Passed));
1630
1631 let conditional_pass = PolicyComplianceStatus::ConditionalPass;
1633 let json = serde_json::to_string(&conditional_pass).expect("should serialize to json");
1634 assert_eq!(json, "\"Conditional Pass\"");
1635
1636 let did_not_pass = PolicyComplianceStatus::DidNotPass;
1637 let json = serde_json::to_string(&did_not_pass).expect("should serialize to json");
1638 assert_eq!(json, "\"Did Not Pass\"");
1639 }
1640
1641 #[test]
1642 fn test_break_build_logic() {
1643 assert!(PolicyApi::should_break_build("Did Not Pass"));
1644 assert!(!PolicyApi::should_break_build("Passed"));
1645 assert!(!PolicyApi::should_break_build("Conditional Pass"));
1646 assert!(!PolicyApi::should_break_build("Not Assessed"));
1649
1650 assert_eq!(PolicyApi::get_exit_code_for_status("Did Not Pass"), 4);
1651 assert_eq!(PolicyApi::get_exit_code_for_status("Passed"), 0);
1652 assert_eq!(PolicyApi::get_exit_code_for_status("Conditional Pass"), 0);
1653 assert_eq!(PolicyApi::get_exit_code_for_status("Not Assessed"), 0);
1656 }
1657
1658 #[test]
1659 fn test_summary_report_serialization() {
1660 let summary_json = r#"{
1661 "app_id": 2676517,
1662 "app_name": "Verascan Java Test",
1663 "build_id": 54209787,
1664 "policy_compliance_status": "Did Not Pass",
1665 "policy_name": "SecureCode Policy",
1666 "policy_version": 1,
1667 "policy_rules_status": "Did Not Pass",
1668 "grace_period_expired": false,
1669 "scan_overdue": "false",
1670 "is_latest_build": false,
1671 "generation_date": "2025-08-05 10:14:45 UTC",
1672 "last_update_time": "2025-08-05 10:00:51 UTC"
1673 }"#;
1674
1675 let summary: Result<SummaryReport, _> = serde_json::from_str(summary_json);
1676 assert!(summary.is_ok());
1677
1678 let summary = summary.expect("should have summary");
1679 assert_eq!(summary.policy_compliance_status, "Did Not Pass");
1680 assert_eq!(summary.app_name, "Verascan Java Test");
1681 assert_eq!(summary.build_id, 54209787);
1682 assert!(PolicyApi::should_break_build(
1683 &summary.policy_compliance_status
1684 ));
1685 }
1686
1687 #[test]
1688 fn test_export_json_structure() {
1689 let summary_report = SummaryReport {
1691 app_id: 2676517,
1692 app_name: "Test App".to_string(),
1693 build_id: 54209787,
1694 policy_compliance_status: "Passed".to_string(),
1695 policy_name: "Test Policy".to_string(),
1696 policy_version: 1,
1697 policy_rules_status: "Passed".to_string(),
1698 grace_period_expired: false,
1699 scan_overdue: "false".to_string(),
1700 is_latest_build: true,
1701 sandbox_name: Some("test-sandbox".to_string()),
1702 sandbox_id: Some(123456),
1703 generation_date: "2025-08-05 10:14:45 UTC".to_string(),
1704 last_update_time: "2025-08-05 10:00:51 UTC".to_string(),
1705 static_analysis: None,
1706 flaw_status: None,
1707 software_composition_analysis: None,
1708 severity: None,
1709 };
1710
1711 let export_json = serde_json::json!({
1712 "summary_report": summary_report,
1713 "export_metadata": {
1714 "exported_at": "2025-08-05T10:14:45Z",
1715 "tool": "verascan",
1716 "export_type": "summary_report",
1717 "scan_configuration": {
1718 "autoscan": true,
1719 "scan_all_nonfatal_top_level_modules": true,
1720 "include_new_modules": true
1721 }
1722 }
1723 });
1724
1725 assert!(
1727 export_json
1728 .get("summary_report")
1729 .and_then(|s| s.get("app_name"))
1730 .map(|v| v.is_string())
1731 .unwrap_or(false)
1732 );
1733 assert!(
1734 export_json
1735 .get("summary_report")
1736 .and_then(|s| s.get("policy_compliance_status"))
1737 .map(|v| v.is_string())
1738 .unwrap_or(false)
1739 );
1740 assert!(
1741 export_json
1742 .get("export_metadata")
1743 .and_then(|e| e.get("export_type"))
1744 .map(|v| v.is_string())
1745 .unwrap_or(false)
1746 );
1747 assert_eq!(
1748 export_json
1749 .get("export_metadata")
1750 .and_then(|e| e.get("export_type"))
1751 .and_then(|v| v.as_str())
1752 .expect("should have export_type"),
1753 "summary_report"
1754 );
1755
1756 let json_string =
1758 serde_json::to_string_pretty(&export_json).expect("should serialize to json");
1759 assert!(json_string.contains("summary_report"));
1760 assert!(json_string.contains("export_metadata"));
1761 }
1762
1763 #[test]
1764 fn test_get_summary_report_with_policy_retry_parameters() {
1765 let app_guid = "test-app-guid";
1769 let build_id = Some("test-build-id");
1770 let sandbox_guid: Option<&str> = None;
1771 let max_retries = 30u32;
1772 let retry_delay_seconds = 10u64;
1773 let debug = false;
1774 let enable_break_build = true;
1775
1776 assert_eq!(app_guid, "test-app-guid");
1778 assert_eq!(build_id, Some("test-build-id"));
1779 assert_eq!(sandbox_guid, None);
1780 assert_eq!(max_retries, 30);
1781 assert_eq!(retry_delay_seconds, 10);
1782 assert!(!debug);
1783 assert!(enable_break_build);
1784 }
1785
1786 #[test]
1787 fn test_policy_status_ready_logic() {
1788 let ready_statuses = vec!["Passed", "Did Not Pass", "Conditional Pass"];
1790 let not_ready_statuses = vec!["", "Not Assessed"];
1791
1792 for status in &ready_statuses {
1794 assert!(
1795 !status.is_empty(),
1796 "Ready status should not be empty: {status}"
1797 );
1798 assert_ne!(
1799 *status, "Not Assessed",
1800 "Ready status should not be 'Not Assessed': {status}"
1801 );
1802 }
1803
1804 for status in ¬_ready_statuses {
1806 let is_not_ready = status.is_empty() || *status == "Not Assessed";
1807 assert!(is_not_ready, "Status should trigger retry: '{status}'");
1808 }
1809 }
1810
1811 #[test]
1812 fn test_combined_method_return_types() {
1813 use std::borrow::Cow;
1814
1815 let compliance_status = Cow::Borrowed("Passed");
1820 assert_eq!(compliance_status.as_ref(), "Passed");
1821
1822 let compliance_status: Option<Cow<'static, str>> = None;
1824 assert!(compliance_status.is_none());
1825 }
1826
1827 #[test]
1828 fn test_debug_logging_parameters() {
1829 let debug_enabled = true;
1831 let debug_disabled = false;
1832
1833 assert!(debug_enabled);
1834 assert!(!debug_disabled);
1835
1836 if debug_enabled {
1839 }
1841
1842 if !debug_disabled {
1843 }
1845 }
1846
1847 #[test]
1848 fn test_break_build_flag_logic() {
1849 let break_build_enabled = true;
1851 let break_build_disabled = false;
1852
1853 if break_build_enabled {
1855 let compliance_returned = true;
1857 assert!(compliance_returned);
1858 }
1859
1860 if !break_build_disabled {
1862 let compliance_returned = false;
1864 assert!(!compliance_returned);
1865 }
1866 }
1867}
1868
1869#[cfg(test)]
1871#[allow(clippy::expect_used)]
1872mod validation_proptests {
1873 use super::validation::*;
1874 use proptest::prelude::*;
1875
1876 fn valid_guid_strategy() -> impl Strategy<Value = String> {
1878 prop_oneof![
1879 prop::string::string_regex(
1881 "[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}"
1882 )
1883 .expect("valid regex for UUID"),
1884 prop::string::string_regex("[0-9a-fA-F]{32}").expect("valid regex for hex string"),
1886 ]
1887 }
1888
1889 fn invalid_guid_strategy() -> impl Strategy<Value = String> {
1891 prop_oneof![
1892 Just("../../../etc/passwd".to_string()),
1894 Just("..\\..\\windows\\system32".to_string()),
1895 prop::string::string_regex("[0-9a-f]{8}\\.\\./{0,20}[0-9a-f]{8}")
1896 .expect("valid regex for path traversal with guid"),
1897 Just("abc123?param=value".to_string()),
1899 Just("abc123&admin=true".to_string()),
1900 Just("abc123#fragment".to_string()),
1901 prop::string::string_regex("[0-9a-zA-Z!@#$%^&*()]{32}")
1903 .expect("valid regex for non-hex chars"),
1904 prop::string::string_regex("[0-9a-f]{1,31}").expect("valid regex for too short"),
1906 prop::string::string_regex("[0-9a-f]{33,100}").expect("valid regex for too long"),
1907 Just("abc123'; DROP TABLE users; --".to_string()),
1909 Just("abc123; rm -rf /".to_string()),
1911 Just("abc123 | cat /etc/passwd".to_string()),
1912 Just("abc123\0malicious".to_string()),
1914 ]
1915 }
1916
1917 fn valid_identifier_strategy() -> impl Strategy<Value = String> {
1919 prop::string::string_regex("[a-zA-Z0-9_-]{1,256}").expect("valid regex for identifier")
1920 }
1921
1922 fn invalid_identifier_strategy() -> impl Strategy<Value = String> {
1924 prop_oneof![
1925 Just("".to_string()),
1927 Just("../etc/passwd".to_string()),
1929 Just("..\\windows\\system32".to_string()),
1930 Just("test?param=value".to_string()),
1932 Just("test&admin=true".to_string()),
1933 Just("test#fragment".to_string()),
1934 prop::string::string_regex(
1936 "[a-zA-Z0-9]{1,10}[@#$%^&*()+=\\[\\]{}|;:'\"<>,./\\\\?]+[a-zA-Z0-9]{0,10}"
1937 )
1938 .expect("valid regex for special chars"),
1939 Just("test'; DROP TABLE users; --".to_string()),
1941 Just("test; rm -rf /".to_string()),
1943 Just("test\u{0000}injection".to_string()),
1945 Just("test\u{001F}control".to_string()),
1946 ]
1947 }
1948
1949 proptest! {
1950 #![proptest_config(ProptestConfig {
1951 cases: if cfg!(miri) { 5 } else { 1000 },
1952 failure_persistence: None,
1953 .. ProptestConfig::default()
1954 })]
1955
1956 #[test]
1957 fn proptest_valid_guids_accepted(guid in valid_guid_strategy()) {
1958 prop_assert!(validate_guid(&guid).is_ok(),
1959 "Valid GUID rejected: {}", guid);
1960 }
1961
1962 #[test]
1963 fn proptest_invalid_guids_rejected(guid in invalid_guid_strategy()) {
1964 prop_assert!(validate_guid(&guid).is_err(),
1965 "Invalid GUID accepted: {}", guid);
1966 }
1967
1968 #[test]
1969 fn proptest_guid_no_path_traversal(
1970 prefix in prop::string::string_regex("[0-9a-f]{8}").expect("valid regex for guid prefix")
1971 ) {
1972 let with_traversal = format!("{}/../../../etc/passwd", prefix);
1973 prop_assert!(validate_guid(&with_traversal).is_err(),
1974 "Path traversal GUID accepted: {}", with_traversal);
1975
1976 let with_backslash = format!("{}\\..\\windows", prefix);
1977 prop_assert!(validate_guid(&with_backslash).is_err(),
1978 "Backslash traversal GUID accepted: {}", with_backslash);
1979 }
1980
1981 #[test]
1982 fn proptest_guid_no_url_injection(
1983 prefix in prop::string::string_regex("[0-9a-f]{16}").expect("valid regex for guid prefix")
1984 ) {
1985 let with_query = format!("{}?admin=true", prefix);
1986 prop_assert!(validate_guid(&with_query).is_err(),
1987 "URL query injection accepted: {}", with_query);
1988
1989 let with_ampersand = format!("{}¶m=value", prefix);
1990 prop_assert!(validate_guid(&with_ampersand).is_err(),
1991 "URL parameter injection accepted: {}", with_ampersand);
1992
1993 let with_fragment = format!("{}#section", prefix);
1994 prop_assert!(validate_guid(&with_fragment).is_err(),
1995 "URL fragment injection accepted: {}", with_fragment);
1996 }
1997
1998 #[test]
1999 fn proptest_valid_identifiers_accepted(id in valid_identifier_strategy()) {
2000 prop_assert!(validate_identifier(&id).is_ok(),
2001 "Valid identifier rejected: {}", id);
2002 }
2003
2004 #[test]
2005 fn proptest_invalid_identifiers_rejected(id in invalid_identifier_strategy()) {
2006 prop_assert!(validate_identifier(&id).is_err(),
2007 "Invalid identifier accepted: {}", id);
2008 }
2009
2010 #[test]
2011 fn proptest_identifier_no_path_traversal(
2012 base in prop::string::string_regex("[a-zA-Z0-9]{5,10}").expect("valid regex for base id")
2013 ) {
2014 let with_dots = format!("{}/../test", base);
2015 prop_assert!(validate_identifier(&with_dots).is_err(),
2016 "Path traversal identifier accepted: {}", with_dots);
2017
2018 let with_slashes = format!("{}/etc/passwd", base);
2019 prop_assert!(validate_identifier(&with_slashes).is_err(),
2020 "Forward slash identifier accepted: {}", with_slashes);
2021 }
2022
2023 #[test]
2024 fn proptest_identifier_no_url_injection(
2025 base in prop::string::string_regex("[a-zA-Z0-9_-]{5,20}").expect("valid regex for base id")
2026 ) {
2027 let with_query = format!("{}?param=value", base);
2028 prop_assert!(validate_identifier(&with_query).is_err(),
2029 "URL query injection in identifier accepted: {}", with_query);
2030
2031 let with_ampersand = format!("{}&admin=true", base);
2032 prop_assert!(validate_identifier(&with_ampersand).is_err(),
2033 "Ampersand injection in identifier accepted: {}", with_ampersand);
2034 }
2035
2036 #[test]
2037 fn proptest_identifier_no_special_chars(
2038 alphanumeric in prop::string::string_regex("[a-zA-Z0-9]{3,10}").expect("valid regex for alphanumeric"),
2039 special_char in "[!@#$%^&*()+=\\[\\]{}|;:'\"<>,./\\\\?]"
2040 ) {
2041 let with_special = format!("{}{}{}", alphanumeric, special_char, alphanumeric);
2042 prop_assert!(validate_identifier(&with_special).is_err(),
2043 "Identifier with special char accepted: {}", with_special);
2044 }
2045 }
2046}
2047
2048#[cfg(test)]
2050#[allow(clippy::expect_used)]
2051mod query_param_proptests {
2052 use super::*;
2053 use proptest::prelude::*;
2054
2055 proptest! {
2056 #![proptest_config(ProptestConfig {
2057 cases: if cfg!(miri) { 5 } else { 1000 },
2058 failure_persistence: None,
2059 .. ProptestConfig::default()
2060 })]
2061
2062 #[test]
2063 fn proptest_policy_list_params_no_duplicate_keys(
2064 name in prop::option::of(prop::string::string_regex("[a-zA-Z0-9 _-]{1,50}").expect("valid regex for policy name")),
2065 policy_type in prop::option::of(prop::string::string_regex("[A-Z]{1,20}").expect("valid regex for policy type")),
2066 is_active in prop::option::of(any::<bool>()),
2067 default_only in prop::option::of(any::<bool>()),
2068 page in prop::option::of(0u32..10000u32),
2069 size in prop::option::of(1u32..1000u32)
2070 ) {
2071 let params = PolicyListParams {
2072 name,
2073 policy_type,
2074 is_active,
2075 default_only,
2076 page,
2077 size,
2078 };
2079
2080 let query_params = params.to_query_params();
2081
2082 let mut seen_keys = std::collections::HashSet::new();
2084 for (key, _) in query_params.iter() {
2085 prop_assert!(!seen_keys.contains(key),
2086 "Duplicate query parameter key: {}", key);
2087 seen_keys.insert(key.clone());
2088 }
2089 }
2090
2091 #[test]
2092 fn proptest_policy_list_params_valid_values(
2093 page in 0u32..10000u32,
2094 size in 1u32..1000u32
2095 ) {
2096 let params = PolicyListParams {
2097 name: None,
2098 policy_type: None,
2099 is_active: Some(true),
2100 default_only: Some(false),
2101 page: Some(page),
2102 size: Some(size),
2103 };
2104
2105 let query_params = params.to_query_params();
2106
2107 let page_param = query_params.iter().find(|(k, _)| k == "page");
2109 let size_param = query_params.iter().find(|(k, _)| k == "size");
2110
2111 if let Some((_, page_value)) = page_param {
2112 prop_assert!(page_value.parse::<u32>().is_ok(),
2113 "Invalid page value: {}", page_value);
2114 }
2115
2116 if let Some((_, size_value)) = size_param {
2117 prop_assert!(size_value.parse::<u32>().is_ok(),
2118 "Invalid size value: {}", size_value);
2119 }
2120 }
2121
2122 #[test]
2123 fn proptest_policy_list_params_string_sanitization(
2124 name in prop::string::string_regex("[a-zA-Z0-9 &=;?#]{1,100}").expect("valid regex for name with special chars")
2125 ) {
2126 let params = PolicyListParams {
2127 name: Some(name.clone()),
2128 policy_type: None,
2129 is_active: None,
2130 default_only: None,
2131 page: None,
2132 size: None,
2133 };
2134
2135 let query_params = params.to_query_params();
2136
2137 let name_param = query_params.iter().find(|(k, _)| k == "name");
2139
2140 if let Some((_, value)) = name_param {
2141 prop_assert_eq!(value, &name);
2143 }
2144 }
2145 }
2146}
2147
2148#[cfg(test)]
2150#[allow(clippy::expect_used)]
2151mod integer_safety_proptests {
2152 use super::*;
2153 use proptest::prelude::*;
2154
2155 proptest! {
2156 #![proptest_config(ProptestConfig {
2157 cases: if cfg!(miri) { 5 } else { 1000 },
2158 failure_persistence: None,
2159 .. ProptestConfig::default()
2160 })]
2161
2162 #[test]
2163 fn proptest_retry_delay_capped(delay in 0u64..u64::MAX) {
2164 let capped = delay.min(MAX_RETRY_DELAY_SECONDS);
2165 prop_assert!(capped <= MAX_RETRY_DELAY_SECONDS,
2166 "Retry delay not properly capped: {}", capped);
2167 prop_assert!(capped <= 300,
2168 "Retry delay exceeds 5 minutes: {}", capped);
2169 }
2170
2171 #[test]
2172 fn proptest_retry_attempts_no_overflow(attempts in 0u32..u32::MAX - 1) {
2173 let incremented = attempts.saturating_add(1);
2174 prop_assert!(incremented >= attempts,
2175 "Retry counter overflowed: {} + 1 = {}", attempts, incremented);
2176 }
2178
2179 #[test]
2180 fn proptest_max_retries_reasonable(max_retries in 0u32..1000u32) {
2181 let test_attempts = max_retries.saturating_add(1);
2183 prop_assert!(test_attempts > max_retries || max_retries == u32::MAX,
2184 "Max retries comparison could overflow");
2185 }
2186
2187 #[test]
2188 fn proptest_retry_delay_multiplication_safe(
2189 retries in 0u32..100u32,
2190 delay in 0u64..MAX_RETRY_DELAY_SECONDS
2191 ) {
2192 let total_delay = (retries as u64).saturating_mul(delay);
2194 prop_assert!(total_delay <= 86400,
2196 "Total delay unreasonably large: {} seconds", total_delay);
2197 }
2198 }
2199}
2200
2201#[cfg(test)]
2203#[allow(clippy::expect_used)]
2204mod string_safety_proptests {
2205 use super::*;
2206 use proptest::prelude::*;
2207
2208 proptest! {
2209 #![proptest_config(ProptestConfig {
2210 cases: if cfg!(miri) { 5 } else { 1000 },
2211 failure_persistence: None,
2212 .. ProptestConfig::default()
2213 })]
2214
2215 #[test]
2216 fn proptest_policy_status_string_utf8_safe(
2217 status in prop::string::string_regex("[ -~]{1,50}").expect("valid regex for ASCII status")
2218 ) {
2219 prop_assert!(status.is_ascii() || status.chars().all(|c| !c.is_control()),
2221 "Status string contains control characters");
2222 }
2223
2224 #[test]
2225 fn proptest_guid_formatting_safe(
2226 guid in prop::string::string_regex("[0-9a-f]{32}").expect("valid regex for guid")
2227 ) {
2228 let endpoint = format!("/appsec/v1/policies/{}", guid);
2229
2230 prop_assert!(!endpoint.contains("{}"),
2232 "Format string injection in endpoint: {}", endpoint);
2233 prop_assert!(!endpoint.contains("%s"),
2234 "Printf-style injection in endpoint: {}", endpoint);
2235
2236 prop_assert!(endpoint.starts_with("/appsec/v1/policies/"),
2238 "Malformed endpoint structure: {}", endpoint);
2239 #[allow(clippy::arithmetic_side_effects)]
2240 {
2241 prop_assert_eq!(endpoint.len(), 20 + guid.len(),
2242 "Unexpected endpoint length");
2243 }
2244 }
2245
2246 #[test]
2247 fn proptest_error_message_no_injection(
2248 user_input in prop::string::string_regex("[ -~]{1,100}").expect("valid regex for user input")
2249 ) {
2250 let error_msg = format!("Invalid GUID: {}", user_input);
2251
2252 prop_assert!(error_msg.starts_with("Invalid GUID: "),
2254 "Error message structure corrupted");
2255 prop_assert!(!error_msg.contains('\0'),
2256 "Null byte in error message");
2257 prop_assert!(error_msg.len() >= 14,
2258 "Error message unexpectedly short");
2259 }
2260
2261 #[test]
2262 fn proptest_compliance_status_values_safe(
2263 status in prop_oneof![
2264 Just("Passed".to_string()),
2265 Just("Did Not Pass".to_string()),
2266 Just("Conditional Pass".to_string()),
2267 Just("Not Assessed".to_string()),
2268 Just("Calculating...".to_string()),
2269 ]
2270 ) {
2271 prop_assert!(status.chars().all(|c| c.is_alphanumeric() || c.is_whitespace() || c == '.'),
2273 "Status contains unexpected characters: {}", status);
2274
2275 let should_break = PolicyApi::should_break_build(&status);
2277 if status == "Did Not Pass" {
2278 prop_assert!(should_break, "Did Not Pass should break build");
2279 } else {
2280 prop_assert!(!should_break, "{} should not break build", status);
2281 }
2282 }
2283 }
2284}
2285
2286#[cfg(test)]
2288#[allow(clippy::expect_used)]
2289mod endpoint_safety_proptests {
2290 use proptest::prelude::*;
2291
2292 proptest! {
2293 #![proptest_config(ProptestConfig {
2294 cases: if cfg!(miri) { 5 } else { 1000 },
2295 failure_persistence: None,
2296 .. ProptestConfig::default()
2297 })]
2298
2299 #[test]
2300 fn proptest_scan_id_endpoint_no_injection(scan_id in 0u64..u64::MAX) {
2301 let endpoint = format!("/appsec/v1/policy-scans/{}", scan_id);
2302
2303 prop_assert!(endpoint.starts_with("/appsec/v1/policy-scans/"),
2305 "Endpoint prefix corrupted: {}", endpoint);
2306 prop_assert!(!endpoint.contains(".."),
2307 "Path traversal in endpoint: {}", endpoint);
2308 prop_assert!(!endpoint.contains("//"),
2309 "Double slash in endpoint: {}", endpoint);
2310
2311 let scan_id_str = endpoint.get(24..).unwrap_or("");
2313 prop_assert!(scan_id_str.parse::<u64>().is_ok(),
2314 "Invalid scan_id in endpoint: {}", scan_id_str);
2315 }
2316
2317 #[test]
2318 fn proptest_app_guid_endpoint_validated(
2319 guid_part in prop::string::string_regex("[0-9a-f]{32}").expect("valid regex for guid")
2320 ) {
2321 let endpoint = format!("/appsec/v2/applications/{}/summary_report", guid_part);
2323
2324 prop_assert!(endpoint.starts_with("/appsec/v2/applications/"),
2326 "Invalid endpoint prefix");
2327 prop_assert!(endpoint.ends_with("/summary_report"),
2328 "Invalid endpoint suffix");
2329 prop_assert!(!endpoint.contains(".."),
2330 "Path traversal in endpoint");
2331 }
2332
2333 #[test]
2334 fn proptest_query_string_no_injection(
2335 build_id in prop::string::string_regex("[a-zA-Z0-9_-]{1,64}").expect("valid regex for build id"),
2336 sandbox_guid in prop::string::string_regex("[0-9a-f]{32}").expect("valid regex for sandbox guid")
2337 ) {
2338 let query_params = [
2340 ("build_id".to_string(), build_id.clone()),
2341 ("context".to_string(), sandbox_guid.clone())
2342 ];
2343
2344 let keys: Vec<_> = query_params.iter().map(|(k, _)| k).collect();
2346 prop_assert_eq!(keys.len(), 2, "Wrong number of query params");
2347 prop_assert_eq!(keys.first().map(|s| s.as_str()), Some("build_id"), "Wrong first key");
2348 prop_assert_eq!(keys.get(1).map(|s| s.as_str()), Some("context"), "Wrong second key");
2349
2350 if let Some((_, val)) = query_params.first() {
2352 prop_assert_eq!(val, &build_id, "build_id value corrupted");
2353 }
2354 if let Some((_, val)) = query_params.get(1) {
2355 prop_assert_eq!(val, &sandbox_guid, "sandbox_guid value corrupted");
2356 }
2357 }
2358 }
2359}
2360
2361#[cfg(test)]
2363#[allow(clippy::expect_used)]
2364mod memory_safety_proptests {
2365 use super::*;
2366 use proptest::prelude::*;
2367
2368 proptest! {
2369 #![proptest_config(ProptestConfig {
2370 cases: if cfg!(miri) { 5 } else { 1000 },
2371 failure_persistence: None,
2372 .. ProptestConfig::default()
2373 })]
2374
2375 #[test]
2376 fn proptest_policy_list_params_from_owned(
2377 name in prop::option::of(prop::string::string_regex("[a-zA-Z0-9_-]{1,50}").expect("valid regex for name")),
2378 page in prop::option::of(0u32..1000u32),
2379 size in prop::option::of(1u32..100u32)
2380 ) {
2381 let params = PolicyListParams {
2382 name: name.clone(),
2383 policy_type: None,
2384 is_active: None,
2385 default_only: None,
2386 page,
2387 size,
2388 };
2389
2390 let query_params: Vec<(String, String)> = params.into();
2392
2393 prop_assert!(query_params.len() <= 6, "Too many query params");
2395
2396 if name.is_some() {
2398 let has_name = query_params.iter().any(|(k, _)| k == "name");
2399 prop_assert!(has_name, "Name parameter lost after move");
2400 }
2401 }
2402
2403 #[test]
2404 fn proptest_policy_list_params_from_ref(
2405 name in prop::option::of(prop::string::string_regex("[a-zA-Z0-9_-]{1,50}").expect("valid regex for name")),
2406 page in prop::option::of(0u32..1000u32)
2407 ) {
2408 let params = PolicyListParams {
2409 name: name.clone(),
2410 policy_type: None,
2411 is_active: None,
2412 default_only: None,
2413 page,
2414 size: None,
2415 };
2416
2417 let query_params: Vec<(String, String)> = Vec::from(¶ms);
2419
2420 let query_params2 = params.to_query_params();
2422
2423 prop_assert_eq!(query_params, query_params2,
2425 "Reference conversion differs from method call");
2426 }
2427
2428 #[test]
2429 fn proptest_vec_allocation_reasonable(
2430 param_count in 1usize..10usize
2431 ) {
2432 let mut params = Vec::new();
2433
2434 for i in 0..param_count {
2435 params.push((format!("key{}", i), format!("value{}", i)));
2436 }
2437
2438 prop_assert_eq!(params.len(), param_count,
2440 "Parameter count mismatch");
2441 prop_assert!(params.capacity() >= param_count,
2442 "Insufficient capacity");
2443 prop_assert!(params.capacity() < param_count.saturating_mul(10),
2445 "Excessive capacity: {} for {} items", params.capacity(), param_count);
2446 }
2447 }
2448}