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>(
274 criticality: &BusinessCriticality,
275 serializer: S,
276) -> Result<S::Ok, S::Error>
277where
278 S: serde::Serializer,
279{
280 serializer.serialize_str(criticality.as_str())
281}
282
283impl std::str::FromStr for BusinessCriticality {
285 type Err = String;
286
287 fn from_str(s: &str) -> Result<Self, Self::Err> {
288 match s {
289 "VERY_HIGH" => Ok(BusinessCriticality::VeryHigh),
290 "HIGH" => Ok(BusinessCriticality::High),
291 "MEDIUM" => Ok(BusinessCriticality::Medium),
292 "LOW" => Ok(BusinessCriticality::Low),
293 "VERY_LOW" => Ok(BusinessCriticality::VeryLow),
294 _ => Err(format!(
295 "Invalid business criticality: '{s}'. Must be one of: VERY_HIGH, HIGH, MEDIUM, LOW, VERY_LOW"
296 )),
297 }
298 }
299}
300
301impl<'de> serde::Deserialize<'de> for BusinessCriticality {
303 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
304 where
305 D: serde::Deserializer<'de>,
306 {
307 let s = String::deserialize(deserializer)?;
308 s.parse().map_err(serde::de::Error::custom)
309 }
310}
311
312#[derive(Debug, Serialize, Deserialize, Clone)]
314pub struct UpdateApplicationRequest {
315 pub profile: UpdateApplicationProfile,
317}
318
319#[derive(Debug, Serialize, Deserialize, Clone)]
321pub struct UpdateApplicationProfile {
322 pub name: Option<String>,
324 pub description: Option<String>,
326 pub business_unit: Option<BusinessUnit>,
328 pub business_owners: Option<Vec<BusinessOwner>>,
330 #[serde(serialize_with = "serialize_business_criticality")]
332 pub business_criticality: BusinessCriticality,
333 pub policies: Option<Vec<Policy>>,
335 pub teams: Option<Vec<Team>>,
337 pub tags: Option<String>,
339 pub custom_fields: Option<Vec<CustomField>>,
341}
342
343#[derive(Debug, Clone, Default)]
345pub struct ApplicationQuery {
346 pub name: Option<String>,
348 pub policy_compliance: Option<String>,
350 pub modified_after: Option<String>,
352 pub modified_before: Option<String>,
354 pub created_after: Option<String>,
356 pub created_before: Option<String>,
358 pub scan_type: Option<String>,
360 pub tags: Option<String>,
362 pub business_unit: Option<String>,
364 pub page: Option<u32>,
366 pub size: Option<u32>,
368}
369
370impl ApplicationQuery {
371 pub fn new() -> Self {
373 Default::default()
374 }
375
376 pub fn with_name(mut self, name: String) -> Self {
378 self.name = Some(name);
379 self
380 }
381
382 pub fn with_policy_compliance(mut self, compliance: String) -> Self {
384 self.policy_compliance = Some(compliance);
385 self
386 }
387
388 pub fn with_modified_after(mut self, date: String) -> Self {
390 self.modified_after = Some(date);
391 self
392 }
393
394 pub fn with_modified_before(mut self, date: String) -> Self {
396 self.modified_before = Some(date);
397 self
398 }
399
400 pub fn with_page(mut self, page: u32) -> Self {
402 self.page = Some(page);
403 self
404 }
405
406 pub fn with_size(mut self, size: u32) -> Self {
408 self.size = Some(size);
409 self
410 }
411
412 pub fn to_query_params(&self) -> Vec<(String, String)> {
414 let mut params = Vec::new();
415
416 if let Some(ref name) = self.name {
417 params.push(("name".to_string(), name.clone()));
418 }
419 if let Some(ref compliance) = self.policy_compliance {
420 params.push(("policy_compliance".to_string(), compliance.clone()));
421 }
422 if let Some(ref date) = self.modified_after {
423 params.push(("modified_after".to_string(), date.clone()));
424 }
425 if let Some(ref date) = self.modified_before {
426 params.push(("modified_before".to_string(), date.clone()));
427 }
428 if let Some(ref date) = self.created_after {
429 params.push(("created_after".to_string(), date.clone()));
430 }
431 if let Some(ref date) = self.created_before {
432 params.push(("created_before".to_string(), date.clone()));
433 }
434 if let Some(ref scan_type) = self.scan_type {
435 params.push(("scan_type".to_string(), scan_type.clone()));
436 }
437 if let Some(ref tags) = self.tags {
438 params.push(("tags".to_string(), tags.clone()));
439 }
440 if let Some(ref business_unit) = self.business_unit {
441 params.push(("business_unit".to_string(), business_unit.clone()));
442 }
443 if let Some(page) = self.page {
444 params.push(("page".to_string(), page.to_string()));
445 }
446 if let Some(size) = self.size {
447 params.push(("size".to_string(), size.to_string()));
448 }
449
450 params
451 }
452}
453
454impl VeracodeClient {
456 pub async fn get_applications(
466 &self,
467 query: Option<ApplicationQuery>,
468 ) -> Result<ApplicationsResponse, VeracodeError> {
469 let endpoint = "/appsec/v1/applications";
470 let query_params = query.as_ref().map(|q| q.to_query_params());
471
472 let response = self.get(endpoint, query_params.as_deref()).await?;
473 let response = Self::handle_response(response).await?;
474
475 let apps_response: ApplicationsResponse = response.json().await?;
476 Ok(apps_response)
477 }
478
479 pub async fn get_application(&self, guid: &str) -> Result<Application, VeracodeError> {
489 let endpoint = format!("/appsec/v1/applications/{guid}");
490
491 let response = self.get(&endpoint, None).await?;
492 let response = Self::handle_response(response).await?;
493
494 let app: Application = response.json().await?;
495 Ok(app)
496 }
497
498 pub async fn create_application(
508 &self,
509 request: CreateApplicationRequest,
510 ) -> Result<Application, VeracodeError> {
511 let endpoint = "/appsec/v1/applications";
512
513 let response = self.post(endpoint, Some(&request)).await?;
514 let response = Self::handle_response(response).await?;
515
516 let app: Application = response.json().await?;
517 Ok(app)
518 }
519
520 pub async fn update_application(
531 &self,
532 guid: &str,
533 request: UpdateApplicationRequest,
534 ) -> Result<Application, VeracodeError> {
535 let endpoint = format!("/appsec/v1/applications/{guid}");
536
537 let response = self.put(&endpoint, Some(&request)).await?;
538 let response = Self::handle_response(response).await?;
539
540 let app: Application = response.json().await?;
541 Ok(app)
542 }
543
544 pub async fn delete_application(&self, guid: &str) -> Result<(), VeracodeError> {
554 let endpoint = format!("/appsec/v1/applications/{guid}");
555
556 let response = self.delete(&endpoint).await?;
557 let _response = Self::handle_response(response).await?;
558
559 Ok(())
560 }
561
562 pub async fn get_non_compliant_applications(&self) -> Result<Vec<Application>, VeracodeError> {
568 let query = ApplicationQuery::new().with_policy_compliance("DID_NOT_PASS".to_string());
569
570 let response = self.get_applications(Some(query)).await?;
571
572 if let Some(embedded) = response.embedded {
573 Ok(embedded.applications)
574 } else {
575 Ok(Vec::new())
576 }
577 }
578
579 pub async fn get_applications_modified_after(
589 &self,
590 date: &str,
591 ) -> Result<Vec<Application>, VeracodeError> {
592 let query = ApplicationQuery::new().with_modified_after(date.to_string());
593
594 let response = self.get_applications(Some(query)).await?;
595
596 if let Some(embedded) = response.embedded {
597 Ok(embedded.applications)
598 } else {
599 Ok(Vec::new())
600 }
601 }
602
603 pub async fn search_applications_by_name(
613 &self,
614 name: &str,
615 ) -> Result<Vec<Application>, VeracodeError> {
616 let query = ApplicationQuery::new().with_name(name.to_string());
617
618 let response = self.get_applications(Some(query)).await?;
619
620 if let Some(embedded) = response.embedded {
621 Ok(embedded.applications)
622 } else {
623 Ok(Vec::new())
624 }
625 }
626
627 pub async fn get_all_applications(&self) -> Result<Vec<Application>, VeracodeError> {
633 let mut all_applications = Vec::new();
634 let mut page = 0;
635
636 loop {
637 let query = ApplicationQuery::new().with_page(page).with_size(100);
638
639 let response = self.get_applications(Some(query)).await?;
640
641 if let Some(embedded) = response.embedded {
642 if embedded.applications.is_empty() {
643 break;
644 }
645 all_applications.extend(embedded.applications);
646 page += 1;
647 } else {
648 break;
649 }
650 }
651
652 Ok(all_applications)
653 }
654
655 pub async fn get_application_by_name(
665 &self,
666 name: &str,
667 ) -> Result<Option<Application>, VeracodeError> {
668 let applications = self.search_applications_by_name(name).await?;
669
670 Ok(applications.into_iter().find(|app| {
672 if let Some(profile) = &app.profile {
673 profile.name == name
674 } else {
675 false
676 }
677 }))
678 }
679
680 pub async fn application_exists_by_name(&self, name: &str) -> Result<bool, VeracodeError> {
690 match self.get_application_by_name(name).await? {
691 Some(_) => Ok(true),
692 None => Ok(false),
693 }
694 }
695
696 pub async fn get_app_id_from_guid(&self, guid: &str) -> Result<String, VeracodeError> {
708 let app = self.get_application(guid).await?;
709 Ok(app.id.to_string())
710 }
711
712 pub async fn create_application_if_not_exists(
728 &self,
729 name: &str,
730 business_criticality: BusinessCriticality,
731 description: Option<String>,
732 team_names: Option<Vec<String>>,
733 ) -> Result<Application, VeracodeError> {
734 if let Some(existing_app) = self.get_application_by_name(name).await? {
736 return Ok(existing_app);
737 }
738
739 let teams = team_names.map(|names| {
743 names
744 .into_iter()
745 .map(|team_name| Team {
746 team_id: None, team_name: Some(team_name),
748 team_legacy_id: None, })
750 .collect()
751 });
752
753 let create_request = CreateApplicationRequest {
754 profile: CreateApplicationProfile {
755 name: name.to_string(),
756 business_criticality,
757 description,
758 business_unit: None,
759 business_owners: None,
760 policies: None,
761 teams,
762 tags: None,
763 custom_fields: None,
764 },
765 };
766
767 self.create_application(create_request).await
768 }
769
770 pub async fn create_application_if_not_exists_simple(
785 &self,
786 name: &str,
787 business_criticality: BusinessCriticality,
788 description: Option<String>,
789 ) -> Result<Application, VeracodeError> {
790 self.create_application_if_not_exists(name, business_criticality, description, None)
791 .await
792 }
793}
794
795#[cfg(test)]
796mod tests {
797 use super::*;
798
799 #[test]
800 fn test_query_params() {
801 let query = ApplicationQuery::new()
802 .with_name("test_app".to_string())
803 .with_policy_compliance("PASSED".to_string())
804 .with_page(1)
805 .with_size(50);
806
807 let params = query.to_query_params();
808 assert!(params.contains(&("name".to_string(), "test_app".to_string())));
809 assert!(params.contains(&("policy_compliance".to_string(), "PASSED".to_string())));
810 assert!(params.contains(&("page".to_string(), "1".to_string())));
811 assert!(params.contains(&("size".to_string(), "50".to_string())));
812 }
813
814 #[test]
815 fn test_application_query_builder() {
816 let query = ApplicationQuery::new()
817 .with_name("MyApp".to_string())
818 .with_policy_compliance("DID_NOT_PASS".to_string())
819 .with_modified_after("2023-01-01T00:00:00.000Z".to_string())
820 .with_page(2)
821 .with_size(25);
822
823 assert_eq!(query.name, Some("MyApp".to_string()));
824 assert_eq!(query.policy_compliance, Some("DID_NOT_PASS".to_string()));
825 assert_eq!(
826 query.modified_after,
827 Some("2023-01-01T00:00:00.000Z".to_string())
828 );
829 assert_eq!(query.page, Some(2));
830 assert_eq!(query.size, Some(25));
831 }
832
833 #[test]
834 fn test_create_application_request_with_teams() {
835 let team_names = vec!["Security Team".to_string(), "Development Team".to_string()];
836 let teams: Vec<Team> = team_names
837 .into_iter()
838 .map(|team_name| Team {
839 team_id: None,
840 team_name: Some(team_name),
841 team_legacy_id: None,
842 })
843 .collect();
844
845 let request = CreateApplicationRequest {
846 profile: CreateApplicationProfile {
847 name: "Test Application".to_string(),
848 business_criticality: BusinessCriticality::Medium,
849 description: Some("Test description".to_string()),
850 business_unit: None,
851 business_owners: None,
852 policies: None,
853 teams: Some(teams.clone()),
854 tags: None,
855 custom_fields: None,
856 },
857 };
858
859 assert_eq!(request.profile.name, "Test Application");
860 assert_eq!(
861 request.profile.business_criticality,
862 BusinessCriticality::Medium
863 );
864 assert!(request.profile.teams.is_some());
865
866 let request_teams = request.profile.teams.unwrap();
867 assert_eq!(request_teams.len(), 2);
868 assert_eq!(
869 request_teams[0].team_name,
870 Some("Security Team".to_string())
871 );
872 assert_eq!(
873 request_teams[1].team_name,
874 Some("Development Team".to_string())
875 );
876 }
877}