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)]
272pub enum BuildError {
273    /// Veracode API error
274    Api(VeracodeError),
275    /// Build not found
276    BuildNotFound,
277    /// Application not found
278    ApplicationNotFound,
279    /// Sandbox not found
280    SandboxNotFound,
281    /// Invalid parameter
282    InvalidParameter(String),
283    /// Build creation failed
284    CreationFailed(String),
285    /// Build update failed
286    UpdateFailed(String),
287    /// Build deletion failed
288    DeletionFailed(String),
289    /// XML parsing error
290    XmlParsingError(String),
291    /// Unauthorized access
292    Unauthorized,
293    /// Permission denied
294    PermissionDenied,
295    /// Build in progress (cannot modify)
296    BuildInProgress,
297}
298
299impl std::fmt::Display for BuildError {
300    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
301        match self {
302            BuildError::Api(err) => write!(f, "API error: {err}"),
303            BuildError::BuildNotFound => write!(f, "Build not found"),
304            BuildError::ApplicationNotFound => write!(f, "Application not found"),
305            BuildError::SandboxNotFound => write!(f, "Sandbox not found"),
306            BuildError::InvalidParameter(msg) => write!(f, "Invalid parameter: {msg}"),
307            BuildError::CreationFailed(msg) => write!(f, "Build creation failed: {msg}"),
308            BuildError::UpdateFailed(msg) => write!(f, "Build update failed: {msg}"),
309            BuildError::DeletionFailed(msg) => write!(f, "Build deletion failed: {msg}"),
310            BuildError::XmlParsingError(msg) => write!(f, "XML parsing error: {msg}"),
311            BuildError::Unauthorized => write!(f, "Unauthorized access"),
312            BuildError::PermissionDenied => write!(f, "Permission denied"),
313            BuildError::BuildInProgress => write!(f, "Build in progress, cannot modify"),
314        }
315    }
316}
317
318impl std::error::Error for BuildError {}
319
320impl From<VeracodeError> for BuildError {
321    fn from(err: VeracodeError) -> Self {
322        BuildError::Api(err)
323    }
324}
325
326impl From<std::io::Error> for BuildError {
327    fn from(err: std::io::Error) -> Self {
328        BuildError::Api(VeracodeError::InvalidResponse(err.to_string()))
329    }
330}
331
332impl From<reqwest::Error> for BuildError {
333    fn from(err: reqwest::Error) -> Self {
334        BuildError::Api(VeracodeError::Http(err))
335    }
336}
337
338/// Build API operations for Veracode platform
339pub struct BuildApi {
340    client: VeracodeClient,
341}
342
343impl BuildApi {
344    /// Create a new `BuildApi` instance
345    #[must_use]
346    pub fn new(client: VeracodeClient) -> Self {
347        Self { client }
348    }
349
350    /// Create a new build
351    ///
352    /// # Arguments
353    ///
354    /// * `request` - The create build request
355    ///
356    /// # Returns
357    ///
358    /// A `Result` containing the created build information or an error.
359    pub async fn create_build(&self, request: &CreateBuildRequest) -> Result<Build, BuildError> {
360        let endpoint = "/api/5.0/createbuild.do";
361
362        // Build query parameters
363        let mut query_params = Vec::new();
364        query_params.push(("app_id", request.app_id.as_str()));
365
366        if let Some(version) = &request.version {
367            query_params.push(("version", version.as_str()));
368        }
369
370        if let Some(lifecycle_stage) = &request.lifecycle_stage {
371            query_params.push(("lifecycle_stage", lifecycle_stage.as_str()));
372        }
373
374        if let Some(launch_date) = &request.launch_date {
375            query_params.push(("launch_date", launch_date.as_str()));
376        }
377
378        if let Some(sandbox_id) = &request.sandbox_id {
379            query_params.push(("sandbox_id", sandbox_id.as_str()));
380        }
381
382        let response = self
383            .client
384            .post_with_query_params(endpoint, &query_params)
385            .await?;
386
387        let status = response.status().as_u16();
388        match status {
389            200 => {
390                let response_text = response.text().await?;
391                self.parse_build_info(&response_text)
392            }
393            400 => {
394                let error_text = response.text().await.unwrap_or_default();
395                Err(BuildError::InvalidParameter(error_text))
396            }
397            401 => Err(BuildError::Unauthorized),
398            403 => Err(BuildError::PermissionDenied),
399            404 => Err(BuildError::ApplicationNotFound),
400            _ => {
401                let error_text = response.text().await.unwrap_or_default();
402                Err(BuildError::CreationFailed(format!(
403                    "HTTP {status}: {error_text}"
404                )))
405            }
406        }
407    }
408
409    /// Update an existing build
410    ///
411    /// # Arguments
412    ///
413    /// * `request` - The update build request
414    ///
415    /// # Returns
416    ///
417    /// A `Result` containing the updated build information or an error.
418    pub async fn update_build(&self, request: &UpdateBuildRequest) -> Result<Build, BuildError> {
419        let endpoint = "/api/5.0/updatebuild.do";
420
421        // Build query parameters
422        let mut query_params = Vec::new();
423        query_params.push(("app_id", request.app_id.as_str()));
424
425        if let Some(build_id) = &request.build_id {
426            query_params.push(("build_id", build_id.as_str()));
427        }
428
429        if let Some(version) = &request.version {
430            query_params.push(("version", version.as_str()));
431        }
432
433        if let Some(lifecycle_stage) = &request.lifecycle_stage {
434            query_params.push(("lifecycle_stage", lifecycle_stage.as_str()));
435        }
436
437        if let Some(launch_date) = &request.launch_date {
438            query_params.push(("launch_date", launch_date.as_str()));
439        }
440
441        if let Some(sandbox_id) = &request.sandbox_id {
442            query_params.push(("sandbox_id", sandbox_id.as_str()));
443        }
444
445        let response = self
446            .client
447            .post_with_query_params(endpoint, &query_params)
448            .await?;
449
450        let status = response.status().as_u16();
451        match status {
452            200 => {
453                let response_text = response.text().await?;
454                self.parse_build_info(&response_text)
455            }
456            400 => {
457                let error_text = response.text().await.unwrap_or_default();
458                Err(BuildError::InvalidParameter(error_text))
459            }
460            401 => Err(BuildError::Unauthorized),
461            403 => Err(BuildError::PermissionDenied),
462            404 => {
463                if request.sandbox_id.is_some() {
464                    Err(BuildError::SandboxNotFound)
465                } else {
466                    Err(BuildError::BuildNotFound)
467                }
468            }
469            _ => {
470                let error_text = response.text().await.unwrap_or_default();
471                Err(BuildError::UpdateFailed(format!(
472                    "HTTP {status}: {error_text}"
473                )))
474            }
475        }
476    }
477
478    /// Delete a build
479    ///
480    /// # Arguments
481    ///
482    /// * `request` - The delete build request
483    ///
484    /// # Returns
485    ///
486    /// A `Result` containing the deletion result or an error.
487    pub async fn delete_build(
488        &self,
489        request: &DeleteBuildRequest,
490    ) -> Result<DeleteBuildResult, BuildError> {
491        let endpoint = "/api/5.0/deletebuild.do";
492
493        // Build query parameters
494        let mut query_params = Vec::new();
495        query_params.push(("app_id", request.app_id.as_str()));
496
497        if let Some(sandbox_id) = &request.sandbox_id {
498            query_params.push(("sandbox_id", sandbox_id.as_str()));
499        }
500
501        let response = self
502            .client
503            .post_with_query_params(endpoint, &query_params)
504            .await?;
505
506        let status = response.status().as_u16();
507        match status {
508            200 => {
509                let response_text = response.text().await?;
510                self.parse_delete_result(&response_text)
511            }
512            400 => {
513                let error_text = response.text().await.unwrap_or_default();
514                Err(BuildError::InvalidParameter(error_text))
515            }
516            401 => Err(BuildError::Unauthorized),
517            403 => Err(BuildError::PermissionDenied),
518            404 => {
519                if request.sandbox_id.is_some() {
520                    Err(BuildError::SandboxNotFound)
521                } else {
522                    Err(BuildError::BuildNotFound)
523                }
524            }
525            _ => {
526                let error_text = response.text().await.unwrap_or_default();
527                Err(BuildError::DeletionFailed(format!(
528                    "HTTP {status}: {error_text}"
529                )))
530            }
531        }
532    }
533
534    /// Get build information
535    ///
536    /// # Arguments
537    ///
538    /// * `request` - The get build info request
539    ///
540    /// # Returns
541    ///
542    /// A `Result` containing the build information or an error.
543    pub async fn get_build_info(&self, request: &GetBuildInfoRequest) -> Result<Build, BuildError> {
544        let endpoint = "/api/5.0/getbuildinfo.do";
545
546        // Build query parameters
547        let mut query_params = Vec::new();
548        query_params.push(("app_id", request.app_id.as_str()));
549
550        if let Some(build_id) = &request.build_id {
551            query_params.push(("build_id", build_id.as_str()));
552        }
553
554        if let Some(sandbox_id) = &request.sandbox_id {
555            query_params.push(("sandbox_id", sandbox_id.as_str()));
556        }
557
558        let response = self
559            .client
560            .get_with_query_params(endpoint, &query_params)
561            .await?;
562
563        let status = response.status().as_u16();
564        match status {
565            200 => {
566                let response_text = response.text().await?;
567                debug!("🌐 Raw XML response from getbuildinfo.do:\n{response_text}");
568                self.parse_build_info(&response_text)
569            }
570            400 => {
571                let error_text = response.text().await.unwrap_or_default();
572                Err(BuildError::InvalidParameter(error_text))
573            }
574            401 => Err(BuildError::Unauthorized),
575            403 => Err(BuildError::PermissionDenied),
576            404 => {
577                if request.sandbox_id.is_some() {
578                    Err(BuildError::SandboxNotFound)
579                } else {
580                    Err(BuildError::BuildNotFound)
581                }
582            }
583            _ => {
584                let error_text = response.text().await.unwrap_or_default();
585                Err(BuildError::Api(VeracodeError::InvalidResponse(format!(
586                    "HTTP {status}: {error_text}"
587                ))))
588            }
589        }
590    }
591
592    /// Get list of builds
593    ///
594    /// # Arguments
595    ///
596    /// * `request` - The get build list request
597    ///
598    /// # Returns
599    ///
600    /// A `Result` containing the build list or an error.
601    pub async fn get_build_list(
602        &self,
603        request: &GetBuildListRequest,
604    ) -> Result<BuildList, BuildError> {
605        let endpoint = "/api/5.0/getbuildlist.do";
606
607        // Build query parameters
608        let mut query_params = Vec::new();
609        query_params.push(("app_id", request.app_id.as_str()));
610
611        if let Some(sandbox_id) = &request.sandbox_id {
612            query_params.push(("sandbox_id", sandbox_id.as_str()));
613        }
614
615        let response = self
616            .client
617            .get_with_query_params(endpoint, &query_params)
618            .await?;
619
620        let status = response.status().as_u16();
621        match status {
622            200 => {
623                let response_text = response.text().await?;
624                self.parse_build_list(&response_text)
625            }
626            400 => {
627                let error_text = response.text().await.unwrap_or_default();
628                Err(BuildError::InvalidParameter(error_text))
629            }
630            401 => Err(BuildError::Unauthorized),
631            403 => Err(BuildError::PermissionDenied),
632            404 => {
633                if request.sandbox_id.is_some() {
634                    Err(BuildError::SandboxNotFound)
635                } else {
636                    Err(BuildError::ApplicationNotFound)
637                }
638            }
639            _ => {
640                let error_text = response.text().await.unwrap_or_default();
641                Err(BuildError::Api(VeracodeError::InvalidResponse(format!(
642                    "HTTP {status}: {error_text}"
643                ))))
644            }
645        }
646    }
647
648    /// Parse build info XML response
649    fn parse_build_info(&self, xml: &str) -> Result<Build, BuildError> {
650        // Check if response contains an error element first
651        if xml.contains("<error>") {
652            let mut reader = Reader::from_str(xml);
653            reader.config_mut().trim_text(true);
654            let mut buf = Vec::new();
655
656            loop {
657                match reader.read_event_into(&mut buf) {
658                    Ok(Event::Start(ref e)) if e.name().as_ref() == b"error" => {
659                        if let Ok(Event::Text(text)) = reader.read_event_into(&mut buf) {
660                            let error_msg = String::from_utf8_lossy(&text);
661                            if error_msg.contains("Could not find a build") {
662                                return Err(BuildError::BuildNotFound);
663                            }
664                            return Err(BuildError::Api(VeracodeError::InvalidResponse(
665                                error_msg.to_string(),
666                            )));
667                        }
668                    }
669                    Ok(Event::Eof) => break,
670                    Err(e) => return Err(BuildError::XmlParsingError(e.to_string())),
671                    _ => {}
672                }
673                buf.clear();
674            }
675        }
676
677        let mut reader = Reader::from_str(xml);
678        reader.config_mut().trim_text(true);
679
680        let mut buf = Vec::new();
681        let mut build = Build {
682            build_id: String::new(),
683            app_id: String::new(),
684            version: None,
685            app_name: None,
686            sandbox_id: None,
687            sandbox_name: None,
688            lifecycle_stage: None,
689            launch_date: None,
690            submitter: None,
691            platform: None,
692            analysis_unit: None,
693            policy_name: None,
694            policy_version: None,
695            policy_compliance_status: None,
696            rules_status: None,
697            grace_period_expired: None,
698            scan_overdue: None,
699            policy_updated_date: None,
700            legacy_scan_engine: None,
701            attributes: HashMap::new(),
702        };
703
704        let mut inside_build = false;
705
706        loop {
707            match reader.read_event_into(&mut buf) {
708                Ok(Event::Start(ref e)) => {
709                    match e.name().as_ref() {
710                        b"build" => {
711                            inside_build = true;
712                            for attr in e.attributes().flatten() {
713                                let key = String::from_utf8_lossy(attr.key.as_ref());
714                                let value = String::from_utf8_lossy(&attr.value);
715
716                                match key.as_ref() {
717                                    "build_id" => build.build_id = value.into_owned(),
718                                    "app_id" => build.app_id = value.into_owned(),
719                                    "version" => build.version = Some(value.into_owned()),
720                                    "app_name" => build.app_name = Some(value.into_owned()),
721                                    "sandbox_id" => build.sandbox_id = Some(value.into_owned()),
722                                    "sandbox_name" => build.sandbox_name = Some(value.into_owned()),
723                                    "lifecycle_stage" => {
724                                        build.lifecycle_stage = Some(value.into_owned())
725                                    }
726                                    "submitter" => build.submitter = Some(value.into_owned()),
727                                    "platform" => build.platform = Some(value.into_owned()),
728                                    "analysis_unit" => {
729                                        build.analysis_unit = Some(value.into_owned())
730                                    }
731                                    "policy_name" => build.policy_name = Some(value.into_owned()),
732                                    "policy_version" => {
733                                        build.policy_version = Some(value.into_owned())
734                                    }
735                                    "policy_compliance_status" => {
736                                        build.policy_compliance_status = Some(value.into_owned())
737                                    }
738                                    "rules_status" => build.rules_status = Some(value.into_owned()),
739                                    "grace_period_expired" => {
740                                        build.grace_period_expired = value.parse::<bool>().ok();
741                                    }
742                                    "scan_overdue" => {
743                                        build.scan_overdue = value.parse::<bool>().ok();
744                                    }
745                                    "legacy_scan_engine" => {
746                                        build.legacy_scan_engine = value.parse::<bool>().ok();
747                                    }
748                                    "launch_date" => {
749                                        if let Ok(date) =
750                                            NaiveDate::parse_from_str(&value, "%m/%d/%Y")
751                                        {
752                                            build.launch_date = Some(date);
753                                        }
754                                    }
755                                    "policy_updated_date" => {
756                                        if let Ok(datetime) =
757                                            chrono::DateTime::parse_from_rfc3339(&value)
758                                        {
759                                            build.policy_updated_date =
760                                                Some(datetime.with_timezone(&Utc));
761                                        }
762                                    }
763                                    _ => {
764                                        build
765                                            .attributes
766                                            .insert(key.into_owned(), value.into_owned());
767                                    }
768                                }
769                            }
770                        }
771                        b"analysis_unit" if inside_build => {
772                            // Parse analysis_unit element nested inside build (primary source for build status)
773                            for attr in e.attributes().flatten() {
774                                let key = String::from_utf8_lossy(attr.key.as_ref());
775                                let value = String::from_utf8_lossy(&attr.value);
776
777                                // Store all analysis_unit attributes, especially status
778                                match key.as_ref() {
779                                    "status" => {
780                                        // Store the analysis_unit status as the primary status
781                                        build
782                                            .attributes
783                                            .insert("status".to_string(), value.into_owned());
784                                    }
785                                    _ => {
786                                        // Store other analysis_unit attributes with prefix
787                                        build
788                                            .attributes
789                                            .insert(format!("analysis_{key}"), value.into_owned());
790                                    }
791                                }
792                            }
793                        }
794                        _ => {}
795                    }
796                }
797                Ok(Event::Empty(ref e)) => {
798                    // Handle self-closing elements like <analysis_unit ... />
799                    if e.name().as_ref() == b"analysis_unit" && inside_build {
800                        for attr in e.attributes().flatten() {
801                            let key = String::from_utf8_lossy(attr.key.as_ref());
802                            let value = String::from_utf8_lossy(&attr.value);
803
804                            match key.as_ref() {
805                                "status" => {
806                                    build
807                                        .attributes
808                                        .insert("status".to_string(), value.into_owned());
809                                }
810                                _ => {
811                                    build
812                                        .attributes
813                                        .insert(format!("analysis_{key}"), value.into_owned());
814                                }
815                            }
816                        }
817                    }
818                }
819                Ok(Event::End(ref e)) => {
820                    if e.name().as_ref() == b"build" {
821                        inside_build = false;
822                    }
823                }
824                Ok(Event::Eof) => break,
825                Err(e) => return Err(BuildError::XmlParsingError(e.to_string())),
826                _ => {}
827            }
828            buf.clear();
829        }
830
831        if build.build_id.is_empty() {
832            return Err(BuildError::XmlParsingError(
833                "No build information found in response".to_string(),
834            ));
835        }
836
837        Ok(build)
838    }
839
840    /// Parse build list XML response
841    fn parse_build_list(&self, xml: &str) -> Result<BuildList, BuildError> {
842        let mut reader = Reader::from_str(xml);
843        reader.config_mut().trim_text(true);
844
845        let mut buf = Vec::new();
846        let mut build_list = BuildList {
847            account_id: None,
848            app_id: String::new(),
849            app_name: None,
850            builds: Vec::new(),
851        };
852
853        loop {
854            match reader.read_event_into(&mut buf) {
855                Ok(Event::Start(ref e)) => match e.name().as_ref() {
856                    b"buildlist" => {
857                        for attr in e.attributes().flatten() {
858                            let key = String::from_utf8_lossy(attr.key.as_ref());
859                            let value = String::from_utf8_lossy(&attr.value);
860
861                            match key.as_ref() {
862                                "account_id" => build_list.account_id = Some(value.into_owned()),
863                                "app_id" => build_list.app_id = value.into_owned(),
864                                "app_name" => build_list.app_name = Some(value.into_owned()),
865                                _ => {}
866                            }
867                        }
868                    }
869                    b"build" => {
870                        let mut build = Build {
871                            build_id: String::new(),
872                            app_id: build_list.app_id.clone(),
873                            version: None,
874                            app_name: build_list.app_name.clone(),
875                            sandbox_id: None,
876                            sandbox_name: None,
877                            lifecycle_stage: None,
878                            launch_date: None,
879                            submitter: None,
880                            platform: None,
881                            analysis_unit: None,
882                            policy_name: None,
883                            policy_version: None,
884                            policy_compliance_status: None,
885                            rules_status: None,
886                            grace_period_expired: None,
887                            scan_overdue: None,
888                            policy_updated_date: None,
889                            legacy_scan_engine: None,
890                            attributes: HashMap::new(),
891                        };
892
893                        for attr in e.attributes().flatten() {
894                            let key = String::from_utf8_lossy(attr.key.as_ref());
895                            let value = String::from_utf8_lossy(&attr.value);
896
897                            match key.as_ref() {
898                                "build_id" => build.build_id = value.into_owned(),
899                                "version" => build.version = Some(value.into_owned()),
900                                "sandbox_id" => build.sandbox_id = Some(value.into_owned()),
901                                "sandbox_name" => build.sandbox_name = Some(value.into_owned()),
902                                "lifecycle_stage" => {
903                                    build.lifecycle_stage = Some(value.into_owned())
904                                }
905                                "submitter" => build.submitter = Some(value.into_owned()),
906                                "platform" => build.platform = Some(value.into_owned()),
907                                "analysis_unit" => build.analysis_unit = Some(value.into_owned()),
908                                "policy_name" => build.policy_name = Some(value.into_owned()),
909                                "policy_version" => build.policy_version = Some(value.into_owned()),
910                                "policy_compliance_status" => {
911                                    build.policy_compliance_status = Some(value.into_owned())
912                                }
913                                "rules_status" => build.rules_status = Some(value.into_owned()),
914                                "grace_period_expired" => {
915                                    build.grace_period_expired = value.parse::<bool>().ok();
916                                }
917                                "scan_overdue" => {
918                                    build.scan_overdue = value.parse::<bool>().ok();
919                                }
920                                "legacy_scan_engine" => {
921                                    build.legacy_scan_engine = value.parse::<bool>().ok();
922                                }
923                                "launch_date" => {
924                                    if let Ok(date) = NaiveDate::parse_from_str(&value, "%m/%d/%Y")
925                                    {
926                                        build.launch_date = Some(date);
927                                    }
928                                }
929                                "policy_updated_date" => {
930                                    if let Ok(datetime) =
931                                        chrono::DateTime::parse_from_rfc3339(&value)
932                                    {
933                                        build.policy_updated_date =
934                                            Some(datetime.with_timezone(&Utc));
935                                    }
936                                }
937                                _ => {
938                                    build
939                                        .attributes
940                                        .insert(key.into_owned(), value.into_owned());
941                                }
942                            }
943                        }
944
945                        if !build.build_id.is_empty() {
946                            build_list.builds.push(build);
947                        }
948                    }
949                    _ => {}
950                },
951                Ok(Event::Eof) => break,
952                Err(e) => return Err(BuildError::XmlParsingError(e.to_string())),
953                _ => {}
954            }
955            buf.clear();
956        }
957
958        Ok(build_list)
959    }
960
961    /// Parse delete build result XML response
962    fn parse_delete_result(&self, xml: &str) -> Result<DeleteBuildResult, BuildError> {
963        let mut reader = Reader::from_str(xml);
964        reader.config_mut().trim_text(true);
965
966        let mut buf = Vec::new();
967        let mut result = String::new();
968
969        loop {
970            match reader.read_event_into(&mut buf) {
971                Ok(Event::Start(ref e)) => {
972                    if e.name().as_ref() == b"result" {
973                        // Read the text content of the result element
974                        if let Ok(Event::Text(e)) = reader.read_event_into(&mut buf) {
975                            result = String::from_utf8_lossy(&e).into_owned();
976                        }
977                    }
978                }
979                Ok(Event::Eof) => break,
980                Err(e) => return Err(BuildError::XmlParsingError(e.to_string())),
981                _ => {}
982            }
983            buf.clear();
984        }
985
986        if result.is_empty() {
987            return Err(BuildError::XmlParsingError(
988                "No result found in delete response".to_string(),
989            ));
990        }
991
992        Ok(DeleteBuildResult { result })
993    }
994}
995
996// Convenience methods implementation
997impl BuildApi {
998    /// Create a build with minimal parameters
999    ///
1000    /// # Arguments
1001    ///
1002    /// * `app_id` - Application ID
1003    /// * `version` - Optional build version
1004    ///
1005    /// # Returns
1006    ///
1007    /// A `Result` containing the created build information or an error.
1008    pub async fn create_simple_build(
1009        &self,
1010        app_id: &str,
1011        version: Option<&str>,
1012    ) -> Result<Build, BuildError> {
1013        let request = CreateBuildRequest {
1014            app_id: app_id.to_string(),
1015            version: version.map(str::to_string),
1016            lifecycle_stage: None,
1017            launch_date: None,
1018            sandbox_id: None,
1019        };
1020
1021        self.create_build(&request).await
1022    }
1023
1024    /// Create a sandbox build
1025    ///
1026    /// # Arguments
1027    ///
1028    /// * `app_id` - Application ID
1029    /// * `sandbox_id` - Sandbox ID
1030    /// * `version` - Optional build version
1031    ///
1032    /// # Returns
1033    ///
1034    /// A `Result` containing the created build information or an error.
1035    pub async fn create_sandbox_build(
1036        &self,
1037        app_id: &str,
1038        sandbox_id: &str,
1039        version: Option<&str>,
1040    ) -> Result<Build, BuildError> {
1041        let request = CreateBuildRequest {
1042            app_id: app_id.to_string(),
1043            version: version.map(str::to_string),
1044            lifecycle_stage: None,
1045            launch_date: None,
1046            sandbox_id: Some(sandbox_id.to_string()),
1047        };
1048
1049        self.create_build(&request).await
1050    }
1051
1052    /// Delete the most recent application build
1053    ///
1054    /// # Arguments
1055    ///
1056    /// * `app_id` - Application ID
1057    ///
1058    /// # Returns
1059    ///
1060    /// A `Result` containing the deletion result or an error.
1061    pub async fn delete_app_build(&self, app_id: &str) -> Result<DeleteBuildResult, BuildError> {
1062        let request = DeleteBuildRequest {
1063            app_id: app_id.to_string(),
1064            sandbox_id: None,
1065        };
1066
1067        self.delete_build(&request).await
1068    }
1069
1070    /// Delete the most recent sandbox build
1071    ///
1072    /// # Arguments
1073    ///
1074    /// * `app_id` - Application ID
1075    /// * `sandbox_id` - Sandbox ID
1076    ///
1077    /// # Returns
1078    ///
1079    /// A `Result` containing the deletion result or an error.
1080    pub async fn delete_sandbox_build(
1081        &self,
1082        app_id: &str,
1083        sandbox_id: &str,
1084    ) -> Result<DeleteBuildResult, BuildError> {
1085        let request = DeleteBuildRequest {
1086            app_id: app_id.to_string(),
1087            sandbox_id: Some(sandbox_id.to_string()),
1088        };
1089
1090        self.delete_build(&request).await
1091    }
1092
1093    /// Get the most recent build info for an application
1094    ///
1095    /// # Arguments
1096    ///
1097    /// * `app_id` - Application ID
1098    ///
1099    /// # Returns
1100    ///
1101    /// A `Result` containing the build information or an error.
1102    pub async fn get_app_build_info(&self, app_id: &str) -> Result<Build, BuildError> {
1103        let request = GetBuildInfoRequest {
1104            app_id: app_id.to_string(),
1105            build_id: None,
1106            sandbox_id: None,
1107        };
1108
1109        self.get_build_info(&request).await
1110    }
1111
1112    /// Get build info for a specific sandbox
1113    ///
1114    /// # Arguments
1115    ///
1116    /// * `app_id` - Application ID
1117    /// * `sandbox_id` - Sandbox ID
1118    ///
1119    /// # Returns
1120    ///
1121    /// A `Result` containing the build information or an error.
1122    pub async fn get_sandbox_build_info(
1123        &self,
1124        app_id: &str,
1125        sandbox_id: &str,
1126    ) -> Result<Build, BuildError> {
1127        let request = GetBuildInfoRequest {
1128            app_id: app_id.to_string(),
1129            build_id: None,
1130            sandbox_id: Some(sandbox_id.to_string()),
1131        };
1132
1133        self.get_build_info(&request).await
1134    }
1135
1136    /// Get list of all builds for an application
1137    ///
1138    /// # Arguments
1139    ///
1140    /// * `app_id` - Application ID
1141    ///
1142    /// # Returns
1143    ///
1144    /// A `Result` containing the build list or an error.
1145    pub async fn get_app_builds(&self, app_id: &str) -> Result<BuildList, BuildError> {
1146        let request = GetBuildListRequest {
1147            app_id: app_id.to_string(),
1148            sandbox_id: None,
1149        };
1150
1151        self.get_build_list(&request).await
1152    }
1153
1154    /// Get list of builds for a sandbox
1155    ///
1156    /// # Arguments
1157    ///
1158    /// * `app_id` - Application ID
1159    /// * `sandbox_id` - Sandbox ID
1160    ///
1161    /// # Returns
1162    ///
1163    /// A `Result` containing the build list or an error.
1164    pub async fn get_sandbox_builds(
1165        &self,
1166        app_id: &str,
1167        sandbox_id: &str,
1168    ) -> Result<BuildList, BuildError> {
1169        let request = GetBuildListRequest {
1170            app_id: app_id.to_string(),
1171            sandbox_id: Some(sandbox_id.to_string()),
1172        };
1173
1174        self.get_build_list(&request).await
1175    }
1176}
1177
1178#[cfg(test)]
1179mod tests {
1180    use super::*;
1181    use crate::VeracodeConfig;
1182
1183    #[test]
1184    fn test_create_build_request() {
1185        let request = CreateBuildRequest {
1186            app_id: "123".to_string(),
1187            version: Some("1.0.0".to_string()),
1188            lifecycle_stage: Some("In Development (pre-Alpha)".to_string()),
1189            launch_date: Some("12/31/2024".to_string()),
1190            sandbox_id: None,
1191        };
1192
1193        assert_eq!(request.app_id, "123");
1194        assert_eq!(request.version, Some("1.0.0".to_string()));
1195        assert_eq!(
1196            request.lifecycle_stage,
1197            Some("In Development (pre-Alpha)".to_string())
1198        );
1199    }
1200
1201    #[test]
1202    fn test_update_build_request() {
1203        let request = UpdateBuildRequest {
1204            app_id: "123".to_string(),
1205            build_id: Some("456".to_string()),
1206            version: Some("1.1.0".to_string()),
1207            lifecycle_stage: Some("Internal or Alpha Testing".to_string()),
1208            launch_date: None,
1209            sandbox_id: Some("789".to_string()),
1210        };
1211
1212        assert_eq!(request.app_id, "123");
1213        assert_eq!(request.build_id, Some("456".to_string()));
1214        assert_eq!(request.sandbox_id, Some("789".to_string()));
1215    }
1216
1217    #[test]
1218    fn test_lifecycle_stage_validation() {
1219        // Test valid lifecycle stages
1220        assert!(is_valid_lifecycle_stage("In Development (pre-Alpha)"));
1221        assert!(is_valid_lifecycle_stage("Internal or Alpha Testing"));
1222        assert!(is_valid_lifecycle_stage("External or Beta Testing"));
1223        assert!(is_valid_lifecycle_stage("Deployed"));
1224        assert!(is_valid_lifecycle_stage("Maintenance"));
1225        assert!(is_valid_lifecycle_stage("Cannot Disclose"));
1226        assert!(is_valid_lifecycle_stage("Not Specified"));
1227
1228        // Test invalid lifecycle stages
1229        assert!(!is_valid_lifecycle_stage("In Development"));
1230        assert!(!is_valid_lifecycle_stage("Development"));
1231        assert!(!is_valid_lifecycle_stage("QA"));
1232        assert!(!is_valid_lifecycle_stage("Production"));
1233        assert!(!is_valid_lifecycle_stage(""));
1234
1235        // Test default
1236        assert_eq!(default_lifecycle_stage(), "In Development (pre-Alpha)");
1237        assert!(is_valid_lifecycle_stage(default_lifecycle_stage()));
1238    }
1239
1240    #[test]
1241    fn test_build_error_display() {
1242        let error = BuildError::BuildNotFound;
1243        assert_eq!(error.to_string(), "Build not found");
1244
1245        let error = BuildError::InvalidParameter("Invalid app_id".to_string());
1246        assert_eq!(error.to_string(), "Invalid parameter: Invalid app_id");
1247
1248        let error = BuildError::CreationFailed("Build creation failed".to_string());
1249        assert_eq!(
1250            error.to_string(),
1251            "Build creation failed: Build creation failed"
1252        );
1253    }
1254
1255    #[tokio::test]
1256    async fn test_build_api_method_signatures() {
1257        async fn _test_build_methods() -> Result<(), Box<dyn std::error::Error>> {
1258            let config = VeracodeConfig::new("test", "test");
1259            let client = VeracodeClient::new(config)?;
1260            let api = client.build_api();
1261
1262            // Test that the method signatures exist and compile
1263            let create_request = CreateBuildRequest {
1264                app_id: "123".to_string(),
1265                version: None,
1266                lifecycle_stage: None,
1267                launch_date: None,
1268                sandbox_id: None,
1269            };
1270
1271            // These calls won't actually execute due to test environment,
1272            // but they validate the method signatures exist
1273            let _: Result<Build, _> = api.create_build(&create_request).await;
1274            let _: Result<Build, _> = api.create_simple_build("123", None).await;
1275            let _: Result<Build, _> = api.create_sandbox_build("123", "456", None).await;
1276            let _: Result<DeleteBuildResult, _> = api.delete_app_build("123").await;
1277            let _: Result<Build, _> = api.get_app_build_info("123").await;
1278            let _: Result<BuildList, _> = api.get_app_builds("123").await;
1279
1280            Ok(())
1281        }
1282
1283        // If this compiles, the methods have correct signatures
1284        // Test passes if no panic occurs
1285    }
1286
1287    #[test]
1288    fn test_build_status_from_str() {
1289        assert_eq!(
1290            BuildStatus::from_string("Incomplete"),
1291            BuildStatus::Incomplete
1292        );
1293        assert_eq!(
1294            BuildStatus::from_string("Results Ready"),
1295            BuildStatus::ResultsReady
1296        );
1297        assert_eq!(
1298            BuildStatus::from_string("Pre-Scan Failed"),
1299            BuildStatus::PreScanFailed
1300        );
1301        assert_eq!(
1302            BuildStatus::from_string("Unknown Status"),
1303            BuildStatus::Unknown("Unknown Status".to_string())
1304        );
1305    }
1306
1307    #[test]
1308    fn test_build_status_to_str() {
1309        assert_eq!(BuildStatus::Incomplete.to_str(), "Incomplete");
1310        assert_eq!(BuildStatus::ResultsReady.to_str(), "Results Ready");
1311        assert_eq!(BuildStatus::PreScanFailed.to_str(), "Pre-Scan Failed");
1312        assert_eq!(
1313            BuildStatus::Unknown("Custom".to_string()).to_str(),
1314            "Custom"
1315        );
1316    }
1317
1318    #[test]
1319    fn test_build_status_deletion_policy_0() {
1320        // Policy 0: Never delete builds
1321        assert!(!BuildStatus::Incomplete.is_safe_to_delete(0));
1322        assert!(!BuildStatus::ResultsReady.is_safe_to_delete(0));
1323        assert!(!BuildStatus::Failed.is_safe_to_delete(0));
1324    }
1325
1326    #[test]
1327    fn test_build_status_deletion_policy_1() {
1328        // Policy 1: Delete only safe builds (incomplete, failed, cancelled states)
1329        assert!(BuildStatus::Incomplete.is_safe_to_delete(1));
1330        assert!(BuildStatus::NotSubmitted.is_safe_to_delete(1));
1331        assert!(BuildStatus::PreScanFailed.is_safe_to_delete(1));
1332        assert!(BuildStatus::Failed.is_safe_to_delete(1));
1333        assert!(BuildStatus::Cancelled.is_safe_to_delete(1));
1334
1335        // Should not delete active or successful builds
1336        assert!(!BuildStatus::ResultsReady.is_safe_to_delete(1));
1337        assert!(!BuildStatus::ScanInProcess.is_safe_to_delete(1));
1338        assert!(!BuildStatus::PreScanSuccess.is_safe_to_delete(1));
1339    }
1340
1341    #[test]
1342    fn test_build_status_deletion_policy_2() {
1343        // Policy 2: Delete any build except Results Ready
1344        assert!(BuildStatus::Incomplete.is_safe_to_delete(2));
1345        assert!(BuildStatus::Failed.is_safe_to_delete(2));
1346        assert!(BuildStatus::ScanInProcess.is_safe_to_delete(2));
1347        assert!(BuildStatus::PreScanSuccess.is_safe_to_delete(2));
1348
1349        // Should not delete Results Ready
1350        assert!(!BuildStatus::ResultsReady.is_safe_to_delete(2));
1351    }
1352
1353    #[test]
1354    fn test_build_status_deletion_policy_invalid() {
1355        // Invalid policy should default to never delete
1356        assert!(!BuildStatus::Incomplete.is_safe_to_delete(3));
1357        assert!(!BuildStatus::Failed.is_safe_to_delete(255));
1358    }
1359}