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_from_attributes<'a>(
868 &self,
869 attributes: impl Iterator<
870 Item = Result<
871 quick_xml::events::attributes::Attribute<'a>,
872 quick_xml::events::attributes::AttrError,
873 >,
874 >,
875 app_id: &str,
876 app_name: &Option<String>,
877 ) -> Build {
878 let mut build = Build {
879 build_id: String::new(),
880 app_id: app_id.to_string(),
881 version: None,
882 app_name: app_name.clone(),
883 sandbox_id: None,
884 sandbox_name: None,
885 lifecycle_stage: None,
886 launch_date: None,
887 submitter: None,
888 platform: None,
889 analysis_unit: None,
890 policy_name: None,
891 policy_version: None,
892 policy_compliance_status: None,
893 rules_status: None,
894 grace_period_expired: None,
895 scan_overdue: None,
896 policy_updated_date: None,
897 legacy_scan_engine: None,
898 attributes: HashMap::new(),
899 };
900
901 for attr in attributes.flatten() {
902 let key = String::from_utf8_lossy(attr.key.as_ref());
903 let value = String::from_utf8_lossy(&attr.value);
904
905 match key.as_ref() {
906 "build_id" => build.build_id = value.into_owned(),
907 "version" => build.version = Some(value.into_owned()),
908 "sandbox_id" => build.sandbox_id = Some(value.into_owned()),
909 "sandbox_name" => build.sandbox_name = Some(value.into_owned()),
910 "lifecycle_stage" => build.lifecycle_stage = Some(value.into_owned()),
911 "submitter" => build.submitter = Some(value.into_owned()),
912 "platform" => build.platform = Some(value.into_owned()),
913 "analysis_unit" => build.analysis_unit = Some(value.into_owned()),
914 "policy_name" => build.policy_name = Some(value.into_owned()),
915 "policy_version" => build.policy_version = Some(value.into_owned()),
916 "policy_compliance_status" => {
917 build.policy_compliance_status = Some(value.into_owned())
918 }
919 "rules_status" => build.rules_status = Some(value.into_owned()),
920 "grace_period_expired" => {
921 build.grace_period_expired = value.parse::<bool>().ok();
922 }
923 "scan_overdue" => {
924 build.scan_overdue = value.parse::<bool>().ok();
925 }
926 "legacy_scan_engine" => {
927 build.legacy_scan_engine = value.parse::<bool>().ok();
928 }
929 "launch_date" => {
930 if let Ok(date) = NaiveDate::parse_from_str(&value, "%m/%d/%Y") {
931 build.launch_date = Some(date);
932 }
933 }
934 "policy_updated_date" => {
935 if let Ok(datetime) = chrono::DateTime::parse_from_rfc3339(&value) {
936 build.policy_updated_date = Some(datetime.with_timezone(&Utc));
937 }
938 }
939 _ => {
940 build
941 .attributes
942 .insert(key.into_owned(), value.into_owned());
943 }
944 }
945 }
946
947 build
948 }
949
950 fn parse_build_list(&self, xml: &str) -> Result<BuildList, BuildError> {
952 let mut reader = Reader::from_str(xml);
953 reader.config_mut().trim_text(true);
954
955 let mut buf = Vec::new();
956 let mut build_list = BuildList {
957 account_id: None,
958 app_id: String::new(),
959 app_name: None,
960 builds: Vec::new(),
961 };
962
963 loop {
964 match reader.read_event_into(&mut buf) {
965 Ok(Event::Start(ref e)) => match e.name().as_ref() {
966 b"buildlist" => {
967 for attr in e.attributes().flatten() {
968 let key = String::from_utf8_lossy(attr.key.as_ref());
969 let value = String::from_utf8_lossy(&attr.value);
970
971 match key.as_ref() {
972 "account_id" => build_list.account_id = Some(value.into_owned()),
973 "app_id" => build_list.app_id = value.into_owned(),
974 "app_name" => build_list.app_name = Some(value.into_owned()),
975 _ => {}
976 }
977 }
978 }
979 b"build" => {
980 let build = self.parse_build_from_attributes(
981 e.attributes(),
982 &build_list.app_id,
983 &build_list.app_name,
984 );
985
986 if !build.build_id.is_empty() {
987 build_list.builds.push(build);
988 }
989 }
990 _ => {}
991 },
992 Ok(Event::Empty(ref e)) => {
993 if e.name().as_ref() == b"build" {
995 let build = self.parse_build_from_attributes(
996 e.attributes(),
997 &build_list.app_id,
998 &build_list.app_name,
999 );
1000
1001 if !build.build_id.is_empty() {
1002 build_list.builds.push(build);
1003 }
1004 }
1005 }
1006 Ok(Event::Eof) => break,
1007 Err(e) => return Err(BuildError::XmlParsingError(e.to_string())),
1008 _ => {}
1009 }
1010 buf.clear();
1011 }
1012
1013 Ok(build_list)
1014 }
1015
1016 fn parse_delete_result(&self, xml: &str) -> Result<DeleteBuildResult, BuildError> {
1018 let mut reader = Reader::from_str(xml);
1019 reader.config_mut().trim_text(true);
1020
1021 let mut buf = Vec::new();
1022 let mut result = String::new();
1023
1024 loop {
1025 match reader.read_event_into(&mut buf) {
1026 Ok(Event::Start(ref e)) => {
1027 if e.name().as_ref() == b"result" {
1028 if let Ok(Event::Text(e)) = reader.read_event_into(&mut buf) {
1030 result = String::from_utf8_lossy(&e).into_owned();
1031 }
1032 }
1033 }
1034 Ok(Event::Eof) => break,
1035 Err(e) => return Err(BuildError::XmlParsingError(e.to_string())),
1036 _ => {}
1037 }
1038 buf.clear();
1039 }
1040
1041 if result.is_empty() {
1042 return Err(BuildError::XmlParsingError(
1043 "No result found in delete response".to_string(),
1044 ));
1045 }
1046
1047 Ok(DeleteBuildResult { result })
1048 }
1049}
1050
1051impl BuildApi {
1053 pub async fn create_simple_build(
1069 &self,
1070 app_id: &str,
1071 version: Option<&str>,
1072 ) -> Result<Build, BuildError> {
1073 let request = CreateBuildRequest {
1074 app_id: app_id.to_string(),
1075 version: version.map(str::to_string),
1076 lifecycle_stage: None,
1077 launch_date: None,
1078 sandbox_id: None,
1079 };
1080
1081 self.create_build(&request).await
1082 }
1083
1084 pub async fn create_sandbox_build(
1101 &self,
1102 app_id: &str,
1103 sandbox_id: &str,
1104 version: Option<&str>,
1105 ) -> Result<Build, BuildError> {
1106 let request = CreateBuildRequest {
1107 app_id: app_id.to_string(),
1108 version: version.map(str::to_string),
1109 lifecycle_stage: None,
1110 launch_date: None,
1111 sandbox_id: Some(sandbox_id.to_string()),
1112 };
1113
1114 self.create_build(&request).await
1115 }
1116
1117 pub async fn delete_app_build(&self, app_id: &str) -> Result<DeleteBuildResult, BuildError> {
1132 let request = DeleteBuildRequest {
1133 app_id: app_id.to_string(),
1134 sandbox_id: None,
1135 };
1136
1137 self.delete_build(&request).await
1138 }
1139
1140 pub async fn delete_sandbox_build(
1156 &self,
1157 app_id: &str,
1158 sandbox_id: &str,
1159 ) -> Result<DeleteBuildResult, BuildError> {
1160 let request = DeleteBuildRequest {
1161 app_id: app_id.to_string(),
1162 sandbox_id: Some(sandbox_id.to_string()),
1163 };
1164
1165 self.delete_build(&request).await
1166 }
1167
1168 pub async fn get_app_build_info(&self, app_id: &str) -> Result<Build, BuildError> {
1183 let request = GetBuildInfoRequest {
1184 app_id: app_id.to_string(),
1185 build_id: None,
1186 sandbox_id: None,
1187 };
1188
1189 self.get_build_info(&request).await
1190 }
1191
1192 pub async fn get_sandbox_build_info(
1208 &self,
1209 app_id: &str,
1210 sandbox_id: &str,
1211 ) -> Result<Build, BuildError> {
1212 let request = GetBuildInfoRequest {
1213 app_id: app_id.to_string(),
1214 build_id: None,
1215 sandbox_id: Some(sandbox_id.to_string()),
1216 };
1217
1218 self.get_build_info(&request).await
1219 }
1220
1221 pub async fn get_app_builds(&self, app_id: &str) -> Result<BuildList, BuildError> {
1236 let request = GetBuildListRequest {
1237 app_id: app_id.to_string(),
1238 sandbox_id: None,
1239 };
1240
1241 self.get_build_list(&request).await
1242 }
1243
1244 pub async fn get_sandbox_builds(
1260 &self,
1261 app_id: &str,
1262 sandbox_id: &str,
1263 ) -> Result<BuildList, BuildError> {
1264 let request = GetBuildListRequest {
1265 app_id: app_id.to_string(),
1266 sandbox_id: Some(sandbox_id.to_string()),
1267 };
1268
1269 self.get_build_list(&request).await
1270 }
1271}
1272
1273#[cfg(test)]
1274mod tests {
1275 use super::*;
1276 use crate::VeracodeConfig;
1277
1278 #[test]
1279 fn test_create_build_request() {
1280 let request = CreateBuildRequest {
1281 app_id: "123".to_string(),
1282 version: Some("1.0.0".to_string()),
1283 lifecycle_stage: Some("In Development (pre-Alpha)".to_string()),
1284 launch_date: Some("12/31/2024".to_string()),
1285 sandbox_id: None,
1286 };
1287
1288 assert_eq!(request.app_id, "123");
1289 assert_eq!(request.version, Some("1.0.0".to_string()));
1290 assert_eq!(
1291 request.lifecycle_stage,
1292 Some("In Development (pre-Alpha)".to_string())
1293 );
1294 }
1295
1296 #[test]
1297 fn test_update_build_request() {
1298 let request = UpdateBuildRequest {
1299 app_id: "123".to_string(),
1300 build_id: Some("456".to_string()),
1301 version: Some("1.1.0".to_string()),
1302 lifecycle_stage: Some("Internal or Alpha Testing".to_string()),
1303 launch_date: None,
1304 sandbox_id: Some("789".to_string()),
1305 };
1306
1307 assert_eq!(request.app_id, "123");
1308 assert_eq!(request.build_id, Some("456".to_string()));
1309 assert_eq!(request.sandbox_id, Some("789".to_string()));
1310 }
1311
1312 #[test]
1313 fn test_lifecycle_stage_validation() {
1314 assert!(is_valid_lifecycle_stage("In Development (pre-Alpha)"));
1316 assert!(is_valid_lifecycle_stage("Internal or Alpha Testing"));
1317 assert!(is_valid_lifecycle_stage("External or Beta Testing"));
1318 assert!(is_valid_lifecycle_stage("Deployed"));
1319 assert!(is_valid_lifecycle_stage("Maintenance"));
1320 assert!(is_valid_lifecycle_stage("Cannot Disclose"));
1321 assert!(is_valid_lifecycle_stage("Not Specified"));
1322
1323 assert!(!is_valid_lifecycle_stage("In Development"));
1325 assert!(!is_valid_lifecycle_stage("Development"));
1326 assert!(!is_valid_lifecycle_stage("QA"));
1327 assert!(!is_valid_lifecycle_stage("Production"));
1328 assert!(!is_valid_lifecycle_stage(""));
1329
1330 assert_eq!(default_lifecycle_stage(), "In Development (pre-Alpha)");
1332 assert!(is_valid_lifecycle_stage(default_lifecycle_stage()));
1333 }
1334
1335 #[test]
1336 fn test_build_error_display() {
1337 let error = BuildError::BuildNotFound;
1338 assert_eq!(error.to_string(), "Build not found");
1339
1340 let error = BuildError::InvalidParameter("Invalid app_id".to_string());
1341 assert_eq!(error.to_string(), "Invalid parameter: Invalid app_id");
1342
1343 let error = BuildError::CreationFailed("Build creation failed".to_string());
1344 assert_eq!(
1345 error.to_string(),
1346 "Build creation failed: Build creation failed"
1347 );
1348 }
1349
1350 #[tokio::test]
1351 async fn test_build_api_method_signatures() {
1352 async fn _test_build_methods() -> Result<(), Box<dyn std::error::Error>> {
1353 let config = VeracodeConfig::new("test", "test");
1354 let client = VeracodeClient::new(config)?;
1355 let api = client.build_api()?;
1356
1357 let create_request = CreateBuildRequest {
1359 app_id: "123".to_string(),
1360 version: None,
1361 lifecycle_stage: None,
1362 launch_date: None,
1363 sandbox_id: None,
1364 };
1365
1366 let _: Result<Build, _> = api.create_build(&create_request).await;
1369 let _: Result<Build, _> = api.create_simple_build("123", None).await;
1370 let _: Result<Build, _> = api.create_sandbox_build("123", "456", None).await;
1371 let _: Result<DeleteBuildResult, _> = api.delete_app_build("123").await;
1372 let _: Result<Build, _> = api.get_app_build_info("123").await;
1373 let _: Result<BuildList, _> = api.get_app_builds("123").await;
1374
1375 Ok(())
1376 }
1377
1378 }
1381
1382 #[test]
1383 fn test_build_status_from_str() {
1384 assert_eq!(
1385 BuildStatus::from_string("Incomplete"),
1386 BuildStatus::Incomplete
1387 );
1388 assert_eq!(
1389 BuildStatus::from_string("Results Ready"),
1390 BuildStatus::ResultsReady
1391 );
1392 assert_eq!(
1393 BuildStatus::from_string("Pre-Scan Failed"),
1394 BuildStatus::PreScanFailed
1395 );
1396 assert_eq!(
1397 BuildStatus::from_string("Unknown Status"),
1398 BuildStatus::Unknown("Unknown Status".to_string())
1399 );
1400 }
1401
1402 #[test]
1403 fn test_build_status_to_str() {
1404 assert_eq!(BuildStatus::Incomplete.to_str(), "Incomplete");
1405 assert_eq!(BuildStatus::ResultsReady.to_str(), "Results Ready");
1406 assert_eq!(BuildStatus::PreScanFailed.to_str(), "Pre-Scan Failed");
1407 assert_eq!(
1408 BuildStatus::Unknown("Custom".to_string()).to_str(),
1409 "Custom"
1410 );
1411 }
1412
1413 #[test]
1414 fn test_build_status_deletion_policy_0() {
1415 assert!(!BuildStatus::Incomplete.is_safe_to_delete(0));
1417 assert!(!BuildStatus::ResultsReady.is_safe_to_delete(0));
1418 assert!(!BuildStatus::Failed.is_safe_to_delete(0));
1419 }
1420
1421 #[test]
1422 fn test_build_status_deletion_policy_1() {
1423 assert!(BuildStatus::Incomplete.is_safe_to_delete(1));
1425 assert!(BuildStatus::NotSubmitted.is_safe_to_delete(1));
1426 assert!(BuildStatus::PreScanFailed.is_safe_to_delete(1));
1427 assert!(BuildStatus::Failed.is_safe_to_delete(1));
1428 assert!(BuildStatus::Cancelled.is_safe_to_delete(1));
1429
1430 assert!(!BuildStatus::ResultsReady.is_safe_to_delete(1));
1432 assert!(!BuildStatus::ScanInProcess.is_safe_to_delete(1));
1433 assert!(!BuildStatus::PreScanSuccess.is_safe_to_delete(1));
1434 }
1435
1436 #[test]
1437 fn test_build_status_deletion_policy_2() {
1438 assert!(BuildStatus::Incomplete.is_safe_to_delete(2));
1440 assert!(BuildStatus::Failed.is_safe_to_delete(2));
1441 assert!(BuildStatus::ScanInProcess.is_safe_to_delete(2));
1442 assert!(BuildStatus::PreScanSuccess.is_safe_to_delete(2));
1443
1444 assert!(!BuildStatus::ResultsReady.is_safe_to_delete(2));
1446 }
1447
1448 #[test]
1449 fn test_build_status_deletion_policy_invalid() {
1450 assert!(!BuildStatus::Incomplete.is_safe_to_delete(3));
1452 assert!(!BuildStatus::Failed.is_safe_to_delete(255));
1453 }
1454}