1use serde::{Deserialize, Serialize};
7
8use crate::{VeracodeClient, VeracodeError};
9
10#[derive(Debug, thiserror::Error)]
12pub enum PipelineError {
13 #[error("Pipeline scan not found")]
14 ScanNotFound,
15 #[error("Permission denied: {0}")]
16 PermissionDenied(String),
17 #[error("Invalid request: {0}")]
18 InvalidRequest(String),
19 #[error("Scan timeout")]
20 ScanTimeout,
21 #[error("Scan findings not ready yet - try again later")]
22 FindingsNotReady,
23 #[error("Application not found: {0}")]
24 ApplicationNotFound(String),
25 #[error(
26 "Multiple applications found with name '{0}'. Please check the application name and ensure it uniquely identifies a single application."
27 )]
28 MultipleApplicationsFound(String),
29 #[error("API error: {0}")]
30 ApiError(#[from] VeracodeError),
31 #[error("HTTP error: {0}")]
32 Http(#[from] reqwest::Error),
33}
34
35#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
37#[serde(rename_all = "UPPERCASE")]
38pub enum DevStage {
39 Development,
40 Testing,
41 Release,
42}
43
44#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
46#[serde(rename_all = "UPPERCASE")]
47pub enum ScanStage {
48 Create,
49 Upload,
50 Start,
51 Details,
52 Findings,
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
57#[serde(rename_all = "UPPERCASE")]
58pub enum ScanStatus {
59 Pending,
60 Uploading,
61 Started,
62 Success,
63 Failure,
64 Cancelled,
65 Timeout,
66 #[serde(rename = "USER_TIMEOUT")]
67 UserTimeout,
68}
69
70impl ScanStatus {
71 pub fn is_successful(&self) -> bool {
73 matches!(self, ScanStatus::Success)
74 }
75
76 pub fn is_failed(&self) -> bool {
78 matches!(
79 self,
80 ScanStatus::Failure
81 | ScanStatus::Cancelled
82 | ScanStatus::Timeout
83 | ScanStatus::UserTimeout
84 )
85 }
86
87 pub fn is_in_progress(&self) -> bool {
89 matches!(
90 self,
91 ScanStatus::Pending | ScanStatus::Uploading | ScanStatus::Started
92 )
93 }
94}
95
96impl std::fmt::Display for ScanStatus {
97 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98 match self {
99 ScanStatus::Pending => write!(f, "PENDING"),
100 ScanStatus::Uploading => write!(f, "UPLOADING"),
101 ScanStatus::Started => write!(f, "STARTED"),
102 ScanStatus::Success => write!(f, "SUCCESS"),
103 ScanStatus::Failure => write!(f, "FAILURE"),
104 ScanStatus::Cancelled => write!(f, "CANCELLED"),
105 ScanStatus::Timeout => write!(f, "TIMEOUT"),
106 ScanStatus::UserTimeout => write!(f, "USER_TIMEOUT"),
107 }
108 }
109}
110
111#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
113pub enum Severity {
114 #[serde(rename = "0")]
115 Informational,
116 #[serde(rename = "1")]
117 VeryLow,
118 #[serde(rename = "2")]
119 Low,
120 #[serde(rename = "3")]
121 Medium,
122 #[serde(rename = "4")]
123 High,
124 #[serde(rename = "5")]
125 VeryHigh,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct SourceFile {
131 pub file: String,
133 pub function_name: Option<String>,
135 pub function_prototype: String,
137 pub line: u32,
139 pub qualified_function_name: String,
141 pub scope: String,
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct FindingFiles {
148 pub source_file: SourceFile,
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct StackDumps {
155 pub stack_dump: Option<Vec<serde_json::Value>>,
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct Finding {
162 pub cwe_id: String,
164 pub display_text: String,
166 pub files: FindingFiles,
168 pub flaw_details_link: Option<String>,
170 pub gob: String,
172 pub issue_id: u32,
174 pub issue_type: String,
176 pub issue_type_id: String,
178 pub severity: u32,
180 pub stack_dumps: Option<StackDumps>,
182 pub title: String,
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize)]
188pub struct FindingsResponse {
189 #[serde(rename = "_links")]
191 pub links: Option<serde_json::Value>,
192 pub scan_id: String,
194 pub scan_status: ScanStatus,
196 pub message: String,
198 pub modules: Vec<String>,
200 pub modules_count: u32,
202 pub findings: Vec<Finding>,
204 pub selected_modules: Vec<String>,
206 pub stack_dump: Option<serde_json::Value>,
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize)]
213pub struct LegacyFinding {
214 pub file: String,
216 pub line: u32,
218 pub issue_type: String,
220 pub severity: u32,
222 pub message: String,
224 pub cwe_id: u32,
226 pub details_link: Option<String>,
228 pub issue_id: Option<String>,
230 pub owasp_category: Option<String>,
232 pub sans_category: Option<String>,
234}
235
236impl Finding {
237 pub fn to_legacy(&self) -> LegacyFinding {
239 LegacyFinding {
240 file: self.files.source_file.file.clone(),
241 line: self.files.source_file.line,
242 issue_type: self.issue_type.clone(),
243 severity: self.severity,
244 message: strip_html_tags(&self.display_text),
245 cwe_id: self.cwe_id.parse().unwrap_or(0),
246 details_link: None,
247 issue_id: Some(self.issue_id.to_string()),
248 owasp_category: None,
249 sans_category: None,
250 }
251 }
252}
253
254fn strip_html_tags(html: &str) -> String {
256 let mut result = String::new();
258 let mut in_tag = false;
259
260 for ch in html.chars() {
261 match ch {
262 '<' => in_tag = true,
263 '>' => in_tag = false,
264 _ if !in_tag => result.push(ch),
265 _ => {}
266 }
267 }
268
269 result.split_whitespace().collect::<Vec<&str>>().join(" ")
271}
272
273#[derive(Debug, Clone, Serialize, Deserialize)]
275pub struct CreateScanRequest {
276 #[serde(skip_serializing_if = "never_skip_string")]
278 pub binary_name: String,
279 #[serde(skip_serializing_if = "never_skip_u64")]
281 pub binary_size: u64,
282 #[serde(skip_serializing_if = "never_skip_string")]
284 pub binary_hash: String,
285 pub project_name: String,
287 #[serde(skip_serializing_if = "Option::is_none")]
289 pub project_uri: Option<String>,
290 pub dev_stage: DevStage,
292 #[serde(skip_serializing_if = "Option::is_none")]
294 pub app_id: Option<String>,
295 #[serde(skip_serializing_if = "Option::is_none")]
297 pub project_ref: Option<String>,
298 #[serde(skip_serializing_if = "Option::is_none")]
300 pub scan_timeout: Option<u32>,
301 #[serde(skip_serializing_if = "Option::is_none")]
303 pub plugin_version: Option<String>,
304 #[serde(skip_serializing_if = "Option::is_none")]
306 pub emit_stack_dump: Option<String>,
307 #[serde(skip_serializing_if = "Option::is_none")]
309 pub include_modules: Option<String>,
310}
311
312fn never_skip_string(_: &String) -> bool {
314 false
315}
316
317fn never_skip_u64(_: &u64) -> bool {
319 false
320}
321
322#[derive(Debug, Clone)]
324pub struct ScanCreationResult {
325 pub scan_id: String,
327 pub upload_uri: Option<String>,
329 pub details_uri: Option<String>,
331 pub start_uri: Option<String>,
333 pub cancel_uri: Option<String>,
335 pub expected_segments: Option<u32>,
337}
338
339#[derive(Debug, Clone, Serialize, Deserialize)]
341pub struct ScanConfig {
342 pub timeout: Option<u32>,
344 pub include_low_severity: Option<bool>,
346 pub max_findings: Option<u32>,
348}
349
350#[derive(Debug, Clone, Serialize, Deserialize)]
352pub struct Scan {
353 pub scan_id: String,
355 pub scan_status: ScanStatus,
357 pub api_version: f64,
359 pub app_id: Option<String>,
361 pub project_name: String,
363 pub project_uri: Option<String>,
365 pub project_ref: Option<String>,
367 pub commit_hash: Option<String>,
369 pub dev_stage: String,
371 pub binary_name: String,
373 pub binary_size: u64,
375 pub binary_hash: String,
377 pub binary_segments_expected: u32,
379 pub binary_segments_uploaded: u32,
381 pub scan_timeout: Option<u32>,
383 pub scan_duration: Option<f64>,
385 pub results_size: Option<f64>,
387 pub message: Option<String>,
389 pub created: String,
391 pub changed: String,
393 pub modules: Vec<serde_json::Value>,
395 pub selected_modules: Vec<serde_json::Value>,
397 pub display_modules: Vec<serde_json::Value>,
399 pub display_selected_modules: Vec<serde_json::Value>,
401 #[serde(rename = "_links")]
403 pub links: Option<serde_json::Value>,
404}
405
406#[derive(Debug, Clone, Serialize, Deserialize)]
408pub struct ScanResults {
409 pub scan: Scan,
411 pub findings: Vec<Finding>,
413 pub summary: FindingsSummary,
415 pub standards: SecurityStandards,
417}
418
419#[derive(Debug, Clone, Serialize, Deserialize)]
421pub struct FindingsSummary {
422 pub very_high: u32,
424 pub high: u32,
426 pub medium: u32,
428 pub low: u32,
430 pub very_low: u32,
432 pub informational: u32,
434 pub total: u32,
436}
437
438#[derive(Debug, Clone, Serialize, Deserialize)]
440pub struct SecurityStandards {
441 pub owasp: Option<StandardCompliance>,
443 pub sans: Option<StandardCompliance>,
445 pub pci: Option<StandardCompliance>,
447 pub cwe: Option<StandardCompliance>,
449}
450
451#[derive(Debug, Clone, Serialize, Deserialize)]
453pub struct StandardCompliance {
454 pub total_rules: u32,
456 pub violations: u32,
458 pub compliance_score: f64,
460 pub violated_rules: Vec<String>,
462}
463
464pub struct PipelineApi {
466 client: VeracodeClient,
467 debug: bool,
468}
469
470impl PipelineApi {
471 pub fn new(client: VeracodeClient) -> Self {
473 Self {
474 client,
475 debug: false,
476 }
477 }
478
479 pub fn new_with_debug(client: VeracodeClient, debug: bool) -> Self {
481 Self { client, debug }
482 }
483
484 fn get_pipeline_base_url(&self) -> String {
486 if self.client.config().base_url.contains("api.veracode.com") {
487 "https://api.veracode.com/pipeline_scan/v1".to_string()
488 } else {
489 format!(
491 "{}/pipeline_scan/v1",
492 self.client.config().base_url.trim_end_matches('/')
493 )
494 }
495 }
496
497 pub async fn lookup_app_id_by_name(&self, app_name: &str) -> Result<String, PipelineError> {
507 let applications = self.client.search_applications_by_name(app_name).await?;
508
509 match applications.len() {
510 0 => Err(PipelineError::ApplicationNotFound(app_name.to_string())),
511 1 => Ok(applications[0].id.to_string()),
512 _ => {
513 eprintln!(
515 "❌ Found {} applications matching '{}':",
516 applications.len(),
517 app_name
518 );
519 for (i, app) in applications.iter().enumerate() {
520 if let Some(ref profile) = app.profile {
521 eprintln!(" {}. ID: {} - Name: '{}'", i + 1, app.id, profile.name);
522 } else {
523 eprintln!(" {}. ID: {} - GUID: {}", i + 1, app.id, app.guid);
524 }
525 }
526 eprintln!(
527 "💡 Please provide a more specific application name that matches exactly one application."
528 );
529 Err(PipelineError::MultipleApplicationsFound(
530 app_name.to_string(),
531 ))
532 }
533 }
534 }
535
536 pub async fn create_scan_with_app_lookup(
547 &self,
548 mut request: CreateScanRequest,
549 app_name: Option<&str>,
550 ) -> Result<ScanCreationResult, PipelineError> {
551 if let Some(name) = app_name {
553 if request.app_id.is_none() {
554 let app_id = self.lookup_app_id_by_name(name).await?;
555 request.app_id = Some(app_id);
556 println!(
557 "✅ Found application '{}' with ID: {}",
558 name,
559 request.app_id.as_ref().unwrap()
560 );
561 }
562 }
563
564 self.create_scan(request).await
565 }
566
567 pub async fn create_scan(
577 &self,
578 mut request: CreateScanRequest,
579 ) -> Result<ScanCreationResult, PipelineError> {
580 if request.plugin_version.is_none() {
582 request.plugin_version = Some("25.2.0-0".to_string());
583 }
584
585 if request.scan_timeout.is_none() {
587 request.scan_timeout = Some(30);
588 }
589
590 let endpoint = "/pipeline_scan/v1/scans";
592 let _full_url = format!("{}{}", self.client.config().base_url, endpoint);
593
594 let response = self
595 .client
596 .post_with_response("/pipeline_scan/v1/scans", Some(&request))
597 .await?;
598
599 let response_text = response.text().await?;
600
601 if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&response_text) {
603 let scan_id = json_value
605 .get("scan_id")
606 .and_then(|id| id.as_str())
607 .ok_or_else(|| {
608 PipelineError::InvalidRequest("Missing scan_id in response".to_string())
609 })?
610 .to_string();
611
612 let links = json_value.get("_links");
614
615 let upload_uri = links
616 .and_then(|links| links.get("upload"))
617 .and_then(|upload| upload.get("href"))
618 .and_then(|href| href.as_str())
619 .map(|s| s.to_string());
620
621 let details_uri = links
622 .and_then(|links| links.get("details"))
623 .and_then(|details| details.get("href"))
624 .and_then(|href| href.as_str())
625 .map(|s| s.to_string());
626
627 let start_uri = links
628 .and_then(|links| links.get("start"))
629 .and_then(|start| start.get("href"))
630 .and_then(|href| href.as_str())
631 .map(|s| s.to_string());
632
633 let cancel_uri = links
634 .and_then(|links| links.get("cancel"))
635 .and_then(|cancel| cancel.get("href"))
636 .and_then(|href| href.as_str())
637 .map(|s| s.to_string());
638
639 let expected_segments = json_value
641 .get("binary_segments_expected")
642 .and_then(|segments| segments.as_u64())
643 .map(|s| s as u32);
644
645 if self.debug {
646 println!("✅ Scan creation response parsed:");
647 println!(" Scan ID: {scan_id}");
648 if let Some(ref uri) = upload_uri {
649 println!(" Upload URI: {uri}");
650 }
651 if let Some(ref uri) = details_uri {
652 println!(" Details URI: {uri}");
653 }
654 if let Some(ref uri) = start_uri {
655 println!(" Start URI: {uri}");
656 }
657 if let Some(ref uri) = cancel_uri {
658 println!(" Cancel URI: {uri}");
659 }
660 if let Some(segments) = expected_segments {
661 println!(" Expected segments: {segments}");
662 }
663 }
664
665 return Ok(ScanCreationResult {
666 scan_id,
667 upload_uri,
668 details_uri,
669 start_uri,
670 cancel_uri,
671 expected_segments,
672 });
673 }
674
675 Err(PipelineError::InvalidRequest(
676 "Failed to parse scan creation response".to_string(),
677 ))
678 }
679
680 pub async fn upload_binary_segments(
699 &self,
700 initial_upload_uri: &str,
701 expected_segments: i32,
702 binary_data: &[u8],
703 file_name: &str,
704 ) -> Result<(), PipelineError> {
705 let total_size = binary_data.len();
706 let segment_size = ((total_size as f64) / (expected_segments as f64)).ceil() as usize;
707
708 if self.debug {
709 println!(
710 "📤 Uploading binary in {expected_segments} segments ({total_size} bytes total)"
711 );
712 println!(" Segment size: {segment_size} bytes each");
713 }
714
715 let mut current_upload_uri = initial_upload_uri.to_string();
716
717 for segment_num in 0..expected_segments {
718 let start_idx = (segment_num as usize) * segment_size;
719 let end_idx = std::cmp::min(start_idx + segment_size, total_size);
720 let segment_data = &binary_data[start_idx..end_idx];
721
722 if self.debug {
723 println!(
724 " Uploading segment {}/{} ({} bytes)...",
725 segment_num + 1,
726 expected_segments,
727 segment_data.len()
728 );
729 }
730
731 match self
732 .upload_single_segment(¤t_upload_uri, segment_data, file_name)
733 .await
734 {
735 Ok(response_text) => {
736 if self.debug {
737 println!(" ✅ Segment {} uploaded successfully", segment_num + 1);
738 }
739
740 if segment_num < expected_segments - 1 {
742 match self.extract_next_upload_uri(&response_text) {
743 Some(next_uri) => {
744 current_upload_uri = next_uri;
745 if self.debug {
746 println!(" 📍 Next segment URI: {current_upload_uri}");
747 }
748 }
749 None => {
750 if self.debug {
751 eprintln!(
752 " ⚠️ No next URI found in response, using current"
753 );
754 }
755 }
756 }
757 }
758 }
759 Err(e) => {
760 eprintln!(" ❌ Failed to upload segment {}: {}", segment_num + 1, e);
761 return Err(e);
762 }
763 }
764 }
765
766 if self.debug {
767 println!("✅ All {expected_segments} segments uploaded successfully");
768 }
769 Ok(())
770 }
771
772 pub async fn upload_binary(
774 &self,
775 scan_id: &str,
776 binary_data: &[u8],
777 ) -> Result<(), PipelineError> {
778 let upload_uri = format!("/pipeline_scan/scans/{scan_id}/segments/1");
780 let expected_segments = 1; let file_name = "binary.tar.gz";
782
783 self.upload_binary_segments(&upload_uri, expected_segments, binary_data, file_name)
784 .await
785 }
786
787 async fn upload_single_segment(
789 &self,
790 upload_uri: &str,
791 segment_data: &[u8],
792 file_name: &str,
793 ) -> Result<String, PipelineError> {
794 let url = if upload_uri.starts_with("http") {
796 upload_uri.to_string()
797 } else {
798 format!("{}{}", self.get_pipeline_base_url(), upload_uri)
799 };
800
801 let mut headers = std::collections::HashMap::new();
803 headers.insert("accept", "application/json");
804 headers.insert("PLUGIN-VERSION", "25.2.0-0"); let response = self
808 .client
809 .upload_file_multipart_put(
810 &url,
811 "file",
812 file_name,
813 segment_data.to_vec(),
814 Some(headers),
815 )
816 .await
817 .map_err(PipelineError::ApiError)?;
818
819 if response.status().is_success() {
820 let response_text = response.text().await?;
821 Ok(response_text)
822 } else {
823 let status = response.status();
824 let error_text = response
825 .text()
826 .await
827 .unwrap_or_else(|_| "Unknown error".to_string());
828 Err(PipelineError::InvalidRequest(format!(
829 "Segment upload failed with status {status}: {error_text}"
830 )))
831 }
832 }
833
834 fn extract_next_upload_uri(&self, response_text: &str) -> Option<String> {
836 if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(response_text) {
838 if let Some(links) = json_value.get("_links") {
840 if let Some(upload) = links.get("upload") {
841 if let Some(href) = upload.get("href") {
842 return href.as_str().map(|s| s.to_string());
843 }
844 }
845 }
846
847 if let Some(upload_url) = json_value.get("upload_url") {
849 return upload_url.as_str().map(|s| s.to_string());
850 }
851 }
852
853 None
854 }
855
856 pub async fn start_scan_with_uri(
867 &self,
868 start_uri: &str,
869 config: Option<ScanConfig>,
870 ) -> Result<(), PipelineError> {
871 let mut payload = serde_json::json!({
873 "scan_status": "STARTED"
874 });
875
876 if let Some(config) = config {
878 if let Some(timeout) = config.timeout {
879 payload["timeout"] = serde_json::Value::Number(timeout.into());
880 }
881 if let Some(include_low_severity) = config.include_low_severity {
882 payload["include_low_severity"] = serde_json::Value::Bool(include_low_severity);
883 }
884 if let Some(max_findings) = config.max_findings {
885 payload["max_findings"] = serde_json::Value::Number(max_findings.into());
886 }
887 }
888
889 let url = if start_uri.starts_with("http") {
891 start_uri.to_string()
892 } else {
893 format!("{}{}", self.get_pipeline_base_url(), start_uri)
894 };
895
896 let auth_header = self
898 .client
899 .generate_auth_header("PUT", &url)
900 .map_err(PipelineError::ApiError)?;
901
902 let response = self
903 .client
904 .client()
905 .put(&url)
906 .header("Authorization", auth_header)
907 .header("accept", "application/json")
908 .header("content-type", "application/json")
909 .json(&payload)
910 .send()
911 .await?;
912
913 if response.status().is_success() {
914 Ok(())
915 } else {
916 let error_text = response
917 .text()
918 .await
919 .unwrap_or_else(|_| "Unknown error".to_string());
920 Err(PipelineError::InvalidRequest(format!(
921 "Failed to start scan: {error_text}"
922 )))
923 }
924 }
925
926 pub async fn start_scan(
937 &self,
938 scan_id: &str,
939 config: Option<ScanConfig>,
940 ) -> Result<(), PipelineError> {
941 let endpoint = format!("/scans/{scan_id}");
942 let url = format!("{}{}", self.get_pipeline_base_url(), endpoint);
943
944 let mut payload = serde_json::json!({
946 "scan_status": "STARTED"
947 });
948
949 if let Some(config) = config {
951 if let Some(timeout) = config.timeout {
952 payload["timeout"] = serde_json::Value::Number(timeout.into());
953 }
954 if let Some(include_low_severity) = config.include_low_severity {
955 payload["include_low_severity"] = serde_json::Value::Bool(include_low_severity);
956 }
957 if let Some(max_findings) = config.max_findings {
958 payload["max_findings"] = serde_json::Value::Number(max_findings.into());
959 }
960 }
961
962 let auth_header = self
964 .client
965 .generate_auth_header("PUT", &url)
966 .map_err(PipelineError::ApiError)?;
967
968 let response = self
969 .client
970 .client()
971 .put(&url)
972 .header("Authorization", auth_header)
973 .header("accept", "application/json")
974 .header("content-type", "application/json")
975 .json(&payload)
976 .send()
977 .await?;
978
979 if response.status().is_success() {
980 Ok(())
981 } else {
982 let error_text = response
983 .text()
984 .await
985 .unwrap_or_else(|_| "Unknown error".to_string());
986 Err(PipelineError::InvalidRequest(format!(
987 "Failed to start scan: {error_text}"
988 )))
989 }
990 }
991
992 pub async fn get_scan_with_uri(&self, details_uri: &str) -> Result<Scan, PipelineError> {
1002 let url = if details_uri.starts_with("http") {
1004 details_uri.to_string()
1005 } else {
1006 format!("{}{}", self.get_pipeline_base_url(), details_uri)
1007 };
1008
1009 let auth_header = self
1011 .client
1012 .generate_auth_header("GET", &url)
1013 .map_err(PipelineError::ApiError)?;
1014
1015 let response = self
1016 .client
1017 .client()
1018 .get(&url)
1019 .header("Authorization", auth_header)
1020 .header("accept", "application/json")
1021 .send()
1022 .await?;
1023
1024 let response_text = response.text().await?;
1025
1026 serde_json::from_str::<Scan>(&response_text).map_err(|e| {
1027 PipelineError::InvalidRequest(format!("Failed to parse scan details: {e}"))
1028 })
1029 }
1030
1031 pub async fn get_scan(&self, scan_id: &str) -> Result<Scan, PipelineError> {
1041 let endpoint = format!("/scans/{scan_id}");
1042 let url = format!("{}{}", self.get_pipeline_base_url(), endpoint);
1043
1044 let auth_header = self
1046 .client
1047 .generate_auth_header("GET", &url)
1048 .map_err(PipelineError::ApiError)?;
1049
1050 let response = self
1051 .client
1052 .client()
1053 .get(&url)
1054 .header("Authorization", auth_header)
1055 .header("accept", "application/json")
1056 .send()
1057 .await?;
1058
1059 let response_text = response.text().await?;
1060
1061 serde_json::from_str::<Scan>(&response_text).map_err(|e| {
1062 PipelineError::InvalidRequest(format!("Failed to parse scan details: {e}"))
1063 })
1064 }
1065
1066 pub async fn get_findings(&self, scan_id: &str) -> Result<Vec<Finding>, PipelineError> {
1081 let endpoint = format!("/scans/{scan_id}/findings");
1082 let url = format!("{}{}", self.get_pipeline_base_url(), endpoint);
1083
1084 let auth_header = self
1088 .client
1089 .generate_auth_header("GET", &url)
1090 .map_err(PipelineError::ApiError)?;
1091
1092 let response = self
1093 .client
1094 .client()
1095 .get(&url)
1096 .header("Authorization", auth_header)
1097 .header("accept", "application/json")
1098 .send()
1099 .await?;
1100
1101 let status = response.status();
1102 let response_text = response.text().await?;
1103
1104 if self.debug {
1106 println!("🔍 Debug - Findings API Response:");
1107 println!(" Status: {status}");
1108 println!(" Response Length: {} bytes", response_text.len());
1109 }
1110
1111 match status.as_u16() {
1112 200 => {
1113 match serde_json::from_str::<FindingsResponse>(&response_text) {
1115 Ok(findings_response) => {
1116 if self.debug {
1117 println!("🔍 Debug - Successfully parsed findings response:");
1118 println!(" Scan Status: {}", findings_response.scan_status);
1119 println!(" Message: {}", findings_response.message);
1120 println!(" Modules: {:?}", findings_response.modules);
1121 println!(" Findings Count: {}", findings_response.findings.len());
1122 }
1123 Ok(findings_response.findings)
1124 }
1125 Err(e) => {
1126 if self.debug {
1127 println!("❌ Debug - Failed to parse FindingsResponse: {e}");
1128 }
1129 if let Ok(json_value) =
1131 serde_json::from_str::<serde_json::Value>(&response_text)
1132 {
1133 if let Some(findings_array) =
1134 json_value.get("findings").and_then(|f| f.as_array())
1135 {
1136 if self.debug {
1137 println!(
1138 "🔍 Debug - Trying fallback parsing of findings array..."
1139 );
1140 }
1141 let findings: Result<Vec<Finding>, _> = findings_array
1142 .iter()
1143 .map(|f| serde_json::from_value(f.clone()))
1144 .collect();
1145 return findings.map_err(|e| {
1146 PipelineError::InvalidRequest(format!(
1147 "Failed to parse findings array: {e}"
1148 ))
1149 });
1150 }
1151 }
1152 Err(PipelineError::InvalidRequest(format!(
1153 "Failed to parse findings response: {e}"
1154 )))
1155 }
1156 }
1157 }
1158 202 => {
1159 Err(PipelineError::FindingsNotReady)
1161 }
1162 _ => {
1163 Err(PipelineError::InvalidRequest(format!(
1165 "Failed to get findings - HTTP {status}: {response_text}"
1166 )))
1167 }
1168 }
1169 }
1170
1171 pub async fn get_results(&self, scan_id: &str) -> Result<ScanResults, PipelineError> {
1186 if self.debug {
1187 println!("🔍 Debug - get_results() getting scan details for: {scan_id}");
1188 }
1189 let scan = self.get_scan(scan_id).await?;
1190 if self.debug {
1191 println!("🔍 Debug - get_results() scan status: {}", scan.scan_status);
1192 println!("🔍 Debug - get_results() calling get_findings() for: {scan_id}");
1193 }
1194 let findings = self.get_findings(scan_id).await?;
1195
1196 let summary = self.calculate_summary(&findings);
1198
1199 let standards = SecurityStandards {
1201 owasp: None,
1202 sans: None,
1203 pci: None,
1204 cwe: None,
1205 };
1206
1207 Ok(ScanResults {
1208 scan,
1209 findings,
1210 summary,
1211 standards,
1212 })
1213 }
1214
1215 pub async fn cancel_scan(&self, scan_id: &str) -> Result<(), PipelineError> {
1225 let endpoint = format!("/scans/{scan_id}/cancel");
1226
1227 let response = self.client.delete_with_response(&endpoint).await?;
1228
1229 if response.status().is_success() {
1230 Ok(())
1231 } else {
1232 let error_text = response
1233 .text()
1234 .await
1235 .unwrap_or_else(|_| "Unknown error".to_string());
1236 Err(PipelineError::InvalidRequest(format!(
1237 "Failed to cancel scan: {error_text}"
1238 )))
1239 }
1240 }
1241
1242 #[deprecated(
1258 note = "Veracode Pipeline Scan API does not support listing scans. Use get_scan() with specific scan ID instead."
1259 )]
1260 pub async fn list_scans(
1261 &self,
1262 _project_name: Option<&str>,
1263 _dev_stage: Option<DevStage>,
1264 _limit: Option<u32>,
1265 ) -> Result<Vec<Scan>, PipelineError> {
1266 Err(PipelineError::InvalidRequest(
1267 "Veracode Pipeline Scan API does not support listing/enumerating scans. Use get_scan() with a specific scan ID instead.".to_string()
1268 ))
1269 }
1270
1271 pub async fn wait_for_completion(
1283 &self,
1284 scan_id: &str,
1285 timeout_minutes: Option<u32>,
1286 poll_interval_seconds: Option<u32>,
1287 ) -> Result<Scan, PipelineError> {
1288 let timeout = timeout_minutes.unwrap_or(60);
1289 let interval = poll_interval_seconds.unwrap_or(10);
1290 let max_polls = (timeout * 60) / interval;
1291
1292 for _ in 0..max_polls {
1293 let scan = self.get_scan(scan_id).await?;
1294
1295 if scan.scan_status.is_successful() || scan.scan_status.is_failed() {
1297 return Ok(scan);
1298 }
1299
1300 tokio::time::sleep(tokio::time::Duration::from_secs(interval as u64)).await;
1302 }
1303
1304 Err(PipelineError::ScanTimeout)
1305 }
1306
1307 fn calculate_summary(&self, findings: &[Finding]) -> FindingsSummary {
1309 let mut summary = FindingsSummary {
1310 very_high: 0,
1311 high: 0,
1312 medium: 0,
1313 low: 0,
1314 very_low: 0,
1315 informational: 0,
1316 total: findings.len() as u32,
1317 };
1318
1319 for finding in findings {
1320 match finding.severity {
1321 5 => summary.very_high += 1,
1322 4 => summary.high += 1,
1323 3 => summary.medium += 1,
1324 2 => summary.low += 1,
1325 1 => summary.very_low += 1,
1326 0 => summary.informational += 1,
1327 _ => {} }
1329 }
1330
1331 summary
1332 }
1333}