1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use chrono::{DateTime, Utc};
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 => write!(f, "Insufficient permissions for policy operation"),
420 PolicyError::Unauthorized => write!(f, "Authentication required - invalid API credentials"),
421 PolicyError::InternalServerError => write!(f, "Internal server error occurred"),
422 PolicyError::Timeout => write!(f, "Policy operation timed out"),
423 }
424 }
425}
426
427impl std::error::Error for PolicyError {}
428
429impl From<VeracodeError> for PolicyError {
430 fn from(err: VeracodeError) -> Self {
431 PolicyError::Api(err)
432 }
433}
434
435impl From<reqwest::Error> for PolicyError {
436 fn from(err: reqwest::Error) -> Self {
437 PolicyError::Api(VeracodeError::Http(err))
438 }
439}
440
441impl From<serde_json::Error> for PolicyError {
442 fn from(err: serde_json::Error) -> Self {
443 PolicyError::Api(VeracodeError::Serialization(err))
444 }
445}
446
447pub struct PolicyApi<'a> {
449 client: &'a VeracodeClient,
450}
451
452impl<'a> PolicyApi<'a> {
453 pub fn new(client: &'a VeracodeClient) -> Self {
455 Self { client }
456 }
457
458 pub async fn list_policies(
468 &self,
469 params: Option<PolicyListParams>,
470 ) -> Result<Vec<SecurityPolicy>, PolicyError> {
471 let endpoint = "/appsec/v1/policies";
472
473 let query_params = params.map(|p| p.to_query_params());
474
475 let response = self.client.get(endpoint, query_params.as_deref()).await?;
476
477 let status = response.status().as_u16();
478 match status {
479 200 => {
480 let policy_response: PolicyListResponse = response.json().await?;
481 let policies = policy_response.embedded
482 .map(|e| e.policy_versions)
483 .unwrap_or_default();
484
485 Ok(policies)
486 }
487 400 => {
488 let error_text = response.text().await.unwrap_or_default();
489 Err(PolicyError::InvalidConfig(error_text))
490 }
491 401 => Err(PolicyError::Unauthorized),
492 403 => Err(PolicyError::PermissionDenied),
493 404 => Err(PolicyError::NotFound),
494 500 => Err(PolicyError::InternalServerError),
495 _ => {
496 let error_text = response.text().await.unwrap_or_default();
497 Err(PolicyError::Api(VeracodeError::InvalidResponse(
498 format!("HTTP {status}: {error_text}")
499 )))
500 }
501 }
502 }
503
504 pub async fn get_policy(&self, policy_guid: &str) -> Result<SecurityPolicy, PolicyError> {
514 let endpoint = format!("/appsec/v1/policies/{policy_guid}");
515
516 let response = self.client.get(&endpoint, None).await?;
517
518 let status = response.status().as_u16();
519 match status {
520 200 => {
521 let policy: SecurityPolicy = response.json().await?;
522 Ok(policy)
523 }
524 400 => {
525 let error_text = response.text().await.unwrap_or_default();
526 Err(PolicyError::InvalidConfig(error_text))
527 }
528 401 => Err(PolicyError::Unauthorized),
529 403 => Err(PolicyError::PermissionDenied),
530 404 => Err(PolicyError::NotFound),
531 500 => Err(PolicyError::InternalServerError),
532 _ => {
533 let error_text = response.text().await.unwrap_or_default();
534 Err(PolicyError::Api(VeracodeError::InvalidResponse(
535 format!("HTTP {status}: {error_text}")
536 )))
537 }
538 }
539 }
540
541 pub async fn get_default_policy(&self) -> Result<SecurityPolicy, PolicyError> {
547 let params = PolicyListParams {
548 default_only: Some(true),
549 ..Default::default()
550 };
551
552 let policies = self.list_policies(Some(params)).await?;
553 policies.into_iter()
556 .find(|p| p.policy_type == "CUSTOMER" && p.organization_id.is_some())
557 .ok_or(PolicyError::NotFound)
558 }
559
560 pub async fn evaluate_policy_compliance(
572 &self,
573 application_guid: &str,
574 policy_guid: &str,
575 sandbox_guid: Option<&str>,
576 ) -> Result<PolicyComplianceResult, PolicyError> {
577 let endpoint = if let Some(sandbox) = sandbox_guid {
578 format!(
579 "/appsec/v1/applications/{application_guid}/sandboxes/{sandbox}/policy/{policy_guid}/compliance"
580 )
581 } else {
582 format!(
583 "/appsec/v1/applications/{application_guid}/policy/{policy_guid}/compliance"
584 )
585 };
586
587 let response = self.client.get(&endpoint, None).await?;
588
589 let status = response.status().as_u16();
590 match status {
591 200 => {
592 let compliance: PolicyComplianceResult = response.json().await?;
593 Ok(compliance)
594 }
595 404 => Err(PolicyError::NotFound),
596 _ => {
597 let error_text = response.text().await.unwrap_or_default();
598 Err(PolicyError::Api(VeracodeError::InvalidResponse(
599 format!("HTTP {status}: {error_text}")
600 )))
601 }
602 }
603 }
604
605 pub async fn initiate_policy_scan(
615 &self,
616 request: PolicyScanRequest,
617 ) -> Result<PolicyScanResult, PolicyError> {
618 let endpoint = "/appsec/v1/policy-scans";
619
620 let response = self.client.post(endpoint, Some(&request)).await?;
621
622 let status = response.status().as_u16();
623 match status {
624 200 | 201 => {
625 let scan_result: PolicyScanResult = response.json().await?;
626 Ok(scan_result)
627 }
628 400 => {
629 let error_text = response.text().await.unwrap_or_default();
630 Err(PolicyError::InvalidConfig(error_text))
631 }
632 404 => Err(PolicyError::NotFound),
633 _ => {
634 let error_text = response.text().await.unwrap_or_default();
635 Err(PolicyError::Api(VeracodeError::InvalidResponse(
636 format!("HTTP {status}: {error_text}")
637 )))
638 }
639 }
640 }
641
642 pub async fn get_policy_scan_result(
652 &self,
653 scan_id: u64,
654 ) -> Result<PolicyScanResult, PolicyError> {
655 let endpoint = format!("/appsec/v1/policy-scans/{scan_id}");
656
657 let response = self.client.get(&endpoint, None).await?;
658
659 let status = response.status().as_u16();
660 match status {
661 200 => {
662 let scan_result: PolicyScanResult = response.json().await?;
663 Ok(scan_result)
664 }
665 404 => Err(PolicyError::NotFound),
666 _ => {
667 let error_text = response.text().await.unwrap_or_default();
668 Err(PolicyError::Api(VeracodeError::InvalidResponse(
669 format!("HTTP {status}: {error_text}")
670 )))
671 }
672 }
673 }
674
675 pub async fn is_policy_scan_complete(&self, scan_id: u64) -> Result<bool, PolicyError> {
685 let scan_result = self.get_policy_scan_result(scan_id).await?;
686 Ok(matches!(scan_result.status, ScanStatus::Completed | ScanStatus::Failed | ScanStatus::Cancelled))
687 }
688
689 pub async fn get_policy_violations(
701 &self,
702 application_guid: &str,
703 policy_guid: &str,
704 sandbox_guid: Option<&str>,
705 ) -> Result<Vec<PolicyViolation>, PolicyError> {
706 let compliance = self.evaluate_policy_compliance(application_guid, policy_guid, sandbox_guid).await?;
707 Ok(compliance.violations.unwrap_or_default())
708 }
709}
710
711impl<'a> PolicyApi<'a> {
713 pub async fn is_application_compliant(
724 &self,
725 application_guid: &str,
726 policy_guid: &str,
727 ) -> Result<bool, PolicyError> {
728 let compliance = self.evaluate_policy_compliance(application_guid, policy_guid, None).await?;
729 Ok(compliance.passed)
730 }
731
732 pub async fn get_compliance_score(
743 &self,
744 application_guid: &str,
745 policy_guid: &str,
746 ) -> Result<Option<f64>, PolicyError> {
747 let compliance = self.evaluate_policy_compliance(application_guid, policy_guid, None).await?;
748 Ok(compliance.score)
749 }
750
751 pub async fn get_active_policies(&self) -> Result<Vec<SecurityPolicy>, PolicyError> {
757 let policies = self.list_policies(None).await?;
760 Ok(policies) }
762}
763
764#[cfg(test)]
765mod tests {
766 use super::*;
767
768 #[test]
769 fn test_policy_list_params_to_query() {
770 let params = PolicyListParams {
771 name: Some("test-policy".to_string()),
772 is_active: Some(true),
773 page: Some(1),
774 size: Some(10),
775 ..Default::default()
776 };
777
778 let query_params = params.to_query_params();
779 assert_eq!(query_params.len(), 4);
780 assert!(query_params.contains(&("name".to_string(), "test-policy".to_string())));
781 assert!(query_params.contains(&("active".to_string(), "true".to_string())));
782 assert!(query_params.contains(&("page".to_string(), "1".to_string())));
783 assert!(query_params.contains(&("size".to_string(), "10".to_string())));
784 }
785
786 #[test]
787 fn test_policy_error_display() {
788 let error = PolicyError::NotFound;
789 assert_eq!(error.to_string(), "Policy not found");
790
791 let error = PolicyError::InvalidConfig("test".to_string());
792 assert_eq!(error.to_string(), "Invalid policy configuration: test");
793
794 let error = PolicyError::Timeout;
795 assert_eq!(error.to_string(), "Policy operation timed out");
796 }
797
798 #[test]
799 fn test_scan_type_serialization() {
800 let scan_type = ScanType::Static;
801 let json = serde_json::to_string(&scan_type).unwrap();
802 assert_eq!(json, "\"static\"");
803
804 let deserialized: ScanType = serde_json::from_str(&json).unwrap();
805 assert!(matches!(deserialized, ScanType::Static));
806 }
807
808 #[test]
809 fn test_policy_compliance_status_serialization() {
810 let status = PolicyComplianceStatus::Pass;
811 let json = serde_json::to_string(&status).unwrap();
812 assert_eq!(json, "\"PASS\"");
813
814 let deserialized: PolicyComplianceStatus = serde_json::from_str(&json).unwrap();
815 assert!(matches!(deserialized, PolicyComplianceStatus::Pass));
816 }
817}