veracode_platform/
build.rs

1//! Build API functionality for Veracode platform.
2//!
3//! This module provides functionality to interact with the Veracode Build XML APIs,
4//! allowing you to create, update, delete, and query builds for applications and sandboxes.
5//! These operations use the XML API endpoints (analysiscenter.veracode.com).
6
7use chrono::{DateTime, NaiveDate, Utc};
8use log::debug;
9use quick_xml::Reader;
10use quick_xml::events::Event;
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13
14use crate::{VeracodeClient, VeracodeError};
15
16/// Valid lifecycle stage values for Veracode builds
17pub const LIFECYCLE_STAGES: &[&str] = &[
18    "In Development (pre-Alpha)",
19    "Internal or Alpha Testing",
20    "External or Beta Testing",
21    "Deployed",
22    "Maintenance",
23    "Cannot Disclose",
24    "Not Specified",
25];
26
27/// Validate if a lifecycle stage value is valid
28#[must_use]
29pub fn is_valid_lifecycle_stage(stage: &str) -> bool {
30    LIFECYCLE_STAGES.contains(&stage)
31}
32
33/// Get the default lifecycle stage for development builds
34#[must_use]
35pub fn default_lifecycle_stage() -> &'static str {
36    "In Development (pre-Alpha)"
37}
38
39/// Build status enumeration based on Veracode Java implementation
40/// These represent the possible build/analysis states that determine deletion safety
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42pub enum BuildStatus {
43    Incomplete,
44    NotSubmitted,
45    SubmittedToEngine,
46    ScanInProcess,
47    PreScanSubmitted,
48    PreScanSuccess,
49    PreScanFailed,
50    PreScanCancelled,
51    PrescanFailed,
52    PrescanCancelled,
53    ScanCancelled,
54    ResultsReady,
55    Failed,
56    Cancelled,
57    Unknown(String), // For any status not explicitly handled
58}
59
60impl BuildStatus {
61    /// Parse a build status string from the Veracode API
62    #[must_use]
63    pub fn from_string(status: &str) -> Self {
64        match status {
65            "Incomplete" => BuildStatus::Incomplete,
66            "Not Submitted" => BuildStatus::NotSubmitted,
67            "Submitted to Engine" => BuildStatus::SubmittedToEngine,
68            "Scan in Process" => BuildStatus::ScanInProcess,
69            "Pre-Scan Submitted" => BuildStatus::PreScanSubmitted,
70            "Pre-Scan Success" => BuildStatus::PreScanSuccess,
71            "Pre-Scan Failed" => BuildStatus::PreScanFailed,
72            "Pre-Scan Cancelled" => BuildStatus::PreScanCancelled,
73            "Prescan Failed" => BuildStatus::PrescanFailed,
74            "Prescan Cancelled" => BuildStatus::PrescanCancelled,
75            "Scan Cancelled" => BuildStatus::ScanCancelled,
76            "Results Ready" => BuildStatus::ResultsReady,
77            "Failed" => BuildStatus::Failed,
78            "Cancelled" => BuildStatus::Cancelled,
79            _ => BuildStatus::Unknown(status.to_string()),
80        }
81    }
82
83    /// Convert build status to string representation
84    #[must_use]
85    pub fn to_str(&self) -> &str {
86        match self {
87            BuildStatus::Incomplete => "Incomplete",
88            BuildStatus::NotSubmitted => "Not Submitted",
89            BuildStatus::SubmittedToEngine => "Submitted to Engine",
90            BuildStatus::ScanInProcess => "Scan in Process",
91            BuildStatus::PreScanSubmitted => "Pre-Scan Submitted",
92            BuildStatus::PreScanSuccess => "Pre-Scan Success",
93            BuildStatus::PreScanFailed => "Pre-Scan Failed",
94            BuildStatus::PreScanCancelled => "Pre-Scan Cancelled",
95            BuildStatus::PrescanFailed => "Prescan Failed",
96            BuildStatus::PrescanCancelled => "Prescan Cancelled",
97            BuildStatus::ScanCancelled => "Scan Cancelled",
98            BuildStatus::ResultsReady => "Results Ready",
99            BuildStatus::Failed => "Failed",
100            BuildStatus::Cancelled => "Cancelled",
101            BuildStatus::Unknown(s) => s,
102        }
103    }
104
105    /// Determine if a build is safe to delete based on its status and deletion policy
106    ///
107    /// Deletion Policy Levels:
108    /// - 0: Never delete builds
109    /// - 1: Delete only "safe" builds (incomplete, failed, cancelled states)
110    /// - 2: Delete any build except "Results Ready"
111    #[must_use]
112    pub fn is_safe_to_delete(&self, deletion_policy: u8) -> bool {
113        match deletion_policy {
114            1 => {
115                // Delete only safe builds (incomplete, failed, cancelled states)
116                matches!(
117                    self,
118                    BuildStatus::Incomplete
119                        | BuildStatus::NotSubmitted
120                        | BuildStatus::PreScanFailed
121                        | BuildStatus::PreScanCancelled
122                        | BuildStatus::PrescanFailed
123                        | BuildStatus::PrescanCancelled
124                        | BuildStatus::ScanCancelled
125                        | BuildStatus::Failed
126                        | BuildStatus::Cancelled
127                )
128            }
129            2 => {
130                // Delete any build except Results Ready
131                !matches!(self, BuildStatus::ResultsReady)
132            }
133            _ => false, // Never delete (0) or invalid policy, default to never delete
134        }
135    }
136}
137
138impl std::fmt::Display for BuildStatus {
139    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140        write!(f, "{}", self.to_str())
141    }
142}
143
144/// Represents a Veracode build
145#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct Build {
147    /// Build ID
148    pub build_id: String,
149    /// Application ID
150    pub app_id: String,
151    /// Build version
152    pub version: Option<String>,
153    /// Application name
154    pub app_name: Option<String>,
155    /// Sandbox ID (if sandbox build)
156    pub sandbox_id: Option<String>,
157    /// Sandbox name (if sandbox build)
158    pub sandbox_name: Option<String>,
159    /// Lifecycle stage
160    pub lifecycle_stage: Option<String>,
161    /// Launch date
162    pub launch_date: Option<NaiveDate>,
163    /// Submitter
164    pub submitter: Option<String>,
165    /// Platform
166    pub platform: Option<String>,
167    /// Analysis unit
168    pub analysis_unit: Option<String>,
169    /// Policy name
170    pub policy_name: Option<String>,
171    /// Policy version
172    pub policy_version: Option<String>,
173    /// Policy compliance status
174    pub policy_compliance_status: Option<String>,
175    /// Rules status
176    pub rules_status: Option<String>,
177    /// Grace period expired
178    pub grace_period_expired: Option<bool>,
179    /// Scan overdue
180    pub scan_overdue: Option<bool>,
181    /// Policy updated date
182    pub policy_updated_date: Option<DateTime<Utc>>,
183    /// Legacy scan engine
184    pub legacy_scan_engine: Option<bool>,
185    /// Additional attributes
186    pub attributes: HashMap<String, String>,
187}
188
189/// List of builds
190#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct BuildList {
192    /// Account ID
193    pub account_id: Option<String>,
194    /// Application ID
195    pub app_id: String,
196    /// Application name
197    pub app_name: Option<String>,
198    /// List of builds
199    pub builds: Vec<Build>,
200}
201
202/// Request for creating a build
203#[derive(Debug, Clone)]
204pub struct CreateBuildRequest {
205    /// Application ID
206    pub app_id: String,
207    /// Build version (optional, system will generate if not provided)
208    pub version: Option<String>,
209    /// Lifecycle stage
210    pub lifecycle_stage: Option<String>,
211    /// Launch date in MM/DD/YYYY format
212    pub launch_date: Option<String>,
213    /// Sandbox ID (optional, for sandbox builds)
214    pub sandbox_id: Option<String>,
215}
216
217/// Request for updating a build
218#[derive(Debug, Clone)]
219pub struct UpdateBuildRequest {
220    /// Application ID
221    pub app_id: String,
222    /// Build ID (optional, defaults to most recent)
223    pub build_id: Option<String>,
224    /// New build version
225    pub version: Option<String>,
226    /// New lifecycle stage
227    pub lifecycle_stage: Option<String>,
228    /// New launch date in MM/DD/YYYY format
229    pub launch_date: Option<String>,
230    /// Sandbox ID (optional, for sandbox builds)
231    pub sandbox_id: Option<String>,
232}
233
234/// Request for deleting a build
235#[derive(Debug, Clone)]
236pub struct DeleteBuildRequest {
237    /// Application ID
238    pub app_id: String,
239    /// Sandbox ID (optional, for sandbox builds)
240    pub sandbox_id: Option<String>,
241}
242
243/// Request for getting build information
244#[derive(Debug, Clone)]
245pub struct GetBuildInfoRequest {
246    /// Application ID
247    pub app_id: String,
248    /// Build ID (optional, defaults to most recent)
249    pub build_id: Option<String>,
250    /// Sandbox ID (optional, for sandbox builds)
251    pub sandbox_id: Option<String>,
252}
253
254/// Request for getting build list
255#[derive(Debug, Clone)]
256pub struct GetBuildListRequest {
257    /// Application ID
258    pub app_id: String,
259    /// Sandbox ID (optional, for sandbox builds only)
260    pub sandbox_id: Option<String>,
261}
262
263/// Result of build deletion
264#[derive(Debug, Clone, Serialize, Deserialize)]
265pub struct DeleteBuildResult {
266    /// Result status (typically "success")
267    pub result: String,
268}
269
270/// Build specific error types
271#[derive(Debug)]
272#[must_use = "Need to handle all error enum types."]
273pub enum BuildError {
274    /// Veracode API error
275    Api(VeracodeError),
276    /// Build not found
277    BuildNotFound,
278    /// Application not found
279    ApplicationNotFound,
280    /// Sandbox not found
281    SandboxNotFound,
282    /// Invalid parameter
283    InvalidParameter(String),
284    /// Build creation failed
285    CreationFailed(String),
286    /// Build update failed
287    UpdateFailed(String),
288    /// Build deletion failed
289    DeletionFailed(String),
290    /// XML parsing error
291    XmlParsingError(String),
292    /// Unauthorized access
293    Unauthorized,
294    /// Permission denied
295    PermissionDenied,
296    /// Build in progress (cannot modify)
297    BuildInProgress,
298}
299
300impl std::fmt::Display for BuildError {
301    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
302        match self {
303            BuildError::Api(err) => write!(f, "API error: {err}"),
304            BuildError::BuildNotFound => write!(f, "Build not found"),
305            BuildError::ApplicationNotFound => write!(f, "Application not found"),
306            BuildError::SandboxNotFound => write!(f, "Sandbox not found"),
307            BuildError::InvalidParameter(msg) => write!(f, "Invalid parameter: {msg}"),
308            BuildError::CreationFailed(msg) => write!(f, "Build creation failed: {msg}"),
309            BuildError::UpdateFailed(msg) => write!(f, "Build update failed: {msg}"),
310            BuildError::DeletionFailed(msg) => write!(f, "Build deletion failed: {msg}"),
311            BuildError::XmlParsingError(msg) => write!(f, "XML parsing error: {msg}"),
312            BuildError::Unauthorized => write!(f, "Unauthorized access"),
313            BuildError::PermissionDenied => write!(f, "Permission denied"),
314            BuildError::BuildInProgress => write!(f, "Build in progress, cannot modify"),
315        }
316    }
317}
318
319impl std::error::Error for BuildError {}
320
321impl From<VeracodeError> for BuildError {
322    fn from(err: VeracodeError) -> Self {
323        BuildError::Api(err)
324    }
325}
326
327impl From<std::io::Error> for BuildError {
328    fn from(err: std::io::Error) -> Self {
329        BuildError::Api(VeracodeError::InvalidResponse(err.to_string()))
330    }
331}
332
333impl From<reqwest::Error> for BuildError {
334    fn from(err: reqwest::Error) -> Self {
335        BuildError::Api(VeracodeError::Http(err))
336    }
337}
338
339/// Build API operations for Veracode platform
340pub struct BuildApi {
341    client: VeracodeClient,
342}
343
344impl BuildApi {
345    /// Create a new `BuildApi` instance
346    #[must_use]
347    pub fn new(client: VeracodeClient) -> Self {
348        Self { client }
349    }
350
351    /// Create a new build
352    ///
353    /// # Arguments
354    ///
355    /// * `request` - The create build request
356    ///
357    /// # Returns
358    ///
359    /// A `Result` containing the created build information or an error.
360    ///
361    /// # Errors
362    ///
363    /// Returns an error if the API request fails, the application is not found,
364    /// authentication fails, or the build creation is rejected by the Veracode platform.
365    pub async fn create_build(&self, request: &CreateBuildRequest) -> Result<Build, BuildError> {
366        let endpoint = "/api/5.0/createbuild.do";
367
368        // Build query parameters
369        let mut query_params = Vec::new();
370        query_params.push(("app_id", request.app_id.as_str()));
371
372        if let Some(version) = &request.version {
373            query_params.push(("version", version.as_str()));
374        }
375
376        if let Some(lifecycle_stage) = &request.lifecycle_stage {
377            query_params.push(("lifecycle_stage", lifecycle_stage.as_str()));
378        }
379
380        if let Some(launch_date) = &request.launch_date {
381            query_params.push(("launch_date", launch_date.as_str()));
382        }
383
384        if let Some(sandbox_id) = &request.sandbox_id {
385            query_params.push(("sandbox_id", sandbox_id.as_str()));
386        }
387
388        let response = self
389            .client
390            .post_with_query_params(endpoint, &query_params)
391            .await?;
392
393        let status = response.status().as_u16();
394        match status {
395            200 => {
396                let response_text = response.text().await?;
397                self.parse_build_info(&response_text)
398            }
399            400 => {
400                let error_text = response.text().await.unwrap_or_default();
401                Err(BuildError::InvalidParameter(error_text))
402            }
403            401 => Err(BuildError::Unauthorized),
404            403 => Err(BuildError::PermissionDenied),
405            404 => Err(BuildError::ApplicationNotFound),
406            _ => {
407                let error_text = response.text().await.unwrap_or_default();
408                Err(BuildError::CreationFailed(format!(
409                    "HTTP {status}: {error_text}"
410                )))
411            }
412        }
413    }
414
415    /// Update an existing build
416    ///
417    /// # Arguments
418    ///
419    /// * `request` - The update build request
420    ///
421    /// # Returns
422    ///
423    /// A `Result` containing the updated build information or an error.
424    ///
425    /// # Errors
426    ///
427    /// Returns an error if the API request fails, authentication fails,
428    /// or the operation is rejected by the Veracode platform.
429    pub async fn update_build(&self, request: &UpdateBuildRequest) -> Result<Build, BuildError> {
430        let endpoint = "/api/5.0/updatebuild.do";
431
432        // Build query parameters
433        let mut query_params = Vec::new();
434        query_params.push(("app_id", request.app_id.as_str()));
435
436        if let Some(build_id) = &request.build_id {
437            query_params.push(("build_id", build_id.as_str()));
438        }
439
440        if let Some(version) = &request.version {
441            query_params.push(("version", version.as_str()));
442        }
443
444        if let Some(lifecycle_stage) = &request.lifecycle_stage {
445            query_params.push(("lifecycle_stage", lifecycle_stage.as_str()));
446        }
447
448        if let Some(launch_date) = &request.launch_date {
449            query_params.push(("launch_date", launch_date.as_str()));
450        }
451
452        if let Some(sandbox_id) = &request.sandbox_id {
453            query_params.push(("sandbox_id", sandbox_id.as_str()));
454        }
455
456        let response = self
457            .client
458            .post_with_query_params(endpoint, &query_params)
459            .await?;
460
461        let status = response.status().as_u16();
462        match status {
463            200 => {
464                let response_text = response.text().await?;
465                self.parse_build_info(&response_text)
466            }
467            400 => {
468                let error_text = response.text().await.unwrap_or_default();
469                Err(BuildError::InvalidParameter(error_text))
470            }
471            401 => Err(BuildError::Unauthorized),
472            403 => Err(BuildError::PermissionDenied),
473            404 => {
474                if request.sandbox_id.is_some() {
475                    Err(BuildError::SandboxNotFound)
476                } else {
477                    Err(BuildError::BuildNotFound)
478                }
479            }
480            _ => {
481                let error_text = response.text().await.unwrap_or_default();
482                Err(BuildError::UpdateFailed(format!(
483                    "HTTP {status}: {error_text}"
484                )))
485            }
486        }
487    }
488
489    /// Delete a build
490    ///
491    /// # Arguments
492    ///
493    /// * `request` - The delete build request
494    ///
495    /// # Returns
496    ///
497    /// A `Result` containing the deletion result or an error.
498    ///
499    /// # Errors
500    ///
501    /// Returns an error if the API request fails, authentication fails,
502    /// or the operation is rejected by the Veracode platform.
503    pub async fn delete_build(
504        &self,
505        request: &DeleteBuildRequest,
506    ) -> Result<DeleteBuildResult, BuildError> {
507        let endpoint = "/api/5.0/deletebuild.do";
508
509        // Build query parameters
510        let mut query_params = Vec::new();
511        query_params.push(("app_id", request.app_id.as_str()));
512
513        if let Some(sandbox_id) = &request.sandbox_id {
514            query_params.push(("sandbox_id", sandbox_id.as_str()));
515        }
516
517        let response = self
518            .client
519            .post_with_query_params(endpoint, &query_params)
520            .await?;
521
522        let status = response.status().as_u16();
523        match status {
524            200 => {
525                let response_text = response.text().await?;
526                self.parse_delete_result(&response_text)
527            }
528            400 => {
529                let error_text = response.text().await.unwrap_or_default();
530                Err(BuildError::InvalidParameter(error_text))
531            }
532            401 => Err(BuildError::Unauthorized),
533            403 => Err(BuildError::PermissionDenied),
534            404 => {
535                if request.sandbox_id.is_some() {
536                    Err(BuildError::SandboxNotFound)
537                } else {
538                    Err(BuildError::BuildNotFound)
539                }
540            }
541            _ => {
542                let error_text = response.text().await.unwrap_or_default();
543                Err(BuildError::DeletionFailed(format!(
544                    "HTTP {status}: {error_text}"
545                )))
546            }
547        }
548    }
549
550    /// Get build information
551    ///
552    /// # Arguments
553    ///
554    /// * `request` - The get build info request
555    ///
556    /// # Returns
557    ///
558    /// A `Result` containing the build information or an error.
559    ///
560    /// # Errors
561    ///
562    /// Returns an error if the API request fails, authentication fails,
563    /// or the operation is rejected by the Veracode platform.
564    pub async fn get_build_info(&self, request: &GetBuildInfoRequest) -> Result<Build, BuildError> {
565        let endpoint = "/api/5.0/getbuildinfo.do";
566
567        // Build query parameters
568        let mut query_params = Vec::new();
569        query_params.push(("app_id", request.app_id.as_str()));
570
571        if let Some(build_id) = &request.build_id {
572            query_params.push(("build_id", build_id.as_str()));
573        }
574
575        if let Some(sandbox_id) = &request.sandbox_id {
576            query_params.push(("sandbox_id", sandbox_id.as_str()));
577        }
578
579        let response = self
580            .client
581            .get_with_query_params(endpoint, &query_params)
582            .await?;
583
584        let status = response.status().as_u16();
585        match status {
586            200 => {
587                let response_text = response.text().await?;
588                debug!("🌐 Raw XML response from getbuildinfo.do:\n{response_text}");
589                self.parse_build_info(&response_text)
590            }
591            400 => {
592                let error_text = response.text().await.unwrap_or_default();
593                Err(BuildError::InvalidParameter(error_text))
594            }
595            401 => Err(BuildError::Unauthorized),
596            403 => Err(BuildError::PermissionDenied),
597            404 => {
598                if request.sandbox_id.is_some() {
599                    Err(BuildError::SandboxNotFound)
600                } else {
601                    Err(BuildError::BuildNotFound)
602                }
603            }
604            _ => {
605                let error_text = response.text().await.unwrap_or_default();
606                Err(BuildError::Api(VeracodeError::InvalidResponse(format!(
607                    "HTTP {status}: {error_text}"
608                ))))
609            }
610        }
611    }
612
613    /// Get list of builds
614    ///
615    /// # Arguments
616    ///
617    /// * `request` - The get build list request
618    ///
619    /// # Returns
620    ///
621    /// A `Result` containing the build list or an error.
622    ///
623    /// # Errors
624    ///
625    /// Returns an error if the API request fails, authentication fails,
626    /// or the operation is rejected by the Veracode platform.
627    pub async fn get_build_list(
628        &self,
629        request: &GetBuildListRequest,
630    ) -> Result<BuildList, BuildError> {
631        let endpoint = "/api/5.0/getbuildlist.do";
632
633        // Build query parameters
634        let mut query_params = Vec::new();
635        query_params.push(("app_id", request.app_id.as_str()));
636
637        if let Some(sandbox_id) = &request.sandbox_id {
638            query_params.push(("sandbox_id", sandbox_id.as_str()));
639        }
640
641        let response = self
642            .client
643            .get_with_query_params(endpoint, &query_params)
644            .await?;
645
646        let status = response.status().as_u16();
647        match status {
648            200 => {
649                let response_text = response.text().await?;
650                self.parse_build_list(&response_text)
651            }
652            400 => {
653                let error_text = response.text().await.unwrap_or_default();
654                Err(BuildError::InvalidParameter(error_text))
655            }
656            401 => Err(BuildError::Unauthorized),
657            403 => Err(BuildError::PermissionDenied),
658            404 => {
659                if request.sandbox_id.is_some() {
660                    Err(BuildError::SandboxNotFound)
661                } else {
662                    Err(BuildError::ApplicationNotFound)
663                }
664            }
665            _ => {
666                let error_text = response.text().await.unwrap_or_default();
667                Err(BuildError::Api(VeracodeError::InvalidResponse(format!(
668                    "HTTP {status}: {error_text}"
669                ))))
670            }
671        }
672    }
673
674    /// Parse build info XML response
675    fn parse_build_info(&self, xml: &str) -> Result<Build, BuildError> {
676        // Check if response contains an error element first
677        if xml.contains("<error>") {
678            let mut reader = Reader::from_str(xml);
679            reader.config_mut().trim_text(true);
680            let mut buf = Vec::new();
681
682            loop {
683                match reader.read_event_into(&mut buf) {
684                    Ok(Event::Start(ref e)) if e.name().as_ref() == b"error" => {
685                        if let Ok(Event::Text(text)) = reader.read_event_into(&mut buf) {
686                            let error_msg = String::from_utf8_lossy(&text);
687                            if error_msg.contains("Could not find a build") {
688                                return Err(BuildError::BuildNotFound);
689                            }
690                            return Err(BuildError::Api(VeracodeError::InvalidResponse(
691                                error_msg.to_string(),
692                            )));
693                        }
694                    }
695                    Ok(Event::Eof) => break,
696                    Err(e) => return Err(BuildError::XmlParsingError(e.to_string())),
697                    _ => {}
698                }
699                buf.clear();
700            }
701        }
702
703        let mut reader = Reader::from_str(xml);
704        reader.config_mut().trim_text(true);
705
706        let mut buf = Vec::new();
707        let mut build = Build {
708            build_id: String::new(),
709            app_id: String::new(),
710            version: None,
711            app_name: None,
712            sandbox_id: None,
713            sandbox_name: None,
714            lifecycle_stage: None,
715            launch_date: None,
716            submitter: None,
717            platform: None,
718            analysis_unit: None,
719            policy_name: None,
720            policy_version: None,
721            policy_compliance_status: None,
722            rules_status: None,
723            grace_period_expired: None,
724            scan_overdue: None,
725            policy_updated_date: None,
726            legacy_scan_engine: None,
727            attributes: HashMap::new(),
728        };
729
730        let mut inside_build = false;
731
732        loop {
733            match reader.read_event_into(&mut buf) {
734                Ok(Event::Start(ref e)) => {
735                    match e.name().as_ref() {
736                        b"build" => {
737                            inside_build = true;
738                            for attr in e.attributes().flatten() {
739                                let key = String::from_utf8_lossy(attr.key.as_ref());
740                                let value = String::from_utf8_lossy(&attr.value);
741
742                                match key.as_ref() {
743                                    "build_id" => build.build_id = value.into_owned(),
744                                    "app_id" => build.app_id = value.into_owned(),
745                                    "version" => build.version = Some(value.into_owned()),
746                                    "app_name" => build.app_name = Some(value.into_owned()),
747                                    "sandbox_id" => build.sandbox_id = Some(value.into_owned()),
748                                    "sandbox_name" => build.sandbox_name = Some(value.into_owned()),
749                                    "lifecycle_stage" => {
750                                        build.lifecycle_stage = Some(value.into_owned())
751                                    }
752                                    "submitter" => build.submitter = Some(value.into_owned()),
753                                    "platform" => build.platform = Some(value.into_owned()),
754                                    "analysis_unit" => {
755                                        build.analysis_unit = Some(value.into_owned())
756                                    }
757                                    "policy_name" => build.policy_name = Some(value.into_owned()),
758                                    "policy_version" => {
759                                        build.policy_version = Some(value.into_owned())
760                                    }
761                                    "policy_compliance_status" => {
762                                        build.policy_compliance_status = Some(value.into_owned())
763                                    }
764                                    "rules_status" => build.rules_status = Some(value.into_owned()),
765                                    "grace_period_expired" => {
766                                        build.grace_period_expired = value.parse::<bool>().ok();
767                                    }
768                                    "scan_overdue" => {
769                                        build.scan_overdue = value.parse::<bool>().ok();
770                                    }
771                                    "legacy_scan_engine" => {
772                                        build.legacy_scan_engine = value.parse::<bool>().ok();
773                                    }
774                                    "launch_date" => {
775                                        if let Ok(date) =
776                                            NaiveDate::parse_from_str(&value, "%m/%d/%Y")
777                                        {
778                                            build.launch_date = Some(date);
779                                        }
780                                    }
781                                    "policy_updated_date" => {
782                                        if let Ok(datetime) =
783                                            chrono::DateTime::parse_from_rfc3339(&value)
784                                        {
785                                            build.policy_updated_date =
786                                                Some(datetime.with_timezone(&Utc));
787                                        }
788                                    }
789                                    _ => {
790                                        build
791                                            .attributes
792                                            .insert(key.into_owned(), value.into_owned());
793                                    }
794                                }
795                            }
796                        }
797                        b"analysis_unit" if inside_build => {
798                            // Parse analysis_unit element nested inside build (primary source for build status)
799                            for attr in e.attributes().flatten() {
800                                let key = String::from_utf8_lossy(attr.key.as_ref());
801                                let value = String::from_utf8_lossy(&attr.value);
802
803                                // Store all analysis_unit attributes, especially status
804                                match key.as_ref() {
805                                    "status" => {
806                                        // Store the analysis_unit status as the primary status
807                                        build
808                                            .attributes
809                                            .insert("status".to_string(), value.into_owned());
810                                    }
811                                    _ => {
812                                        // Store other analysis_unit attributes with prefix
813                                        build
814                                            .attributes
815                                            .insert(format!("analysis_{key}"), value.into_owned());
816                                    }
817                                }
818                            }
819                        }
820                        _ => {}
821                    }
822                }
823                Ok(Event::Empty(ref e)) => {
824                    // Handle self-closing elements like <analysis_unit ... />
825                    if e.name().as_ref() == b"analysis_unit" && inside_build {
826                        for attr in e.attributes().flatten() {
827                            let key = String::from_utf8_lossy(attr.key.as_ref());
828                            let value = String::from_utf8_lossy(&attr.value);
829
830                            match key.as_ref() {
831                                "status" => {
832                                    build
833                                        .attributes
834                                        .insert("status".to_string(), value.into_owned());
835                                }
836                                _ => {
837                                    build
838                                        .attributes
839                                        .insert(format!("analysis_{key}"), value.into_owned());
840                                }
841                            }
842                        }
843                    }
844                }
845                Ok(Event::End(ref e)) => {
846                    if e.name().as_ref() == b"build" {
847                        inside_build = false;
848                    }
849                }
850                Ok(Event::Eof) => break,
851                Err(e) => return Err(BuildError::XmlParsingError(e.to_string())),
852                _ => {}
853            }
854            buf.clear();
855        }
856
857        if build.build_id.is_empty() {
858            return Err(BuildError::XmlParsingError(
859                "No build information found in response".to_string(),
860            ));
861        }
862
863        Ok(build)
864    }
865
866    /// Parse build list XML response
867    fn parse_build_list(&self, xml: &str) -> Result<BuildList, BuildError> {
868        let mut reader = Reader::from_str(xml);
869        reader.config_mut().trim_text(true);
870
871        let mut buf = Vec::new();
872        let mut build_list = BuildList {
873            account_id: None,
874            app_id: String::new(),
875            app_name: None,
876            builds: Vec::new(),
877        };
878
879        loop {
880            match reader.read_event_into(&mut buf) {
881                Ok(Event::Start(ref e)) => match e.name().as_ref() {
882                    b"buildlist" => {
883                        for attr in e.attributes().flatten() {
884                            let key = String::from_utf8_lossy(attr.key.as_ref());
885                            let value = String::from_utf8_lossy(&attr.value);
886
887                            match key.as_ref() {
888                                "account_id" => build_list.account_id = Some(value.into_owned()),
889                                "app_id" => build_list.app_id = value.into_owned(),
890                                "app_name" => build_list.app_name = Some(value.into_owned()),
891                                _ => {}
892                            }
893                        }
894                    }
895                    b"build" => {
896                        let mut build = Build {
897                            build_id: String::new(),
898                            app_id: build_list.app_id.clone(),
899                            version: None,
900                            app_name: build_list.app_name.clone(),
901                            sandbox_id: None,
902                            sandbox_name: None,
903                            lifecycle_stage: None,
904                            launch_date: None,
905                            submitter: None,
906                            platform: None,
907                            analysis_unit: None,
908                            policy_name: None,
909                            policy_version: None,
910                            policy_compliance_status: None,
911                            rules_status: None,
912                            grace_period_expired: None,
913                            scan_overdue: None,
914                            policy_updated_date: None,
915                            legacy_scan_engine: None,
916                            attributes: HashMap::new(),
917                        };
918
919                        for attr in e.attributes().flatten() {
920                            let key = String::from_utf8_lossy(attr.key.as_ref());
921                            let value = String::from_utf8_lossy(&attr.value);
922
923                            match key.as_ref() {
924                                "build_id" => build.build_id = value.into_owned(),
925                                "version" => build.version = Some(value.into_owned()),
926                                "sandbox_id" => build.sandbox_id = Some(value.into_owned()),
927                                "sandbox_name" => build.sandbox_name = Some(value.into_owned()),
928                                "lifecycle_stage" => {
929                                    build.lifecycle_stage = Some(value.into_owned())
930                                }
931                                "submitter" => build.submitter = Some(value.into_owned()),
932                                "platform" => build.platform = Some(value.into_owned()),
933                                "analysis_unit" => build.analysis_unit = Some(value.into_owned()),
934                                "policy_name" => build.policy_name = Some(value.into_owned()),
935                                "policy_version" => build.policy_version = Some(value.into_owned()),
936                                "policy_compliance_status" => {
937                                    build.policy_compliance_status = Some(value.into_owned())
938                                }
939                                "rules_status" => build.rules_status = Some(value.into_owned()),
940                                "grace_period_expired" => {
941                                    build.grace_period_expired = value.parse::<bool>().ok();
942                                }
943                                "scan_overdue" => {
944                                    build.scan_overdue = value.parse::<bool>().ok();
945                                }
946                                "legacy_scan_engine" => {
947                                    build.legacy_scan_engine = value.parse::<bool>().ok();
948                                }
949                                "launch_date" => {
950                                    if let Ok(date) = NaiveDate::parse_from_str(&value, "%m/%d/%Y")
951                                    {
952                                        build.launch_date = Some(date);
953                                    }
954                                }
955                                "policy_updated_date" => {
956                                    if let Ok(datetime) =
957                                        chrono::DateTime::parse_from_rfc3339(&value)
958                                    {
959                                        build.policy_updated_date =
960                                            Some(datetime.with_timezone(&Utc));
961                                    }
962                                }
963                                _ => {
964                                    build
965                                        .attributes
966                                        .insert(key.into_owned(), value.into_owned());
967                                }
968                            }
969                        }
970
971                        if !build.build_id.is_empty() {
972                            build_list.builds.push(build);
973                        }
974                    }
975                    _ => {}
976                },
977                Ok(Event::Eof) => break,
978                Err(e) => return Err(BuildError::XmlParsingError(e.to_string())),
979                _ => {}
980            }
981            buf.clear();
982        }
983
984        Ok(build_list)
985    }
986
987    /// Parse delete build result XML response
988    fn parse_delete_result(&self, xml: &str) -> Result<DeleteBuildResult, BuildError> {
989        let mut reader = Reader::from_str(xml);
990        reader.config_mut().trim_text(true);
991
992        let mut buf = Vec::new();
993        let mut result = String::new();
994
995        loop {
996            match reader.read_event_into(&mut buf) {
997                Ok(Event::Start(ref e)) => {
998                    if e.name().as_ref() == b"result" {
999                        // Read the text content of the result element
1000                        if let Ok(Event::Text(e)) = reader.read_event_into(&mut buf) {
1001                            result = String::from_utf8_lossy(&e).into_owned();
1002                        }
1003                    }
1004                }
1005                Ok(Event::Eof) => break,
1006                Err(e) => return Err(BuildError::XmlParsingError(e.to_string())),
1007                _ => {}
1008            }
1009            buf.clear();
1010        }
1011
1012        if result.is_empty() {
1013            return Err(BuildError::XmlParsingError(
1014                "No result found in delete response".to_string(),
1015            ));
1016        }
1017
1018        Ok(DeleteBuildResult { result })
1019    }
1020}
1021
1022// Convenience methods implementation
1023impl BuildApi {
1024    /// Create a build with minimal parameters
1025    ///
1026    /// # Arguments
1027    ///
1028    /// * `app_id` - Application ID
1029    /// * `version` - Optional build version
1030    ///
1031    /// # Returns
1032    ///
1033    /// A `Result` containing the created build information or an error.
1034    ///
1035    /// # Errors
1036    ///
1037    /// Returns an error if the API request fails, authentication fails,
1038    /// or the operation is rejected by the Veracode platform.
1039    pub async fn create_simple_build(
1040        &self,
1041        app_id: &str,
1042        version: Option<&str>,
1043    ) -> Result<Build, BuildError> {
1044        let request = CreateBuildRequest {
1045            app_id: app_id.to_string(),
1046            version: version.map(str::to_string),
1047            lifecycle_stage: None,
1048            launch_date: None,
1049            sandbox_id: None,
1050        };
1051
1052        self.create_build(&request).await
1053    }
1054
1055    /// Create a sandbox build
1056    ///
1057    /// # Arguments
1058    ///
1059    /// * `app_id` - Application ID
1060    /// * `sandbox_id` - Sandbox ID
1061    /// * `version` - Optional build version
1062    ///
1063    /// # Returns
1064    ///
1065    /// A `Result` containing the created build information or an error.
1066    ///
1067    /// # Errors
1068    ///
1069    /// Returns an error if the API request fails, authentication fails,
1070    /// or the operation is rejected by the Veracode platform.
1071    pub async fn create_sandbox_build(
1072        &self,
1073        app_id: &str,
1074        sandbox_id: &str,
1075        version: Option<&str>,
1076    ) -> Result<Build, BuildError> {
1077        let request = CreateBuildRequest {
1078            app_id: app_id.to_string(),
1079            version: version.map(str::to_string),
1080            lifecycle_stage: None,
1081            launch_date: None,
1082            sandbox_id: Some(sandbox_id.to_string()),
1083        };
1084
1085        self.create_build(&request).await
1086    }
1087
1088    /// Delete the most recent application build
1089    ///
1090    /// # Arguments
1091    ///
1092    /// * `app_id` - Application ID
1093    ///
1094    /// # Returns
1095    ///
1096    /// A `Result` containing the deletion result or an error.
1097    ///
1098    /// # Errors
1099    ///
1100    /// Returns an error if the API request fails, authentication fails,
1101    /// or the operation is rejected by the Veracode platform.
1102    pub async fn delete_app_build(&self, app_id: &str) -> Result<DeleteBuildResult, BuildError> {
1103        let request = DeleteBuildRequest {
1104            app_id: app_id.to_string(),
1105            sandbox_id: None,
1106        };
1107
1108        self.delete_build(&request).await
1109    }
1110
1111    /// Delete the most recent sandbox build
1112    ///
1113    /// # Arguments
1114    ///
1115    /// * `app_id` - Application ID
1116    /// * `sandbox_id` - Sandbox ID
1117    ///
1118    /// # Returns
1119    ///
1120    /// A `Result` containing the deletion result or an error.
1121    ///
1122    /// # Errors
1123    ///
1124    /// Returns an error if the API request fails, authentication fails,
1125    /// or the operation is rejected by the Veracode platform.
1126    pub async fn delete_sandbox_build(
1127        &self,
1128        app_id: &str,
1129        sandbox_id: &str,
1130    ) -> Result<DeleteBuildResult, BuildError> {
1131        let request = DeleteBuildRequest {
1132            app_id: app_id.to_string(),
1133            sandbox_id: Some(sandbox_id.to_string()),
1134        };
1135
1136        self.delete_build(&request).await
1137    }
1138
1139    /// Get the most recent build info for an application
1140    ///
1141    /// # Arguments
1142    ///
1143    /// * `app_id` - Application ID
1144    ///
1145    /// # Returns
1146    ///
1147    /// A `Result` containing the build information or an error.
1148    ///
1149    /// # Errors
1150    ///
1151    /// Returns an error if the API request fails, authentication fails,
1152    /// or the operation is rejected by the Veracode platform.
1153    pub async fn get_app_build_info(&self, app_id: &str) -> Result<Build, BuildError> {
1154        let request = GetBuildInfoRequest {
1155            app_id: app_id.to_string(),
1156            build_id: None,
1157            sandbox_id: None,
1158        };
1159
1160        self.get_build_info(&request).await
1161    }
1162
1163    /// Get build info for a specific sandbox
1164    ///
1165    /// # Arguments
1166    ///
1167    /// * `app_id` - Application ID
1168    /// * `sandbox_id` - Sandbox ID
1169    ///
1170    /// # Returns
1171    ///
1172    /// A `Result` containing the build information or an error.
1173    ///
1174    /// # Errors
1175    ///
1176    /// Returns an error if the API request fails, authentication fails,
1177    /// or the operation is rejected by the Veracode platform.
1178    pub async fn get_sandbox_build_info(
1179        &self,
1180        app_id: &str,
1181        sandbox_id: &str,
1182    ) -> Result<Build, BuildError> {
1183        let request = GetBuildInfoRequest {
1184            app_id: app_id.to_string(),
1185            build_id: None,
1186            sandbox_id: Some(sandbox_id.to_string()),
1187        };
1188
1189        self.get_build_info(&request).await
1190    }
1191
1192    /// Get list of all builds for an application
1193    ///
1194    /// # Arguments
1195    ///
1196    /// * `app_id` - Application ID
1197    ///
1198    /// # Returns
1199    ///
1200    /// A `Result` containing the build list or an error.
1201    ///
1202    /// # Errors
1203    ///
1204    /// Returns an error if the API request fails, authentication fails,
1205    /// or the operation is rejected by the Veracode platform.
1206    pub async fn get_app_builds(&self, app_id: &str) -> Result<BuildList, BuildError> {
1207        let request = GetBuildListRequest {
1208            app_id: app_id.to_string(),
1209            sandbox_id: None,
1210        };
1211
1212        self.get_build_list(&request).await
1213    }
1214
1215    /// Get list of builds for a sandbox
1216    ///
1217    /// # Arguments
1218    ///
1219    /// * `app_id` - Application ID
1220    /// * `sandbox_id` - Sandbox ID
1221    ///
1222    /// # Returns
1223    ///
1224    /// A `Result` containing the build list or an error.
1225    ///
1226    /// # Errors
1227    ///
1228    /// Returns an error if the API request fails, authentication fails,
1229    /// or the operation is rejected by the Veracode platform.
1230    pub async fn get_sandbox_builds(
1231        &self,
1232        app_id: &str,
1233        sandbox_id: &str,
1234    ) -> Result<BuildList, BuildError> {
1235        let request = GetBuildListRequest {
1236            app_id: app_id.to_string(),
1237            sandbox_id: Some(sandbox_id.to_string()),
1238        };
1239
1240        self.get_build_list(&request).await
1241    }
1242}
1243
1244#[cfg(test)]
1245mod tests {
1246    use super::*;
1247    use crate::VeracodeConfig;
1248
1249    #[test]
1250    fn test_create_build_request() {
1251        let request = CreateBuildRequest {
1252            app_id: "123".to_string(),
1253            version: Some("1.0.0".to_string()),
1254            lifecycle_stage: Some("In Development (pre-Alpha)".to_string()),
1255            launch_date: Some("12/31/2024".to_string()),
1256            sandbox_id: None,
1257        };
1258
1259        assert_eq!(request.app_id, "123");
1260        assert_eq!(request.version, Some("1.0.0".to_string()));
1261        assert_eq!(
1262            request.lifecycle_stage,
1263            Some("In Development (pre-Alpha)".to_string())
1264        );
1265    }
1266
1267    #[test]
1268    fn test_update_build_request() {
1269        let request = UpdateBuildRequest {
1270            app_id: "123".to_string(),
1271            build_id: Some("456".to_string()),
1272            version: Some("1.1.0".to_string()),
1273            lifecycle_stage: Some("Internal or Alpha Testing".to_string()),
1274            launch_date: None,
1275            sandbox_id: Some("789".to_string()),
1276        };
1277
1278        assert_eq!(request.app_id, "123");
1279        assert_eq!(request.build_id, Some("456".to_string()));
1280        assert_eq!(request.sandbox_id, Some("789".to_string()));
1281    }
1282
1283    #[test]
1284    fn test_lifecycle_stage_validation() {
1285        // Test valid lifecycle stages
1286        assert!(is_valid_lifecycle_stage("In Development (pre-Alpha)"));
1287        assert!(is_valid_lifecycle_stage("Internal or Alpha Testing"));
1288        assert!(is_valid_lifecycle_stage("External or Beta Testing"));
1289        assert!(is_valid_lifecycle_stage("Deployed"));
1290        assert!(is_valid_lifecycle_stage("Maintenance"));
1291        assert!(is_valid_lifecycle_stage("Cannot Disclose"));
1292        assert!(is_valid_lifecycle_stage("Not Specified"));
1293
1294        // Test invalid lifecycle stages
1295        assert!(!is_valid_lifecycle_stage("In Development"));
1296        assert!(!is_valid_lifecycle_stage("Development"));
1297        assert!(!is_valid_lifecycle_stage("QA"));
1298        assert!(!is_valid_lifecycle_stage("Production"));
1299        assert!(!is_valid_lifecycle_stage(""));
1300
1301        // Test default
1302        assert_eq!(default_lifecycle_stage(), "In Development (pre-Alpha)");
1303        assert!(is_valid_lifecycle_stage(default_lifecycle_stage()));
1304    }
1305
1306    #[test]
1307    fn test_build_error_display() {
1308        let error = BuildError::BuildNotFound;
1309        assert_eq!(error.to_string(), "Build not found");
1310
1311        let error = BuildError::InvalidParameter("Invalid app_id".to_string());
1312        assert_eq!(error.to_string(), "Invalid parameter: Invalid app_id");
1313
1314        let error = BuildError::CreationFailed("Build creation failed".to_string());
1315        assert_eq!(
1316            error.to_string(),
1317            "Build creation failed: Build creation failed"
1318        );
1319    }
1320
1321    #[tokio::test]
1322    async fn test_build_api_method_signatures() {
1323        async fn _test_build_methods() -> Result<(), Box<dyn std::error::Error>> {
1324            let config = VeracodeConfig::new("test", "test");
1325            let client = VeracodeClient::new(config)?;
1326            let api = client.build_api()?;
1327
1328            // Test that the method signatures exist and compile
1329            let create_request = CreateBuildRequest {
1330                app_id: "123".to_string(),
1331                version: None,
1332                lifecycle_stage: None,
1333                launch_date: None,
1334                sandbox_id: None,
1335            };
1336
1337            // These calls won't actually execute due to test environment,
1338            // but they validate the method signatures exist
1339            let _: Result<Build, _> = api.create_build(&create_request).await;
1340            let _: Result<Build, _> = api.create_simple_build("123", None).await;
1341            let _: Result<Build, _> = api.create_sandbox_build("123", "456", None).await;
1342            let _: Result<DeleteBuildResult, _> = api.delete_app_build("123").await;
1343            let _: Result<Build, _> = api.get_app_build_info("123").await;
1344            let _: Result<BuildList, _> = api.get_app_builds("123").await;
1345
1346            Ok(())
1347        }
1348
1349        // If this compiles, the methods have correct signatures
1350        // Test passes if no panic occurs
1351    }
1352
1353    #[test]
1354    fn test_build_status_from_str() {
1355        assert_eq!(
1356            BuildStatus::from_string("Incomplete"),
1357            BuildStatus::Incomplete
1358        );
1359        assert_eq!(
1360            BuildStatus::from_string("Results Ready"),
1361            BuildStatus::ResultsReady
1362        );
1363        assert_eq!(
1364            BuildStatus::from_string("Pre-Scan Failed"),
1365            BuildStatus::PreScanFailed
1366        );
1367        assert_eq!(
1368            BuildStatus::from_string("Unknown Status"),
1369            BuildStatus::Unknown("Unknown Status".to_string())
1370        );
1371    }
1372
1373    #[test]
1374    fn test_build_status_to_str() {
1375        assert_eq!(BuildStatus::Incomplete.to_str(), "Incomplete");
1376        assert_eq!(BuildStatus::ResultsReady.to_str(), "Results Ready");
1377        assert_eq!(BuildStatus::PreScanFailed.to_str(), "Pre-Scan Failed");
1378        assert_eq!(
1379            BuildStatus::Unknown("Custom".to_string()).to_str(),
1380            "Custom"
1381        );
1382    }
1383
1384    #[test]
1385    fn test_build_status_deletion_policy_0() {
1386        // Policy 0: Never delete builds
1387        assert!(!BuildStatus::Incomplete.is_safe_to_delete(0));
1388        assert!(!BuildStatus::ResultsReady.is_safe_to_delete(0));
1389        assert!(!BuildStatus::Failed.is_safe_to_delete(0));
1390    }
1391
1392    #[test]
1393    fn test_build_status_deletion_policy_1() {
1394        // Policy 1: Delete only safe builds (incomplete, failed, cancelled states)
1395        assert!(BuildStatus::Incomplete.is_safe_to_delete(1));
1396        assert!(BuildStatus::NotSubmitted.is_safe_to_delete(1));
1397        assert!(BuildStatus::PreScanFailed.is_safe_to_delete(1));
1398        assert!(BuildStatus::Failed.is_safe_to_delete(1));
1399        assert!(BuildStatus::Cancelled.is_safe_to_delete(1));
1400
1401        // Should not delete active or successful builds
1402        assert!(!BuildStatus::ResultsReady.is_safe_to_delete(1));
1403        assert!(!BuildStatus::ScanInProcess.is_safe_to_delete(1));
1404        assert!(!BuildStatus::PreScanSuccess.is_safe_to_delete(1));
1405    }
1406
1407    #[test]
1408    fn test_build_status_deletion_policy_2() {
1409        // Policy 2: Delete any build except Results Ready
1410        assert!(BuildStatus::Incomplete.is_safe_to_delete(2));
1411        assert!(BuildStatus::Failed.is_safe_to_delete(2));
1412        assert!(BuildStatus::ScanInProcess.is_safe_to_delete(2));
1413        assert!(BuildStatus::PreScanSuccess.is_safe_to_delete(2));
1414
1415        // Should not delete Results Ready
1416        assert!(!BuildStatus::ResultsReady.is_safe_to_delete(2));
1417    }
1418
1419    #[test]
1420    fn test_build_status_deletion_policy_invalid() {
1421        // Invalid policy should default to never delete
1422        assert!(!BuildStatus::Incomplete.is_safe_to_delete(3));
1423        assert!(!BuildStatus::Failed.is_safe_to_delete(255));
1424    }
1425}