veracode_platform/
app.rs

1//! Application-specific functionality built on top of the core client.
2//!
3//! This module contains application-specific methods and convenience functions
4//! that use the core `VeracodeClient` to perform application-related operations.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9use crate::VeracodeError;
10use crate::client::VeracodeClient;
11
12/// Represents a Veracode application.
13///
14/// This struct contains all the information about a Veracode application,
15/// including its profile, scans, and metadata.
16#[derive(Debug, Serialize, Deserialize, Clone)]
17pub struct Application {
18    /// Globally unique identifier (GUID) for the application
19    pub guid: String,
20    /// Unique numeric identifier for id the application
21    pub id: u64,
22    /// Organization ID
23    pub oid: Option<u64>,
24    /// Organization ID
25    pub alt_org_id: Option<u64>,
26    /// Unique numeric identifier for `organization_id` the application
27    pub organization_id: Option<u64>,
28    /// ISO 8601 timestamp of the last completed scan
29    pub created: String,
30    /// ISO 8601 timestamp when the application was last modified
31    pub modified: Option<String>,
32    /// ISO 8601 timestamp of the last completed scan
33    pub last_completed_scan_date: Option<String>,
34    /// ISO 8601 timestamp of the last policy compliance check
35    pub last_policy_compliance_check_date: Option<String>,
36    /// URL to the application profile in the Veracode platform
37    pub app_profile_url: Option<String>,
38    /// Detailed application profile information
39    pub profile: Option<Profile>,
40    /// List of scans associated with this application
41    pub scans: Option<Vec<Scan>>,
42    /// URL to the application profile in the Veracode platform
43    pub results_url: Option<String>,
44}
45
46/// Application profile information.
47#[derive(Debug, Serialize, Deserialize, Clone)]
48pub struct Profile {
49    /// Profile name
50    pub name: String,
51    /// Profile description
52    pub description: Option<String>,
53    /// Profile tags
54    pub tags: Option<String>,
55    /// Business unit associated with the application
56    pub business_unit: Option<BusinessUnit>,
57    /// List of business owners
58    pub business_owners: Option<Vec<BusinessOwner>>,
59    /// List of policies applied to the application
60    pub policies: Option<Vec<Policy>>,
61    /// List of teams associated with the application
62    pub teams: Option<Vec<Team>>,
63    /// Archer application name
64    pub archer_app_name: Option<String>,
65    /// Custom fields
66    pub custom_fields: Option<Vec<CustomField>>,
67    /// Business criticality level (required)
68    #[serde(serialize_with = "serialize_business_criticality")]
69    pub business_criticality: BusinessCriticality,
70    /// Application Profile Settings
71    pub settings: Option<Settings>,
72    /// Customer Managed Encryption Key (CMEK) alias for encrypting application data
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub custom_kms_alias: Option<String>,
75    /// Repository URL for the application (e.g., Git repository URL)
76    #[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    /// Profile name
83    pub nextday_consultation_allowed: bool,
84    /// Profile description
85    pub static_scan_xpa_or_dpa: bool,
86    /// Profile tags
87    pub dynamic_scan_approval_not_required: bool,
88    /// Business unit associated with the application
89    pub sca_enabled: bool,
90    /// List of business owners
91    pub static_scan_xpp_enabled: bool,
92}
93
94/// Business unit information.
95#[derive(Debug, Serialize, Deserialize, Clone)]
96pub struct BusinessUnit {
97    /// Business unit ID
98    pub id: Option<u64>,
99    /// Business unit name
100    pub name: Option<String>,
101    /// Business unit GUID
102    pub guid: Option<String>,
103}
104
105/// Business owner information.
106#[derive(Debug, Serialize, Deserialize, Clone)]
107pub struct BusinessOwner {
108    /// Owner's email address
109    pub email: Option<String>,
110    /// Owner's name
111    pub name: Option<String>,
112}
113
114/// Policy information.
115#[derive(Debug, Serialize, Deserialize, Clone)]
116pub struct Policy {
117    /// Policy GUID
118    pub guid: String,
119    /// Policy name
120    pub name: String,
121    /// Whether this is the default policy
122    pub is_default: bool,
123    /// Policy compliance status
124    pub policy_compliance_status: Option<String>,
125}
126
127/// Team information.
128#[derive(Debug, Serialize, Deserialize, Clone)]
129pub struct Team {
130    /// Team GUID (primary identifier)
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub guid: Option<String>,
133    /// Team ID
134    #[serde(skip_serializing_if = "Option::is_none")]
135    pub team_id: Option<u64>,
136    /// Team name
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub team_name: Option<String>,
139    /// Legacy team ID
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub team_legacy_id: Option<u64>,
142}
143
144/// Custom field information.
145#[derive(Debug, Serialize, Deserialize, Clone)]
146pub struct CustomField {
147    /// Field name
148    pub name: Option<String>,
149    /// Field value
150    pub value: Option<String>,
151}
152
153/// Scan information.
154#[derive(Debug, Serialize, Deserialize, Clone)]
155pub struct Scan {
156    /// Scan ID
157    pub scan_id: Option<u64>,
158    /// Type of scan (STATIC, DYNAMIC, etc.)
159    pub scan_type: Option<String>,
160    /// Scan status
161    pub status: Option<String>,
162    /// URL to the scan results
163    pub scan_url: Option<String>,
164    /// When the scan was last modified
165    pub modified_date: Option<String>,
166    /// Internal scan status
167    pub internal_status: Option<String>,
168    /// Related links
169    pub links: Option<Vec<Link>>,
170    /// Fallback scan type
171    pub fallback_type: Option<String>,
172    /// Full scan type
173    pub full_type: Option<String>,
174}
175
176/// Link information.
177#[derive(Debug, Serialize, Deserialize, Clone)]
178pub struct Link {
179    /// Link relationship
180    pub rel: Option<String>,
181    /// Link URL
182    pub href: Option<String>,
183}
184
185/// Response from the Applications API.
186#[derive(Debug, Serialize, Deserialize, Clone)]
187pub struct ApplicationsResponse {
188    /// Embedded applications data
189    #[serde(rename = "_embedded")]
190    pub embedded: Option<EmbeddedApplications>,
191    /// Pagination information
192    pub page: Option<PageInfo>,
193    /// Response links
194    #[serde(rename = "_links")]
195    pub links: Option<HashMap<String, Link>>,
196}
197
198/// Embedded applications in the response.
199#[derive(Debug, Serialize, Deserialize, Clone)]
200pub struct EmbeddedApplications {
201    /// List of applications
202    pub applications: Vec<Application>,
203}
204
205/// Pagination information.
206#[derive(Debug, Serialize, Deserialize, Clone)]
207pub struct PageInfo {
208    /// Number of items per page
209    pub size: Option<u32>,
210    /// Current page number
211    pub number: Option<u32>,
212    /// Total number of elements
213    pub total_elements: Option<u64>,
214    /// Total number of pages
215    pub total_pages: Option<u32>,
216}
217
218/// Request for creating a new application.
219#[derive(Debug, Serialize, Deserialize, Clone)]
220pub struct CreateApplicationRequest {
221    /// Application profile information
222    pub profile: CreateApplicationProfile,
223}
224
225/// Profile information for creating an application.
226#[derive(Debug, Serialize, Deserialize, Clone)]
227pub struct CreateApplicationProfile {
228    /// Application name
229    pub name: String,
230    /// Business criticality level (required)
231    #[serde(serialize_with = "serialize_business_criticality")]
232    pub business_criticality: BusinessCriticality,
233    /// Application description
234    #[serde(skip_serializing_if = "Option::is_none")]
235    pub description: Option<String>,
236    /// Business unit
237    #[serde(skip_serializing_if = "Option::is_none")]
238    pub business_unit: Option<BusinessUnit>,
239    /// Business owners
240    #[serde(skip_serializing_if = "Option::is_none")]
241    pub business_owners: Option<Vec<BusinessOwner>>,
242    /// Policies
243    #[serde(skip_serializing_if = "Option::is_none")]
244    pub policies: Option<Vec<Policy>>,
245    /// Teams
246    #[serde(skip_serializing_if = "Option::is_none")]
247    pub teams: Option<Vec<Team>>,
248    /// Tags
249    #[serde(skip_serializing_if = "Option::is_none")]
250    pub tags: Option<String>,
251    /// Custom fields
252    #[serde(skip_serializing_if = "Option::is_none")]
253    pub custom_fields: Option<Vec<CustomField>>,
254    /// Customer Managed Encryption Key (CMEK) alias for encrypting application data
255    #[serde(skip_serializing_if = "Option::is_none")]
256    pub custom_kms_alias: Option<String>,
257    /// Repository URL for the application (e.g., Git repository URL)
258    #[serde(skip_serializing_if = "Option::is_none")]
259    pub repo_url: Option<String>,
260}
261
262/// Business criticality levels for applications
263#[derive(Debug, Clone, Copy, PartialEq, Eq)]
264pub enum BusinessCriticality {
265    VeryHigh,
266    High,
267    Medium,
268    Low,
269    VeryLow,
270}
271
272impl BusinessCriticality {
273    /// Convert to the string value expected by the API
274    #[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
298/// Custom serializer for `BusinessCriticality`
299fn 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
309/// Parse `BusinessCriticality` from string
310impl 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
327/// Deserialize `BusinessCriticality` from string
328impl<'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/// Request for updating an application.
339#[derive(Debug, Serialize, Deserialize, Clone)]
340pub struct UpdateApplicationRequest {
341    /// Application profile information
342    pub profile: UpdateApplicationProfile,
343}
344
345/// Profile information for updating an application.
346#[derive(Debug, Serialize, Deserialize, Clone)]
347pub struct UpdateApplicationProfile {
348    /// Application name
349    pub name: Option<String>,
350    /// Application description
351    pub description: Option<String>,
352    /// Business unit
353    pub business_unit: Option<BusinessUnit>,
354    /// Business owners
355    pub business_owners: Option<Vec<BusinessOwner>>,
356    /// Business criticality level (required)
357    #[serde(serialize_with = "serialize_business_criticality")]
358    pub business_criticality: BusinessCriticality,
359    /// Policies
360    pub policies: Option<Vec<Policy>>,
361    /// Teams
362    pub teams: Option<Vec<Team>>,
363    /// Tags
364    pub tags: Option<String>,
365    /// Custom fields
366    pub custom_fields: Option<Vec<CustomField>>,
367    /// Customer Managed Encryption Key (CMEK) alias for encrypting application data
368    #[serde(skip_serializing_if = "Option::is_none")]
369    pub custom_kms_alias: Option<String>,
370    /// Repository URL for the application (e.g., Git repository URL)
371    #[serde(skip_serializing_if = "Option::is_none")]
372    pub repo_url: Option<String>,
373}
374
375/// Query parameters for filtering applications.
376#[derive(Debug, Clone, Default)]
377pub struct ApplicationQuery {
378    /// Filter by application name (partial match)
379    pub name: Option<String>,
380    /// Filter by policy compliance status (PASSED, `DID_NOT_PASS`, etc.)
381    pub policy_compliance: Option<String>,
382    /// Filter applications modified after this date (ISO 8601 format)
383    pub modified_after: Option<String>,
384    /// Filter applications modified before this date (ISO 8601 format)
385    pub modified_before: Option<String>,
386    /// Filter applications created after this date (ISO 8601 format)
387    pub created_after: Option<String>,
388    /// Filter applications created before this date (ISO 8601 format)
389    pub created_before: Option<String>,
390    /// Filter by scan type (STATIC, DYNAMIC, MANUAL, SCA)
391    pub scan_type: Option<String>,
392    /// Filter by tags (comma-separated)
393    pub tags: Option<String>,
394    /// Filter by business unit name
395    pub business_unit: Option<String>,
396    /// Page number for pagination (0-based)
397    pub page: Option<u32>,
398    /// Number of items per page (default: 20, max: 500)
399    pub size: Option<u32>,
400}
401
402impl ApplicationQuery {
403    /// Create a new empty query.
404    #[must_use]
405    pub fn new() -> Self {
406        ApplicationQuery::default()
407    }
408
409    /// Filter applications by name (partial match).
410    #[must_use]
411    pub fn with_name(mut self, name: &str) -> Self {
412        self.name = Some(name.to_string());
413        self
414    }
415
416    /// Filter applications by policy compliance status.
417    #[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    /// Filter applications modified after the specified date.
424    #[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    /// Filter applications modified before the specified date.
431    #[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    /// Set the page number for pagination.
438    #[must_use]
439    pub fn with_page(mut self, page: u32) -> Self {
440        self.page = Some(page);
441        self
442    }
443
444    /// Set the number of items per page.
445    #[must_use]
446    pub fn with_size(mut self, size: u32) -> Self {
447        self.size = Some(size);
448        self
449    }
450
451    /// Convert the query to URL query parameters.
452    #[must_use]
453    pub fn to_query_params(&self) -> Vec<(String, String)> {
454        Vec::from(self)
455    }
456}
457
458/// Convert `ApplicationQuery` to query parameters by borrowing (allows reuse)
459impl 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
501/// Convert `ApplicationQuery` to query parameters by consuming (better performance)
502impl 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
544/// Application-specific methods that build on the core client.
545impl VeracodeClient {
546    /// Get all applications with optional filtering.
547    ///
548    /// # Arguments
549    ///
550    /// * `query` - Optional query parameters to filter the results
551    ///
552    /// # Returns
553    ///
554    /// A `Result` containing an `ApplicationsResponse` with the list of applications.
555    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    /// Get a specific application by its GUID.
570    ///
571    /// # Arguments
572    ///
573    /// * `guid` - The GUID of the application to retrieve
574    ///
575    /// # Returns
576    ///
577    /// A `Result` containing the `Application` details.
578    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    /// Create a new application.
589    ///
590    /// # Arguments
591    ///
592    /// * `request` - The application creation request containing profile information
593    ///
594    /// # Returns
595    ///
596    /// A `Result` containing the created `Application`.
597    pub async fn create_application(
598        &self,
599        request: &CreateApplicationRequest,
600    ) -> Result<Application, VeracodeError> {
601        let endpoint = "/appsec/v1/applications";
602
603        // Debug: Log the exact JSON being sent to the API
604        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    /// Update an existing application.
619    ///
620    /// # Arguments
621    ///
622    /// * `guid` - The GUID of the application to update
623    /// * `request` - The update request containing the new profile information
624    ///
625    /// # Returns
626    ///
627    /// A `Result` containing the updated `Application`.
628    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    /// Delete an application.
643    ///
644    /// # Arguments
645    ///
646    /// * `guid` - The GUID of the application to delete
647    ///
648    /// # Returns
649    ///
650    /// A `Result` indicating success or failure.
651    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    /// Get applications that failed policy compliance.
661    ///
662    /// # Returns
663    ///
664    /// A `Result` containing a `Vec<Application>` of non-compliant applications.
665    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    /// Get applications modified after a specific date.
678    ///
679    /// # Arguments
680    ///
681    /// * `date` - ISO 8601 formatted date string
682    ///
683    /// # Returns
684    ///
685    /// A `Result` containing a `Vec<Application>` of applications modified after the date.
686    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    /// Search applications by name.
702    ///
703    /// # Arguments
704    ///
705    /// * `name` - The name to search for (partial matches are supported)
706    ///
707    /// # Returns
708    ///
709    /// A `Result` containing a `Vec<Application>` of applications matching the name.
710    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    /// Get all applications with automatic pagination.
726    ///
727    /// # Returns
728    ///
729    /// A `Result` containing a `Vec<Application>` of all applications.
730    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    /// Get application by name (exact match).
754    ///
755    /// # Arguments
756    ///
757    /// * `name` - The exact name of the application to find
758    ///
759    /// # Returns
760    ///
761    /// A `Result` containing an `Option<Application>` if found.
762    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        // Find exact match (search_applications_by_name does partial matching)
769        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    /// Check if application exists by name.
779    ///
780    /// # Arguments
781    ///
782    /// * `name` - The name of the application to check
783    ///
784    /// # Returns
785    ///
786    /// A `Result` containing a boolean indicating if the application exists.
787    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    /// Get numeric `app_id` from application GUID.
795    ///
796    /// This is needed for XML API operations that require numeric IDs.
797    ///
798    /// # Arguments
799    ///
800    /// * `guid` - The application GUID
801    ///
802    /// # Returns
803    ///
804    /// A `Result` containing the numeric `app_id` as a string.
805    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    /// Create application if it doesn't exist, or return existing application.
811    ///
812    /// This method implements the "check and create" pattern commonly needed
813    /// for automated workflows.
814    ///
815    /// # Arguments
816    ///
817    /// * `name` - The name of the application
818    /// * `business_criticality` - Business criticality level (required for creation)
819    /// * `description` - Optional description for new applications
820    /// * `team_names` - Optional list of team names to assign to the application
821    /// * `repo_url` - Optional repository URL for the application (e.g., Git repository URL)
822    ///
823    /// # Returns
824    ///
825    /// A `Result` containing the application (existing or newly created).
826    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        // First, check if application already exists
835        if let Some(existing_app) = self.get_application_by_name(name).await? {
836            return Ok(existing_app);
837        }
838
839        // Application doesn't exist, create it
840
841        // Convert team names to Team objects with GUIDs if provided
842        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, // Not needed when using GUID
853                            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    /// Create application if it doesn't exist, or return existing application (with team GUIDs).
896    ///
897    /// This method allows specifying teams by their GUID, which is the preferred
898    /// approach for programmatic application creation.
899    ///
900    /// # Arguments
901    ///
902    /// * `name` - The name of the application
903    /// * `business_criticality` - Business criticality level (required for creation)
904    /// * `description` - Optional description for new applications
905    /// * `team_guids` - Optional list of team GUIDs to assign to the application
906    ///
907    /// # Returns
908    ///
909    /// A `Result` containing the application (existing or newly created).
910    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        // First, check if application already exists
918        if let Some(existing_app) = self.get_application_by_name(name).await? {
919            return Ok(existing_app);
920        }
921
922        // Application doesn't exist, create it
923
924        // Convert team GUIDs to Team objects if provided
925        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,        // Will be assigned by Veracode
931                    team_name: None,      // Not needed when using GUID
932                    team_legacy_id: None, // Will be assigned by Veracode
933                })
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    /// Create application if it doesn't exist, or return existing application (without teams).
957    ///
958    /// This is a convenience method that maintains backward compatibility
959    /// for callers that don't need to specify teams.
960    ///
961    /// # Arguments
962    ///
963    /// * `name` - The name of the application
964    /// * `business_criticality` - Business criticality level (required for creation)
965    /// * `description` - Optional description for new applications
966    ///
967    /// # Returns
968    ///
969    /// A `Result` containing the application (existing or newly created).
970    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    /// Enable Customer Managed Encryption Key (CMEK) on an application
981    ///
982    /// This method updates an existing application to use a customer-managed encryption key.
983    /// The KMS alias must be properly formatted and the key must be accessible to Veracode.
984    ///
985    /// # Arguments
986    ///
987    /// * `app_guid` - The GUID of the application to enable encryption on
988    /// * `kms_alias` - The AWS KMS alias to use for encryption (must start with "alias/")
989    ///
990    /// # Returns
991    ///
992    /// A `Result` containing the updated application or an error.
993    ///
994    /// # Examples
995    ///
996    /// ```no_run
997    /// # use veracode_platform::{VeracodeClient, VeracodeConfig};
998    /// # use std::sync::Arc;
999    /// # use secrecy::SecretString;
1000    /// # #[tokio::main]
1001    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1002    /// let config = VeracodeConfig::from_arc_credentials(
1003    ///     Arc::new(SecretString::from("api_id")),
1004    ///     Arc::new(SecretString::from("api_key"))
1005    /// );
1006    /// let client = VeracodeClient::new(config)?;
1007    ///
1008    /// let app = client.enable_application_encryption(
1009    ///     "app-guid-123",
1010    ///     "alias/my-encryption-key"
1011    /// ).await?;
1012    /// # Ok(())
1013    /// # }
1014    /// ```
1015    pub async fn enable_application_encryption(
1016        &self,
1017        app_guid: &str,
1018        kms_alias: &str,
1019    ) -> Result<Application, VeracodeError> {
1020        // Validate KMS alias format
1021        validate_kms_alias(kms_alias).map_err(VeracodeError::InvalidConfig)?;
1022
1023        // Get current application to preserve existing settings
1024        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        // Create update request with CMEK enabled
1031        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    /// Change the encryption key for an application with CMEK enabled
1051    ///
1052    /// This method updates the KMS alias used for encrypting an application's data.
1053    /// The application must already have CMEK enabled.
1054    ///
1055    /// # Arguments
1056    ///
1057    /// * `app_guid` - The GUID of the application to update
1058    /// * `new_kms_alias` - The new AWS KMS alias to use for encryption
1059    ///
1060    /// # Returns
1061    ///
1062    /// A `Result` containing the updated application or an error.
1063    pub async fn change_encryption_key(
1064        &self,
1065        app_guid: &str,
1066        new_kms_alias: &str,
1067    ) -> Result<Application, VeracodeError> {
1068        // Validate new KMS alias format
1069        validate_kms_alias(new_kms_alias).map_err(VeracodeError::InvalidConfig)?;
1070
1071        // Get current application
1072        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        // Create update request with new KMS alias
1079        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    /// Get the encryption status of an application
1099    ///
1100    /// This method retrieves the current CMEK configuration for an application.
1101    ///
1102    /// # Arguments
1103    ///
1104    /// * `app_guid` - The GUID of the application to check
1105    ///
1106    /// # Returns
1107    ///
1108    /// A `Result` containing the KMS alias if CMEK is enabled, None if disabled, or an error.
1109    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        // CMEK is stored directly in the profile as custom_kms_alias, not in custom_fields
1116        Ok(app.profile.and_then(|profile| profile.custom_kms_alias))
1117    }
1118}
1119
1120/// Validates an AWS KMS alias format
1121///
1122/// AWS KMS aliases must follow specific naming conventions:
1123/// - Must be prefixed with "alias/"
1124/// - Total length must be between 8-256 characters
1125/// - Can contain alphanumeric characters, hyphens, underscores, and forward slashes
1126/// - Cannot begin or end with "aws" (reserved by AWS)
1127///
1128/// # Examples
1129///
1130/// ```
1131/// use veracode_platform::app::validate_kms_alias;
1132///
1133/// assert!(validate_kms_alias("alias/my-app-key").is_ok());
1134/// assert!(validate_kms_alias("alias/my_app_key_2024").is_ok());
1135/// assert!(validate_kms_alias("invalid-alias").is_err());
1136/// assert!(validate_kms_alias("alias/aws-managed").is_err());
1137/// ```
1138pub fn validate_kms_alias(alias: &str) -> Result<(), String> {
1139    // Check prefix
1140    if !alias.starts_with("alias/") {
1141        return Err("KMS alias must start with 'alias/'".to_string());
1142    }
1143
1144    // Check length (including the "alias/" prefix) - minimum 8 characters for meaningful alias
1145    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    // Extract the alias name part (after "alias/")
1150    let alias_name = &alias[6..];
1151
1152    // Check for AWS reserved prefixes
1153    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    // Check valid characters: alphanumeric, hyphens, underscores, forward slashes
1158    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    // Check that it's not empty after prefix
1166    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        // Test that custom_kms_alias is included when Some
1306        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        // Test that custom_kms_alias is excluded when None
1325        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        // Test that custom_kms_alias is included when Some
1346        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        // Test that custom_kms_alias is excluded when None
1365        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        // Valid aliases
1386        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        // Missing prefix
1395        assert!(validate_kms_alias("my-app-key").is_err());
1396        assert!(validate_kms_alias("invalid-alias").is_err());
1397
1398        // Wrong prefix
1399        assert!(validate_kms_alias("arn:aws:kms:us-east-1:123456789:alias/my-key").is_err());
1400
1401        // AWS reserved names
1402        assert!(validate_kms_alias("alias/aws-managed").is_err());
1403        assert!(validate_kms_alias("alias/my-key-aws").is_err());
1404
1405        // Empty alias name
1406        assert!(validate_kms_alias("alias/").is_err());
1407
1408        // Too short
1409        assert!(validate_kms_alias("alias/a").is_err());
1410
1411        // Invalid characters
1412        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        // Too long (over 256 characters)
1417        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        // Test that existing application creation still works without CMEK field
1424        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        // Should serialize successfully
1443        let json = serde_json::to_string(&request).unwrap();
1444
1445        // Should not contain CMEK field
1446        assert!(!json.contains("custom_kms_alias"));
1447
1448        // Should still contain required fields
1449        assert!(json.contains("name"));
1450        assert!(json.contains("business_criticality"));
1451        assert!(json.contains("Legacy Application"));
1452
1453        // Should be able to deserialize back
1454        let _deserialized: CreateApplicationRequest = serde_json::from_str(&json).unwrap();
1455    }
1456
1457    #[test]
1458    fn test_cmek_field_deserialization() {
1459        // Test deserializing JSON with CMEK field
1460        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        // Test deserializing JSON without CMEK field (backward compatibility)
1475        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}