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 #[serde(skip_serializing_if = "Option::is_none")]
74 pub custom_kms_alias: Option<String>,
75 #[serde(skip_serializing_if = "Option::is_none")]
77 pub repo_url: Option<String>,
78}
79
80#[derive(Debug, Serialize, Deserialize, Clone)]
81pub struct Settings {
82 pub nextday_consultation_allowed: bool,
84 pub static_scan_xpa_or_dpa: bool,
86 pub dynamic_scan_approval_not_required: bool,
88 pub sca_enabled: bool,
90 pub static_scan_xpp_enabled: bool,
92}
93
94#[derive(Debug, Serialize, Deserialize, Clone)]
96pub struct BusinessUnit {
97 pub id: Option<u64>,
99 pub name: Option<String>,
101 pub guid: Option<String>,
103}
104
105#[derive(Debug, Serialize, Deserialize, Clone)]
107pub struct BusinessOwner {
108 pub email: Option<String>,
110 pub name: Option<String>,
112}
113
114#[derive(Debug, Serialize, Deserialize, Clone)]
116pub struct Policy {
117 pub guid: String,
119 pub name: String,
121 pub is_default: bool,
123 pub policy_compliance_status: Option<String>,
125}
126
127#[derive(Debug, Serialize, Deserialize, Clone)]
129pub struct Team {
130 #[serde(skip_serializing_if = "Option::is_none")]
132 pub guid: Option<String>,
133 #[serde(skip_serializing_if = "Option::is_none")]
135 pub team_id: Option<u64>,
136 #[serde(skip_serializing_if = "Option::is_none")]
138 pub team_name: Option<String>,
139 #[serde(skip_serializing_if = "Option::is_none")]
141 pub team_legacy_id: Option<u64>,
142}
143
144#[derive(Debug, Serialize, Deserialize, Clone)]
146pub struct CustomField {
147 pub name: Option<String>,
149 pub value: Option<String>,
151}
152
153#[derive(Debug, Serialize, Deserialize, Clone)]
155pub struct Scan {
156 pub scan_id: Option<u64>,
158 pub scan_type: Option<String>,
160 pub status: Option<String>,
162 pub scan_url: Option<String>,
164 pub modified_date: Option<String>,
166 pub internal_status: Option<String>,
168 pub links: Option<Vec<Link>>,
170 pub fallback_type: Option<String>,
172 pub full_type: Option<String>,
174}
175
176#[derive(Debug, Serialize, Deserialize, Clone)]
178pub struct Link {
179 pub rel: Option<String>,
181 pub href: Option<String>,
183}
184
185#[derive(Debug, Serialize, Deserialize, Clone)]
187pub struct ApplicationsResponse {
188 #[serde(rename = "_embedded")]
190 pub embedded: Option<EmbeddedApplications>,
191 pub page: Option<PageInfo>,
193 #[serde(rename = "_links")]
195 pub links: Option<HashMap<String, Link>>,
196}
197
198#[derive(Debug, Serialize, Deserialize, Clone)]
200pub struct EmbeddedApplications {
201 pub applications: Vec<Application>,
203}
204
205#[derive(Debug, Serialize, Deserialize, Clone)]
207pub struct PageInfo {
208 pub size: Option<u32>,
210 pub number: Option<u32>,
212 pub total_elements: Option<u64>,
214 pub total_pages: Option<u32>,
216}
217
218#[derive(Debug, Serialize, Deserialize, Clone)]
220pub struct CreateApplicationRequest {
221 pub profile: CreateApplicationProfile,
223}
224
225#[derive(Debug, Serialize, Deserialize, Clone)]
227pub struct CreateApplicationProfile {
228 pub name: String,
230 #[serde(serialize_with = "serialize_business_criticality")]
232 pub business_criticality: BusinessCriticality,
233 #[serde(skip_serializing_if = "Option::is_none")]
235 pub description: Option<String>,
236 #[serde(skip_serializing_if = "Option::is_none")]
238 pub business_unit: Option<BusinessUnit>,
239 #[serde(skip_serializing_if = "Option::is_none")]
241 pub business_owners: Option<Vec<BusinessOwner>>,
242 #[serde(skip_serializing_if = "Option::is_none")]
244 pub policies: Option<Vec<Policy>>,
245 #[serde(skip_serializing_if = "Option::is_none")]
247 pub teams: Option<Vec<Team>>,
248 #[serde(skip_serializing_if = "Option::is_none")]
250 pub tags: Option<String>,
251 #[serde(skip_serializing_if = "Option::is_none")]
253 pub custom_fields: Option<Vec<CustomField>>,
254 #[serde(skip_serializing_if = "Option::is_none")]
256 pub custom_kms_alias: Option<String>,
257 #[serde(skip_serializing_if = "Option::is_none")]
259 pub repo_url: Option<String>,
260}
261
262#[derive(Debug, Clone, Copy, PartialEq, Eq)]
264pub enum BusinessCriticality {
265 VeryHigh,
266 High,
267 Medium,
268 Low,
269 VeryLow,
270}
271
272impl BusinessCriticality {
273 #[must_use]
275 pub fn as_str(&self) -> &'static str {
276 match self {
277 BusinessCriticality::VeryHigh => "VERY_HIGH",
278 BusinessCriticality::High => "HIGH",
279 BusinessCriticality::Medium => "MEDIUM",
280 BusinessCriticality::Low => "LOW",
281 BusinessCriticality::VeryLow => "VERY_LOW",
282 }
283 }
284}
285
286impl From<BusinessCriticality> for String {
287 fn from(criticality: BusinessCriticality) -> Self {
288 criticality.as_str().to_string()
289 }
290}
291
292impl std::fmt::Display for BusinessCriticality {
293 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
294 write!(f, "{}", self.as_str())
295 }
296}
297
298fn serialize_business_criticality<S>(
300 criticality: &BusinessCriticality,
301 serializer: S,
302) -> Result<S::Ok, S::Error>
303where
304 S: serde::Serializer,
305{
306 serializer.serialize_str(criticality.as_str())
307}
308
309impl std::str::FromStr for BusinessCriticality {
311 type Err = String;
312
313 fn from_str(s: &str) -> Result<Self, Self::Err> {
314 match s {
315 "VERY_HIGH" => Ok(BusinessCriticality::VeryHigh),
316 "HIGH" => Ok(BusinessCriticality::High),
317 "MEDIUM" => Ok(BusinessCriticality::Medium),
318 "LOW" => Ok(BusinessCriticality::Low),
319 "VERY_LOW" => Ok(BusinessCriticality::VeryLow),
320 _ => Err(format!(
321 "Invalid business criticality: '{s}'. Must be one of: VERY_HIGH, HIGH, MEDIUM, LOW, VERY_LOW"
322 )),
323 }
324 }
325}
326
327impl<'de> serde::Deserialize<'de> for BusinessCriticality {
329 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
330 where
331 D: serde::Deserializer<'de>,
332 {
333 let s = String::deserialize(deserializer)?;
334 s.parse().map_err(serde::de::Error::custom)
335 }
336}
337
338#[derive(Debug, Serialize, Deserialize, Clone)]
340pub struct UpdateApplicationRequest {
341 pub profile: UpdateApplicationProfile,
343}
344
345#[derive(Debug, Serialize, Deserialize, Clone)]
347pub struct UpdateApplicationProfile {
348 pub name: Option<String>,
350 pub description: Option<String>,
352 pub business_unit: Option<BusinessUnit>,
354 pub business_owners: Option<Vec<BusinessOwner>>,
356 #[serde(serialize_with = "serialize_business_criticality")]
358 pub business_criticality: BusinessCriticality,
359 pub policies: Option<Vec<Policy>>,
361 pub teams: Option<Vec<Team>>,
363 pub tags: Option<String>,
365 pub custom_fields: Option<Vec<CustomField>>,
367 #[serde(skip_serializing_if = "Option::is_none")]
369 pub custom_kms_alias: Option<String>,
370 #[serde(skip_serializing_if = "Option::is_none")]
372 pub repo_url: Option<String>,
373}
374
375#[derive(Debug, Clone, Default)]
377pub struct ApplicationQuery {
378 pub name: Option<String>,
380 pub policy_compliance: Option<String>,
382 pub modified_after: Option<String>,
384 pub modified_before: Option<String>,
386 pub created_after: Option<String>,
388 pub created_before: Option<String>,
390 pub scan_type: Option<String>,
392 pub tags: Option<String>,
394 pub business_unit: Option<String>,
396 pub page: Option<u32>,
398 pub size: Option<u32>,
400}
401
402impl ApplicationQuery {
403 #[must_use]
405 pub fn new() -> Self {
406 ApplicationQuery::default()
407 }
408
409 #[must_use]
411 pub fn with_name(mut self, name: &str) -> Self {
412 self.name = Some(name.to_string());
413 self
414 }
415
416 #[must_use]
418 pub fn with_policy_compliance(mut self, compliance: &str) -> Self {
419 self.policy_compliance = Some(compliance.to_string());
420 self
421 }
422
423 #[must_use]
425 pub fn with_modified_after(mut self, date: &str) -> Self {
426 self.modified_after = Some(date.to_string());
427 self
428 }
429
430 #[must_use]
432 pub fn with_modified_before(mut self, date: &str) -> Self {
433 self.modified_before = Some(date.to_string());
434 self
435 }
436
437 #[must_use]
439 pub fn with_page(mut self, page: u32) -> Self {
440 self.page = Some(page);
441 self
442 }
443
444 #[must_use]
446 pub fn with_size(mut self, size: u32) -> Self {
447 self.size = Some(size);
448 self
449 }
450
451 #[must_use]
453 pub fn to_query_params(&self) -> Vec<(String, String)> {
454 Vec::from(self)
455 }
456}
457
458impl From<&ApplicationQuery> for Vec<(String, String)> {
460 fn from(query: &ApplicationQuery) -> Self {
461 let mut params = Vec::new();
462
463 if let Some(ref name) = query.name {
464 params.push(("name".to_string(), name.clone()));
465 }
466 if let Some(ref compliance) = query.policy_compliance {
467 params.push(("policy_compliance".to_string(), compliance.clone()));
468 }
469 if let Some(ref date) = query.modified_after {
470 params.push(("modified_after".to_string(), date.clone()));
471 }
472 if let Some(ref date) = query.modified_before {
473 params.push(("modified_before".to_string(), date.clone()));
474 }
475 if let Some(ref date) = query.created_after {
476 params.push(("created_after".to_string(), date.clone()));
477 }
478 if let Some(ref date) = query.created_before {
479 params.push(("created_before".to_string(), date.clone()));
480 }
481 if let Some(ref scan_type) = query.scan_type {
482 params.push(("scan_type".to_string(), scan_type.clone()));
483 }
484 if let Some(ref tags) = query.tags {
485 params.push(("tags".to_string(), tags.clone()));
486 }
487 if let Some(ref business_unit) = query.business_unit {
488 params.push(("business_unit".to_string(), business_unit.clone()));
489 }
490 if let Some(page) = query.page {
491 params.push(("page".to_string(), page.to_string()));
492 }
493 if let Some(size) = query.size {
494 params.push(("size".to_string(), size.to_string()));
495 }
496
497 params
498 }
499}
500
501impl From<ApplicationQuery> for Vec<(String, String)> {
503 fn from(query: ApplicationQuery) -> Self {
504 let mut params = Vec::new();
505
506 if let Some(name) = query.name {
507 params.push(("name".to_string(), name));
508 }
509 if let Some(compliance) = query.policy_compliance {
510 params.push(("policy_compliance".to_string(), compliance));
511 }
512 if let Some(date) = query.modified_after {
513 params.push(("modified_after".to_string(), date));
514 }
515 if let Some(date) = query.modified_before {
516 params.push(("modified_before".to_string(), date));
517 }
518 if let Some(date) = query.created_after {
519 params.push(("created_after".to_string(), date));
520 }
521 if let Some(date) = query.created_before {
522 params.push(("created_before".to_string(), date));
523 }
524 if let Some(scan_type) = query.scan_type {
525 params.push(("scan_type".to_string(), scan_type));
526 }
527 if let Some(tags) = query.tags {
528 params.push(("tags".to_string(), tags));
529 }
530 if let Some(business_unit) = query.business_unit {
531 params.push(("business_unit".to_string(), business_unit));
532 }
533 if let Some(page) = query.page {
534 params.push(("page".to_string(), page.to_string()));
535 }
536 if let Some(size) = query.size {
537 params.push(("size".to_string(), size.to_string()));
538 }
539
540 params
541 }
542}
543
544impl VeracodeClient {
546 pub async fn get_applications(
556 &self,
557 query: Option<ApplicationQuery>,
558 ) -> Result<ApplicationsResponse, VeracodeError> {
559 let endpoint = "/appsec/v1/applications";
560 let query_params = query.as_ref().map(Vec::from);
561
562 let response = self.get(endpoint, query_params.as_deref()).await?;
563 let response = Self::handle_response(response).await?;
564
565 let apps_response: ApplicationsResponse = response.json().await?;
566 Ok(apps_response)
567 }
568
569 pub async fn get_application(&self, guid: &str) -> Result<Application, VeracodeError> {
579 let endpoint = format!("/appsec/v1/applications/{guid}");
580
581 let response = self.get(&endpoint, None).await?;
582 let response = Self::handle_response(response).await?;
583
584 let app: Application = response.json().await?;
585 Ok(app)
586 }
587
588 pub async fn create_application(
598 &self,
599 request: &CreateApplicationRequest,
600 ) -> Result<Application, VeracodeError> {
601 let endpoint = "/appsec/v1/applications";
602
603 if let Ok(json_payload) = serde_json::to_string_pretty(&request) {
605 log::debug!(
606 "🔍 Creating application with JSON payload: {}",
607 json_payload
608 );
609 }
610
611 let response = self.post(endpoint, Some(&request)).await?;
612 let response = Self::handle_response(response).await?;
613
614 let app: Application = response.json().await?;
615 Ok(app)
616 }
617
618 pub async fn update_application(
629 &self,
630 guid: &str,
631 request: &UpdateApplicationRequest,
632 ) -> Result<Application, VeracodeError> {
633 let endpoint = format!("/appsec/v1/applications/{guid}");
634
635 let response = self.put(&endpoint, Some(&request)).await?;
636 let response = Self::handle_response(response).await?;
637
638 let app: Application = response.json().await?;
639 Ok(app)
640 }
641
642 pub async fn delete_application(&self, guid: &str) -> Result<(), VeracodeError> {
652 let endpoint = format!("/appsec/v1/applications/{guid}");
653
654 let response = self.delete(&endpoint).await?;
655 let _response = Self::handle_response(response).await?;
656
657 Ok(())
658 }
659
660 pub async fn get_non_compliant_applications(&self) -> Result<Vec<Application>, VeracodeError> {
666 let query = ApplicationQuery::new().with_policy_compliance("DID_NOT_PASS");
667
668 let response = self.get_applications(Some(query)).await?;
669
670 if let Some(embedded) = response.embedded {
671 Ok(embedded.applications)
672 } else {
673 Ok(Vec::new())
674 }
675 }
676
677 pub async fn get_applications_modified_after(
687 &self,
688 date: &str,
689 ) -> Result<Vec<Application>, VeracodeError> {
690 let query = ApplicationQuery::new().with_modified_after(date);
691
692 let response = self.get_applications(Some(query)).await?;
693
694 if let Some(embedded) = response.embedded {
695 Ok(embedded.applications)
696 } else {
697 Ok(Vec::new())
698 }
699 }
700
701 pub async fn search_applications_by_name(
711 &self,
712 name: &str,
713 ) -> Result<Vec<Application>, VeracodeError> {
714 let query = ApplicationQuery::new().with_name(name);
715
716 let response = self.get_applications(Some(query)).await?;
717
718 if let Some(embedded) = response.embedded {
719 Ok(embedded.applications)
720 } else {
721 Ok(Vec::new())
722 }
723 }
724
725 pub async fn get_all_applications(&self) -> Result<Vec<Application>, VeracodeError> {
731 let mut all_applications = Vec::new();
732 let mut page = 0;
733
734 loop {
735 let query = ApplicationQuery::new().with_page(page).with_size(100);
736
737 let response = self.get_applications(Some(query)).await?;
738
739 if let Some(embedded) = response.embedded {
740 if embedded.applications.is_empty() {
741 break;
742 }
743 all_applications.extend(embedded.applications);
744 page += 1;
745 } else {
746 break;
747 }
748 }
749
750 Ok(all_applications)
751 }
752
753 pub async fn get_application_by_name(
763 &self,
764 name: &str,
765 ) -> Result<Option<Application>, VeracodeError> {
766 let applications = self.search_applications_by_name(name).await?;
767
768 Ok(applications.into_iter().find(|app| {
770 if let Some(profile) = &app.profile {
771 profile.name == name
772 } else {
773 false
774 }
775 }))
776 }
777
778 pub async fn application_exists_by_name(&self, name: &str) -> Result<bool, VeracodeError> {
788 match self.get_application_by_name(name).await? {
789 Some(_) => Ok(true),
790 None => Ok(false),
791 }
792 }
793
794 pub async fn get_app_id_from_guid(&self, guid: &str) -> Result<String, VeracodeError> {
806 let app = self.get_application(guid).await?;
807 Ok(app.id.to_string())
808 }
809
810 pub async fn create_application_if_not_exists(
827 &self,
828 name: &str,
829 business_criticality: BusinessCriticality,
830 description: Option<String>,
831 team_names: Option<Vec<String>>,
832 repo_url: Option<String>,
833 ) -> Result<Application, VeracodeError> {
834 if let Some(existing_app) = self.get_application_by_name(name).await? {
836 return Ok(existing_app);
837 }
838
839 let teams = if let Some(names) = team_names {
843 let identity_api = self.identity_api();
844 let mut resolved_teams = Vec::new();
845
846 for team_name in names {
847 match identity_api.get_team_guid_by_name(&team_name).await {
848 Ok(Some(team_guid)) => {
849 resolved_teams.push(Team {
850 guid: Some(team_guid),
851 team_id: None,
852 team_name: None, team_legacy_id: None,
854 });
855 }
856 Ok(None) => {
857 return Err(VeracodeError::NotFound(format!(
858 "Team '{}' not found",
859 team_name
860 )));
861 }
862 Err(identity_err) => {
863 return Err(VeracodeError::InvalidResponse(format!(
864 "Failed to lookup team '{}': {}",
865 team_name, identity_err
866 )));
867 }
868 }
869 }
870
871 Some(resolved_teams)
872 } else {
873 None
874 };
875
876 let create_request = CreateApplicationRequest {
877 profile: CreateApplicationProfile {
878 name: name.to_string(),
879 business_criticality,
880 description,
881 business_unit: None,
882 business_owners: None,
883 policies: None,
884 teams,
885 tags: None,
886 custom_fields: None,
887 custom_kms_alias: None,
888 repo_url,
889 },
890 };
891
892 self.create_application(&create_request).await
893 }
894
895 pub async fn create_application_if_not_exists_with_team_guids(
911 &self,
912 name: &str,
913 business_criticality: BusinessCriticality,
914 description: Option<String>,
915 team_guids: Option<Vec<String>>,
916 ) -> Result<Application, VeracodeError> {
917 if let Some(existing_app) = self.get_application_by_name(name).await? {
919 return Ok(existing_app);
920 }
921
922 let teams = team_guids.map(|guids| {
926 guids
927 .into_iter()
928 .map(|team_guid| Team {
929 guid: Some(team_guid),
930 team_id: None, team_name: None, team_legacy_id: None, })
934 .collect()
935 });
936
937 let create_request = CreateApplicationRequest {
938 profile: CreateApplicationProfile {
939 name: name.to_string(),
940 business_criticality,
941 description,
942 business_unit: None,
943 business_owners: None,
944 policies: None,
945 teams,
946 tags: None,
947 custom_fields: None,
948 custom_kms_alias: None,
949 repo_url: None,
950 },
951 };
952
953 self.create_application(&create_request).await
954 }
955
956 pub async fn create_application_if_not_exists_simple(
971 &self,
972 name: &str,
973 business_criticality: BusinessCriticality,
974 description: Option<String>,
975 ) -> Result<Application, VeracodeError> {
976 self.create_application_if_not_exists(name, business_criticality, description, None, None)
977 .await
978 }
979
980 pub async fn enable_application_encryption(
1016 &self,
1017 app_guid: &str,
1018 kms_alias: &str,
1019 ) -> Result<Application, VeracodeError> {
1020 validate_kms_alias(kms_alias).map_err(VeracodeError::InvalidConfig)?;
1022
1023 let current_app = self.get_application(app_guid).await?;
1025
1026 let profile = current_app
1027 .profile
1028 .ok_or_else(|| VeracodeError::NotFound("Application profile not found".to_string()))?;
1029
1030 let update_request = UpdateApplicationRequest {
1032 profile: UpdateApplicationProfile {
1033 name: Some(profile.name),
1034 description: profile.description,
1035 business_unit: profile.business_unit,
1036 business_owners: profile.business_owners,
1037 business_criticality: profile.business_criticality,
1038 policies: profile.policies,
1039 teams: profile.teams,
1040 tags: profile.tags,
1041 custom_fields: profile.custom_fields,
1042 custom_kms_alias: Some(kms_alias.to_string()),
1043 repo_url: profile.repo_url,
1044 },
1045 };
1046
1047 self.update_application(app_guid, &update_request).await
1048 }
1049
1050 pub async fn change_encryption_key(
1064 &self,
1065 app_guid: &str,
1066 new_kms_alias: &str,
1067 ) -> Result<Application, VeracodeError> {
1068 validate_kms_alias(new_kms_alias).map_err(VeracodeError::InvalidConfig)?;
1070
1071 let current_app = self.get_application(app_guid).await?;
1073
1074 let profile = current_app
1075 .profile
1076 .ok_or_else(|| VeracodeError::NotFound("Application profile not found".to_string()))?;
1077
1078 let update_request = UpdateApplicationRequest {
1080 profile: UpdateApplicationProfile {
1081 name: Some(profile.name),
1082 description: profile.description,
1083 business_unit: profile.business_unit,
1084 business_owners: profile.business_owners,
1085 business_criticality: profile.business_criticality,
1086 policies: profile.policies,
1087 teams: profile.teams,
1088 tags: profile.tags,
1089 custom_fields: profile.custom_fields,
1090 custom_kms_alias: Some(new_kms_alias.to_string()),
1091 repo_url: profile.repo_url,
1092 },
1093 };
1094
1095 self.update_application(app_guid, &update_request).await
1096 }
1097
1098 pub async fn get_application_encryption_status(
1110 &self,
1111 app_guid: &str,
1112 ) -> Result<Option<String>, VeracodeError> {
1113 let app = self.get_application(app_guid).await?;
1114
1115 Ok(app.profile.and_then(|profile| profile.custom_kms_alias))
1117 }
1118}
1119
1120pub fn validate_kms_alias(alias: &str) -> Result<(), String> {
1139 if !alias.starts_with("alias/") {
1141 return Err("KMS alias must start with 'alias/'".to_string());
1142 }
1143
1144 if alias.len() < 8 || alias.len() > 256 {
1146 return Err("KMS alias must be between 8 and 256 characters long".to_string());
1147 }
1148
1149 let alias_name = &alias[6..];
1151
1152 if alias_name.starts_with("aws") || alias_name.ends_with("aws") {
1154 return Err("KMS alias cannot begin or end with 'aws' (reserved by AWS)".to_string());
1155 }
1156
1157 if !alias_name
1159 .chars()
1160 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '/')
1161 {
1162 return Err("KMS alias can only contain alphanumeric characters, hyphens, underscores, and forward slashes".to_string());
1163 }
1164
1165 if alias_name.is_empty() {
1167 return Err("KMS alias name cannot be empty after 'alias/' prefix".to_string());
1168 }
1169
1170 Ok(())
1171}
1172
1173#[cfg(test)]
1174mod tests {
1175 use super::*;
1176
1177 #[test]
1178 fn test_query_params() {
1179 let query = ApplicationQuery::new()
1180 .with_name("test_app")
1181 .with_policy_compliance("PASSED")
1182 .with_page(1)
1183 .with_size(50);
1184
1185 let params = query.to_query_params();
1186 assert!(params.contains(&("name".to_string(), "test_app".to_string())));
1187 assert!(params.contains(&("policy_compliance".to_string(), "PASSED".to_string())));
1188 assert!(params.contains(&("page".to_string(), "1".to_string())));
1189 assert!(params.contains(&("size".to_string(), "50".to_string())));
1190 }
1191
1192 #[test]
1193 fn test_application_query_builder() {
1194 let query = ApplicationQuery::new()
1195 .with_name("MyApp")
1196 .with_policy_compliance("DID_NOT_PASS")
1197 .with_modified_after("2023-01-01T00:00:00.000Z")
1198 .with_page(2)
1199 .with_size(25);
1200
1201 assert_eq!(query.name, Some("MyApp".to_string()));
1202 assert_eq!(query.policy_compliance, Some("DID_NOT_PASS".to_string()));
1203 assert_eq!(
1204 query.modified_after,
1205 Some("2023-01-01T00:00:00.000Z".to_string())
1206 );
1207 assert_eq!(query.page, Some(2));
1208 assert_eq!(query.size, Some(25));
1209 }
1210
1211 #[test]
1212 fn test_create_application_request_with_teams() {
1213 let team_names = vec!["Security Team".to_string(), "Development Team".to_string()];
1214 let teams: Vec<Team> = team_names
1215 .into_iter()
1216 .map(|team_name| Team {
1217 guid: None,
1218 team_id: None,
1219 team_name: Some(team_name),
1220 team_legacy_id: None,
1221 })
1222 .collect();
1223
1224 let request = CreateApplicationRequest {
1225 profile: CreateApplicationProfile {
1226 name: "Test Application".to_string(),
1227 business_criticality: BusinessCriticality::Medium,
1228 description: Some("Test description".to_string()),
1229 business_unit: None,
1230 business_owners: None,
1231 policies: None,
1232 teams: Some(teams.clone()),
1233 tags: None,
1234 custom_fields: None,
1235 custom_kms_alias: None,
1236 repo_url: None,
1237 },
1238 };
1239
1240 assert_eq!(request.profile.name, "Test Application");
1241 assert_eq!(
1242 request.profile.business_criticality,
1243 BusinessCriticality::Medium
1244 );
1245 assert!(request.profile.teams.is_some());
1246
1247 let request_teams = request.profile.teams.unwrap();
1248 assert_eq!(request_teams.len(), 2);
1249 assert_eq!(
1250 request_teams[0].team_name,
1251 Some("Security Team".to_string())
1252 );
1253 assert_eq!(
1254 request_teams[1].team_name,
1255 Some("Development Team".to_string())
1256 );
1257 }
1258
1259 #[test]
1260 fn test_create_application_request_with_team_guids() {
1261 let team_guids = vec!["team-guid-1".to_string(), "team-guid-2".to_string()];
1262 let teams: Vec<Team> = team_guids
1263 .into_iter()
1264 .map(|team_guid| Team {
1265 guid: Some(team_guid),
1266 team_id: None,
1267 team_name: None,
1268 team_legacy_id: None,
1269 })
1270 .collect();
1271
1272 let request = CreateApplicationRequest {
1273 profile: CreateApplicationProfile {
1274 name: "Test Application".to_string(),
1275 business_criticality: BusinessCriticality::High,
1276 description: Some("Test description".to_string()),
1277 business_unit: None,
1278 business_owners: None,
1279 policies: None,
1280 teams: Some(teams.clone()),
1281 tags: None,
1282 custom_fields: None,
1283 custom_kms_alias: None,
1284 repo_url: None,
1285 },
1286 };
1287
1288 assert_eq!(request.profile.name, "Test Application");
1289 assert_eq!(
1290 request.profile.business_criticality,
1291 BusinessCriticality::High
1292 );
1293 assert!(request.profile.teams.is_some());
1294
1295 let request_teams = request.profile.teams.unwrap();
1296 assert_eq!(request_teams.len(), 2);
1297 assert_eq!(request_teams[0].guid, Some("team-guid-1".to_string()));
1298 assert_eq!(request_teams[1].guid, Some("team-guid-2".to_string()));
1299 assert!(request_teams[0].team_name.is_none());
1300 assert!(request_teams[1].team_name.is_none());
1301 }
1302
1303 #[test]
1304 fn test_create_application_profile_cmek_serialization() {
1305 let profile_with_cmek = CreateApplicationProfile {
1307 name: "Test Application".to_string(),
1308 business_criticality: BusinessCriticality::High,
1309 description: None,
1310 business_unit: None,
1311 business_owners: None,
1312 policies: None,
1313 teams: None,
1314 tags: None,
1315 custom_fields: None,
1316 custom_kms_alias: Some("alias/my-app-key".to_string()),
1317 repo_url: None,
1318 };
1319
1320 let json = serde_json::to_string(&profile_with_cmek).unwrap();
1321 assert!(json.contains("custom_kms_alias"));
1322 assert!(json.contains("alias/my-app-key"));
1323
1324 let profile_without_cmek = CreateApplicationProfile {
1326 name: "Test Application".to_string(),
1327 business_criticality: BusinessCriticality::High,
1328 description: None,
1329 business_unit: None,
1330 business_owners: None,
1331 policies: None,
1332 teams: None,
1333 tags: None,
1334 custom_fields: None,
1335 custom_kms_alias: None,
1336 repo_url: None,
1337 };
1338
1339 let json = serde_json::to_string(&profile_without_cmek).unwrap();
1340 assert!(!json.contains("custom_kms_alias"));
1341 }
1342
1343 #[test]
1344 fn test_update_application_profile_cmek_serialization() {
1345 let profile_with_cmek = UpdateApplicationProfile {
1347 name: Some("Updated Application".to_string()),
1348 description: None,
1349 business_unit: None,
1350 business_owners: None,
1351 business_criticality: BusinessCriticality::Medium,
1352 policies: None,
1353 teams: None,
1354 tags: None,
1355 custom_fields: None,
1356 custom_kms_alias: Some("alias/updated-key".to_string()),
1357 repo_url: None,
1358 };
1359
1360 let json = serde_json::to_string(&profile_with_cmek).unwrap();
1361 assert!(json.contains("custom_kms_alias"));
1362 assert!(json.contains("alias/updated-key"));
1363
1364 let profile_without_cmek = UpdateApplicationProfile {
1366 name: Some("Updated Application".to_string()),
1367 description: None,
1368 business_unit: None,
1369 business_owners: None,
1370 business_criticality: BusinessCriticality::Medium,
1371 policies: None,
1372 teams: None,
1373 tags: None,
1374 custom_fields: None,
1375 custom_kms_alias: None,
1376 repo_url: None,
1377 };
1378
1379 let json = serde_json::to_string(&profile_without_cmek).unwrap();
1380 assert!(!json.contains("custom_kms_alias"));
1381 }
1382
1383 #[test]
1384 fn test_validate_kms_alias_valid_cases() {
1385 assert!(validate_kms_alias("alias/my-app-key").is_ok());
1387 assert!(validate_kms_alias("alias/my_app_key_2024").is_ok());
1388 assert!(validate_kms_alias("alias/app/environment/key").is_ok());
1389 assert!(validate_kms_alias("alias/123-test-key").is_ok());
1390 }
1391
1392 #[test]
1393 fn test_validate_kms_alias_invalid_cases() {
1394 assert!(validate_kms_alias("my-app-key").is_err());
1396 assert!(validate_kms_alias("invalid-alias").is_err());
1397
1398 assert!(validate_kms_alias("arn:aws:kms:us-east-1:123456789:alias/my-key").is_err());
1400
1401 assert!(validate_kms_alias("alias/aws-managed").is_err());
1403 assert!(validate_kms_alias("alias/my-key-aws").is_err());
1404
1405 assert!(validate_kms_alias("alias/").is_err());
1407
1408 assert!(validate_kms_alias("alias/a").is_err());
1410
1411 assert!(validate_kms_alias("alias/my@key").is_err());
1413 assert!(validate_kms_alias("alias/my key").is_err());
1414 assert!(validate_kms_alias("alias/my.key").is_err());
1415
1416 let long_alias = format!("alias/{}", "a".repeat(251));
1418 assert!(validate_kms_alias(&long_alias).is_err());
1419 }
1420
1421 #[test]
1422 fn test_cmek_backward_compatibility() {
1423 let legacy_profile = CreateApplicationProfile {
1425 name: "Legacy Application".to_string(),
1426 business_criticality: BusinessCriticality::High,
1427 description: Some("Legacy app without CMEK".to_string()),
1428 business_unit: None,
1429 business_owners: None,
1430 policies: None,
1431 teams: None,
1432 tags: None,
1433 custom_fields: None,
1434 custom_kms_alias: None,
1435 repo_url: None,
1436 };
1437
1438 let request = CreateApplicationRequest {
1439 profile: legacy_profile,
1440 };
1441
1442 let json = serde_json::to_string(&request).unwrap();
1444
1445 assert!(!json.contains("custom_kms_alias"));
1447
1448 assert!(json.contains("name"));
1450 assert!(json.contains("business_criticality"));
1451 assert!(json.contains("Legacy Application"));
1452
1453 let _deserialized: CreateApplicationRequest = serde_json::from_str(&json).unwrap();
1455 }
1456
1457 #[test]
1458 fn test_cmek_field_deserialization() {
1459 let json_with_cmek = r#"{
1461 "profile": {
1462 "name": "Test App",
1463 "business_criticality": "HIGH",
1464 "custom_kms_alias": "alias/test-key"
1465 }
1466 }"#;
1467
1468 let request: CreateApplicationRequest = serde_json::from_str(json_with_cmek).unwrap();
1469 assert_eq!(
1470 request.profile.custom_kms_alias,
1471 Some("alias/test-key".to_string())
1472 );
1473
1474 let json_without_cmek = r#"{
1476 "profile": {
1477 "name": "Test App",
1478 "business_criticality": "HIGH"
1479 }
1480 }"#;
1481
1482 let request: CreateApplicationRequest = serde_json::from_str(json_without_cmek).unwrap();
1483 assert_eq!(request.profile.custom_kms_alias, None);
1484 }
1485}