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}
76
77#[derive(Debug, Serialize, Deserialize, Clone)]
78pub struct Settings {
79    /// Profile name
80    pub nextday_consultation_allowed: bool,
81    /// Profile description
82    pub static_scan_xpa_or_dpa: bool,
83    /// Profile tags
84    pub dynamic_scan_approval_not_required: bool,
85    /// Business unit associated with the application
86    pub sca_enabled: bool,
87    /// List of business owners
88    pub static_scan_xpp_enabled: bool,
89}
90
91/// Business unit information.
92#[derive(Debug, Serialize, Deserialize, Clone)]
93pub struct BusinessUnit {
94    /// Business unit ID
95    pub id: Option<u64>,
96    /// Business unit name
97    pub name: Option<String>,
98    /// Business unit GUID
99    pub guid: Option<String>,
100}
101
102/// Business owner information.
103#[derive(Debug, Serialize, Deserialize, Clone)]
104pub struct BusinessOwner {
105    /// Owner's email address
106    pub email: Option<String>,
107    /// Owner's name
108    pub name: Option<String>,
109}
110
111/// Policy information.
112#[derive(Debug, Serialize, Deserialize, Clone)]
113pub struct Policy {
114    /// Policy GUID
115    pub guid: String,
116    /// Policy name
117    pub name: String,
118    /// Whether this is the default policy
119    pub is_default: bool,
120    /// Policy compliance status
121    pub policy_compliance_status: Option<String>,
122}
123
124/// Team information.
125#[derive(Debug, Serialize, Deserialize, Clone)]
126pub struct Team {
127    /// Team GUID (primary identifier)
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub guid: Option<String>,
130    /// Team ID
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub team_id: Option<u64>,
133    /// Team name
134    #[serde(skip_serializing_if = "Option::is_none")]
135    pub team_name: Option<String>,
136    /// Legacy team ID
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub team_legacy_id: Option<u64>,
139}
140
141/// Custom field information.
142#[derive(Debug, Serialize, Deserialize, Clone)]
143pub struct CustomField {
144    /// Field name
145    pub name: Option<String>,
146    /// Field value
147    pub value: Option<String>,
148}
149
150/// Scan information.
151#[derive(Debug, Serialize, Deserialize, Clone)]
152pub struct Scan {
153    /// Scan ID
154    pub scan_id: Option<u64>,
155    /// Type of scan (STATIC, DYNAMIC, etc.)
156    pub scan_type: Option<String>,
157    /// Scan status
158    pub status: Option<String>,
159    /// URL to the scan results
160    pub scan_url: Option<String>,
161    /// When the scan was last modified
162    pub modified_date: Option<String>,
163    /// Internal scan status
164    pub internal_status: Option<String>,
165    /// Related links
166    pub links: Option<Vec<Link>>,
167    /// Fallback scan type
168    pub fallback_type: Option<String>,
169    /// Full scan type
170    pub full_type: Option<String>,
171}
172
173/// Link information.
174#[derive(Debug, Serialize, Deserialize, Clone)]
175pub struct Link {
176    /// Link relationship
177    pub rel: Option<String>,
178    /// Link URL
179    pub href: Option<String>,
180}
181
182/// Response from the Applications API.
183#[derive(Debug, Serialize, Deserialize, Clone)]
184pub struct ApplicationsResponse {
185    /// Embedded applications data
186    #[serde(rename = "_embedded")]
187    pub embedded: Option<EmbeddedApplications>,
188    /// Pagination information
189    pub page: Option<PageInfo>,
190    /// Response links
191    #[serde(rename = "_links")]
192    pub links: Option<HashMap<String, Link>>,
193}
194
195/// Embedded applications in the response.
196#[derive(Debug, Serialize, Deserialize, Clone)]
197pub struct EmbeddedApplications {
198    /// List of applications
199    pub applications: Vec<Application>,
200}
201
202/// Pagination information.
203#[derive(Debug, Serialize, Deserialize, Clone)]
204pub struct PageInfo {
205    /// Number of items per page
206    pub size: Option<u32>,
207    /// Current page number
208    pub number: Option<u32>,
209    /// Total number of elements
210    pub total_elements: Option<u64>,
211    /// Total number of pages
212    pub total_pages: Option<u32>,
213}
214
215/// Request for creating a new application.
216#[derive(Debug, Serialize, Deserialize, Clone)]
217pub struct CreateApplicationRequest {
218    /// Application profile information
219    pub profile: CreateApplicationProfile,
220}
221
222/// Profile information for creating an application.
223#[derive(Debug, Serialize, Deserialize, Clone)]
224pub struct CreateApplicationProfile {
225    /// Application name
226    pub name: String,
227    /// Business criticality level (required)
228    #[serde(serialize_with = "serialize_business_criticality")]
229    pub business_criticality: BusinessCriticality,
230    /// Application description
231    #[serde(skip_serializing_if = "Option::is_none")]
232    pub description: Option<String>,
233    /// Business unit
234    #[serde(skip_serializing_if = "Option::is_none")]
235    pub business_unit: Option<BusinessUnit>,
236    /// Business owners
237    #[serde(skip_serializing_if = "Option::is_none")]
238    pub business_owners: Option<Vec<BusinessOwner>>,
239    /// Policies
240    #[serde(skip_serializing_if = "Option::is_none")]
241    pub policies: Option<Vec<Policy>>,
242    /// Teams
243    #[serde(skip_serializing_if = "Option::is_none")]
244    pub teams: Option<Vec<Team>>,
245    /// Tags
246    #[serde(skip_serializing_if = "Option::is_none")]
247    pub tags: Option<String>,
248    /// Custom fields
249    #[serde(skip_serializing_if = "Option::is_none")]
250    pub custom_fields: Option<Vec<CustomField>>,
251    /// Customer Managed Encryption Key (CMEK) alias for encrypting application data
252    #[serde(skip_serializing_if = "Option::is_none")]
253    pub custom_kms_alias: Option<String>,
254}
255
256/// Business criticality levels for applications
257#[derive(Debug, Clone, Copy, PartialEq, Eq)]
258pub enum BusinessCriticality {
259    VeryHigh,
260    High,
261    Medium,
262    Low,
263    VeryLow,
264}
265
266impl BusinessCriticality {
267    /// Convert to the string value expected by the API
268    #[must_use]
269    pub fn as_str(&self) -> &'static str {
270        match self {
271            BusinessCriticality::VeryHigh => "VERY_HIGH",
272            BusinessCriticality::High => "HIGH",
273            BusinessCriticality::Medium => "MEDIUM",
274            BusinessCriticality::Low => "LOW",
275            BusinessCriticality::VeryLow => "VERY_LOW",
276        }
277    }
278}
279
280impl From<BusinessCriticality> for String {
281    fn from(criticality: BusinessCriticality) -> Self {
282        criticality.as_str().to_string()
283    }
284}
285
286impl std::fmt::Display for BusinessCriticality {
287    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
288        write!(f, "{}", self.as_str())
289    }
290}
291
292/// Custom serializer for `BusinessCriticality`
293fn serialize_business_criticality<S>(
294    criticality: &BusinessCriticality,
295    serializer: S,
296) -> Result<S::Ok, S::Error>
297where
298    S: serde::Serializer,
299{
300    serializer.serialize_str(criticality.as_str())
301}
302
303/// Parse `BusinessCriticality` from string
304impl std::str::FromStr for BusinessCriticality {
305    type Err = String;
306
307    fn from_str(s: &str) -> Result<Self, Self::Err> {
308        match s {
309            "VERY_HIGH" => Ok(BusinessCriticality::VeryHigh),
310            "HIGH" => Ok(BusinessCriticality::High),
311            "MEDIUM" => Ok(BusinessCriticality::Medium),
312            "LOW" => Ok(BusinessCriticality::Low),
313            "VERY_LOW" => Ok(BusinessCriticality::VeryLow),
314            _ => Err(format!(
315                "Invalid business criticality: '{s}'. Must be one of: VERY_HIGH, HIGH, MEDIUM, LOW, VERY_LOW"
316            )),
317        }
318    }
319}
320
321/// Deserialize `BusinessCriticality` from string
322impl<'de> serde::Deserialize<'de> for BusinessCriticality {
323    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
324    where
325        D: serde::Deserializer<'de>,
326    {
327        let s = String::deserialize(deserializer)?;
328        s.parse().map_err(serde::de::Error::custom)
329    }
330}
331
332/// Request for updating an application.
333#[derive(Debug, Serialize, Deserialize, Clone)]
334pub struct UpdateApplicationRequest {
335    /// Application profile information
336    pub profile: UpdateApplicationProfile,
337}
338
339/// Profile information for updating an application.
340#[derive(Debug, Serialize, Deserialize, Clone)]
341pub struct UpdateApplicationProfile {
342    /// Application name
343    pub name: Option<String>,
344    /// Application description
345    pub description: Option<String>,
346    /// Business unit
347    pub business_unit: Option<BusinessUnit>,
348    /// Business owners
349    pub business_owners: Option<Vec<BusinessOwner>>,
350    /// Business criticality level (required)
351    #[serde(serialize_with = "serialize_business_criticality")]
352    pub business_criticality: BusinessCriticality,
353    /// Policies
354    pub policies: Option<Vec<Policy>>,
355    /// Teams
356    pub teams: Option<Vec<Team>>,
357    /// Tags
358    pub tags: Option<String>,
359    /// Custom fields
360    pub custom_fields: Option<Vec<CustomField>>,
361    /// Customer Managed Encryption Key (CMEK) alias for encrypting application data
362    #[serde(skip_serializing_if = "Option::is_none")]
363    pub custom_kms_alias: Option<String>,
364}
365
366/// Query parameters for filtering applications.
367#[derive(Debug, Clone, Default)]
368pub struct ApplicationQuery {
369    /// Filter by application name (partial match)
370    pub name: Option<String>,
371    /// Filter by policy compliance status (PASSED, `DID_NOT_PASS`, etc.)
372    pub policy_compliance: Option<String>,
373    /// Filter applications modified after this date (ISO 8601 format)
374    pub modified_after: Option<String>,
375    /// Filter applications modified before this date (ISO 8601 format)
376    pub modified_before: Option<String>,
377    /// Filter applications created after this date (ISO 8601 format)
378    pub created_after: Option<String>,
379    /// Filter applications created before this date (ISO 8601 format)
380    pub created_before: Option<String>,
381    /// Filter by scan type (STATIC, DYNAMIC, MANUAL, SCA)
382    pub scan_type: Option<String>,
383    /// Filter by tags (comma-separated)
384    pub tags: Option<String>,
385    /// Filter by business unit name
386    pub business_unit: Option<String>,
387    /// Page number for pagination (0-based)
388    pub page: Option<u32>,
389    /// Number of items per page (default: 20, max: 500)
390    pub size: Option<u32>,
391}
392
393impl ApplicationQuery {
394    /// Create a new empty query.
395    #[must_use]
396    pub fn new() -> Self {
397        ApplicationQuery::default()
398    }
399
400    /// Filter applications by name (partial match).
401    #[must_use]
402    pub fn with_name(mut self, name: &str) -> Self {
403        self.name = Some(name.to_string());
404        self
405    }
406
407    /// Filter applications by policy compliance status.
408    #[must_use]
409    pub fn with_policy_compliance(mut self, compliance: &str) -> Self {
410        self.policy_compliance = Some(compliance.to_string());
411        self
412    }
413
414    /// Filter applications modified after the specified date.
415    #[must_use]
416    pub fn with_modified_after(mut self, date: &str) -> Self {
417        self.modified_after = Some(date.to_string());
418        self
419    }
420
421    /// Filter applications modified before the specified date.
422    #[must_use]
423    pub fn with_modified_before(mut self, date: &str) -> Self {
424        self.modified_before = Some(date.to_string());
425        self
426    }
427
428    /// Set the page number for pagination.
429    #[must_use]
430    pub fn with_page(mut self, page: u32) -> Self {
431        self.page = Some(page);
432        self
433    }
434
435    /// Set the number of items per page.
436    #[must_use]
437    pub fn with_size(mut self, size: u32) -> Self {
438        self.size = Some(size);
439        self
440    }
441
442    /// Convert the query to URL query parameters.
443    #[must_use]
444    pub fn to_query_params(&self) -> Vec<(String, String)> {
445        Vec::from(self)
446    }
447}
448
449/// Convert `ApplicationQuery` to query parameters by borrowing (allows reuse)
450impl From<&ApplicationQuery> for Vec<(String, String)> {
451    fn from(query: &ApplicationQuery) -> Self {
452        let mut params = Vec::new();
453
454        if let Some(ref name) = query.name {
455            params.push(("name".to_string(), name.clone()));
456        }
457        if let Some(ref compliance) = query.policy_compliance {
458            params.push(("policy_compliance".to_string(), compliance.clone()));
459        }
460        if let Some(ref date) = query.modified_after {
461            params.push(("modified_after".to_string(), date.clone()));
462        }
463        if let Some(ref date) = query.modified_before {
464            params.push(("modified_before".to_string(), date.clone()));
465        }
466        if let Some(ref date) = query.created_after {
467            params.push(("created_after".to_string(), date.clone()));
468        }
469        if let Some(ref date) = query.created_before {
470            params.push(("created_before".to_string(), date.clone()));
471        }
472        if let Some(ref scan_type) = query.scan_type {
473            params.push(("scan_type".to_string(), scan_type.clone()));
474        }
475        if let Some(ref tags) = query.tags {
476            params.push(("tags".to_string(), tags.clone()));
477        }
478        if let Some(ref business_unit) = query.business_unit {
479            params.push(("business_unit".to_string(), business_unit.clone()));
480        }
481        if let Some(page) = query.page {
482            params.push(("page".to_string(), page.to_string()));
483        }
484        if let Some(size) = query.size {
485            params.push(("size".to_string(), size.to_string()));
486        }
487
488        params
489    }
490}
491
492/// Convert `ApplicationQuery` to query parameters by consuming (better performance)
493impl From<ApplicationQuery> for Vec<(String, String)> {
494    fn from(query: ApplicationQuery) -> Self {
495        let mut params = Vec::new();
496
497        if let Some(name) = query.name {
498            params.push(("name".to_string(), name));
499        }
500        if let Some(compliance) = query.policy_compliance {
501            params.push(("policy_compliance".to_string(), compliance));
502        }
503        if let Some(date) = query.modified_after {
504            params.push(("modified_after".to_string(), date));
505        }
506        if let Some(date) = query.modified_before {
507            params.push(("modified_before".to_string(), date));
508        }
509        if let Some(date) = query.created_after {
510            params.push(("created_after".to_string(), date));
511        }
512        if let Some(date) = query.created_before {
513            params.push(("created_before".to_string(), date));
514        }
515        if let Some(scan_type) = query.scan_type {
516            params.push(("scan_type".to_string(), scan_type));
517        }
518        if let Some(tags) = query.tags {
519            params.push(("tags".to_string(), tags));
520        }
521        if let Some(business_unit) = query.business_unit {
522            params.push(("business_unit".to_string(), business_unit));
523        }
524        if let Some(page) = query.page {
525            params.push(("page".to_string(), page.to_string()));
526        }
527        if let Some(size) = query.size {
528            params.push(("size".to_string(), size.to_string()));
529        }
530
531        params
532    }
533}
534
535/// Application-specific methods that build on the core client.
536impl VeracodeClient {
537    /// Get all applications with optional filtering.
538    ///
539    /// # Arguments
540    ///
541    /// * `query` - Optional query parameters to filter the results
542    ///
543    /// # Returns
544    ///
545    /// A `Result` containing an `ApplicationsResponse` with the list of applications.
546    pub async fn get_applications(
547        &self,
548        query: Option<ApplicationQuery>,
549    ) -> Result<ApplicationsResponse, VeracodeError> {
550        let endpoint = "/appsec/v1/applications";
551        let query_params = query.as_ref().map(Vec::from);
552
553        let response = self.get(endpoint, query_params.as_deref()).await?;
554        let response = Self::handle_response(response).await?;
555
556        let apps_response: ApplicationsResponse = response.json().await?;
557        Ok(apps_response)
558    }
559
560    /// Get a specific application by its GUID.
561    ///
562    /// # Arguments
563    ///
564    /// * `guid` - The GUID of the application to retrieve
565    ///
566    /// # Returns
567    ///
568    /// A `Result` containing the `Application` details.
569    pub async fn get_application(&self, guid: &str) -> Result<Application, VeracodeError> {
570        let endpoint = format!("/appsec/v1/applications/{guid}");
571
572        let response = self.get(&endpoint, None).await?;
573        let response = Self::handle_response(response).await?;
574
575        let app: Application = response.json().await?;
576        Ok(app)
577    }
578
579    /// Create a new application.
580    ///
581    /// # Arguments
582    ///
583    /// * `request` - The application creation request containing profile information
584    ///
585    /// # Returns
586    ///
587    /// A `Result` containing the created `Application`.
588    pub async fn create_application(
589        &self,
590        request: &CreateApplicationRequest,
591    ) -> Result<Application, VeracodeError> {
592        let endpoint = "/appsec/v1/applications";
593
594        // Debug: Log the exact JSON being sent to the API
595        if let Ok(json_payload) = serde_json::to_string_pretty(&request) {
596            log::debug!(
597                "🔍 Creating application with JSON payload: {}",
598                json_payload
599            );
600        }
601
602        let response = self.post(endpoint, Some(&request)).await?;
603        let response = Self::handle_response(response).await?;
604
605        let app: Application = response.json().await?;
606        Ok(app)
607    }
608
609    /// Update an existing application.
610    ///
611    /// # Arguments
612    ///
613    /// * `guid` - The GUID of the application to update
614    /// * `request` - The update request containing the new profile information
615    ///
616    /// # Returns
617    ///
618    /// A `Result` containing the updated `Application`.
619    pub async fn update_application(
620        &self,
621        guid: &str,
622        request: &UpdateApplicationRequest,
623    ) -> Result<Application, VeracodeError> {
624        let endpoint = format!("/appsec/v1/applications/{guid}");
625
626        let response = self.put(&endpoint, Some(&request)).await?;
627        let response = Self::handle_response(response).await?;
628
629        let app: Application = response.json().await?;
630        Ok(app)
631    }
632
633    /// Delete an application.
634    ///
635    /// # Arguments
636    ///
637    /// * `guid` - The GUID of the application to delete
638    ///
639    /// # Returns
640    ///
641    /// A `Result` indicating success or failure.
642    pub async fn delete_application(&self, guid: &str) -> Result<(), VeracodeError> {
643        let endpoint = format!("/appsec/v1/applications/{guid}");
644
645        let response = self.delete(&endpoint).await?;
646        let _response = Self::handle_response(response).await?;
647
648        Ok(())
649    }
650
651    /// Get applications that failed policy compliance.
652    ///
653    /// # Returns
654    ///
655    /// A `Result` containing a `Vec<Application>` of non-compliant applications.
656    pub async fn get_non_compliant_applications(&self) -> Result<Vec<Application>, VeracodeError> {
657        let query = ApplicationQuery::new().with_policy_compliance("DID_NOT_PASS");
658
659        let response = self.get_applications(Some(query)).await?;
660
661        if let Some(embedded) = response.embedded {
662            Ok(embedded.applications)
663        } else {
664            Ok(Vec::new())
665        }
666    }
667
668    /// Get applications modified after a specific date.
669    ///
670    /// # Arguments
671    ///
672    /// * `date` - ISO 8601 formatted date string
673    ///
674    /// # Returns
675    ///
676    /// A `Result` containing a `Vec<Application>` of applications modified after the date.
677    pub async fn get_applications_modified_after(
678        &self,
679        date: &str,
680    ) -> Result<Vec<Application>, VeracodeError> {
681        let query = ApplicationQuery::new().with_modified_after(date);
682
683        let response = self.get_applications(Some(query)).await?;
684
685        if let Some(embedded) = response.embedded {
686            Ok(embedded.applications)
687        } else {
688            Ok(Vec::new())
689        }
690    }
691
692    /// Search applications by name.
693    ///
694    /// # Arguments
695    ///
696    /// * `name` - The name to search for (partial matches are supported)
697    ///
698    /// # Returns
699    ///
700    /// A `Result` containing a `Vec<Application>` of applications matching the name.
701    pub async fn search_applications_by_name(
702        &self,
703        name: &str,
704    ) -> Result<Vec<Application>, VeracodeError> {
705        let query = ApplicationQuery::new().with_name(name);
706
707        let response = self.get_applications(Some(query)).await?;
708
709        if let Some(embedded) = response.embedded {
710            Ok(embedded.applications)
711        } else {
712            Ok(Vec::new())
713        }
714    }
715
716    /// Get all applications with automatic pagination.
717    ///
718    /// # Returns
719    ///
720    /// A `Result` containing a `Vec<Application>` of all applications.
721    pub async fn get_all_applications(&self) -> Result<Vec<Application>, VeracodeError> {
722        let mut all_applications = Vec::new();
723        let mut page = 0;
724
725        loop {
726            let query = ApplicationQuery::new().with_page(page).with_size(100);
727
728            let response = self.get_applications(Some(query)).await?;
729
730            if let Some(embedded) = response.embedded {
731                if embedded.applications.is_empty() {
732                    break;
733                }
734                all_applications.extend(embedded.applications);
735                page += 1;
736            } else {
737                break;
738            }
739        }
740
741        Ok(all_applications)
742    }
743
744    /// Get application by name (exact match).
745    ///
746    /// # Arguments
747    ///
748    /// * `name` - The exact name of the application to find
749    ///
750    /// # Returns
751    ///
752    /// A `Result` containing an `Option<Application>` if found.
753    pub async fn get_application_by_name(
754        &self,
755        name: &str,
756    ) -> Result<Option<Application>, VeracodeError> {
757        let applications = self.search_applications_by_name(name).await?;
758
759        // Find exact match (search_applications_by_name does partial matching)
760        Ok(applications.into_iter().find(|app| {
761            if let Some(profile) = &app.profile {
762                profile.name == name
763            } else {
764                false
765            }
766        }))
767    }
768
769    /// Check if application exists by name.
770    ///
771    /// # Arguments
772    ///
773    /// * `name` - The name of the application to check
774    ///
775    /// # Returns
776    ///
777    /// A `Result` containing a boolean indicating if the application exists.
778    pub async fn application_exists_by_name(&self, name: &str) -> Result<bool, VeracodeError> {
779        match self.get_application_by_name(name).await? {
780            Some(_) => Ok(true),
781            None => Ok(false),
782        }
783    }
784
785    /// Get numeric `app_id` from application GUID.
786    ///
787    /// This is needed for XML API operations that require numeric IDs.
788    ///
789    /// # Arguments
790    ///
791    /// * `guid` - The application GUID
792    ///
793    /// # Returns
794    ///
795    /// A `Result` containing the numeric `app_id` as a string.
796    pub async fn get_app_id_from_guid(&self, guid: &str) -> Result<String, VeracodeError> {
797        let app = self.get_application(guid).await?;
798        Ok(app.id.to_string())
799    }
800
801    /// Create application if it doesn't exist, or return existing application.
802    ///
803    /// This method implements the "check and create" pattern commonly needed
804    /// for automated workflows.
805    ///
806    /// # Arguments
807    ///
808    /// * `name` - The name of the application
809    /// * `business_criticality` - Business criticality level (required for creation)
810    /// * `description` - Optional description for new applications
811    /// * `team_names` - Optional list of team names to assign to the application
812    ///
813    /// # Returns
814    ///
815    /// A `Result` containing the application (existing or newly created).
816    pub async fn create_application_if_not_exists(
817        &self,
818        name: &str,
819        business_criticality: BusinessCriticality,
820        description: Option<String>,
821        team_names: Option<Vec<String>>,
822    ) -> Result<Application, VeracodeError> {
823        // First, check if application already exists
824        if let Some(existing_app) = self.get_application_by_name(name).await? {
825            return Ok(existing_app);
826        }
827
828        // Application doesn't exist, create it
829
830        // Convert team names to Team objects with GUIDs if provided
831        let teams = if let Some(names) = team_names {
832            let identity_api = self.identity_api();
833            let mut resolved_teams = Vec::new();
834
835            for team_name in names {
836                match identity_api.get_team_guid_by_name(&team_name).await {
837                    Ok(Some(team_guid)) => {
838                        resolved_teams.push(Team {
839                            guid: Some(team_guid),
840                            team_id: None,
841                            team_name: None, // Not needed when using GUID
842                            team_legacy_id: None,
843                        });
844                    }
845                    Ok(None) => {
846                        return Err(VeracodeError::NotFound(format!(
847                            "Team '{}' not found",
848                            team_name
849                        )));
850                    }
851                    Err(identity_err) => {
852                        return Err(VeracodeError::InvalidResponse(format!(
853                            "Failed to lookup team '{}': {}",
854                            team_name, identity_err
855                        )));
856                    }
857                }
858            }
859
860            Some(resolved_teams)
861        } else {
862            None
863        };
864
865        let create_request = CreateApplicationRequest {
866            profile: CreateApplicationProfile {
867                name: name.to_string(),
868                business_criticality,
869                description,
870                business_unit: None,
871                business_owners: None,
872                policies: None,
873                teams,
874                tags: None,
875                custom_fields: None,
876                custom_kms_alias: None,
877            },
878        };
879
880        self.create_application(&create_request).await
881    }
882
883    /// Create application if it doesn't exist, or return existing application (with team GUIDs).
884    ///
885    /// This method allows specifying teams by their GUID, which is the preferred
886    /// approach for programmatic application creation.
887    ///
888    /// # Arguments
889    ///
890    /// * `name` - The name of the application
891    /// * `business_criticality` - Business criticality level (required for creation)
892    /// * `description` - Optional description for new applications
893    /// * `team_guids` - Optional list of team GUIDs to assign to the application
894    ///
895    /// # Returns
896    ///
897    /// A `Result` containing the application (existing or newly created).
898    pub async fn create_application_if_not_exists_with_team_guids(
899        &self,
900        name: &str,
901        business_criticality: BusinessCriticality,
902        description: Option<String>,
903        team_guids: Option<Vec<String>>,
904    ) -> Result<Application, VeracodeError> {
905        // First, check if application already exists
906        if let Some(existing_app) = self.get_application_by_name(name).await? {
907            return Ok(existing_app);
908        }
909
910        // Application doesn't exist, create it
911
912        // Convert team GUIDs to Team objects if provided
913        let teams = team_guids.map(|guids| {
914            guids
915                .into_iter()
916                .map(|team_guid| Team {
917                    guid: Some(team_guid),
918                    team_id: None,        // Will be assigned by Veracode
919                    team_name: None,      // Not needed when using GUID
920                    team_legacy_id: None, // Will be assigned by Veracode
921                })
922                .collect()
923        });
924
925        let create_request = CreateApplicationRequest {
926            profile: CreateApplicationProfile {
927                name: name.to_string(),
928                business_criticality,
929                description,
930                business_unit: None,
931                business_owners: None,
932                policies: None,
933                teams,
934                tags: None,
935                custom_fields: None,
936                custom_kms_alias: None,
937            },
938        };
939
940        self.create_application(&create_request).await
941    }
942
943    /// Create application if it doesn't exist, or return existing application (without teams).
944    ///
945    /// This is a convenience method that maintains backward compatibility
946    /// for callers that don't need to specify teams.
947    ///
948    /// # Arguments
949    ///
950    /// * `name` - The name of the application
951    /// * `business_criticality` - Business criticality level (required for creation)
952    /// * `description` - Optional description for new applications
953    ///
954    /// # Returns
955    ///
956    /// A `Result` containing the application (existing or newly created).
957    pub async fn create_application_if_not_exists_simple(
958        &self,
959        name: &str,
960        business_criticality: BusinessCriticality,
961        description: Option<String>,
962    ) -> Result<Application, VeracodeError> {
963        self.create_application_if_not_exists(name, business_criticality, description, None)
964            .await
965    }
966
967    /// Enable Customer Managed Encryption Key (CMEK) on an application
968    ///
969    /// This method updates an existing application to use a customer-managed encryption key.
970    /// The KMS alias must be properly formatted and the key must be accessible to Veracode.
971    ///
972    /// # Arguments
973    ///
974    /// * `app_guid` - The GUID of the application to enable encryption on
975    /// * `kms_alias` - The AWS KMS alias to use for encryption (must start with "alias/")
976    ///
977    /// # Returns
978    ///
979    /// A `Result` containing the updated application or an error.
980    ///
981    /// # Examples
982    ///
983    /// ```no_run
984    /// # use veracode_platform::{VeracodeClient, VeracodeConfig};
985    /// # use std::sync::Arc;
986    /// # use secrecy::SecretString;
987    /// # #[tokio::main]
988    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
989    /// let config = VeracodeConfig::from_arc_credentials(
990    ///     Arc::new(SecretString::from("api_id")),
991    ///     Arc::new(SecretString::from("api_key"))
992    /// );
993    /// let client = VeracodeClient::new(config)?;
994    ///
995    /// let app = client.enable_application_encryption(
996    ///     "app-guid-123",
997    ///     "alias/my-encryption-key"
998    /// ).await?;
999    /// # Ok(())
1000    /// # }
1001    /// ```
1002    pub async fn enable_application_encryption(
1003        &self,
1004        app_guid: &str,
1005        kms_alias: &str,
1006    ) -> Result<Application, VeracodeError> {
1007        // Validate KMS alias format
1008        validate_kms_alias(kms_alias).map_err(VeracodeError::InvalidConfig)?;
1009
1010        // Get current application to preserve existing settings
1011        let current_app = self.get_application(app_guid).await?;
1012
1013        let profile = current_app
1014            .profile
1015            .ok_or_else(|| VeracodeError::NotFound("Application profile not found".to_string()))?;
1016
1017        // Create update request with CMEK enabled
1018        let update_request = UpdateApplicationRequest {
1019            profile: UpdateApplicationProfile {
1020                name: Some(profile.name),
1021                description: profile.description,
1022                business_unit: profile.business_unit,
1023                business_owners: profile.business_owners,
1024                business_criticality: profile.business_criticality,
1025                policies: profile.policies,
1026                teams: profile.teams,
1027                tags: profile.tags,
1028                custom_fields: profile.custom_fields,
1029                custom_kms_alias: Some(kms_alias.to_string()),
1030            },
1031        };
1032
1033        self.update_application(app_guid, &update_request).await
1034    }
1035
1036    /// Change the encryption key for an application with CMEK enabled
1037    ///
1038    /// This method updates the KMS alias used for encrypting an application's data.
1039    /// The application must already have CMEK enabled.
1040    ///
1041    /// # Arguments
1042    ///
1043    /// * `app_guid` - The GUID of the application to update
1044    /// * `new_kms_alias` - The new AWS KMS alias to use for encryption
1045    ///
1046    /// # Returns
1047    ///
1048    /// A `Result` containing the updated application or an error.
1049    pub async fn change_encryption_key(
1050        &self,
1051        app_guid: &str,
1052        new_kms_alias: &str,
1053    ) -> Result<Application, VeracodeError> {
1054        // Validate new KMS alias format
1055        validate_kms_alias(new_kms_alias).map_err(VeracodeError::InvalidConfig)?;
1056
1057        // Get current application
1058        let current_app = self.get_application(app_guid).await?;
1059
1060        let profile = current_app
1061            .profile
1062            .ok_or_else(|| VeracodeError::NotFound("Application profile not found".to_string()))?;
1063
1064        // Create update request with new KMS alias
1065        let update_request = UpdateApplicationRequest {
1066            profile: UpdateApplicationProfile {
1067                name: Some(profile.name),
1068                description: profile.description,
1069                business_unit: profile.business_unit,
1070                business_owners: profile.business_owners,
1071                business_criticality: profile.business_criticality,
1072                policies: profile.policies,
1073                teams: profile.teams,
1074                tags: profile.tags,
1075                custom_fields: profile.custom_fields,
1076                custom_kms_alias: Some(new_kms_alias.to_string()),
1077            },
1078        };
1079
1080        self.update_application(app_guid, &update_request).await
1081    }
1082
1083    /// Get the encryption status of an application
1084    ///
1085    /// This method retrieves the current CMEK configuration for an application.
1086    ///
1087    /// # Arguments
1088    ///
1089    /// * `app_guid` - The GUID of the application to check
1090    ///
1091    /// # Returns
1092    ///
1093    /// A `Result` containing the KMS alias if CMEK is enabled, None if disabled, or an error.
1094    pub async fn get_application_encryption_status(
1095        &self,
1096        app_guid: &str,
1097    ) -> Result<Option<String>, VeracodeError> {
1098        let app = self.get_application(app_guid).await?;
1099
1100        // CMEK is stored directly in the profile as custom_kms_alias, not in custom_fields
1101        Ok(app.profile.and_then(|profile| profile.custom_kms_alias))
1102    }
1103}
1104
1105/// Validates an AWS KMS alias format
1106///
1107/// AWS KMS aliases must follow specific naming conventions:
1108/// - Must be prefixed with "alias/"
1109/// - Total length must be between 8-256 characters
1110/// - Can contain alphanumeric characters, hyphens, underscores, and forward slashes
1111/// - Cannot begin or end with "aws" (reserved by AWS)
1112///
1113/// # Examples
1114///
1115/// ```
1116/// use veracode_platform::app::validate_kms_alias;
1117///
1118/// assert!(validate_kms_alias("alias/my-app-key").is_ok());
1119/// assert!(validate_kms_alias("alias/my_app_key_2024").is_ok());
1120/// assert!(validate_kms_alias("invalid-alias").is_err());
1121/// assert!(validate_kms_alias("alias/aws-managed").is_err());
1122/// ```
1123pub fn validate_kms_alias(alias: &str) -> Result<(), String> {
1124    // Check prefix
1125    if !alias.starts_with("alias/") {
1126        return Err("KMS alias must start with 'alias/'".to_string());
1127    }
1128
1129    // Check length (including the "alias/" prefix) - minimum 8 characters for meaningful alias
1130    if alias.len() < 8 || alias.len() > 256 {
1131        return Err("KMS alias must be between 8 and 256 characters long".to_string());
1132    }
1133
1134    // Extract the alias name part (after "alias/")
1135    let alias_name = &alias[6..];
1136
1137    // Check for AWS reserved prefixes
1138    if alias_name.starts_with("aws") || alias_name.ends_with("aws") {
1139        return Err("KMS alias cannot begin or end with 'aws' (reserved by AWS)".to_string());
1140    }
1141
1142    // Check valid characters: alphanumeric, hyphens, underscores, forward slashes
1143    if !alias_name
1144        .chars()
1145        .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '/')
1146    {
1147        return Err("KMS alias can only contain alphanumeric characters, hyphens, underscores, and forward slashes".to_string());
1148    }
1149
1150    // Check that it's not empty after prefix
1151    if alias_name.is_empty() {
1152        return Err("KMS alias name cannot be empty after 'alias/' prefix".to_string());
1153    }
1154
1155    Ok(())
1156}
1157
1158#[cfg(test)]
1159mod tests {
1160    use super::*;
1161
1162    #[test]
1163    fn test_query_params() {
1164        let query = ApplicationQuery::new()
1165            .with_name("test_app")
1166            .with_policy_compliance("PASSED")
1167            .with_page(1)
1168            .with_size(50);
1169
1170        let params = query.to_query_params();
1171        assert!(params.contains(&("name".to_string(), "test_app".to_string())));
1172        assert!(params.contains(&("policy_compliance".to_string(), "PASSED".to_string())));
1173        assert!(params.contains(&("page".to_string(), "1".to_string())));
1174        assert!(params.contains(&("size".to_string(), "50".to_string())));
1175    }
1176
1177    #[test]
1178    fn test_application_query_builder() {
1179        let query = ApplicationQuery::new()
1180            .with_name("MyApp")
1181            .with_policy_compliance("DID_NOT_PASS")
1182            .with_modified_after("2023-01-01T00:00:00.000Z")
1183            .with_page(2)
1184            .with_size(25);
1185
1186        assert_eq!(query.name, Some("MyApp".to_string()));
1187        assert_eq!(query.policy_compliance, Some("DID_NOT_PASS".to_string()));
1188        assert_eq!(
1189            query.modified_after,
1190            Some("2023-01-01T00:00:00.000Z".to_string())
1191        );
1192        assert_eq!(query.page, Some(2));
1193        assert_eq!(query.size, Some(25));
1194    }
1195
1196    #[test]
1197    fn test_create_application_request_with_teams() {
1198        let team_names = vec!["Security Team".to_string(), "Development Team".to_string()];
1199        let teams: Vec<Team> = team_names
1200            .into_iter()
1201            .map(|team_name| Team {
1202                guid: None,
1203                team_id: None,
1204                team_name: Some(team_name),
1205                team_legacy_id: None,
1206            })
1207            .collect();
1208
1209        let request = CreateApplicationRequest {
1210            profile: CreateApplicationProfile {
1211                name: "Test Application".to_string(),
1212                business_criticality: BusinessCriticality::Medium,
1213                description: Some("Test description".to_string()),
1214                business_unit: None,
1215                business_owners: None,
1216                policies: None,
1217                teams: Some(teams.clone()),
1218                tags: None,
1219                custom_fields: None,
1220                custom_kms_alias: None,
1221            },
1222        };
1223
1224        assert_eq!(request.profile.name, "Test Application");
1225        assert_eq!(
1226            request.profile.business_criticality,
1227            BusinessCriticality::Medium
1228        );
1229        assert!(request.profile.teams.is_some());
1230
1231        let request_teams = request.profile.teams.unwrap();
1232        assert_eq!(request_teams.len(), 2);
1233        assert_eq!(
1234            request_teams[0].team_name,
1235            Some("Security Team".to_string())
1236        );
1237        assert_eq!(
1238            request_teams[1].team_name,
1239            Some("Development Team".to_string())
1240        );
1241    }
1242
1243    #[test]
1244    fn test_create_application_request_with_team_guids() {
1245        let team_guids = vec!["team-guid-1".to_string(), "team-guid-2".to_string()];
1246        let teams: Vec<Team> = team_guids
1247            .into_iter()
1248            .map(|team_guid| Team {
1249                guid: Some(team_guid),
1250                team_id: None,
1251                team_name: None,
1252                team_legacy_id: None,
1253            })
1254            .collect();
1255
1256        let request = CreateApplicationRequest {
1257            profile: CreateApplicationProfile {
1258                name: "Test Application".to_string(),
1259                business_criticality: BusinessCriticality::High,
1260                description: Some("Test description".to_string()),
1261                business_unit: None,
1262                business_owners: None,
1263                policies: None,
1264                teams: Some(teams.clone()),
1265                tags: None,
1266                custom_fields: None,
1267                custom_kms_alias: None,
1268            },
1269        };
1270
1271        assert_eq!(request.profile.name, "Test Application");
1272        assert_eq!(
1273            request.profile.business_criticality,
1274            BusinessCriticality::High
1275        );
1276        assert!(request.profile.teams.is_some());
1277
1278        let request_teams = request.profile.teams.unwrap();
1279        assert_eq!(request_teams.len(), 2);
1280        assert_eq!(request_teams[0].guid, Some("team-guid-1".to_string()));
1281        assert_eq!(request_teams[1].guid, Some("team-guid-2".to_string()));
1282        assert!(request_teams[0].team_name.is_none());
1283        assert!(request_teams[1].team_name.is_none());
1284    }
1285
1286    #[test]
1287    fn test_create_application_profile_cmek_serialization() {
1288        // Test that custom_kms_alias is included when Some
1289        let profile_with_cmek = CreateApplicationProfile {
1290            name: "Test Application".to_string(),
1291            business_criticality: BusinessCriticality::High,
1292            description: None,
1293            business_unit: None,
1294            business_owners: None,
1295            policies: None,
1296            teams: None,
1297            tags: None,
1298            custom_fields: None,
1299            custom_kms_alias: Some("alias/my-app-key".to_string()),
1300        };
1301
1302        let json = serde_json::to_string(&profile_with_cmek).unwrap();
1303        assert!(json.contains("custom_kms_alias"));
1304        assert!(json.contains("alias/my-app-key"));
1305
1306        // Test that custom_kms_alias is excluded when None
1307        let profile_without_cmek = CreateApplicationProfile {
1308            name: "Test Application".to_string(),
1309            business_criticality: BusinessCriticality::High,
1310            description: None,
1311            business_unit: None,
1312            business_owners: None,
1313            policies: None,
1314            teams: None,
1315            tags: None,
1316            custom_fields: None,
1317            custom_kms_alias: None,
1318        };
1319
1320        let json = serde_json::to_string(&profile_without_cmek).unwrap();
1321        assert!(!json.contains("custom_kms_alias"));
1322    }
1323
1324    #[test]
1325    fn test_update_application_profile_cmek_serialization() {
1326        // Test that custom_kms_alias is included when Some
1327        let profile_with_cmek = UpdateApplicationProfile {
1328            name: Some("Updated Application".to_string()),
1329            description: None,
1330            business_unit: None,
1331            business_owners: None,
1332            business_criticality: BusinessCriticality::Medium,
1333            policies: None,
1334            teams: None,
1335            tags: None,
1336            custom_fields: None,
1337            custom_kms_alias: Some("alias/updated-key".to_string()),
1338        };
1339
1340        let json = serde_json::to_string(&profile_with_cmek).unwrap();
1341        assert!(json.contains("custom_kms_alias"));
1342        assert!(json.contains("alias/updated-key"));
1343
1344        // Test that custom_kms_alias is excluded when None
1345        let profile_without_cmek = UpdateApplicationProfile {
1346            name: Some("Updated Application".to_string()),
1347            description: None,
1348            business_unit: None,
1349            business_owners: None,
1350            business_criticality: BusinessCriticality::Medium,
1351            policies: None,
1352            teams: None,
1353            tags: None,
1354            custom_fields: None,
1355            custom_kms_alias: None,
1356        };
1357
1358        let json = serde_json::to_string(&profile_without_cmek).unwrap();
1359        assert!(!json.contains("custom_kms_alias"));
1360    }
1361
1362    #[test]
1363    fn test_validate_kms_alias_valid_cases() {
1364        // Valid aliases
1365        assert!(validate_kms_alias("alias/my-app-key").is_ok());
1366        assert!(validate_kms_alias("alias/my_app_key_2024").is_ok());
1367        assert!(validate_kms_alias("alias/app/environment/key").is_ok());
1368        assert!(validate_kms_alias("alias/123-test-key").is_ok());
1369    }
1370
1371    #[test]
1372    fn test_validate_kms_alias_invalid_cases() {
1373        // Missing prefix
1374        assert!(validate_kms_alias("my-app-key").is_err());
1375        assert!(validate_kms_alias("invalid-alias").is_err());
1376
1377        // Wrong prefix
1378        assert!(validate_kms_alias("arn:aws:kms:us-east-1:123456789:alias/my-key").is_err());
1379
1380        // AWS reserved names
1381        assert!(validate_kms_alias("alias/aws-managed").is_err());
1382        assert!(validate_kms_alias("alias/my-key-aws").is_err());
1383
1384        // Empty alias name
1385        assert!(validate_kms_alias("alias/").is_err());
1386
1387        // Too short
1388        assert!(validate_kms_alias("alias/a").is_err());
1389
1390        // Invalid characters
1391        assert!(validate_kms_alias("alias/my@key").is_err());
1392        assert!(validate_kms_alias("alias/my key").is_err());
1393        assert!(validate_kms_alias("alias/my.key").is_err());
1394
1395        // Too long (over 256 characters)
1396        let long_alias = format!("alias/{}", "a".repeat(251));
1397        assert!(validate_kms_alias(&long_alias).is_err());
1398    }
1399
1400    #[test]
1401    fn test_cmek_backward_compatibility() {
1402        // Test that existing application creation still works without CMEK field
1403        let legacy_profile = CreateApplicationProfile {
1404            name: "Legacy Application".to_string(),
1405            business_criticality: BusinessCriticality::High,
1406            description: Some("Legacy app without CMEK".to_string()),
1407            business_unit: None,
1408            business_owners: None,
1409            policies: None,
1410            teams: None,
1411            tags: None,
1412            custom_fields: None,
1413            custom_kms_alias: None,
1414        };
1415
1416        let request = CreateApplicationRequest {
1417            profile: legacy_profile,
1418        };
1419
1420        // Should serialize successfully
1421        let json = serde_json::to_string(&request).unwrap();
1422
1423        // Should not contain CMEK field
1424        assert!(!json.contains("custom_kms_alias"));
1425
1426        // Should still contain required fields
1427        assert!(json.contains("name"));
1428        assert!(json.contains("business_criticality"));
1429        assert!(json.contains("Legacy Application"));
1430
1431        // Should be able to deserialize back
1432        let _deserialized: CreateApplicationRequest = serde_json::from_str(&json).unwrap();
1433    }
1434
1435    #[test]
1436    fn test_cmek_field_deserialization() {
1437        // Test deserializing JSON with CMEK field
1438        let json_with_cmek = r#"{
1439            "profile": {
1440                "name": "Test App",
1441                "business_criticality": "HIGH",
1442                "custom_kms_alias": "alias/test-key"
1443            }
1444        }"#;
1445
1446        let request: CreateApplicationRequest = serde_json::from_str(json_with_cmek).unwrap();
1447        assert_eq!(
1448            request.profile.custom_kms_alias,
1449            Some("alias/test-key".to_string())
1450        );
1451
1452        // Test deserializing JSON without CMEK field (backward compatibility)
1453        let json_without_cmek = r#"{
1454            "profile": {
1455                "name": "Test App",
1456                "business_criticality": "HIGH"
1457            }
1458        }"#;
1459
1460        let request: CreateApplicationRequest = serde_json::from_str(json_without_cmek).unwrap();
1461        assert_eq!(request.profile.custom_kms_alias, None);
1462    }
1463}