1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10use crate::{VeracodeClient, VeracodeError};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct SecurityPolicy {
15 pub guid: String,
17 pub name: String,
19 pub description: Option<String>,
21 #[serde(rename = "type")]
23 pub policy_type: String,
24 pub version: u32,
26 pub created: Option<DateTime<Utc>>,
28 pub modified_by: Option<String>,
30 pub organization_id: Option<u64>,
32 pub category: String,
34 pub vendor_policy: bool,
36 pub scan_frequency_rules: Vec<ScanFrequencyRule>,
38 pub finding_rules: Vec<FindingRule>,
40 pub custom_severities: Vec<serde_json::Value>,
42 pub sev5_grace_period: u32,
44 pub sev4_grace_period: u32,
45 pub sev3_grace_period: u32,
46 pub sev2_grace_period: u32,
47 pub sev1_grace_period: u32,
48 pub sev0_grace_period: u32,
49 pub score_grace_period: u32,
51 pub sca_blacklist_grace_period: u32,
53 pub sca_grace_periods: Option<serde_json::Value>,
55 pub evaluation_date: Option<DateTime<Utc>>,
57 pub evaluation_date_type: Option<String>,
59 pub capabilities: Vec<String>,
61 #[serde(rename = "_links")]
63 pub links: Option<serde_json::Value>,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
68#[serde(rename_all = "UPPERCASE")]
69pub enum PolicyComplianceStatus {
70 Pass,
72 Fail,
74 Pending,
76 NotDetermined,
78 Error,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct PolicyRule {
85 pub id: String,
87 pub name: String,
89 pub description: Option<String>,
91 pub rule_type: String,
93 pub criteria: Option<serde_json::Value>,
95 pub enabled: bool,
97 pub severity: Option<String>,
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct PolicyThresholds {
104 pub very_high: Option<u32>,
106 pub high: Option<u32>,
108 pub medium: Option<u32>,
110 pub low: Option<u32>,
112 pub very_low: Option<u32>,
114 pub score_threshold: Option<f64>,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct PolicyScanRequest {
121 pub application_guid: String,
123 pub policy_guid: String,
125 pub scan_type: ScanType,
127 pub sandbox_guid: Option<String>,
129 pub config: Option<PolicyScanConfig>,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
135#[serde(rename_all = "lowercase")]
136pub enum ScanType {
137 Static,
139 Dynamic,
141 Sca,
143 Manual,
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct PolicyScanConfig {
150 pub auto_submit: Option<bool>,
152 pub timeout_minutes: Option<u32>,
154 pub include_third_party: Option<bool>,
156 pub modules: Option<Vec<String>>,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct PolicyScanResult {
163 pub scan_id: u64,
165 pub application_guid: String,
167 pub policy_guid: String,
169 pub status: ScanStatus,
171 pub scan_type: ScanType,
173 pub started: DateTime<Utc>,
175 pub completed: Option<DateTime<Utc>>,
177 pub compliance_result: Option<PolicyComplianceResult>,
179 pub findings_summary: Option<FindingsSummary>,
181 pub results_url: Option<String>,
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize)]
187#[serde(rename_all = "UPPERCASE")]
188pub enum ScanStatus {
189 Queued,
191 Running,
193 Completed,
195 Failed,
197 Cancelled,
199 Timeout,
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize)]
205pub struct PolicyComplianceResult {
206 pub status: PolicyComplianceStatus,
208 pub score: Option<f64>,
210 pub passed: bool,
212 pub breakdown: Option<ComplianceBreakdown>,
214 pub violations: Option<Vec<PolicyViolation>>,
216 pub summary: Option<String>,
218}
219
220#[derive(Debug, Clone, Serialize, Deserialize)]
222pub struct ComplianceBreakdown {
223 pub very_high: u32,
225 pub high: u32,
227 pub medium: u32,
229 pub low: u32,
231 pub very_low: u32,
233 pub total: u32,
235}
236
237#[derive(Debug, Clone, Serialize, Deserialize)]
239pub struct PolicyViolation {
240 pub violation_type: String,
242 pub severity: String,
244 pub description: String,
246 pub count: u32,
248 pub threshold_exceeded: Option<u32>,
250 pub actual_value: Option<u32>,
252}
253
254#[derive(Debug, Clone, Serialize, Deserialize)]
256pub struct FindingsSummary {
257 pub total: u32,
259 pub open: u32,
261 pub fixed: u32,
263 pub by_severity: HashMap<String, u32>,
265 pub by_category: Option<HashMap<String, u32>>,
267}
268
269#[derive(Debug, Clone, Serialize, Deserialize)]
271pub struct ScanFrequencyRule {
272 pub scan_type: String,
274 pub frequency: String,
276}
277
278#[derive(Debug, Clone, Serialize, Deserialize)]
280pub struct FindingRule {
281 #[serde(rename = "type")]
283 pub rule_type: String,
284 pub scan_type: Vec<String>,
286 pub value: String,
288 pub advanced_options: Option<serde_json::Value>,
290}
291
292#[derive(Debug, Clone, Serialize, Deserialize)]
294pub struct FindingRuleAdvancedOptions {
295 pub override_severity: Option<bool>,
297 pub build_action: Option<String>,
299 pub component_dependency: Option<String>,
301 pub vulnerable_methods: Option<String>,
303 pub selected_licenses: Option<Vec<String>>,
305 pub override_severity_level: Option<String>,
307 pub allowed_nonoss_licenses: Option<bool>,
309 pub allowed_unrecognized_licenses: Option<bool>,
311 pub all_licenses_must_meet_requirement: Option<bool>,
313 pub is_blocklist: Option<bool>,
315}
316
317#[derive(Debug, Clone, Default)]
319pub struct PolicyListParams {
320 pub name: Option<String>,
322 pub policy_type: Option<String>,
324 pub is_active: Option<bool>,
326 pub default_only: Option<bool>,
328 pub page: Option<u32>,
330 pub size: Option<u32>,
332}
333
334impl PolicyListParams {
335 pub fn to_query_params(&self) -> Vec<(String, String)> {
337 let mut params = Vec::new();
338
339 if let Some(name) = &self.name {
340 params.push(("name".to_string(), name.clone()));
341 }
342 if let Some(policy_type) = &self.policy_type {
343 params.push(("type".to_string(), policy_type.clone()));
344 }
345 if let Some(is_active) = self.is_active {
346 params.push(("active".to_string(), is_active.to_string()));
347 }
348 if let Some(default_only) = self.default_only {
349 params.push(("default".to_string(), default_only.to_string()));
350 }
351 if let Some(page) = self.page {
352 params.push(("page".to_string(), page.to_string()));
353 }
354 if let Some(size) = self.size {
355 params.push(("size".to_string(), size.to_string()));
356 }
357
358 params
359 }
360}
361
362#[derive(Debug, Clone, Serialize, Deserialize)]
364pub struct PolicyListResponse {
365 #[serde(rename = "_embedded")]
366 pub embedded: Option<PolicyEmbedded>,
367 pub page: Option<PageInfo>,
368 #[serde(rename = "_links")]
369 pub links: Option<serde_json::Value>,
370}
371
372#[derive(Debug, Clone, Serialize, Deserialize)]
374pub struct PolicyEmbedded {
375 #[serde(rename = "policy_versions")]
376 pub policy_versions: Vec<SecurityPolicy>,
377}
378
379#[derive(Debug, Clone, Serialize, Deserialize)]
381pub struct PageInfo {
382 pub size: u32,
383 pub number: u32,
384 pub total_elements: u32,
385 pub total_pages: u32,
386}
387
388#[derive(Debug)]
390pub enum PolicyError {
391 Api(VeracodeError),
393 NotFound,
395 InvalidConfig(String),
397 ScanFailed(String),
399 EvaluationError(String),
401 PermissionDenied,
403 Unauthorized,
405 InternalServerError,
407 Timeout,
409}
410
411impl std::fmt::Display for PolicyError {
412 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
413 match self {
414 PolicyError::Api(err) => write!(f, "API error: {err}"),
415 PolicyError::NotFound => write!(f, "Policy not found"),
416 PolicyError::InvalidConfig(msg) => write!(f, "Invalid policy configuration: {msg}"),
417 PolicyError::ScanFailed(msg) => write!(f, "Policy scan failed: {msg}"),
418 PolicyError::EvaluationError(msg) => write!(f, "Policy evaluation error: {msg}"),
419 PolicyError::PermissionDenied => {
420 write!(f, "Insufficient permissions for policy operation")
421 }
422 PolicyError::Unauthorized => {
423 write!(f, "Authentication required - invalid API credentials")
424 }
425 PolicyError::InternalServerError => write!(f, "Internal server error occurred"),
426 PolicyError::Timeout => write!(f, "Policy operation timed out"),
427 }
428 }
429}
430
431impl std::error::Error for PolicyError {}
432
433impl From<VeracodeError> for PolicyError {
434 fn from(err: VeracodeError) -> Self {
435 PolicyError::Api(err)
436 }
437}
438
439impl From<reqwest::Error> for PolicyError {
440 fn from(err: reqwest::Error) -> Self {
441 PolicyError::Api(VeracodeError::Http(err))
442 }
443}
444
445impl From<serde_json::Error> for PolicyError {
446 fn from(err: serde_json::Error) -> Self {
447 PolicyError::Api(VeracodeError::Serialization(err))
448 }
449}
450
451pub struct PolicyApi<'a> {
453 client: &'a VeracodeClient,
454}
455
456impl<'a> PolicyApi<'a> {
457 pub fn new(client: &'a VeracodeClient) -> Self {
459 Self { client }
460 }
461
462 pub async fn list_policies(
472 &self,
473 params: Option<PolicyListParams>,
474 ) -> Result<Vec<SecurityPolicy>, PolicyError> {
475 let endpoint = "/appsec/v1/policies";
476
477 let query_params = params.map(|p| p.to_query_params());
478
479 let response = self.client.get(endpoint, query_params.as_deref()).await?;
480
481 let status = response.status().as_u16();
482 match status {
483 200 => {
484 let policy_response: PolicyListResponse = response.json().await?;
485 let policies = policy_response
486 .embedded
487 .map(|e| e.policy_versions)
488 .unwrap_or_default();
489
490 Ok(policies)
491 }
492 400 => {
493 let error_text = response.text().await.unwrap_or_default();
494 Err(PolicyError::InvalidConfig(error_text))
495 }
496 401 => Err(PolicyError::Unauthorized),
497 403 => Err(PolicyError::PermissionDenied),
498 404 => Err(PolicyError::NotFound),
499 500 => Err(PolicyError::InternalServerError),
500 _ => {
501 let error_text = response.text().await.unwrap_or_default();
502 Err(PolicyError::Api(VeracodeError::InvalidResponse(format!(
503 "HTTP {status}: {error_text}"
504 ))))
505 }
506 }
507 }
508
509 pub async fn get_policy(&self, policy_guid: &str) -> Result<SecurityPolicy, PolicyError> {
519 let endpoint = format!("/appsec/v1/policies/{policy_guid}");
520
521 let response = self.client.get(&endpoint, None).await?;
522
523 let status = response.status().as_u16();
524 match status {
525 200 => {
526 let policy: SecurityPolicy = response.json().await?;
527 Ok(policy)
528 }
529 400 => {
530 let error_text = response.text().await.unwrap_or_default();
531 Err(PolicyError::InvalidConfig(error_text))
532 }
533 401 => Err(PolicyError::Unauthorized),
534 403 => Err(PolicyError::PermissionDenied),
535 404 => Err(PolicyError::NotFound),
536 500 => Err(PolicyError::InternalServerError),
537 _ => {
538 let error_text = response.text().await.unwrap_or_default();
539 Err(PolicyError::Api(VeracodeError::InvalidResponse(format!(
540 "HTTP {status}: {error_text}"
541 ))))
542 }
543 }
544 }
545
546 pub async fn get_default_policy(&self) -> Result<SecurityPolicy, PolicyError> {
552 let params = PolicyListParams {
553 default_only: Some(true),
554 ..Default::default()
555 };
556
557 let policies = self.list_policies(Some(params)).await?;
558 policies
561 .into_iter()
562 .find(|p| p.policy_type == "CUSTOMER" && p.organization_id.is_some())
563 .ok_or(PolicyError::NotFound)
564 }
565
566 pub async fn evaluate_policy_compliance(
578 &self,
579 application_guid: &str,
580 policy_guid: &str,
581 sandbox_guid: Option<&str>,
582 ) -> Result<PolicyComplianceResult, PolicyError> {
583 let endpoint = if let Some(sandbox) = sandbox_guid {
584 format!(
585 "/appsec/v1/applications/{application_guid}/sandboxes/{sandbox}/policy/{policy_guid}/compliance"
586 )
587 } else {
588 format!("/appsec/v1/applications/{application_guid}/policy/{policy_guid}/compliance")
589 };
590
591 let response = self.client.get(&endpoint, None).await?;
592
593 let status = response.status().as_u16();
594 match status {
595 200 => {
596 let compliance: PolicyComplianceResult = response.json().await?;
597 Ok(compliance)
598 }
599 404 => Err(PolicyError::NotFound),
600 _ => {
601 let error_text = response.text().await.unwrap_or_default();
602 Err(PolicyError::Api(VeracodeError::InvalidResponse(format!(
603 "HTTP {status}: {error_text}"
604 ))))
605 }
606 }
607 }
608
609 pub async fn initiate_policy_scan(
619 &self,
620 request: PolicyScanRequest,
621 ) -> Result<PolicyScanResult, PolicyError> {
622 let endpoint = "/appsec/v1/policy-scans";
623
624 let response = self.client.post(endpoint, Some(&request)).await?;
625
626 let status = response.status().as_u16();
627 match status {
628 200 | 201 => {
629 let scan_result: PolicyScanResult = response.json().await?;
630 Ok(scan_result)
631 }
632 400 => {
633 let error_text = response.text().await.unwrap_or_default();
634 Err(PolicyError::InvalidConfig(error_text))
635 }
636 404 => Err(PolicyError::NotFound),
637 _ => {
638 let error_text = response.text().await.unwrap_or_default();
639 Err(PolicyError::Api(VeracodeError::InvalidResponse(format!(
640 "HTTP {status}: {error_text}"
641 ))))
642 }
643 }
644 }
645
646 pub async fn get_policy_scan_result(
656 &self,
657 scan_id: u64,
658 ) -> Result<PolicyScanResult, PolicyError> {
659 let endpoint = format!("/appsec/v1/policy-scans/{scan_id}");
660
661 let response = self.client.get(&endpoint, None).await?;
662
663 let status = response.status().as_u16();
664 match status {
665 200 => {
666 let scan_result: PolicyScanResult = response.json().await?;
667 Ok(scan_result)
668 }
669 404 => Err(PolicyError::NotFound),
670 _ => {
671 let error_text = response.text().await.unwrap_or_default();
672 Err(PolicyError::Api(VeracodeError::InvalidResponse(format!(
673 "HTTP {status}: {error_text}"
674 ))))
675 }
676 }
677 }
678
679 pub async fn is_policy_scan_complete(&self, scan_id: u64) -> Result<bool, PolicyError> {
689 let scan_result = self.get_policy_scan_result(scan_id).await?;
690 Ok(matches!(
691 scan_result.status,
692 ScanStatus::Completed | ScanStatus::Failed | ScanStatus::Cancelled
693 ))
694 }
695
696 pub async fn get_policy_violations(
708 &self,
709 application_guid: &str,
710 policy_guid: &str,
711 sandbox_guid: Option<&str>,
712 ) -> Result<Vec<PolicyViolation>, PolicyError> {
713 let compliance = self
714 .evaluate_policy_compliance(application_guid, policy_guid, sandbox_guid)
715 .await?;
716 Ok(compliance.violations.unwrap_or_default())
717 }
718}
719
720impl<'a> PolicyApi<'a> {
722 pub async fn is_application_compliant(
733 &self,
734 application_guid: &str,
735 policy_guid: &str,
736 ) -> Result<bool, PolicyError> {
737 let compliance = self
738 .evaluate_policy_compliance(application_guid, policy_guid, None)
739 .await?;
740 Ok(compliance.passed)
741 }
742
743 pub async fn get_compliance_score(
754 &self,
755 application_guid: &str,
756 policy_guid: &str,
757 ) -> Result<Option<f64>, PolicyError> {
758 let compliance = self
759 .evaluate_policy_compliance(application_guid, policy_guid, None)
760 .await?;
761 Ok(compliance.score)
762 }
763
764 pub async fn get_active_policies(&self) -> Result<Vec<SecurityPolicy>, PolicyError> {
770 let policies = self.list_policies(None).await?;
773 Ok(policies) }
775}
776
777#[cfg(test)]
778mod tests {
779 use super::*;
780
781 #[test]
782 fn test_policy_list_params_to_query() {
783 let params = PolicyListParams {
784 name: Some("test-policy".to_string()),
785 is_active: Some(true),
786 page: Some(1),
787 size: Some(10),
788 ..Default::default()
789 };
790
791 let query_params = params.to_query_params();
792 assert_eq!(query_params.len(), 4);
793 assert!(query_params.contains(&("name".to_string(), "test-policy".to_string())));
794 assert!(query_params.contains(&("active".to_string(), "true".to_string())));
795 assert!(query_params.contains(&("page".to_string(), "1".to_string())));
796 assert!(query_params.contains(&("size".to_string(), "10".to_string())));
797 }
798
799 #[test]
800 fn test_policy_error_display() {
801 let error = PolicyError::NotFound;
802 assert_eq!(error.to_string(), "Policy not found");
803
804 let error = PolicyError::InvalidConfig("test".to_string());
805 assert_eq!(error.to_string(), "Invalid policy configuration: test");
806
807 let error = PolicyError::Timeout;
808 assert_eq!(error.to_string(), "Policy operation timed out");
809 }
810
811 #[test]
812 fn test_scan_type_serialization() {
813 let scan_type = ScanType::Static;
814 let json = serde_json::to_string(&scan_type).unwrap();
815 assert_eq!(json, "\"static\"");
816
817 let deserialized: ScanType = serde_json::from_str(&json).unwrap();
818 assert!(matches!(deserialized, ScanType::Static));
819 }
820
821 #[test]
822 fn test_policy_compliance_status_serialization() {
823 let status = PolicyComplianceStatus::Pass;
824 let json = serde_json::to_string(&status).unwrap();
825 assert_eq!(json, "\"PASS\"");
826
827 let deserialized: PolicyComplianceStatus = serde_json::from_str(&json).unwrap();
828 assert!(matches!(deserialized, PolicyComplianceStatus::Pass));
829 }
830}