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