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