1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9use crate::{VeracodeError};
10use crate::client::VeracodeClient;
11
12#[derive(Debug, Serialize, Deserialize, Clone)]
17pub struct Application {
18 pub guid: String,
20 pub id: u64,
22 pub oid: Option<u64>,
24 pub alt_org_id: Option<u64>,
26 pub organization_id: Option<u64>,
28 pub created: String,
30 pub modified: Option<String>,
32 pub last_completed_scan_date: Option<String>,
34 pub last_policy_compliance_check_date: Option<String>,
36 pub app_profile_url: Option<String>,
38 pub profile: Option<Profile>,
40 pub scans: Option<Vec<Scan>>,
42 pub results_url: Option<String>,
44}
45
46#[derive(Debug, Serialize, Deserialize, Clone)]
48pub struct Profile {
49 pub name: String,
51 pub description: Option<String>,
53 pub tags: Option<String>,
55 pub business_unit: Option<BusinessUnit>,
57 pub business_owners: Option<Vec<BusinessOwner>>,
59 pub policies: Option<Vec<Policy>>,
61 pub teams: Option<Vec<Team>>,
63 pub archer_app_name: Option<String>,
65 pub custom_fields: Option<Vec<CustomField>>,
67 #[serde(serialize_with = "serialize_business_criticality")]
69 pub business_criticality: BusinessCriticality,
70 pub settings: Option<Settings>
72}
73
74#[derive(Debug, Serialize, Deserialize, Clone)]
75pub struct Settings {
76 pub nextday_consultation_allowed: bool,
78 pub static_scan_xpa_or_dpa: bool,
80 pub dynamic_scan_approval_not_required: bool,
82 pub sca_enabled: bool,
84 pub static_scan_xpp_enabled: bool,
86}
87
88#[derive(Debug, Serialize, Deserialize, Clone)]
90pub struct BusinessUnit {
91 pub id: Option<u64>,
93 pub name: Option<String>,
95 pub guid: Option<String>,
97}
98
99#[derive(Debug, Serialize, Deserialize, Clone)]
101pub struct BusinessOwner {
102 pub email: Option<String>,
104 pub name: Option<String>,
106}
107
108#[derive(Debug, Serialize, Deserialize, Clone)]
110pub struct Policy {
111 pub guid: String,
113 pub name: String,
115 pub is_default: bool,
117 pub policy_compliance_status: Option<String>,
119}
120
121#[derive(Debug, Serialize, Deserialize, Clone)]
123pub struct Team {
124 pub team_id: Option<u64>,
126 pub team_name: Option<String>,
128 pub team_legacy_id: Option<u64>,
130}
131
132#[derive(Debug, Serialize, Deserialize, Clone)]
134pub struct CustomField {
135 pub name: Option<String>,
137 pub value: Option<String>,
139}
140
141#[derive(Debug, Serialize, Deserialize, Clone)]
143pub struct Scan {
144 pub scan_id: Option<u64>,
146 pub scan_type: Option<String>,
148 pub status: Option<String>,
150 pub scan_url: Option<String>,
152 pub modified_date: Option<String>,
154 pub internal_status: Option<String>,
156 pub links: Option<Vec<Link>>,
158 pub fallback_type: Option<String>,
160 pub full_type: Option<String>,
162}
163
164#[derive(Debug, Serialize, Deserialize, Clone)]
166pub struct Link {
167 pub rel: Option<String>,
169 pub href: Option<String>,
171}
172
173#[derive(Debug, Serialize, Deserialize, Clone)]
175pub struct ApplicationsResponse {
176 #[serde(rename = "_embedded")]
178 pub embedded: Option<EmbeddedApplications>,
179 pub page: Option<PageInfo>,
181 #[serde(rename = "_links")]
183 pub links: Option<HashMap<String, Link>>,
184}
185
186#[derive(Debug, Serialize, Deserialize, Clone)]
188pub struct EmbeddedApplications {
189 pub applications: Vec<Application>,
191}
192
193#[derive(Debug, Serialize, Deserialize, Clone)]
195pub struct PageInfo {
196 pub size: Option<u32>,
198 pub number: Option<u32>,
200 pub total_elements: Option<u64>,
202 pub total_pages: Option<u32>,
204}
205
206#[derive(Debug, Serialize, Deserialize, Clone)]
208pub struct CreateApplicationRequest {
209 pub profile: CreateApplicationProfile,
211}
212
213#[derive(Debug, Serialize, Deserialize, Clone)]
215pub struct CreateApplicationProfile {
216 pub name: String,
218 #[serde(serialize_with = "serialize_business_criticality")]
220 pub business_criticality: BusinessCriticality,
221 pub description: Option<String>,
223 pub business_unit: Option<BusinessUnit>,
225 pub business_owners: Option<Vec<BusinessOwner>>,
227 pub policies: Option<Vec<Policy>>,
229 pub teams: Option<Vec<Team>>,
231 pub tags: Option<String>,
233 pub custom_fields: Option<Vec<CustomField>>,
235}
236
237#[derive(Debug, Clone, Copy, PartialEq, Eq)]
239pub enum BusinessCriticality {
240 VeryHigh,
241 High,
242 Medium,
243 Low,
244 VeryLow,
245}
246
247impl BusinessCriticality {
248 pub fn as_str(&self) -> &'static str {
250 match self {
251 BusinessCriticality::VeryHigh => "VERY_HIGH",
252 BusinessCriticality::High => "HIGH",
253 BusinessCriticality::Medium => "MEDIUM",
254 BusinessCriticality::Low => "LOW",
255 BusinessCriticality::VeryLow => "VERY_LOW",
256 }
257 }
258}
259
260impl From<BusinessCriticality> for String {
261 fn from(criticality: BusinessCriticality) -> Self {
262 criticality.as_str().to_string()
263 }
264}
265
266impl std::fmt::Display for BusinessCriticality {
267 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
268 write!(f, "{}", self.as_str())
269 }
270}
271
272fn serialize_business_criticality<S>(criticality: &BusinessCriticality, serializer: S) -> Result<S::Ok, S::Error>
274where
275 S: serde::Serializer,
276{
277 serializer.serialize_str(criticality.as_str())
278}
279
280impl std::str::FromStr for BusinessCriticality {
282 type Err = String;
283
284 fn from_str(s: &str) -> Result<Self, Self::Err> {
285 match s {
286 "VERY_HIGH" => Ok(BusinessCriticality::VeryHigh),
287 "HIGH" => Ok(BusinessCriticality::High),
288 "MEDIUM" => Ok(BusinessCriticality::Medium),
289 "LOW" => Ok(BusinessCriticality::Low),
290 "VERY_LOW" => Ok(BusinessCriticality::VeryLow),
291 _ => Err(format!("Invalid business criticality: '{s}'. Must be one of: VERY_HIGH, HIGH, MEDIUM, LOW, VERY_LOW")),
292 }
293 }
294}
295
296impl<'de> serde::Deserialize<'de> for BusinessCriticality {
298 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
299 where
300 D: serde::Deserializer<'de>,
301 {
302 let s = String::deserialize(deserializer)?;
303 s.parse().map_err(serde::de::Error::custom)
304 }
305}
306
307#[derive(Debug, Serialize, Deserialize, Clone)]
309pub struct UpdateApplicationRequest {
310 pub profile: UpdateApplicationProfile,
312}
313
314#[derive(Debug, Serialize, Deserialize, Clone)]
316pub struct UpdateApplicationProfile {
317 pub name: Option<String>,
319 pub description: Option<String>,
321 pub business_unit: Option<BusinessUnit>,
323 pub business_owners: Option<Vec<BusinessOwner>>,
325 #[serde(serialize_with = "serialize_business_criticality")]
327 pub business_criticality: BusinessCriticality,
328 pub policies: Option<Vec<Policy>>,
330 pub teams: Option<Vec<Team>>,
332 pub tags: Option<String>,
334 pub custom_fields: Option<Vec<CustomField>>,
336}
337
338#[derive(Debug, Clone, Default)]
340pub struct ApplicationQuery {
341 pub name: Option<String>,
343 pub policy_compliance: Option<String>,
345 pub modified_after: Option<String>,
347 pub modified_before: Option<String>,
349 pub created_after: Option<String>,
351 pub created_before: Option<String>,
353 pub scan_type: Option<String>,
355 pub tags: Option<String>,
357 pub business_unit: Option<String>,
359 pub page: Option<u32>,
361 pub size: Option<u32>,
363}
364
365impl ApplicationQuery {
366 pub fn new() -> Self {
368 Default::default()
369 }
370
371 pub fn with_name(mut self, name: String) -> Self {
373 self.name = Some(name);
374 self
375 }
376
377 pub fn with_policy_compliance(mut self, compliance: String) -> Self {
379 self.policy_compliance = Some(compliance);
380 self
381 }
382
383 pub fn with_modified_after(mut self, date: String) -> Self {
385 self.modified_after = Some(date);
386 self
387 }
388
389 pub fn with_modified_before(mut self, date: String) -> Self {
391 self.modified_before = Some(date);
392 self
393 }
394
395 pub fn with_page(mut self, page: u32) -> Self {
397 self.page = Some(page);
398 self
399 }
400
401 pub fn with_size(mut self, size: u32) -> Self {
403 self.size = Some(size);
404 self
405 }
406
407 pub fn to_query_params(&self) -> Vec<(String, String)> {
409 let mut params = Vec::new();
410
411 if let Some(ref name) = self.name {
412 params.push(("name".to_string(), name.clone()));
413 }
414 if let Some(ref compliance) = self.policy_compliance {
415 params.push(("policy_compliance".to_string(), compliance.clone()));
416 }
417 if let Some(ref date) = self.modified_after {
418 params.push(("modified_after".to_string(), date.clone()));
419 }
420 if let Some(ref date) = self.modified_before {
421 params.push(("modified_before".to_string(), date.clone()));
422 }
423 if let Some(ref date) = self.created_after {
424 params.push(("created_after".to_string(), date.clone()));
425 }
426 if let Some(ref date) = self.created_before {
427 params.push(("created_before".to_string(), date.clone()));
428 }
429 if let Some(ref scan_type) = self.scan_type {
430 params.push(("scan_type".to_string(), scan_type.clone()));
431 }
432 if let Some(ref tags) = self.tags {
433 params.push(("tags".to_string(), tags.clone()));
434 }
435 if let Some(ref business_unit) = self.business_unit {
436 params.push(("business_unit".to_string(), business_unit.clone()));
437 }
438 if let Some(page) = self.page {
439 params.push(("page".to_string(), page.to_string()));
440 }
441 if let Some(size) = self.size {
442 params.push(("size".to_string(), size.to_string()));
443 }
444
445 params
446 }
447}
448
449impl VeracodeClient {
451 pub async fn get_applications(&self, query: Option<ApplicationQuery>) -> Result<ApplicationsResponse, VeracodeError> {
461 let endpoint = "/appsec/v1/applications";
462 let query_params = query.as_ref().map(|q| q.to_query_params());
463
464 let response = self.get(endpoint, query_params.as_deref()).await?;
465 let response = Self::handle_response(response).await?;
466
467 let apps_response: ApplicationsResponse = response.json().await?;
468 Ok(apps_response)
469 }
470
471 pub async fn get_application(&self, guid: &str) -> Result<Application, VeracodeError> {
481 let endpoint = format!("/appsec/v1/applications/{guid}");
482
483 let response = self.get(&endpoint, None).await?;
484 let response = Self::handle_response(response).await?;
485
486 let app: Application = response.json().await?;
487 Ok(app)
488 }
489
490 pub async fn create_application(&self, request: CreateApplicationRequest) -> Result<Application, VeracodeError> {
500 let endpoint = "/appsec/v1/applications";
501
502 let response = self.post(endpoint, Some(&request)).await?;
503 let response = Self::handle_response(response).await?;
504
505 let app: Application = response.json().await?;
506 Ok(app)
507 }
508
509 pub async fn update_application(&self, guid: &str, request: UpdateApplicationRequest) -> Result<Application, VeracodeError> {
520 let endpoint = format!("/appsec/v1/applications/{guid}");
521
522 let response = self.put(&endpoint, Some(&request)).await?;
523 let response = Self::handle_response(response).await?;
524
525 let app: Application = response.json().await?;
526 Ok(app)
527 }
528
529 pub async fn delete_application(&self, guid: &str) -> Result<(), VeracodeError> {
539 let endpoint = format!("/appsec/v1/applications/{guid}");
540
541 let response = self.delete(&endpoint).await?;
542 let _response = Self::handle_response(response).await?;
543
544 Ok(())
545 }
546
547 pub async fn get_non_compliant_applications(&self) -> Result<Vec<Application>, VeracodeError> {
553 let query = ApplicationQuery::new()
554 .with_policy_compliance("DID_NOT_PASS".to_string());
555
556 let response = self.get_applications(Some(query)).await?;
557
558 if let Some(embedded) = response.embedded {
559 Ok(embedded.applications)
560 } else {
561 Ok(Vec::new())
562 }
563 }
564
565 pub async fn get_applications_modified_after(&self, date: &str) -> Result<Vec<Application>, VeracodeError> {
575 let query = ApplicationQuery::new()
576 .with_modified_after(date.to_string());
577
578 let response = self.get_applications(Some(query)).await?;
579
580 if let Some(embedded) = response.embedded {
581 Ok(embedded.applications)
582 } else {
583 Ok(Vec::new())
584 }
585 }
586
587 pub async fn search_applications_by_name(&self, name: &str) -> Result<Vec<Application>, VeracodeError> {
597 let query = ApplicationQuery::new()
598 .with_name(name.to_string());
599
600 let response = self.get_applications(Some(query)).await?;
601
602 if let Some(embedded) = response.embedded {
603 Ok(embedded.applications)
604 } else {
605 Ok(Vec::new())
606 }
607 }
608
609 pub async fn get_all_applications(&self) -> Result<Vec<Application>, VeracodeError> {
615 let mut all_applications = Vec::new();
616 let mut page = 0;
617
618 loop {
619 let query = ApplicationQuery::new()
620 .with_page(page)
621 .with_size(100);
622
623 let response = self.get_applications(Some(query)).await?;
624
625 if let Some(embedded) = response.embedded {
626 if embedded.applications.is_empty() {
627 break;
628 }
629 all_applications.extend(embedded.applications);
630 page += 1;
631 } else {
632 break;
633 }
634 }
635
636 Ok(all_applications)
637 }
638
639 pub async fn get_application_by_name(&self, name: &str) -> Result<Option<Application>, VeracodeError> {
649 let applications = self.search_applications_by_name(name).await?;
650
651 Ok(applications.into_iter().find(|app| {
653 if let Some(profile) = &app.profile {
654 profile.name == name
655 } else {
656 false
657 }
658 }))
659 }
660
661 pub async fn application_exists_by_name(&self, name: &str) -> Result<bool, VeracodeError> {
671 match self.get_application_by_name(name).await? {
672 Some(_) => Ok(true),
673 None => Ok(false),
674 }
675 }
676
677 pub async fn get_app_id_from_guid(&self, guid: &str) -> Result<String, VeracodeError> {
689 let app = self.get_application(guid).await?;
690 Ok(app.id.to_string())
691 }
692
693 pub async fn create_application_if_not_exists(
708 &self,
709 name: &str,
710 business_criticality: BusinessCriticality,
711 description: Option<String>,
712 ) -> Result<Application, VeracodeError> {
713 if let Some(existing_app) = self.get_application_by_name(name).await? {
715 return Ok(existing_app);
716 }
717
718 let create_request = CreateApplicationRequest {
720 profile: CreateApplicationProfile {
721 name: name.to_string(),
722 business_criticality,
723 description,
724 business_unit: None,
725 business_owners: None,
726 policies: None,
727 teams: None,
728 tags: None,
729 custom_fields: None,
730 },
731 };
732
733 self.create_application(create_request).await
734 }
735}
736
737#[cfg(test)]
738mod tests {
739 use super::*;
740
741 #[test]
742 fn test_query_params() {
743 let query = ApplicationQuery::new()
744 .with_name("test_app".to_string())
745 .with_policy_compliance("PASSED".to_string())
746 .with_page(1)
747 .with_size(50);
748
749 let params = query.to_query_params();
750 assert!(params.contains(&("name".to_string(), "test_app".to_string())));
751 assert!(params.contains(&("policy_compliance".to_string(), "PASSED".to_string())));
752 assert!(params.contains(&("page".to_string(), "1".to_string())));
753 assert!(params.contains(&("size".to_string(), "50".to_string())));
754 }
755
756 #[test]
757 fn test_application_query_builder() {
758 let query = ApplicationQuery::new()
759 .with_name("MyApp".to_string())
760 .with_policy_compliance("DID_NOT_PASS".to_string())
761 .with_modified_after("2023-01-01T00:00:00.000Z".to_string())
762 .with_page(2)
763 .with_size(25);
764
765 assert_eq!(query.name, Some("MyApp".to_string()));
766 assert_eq!(query.policy_compliance, Some("DID_NOT_PASS".to_string()));
767 assert_eq!(query.modified_after, Some("2023-01-01T00:00:00.000Z".to_string()));
768 assert_eq!(query.page, Some(2));
769 assert_eq!(query.size, Some(25));
770 }
771}