1use 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
16pub 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#[must_use]
29pub fn is_valid_lifecycle_stage(stage: &str) -> bool {
30 LIFECYCLE_STAGES.contains(&stage)
31}
32
33#[must_use]
35pub fn default_lifecycle_stage() -> &'static str {
36 "In Development (pre-Alpha)"
37}
38
39#[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), }
59
60impl BuildStatus {
61 #[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 #[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 #[must_use]
112 pub fn is_safe_to_delete(&self, deletion_policy: u8) -> bool {
113 match deletion_policy {
114 1 => {
115 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 !matches!(self, BuildStatus::ResultsReady)
132 }
133 _ => false, }
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#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct Build {
147 pub build_id: String,
149 pub app_id: String,
151 pub version: Option<String>,
153 pub app_name: Option<String>,
155 pub sandbox_id: Option<String>,
157 pub sandbox_name: Option<String>,
159 pub lifecycle_stage: Option<String>,
161 pub launch_date: Option<NaiveDate>,
163 pub submitter: Option<String>,
165 pub platform: Option<String>,
167 pub analysis_unit: Option<String>,
169 pub policy_name: Option<String>,
171 pub policy_version: Option<String>,
173 pub policy_compliance_status: Option<String>,
175 pub rules_status: Option<String>,
177 pub grace_period_expired: Option<bool>,
179 pub scan_overdue: Option<bool>,
181 pub policy_updated_date: Option<DateTime<Utc>>,
183 pub legacy_scan_engine: Option<bool>,
185 pub attributes: HashMap<String, String>,
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct BuildList {
192 pub account_id: Option<String>,
194 pub app_id: String,
196 pub app_name: Option<String>,
198 pub builds: Vec<Build>,
200}
201
202#[derive(Debug, Clone)]
204pub struct CreateBuildRequest {
205 pub app_id: String,
207 pub version: Option<String>,
209 pub lifecycle_stage: Option<String>,
211 pub launch_date: Option<String>,
213 pub sandbox_id: Option<String>,
215}
216
217#[derive(Debug, Clone)]
219pub struct UpdateBuildRequest {
220 pub app_id: String,
222 pub build_id: Option<String>,
224 pub version: Option<String>,
226 pub lifecycle_stage: Option<String>,
228 pub launch_date: Option<String>,
230 pub sandbox_id: Option<String>,
232}
233
234#[derive(Debug, Clone)]
236pub struct DeleteBuildRequest {
237 pub app_id: String,
239 pub sandbox_id: Option<String>,
241}
242
243#[derive(Debug, Clone)]
245pub struct GetBuildInfoRequest {
246 pub app_id: String,
248 pub build_id: Option<String>,
250 pub sandbox_id: Option<String>,
252}
253
254#[derive(Debug, Clone)]
256pub struct GetBuildListRequest {
257 pub app_id: String,
259 pub sandbox_id: Option<String>,
261}
262
263#[derive(Debug, Clone, Serialize, Deserialize)]
265pub struct DeleteBuildResult {
266 pub result: String,
268}
269
270#[derive(Debug)]
272#[must_use = "Need to handle all error enum types."]
273pub enum BuildError {
274 Api(VeracodeError),
276 BuildNotFound,
278 ApplicationNotFound,
280 SandboxNotFound,
282 InvalidParameter(String),
284 CreationFailed(String),
286 UpdateFailed(String),
288 DeletionFailed(String),
290 XmlParsingError(String),
292 Unauthorized,
294 PermissionDenied,
296 BuildInProgress,
298}
299
300impl std::fmt::Display for BuildError {
301 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
302 match self {
303 BuildError::Api(err) => write!(f, "API error: {err}"),
304 BuildError::BuildNotFound => write!(f, "Build not found"),
305 BuildError::ApplicationNotFound => write!(f, "Application not found"),
306 BuildError::SandboxNotFound => write!(f, "Sandbox not found"),
307 BuildError::InvalidParameter(msg) => write!(f, "Invalid parameter: {msg}"),
308 BuildError::CreationFailed(msg) => write!(f, "Build creation failed: {msg}"),
309 BuildError::UpdateFailed(msg) => write!(f, "Build update failed: {msg}"),
310 BuildError::DeletionFailed(msg) => write!(f, "Build deletion failed: {msg}"),
311 BuildError::XmlParsingError(msg) => write!(f, "XML parsing error: {msg}"),
312 BuildError::Unauthorized => write!(f, "Unauthorized access"),
313 BuildError::PermissionDenied => write!(f, "Permission denied"),
314 BuildError::BuildInProgress => write!(f, "Build in progress, cannot modify"),
315 }
316 }
317}
318
319impl std::error::Error for BuildError {}
320
321impl From<VeracodeError> for BuildError {
322 fn from(err: VeracodeError) -> Self {
323 BuildError::Api(err)
324 }
325}
326
327impl From<std::io::Error> for BuildError {
328 fn from(err: std::io::Error) -> Self {
329 BuildError::Api(VeracodeError::InvalidResponse(err.to_string()))
330 }
331}
332
333impl From<reqwest::Error> for BuildError {
334 fn from(err: reqwest::Error) -> Self {
335 BuildError::Api(VeracodeError::Http(err))
336 }
337}
338
339pub struct BuildApi {
341 client: VeracodeClient,
342}
343
344impl BuildApi {
345 #[must_use]
347 pub fn new(client: VeracodeClient) -> Self {
348 Self { client }
349 }
350
351 pub async fn create_build(&self, request: &CreateBuildRequest) -> Result<Build, BuildError> {
366 let endpoint = "/api/5.0/createbuild.do";
367
368 let mut query_params = Vec::new();
370 query_params.push(("app_id", request.app_id.as_str()));
371
372 if let Some(version) = &request.version {
373 query_params.push(("version", version.as_str()));
374 }
375
376 if let Some(lifecycle_stage) = &request.lifecycle_stage {
377 query_params.push(("lifecycle_stage", lifecycle_stage.as_str()));
378 }
379
380 if let Some(launch_date) = &request.launch_date {
381 query_params.push(("launch_date", launch_date.as_str()));
382 }
383
384 if let Some(sandbox_id) = &request.sandbox_id {
385 query_params.push(("sandbox_id", sandbox_id.as_str()));
386 }
387
388 let response = self
389 .client
390 .post_with_query_params(endpoint, &query_params)
391 .await?;
392
393 let status = response.status().as_u16();
394 match status {
395 200 => {
396 let response_text = response.text().await?;
397 self.parse_build_info(&response_text)
398 }
399 400 => {
400 let error_text = response.text().await.unwrap_or_default();
401 Err(BuildError::InvalidParameter(error_text))
402 }
403 401 => Err(BuildError::Unauthorized),
404 403 => Err(BuildError::PermissionDenied),
405 404 => Err(BuildError::ApplicationNotFound),
406 _ => {
407 let error_text = response.text().await.unwrap_or_default();
408 Err(BuildError::CreationFailed(format!(
409 "HTTP {status}: {error_text}"
410 )))
411 }
412 }
413 }
414
415 pub async fn update_build(&self, request: &UpdateBuildRequest) -> Result<Build, BuildError> {
430 let endpoint = "/api/5.0/updatebuild.do";
431
432 let mut query_params = Vec::new();
434 query_params.push(("app_id", request.app_id.as_str()));
435
436 if let Some(build_id) = &request.build_id {
437 query_params.push(("build_id", build_id.as_str()));
438 }
439
440 if let Some(version) = &request.version {
441 query_params.push(("version", version.as_str()));
442 }
443
444 if let Some(lifecycle_stage) = &request.lifecycle_stage {
445 query_params.push(("lifecycle_stage", lifecycle_stage.as_str()));
446 }
447
448 if let Some(launch_date) = &request.launch_date {
449 query_params.push(("launch_date", launch_date.as_str()));
450 }
451
452 if let Some(sandbox_id) = &request.sandbox_id {
453 query_params.push(("sandbox_id", sandbox_id.as_str()));
454 }
455
456 let response = self
457 .client
458 .post_with_query_params(endpoint, &query_params)
459 .await?;
460
461 let status = response.status().as_u16();
462 match status {
463 200 => {
464 let response_text = response.text().await?;
465 self.parse_build_info(&response_text)
466 }
467 400 => {
468 let error_text = response.text().await.unwrap_or_default();
469 Err(BuildError::InvalidParameter(error_text))
470 }
471 401 => Err(BuildError::Unauthorized),
472 403 => Err(BuildError::PermissionDenied),
473 404 => {
474 if request.sandbox_id.is_some() {
475 Err(BuildError::SandboxNotFound)
476 } else {
477 Err(BuildError::BuildNotFound)
478 }
479 }
480 _ => {
481 let error_text = response.text().await.unwrap_or_default();
482 Err(BuildError::UpdateFailed(format!(
483 "HTTP {status}: {error_text}"
484 )))
485 }
486 }
487 }
488
489 pub async fn delete_build(
504 &self,
505 request: &DeleteBuildRequest,
506 ) -> Result<DeleteBuildResult, BuildError> {
507 let endpoint = "/api/5.0/deletebuild.do";
508
509 let mut query_params = Vec::new();
511 query_params.push(("app_id", request.app_id.as_str()));
512
513 if let Some(sandbox_id) = &request.sandbox_id {
514 query_params.push(("sandbox_id", sandbox_id.as_str()));
515 }
516
517 let response = self
518 .client
519 .post_with_query_params(endpoint, &query_params)
520 .await?;
521
522 let status = response.status().as_u16();
523 match status {
524 200 => {
525 let response_text = response.text().await?;
526 self.parse_delete_result(&response_text)
527 }
528 400 => {
529 let error_text = response.text().await.unwrap_or_default();
530 Err(BuildError::InvalidParameter(error_text))
531 }
532 401 => Err(BuildError::Unauthorized),
533 403 => Err(BuildError::PermissionDenied),
534 404 => {
535 if request.sandbox_id.is_some() {
536 Err(BuildError::SandboxNotFound)
537 } else {
538 Err(BuildError::BuildNotFound)
539 }
540 }
541 _ => {
542 let error_text = response.text().await.unwrap_or_default();
543 Err(BuildError::DeletionFailed(format!(
544 "HTTP {status}: {error_text}"
545 )))
546 }
547 }
548 }
549
550 pub async fn get_build_info(&self, request: &GetBuildInfoRequest) -> Result<Build, BuildError> {
565 let endpoint = "/api/5.0/getbuildinfo.do";
566
567 let mut query_params = Vec::new();
569 query_params.push(("app_id", request.app_id.as_str()));
570
571 if let Some(build_id) = &request.build_id {
572 query_params.push(("build_id", build_id.as_str()));
573 }
574
575 if let Some(sandbox_id) = &request.sandbox_id {
576 query_params.push(("sandbox_id", sandbox_id.as_str()));
577 }
578
579 let response = self
580 .client
581 .get_with_query_params(endpoint, &query_params)
582 .await?;
583
584 let status = response.status().as_u16();
585 match status {
586 200 => {
587 let response_text = response.text().await?;
588 debug!("🌐 Raw XML response from getbuildinfo.do:\n{response_text}");
589 self.parse_build_info(&response_text)
590 }
591 400 => {
592 let error_text = response.text().await.unwrap_or_default();
593 Err(BuildError::InvalidParameter(error_text))
594 }
595 401 => Err(BuildError::Unauthorized),
596 403 => Err(BuildError::PermissionDenied),
597 404 => {
598 if request.sandbox_id.is_some() {
599 Err(BuildError::SandboxNotFound)
600 } else {
601 Err(BuildError::BuildNotFound)
602 }
603 }
604 _ => {
605 let error_text = response.text().await.unwrap_or_default();
606 Err(BuildError::Api(VeracodeError::InvalidResponse(format!(
607 "HTTP {status}: {error_text}"
608 ))))
609 }
610 }
611 }
612
613 pub async fn get_build_list(
628 &self,
629 request: &GetBuildListRequest,
630 ) -> Result<BuildList, BuildError> {
631 let endpoint = "/api/5.0/getbuildlist.do";
632
633 let mut query_params = Vec::new();
635 query_params.push(("app_id", request.app_id.as_str()));
636
637 if let Some(sandbox_id) = &request.sandbox_id {
638 query_params.push(("sandbox_id", sandbox_id.as_str()));
639 }
640
641 let response = self
642 .client
643 .get_with_query_params(endpoint, &query_params)
644 .await?;
645
646 let status = response.status().as_u16();
647 match status {
648 200 => {
649 let response_text = response.text().await?;
650 self.parse_build_list(&response_text)
651 }
652 400 => {
653 let error_text = response.text().await.unwrap_or_default();
654 Err(BuildError::InvalidParameter(error_text))
655 }
656 401 => Err(BuildError::Unauthorized),
657 403 => Err(BuildError::PermissionDenied),
658 404 => {
659 if request.sandbox_id.is_some() {
660 Err(BuildError::SandboxNotFound)
661 } else {
662 Err(BuildError::ApplicationNotFound)
663 }
664 }
665 _ => {
666 let error_text = response.text().await.unwrap_or_default();
667 Err(BuildError::Api(VeracodeError::InvalidResponse(format!(
668 "HTTP {status}: {error_text}"
669 ))))
670 }
671 }
672 }
673
674 fn parse_build_info(&self, xml: &str) -> Result<Build, BuildError> {
676 if xml.contains("<error>") {
678 let mut reader = Reader::from_str(xml);
679 reader.config_mut().trim_text(true);
680 let mut buf = Vec::new();
681
682 loop {
683 match reader.read_event_into(&mut buf) {
684 Ok(Event::Start(ref e)) if e.name().as_ref() == b"error" => {
685 if let Ok(Event::Text(text)) = reader.read_event_into(&mut buf) {
686 let error_msg = String::from_utf8_lossy(&text);
687 if error_msg.contains("Could not find a build") {
688 return Err(BuildError::BuildNotFound);
689 }
690 return Err(BuildError::Api(VeracodeError::InvalidResponse(
691 error_msg.to_string(),
692 )));
693 }
694 }
695 Ok(Event::Eof) => break,
696 Err(e) => return Err(BuildError::XmlParsingError(e.to_string())),
697 _ => {}
698 }
699 buf.clear();
700 }
701 }
702
703 let mut reader = Reader::from_str(xml);
704 reader.config_mut().trim_text(true);
705
706 let mut buf = Vec::new();
707 let mut build = Build {
708 build_id: String::new(),
709 app_id: String::new(),
710 version: None,
711 app_name: None,
712 sandbox_id: None,
713 sandbox_name: None,
714 lifecycle_stage: None,
715 launch_date: None,
716 submitter: None,
717 platform: None,
718 analysis_unit: None,
719 policy_name: None,
720 policy_version: None,
721 policy_compliance_status: None,
722 rules_status: None,
723 grace_period_expired: None,
724 scan_overdue: None,
725 policy_updated_date: None,
726 legacy_scan_engine: None,
727 attributes: HashMap::new(),
728 };
729
730 let mut inside_build = false;
731
732 loop {
733 match reader.read_event_into(&mut buf) {
734 Ok(Event::Start(ref e)) => {
735 match e.name().as_ref() {
736 b"build" => {
737 inside_build = true;
738 for attr in e.attributes().flatten() {
739 let key = String::from_utf8_lossy(attr.key.as_ref());
740 let value = String::from_utf8_lossy(&attr.value);
741
742 match key.as_ref() {
743 "build_id" => build.build_id = value.into_owned(),
744 "app_id" => build.app_id = value.into_owned(),
745 "version" => build.version = Some(value.into_owned()),
746 "app_name" => build.app_name = Some(value.into_owned()),
747 "sandbox_id" => build.sandbox_id = Some(value.into_owned()),
748 "sandbox_name" => build.sandbox_name = Some(value.into_owned()),
749 "lifecycle_stage" => {
750 build.lifecycle_stage = Some(value.into_owned())
751 }
752 "submitter" => build.submitter = Some(value.into_owned()),
753 "platform" => build.platform = Some(value.into_owned()),
754 "analysis_unit" => {
755 build.analysis_unit = Some(value.into_owned())
756 }
757 "policy_name" => build.policy_name = Some(value.into_owned()),
758 "policy_version" => {
759 build.policy_version = Some(value.into_owned())
760 }
761 "policy_compliance_status" => {
762 build.policy_compliance_status = Some(value.into_owned())
763 }
764 "rules_status" => build.rules_status = Some(value.into_owned()),
765 "grace_period_expired" => {
766 build.grace_period_expired = value.parse::<bool>().ok();
767 }
768 "scan_overdue" => {
769 build.scan_overdue = value.parse::<bool>().ok();
770 }
771 "legacy_scan_engine" => {
772 build.legacy_scan_engine = value.parse::<bool>().ok();
773 }
774 "launch_date" => {
775 if let Ok(date) =
776 NaiveDate::parse_from_str(&value, "%m/%d/%Y")
777 {
778 build.launch_date = Some(date);
779 }
780 }
781 "policy_updated_date" => {
782 if let Ok(datetime) =
783 chrono::DateTime::parse_from_rfc3339(&value)
784 {
785 build.policy_updated_date =
786 Some(datetime.with_timezone(&Utc));
787 }
788 }
789 _ => {
790 build
791 .attributes
792 .insert(key.into_owned(), value.into_owned());
793 }
794 }
795 }
796 }
797 b"analysis_unit" if inside_build => {
798 for attr in e.attributes().flatten() {
800 let key = String::from_utf8_lossy(attr.key.as_ref());
801 let value = String::from_utf8_lossy(&attr.value);
802
803 match key.as_ref() {
805 "status" => {
806 build
808 .attributes
809 .insert("status".to_string(), value.into_owned());
810 }
811 _ => {
812 build
814 .attributes
815 .insert(format!("analysis_{key}"), value.into_owned());
816 }
817 }
818 }
819 }
820 _ => {}
821 }
822 }
823 Ok(Event::Empty(ref e)) => {
824 if e.name().as_ref() == b"analysis_unit" && inside_build {
826 for attr in e.attributes().flatten() {
827 let key = String::from_utf8_lossy(attr.key.as_ref());
828 let value = String::from_utf8_lossy(&attr.value);
829
830 match key.as_ref() {
831 "status" => {
832 build
833 .attributes
834 .insert("status".to_string(), value.into_owned());
835 }
836 _ => {
837 build
838 .attributes
839 .insert(format!("analysis_{key}"), value.into_owned());
840 }
841 }
842 }
843 }
844 }
845 Ok(Event::End(ref e)) => {
846 if e.name().as_ref() == b"build" {
847 inside_build = false;
848 }
849 }
850 Ok(Event::Eof) => break,
851 Err(e) => return Err(BuildError::XmlParsingError(e.to_string())),
852 _ => {}
853 }
854 buf.clear();
855 }
856
857 if build.build_id.is_empty() {
858 return Err(BuildError::XmlParsingError(
859 "No build information found in response".to_string(),
860 ));
861 }
862
863 Ok(build)
864 }
865
866 fn parse_build_list(&self, xml: &str) -> Result<BuildList, BuildError> {
868 let mut reader = Reader::from_str(xml);
869 reader.config_mut().trim_text(true);
870
871 let mut buf = Vec::new();
872 let mut build_list = BuildList {
873 account_id: None,
874 app_id: String::new(),
875 app_name: None,
876 builds: Vec::new(),
877 };
878
879 loop {
880 match reader.read_event_into(&mut buf) {
881 Ok(Event::Start(ref e)) => match e.name().as_ref() {
882 b"buildlist" => {
883 for attr in e.attributes().flatten() {
884 let key = String::from_utf8_lossy(attr.key.as_ref());
885 let value = String::from_utf8_lossy(&attr.value);
886
887 match key.as_ref() {
888 "account_id" => build_list.account_id = Some(value.into_owned()),
889 "app_id" => build_list.app_id = value.into_owned(),
890 "app_name" => build_list.app_name = Some(value.into_owned()),
891 _ => {}
892 }
893 }
894 }
895 b"build" => {
896 let mut build = Build {
897 build_id: String::new(),
898 app_id: build_list.app_id.clone(),
899 version: None,
900 app_name: build_list.app_name.clone(),
901 sandbox_id: None,
902 sandbox_name: None,
903 lifecycle_stage: None,
904 launch_date: None,
905 submitter: None,
906 platform: None,
907 analysis_unit: None,
908 policy_name: None,
909 policy_version: None,
910 policy_compliance_status: None,
911 rules_status: None,
912 grace_period_expired: None,
913 scan_overdue: None,
914 policy_updated_date: None,
915 legacy_scan_engine: None,
916 attributes: HashMap::new(),
917 };
918
919 for attr in e.attributes().flatten() {
920 let key = String::from_utf8_lossy(attr.key.as_ref());
921 let value = String::from_utf8_lossy(&attr.value);
922
923 match key.as_ref() {
924 "build_id" => build.build_id = value.into_owned(),
925 "version" => build.version = Some(value.into_owned()),
926 "sandbox_id" => build.sandbox_id = Some(value.into_owned()),
927 "sandbox_name" => build.sandbox_name = Some(value.into_owned()),
928 "lifecycle_stage" => {
929 build.lifecycle_stage = Some(value.into_owned())
930 }
931 "submitter" => build.submitter = Some(value.into_owned()),
932 "platform" => build.platform = Some(value.into_owned()),
933 "analysis_unit" => build.analysis_unit = Some(value.into_owned()),
934 "policy_name" => build.policy_name = Some(value.into_owned()),
935 "policy_version" => build.policy_version = Some(value.into_owned()),
936 "policy_compliance_status" => {
937 build.policy_compliance_status = Some(value.into_owned())
938 }
939 "rules_status" => build.rules_status = Some(value.into_owned()),
940 "grace_period_expired" => {
941 build.grace_period_expired = value.parse::<bool>().ok();
942 }
943 "scan_overdue" => {
944 build.scan_overdue = value.parse::<bool>().ok();
945 }
946 "legacy_scan_engine" => {
947 build.legacy_scan_engine = value.parse::<bool>().ok();
948 }
949 "launch_date" => {
950 if let Ok(date) = NaiveDate::parse_from_str(&value, "%m/%d/%Y")
951 {
952 build.launch_date = Some(date);
953 }
954 }
955 "policy_updated_date" => {
956 if let Ok(datetime) =
957 chrono::DateTime::parse_from_rfc3339(&value)
958 {
959 build.policy_updated_date =
960 Some(datetime.with_timezone(&Utc));
961 }
962 }
963 _ => {
964 build
965 .attributes
966 .insert(key.into_owned(), value.into_owned());
967 }
968 }
969 }
970
971 if !build.build_id.is_empty() {
972 build_list.builds.push(build);
973 }
974 }
975 _ => {}
976 },
977 Ok(Event::Eof) => break,
978 Err(e) => return Err(BuildError::XmlParsingError(e.to_string())),
979 _ => {}
980 }
981 buf.clear();
982 }
983
984 Ok(build_list)
985 }
986
987 fn parse_delete_result(&self, xml: &str) -> Result<DeleteBuildResult, BuildError> {
989 let mut reader = Reader::from_str(xml);
990 reader.config_mut().trim_text(true);
991
992 let mut buf = Vec::new();
993 let mut result = String::new();
994
995 loop {
996 match reader.read_event_into(&mut buf) {
997 Ok(Event::Start(ref e)) => {
998 if e.name().as_ref() == b"result" {
999 if let Ok(Event::Text(e)) = reader.read_event_into(&mut buf) {
1001 result = String::from_utf8_lossy(&e).into_owned();
1002 }
1003 }
1004 }
1005 Ok(Event::Eof) => break,
1006 Err(e) => return Err(BuildError::XmlParsingError(e.to_string())),
1007 _ => {}
1008 }
1009 buf.clear();
1010 }
1011
1012 if result.is_empty() {
1013 return Err(BuildError::XmlParsingError(
1014 "No result found in delete response".to_string(),
1015 ));
1016 }
1017
1018 Ok(DeleteBuildResult { result })
1019 }
1020}
1021
1022impl BuildApi {
1024 pub async fn create_simple_build(
1040 &self,
1041 app_id: &str,
1042 version: Option<&str>,
1043 ) -> Result<Build, BuildError> {
1044 let request = CreateBuildRequest {
1045 app_id: app_id.to_string(),
1046 version: version.map(str::to_string),
1047 lifecycle_stage: None,
1048 launch_date: None,
1049 sandbox_id: None,
1050 };
1051
1052 self.create_build(&request).await
1053 }
1054
1055 pub async fn create_sandbox_build(
1072 &self,
1073 app_id: &str,
1074 sandbox_id: &str,
1075 version: Option<&str>,
1076 ) -> Result<Build, BuildError> {
1077 let request = CreateBuildRequest {
1078 app_id: app_id.to_string(),
1079 version: version.map(str::to_string),
1080 lifecycle_stage: None,
1081 launch_date: None,
1082 sandbox_id: Some(sandbox_id.to_string()),
1083 };
1084
1085 self.create_build(&request).await
1086 }
1087
1088 pub async fn delete_app_build(&self, app_id: &str) -> Result<DeleteBuildResult, BuildError> {
1103 let request = DeleteBuildRequest {
1104 app_id: app_id.to_string(),
1105 sandbox_id: None,
1106 };
1107
1108 self.delete_build(&request).await
1109 }
1110
1111 pub async fn delete_sandbox_build(
1127 &self,
1128 app_id: &str,
1129 sandbox_id: &str,
1130 ) -> Result<DeleteBuildResult, BuildError> {
1131 let request = DeleteBuildRequest {
1132 app_id: app_id.to_string(),
1133 sandbox_id: Some(sandbox_id.to_string()),
1134 };
1135
1136 self.delete_build(&request).await
1137 }
1138
1139 pub async fn get_app_build_info(&self, app_id: &str) -> Result<Build, BuildError> {
1154 let request = GetBuildInfoRequest {
1155 app_id: app_id.to_string(),
1156 build_id: None,
1157 sandbox_id: None,
1158 };
1159
1160 self.get_build_info(&request).await
1161 }
1162
1163 pub async fn get_sandbox_build_info(
1179 &self,
1180 app_id: &str,
1181 sandbox_id: &str,
1182 ) -> Result<Build, BuildError> {
1183 let request = GetBuildInfoRequest {
1184 app_id: app_id.to_string(),
1185 build_id: None,
1186 sandbox_id: Some(sandbox_id.to_string()),
1187 };
1188
1189 self.get_build_info(&request).await
1190 }
1191
1192 pub async fn get_app_builds(&self, app_id: &str) -> Result<BuildList, BuildError> {
1207 let request = GetBuildListRequest {
1208 app_id: app_id.to_string(),
1209 sandbox_id: None,
1210 };
1211
1212 self.get_build_list(&request).await
1213 }
1214
1215 pub async fn get_sandbox_builds(
1231 &self,
1232 app_id: &str,
1233 sandbox_id: &str,
1234 ) -> Result<BuildList, BuildError> {
1235 let request = GetBuildListRequest {
1236 app_id: app_id.to_string(),
1237 sandbox_id: Some(sandbox_id.to_string()),
1238 };
1239
1240 self.get_build_list(&request).await
1241 }
1242}
1243
1244#[cfg(test)]
1245mod tests {
1246 use super::*;
1247 use crate::VeracodeConfig;
1248
1249 #[test]
1250 fn test_create_build_request() {
1251 let request = CreateBuildRequest {
1252 app_id: "123".to_string(),
1253 version: Some("1.0.0".to_string()),
1254 lifecycle_stage: Some("In Development (pre-Alpha)".to_string()),
1255 launch_date: Some("12/31/2024".to_string()),
1256 sandbox_id: None,
1257 };
1258
1259 assert_eq!(request.app_id, "123");
1260 assert_eq!(request.version, Some("1.0.0".to_string()));
1261 assert_eq!(
1262 request.lifecycle_stage,
1263 Some("In Development (pre-Alpha)".to_string())
1264 );
1265 }
1266
1267 #[test]
1268 fn test_update_build_request() {
1269 let request = UpdateBuildRequest {
1270 app_id: "123".to_string(),
1271 build_id: Some("456".to_string()),
1272 version: Some("1.1.0".to_string()),
1273 lifecycle_stage: Some("Internal or Alpha Testing".to_string()),
1274 launch_date: None,
1275 sandbox_id: Some("789".to_string()),
1276 };
1277
1278 assert_eq!(request.app_id, "123");
1279 assert_eq!(request.build_id, Some("456".to_string()));
1280 assert_eq!(request.sandbox_id, Some("789".to_string()));
1281 }
1282
1283 #[test]
1284 fn test_lifecycle_stage_validation() {
1285 assert!(is_valid_lifecycle_stage("In Development (pre-Alpha)"));
1287 assert!(is_valid_lifecycle_stage("Internal or Alpha Testing"));
1288 assert!(is_valid_lifecycle_stage("External or Beta Testing"));
1289 assert!(is_valid_lifecycle_stage("Deployed"));
1290 assert!(is_valid_lifecycle_stage("Maintenance"));
1291 assert!(is_valid_lifecycle_stage("Cannot Disclose"));
1292 assert!(is_valid_lifecycle_stage("Not Specified"));
1293
1294 assert!(!is_valid_lifecycle_stage("In Development"));
1296 assert!(!is_valid_lifecycle_stage("Development"));
1297 assert!(!is_valid_lifecycle_stage("QA"));
1298 assert!(!is_valid_lifecycle_stage("Production"));
1299 assert!(!is_valid_lifecycle_stage(""));
1300
1301 assert_eq!(default_lifecycle_stage(), "In Development (pre-Alpha)");
1303 assert!(is_valid_lifecycle_stage(default_lifecycle_stage()));
1304 }
1305
1306 #[test]
1307 fn test_build_error_display() {
1308 let error = BuildError::BuildNotFound;
1309 assert_eq!(error.to_string(), "Build not found");
1310
1311 let error = BuildError::InvalidParameter("Invalid app_id".to_string());
1312 assert_eq!(error.to_string(), "Invalid parameter: Invalid app_id");
1313
1314 let error = BuildError::CreationFailed("Build creation failed".to_string());
1315 assert_eq!(
1316 error.to_string(),
1317 "Build creation failed: Build creation failed"
1318 );
1319 }
1320
1321 #[tokio::test]
1322 async fn test_build_api_method_signatures() {
1323 async fn _test_build_methods() -> Result<(), Box<dyn std::error::Error>> {
1324 let config = VeracodeConfig::new("test", "test");
1325 let client = VeracodeClient::new(config)?;
1326 let api = client.build_api()?;
1327
1328 let create_request = CreateBuildRequest {
1330 app_id: "123".to_string(),
1331 version: None,
1332 lifecycle_stage: None,
1333 launch_date: None,
1334 sandbox_id: None,
1335 };
1336
1337 let _: Result<Build, _> = api.create_build(&create_request).await;
1340 let _: Result<Build, _> = api.create_simple_build("123", None).await;
1341 let _: Result<Build, _> = api.create_sandbox_build("123", "456", None).await;
1342 let _: Result<DeleteBuildResult, _> = api.delete_app_build("123").await;
1343 let _: Result<Build, _> = api.get_app_build_info("123").await;
1344 let _: Result<BuildList, _> = api.get_app_builds("123").await;
1345
1346 Ok(())
1347 }
1348
1349 }
1352
1353 #[test]
1354 fn test_build_status_from_str() {
1355 assert_eq!(
1356 BuildStatus::from_string("Incomplete"),
1357 BuildStatus::Incomplete
1358 );
1359 assert_eq!(
1360 BuildStatus::from_string("Results Ready"),
1361 BuildStatus::ResultsReady
1362 );
1363 assert_eq!(
1364 BuildStatus::from_string("Pre-Scan Failed"),
1365 BuildStatus::PreScanFailed
1366 );
1367 assert_eq!(
1368 BuildStatus::from_string("Unknown Status"),
1369 BuildStatus::Unknown("Unknown Status".to_string())
1370 );
1371 }
1372
1373 #[test]
1374 fn test_build_status_to_str() {
1375 assert_eq!(BuildStatus::Incomplete.to_str(), "Incomplete");
1376 assert_eq!(BuildStatus::ResultsReady.to_str(), "Results Ready");
1377 assert_eq!(BuildStatus::PreScanFailed.to_str(), "Pre-Scan Failed");
1378 assert_eq!(
1379 BuildStatus::Unknown("Custom".to_string()).to_str(),
1380 "Custom"
1381 );
1382 }
1383
1384 #[test]
1385 fn test_build_status_deletion_policy_0() {
1386 assert!(!BuildStatus::Incomplete.is_safe_to_delete(0));
1388 assert!(!BuildStatus::ResultsReady.is_safe_to_delete(0));
1389 assert!(!BuildStatus::Failed.is_safe_to_delete(0));
1390 }
1391
1392 #[test]
1393 fn test_build_status_deletion_policy_1() {
1394 assert!(BuildStatus::Incomplete.is_safe_to_delete(1));
1396 assert!(BuildStatus::NotSubmitted.is_safe_to_delete(1));
1397 assert!(BuildStatus::PreScanFailed.is_safe_to_delete(1));
1398 assert!(BuildStatus::Failed.is_safe_to_delete(1));
1399 assert!(BuildStatus::Cancelled.is_safe_to_delete(1));
1400
1401 assert!(!BuildStatus::ResultsReady.is_safe_to_delete(1));
1403 assert!(!BuildStatus::ScanInProcess.is_safe_to_delete(1));
1404 assert!(!BuildStatus::PreScanSuccess.is_safe_to_delete(1));
1405 }
1406
1407 #[test]
1408 fn test_build_status_deletion_policy_2() {
1409 assert!(BuildStatus::Incomplete.is_safe_to_delete(2));
1411 assert!(BuildStatus::Failed.is_safe_to_delete(2));
1412 assert!(BuildStatus::ScanInProcess.is_safe_to_delete(2));
1413 assert!(BuildStatus::PreScanSuccess.is_safe_to_delete(2));
1414
1415 assert!(!BuildStatus::ResultsReady.is_safe_to_delete(2));
1417 }
1418
1419 #[test]
1420 fn test_build_status_deletion_policy_invalid() {
1421 assert!(!BuildStatus::Incomplete.is_safe_to_delete(3));
1423 assert!(!BuildStatus::Failed.is_safe_to_delete(255));
1424 }
1425}