1use chrono::{DateTime, NaiveDate, Utc};
8use quick_xml::Reader;
9use quick_xml::events::Event;
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12
13use crate::{VeracodeClient, VeracodeError};
14
15pub const LIFECYCLE_STAGES: &[&str] = &[
17 "In Development (pre-Alpha)",
18 "Internal or Alpha Testing",
19 "External or Beta Testing",
20 "Deployed",
21 "Maintenance",
22 "Cannot Disclose",
23 "Not Specified",
24];
25
26pub fn is_valid_lifecycle_stage(stage: &str) -> bool {
28 LIFECYCLE_STAGES.contains(&stage)
29}
30
31pub fn default_lifecycle_stage() -> &'static str {
33 "In Development (pre-Alpha)"
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
39pub enum BuildStatus {
40 Incomplete,
41 NotSubmitted,
42 SubmittedToEngine,
43 ScanInProcess,
44 PreScanSubmitted,
45 PreScanSuccess,
46 PreScanFailed,
47 PreScanCancelled,
48 PrescanFailed,
49 PrescanCancelled,
50 ScanCancelled,
51 ResultsReady,
52 Failed,
53 Cancelled,
54 Unknown(String), }
56
57impl BuildStatus {
58 pub fn from_string(status: &str) -> Self {
60 match status {
61 "Incomplete" => BuildStatus::Incomplete,
62 "Not Submitted" => BuildStatus::NotSubmitted,
63 "Submitted to Engine" => BuildStatus::SubmittedToEngine,
64 "Scan in Process" => BuildStatus::ScanInProcess,
65 "Pre-Scan Submitted" => BuildStatus::PreScanSubmitted,
66 "Pre-Scan Success" => BuildStatus::PreScanSuccess,
67 "Pre-Scan Failed" => BuildStatus::PreScanFailed,
68 "Pre-Scan Cancelled" => BuildStatus::PreScanCancelled,
69 "Prescan Failed" => BuildStatus::PrescanFailed,
70 "Prescan Cancelled" => BuildStatus::PrescanCancelled,
71 "Scan Cancelled" => BuildStatus::ScanCancelled,
72 "Results Ready" => BuildStatus::ResultsReady,
73 "Failed" => BuildStatus::Failed,
74 "Cancelled" => BuildStatus::Cancelled,
75 _ => BuildStatus::Unknown(status.to_string()),
76 }
77 }
78
79 pub fn to_str(&self) -> &str {
81 match self {
82 BuildStatus::Incomplete => "Incomplete",
83 BuildStatus::NotSubmitted => "Not Submitted",
84 BuildStatus::SubmittedToEngine => "Submitted to Engine",
85 BuildStatus::ScanInProcess => "Scan in Process",
86 BuildStatus::PreScanSubmitted => "Pre-Scan Submitted",
87 BuildStatus::PreScanSuccess => "Pre-Scan Success",
88 BuildStatus::PreScanFailed => "Pre-Scan Failed",
89 BuildStatus::PreScanCancelled => "Pre-Scan Cancelled",
90 BuildStatus::PrescanFailed => "Prescan Failed",
91 BuildStatus::PrescanCancelled => "Prescan Cancelled",
92 BuildStatus::ScanCancelled => "Scan Cancelled",
93 BuildStatus::ResultsReady => "Results Ready",
94 BuildStatus::Failed => "Failed",
95 BuildStatus::Cancelled => "Cancelled",
96 BuildStatus::Unknown(s) => s,
97 }
98 }
99
100 pub fn is_safe_to_delete(&self, deletion_policy: u8) -> bool {
107 match deletion_policy {
108 0 => false, 1 => {
110 matches!(
112 self,
113 BuildStatus::Incomplete
114 | BuildStatus::NotSubmitted
115 | BuildStatus::PreScanFailed
116 | BuildStatus::PreScanCancelled
117 | BuildStatus::PrescanFailed
118 | BuildStatus::PrescanCancelled
119 | BuildStatus::ScanCancelled
120 | BuildStatus::Failed
121 | BuildStatus::Cancelled
122 )
123 }
124 2 => {
125 !matches!(self, BuildStatus::ResultsReady)
127 }
128 _ => false, }
130 }
131}
132
133impl std::fmt::Display for BuildStatus {
134 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135 write!(f, "{}", self.to_str())
136 }
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct Build {
142 pub build_id: String,
144 pub app_id: String,
146 pub version: Option<String>,
148 pub app_name: Option<String>,
150 pub sandbox_id: Option<String>,
152 pub sandbox_name: Option<String>,
154 pub lifecycle_stage: Option<String>,
156 pub launch_date: Option<NaiveDate>,
158 pub submitter: Option<String>,
160 pub platform: Option<String>,
162 pub analysis_unit: Option<String>,
164 pub policy_name: Option<String>,
166 pub policy_version: Option<String>,
168 pub policy_compliance_status: Option<String>,
170 pub rules_status: Option<String>,
172 pub grace_period_expired: Option<bool>,
174 pub scan_overdue: Option<bool>,
176 pub policy_updated_date: Option<DateTime<Utc>>,
178 pub legacy_scan_engine: Option<bool>,
180 pub attributes: HashMap<String, String>,
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct BuildList {
187 pub account_id: Option<String>,
189 pub app_id: String,
191 pub app_name: Option<String>,
193 pub builds: Vec<Build>,
195}
196
197#[derive(Debug, Clone)]
199pub struct CreateBuildRequest {
200 pub app_id: String,
202 pub version: Option<String>,
204 pub lifecycle_stage: Option<String>,
206 pub launch_date: Option<String>,
208 pub sandbox_id: Option<String>,
210}
211
212#[derive(Debug, Clone)]
214pub struct UpdateBuildRequest {
215 pub app_id: String,
217 pub build_id: Option<String>,
219 pub version: Option<String>,
221 pub lifecycle_stage: Option<String>,
223 pub launch_date: Option<String>,
225 pub sandbox_id: Option<String>,
227}
228
229#[derive(Debug, Clone)]
231pub struct DeleteBuildRequest {
232 pub app_id: String,
234 pub sandbox_id: Option<String>,
236}
237
238#[derive(Debug, Clone)]
240pub struct GetBuildInfoRequest {
241 pub app_id: String,
243 pub build_id: Option<String>,
245 pub sandbox_id: Option<String>,
247}
248
249#[derive(Debug, Clone)]
251pub struct GetBuildListRequest {
252 pub app_id: String,
254 pub sandbox_id: Option<String>,
256}
257
258#[derive(Debug, Clone, Serialize, Deserialize)]
260pub struct DeleteBuildResult {
261 pub result: String,
263}
264
265#[derive(Debug)]
267pub enum BuildError {
268 Api(VeracodeError),
270 BuildNotFound,
272 ApplicationNotFound,
274 SandboxNotFound,
276 InvalidParameter(String),
278 CreationFailed(String),
280 UpdateFailed(String),
282 DeletionFailed(String),
284 XmlParsingError(String),
286 Unauthorized,
288 PermissionDenied,
290 BuildInProgress,
292}
293
294impl std::fmt::Display for BuildError {
295 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
296 match self {
297 BuildError::Api(err) => write!(f, "API error: {err}"),
298 BuildError::BuildNotFound => write!(f, "Build not found"),
299 BuildError::ApplicationNotFound => write!(f, "Application not found"),
300 BuildError::SandboxNotFound => write!(f, "Sandbox not found"),
301 BuildError::InvalidParameter(msg) => write!(f, "Invalid parameter: {msg}"),
302 BuildError::CreationFailed(msg) => write!(f, "Build creation failed: {msg}"),
303 BuildError::UpdateFailed(msg) => write!(f, "Build update failed: {msg}"),
304 BuildError::DeletionFailed(msg) => write!(f, "Build deletion failed: {msg}"),
305 BuildError::XmlParsingError(msg) => write!(f, "XML parsing error: {msg}"),
306 BuildError::Unauthorized => write!(f, "Unauthorized access"),
307 BuildError::PermissionDenied => write!(f, "Permission denied"),
308 BuildError::BuildInProgress => write!(f, "Build in progress, cannot modify"),
309 }
310 }
311}
312
313impl std::error::Error for BuildError {}
314
315impl From<VeracodeError> for BuildError {
316 fn from(err: VeracodeError) -> Self {
317 BuildError::Api(err)
318 }
319}
320
321impl From<std::io::Error> for BuildError {
322 fn from(err: std::io::Error) -> Self {
323 BuildError::Api(VeracodeError::InvalidResponse(err.to_string()))
324 }
325}
326
327impl From<reqwest::Error> for BuildError {
328 fn from(err: reqwest::Error) -> Self {
329 BuildError::Api(VeracodeError::Http(err))
330 }
331}
332
333pub struct BuildApi {
335 client: VeracodeClient,
336}
337
338impl BuildApi {
339 pub fn new(client: VeracodeClient) -> Self {
341 Self { client }
342 }
343
344 pub async fn create_build(&self, request: CreateBuildRequest) -> Result<Build, BuildError> {
354 let endpoint = "/api/5.0/createbuild.do";
355
356 let mut query_params = Vec::new();
358 query_params.push(("app_id", request.app_id.as_str()));
359
360 if let Some(version) = &request.version {
361 query_params.push(("version", version.as_str()));
362 }
363
364 if let Some(lifecycle_stage) = &request.lifecycle_stage {
365 query_params.push(("lifecycle_stage", lifecycle_stage.as_str()));
366 }
367
368 if let Some(launch_date) = &request.launch_date {
369 query_params.push(("launch_date", launch_date.as_str()));
370 }
371
372 if let Some(sandbox_id) = &request.sandbox_id {
373 query_params.push(("sandbox_id", sandbox_id.as_str()));
374 }
375
376 let response = self
377 .client
378 .post_with_query_params(endpoint, &query_params)
379 .await?;
380
381 let status = response.status().as_u16();
382 match status {
383 200 => {
384 let response_text = response.text().await?;
385 self.parse_build_info(&response_text)
386 }
387 400 => {
388 let error_text = response.text().await.unwrap_or_default();
389 Err(BuildError::InvalidParameter(error_text))
390 }
391 401 => Err(BuildError::Unauthorized),
392 403 => Err(BuildError::PermissionDenied),
393 404 => Err(BuildError::ApplicationNotFound),
394 _ => {
395 let error_text = response.text().await.unwrap_or_default();
396 Err(BuildError::CreationFailed(format!(
397 "HTTP {status}: {error_text}"
398 )))
399 }
400 }
401 }
402
403 pub async fn update_build(&self, request: UpdateBuildRequest) -> Result<Build, BuildError> {
413 let endpoint = "/api/5.0/updatebuild.do";
414
415 let mut query_params = Vec::new();
417 query_params.push(("app_id", request.app_id.as_str()));
418
419 if let Some(build_id) = &request.build_id {
420 query_params.push(("build_id", build_id.as_str()));
421 }
422
423 if let Some(version) = &request.version {
424 query_params.push(("version", version.as_str()));
425 }
426
427 if let Some(lifecycle_stage) = &request.lifecycle_stage {
428 query_params.push(("lifecycle_stage", lifecycle_stage.as_str()));
429 }
430
431 if let Some(launch_date) = &request.launch_date {
432 query_params.push(("launch_date", launch_date.as_str()));
433 }
434
435 if let Some(sandbox_id) = &request.sandbox_id {
436 query_params.push(("sandbox_id", sandbox_id.as_str()));
437 }
438
439 let response = self
440 .client
441 .post_with_query_params(endpoint, &query_params)
442 .await?;
443
444 let status = response.status().as_u16();
445 match status {
446 200 => {
447 let response_text = response.text().await?;
448 self.parse_build_info(&response_text)
449 }
450 400 => {
451 let error_text = response.text().await.unwrap_or_default();
452 Err(BuildError::InvalidParameter(error_text))
453 }
454 401 => Err(BuildError::Unauthorized),
455 403 => Err(BuildError::PermissionDenied),
456 404 => {
457 if request.sandbox_id.is_some() {
458 Err(BuildError::SandboxNotFound)
459 } else {
460 Err(BuildError::BuildNotFound)
461 }
462 }
463 _ => {
464 let error_text = response.text().await.unwrap_or_default();
465 Err(BuildError::UpdateFailed(format!(
466 "HTTP {status}: {error_text}"
467 )))
468 }
469 }
470 }
471
472 pub async fn delete_build(
482 &self,
483 request: DeleteBuildRequest,
484 ) -> Result<DeleteBuildResult, BuildError> {
485 let endpoint = "/api/5.0/deletebuild.do";
486
487 let mut query_params = Vec::new();
489 query_params.push(("app_id", request.app_id.as_str()));
490
491 if let Some(sandbox_id) = &request.sandbox_id {
492 query_params.push(("sandbox_id", sandbox_id.as_str()));
493 }
494
495 let response = self
496 .client
497 .post_with_query_params(endpoint, &query_params)
498 .await?;
499
500 let status = response.status().as_u16();
501 match status {
502 200 => {
503 let response_text = response.text().await?;
504 self.parse_delete_result(&response_text)
505 }
506 400 => {
507 let error_text = response.text().await.unwrap_or_default();
508 Err(BuildError::InvalidParameter(error_text))
509 }
510 401 => Err(BuildError::Unauthorized),
511 403 => Err(BuildError::PermissionDenied),
512 404 => {
513 if request.sandbox_id.is_some() {
514 Err(BuildError::SandboxNotFound)
515 } else {
516 Err(BuildError::BuildNotFound)
517 }
518 }
519 _ => {
520 let error_text = response.text().await.unwrap_or_default();
521 Err(BuildError::DeletionFailed(format!(
522 "HTTP {status}: {error_text}"
523 )))
524 }
525 }
526 }
527
528 pub async fn get_build_info(&self, request: GetBuildInfoRequest) -> Result<Build, BuildError> {
538 let endpoint = "/api/5.0/getbuildinfo.do";
539
540 let mut query_params = Vec::new();
542 query_params.push(("app_id", request.app_id.as_str()));
543
544 if let Some(build_id) = &request.build_id {
545 query_params.push(("build_id", build_id.as_str()));
546 }
547
548 if let Some(sandbox_id) = &request.sandbox_id {
549 query_params.push(("sandbox_id", sandbox_id.as_str()));
550 }
551
552 let response = self
553 .client
554 .get_with_query_params(endpoint, &query_params)
555 .await?;
556
557 let status = response.status().as_u16();
558 match status {
559 200 => {
560 let response_text = response.text().await?;
561 self.parse_build_info(&response_text)
562 }
563 400 => {
564 let error_text = response.text().await.unwrap_or_default();
565 Err(BuildError::InvalidParameter(error_text))
566 }
567 401 => Err(BuildError::Unauthorized),
568 403 => Err(BuildError::PermissionDenied),
569 404 => {
570 if request.sandbox_id.is_some() {
571 Err(BuildError::SandboxNotFound)
572 } else {
573 Err(BuildError::BuildNotFound)
574 }
575 }
576 _ => {
577 let error_text = response.text().await.unwrap_or_default();
578 Err(BuildError::Api(VeracodeError::InvalidResponse(format!(
579 "HTTP {status}: {error_text}"
580 ))))
581 }
582 }
583 }
584
585 pub async fn get_build_list(
595 &self,
596 request: GetBuildListRequest,
597 ) -> Result<BuildList, BuildError> {
598 let endpoint = "/api/5.0/getbuildlist.do";
599
600 let mut query_params = Vec::new();
602 query_params.push(("app_id", request.app_id.as_str()));
603
604 if let Some(sandbox_id) = &request.sandbox_id {
605 query_params.push(("sandbox_id", sandbox_id.as_str()));
606 }
607
608 let response = self
609 .client
610 .get_with_query_params(endpoint, &query_params)
611 .await?;
612
613 let status = response.status().as_u16();
614 match status {
615 200 => {
616 let response_text = response.text().await?;
617 self.parse_build_list(&response_text)
618 }
619 400 => {
620 let error_text = response.text().await.unwrap_or_default();
621 Err(BuildError::InvalidParameter(error_text))
622 }
623 401 => Err(BuildError::Unauthorized),
624 403 => Err(BuildError::PermissionDenied),
625 404 => {
626 if request.sandbox_id.is_some() {
627 Err(BuildError::SandboxNotFound)
628 } else {
629 Err(BuildError::ApplicationNotFound)
630 }
631 }
632 _ => {
633 let error_text = response.text().await.unwrap_or_default();
634 Err(BuildError::Api(VeracodeError::InvalidResponse(format!(
635 "HTTP {status}: {error_text}"
636 ))))
637 }
638 }
639 }
640
641 fn parse_build_info(&self, xml: &str) -> Result<Build, BuildError> {
643 if xml.contains("<error>") {
645 let mut reader = Reader::from_str(xml);
646 reader.config_mut().trim_text(true);
647 let mut buf = Vec::new();
648
649 loop {
650 match reader.read_event_into(&mut buf) {
651 Ok(Event::Start(ref e)) if e.name().as_ref() == b"error" => {
652 if let Ok(Event::Text(text)) = reader.read_event_into(&mut buf) {
653 let error_msg = String::from_utf8_lossy(&text);
654 if error_msg.contains("Could not find a build") {
655 return Err(BuildError::BuildNotFound);
656 } else {
657 return Err(BuildError::Api(VeracodeError::InvalidResponse(
658 error_msg.to_string(),
659 )));
660 }
661 }
662 }
663 Ok(Event::Eof) => break,
664 Err(e) => return Err(BuildError::XmlParsingError(e.to_string())),
665 _ => {}
666 }
667 buf.clear();
668 }
669 }
670
671 let mut reader = Reader::from_str(xml);
672 reader.config_mut().trim_text(true);
673
674 let mut buf = Vec::new();
675 let mut build = Build {
676 build_id: String::new(),
677 app_id: String::new(),
678 version: None,
679 app_name: None,
680 sandbox_id: None,
681 sandbox_name: None,
682 lifecycle_stage: None,
683 launch_date: None,
684 submitter: None,
685 platform: None,
686 analysis_unit: None,
687 policy_name: None,
688 policy_version: None,
689 policy_compliance_status: None,
690 rules_status: None,
691 grace_period_expired: None,
692 scan_overdue: None,
693 policy_updated_date: None,
694 legacy_scan_engine: None,
695 attributes: HashMap::new(),
696 };
697
698 let mut inside_build = false;
699
700 loop {
701 match reader.read_event_into(&mut buf) {
702 Ok(Event::Start(ref e)) => {
703 match e.name().as_ref() {
704 b"build" => {
705 inside_build = true;
706 for attr in e.attributes().flatten() {
707 let key = String::from_utf8_lossy(attr.key.as_ref());
708 let value = String::from_utf8_lossy(&attr.value);
709
710 match key.as_ref() {
711 "build_id" => build.build_id = value.to_string(),
712 "app_id" => build.app_id = value.to_string(),
713 "version" => build.version = Some(value.to_string()),
714 "app_name" => build.app_name = Some(value.to_string()),
715 "sandbox_id" => build.sandbox_id = Some(value.to_string()),
716 "sandbox_name" => build.sandbox_name = Some(value.to_string()),
717 "lifecycle_stage" => {
718 build.lifecycle_stage = Some(value.to_string())
719 }
720 "submitter" => build.submitter = Some(value.to_string()),
721 "platform" => build.platform = Some(value.to_string()),
722 "analysis_unit" => {
723 build.analysis_unit = Some(value.to_string())
724 }
725 "policy_name" => build.policy_name = Some(value.to_string()),
726 "policy_version" => {
727 build.policy_version = Some(value.to_string())
728 }
729 "policy_compliance_status" => {
730 build.policy_compliance_status = Some(value.to_string())
731 }
732 "rules_status" => build.rules_status = Some(value.to_string()),
733 "grace_period_expired" => {
734 build.grace_period_expired = value.parse::<bool>().ok();
735 }
736 "scan_overdue" => {
737 build.scan_overdue = value.parse::<bool>().ok();
738 }
739 "legacy_scan_engine" => {
740 build.legacy_scan_engine = value.parse::<bool>().ok();
741 }
742 "launch_date" => {
743 if let Ok(date) =
744 NaiveDate::parse_from_str(&value, "%m/%d/%Y")
745 {
746 build.launch_date = Some(date);
747 }
748 }
749 "policy_updated_date" => {
750 if let Ok(datetime) =
751 chrono::DateTime::parse_from_rfc3339(&value)
752 {
753 build.policy_updated_date =
754 Some(datetime.with_timezone(&Utc));
755 }
756 }
757 _ => {
758 build.attributes.insert(key.to_string(), value.to_string());
759 }
760 }
761 }
762 }
763 b"analysis_unit" if inside_build => {
764 for attr in e.attributes().flatten() {
766 let key = String::from_utf8_lossy(attr.key.as_ref());
767 let value = String::from_utf8_lossy(&attr.value);
768
769 match key.as_ref() {
771 "status" => {
772 build
774 .attributes
775 .insert("status".to_string(), value.to_string());
776 }
777 _ => {
778 build
780 .attributes
781 .insert(format!("analysis_{key}"), value.to_string());
782 }
783 }
784 }
785 }
786 _ => {}
787 }
788 }
789 Ok(Event::Empty(ref e)) => {
790 if e.name().as_ref() == b"analysis_unit" && inside_build {
792 for attr in e.attributes().flatten() {
793 let key = String::from_utf8_lossy(attr.key.as_ref());
794 let value = String::from_utf8_lossy(&attr.value);
795
796 match key.as_ref() {
797 "status" => {
798 build
799 .attributes
800 .insert("status".to_string(), value.to_string());
801 }
802 _ => {
803 build
804 .attributes
805 .insert(format!("analysis_{key}"), value.to_string());
806 }
807 }
808 }
809 }
810 }
811 Ok(Event::End(ref e)) => {
812 if e.name().as_ref() == b"build" {
813 inside_build = false;
814 }
815 }
816 Ok(Event::Eof) => break,
817 Err(e) => return Err(BuildError::XmlParsingError(e.to_string())),
818 _ => {}
819 }
820 buf.clear();
821 }
822
823 if build.build_id.is_empty() {
824 return Err(BuildError::XmlParsingError(
825 "No build information found in response".to_string(),
826 ));
827 }
828
829 Ok(build)
830 }
831
832 fn parse_build_list(&self, xml: &str) -> Result<BuildList, BuildError> {
834 let mut reader = Reader::from_str(xml);
835 reader.config_mut().trim_text(true);
836
837 let mut buf = Vec::new();
838 let mut build_list = BuildList {
839 account_id: None,
840 app_id: String::new(),
841 app_name: None,
842 builds: Vec::new(),
843 };
844
845 loop {
846 match reader.read_event_into(&mut buf) {
847 Ok(Event::Start(ref e)) => match e.name().as_ref() {
848 b"buildlist" => {
849 for attr in e.attributes().flatten() {
850 let key = String::from_utf8_lossy(attr.key.as_ref());
851 let value = String::from_utf8_lossy(&attr.value);
852
853 match key.as_ref() {
854 "account_id" => build_list.account_id = Some(value.to_string()),
855 "app_id" => build_list.app_id = value.to_string(),
856 "app_name" => build_list.app_name = Some(value.to_string()),
857 _ => {}
858 }
859 }
860 }
861 b"build" => {
862 let mut build = Build {
863 build_id: String::new(),
864 app_id: build_list.app_id.clone(),
865 version: None,
866 app_name: build_list.app_name.clone(),
867 sandbox_id: None,
868 sandbox_name: None,
869 lifecycle_stage: None,
870 launch_date: None,
871 submitter: None,
872 platform: None,
873 analysis_unit: None,
874 policy_name: None,
875 policy_version: None,
876 policy_compliance_status: None,
877 rules_status: None,
878 grace_period_expired: None,
879 scan_overdue: None,
880 policy_updated_date: None,
881 legacy_scan_engine: None,
882 attributes: HashMap::new(),
883 };
884
885 for attr in e.attributes().flatten() {
886 let key = String::from_utf8_lossy(attr.key.as_ref());
887 let value = String::from_utf8_lossy(&attr.value);
888
889 match key.as_ref() {
890 "build_id" => build.build_id = value.to_string(),
891 "version" => build.version = Some(value.to_string()),
892 "sandbox_id" => build.sandbox_id = Some(value.to_string()),
893 "sandbox_name" => build.sandbox_name = Some(value.to_string()),
894 "lifecycle_stage" => {
895 build.lifecycle_stage = Some(value.to_string())
896 }
897 "submitter" => build.submitter = Some(value.to_string()),
898 "platform" => build.platform = Some(value.to_string()),
899 "analysis_unit" => build.analysis_unit = Some(value.to_string()),
900 "policy_name" => build.policy_name = Some(value.to_string()),
901 "policy_version" => build.policy_version = Some(value.to_string()),
902 "policy_compliance_status" => {
903 build.policy_compliance_status = Some(value.to_string())
904 }
905 "rules_status" => build.rules_status = Some(value.to_string()),
906 "grace_period_expired" => {
907 build.grace_period_expired = value.parse::<bool>().ok();
908 }
909 "scan_overdue" => {
910 build.scan_overdue = value.parse::<bool>().ok();
911 }
912 "legacy_scan_engine" => {
913 build.legacy_scan_engine = value.parse::<bool>().ok();
914 }
915 "launch_date" => {
916 if let Ok(date) = NaiveDate::parse_from_str(&value, "%m/%d/%Y")
917 {
918 build.launch_date = Some(date);
919 }
920 }
921 "policy_updated_date" => {
922 if let Ok(datetime) =
923 chrono::DateTime::parse_from_rfc3339(&value)
924 {
925 build.policy_updated_date =
926 Some(datetime.with_timezone(&Utc));
927 }
928 }
929 _ => {
930 build.attributes.insert(key.to_string(), value.to_string());
931 }
932 }
933 }
934
935 if !build.build_id.is_empty() {
936 build_list.builds.push(build);
937 }
938 }
939 _ => {}
940 },
941 Ok(Event::Eof) => break,
942 Err(e) => return Err(BuildError::XmlParsingError(e.to_string())),
943 _ => {}
944 }
945 buf.clear();
946 }
947
948 Ok(build_list)
949 }
950
951 fn parse_delete_result(&self, xml: &str) -> Result<DeleteBuildResult, BuildError> {
953 let mut reader = Reader::from_str(xml);
954 reader.config_mut().trim_text(true);
955
956 let mut buf = Vec::new();
957 let mut result = String::new();
958
959 loop {
960 match reader.read_event_into(&mut buf) {
961 Ok(Event::Start(ref e)) => {
962 if e.name().as_ref() == b"result" {
963 if let Ok(Event::Text(e)) = reader.read_event_into(&mut buf) {
965 result = String::from_utf8_lossy(&e).to_string();
966 }
967 }
968 }
969 Ok(Event::Eof) => break,
970 Err(e) => return Err(BuildError::XmlParsingError(e.to_string())),
971 _ => {}
972 }
973 buf.clear();
974 }
975
976 if result.is_empty() {
977 return Err(BuildError::XmlParsingError(
978 "No result found in delete response".to_string(),
979 ));
980 }
981
982 Ok(DeleteBuildResult { result })
983 }
984}
985
986impl BuildApi {
988 pub async fn create_simple_build(
999 &self,
1000 app_id: &str,
1001 version: Option<&str>,
1002 ) -> Result<Build, BuildError> {
1003 let request = CreateBuildRequest {
1004 app_id: app_id.to_string(),
1005 version: version.map(|s| s.to_string()),
1006 lifecycle_stage: None,
1007 launch_date: None,
1008 sandbox_id: None,
1009 };
1010
1011 self.create_build(request).await
1012 }
1013
1014 pub async fn create_sandbox_build(
1026 &self,
1027 app_id: &str,
1028 sandbox_id: &str,
1029 version: Option<&str>,
1030 ) -> Result<Build, BuildError> {
1031 let request = CreateBuildRequest {
1032 app_id: app_id.to_string(),
1033 version: version.map(|s| s.to_string()),
1034 lifecycle_stage: None,
1035 launch_date: None,
1036 sandbox_id: Some(sandbox_id.to_string()),
1037 };
1038
1039 self.create_build(request).await
1040 }
1041
1042 pub async fn delete_app_build(&self, app_id: &str) -> Result<DeleteBuildResult, BuildError> {
1052 let request = DeleteBuildRequest {
1053 app_id: app_id.to_string(),
1054 sandbox_id: None,
1055 };
1056
1057 self.delete_build(request).await
1058 }
1059
1060 pub async fn delete_sandbox_build(
1071 &self,
1072 app_id: &str,
1073 sandbox_id: &str,
1074 ) -> Result<DeleteBuildResult, BuildError> {
1075 let request = DeleteBuildRequest {
1076 app_id: app_id.to_string(),
1077 sandbox_id: Some(sandbox_id.to_string()),
1078 };
1079
1080 self.delete_build(request).await
1081 }
1082
1083 pub async fn get_app_build_info(&self, app_id: &str) -> Result<Build, BuildError> {
1093 let request = GetBuildInfoRequest {
1094 app_id: app_id.to_string(),
1095 build_id: None,
1096 sandbox_id: None,
1097 };
1098
1099 self.get_build_info(request).await
1100 }
1101
1102 pub async fn get_sandbox_build_info(
1113 &self,
1114 app_id: &str,
1115 sandbox_id: &str,
1116 ) -> Result<Build, BuildError> {
1117 let request = GetBuildInfoRequest {
1118 app_id: app_id.to_string(),
1119 build_id: None,
1120 sandbox_id: Some(sandbox_id.to_string()),
1121 };
1122
1123 self.get_build_info(request).await
1124 }
1125
1126 pub async fn get_app_builds(&self, app_id: &str) -> Result<BuildList, BuildError> {
1136 let request = GetBuildListRequest {
1137 app_id: app_id.to_string(),
1138 sandbox_id: None,
1139 };
1140
1141 self.get_build_list(request).await
1142 }
1143
1144 pub async fn get_sandbox_builds(
1155 &self,
1156 app_id: &str,
1157 sandbox_id: &str,
1158 ) -> Result<BuildList, BuildError> {
1159 let request = GetBuildListRequest {
1160 app_id: app_id.to_string(),
1161 sandbox_id: Some(sandbox_id.to_string()),
1162 };
1163
1164 self.get_build_list(request).await
1165 }
1166}
1167
1168#[cfg(test)]
1169mod tests {
1170 use super::*;
1171 use crate::VeracodeConfig;
1172
1173 #[test]
1174 fn test_create_build_request() {
1175 let request = CreateBuildRequest {
1176 app_id: "123".to_string(),
1177 version: Some("1.0.0".to_string()),
1178 lifecycle_stage: Some("In Development (pre-Alpha)".to_string()),
1179 launch_date: Some("12/31/2024".to_string()),
1180 sandbox_id: None,
1181 };
1182
1183 assert_eq!(request.app_id, "123");
1184 assert_eq!(request.version, Some("1.0.0".to_string()));
1185 assert_eq!(
1186 request.lifecycle_stage,
1187 Some("In Development (pre-Alpha)".to_string())
1188 );
1189 }
1190
1191 #[test]
1192 fn test_update_build_request() {
1193 let request = UpdateBuildRequest {
1194 app_id: "123".to_string(),
1195 build_id: Some("456".to_string()),
1196 version: Some("1.1.0".to_string()),
1197 lifecycle_stage: Some("Internal or Alpha Testing".to_string()),
1198 launch_date: None,
1199 sandbox_id: Some("789".to_string()),
1200 };
1201
1202 assert_eq!(request.app_id, "123");
1203 assert_eq!(request.build_id, Some("456".to_string()));
1204 assert_eq!(request.sandbox_id, Some("789".to_string()));
1205 }
1206
1207 #[test]
1208 fn test_lifecycle_stage_validation() {
1209 assert!(is_valid_lifecycle_stage("In Development (pre-Alpha)"));
1211 assert!(is_valid_lifecycle_stage("Internal or Alpha Testing"));
1212 assert!(is_valid_lifecycle_stage("External or Beta Testing"));
1213 assert!(is_valid_lifecycle_stage("Deployed"));
1214 assert!(is_valid_lifecycle_stage("Maintenance"));
1215 assert!(is_valid_lifecycle_stage("Cannot Disclose"));
1216 assert!(is_valid_lifecycle_stage("Not Specified"));
1217
1218 assert!(!is_valid_lifecycle_stage("In Development"));
1220 assert!(!is_valid_lifecycle_stage("Development"));
1221 assert!(!is_valid_lifecycle_stage("QA"));
1222 assert!(!is_valid_lifecycle_stage("Production"));
1223 assert!(!is_valid_lifecycle_stage(""));
1224
1225 assert_eq!(default_lifecycle_stage(), "In Development (pre-Alpha)");
1227 assert!(is_valid_lifecycle_stage(default_lifecycle_stage()));
1228 }
1229
1230 #[test]
1231 fn test_build_error_display() {
1232 let error = BuildError::BuildNotFound;
1233 assert_eq!(error.to_string(), "Build not found");
1234
1235 let error = BuildError::InvalidParameter("Invalid app_id".to_string());
1236 assert_eq!(error.to_string(), "Invalid parameter: Invalid app_id");
1237
1238 let error = BuildError::CreationFailed("Build creation failed".to_string());
1239 assert_eq!(
1240 error.to_string(),
1241 "Build creation failed: Build creation failed"
1242 );
1243 }
1244
1245 #[tokio::test]
1246 async fn test_build_api_method_signatures() {
1247 async fn _test_build_methods() -> Result<(), Box<dyn std::error::Error>> {
1248 let config = VeracodeConfig::new("test".to_string(), "test".to_string());
1249 let client = VeracodeClient::new(config)?;
1250 let api = client.build_api();
1251
1252 let create_request = CreateBuildRequest {
1254 app_id: "123".to_string(),
1255 version: None,
1256 lifecycle_stage: None,
1257 launch_date: None,
1258 sandbox_id: None,
1259 };
1260
1261 let _: Result<Build, _> = api.create_build(create_request).await;
1264 let _: Result<Build, _> = api.create_simple_build("123", None).await;
1265 let _: Result<Build, _> = api.create_sandbox_build("123", "456", None).await;
1266 let _: Result<DeleteBuildResult, _> = api.delete_app_build("123").await;
1267 let _: Result<Build, _> = api.get_app_build_info("123").await;
1268 let _: Result<BuildList, _> = api.get_app_builds("123").await;
1269
1270 Ok(())
1271 }
1272
1273 }
1276
1277 #[test]
1278 fn test_build_status_from_str() {
1279 assert_eq!(
1280 BuildStatus::from_string("Incomplete"),
1281 BuildStatus::Incomplete
1282 );
1283 assert_eq!(
1284 BuildStatus::from_string("Results Ready"),
1285 BuildStatus::ResultsReady
1286 );
1287 assert_eq!(
1288 BuildStatus::from_string("Pre-Scan Failed"),
1289 BuildStatus::PreScanFailed
1290 );
1291 assert_eq!(
1292 BuildStatus::from_string("Unknown Status"),
1293 BuildStatus::Unknown("Unknown Status".to_string())
1294 );
1295 }
1296
1297 #[test]
1298 fn test_build_status_to_str() {
1299 assert_eq!(BuildStatus::Incomplete.to_str(), "Incomplete");
1300 assert_eq!(BuildStatus::ResultsReady.to_str(), "Results Ready");
1301 assert_eq!(BuildStatus::PreScanFailed.to_str(), "Pre-Scan Failed");
1302 assert_eq!(
1303 BuildStatus::Unknown("Custom".to_string()).to_str(),
1304 "Custom"
1305 );
1306 }
1307
1308 #[test]
1309 fn test_build_status_deletion_policy_0() {
1310 assert!(!BuildStatus::Incomplete.is_safe_to_delete(0));
1312 assert!(!BuildStatus::ResultsReady.is_safe_to_delete(0));
1313 assert!(!BuildStatus::Failed.is_safe_to_delete(0));
1314 }
1315
1316 #[test]
1317 fn test_build_status_deletion_policy_1() {
1318 assert!(BuildStatus::Incomplete.is_safe_to_delete(1));
1320 assert!(BuildStatus::NotSubmitted.is_safe_to_delete(1));
1321 assert!(BuildStatus::PreScanFailed.is_safe_to_delete(1));
1322 assert!(BuildStatus::Failed.is_safe_to_delete(1));
1323 assert!(BuildStatus::Cancelled.is_safe_to_delete(1));
1324
1325 assert!(!BuildStatus::ResultsReady.is_safe_to_delete(1));
1327 assert!(!BuildStatus::ScanInProcess.is_safe_to_delete(1));
1328 assert!(!BuildStatus::PreScanSuccess.is_safe_to_delete(1));
1329 }
1330
1331 #[test]
1332 fn test_build_status_deletion_policy_2() {
1333 assert!(BuildStatus::Incomplete.is_safe_to_delete(2));
1335 assert!(BuildStatus::Failed.is_safe_to_delete(2));
1336 assert!(BuildStatus::ScanInProcess.is_safe_to_delete(2));
1337 assert!(BuildStatus::PreScanSuccess.is_safe_to_delete(2));
1338
1339 assert!(!BuildStatus::ResultsReady.is_safe_to_delete(2));
1341 }
1342
1343 #[test]
1344 fn test_build_status_deletion_policy_invalid() {
1345 assert!(!BuildStatus::Incomplete.is_safe_to_delete(3));
1347 assert!(!BuildStatus::Failed.is_safe_to_delete(255));
1348 }
1349}